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

Ruby 템플릿 자세히 알아보기:파서

오늘 우리는 Ruby 템플릿으로의 여정을 계속합니다. 어휘분석기가 준비되면 다음 단계인 파서로 넘어갑시다.

지난 시간에 우리는 문자열 보간법을 살펴보았고 이어서 우리 고유의 템플릿 언어를 만드는 데 뛰어 들었습니다. 템플릿을 읽고 토큰 스트림으로 변환하는 렉서를 구현하는 것으로 시작했습니다. 오늘 우리는 함께 제공되는 파서를 구현할 것입니다. 또한 약간의 언어 이론에 대해 알아보겠습니다.

시작합니다!

추상 구문 트리

Welcome to {{name}}에 대한 간단한 예제 템플릿을 다시 살펴보겠습니다. . 렉서를 사용하여 문자열을 토큰화하면 다음과 같은 토큰 목록이 표시됩니다.

Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]

궁극적으로 우리는 템플릿을 평가하고 표현식을 실제 값으로 바꾸기를 원합니다. 문제를 좀 더 어렵게 만들기 위해 반복 및 조건문을 허용하는 복잡한 블록 표현식도 평가하려고 합니다.

이를 위해 템플릿의 논리적 구조를 설명하는 추상 구문 트리(AST)를 생성해야 합니다. 트리는 다른 노드를 참조하거나 토큰의 추가 데이터를 저장할 수 있는 노드로 구성됩니다.

간단한 예에서 원하는 추상 구문 트리는 다음과 같습니다.

문법 정의

문법을 정의하기 위해 언어의 이론적 기초부터 시작하겠습니다. 다른 프로그래밍 언어와 마찬가지로 우리의 템플릿 언어는 컨텍스트 프리 언어이므로 컨텍스트 프리 문법으로 설명할 수 있습니다. (자세한 Wikipedia 설명에 있는 수학적 표기법에 겁먹지 마세요. 개념은 매우 직관적이며 개발자 친화적인 문법 표기법이 있습니다.)

문맥 자유 문법은 언어의 가능한 모든 문자열이 구성되는 방법을 설명하는 일련의 규칙입니다. EBNF 표기법에서 템플릿 언어의 문법을 살펴보겠습니다.

template = statements;
statements = { statement };
statement = CONTENT | expression | block_expression;
expression = OPEN_EXPRESSION, IDENTIFIER, arguments, CLOSE;
block_expression = OPEN_BLOCK, IDENTIFIER, arguments, CLOSE, statements, [ OPEN_INVERSE, CLOSE, statements ], OPEN_END_BLOCK, IDENTIFIER, CLOSE;
arguments = { IDENTIFIER };

각 할당은 규칙을 정의합니다. 규칙의 이름은 왼쪽에 있고 렉서의 다른 규칙(소문자) 또는 토큰(대문자)이 오른쪽에 있습니다. 규칙과 토큰은 쉼표 ,를 사용하여 연결할 수 있습니다. 또는 파이프 |를 사용하여 대체 상징. 중괄호 { ... } 안의 규칙 및 토큰 여러 번 반복될 수 있습니다. 대괄호 [ ... ] 안에 있을 때 , 선택 사항으로 간주됩니다.

위의 문법은 템플릿이 문장으로 구성되어 있음을 설명하는 간결한 방법입니다. 명령문은 CONTENT 토큰, 표현식 또는 블록 표현식. 표현식은 OPEN_EXPRESSION입니다. 토큰, 뒤에 IDENTIFIER 토큰, 인수, CLOSE 토큰. 그리고 블록 표현식은 자연어로 설명하려고 하는 것보다 위와 같은 표기법을 사용하는 것이 더 나은 이유를 보여주는 완벽한 예입니다.

위와 같은 문법 정의에서 자동으로 파서를 생성하는 도구가 있습니다. 하지만 진정한 Ruby Magic 전통에서 재미를 느끼고 직접 파서를 구축해 보겠습니다. 이 과정에서 한두 가지를 배우길 바랍니다.

파서 구축

언어 이론은 제쳐두고 실제로 파서를 구축해 봅시다. 훨씬 더 단순하지만 여전히 유효한 템플릿으로 시작하겠습니다. Welcome to Ruby Magic . 이 템플릿에는 표현식이 없으며 토큰 목록은 단 하나의 요소로 구성됩니다. 다음과 같습니다.

[[:CONTENT, "Welcome to Ruby Magic"]]

먼저 파서 클래스를 설정합니다. 다음과 같습니다.

module Magicbars
  class Parser
    def self.parse(tokens)
      new(tokens).parse
    end
 
    attr_reader :tokens
 
    def initialize(tokens)
      @tokens = tokens
    end
 
    def parse
      # Parsing starts here
    end
  end
