Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

parser-gen

A Lua parser generator that makes it possible to describe grammars in a PEG syntax. The tool will parse a given input using a provided grammar and if the matching is successful produce an AST as an output with the captured values using Lpeg. If the matching fails, labelled errors can be used in the grammar to indicate failure position, and recovery grammars are generated to continue parsing the input using LpegLabel. The tool can also automatically generate error labels and recovery grammars for LL(1) grammars.


Table of contents

Requirements

lua >= 5.1
lpeglabel >= 1.2.0 && lpeglabel <= 1.4.0

Syntax

compile

This function generates a PEG parser from the grammar description.

localpg=require"parser-gen"grammar=pg.compile(input,definitions [, errorgen, noast])

Arguments:

input - A string containing a PEG grammar description. For complete PEG syntax see the grammar section of this document.

definitions - table of custom functions and definitions used inside the grammar, for example {equals=equals}, where equals is a function.

errorgen - EXPERIMENTAL optional boolean parameter(default:false), when enabled generates error labels automatically. Works well only on LL(1) grammars. Custom error labels have precedence over automatically generated ones.

noast - optional boolean parameter(default:false), when enabled does not generate an AST for the parse.

Output:

grammar - a compiled grammar on success, throws error on failure.

setlabels

If custom error labels are used, the function setlabels allows setting their description (and custom recovery pattern):

pg.setlabels(t)

Example table of a simple error and one with a custom recovery expression:

-- grammar rule: " ifexp <- 'if' exp 'then'^missingThen stmt 'end'^missingEnd "localt= {
missingEnd="Missing 'end' in if expression",
missingThen= {"Missing 'then' in if expression", " (!stmt .)* "} -- a custom recovery pattern
}
pg.setlabels(t)

If the recovery pattern is not set, then the one specified by the rule SYNC will be used. It is by default set to:

SKIP<-%s/%nl-- a space ' ' or newline '\n' characterSYNC<- .? (!SKIP .)*

Learn more about special rules in the grammar section.

parse

This operation attempts to match a grammar to the given input.

result, errors=pg.parse(input, grammar [, errorfunction])

Arguments:

input - an input string that the tool will attempt to parse.

grammar - a compiled grammar.

errorfunction - an optional function that will be called if an error is encountered, with the arguments desc for the error description set using setlabels(); location indicators line and col; the remaining string before failure sfail and a custom recovery expression trec if available. Example:

localerrs=0localfunctionprinterror(desc,line,col,sfail,trec)
errs=errs+1print("Error #"..errs..": "..desc.." before '"..sfail.."' on line "..line.."(col "..col..")")
endresult, errors=pg.parse(input,grammar,printerror)

Output:

If the parse is succesful, the function returns an abstract syntax tree containing the captures result and a table of any encountered errors. If the parse was unsuccessful, result is going to be nil. Also, if the noast option is enabled when compiling the grammar, the function will then produce the longest match length or any custom captures used.

calcline

Calculates line and column information regarding position i of the subject (exported from the relabel module).

line, col=pg.calcline(subject, position)

Arguments:

subject - subject string

position - position inside the string, for example, the one given by automatic AST generation.

usenodes

When AST generation is enabled, this function will enable the "node" mode, where only rules tagged with a node prefix will generate AST entries. Must be used before compiling the grammar.

pg.usenodes(value)

Arguments:

value - a boolean value that enables or disables this function

Grammar Syntax

The grammar used for this tool is described using a PEG-like syntax, that is identical to the one provided by the re module, with an extension of labelled failures provided by relabel module (except numbered labels). That is, all grammars that work with relabel should work with parser-gen as long as numbered error labels are not used, as they are not supported by parser-gen.

Since a parser generated with parser-gen automatically consumes space characters, builds ASTs and generates errors, additional extensions have been added based on the ANTLR syntax.

Basic syntax

