Skip to content
Doug Torrance edited this page Aug 13, 2026 · 1 revision

Note: This grammar was AI-generated by Claude Opus 5. 🤖

D Language Grammar

EBNF description of D, the language in which the Macaulay2 interpreter is written. It is a companion to the Macaulay2 language grammar and the SimpleDoc language grammar, and like them it is meant to be enough to write a parser from — a tree-sitter grammar, an Emacs SMIE grammar, a syntax highlighter.

Everything below is derived from the compiler, scc1, whose source is in M2/Macaulay2/c. The whole front end is two files: grammar.y holds a yacc grammar together with a hand-written yylex (lines 375–461), and readfile.c holds the scanner gettoken (lines 124–191) and the built-in operator table (lines 196–223). There is no lex or flex input. Claims about what does and does not parse were checked by running scc1.

Overview

D source files live in M2/Macaulay2/d. A .d file is translated to C and a .dd file to C++, but the two are syntactically identical: the extension reaches only the choice of output file name (scc1.c:274–290), never the lexer or the parser. The generated .sig files under the build directory are D source as well, differing only in that they use the signature form, which hand-written files never do.

Two facts shape everything that follows, and a parser writer should read them before the tables.

Almost everything is one nonterminal. The grammar has ten nonterminals. Eight of them handle statement lists and argument lists, one accumulates the arms of a when, and all of the language proper is the tenth, expr. There is no statement syntax, no declaration syntax, and no type syntax. x := 1, f(x:T):W := e, {a:int, b:string}, T or U, and array(int) are all applications of infix operators and call syntax to plain identifiers, sorted out afterwards by the semantic pass in chk.c. A grammar transcribed literally from grammar.y will parse D correctly and produce a nearly flat tree; see Forms the grammar does not distinguish.

The operator set is not fixed by the grammar. grammar.y reserves ten numbered precedence levels, each in a left, right, and prefix flavour, and the operator spellings are registered into a trie at run time by the leftOperator, rightOperator, and prefixOperator statements. The standard operators come from an implicit prelude that scc1 parses before every file (readfile.c:196–223), and any D file may register more. No file shipped in M2/Macaulay2/d does, so a static grammar may hard-code the table in Operator Tables — but it is hard-coding a default, not a property of the language.

Notation

The terminals below are the tokens yylex actually returns, and are named as grammar.y names them. Where a token has no fixed spelling — the ten numbered operator levels, and the placeholders yacc uses only to carry precedence — the yacc name is used, in THIS_FONT. infixN, infixNr, and prefixN stand for the whole family, N ranging over 1…10.

The complete set of terminals, which is the interface a lexer has to produce:

  • IDENTIFIER INTEGER NUMBER STRINGCONST
  • the thirty operator tokens infix1infix10, infix1rinfix10r, prefix1prefix10
  • COLON OROR ANDAND OR BRACEPLUS EXPORT HEADER STRINGOP
  • one token per remaining keyword: IF THEN ELSE WHEN IS FOR FOREACH WHILE DO AT IN BY FROM TO NEW LEN BREAK PROVIDE RETURN FUNCTION USE PACKAGE SIGNATURE OPERATORLEFT OPERATORRIGHT OPERATORPREFIX
  • the literal characters ; ( ) { } , -

whiledo and ifte are not terminals a lexer produces — they exist only to carry precedence. Neither are SELF, OP, SUPER, and SPACE; see the Notes.

Character-level productions describe the scanner; everything from Grammar onwards is over tokens.

Lexical Rules

source     ::= (whitespace | comment | token)*

whitespace ::= " " | "\t" | "\n" | "\r" | "\f"
comment    ::= "--" [^\n]*

token      ::= operator | char | string | lexeme

operator   ::= (* longest match in the operator trie; see Operator Tables *)

