오늘 우리는 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_content
및 parse_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_statements
및 parse_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 메일링리스트를 구독하면 소식이 나올 때 알림을 받을 수 있습니다.