The syntax of parser-gen grammars is somewhat similar to regex syntax. The next table summarizes the tools syntax. A p represents an arbitrary pattern; num represents a number ([0-9]+); name represents an identifier ([a-zA-Z][a-zA-Z0-9_]*).defs is the definitions table provided when compiling the grammar. Note that error names must be set using setlabels() before compiling the grammar. Constructions are listed in order of decreasing precedence.

SyntaxDescription
( p )grouping
'string'literal string
"string"literal string
[class]character class
.any character
%namepattern defs[name] or a pre-defined pattern
namenon terminal
<name>non terminal
%{name}error label
{}position capture
{ p }simple capture
{: p :}anonymous group capture
{:name: p :}named group capture
{~ p ~}substitution capture
{| p |}table capture
=nameback reference
p ?optional match
p *zero or more repetitions
p +one or more repetitions
p^numexactly n repetitions
p^+numat least n repetitions
p^-numat most n repetitions
p^namematch p or throw error label name.
p -> 'string'string capture
p -> "string"string capture
p -> numnumbered capture
p -> namefunction/query/string capture equivalent to p / defs[name]
p => namematch-time capture equivalent to lpeg.Cmt(p, defs[name])
& pand predicate
! pnot predicate
p1 p2concatenation
p1 //{name [, name, ...]} p2specifies recovery pattern p2 for p1 when one of the labels is thrown
p1 / p2ordered choice
(name <- p)+grammar

The grammar below is used to match balanced parenthesis

balanced<-"(" ([^()] /balanced)*")" 

For more examples check out the re page, see the Tiny parser below or the Lua parser writen with this tool.

Error labels

Error labels are provided by the relabel function %{errorname} (errorname must follow [A-Za-z][A-Za-z0-9_]* format). Usually we use error labels in a syntax like 'a' ('b' / %{errB}) 'c', which throws an error label if 'b' is not matched. This syntax is quite complicated so an additional syntax is allowed 'a' 'b'^errB 'c', which allows cleaner description of grammars. Note: all errors must be defined in a table using parser-gen.setlabels() before compiling and parsing the grammar.

Tokens

Non-terminals with names in all capital letters, i.e. [A-Z]+, are considered tokens and are treated as a single object in parsing. That is, the whole string matched by a token is captured in a single AST entry and space characters are not consumed. Consider two examples:

