PEGO Bytecode Specification
This document specifies the bytecode (the module) that a PEGO grammar compiles to, and the behavior of the virtual machine (VM) that executes it. A runtime for another language must implement this document. The meaning of the language that the bytecode implements is defined by the Grammar Language Specification; for the results a parse must produce, see Parsing.
The reference implementation is the Go package internal/bytecode (bytecode.go, the instruction set and module) with the bytecode compiler bcompile.go in internal/compile and the VMs vm.go and ivm.go in internal/engine. A bytecode VM must produce the same results as the closure backend.
Terminology
| Term | Meaning |
|---|---|
| match code | The instruction sequence that matches input (rule bodies, Operators Rule parts) |
| expression code | The instruction sequence that evaluates Value Actions and Predicate Actions |
| entry stack | The stack of backtrack entries pushed by match instructions (Failure) |
| expectation | A string describing what was expected at a position, used in syntax errors |
| farthest failure | The farthest position at which a failure was recorded, together with the set of expectations recorded at that position |
| level | The Operators Rule binding level passed to a rule call (min); 0 for an ordinary call |
| value-free twin | A copy of a rule compiled so that it builds no value (Rule table) |
| transient rule | A rule that is not memoized in normal parses (Rule calls) |
Values
| Value | Description |
|---|---|
| Node | Type name, rule name (optional), range [start, end), text, list of children, and fields (name → value). A node also carries a flag that marks it as holding text: a string node or an error node. |
| Integer, string, boolean | Used by Value Actions and Predicate Actions |
| nil | The absence of a value |
| Function | A lambda: the start of its expression code, its number of parameters, and the locals captured when it was created |
The reserved bytecode node types are the kinds string, tuple, list and error.
The Closure-only #missing pragma and missing type have no bytecode encoding;
VM parsing, binary serialization and module construction reject those grammars.
A node is fresh only while the body of the rule that created it is executing (Rule completion).
Input and Positions
The input is a sequence of characters. The position unit is chosen for each parse: Unicode code points or UTF-8 bytes. Reading a character returns the character (a code point) at the current position and its size in position units. Each invalid UTF-8 byte is read as one U+FFFD character whose size is one unit (one byte, or one code point).
Module
A module consists of the following tables and code sequences.
| Table | Elements |
|---|---|
| String table | Strings. All other tables and instructions refer to strings by index. |
| Character class table | A list of ranges (pairs of lower and upper bounds), a negation flag, and an expectation string |
| Type table | Type name, and either the field names of a struct type or, for a named node type type N = node[T], the text of its underlying node type node[T] |
| Field-list table | Lists of field names used by ENEW |
| Scope table | The capture names of a repetition element, in slot order |
| Variable table | The declared variables: name, type (int, string or bool) and initial value. Every parse begins with the environment holding each variable bound to its initial value. |
| Rule table | See Rule table |
| Operators Rule table | See Operators Rule table |
| Match code | A sequence of instructions |
| Expression code | A sequence of instructions |
An instruction consists of an opcode and up to three integer operands, A, B and C. A jump target is the index of an instruction in the same code sequence.
Operands are signed 32-bit integers. Repetition bounds that exceed this range
use REPEATW or SCANW, whose bound operands index EINT constants in the
expression code. These constants preserve the full implementation-int range;
they are read directly, without evaluating an action. Ordinary instructions
retain their compact representation.
The public grammar AST treats every negative repetition maximum as unbounded.
Compilation canonicalizes it to -1 before narrowing operands or creating wide
bound constants. The AST's original field is preserved; serialized bytecode
uses the existing -1 sentinel for both REPEAT and SCAN variants.
Rule Table
| Item | Description |
|---|---|
| Name | Rule name |
| Body | Start of the body in the match code. The body pushes the value of its scope (or none, if the body is value-free) and ends with RETURN: the single value of the body, or a tuple node of its values when the scope is a tuple. |
| Scope | The capture names of the body, in slot order |
| Action | Start of the action in the expression code, or -1 if there is none |
| Body is a sequence | Whether the body's value is a tuple; used to interpret $n |
| Node type | Name of the named node type a rule declared with one converts its value to (see Rule Completion), or -1 if there is none. For a rule with an Operators Rule, it is the named node type every operator's value is converted to (see Operators Rule) |
| Memoize, left-recursion leader, position-dependent | Results of static analysis |
| Operators Rule | Index into the Operators Rule table, or -1 if there is none |
| Value-free twin | Whether this entry is a value-free twin (see below). The value of a value-free twin is always nil. |
| Transient | The rule is not memoized in normal parses (Rule calls) |
| Where | The text that names the rule in the message of a failed action, as an index into the string table, or -1 for the name. The hidden rule of an inline action is named by the rule that holds the action. |
| Splice | The rule makes no node of its own (a hidden rule without an action). Its body pushes the tuple node of its elements, if the body builds one, else its one element or nil; the value of the rule is that result as it is, without the captures of the body, and its callers take its elements with SPREAD. |
| Variables | Names (string table indices, sorted) of the variables read by predicates in the rule or the rules it calls; memo entries are keyed by their values |
Value-free twins. When a rule that returns a CST is called from a place where its value is discarded (#atomic, -, !, or a value-free body), the compiler compiles a second version of its body that builds no value. The twin is placed in the rule table after all original rules, and the CALL refers to it. The twin has the same name as the original rule; lookups of a start rule by name ignore twins. Because the twin matches the same input in the same way, results do not change. Rules that belong to a left-recursive cycle have no twin.
Value-free bodies. The body of a rule whose action does not refer to $n is compiled without building values. Capture targets still build their values.
Operators Rule Table
| Item | Description |
|---|---|
| skip | Start of the skip code in the match code, or -1 if there is none. The code builds no value and ends with END. |
| operand | A list of lines |
| Prefix operators / infix and postfix operators | Lists of operators (index, kind, associativity, level, line) |
A line has the start of its code in the match code (the code pushes one value and ends with END), a scope, an action (start in the expression code, or -1), whether its body is a sequence, and the type its value is checked against when the line produces it (as the rule table's check, with the rule's name in the error), or -1. An operand line pushes the value of its scope, as a rule body does. An operator line pushes the operator's value: when its body is a sequence, that value is the tuple node of the operator's values, and the line's captures are attached to it.
Operator kinds are encoded as 0 = prefix, 1 = postfix, 2 = infix. Associativity is encoded as 0 = left, 1 = right, 2 = none.
VM State
| State | Description |
|---|---|
pos |
Current position |
| Value stack | Values built during matching |
| Entry stack | Points to return to on failure (see Failure). Each rule body has its own base. |
| Frame | The capture slots of the current scope |
| Capture trail | Writes to captures (frame, slot, previous value) |
| Environment | Variables, as a persistent list. A parse starts from the environment built from the variable table, so every declared variable is bound before the start rule runs. |
| Recovered errors | Syntax errors recovered by #recover |
silent |
While positive, expectations are not recorded |
| Farthest failure | A position and the set of expectations recorded there |
| Repetition state | For each active repetition: count, value-stack base, maximum, and scope |
| Memo | (rule, position, level) → result |
Save and restore. Saving the state records pos, the height of the value stack, the height of the entry stack, the number of repetition states, the length of the capture trail, the environment, the number of recovered errors, and the frame. Restoring returns each of these to the recorded value.
Unwinding the capture trail writes each recorded previous value back into its slot.
Value-free recognition uses NEXT mode 0 even for a source repetition marked
#stream. Mode 2 requires an element value on the stack; it belongs to a
value-building stream-capable program. Element capture frames remain available
to predicates in mode 0, but are not attached to an element value.
Failure
When an instruction fails, the VM pops entries from the entry stack of the current rule body and handles each according to its kind. If there are no entries left, the rule body fails.
| Entry | Pushed by | When popped on failure |
|---|---|---|
| Choice | CHOICE |
Restore. If the entry is marked as cut, continue failing; otherwise jump to the recorded target. |
| Iteration | ITER |
Restore. If the entry is marked as cut, continue failing; otherwise jump to the recorded target. |
| Positive lookahead | LOOK (neg = 0) |
Restore silent, restore the state, and continue failing. |
| Negative lookahead | LOOK (neg = 1) |
Restore silent, restore the state, and jump to the recorded target (the negative lookahead succeeds). |
| Label | LABEL |
Restore the saved farthest failure. For mode B=0, report A as an explicit error message; for B=1, use A as the diagnostic label for ordinary expectations recorded at the wrapped expression's farthest position. Preserve explicit messages and continue failing. |
| Recovery | RECOVER |
See Recovery. |
| Skip | Recovery handling | Restore, add the expectations recorded inside to the farthest failure, and continue failing. |
Match Instructions
b denotes an operand that selects whether the instruction pushes a value (1) or not (0). Matching behaves the same either way.
| Code | Instruction | Operands | Behavior |
|---|---|---|---|
| 1 | STR |
A string, B expectation, C b | If the characters of string A follow the current position, advance past them and, if b, push a string node. Otherwise record B as an expectation at the start position and fail. |
| 2 | CLASS |
A class, B expectation, C b | If the current character is in the class (not in it, for a negated class), advance one character and, if b, push a string node. Otherwise record B at the current position and fail. |
| 3 | ANY |
A b | If there is a character, advance one character (and, if b, push a string node). Otherwise record any character and fail. |
| 33 | SCAN |
A class, B min, C max | A value-free repetition of one character. Advance over characters that are in class A (any character if A is -1) until a character does not match or C characters have been read (no limit if C is -1). If it stopped at a non-matching character (or at the end of input), record the class's expectation (any character if A is -1) at that position. Fail if fewer than B characters were read. |
| 34 | GUARD |
A class, B depth, C target | First-character dispatch, emitted before the CHOICE of an alternative that must begin with a given terminal. If the next character is not in class A (or there is none) and the current call depth plus B does not exceed the nesting limit, record the class's expectation at the current position and go to C (the next alternative); otherwise continue. For a literal, A is a class of its first character whose expectation is the literal's; for a case-insensitive literal the class also contains the other case of an ASCII letter. The alternative would have failed there recording just that expectation, so skipping it changes nothing but speed. |
| 4 | TOP |
A b | If b, push an empty string node. |
| 5 | FAIL |
Fail. | |
| 6 | ASSERT |
A kind | 0: the position is 0. 1: at the end of input. 2: the position is 0 or the preceding character is \n. 3: at the end of input, or the current character is \n or \r. If the condition does not hold, record beginning of input, end of input, beginning of line or end of line respectively, and fail. |
| 7 | JUMP |
A target | Jump to A. |
| 8 | CHOICE |
A target | Save the state and push a choice entry with target A. |
| 9 | COMMIT |
A target | Remove the top entry (a choice) and jump to A. |
| 10 | CUT |
Scan the entry stack from the top and mark the first choice or iteration entry as cut. If a lookahead entry or the base of the rule body is reached first, do nothing. Label and recovery entries are skipped over. | |
| 11 | PUSHPOS |
Push the current position as a value. | |
| 12 | PUSHNIL |
Push nil. | |
| 13 | SEQ |
A n | Pop n values and the position below them, and push a tuple node whose children are those n values, whose range runs from that position to the current position, and which is fresh. |
| 14 | ATOMIC |
A b | Pop a position and, if b, push a string node of the text from that position to the current position. |
| 15 | CAPTURE |
A slot, B pop | Write the top value into slot A of the current frame and record the write in the capture trail. If B is 1, also pop the value (a capture in a value-free context). |
| 16 | REPEAT |
A min, B max, C scope | Push a repetition state (count 0, value-stack base at the current height). B = -1 means no maximum. C is a scope-table index, or -1 if elements have no scope. When the repetition builds a value, a PUSHPOS immediately before it pushes the start position. |
| 35 | REPEATW |
A min constant, B max constant, C scope | Like REPEAT, with the bounds read from the EINT instructions at expression-code indices A and B. The scope, iteration, streaming and resumption semantics are unchanged. |
| 36 | SCANW |
A class, B min constant, C max constant | Like SCAN, with the bounds read from the EINT instructions at expression-code indices B and C. |
| 37 | SPREAD |
A n, B tuple | After the CALL of a rule that splices, pop its result and push its n elements: if B is 1 the children of the tuple node it returned, otherwise the result itself when n is 1, and nothing when n is 0. |
| 38 | STRF |
A folded string, B expectation, C b | Like STR, for a case-insensitive literal. A is the literal with each ASCII letter A-Z replaced by its lowercase letter. Each input character is folded the same way before it is compared, so only ASCII letters are case-insensitive. B is the literal with its suffix i. If b is 1, the pushed string node's text is the consumed input, not the literal. |
| 17 | ITER |
A exit | If the repetition has a finite maximum and its count has reached it, jump to A without executing the element. Otherwise save the state and push an iteration entry whose target is A (the exit of the loop). If the repetition has a scope, switch to a new frame. |
| 18 | NEXT |
A loop, B mode, C slot | One iteration succeeded. Remove the iteration entry and restore the frame. If the repetition has a scope and the mode is 1 or 2, attach the captures to the element value (with mode 0 the scope exists only for predicates inside the element). Increment the count. Mode 0 keeps no value (a value-free repetition), 1 keeps the value, 2 passes the value to the stream consumer, and 3 pushes the value of slot C of the element's frame as the element value: a projected repetition, whose elements are matched without values (map($x, (e) => e.$f) reads only that capture; see internal/compile/project.go). Then, if the iteration consumed no input, or the count has reached the maximum, jump to the exit recorded by ITER; otherwise jump to A. |
| 19 | ENDREPEAT |
A b | Pop the repetition state. Fail if the count is below the minimum. If b, pop the values above the value-stack base and the start position below them, and push a list node (fresh). |
| 20 | LOOK |
A target, B neg | Save the state, push a lookahead entry and increment silent. A is used only by a negative lookahead (B = 1). |
| 21 | ENDLOOK |
A neg | The inner expression succeeded. Positive: remove the entry, restore silent, restore only pos and the value-stack height, and continue. Negative: restore silent, restore the state, and fail. |
| 22 | CALL |
A rule, B level, C keep | Call rule A at level B (Rule calls). On success, push the value if keep is 1. |
| 23 | OPERATORS_RULE |
A table | Parse Operators Rule table A at level min and push its value. min is the level of the CALL that invoked this body. |
| 24 | PRED |
A expression | Evaluate expression code A. Fail if evaluation produces an error or false. |
| 25 | BIND |
A name, B expression, C 0 | Evaluate expression code B. Fail on error; otherwise add variable A (a string index) to the environment. The value is not checked: the compiler has proved it assignable to the declared type of variable A, so a module is trusted to bind values of that type, and a variable holds any value, so a module that does not cannot break memory safety. C is 0. |
| 26 | LABEL |
A message, B mode | Save the farthest failure, reset it to (current position, empty), and push a label entry. B=0 applies @error; B=1 applies @diagnostic and renames ordinary expected items from the wrapped failure. |
| 27 | ENDLABEL |
Remove the entry, add the expectations recorded inside to the saved farthest failure, and make that the farthest failure again. | |
| 28 | RECOVER |
A target | Save the state, save the farthest failure and reset it to (current position, empty), and push a recovery entry with target A (the skip code). |
| 29 | ENDRECOVER |
A target | The inner expression succeeded. Remove the entry, merge the recorded expectations back as for ENDLABEL, and jump to A. |
| 30 | ENDSKIP |
A b | The skip succeeded. Fail if it consumed no input. Otherwise add the error to the recovered errors and, if b, push an error node. |
| 31 | RETURN |
The rule body succeeded. | |
| 32 | END |
An Operators Rule part succeeded. |
Streaming. NEXT with mode 2 is emitted only for the repetition marked #stream in the start rule. When the VM is parsing a stream and this repetition runs in the start rule's body (call depth 1), the element value is not kept: it is passed to the consumer (no longer fresh), the input up to the current position is committed (earlier input and memo entries are discarded), and the loop exits if the iteration consumed no input and otherwise jumps to A. On reentry, ITER enforces the finite maximum before executing another element; a zero maximum executes no elements. In any other parse, mode 2 behaves like mode 1.
Recovery
When a recovery entry is popped on failure, the VM:
- Takes the farthest failure recorded inside (a position and its expectations) and restores the saved farthest failure.
- Restores the state.
- Pushes a skip entry (holding a saved state for restoring and the failure from step 1) and jumps to the recorded target (the skip code).
ENDSKIP builds a syntax error from the failure held by the skip entry. The error node spans from the start of the recovery to the current position; its text is the text of that range and its message field is the string form of the syntax error.
Rule Calls
CALL r, min calls rule r as follows. The call is memoized if the rule is a leader, or if it is marked for memoization and is not transient. In an incremental parse, a transient rule is memoized when it is marked for memoization, so that results from the previous parse can be reused.
A rule is marked transient when it calls no other rule, or when it is referenced only once in the grammar. Re-evaluating the former only scans the input once more; the latter is re-evaluated at the same position only when its single caller is. In both cases a memo entry would never be reused.
- Look up the memo: a provisional result of a growing left recursion for (r, pos, min), whatever the variable values it was stored with, or else a result for (r, pos, min) and, when the rule reads variables, for the current values of those variables (the rule table's variable list). If there is one, and it is growing, or it was not computed inside a lookahead, or the VM is currently inside a lookahead, use it: record the expectations stored with the result (unless it is growing) and its recovered errors; on success, advance the position and return the value.
- If the call is memoized, save the farthest failure and reset it to (start position, empty).
- If the rule is a leader, evaluate it by growing the seed; otherwise evaluate the body once. On failure, remove the errors recovered during this call.
- If the call is memoized, store in the memo the result, the end position, the recovered errors, whether the call ran inside a lookahead, and the expectations recorded inside; then merge those expectations into the saved farthest failure and restore it.
- On failure, reset the position to the start position.
Body Evaluation
To evaluate a body, the VM creates a new frame (sized for the rule's scope), saves the frame, the environment and the length of the capture trail, and runs the body's code. While the body runs, it uses its own base of the entry stack and of the value stack. When the body finishes, the VM restores the frame and the capture trail; on success, it performs rule completion; finally it restores the environment, so variables defined in the body are not visible to the caller.
Left Recursion
A leader rule is evaluated as follows:
- Store a failure for (r, pos, min) in the memo, marked as growing.
- Reset the position to the start position, reset the recovered errors to their state before the call, and evaluate the body.
- If the body fails, or does not advance beyond the previous success, stop. Otherwise store the result in the memo, marked as growing, and go back to step 2.
- Finalize the last result (clear the growing mark) and return it.
Rule Completion
The value of the rule is built from the body value v and the start position:
- With an action: evaluate the action with the
$nitems set to the children of v if the body is a sequence, and to [v] otherwise. If the result is a node newly created by the action, its range becomes the range of the rule. Anint,stringorboolresult becomes a value node with the range of the rule. - Otherwise: if the scope has captures, attach them. If v is fresh, give it the rule name if it has none, and clear its fresh mark.
When the rule's node type is a named node type, the value built above is then checked against the rule's check, which names the underlying node type, and converted to the named node type as ENODE converts it; a value that is not a string, tuple or list node fails the parse with cannot convert <value type> to <type> in result of rule <name>.
Attaching captures: if no capture is non-nil, do nothing. If v is nil, is not fresh, or is itself one of the captures, wrap v in a tuple node whose only child is v, whose range runs from the start position to the current position, and which is fresh. Then add each non-nil capture as a field under its name.
Operators Rule
The algorithm is the same as operatorsRuleParse in the closure backend; see the parsing rules in Operators Rule. Each part's code runs with its own entry-stack base and frame.
- Skip: increment
silent, run the skip code, and on failure reset the position. - Longest match: for each candidate operator, save the state, run the line's code in a new frame, and on success record the end position, the value, the frame, the environment, whether a cut was passed, and the recovered errors; then restore the state (writing the recorded captures back into the frame). A prefix or postfix candidate that succeeds without consuming input is treated as not matching. Select the candidate that advanced farthest; on a tie, select the one declared first. Also record whether some candidate failed after passing a cut, including an empty prefix/postfix match rejected as a candidate. A successful selected candidate takes precedence over this aggregate failure; each candidate's cut scope is isolated.
- Head: after skip, try the longest match among the prefix operators. If none matches and some candidate failed after passing a cut, the whole expression fails without trying operands. Otherwise, if one matches, apply it and parse the right operand at the operator's level. If the right operand fails, fail if a cut was passed; otherwise backtrack and try the operands. Operands are tried in declaration order; if an operand fails after passing a cut, the whole expression fails.
- Tail: after skip, try the longest match among the infix and postfix operators. If there is no candidate, stop (or fail, if a candidate failed after a cut). If the operator's level is less than or equal to
min, or the operator is non-associative and a non-associative operator of the same level was just applied, backtrack and stop. A postfix operator is applied immediately. For an infix operator, parse the right operand at the operator's level (level - 1 for a right-associative operator); if that fails, fail if a cut was passed, and otherwise backtrack and stop. - Values: if the operator has an action, evaluate it with local 0 =
$lhs, 1 =$rhsand 2 =$op(a string node of the operator part's text). Otherwise build a tuple node whose children are[op, rhs],[lhs, op]or[lhs, op, rhs]according to the operator kind, whoseoperatorfield is the operator's index, and which carries the rule name. The value of an operand is its action's result if it has an action; otherwise its captures are attached and, if it is fresh, it is given the rule name. When the rule's node type is a named node type, the value of every operator (the action's result, or the tuple node, which is then built with that type) is converted to it asENODEconverts it, with the failurecannot convert <value type> to <type> in result of rule <name>; the value of an operand is not converted, and the rule's result as a whole is not converted.
Execution Models
A runtime implements one or both of two execution models, which differ in how rule calls and the Operators Rule are executed. Both must produce the same results.
- Recursive model:
CALLandOPERATORS_RULEinvoke the procedures in Rule calls and Operators Rule as host-language functions. The maximum nesting depth is bounded by the host stack. - Iterative model: execution suspends at
CALLandOPERATORS_RULEand resumes when the result is available (after aCALL, on success it pushes the value if keep is 1 and continues with the next instruction; on failure it proceeds as in Failure). Rule calls, left-recursion growth, the Operators Rule loop, longest match and skip are implemented as state machines whose state is kept in stack entries, and they perform the steps above in the same order. Host stack usage is constant, independent of how deeply the input nests.
In the reference implementation, both models share the instruction interpreter (step) and the stages of a rule call (callBegin, growBegin, growStep, growEnd, invokeBegin, invokeEnd, callEnd); they differ only in the order in which these are invoked (vm.go, ivm.go).
The reference implementation also limits the nesting depth of rule calls (100,000 by default, 10,000,000 for the iterative model; configurable with pego.WithMaxDepth) and reports a parse error when the limit is exceeded.
Actions and predicates may be evaluated with host function calls in either model, because their nesting depth is fixed by the grammar and does not depend on the input.
Expression Code
Actions and predicates are evaluated by stack-based expression code. An evaluation error in an action is an error of the whole parse (action in <rule name>: <message>); in a predicate it is a failure.
| Code | Instruction | Operands | Behavior |
|---|---|---|---|
| 100 | EINT |
A, B value | Push the integer A + B × 2³² (A and B signed, with 64-bit wrap-around): A holds its low 32 bits, and B is 0 for the integers of 32 bits. |
| 101 | ESTR |
A string | Push a string. |
| 102 | EBOOL |
A value | Push a boolean (0 or 1). |
| 103 | ENIL |
Push nil. | |
| 104 | ECAP |
A slot | Push the value of a capture slot. |
| 105 | EITEM |
A n | Push $n. For n = 0, push a list value of all items. An out-of-range n is an error. |
| 106 | ELOCAL |
A index | Push a local (a lambda parameter, or $lhs, $rhs, $op). |
| 107 | EVAR |
A name | Push the value of a variable. An undefined variable is an error. |
| 108 | EMEMBER |
A name | Pop a value and push its field. Start, End and Value are built in for a node with a range: the range, and the text of a string node or the elements of a tuple or list node. A field not declared by the struct type is an error; a declared field that is absent yields nil. A name $y reads capture y: it is an error on a struct value, and an unmatched capture yields nil. |
| 109 | ENEW |
A type, B field list | Pop as many values as the field list has names, and push a struct node. |
| 110 | EBIN |
A operator | Pop two values and push the result of the operation. The operator [] indexes the list below with the int on top. |
| 111 | EUNARY |
A operator | Pop a value and push the result of the operation. |
| 112, 113 | EAND / EOR |
A target | It is an error if the top value is not a boolean. EAND on false, and EOR on true, leave the value and jump to A. Otherwise pop the value and continue. |
| 114 | EBOOLCHK |
A operator | It is an error if the top value is not a boolean. |
| 115 | EFUNC |
A start, B parameter count | Push a function that captures the current locals. |
| 116 | ECALL |
A built-in, B argument count | Pop the arguments, call the built-in function, and push the result. |
| 117 | ERET |
Return the top value as the value of the expression. | |
| 118 | ETEXTCHK |
It is an error (the same as text's) if text of the top value is not defined. The value stays. |
|
| 119 | ETEXTEQ |
A neg | Pop two values and push whether their texts (as text gives them; both were checked with ETEXTCHK) are equal, or unequal if neg is 1. |
| 120 | ELISTBEGIN |
Start gathering the elements of a list. Gatherings nest. | |
| 121 | ELISTPUSH |
A n | Pop n values and add them, in order, to the list being gathered, with the checks of list. |
| 122 | EMAPPUSH |
Pop a function and a list (the list below) and add the results of calling the function on each element, with the checks of map. |
|
| 123 | ELISTEND |
Finish the innermost gathering and push its elements as a list value, as concat builds it. |
|
| 124 | Not used. | ||
| 125 | Not used. | ||
| 126 | ECAST |
A type, B line, C column | Check that the value on top of the stack, which stays there, fits type A, for a cast x.(T) (see Casts); a mismatch raises the evaluation error cannot cast <value type> to <A> at input position <N> (line <L>, column <C>), where N is the position where the evaluation started, followed by ; cast at grammar position <B>:<C> when B is positive. B and C are the grammar position of the cast, or 0 and 0 when the module has no source information (the compiler writes them only then). Each list element examined is one execution step. A is the type after alias resolution, as the value checker writes it (*Ident, []*(Ident | Number), int | string): a join with nil is an optional type. The compiler omits it when every value fits the type, such as _ or a type containing _. It is the only instruction that checks the type of a value: the compiler proves every other value fits the location it flows to, or rejects the grammar. |
| 127 | ENODE |
A type, B 0 | Pop a node and push it converted to the named node type A, for a conversion N{x}: a copy of the node with the same range, value and captures, whose type is A and which has no rule name. A node that already has type A is pushed unchanged. A value that is not a string, tuple or list node raises the evaluation error cannot convert <value type> to <A>; whether the value fits the underlying type of A is decided when the grammar is compiled. |
| 128 | ETUPLE |
A n | Pop n values and push a tuple value of them, in order, for a tuple literal (x, y). As in list, a value that is not a node, nil, an int, a string or a bool raises an evaluation error, and an int, string or bool becomes a box element. |
Names (EVAR, EMEMBER), operators (EBIN, EUNARY, EBOOLCHK, written as their source symbol such as + or &&) and type names (ENEW) are string-table indices.
Typed list constructors use ECALL with the list built-in; their element
types are checked at compilation and require no additional VM instruction or
per-element concrete-type guard.
Calling a function evaluates its code from its start, with the locals captured at creation followed by the arguments as its locals.
Instructions 118–123 are shortcuts with the same results as the general instructions. The reference compiler emits ETEXTCHK after each operand and ETEXTEQ for text(a) == text(b) and text(a) != text(b), which compares the texts without making them values, and ELISTBEGIN, ELISTPUSH/EMAPPUSH per argument and ELISTEND for a concat call whose arguments are all list, map or such concat calls, which builds no intermediate list. In both cases the errors arise in the same order as with ECALL.
Built-in function indices: 0 len, 1 text, 2 foldl, 3 foldr, 4 map, 5 list, 6 concat, 7 filter. Their semantics are defined in Actions.
Error Messages
A syntax error (see Syntax errors) is formatted as line:column: syntax error: expected A, B (expectations sorted lexicographically). If @error messages apply, it is formatted as line:column: message (multiple messages joined with ; ). @diagnostic changes ordinary expected-item descriptions but does not replace explicit messages.
Expectation strings are those the compiler placed in the string table: literals in quotes, character classes in [...] form, any character, and so on.
File Format
A compiled grammar uses container version 6. It stores one execution-only compile result: the module of one mode and the start rule, with no grammar AST and no output schema (see Compiled Grammars). The container version and the module's instruction-set version are separate version axes. A loader accepts only container version 6. Integers are variable-length: unsigned integers use LEB128 and signed integers use zigzag-encoded LEB128. A boolean is one byte (0 or 1).
| Part | Contents |
|---|---|
| Magic | PEGOC\x00 (6 bytes) |
| Version | 1 byte: 5. A loader refuses any other value. |
| Mode | 1 byte: 0 parse, 1 recognize, 2 document. The module serves the operations of this mode only. A parse-mode or document-mode module is the module of the grammar; a recognize-mode module is the module of its recognizer. A loader refuses any other value. |
| Instruction-set version | Unsigned integer for this module: 26. Every module uses this version, whatever instructions it contains. Earlier versions built the syntax trees that preceded the value model, held terminal types, wrote field types with the earlier node type names, had no named node types, had no default-value flag, had no tuple values, carried text struct node types and the text flag of EMEMBER and EBIN, did not give the node type of an Operators Rule its meaning, had no variable table, had no start rules, or checked field values, rule results and bindings when they ran (the ECHECK instruction, the check operands of BIND and ENODE, and the checks of rules and lines); a runtime rejects them and asks for the grammar to be compiled again. |
| String table | Count, then each string (byte length and UTF-8 bytes), all unsigned. The module's string table is a prefix of this table. |
| Start rule | String index, unsigned: the start rule the saved parser uses. It is a start rule of the module, or the empty string for the module's default start rule. |
| Module | See below |
| Checksum | CRC-32 (IEEE) of everything from the magic up to this point, 4 bytes little-endian |
The module is laid out in the following order. A list is a count (unsigned) followed by its elements. Indices, positions and other integers inside the module are signed unless stated otherwise.
- The number of module strings, unsigned (this many strings from the start of the string table form the module's string table).
- Source information, a boolean: whether the values a parse returns carry the rule names and capture names of the grammar. A loaded program keeps it.
- Default value, a boolean: whether the program was compiled with default values, which drop value actions and the declared types of rules (except for the rules that make values read by predicates) before compilation. A loaded program keeps it.
- Character class table: a list of (negated, expectation string, number of ranges (unsigned), followed by the lower and upper bound of each range).
- Type table: a list of (name, list of field names, underlying node type). The underlying node type is a string index, or -1 for a struct type.
- Field-list table, then scope table: each a list of lists of string indices.
- Variable table: a list of (name, type, integer, string). The type is 0 for
int, 1 forstringand 2 forbool. The integer is the initial value of anintvariable, and 0 or 1 for aboolvariable. The string is the string table index of the initial value of astringvariable, and -1 for the other types. - Rule table: a list of (name, body, scope, action, node type, memoize, leader, position-dependent, Operators Rule, value-free twin, transient, where, splice, variables, check).
- Start rules: a list of the rule table indices of the start rules, the rules marked
@start, in increasing order, followed by the rule table index of the default start rule. Parsing begins only at these rules. - Operators Rule table: a list of (skip, list of operand lines, list of prefix operators, list of infix and postfix operators). A line is (start, scope, action, body is a sequence, check); an operator is (index, kind, associativity, level, line).
- Match code, then expression code: each a list of instructions (opcode as 1 byte, then
A,B,C).
Load-Time Validation
On load, a runtime checks the following and rejects the file if any check fails:
- The checksum, the version, the mode and the instruction-set version are valid, the start rule is empty or a start rule of the module, and no data remains after the module.
- Each variable is named once, has a valid type, and has an initial value that matches its type.
- Every reference in the tables and instructions (strings, character classes, scopes, field lists, rules, Operators Rule tables, expression code, built-in functions) is in range.
- There is at least one start rule. The start rules are in increasing order and have distinct names, and the default start rule is one of them.
- Every opcode is known, and every flag operand is within its range.
- The node type of a rule names a named node type, or a declared type with a field
Text. The node type of a rule with an Operators Rule names a named node type. - A named node type has no fields, and its underlying node type is a type text, parsed as the type of
ECASTis, of the formnode[T]. The type A ofENODEnames a named node type, and B is 0. - Wide bounds reference
EINTconstants, fit the runtime's implementationint, and satisfy minimum ≥ 0 and maximum = -1 or maximum ≥ minimum. - B and C of
ECASTare not negative. The type text ofECASTparses as a field type naming only declared types,error,node[T], tuple types(T, ...),int,string,boolandnil. These type texts are resolved types, which contain no&or_|_, and no_other than the argument ofnode[_]. - Every jump target is forward. Only
NEXTjumps backward. The target ofEFUNCis also forward. - Every
RECOVERbody ends at anENDRECOVERimmediately before its skip target; that instruction jumps past anENDSKIP. These recovery regions are nested and contain no stream-modeNEXT(mode 2) in either the protected body or skip. Element-local recovery ends before the enclosing streaming repetition'sNEXT.
A runtime does not verify that instruction sequences use the value stack and the entry stack correctly, and it trusts the mode as it trusts the module's types: it checks that the mode byte names a mode, not that the module is the one a compiler writes for that mode. A file whose mode does not match its module MAY return wrong results without an error, such as no value from a parse. Runtime errors caused by invalid code are reported as parse errors, but termination is not guaranteed, so load files only from trusted sources.