Language Reference
Language Reference
The complete reference for the Grid language: model structure,
references, literals, the type system, expressions, operators, and
control flow. This is the long-form companion to the feature checklist
in features.md.
Read quickstart.md first if you've never written a
Grid model. Use this page when you need the precise rule.
Style note. Where Grid accepts more than one spelling,
style-guide.mdpicks the canonical one.
Contents
- Model Files And Structure
- Statements
- References And Ranges
- Literals And Values
- Type System
- Expressions
- Operator Precedence
- Control Flow
- Functions And Calls
- Lambdas And Higher-Order Forms
- Arrays, Spilling, And Comprehensions
- Pipelines And Infix Sugar
- Reserved Words
- Multi-line And Layout Rules
- See Also
1. Model Files And Structure
A Grid program is a .grid source file. It is a sequence of
statements — mostly assignments — optionally wrapped in model
directives.
MODEL "My Model"
DESCRIPTION "What this model does."
VERSION "1.0.0"
AUTHOR "you@example.com"
TAGS "demo", "v1"
# Statements go here
A1 is currency = 100000
B1 is currency = A1 * 0.93
END MODEL1.1 Directives
Directives are metadata headers. All are optional — a file with bare statements parses fine — but shared models include them.
| Directive | Meaning |
|---|---|
MODEL "..." |
Human-readable model title |
DESCRIPTION "..." |
One-sentence description |
VERSION "..." |
Semantic version string |
AUTHOR "..." |
Author identity |
TAGS "a", "b" |
Comma-separated tags |
RUNTIME "..." |
Legacy. Parsed for back-compat; omit it in new models |
END MODEL |
Closes the model body |
Do not choose an engine in model source: the RUNTIME directive still parses
for older files, but new models should omit it.
1.2 Statement separation
Statements are separated by newlines or semicolons:
A1 = 1
B1 = A1 + 1
C1 = 2; D1 = 3 # semicolons separate statements on one lineNewlines fold (continue the statement) inside open brackets and a few block forms; see §14.
1.3 Comments
# line comment to end of line
A1 = 1 # trailing comment
/* block comment,
may span multiple lines */
B1 = 22. Statements
Almost every statement is an assignment: a target, an operator, and
an expression. Assignments are covered in depth in
assignments.md; this is the summary.
The target denotes a binding through an address. Coordinate targets denote
cells; named targets can define calculations or, for a direct reference, alias
an existing binding. See concepts.md for the vocabulary.
2.1 Assignment shapes
<target> = <expression> # eager
<target> ~= <expression> # lazy
<target> ?= <expression> # conditional eager (skip on error)
<target> IS <type-tag> = <expression> # typed
<type-tag> <target> = <expression> # typed, prefix form
[a, b, ...rest] = <expression> # destructuring
<range> = <expression> # broadcast to a finite rectangle| Operator | Semantics |
|---|---|
= |
Eager — recompute whenever an input changes |
~= |
Lazy — compute only when read; cache until invalidated |
?= |
Conditional eager — if the RHS errors, keep the previous value |
+= -= *= /= |
Rule-action compound — read-modify-write a single-cell or named target using its committed value; ordinary top-level arithmetic use is rejected |
On a table-typed target, +=/^=/*=/-= mean record
mutations (insert/upsert/update/delete), not arithmetic. See
assignments.md.
2.2 Decorators
input and output mark a binding's external write/read surface;
default marks a formula as an overridable base value:
input price = 0
output total = price * qty
default estimate = model_value # a rule/input may override; RESET restores itinput/output are advisory by default and strictly enforced in
protected mode. default opts a formula into being overridden by a rule
(see rule actions); a
rule that overrides a non-default formula raises GRID_MIXED_OWNERSHIP.
See assignments.md.
2.3 Schedule modifiers and rule blocks
A single assignment can carry a schedule modifier (ONCE, EVERY,
AT), and WHEN/EVERY/AT blocks group rule actions. These are
covered in rules-and-schedules.md.
2.4 SOLVE statements
SOLVE is a non-assignment statement that declares a goal-seek or bounded
optimization over workbook cells:
SOLVE Z9 = A2 IN [0, 1] GOAL A3 = 200000 # drive A3 to a target
SOLVE B9 = A1 IN [0, 5] MINIMIZE A2 # minimize an objective cell
SOLVE B9 = A1 IN [0, 5] MAXIMIZE A2 # maximize an objective cellThe result cell (Z9) receives the solved value; the variable cell (A2)
is never mutated, so the sheet stays consistent and auditable. Solves are
reactive — editing an upstream input re-solves. Full syntax, limits, and the
solverWrites report are in
assignments.md.
2.5 Definitions
A parenthesised parameter list on the left of = declares a reusable,
named, parameterised formula. It is a compile-time macro: it emits no cell
of its own, and each call site is expanded in place into a
LET(param, arg, …, body) binding. Define once, call from any cell:
taxDue(income, rate) = MAX(income, 0) * rate
hypot(a, b) = SQRT(a * a + b * b)
B4 = taxDue(A1, 0.21)
C7 = taxDue(A2, 0.15)
D9 = hypot(3, 4) # 5The parameter list is what designates a definition — no keyword is
required. A leading DEFINE is accepted for those who prefer it explicit
(DEFINE hypot(a, b) = …) but is optional; the shorter DEF is also
accepted as a synonym. Contrast a definition with a plain named binding, which
has no parameter list: pi = 3.14159.
Names are case-insensitive like the builtin catalog, and a definition
shadows a same-named builtin at its call sites. A definition may call an
earlier definition (they compose); a call before the definition, a
self-reference, and mutual recursion are not expanded and resolve as
ordinary — unresolved — calls. Calling with the wrong number of arguments
is a compile error, and partial application (taxDue(A1, _)) is not
supported.
The compiler infers one generalized signature per definition and instantiates
fresh type variables at each call. Field access creates open record-row
requirements (row.amount accepts additional fields), while MAP, REDUCE,
and SCAN create collection protocol constraints backed by the runtime's
shared capability matrix. Structural violations are compile errors; scalar
mismatches follow the authored coercion mode, remaining warnings in loose
source and becoming errors under strict. FUNCTION(name) exposes the
inferred type and effect row. See
named-functions.md for the complete contract.
2.6 Function reflection and calculus
Named definitions are inspectable at compile time. These forms fold into ordinary constant Grid values; they do not introduce mutable runtime function objects:
square(x) = x * x
A1 = ARGS(square) # ["x"]
A2 = BODY(square) # {kind: "binary", op: "*", left: ..., right: ...}
A3 = FUNCTION(square) # {schema: "grid.function.v1", name: "SQUARE",
# arity: 1, args: ["x"], body: ...}BODY returns a normalized expression tree. Nodes use a small open schema:
literal nodes carry kind and value; parameters and references carry
kind and name; unary and binary nodes add op; calls add name and
args. Cell and range nodes retain canonical addresses, including qualifiers;
relative and spatial references retain their offsets or directions. Constants,
arrays, objects, typed literals, and neighborhoods likewise retain their
structure. The reflection match is exhaustive over the expression tree, so a
new expression form must define its public representation before the compiler
will build. As with ordinary Grid object literals, object values use the cell
surface's key/value-row representation. Imported names work too:
FUNCTION(math.square).
The same definition can feed calculus without manually rewriting it as a lambda:
position(t) = t ^ 3 - 2 * t
velocity(t) = D(position, t)
acceleration(t) = D(velocity, t)
rate(t) = EXP(-t)
accumulated(t) = INTEGRAL(rate, 0, t)D(fn, x[, step]) is the concise spelling of
DERIVATIVE(fn, x[, step]). The compiler turns the named definition into a
lambda, then the runtime uses exact automatic differentiation where the
expression is supported and central differences otherwise. This means an
opaque or unfamiliar function remains differentiable as long as it can be
evaluated numerically.
For a multi-parameter definition, name the parameter being varied inside a wrapper definition; the remaining parameters are captured normally:
demand(price, income) = price ^ 2 + 3 * income
priceSensitivity(price, income) = D(demand, price)INTEGRAL(fn, start, end[, control]) similarly lowers to the existing
adaptive numerical INTEGRATE implementation and currently accepts a
one-parameter definition. These forms establish a stable reflected-AST
foundation for later symbolic transforms without requiring symbolic algebra
for numerical calculus to be useful today.
The same one-parameter definition can be realized as graph-ready values:
wave(x) = SIN(x)
input viewMin = -PI
input viewMax = PI
input viewPixels = 800
curve = SAMPLE(wave, viewMin, viewMax, viewPixels)
selected = SAMPLE(wave, [-PI, 0, PI])
adaptive = SAMPLE(wave, viewMin, viewMax, 0.001)
sampleInfo = SAMPLE_INFO(wave, viewMin, viewMax, 0.001)SAMPLE(fn, coordinates) evaluates explicit coordinates.
SAMPLE(fn, start, end) adaptively samples the interval; a fractional
control between 0 and 1 sets its error tolerance, while an integer control
of 2–65,536 requests that many evenly spaced points. The result is an array
whose first row is ["x", "y"], followed by the realized points, so it binds
directly to a Charts surface. Bounds and resolution are ordinary reactive
dependencies: changing them, including from a chart viewport, recomputes the
sample. The existing Frame ETL SAMPLE(frame, count) remains unchanged;
the named-function form is selected only when its first argument names a
definition.
SAMPLE_INFO accepts the same arguments and returns metadata with schema
grid.sample.v1: sampling mode, actual and requested point counts, tolerance,
detected discontinuities, and flags indicating whether the point or adaptive
depth safety limit was reached. Sampling inserts undefined/error-valued break
points at detected discontinuities so line charts do not connect across
asymptotes.
For a multi-parameter family, name the varying parameter and bind every fixed parameter in an object:
demand(price, income) = 120 - 2 * price + 0.01 * income
input CurrentIncome = 80000
demandCurve = SAMPLE(
demand,
price,
{ income: CurrentIncome },
0,
100,
800
)Those bindings are ordinary reactive expressions, so changing
CurrentIncome recomputes the family curve.
2.7 Choice sets — CHOICE / CHOICES
CHOICE declares a one-of enum — a closed set of symbol literals. It
drives MATCH checking: a match whose arms are
symbols from a declared set warns when an arm names a symbol outside the
set, or when the match is non-exhaustive (a member is left uncovered with
no _ catch-all). Matches unrelated to any declared set are never flagged.
CHOICE status = :green | :amber | :red
A1 = MATCH(health, :green -> "ok", :amber -> "warn", :red -> "page") # exhaustive
A2 = MATCH(health, :green -> "ok", _ -> "other") # catch-all, ok
A3 = MATCH(health, :green -> 1, :red -> 3) # warns: missing :amberCHOICES declares a many-of flag set. Each member is implicitly
assigned a distinct power-of-two bit (member i → 1 << i), so a single
numeric value can carry several flags at once. A member symbol lowers to
its bit value in expressions — combine flags with + (or BITOR) and test
membership with HAS:
CHOICES perms = :read | :write | :exec # :read=1, :write=2, :exec=4
E1 = :read + :write # 3 — one value, two flags
E2 = E1 HAS :read # TRUE — has read
E3 = E1 HAS :exec # FALSE — no exec
E4 = E1 HAS :read + :write # TRUE — has bothx HAS flags tests that every requested bit is set (it lowers to
BITAND(x, flags) = flags). Over a value that isn't a flag set, HAS
keeps its ordinary collection-membership meaning. A symbol should belong to
only one declared set; declaring it in two warns (in a flag set it is a
number, elsewhere a symbol).
CHOICE also has a payload-bearing algebraic form. Constructors use qualified
names and typed fields:
CHOICE Delivery = Pending | Delivered(at: date) | Failed(message: string)
value = Delivery.Delivered(dt"2026-07-27")
label = MATCH(
value,
Delivery.Pending() -> "pending",
Delivery.Delivered(at) -> TEXT(at),
Delivery.Failed(message) -> message
)Unlike symbol choices, constructor matches are fail-closed: every constructor
needs an unguarded arm unless _ supplies a catch-all. Fields may refer to the
enclosing type for finite recursive values. See
algebraic-data-types.md for contracts, matching,
the depth bound, and provenance.
2.8 Module imports — USE
USE "<path>" AS <alias> imports another source module's definitions under a
namespace. Reusable modules conventionally use the .gs extension:
USE "shared/finance.gs" AS fin
B1 = fin.net_margin(revenue, cost) # calls the imported definition.gs is plain Grid source with a definitions-only contract. It may contain
named functions, eager named constants, choices, units, directives, and
transitive imports. Cells, ranges, rules, solves, mutations, scheduled or lazy
bindings, and state/input/output bindings are rejected. Imported definitions
inline at the call site exactly like local ones, and their arity is checked.
Named constants are imported through the same namespace. A duplicate alias, a
missing module, or importing when no module resolver is configured each raise
an error.
USE remains extension-agnostic and continues to accept legacy .grid
module paths. .grid denotes an executable model source file; .gs makes the
library-only intent machine-checkable.
USE "path" EXPOSING (name, other_name) imports selected definitions into
the local namespace. Imported modules may themselves use modules, and cycles
are rejected with the full import chain. The compiler ships
shared/politics.gs and shared/socsci.gs; other module sources are
provided by the embedding compiler's resolver.
3. References And Ranges
A reference names a cell or a region.
3.1 Cell references
| Form | Example | Notes |
|---|---|---|
| A1 | A1, AA100 |
Column letters + row number |
| Absolute | $A$1, A$1, $A1 |
$ pins column/row for fill |
| R1C1 | R1C1 |
Row/column numbers |
| Relative R1C1 | R[1]C[-2] |
Brackets denote an offset from the current cell |
| Named | Revenue, tax_rate |
A semantic address for a binding or named calculation |
| Namespace-qualified | Sheet1!A1, 'Q4 Revenue'!B2, Dataset_1!config |
! is a namespace specifier, not an accessor |
The ! join is a flat symbol name. The left side is a sheet or
workbook-surface namespace; the right side is a cell address or named
slot. Quote the namespace with single quotes when it contains spaces.
A plain direct-reference named assignment creates an address alias:
Revenue = A1Revenue and A1 then address the same reactive binding. A named expression
such as Tax = Revenue * rate creates a distinct calculation. See
assignments.md for the exact
boundary and observable behavior.
3.2 Ranges
| Form | Example | Allowed as a target? |
|---|---|---|
| Finite rectangle | A1:B10, R1C1:R3C3 |
Yes (must be finite) |
| Full column | A:A, A:C |
Expression only |
| Full row | 1:1, 1:3 |
Expression only |
Range targets must be finite rectangles. Full-column/row ranges are
valid in expression positions (e.g. SUM(A:A)) but never as assignment
targets.
3.3 Spill and structured references
A1# # the whole spilled rectangle anchored at A1
Sales[Revenue] # structured (table column) reference
data@"amount" # header selection (single key)Removed. A prefix
@on a reference (@A:A, "implicit intersection") was removed — it never reduced a range to the current row/column value;UnaryOp::Atwas pure identity, so the syntax was a silent no-op. It now rejects withGRID_IMPLICIT_INTERSECTION_REMOVED. Prefix@is now reserved for temporal coordinates (§3.5). Postfix@remains temporal after a reference and selects headers or fields after a structured value. Reference a cell directly instead (A5, not@A:A).
3.4 Spatial and neighborhood references
ABOVE BELOW LEFT RIGHT # adjacent cell
ABOVE(3) BELOW(2) LEFT(4) RIGHT(1) # offset by N
NEIGHBORS NEIGHBORS(2) # surrounding ring(s)Bare LEFT / RIGHT (no parentheses) are spatial references.
LEFT(text, n) / RIGHT(text, n) (with arguments) are the text
functions. The grammar disambiguates by the argument list.
3.5 Temporal references
A1@-1 # previous revision
A1@dt"2026-04-10T15:30:00Z" # value as of a timestamp
A1@2026-04-10 # same timestamp coordinate, authoring shorthand
A1@today # start of today
A1@monday # most recent Monday, including today
A1@january # most recent January 1, including this month
A1@1776 # January 1, 1776 CE
A1@5000BCE # year-start coordinate in the BCE eraA1@-N resolves the referenced cell at N revisions before the current value.
A1@dt"..." and A1@<temporal-coordinate> resolve the latest revision at or
before the supplied instant. Bare @ coordinates use past/current anchoring for
calendar words: @monday is the most recent Monday and @january is the most
recent January 1.
Current-cell lineage is exposed by the resident WHY(cell) inspection request,
which returns the dependency trace and latest moved inputs. From the CLI, use
gridctl cell why <model-id> <symbol>. WHY is not a workbook formula because
provenance describes the evaluated model rather than becoming model state.
WHY reports semantic lineage and the current effective binding state. It does
not make cache residency, evaluation order, parallelization, or the selected
runtime strategy part of the language contract. EXPLAIN may expose a current
physical plan for inspection; equivalent evaluations may use a different plan.
3.6 Iteration context
Inside broadcast and rule contexts, these resolve to per-cell position:
ROW_INDEX COL_INDEX IS_FIRST IS_LAST CELL_COUNT4. Literals And Values
Every value has one of nine kinds: blank, number, string,
boolean, date, array, object, complex, error. Literals are
how you write them.
4.1 Numbers
42 3.14 .5 1.2e9 1.2e-9
1_000_000 0.000_001 # numeric separators
0xFF 0b1010 0o755 # hex / binary / octal
5i 3.14e-2i 0xFFi # imaginary (complex)4.2 Unit and suffix literals
Numeric literals may carry a unit suffix; the value is the plain number, the suffix attaches a type tag:
| Suffix | Example | Meaning |
|---|---|---|
pct |
21pct |
percentage → 0.21 |
bp, bps |
25bps |
basis points → 0.0025 |
deg, rad |
45deg |
angle |
ms s min h d w mo y |
5min, 2d |
duration |
KB MB GB TB |
512MB |
data size |
| 3-letter code | 100USD |
typed currency (currency:USD) |
votes seats persons |
100votes |
distinct civic count dimensions |
% (postfix) |
50% |
0.5 |
Explicit type annotations also accept unit expressions such as unit:m/s,
unit:kg*m/s^2, unit:m^1/2, unit:m^0.5, absolute temperatures
(unit:C, unit:F, unit:K), temperature deltas (unit:delta_C,
unit:delta_F, unit:delta_K), and ISO currency codes. These unit tags remain
numeric at the value-kind layer while the dimensional MIR pass gives them
load-bearing unit semantics.
votes, seats, and persons are deliberately incompatible dimensions, so
strict dimensional checking rejects formulas such as 100votes + 5seats.
Models can declare scale-only custom units before formulas:
unit smoot = 1.7018 m
unit smoot_per_second = smoot/s
R1C1 = 2 INTO unit:smootCustom units layer over the built-in catalog but cannot shadow built-ins or use affine bases such as Celsius/Fahrenheit. Use explicit delta temperature units for temperature differences.
4.3 Strings
"double quoted"
"""triple quoted, may span
lines and contain "quotes" """
raw"\d+" # no escape processing
raw"""...multi..."""
`total={A1}` # backtick interpolation, optional :format
`amount: {A1:"$#,##0.00"}`Strings use double quotes. Single quotes are reserved for quoted
namespace specifiers ('Q4 Revenue'!A1), not strings. Interpolation
supports an optional format spec after : inside {...}.
4.4 Booleans and blank
TRUE FALSE # case-insensitive
BLANK # absence of a value — not the same as ""BLANK means "no value". "" is an empty string. ISBLANK("") is
FALSE. See errors.md.
4.5 Symbols
A :identifier is a symbol literal — a lightweight enum/state label that
carries the symbol type tag:
status = :draft
color = MATCH(state, "ok" -> :green, _ -> :red)4.6 Dates, datetimes, durations, cron
date"2026-04-10" d"2026-04-10"
datetime"2026-04-10T15:30:00Z" dt"2026-04-10T15:30:00Z"
duration"P1D" dur"PT15M"
cron"0 9 * * 1-5"For human-authored temporal coordinates, Grid also accepts quote-free @
forms:
@today @yesterday @tomorrow
@monday @tuesday @wednesday
@january @february @march
@1776 @2026 @107AD @5000BCE
@2026-04-10 @2026-04-10T15:30:00Z @12/31/2025Standalone @YYYY-MM-DD and @M/D/YYYY act as date values. Relative words,
weekdays, months, and era-qualified years act as temporal coordinates resolved
against the evaluation clock and calendar registry.
4.7 Patterns and wildcards
/^INV-\d+$/i # regex literal with flags
glob"*.csv" # glob pattern
wild"???-*.csv" # wildcard pattern4.8 Domain-typed string literals
url"https://example.com/path?q=1"
mol"H2O" molecule"C8H10N4O2"
dna"ATCG"
color"#ff8800" colour"hsl(30, 100%, 50%)"
note"C#5"
bin"DEADBEEF" bytes"00112233" b64"SGVsbG8="4.9 Markup typed string literals
html"<p>Hello</p>"
xhtml"<p>Hello</p>"
svg"""<svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="4"/></svg>"""
jsx"""render(<App />);"""html, xhtml, svg, and jsx are typed strings: Grid preserves the markup/code
payload verbatim and attaches the corresponding type tag. Use xml when
you want a structured parsed object value; use these markup tags when
round-tripping or rendering exact source text matters.
Any typed-literal tag also accepts an HTML-style payload form when that is clearer for embedded content:
<dna>ATCG</dna>
<json>{"a": 1, "b": [2, 3]}</json>
<html><p>Hello</p></html>
<jsx>render(<App />);</jsx>
R1C1 = R1C2@<dt>2026-04-10T15:30:00Z</dt>The opening tag must be tight (<dna>, not < dna >), and the closing
tag must match the opener spelling case-insensitively (<mol>...</mol>,
not <mol>...</molecule>). The payload is verbatim: backslashes are not
escape sequences, newlines are preserved, and nested non-matching tags
are just content. If the payload itself can contain the exact closing
sequence (</tag>), use the quoted or triple-quoted form instead.
For example, <svg><circle/></svg> uses the outer <svg>...</svg> as
the typed-literal delimiter and stores <circle/>; use
svg"""<svg><circle/></svg>""" to preserve a complete SVG document with
its root element.
4.10 Arrays
[1, 2, 3] # row vector
[1; 2; 3] # column vector (semicolons = new row)
[1, 2; 3, 4] # 2x2 matrix
[0, ...A1:A3, 4] # spread (desugars to HSTACK)4.11 Objects
{name: "Ada", level: 9}
{"first name": "Ada"} # quoted keys for spaces4.12 Structured / embedded values
json"""{"a": 1, "b": [2, 3]}"""
yaml"""
a: 1
b: [2, 3]
"""
toml"""title = "demo""""
csv"""a,b\n1,2"""
tsv"""a\tb\n1\t2"""
xml"""<root/>"""4.13 Error literals
Nine error codes are source-writable with the Excel-compatible spelling:
#N/A #DIV/0! #VALUE! #REF!
#NAME? #NULL! #NUM! #CALC! #SPILL!A1 = #N/A
B1 = IF(missing, #N/A, value)Two additional codes are Grid-generated only — the parser rejects them as literals because they describe structural problems source code should not claim to emit:
| Code | Generated when |
|---|---|
#TYPE! |
A type-tag check fails on an assignment |
#CIRCULAR_REF! |
The dependency graph has a cycle and iterative calc is off |
Errors are first-class typed values that propagate through formulas.
The full catalog, propagation rules, and recovery forms are in
errors.md.
5. Type System
Grid values carry two layers of type information:
- A kind — the structural type Grid enforces (
number,string,boolean,date,array,object,complex,error,blank). - A type tag — optional semantic meaning layered on a kind
(
currency,percentage,fx_rate, …).
5.1 Kinds
| Kind | Examples |
|---|---|
blank |
BLANK |
number |
42, 3.14, 0xFF, 21pct, 25bps |
string |
"hello", :label, html"<p>Hi</p>", svg"""<svg/>""", <jsx>render(<App />);</jsx> |
boolean |
TRUE, FALSE |
date |
d"2026-04-10", dt"2026-04-10T15:30:00Z" |
array |
[1, 2, 3], [1; 2; 3], [1, 2; 3, 4] |
object |
{a: 1}, json"""...""", <json>...</json>, regex literals |
complex |
5i, 3 + 4i |
error |
#N/A, #DIV/0!, … |
5.2 Type Tags
A type tag attaches semantic meaning without changing the kind. Apply
one at assignment with IS (or the prefix form), or inside an
expression with INTO:
A1 IS currency = 100000
currency A1 = 100000 # equivalent prefix form
B1 = A1 * fx_rate INTO currency # tag a value mid-expressionas is not a type annotation (A1 as currency is an error). It is
reserved for SQL-style aliasing in USING and RENAME.
Tags form a hierarchy. Each tag refines a more general type, all the
way up to a representation root — USD is a currency is a number;
json is a object. A tag is legal wherever any of its ancestors is,
and a leaf implies its ancestors: writing is USD needs no separate
currency.
number ── representations: number(f64) · bigint · decimal
├─ currency ───────► USD · EUR · JPY … (currency:<code>)
├─ unit ───────────► m · m/s · kg … (unit:<expression>)
├─ percentage · bps · rate · score · angle · data_size · fx_rate
string ── symbol · duration · cron · html · xhtml · svg · jsx
date ── datetime
array ── csv · tsv
object ── json · yaml · toml · xml · regex · url · molecule · dna · color · note · bytes
complex
error ── error:<CODE>Concrete instances take the parent-label qualifier: money is
currency:USD (or bare USD), physical quantities are unit:m/s. A
currency is never spelled unit:USD — that is an error pointing you at
currency:USD.
The bigint and decimal tags are exact: bigint carries an
arbitrary-precision integer and decimal a fixed-precision decimal, and both
stay exact through arithmetic and the number-theory functions instead of
rounding like ordinary floating-point numbers. Create them with the
bigint"..." / decimal"..." literals, the BIGINT(...) constructor, or by
tagging a value (x INTO bigint).
5.3 Tag validation and stickiness
Under strict, a value that cannot inhabit
its tag's representation root is a #TYPE! — the coercion axis of the
strictness umbrella governs this, so strict except coercion turns it off.
loose (the default) applies the tag as an overlay without checking.
strict
A1 is currency = "hello" # #TYPE! — a word-string is not a number
A1 is currency = "100" # OK — the numeric string coerces to a number
A1 is currency = 100 # OK — canonicalThe check is by representation root, so it also refuses an object, array, or
date tagged as a number. Tagging a non-currency value is only flagged under
strict; loose keeps today's permissive overlay.
5.4 Strictness — strict / loose
A model chooses how strict Grid is with one keyword at the top:
strict # strict everywhere
loose # spreadsheet-friendly (the default; omit it for the same effect)Strictness has two axes — dimensions (unit/currency safety) and
coercion (silent value conversions like blank → 0, "5" + 3). The
umbrella keyword sets both. When you need to differ on one, add a single
except clause:
strict except coercion # units strict, but keep spreadsheet coercion
loose except dimensions # loose values, but still catch unit mistakesOmitting the header entirely is loose (today's behavior).
Under strict, a lossy value coercion in an arithmetic or comparison
expression is a TYPE_ERROR rather than a silent conversion:
strict
A2 = A1 + 1 # errors if A1 is blank or a string like "5"
A2 = 40 + 2 # fine — no coercion, evaluates to 42loose (the default) keeps the spreadsheet behavior: blank → 0 and
"5" + 3 → 8. Genuine numbers are never affected either way — strictness
only rejects the silent conversion, not the arithmetic.
Rollout note.
strictgoverns the dimensional axis in full and the coercion axis for scalar arithmetic and comparison operands (theblank → 0/"5" + 3conversions above). Constant folding and affine-chain compaction operate only on numeric operands, which carry no coercion, so no real conversion slips past. Like TypeScript'sstrict, the switch gains coverage over time under one word.
Dimensional checking (the units axis)
The dimensional axis can also be set on its own with the legacy per-axis header (equivalent to the umbrella driving just that axis):
dimensions warn
dimensions strict
dimensions offWhen omitted, Grid uses its undeclared default. warn emits diagnostics for
incompatible concrete units/currencies, strict blocks the model on conflicts,
and off skips dimensional checking.
FX_RATE(base, quote) is directional (quote/base), so
amountEUR * FX_RATE("EUR", "USD") is USD.
Dimensional inference flows through arithmetic, branches, aggregates,
SQRT/POWER with static exponents, CONVERT, and static lookup return
positions. Fully dynamic lookup/index results stay unknown unless Grid can prove
a static return range, row, or column.
For deterministic model-owned rates, declare the rate cell explicitly:
fx_rate R1C2 = EUR/USD
R1C1 = 50EUR
R1C2 = 1.08
R1C3 = R1C1 * R1C2 + 10USDThe declaration gives R1C2 dimension USD/EUR; it does not fetch a rate or
perform ambient conversion. The numeric rate must come from model data, a table,
or an explicit connector input.
Matrix shapes and value bounds (static proofs)
Two more compile-time analyses ride the same warn-by-default, strict-promoted machinery as units:
-
Matrix shapes. Grid infers the rows × columns extent of ranges, array literals, and nested matrix calls, and flags impossible combinations at edit time —
MMULToperands whose inner dimensions can't match, a non-square matrix fed toMINVERSE/MDETERM, a badMUNITsize,SUMPRODUCTarguments of different shapes (GRID_MATRIX_SHAPE_MISMATCH). -
Value bounds. Grid propagates numeric intervals through formulas. A
VALIDATEclause on an input cell seeds the range other formulas observe; aVALIDATEclause on a computed cell is a static assertion the compiler proves (recorded as a proof), refutes (GRID_VALIDATE_UNSATISFIABLE), or reports as not statically checkable (GRID_VALIDATE_UNVERIFIED):input x = 0 VALIDATE 0..1 y = x / (x + 1) VALIDATE 0..1 # proven at compile time z = x + 2 VALIDATE 0..1 # can never hold — flagged
Under dimensions strict (or the umbrella strict), shape mismatches
and refuted VALIDATE assertions are compile errors rather than
warnings. Anything the analyses cannot prove stays silent — unknown
shapes and unbounded ranges never diagnose.
Tags are sticky: once set, a tag persists across recomputations
until a different assignment overwrites or omits it. See
assignments.md and
coercion.md for the coercion rules behind tag
checks.
5.5 Unary predicates — is a :predicate
Where a type says what a value is (a closed, single-lineage
hierarchy — §5.2), a predicate says what is true about a symbol. Unary
predicates are an open, multi-valued set of colon names orthogonal to the type.
A fieldless predicate needs no declaration. A structured predicate uses a
typed schema declared with predicate. The same schema types a
relates to ... via :predicate(...) graph payload:
predicate reviewed(by: string, score: number)
x is :happy USD = 1.00 # type USD, plus predicate :happy
x is a :draft :urgent = 5 # several predicates, no type needed
x is a :reviewed(reviewer, 0.95) USD = 1.00
ready = x is :urgent # → TRUE
checked = x is :reviewed # → TRUE (presence, independent of fields)
reviewer1 = PREDICATES(x).reviewed.by # → reviewer
reviewer2 = x.predicates.reviewed.by # equivalent property-style accessThe a or an after declaration-side is is optional and has no semantic
effect; the colon distinguishes a predicate from a type tag. The declaration
colon is optional too: predicate reviewed(...) and
predicate :reviewed(...) declare the same schema. The former tag and
relation declarations remain accepted aliases. The declaration fixes field
order and type wherever :reviewed(...) is used. An undeclared predicate
cannot carry fields. Both
structured-access spellings above resolve through the same schema-checked path.
Selecting the predicate without a field (PREDICATES(x).reviewed or
x.predicates.reviewed) returns its Boolean presence; selecting a field on an absent
predicate returns BLANK. A misspelled or undeclared field is a compile-time
diagnostic, and PREDICATES(x) by itself is incomplete. TAGS(x) and .tags
remain accepted aliases.
Declared field types are contracts, not loose type overlays. A payload value
that cannot inhabit its field type produces #TYPE! even in loose mode, and a
presence query cannot hide that invalid payload. Predicate declarations in
imported .gs libraries supply the same field schema to the importer.
The field names kind, relation_id, predicate, target, membership_id, and
present are reserved for carrier-owned relationship, membership, or state
metadata. The former carrier name tag also remains reserved for compatibility.
Unary predicates describe the symbol, not the value, so they do not flow
through arithmetic: after y = x + 1, y is :happy is FALSE. A query
on a symbol without that predicate is FALSE, never an error. Predicates are
order-independent — the declaration may come after the query.
Structured unary predicates are single-valued by default. An optional
trailing one makes that choice explicit. Repeating a single-valued predicate
on one declaration is an error.
Use many by (...) to retain several instances and name the fields that form a
stable, possibly composite identity:
predicate reviewed(
id: symbol,
by: symbol,
at: datetime
) many by (id)
x is :reviewed(:r1, :ada, NOW()) :reviewed(:r2, :lin, NOW()) = report
review_ids = PREDICATES(x).reviewed.id # array: [:r1, :r2]
reviewers = x.predicates.reviewed.by # array: [:ada, :lin]Each identity field must name a declared field, may occur only once, and at
least one is required. For a composite key such as many by (tenant, id), a
keyed removal supplies values in exactly that order. Repeated attachment or
addition of the same identity replaces its entire payload in place (last write
wins) while preserving stable first-insertion order. Field access on a many
predicate returns an array in that order; presence is true when at least one
instance remains, and an absent/empty field view is BLANK.
Unary predicates are live: each colon name on a symbol compiles to one
hidden reactive state binding, whether it holds one record or a collection, so
structured updates are atomic. For a single predicate, mutate with
+= :reviewed(...) (add or replace) and remove with bare -= :reviewed.
For a many predicate inside a rule, += requires the complete declared
payload and upserts by identity, -= :reviewed(key, ...) removes one matching
identity, and bare -= :reviewed removes all instances. Every reader reacts:
x is a :flagged = 5 # declared with the predicate on
WHEN risk > limit THEN
x -= :flagged # reactively clear it
END
alert = x is :flagged # flips to FALSE once the rule firesA numeric compound assignment in a rule (x += 3) remains arithmetic — only a
colon-predicate operand is treated as a predicate operation. A fieldless
predicate may be introduced by a rule without a declaration; its base state is
absent until an add rule fires. A structured predicate requires its
declaration. Keyed removal of a many predicate is a reactive state operation
and is therefore rule-only; a top-level bare remove can still clear its static
base. The many designation governs predicate instances; graph relationship
edges retain their own independent identity even though they share the
predicate's field contract.
For reverse lookup, use the unified reactive MODEL.PREDICATES Graph view. Its
unary portion is bipartite: a predicate vertex points to every model symbol to
which it currently applies. MODEL.TAGS remains a unary-only compatibility
view.
reviewed_symbols = MODEL.PREDICATES.NEIGHBORS(:reviewed)
predicates_on_a1 = MODEL.PREDICATES.IN_NEIGHBORS("A1")
review_instances = MODEL.PREDICATES.OUT_DEGREE(:reviewed)The namespace distinction is exact: colon symbols such as :reviewed are
predicate vertices, while model-symbol vertices are their canonical text names
such as "A1" or "order". Thus a model symbol named reviewed is distinct
from the predicate :reviewed; the leading colon provides the separate
predicate namespace. NEIGHBORS returns each matching model symbol once. A
many predicate creates one parallel :applies edge per identity, so
OUT_DEGREE and EDGE_COUNT count instances. Declared fields, their .__type
metadata, and the stable membership ID are edge properties. Predicate vertices
publish kind: :predicate; membership edges publish kind: :applies plus
predicate, target, and membership_id. Rule adds, keyed removals, and bare
remove-all operations update this view reactively.
Predicate views remain separate from MODEL.GRAPH, so unary predicates do not
alter dependency topology, components, PageRank, or path results. Membership
IDs are opaque versioned strings (currently tag-membership:v2:<sha256>),
stable for the same predicate, target, and complete typed identity across
payload-only upserts. Compare or retain the whole string; do not parse its
digest.
5.6 Predicate rules and qualitative relations
Open predicates support finite rules, laws, graph quantifiers, constraints, and proof provenance:
predicate releasable means
reviewed
AND NOT blocked
AND EVERY dependency IS :releasable
predicate touches IS symmetric
predicate contains IS inverse OF inside
predicate before IS transitive AND asymmetric
predicate owner IS functional
answer = EXISTS dependency OF service WHERE dependency IS :vulnerable
safe = NO path VIA flow* FROM public_input TO secret_output AVOIDS :sanitized
sealed = NO path VIA ingress THEN (transform OR archive)* THEN store
FROM public_input TO secret_output AVOIDS :sanitizedThe predicate introducer also disambiguates open logical laws whose names
overlap qualitative relations. The corresponding bare law form is rejected.
Top-level MUST, MAY, and CANNOT declare modal constraints; in an
assignment they query a proposition. therefore proposition checks that the
current facts and rules entail a conclusion. Inspecting a query cell with
gridctl cell why returns the rule/law proof chain. See the
predicate guide for fixed-point
and open-world semantics. In particular, unknown is possible but neither
entailed nor prohibited: it makes MUST and CANNOT false and MAY true.
Top-level modalities and assigned open-predicate MAY/CANNOT each name one
ground atom. Assigned MUST may contain a compound proposition.
The explicit p(x) := ... form remains available for advanced generated
source. NO path without VIA selects only the model dependency relation.
Paths compose with THEN, choose with OR, repeat with postfix *, and run
backward with REVERSE; parentheses group a route. Their precedence is *,
REVERSE, THEN, OR. Every relation consumes one edge; write repetition
explicitly, such as flow*. Bare VIA flow remains a Predicate v3
compatibility spelling and formats as VIA flow*. A path WHY proof carries
either a checked product-frontier certificate or the ordered counterexample
route that disproves the claim.
A bare clause asserts a fact; the same clause in an assignment asks whether that fact must follow. Temporal, topological, and conceptual-set predicates share the same query, reactivity, contradiction, and explanation surface while keeping their inference components independent.
Temporal predicates
design before build
build before launch
answer = design before launch # TRUE
possible = MAY(build meets launch)
ruled_out = CANNOT(launch before design)Assertions may contain alternatives, complements, and ordinary reactive conditions:
packing (before OR meets) pickup
packing (NOT meets) pickup
packing before pickup UNLESS expedited
packing meets pickup IF expeditedRELATION(x, y) returns the possibilities left after closure as a structured
relation_set with family, state, stable relations, and natural
display. Its fields compose normally: after where = RELATION(x, y),
where.family is "temporal", "topology", or "sets".
INTERVAL(start, end) binds numeric, date, or datetime endpoints to a symbol;
temporal queries derive the exact live relation from those endpoints and
intersect it with authored facts. Endpoint types must match and start must
precede end. RELATION_STATUS(x) reports the connected component's consistency
status.
The resident WHY(query_cell) inspection includes the active assertions and
retained composition steps. The complete primitive vocabulary is before,
starts, during, finishes, equals, finished by, contains,
started by, overlapped by, met by, and after. Underscore spellings of
the four multiword converses remain compatibility aliases.
Topological predicates
Topology is written as ordinary spatial language:
island separate from mainland
porch touches house
meadow partially overlaps floodplain
copy coincides with original
parcel inside district
district encloses parcel
road intersects propertyinside and encloses mean proper containment regardless of boundary
contact. Refine them only when the distinction matters:
shed inside lot with boundary contact
house inside lot without boundary contact
lot encloses shed with boundary contact
lot encloses house without boundary contactintersects means anything except separate from. encloses is deliberately
distinct from contains, whose temporal and collection meanings remain
unchanged.
Conceptual-set predicates
Conceptual sets describe membership extents without enumerating their members:
vip_customers included in active_customers
employees disjoint from contractors
campaign_a overlaps with campaign_b
audience_a same members as audience_bincluded in allows equality; strictly included in requires a proper
subset. includes and strictly includes are the converses. overlaps with
requires at least one shared member, while partially overlaps with also
requires each set to have members outside the other. Empty extents are exact:
two empty sets have the same members and are disjoint.
Named values ground these relations from their current direct members. Scalars
act as singleton extents; arrays and native collections use the ordinary
duplicate-insensitive, type-sensitive collection equality rules. A named Graph
projection, such as MODEL.PREDICATES.NEIGHBORS(:reviewed), participates directly and
recomputes reactively.
All three families support OR, family-local NOT, MUST, MAY, CANNOT,
and reactive IF / UNLESS. RELATION and RELATION_STATUS normally infer
the family from authored facts. If the same symbols participate in several
families, select it explicitly:
when = RELATION(phase, release, temporal)
where = RELATION(phase, release, topology)
who = RELATION(cohort_a, cohort_b, sets)
state = RELATION_STATUS(phase, topology)See the predicate guide for the complete authoring model,
contradiction behavior, explanation, bounds, and the MODEL.PREDICATES
projection.
The qualitative_relations spec in the repository covers the formal
semantics, contradiction behavior, and MODEL.PREDICATES projection.
6. Expressions
The right-hand side of an assignment is an expression. Grid supports the full spreadsheet expression surface plus a set of readable extensions.
6.1 Operators by family
| Family | Operators |
|---|---|
| Unary | +, -, !, NOT, reductions +/, */, &/, temporal prefix @ |
| Access | ., ?., [], ?[], slices [a:b:c], from-end [^n], header @"k" / @[...] / @![...] |
| Postfix | % (percent), ! (factorial), !! (double factorial) |
| Power | ^, ** (right-associative) |
| Combinatoric | CHOOSE→COMBIN, PERMUTE→PERMUT, MULTICHOOSE→COMBINA |
| Bitwise | BITAND, BITOR, BITXOR, BITLSHIFT/SHL, BITRSHIFT/SHR |
| Multiplicative | *, /, // (floor div), %%/MOD/MODULO |
| Domain | %OF→PERCENTOF, TO→GROWTH |
| Additive | +, -, ++→VSTACK, ±/+/-→UNCERTAIN |
| Concatenation | & |
| Comparison | =, <>/!=, <, <=, >, >=, <=>, IN, LIKE, BETWEEN, IS …, HAS, SUBSET OF, DIVIDES, COPRIME, … |
| Set | UNION, INTERSECT, EXCEPT, EXCLUDE |
| Logical | AND/&&, OR/||, XOR |
| Nullish | ??, DEFAULT |
| Conditional | THEN … ELSE |
| Apply / pipe | OF, >>/|> |
| Clauses | UNLESS, ASSERT … ELSE, WHERE, USING … AS |
The extended infix operators (divisibility, collection relations,
bitwise, PICK/OMIT/RENAME, INTO, conditional pipe steps) have
their own deep dive in infix-operators.md.
6.2 Comparison details
0 <= score <= 100 # chained comparison (special-cased)
a <=> b # spaceship: -1 / 0 / 1
x BETWEEN 1 AND 10 # range test, plus NOT BETWEEN
name LIKE "J*n" # wildcard match, plus ILIKE / NOT LIKE
text CONTAINS "needle" # case-sensitive contiguous substring
[1, 2, 3] CONTAINS [2, 3] # contiguous ordered elements
value IN [1, 2, 3] # direct-element membership, plus NOT ININ and HAS are converse spellings and compare one whole direct element with
deep, type-sensitive equality; they never recursively flatten a nested value.
For 2-D arrays, direct elements are rows, including column literals: [1; 2; 3]
exposes one-cell rows, so 1 IN [1; 2; 3] is false while [1] IN [1; 2; 3] is
true. SUBSET OF and SUPERSET OF use those same direct elements with
unordered, duplicate-insensitive set semantics.
CONTAINS instead requires a contiguous run in an ordered value (text, array,
tuple, vector, or deque) and is case-sensitive for text. Set and map membership
operates on keys; unordered sets and maps do not support CONTAINS.
A scalar used where a membership or set-relation operand is expected is a
singleton collection. Thus 1 IN 1, 1 SUBSET OF [1, 2], and
[1, 2] SUPERSET OF 1 are true. Empty-set laws apply: [] SUBSET OF x is true
and [] OVERLAPS x is false. For ordered containment, a scalar right operand is
one-element pattern and an empty pattern always matches. Callable IN requires
a value plus at least one candidate; CONTAINS, SUBSET, SUPERSET, and
OVERLAPS require exactly two arguments; ISEMPTY requires exactly one.
ILIKE is wildcard matching rather than a second containment operator. Use
text ILIKE "*needle*" for a case-insensitive substring test. The one
contextual exception to ordinary HAS membership is a value declared by
CHOICES: there flags HAS requested means every requested bit is set, as
described in CHOICE and CHOICES.
Errors participate as structural values identified by error code; diagnostic
message text does not affect membership. Two #VALUE! values therefore match,
while #VALUE! and #N/A do not. Non-finite numbers retain numeric equality:
NaN never equals any value (including another NaN), while infinities compare
equal only when their signs match.
Set-like relations index and deduplicate one candidate side, then verify hash
collisions with full structural equality. Ordered collection CONTAINS uses
linear-time sequence matching with a direct single-element fast path. These
paths do not recursively flatten operands or warm a descriptor's compatibility
row cache.
Bare comparison is non-associative: 0 < x < 10 is not valid as a
generic chain — use the special chained form above, BETWEEN, or
0 < x AND x < 10.
6.3 IS predicates
IS lowers to a type or domain predicate; IS NOT negates:
x IS BLANK x IS EMPTY x IS NUMBER x IS NOT TEXT
x IS ERROR x IS NA x IS DATE x IS DURATION
n IS ODD n IS EVEN n IS PRIME n IS MULTIPLE OF 5
x IS URL x IS COLOR x IS COMPLEX6.4 Access, slicing, and chaining
obj.field obj["field"] obj?.maybe?.deep
data[1] data[-1] data[^1] # last element
data[2:5] data[:3] data[3:] data[1:10:2]
data@"amount" data@["id", "amount"] data@!["secret"]6.5 Reductions and broadcast-dot
+/ A1:A10 # sum-reduce */ A1:A10 # product-reduce
&/ A1:A10 # concat-reduce
SIN.([1, 2, 3]) # broadcast a scalar function over an array
ROUND.(values, 2) # broadcast with extra args6.6 Domain and special operators
25 %OF 200 # PERCENTOF(25, 200)
100 TO 200 # growth/range domain operator
5 CHOOSE 2 # COMBIN(5, 2)
100 +/- 5 # uncertainty → UNCERTAIN(100, 5)
(A1 * 2 + 10) CLAMP[0, 100] # clamp suffix7. Operator Precedence
From tightest (1) to loosest (19). When in doubt, parenthesize.
| Level | Family | Operators |
|---|---|---|
| 1 | unary | @, +, -, !, NOT, +/, */, &/ |
| 2 | access | ., ?., [], ?[], [a:b], [a:b:c], [^n], @key, @[...], @![...] |
| 3 | postfix | %, !, !! |
| 4 | power | ^, ** (right-associative) |
| 5 | combinatoric | CHOOSE, PERMUTE, MULTICHOOSE |
| 6 | bitwise | BITAND, BITOR, BITXOR, SHL, SHR |
| 7 | multiplicative | *, /, //, %%, MOD, MODULO |
| 8 | domain | %OF, TO |
| 9 | additive | +, -, ++, ± |
| 10 | concatenation | & |
| 11 | comparison | =, <>, !=, <, <=, >, >=, <=>, IN, LIKE, BETWEEN, IS …, HAS, SUBSET OF, DIVIDES, COPRIME, CLAMP[…] |
| 12 | set | UNION, INTERSECT, EXCEPT, EXCLUDE |
| 13 | logical | AND, &&, OR, ||, XOR |
| 14 | nullish | ??, DEFAULT |
| 15 | conditional | THEN … ELSE |
| 16 | apply | OF (right-associative; reversed >>) |
| 17 | pipe / clause | >>, |>, UNLESS |
| 18 | clause | ASSERT, ASSERT … ELSE |
| 19 | clause | WHERE |
The decisions that bite most often:
- Power is right-associative:
2 ^ 3 ^ 2=2 ^ (3 ^ 2)=512. &binds tighter than comparison:"a" & "b" = "ab"compares the concatenation, as expected.- Comparison is non-associative: write
x BETWEEN 0 AND 10, not a bare0 < x < 10chain. THEN … ELSEis below logical:A AND B THEN x ELSE yreads as(A AND B) THEN x ELSE y.- Pipes are below conditionals:
cond THEN x ELSE y >> f()reads as(cond THEN x ELSE y) >> f(). - Combinatoric binds tighter than
*:5 * 3 CHOOSE 2is5 * COMBIN(3, 2).
8. Control Flow
Grid is an expression language: control flow produces values rather than performing statements. Every form below is an expression you can assign, nest, or pass as an argument.
8.1 Conditional — THEN … ELSE
The single-condition form. Reads top to bottom, may chain:
C1 = score > 0 THEN "positive" ELSE "non-positive"
C2 = x > 90 THEN "A" ELSE x > 80 THEN "B" ELSE "C" # chainedA chained ladder may wrap across lines at each THEN / ELSE
(see §14):
C1 = score > 90 THEN "A"
ELSE score > 80 THEN "B"
ELSE "C"The ladder folds at every point that expects a continuation — a
line-ending THEN/ELSE, or a continuation line beginning with ELSE.
A completed ladder still ends at the newline. For very long ladders,
CASE or MATCH may still read better.
8.2 Multi-branch — CASE WHEN
When each branch tests a different condition; desugars to IFS. Spans
multiple lines and ends with END:
tier = CASE
WHEN score >= 0.9 THEN :excellent
WHEN score >= 0.7 THEN :good
WHEN score >= 0.5 THEN :fair
ELSE :poor
END8.3 Value matching — MATCH
When every arm compares the same subject; desugars to IFS. _ is
the catch-all. MATCH is a call, so its arms may stay on one line or
wrap across lines (§14.1):
color = MATCH(status, "draft" -> :gray, "live" -> :green, _ -> :red)Error-kind arms. When any arm is an error literal, MATCH branches on
the subject's error kind by name, without propagating: value arms are
guarded so an error subject skips them, and the error arms discriminate the
kind (#NULL!, #DIV/0!, #VALUE!, #REF!, #NAME?, #NUM!, #N/A).
This makes errors typed and matchable rather than something a formula can
only swallow whole with TRY / IFERROR:
safe = MATCH(risky,
#DIV/0! -> 0, # divide-by-zero → 0
#N/A -> BLANK, # missing lookup → blank
_ -> risky) # anything else (incl. a plain value) passes through8.4 Local bindings — DO … END
Introduce named intermediates ending in a result expression. Pure blocks
desugar to LET; blocks containing async-worker calls become dependency-aware
workflows:
B3 = DO
margin = revenue - cost
deduction = 2500
MAX(margin - deduction, 0)
ENDLET(name, value, body) is also available as a function; DO is the
canonical multi-line form.
Async bindings start as soon as their arguments and active branch are ready. Independent calls may run concurrently; a binding that reads an earlier result waits for it. The final expression is an implicit join over every call started by the block.
result = DO
a = HTTP_JSON(url_a) # starts with b
b = HTTP_JSON(url_b)
combine(a, b) # waits for both
END
page2 = DO
first = HTTP_JSON(url_a)
second = HTTP_JSON(first.next_url) # waits for first by dependency
second
ENDAWAIT value adds an ordering barrier for later async calls without changing
the value. DO SYNC inserts that barrier after every async binding:
result = DO
first = MUTATE_A()
second = MUTATE_B() # may start with first
AWAIT first
second
END
result = DO SYNC
first = MUTATE_A()
second = MUTATE_B() # starts after first settles
second
ENDAWAIT on a synchronous expression is a compile error. Unchosen conditional
branches never launch their calls. A failed stage prevents dependent stages
and fails the block; v1 has no implicit retry, cancellation, or timeout.
8.4.1 Bounded effect traversal
MAP, FILTER, REDUCE, and the other ordinary collection operators require
pure lambdas. Grid infers job-queue effects from the lowered lambda body,
including the body of an inlined named function, and rejects an effectful
lambda in those operators. Use an explicit traversal when one effect should run
for each element:
serial = TRAVERSE(urls, url => HTTP_JSON(url), 100)
parallel = PAR_TRAVERSE(urls, url => HTTP_JSON(url), 8, 100)The final argument is a static safety limit, not a batch size. It defaults to
64 and must be an integer from 1 through 256. If the runtime collection is
larger, the traversal fails with ASYNC/TRAVERSE_LIMIT_EXCEEDED before
launching any request. PAR_TRAVERSE also requires a literal concurrency bound
that is no greater than the limit.
TRAVERSE admits one request at a time. PAR_TRAVERSE uses a sliding window,
so completing any slot admits the slot one window-width later; it does not wait
for a whole batch. Results are always assembled in source order, independent
of completion order. Arrays traverse row-major, a scalar is a singleton, and a
top-level blank is empty. Native iterable collections traverse their stable
logical elements; keyed collections use the same entry values as REDUCE.
Non-iterable sketches and streams fail with ASYNC/TRAVERSE_SOURCE before an
effect launches. An algebraic choice remains one scalar value even though its
resident representation is a sealed record.
The lambda must contain exactly one inferred job-queue call per element. The source expression itself must be pure and already available. Grid evaluates it once before admitting element effects, and callback-local bindings cannot capture the compiler's element read. Publish an async source result first instead of hiding a second effect in the traversal source. Failure is fail-fast: a failed request becomes the traversal result and suppresses dependent requests that have not started; already-started parallel requests are not rolled back. Traversal itself performs no implicit retry. Function/connector timeout and retry policy remains owned by the selected job route, and a terminal timeout or exhausted retry is an ordinary failed request that follows the same fail-fast rule. This keeps retries idempotency-aware instead of silently replaying arbitrary effects in the formula evaluator.
8.5 Error-tolerant bindings — WITH … THEN … ELSE
Bind a sequence of values; if any step errors, return the ELSE
fallback. Bindings may span lines (each , at end of line), and the
THEN <expr> ELSE <fallback> tail folds at THEN/ELSE too:
result = WITH data = HTTP_JSON(url), first = data.results[1]
THEN first.name
ELSE "unavailable"8.6 Inline clauses — WHERE / USING / UNLESS / ASSERT
total + tax WHERE total = SUM(A1:A10), tax = total * 0.21
total + tax USING total AS SUM(A1:A10), tax AS total * 0.21
amount UNLESS amount = 0 # IF(NOT(cond), value)
value ASSERT value > 0 ELSE #N/A # guard with custom errorWHERE and USING are post-fix binding clauses (both desugar to
LET). UNLESS suppresses a value on a condition. ASSERT short-
circuits a value through a predicate.
8.7 Error recovery — TRY / IFERROR / DEFAULT
TRY primary() THEN backup() ELSE "n/a" # try chain
IFERROR(risky(), 0) # function form
A1 DEFAULT 0 # blank-or-error fallback (?? is sugar)
A1 ?= MAYBE_FAIL() # conditional assignmentFull error semantics live in errors.md.
9. Functions And Calls
Grid ships a large built-in catalog (2,000+ functions). Function names
are case-insensitive at parse time; emit them uppercase. Names may
contain dots (Z.TEST, CEILING.MATH, INTERPOLATE.LINEAR).
SUM(A1:A10)
ROUND(3.14159, 2)
Z.TEST(data, 0.5)9.1 Named arguments
ROUND(3.14159 AS number, 2 AS digits)
FX_RATE("GBP" AS base, "USD" AS quote)AS binds the preceding value by parameter name; positional and named arguments
may be mixed with positional first.
9.2 Variadics and partial application
SUM(1, 2, 3, 4) # variadic tail
ADD(5, _) # partial application → a lambda valueA _ placeholder inside a call (outside a higher-order helper) produces
a lambda: ADD(5, _) ≡ LAMBDA(__p, ADD(5, __p)).
9.3 Execution classes
| Class | Meaning |
|---|---|
sync_builtin |
Returns immediately in the formula evaluator |
async_worker |
Enqueues a job; result is written back asynchronously |
External functions (FX_RATE, HTTP_JSON, ML_SCORE, AI_PROMPT,
ASK_*, PG_SELECT) are async_worker. See
external-functions.md.
9.4 Finding functions
functions.md— every function by category, with signature, return type, and aliases.function-compatibility.md— each name's compatibility label (excel_exact,grid_extension, …) and aliases.
Alongside the familiar spreadsheet catalog, Grid ships an analytical and
scientific-computing surface as ordinary functions (all grid_extension):
- Optimization —
LINEAR_PROGRAM,MIXED_INTEGER_PROGRAM,LINEAR_PROGRAM_SENSITIVITY(shadow prices, reduced costs, IIS),MINIMIZE/MAXIMIZE,MINIMIZE.GLOBAL/MAXIMIZE.GLOBAL,BOUNDED_MINIMIZE_N,GOALSEEK,PORTFOLIO_QP_WEIGHTS. For a declarative goal-seek over cells, prefer theSOLVEstatement. - Differential equations —
ODE_SOLVE,ODE_FINAL,ODE_SAMPLE,ODE_EVENT_TIME,HEAT_SOLVE_1D/HEAT_SOLVE_2D,PDE_SOLVE_1D(adaptive Dormand-Prince with automatic stiff/TR-BDF2 switching). - Spectral linear algebra —
EIGEN.VALUES/EIGEN.VECTORS,EIGEN.VALUES.COMPLEX,EIGEN.SPECTRAL_RADIUS,SVD.VALUES/SVD.U/SVD.V/SVD.RANK. - Nonlinear solve and fit —
NSOLVE,CURVE_FIT,FINDROOT, interpolation and splines (INTERPOLATE.CUBIC,INTERPOLATE.PCHIP). - Signal processing —
SIGNAL.PSD,WINDOW.HANN/HAMMING/BLACKMAN,POWER_SPECTRUM,DFT/FFT,CONVOLVE,CORRELATE. - Symbolic mathematics — the
SYMBOLIC.*family oversymboldeclarations:SYMBOLIC.CANONICALIZE/SIMPLIFY/EXPAND/FACTOR/COLLECT/CANCEL,SYMBOLIC.D,SYMBOLIC.SOLVE,SYMBOLIC.RATIONAL/APPROXIMATE,SYMBOLIC.ASSUMPTIONS/DOMAIN/DECIDE,SYMBOLIC.SUBSTITUTION/EVALUATE. Exact, assumption-aware, and certificate-carrying; seesymbolic-mathematics.md.
Worked examples for these live in cookbook.md.
MIXED_INTEGER_PROGRAM requires a solver-enabled build; on other builds it
returns an explicit capability error rather than a wrong answer.
10. Lambdas And Higher-Order Forms
10.1 Lambda syntax
x => x * 2 # single-parameter arrow
(acc, x) => acc + x # multi-parameter arrow
LAMBDA(x, x + 1) # function form
_ # placeholder lambda (single)
_1, _2 # positional placeholdersPrefer arrow lambdas. Reserve LAMBDA(...) for when you want an
explicit named function value. Use placeholders (_, _1, _2) only where
the surrounding higher-order helper owns the element binding, or where _
marks the whole-value insertion slot of a pipe call. In a functional
FILTER pipe step, prefer the named form FILTER(value => predicate) so the
element binding cannot be confused with pipe insertion.
10.2 Higher-order helpers
MAP(A1:A10, x => x * 2)
REDUCE(0, A1:A10, (acc, x) => acc + x)
SCAN(0, A1:A10, (acc, x) => acc + x)
BYROW(matrix, row => SUM(row))
BYCOL(matrix, col => AVERAGE(col))
MAKEARRAY(rows, cols, (r, c) => r * c)For a callback with local bindings, use a named trailing block:
MAP(rows) WITH row DO
amount = row.amount DEFAULT 0
ROUND(amount * exchange_rate, 2)
ENDThis lowers to the helper's existing final lambda argument. MAP takes one
name per source; REDUCE, SCAN, and MAKEARRAY take two; ITERATE,
BYROW, and BYCOL take one.
11. Arrays, Spilling, And Comprehensions
11.1 Spilling
A formula that returns an array writes (spills) into a rectangle of
cells anchored at its target. Refer to the whole spilled block with the
# suffix:
A1 = SORT(B1:B100, -1) # spills down from A1
C1 = SUM(A1#) # sum the whole spilled rectangleIf a spill would overwrite a populated cell, the anchor becomes
#SPILL!.
11.2 Comprehensions
Array comprehensions read more naturally than MAKEARRAY for
index-driven fills; they lower to a single array cell:
[x * 2 FOR x IN A1:A10 IF x > 0]
[r * c FOR r IN 1..100, c IN 1..10]11.3 Sequences
1..10 # inclusive 1 through 10
1..<10 # exclusive upper bound
1..2..10 # step of 2
d"2026-01-01"..d"2026-12-31" # date sequence11.4 Constant fills
Prefer array-valued formulas over giant range targets:
A1 = ZEROS(1000, 1) # column of zeros
A1 = ONES(1000, 1)
A1 = FILL("TBD", 1000, 1) # any constant
A1 = REPEAT(0, 1000)See assignments.md.
12. Pipelines And Infix Sugar
12.1 Pipe — >> / |>
Thread a value through a sequence of calls. _ marks the insertion
point when it isn't the first argument:
A1 = data >> FILTER(value => value > 0) >> SORT(_, -1) >> TAKE(5)
B1 = data |> WHERE("amount > 0") |> COLLECT()Desugars: x >> f() → f(x); x >> f(_, y) → f(x, y).
FILTER has one additional pipe rule:
values |> FILTER(value => predicate)
values |> FILTER(value => predicate, if_empty)These lower respectively to FILTER(values, MAP(values, value => predicate))
and FILTER(values, MAP(values, value => predicate), if_empty). Outside a pipe,
the ordinary signature remains FILTER(values, include_mask [, if_empty]).
The legacy pipe spelling FILTER(_, _ > 0) is accepted and normalized to the
same tree, but named lambdas are canonical.
Every pipe evaluates stages from left to right. Desugaring occurs before MIR,
so generic evaluation and compiler fusion share one semantic tree; optimization
does not change ordering, errors, or the authored empty fallback. See
collections.md for collection
residency and incremental-performance behavior.
12.2 Conditional pipe step
Append IF cond to apply just that step conditionally:
A1 = x >> ABS() IF x > 0 # IF(x > 0, ABS(x), x)12.3 Infix apply — OF
The function-first mirror of >> (right-associative):
ABS() OF A1 # ≡ A1 >> ABS()
ROUND(_, 2) OF ABS() OF A1 # ≡ A1 >> ABS() >> ROUND(_, 2)The left operand must be a call; the right operand must be a value.
12.4 Projection and cast
users PICK id, glob"*name*" # bare identifiers are literal keys
profile OMIT glob"*token*" # drop matching keys
table RENAME "Revenue" AS "rev" # rename keys/headers
amount INTO currency # TYPE_TAG(amount, "currency")12.5 Native relational query syntax
Grid accepts a first SQL-shaped relational expression:
Active =
SELECT status, SUM(amount) AS total, COUNT(DISTINCT customer) AS customers
FROM Orders
WHERE status IN ("paid", "pending") OR amount BETWEEN 10 AND 30
GROUP BY status
HAVING total > 100
ORDER BY total DESC
LIMIT 25This is source syntax, not a SQL string. The compiler lowers it to a structured relational call:
REL_SELECT(source, projection, predicate, order, limit, group, having)The language promise is one relational meaning with source-dependent physical
execution. Header arrays and Tuple/Vector/Deque collections of Record
rows execute natively today. Queries preserve the collection sequence family
and return projected Record rows. Unknown columns and inconsistent record
schemas are errors rather than silently omitted data. Connector-produced Frame
sources also execute as native query plans and can be chained as the source of
another SELECT. Warehouse-backed plans push this relational subset to
BigQuery, Snowflake, Redshift, and Databricks and execute supported residuals
in DataFusion. WHERE accepts scalar comparisons, IS NULL, IS NOT NULL,
IN, inclusive BETWEEN, and SQL LIKE/ILIKE, including their negated
forms, composed with NOT, AND, OR, and parentheses. % matches zero or
more characters and _ matches one character; patterns are string literals
limited to 4,096 bytes. IN accepts up
to 1,024 non-blank scalar literals. SQL precedence applies: NOT binds first,
then AND, then OR. Projection items may be columns, aliased columns, or
numeric scalar expressions using literals, columns, parentheses, unary
+/-, and +, -, *, /, %. Portable scalar calls are COALESCE
(two to sixteen arguments), two-argument NULLIF, and unary ABS, SQRT,
FLOOR, CEIL, ROUND, LOWER, UPPER, and LENGTH, plus two-argument
POWER. CEILING is accepted as an authoring alias for CEIL. Searched
CASE WHEN predicate THEN scalar ... ELSE scalar END and simple
CASE scalar WHEN scalar THEN scalar ... ELSE scalar END accept one to 64 arms;
computed items require AS. Division or remainder by zero produces
blank/NULL; so does SQRT of a negative value. ROUND rounds halves away
from zero. POWER returns blank/NULL for a negative base with a fractional
exponent, zero with a negative exponent, or a non-finite resident result.
NULLIF(left, right) returns blank/NULL when both non-null
arguments compare equal, otherwise it returns left. CASE evaluates arms
in order, selecting only the first TRUE condition; FALSE and UNKNOWN
fall through, and an omitted ELSE returns blank/NULL. The compiler emits
canonical scalar-equality arms for simple CASE; blank/NULL discriminators
do not match blank/NULL arms and fall through according to SQL three-valued
logic. The canonical predicate and scalar trees are consumed by
resident collections, Frames, Polars, DataFusion, and provider parameter
binding. Blank/NULL equality is explicit, ordered null comparisons are
rejected, and nulls sort last in either direction. Other predicates use SQL
three-valued logic: blank input produces UNKNOWN, negation preserves it, and
only TRUE passes a filter. GROUP BY supports COUNT(*) plus COUNT,
COUNT(DISTINCT ...), SUM, AVG, MIN, and MAX over the same portable
scalar-expression tree used by projections and predicates, with explicit
aliases. For example, SUM(price * quantity) AS revenue, AVG(ABS(delta)) AS mean_delta, and COUNT(DISTINCT LOWER(email)) AS users are portable across
resident, Frame, Polars, DataFusion, and supported warehouse execution.
COUNT ignores blank arguments, while the other aggregates ignore blanks and
return blank for an empty input. At most 64 group keys and 256 aggregate
outputs are accepted per query.
SELECT DISTINCT ... removes duplicate projected rows before the outer
ORDER BY and limit stage. LIMIT n, FETCH FIRST n ROW|ROWS ONLY, and
FETCH NEXT n ROW|ROWS ONLY are equivalent spellings. Duplicate blank/NULL
values collapse to one row.
Header arrays retain their header, native record sequences retain their
Tuple/Vector/Deque family, and source-proven Frames lower to the same
typed distinct stage used by Polars and DataFusion execution.
Complete SELECT results compose with SQL set operators:
SELECT id FROM CurrentCustomers
UNION ALL
SELECT customer_id FROM ImportedCustomers
EXCEPT
SELECT customer_id FROM SuppressedCustomers
ORDER BY id
LIMIT 1000UNION, INTERSECT, and EXCEPT all accept ALL. Without ALL, duplicate
typed rows collapse and blank/NULL rows compare equal. With ALL, SQL
multiset counts are preserved. Arms match columns by position, must have equal
column counts, and retain names from the left arm. INTERSECT binds before
UNION and EXCEPT; UNION and EXCEPT associate left to right. The final
ORDER BY and the canonical limit stage apply to the complete set expression.
Resident arrays,
native record sequences, and source-proven Frames share this contract.
Joins require explicit, distinct source aliases and qualified column names:
SELECT o.id AS order_id, c.name AS customer
FROM Orders AS o LEFT JOIN Customers AS c ON o.customer_id = c.idINNER, LEFT, RIGHT, and FULL joins accept equality conditions joined by
AND, up to 16 key pairs. Null/blank keys never match. Resident array and
record sources and source-proven Frames use bounded typed hash joins; exceeding
two million resident output rows is an explicit error. HAVING is evaluated
after grouping and before ordering, limit, and final projection.
Parenthesized queries are relation-valued derived tables, and non-recursive SQL common-table expressions are lexical relation bindings:
WITH active AS (SELECT id, amount FROM Orders WHERE amount > 0)
SELECT q.id FROM (SELECT id, amount FROM active) AS q WHERE q.amount >= 20Up to 64 case-insensitively distinct CTE names are accepted. Source-proven CTE
chains fuse into the same physical Frame region as derived tables. A CTE body
must be a SELECT (or another SQL WITH query). Single-source aliases are valid
throughout projection, filtering, grouping, windows, HAVING, and ordering.
Resident header arrays, Tuple<Record>, Vector<Record>, or Deque<Record>
support resource-governed recursive binding groups:
WITH RECURSIVE nums AS (
SELECT n FROM Seed
UNION ALL
SELECT n + 1 AS n FROM nums WHERE n < 100
)
SELECT n FROM nums ORDER BY nThe recursive term reads the previous frontier. UNION retains only novel
rows, while UNION ALL preserves duplicates. Authored SQL runs until fixpoint
without a fixed iteration ceiling. Execution reports an error at 2,000,000
output rows or when the resident-work budget is exhausted; it never returns a
silently partial result. Column lists rename the established seed schema and
are enforced by arity. Multiple bindings use simultaneous working-table semantics: every
recursive term reads all prior frontiers, and no next frontier becomes visible
until the following iteration. Source-proven recursive groups promote to one
resident frame.recursive_cte_group region, including column lists, multiple
and mutually recursive bindings, set expressions, and recursive joins
against additional resident Frame handles. DataFusion and connector inputs can
feed those handles after hydration; recursive SQL pushdown into a remote
provider is not required for exact execution.
Uncorrelated, one-column subqueries are accepted as scalar comparison operands
and as the value source for IN during resident execution. A scalar subquery
returns blank for zero rows and errors above one row. Equality-correlatable
EXISTS, NOT EXISTS, IN, and NOT IN subqueries over explicitly aliased
sources are decorrelated into duplicate-free composite-key hash semi/anti
joins; inner-local predicates and trailing outer AND predicates are retained.
Correlated NOT IN is null-aware: a null in the matching inner partition
produces SQL UNKNOWN instead of incorrectly passing the outer row. Correlated
scalar aggregates are also accepted as explicitly aliased projections:
SELECT c.id,
(SELECT SUM(o.amount) AS total
FROM Orders AS o
WHERE o.customer_id = c.id AND o.status = "paid") AS spend,
(SELECT COUNT(*) AS total
FROM Orders AS o
WHERE o.customer_id = c.id AND o.status = "paid") AS paid_orders
FROM Customers AS cThe inner query must project exactly one aggregate and may use one to 16
equality correlation keys plus inner-local AND filters. Grid groups and
indexes each inner query once rather than evaluating it for every outer row.
A missing group yields zero for COUNT and blank/NULL for the other
aggregates. Multiple projections compose for resident relations and
source-proven Frames. Correlation under general boolean OR is not yet
accepted.
Window projections use function(...) OVER (...) AS name. A reusable complete
window specification can be named in the query's WINDOW clause and referenced
as OVER name:
SELECT id,
ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC, id) AS row_num,
LAG(amount, 2, 0) OVER (PARTITION BY region ORDER BY id) AS prior,
SUM(amount) OVER (
PARTITION BY region ORDER BY id
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS rolling
FROM OrdersSELECT id, ROW_NUMBER() OVER newest AS row_num
FROM Orders
WINDOW newest AS (PARTITION BY region ORDER BY amount DESC, id)
QUALIFY row_num = 1Supported functions are ROW_NUMBER, RANK, DENSE_RANK, NTILE,
PERCENT_RANK, CUME_DIST, LAG, LEAD, FIRST_VALUE, LAST_VALUE,
NTH_VALUE, and windowed COUNT, SUM, AVG, MIN, and MAX. Ranking,
distribution, navigation, and value functions require window ordering. NTILE
uses a positive bucket count and NTH_VALUE uses a one-based positive index.
Navigation offsets are non-negative
integers no greater than 1,000,000. Their optional third argument is a number,
string, boolean, or BLANK literal returned when the requested row is outside
the partition; omission defaults to blank. Aggregate windows without ordering
cover the whole partition; ordered aggregate windows use the peer-aware default
frame. Aggregate and value windows also accept
ROWS BETWEEN <bound> AND <bound> or RANGE BETWEEN <bound> AND <bound>, where
a bound is UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW, n FOLLOWING,
or UNBOUNDED FOLLOWING; offsets are capped at 1,000,000 and the start cannot
follow the end. Offset RANGE frames require exactly one finite numeric order
key; peer-only ranges may use multiple order keys. Polars fails closed on
RANGE, while resident, Frame, DataFusion, and warehouse paths preserve it.
A frame wholly outside a partition is empty. Windows in a
grouped query run after aggregation and HAVING, so they may order, partition,
or aggregate over grouped output aliases. QUALIFY runs after those analytic
outputs are projected and before final DISTINCT, ORDER BY, and result
extent. It may therefore refer to an analytic projection alias. Named windows
are complete specifications: OVER name does not accept a partial override or
inheritance clause.
The full operator-by-operator treatment — divisibility, collection
relations, bitwise, projection, cast, conditional pipe — is in
infix-operators.md.
13. Reserved Words
Grid's grammar is contextual — it has no truly reserved words, so a clever parse can often accept a keyword as an identifier. Don't rely on that. The structural keywords below are effectively reserved: using them as binding names creates ambiguous parses and breaks under grammar extensions.
Structural / block keywords:
MODEL DESCRIPTION VERSION AUTHOR TAGS RUNTIME END
WHEN CHANGES BECOMES EVERY AT THEN ELSE DO CASE WITH MATCH
LAMBDA LET TRY ASSERT WHERE USING UNLESS USE
INPUT OUTPUT DEFAULT RESET CLEAR IS INTO ONCE SKIP MISSED
BACKFILL DEBOUNCE THROTTLE FOR IN AS STRICT LOOSE
SERIAL CONCURRENT LATEST QUEUE LEDGER OVERFLOW
DIMENSIONS COERCION
GAME MECHANISMSTRICT / LOOSE lead the strictness directive (with EXCEPT, which is
also the set-difference operator); DIMENSIONS / COERCION name the axes.
DEFAULT is also the null-fallback operator; RESET / CLEAR are also
rule-action modes. They are contextual — default leads an assignment,
RESET / CLEAR lead a rule action — so they don't collide in practice,
but don't bind values to those names.
GAME <name> { ... } and MECHANISM <name> { ... } introduce the optional
declarative strategic-model blocks documented in
game-theory.md. Their lookahead is deliberately narrow:
the legacy-compatible bindings GAME = value and MECHANISM = value remain
ordinary assignments.
GRAMMAR, GENERATOR, PATTERN, THEORY, ALGEBRA, TRANSFORM,
TRANSLATE, SOLVER, and THEOREM introduce the grammar-and-tree
declaration blocks documented in grammars.md. They are
contextual in the same way: an ordinary identifier with one of those spellings
keeps its existing meaning unless the complete declaration header matches.
symbol <name> declares an immutable mathematical unknown for Grid-native
symbolic mathematics, documented in
symbolic-mathematics.md. It is contextual the
same way — symbol = 4 remains an ordinary binding named symbol; only the
complete declaration form declares an unknown.
Operator-word keywords (also avoid as names):
AND OR XOR NOT MOD MODULO DIVIDES COPRIME CHOOSE PERMUTE
MULTICHOOSE BITAND BITOR BITXOR SHL SHR UNION INTERSECT EXCEPT
EXCLUDE HAS SUBSET SUPERSET OVERLAPS LIKE ILIKE BETWEEN CONTAINS
STARTS ENDS PICK OMIT RENAME OF TO DEFAULT TRUE FALSE BLANKA note on LEFT / RIGHT: bare (no parentheses) they are spatial
references; with arguments (LEFT(text, n)) they are text
functions. Don't bind a value named LEFT or RIGHT.
Pick descriptive names (revenue, total_cost, is_active) and this
never bites you.
14. Multi-line And Layout Rules
Most statements live on one line. Newlines either fold (continue the current expression) or separate statements, depending on context.
14.1 Multi-line Rules (important)
Newlines fold (the expression continues) inside:
- Open
(...),[...],{...}— function-call argument lists, parenthesized expressions, array literals, object literals, comprehensions, andMATCH(...)(which is a call). DO … ENDbodies (between bindings and the result).CASE WHEN … ELSE … END(between arms;ELSEmay start its own line).WHEN/EVERY/ATrule bodies.WITHbinding lists (after a trailing,).THEN … ELSEladders and theWITH … THEN … ELSEtail, at eachTHEN/ELSE. The fold is scoped to those keywords: a line-endingTHEN/ELSEcontinues, and a continuation line may start withELSE. A completed ladder still ends at the newline, so the next statement is never absorbed.
Newlines separate statements everywhere else.
# Folds — inside brackets
A1 = SUM(
B1, B2,
B3
)
# Folds — DO body
B1 = DO
x = 1
y = 2
x + y
END
# Folds — chained THEN/ELSE wraps at each THEN/ELSE
C1 = a THEN 1
ELSE b THEN 2
ELSE 314.2 Other layout conventions
- One statement per line is the canonical style.
- Put spaces around operators:
A1 = SUM(B1:B5). - Trailing commas are allowed in calls, arrays, objects, and
MATCHarms.
See style-guide.md for the full canonical style.
15. See Also
quickstart.md— Getting Started.features.md— feature checklist.assignments.md— every assignment shape.coercion.md— type coercion rules.errors.md— error codes and recovery.rules-and-schedules.md—WHEN/EVERY/AT.external-functions.md— async functions and~=.infix-operators.md— extended infix operators.functions.md— the function catalog.cell-metadata.md— formats, styles, validation.performance.md— performance patterns.graphs.md— native graph values, declarative algorithms, fixed points, and GraphEXPLAIN.grammars.md— grammars, parsers, and grammar-bound trees: patterns, theories, algebras, transforms, translations, solvers, and checked theorems.