-- a token non-terminalgrammar=pg.compile[[	WORD <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}
-- a non-token non-terminalgrammar=pg.compile[[	word <- [A-Z]+]]res, _=pg.parse("AA A", grammar) -- outputs {rule="word", "A", "A", "A"}

Fragments

If a token definition is followed by a fragment keyword, then the parser does not build an AST entry for that token. Essentially, these rules are used to simplify grammars without building unnecessarily complicated ASTS. Example of fragment usage:

grammar=pg.compile[[	WORD <- LETTER+	fragment LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Without using fragment:

grammar=pg.compile[[	WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", {rule="LETTER", "A"}, {rule="LETTER", "A"}}

Nodes

When node mode is enabled using pg.usenodes(true) only rules prefixed with a node keyword will generate AST entries:

grammar=pg.compile[[	node WORD <- LETTER+	LETTER <- [A-Z]]]res, _=pg.parse("AA A", grammar) -- outputs {rule="WORD", "AA"}

Special rules

There are two special rules used by the grammar:

SKIP

The SKIP rule identifies which characters to skip in a grammar. For example, most programming languages do not take into acount any space or newline characters. By default, SKIP is set to:

SKIP<-%s/%nl

This rule can be extended to contain semicolons ';', comments, or any other patterns that the parser can safely ignore.

Character skipping can be disabled by using:

SKIP<-''

SYNC

This rule specifies the general recovery expression both for custom errors and automatically generated ones. By default:

SYNC<- .? (!SKIP .)*

The default SYNC rule consumes any characters until the next character matched by SKIP, usually a space or a newline. That means, if some statement in a program is invalid, the parser will continue parsing after a space or a newline character.

For some programming languages it might be useful to skip to a semicolon or a keyword, since they usually indicate the end of a statement, so SYNC could be something like:

HELPER<-';' /'end' /SKIP-- etcSYNC<- (!HELPER .)*SKIP*-- we can consume the spaces after syncing with them as well

Recovery grammars can be disabled by using:

SYNC<-''

Example: Tiny parser

Below is the full code from parsers/tiny-parser.lua:

localpg=require"parser-gen"localpeg=require"peg-parser"localerrs= {errMissingThen="Missing Then"} -- one custom errorpg.setlabels(errs)
--warning: experimental error generation function is enabled. If the grammar isn't LL(1), set errorgen to falselocalerrorgen=truelocalgrammar=pg.compile([[	program <- stmtsequence !. 	stmtsequence <- statement (';' statement)* 	statement <- ifstmt / repeatstmt / assignstmt / readstmt / writestmt	ifstmt <- 'if' exp 'then'^errMissingThen stmtsequence elsestmt? 'end' 	elsestmt <- ('else' stmtsequence)	repeatstmt <- 'repeat' stmtsequence 'until' exp 	assignstmt <- IDENTIFIER ':=' exp 	readstmt <- 'read' IDENTIFIER 	writestmt <- 'write' exp 	exp <- simpleexp (COMPARISONOP simpleexp)*	COMPARISONOP <- '<' / '='	simpleexp <- term (ADDOP term)* 	ADDOP <- [+-]	term <- factor (MULOP factor)*	MULOP <- [*/]	factor <- '(' exp ')' / NUMBER / IDENTIFIER	NUMBER <- '-'? [0-9]+	KEYWORDS <- 'if' / 'repeat' / 'read' / 'write' / 'then' / 'else' / 'end' / 'until' 	RESERVED <- KEYWORDS ![a-zA-Z]	IDENTIFIER <- !RESERVED [a-zA-Z]+	HELPER <- ';' / %nl / %s / KEYWORDS / !.	SYNC <- (!HELPER .)*]], _, errorgen)
localerrors=0localfunctionprinterror(desc,line,col,sfail,trec)
errors=errors+1print("Error #"..errors..": "..desc.." on line "..line.."(col "..col..")")
endlocalfunctionparse(input)
errors=0result, errors=pg.parse(input,grammar,printerror)
returnresult, errorsendifarg[1] then-- argument must be in quotes if it contains spacesres, errs=parse(arg[1])
peg.print_t(res)
peg.print_r(errs)
endlocalret= {parse=parse}
returnret

For input: lua tiny-parser-nocap.lua "if a b:=1" we get:

Error#1: MissingThenonline1(col6)
Error#2: Expectedstmtsequenceonline1(col9)
Error#3: Expected'end' online1(col9)
-- ast:rule='program',
pos=1,
{
rule='stmtsequence',
pos=1,
{
rule='statement',
pos=1,
{
rule='ifstmt',
pos=1,
'if',
{
rule='exp',
pos=4,
{
rule='simpleexp',
pos=4,
{
rule='term',
pos=4,
{
rule='factor',
pos=4,
{
rule='IDENTIFIER',
pos=4,
'a',
},
},
},
},
},
},
},
},
-- error table:
[1] => {
[msg] =>'Missing Then' -- custom error is used over the automatically generated one
[line] =>'1'
[col] =>'6'
[label] =>'errMissingThen'
}
[2] => {
[msg] =>'Expected stmtsequence' -- automatically generated errors
[line] =>'1'
[col] =>'9'
[label] =>'errorgen6'
}
[3] => {
[msg] =>'Expected 'end''
[line] =>'1'
[col] =>'9'
[label] =>'errorgen4'
}

About

A parser generator in Lua using PEG syntax.

Resources

Stars

51 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages