Variables

A variable keeps something a rule already matched, so a later part of the grammar can use it again. This is what turns a grammar from a shape into a grammar that can check things.

There are two variable types and one set type:

Keyword

Holds

texvar

a span of text

numvar

a number, 0 upward — no negatives, no decimals

semvar

a set of text the grammar builds and later matches against

Capturing with =>

Inside an action, (unit => target) runs the unit and keeps what it matched. The target is either a new declaration or a variable already in hand:

A := (word => texvar x) x;             # capture, then require the same text again
B := (word => texvar x) (word => x);   # declare once, assign again
C := (n::to_num => numvar v);          # a producer fills a numvar

A variable must be assigned on every path that reaches a read, or the grammar is refused. Nothing is zeroed, so there is no safe stale value to fall back on.

An alternative that assigns and then loses is rolled back. Only the winning alternative’s assignments survive a choice.

Globals

A global is declared at the top level and lives for the whole parse. It must be given an initial value:

texvar greeting = "Hello World";
numvar limit = 50;

The initial value is set once, before any input is read — so writing greeting in a grammar matches that text until something assigns over it.

Local scope

A variable declared inside an action belongs to that action, and to one entry into it. Calling the action again, recursion included, gives the new activation its own copy, so an outer value is never disturbed by an inner one:

w = <a:z>;
rec := (w => texvar x) rec? x;   # an even-length palindrome

Semvar — a set the grammar learns

A semvar is a set of text that the parse adds to and later matches against. It is what a semantic predicate needs and a plain variable cannot give: a grammar that knows which identifiers have been declared.

Sets are global, and there are at most four of them.

semvar kind = "int", "short";   # with members from the start
semvar name;                    # starting empty

decl := "typedef" . (ident => kind) . ";";   # add what was parsed
use  := kind;                                # match any member, longest first
alt  := kind::first;                         # ... earliest added instead

Writing the set’s name matches any member, taking the longest. ::first takes the earliest added instead.

Scope — a depth the input moves

A scope is a depth counter that the input moves. Leaving a scope forgets whatever was added inside it to the sets bound to that scope. Ordinary block scoping, in other words.

At most four, the same ceiling as the sets — and it follows from them, since a scope exists to empty the sets bound to it and one with nothing bound is refused.

scope blk
    begin = "{";
    end   = "}";

feat {"scope": blk} semvar kind = "int";   # bound to blk

block := blk::begin . item* . blk::end;

feat { ... } goes in front of the thing it configures, the same place it goes in front of an action that binds a precedence table. What the keys mean is not shared: an action reads "bind", a semvar reads "scope".

blk::begin runs the begin grammar and, if it matches, goes one level deeper. blk::end comes back out and empties every set bound to blk of whatever was added inside. A set that is not bound to a scope never forgets.

begin and end may sit in different actions, and blocks nest freely.

Several brackets, paired by position

Written as a list, the two sides pair up position by position: whatever opened a level is the only thing that closes it.

scope blk
    begin = ["{", "("];
    end   = ["}", ")"];

{ ... } and ( ... ) both open and close a level, and { ... ) does not close at all. Written as a choice instead — begin = "{" | "("; — the pairing is gone and { ) closes, which is the whole difference the list makes.

Two to four positions, and the two lists must be the same length. A one-item list is the ordinary scope written the long way, and compiles to the same bytes.

Each open level costs one byte, so how deep a scope may nest is how many bytes it was given — 64 by default, and scope-ordered-buffer-size sets it.

try — a name read before it is declared

A semvar only matches what has already been added, which is why C has forward declarations. In a language where every declaration in a file is visible to every other, the grammar has to read a name on trust:

semvar type;

decl := "type" . (ident => type) . ";";
use  := try type [ident] . ";";

try type [ident] matches an ordinary ident and writes down a promise: that text will be declared into type before the input ends. A later => type of the same text keeps it. What is still owed when the input runs out is an error, and so is a name that was declared into some other set.

The bare name is the set. The bracketed unit is the replacement matcher — what runs in place of the set, matching exactly what it would have matched on its own. One of each, and only a semvar may be named.

Warning

try classifies, it does not restructure. It works when both readings are the same tree and differ only in what the name in it is called. T * x; and a * b; are two different trees, and a promise cannot retract a shape that is already built.

See also

Logic blocks read these values — see Logic Block — and an IF statement branches on them. texvar, numvar, semvar and scope are reserved words, as are begin, end, first and clear.