char       ::= "'" ( [^\\] | "\\" any_char ) "'"
string     ::= '"' ( [^"\\] | "\\" any_char )* '"'

lexeme     ::= "." | ","          (* one character only; unreachable in practice,
                                    since both are matched by the trie first *)
             | [0-9]+ "." word_char*
             | any_char word_char*
word_char  ::= [A-Za-z0-9_]

any_char   ::= (* any single character, including a newline *)

A lexeme is then classified — as an integer, as a double, or, failing both, as a word:

integer    ::= "0x" [0-9a-fA-F]+
             | [0-9]+
double     ::= [0-9]+ "." [0-9]*
word       ::= (* any lexeme that is neither *)

A word whose spelling is in the keyword table becomes that keyword; every other word becomes IDENTIFIER. Integers become INTEGER; doubles and character constants both become NUMBER; strings become STRINGCONST.

Note the third lexeme alternative: the scanner advances past the first character unconditionally and only then starts requiring word characters (readfile.c:174–182). Any character that reaches gettoken therefore starts a lexeme and absorbs the word characters after it. Since a character the operator trie matches never reaches gettoken, the characters this applies to are [ ] { } @ # ? $ \ and the backtick — plus every byte ≥ 0x80 that the trie does not alias. So x[i] is not four tokens: it is x, the identifier [i, and the identifier ]. (Verified: z := [1]; lexes as the two identifiers [1 and ].)

The practical consequence is that D has almost no lexical errors. Any run of junk becomes an identifier and is reported much later as an undefined symbol. The only hard lexical failures are a malformed character or string constant, and those are fatal — see below.

Whitespace and comments

Whitespace is exactly space, \n, \t, \r, and \f (grammar.y:382). Newlines are not significant — they are ordinary whitespace, and ; separates expressions. Column numbers in diagnostics expand tabs to the next multiple of -tabwidth (default 8), which is the only thing indentation affects.

-- to end of line is the only comment syntax. There is no block comment form: /* … */ is a syntax error. Note also that // is not a comment — it is a left-associative infix operator at level 7 (integer quotient).

The -- test runs before the operator trie is consulted (grammar.y:388–392), so -- always wins over two applications of -; in x := 1--2; everything from -- on is a comment, including the ;.

Tokenization order

For each token the lexer first tries the operator trie, by longest match (tokenlength, grammar.y:299–314), and only falls back to gettoken if the trie matches nothing. Hence := beats :, and === beats == beats =, with no lookahead rules needed.

Two things follow that a hand-written lexer needs. The trie is consulted only at a token boundary, never inside a lexeme — and since a character the trie matches never reaches gettoken, an identifier can never contain one. And the terminal a matched operator produces is fixed entirely by the level and flavour it was registered at. So the operator layer is exactly a flat spelling → terminal map with longest-match, and nothing more.

The trie masks each byte with 0x7f (grammar.y:232, 306), so bytes ≥ 0x80 alias onto ASCII when operators are matched.

Words and numbers

Words, integers, and doubles all come out of one scanning loop (readfile.c:174–189), which is why the lexeme production above covers all three. The loop consumes word characters, and consumes a . only if every character so far has been a digit. The consequences are worth stating individually, because several of them are surprising and all were confirmed by running the compiler:

Written Lexes as Because
12.34 one double digits, then ., then digits
12. one double the fractional digits are optional
1.2.3 double 1.2, ., 3 at most one . per lexeme
12.a one identifier, spelled 12.a the . is consumed, then the classification fails
.5 ., 5not a double a leading . is never absorbed into the lexeme that follows
1e10 an identifier there is no exponent syntax
1.2e3 an identifier likewise — the mantissa being a double changes nothing
0x10 an integer
0X10 an identifier the prefix is lowercase 0x only
0x an identifier the hex digit run may not be empty (readfile.c:79)
0x1.5 integer 0x1, ., 5 the x stops the "all digits so far" condition
1a an identifier the scan never requires an alphabetic first character

The identifier rows are all one phenomenon: anything that fails both the integer and the double test silently becomes a word, so a mistyped numeral is reported much later as an undefined symbol rather than as a lexical error. There is no octal or binary prefix, no digit separator, and no suffix.

.5 is worth one more sentence, because a lexeme starting with . is not just short — it stops the parse. . and , are special-cased at readfile.c:170–172, which advances exactly one character and returns it without ever consulting the numeric classifiers. So .5 is the . operator followed by 5, and since . has no prefix form, z := .5; is a syntax error, not a mis-lexed number. (doubleq would in fact accept .5 — its leading-digit test is commented out at readfile.c:92 — but no lexeme .5 can ever reach it.)

Negative literals do not exist. The integer and double recognisers accept a leading - (readfile.c:83, 91), but - is a registered operator, so the lexer can never hand them one; -1 is unary minus applied to 1.

Character and string constants

A character constant is ', then either one character or a backslash and one character, then '. The escapes are \0 \a \b \e \f \n \r \t \v, and \ before anything else yields that character (readfile.c:109–122). There are no octal or hex escapes.

Two consequences. ''' is legal — the unescaped ' is taken as the payload, so it is the character 39 (verified). And anything longer than one character is a fatal error, not a recoverable one: '\101' and '' both abort the compilation with "character constant not terminated" (readfile.c:143–145) (verified). An unterminated string likewise aborts with "file ends before string". These are the only lexical failures in the language, and unlike syntax errors there is no recovery from them.

A string constant runs from " to the next unescaped ". A backslash causes the next character to be skipped, and that is all the scanner does with it — escapes are deliberately not interpreted at this stage (readfile.c:166), so the set of meaningful escapes inside a D string is whatever the C or C++ compiler ultimately accepts. Newlines may appear inside a string, which is how multi-line header "…" blocks are written.

op

op is a keyword that puts the lexer into a one-shot mode (grammar.y:438–454): it consumes everything from immediately after op up to the first whitespace, ,, ;, (, or ), and returns it as an IDENTIFIER. This is how an operator is named as an ordinary identifier — op<< is the identifier <<, op== the identifier ==.

The operator text must be adjacent, and a space produces the empty identifier rather than an error. z := op + 1; parses cleanly, as + applied to the empty identifier and 1; the failure surfaces as "not a function or type" from the semantic pass (verified). A parser should treat op as a lexical mode, not as a prefix operator.

Keywords

These are the reserved words, from yyinit (grammar.y:320–361). A keyword is registered on the interned string itself, so it is reserved everywhere and can never be an identifier.

Words Token
if IF
then THEN
else ELSE
when WHEN
is IS
while until WHILE
for FOR
foreach FOREACH
do DO
from to by at in FROM TO BY AT IN
new NEW
len LEN
break BREAK
provide PROVIDE
return RETURN
or OR
function FUNCTION
export import threadLocal constant EXPORT
header declarations HEADER
Pointer atomicPointer Type atomicType arithmeticType integerType STRINGOP
leftOperator OPERATORLEFT
rightOperator OPERATORRIGHT
prefixOperator OPERATORPREFIX
use USE
package PACKAGE
signature SIGNATURE
op (handled in the lexer; see op)

Four groups of distinct words share a token and are therefore syntactically interchangeable, though they mean different things: while/until, header/declarations, the four in the EXPORT row, and the six in the STRINGOP row. A grammar meant for highlighting or navigation should keep them as separate terminals sharing one rule shape.

Words that are not keywords

These look like syntax and are not. They are ordinary identifiers, predefined as global symbols by init_dictionary (dictionary.c:54–107) from the table in keywords.h, and given meaning by chk.c:

null true false self int char bool double void exits returns array tarray length Ccode lvalue GCmalloc const

This is why array(int), Ccode(int,"…",x), null(), and length(v) need no productions of their own: each is the ordinary call form expr "(" … ")". const in particular is not reserved — it is used as a function name in M2/Macaulay2/d/M2.d. The reserved word in that family is constant.

keywords.h is the authority, and it holds more than the list above. Compiler-internal names there whose spelling contains no _ are typeable too, so memcpy, tagged_object, array_check, array_len_check, and array_take are also predefined identifiers. Names with a trailing underscore — block_, object_, tmp_, bad_ — are deliberately unspellable; the comment at keywords.h:19 says so: "we put an underscore in a symbol name so the user can't use it".

Names such as nothing, ushort, ulong, and float are one step further out: they are not known to the compiler at all, but defined in D itself, in M2/Macaulay2/d/arithmetic.d. Lexically they are indistinguishable from any other identifier.

Operator Tables

grammar.y:15–45 declares the complete precedence ladder. The table below is that declaration, lowest precedence first, with the operators registered at each level filled in. The Level column gives the number to pass to leftOperator, rightOperator, or prefixOperator; rows without a number cannot be registered into.

Level yacc row Assoc Operators
';' left ;
')' left )
whiledo PROVIDE RETURN left provide, return, and — via %prec whiledo — every when, loop, and new form
ifte left if … then … else
1 infix1 prefix1 left (none)
1 infix1r right ::= :=
EXPORT left export import threadLocal constant
COLON right :
2 infix2 prefix2 left =
2 infix2r right (none)
3 infix3 prefix3 left ^^ << |
3 infix3r right >>
OROR left ||
ANDAND left &&
OR left or
4 infix4 prefix4 left &  ·  prefix !
4 infix4r right (none)
5 infix5 prefix5 left <= < >= > == != === =!= =>  ·  prefix ~
5 infix5r right (none)
6 infix6 prefix6 '-' left + , binary -
6 infix6r right (none)
7 infix7 prefix7 left // / * %  ·  (prefix - parses here, but is not a prefix7 registration — see below)
7 infix7r right (none)
SPACE left (unused)
8 infix8 prefix8 left ^
8 infix8r right (none)
9 infix9 prefix9 left (none)
9 infix9r right (none)
10 infix10 prefix10 '(' left . , the call bracket (
10 infix10r right (none)
keyword row left if then else when is for foreach while do at in by from to new len break function {+ package signature use header leftOperator rightOperator prefixOperator, the STRINGOP words

Everything in the numbered levels comes from the prelude at readfile.c:196–223. The rest is registered by yyinit (grammar.y:362–372): : as COLON, || as OROR, && as ANDAND, {+ as BRACEPLUS, and - { } , ; ( ) as themselves.

Three consequences that a precedence-climbing or SMIE implementation has to get right, all verified against the compiler's output:

||, &&, and or sit in the middle of the ladder, not at the bottom. They are looser than & and the comparisons, but tighter than |, <<, >>, and ^^. So a | b && c parses as a | (b && c), and a && b & c as a && (b & c). They are also ordered among themselves, || loosest and or tightest, so a or b && c is (a or b) && c and a || b or c is a || (b or c).

Prefix - binds tighter than infix -, and is not a registered operator. Binary - sits at level 6 with +, but as the literal '-' in the %left row (grammar.y:34), not as infix6; and prefix - is the hand-written rule '-' expr %prec infix7 (grammar.y:176), not a prefix7 registration. The prelude registers no prefix operator at level 7 at all. The effect is that - a * b is (- a) * b and - a + b is (- a) + b, while - a ^ b is - (a ^ b).

export and friends bind tighter than :=. EXPORT is above infix1r in the ladder, so export x := e parses as (export x) := e — the modifier attaches to the name, not to the definition.

Grammar

Transcribed from grammar.y:50–212. Bison reports no conflicts for this grammar, so the precedence declarations above resolve every choice.

Top level

The literal productions are written in yacc's reversed-accumulator style:

wrappedprogram ::= program

program        ::= exprlist | exprlistsemi
                 | exprlist error | exprlistsemi error
                 | ε

exprlist       ::= reverseexprlist
exprlistsemi   ::= reverseexprlistsemi

reverseexprlist     ::= reverseexprlistsemi expr
reverseexprlistsemi ::= reverseexprlist ";"
                      | reverseexprlistsemi ";"
                      | expr ";"
                      | ";"
                      | error ";"

The language those five rules generate is easier to read collapsed:

exprlistsemi ::= ( expr? ";" )+          (* ends with ";" *)
exprlist     ::= ( expr? ";" )+ expr     (* ends with an expression *)
program      ::= ( expr? ";" )+ expr? | ε

So empty expressions are allowed (;; is legal), and the final ; of a file is optional — but only because some earlier ; has already satisfied the +. A file holding exactly one expression and no semicolon does not parse, while ; x := 1 does. The exprlist / exprlistsemi distinction is not cosmetic: the two are separate productions at every use site, because whether a block ends in ; determines its value.

Argument lists

arglistornull ::= ε | expr | arglist
arglist       ::= expr "," expr
                | expr "," arglist

arglist has at least two elements. A single expression is the separate arglistornull alternative, and the difference is observable: after an infix operator, x << (y) takes the ordinary grouping route and yields a binary node, while x << (y,w) takes the multi-argument route below and yields a three-child node (verified). There are no trailing commas — f(a,b,) is a syntax error — and ; never appears in an argument list, so f(a;b) is one too.

, is registered in the operator trie, but only as SELF (grammar.y:369), so that the lexer returns it as the literal character; it has no precedence row and no infix production of its own. It reaches the parser only through arglist — which is to say in calls, { … }, {+ … }, function(…), and the multi-argument infix family.

Expressions

expr has 104 of the grammar's 125 productions. (Bison reports 126, counting its own augmented rule $accept: wrappedprogram $end.)

Operator applications

expr ::= expr infixN expr              (* N = 1..10, left-associative *)
       | expr infixNr expr             (* N = 1..10, right-associative *)
       | prefixN expr                  (* N = 1..10 *)
       | expr "-" expr
       | "-" expr                      (* %prec infix7 *)
       | expr "||" expr
       | expr "&&" expr
       | expr "or" expr
       | expr ":" expr

- needs its own two rules because the lexer returns it as a literal character rather than as a numbered operator token.

An or chain is flattened by its parse action (grammar.y:106–110) into a single n-ary node, rather than left-nested as the other left-associative operators are.

There is a second family of infix rules, one per level and flavour, for operators taking more than two arguments (grammar.y:154–173):

expr ::= expr infixN "(" arglist ")"
       | expr infixNr "(" arglist ")"

This is what allows both the definition (x:T) << (y:U,z:W) : S := e and the corresponding use x << (y,z).

The family covers only the twenty numbered infix tokens. There is no such rule for -, :, ||, &&, or, or any prefix operator, so - (a,b), a : (b,c), and a || (b,c) are all syntax errors while a . (b,c) is not (verified).

Application, grouping, and blocks

expr ::= expr "(" arglistornull ")"    (* call, cast, or construction *)
       | "(" expr ")"                  (* grouping *)
       | "(" exprlist ")"              (* block, value is the last expression *)
       | "(" exprlistsemi ")"          (* block, value is void *)

Objects

expr ::= "{" arglistornull "}"         (* struct type or object *)
       | "{+" arglistornull "}"        (* tagged struct type or object *)

{+ is a single token. Both forms may be empty; {+} parses.

Conditionals and type-case

expr      ::= "if" expr "then" expr "else" expr    (* %prec ifte *)
            | "if" expr "then" expr                (* %prec ifte *)

expr      ::= typecasen                            (* %prec whiledo *)
            | typecasen "else" expr                (* %prec whiledo *)
typecasen ::= "when" expr "is" expr "do" expr      (* %prec whiledo *)
            | typecasen "is" expr "do" expr        (* %prec whiledo *)

An is clause takes an arbitrary expr, which is how both is x:T do … (a : application) and is T do … (a bare identifier) are accepted by one rule. else always attaches to the innermost open when or if.

Loops

All nine carry %prec whiledo.

expr ::= "while" expr "do" expr

       | "for" expr "do" expr
       | "for" expr "to" expr "do" expr
       | "for" expr "from" expr "to" expr "do" expr
       | "for" expr "from" expr "to" expr "by" expr "do" expr

       | "foreach" expr "in" expr "do" expr
       | "foreach" expr "in" expr "by" expr "do" expr
       | "foreach" expr "at" expr "in" expr "do" expr
       | "foreach" expr "at" expr "in" expr "by" expr "do" expr

until … do … uses the same production as while; the two are one token. There is no while … list … production — the form appears in M2/Macaulay2/c/README marked as possibly unimplemented, and it is in fact a syntax error.

Array construction

The four new forms carry %prec whiledo.

expr ::= "new" expr "len" expr "at" expr "do" expr
       | "new" expr "len" expr           "do" expr
       | "new" expr           "at" expr "do" expr
       | "new" expr                     "do" expr
       | "provide" expr

What whiledo does

whiledo sits third from the bottom of the ladder, so the body of a when, a loop, a new, a return, or a provide extends as far right as it can. This governs seventeen productions and is the single thing most likely to be got wrong by a precedence-driven parser written from the productions alone. All verified against the compiler's own parse trees:

Written Parses as
z := while a do b + c; z := (while a do (b + c))
z := return a + b; z := (return (a + b))
z := provide a + b; z := (provide (a + b))
z := new a do b + c; z := (new a do (b + c))
z := when a is b do c + d; z := (when a is b do (c + d))
z := if a then b else c + d; z := (if a then b else (c + d))

These are ordinary expressions, so they also appear in operand position: z := 1 + while a do b; parses.

Control transfer

expr ::= "return" expr
       | "return" "(" ")"
       | "return"
       | "break"

return () has its own production, because () is not an expression and so cannot be reached through return expr.

Function types and modifiers

expr ::= "function" "(" arglistornull ")" ":" expr
       | EXPORT expr                   (* export | import | threadLocal | constant *)

Directives

expr ::= "use" IDENTIFIER
       | HEADER STRINGCONST            (* header "" | declarations "" *)
       | STRINGOP STRINGCONST          (* Pointer "" | Type "" |*)
       | "leftOperator"   INTEGER STRINGCONST
       | "rightOperator"  INTEGER STRINGCONST
       | "prefixOperator" INTEGER STRINGCONST

The three operator-defining forms take effect in the parse action, so a new operator is usable only after the statement that registers it, and only in the same compilation.

Packages

expr ::= "package"   IDENTIFIER "(" package_body ")"
       | "signature" IDENTIFIER "(" package_body ")"

package_body ::= ε | expr | exprlist | exprlistsemi

A file named on the command line is implicitly wrapped in package <file stem> ( … ), the stem being its basename without directory or extension. The wrap is conditional on a lexer flag (grammar.y:52) which readfile sets and sigreadfile clears (readfile.c:281, 339) — so a .sig file is not wrapped, and that is precisely why it has to carry an explicit signature header of its own. No hand-written file in M2/Macaulay2/d uses either form.

Atoms

expr ::= IDENTIFIER | INTEGER | NUMBER | STRINGCONST

NUMBER covers both doubles and character constants.

Forms the grammar does not distinguish

Nearly every construct a D author thinks of as a distinct piece of syntax is built from the generic rules above. A parser that wants meaningful node types has to recognise these shapes itself; the grammar will not do it.

Written Parsed as
x := e := applied to an identifier and e
x : T := e := applied to x : T and e: binds tighter
f(x:T,y:U):W := e := applied to (f(x:T, y:U)) : W and e, where f(…) is the call rule and each x:T is a : application
f(x:T):W; the same, without the := — a bare expression statement
f ::= e ::= applied to an identifier and e
export x := e := applied to export x and e
X := {a:int, b:string} := applied to an identifier and a { … } object whose members are : applications
X := {+ a:int } the same with the {+ form
E := A or B or null := applied to an identifier and one n-ary or node
A := array(int) := applied to an identifier and a call
A := array(int,4) the same, with a two-element argument list
T := Type "struct foo" := applied to an identifier and the STRINGOP STRINGCONST form — the one type constructor with a production of its own
T(a,b,c) and T(x) calls, indistinguishable from a function call
Ccode(int,"…",x) a call
x.a and x.3 . applications, indistinguishable from each other
(x:T) + (y:U) : W := e := applied to ((x:T) + (y:U)) : W and e
- (x:T) : W := e := applied to (- (x:T)) : W and e
z := (a,b) a three-child := node, via the multi-argument infix rule — (a,b) is not a tuple, and := here has two right operands (verified)

That last row is worth dwelling on: (a,b) is not a value. On its own, (a,b); is a syntax error, and so is f((a,b));. It parses in z := (a,b); only because := is one of the twenty operators with a multi-argument form.

Recognising a definition

Since the grammar will not tell them apart, here is the shape to match. A definition is a top-level (or block-level) expression whose root is := or ::=, where the left operand is, after stripping an optional EXPORT modifier:

  • an identifier → a variable or type definition;
  • a : application whose left side is an identifier → a typed variable;
  • a call → a function definition, the arguments being : applications;
  • a : application whose left side is a call → a function definition with a declared return type;
  • a : application whose left side is an infix or prefix application of parenthesized : applications → an operator definition.

A left operand that is a call or a :-over-call with no := is a forward declaration rather than a definition.

The only constructs with dedicated syntax are the ones listed under Expressions: if, when, the loops, new/provide, return, break, function(…):T, the EXPORT modifiers, use, the header and STRINGOP and operator-defining directives, package, and signature.

Notes

A file holding exactly one expression must end in a semicolon. x := 1 is a syntax error; x := 1; and ; x := 1 both parse, and so does x := 1; y := 2 (verified). The reason is in the collapsed program form above — the final expression is optional, but only after the ( expr? ";" )+ has been satisfied at least once. An editor that helpfully strips a trailing semicolon can break a one-line file.

A block's value depends on its final semicolon. (a;b) uses exprlist and has the value of b; (a;b;) uses exprlistsemi and has no value. The two are different productions, so the trailing ; is syntax, not formatting.

() is not an expression, but four productions accept an empty parenthesis pair anyway: a call f(), return (), function () : T, and the empty package/signature body. Only a bare z := (); is a syntax error (verified).

The spelling that was written survives into the tree. Where several keywords share a token — while/until and the rest — the parse action stores the interned string as it appeared, so distinguishing them costs a parser nothing. until false do 3 yields a node whose text is until, not while (verified).

Error recovery happens at ;. Three of the list productions carry error (grammar.y:64–65, 77), and the two in program execute yyerrok; yyclearin;. A parser with error nodes should resynchronise the same way.

Whitespace around . does not matter. M2/Macaulay2/c/README:375–377 states that a. 1 and a . 1 select the first element of a while a .1 is a syntax error. That is no longer true: the rule that caused it is disabled with #if 0 at grammar.y:394–396, and all four spacings — a.1, a. 1, a .1, a . 1 — now parse identically (verified). Note this has nothing to do with .5, which is blocked by readfile.c:170 instead; see Words and numbers.

Four terminals appear in no production, and a grammar has no reason to mention any of them (grammar.output lists them under "Terminals unused in grammar"). They differ in kind: SELF and OP are live values that yylex intercepts and converts before returning, SPACE occurs only in a %left declaration, and SUPER occurs only in the %token line.

-nomacros changes the language. With that flag scc1 skips the prelude, so :=, =, ., +, and everything else in the numbered levels ceases to be an operator. Only the tokens from yyinit survive. Nothing in the build uses it, but it is why the operator table cannot be described as built in.

Clone this wiki locally