Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby로 프로그래밍 언어 구축:인터프리터, 2부

<블록 인용>

Github의 전체 소스

Stoffle 프로그래밍 언어의 완전한 구현은 GitHub에서 사용할 수 있습니다. 버그를 발견하거나 질문이 있는 경우 언제든지 문제를 열어주세요.

이 블로그 게시물에서 우리는 완전히 Ruby로 구축된 장난감 프로그래밍 언어인 Stoffle용 인터프리터를 계속 구현할 것입니다. 우리는 이전 게시물에서 인터프리터를 시작했습니다. 이 시리즈의 첫 번째 부분에서 이 프로젝트에 대한 자세한 내용을 읽을 수 있습니다.

지난 포스트에서 변수, 조건부, 단항 및 이진 연산자, 데이터 유형, 콘솔로 인쇄 등 Stoffle의 더 간단한 기능을 구현하는 방법을 다루었습니다. 이제 소매를 걷어붙이고 더 어려운 나머지 부분인 함수 정의, 함수 호출, 변수 범위 지정 및 루프를 다룰 때입니다.

이전에 했던 것처럼 이 포스트의 처음부터 끝까지 동일한 예제 프로그램을 사용할 것입니다. Stoffle 예제 프로그램에서 각기 다른 구조를 구현하기 위해 인터프리터에서 필요한 구현을 탐색하면서 한 줄씩 살펴보겠습니다. 마지막으로 인터프리터가 작동하는 모습을 보고 시리즈의 이전 기사에서 만든 CLI를 사용하여 프로그램을 실행합니다.

가우스가 돌아왔습니다

기억력이 좋다면 시리즈의 2부에서 Lexer를 구축하는 방법에 대해 논의했다는 것을 기억할 수 있을 것입니다. 그 게시물에서 우리는 Stoffle의 구문을 설명하기 위해 일련의 숫자를 요약하는 프로그램을 살펴보았습니다. 이 기사의 끝에서 우리는 마침내 앞서 언급한 프로그램을 실행할 수 있게 될 것입니다! 자, 여기 다시 프로그램이 있습니다:

fn sum_integers: first_integer, last_integer
  i = first_integer
  sum = 0
  while i <= last_integer
    sum = sum + i

    i = i + 1
  end

  println(sum)
end

sum_integers(1, 100)

정수 합계 프로그램의 추상 구문 트리(AST)는 다음과 같습니다.

Ruby로 프로그래밍 언어 구축:인터프리터, 2부

<블록 인용>

스토플 샘플 프로그램에 영감을 준 수학자

Carl Friedrich Gauss는 불과 7세에 일련의 숫자를 요약하는 공식을 스스로 알아냈습니다.

눈치채셨겠지만 우리 프로그램은 가우스가 고안한 공식을 사용하지 않습니다. 오늘날 우리는 컴퓨터를 가지고 있기 때문에 이 문제를 "무차별 대입" 방식으로 해결할 수 있습니다. 우리의 실리콘 친구들이 우리를 위해 열심히 일하게 하십시오.

함수 정의

프로그램에서 가장 먼저 하는 일은 sum_integers를 정의하는 것입니다. 기능. 함수를 선언한다는 것은 무엇을 의미합니까? 짐작할 수 있듯이 이는 변수에 값을 할당하는 것과 유사한 작업입니다. 함수를 정의할 때 이름(즉, 함수 이름, 식별자)을 하나 이상의 표현식(즉, 함수의 본문)과 연결합니다. 또한 함수 호출 중에 전달된 값이 바인딩되어야 하는 이름을 등록합니다. 이러한 식별자는 함수 실행 중에 지역 변수가 되며 매개변수라고 합니다. 함수가 호출될 때 전달되고 매개변수에 바인딩된 값은 인수입니다.

#interpret_function_definition을 살펴보겠습니다. :

def interpret_function_definition(fn_def)
  env[fn_def.function_name_as_str] = fn_def