end

클래스는 토큰 배열을 가져와 저장합니다. parse라는 공개 메서드가 하나만 있습니다. 토큰을 AST로 변환합니다.

문법을 되돌아보면 최상위 규칙은 template입니다. . 이는 parse , 구문 분석 프로세스가 시작될 때 Template 노드.

노드는 자체 동작이 없는 단순한 클래스입니다. 그들은 다른 노드를 연결하거나 토큰의 일부 값을 저장합니다. Template은 다음과 같습니다. 노드는 다음과 같습니다.

module Magicbars
  module Nodes
    class Template
      attr_reader :statements
 
      def initialize(statements)
        @statements = statements
      end
    end
  end
end

예제가 작동하도록 하려면 Content도 필요합니다. 마디. 텍스트 내용만 저장합니다("Welcome to Ruby Magic" ) 토큰에서.

module Magicbars
  module Nodes
    class Content
      attr_reader :content
 
      def initialize(content)
        @content = content
      end
    end
  end
end

다음으로 parse 메소드를 구현하여 Template의 인스턴스를 생성해 보겠습니다. 및 Content 인스턴스 올바르게 연결하십시오.

def parse
  Magicbars::Nodes::Template.new(parse_content)
end
 
def parse_content
  return unless tokens[0][0] == :CONTENT
 
  Magicbars::Nodes::Content.new(tokens[0][1])
end

파서를 실행하면 올바른 결과를 얻습니다.

Magicbars::Parser.parse(tokens)
# => #<Magicbars::Nodes::Template:0x00007fe90e939410 @statements=#<Magicbars::Nodes::Content:0x00007fe90e939578 @content="Welcome to Ruby Magic">>

분명히 이것은 하나의 콘텐츠 노드만 있는 간단한 예제에서만 작동합니다. 실제로 표현식을 포함하는 더 복잡한 예제로 전환해 보겠습니다. Welcome to {{name}} .

Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]

이를 위해서는 Expression이 필요합니다. 노드 및 Identifier 마디. Expression 노드는 식별자와 모든 인수(문법에 따라 0개 이상의 Identifier 배열임)를 저장합니다. 노드). 다른 노드와 마찬가지로 여기에서도 볼 것이 많지 않습니다.

module Magicbars
  module Nodes
    class Expression
      attr_reader :identifier, :arguments
 
      def initialize(identifier, arguments)
        @identifier = identifier
        @arguments = arguments
      end
    end
  end
end
 
module Magicbars
  module Nodes
    class Identifier
      attr_reader :value
 
      def initialize(value)
        @value = value.to_sym
      end
    end
  end
end

새 노드가 준비되면 parse를 수정해 보겠습니다. 정규 콘텐츠와 표현식을 모두 처리하는 방법입니다. parse_statements를 도입하여 이를 수행합니다. parse_statement를 계속 호출하는 메소드 값을 반환하는 한.

def parse
  Magicbars::Nodes::Template.new(parse_statements)
end
 
def parse_statements
  results = []
 
  while result = parse_statement
    results << result
  end
 
  results
end

parse_statement 자체적으로 먼저 parse_content를 호출합니다. 값이 반환되지 않으면 parse_expression을 호출합니다. .

def parse_statement
  parse_content || parse_expression
end

parse_statement 메소드가 statement과 매우 유사해 보이기 시작했습니다. 문법의 법칙? 여기서 사전에 명시적으로 문법을 작성하는 데 시간을 들이는 것이 올바른 길을 가고 있는지 확인하는 데 많은 도움이 됩니다.

다음으로 parse_content를 수정해 보겠습니다. 첫 번째 토큰만 보지 않도록 합니다. 추가 @position을 도입하여 이를 수행합니다. 이니셜라이저에서 인스턴스 변수를 만들고 이를 사용하여 현재 토큰을 가져옵니다.

attr_reader :tokens, :position
 
def initialize(tokens)
  @tokens = tokens
  @position = 0
end
 
# ...
 
def parse_content
  return unless token = tokens[position]
  return unless token[0] == :CONTENT
 
  @position += 1
 
  Magicbars::Nodes::Content.new(token[1])
end

parse_content 메소드는 이제 현재 토큰을 보고 유형을 확인합니다. CONTENT인 경우 토큰이 있으면 위치를 증가시키고(현재 토큰이 성공적으로 구문 분석되었기 때문에) 토큰의 콘텐츠를 사용하여 Content 마디. 현재 토큰이 없거나(토큰의 끝에 있기 때문에) 유형이 일치하지 않으면 메서드가 일찍 종료되고 nil을 반환합니다. .

