Skip to content

5.1 렉싱과 파싱 — 문자열에서 프로그램 구조를 복원하기

소스 코드는 컴파일러에게 그저 문자의 나열이다. 의미 분석·변환·진단은 전부 계층 구조를 요구하므로, 프런트엔드의 일은 문자열에서 문법이 약속한 구조를 복원하는 것이다. 이 문서는 렉서와 파서의 책임 분담, 연산자 우선순위가 AST 모양을 결정하는 방식, 재귀 하강과 LR 계열의 선택 기준을 세우고, 좋은 AST는 무엇을 버리고 무엇을 보존해야 하는가를 포매터·IDE·codemod라는 소비자의 요구에서 역산한다.

학습 목표

  • 렉서와 파서의 책임 경계를 설명하고, 토큰이 종류·값과 함께 소스 범위(span)를 보존해야 하는 이유를 설명한다.
  • 연산자 우선순위와 결합 법칙이 AST 모양으로 번역되는 방식을 구체적인 트리로 판독한다.
  • concrete syntax tree와 AST의 차이를 설명하고, 소비자(의미 분석, 포매터, codemod)에 따라 보존 기준이 달라지는 이유를 판단한다.
  • 재귀 하강과 LR 계열 파서의 트레이드오프를 비교하고 상황에 맞는 쪽을 고른다.
  • fail-fast와 panic-mode 오류 복구의 목적을 구분하고, 복구가 만드는 후속 가짜 오류를 진단한다.

배경: 왜 이것이 존재하는가

let result = add(1, 2 * 3);이라는 27글자에서 "add를 호출한 결과를 result에 묶는다, 두 번째 인자는 2×3이다"라는 사실을 얻으려면 두 종류의 일이 필요하다. 문자들을 의미 있는 어휘 단위로 묶는 일(result는 여섯 글자가 아니라 식별자 하나다)과, 어휘 단위들의 계층 관계를 세우는 일(2 * 3add의 인자이지, 1, 2가 곱해지는 것이 아니다)이다.

이 두 일을 한 단계로 할 수도 있지만, 거의 모든 구현이 렉싱과 파싱으로 나눈다. 이유는 챕터 2(계산 이론)에서 다루는 언어 표현력 계층과 정확히 맞물린다 — 토큰의 모양(식별자, 숫자, 연산자)은 정규 언어 수준이라 상태 기계 하나로 싸게 인식되고, 중첩 구조(괄호, 블록)는 정규 언어로 표현할 수 없어 context-free 문법과 스택이 필요하다. 표현력이 다른 두 문제를 분리하면 각각이 단순해지고, 파서는 공백·주석 처리에서 해방되어 토큰열이라는 균일한 입력만 다루면 된다.

역사적으로 이 단계는 컴파일러의 전유물이 아니게 됐다. 포매터, 린터, IDE의 자동완성과 리팩터링, codemod — 코드를 읽고 쓰는 모든 도구가 같은 프런트엔드 문제를 풀고 있고, 도구마다 요구가 달라서 "좋은 트리"의 기준도 달라진다. 이 문서의 후반부가 그 기준을 다룬다.

핵심 개념

이 문서의 예제는 챕터 5 공통의 최소 장난감 언어 Toy를 사용한다. 숫자·불리언, 산술(+ - * /)과 비교(== < <=), let 지역 변수, if/else, 이름으로 호출하는 함수만 있는 언어다. 아래 토큰열·AST·오류 출력은 이 절의 규칙대로 구현한 렉서·파서(TypeScript, Node.js 24)를 실행해 확인한 결과다.

렉싱 — 문자를 어휘 단위로 묶는다

렉서(lexer, tokenizer)는 문자 스트림을 토큰의 열로 바꾼다. 토큰 하나는 종류(kind), 원문 조각(lexeme), 그리고 소스 범위를 담는다. let result = add(1, 2 * 3);의 토큰열은 다음과 같다(범위는 0 기준 절대 offset, [시작, 끝)).

text
LET     "let"     [0, 3)      IDENT   "add"     [13, 16)     NUMBER  "3"   [24, 25)
IDENT   "result"  [4, 10)     LPAREN  "("       [16, 17)     RPAREN  ")"   [25, 26)
EQ      "="       [11, 12)    NUMBER  "1"       [17, 18)     SEMI    ";"   [26, 27)
                              COMMA   ","       [18, 19)     EOF     ""    [27, 27)
                              NUMBER  "2"       [20, 21)
                              STAR    "*"       [22, 23)