end

꽤 간단하죠? 이 시리즈의 마지막 게시물에서 기억할 수 있듯이 인터프리터가 인스턴스화될 때 환경을 만듭니다. 이것은 프로그램 상태를 유지하는 데 사용되는 장소이며 우리의 경우 단순히 Ruby 해시입니다. 지난 게시물에서 변수와 이에 바인딩된 값이 env에 어떻게 저장되는지 살펴보았습니다. . 함수 정의도 거기에 저장됩니다. 키는 함수 이름이고 값은 함수를 정의하는 데 사용되는 AST 노드입니다(Stoffle::AST::FunctionDefinition ). 다음은 이 AST 노드에 대한 정보입니다.

class Stoffle::AST::FunctionDefinition < Stoffle::AST::Expression
  attr_accessor :name, :params, :body

  def initialize(fn_name = nil, fn_params = [], fn_body = nil)
    @name = fn_name
    @params = fn_params
    @body = fn_body
  end

  def function_name_as_str
    # The instance variable @name is an AST::Identifier.
    name.name
  end

  def ==(other)
    children == other&.children
  end

  def children
    [name, params, body]
  end
end

Stoffle::AST::FunctionDefinition과 관련된 함수 이름을 가짐 기능을 실행하는 데 필요한 모든 정보에 액세스할 수 있음을 의미합니다. 예를 들어, 예상되는 인수의 수를 가지고 있으며 함수 호출에서 제공하지 않으면 쉽게 오류를 발생시킬 수 있습니다. 다음으로 함수 호출 해석을 담당하는 코드를 탐색할 때 이 정보와 기타 세부 정보를 볼 수 있습니다.

함수 호출

예제를 계속 진행하면서 이제 함수 호출에 집중하겠습니다. sum_integers 정의 후 함수에서 숫자 1과 100을 인수로 전달하는 것을 호출합니다.

fn sum_integers: first_integer, last_integer
  i = first_integer
  sum = 0
  while i <= last_integer
    sum = sum + i

    i = i + 1
  end

  println(sum)
end

sum_integers(1, 100)

함수 호출의 해석은 #interpret_function_call에서 발생합니다. :

def interpret_function_call(fn_call)
  return if println(fn_call)

  fn_def = fetch_function_definition(fn_call.function_name_as_str)

  stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call)

  assign_function_args_to_params(stack_frame)

  # Executing the function body.
  call_stack << stack_frame
  value = interpret_nodes(fn_def.body.expressions)
  call_stack.pop
  value
end

이것은 복잡한 기능이므로 여기에서 시간을 할애해야 합니다. 지난 기사에서 설명한 것처럼 첫 번째 줄은 호출되는 함수가 println인지 확인하는 역할을 합니다. . 여기의 경우와 같이 사용자 정의 함수를 다루는 경우 #fetch_function_definition을 사용하여 정의를 가져옵니다. . 아래와 같이 이 함수는 평범한 항해이며 기본적으로 Stoffle::AST::FunctionDefinition을 검색합니다. 이전에 환경에 저장한 AST 노드 또는 함수가 존재하지 않으면 오류가 발생합니다.

def fetch_function_definition(fn_name)
  fn_def = env[fn_name]
  raise Stoffle::Error::Runtime::UndefinedFunction.new(fn_name) if fn_def.nil?

  fn_def
end

#interpret_function_call로 돌아가기 , 상황이 더 흥미로워지기 시작합니다. 간단한 장난감 언어의 기능에 대해 생각할 때 두 가지 특별한 관심사가 있습니다. 먼저 함수에 로컬인 변수를 추적하는 전략이 필요합니다. return도 처리해야 합니다. 표현. 이러한 문제를 해결하기 위해 frame이라고 하는 새 개체를 인스턴스화합니다. , 함수가 호출될 때마다. 동일한 함수가 여러 번 호출되더라도 각각의 새 호출은 새 프레임을 인스턴스화합니다. 이 객체는 함수에 로컬인 모든 변수를 보유합니다. 한 함수가 다른 함수를 호출할 수 있기 때문에 프로그램의 실행 흐름을 표시하고 추적하는 방법이 있어야 합니다. 이를 위해 호출 스택이라는 스택 데이터 구조를 사용합니다. . Ruby에서 #push가 있는 표준 배열 및 #pop 메소드는 스택 구현으로 수행됩니다.

