Grammar

Grammar

텍스트를 규칙에 따라 분석하고 싶을 때, Raku에서는 grammar 선언으로 문법을 정의할 수 있어요. 그렇게 선언된 모든 문법 타입의 기반이 되는 클래스가 바로 Grammar예요.

출처: Raku 공식 문서 — Grammar

본문

GrammarMatch를 상속하는 클래스예요. grammar로 선언하면서 슈퍼클래스를 따로 명시하지 않은 모든 타입은 Grammar의 서브클래스가 됩니다.

class Grammar is Match {}

예를 들어 식별자(identifier)를 파싱하는 문법을 하나 만들어 보면, 우리가 만든 Identifier 문법이 실제로 Grammar의 일종인지 확인할 수 있어요.

grammar Identifier {
    token TOP       { <initial> <rest>* }
    token initial   { <+myletter +[_]> }
    token rest      { <+myletter +mynumber +[_]> }
    token myletter  { <[A..Za..z]> }
    token mynumber  { <[0..9]> }
}

say Identifier.isa(Grammar);                # OUTPUT: «True␤»
my $match = Identifier.parse('W4anD0eR96');
say ~$match;                                # OUTPUT: «W4anD0eR96␤»

문법에 대한 더 자세한 내용은 문법(grammars) 문서문법 튜토리얼에서 다뤄요.

메서드

method parse

method parse($target, :$rule = 'TOP',  Capture() :$args = \(), Mu :$actions = Mu, *%opt)

$target을 파싱해요. $targetStr이 아니면 Str로 강제 변환되고, 시작 규칙으로는 $rule을 써요. $args를 주면 시작 규칙에 추가 인자로 전달됩니다.

grammar RepeatChar {
    token start($character) { $character+ }
}

say RepeatChar.parse('aaaaaa', :rule('start'), :args(\('a')));
say RepeatChar.parse('bbbbbb', :rule('start'), :args(\('b')));

# OUTPUT:
# 「aaaaaa」
# 「bbbbbb」

actions라는 이름 인자를 넘기면, 각 regex 매치가 성공할 때마다 같은 이름의 메서드가 actions 객체에서 호출돼요. 이때 매치 객체가 유일한 위치 인자로 전달되지요.

my $actions = class { method TOP($/) { say "7" } };
grammar { token TOP { a { say "42" } b } }.parse('ab', :$actions);
# OUTPUT: «42␤7␤»

추가 이름 인자는 매칭 옵션으로 쓰여요. 예를 들어 :pos(4)를 주면 0부터 세는 기준으로 다섯 번째 문자부터 파싱을 시작해요. regex가 가질 수 있는 매칭 부사(adverb)는 여러 종류가 있는데, :s:i처럼 컴파일 타임에 적용되는 부사는 .parse에 넘길 수 없어요. regex는 이미 컴파일되어 있으니까요. 대신 :pos:continue처럼 런타임 동작에 영향을 주는 부사는 넘길 수 있습니다.

say RepeatChar.parse('bbbbbb', :rule('start'), :args(\('b')), :pos(4)).Str;
# OUTPUT: «bb␤»

parse 메서드는 매치가 끝났을 때 커서(cursor)가 대상 문자열의 끝에 도달해야만 성공해요. 중간에서 멈출 수 있게 하려면 subparse 메서드를 쓰면 됩니다.

문법의 최상위 regex는 백트래킹(backtrack)할 수 있게 허용돼요. 성공하면 Match를, 실패하면 Nil을 돌려줘요.

method subparse

method subparse($target, :$rule = 'TOP', Capture() :$args = \(),  Mu :$actions = Mu, *%opt)

parse와 거의 똑같이 동작하지만, 한 가지가 달라요. 성공하기 위해 커서가 문자열 끝까지 도달할 필요가 없어요. 즉 전체 문자열을 맞출 필요가 없다는 뜻이죠.

parse와 달리 subparse는 항상 Match를 돌려줘요. 문법이 매치에 실패하면 실패한 매치(그래서 거짓인 값)를 돌려주는 식이에요.

grammar RepeatChar {
    token start($character) { $character+ }
}

say RepeatChar.subparse('bbbabb', :rule('start'), :args(\('b')));
say RepeatChar.parse(   'bbbabb', :rule('start'), :args(\('b')));
say RepeatChar.subparse('bbbabb', :rule('start'), :args(\('a')));
say RepeatChar.subparse('bbbabb', :rule('start'), :args(\('a')), :pos(3));


# OUTPUT:
# 「bbb」
# Nil
# #<failed match>
# 「a」

method parsefile

method parsefile(Str(Cool) $filename, :$enc, *%opts)

$enc 인코딩으로 $filename 파일을 읽어서 파싱해요. 모든 이름 인자는 parse 메서드로 그대로 전달됩니다.

grammar Identifiers {
    token TOP        { [<identifier><.ws>]+ }
    token identifier { <initial> <rest>* }
    token initial    { <+myletter +[_]> }
    token rest       { <+myletter +mynumber +[_]> }
    token myletter   { <[A..Za..z]> }
    token mynumber   { <[0..9]> }
}

say Identifiers.parsefile('users.txt', :enc('UTF-8'))
    .Str.trim.subst(/\n/, ',', :g);

# users.txt :
# TimToady
# lizmat
# jnthn
# moritz
# zoffixznet
# MasterDuke17

# OUTPUT: «TimToady,lizmat,jnthn,moritz,zoffixznet,MasterDuke17␤»