관찰할 것 세 가지. 첫째, 공백은 토큰이 되지 않고 사라졌다 — 그러나 offset에는 흔적이 남아 있어([0,3) 다음이 [4,10)) 원문을 재구성할 수 있다. 둘째, let은 식별자와 같은 모양이지만 키워드로 분류됐다. 렉서는 식별자를 최대한 길게 읽은 뒤 키워드 표와 대조하는 방식이 일반적이다. 셋째, 위치를 line/column이 아니라 절대 offset으로 저장했다. line/column은 진단을 출력하는 순간 원문에서 계산하면 되고, offset은 계산이 싸고 편집 도구의 텍스트 연산과 그대로 호환된다.

토큰 경계가 모호할 때의 규칙은 maximal munch(최장 일치)다 — 가능한 가장 긴 토큰을 만든다. <=<=가 아니라 <= 하나다. 이 규칙은 거의 항상 옳지만 반례도 만든다. C++에서 std::vector<std::vector<int>>>>가 오른쪽 시프트 연산자 하나로 묶여 버린 것이 유명한 사례로, C++11이 문법 쪽을 고쳐서 해소했다. 어휘 규칙 하나가 언어 명세 수준의 결정이 되는 것이다.

파싱 — 토큰열에서 계층을 복원한다

파서는 두 가지 책임을 진다. 토큰열이 문법에 맞는지 판정하고(맞지 않으면 좋은 진단을 만들고), 맞다면 이후 단계가 쓸 계층 구조 — 통상 AST(abstract syntax tree) — 를 만든다.

계층의 모양을 결정하는 것이 우선순위(precedence)와 결합 법칙(associativity)이다. 토큰열에는 1 + 2 * 3이 평평하게 놓여 있을 뿐, "곱셈 먼저"는 어디에도 없다. 그 규칙은 문법이 부여하고 파서가 트리 모양으로 번역한다.

text
1 + 2 * 3        →      (+)                8 - 4 - 2      →        (-)
                        /   \                                     /   \
                       1    (*)                                 (-)    2
                           /   \                               /   \
                          2     3                             8     4

곱셈이 덧셈보다 우선순위가 높으므로 *가 더 깊은 곳에 놓이고(먼저 평가되고), 뺄셈은 좌결합이므로 8 - 4 - 2(8 - 4) - 2로 왼쪽이 깊어진다. AST를 판독하는 규칙은 하나다: 깊은 노드가 먼저 평가된다. 연산자 관련 버그(예: 우선순위를 잘못 세운 DSL)를 추적할 때 문자열이 아니라 트리를 덤프해서 봐야 하는 이유다.

let result = add(1, 2 * 3); 전체의 AST는 다음 모양이 된다(각 노드는 span도 담지만 생략).

text
Let (name: "result")
└─ init: Call (callee: "add")
   ├─ args[0]: Num 1
   └─ args[1]: Binary (*)
      ├─ Num 2
      └─ Num 3

concrete syntax와 AST — 무엇을 버릴 것인가

파스 트리를 문법 유도 과정 그대로 만들면 concrete syntax tree(CST, parse tree)가 된다. 모든 괄호, 구분자, 중간 문법 규칙이 노드로 남는다. AST는 거기서 표면 구문을 버린 것이다 — (1 + 2) * 3의 괄호는 노드가 되지 않고, +*보다 깊이 놓이는 구조 자체가 괄호의 의미를 담는다. 쉼표·세미콜론도 구조에 흡수되어 사라진다.

무엇을 버려도 되는가의 기준은 트리의 소비자다.

  • 의미 분석·최적화가 소비자라면 표면 구문은 소음이다. 괄호를 남기면 모든 패스가 "괄호 노드는 통과"라는 코드를 반복해야 한다.
  • 진단이 소비자라면 소스 span은 버릴 수 없다. "3번째 인자의 타입이 틀렸다"를 밑줄로 보여 주려면 그 인자 노드가 자기 범위를 알아야 한다.
  • 포매터·리팩터링 도구가 소비자라면 주석, 원래 괄호, 심지어 공백 스타일까지 필요하다. 그래서 이런 도구는 AST가 아니라 CST에 가까운 트리(lossless syntax tree)를 쓴다.

