Go 레퍼런스

ANTLR과 유사하게(그리고 Bison/Yacc와 달리), Lox는 문법과 사용자가 작성한 코드를 분리합니다. ANTLR과 달리, Lox는 문법과 사용자가 작성한 코드를 모두 분석하여 파서가 어떻게 생성되어야 하는지 결정합니다. 프로덕션(문법/lox)을 액션(사용자 작성/Go)과 매칭하고 이러한 관계를 사용하여 액션 매개변수와 반환값의 타입을 검사합니다. 결과적으로 사용자가 작성한 Go 코드는 파서가 생성되기 전에 특정 요구사항을 충족해야 합니다.

항상 최신 Lox 릴리스를 사용하는 것이 중요합니다. 특히 Go 툴체인을 업데이트한 후에는 더욱 그렇습니다. lox가 이상하거나 설명할 수 없는 Go 관련 오류를 반환한다면, lox 버전이 너무 오래되었을 가능성이 높습니다.

Lox는 생성된 코드와 사용자가 작성한 Go 코드 간의 심볼 충돌을 방지하기 위해 관용적이지 않은 접두사를 사용합니다. 구체적으로 on_ 접두사(아래 설명)와 단일 언더스코어 _ 접두사(예: _TokenToString)를 예약합니다. Lox는 향후 _ 접두사를 사용하는 더 많은 심볼을 추가할 권리를 보유합니다.

토큰 타입

파서 패키지는 Lox가 토큰을 표현하는 데 사용할 Token이라는 타입을 정의해야 합니다. Token은 렉서가 반환하는 타입과 동일해야 합니다. 프로젝트에서 simplelexer를 사용하는 경우(초보자에게 권장), Token은 simplelexer의 Token 타입에 대한 별칭이어야 합니다:

type Token = simplelexer.Token

파서 타입

파서 패키지는 파서 타입도 정의해야 합니다. 이름은 중요하지 않지만, lox 도구가 생성할 lox 타입을 임베드해야 합니다:

type myParser struct {
  lox

  // 다른 필드들.
}

lox를 임베드하면 해당 타입이 파서 타입으로 표시됩니다. Lox는 이 타입에서 액션을 찾고, 이 타입에 메서드도 생성합니다.

액션 메서드

각 문법 프로덕션은 파서가 해당 프로덕션을 리듀스할 때 실행할 대응하는 Go 액션 메서드를 가져야 합니다. 프로덕션 메서드는 파서 타입에 정의되어야 합니다. 메서드 이름은 on_<rule> 또는 on_<rule>__<suffix> 패턴을 따라야 합니다. 여기서 <rule>은 프로덕션을 정의하는 규칙의 이름이고 <suffix>는 동일한 규칙에 대해 여러 액션 메서드를 허용하기 위한 선택적 문자열입니다. <suffix>의 실제 값은 중요하지 않으며, Lox에 의해 무시됩니다.

액션 메서드는 단일 결과를 반환해야 합니다. 규칙에 두 개 이상의 액션 메서드가 있는 경우, 모두 동일한 타입을 반환해야 합니다. 각 프로덕션 항은 액션 매개변수와 대응되어야 합니다. 항이 참조하는 규칙이나 토큰의 Go 타입은 대응하는 매개변수 타입에 할당 가능해야 합니다.

예를 들어:

statement = ID '=' expr
          | 'call' ID

func (p *myParser) on_statement__assign(id Token, _ Token, e Expr) Statement {
    return &AssignStat{id, e}
}

func (p *myParser) on_statement__call(_ Token, id Token) Statement {
    return &CallStat{id}
}

메서드 on_statement__assign은 프로덕션 statement = ID '=' expr과 다음과 같은 이유로 매칭됩니다:

프로덕션은 단일 액션 메서드와 매칭되어야 합니다.

예를 들어:

statement = 'ID' = 'expr'
func (p *myParser) on_statement__1(id Token, _ Token, e Expr) Statement {
    return &AssignStat{id, e}
}
func (p *myParser) on_statement__2(id, _, e any) Statement {
    return &AssignStat{id.(Token), e.(Expr)}
}

이 예제에서 loxon_statement__1on_statement__2 모두 하나의 statement 프로덕션에 대한 액션 메서드로 사용될 수 있기 때문에 오류를 생성합니다.

_onBounds

파서 타입_onBounds라는 메서드를 정의하면, 생성된 파서는 모든 리듀스 아티팩트에 대해 시작 및 끝 토큰 형태로 어휘 경계를 정의하는 정보와 함께 이를 한 번 호출합니다. 예를 들어, 이는 AST에 소스 위치 정보를 저장하는 데 사용할 수 있습니다.

_onBounds가 지정된 경우, 다음 시그니처를 가져야 합니다:

func (p *yourParser) _onBounds(r any, begin, end Token) {
    // ...
}

여기서 r은 리듀스 아티팩트이고, beginend는 경계 토큰입니다.

_onBounds를 정의한 후 파서를 재생성하기 위해 lox를 실행해야 합니다. 그렇지 않으면 호출되지 않습니다.

오류 로깅 목적으로 AST와 소스 위치 정보를 연결하는 데 _onBounds를 사용하는 방법의 예제는 Bolox를 확인하세요.

생성된 코드

Lox가 생성한 파일은 다음 패턴을 따릅니다: *.gen.go (예: parser.gen.go). 생성된 코드에는 파서, 렉서 상태 머신 및 기타 지원 타입과 함수가 포함됩니다. 이 섹션은 참조/사용할 수 있는 코드를 문서화합니다.

lox

Lox는 파서를 실행하는 데 사용되는 데이터 구조를 포함하는 lox 타입을 생성합니다. 동일한 패키지에 이 타입을 임베드하는 struct를 정의해야 합니다. 이것은 Lox에게 이 struct가 파서임을 알려줍니다. Lox는 이 타입에 파서를 구현하는 메서드를 추가합니다.

_Lexer

Lox는 다음과 같이 _Lexer 인터페이스를 생성합니다:

type _Lexer interface {
	ReadToken() (Token, int)
}

_Lexer는 파서가 요구하는 인터페이스를 정의합니다. 재미있는 사실: Lox는 실제 렉서를 생성하지 않고, 렉서를 위한 상태 머신을 생성합니다. _Lexer 구현을 제공하는 것은 사용자의 책임입니다. simplelexer는 Lox에 포함된 예제에서 사용되는 렉서 구현이며, 대부분의 프로젝트에 충분합니다. 외부 의존성으로 사용하는 대신 프로젝트에 복사할 수 있을 정도로 작고 간단합니다.