<블록 인용>

호출 스택 및 스택 프레임

호출 스택 및 스택 프레임이라는 용어를 느슨하게 사용하고 있음을 명심하십시오. 프로세서와 저수준 프로그래밍 언어에도 일반적으로 호출 스택과 스택 프레임이 있지만, 우리가 가지고 있는 장난감 언어와 정확히 일치하지는 않습니다.

이러한 개념이 호기심을 불러일으켰다면 호출 스택과 스택 프레임을 조사하는 것이 좋습니다. 금속에 더 가까이 가고 싶다면 프로세서 호출 스택을 구체적으로 살펴보는 것이 좋습니다.

다음은 Stoffle::Runtime::StackFrame을 구현하는 코드입니다. :

module Stoffle
  module Runtime
    class StackFrame
      attr_reader :fn_def, :fn_call, :env

      def initialize(fn_def_ast, fn_call_ast)
        @fn_def = fn_def_ast
        @fn_call = fn_call_ast
        @env = {}
      end
    end
  end
end

이제 #interpret_function_call로 돌아갑니다. , 다음 단계는 함수 호출에서 전달된 값을 함수 본문 내에서 로컬 변수로 액세스할 수 있는 각 예상 매개변수에 할당하는 것입니다. #assign_function_args_to_params 이 단계를 담당합니다.

def assign_function_args_to_params(stack_frame)
  fn_def = stack_frame.fn_def
  fn_call = stack_frame.fn_call

  given = fn_call.args.length
  expected = fn_def.params.length
  if given != expected
    raise Stoffle::Error::Runtime::WrongNumArg.new(fn_def.function_name_as_str, given, expected)
  end

  # Applying the values passed in this particular function call to the respective defined parameters.
  if fn_def.params != nil
    fn_def.params.each_with_index do |param, i|
      if env.has_key?(param.name)
        # A global variable is already defined. We assign the passed in value to it.
        env[param.name] = interpret_node(fn_call.args[i])
      else
        # A global variable with the same name doesn't exist. We create a new local variable.
        stack_frame.env[param.name] = interpret_node(fn_call.args[i])
      end
    end
  end
end

#assign_function_args_to_params를 살펴보기 전에 구현하려면 먼저 변수 범위 지정에 대해 간략하게 설명해야 합니다. 이것은 복잡하고 미묘한 주제입니다. Stoffle의 경우 매우 실용적이고 간단한 솔루션을 채택하겠습니다. 우리의 작은 언어에서 새로운 범위를 생성하는 유일한 구조는 함수입니다. 또한 전역 변수가 항상 먼저 옵니다. 결과적으로 함수 외부에서 선언된 모든 변수(즉, 첫 번째 사용)는 전역 변수이며 env에 저장됩니다. . 함수 내부의 변수는 로컬 변수이며 env에 저장됩니다. 함수 호출을 해석하는 동안 생성된 스택 프레임의 그러나 한 가지 예외가 있습니다. 기존 전역 변수와 충돌하는 변수 이름입니다. 충돌이 발생하면 지역 변수는 그렇지 않습니다. 생성되고 기존 전역 변수를 읽고 할당합니다.

