Skip to main content

Cortex Syntax

Notation

In the grammar below, the following notation is used:

  • An arrow (→) marks grammar productions and can be read as "can consist of"
  • Syntactic categories are written in lowercase italic (newline) on both sides of a production rule.
  • Placeholders for recursive syntactic categories are indicated by ···.
  • Literal words and punctuation are indicated in bold (+) or as a Unicode codepoint (U+00A0) or as a Unicode codepoint range (U+2000-U+200A).
  • Alternatives are indicated by a vertical bar (|)
  • Optional elements are indicated in square brackets
  • Elements that can repeat 1 or more times are indicated by a trailing plus sign
  • Elements that can repeat 0 or more times are indicated by a trailing star sign
  • Elements that can repeat 0 or more times, separated by a another element are indicated with a trailing hash sign, followed by the separator. If no separator is provided, the comma (,) is implied.

Grammar overview

The productions below describe the source forms accepted by the current parser. The Unicode identifier rules are delegated to the MathJSON symbol profile, and the type following a : or return arrow is parsed using the Compute Engine type language. Detailed match patterns are documented under Control Flow.

quoted-text-item → U+0000-U+0009 U+000B-U+000C U+000E-U+0021 U+0023-U+2027 U+202A-U+D7FF | U+E000-U+10FFFF

linebreak → (U+000A [U+000D]) | U+000D | U+2028 | U+2029

unicode-charquoted-text-item | linebreak | U+0022

pattern-syntax → U+0021-U+002F | U+003A-U+0040 | U+005b-U+005E | U+0060 | U+007b-U+007e | U+00A1-U+00A7 | U+00A9 | U+00AB-U+00AC | U+00AE | U+00B0-U+00B1 | U+00B6 | U+00BB | U+00BF | U+00D7 | U+00F7 | U+2010-U+203E | U+2041-U+2053 | U+2190-U+2775 | U+2794-U+27EF | U+3001-U+3003 | U+3008-U+3020 | U+3030 | U+FD3E | U+FD3F | U+FE45 | U+FE46

inline-space → U+0009 | U+0020

pattern-whitespaceinline-space | U+000A | U+000B | U+000C | U+000D | U+0085 | U+200E | U+200F | U+2028 | U+2029

whitespacepattern-whitespace | U+0000 | U+00A0 | U+1680 | U+180E | U+2000-U+200A | U+202f | U+205f | U+3000

line-comment// (unicode-char)* linebreak)

block-comment/* (((unicode-char)* linebreak)) | block-comment) */

digit → U+0030-U+0039 | U+FF10-U+FF19

hex-digitdigit | U+0041-U+0046 | U+0061-U+0066 | U+FF21-FF26 | U+FF41-U+FF46

binary-digit → U+0030 | U+0031 | U+FF10 | U+FF11

numerical-constantNaN | Infinity | +Infinity | -Infinity

base-10-exponent → (e | E) [sign](digit)+

base-2-exponent → (p | P) [sign](digit)+

exponentbase-10-exponent | base-2-exponent

binary-number0b (binary-digit)+ [. (binary-digit)+ ][exponent]

hexadecimal-number0x (hex-digit)+ [. (hex-digit)+ ][base-2-exponent]

decimal-number → (digit)+ [. (digit)+ ][exponent]

The digit runs of a number literal may contain _ grouping separators (1_000, 0xFF_FF); an underscore is ignored and never begins or ends a run. A hexadecimal-number takes only a base-2-exponent because e and E are hexadecimal digits, so they cannot double as an exponent marker.

sign+ | -

signed-numbernumerical-constant | ([sign] (binary-number | hexadecimal-number | decimal-number))

symbolverbatim-symbol | inline-symbol

verbatim-symbol` symbol-start (symbol-continue)* `

The content of a verbatim-symbol is taken literally: no escape sequences are applied, and it must be a valid MathJSON symbol name. The form exists to write symbols whose name is a reserved word, e.g. `while`.

inline-symbolsymbol-start (symbol-continue)*

symbol-start and symbol-continue follow the MathJSON symbol profile. Reserved words are not accepted as inline-symbol; use the verbatim form.

escape-expression\( expression )

single-line-string" (escape-sequence | escape-expression | quoted-text-item)* "

multiline-string""" multiline-string-line """