즉 "AST 설계"는 미적 선택이 아니라 소비자 요구의 합집합을 정하는 일이다. 컴파일러 교과서의 AST가 실무 도구의 트리와 다르게 생긴 이유가 여기 있다.

재귀 하강 — 문법을 코드로 직역한다

재귀 하강(recursive descent) 파서는 문법 규칙 하나를 함수 하나로 옮긴다. Toy의 표현식 문법과 파서 골격은 이렇게 대응한다(전체 구현이 아니라 대응 관계를 보이기 위한 발췌다).

text
comparison → additive (('==' | '<' | '<=') additive)*
additive   → multiplicative (('+' | '-') multiplicative)*
multiplicative → callOrAtom (('*' | '/') callOrAtom)*
ts
// 우선순위 낮은 규칙이 높은 규칙을 호출한다 — 호출 깊이가 곧 트리 깊이다
function parseAdditive(): Expr {
  let left = parseMultiplicative();
  while (peek().kind === 'PLUS' || peek().kind === 'MINUS') {
    const op = advance();
    const right = parseMultiplicative();
    left = { kind: 'Binary', op: op.lexeme, left, right }; // 좌결합: 왼쪽으로 쌓는다
  }
  return left;
}

낮은 우선순위 규칙이 높은 우선순위 규칙을 호출하므로 높은 우선순위 연산자가 트리 깊은 곳에 만들어지고, while로 왼쪽에 쌓으면 좌결합이 된다. 문법이 곧 코드라서 디버거로 따라갈 수 있고, 오류 지점에서 "지금 무엇을 파싱하던 중인지"를 함수 스택이 알고 있으므로 맥락 있는 오류 메시지를 만들기 좋다. 대부분의 산업용 컴파일러(GCC, Clang, V8, TypeScript, Roslyn)가 손으로 쓴 재귀 하강을 쓰는 이유다.

제약도 문법에서 직접 나온다. additive → additive '+' term 같은 좌재귀 규칙을 그대로 함수로 옮기면 자기 자신을 무한히 호출한다. 위처럼 반복문 형태로 바꾸거나 문법을 좌재귀 없는 형태로 변환해야 한다. 또 하나, 재귀 하강은 토큰 몇 개를 미리 보고(lookahead) 어느 규칙인지 결정할 수 있는 문법에서 자연스럽다 — 결정이 안 되는 문법은 백트래킹이나 특수 처리가 필요해진다.

LR 계열 — 문법을 데이터로 컴파일한다

LR 파싱은 다른 접근이다. 문법을 파서 생성기(yacc/bison 등)에 주면, 생성기가 문법을 분석해 상태 기계 테이블을 만들고, 실행기는 그 테이블을 따라 토큰을 스택에 쌓다가(shift) 규칙이 완성되면 묶는다(reduce). 손으로 옮기는 대신 문법이 데이터로 컴파일되는 것이다.

재귀 하강 (하향식)LR 계열 (상향식, 생성기)
문법 표현력lookahead로 결정 가능해야, 좌재귀 불가더 넓다 — 좌재귀 그대로, LL로 곤란한 문법도 처리
문법과 구현의 거리코드로 직역, 임의 로직 삽입 자유문법 파일이 곧 명세 — 문법 변경에 강하다
오류 메시지맥락을 아는 함수가 직접 작성 — 품질 상한이 높다기본은 "unexpected token" — 좋게 만들려면 별도 투자
문법 결함 발견실행해 봐야 드러나는 경우가 많다생성 시점에 shift/reduce 충돌로 보고 — 모호성 조기 검출
대표 사용처산업용 컴파일러·IDE 프런트엔드문법이 자주 바뀌는 DSL, 언어 프로토타입, 명세 검증

충돌 보고는 양날이다. 생성기가 "이 문법은 이 지점에서 모호하다"고 컴파일 시점에 알려 주는 것은 언어 설계 단계에서 귀중하지만, 충돌 메시지를 해석하고 문법을 고치는 일 자체가 LR 이론의 이해를 요구한다. 테이블을 손으로 구성하는 절차는 이 챕터의 범위 밖이다 — 선택 기준까지만 가져가면 된다: 언어가 안정적이고 진단 품질이 중요하면 재귀 하강, 문법 자체가 실험 대상이고 모호성 검출이 중요하면 생성기가 기본값이다.

오류 처리 — 어디서 멈추고 어떻게 계속할 것인가

문법에 맞지 않는 입력에서 파서가 할 일은 소비자에 따라 다르다.

fail-fast: 첫 오류에서 진단을 만들고 중단한다. 배치 컴파일이나 스크립트 실행이라면 충분하고, 구현이 단순하며 가짜 오류가 없다. Toy 파서에서 닫는 괄호를 빠뜨리면:

text
error at 1:26 (offset 25..26): expected ')' but found ';'
  let result = add(1, 2 * 3;
                           ^

진단의 재료가 전부 토큰의 span에서 나온다는 점을 보라. 오류를 일으킨 토큰(;)의 범위가 밑줄 위치가 되고, line/column은 offset에서 그 자리에서 계산했다.

panic-mode 복구: 파일 전체의 오류를 한 번에 보고하려면 첫 오류에서 멈출 수 없다. panic-mode는 오류 지점부터 동기화 토큰까지 토큰을 버리고 파싱을 재개한다. 동기화 지점은 "여기서부터는 새 구문이 시작된다"고 믿을 수 있는 토큰 — 문장 경계(;), 블록 경계(}), 선언 키워드(let, fn) — 으로 고른다. 문장 하나가 망가져도 다음 문장부터는 유효한 진단을 만들 수 있다.

복구의 비용은 후속 가짜 오류(cascading error)다. 복구가 문맥을 잘못 맞추면, 그 뒤의 멀쩡한 코드가 연쇄적으로 오류로 보고된다. 오류 목록에서 첫 번째 오류만 믿고 나머지는 첫 오류를 고친 뒤 다시 보는 습관이 실무적으로 유효한 이유다.

실무 관점

도구가 요구하는 트리는 컴파일러의 AST가 아니다

  • 포매터(Prettier 등)는 주석·원 괄호·개행 의도를 보존해야 하므로 lossless한 트리와 원문 offset을 유지한다. "AST에는 주석이 없다"는 컴파일러 관점의 상식이 포매터에서는 설계 결함이 된다.
  • IDE는 사용자가 타이핑하는 중간의 항상 깨져 있는 코드에서 자동완성·구조 탐색을 제공해야 한다. 그래서 IDE급 파서는 오류 노드를 트리에 포함시키는 복구 파싱이 기본이고, 이것이 배치 컴파일러와 프런트엔드를 공유하기 어려웠던 전통적 이유다(현대 컴파일러들이 프런트엔드를 IDE 요구 중심으로 재설계하는 추세다).
  • codemod(대규모 자동 수정)가 문자열 치환 대신 AST 변환을 쓰는 이유는 정확히 렉서·파서가 해결한 문제 때문이다 — 문자열 foo는 식별자일 수도, 문자열 리터럴 안일 수도, 주석 안일 수도 있다. 토큰 종류와 트리 문맥이 있어야 "식별자 foo만" 안전하게 겨냥할 수 있다.

span을 처음부터 설계하라

파서를 처음 만들 때 가장 흔한 후회가 위치 정보를 나중에 붙이려는 것이다. 진단, 소스 맵, 포매팅, IDE 연동 전부가 span을 요구하고, 트리 구축 후에 위치를 복원할 방법은 없다. 규칙은 단순하다 — 모든 토큰과 모든 AST 노드가 절대 offset 기반 span을 갖고, line/column은 출력 시점에 계산한다. JavaScript 계열이라면 offset의 단위(UTF-16 code unit인지 코드포인트인지)까지 명시해야 편집기와 어긋나지 않는다.

통념: "파서는 문법만 맞는지 확인하면 끝이다"

수락/거부 판정은 파서 책임의 절반도 안 된다. 실제 파서의 품질은 거부할 때(진단과 복구)와 수락한 뒤(트리가 소비자에게 주는 정보)에 결정된다. "파싱은 해결된 문제"라는 말이 절반만 맞는 이유다 — 인식 알고리즘은 해결됐지만, 오류 복구와 트리 설계는 도구마다 다시 푸는 설계 문제다.

더 깊이

렉서와 파서의 경계가 무너지는 곳 — 컨텍스트 의존 토큰화

