Quick Reference

A one-page summary of every APML feature. Each entry links to its full chapter.

Module — details

program MyParser;         # name the module
program "algodal" json;   # ... in two parts; the full name is algodaljson

Actions — details

a  = "x";   # charseq action (=)  -> flat characters
b := "x";   # syntac action (:=)  -> AST node

Series, Options, Groups — details

s = A B C;          # series: A then B then C
o = A | B;          # option (OR): every alternative checked, longest wins
f = A / B;          # option (Firstly OR): stop at first match
g = A (B | C) D;    # group as a single unit

Comments — details

# line comment to end of line

Alias — details

alias tab \x09;     # a name for a literal value

Characters — details

A = char;        # any single utf-8 character
B = <A:Z>;       # character block; ranges use ':'
C = \x41;        # hex literal (A)
D = \u0041;     # unicode literal (A)
E = \x43,41,54;  # chain -> "CAT"
F = \x41:5A;     # range A..Z

Counters — details

<A:Z>+;      # one or more
<A:Z>*;      # zero or more
<A:Z>?;      # zero or one (optional)
<A:Z>-5;     # exactly 5
<A:Z>-5+;    # five or more
<A:Z>-7:12;  # between 7 and 12

Built-in Actions — details

spc;   # space or tab
nl;    # line break
eol;   # end-of-line (a line break or end of input)
eof;   # end of input (matches zero width)

Inbetween (skip) — details

stmt = "(" . "A" . ")";  # '.' skips per the config below
. { spc, nl };           # required whenever '.' is used

Stopping a counter — details

char*::until("-->")   # repeat until "-->", and take it too
(char* ^ "-->")       # repeat until "-->", and leave it for the next unit

A counter followed by its own base needs neither — A+ A is compiled as A A+.

Permutation — details

A = perm["A" "B" "C"];  # match members in any order, each once

Text Functions — details

tex::order("ABC");   # same length, any character order
tex::oneof("ABC");   # one character from the set
tex::icase("ABC");   # case-insensitive exact match
tex::icase(word);    # ...over a texvar, or a semvar's members

Result Functions — details

A predicate asks a question about the matched text and can only pass or fail it. A producer answers with a number instead, and never fails anything.

# predicates
name::is("Fred");        # matched text equals
name::not("Amber");      # ... does not equal
name::subkind("Fr");     # ... contains

# narrowing what is asked about
name::part(1);           # 1-indexed character
name::part(1:4);         # characters 1..4
name::part(2+);          # character 2 to the end
name::part(1)::is("F");  # chained

# producers -- no parentheses, they take no argument
name::char_count;        # characters matched
name::to_num;            # matched text as a number; 0 if it is not one
A+::iter_steps;          # times a counter ran; 1 if not counted, 0 if none

# a producer may be compared against a NUMBER
name::char_count::is(3);      # exactly three characters
name::to_num::is(200);        # the value 200 -- so "0200" matches too
name::char_count::is(limit);  # ... or against a numvar

Variables — details

Local to one action, or global to the whole parse. A texvar holds a span of text; a numvar holds a number.

texvar greeting = "Hello";   # global, set before parsing starts
numvar limit = 3;            # global number

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.

Semvar — details

A set of text the grammar builds as it parses and then matches against — what a semantic predicate needs and a plain variable cannot give. Global, at most four.

semvar kind = "int", "short";        # declared members
semvar name;                         # starts empty

decl := "typedef" (ident => kind) ";";   # add what was parsed
use  := kind;                            # match any member, longest first
alt  := kind::first;                     # ... earliest added instead
soon := try kind [ident] ";";            # read it before it is declared

try set [unit] matches the bracketed unit — the replacement matcher — and promises that text will be declared into set before the input ends. A promise still owed when the input runs out is an error. One semvar, one unit.

Scope — details

A depth the input moves. Leaving a scope forgets whatever was added to the sets bound to it — ordinary block scoping.

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

feat {"scope": blk} semvar kind = "int";   # bound; an unbound set never forgets

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

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

Written as lists, the brackets pair by position — whatever opened a level is the only thing that closes it, so { ... ) does not close. Two to four positions, and the two lists must be the same length:

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

Logic Blocks — details

A logic block tests values. It reads no input and moves no cursor: it succeeds or fails, and consumes nothing either way. {{ ... }} is final — it runs for what it does, always holds, and nothing may follow it in the grammar.