자, 이제 변수 범위 지정 전략이 명확해졌으므로 #assign_function_args_to_params로 돌아가 보겠습니다. . 메서드의 첫 번째 부분에서는 먼저 전달된 스택 프레임 개체에서 함수 정의 및 함수 호출 노드를 검색합니다. 이러한 항목이 있으면 제공된 인수의 수가 매개변수의 수와 일치하는지 여부를 쉽게 확인할 수 있습니다. 호출되는 함수가 예상됩니다. 주어진 인수와 예상 매개변수가 일치하지 않으면 오류가 발생합니다. #assign_function_args_to_params의 마지막 부분에서 , 우리는 함수 호출 중에 제공된 인수(즉, 값)를 각각의 매개변수(즉, 함수 내부의 지역 변수)에 할당합니다. 매개변수 이름이 기존 전역 변수와 충돌하는지 여부를 확인합니다. 앞에서 설명한 것처럼 이 경우 함수의 스택 프레임 내부에 지역 변수를 생성하지 않고 전달된 값을 기존 전역 변수에 적용하기만 하면 됩니다.

#interpret_function_call로 돌아가기 , 마침내 새로 생성된 스택 프레임을 호출 스택으로 푸시합니다. 그런 다음 오랜 친구를 #interpret_nodes라고 부릅니다. 함수 본문 해석을 시작하려면:

def interpret_function_call(fn_call)
  return if println(fn_call)

  fn_def = fetch_function_definition(fn_call.function_name_as_str)

  stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call)

  assign_function_args_to_params(stack_frame)

  # Executing the function body.
  call_stack << stack_frame
  value = interpret_nodes(fn_def.body.expressions)
  call_stack.pop
  value
end

함수 본문 해석

함수 호출 자체를 해석했으므로 이제 함수 본문을 해석할 차례입니다.

fn sum_integers: first_integer, last_integer
  i = first_integer
  sum = 0
  while i <= last_integer
    sum = sum + i

    i = i + 1
  end

  println(sum)
end

sum_integers(1, 100)

sum_integers의 처음 두 줄 함수는 변수 할당입니다. 이 시리즈의 이전 블로그 게시물에서 이 주제를 다뤘습니다. 그러나 이제 우리는 가변 범위를 갖게 되었고 결과적으로 할당을 처리하는 코드가 약간 변경되었습니다. 살펴보겠습니다:

def interpret_var_binding(var_binding)
  if call_stack.length > 0
    # We are inside a function. If the name points to a global var, we assign the value to it.
    # Otherwise, we create and / or assign to a local var.
    if env.has_key?(var_binding.var_name_as_str)
      env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
    else
      call_stack.last.env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
    end
  else
    # We are not inside a function. Therefore, we create and / or assign to a global var.
    env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
  end
end

함수 호출을 위해 생성된 스택 프레임을 call_stack에 푸시했을 때를 기억하십니까? ? call_stack 길이가 0보다 큽니다(즉, 적어도 하나의 스택 프레임). 현재 해석 중인 코드의 경우와 같이 함수 내부에 있는 경우 현재 값을 바인딩하려는 변수와 이름이 같은 전역 변수가 이미 있는지 확인합니다. 이미 알고 있듯이 충돌이 발생하면 기존 전역 변수에 값을 할당하기만 하고 로컬 변수는 생성되지 않습니다. 이름이 사용되지 않을 때는 새 지역 변수를 만들고 여기에 원하는 값을 할당합니다. call_stack 이후 스택(즉, 후입선출 데이터 구조)인 경우 이 지역 변수가 env에 저장되어야 함을 알고 있습니다. 마지막 스택 프레임(즉, 현재 처리 중인 함수에 대해 생성된 프레임). 마지막으로 #interpret_var_binding의 마지막 부분입니다. 기능 외부에서 발생하는 할당을 다룹니다. 함수만 Stoffle에서 새 범위를 생성하기 때문에 함수 외부에서 생성된 변수는 항상 전역적이고 인스턴스 변수 env에 저장되므로 여기서는 아무 것도 변경되지 않습니다. .

프로그램으로 돌아가서 다음 단계는 정수를 합산하는 루프를 해석하는 것입니다. 기억을 새로고침하고 Stoffle 프로그램의 AST를 다시 살펴보겠습니다.