렉싱이 정규 언어 수준이라는 분리는 대부분의 언어에서 근사적으로만 참이다. JavaScript의 /는 위치에 따라 나눗셈이거나 정규식 리터럴의 시작이다 — a / b / g/ab/g를 렉서 단독으로는 구분할 수 없다. 그래서 ECMAScript 명세는 어휘 문법에 목표 심볼을 여러 개 두고(InputElementDiv, InputElementRegExp 등) 파서의 문맥이 렉서의 모드를 고른다. 비슷한 사례로 템플릿 리터럴의 }(중첩 모드 스택), Python의 들여쓰기(렉서가 INDENT/DEDENT 토큰을 합성), C의 T * p(typedef 여부에 따라 곱셈/포인터 선언 — 파싱이 심볼 테이블을 참조하는 고전적 사례)가 있다. 프런트엔드를 설계할 때 이런 지점을 미리 파악해 두지 않으면 렉서·파서 인터페이스를 나중에 크게 고치게 된다.

문법의 모호성은 파서가 해결하지 못한다

dangling else가 고전이다 — if (a) if (b) x; else y;에서 else는 어느 if의 것인가? 문법이 모호하면 두 파스 트리가 모두 유효하고, 어느 쪽이 맞는지는 파서 구현이 아니라 언어 명세가 정할 일이다(대부분 "가장 가까운 if"로 규정하고, 문법이나 파서가 그 선택을 구현한다). 구현이 명세 없이 암묵적으로 한쪽을 고르면, 그 선택이 곧 비공식 명세가 되어 다른 구현과 어긋나기 시작한다. DSL을 만들 때 생성기의 모호성 검출이 유용한 이유이자, "파서가 잘 돌아간다"와 "문법이 잘 정의됐다"가 다른 명제인 이유다.

파서를 벗어난 검사를 문법에 욱여넣지 않는다

"함수 인자는 최대 255개", "break는 루프 안에서만" 같은 규칙은 context-free 문법으로 표현하기 어렵거나 문법을 심하게 복잡하게 만든다. 이런 검사는 파싱 후 AST 위의 별도 패스(의미 분석)로 미루는 것이 표준 구조다. 문법은 구조 복원까지만 맡기고, 맥락이 필요한 규칙은 맥락을 가진 단계에 준다 — 단계별 책임 분리의 또 다른 사례다. 이름 바인딩·스코프처럼 AST에 의미를 부여하는 다음 단계는 챕터 4(언어 이론과 타입 시스템)가 다룬다.

정리

  • 렉서는 문자를 토큰(종류·lexeme·span)으로 묶고, 파서는 토큰열에서 우선순위·결합 법칙이 반영된 계층 구조를 복원한다. 두 단계의 분리는 정규 언어와 context-free 언어라는 표현력 차이와 맞물린다.
  • AST 판독 규칙은 "깊은 노드가 먼저 평가된다"이다. 우선순위는 트리 깊이로, 결합 법칙은 쌓이는 방향으로 번역된다.
  • AST는 CST에서 표면 구문을 버린 것이고, 무엇을 버려도 되는지는 소비자가 정한다. 진단에는 span이, 포매터에는 주석·괄호가, 의미 분석에는 군더더기 없는 구조가 필요하다.
  • 재귀 하강은 코드 직역과 진단 품질에, LR 생성기는 문법 표현력과 모호성 조기 검출에 강하다. 산업용 컴파일러의 기본값은 손으로 쓴 재귀 하강이다.
  • 오류 복구는 파일 전체 진단을 위한 투자이고, 동기화 지점 선택이 품질을 좌우하며, 후속 가짜 오류라는 비용이 따른다.

확인 문제

1. 팀의 DSL에서 2 + 3 < 4 * 2 == true가 기대와 다르게 평가된다는 제보가 왔다. 파서 구현을 읽기 전에 AST 덤프를 먼저 보라고 조언하는 이유를 설명하고, 이 식이 ((2 + 3) < (4 * 2)) == true로 파싱됐는지 확인하려면 트리의 어떤 모양을 봐야 하는지 서술하라.

정답과 해설

