JParser (9) Abstract Syntax Tree 프로세서
파스 트리는 말그대로 파스의 결과를 나타내는 것으로, 인풋 스트링의 모든 글자에 대한 모든 derivation의 정보를 모두 포함한다. 하지만 실제로 파싱 결과를 이용할 때는 보통 파스 트리 정보 중 관심 있는 일부 정보만 필요하다. 예를 들어 컴파일러는 프로그램에 포함된 주석같은 것에는 관심이 없다. 그래서 파스 트리를 가공한 abstract syntax tree, 줄여서 AST라는 것을 도입하게 된다.
간단한 문법도 직접 파스 트리를 AST로 바꾸는 코드를 만들려면 코드가 간단치 않다. 복잡하다기보단 코드 양이 많다. 앞에서부터 계속 사용해오고 있는 다음의 예제를 보자.
Expr = Term WS '+' WS Expr
| Term
Term = Factor WS '*' WS Term
| Factor
Factor = '0-9'
| '(' WS Expr WS ')'
WS = ' '*
이 문법으로 파싱한 결과로 나온 파스 트리를 AST로 바꾸려는 경우를 생각해보자. 먼저 AST 자료 구조를 정의해야 한다. Expr
심볼은 덧셈 연산 수식을 나타낼 수 있고, Term
심볼이 될 수도 있다. Term
심볼은 곱셈 연산 수식을 나타낼 수 있고, Factor
심볼일 수 있다. Factor
는 한자리 숫자이거나 괄호로 묶인 Expr
일 수 있다. 이 정보를 자료 구조로 나타낸다면 다음과 같은 형태가 될 것이다.
sealed trait Expr
case class Add(lhs: Term, rhs: Expr) extends Expr
sealed trait Term extends Expr
case class Multiply(lhs: Factor, rhs: Term) extends Term
sealed trait Factor extends Term
case class Number(value: String) extends Factor
case class Paren(body: Expr) extends Factor
Paren
클래스가 별도로 있는 것은 Term
이나 Factor
로 들어와서 괄호 수식이 나온 경우, 즉 (1)+2
와 같은 경우 (1)
부분은 바로 Term
이 되어 Expr값을 가질 수 없기 때문이다.
클래스 구조를 잡은 뒤엔 파스 결과를 받아서 이들 클래스 값으로 변환하는 코드도 작성해야 한다.
def matchExpr(node: ParseResultTree.Node): Expr = {
val BindNode(v1, v2) = node
val v12 = v1.id match {
case 3 =>
val v3 = v2.asInstanceOf[SequenceNode].children.head
val BindNode(v4, v5) = v3
assert(v4.id == 4)
val v6 = v2.asInstanceOf[SequenceNode].children(4)
val BindNode(v7, v8) = v6
assert(v7.id == 2)
Add(matchTerm(v5), matchExpr(v8))
case 21 =>
val v9 = v2.asInstanceOf[SequenceNode].children.head
val BindNode(v10, v11) = v9
assert(v10.id == 4)
matchTerm(v11)
}
v12
}
“case 3” 이후의 경우는 Term WS '+' WS Expr
로 매치된 경우이고 “case 21”의 경우는 Term
으로 매치된 경우이다. 중간 중간 등장하는 숫자들은 NGrammar로 변환된 심볼의 ID이다.
여기서 끝이 아니라 matchExpr
과 비슷한 matchTerm
, matchFactor
등의 함수도 만들어야 한다. 이런 단순한 문법 하나도 처리하려면 이렇게 많은 작업이 필요하다.
작업의 양이 많은 것 외에도, 이런 코드를 만들 때 또 한가지 문제는, CDG 문법이 실제 Grammar
클래스로 변환되는 과정이 사용자에겐 헷갈릴 수 있다는 점이다. 위의 matchExpr
함수에서 case 21
의 경우, RHS에는 Term
이라는 심볼 하나밖에 없지만 실제 Grammar
함수로 변환될 때 Term
심볼 하나를 가진 시퀀스로 변환되었기 때문에 .asInstanceOf[SequenceNode].children.head
와 같은 코드가 포함된 것을 볼 수 있다.
다행히 이런 작업들은 모두 자동화될 수 있다. 물론 사용자가 원하는 AST가 어떤 형태인지는 사용자가 지정해주어야 하고, 그 정보로부터 위와 같이 클래스 구조를 정의하고, 파스 노드를 AST 클래스로 변경하는 코드를 생성해주는 기능이 jparser에 구현되어 있다. 사용자는 문법 정의에 자신이 원하는 AST 구조에 대한 정보를 입력하는데, 그 기능을 앞으로 AST 프로세서라고 부를 것이다.
AST 프로세서 문법
AST 프로세서는 derivation rule의 RHS에 다른 심볼들과 같이 등장한다. AST 프로세서는 $0
과 같이 $
기호 뒤에 숫자가 오는 형태이거나 {Abc(value=ispresnet($1) ? str($0):"foo")}
와 같이 중괄호로 묶인 수식의 형태이다.
예제를 보면서 이야기해보자.
Expr: Expr = Term WS '+' WS Expr {Add(lhs=$0, rhs=$4)}
| Term
Term: Term = Factor WS '*' WS Term {Multiply(lhs=$0, rhs=$4)}
| Factor
Factor: Factor = '0-9' {Number(value=str($0))}
| '(' WS Expr WS ')' {Paren(body=$2)}
WS = ' '*
앞서 살펴보았던 문법에 AST 프로세서들을 추가하였다. 앞서 보았던 자료 구조와 matchExpr
함수는 사실 jparser가 이 문법으로부터 생성해낸 코드(를 살짝 수정한 것)이다.
Expr: Expr
에서 콜론 왼쪽은 넌터미널 이름이고 오른쪽은 타입의 이름이다. 심볼의 이름과 클래스 이름의 별도의 세계에 존재하는 것이기 때문에 이름이 동일해도 상관 없다. Expr
이라는 넌터미널은 Expr
클래스로 변환됨을 명시적으로 정의한 것이다. 이런 명시적 타입을 생략할 수 있는 경우도 있는데 이 경우엔 필요하다(왜 필요한지는 다음 포스팅에서 자세히 살펴보자).
Expr
넌터미널의 첫번째 RHS Term WS '+' WS Expr {Add(lhs=$0, rhs=$4)}
에서 $0
은 그 시퀀스의 첫번째 심볼(Term
심볼)의 값을 나타내고, $4
는 다섯번째 심볼(Expr
심볼)의 값을 나타낸다. Add
는 클래스의 이름이고, lhs
와 rhs
는 클래스의 멤버 변수 이름이다. 따라서 Add(lhs=$0, rhs=$4)
는 이 RHS가 매치되는 경우, 첫번재 심볼인 Term
의 결과를 lhs
라는 이름의 멤버 변수로, 다섯번째 심볼인 Expr
의 결과를 rhs
라는 이름의 멤버 변수로 갖는 Add
라는 클래스를 만든다는 의미이다. 보다시피 별도로 클래스 정의는 필요 없으며, AST 프로세서 정의에 나오는 클래스의 이름들과 멤버 변수들의 이름들을 취합하고 멤버 변수들의 타입을 유추해서 데이터 구조를 생성해준다.
시퀀스 심볼에는 프로세스건 심볼이건 가장 마지막 것의 값을 시퀀스 전체를 대표하는 값으로 본다. 그래서 Term WS '+' WS Expr {Add(lhs=$0, rhs=$4)}
라는 시퀀스의 실행 결과는 마지막 프로세서의 실행 결과가 된다.
Expr: Expr = Term
과 Term: Term
으로부터 Expr
타입이 Term
타입의 값을 받을 수 있어야함을 알 수 있다. 이는 Expr
클래스가 Term
클래스의 상위 클래스임을 뜻한다. 문법 전체에 이런 식으로 정보를 취합하여 클래스 정의에 반영한다. Term
클래스는 Factor
의 상위 클래스여야하고, Factor
클래스는 Number
와 Paren
의 상위 클래스여야 한다.
Factor
의 첫번째 RHS에는 {Number(value=str($0))}
이라는 수식이 있다. 예상대로 str
은 $0
을 스트링으로 바꿔주는 빌트인 함수이다.
같은 문법으로부터 AST를 다른 형태로 추출할 수도 있다. 다음의 예를 보자.
Expr: Expr = Term WS '+' WS Expr {BinOp(op:%Op=%Add, lhs=$0, rhs=$4)}
| Term
Term: Term = Factor WS '*' WS Term {BinOp(op=%Mul, lhs=$0, rhs=$4)}
| Factor
Factor: Factor = '0-9' {Number(value=str($0))}
| '(' WS Expr WS ')' {Paren(body=$2)}
WS = ' '*
문법은 완전히 동일하지만 덧셈과 곱하기 연산 수식을 나타내는 방식이 바뀌었다. BinOp
이라는 클래스를 만들었고 그 클래스에 op
이라는 필드를 추가했다. %Op
과 같이 %
가 앞에 붙은 것은 enum 타입의 이름이다. %Op.Add
, %Op.Mul
과 같이 enum의 이름을 적을 수 있는데, %Op
타입인것이 분명한 경우엔 %Add
, %Mul
과 같이 타입명을 생략해도 된다. 각 enum 타입에 어떤 값이 지정되었는지 취합해서 enum 타입 정의에 반영해 준다.
sealed trait Expr
case class BinOp(op: Op.Value, lhs: Term, rhs: Expr) extends Term
object Op extends Enumeration { val Add, Mul = Value }
sealed trait Term extends Expr
sealed trait Factor extends Term
case class Number(value: String) extends Factor
case class Paren(body: Expr) extends Factor
Add
, Multiply
클래스가 사라지고 Op
이라는 enum 타입과 BinOp
클래스가 생성되었다.
데이터 타입
AST 프로세서에는 다음과 같은 타입이 있다.
- 참/거짓 중 하나의 값을 갖는 불리언(
boolean
) - 캐릭터(
char
) - 스트링(
string
) - 리스트(e.g.
[string]
) - enum(e.g.
%Op
) - 커스텀 클래스(e.g.
BinOp
)
null
도 지원되며, 타입 뒤에 물음표를 붙여서 명시적으로 nullable로 정의할 수 있다. nullable로 명시되지 않은 타입은 null값을 가질 수 없다. 숫자 타입은 없다. any
타입도 지정할 수 있지만 나는 한 번도 써본 적이 없다.
AST 프로세싱 규칙
AST 프로세서를 지정하지 않으면 심볼은 타입에 따라 다음과 같이 처리된다.
- terminal 심볼은 실제 입력된 char 값으로 처리된다.
- nonterminal 심볼은 명시적으로 타입이 정의된 경우 그 타입을, 그렇지 않은 경우 RHS들의 타입의 union 타입인 것으로 본다.
- repeat 심볼(
A*
,A+
)는 심볼 A의 타입의 리스트가 된다. - optional 심볼(
A?
)는 심볼 A의 타입의 optional(nullable) 타입이 된다. - longest 심볼(
<A>
)은A
의 값을 그대로 따라간다. - join 심볼(
A&B
)는 기본적으로는 해당 영역에서 매치되는A
의 값을 따라가지만, 해당 심볼을 가리킬 때$0
대신$>0
라고 쓰면B
의 값도 사용 가능하다. - except 심볼(
A&B
)는A
의 값을 따라간다. - lookahead 심볼은 빈 시퀀스로 처리된다.
- 스트링 심볼은 스트링 값으로 처리된다.
앞서 말한대로 시퀀스는 시퀀스의 마지막 element 값을 따라간다. 시퀀스의 마지막 element가 프로세서면 프로세서가 반환하는 값, 심볼이면 심볼의 값이 된다.
AST 프로세서 expression
- 3항 연산자, e.g.
ispresent($1)? str($2):"hello"
- 불리언 연산자, e.g.
$0 && $2
,$1 || $3
,!$1
- 비교 연산자:
==
,!=
- elvis 연산자, e.g.
$0 ?: []
- 덧셈 연산자, e.g.
$0 + $1
,['a', 'b'] + ['c']
,"Hello" + "World"
. 리스트나 스트링에 적용 가능하다. - 클래스 인스턴스 생성, e.g.
Hello(world=$1, value="Hahaha")
- 함수:
str
,chr
,isempty
,ispresent
- bound expression, e.g.
$0{$1}
,$0$1
.$0
이 시퀀스인 경우,$0
의 가장 마지막에$1
을 쓴 것과 같은 효과. - enum, e.g.
%EnumType.EnumValue
,%EnumValue
- 리스트:
[$0, $1, $2]
- literal:
null
,true
/false
, character'a'
, string"abc"
AST 프로세서를 포함한 CDG 문법
다음은 AST 프로세서 기능을 포함한 CDG의 문법을 AST 프로세서 기능을 포함한 CDG로 정의한 것이다. (MetaLang3Grammar.scala) 실제 jparser 구현체에서 사용하는 문법 정의이다. 중간 중간 문법에 대한 해석을 붙여 놓았다.
Grammar = WS Def (WSNL Def)* WS {Grammar(defs=[$1] + $2)}
Def: Def = Rule | TypeDef
Rule = LHS WS '=' WS (RHS (WS '|' WS RHS)* {[$0] + $1}) {Rule(lhs=$0, rhs=$4)}
// $4는 괄호 안 시퀀스의 마지막 값인 [$0] + $1 을 가리킨다. $4{[$0] + $1} 라고 바꿔 쓸 수 있다.
LHS = Nonterminal (WS ':' WS TypeDesc)? {LHS(name=$0, typeDesc=$1)}
// LHS의 typeDesc는 TypeDesc 심볼의 타입(TypeDesc 클래스)의 nullable 타입이다.
RHS = Sequence
Sequence = Elem (WS Elem)* {Sequence<Symbol>(seq=[$0] + $1)}
// Sequence<Symbol>이라고 쓰면 Sequence 타입이 Symbol 타입을 상속받아야 함을 명시한 것
// $0은 Elem이고 $1은 Elem의 리스트 타입이므로 둘을 이어 붙이려면 [$0] + $1 라고 써야 한다.
Elem: Elem = Symbol | Processor
// Symbol
Symbol: Symbol = BinSymbol
BinSymbol: BinSymbol = BinSymbol WS '&' WS PreUnSymbol {JoinSymbol(body=$0, join=$4)}
| BinSymbol WS '-' WS PreUnSymbol {ExceptSymbol(body=$0, except=$4)}
| PreUnSymbol
PreUnSymbol: PreUnSymbol = '^' WS PreUnSymbol {FollowedBy(followedBy=$2)}
| '!' WS PreUnSymbol {NotFollowedBy(notFollowedBy=$2)}
| PostUnSymbol
PostUnSymbol: PostUnSymbol = PostUnSymbol WS '?' {Optional(body=$0)}
| PostUnSymbol WS '*' {RepeatFromZero(body=$0)}
| PostUnSymbol WS '+' {RepeatFromOne(body=$0)}
| AtomSymbol
AtomSymbol: AtomSymbol = Terminal
| TerminalChoice
| StringSymbol
| Nonterminal
| '(' WS InPlaceChoices WS ')' $2 // $2 대신 {$2}라고 써도 같은 의미이다.
| Longest
| EmptySequence
Terminal: Terminal = '\'' TerminalChar '\'' $1
| '.' {AnyTerminal()} // 인자를 받지 않는 클래스도 있을 수 있다
TerminalChoice: TerminalChoice = '\'' TerminalChoiceElem TerminalChoiceElem+ '\'' {TerminalChoice(choices=[$1] + $2)}
| '\'' TerminalChoiceRange '\'' {TerminalChoice(choices=[$1])}
// 클래스의 멤버 변수 이름과 순서는 한 번만 정의하면 된다.
// 즉, 위에서 TerminalChoice가 choices라는 하나의 파라메터를 받는 것을 알 수 있으니 두번째는 TerminalChoice([$1]) 라고만 써도 된다.
TerminalChoiceElem: TerminalChoiceElem = TerminalChoiceChar
| TerminalChoiceRange
TerminalChoiceRange = TerminalChoiceChar '-' TerminalChoiceChar {TerminalChoiceRange(start=$0, end=$2)}
StringSymbol = '"' StringChar* '"' {StringSymbol(value=$1)}
Nonterminal = NonterminalName {Nonterminal(name=$0)}
InPlaceChoices = Sequence (WS '|' WS Sequence)* {InPlaceChoices(choices=[$0] + $1)}
Longest = '<' WS InPlaceChoices WS '>' {Longest(choices=$2)}
EmptySequence = '#' {EmptySeq()}
TerminalChar: TerminalChar = .-'\\' {CharAsIs(value=$0)}
| '\\' '\'\\bnrt' {CharEscaped(escapeCode=$1)}
| UnicodeChar
TerminalChoiceChar: TerminalChoiceChar = .-'\'\-\\' {CharAsIs(value=$0)}
| '\\' '\'\-\\bnrt' {CharEscaped(escapeCode=$1)}
| UnicodeChar
StringChar: StringChar = .-'"\\' {CharAsIs(value=$0)}
| '\\' '"\\bnrt' {CharEscaped(escapeCode=$1)}
| UnicodeChar
UnicodeChar = '\\' 'u' '0-9A-Fa-f' '0-9A-Fa-f' '0-9A-Fa-f' '0-9A-Fa-f' {CharUnicode(code=[$2, $3, $4, $5])}
// Processor
Processor: Processor = Ref
| PExprBlock
Ref: Ref = ValRef | RawRef
ValRef = '$' CondSymPath? RefIdx {ValRef(idx=$2, condSymPath=$1)}
CondSymPath: [%CondSymDir{BODY, COND}] = ('<' {%BODY} | '>' {%COND})+
// %CondSymDir{BODY, COND} 라고 쓰면 CondSymDir이란 enum 타입의 멤버 값이 BODY와 COND가 있다는 의미이다.
// enum의 멤버 값을 이렇게 명시하면 명시된 값만 사용할 수 있다. 문법 정의의 다른 곳에서 명시되지 않은 값을 쓰려고 할 때 오류가 발생한다.
RawRef = "\\$" CondSymPath? RefIdx {RawRef(idx=$2, condSymPath=$1)}
PExprBlock = '{' WS PExpr WS '}' {ProcessorBlock(body=$2)}
PExpr: PExpr = TernaryExpr WS ':' WS TypeDesc {TypedPExpr(body=$0, typ=$4)}
| TernaryExpr
TernaryExpr: TernaryExpr = BoolOrExpr WS '?' WS <TernaryExpr> WS ':' WS <TernaryExpr> {TernaryOp(cond=$0, ifTrue=$4, ifFalse=$8)}
| BoolOrExpr
BoolOrExpr: BoolOrExpr = BoolAndExpr WS "&&" WS BoolOrExpr {BinOp(op=%Op.AND, lhs=$0, rhs=$4)}
| BoolAndExpr
BoolAndExpr: BoolAndExpr = BoolEqExpr WS "||" WS BoolAndExpr {BinOp(op=%Op.OR, lhs=$0, rhs=$4)}
| BoolEqExpr
BoolEqExpr: BoolEqExpr = ElvisExpr WS ("==" {%Op.EQ} | "!=" {%Op.NE}) WS BoolEqExpr {BinOp(op=$2, lhs=$0, rhs=$4)}
| ElvisExpr
ElvisExpr: ElvisExpr = AdditiveExpr WS "?:" WS ElvisExpr {ElvisOp(value=$0, ifNull=$4)}
| AdditiveExpr
AdditiveExpr: AdditiveExpr = PrefixNotExpr WS ('+' {%Op.ADD}) WS AdditiveExpr {BinOp(op=$2, lhs=$0, rhs=$4)}
| PrefixNotExpr
PrefixNotExpr: PrefixNotExpr = '!' WS PrefixNotExpr {PrefixOp(op=%PreOp.NOT, expr=$2)}
| Atom
Atom: Atom = Ref
| BindExpr
| NamedConstructExpr
| FuncCallOrConstructExpr
| ArrayExpr
| Literal
| EnumValue
| '(' WS PExpr WS ')' {ExprParen(body=$2)}
BindExpr = ValRef BinderExpr {BindExpr(ctx=$0, binder=$1)}
BinderExpr: BinderExpr = Ref
| BindExpr
| PExprBlock
NamedConstructExpr = TypeName (WS SuperTypes)? WS NamedConstructParams {NamedConstructExpr(typeName=$0, params=$3, supers=$1)}
NamedConstructParams = '(' WS (NamedParam (WS ',' WS NamedParam)* WS {[$0] + $1}) ')' $2
NamedParam = ParamName (WS ':' WS TypeDesc)? WS '=' WS PExpr {NamedParam(name=$0, typeDesc=$1, expr=$5)}
FuncCallOrConstructExpr = TypeOrFuncName WS CallParams {FuncCallOrConstructExpr(funcName=$0, params=$2)}
CallParams: [PExpr] = '(' WS (PExpr (WS ',' WS PExpr)* WS {[$0] + $1})? ')' {$2 ?: []}
// $2는 PExpr 리스트의 nullable 타입이다. elvis 연산자로 $2가 null이면 빈 리스트를 반환하도록 하였다.
ArrayExpr = '[' WS (PExpr (WS ',' WS PExpr)* WS)? ']' {ArrayExpr(elems: [PExpr]=$2{[$0] + $1} ?: [])}
Literal: Literal = "null" {NullLiteral()}
| ("true" {true} | "false" {false}) {BoolLiteral(value=$0)}
| '\'' CharChar '\'' {CharLiteral(value=$1)}
| '"' StrChar* '"' {StrLiteral(value=$1)}
EnumValue: AbstractEnumValue = <CanonicalEnumValue | ShortenedEnumValue>
CanonicalEnumValue = EnumTypeName '.' EnumValueName {CanonicalEnumValue(enumName=$0, valueName=$2)}
ShortenedEnumValue = '%' EnumValueName {ShortenedEnumValue(valueName=$1)}
// ShortenedEnumValue는 값이 enum type이 무엇인지 확실할 때만 사용할 수 있다.
// TypeDef
TypeDef: TypeDef = ClassDef
| SuperDef
| EnumTypeDef
ClassDef: ClassDef = TypeName WS SuperTypes {AbstractClassDef(name=$0, supers=$2)}
| TypeName WS ClassParamsDef {ConcreteClassDef(name=$0, supers: [TypeName]?=null, params=$2)}
| TypeName WS SuperTypes WS ClassParamsDef {ConcreteClassDef(name=$0, supers=$2, params=$4)}
SuperTypes: [TypeName] = '<' WS (TypeName (WS ',' WS TypeName)* WS {[$0] + $1})? '>' {$2 ?: []}
ClassParamsDef: [ClassParamDef] = '(' WS (ClassParamDef (WS ',' WS ClassParamDef)* WS {[$0] + $1})? WS ')' {$2 ?: []}
ClassParamDef = ParamName (WS ':' WS TypeDesc)? {ClassParamDef(name=$0, typeDesc=$1)}
SuperDef = TypeName (WS SuperTypes)? WS '{' (WS SubTypes)? WS '}' {SuperDef(typeName=$0, subs=$4, supers=$1)}
SubTypes = SubType (WS ',' WS SubType)* {[$0] + $1}
SubType: SubType = TypeName | ClassDef | SuperDef
EnumTypeDef = EnumTypeName WS '{' WS (Id (WS ',' WS Id)* {[$0] + $1}) WS '}' {EnumTypeDef(name=$0, values=$4)}
// TypeDesc
TypeDesc = NonNullTypeDesc (WS '?')? {TypeDesc(typ=$0, optional=ispresent($1))}
NonNullTypeDesc: NonNullTypeDesc = TypeName
| '[' WS TypeDesc WS ']' {ArrayTypeDesc(elemType=$2)}
| ValueType
| AnyType
| EnumTypeName
| TypeDef
ValueType: ValueType = "boolean" {BooleanType()}
| "char" {CharType()}
| "string" {StringType()}
AnyType = "any" {AnyType()}
EnumTypeName = '%' Id {EnumTypeName(name=str($1))}
// Common
TypeName = IdNoKeyword {TypeName(name=$0)}
| '`' Id '`' {TypeName(name=$1)}
NonterminalName = IdNoKeyword {NonterminalName(name=$0)}
| '`' Id '`' {NonterminalName(name=$1)}
TypeOrFuncName = IdNoKeyword {TypeOrFuncName(name=$0)}
| '`' Id '`' {TypeOrFuncName(name=$1)}
ParamName = IdNoKeyword {ParamName(name=$0)}
| '`' Id '`' {ParamName(name=$1)}
EnumValueName = Id {EnumValueName(name=$0)}
Keyword: %KeyWord = "boolean" {%BOOLEAN}
| "char" {%CHAR}
| "string" {%STRING}
| "true" {%TRUE}
| "false" {%FALSE}
| "null" {%NULL}
// Keyword가 %Keyword 타입이므로 Keyword의 RHS에 나오는 값들은 모두 %Keyword 타입임을 알 수 있다.
StrChar = StringChar
CharChar = TerminalChar
RefIdx = <'0' | '1-9' '0-9'*> {str(\$0)}
// \$0라고 쓰면 $0의 parse tree를 있는대로 반환한다.
// 여기서 \$0 대신 $0라고 쓰면 '1-9' '0-9'*에서 '0-9'*만 반영될 것이다.
Id = <'a-zA-Z_' 'a-zA-Z0-9_'*> {str(\$0)}
IdNoKeyword = Id-Keyword {str(\$0)}
WS = (' \n\r\t' | Comment)*
WSNL = <(' \r\t' | Comment)* '\n' WS>
Comment = LineComment | BlockComment
LineComment = "//" (.-'\n')* (EOF | '\n')
BlockComment = "/*" ((. !"*/")* .)? "*/"
EOF = !.
// !.는 파일의 마지막을 의미한다.
// .은 모든 글자에 매치되므로, !.가 나오면 다음에 어떤 글자가 와도 매치될 수 없기 때문이다.
전체 목차:
다음 포스팅에서는 parse tree에서 AST 프로세서 코드를 생성하는 원리를 코드와 함께 살펴보자.
- JParser (1) 파싱
- JParser (2) Conditional Derivation Grammar
- JParser (3) CDG 사용 팁
- JParser (4) Naive 파싱 알고리즘
- JParser (5) Naive 파싱 알고리즘 동작 예제
- JParser (6) 승인 조건
- JParser (7) 파스 트리 재구성
- JParser (8) 마일스톤 파싱 알고리즘 (WIP)
- JParser (9) Abstract Syntax Tree 프로세서
- JParser (10) AST 프로세서 구현 (WIP)
- JParser (11) 결론 1 실제 사용 사례
- JParser (12) 결론 2 future works