개선된 parse_content 메서드가 준비되어 있으므로 새로운 parse_expression을 처리해 보겠습니다. 방법.

def parse_expression
  return unless token = tokens[position]
  return unless token[0] == :OPEN_EXPRESSION
 
  @position += 1
 
  identifier = parse_identifier
  arguments = parse_arguments
 
  if !tokens[position] || tokens[position][0] != :CLOSE
    raise "Unexpected token #{tokens[position][0]}. Expected :CLOSE."
  end
 
  @position += 1
 
  Magicbars::Nodes::Expression.new(identifier, arguments)
end

먼저 현재 토큰이 있고 해당 유형이 OPEN_EXPRESSION인지 확인합니다. . 그렇다면 다음 토큰으로 진행하고 parse_identifier를 호출하여 식별자와 인수를 구문 분석합니다. 및 parse_arguments , 각각. 두 방법 모두 해당 노드를 반환하고 현재 토큰을 진행합니다. 완료되면 현재 토큰이 존재하고 :CLOSE인지 확인합니다. 토큰. 그렇지 않은 경우 오류가 발생합니다. 그렇지 않으면 새로 생성된 Expression을 반환하기 전에 위치를 마지막으로 한 번 더 진행합니다. 노드.

이 시점에서 몇 가지 패턴이 나타납니다. 다음 토큰으로 여러 번 진행하고 현재 토큰과 해당 유형이 있는지도 확인합니다. 이에 대한 코드가 다소 복잡하므로 두 가지 도우미 메서드를 소개하겠습니다.

def expect(*expected_tokens)
  upcoming = tokens[position, expected_tokens.size]
 
  if upcoming.map(&:first) == expected_tokens
    advance(expected_tokens.size)
    upcoming
  end
end
 
def advance(offset = 1)
  @position += offset
end

expect 메소드는 다양한 수의 토큰 유형을 취하여 토큰 스트림의 다음 토큰에 대해 확인합니다. 모두 일치하면 일치하는 토큰을 지나쳐 반환합니다. advance 메소드는 @position을 증가시킵니다. 주어진 오프셋에 의한 인스턴스 변수.

다음 예상 토큰에 대한 유연성이 없는 경우 토큰이 일치하지 않을 때 멋진 오류 메시지를 표시하는 메서드도 도입합니다.

def need(*required_tokens)
  upcoming = tokens[position, required_tokens.size]
  expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
end

이러한 도우미 메서드를 사용하여 parse_contentparse_expression 이제 더 깨끗하고 읽기 쉽습니다.

def parse_content
  if content = expect(:CONTENT)
    Magicbars::Nodes::Content.new(content[0][1])
  end
end
 
def parse_expression
  return unless expect(:OPEN_EXPRESSION)
 
  identifier = parse_identifier
  arguments = parse_arguments
 
  need(:CLOSE)
 
  Magicbars::Nodes::Expression.new(identifier, arguments)
end

마지막으로 parse_identifier도 살펴보겠습니다. 및 parse_arguments . 도우미 메서드 덕분에 parse_identifier 메소드는 parse_content만큼 간단합니다. 방법. 유일한 차이점은 다른 노드 유형을 반환한다는 것입니다.

def parse_identifier
  if identifier = expect(:IDENTIFIER)
    Magicbars::Nodes::Identifier.new(identifier[0][1])
  end
end

parse_arguments를 구현할 때 메소드에서 parse_statements와 거의 동일하다는 것을 알았습니다. 방법. 유일한 차이점은 parse_identifier를 호출한다는 것입니다. parse_statement 대신 . 다른 도우미 메서드를 도입하여 중복된 논리를 제거할 수 있습니다.

def repeat(method)
  results = []
 
  while result = send(method)
    results << result
  end
 
  results
end

repeat 메소드는 send를 사용합니다. 더 이상 노드를 반환하지 않을 때까지 주어진 메서드 이름을 호출합니다. 그런 일이 발생하면 수집된 결과(또는 빈 배열만)가 반환됩니다. 이 도우미를 사용하면 두 parse_statementsparse_arguments 한 줄 방법이 됩니다.

def parse_statements
  repeat(:parse_statement)
end
 
def parse_arguments
  repeat(:parse_identifier)
end

이러한 모든 변경 사항이 적용되면 토큰 스트림을 구문 분석해 보겠습니다.

Magicbars::Parser.parse(tokens)
# => #<Magicbars::Nodes::Template:0x00007f91a602f910
#     @statements=
#      [#<Magicbars::Nodes::Content:0x00007f91a58802c8 @content="Welcome to ">,
#       #<Magicbars::Nodes::Expression:0x00007f91a602fcd0
#        @arguments=[],
#        @identifier=
#         #<Magicbars::Nodes::Identifier:0x00007f91a5880138 @value=:name>  >