우선순위·결합 법칙 버그는 소스 문자열에는 보이지 않고 트리 모양에만 나타난다. 파서 코드를 읽는 것보다 산출물을 보는 쪽이 "규칙이 실제로 어떻게 적용됐는가"의 직접 증거다. 기대 모양: 루트가 ==, 그 왼쪽 자식이 <, <의 왼쪽이 +(자식 2, 3), 오른쪽이 *(자식 4, 2), ==의 오른쪽이 true. 즉 우선순위가 높은 연산자일수록 깊이 놓였는지(* +<보다, <==보다 깊은지)를 확인한다. 만약 <*보다 깊다면 비교가 산술보다 먼저 묶인 것이므로 우선순위 규칙 구현(재귀 하강이라면 함수 호출 순서)이 뒤집힌 것이다.

2. IDE 팀이 "파일에 문법 오류가 하나라도 있으면 자동완성이 전부 꺼진다"는 버그를 보고했다. 원인이 파서의 어떤 설계 선택에 있을 가능성이 높고, 무엇을 바꿔야 하는가? 그 변경이 새로 만드는 문제도 한 가지 지적하라.

정답과 해설

파서가 fail-fast로 설계되어 첫 오류에서 트리 생성을 포기했을 가능성이 높다. 자동완성은 커서 주변의 트리 문맥을 요구하므로, 트리가 아예 없으면 기능 전체가 꺼진다. 변경 방향은 복구 파싱이다 — 오류 지점에서 동기화 토큰까지 건너뛰고 오류 노드를 트리에 포함시켜, 깨진 구간 밖에서는 유효한 트리를 유지한다. 새로 생기는 문제: 복구가 문맥을 잘못 잡으면 후속 가짜 오류가 연쇄되고, 오류 노드가 섞인 트리를 소비하는 모든 코드(타입 검사, 자동완성)가 "불완전한 노드"라는 경우를 처리해야 하므로 소비자 쪽 복잡성이 늘어난다.

3. 대규모 코드베이스에서 getConfig라는 함수 이름을 loadConfig로 바꾸는 작업을 정규식 치환으로 하다가, 문자열 리터럴과 주석 안의 getConfig, 그리고 다른 객체의 동명 메서드까지 바뀌는 사고가 났다. 이 사고를 렉서·파서의 책임 분해로 설명하고, AST 기반 codemod가 이 중 무엇을 해결하고 무엇을 해결하지 못하는지 구분하라.

정답과 해설

정규식은 문자 수준에서 동작하므로 토큰 종류(식별자 vs 문자열 내용 vs 주석)를 모르고, 트리 문맥(어느 선언에 묶인 이름인지)도 모른다. AST 기반 codemod는 렉싱이 제공하는 정보로 문자열·주석 안의 출현을 배제하고, 트리 문맥으로 "함수 호출의 callee 위치인 식별자"만 겨냥할 수 있다. 해결하지 못하는 것: 다른 객체의 동명 메서드 구분은 구문이 아니라 이름 바인딩과 타입 정보를 요구한다 — a.getConfig()a가 무엇인지는 AST만으로 모른다. 이를 위해서는 스코프 분석이나 타입 체커가 결합된 도구(언어 서비스 기반 리네임)가 필요하다. 구문 수준 도구와 의미 수준 도구의 경계를 아는 것이 안전한 자동 수정의 전제다.

참고 자료

  • Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman, Compilers: Principles, Techniques, and Tools 2nd ed. (2006), Ch. 3–4 — 렉싱·파싱 이론의 표준 서술. maximal munch, LR 테이블, 오류 복구의 원전 격 정리다.
  • Keith D. Cooper, Linda Torczon, Engineering a Compiler 3rd ed. (2022), Ch. 2–3 — 재귀 하강과 LR의 구현 관점 비교, AST 설계 판단을 확인한다.
  • Robert Nystrom, Crafting Interpreters — 재귀 하강 파서와 panic-mode 복구를 완전한 코드로 따라가는 자료. 이 문서의 Toy 파서와 같은 구조를 더 큰 언어로 확장해 볼 때 참고한다.
  • ECMA-262, ECMAScript Language Specification — Lexical Grammar — 어휘 문법의 목표 심볼이 여러 개인 실제 사례(/의 컨텍스트 의존성)를 명세 원문으로 확인한다.
  • rust-analyzer, Syntax in rust-analyzer — IDE급 도구가 요구하는 lossless·오류 내성 구문 트리 설계의 공개된 실전 문서.