├── .gitignore ├── Changes ├── dist.ini ├── t ├── slang.rakutest ├── generate.rakutest ├── parse.rakutest └── abnf.rakutest ├── .github └── workflows │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── META6.json ├── LICENSE ├── run-tests ├── lib ├── Slang │ ├── BNF.rakumod │ └── ABNF.rakumod └── Grammar │ ├── BNF.rakumod │ └── ABNF.rakumod └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .precomp/ 2 | /Grammar-ABNF-* 3 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for Grammar::BNF 2 | 3 | {{$NEXT}} 4 | 5 | 1.1 2024-12-11T18:59:42+01:00 6 | - Initial version as a Raku Community module 7 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = Grammar::BNF 2 | 3 | [ReadmeFromPod] 4 | filename = lib/Grammar/BNF.rakumod 5 | 6 | [UploadToZef] 7 | 8 | [Badges] 9 | provider = github-actions/linux.yml 10 | provider = github-actions/macos.yml 11 | provider = github-actions/windows.yml 12 | -------------------------------------------------------------------------------- /t/slang.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Slang::BNF; 3 | 4 | plan 2; 5 | 6 | bnf-grammar A::B { 7 | ::= "bar" 8 | }; 9 | 10 | ok(A::B.parse("bar"), "Parse succeeds"); 11 | ok(!A::B.parse("far"), "Parse fails when it doesn't match"); 12 | 13 | # vim: expandtab shiftwidth=4 14 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | raku: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | raku-version: 18 | - 'latest' 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: Raku/setup-raku@v1 23 | with: 24 | raku-version: ${{ matrix.raku-version }} 25 | - name: Install Dependencies 26 | run: zef install --/test --test-depends --deps-only . 27 | - name: Run Special Tests 28 | run: raku run-tests -i 29 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | raku: 13 | strategy: 14 | matrix: 15 | os: 16 | - macos-latest 17 | raku-version: 18 | - 'latest' 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: Raku/setup-raku@v1 23 | with: 24 | raku-version: ${{ matrix.raku-version }} 25 | - name: Install Dependencies 26 | run: zef install --/test --test-depends --deps-only . 27 | - name: Run Special Tests 28 | run: raku run-tests -i 29 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | raku: 13 | strategy: 14 | matrix: 15 | os: 16 | - windows-latest 17 | raku-version: 18 | - 'latest' 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: Raku/setup-raku@v1 23 | with: 24 | raku-version: ${{ matrix.raku-version }} 25 | - name: Install Dependencies 26 | run: zef install --/test --test-depends --deps-only . 27 | - name: Run Special Tests 28 | run: raku run-tests -i 29 | -------------------------------------------------------------------------------- /t/generate.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Grammar::BNF; 3 | 4 | plan 4; 5 | 6 | my ($t, $p); 7 | 8 | $t = q[ 9 | ::= "bar" 10 | ]; 11 | ok Grammar::BNF.generate($t, name => 'test1').new.parse('bar'); 12 | 13 | $t = q[ 14 | ::= 15 | ::= "baz" 16 | ]; 17 | ok Grammar::BNF.generate($t, name => 'test2').new.parse('baz'); 18 | 19 | 20 | $t = q[ 21 | ::= | 22 | ::= "bar" | 23 | ::= "buzz" 24 | ]; 25 | ok Grammar::BNF.generate($t).new.parse('buzz'); 26 | 27 | $t = q[ 28 | ::= 29 | ::= 'foo' 30 | ::= 'bar' 31 | ]; 32 | ok Grammar::BNF.generate($t).new.parse('foobar'); 33 | 34 | # TODO: Tests for other things in parse.t 35 | 36 | # vim: expandtab shiftwidth=4 37 | -------------------------------------------------------------------------------- /META6.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": "zef:raku-community-modules", 3 | "authors": [ 4 | "Tadeusz Sośnierz" 5 | ], 6 | "build-depends": [ 7 | ], 8 | "depends": [ 9 | ], 10 | "description": "Parse (A)BNF grammars and generate Raku grammars from them", 11 | "license": "MIT", 12 | "name": "Grammar::BNF", 13 | "perl": "6", 14 | "provides": { 15 | "Grammar::ABNF": "lib/Grammar/ABNF.rakumod", 16 | "Grammar::BNF": "lib/Grammar/BNF.rakumod", 17 | "Slang::ABNF": "lib/Slang/ABNF.rakumod", 18 | "Slang::BNF": "lib/Slang/BNF.rakumod" 19 | }, 20 | "resources": [ 21 | ], 22 | "source-url": "https://github.com/raku-community-modules/Grammar-BNF.git", 23 | "tags": [ 24 | ], 25 | "test-depends": [ 26 | ], 27 | "version": "1.1" 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Tadeusz Sośnierz 4 | Copyright (c) 2015 Brian S. Julin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /t/parse.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Grammar::BNF; 3 | 4 | plan 6; 5 | 6 | my $t; 7 | 8 | $t = q[ 9 | ::= "bar" 10 | ]; 11 | ok Grammar::BNF.new.parse($t); 12 | 13 | $t = q[ 14 | ::= 15 | ::= "baz" 16 | ]; 17 | ok Grammar::BNF.new.parse($t); 18 | 19 | $t = q[ 20 | ::= | 21 | ::= "bar" | 22 | ::= "buzz" 23 | ]; 24 | ok Grammar::BNF.new.parse($t); 25 | 26 | $t = q[ 27 | ::= 28 | ]; 29 | ok Grammar::BNF.new.parse($t); 30 | 31 | $t = q[ 32 | ::= 33 | 34 | ::= 35 | | 36 | 37 | ::= "." | 38 | 39 | ::= 40 | 41 | ::= "," 42 | 43 | ::= "Sr." | "Jr." | | "" 44 | ::= | "" 45 | ]; 46 | ok Grammar::BNF.new.parse($t); 47 | 48 | # unmodified grammar from wikipedia 49 | # is actually wrong and needs more optional whitespace; see the Perl 6 grammar 50 | $t = q[ 51 | ::= | 52 | ::= "<" ">" "::=" 53 | ::= " " | "" 54 | ::= | "|" 55 | ::= | 56 | ::= | 57 | ::= | "<" ">" 58 | ::= '"' '"' | "'" "'" 59 | ]; 60 | ok Grammar::BNF.new.parse($t); 61 | 62 | # vim: expandtab shiftwidth=4 63 | -------------------------------------------------------------------------------- /run-tests: -------------------------------------------------------------------------------- 1 | unit sub MAIN(:a($author), :i($install)); 2 | 3 | say run(, :out).out.slurp.chomp; 4 | say "Running on $*DISTRO.gist().\n"; 5 | 6 | say "Testing { 7 | "dist.ini".IO.lines.head.substr(7) 8 | }{ 9 | " including author tests" if $author 10 | }"; 11 | 12 | my @failed; 13 | my $done = 0; 14 | 15 | sub process($proc, $filename) { 16 | if $proc { 17 | $proc.out.slurp; 18 | } 19 | else { 20 | @failed.push($filename); 21 | if $proc.out.slurp -> $stdout { 22 | my @lines = $stdout.lines; 23 | with @lines.first( 24 | *.starts-with(" from gen/moar/stage2"),:k) 25 | -> $index { 26 | say @lines[^$index].join("\n"); 27 | } 28 | else { 29 | say $stdout; 30 | } 31 | } 32 | else { 33 | say "No output received, exit-code $proc.exitcode() ($proc.signal()):\n$proc.os-error()"; 34 | } 35 | } 36 | } 37 | 38 | sub install() { 39 | my $zef := $*DISTRO.is-win ?? 'zef.bat' !! 'zef'; 40 | my $proc := run $zef, "install", ".", "--verbose", "--/test", :out,:err,:merge; 41 | process($proc, "*installation*"); 42 | } 43 | 44 | sub test-dir($dir) { 45 | for $dir.IO.dir(:test(*.ends-with: '.t' | '.rakutest')).map(*.Str).sort { 46 | say "=== $_"; 47 | my $proc := run "raku", "--ll-exception", "-I.", $_, :out,:err,:merge; 48 | process($proc, $_); 49 | $done++; 50 | } 51 | } 52 | 53 | test-dir("t"); 54 | test-dir($_) for dir("t", :test({ !.starts-with(".") && "t/$_".IO.d})).map(*.Str).sort; 55 | test-dir("xt") if $author && "xt".IO.e; 56 | install if $install; 57 | 58 | if @failed { 59 | say "\nFAILED: {+@failed} of $done:"; 60 | say " $_" for @failed; 61 | exit +@failed; 62 | } 63 | 64 | say "\nALL {"$done " if $done > 1}OK"; 65 | 66 | # vim: expandtab shiftwidth=4 67 | -------------------------------------------------------------------------------- /lib/Slang/BNF.rakumod: -------------------------------------------------------------------------------- 1 | # Adaptation of ruoso++'s Grammar::EBNF slang code to Grammar::BNF 2 | 3 | use Grammar::BNF; 4 | use nqp; 5 | use QAST:from; 6 | sub EXPORT(|) { 7 | my sub lk(Mu \h, \k) { 8 | nqp::atkey(nqp::findmethod(h, 'hash')(h), k) 9 | } 10 | role Slang::BNF { 11 | rule package_declarator:sym { 12 | :my $*OUTERPACKAGE := self.package; 13 | 14 | :my $*name; 15 | { $*name := lk($/,'longname').Str } 16 | \{ 17 | 18 | \} 19 | <.set_braid_from(self)> 20 | } 21 | } 22 | role Slang::BNF::Actions { 23 | method package_declarator:sym(Mu $/) { 24 | # Bits extracted from rakudo/src/Perl6/Grammar.nqp (package_def) 25 | my $longname := $*W.dissect_longname(lk($/,'longname')); 26 | my $outer := $*W.cur_lexpad(); 27 | # Locate any existing symbol. Note that it's only a match 28 | # with "my" if we already have a declaration in this scope. 29 | my $exists := 0; 30 | my $name := nqp::getattr($longname.type_name_parts('package name', :decl(1)), List, '$!reified'); 31 | my $target_package := 32 | $longname && $longname.is_declared_in_global() 33 | ?? $*GLOBALish 34 | !! $*OUTERPACKAGE; 35 | my $*PACKAGE = lk($/,"rules").made(); 36 | $*W.install_package($/, $name, 'our', 'bnf-grammar', 37 | $target_package, $outer, $*PACKAGE); 38 | $/.'make'(QAST::IVal.new(:value(1))); 39 | } 40 | } 41 | 42 | my Mu $MAIN-grammar := nqp::atkey(%*LANG, 'MAIN'); 43 | my $grammar := $MAIN-grammar.^mixin(Slang::BNF); 44 | my Mu $MAIN-actions := nqp::atkey(%*LANG, 'MAIN-actions'); 45 | my $actions := $MAIN-actions.^mixin(Slang::BNF::Actions); 46 | 47 | # old way 48 | try { 49 | nqp::bindkey(%*LANG, 'MAIN', $grammar); 50 | nqp::bindkey(%*LANG, 'MAIN-actions', $actions); 51 | nqp::bindkey(%*LANG, 'Grammar::BNF', Grammar::BNF); 52 | nqp::bindkey(%*LANG, 'Grammar::BNF-actions', Grammar::BNF-actions); 53 | } 54 | # new way 55 | try { 56 | $*LANG.define_slang("MAIN", $grammar, $actions); 57 | $*LANG.define_slang("Grammar::BNF", Grammar::BNF, Grammar::BNF-actions); 58 | } 59 | {} 60 | } 61 | 62 | # vim: expandtab shiftwidth=4 63 | -------------------------------------------------------------------------------- /lib/Slang/ABNF.rakumod: -------------------------------------------------------------------------------- 1 | # Adaptation of ruoso++'s Grammar::EBNF slang code to Grammar::ABNF 2 | 3 | use Grammar::ABNF; 4 | use nqp; 5 | use QAST:from; 6 | sub EXPORT(|) { 7 | my sub lk(Mu \h, \k) { 8 | nqp::atkey(nqp::findmethod(h, 'hash')(h), k) 9 | } 10 | role Slang::ABNF { 11 | rule package_declarator:sym { 12 | :my $*OUTERPACKAGE := self.package; 13 | 14 | :my $*name; 15 | :my %*rules; 16 | :my @*ruleorder; 17 | :my $*indent; 18 | { $*name := lk($/,'longname').Str } 19 | \{ 20 | 21 | \} 22 | } 23 | } 24 | role Slang::ABNF::Actions { 25 | method package_declarator:sym(Mu $/) { 26 | # Bits extracted from rakudo/src/Perl6/Grammar.nqp (package_def) 27 | my $longname := $*W.dissect_longname(lk($/,'longname')); 28 | my $outer := $*W.cur_lexpad(); 29 | # Locate any existing symbol. Note that it's only a match 30 | # with "my" if we already have a declaration in this scope. 31 | my $exists := 0; 32 | my $name := nqp::getattr($longname.type_name_parts('package name', :decl(1)), List, '$!reified'); 33 | my $target_package := 34 | $longname && $longname.is_declared_in_global() 35 | ?? $*GLOBALish 36 | !! $*OUTERPACKAGE; 37 | my $*PACKAGE = lk($/,"rules").made(); 38 | $*W.install_package($/, $name, 'our', 'abnf-grammar', 39 | $target_package, $outer, $*PACKAGE); 40 | $/.'make'(QAST::IVal.new(:value(1))); 41 | 42 | } 43 | } 44 | 45 | my Mu $MAIN-grammar := nqp::atkey(%*LANG, 'MAIN'); 46 | my $grammar := $MAIN-grammar.^mixin(Slang::ABNF); 47 | my Mu $MAIN-actions := nqp::atkey(%*LANG, 'MAIN-actions'); 48 | my $actions := $MAIN-actions.^mixin(Slang::ABNF::Actions); 49 | 50 | # old way 51 | try { 52 | nqp::bindkey(%*LANG, 'MAIN', $grammar); 53 | nqp::bindkey(%*LANG, 'MAIN-actions', $actions); 54 | nqp::bindkey(%*LANG, 'Grammar::ABNF::Slang', 55 | Grammar::ABNF::Slang); 56 | nqp::bindkey(%*LANG, 'Grammar::ABNF::Slang-actions', 57 | Grammar::ABNF::Slang-actions); 58 | } 59 | # new way 60 | try { 61 | $*LANG.define_slang("MAIN", $grammar, $actions); 62 | $*LANG.define_slang("Grammar::ABNF::Slang", Grammar::ABNF::Slang, Grammar::ABNF::Slang-actions ); 63 | } 64 | {} 65 | } 66 | 67 | # vim: expandtab shiftwidth=4 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/raku-community-modules/Grammar-BNF/actions/workflows/linux.yml/badge.svg)](https://github.com/raku-community-modules/Grammar-BNF/actions) [![Actions Status](https://github.com/raku-community-modules/Grammar-BNF/actions/workflows/macos.yml/badge.svg)](https://github.com/raku-community-modules/Grammar-BNF/actions) [![Actions Status](https://github.com/raku-community-modules/Grammar-BNF/actions/workflows/windows.yml/badge.svg)](https://github.com/raku-community-modules/Grammar-BNF/actions) 2 | 3 | NAME 4 | ==== 5 | 6 | Grammar::BNF - Parse (A)BNF grammars and generate Raku grammars from them 7 | 8 | SYNOPSIS 9 | ======== 10 | 11 | ```raku 12 | use Grammar::BNF; 13 | my $g = Grammar::BNF.generate(Q:to); 14 | ::= 15 | ::= "bar" 16 | END 17 | ``` 18 | 19 | DESCRIPTION 20 | =========== 21 | 22 | This distribution contains modules for creating Raku Grammar objects using BNF flavored grammar definition syntax. Currently BNF and ABNF are supported. 23 | 24 | In addition, the distribution contains Slang modules which allow use of the grammar definition syntax inline in Raku code. These modules may relax their respective syntax slightly to allow for smoother language integration. 25 | 26 | IDIOMS 27 | ====== 28 | 29 | This simple example shows how to turn a simple two-line grammar definition in BNF syntax into a grammar named `MyGrammar`, and then uses the resulting grammar to parse the string 'barbar'; 30 | 31 | ```raku 32 | use Grammar::BNF; 33 | my $g = Grammar::BNF.generate(Q:to); 34 | ::= 35 | ::= "bar" 36 | END 37 | ); 38 | $g.parse('barbar').say; # 「barbar」 39 | # foo2 => 「barbar」 40 | # foo => 「bar」 41 | # foo => 「bar」 42 | ``` 43 | 44 | Alternatively, you may use a slang to define grammars inline: 45 | 46 | ```raku 47 | use Slang::BNF; 48 | bnf-grammar MyGrammar { 49 | ::= 50 | ::= "bar" 51 | }; # currently you need this semicolon 52 | MyGrammar.parse('barbar').say; # same as above 53 | ``` 54 | 55 | In either case, the first rule appearing in the grammar definition will be aliased to 'TOP', and will be the default rule applied by `.parse`. This is in most respects a true Raku, so subrules may be invoked: 56 | 57 | ```raku 58 | MyGrammar.parse('bar',:rule).say; # 「bar」 59 | ``` 60 | 61 | ...and the Grammar may be subclassed to add or replace rules with Perl 6 rules: 62 | 63 | ```raku 64 | grammar MyOtherGrammar is MyGrammar { 65 | token foo { B } 66 | token ar { ar } 67 | } 68 | MyOtherGrammar.parse('BarBar').say; # 「BarBar」 69 | # foo2 => 「BarBar」 70 | # foo => 「Bar」 71 | # ar => 「ar」 72 | # foo => 「Bar」 73 | # ar => 「ar」 74 | ``` 75 | 76 | Currently you have to subclass with a Raku grammar for actions classes to be provided, but hopefully that limitation will be overcome: 77 | 78 | ```raku 79 | class MyActions { method foo ($match) { "OHAI".say } } 80 | MyOtherGrammar.parse('BarBar', :actions(MyActions)); # says OHAI twice 81 | ``` 82 | 83 | AUTHOR 84 | ====== 85 | 86 | Tadeusz Sośnierz 87 | 88 | COPYRIGHT AND LICENSE 89 | ===================== 90 | 91 | Copyright 2010 - 2017 Tadeusz Sośnierz 92 | 93 | Copyright 2024 Raku Community 94 | 95 | This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0. 96 | 97 | -------------------------------------------------------------------------------- /lib/Grammar/BNF.rakumod: -------------------------------------------------------------------------------- 1 | my class Actions { ... } 2 | 3 | grammar Grammar::BNF { 4 | token TOP { 5 | \s* + \s* 6 | } 7 | 8 | # Apparently when used for slang we need a lowercase top rule? 9 | token main_syntax { 10 | 11 | } 12 | 13 | token rule { 14 | '<' '>' '::=' 15 | } 16 | 17 | token opt-ws { 18 | \h* 19 | } 20 | 21 | token rule-name { 22 | # If we want something other than legal perl 6 identifiers, 23 | # we would have to implement a FALLBACK. BNF "specifications" 24 | # diverge on what is a legal rule name but most expectations are 25 | # covered by legal Perl 6 identifiers. Care should be taken to 26 | # shield from evaluation of metacharacters on a Perl 6 level. 27 | <.ident>+ % [ <[\-\']> ] 28 | } 29 | 30 | token expression { 31 | +% [\s* '|' ] 32 | } 33 | 34 | token line-end { 35 | [ \n ]+ 36 | } 37 | 38 | token list { 39 | +% 40 | } 41 | 42 | token term { 43 | | '<' '>' 44 | } 45 | 46 | token literal { 47 | '"' <-["]>* '"' | "'" <-[']>* "'" 48 | } 49 | 50 | # Provide a parse with defaults and also define our per-parse scope. 51 | method parse(|c) { 52 | my $*name = c // 'BNFGrammar'; 53 | my %hmod = c.hash; 54 | %hmod:delete; 55 | %hmod = Actions unless %hmod:exists; 56 | my \cmod = \(|c.list, |%hmod); 57 | nextwith(|cmod); 58 | } 59 | 60 | # We may want to rename this given jnthn's Grammar::Generative 61 | method generate(|c) { 62 | my $res = self.parse(|c); 63 | fail("parse *of* an BNF grammar definition failed.") unless $res; 64 | return $res.ast; 65 | } 66 | } 67 | 68 | my class Actions { 69 | 70 | my sub guts($/, $rule) { 71 | use MONKEY-SEE-NO-EVAL; 72 | # Note: $*name can come from .parse above or from Slang::BNF 73 | my $grmr := Metamodel::GrammarHOW.new_type(:name($*name)); 74 | my $top = EVAL 'token { <' ~ $rule[0].ast.key ~ '> }'; 75 | $grmr.^add_method('TOP', $top); 76 | $top.set_name('TOP'); # Makes it appear in .^methods 77 | for $rule.map(*.ast) -> $rule { 78 | $rule.value.set_name($rule.key); 79 | $grmr.^add_method($rule.key, $rule.value); 80 | } 81 | $grmr.^compose; 82 | } 83 | 84 | method TOP($/) { 85 | make guts($/, $); 86 | } 87 | 88 | method main_syntax($/) { 89 | make guts($/, $); 90 | } 91 | 92 | method rule($/) { 93 | make $.ast => $.ast; 94 | } 95 | 96 | method rule-name($/) { 97 | make ~$/; 98 | } 99 | 100 | method expression($/) { 101 | use MONKEY-SEE-NO-EVAL; 102 | make EVAL 'token { [ ' ~ $.map(*.ast).join(' | ') ~ ' ] }'; 103 | } 104 | 105 | method list($/) { 106 | make $.map(*.ast).join(' '); 107 | } 108 | 109 | method term($/) { 110 | make ~$/; 111 | } 112 | 113 | method literal($/) { 114 | # Prevent evalaution of metachars at Perl 6 level 115 | make ('[ ', ' ]').join(~$/.ords.fmt('\x%x',' ')); 116 | } 117 | } 118 | 119 | # For the slang guts we need an actions class we can find. 120 | class Grammar::BNF-actions is Actions { }; 121 | 122 | =begin pod 123 | 124 | =head1 NAME 125 | 126 | Grammar::BNF - Parse (A)BNF grammars and generate Raku grammars from them 127 | 128 | =head1 SYNOPSIS 129 | 130 | =begin code :lang 131 | 132 | use Grammar::BNF; 133 | my $g = Grammar::BNF.generate(Q:to); 134 | ::= 135 | ::= "bar" 136 | END 137 | 138 | =end code 139 | 140 | =head1 DESCRIPTION 141 | 142 | This distribution contains modules for creating Raku Grammar 143 | objects using BNF flavored grammar definition syntax. Currently 144 | BNF and ABNF are supported. 145 | 146 | In addition, the distribution contains Slang modules which allow 147 | use of the grammar definition syntax inline in Raku code. These 148 | modules may relax their respective syntax slightly to allow for 149 | smoother language integration. 150 | 151 | =head1 IDIOMS 152 | 153 | This simple example shows how to turn a simple two-line grammar 154 | definition in BNF syntax into a grammar named C, and 155 | then uses the resulting grammar to parse the string 'barbar'; 156 | 157 | =begin code :lang 158 | 159 | use Grammar::BNF; 160 | my $g = Grammar::BNF.generate(Q:to); 161 | ::= 162 | ::= "bar" 163 | END 164 | ); 165 | $g.parse('barbar').say; # 「barbar」 166 | # foo2 => 「barbar」 167 | # foo => 「bar」 168 | # foo => 「bar」 169 | 170 | =end code 171 | 172 | Alternatively, you may use a slang to define grammars inline: 173 | 174 | =begin code :lang 175 | 176 | use Slang::BNF; 177 | bnf-grammar MyGrammar { 178 | ::= 179 | ::= "bar" 180 | }; # currently you need this semicolon 181 | MyGrammar.parse('barbar').say; # same as above 182 | 183 | =end code 184 | 185 | In either case, the first rule appearing in the grammar definition will 186 | be aliased to 'TOP', and will be the default rule applied by C<.parse>. 187 | This is in most respects a true Raku, so subrules may be invoked: 188 | 189 | =begin code :lang 190 | 191 | MyGrammar.parse('bar',:rule).say; # 「bar」 192 | 193 | =end code 194 | 195 | ...and the Grammar may be subclassed to add or replace rules with Perl 6 196 | rules: 197 | 198 | =begin code :lang 199 | 200 | grammar MyOtherGrammar is MyGrammar { 201 | token foo { B } 202 | token ar { ar } 203 | } 204 | MyOtherGrammar.parse('BarBar').say; # 「BarBar」 205 | # foo2 => 「BarBar」 206 | # foo => 「Bar」 207 | # ar => 「ar」 208 | # foo => 「Bar」 209 | # ar => 「ar」 210 | 211 | =end code 212 | 213 | Currently you have to subclass with a Raku grammar for actions classes 214 | to be provided, but hopefully that limitation will be overcome: 215 | 216 | =begin code :lang 217 | 218 | class MyActions { method foo ($match) { "OHAI".say } } 219 | MyOtherGrammar.parse('BarBar', :actions(MyActions)); # says OHAI twice 220 | 221 | =end code 222 | 223 | =head1 AUTHOR 224 | 225 | Tadeusz Sośnierz 226 | 227 | =head1 COPYRIGHT AND LICENSE 228 | 229 | Copyright 2010 - 2017 Tadeusz Sośnierz 230 | 231 | Copyright 2024 Raku Community 232 | 233 | This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0. 234 | 235 | =end pod 236 | 237 | # vim: expandtab shiftwidth=4 238 | -------------------------------------------------------------------------------- /lib/Grammar/ABNF.rakumod: -------------------------------------------------------------------------------- 1 | =begin pod 2 | 3 | =head1 NAME 4 | 5 | Grammar ABNF - Parse ABNF grammars and create Raku Grammars from them 6 | 7 | =head1 SYNOPSIS 8 | 9 | =begin code :lang 10 | 11 | use Grammar::ABNF; 12 | 13 | my $g = Grammar::ABNF.parse(qq:to, :name).made; 14 | macaddr = 5( octet [ ":" / "-" ] ) octet\r 15 | octet = 2HEXDIGIT\r 16 | HEXDIGIT = %x30-39 / %x41-46 / %x61-66\r 17 | END 18 | 19 | $g.parse('02-BF-C0-00-02-01')».Str.print; # 02BFC0000201 20 | 21 | =end code 22 | 23 | =head1 DESCRIPTION 24 | 25 | The Grammar::ABNF module provides a Grammar named C 26 | which parses ABNF grammar definitions. It also provides a smaller 27 | grammar named C containing only the (mostly 28 | terminal) core ABNF rules. 29 | 30 | The module may also be used to produce working Raku Cs from 31 | the parsed ABNF definitions. 32 | 33 | =end pod 34 | 35 | my class ABNF-Actions {...}; 36 | 37 | #|{ This grammar contains the core ABNF ruleset as defined in 38 | RFC 5234 Appendix B.1. The rule names are uppercase as they 39 | appear in the RFC and must be used as such; this grammar 40 | does not perform case folding. 41 | } 42 | grammar Grammar::ABNF::Core { 43 | # RFC 5234 Appendix B.1. "Core Rules" 44 | token ALPHA { <[\x41..\x5a] + [\x61..\x7a]> } 45 | token BIT { <[\x30 \x31]> } 46 | token CHAR { <[\x01..\x7f]> } 47 | token CR { \x0d } 48 | token CRLF { \x0d \x0a } 49 | token CTL { <[\x00..\x1f] + [\x7f]> } 50 | token DIGIT { <[\x30..\x39]> } 51 | token DQUOTE { \x22 } 52 | token HEXDIG { <+ DIGIT + [\x41..\x46] + [\x61..\x66]> } 53 | token HTAB { \x09 } 54 | token LWSP { [ <.WSP> | <.CRLF> <.WSP> ]* } 55 | token LF { \x0a } 56 | token OCTET { <[\x00..\xff]> } 57 | token SP { \x20 } 58 | token VCHAR { <[\x21..\x7e]> } 59 | token WSP { <+SP + HTAB> } 60 | } 61 | 62 | #|{ This grammar contains the full ABNF ruleset as defined in 63 | RFC 5234. The extra rules C, C and C 64 | are present and used for internal purposes. 65 | 66 | Note that the C rule is strictly conformant. If you 67 | want to accept alternative newlines, you must override it 68 | by defining a subclass. 69 | 70 | Currently this module does not handle multi-line rules nor 71 | even any whitespace prior to the rule on a line. Support 72 | for that is planned. For now, use heredocs to de-indent. 73 | } 74 | grammar Grammar::ABNF is Grammar::ABNF::Core { 75 | 76 | token TOP { 77 | # TODO deal with indentation and folded rules 78 | # \s*? (\s*) 79 | # { $*indent = $/[0].chars; } 80 | \s* 81 | } 82 | 83 | token main_syntax { } 84 | 85 | # Rule names are directly from RFC 5234 Section 4 for this section 86 | token rulelist { 87 | # [ | [ <.c-wsp>* <.c-nl> ] ]+ 88 | # RFC 5234 errata ID 3076 89 | [ | [ <.WSP>* <.c-nl> ] ]+ 90 | } 91 | 92 | # Altered for indenting behavior 93 | regex rule { 94 | # New rules can start if they are not indented more than the last rule 95 | # ( <.WSP>**{0..$*indent} ) 96 | <.c-nl> 97 | # ratchet in the indent 98 | # { $*indent = $0.chars } 99 | } 100 | 101 | # This is not in the RFC but helps keep things DRY 102 | token name { 103 | :i (<+alpha><+alnum +[-]>+) 104 | } 105 | 106 | token rulename { 107 | [ '<' '>' ] | 108 | } 109 | 110 | token defined-as { 111 | <.c-wsp>* ("=" | "=/") <.c-wsp>* 112 | } 113 | 114 | token elements { 115 | # <.c-wsp>* 116 | # RFC 5234 errata ID 2968 117 | <.WSP>* 118 | } 119 | 120 | # We just do this the way the RFC does, though unnecessary 121 | regex c-wsp { 122 | <.WSP> | [ <.c-nl> <.WSP> ] 123 | } 124 | 125 | regex c-nl { 126 | [ <.comment> | <.CRLF> ] 127 | } 128 | 129 | token comment { 130 | ';' ( <+WSP +VCHAR> )* <.CRLF> 131 | } 132 | 133 | regex alternation { 134 | + % [ <.c-wsp>* "/" <.c-wsp>* ] 135 | } 136 | 137 | regex concatenation { 138 | + % <.c-wsp>+ 139 | } 140 | 141 | regex repetition { 142 | ? 143 | } 144 | 145 | token repeat { 146 | [$=[<.DIGIT>+]]? [$='*']? [$=[<.DIGIT>+]]? { 147 | X::Syntax::Regex::MalformedRange.new.throw 148 | if ($/ // 0) > ($/ // Inf); 149 | } 150 | } 151 | 152 | token element { 153 | || |