Ruby로 프로그래밍 언어 구축:인터프리터, 2부

루프를 나타내는 노드는 Stoffle::AST::Repetition입니다. :

class Stoffle::AST::Repetition < Stoffle::AST::Expression
  attr_accessor :condition, :block

  def initialize(cond_expr = nil, repetition_block = nil)
    @condition = cond_expr
    @block = repetition_block
  end

  def ==(other)
    children == other&.children
  end

  def children
    [condition, block]
  end
end

이 AST 노드는 기본적으로 이전 기사에서 살펴본 개념을 결합합니다. 조건의 경우 일반적으로 루트(표현식의 AST 루트 노드에 대해 생각)에 Stoffle::AST::BinaryOperator가 있는 표현식이 있습니다. (예:'>', '또는' 등). 루프 본문에는 Stoffle::AST::Block이 있습니다. . 이게 말이 됩니까? 루프의 가장 기본적인 형태는 하나 이상의 표현식(블록 ) 표현식이 참인 동안(즉, 조건부 동안 반복됨) 진실한 값으로 평가).

인터프리터의 해당 메소드는 #interpret_repetition입니다. :

def interpret_repetition(repetition)
  while interpret_node(repetition.condition)
    interpret_nodes(repetition.block.expressions)
  end
end

여기에서 이 방법의 단순성(그리고 감히 말하지만 아름다움)에 놀랄 수 있습니다. 과거 기사에서 이미 살펴본 방법을 결합하여 루프 해석을 구현할 수 있습니다. Ruby의 while 사용 루프에서 Stoffle 루프를 구성하는 노드를 계속 해석하는지 확인할 수 있습니다(#interpret_nodes를 반복적으로 호출하여 ) 조건부의 평가가 참인 동안. 조건을 평가하는 작업은 일반적인 용의자 #interpret_node를 호출하는 것만큼 쉽습니다. 방법.

함수에서 복귀

거의 결승점에 다다랐습니다! 루프 후, 합산 결과를 콘솔에 출력합니다. 시리즈의 마지막 부분에서 이미 다루었으므로 다시 다루지 않습니다. 간단히 요약하자면 println 함수는 Stoffle 자체에서 제공하며 인터프리터 내부에서는 단순히 Ruby의 자체 puts를 사용하고 있습니다. 방법.

이 게시물을 마치려면 #interpret_nodes를 다시 방문해야 합니다. . 그것의 최종 버전은 우리가 과거에 보았던 것과 약간 다릅니다. 이제 함수에서 반환 및 호출 스택 해제를 처리하는 코드가 포함됩니다. 다음은 #interpret_nodes의 완성된 버전입니다. 완전한 영광:

def interpret_nodes(nodes)
  last_value = nil

  nodes.each do |node|
    last_value = interpret_node(node)

    if return_detected?(node)
      raise Stoffle::Error::Runtime::UnexpectedReturn unless call_stack.length > 0

      self.unwind_call_stack = call_stack.length # We store the current stack level to know when to stop returning.
      return last_value
    end

    if unwind_call_stack == call_stack.length
      # We are still inside a function that returned, so we keep on bubbling up from its structures (e.g., conditionals, loops etc).
      return last_value
    elsif unwind_call_stack > call_stack.length
      # We returned from the function, so we reset the "unwind indicator".
      self.unwind_call_stack = -1
    end
  end

  last_value
end

이미 알고 있듯이 #interpret_nodes 많은 표현을 해석해야 할 때마다 사용됩니다. 이것은 프로그램 해석을 시작하는 데 사용되며 블록과 연결된 노드가 있는 모든 경우에 사용됩니다(예:Stoffle::AST::FunctionDefinition ). 특히, 함수를 다룰 때 두 가지 시나리오가 있습니다:함수 해석과 return 표현식 또는 함수를 끝까지 해석하고 return을 치지 않음 표현. 두 번째 경우에는 함수에 명시적인 return이 없음을 의미합니다. 우리가 겪은 표현식이나 코드 경로에 return이 없습니다. .

계속하기 전에 기억을 새로고침합시다. 위의 몇 단락에서 기억할 수 있듯이 #interpret_nodes sum_integers 해석을 시작할 때 호출되었습니다. 함수(즉, 프로그램에서 호출되었을 때). 이번에도 우리가 진행 중인 프로그램의 소스 코드는 다음과 같습니다.

fn sum_integers: first_integer, last_integer
  i = first_integer
  sum = 0
  while i <= last_integer
    sum = sum + i

    i = i + 1
  end

  println(sum)
end

sum_integers(1, 100)

우리는 함수 해석의 끝에 있습니다. 짐작하시겠지만 우리 함수에는 명시적인 return이 없습니다. . 이것은 #interpret_nodes의 가장 쉬운 경로입니다. . 우리는 기본적으로 모든 함수 노드를 반복하며 마지막에 해석된 마지막 표현식의 값을 반환합니다(빠른 알림:Stoffle에는 암시적 반환이 있습니다). 이것은 우리를 결승선에 이르게 하고 우리 프로그램의 해석을 마칩니다.

우리 프로그램이 이제 완전히 해석되었지만 이 기사의 주요 목적은 인터프리터의 구현을 설명하는 것이므로 여기에서 조금 더 시간을 내어 인터프리터가 return 함수 내부.

먼저 return 표현식은 작업 시작 시 평가됩니다. 그 값은 반환되는 것에 대한 평가가 될 것입니다. 다음은 Stoffle::AST::Return의 코드입니다. :

class Stoffle::AST::Return < Stoffle::AST::Expression
  attr_accessor :expression

  def initialize(expr)
    @expression = expr
  end

  def ==(other)
    children == other&.children
  end

  def children
    [expression]
  end
end

그런 다음 return을 감지하는 간단한 조건이 있습니다. AST 노드. 이 작업을 수행한 후 먼저 온전성 검사를 수행하여 함수 내부에 있는지 확인합니다. 그렇게 하려면 단순히 호출 스택의 길이를 확인할 수 있습니다. 길이가 0보다 크다는 것은 우리가 실제로 함수 안에 있다는 것을 의미합니다. Stoffle에서는 return의 사용을 허용하지 않습니다. 함수 외부의 표현식이므로 이 검사가 실패하면 오류가 발생합니다. 프로그래머가 의도한 값을 반환하기 전에 먼저 호출 스택의 현재 길이를 기록하여 인스턴스 변수 unwind_call_stack에 저장합니다. . 이것이 왜 중요한지 이해하기 위해 #interpret_function_call을 검토해 보겠습니다. :

def interpret_function_call(fn_call)
  return if println(fn_call)

  fn_def = fetch_function_definition(fn_call.function_name_as_str)

  stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call)

  assign_function_args_to_params(stack_frame)

  # Executing the function body.
  call_stack << stack_frame
  value = interpret_nodes(fn_def.body.expressions)
  call_stack.pop
  value