읽기가 조금 어렵지만 실제로 올바른 추상 구문 트리입니다. Template 노드에 Content가 있습니다. 및 Expression 성명. Content 노드의 값은 "Welcome to "입니다. 및 Expression 노드의 식별자는 Identifier입니다. :name이 있는 노드 그 가치로.

블록 표현식 구문 분석

파서 구현을 완료하려면 여전히 블록 표현식의 구문 분석을 구현해야 합니다. 참고로 여기에서 구문 분석할 템플릿이 있습니다.

Welcome to {{name}}!
 
{{#if subscribed}}
  Thank you for subscribing to our mailing list.
{{else}}
  Please sign up for our mailing list to be notified about new articles!
{{/if}}
 
Your friends at {{company_name}}

이를 위해 먼저 BlockExpression을 소개하겠습니다. 마디. 이 노드는 더 많은 데이터를 저장하지만 다른 작업을 수행하지 않으므로 그다지 흥미롭지 않습니다.

module Magicbars
  module Nodes
    class BlockExpression
      attr_reader :identifier, :arguments, :statements, :inverse_statements
 
      def initialize(identifier, arguments, statements, inverse_statements)
        @identifier = identifier
        @arguments = arguments
        @statements = statements
        @inverse_statements = inverse_statements
      end
    end
  end
end

Expression처럼 노드에서 식별자와 모든 인수를 저장합니다. 또한 블록 및 역 블록의 명령문도 저장합니다.

문법을 되돌아보면 블록 표현식을 구문 분석하기 위해 parse_statements를 수정해야 함을 알 수 있습니다. parse_block_expression 호출이 있는 메서드 . 이제 문법의 규칙처럼 보입니다.

def parse_statement
  parse_content || parse_expression || parse_block_expression
end

parse_block_expression 방법 자체는 조금 더 복잡합니다. 하지만 도우미 메서드 덕분에 여전히 읽기 쉽습니다.

def parse_block_expression
  return unless expect(:OPEN_BLOCK)
 
  identifier = parse_identifier
  arguments = parse_arguments
 
  need(:CLOSE)
 
  statements = parse_statements
 
  if expect(:OPEN_INVERSE, :CLOSE)
    inverse_statements = parse_statements
  end
 
  need(:OPEN_END_BLOCK)
 
  if identifier.value != parse_identifier.value
    raise("Error. Identifier in closing expression does not match identifier in opening expression")
  end
 
  need(:CLOSE)
 
  Magicbars::Nodes::BlockExpression.new(identifier, arguments, statements, inverse_statements)
end

첫 번째 부분은 parse_expression과 매우 유사합니다. 방법. 식별자와 인수를 사용하여 여는 블록 표현식을 구문 분석합니다. 그 후 parse_statements를 호출합니다. 블록 내부를 구문 분석합니다.

완료되면 {{else}}가 있는지 확인합니다. OPEN_INVERSE로 식별되는 표현식 토큰 다음에 CLOSE 토큰. 두 토큰이 모두 발견되면 parse_statements를 호출합니다. 다시 역 블록을 구문 분석합니다. 그렇지 않으면 해당 부분을 완전히 건너뜁니다.

마지막으로 열린 블록 표현식과 동일한 식별자를 사용하는 끝 블록 표현식이 있는지 확인합니다. 식별자가 일치하지 않으면 오류가 발생합니다. 그렇지 않으면 새 BlockExpression을 만듭니다. 노드를 반환하고 반환합니다.

고급 블록 표현식 템플릿의 토큰으로 파서를 호출하면 템플릿에 대한 AST가 반환됩니다. 가독성이 떨어지므로 여기에 예제 출력을 포함하지 않겠습니다. 대신 생성된 AST의 시각적 표현이 있습니다.

parse_statements를 호출하기 때문에 parse_block_expression 내부 , 블록과 역 블록 모두 일반 콘텐츠뿐만 아니라 더 많은 표현식, 블록 표현식을 포함할 수 있습니다.

여정은 계속됩니다...

우리는 우리 고유의 템플릿 언어를 구현하기 위한 여정에서 상당한 진전을 이루었습니다. 언어 이론을 잠시 살펴본 후 템플릿 언어에 대한 문법을 ​​정의하고 이를 사용하여 처음부터 구문 분석기를 구현했습니다.

렉서와 파서가 모두 있는 상태에서 템플릿에서 보간된 문자열을 생성하는 인터프리터만 누락되었습니다. 이 부분은 RubyMagic의 차기 버전에서 다룰 것입니다. Ruby Magic 메일링리스트를 구독하면 소식이 나올 때 알림을 받을 수 있습니다.