-
Notifications
You must be signed in to change notification settings - Fork 292
D language grammar
Note: This grammar was AI-generated by Claude Opus 5. 🤖
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.
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.
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:
-
IDENTIFIERINTEGERNUMBERSTRINGCONST - the thirty operator tokens
infix1…infix10,infix1r…infix10r,prefix1…prefix10 -
COLONORORANDANDORBRACEPLUSEXPORTHEADERSTRINGOP - one token per remaining keyword:
IFTHENELSEWHENISFORFOREACHWHILEDOATINBYFROMTONEWLENBREAKPROVIDERETURNFUNCTIONUSEPACKAGESIGNATUREOPERATORLEFTOPERATORRIGHTOPERATORPREFIX - 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.
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 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 ;.
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, 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 |
., 5 — not 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.
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 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.
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.
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.
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.
Transcribed from grammar.y:50–212. Bison reports no conflicts for this
grammar, so the precedence declarations above resolve every choice.
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.
arglistornull ::= ε | expr | arglist
arglist ::= expr "," expr
| expr "," arglistarglist 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.
expr has 104 of the grammar's 125 productions. (Bison reports 126, counting its
own augmented rule $accept: wrappedprogram $end.)
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).
expr ::= expr "(" arglistornull ")" (* call, cast, or construction *)
| "(" expr ")" (* grouping *)
| "(" exprlist ")" (* block, value is the last expression *)
| "(" exprlistsemi ")" (* block, value is void *)expr ::= "{" arglistornull "}" (* struct type or object *)
| "{+" arglistornull "}" (* tagged struct type or object *){+ is a single token. Both forms may be empty; {+} parses.
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.
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" expruntil … 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.
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" exprwhiledo 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.
expr ::= "return" expr
| "return" "(" ")"
| "return"
| "break"return () has its own production, because () is not an expression and so
cannot be reached through return expr.
expr ::= "function" "(" arglistornull ")" ":" expr
| EXPORT expr (* export | import | threadLocal | constant *)expr ::= "use" IDENTIFIER
| HEADER STRINGCONST (* header "…" | declarations "…" *)
| STRINGOP STRINGCONST (* Pointer "…" | Type "…" | … *)
| "leftOperator" INTEGER STRINGCONST
| "rightOperator" INTEGER STRINGCONST
| "prefixOperator" INTEGER STRINGCONSTThe 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.
expr ::= "package" IDENTIFIER "(" package_body ")"
| "signature" IDENTIFIER "(" package_body ")"
package_body ::= ε | expr | exprlist | exprlistsemiA 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.
expr ::= IDENTIFIER | INTEGER | NUMBER | STRINGCONSTNUMBER covers both doubles and character constants.
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.
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.
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.
Homepage | Projects | Packages | Documentation | Events | Google Group