end

여기, #interpret_function_call 끝에 , 함수를 해석한 후 호출 스택에서 스택 프레임을 팝합니다. 결과적으로 호출 스택의 길이가 1로 줄어듭니다. 반환을 감지한 순간 스택의 길이를 유지했기 때문에 #interpret_nodes에서 새 노드를 해석할 때마다 이 초기 길이를 비교할 수 있습니다. . #interpret_nodes의 노드 반복자 내에서 이 작업을 수행하는 세그먼트를 살펴보겠습니다. :

def interpret_nodes(nodes)
  # ...

  nodes.each do |node|
    # ...

    if unwind_call_stack == call_stack.length
      # We are still inside a function that returned, so we keep on bubbling up from its structures (e.g., conditionals, loops etc).
      return last_value
    elsif unwind_call_stack > call_stack.length
      # We returned from the function, so we reset the "unwind indicator".
      self.unwind_call_stack = -1
    end

    # ...
  end

  # ...
end

처음에는 이해하기가 다소 어려울 수 있습니다. GitHub에서 전체 구현을 확인하고 인터프리터의 이 마지막 부분을 이해하는 데 도움이 될 수 있다고 생각되면 함께 사용하는 것이 좋습니다. 여기서 염두에 두어야 할 중요한 점은 일반적인 프로그램에는 깊이 중첩된 구조가 많다는 것입니다. 따라서 #interpret_nodes 실행 중 일반적으로 #interpret_nodes에 대한 새 호출이 발생합니다. , 이로 인해 #interpret_nodes에 대한 더 많은 호출이 발생할 수 있습니다. 등등! return을 누르면 함수 내부에서 우리는 깊이 중첩된 구조 내부에 있을 수 있습니다. 예를 들어 return이 루프의 일부인 조건문 안에 있습니다. 함수에서 반환하려면 모든 #interpret_nodes에서 반환해야 합니다. #interpret_function_call에 의해 시작된 것에서 돌아올 때까지 호출 (즉, #interpret_nodes 호출 함수 본문의 해석을 시작했습니다.

