Grid Language Reference
Grid Language Reference
The complete reference for the Grid formula language.
This document is normative: every form documented here is supported by
the parser at version 1.0.0-rc.*. Forms not documented here may exist
in the parser as experimental sugar but are not guaranteed to be stable.
The companion documents are:
assignments.md— assignment statements in detail.rules-and-schedules.md— rule blocks.external-functions.md— async functions.errors.md— the error model.functions.md— the built-in function catalog.
1. Source File Structure
A .grid source file is a sequence of statements, optionally preceded
by metadata directives.
[<directive> ...]
<statement>
<statement>
...
[END MODEL]Statements are separated by newlines or semicolons. A trailing separator is allowed.
1.1 Comments
| Form | Effect |
|---|---|
# text to end of line |
Line comment |
/* text */ |
Block comment, may span lines |
# This is a header comment.
A1 = 10 # Trailing comment.
/*
Multiline
block comment.
*/Edge case. A
#immediately followed by a known error-literal spelling (N/A,DIV/0!,VALUE!,REF!,NAME?,NULL!,NUM!,CALC!,SPILL!) is parsed as an error literal, not a comment. So# N/A this is fineis a comment but#N/Ais the error literal. Add a space (# X) to force comment behavior when in doubt.
1.2 Metadata Directives
Directives appear at the top of the file, before any statements. All are optional but the canonical style strongly prefers a complete header.
| Directive | Form | Purpose |
|---|---|---|
MODEL |
MODEL "name" |
Human-readable model name |
DESCRIPTION |
DESCRIPTION "text" |
Short description |
RUNTIME |
RUNTIME "lua_generated", RUNTIME "lua_interpreter", or RUNTIME "ts" |
Selects runtime profile |
VERSION |
VERSION "1.0.0" |
Semver string |
AUTHOR |
AUTHOR "name" |
Author or team |
TAGS |
TAGS "a", "b", "c" |
Free-form tags |
END MODEL |
END MODEL |
Closes the file (optional) |
The RUNTIME directive matters most:
| Value | Meaning |
|---|---|
"lua_generated" |
Model compiles to a self-contained Redis Functions Lua library. Rule blocks (WHEN/EVERY/AT) and rule-action assignment modifiers (+=, ?=, …) are supported via the TS GridService orchestrator with smart wakeup. |
"lua_interpreter" |
Same execution model as lua_generated, but the model is shipped as a JSON IR that a shared Lua runtime in Redis loads. Rule support is identical. |
"ts" |
Model runs in the in-process TypeScript runtime. Behaviorally equivalent to the Lua runtimes for the portable subset; useful for unit tests, local development, and any code path that needs to execute outside Redis. |
If no RUNTIME is set, the engine falls back to the deploy-time
default (GRID_RUNTIME_ENGINE env var, defaulting to "ts").
Practical note:
lua_generatedandlua_interpreterare behaviorally identical for the portable subset. Picklua_generatedwhen you want a one-Lua-script-per-model deployment artifact, andlua_interpreterwhen you'd rather ship a JSON IR alongside a single shared runtime script.
2. Statements
Top-level statements come in three shapes:
- Assignments — bind a value to a target cell.
- Rule statements —
WHEN/EVERY/ATblocks. - Metadata directives — covered above.
See assignments.md and
rules-and-schedules.md for full coverage.
The summary:
# Eager assignment
A1 = 10
# Lazy assignment
A2 ~= EXPENSIVE_CALL()
# Conditional eager (only assign if expression is non-error)
A3 ?= MAYBE_FAIL()
# Compound assignment
A4 += 5
# Typed assignment
A5 as currency = A1 * 100
# Range assignment (broadcasts to each cell)
A6:A10 = B1
# Destructuring
[total, avg, ...rest] = SUMMARY(B1:B10)
# Input-decorated assignment (writable surface in protected mode)
input price = 0
# Output-decorated assignment (readable surface in protected mode)
output total = price * qty
# Rule block
WHEN A1 > 0 THEN
C1 = "active"
END2.1 Decorators
Two decorator keywords may prefix any assignment:
| Decorator | Position | Effect |
|---|---|---|
input |
First, before any other token | Cell is part of the model's external write surface (setInput/setBatchInput). Restricted to plain = assignments with a default value. |
output |
Either before or after input |
Cell is part of the model's external read surface (resolve/getRange/getModelSnapshot). Permitted on every assignment shape, including rule-action targets. |
In a deployment with protectedMode: true the runtime strictly
enforces both surfaces — only declared inputs accept writes, only
declared outputs return reads. See
assignments.md § 5
for the full semantics.
3. Cell References
A cell name is a symbol. Each cell holds exactly one typed value.
3.1 A1 Notation
A1 # column A, row 1
AA100 # column AA, row 100
$A$1 # absolute (locks both column and row)
$A1 # column-absolute, row-relative
A$1 # column-relative, row-absolute3.2 R1C1 Notation
R1C1 # row 1, column 1
R[1]C[-1] # one row down, one column left (relative)
R1C[2] # row 1 (absolute), two columns right3.3 Named References
Plain identifiers are named references:
Revenue
fx.rate
total_2026Names may include letters, digits, underscores, and .. They must not
start with a digit and must not look like an A1 or R1C1 address.
3.4 Sheet And Workbook Qualifiers
Sheet1!A1 # sheet-qualified
'Q4 Revenue'!B2 # quoted (use single quotes for spaces)
'[Book1]Sheet1'!A1 # workbook + sheetSingle quotes are reserved for sheet/workbook qualifiers. Do not use them for string literals.
3.5 Ranges
A1:B10 # finite rectangle
R1C1:R3C3 # finite rectangle in R1C1
A:A # full column A
A:C # columns A through C
1:1 # full row 1
1:3 # rows 1 through 33.6 Spill References
If a formula returns an array, it spills into a rectangle anchored at
its target. The # suffix returns that whole rectangle as a single
array value:
A1 = [1, 2, 3, 4, 5] # spills into A1:A5
B1 = SUM(A1#) # 15
C1 = A1# * 2 # broadcasts; spills into C1:C53.7 Spatial And Neighborhood References (TS Runtime)
Available only in the TS runtime. Useful inside rule actions:
ABOVE # cell directly above the current target
BELOW # cell directly below
LEFT # cell directly to the left
RIGHT # cell directly to the right
ABOVE(3) # three rows above
SUM(BELOW) # all cells below the current target
NEIGHBORS # 1-cell radius around the current target
NEIGHBORS(2) # 2-cell radius3.8 Temporal References
A reference can be qualified with a temporal suffix that points at a prior or absolute revision:
A1@-1 # value of A1 one revision back
A1@-5 # value of A1 five revisions back
A1@dt"2026-04-10T15:30Z" # value of A1 at the given timestampThe syntax parses and lowers correctly on every runtime, but v1
does not implement history-backed evaluation. Resolving a temporal
reference returns an #N/A error with the diagnostic:
Temporal references are not implemented in v1 (
<symbol>@offset). The syntax is reserved; runtime support is planned for a future release.
Both the TS runtime and the Lua runtimes emit this same error so callers see a uniform diagnostic. Treat temporal references as a parser-supported construct that is forward-compatible with the planned history store; do not rely on them for working models in v1.
4. Literals
4.1 Numbers
| Form | Example |
|---|---|
| Integer | 42 |
| Decimal | 3.14, .5, 1. |
| Scientific | 1e3, 2.5E-4 |
| Underscored | 1_000_000, 0.000_001 |
| Hex | 0xFF |
| Binary | 0b1010 |
| Octal | 0o755 |
4.2 Strings
| Form | Example | Notes |
|---|---|---|
| Double-quoted | "hello" |
Escape quote with "" |
| Triple-quoted | """line 1\nline 2""" |
Preserves newlines |
| Raw | raw"C:\temp" |
No backslash escapes |
| Raw triple | raw"""...""" |
Both raw and multiline |
| Backtick | `hello {A1}` |
Interpolation with {expr} |
Backtick strings support format specifiers via ::
`Total: {A1:$#,##0.00}` # desugars to TEXT(A1, "$#,##0.00")Strings always use double quotes. Single quotes are reserved for sheet qualifiers.
4.3 Booleans
TRUE
FALSE
true # case-insensitive
False4.4 Blank
BLANK # explicit blank valueBLANK is distinct from the empty string "". Blank cells coerce to
0 numerically and FALSE logically. Use IS BLANK to test.
4.5 Symbol Literals
Enum-like labels, prefixed with ::
:draft
:pending
:approved
:graySymbols are stored as strings with typeTag = "symbol".
4.6 Typed String Literals
Short tagged strings parse to a typed value:
| Prefix | Example | Result type |
|---|---|---|
date / d |
date"2026-04-10", d"2026-04-10" |
date |
datetime / dt |
dt"2026-04-10T15:30:00Z" |
date |
duration / dur |
dur"P1D", duration"PT15M" |
duration |
cron |
cron"0 9 * * 1-5" |
cron |
glob |
glob"report-*.csv" |
string (glob pattern) |
wild |
wild"inv-????-*.csv" |
string (wildcard) |
raw |
raw"^\d+$" |
string (no escapes) |
url |
url"https://example.com/api?q=1" |
object (typeTag url) |
mol / molecule |
mol"H2O", molecule"C8H10N4O2" |
object (typeTag molecule) |
dna |
dna"ATCG" |
object (typeTag dna) |
color / colour |
color"#ff8800", color"rgb(255,136,0)" |
object (typeTag color) |
note |
note"A4", note"C#5" |
object (typeTag note) |
bin / bytes |
bin"DEADBEEF", bytes"00112233" |
object (typeTag bytes) hex |
b64 |
b64"SGVsbG8=" |
object (typeTag bytes) base64 |
Domain literals carry both the parsed payload and the original textual
form, so URL_PATH(url"https://x/y") resolves without re-parsing while
COERCE("https://x/y", url) still produces the same canonical value.
4.7 Structured Literals
Parse JSON / YAML / TOML / XML directly into object values:
A1 = json"""{"a": 1, "nested": {"b": true}}"""
A2 = yaml"""
a: 1
nested:
b: true
"""
A3 = toml"""title = "TOML"
[owner]
name = "Tom"
"""
A4 = xml"""<root foo="1"><bar>true</bar></root>"""4.8 Delimited Table Literals
A1 = csv"""
name,amount
Alice,10
Bob,20
"""
A2 = tsv"""
name amount
Alice 10
Bob 20
"""These spill into a rectangular array.
4.9 Regex Literals
JavaScript-style:
/^INV-\d+$/ # no flags
/^Order #(\d+)/i # case-insensitive4.10 Suffix Literals
| Category | Examples | typeTag |
|---|---|---|
| Duration | 30s, 15min, 6h, 3d, 2w, 4mo, 1y, 250ms |
duration |
| Basis points | 25bp, 25bps |
basis_points |
| Percentage | 21pct, 5pct |
percentage |
| Angle | 45deg, 1.57rad |
angle |
| Data size | 512KB, 2GB, 1TB |
data_size |
| Currency / 3-letter codes | 100USD, 50EUR |
unit:USD, unit:EUR |
The full duration suffix list is ms, s, min, h, d, w, mo, y.
Any uppercase 3-letter code is parsed as a unit literal (100ABC →
typeTag = "unit:ABC").
4.11 Array Literals
[1, 2, 3] # row vector
[1; 2; 3] # column vector
[1, 2; 3, 4] # 2x2 matrix
[1, 2, 3, ...A1:A5] # spread an existing range into the literalArrays may spread other arrays or ranges with ....
4.12 Error Literals
Nine error codes are writable in source:
#N/A #DIV/0! #VALUE! #REF!
#NAME? #NULL! #NUM! #CALC! #SPILL!Two more codes (#TYPE!, #CIRC!) exist only as runtime-generated
values; the parser rejects them as literals. See
errors.md for what each error means and when it fires.
4.13 Object Literals
Curly braces build records (key→value maps). Keys may be bare identifiers or double-quoted strings; values are any expression.
{} # empty object
{name: "Ada", level: 9} # bare-identifier keys
{"first name": "Ada", "level": 9} # quoted keys (any string)
{id: row@"id", total: row@"amount" * row@"fx"} # values can be expressions
{owner: customer, address: customer.address} # values can be other objectsObjects share their access syntax with arrays — both work through dot,
bracket, and INDEX:
person = {name: "Ada", role: "engineer"}
person.name # "Ada" (dot access)
person["role"] # "engineer" (bracket access)
INDEX(person, "name") # "Ada" (function form)
person?.missing # BLANK (optional chaining)Pair object literals with OBJECTS to iterate
table rows by header name; see section 6.15 for the
full table-comprehension pattern.
Object literals participate in the value system as the object kind (see
5.1 Value Kinds). Like arrays, they may be nested inside
other objects or inside arrays.
4.14 Imaginary / Complex Literals
A trailing i (or j) suffix on a numeric literal makes it imaginary:
A1 = 5i # 0 + 5i kind=complex, typeTag=complex
A2 = 3 + 4i # 3 + 4i
A3 = (2.5e-3)i # 0 + 0.0025i
A4 = 1i ^ 2 # -1 + 0i integer powers stay exactComplex values are a first-class ValueKind (see
5.1 Value Kinds) and participate in +, -, *, /,
and ^ directly — no need for IMSUM/IMSUB. Ordering operators
(<, <=, >, >=) and MIN/MAX reject complex operands.
Build complex values explicitly with MAKE_COMPLEX(re, im) and decompose
them with REAL, IMAGINARY, MODULUS, ARGUMENT, and CONJUGATE. The
legacy Excel-compatible IM* family still accepts both typed complex
values and "a+bi" strings.
4.15 Domain Object Literals
The typed-string prefixes from 4.6 cover six domain object kinds — URL, molecule, DNA, color, musical note, and binary bytes. Each prefix dispatches to a domain parser that returns a structured object with a stable shape, so downstream functions operate on parsed fields rather than re-parsing on every call:
A1 = url"https://example.com/api?q=1"
A2 = URL_HOST(A1) # "example.com"
A3 = URL_QUERY_PARAM(A1, "q") # "1"
B1 = mol"C8H10N4O2" # caffeine
B2 = MOL_MASS(B1) # 194.19...
B3 = MOL_ATOM_COUNT(B1, "N") # 4
C1 = dna"ATCGGCTA"
C2 = DNA_GC(C1) # 0.5
C3 = DNA_REVCOMP(C1) # dna value: TAGCCGAT
D1 = color"#ff8800"
D2 = COLOR_R(D1) # 255
D3 = COLOR_LUMINANCE(D1) # 0.55... (Rec.709)
E1 = note"A4"
E2 = NOTE_FREQUENCY(E1) # 440
E3 = NOTE_MIDI(E1) # 69
F1 = bin"DEADBEEF" # bytes value, length 4
F2 = BYTES_TO_BASE64(F1) # "3q2+7w=="Domain values round-trip through Redis (the canonical text form is stored
under the cell's _values slot, the structured payload is reconstructed
from _types).
5. Type System
5.1 Value Kinds
Every Grid value has a kind — the underlying primitive shape:
| Kind | Holds |
|---|---|
blank |
Absence of a value |
number |
A double-precision float |
string |
UTF-8 text |
boolean |
true or false |
date |
An ISO-8601 instant |
array |
A 2D rectangular grid of typed values |
object |
A structured value (JSON-like) |
complex |
A complex number re + im·i (both parts are doubles) |
error |
A typed error with code and message |
The complex kind is unorderable — relational operators (<, <=, >,
>=) and MIN/MAX raise #NUM! on complex operands. Equality and
arithmetic (+, -, *, /, ^) are fully supported.
5.2 Type Tags
A type tag is a semantic label layered on top of a kind. The same
number kind can carry many type tags:
| Tag | Kind | Meaning |
|---|---|---|
number |
number |
Plain number |
currency |
number |
Currency amount (display with currency symbol) |
amount |
number |
Generic monetary or quantity amount |
percentage / percent |
number |
Percentage (display as %) |
basis_points / bps |
number |
Basis points |
score |
number |
Score (0..1 typical) |
rate |
number |
Generic rate |
fx_rate |
number |
Foreign exchange rate |
<anything>_rate |
number |
Any tag ending in _rate is treated as a numeric rate |
integer |
number |
Integer-valued |
decimal |
number |
Decimal |
angle |
number |
Angle (radians or degrees) |
data_size |
number |
Byte count |
unit:XXX |
number |
Any three-letter coded unit (e.g. unit:USD, unit:EUR) |
string |
string |
Plain text |
symbol |
string |
Enum-like label (from :foo literal) |
duration |
string |
ISO-8601 duration |
cron |
string |
Cron expression |
regex |
object |
Compiled regex |
lambda |
object |
A LAMBDA function value |
uncertainty |
object |
Mean ± uncertainty value (from ± operator or UNCERTAIN()) |
date / datetime |
date |
ISO-8601 instant |
csv / tsv |
array |
Delimited table |
json / yaml / toml / xml |
object |
Parsed structured value |
complex |
complex |
Complex number { re, im } (from Ni literal or MAKE_COMPLEX) |
url |
object |
Parsed URL { scheme, host, port, path, query, fragment, raw } |
molecule |
object |
Chemical formula { formula, atoms } (from mol"…") |
dna |
object |
DNA sequence { sequence } over ACGTN (from dna"…") |
color |
object |
Color { r, g, b, a } 0–255 + alpha 0–1 (from color"…") |
note |
object |
Musical note { pitch, accidental, octave, midi } (from note"…") |
bytes |
object |
Byte blob { bytes: number[] } (from bin"…" / b64"…") |
error:XXX |
error |
Error with code XXX |
Type tags can be applied at assignment time:
A1 as currency = 100000
A2 as percentage = 21pct
A3 as date = d"2026-04-10"The runtime validates compatibility: A1 as currency = "hello" is
rejected with #TYPE!.
5.3 Coercion Rules
When a function expects a kind that doesn't match what's passed, Grid applies these rules:
| From → To | Rule |
|---|---|
string → number |
Number(s); NaN becomes #VALUE! |
boolean → number |
TRUE → 1, FALSE → 0 |
blank → number |
0 |
number → boolean |
non-zero → TRUE, zero → FALSE |
string → boolean |
non-empty → TRUE, empty → FALSE |
blank → boolean |
FALSE |
number / boolean → string |
String(value) |
blank → string |
"" |
error → anything |
propagates as the error value |
Errors short-circuit. A function that receives an error argument
returns that error unchanged unless the function is explicitly
error-handling (IFERROR, ISERROR, TRY, etc.).
6. Expressions
6.1 Function Calls
SUM(A1:A3)
ROUND(3.14159, 2)
CONCAT("hello", " ", "world")
SUM(1, 2, 3,) # trailing comma is allowed
INTERPOLATE.LINEAR(x, xs, ys)
Z.TEST(arr, 0, 1)Function names are case-insensitive. The canonical style uses
uppercase. Trailing commas are accepted in function calls, array
literals, and MATCH arms — handy for templated codegen.
Function names may include dots (INTERPOLATE.LINEAR, Z.TEST,
F.DIST, etc.). The dot is part of the identifier and not a property
access. The full set of dotted function names is in
functions.md.
A function name with .( instead of ( invokes the broadcast
form, which applies the function elementwise across array arguments:
SIN.([1, 2, 3]) # MAP([1,2,3], SIN(_))
ROUND.(prices, 2) # ROUND each element of prices to 2 digits6.2 Named Arguments
ROUND(number := 3.14159, digits := 2)
FX_RATE(base := "EUR", quote := "USD")Named arguments may appear in any order. Mixing positional and named arguments is allowed; positional first.
6.3 Arrow Lambdas
For higher-order functions:
x => x * 2
(acc, x) => acc + x
(units, price) => ROUND(units * price, 2)6.4 LAMBDA (Excel-style)
LAMBDA(x, x * 2)
LAMBDA(x, y, x + y)LAMBDA returns a function value usable by MAP, REDUCE, SCAN,
MAKEARRAY, BYROW, BYCOL, etc.
6.5 Placeholder Lambdas
Inside higher-order helpers and pipes, _, _1, _2 are placeholders:
MAP(A1:A3, _ * 2)
REDUCE(0, A1:A3, _1 + _2)
A1 >> ROUND(_, 2)Outside higher-order helpers and pipes, a standalone placeholder
expression like ADD(5, _) produces a LAMBDA(__p1, ADD(5, __p1))
function value, not an evaluated expression.
6.6 Conditional (THEN ... ELSE)
A1 > 0 THEN "positive" ELSE "non-positive"For multiple tiers, chain right-to-left (single line):
score >= 0.9 THEN "A" ELSE score >= 0.7 THEN "B" ELSE score >= 0.5 THEN "C" ELSE "F"If the expression gets too long for one line, switch to CASE WHEN
or extract intermediates with DO ... END.
The C-style ?: form is also accepted (A1 > 0 ? "yes" : "no") but
not preferred.
6.7 Null Coalescing
A1 ?? 0 # if A1 is BLANK or error, return 0
A2 DEFAULT "n/a" # canonical form, same effectDEFAULT is the canonical spelling; ?? is accepted sugar.
6.8 Pattern Matching With MATCH
MATCH(status, "active" -> 1, "hold" IF score > 10 -> 2, _ -> 0)Branches are <pattern> -> <result>, optionally guarded by IF. The
last _ arm matches anything. MATCH is a function call, so its arms
can wrap across lines for readability — see
§14.1:
MATCH(status,
"draft" -> :gray,
"pending" -> :amber,
"approved" -> :green,
_ -> :red
)6.9 SQL-Style CASE WHEN
CASE WHEN score >= 90 THEN "A"
WHEN score >= 80 THEN "B"
WHEN score >= 70 THEN "C"
ELSE "F"
ENDDesugars to IFS(...). Use this when each branch tests a different
condition. Use MATCH when each branch compares the same subject.
6.10 Local Bindings With DO ... END
DO
revenue = SUM(A1:A12)
cost = SUM(B1:B12)
revenue - cost
ENDThe last expression is the value of the block. Single-line form:
DO revenue = SUM(A1:A12); cost = SUM(B1:B12); revenue - cost ENDDesugars to LET(revenue, SUM(A1:A12), cost, SUM(B1:B12), revenue - cost).
6.11 LET (Excel-Style)
LET(x, 5, y, x + 1, x * y)DO ... END is preferred for readability; LET is the lower-level
form that DO desugars to.
6.12 WITH Expressions (Error-Tolerant Chains)
WITH data = HTTP_JSON(url), users = data.results
THEN users[1].name ELSE "unavailable"Desugars to nested IFERROR(LET(...), fallback). Use this for
external-data chains where any step might fail.
Bindings may span lines (with
,at end-of-line), butTHEN <expr> ELSE <fallback>must be on a single line.
6.13 Pipes
Forward-pipe >>:
A1 >> ABS() >> ROUND(2) # ROUND(ABS(A1), 2)
A1 >> ROUND(_, 2) # ROUND(A1, 2) with explicit slot
A1 >> IF(_ > 0, _, 0) # IF(A1 > 0, A1, 0)Conditional pipe (a regular pipe with a trailing IF):
data >> SORT(_) IF LEN(data) < 1000 # IF(LEN(data) < 1000, SORT(data), data)When the trailing IF condition is false, the original value is
passed through unchanged. The same >> operator handles both the
unconditional and conditional forms.
Pipe inspection TAP:
data >> TAP >> SORT(_) # logs data, passes through
data >> TAP("label") >> SORT(_) # labeled tap6.14 Sequence Shorthand
1..10 # 1, 2, 3, ..., 10 (inclusive)
1..<10 # 1, 2, 3, ..., 9 (half-open)
1..2..10 # 1, 3, 5, 7, 9 (with step)
1..<2..10 # 1, 3, 5, 7, 9 (half-open with step)
date"2026-01-01"..date"2026-01-03"
date"2026-01-01"..duration"P2D"..date"2026-01-05"
d"2026-01-01"..3d..d"2026-01-07"6.15 List Comprehensions
Comprehensions desugar to single-cell array calculations, so a comprehension
of any size occupies one dependency-graph node — much cheaper than a range
broadcast like A1:A1000 = ....
[x * 2 FOR x IN A1:A10] # 1D map: returns an N-element column
[x FOR x IN A1:A10 IF x > 0] # 1D filter: keeps elements where the predicate is true
[r * c FOR r IN 1..3, c IN 1..4] # 2D: produces a true 3×4 grid
[r * c FOR r IN 1..3, c IN 1..3 IF r <= c] # 2D + IF: cells where the predicate is false become BLANKLowering rules:
[body FOR x IN src]→MAP(src, LAMBDA(x, body)).[body FOR x IN src IF cond]→FILTERoversrcusing a boolean mask built fromcond, thenMAPover the surviving elements.FILTERreturns a row, so a filtered 1D comprehension is a 1×N row even whensrcis a column.[body FOR r IN s1, c IN s2]→MAKEARRAY(rows(s1), rows(s2), …)— a proper 2D grid; not a "column of rows".[body FOR r IN s1, c IN s2 IF cond]→ same 2D shape, with cells wherecondis false replaced byBLANK. This preserves a rectangular array; use a single-generator filter when you want a shorter result.
Three or more generators are rejected at parse time. Use nested
comprehensions or MAKEARRAY directly for higher dimensions.
Comprehensions Over A Table
When the source is a table-with-headers (a 2D array whose first row names the
columns), wrap it with OBJECTS to iterate row-by-row using the header
names instead of column indices. Combining OBJECTS with object literals
gives a SQL-like project / filter:
data = ["id", "amount", "fx", "status";
1, 100, 1.0, "active";
2, 200, 1.1, "inactive";
3, 50, 0.9, "active"]
# SELECT id, amount * fx AS total FROM data WHERE status = "active"
totals = [{id: r.id, total: r.amount * r.fx}
FOR r IN OBJECTS(data)
IF r.status = "active"]
# -> [{id: 1, total: 100}, {id: 3, total: 45}]OBJECTS is the natural inverse of writing a table as [headers; row1; row2; …]. See functions.md for the full signature.
6.16 Property And Index Access
Dot and bracket access work uniformly across arrays (numeric index, 1-based)
and objects (string field name). Both lower to INDEX so the underlying
function dispatches by the source's value kind:
arr = [10, 20, 30]
arr[2] # 20 (array, 1-based)
obj = {name: "Ada", role: "engineer"}
obj.name # "Ada" (object, identifier key)
obj["role"] # "engineer" (object, string key)
INDEX(obj, "name") # "Ada" (function form)
HTTP_JSON(url)["results"] # works on JSON-shaped objects
HTTP_JSON(url).results
HTTP_JSON(url)?.results # optional chaining: BLANK if missing
HTTP_JSON(url)?["results"]Dot access uses an identifier-shaped key (obj.name); for keys that aren't
valid bare identifiers (with spaces, leading digits, etc.) use the bracket
form: obj["first name"].
Tip — to avoid ambiguity with Excel structured references like
Sales[Revenue], use a quoted string when bracket-accessing an object:
obj["Revenue"] (object access, returns the field) versus Sales[Revenue]
(structured reference, returns the named column).
6.17 Slicing And Negative Indexing
data[2:5] # elements 2 through 5 (1-indexed, inclusive)
data[:3] # first 3
data[3:] # from index 3 to end
data[^3:] # last 3 (^N = from end)
data[-1] # last element
data[1:10:2] # every 2nd element from 1 to 106.18 Set Operators
[1, 2, 3] UNION [3, 4, 5] # [1, 2, 3, 4, 5]
[1, 2, 3] INTERSECT [2, 3, 4] # [2, 3]
[1, 2, 3] EXCEPT [2] # [1, 3]
[1, 2, 3] EXCLUDE [2] # [1, 3] — same as EXCEPTEXCEPT and EXCLUDE are synonyms.
6.19 Reduction Operators (APL-Style)
+/ A1:A10 # SUM(A1:A10)
*/ A1:A10 # PRODUCT(A1:A10)
&/ A1:A10 # CONCAT(A1:A10)6.20 Broadcast Dot
Apply a function elementwise without MAP:
SIN.([1, 2, 3]) # MAP([1,2,3], SIN(_))
ROUND.(data, 2) # ROUND elementwise across data6.21 Spaceship Comparison
A1 <=> B1 # -1 if A1 < B1, 0 if equal, 1 if A1 > B16.22 Expression Clauses
amount UNLESS amount = 0
total * rate WHERE total = SUM(A1:A10), rate = 0.1
total * rate USING total AS SUM(A1:A10), rate AS 0.1
value ASSERT value > 0
value ASSERT value > 0 ELSE #N/A
customer WITH KEYS "id", "name", "email"
customer WITHOUT KEYS "ssn", "password_hash"| Clause | Meaning |
|---|---|
UNLESS |
If condition true, return BLANK; else return value |
WHERE |
Bind names visible in the value expression (uses =) |
USING |
Same as WHERE but uses AS instead of = for binding |
ASSERT |
If condition false, return #VALUE! (or ELSE clause) |
WITH KEYS |
Project an object or 2D-array-with-headers onto the listed keys (desugars to PICK) |
WITHOUT KEYS |
Drop the listed keys from an object or 2D-array-with-headers (desugars to OMIT) |
WHERE and USING both desugar to LET(...). Pick whichever reads
better:
total + tax WHERE total = SUM(A1:A10), tax = total * 0.21
total + tax USING total AS SUM(A1:A10), tax AS total * 0.21WITH KEYS and WITHOUT KEYS both work on objects and on 2D arrays
that carry a header row. They are clause sugar over the PICK and
OMIT functions, with the same shape preservation rules:
customer WITH KEYS "id", "name" # = PICK(customer, "id", "name")
customer WITHOUT KEYS "ssn" # = OMIT(customer, "ssn")
purchases WITH KEYS "id", "amount" # 2D array → 2D array, header row preservedThe bindings list may span lines (after each ,), but the value
expression and the clause keyword must be on one line each.
6.23 Type And Text Predicates
value IS BLANK
amount IS NOT NUMBER
started_at IS DATE
email ENDS WITH "@acme.com"
name STARTS WITH "Jo"
notes CONTAINS "urgent"
customer ILIKE "jo%"
customer NOT ILIKE "x%"Type predicates: IS BLANK, IS NUMBER, IS STRING, IS BOOLEAN,
IS DATE, IS ARRAY, IS OBJECT, IS ERROR, IS COMPLEX, IS URL,
IS MOLECULE, IS DNA, IS COLOR (alias IS COLOUR), IS NOTE,
IS BYTES. Each accepts IS NOT.
Domain predicates check the value's typeTag (so mol"H2O" IS MOLECULE
is TRUE, but a plain string like "H2O" IS MOLECULE is FALSE).
IS COMPLEX matches the underlying complex ValueKind regardless of
how the value was constructed.
6.24 Structured Table References
Sales[Revenue] # equivalent to TABLE_REF("Sales", "Revenue")6.25 Partial Application
When a placeholder appears in a function call outside a higher-order context:
ADD(5, _) # = LAMBDA(__p1, ADD(5, __p1))The result is a function value. To actually evaluate immediately, provide all arguments.
6.26 QUERY (SQL-Like)
QUERY(data, "SELECT * WHERE col1 > 5 ORDER BY col2 DESC LIMIT 10")Supports SELECT, WHERE, ORDER BY, LIMIT. Use this for tabular
filtering when an array is shaped like a table.
6.27 Header Access (@, @[…], @![…])
The postfix @ operator selects by header name on a 2D array, or by
key on an object. Three shapes are supported:
data@"price" # single column / key: COLUMN_BY_HEADER(data, "price")
data@["id", "name", "email"] # project to a key list: PICK(data, "id", "name", "email")
data@!["ssn", "secret"] # drop a key list: OMIT(data, "ssn", "secret")| Form | Desugars to | Source kinds | Result kind |
|---|---|---|---|
expr @ key |
COLUMN_BY_HEADER(expr, key) |
array |
array (single column) |
expr @ [k1, k2, …] |
PICK(expr, k1, k2, …) |
object or array |
same kind, projected |
expr @ ! [k1, k2, …] |
OMIT(expr, k1, k2, …) |
object or array |
same kind, drop listed |
For the bracketed forms the keys are any primary expressions —
typically string literals, but also references, function calls, etc.
Spaces around @, !, and the brackets are all permitted.
Examples:
revenue = data@"amount"
slim = customer@["id", "email"]
audit = customer@!["password_hash", "ssn"]
top10 = SORT(data@["product", "revenue"], 2, FALSE)[1:10]6.28 Domain Operators (%OF, TO)
Two binary infix operators express common domain math:
25 %OF 200 # PERCENTOF(25, 200) → 0.125 (25 is 12.5% of 200)
100 TO 200 # GROWTH(100, 200) → 1.0 (100% growth from 100 to 200)%OF is the percentage-of operator (left as a fraction of right).
TO is the growth-rate operator (relative growth from left to right).
6.29 Combinatoric Operators
Three infix operators desugar to combinatoric functions:
5 CHOOSE 2 # COMBIN(5, 2) → 10 (binomial coefficient)
5 PERMUTE 2 # PERMUT(5, 2) → 20 (permutations)
5 MULTICHOOSE 2 # COMBINA(5, 2) (multiset coefficient)The
CHOOSEoperator is not the same as theCHOOSEExcel function (which picks an element by index). The infixCHOOSEis always a binomial coefficient. TheCHOOSE(...)function call is a selector. Context disambiguates.
6.30 Tolerance / Uncertainty
A value can carry an explicit ± or +/- uncertainty:
100 ± 5 # UNCERTAIN(100, 5)
100 +/- 5 # same
weight ± measurement_errorThe result is an object value with mean and uncertainty fields.
6.31 CLAMP Postfix
Constrain a value to a range with a postfix CLAMP[lo, hi]:
score CLAMP[0, 1] # CLAMP(score, 0, 1)
A1 + 100 CLAMP[0, 50] # CLAMP(A1 + 100, 0, 50)CLAMP is a comparison-level postfix; it binds tighter than logical
operators but looser than arithmetic.
6.32 TRY With Multiple Branches
TRY accepts a chain of THEN branches; each is tried in order, and
the first one that doesn't return an error wins. An optional ELSE
provides the final fallback (default is #N/A):
TRY 1 / divisor ELSE 0 # single branch
TRY primary() THEN backup() ELSE "n/a"
TRY a/b THEN c/d THEN e/f ELSE 0 # try a/b first, then c/d, then e/f, finally 0Desugars to nested IFERROR(body, IFERROR(branch1, IFERROR(branch2, fallback))).
7. Operator Precedence
Highest to lowest. Within a row, operators are listed by associativity.
| # | Level | Operators |
|---|---|---|
| 1 | Unary / reduction | @ (implicit intersection), + - ! NOT, +/ */ &/ |
| 2 | Member / index / header access | .field ?.field ["key"] ?["key"] [start:end] [start:end:step] [^n] @key @[k1, k2, …] @![k1, k2, …] |
| 3 | Postfix arithmetic | % ! !! |
| 4 | Power | ^ ** (right-associative) |
| 5 | Combinatoric | CHOOSE PERMUTE MULTICHOOSE |
| 6 | Multiplicative | * / // %% |
| 7 | Domain | %OF TO |
| 8 | Additive | + - (with optional ± / +/- uncertainty postfix) |
| 9 | Concatenation | & |
| 10 | Comparison + suffixes | = <> != < <= > >= <=> IN NOT IN LIKE NOT LIKE ILIKE NOT ILIKE STARTS WITH NOT STARTS WITH ENDS WITH NOT ENDS WITH CONTAINS NOT CONTAINS BETWEEN NOT BETWEEN IS IS NOT CLAMP[lo, hi] |
| 11 | Set | UNION INTERSECT EXCEPT EXCLUDE |
| 12 | Logical | AND &&, XOR, OR || |
| 13 | Null coalescing | ?? DEFAULT |
| 14 | Conditional | THEN ... ELSE |
| 15 | Pipe | >> (with optional trailing IF cond) |
| 16 | UNLESS clause | UNLESS |
| 17 | ASSERT clause | ASSERT |
| 18 | WHERE / WITH KEYS clauses | WHERE, USING, WITH KEYS, WITHOUT KEYS |
To force a different order, use parentheses.
The
^operator is right-associative.2^3^2parses as2^(3^2) = 512, not(2^3)^2 = 64. This matches Excel.
8. Implicit Intersection (@)
When a single-cell context references a range, prefix with @ to
collapse the range to the intersected cell:
B1 = @A:A # the cell in column A on the same row as B1This matches Excel's implicit intersection behavior introduced alongside dynamic arrays.
9. Evaluation Modes
| Operator | Mode | When the cell evaluates |
|---|---|---|
= |
Eager | Whenever any input symbol changes |
~= |
Lazy | Only when something reads it |
?= |
Conditional eager | Same as =, but skip the assignment if the RHS is an error |
+= -= *= /= |
Eager update | Single-cell mutation: A1 += 5 ≡ A1 = A1 + 5 |
Compound assignments and conditional eager are not supported inside rule action bodies.
9.1 Decorators And Protected Mode
| Decorator | Allowed on | Effect |
|---|---|---|
input |
Plain = only, with a default value, top-level statements only |
Cell is the model's external write surface |
output |
Every assignment shape (eager, lazy, conditional, compound, scheduled, range, destructuring, rule-action) | Cell is the model's external read surface |
When a model is deployed with protectedMode: true, the runtime
rejects any setInput to a non-input symbol and any resolve /
getRange of a non-output symbol. Deploys also fail if any
implicit input (a referenced-but-unassigned symbol) is missing an
input declaration, or if the model declares neither input nor
output.
See assignments.md for full coverage.
10. Execution Classes
Each function declares an execution class:
| Class | Where it runs |
|---|---|
lua_sync |
Inside Redis (compiled to Lua) or in the TS runtime; returns a value synchronously |
external_async |
In a worker process via the job queue; writes back asynchronously |
Today three external functions are built in: FX_RATE, HTTP_JSON,
ML_SCORE. See external-functions.md for
the async semantics.
A model with an external_async call may still be RUNTIME "lua_generated":
the compiler lowers the external call into a synthetic symbol, which
the worker resolves and writes back through the runtime.
10.1 TS ↔ Lua Parity Caveats
For the portable subset (lua_sync functions, all literals, all
operators, and DO/CASE WHEN/MATCH/WITH/TRY constructs), the TS and
Lua runtimes are designed to produce byte-equivalent results.
A formal parity test corpus is on the v1 roadmap (see
V1_SCOPE.md axis A) but has known gaps today.
Current known divergences:
| Function / construct | Divergence |
|---|---|
LEN(s) |
Lua counts bytes (string.len), TS counts UTF-16 code units (s.length). Multi-byte characters disagree. |
MID, LEFT, RIGHT |
Same byte-vs-codeunit issue when strings contain multi-byte characters. |
IF(condition, ...) truthiness |
Both runtimes treat 0, "", BLANK, FALSE, and any error as falsy. Other values are truthy. |
| Numeric edge cases | Infinity, -Infinity, NaN are coerced to #NUM! in some Lua paths but pass through in TS. |
| Float representation | Both use IEEE 754 doubles; rounding-display gotchas apply equally. |
RAND, RANDBETWEEN, NOW, TODAY |
Marked volatile; values won't match across runtimes for the same call (use deterministic inputs in tests). |
Until parity tests are in place, treat these as caveats when porting Excel-derived models. For ASCII-only text and numeric workloads, the two runtimes are interchangeable in practice.
11. Spilling
A formula returning an array writes into a rectangle anchored at its target.
A1 = [10, 20, 30] # spills into A1, A2, A3
B1 = A1# * 2 # spills into B1, B2, B3 with values 20, 40, 60Rules:
- The spill rectangle starts at the formula's target cell.
- If a cell in the spill rectangle is already populated, the formula
yields
#SPILL!. A1#refers to the entire spilled rectangle as a single array value.- Implicit broadcasting: scalar-to-array, array-to-array of compatible shape.
12. Array Broadcasting
Two arrays broadcast if they have compatible shapes:
| Shape A | Shape B | Result |
|---|---|---|
| scalar | r×c |
r×c (scalar repeated) |
r×1 |
r×c |
r×c (column broadcast across cols) |
1×c |
r×c |
r×c (row broadcast across rows) |
r×c |
r×c |
r×c (elementwise) |
Incompatible shapes yield #VALUE!.
13. Keywords
Grid keywords come in three tiers based on how strictly the lexer
guards them. Tier A and Tier B keywords are hard-reserved: they
cannot appear as bare identifiers in any binding or reference
position. Tier C words are contextual: their keyword role is
matched first (e.g. IF(…) as a conditional) but the same names can
still bind to user variables.
Identifier-vs-keyword matching is case-insensitive (AND, and,
And are all the logical operator). Function names are also
case-insensitive (SUM, sum, Sum all bind to the same function).
13.1 Tier A — Structural (hard-reserved)
Block scaffolding, schedule modifiers, clause keywords, and header directives. These are never function-callable and may never appear as a bare identifier.
| Group | Words |
|---|---|
| Rule-block scaffolding | WHEN, THEN, END |
| Schedule modifiers | EVERY, AT, ONCE |
| Missed-run policy | SKIP, MISSED, BACKFILL |
| Type-tag introducer | AS |
| Clause / block keywords | ELSE, UNLESS, ASSERT, USING, WHERE, DO, FOR, CASE, TRY, WITH, WITHOUT, KEYS, DEFAULT |
| Assignment decorators | INPUT, OUTPUT |
| Header directives | MODEL, DESCRIPTION, RUNTIME, VERSION, AUTHOR, TAGS |
13.2 Tier B — Operators, Predicates, Literals (hard-reserved)
Logical / set operators, comparison particles, type-predicate
literals, identifier-shaped binary and postfix operators, and the
type-shaped predicate names. Also hard-reserved as bare identifiers;
many of these are also legal function-call names (AND(…), OR(…),
NOT(…), XOR(…), UNION(…), INTERSECT(…), LEFT(…), RIGHT(…),
CHOOSE(…), MATCH(…), NEIGHBORS(…), CLAMP(…), NUMBER(…),
TEXT(…), DATE(…), …) — those calls match at the function-call
rule before identifier resolution is attempted.
| Group | Words |
|---|---|
| Logical | AND, OR, XOR, NOT |
| Comparison particles | IS, IN, BETWEEN, LIKE, ILIKE, CONTAINS, STARTS, ENDS |
| Sequence / range | TO, BY |
| Set | UNION, INTERSECT, EXCEPT, EXCLUDE |
| Literals / type predicates | BLANK, NA, TRUE, FALSE |
| Spatial directionals | ABOVE, BELOW, LEFT, RIGHT |
| Combinatoric / postfix operators | CHOOSE, PERMUTE, MULTICHOOSE, CLAMP |
| Pattern-matching / spatial keywords | MATCH, NEIGHBORS |
IS-predicate type names |
NUMBER, TEXT, STRING, BOOLEAN, DATE, DATETIME, DURATION |
13.3 Tier C — Contextual (callable, not reserved)
These names act as keywords in their syntactic role but remain available as identifiers and as function calls. Use them as identifiers only when you really must — readability still suffers.
IF— used as a function (IF(c, t, e)), as a comprehension filter ([x FOR x IN s IF x > 0]), as a pipe gate (x >> f IF cond), and as a match-arm guard (pat IF cond -> result). Permitted as an identifier because real-world data analysis is full of column names likeif_modified_since,gif, etc.; the contextual disambiguation is reliable.LOGICAL,ERROR— appear only afterIS/IS NOT(the predicate context). Allowed as identifiers because they read naturally as variable names.
Contextual literal prefixes (lowercase, quote-triggered)
These are not reserved as identifiers because they only become
keywords when immediately followed by " or """:
raw, glob, wild, cron, date, datetime, duration, dur,
dt, d, json, yaml, toml, xml, csv, tsv
So date = today() works fine; only date"2026-01-01" triggers the
typed literal.
Contextual suffix units (number-triggered)
These are not reserved as identifiers because they only become units when immediately preceded by a number:
ms, s, min, h, d, w, mo, y, bp, bps, pct, deg,
rad, KB, MB, GB, TB, plus any 3-letter UPPERCASE unit (e.g.
USD, EUR, GBP).
So min = 0 works fine; only 5min parses as a duration literal.
Caveat: in a binding context like LET(min, 5, 10min + 1), the body
expression 10min parses as a unit literal and silently shadows the
min binding. If you bind a variable that shares its name with a
unit, prefer to write the unit form differently (or rename the
variable) to avoid surprise.
13.4 Diagnostics
Using a Tier A or Tier B word as a binding name produces a
PARSE_RESERVED_WORD diagnostic with a precise span over the
offending token. The parser flags the first reserved word it
encounters in a binding position:
A1 = LET(WHEN, 1, WHEN + 2)
^^^^
# `WHEN` is a reserved word in Grid and cannot be used as an identifier.
# Pick a different name (for example, prefix or suffix it), or use the
# keyword in its intended syntactic role.Reserved-word checks fire in every binding or reference position:
assignment targets, named references, LET / WHERE / USING / DO
/ WITH bindings, comprehension iteration variables, lambda
parameters, and destructuring elements.
Reserved-word checks do not fire in label positions, because labels reference existing names rather than introducing new ones:
- Type-tag values (
A1 as number = 1) — the type tag is a label for a type kind, not a binding. Reserved type-shaped words are accepted exactly as users would expect to write them. - Named-argument labels (
ROUND(number := 3.14, digits := 2)) — the label names a function parameter from metadata, not a new variable. Any identifier (including reserved words) is accepted at parse time; the runtime validates the label against the function's parameter list. - Symbol literal atoms (
:end,:active,:number) — the leading colon makes these unambiguously tag-like atoms; reserved words are fine.
Function-call sites (AND(B1, C1), LEFT(text, 3), UNION(a, b),
MATCH(x, 1 -> "a"), NEIGHBORS(2), CLAMP(x, 0, 10)) and the
dedicated grammar slots for keywords (WHEN ... THEN ... END,
EVERY 5min, value as money, x IS NUMBER) are unaffected.
14. Whitespace And Identifiers
- Identifiers match
[A-Za-z_][A-Za-z0-9_.]*. - Whitespace separates tokens within expressions.
- Newlines separate statements at the top level.
14.1 Multi-Line Rules (Important)
Grid is line-oriented at the top level — one statement per line — but
expressions may span multiple lines whenever the parser is inside an
unclosed bracket ((...), [...], or {...}). This is the same
rule Python and JavaScript use, and it gives you room to format long
calls, literals, and comprehensions for readability.
The rule, in one sentence: a newline that appears while any bracket is still open is treated as ordinary whitespace; a newline at the top level always ends the current statement.
| Construct | Multi-line allowed because... |
|---|---|
Function calls f(a, b, c) |
inside (...) |
Parenthesized expressions (a + b * c) |
inside (...) |
Array literals [1, 2, 3] |
inside [...] |
Object literals { key: value } |
inside {...} |
Comprehensions [expr FOR x IN xs IF p] |
inside [...] |
MATCH(subject, a -> b, ...) |
it's a function call |
Bracket access obj[key], slices arr[a:b] |
inside [...] |
Lambda parameter lists (a, b) => |
inside (...) |
DO ... END |
block form, newlines between bindings |
CASE WHEN ... ELSE ... END |
block form, newlines between arms |
WHEN / EVERY / AT rule bodies |
block form, newlines between actions |
WITH <bindings> THEN <expr> ELSE <expr> |
bindings only — see below |
| Top-level statements | newlines separate statements |
Newlines are still not allowed in expressions that aren't bracketed:
- chained
THEN ... ELSEladders (no surrounding brackets to fold) - the
THEN <expr> ELSE <expr>tail of aWITH(the bindings list is newline-friendly; the conclusion is single-line)
Comments inside a bracketed block are fine — # ... line comments
still terminate at the next real newline, and /* ... */ block
comments are passed through verbatim.
Examples:
# OK — wrap long calls across lines
A1 = SUM(
revenue,
-costs,
-taxes,
bonus
)
# OK — multi-line MATCH reads like a table
A2 = MATCH(status,
"draft" -> :gray,
"pending" -> :amber,
"approved" -> :green,
_ -> :red
)
# OK — multi-line object literal
A3 = {
name: "Acme",
founded: 1999,
active: TRUE
}
# OK — comprehension across lines
A4 = [
row.revenue
FOR row IN sales
IF row.region = "NA"
]
# OK — bracket-balanced expression
A5 = (
base
+ adjustment
+ bonus
)
# OK — DO body spans lines (block form)
A6 = DO
x = 1
y = 2
x + y
END
# OK — CASE WHEN arms span lines (block form)
A7 = CASE
WHEN x > 0 THEN "pos"
WHEN x < 0 THEN "neg"
ELSE "zero"
END
# NOT OK — multi-line chained THEN/ELSE (no brackets to fold into)
A8 = score >= 90 THEN "A"
ELSE score >= 80 THEN "B"
ELSE "F"
# Fix: keep on one line, or use CASE WHEN
A8 = score >= 90 THEN "A" ELSE score >= 80 THEN "B" ELSE "F"
A8 = CASE
WHEN score >= 90 THEN "A"
WHEN score >= 80 THEN "B"
ELSE "F"
ENDIf you find yourself wanting to break an expression that isn't
bracketed, either wrap it in (...) to opt into continuation, or
extract intermediate names with DO:
A1 = (
base
+ bonus
- taxes
)
A1 = DO
bonus = MATCH(grade, "A" -> 5000, "B" -> 3000, "C" -> 1000, _ -> 0)
bonus * 1.1
END15. Worked Examples
The seven canonical models cover the language end to end:
| File | Focus |
|---|---|
01-foundations.grid |
Header, types, DO, THEN ELSE, DEFAULT |
02-array-analytics.grid |
MAP, REDUCE, SCAN, MAKEARRAY, BYROW, BYCOL |
03-text-and-quality.grid |
Regex, wildcards, MATCH, TRY, blanks |
04-external-enrichment.grid |
Async functions, lazy cells, named args, fallbacks |
05-rulebook-operations.grid |
WHEN, EVERY, AT, range rule actions |
06-portfolio-risk-engine.grid |
Large portable model: FX, weights, carry, stress |
07-treasury-control-plane.grid |
Very large model: liquidity, schedules, rules. Runs on lua_generated end-to-end. |
Read them in order. Each adds one concept group on top of the previous.
16. JSON-Friendly Grammar Profile
For tooling, the same surface is encoded in
grammar.json. It is the source of truth for
machine-readable consumption.