A := (word => texvar x) {x == "cat"};      # a test
B := "a" {{9 => n}};                       # a deed, always holds

Operators, in C’s precedence, tightest first:

NOT a            # negation
a * b   a / b    # division by zero is 0
a + b   a - b    # a - b clamps at 0
a < b   a <= b   a > b   a >= b
a == b  a != b   # text compares by bytes; == and != are all text has
a AND b          # short-circuits: b is not evaluated when a is false
a OR b           # short-circuits: b is not evaluated when a is true
TRUE  FALSE      # one and zero
set::is(t)  set::not(t)  set::clear    # questions about a semvar
error("...")                           # stop the whole parse

Statements are separated by ;, and the last one’s value is the block’s answer. A statement that produced nothing — an assignment, a cleared set — is a deed rather than an answer, and a deed is no reason to fail.

error("...")

Every other failure means “this did not match here”, and it sends the machine looking for another reading. error() means the input is wrong and there is nothing else to try: it travels straight out of options, series, counters, permutations and if conditions alike, ends the run, and its message becomes the run’s error.

z := (word => texvar x) {x == "cat" OR error("only cats here")};

Because OR short-circuits, that reports nothing for a cat and stops for a dog. {{ }} does not absorb it either — always holding is an answer, and error() does not give one.

IF Statements — details

if (cond) [then | else]     # both branches
if (cond) [then]            # then only
if (cond) [| else]          # else only

The condition may be a logic block or a grammar; a grammar condition consumes what it matched. With no branch to take, an if is like E? — the cursor resets and the next unit is read.

Feature Config

How a declaration is configured, written apart from what it declares. Entries are keyed, so order never matters; a key the feature does not read is an error.

{ "key" : value , ... }     # value is a label, a number or a string

Config — details

What the grammar asks the machine to build. Optional; every setting has a default and what you do not write keeps it.

config {
    "ast-node-text" : FALSE,     # literals stop being nodes
    "ast-node-char" : TRUE,      # char literals start being nodes
};

setting

default

ast-node-action

TRUE

ast-node-text · -text-counter · -text-series

TRUE

ast-node-char · -char-counter · -char-series

FALSE

A matched literal is a node named by what it matched, and adjacent literals make one node — "A" "B" is text "AB". A . between them ends the run, so "a" . "b" stays two. Turn everything off and a parse builds no tree at all, which is all a recogniser needs.

Changing a setting changes the AST. An interpreter is written against one config.

Parser Block — details

parser {
    main_grammar;   # the single start grammar; the loop re-runs it over what remains
};
parser { A | B | C; };       # an option
parser { A B C; };           # a series
parser { item; };            # the loop walks adjacent items
parser { item (. item)*; };  # items separated by the inbetween (`.`)

Node IDs — details

node_id {
    Label: "saved name",    # what an action is saved as in the AST
};

Linking — details

link p2;                    # a separate machine, called into
link "algodal" json;        # sugar for `link algodaljson;`
X := p2::value;             # always qualified

Binding Power — details

bindpow bp {
    "+" : (50, 51) ;        # r = l + 1  -> left-associative
    "^" : (11, 10) ;        # r = l - 1  -> right-associative
    "not" : (0, 70) ;       # (0, r) prefix; (l, 0) postfix
};

feat {"bind": bp} expr := atom | expr . "+" . expr | "not" . expr;

Foreign Bodiess — details

indent = _;                        # body lives in your C code
foreign { indent: "apm_py_indent" };

AST Maps — details

(A . B) -> (B A)          # order in the map is order in the tree
(A . B) -> (A: (B))       # A becomes B's parent
(A . B) -> (A)            # B is discarded
(A . B) -> (A [B])        # B's children take B's place
(A => texvar v) -> (A node("Extra", v))    # a node nothing matched
('a'A . 'b'A) -> ('b' 'a')                 # labels tell two apart

System Functions — details

error("only cats here")     # stop the parse; nothing else is tried

Keywords — details

Every word the language spells out is reserved and cannot be a declared name. Config and feature keys are quoted strings, so they are exempt.

How to Use — details

apma mylang.apm -o mylang.apmb
ApmBinary   program = ApmReadBinaryFile("mylang.apmb", &ok);
ApmVmResult result  = ApmVmRun(&program, config);