extended-string → (#)+ " (unicode-char)* " (#)+

The number of trailing # must match the number of leading # that opened the literal (#"…"#, ##"…"##, …). No escape sequences are applied inside an extended string, so it can hold " and \ literally.

stringsingle-line-string | multiline-string | extended-string

String escapes, interpolation, multiline indentation and continuation are specified in Literals.

parenthesized( expression )

list[ [(expression)#,] ]

set{ [(expression)#,] }

dictionary{ [(key-value-pair)#,] } | {->}

key-value-pairexpression -> expression

block{ [(statement)#statement-separator] }

do-blockdo block

latex-island$ (unicode-char | \$)* $

pragma#line | #column | #url | #filename | #date | #time | pragma-call

pragma-call → (#env | #navigator | #warning | #error) ( [(expression)#,] )

if-expressionif expression block [else (block | if-expression)]

match-expressionmatch expression { match-case+ }

primarysigned-number | symbol | string | pragma | latex-island | parenthesized | list | set | dictionary | do-block | if-expression | match-expression

call-clause( [(expression)#,] )

index-clause[ (expression)#, ]

postfix-expressionprimary (call-clause | index-clause | !)*

expressionprimary | prefix-expression | infix-expression | postfix-expression

prefix-expression → (- | !) expression

infix-expressionexpression operator expression

parametersymbol [: type]

parameters( [(parameter)#,] )

declaration → (let | const) symbol [: type] [= expression] | symbol : type [= expression]

function-definitionsymbol parameters [-> type] = expression | function symbol parameters [-> type] block

while-statementwhile expression block

for-statementfor symbol in expression block

statementdeclaration | function-definition | while-statement | for-statement | expression

statement-separator; | linebreak

shebang#! (unicode-char)* (linebreak | _eof)

cortex → ([shebang] (statement)#statement-separator [eof])

The Pratt (precedence-climbing) grammar for _infix-expression_, _prefix-expression_, and _postfix-expression_ — the operator set, its precedence, and its associativity — is documented as a table in Operators rather than spelled out production by production; the whitespace rule described there (an infix operator has whitespace on both sides or neither; a prefix operator has no whitespace after it, and a postfix operator none before it) is part of this grammar, not a separate lexical concern.

Statements and sequencing

A program is a sequence of statements separated by a linebreak or a ;. Two expressions on the same line with no separator between them is not a silent sequence — it is a diagnostic:

1 2
Error: unexpected-symbol "2"

A well-formed multi-statement program wraps its statements in ["Block", …]; a program consisting of a single statement is returned unwrapped (no Block wrapper):

a
2
["Block", "a", 2]

; is interchangeable with a linebreak as a separator:

a; 2
["Block", "a", 2]

Primary expressions

A primary is the leaf of the expression grammar — the thing an operator or a call/index applies to. The primary forms are:

  • a number: 2, 3.14, 0x1F, 0b101
  • a symbol: x, Add
  • a verbatim symbol: `while`
  • a string: "hello"
  • a pragma: #env("HOME")
  • a parenthesized expression: (2 + 3)
  • a list: [1, 2, 3]
  • a set: {1, 2, 3}
  • a dictionary: {one -> 1, two -> 2}
  • a do { … } block expression: do { let t = 3; t + 1 }
  • a $…$ LaTeX island: $\frac{1}{2}$ — see LaTeX Islands
  • a function call: f(x, y)
  • an index expression: xs[i]

Calls and indexing

A call is a symbol (or another primary) immediately followed — with no whitespace — by a parenthesized, comma-separated argument list:

f(x, y) // ["f", "x", "y"]
f() // ["f"]

If the callee is not a bare symbol (for example, a parenthesized expression or the result of another call), the call lowers to Apply:

(getF())(x) // ["Apply", ["getF"], "x"]
(a + b)(2+1) // ["Apply", ["Add", "a", "b"], ["Add", 2, 1]]

Indexing is a primary immediately followed — with no whitespace — by a bracketed index expression, and lowers to At. Indexing is 1-based, matching the engine convention (xs[1] is the first element):

xs[i] // ["At", "xs", "i"]
f(x)[0] // ["At", ["f", "x"], 0]

In both cases the ( or [ must directly abut the callee/indexed expression: whitespace before it means the parenthesized/bracketed form is a separate primary (a parenthesized expression or a list literal), not a call/index — the same whitespace-sensitivity that governs operators.

Collections, tuples, and dictionaries

  • List: [a, b]["List", "a", "b"]; []["List"].
  • Set: {a, b}["Set", "a", "b"]; {}["Set"].
  • Tuple: (a, b)["Tuple", "a", "b"]; a single parenthesized element, (a), is just the parenthesized expression a, not a one-element tuple; () is a diagnostic (expression-expected) — there is no empty tuple — except immediately before a mapsto arrow, where () |-> expr is a zero-parameter lambda (["Function", body]).
  • Dictionary: {k -> v}["Dictionary", ["KeyValuePair", {str: "k"}, "v"]]; an unquoted key becomes a string key. The empty dictionary is spelled {->} (not {}, which is the empty set) and lowers to ["Dictionary"].

{ … } is disambiguated by looking at the first element once it has been parsed: if it is followed by a top-level ->, the whole { … } is a dictionary and every subsequent element must also be a key -> value pair; otherwise { … } is a set.

A { in expression position is therefore always a collection literal (set or dictionary); to open a statement block in expression position, prefix it with do. do { … } is a block expression (the engine's Block) — a statement sequence whose value is its last statement — while a bare { … } stays a set/dictionary. See Blocks.

{ one -> 1, two -> 2 }
["Dictionary",
["KeyValuePair", {"str": "one"}, 1],
["KeyValuePair", {"str": "two"}, 2]]

Trailing commas are allowed in every collection form (lists, sets, tuples, dictionaries, and call/index argument lists) — friendly to notebook editing and diffs:

[1, 2, 3,] // same as [1, 2, 3]

A bare, top-level comma-separated sequence with no enclosing delimiter (for example 1, 2, 3 on its own) is not a Sequence literal — it is a diagnostic. Sequence is available only as an explicit call: Sequence(1, 2, 3)["Sequence", 1, 2, 3].

Round-trip and serialization normalizations

serializeCortex and parseCortex are inverses over the MathJSON the grammar can produce, up to a small set of documented normalizations. parseCortex(serializeCortex(e)) is structurally equal to e after applying:

  • Number formatting2, {num: "2"} and "2" are the same number; the serializer emits a single canonical spelling (with _ digit grouping), which re-parses to a {num} object.
  • Negate of a literal["Negate", 3] serializes to -3 and ["Negate", -1] to 1; both re-parse as a signed num literal rather than a Negate node (the sign is folded into the number).
  • RationalDivide["Rational", 1, 2] serializes to 1 / 2. There is no rational literal in the grammar, so it re-parses as ["Divide", 1, 2].
  • Invisible multiply — a binary ["Multiply", {num}, {sym}] serializes to the juxtaposed form 2x (only when the two abut and re-lex unambiguously as a number followed by a symbol). All other products — n-ary, number×group (2(x+1)), group×group — stay explicit *, because (x+y)(3+4) would otherwise re-parse as Apply, not Multiply.
  • Associativity — the left-associative operators (Add/Subtract/Multiply/Divide/And/Or) re-parse into left-nested binary trees; a flat n-ary form and its left-nested spelling are the same expression.

Comments are not preserved by a round-trip — see Comments.

If and Match have dedicated expression spellings. Other MathJSON heads that do not have a special surface form serialize as ordinary function calls.

Relationship to the loose math parser

Cortex is a programming-language syntax. The Compute Engine also ships a loose math parser (ce.parse(src, { canonical: false })) that reads LaTeX/ASCII-math notation. The two share a few surface forms but are not the same language, and they overlap only partially:

SourceCortex parseCortexLoose ce.parse (non-canonical)Agree?
[1, 2, 3]["List", 1, 2, 3]["List", 1, 2, 3]✅ same
x^2["Power", "x", 2]["Power", "x", 2]✅ same
2**3["Power", 2, 3]math-parser artifact (** is not an operator)❌ diverge
a |> b["Pipe", "a", "b"]["Pipe", "a", "b"]✅ same
f(x, y)["f", "x", "y"] (call)["InvisibleOperator", "f", ["Delimiter", …]]❌ diverge
sin"sin" (a symbol)["InvisibleOperator", "s", "i", "n"]❌ diverge
2x["Multiply", 2, "x"]["InvisibleOperator", 2, "x"]❌ diverge

The remaining divergences are intentional: in Cortex a juxtaposed name is a single identifier (sin is one symbol, not s·i·n), f(x, y) is a function call, and ** is exponentiation. The two parsers do agree that |> produces Pipe. Do not rely on them agreeing except on the rows marked same.