위의 코드 부분에서 우리가 이 작업을 수행하는 방법을 정확히 강조합니다. @unwind_call_stack에서 양수 값을 가짐 그리고 호출 스택의 현재 길이와 같은 하나, 우리는 우리가 함수 안에 있고 여전히 return하지 않았음을 확실히 알고 있습니다. #interpret_function_call에 의해 시작된 원래 호출에서 . 이것이 마침내 발생하면 @unwind_call_stack 호출 스택의 현재 길이보다 큽니다. 따라서 반환된 함수를 종료했으며 더 이상 버블링 프로세스를 계속할 필요가 없음을 알고 있습니다. 그런 다음 @unwind_call_stack을 재설정합니다. . @unwind_call_stack을 사용하려면 맑은, 가능한 값은 다음과 같습니다.

  • -1 , 초기 및 중립 값으로 반환된 함수 내부에 없음을 나타냅니다.
  • 호출 스택 길이와 동일한 양수 , 반환된 함수 안에 여전히 있음을 나타냅니다.
  • 호출 스택 길이보다 큰 양수 , 반환된 함수 안에 더 이상 존재하지 않음을 나타냅니다.

Stoffle CLI를 사용하여 프로그램 실행

시리즈의 이전 기사에서는 Stoffle 프로그램을 더 쉽게 해석할 수 있도록 간단한 CLI를 만들었습니다. 인터프리터의 구현을 살펴보았으므로 이제 프로그램을 실행하여 인터프리터가 실행되는 것을 보겠습니다. 위의 여러 섹션에서 볼 수 있듯이 우리의 코드는 sum_integers를 정의한 다음 호출합니다. 1 인수를 전달하는 함수 및 100 . 인터프리터가 제대로 작동하면 5050.0이 표시됩니다. (1에서 시작하여 100으로 끝나는 정수 집합의 합) 콘솔에 출력됨:

Ruby로 프로그래밍 언어 구축:인터프리터, 2부

결말 생각

이 게시물에서는 인터프리터를 완성하는 데 필요한 마지막 부분을 구현했습니다. 우리는 함수 정의, 함수 호출, 변수 범위 지정 및 루프를 다루었습니다. 이제 간단하지만 작동하는 프로그래밍 언어가 생겼습니다!

이 시리즈의 다음 부분과 마지막 부분에서는 프로그래밍 언어 구현 연구를 계속하려는 사람들을 위한 훌륭한 옵션으로 간주되는 몇 가지 리소스를 공유할 것입니다. 또한 Stoffle 버전을 개선하면서 학습을 계속할 수 있는 몇 가지 과제를 제안할 것입니다. 나중에 봐요; 챠오!