# Assignments

Assignments are the primary statement form. They define or update addressable
bindings; a cell is the coordinate-addressed kind of binding. See
[`concepts.md`](./concepts.md) for the vocabulary used here.

This doc covers all assignment shapes. For rule blocks, see
[`rules-and-schedules.md`](./rules-and-schedules.md). For the
language as a whole, see [`reference.md`](./reference.md).

---

## 1. Anatomy

```text
<target> IS <type-tag> <op> <expression>
<type-tag> <target> <op> <expression>
```

| Part | Required | Notes |
|---|---|---|
| `<target>` | yes | A cell reference, named value, or finite range |
| `<type-tag>` | no | Semantic or native-kind contract, e.g. `currency`, `List of String`, or `Tensor[?, 4]` |
| `<op>` | yes | At top level, one of `=`, `~=`, `?=`. Compound operators are contextual: arithmetic in rule actions, colon-predicate add/remove (optionally with declared fields), and typed mutations such as table updates. |
| `<expression>` | yes | Any expression — see [`reference.md`](./reference.md#6-expressions) |

Examples:

```text
A1 = 10
A2 is currency = A1 * 100
currency A3 = A1 * 100
A3 ~= EXPENSIVE_FETCH()
A4 ?= MAYBE_FAIL()
A5 += :reviewed
A6:A10 = B1
```

---

## 2. Assignment Operators

### 2.1 `=` (Eager)

The default. The cell recomputes every time any of its inputs change.

```text
A1 = SUM(B1:B10)
```

When any of `B1` through `B10` change, `A1` recomputes immediately.

### 2.2 `~=` (Lazy)

The cell does not compute until something reads it. Useful for
expensive or external work.

```text
A2 ~= ML_SCORE([0.1, 0.2, 0.3, 0.4])
```

Reads happen when the cell is displayed, inspected, or referenced by another
cell. A downstream eager cell that references `A2` triggers `A2` to compute.

Once computed, the value is cached. The cache invalidates when an
input changes, returning the cell to the lazy state.

### 2.3 `?=` (Conditional Eager)

Same as `=`, but if the right-hand side evaluates to an error, the
assignment is skipped — the existing value (if any) is preserved.

```text
A3 ?= MAYBE_FAIL()
```

If `MAYBE_FAIL()` returns `#N/A`, `A3` keeps its previous value rather
than becoming `#N/A`.

### 2.4 Compound Assignments

```text
WHEN increment_requested THEN
  A4 += 5
  B4 -= 5
  C4 *= 2
  D4 /= 2
END
```

These work only on single-cell targets. The right-hand side is
evaluated against the current value of the target.

Arithmetic compound assignment is valid only in rule actions, where the target
has a committed value and the rule transaction supplies a real prior-value
source. At top level, Grid rejects it with
`GRID_TOP_LEVEL_COMPOUND_ASSIGNMENT`; use a reactive rule action for state
changes. Typed mutation contexts such as table updates retain their documented
operator meanings.

### 2.5 Graph handle declarations

`graph` is an exact named-handle declaration rather than a cell type. It uses
plain `=` and a graph-producing expression:

```text
graph roads = GRAPH(edges AS edges, "id" AS edge_id, "from" AS from, "to" AS to)
graph reversed = roads.REVERSE()
```

Graph handles cannot target cells or ranges and do not support decorators,
mutation operators, lazy/conditional assignment, or schedules. See
[Graph Authoring](./graph-authoring.md) for construction and query semantics.

### 2.6 Table mutations (type-dispatched)

When the assignment target is a **`table`-typed named wire**, compound
operators mean **record mutations** against the model's embedded table
store — not arithmetic. Grid applies them as record operations rather
than binary expressions.

| Operator | Meaning on `table` symbol |
|---|---|
| `+=` | Insert row(s) |
| `^=` | Upsert (merge on row `_id`) |
| `*=` | Update (patch by `_id` or filter) |
| `-=` | Delete (by `_id` or filter) |

```text
table Orders += { name: "Acme", amount: 100 }
table Orders += COLLECT(inbound_frame)
table Orders ^= { id: "rec01HXYZ", status: "Shipped" }
table Orders *= { id: "rec01HXYZ", amount: 200 }
table Orders -= { id: "rec01HXYZ" }
```

Inside rule actions, cells and numeric named wires use `+=` / `-=` / `*=` /
`/=` for arithmetic read-modify-write. Ordinary top-level arithmetic compound
assignment is rejected. `^=` remains the table upsert operator in this typed
mutation context; exponentiation remains an expression operator.

A standalone top-level table-mutation statement is an explicit host command:
it commits once when submitted, rather than becoming a recalculating formula.
The same four forms are valid inside `WHEN`/`EVERY`/`AT` bodies. There the RHS
uses the rule's pre-commit snapshot and the durable table write runs after the
rule's atomic cell-state commit.

---

## 3. Targets

### 3.1 Single Cell

```text
A1 = 10
$A$1 = 10
Sheet1!A1 = 10
'Q4 Revenue'!B2 = 10
Revenue = 10                 # named reference
```

#### Workbook surfaces

Surfaces are namespaces that store **component values** with the same
**namespace specifier** syntax as sheets (`Namespace!Component`).

```text
Dataset_1!type = "dataset"
Dataset_1!config = """
version = 1
kind = "dataset"
title = "Dataset_1"
...
"""
Dataset_1!layout = ""
Dataset_1!data = ""
```

The tab **Dataset_1** appears when `Dataset_1!type` names a known surface
kind. The frontend reads component values (especially `!config`) and renders
the matching surface UI.

See [`../product/surfaces.md`](../product/surfaces.md) for the surface families.

### 3.2 Named Address Aliases

A plain named assignment whose entire right-hand side is one absolute reference
adds another address for the existing binding:

```text
A1 = 125000
Revenue = A1
ReportedRevenue = Revenue
```

`A1`, `Revenue`, and `ReportedRevenue` address the same reactive binding. An
external write or rule override through any of those names changes the value
seen through all of them. They also share history and explanation identity;
the aliases do not add calculation steps.

This rule applies only when the target is a name. Coordinate-to-coordinate
assignment retains spreadsheet behavior:

```text
B1 = A1                    # a second cell whose formula reads A1
Tax = Revenue * tax_rate   # a separate named calculation
```

The direct-reference form is deliberately recognizable from source. It is an
alias when all of the following are true:

- the target is a named address;
- the right-hand side is exactly one absolute cell or named reference;
- the assignment is ordinary eager `=`;
- it has no type contract, `default`, `STATE`, schedule, lazy mode, or
  conditional mode.

`input` and `output` only declare the external surface, so they may label an
alias without creating another calculation:

```text
output Revenue = A1
input Assumption = B2
```

When a name needs its own type, state, schedule, or error-preservation policy,
Grid creates an independent named binding even if the expression is a single
reference. Unary predicates remain properties of the authored name rather
than flowing through the aliased value.

### 3.3 Finite Ranges

A range target broadcasts the right-hand side to each cell in the
range. The range must be a finite rectangle.

```text
A1:A10 = B1                  # writes B1 to each of A1..A10
A1:C3 = 0                    # writes 0 to all 9 cells
A1:A5 = [10, 20, 30, 40, 50] # writes the array elementwise
```

Full-column (`A:A`) and full-row (`1:1`) targets are **not** allowed.

### 3.4 Destructuring

Bind multiple symbols from a single array-returning expression:

```text
[total, avg, min, max] = SUMMARY(B1:B10)
```

Rest capture binds the tail:

```text
[head, ...tail] = ARRAY_OF_VALUES
```

The number of named bindings before `...` must not exceed the array
length.

---

## 4. Type Tags

A type tag attaches semantic meaning to the value. The grammar is:

```text
<target> IS [A | AN] <type-tag> = <expression>
```

Equivalently, prefix notation is accepted:

```text
<type-tag> <target> = <expression>
```

```text
A1 IS currency = 100000
A1 IS A currency = 100000
A1 IS AN fx_rate = 1.0825
currency A1 = 100000
currency x = 10
A2 is percentage = 21pct
A3 is date = d"2026-04-10"
A4 is bps = 25bps
A5 is score = ML_SCORE(features)
A6 is unit:m/s = distance / duration
```

Under [`strict`](./reference.md#54-strictness--strict--loose), a value that
cannot inhabit the tag's representation root is a `#TYPE!` — tagging
`is currency` with a word-string errors, while a numeric string still
coerces. `loose` (the default) applies the tag as an overlay without
checking. This rides the coercion axis, so `strict except coercion` turns
it off.

Tags form a **hierarchy** (`USD` is a `currency` is a `number`), and a leaf
implies its ancestors — `is USD` needs no separate `currency`. See
[`reference.md`](./reference.md#52-type-tags) for the full tree.

Unit and currency tags are load-bearing in dimensional analysis. Physical
units are built in (`unit:m`, `unit:C`, `unit:delta_C`) and currencies take
the `currency:` qualifier (`currency:USD`, or bare `USD`); models may also
declare scale-only custom units near the top of the source:

```text
unit smoot = 1.7018 m
distance is unit:smoot = 2
```

### Native value kinds and optional contracts

Native structures use the same `IS` relationship. The shortest useful type is
the kind itself:

```grid
items IS List = VECTOR(1, "two", {name: "three"})
pending IS Deque = DEQUE()
weights IS Tensor = A1:D1000
```

Those declarations do not demand homogeneous elements or a predeclared shape.
The constructor, inferred expression, and resident runtime value establish the
kind; an annotation merely adds a contract when the author wants one. Omitting
the annotation entirely remains equally valid.

Parameterized contracts participate in Grid's coercion axis. `strict` checks
their contents recursively, `warn` keeps the value and emits a coercion
diagnostic, and the default `loose` mode does not scan collection contents.
Bare kinds always remain open. Type nesting is bounded at 64 levels so hostile
source cannot exhaust the compiler stack.

Use `of` to add an element contract and `to` for a keyed value contract. Types
nest without switching to a punctuation-heavy mini-language:

```grid
names IS List of String = VECTOR("Ada", "Edsger")
jobs IS Deque of Job = DEQUE()
orders IS Map of Customer Id to List of Order = MAP_OF()
routes IS Trie of Route = TRIE()
latency IS Quantile Sketch = KLL()
```

`List` is the language name for Grid's persistent indexed sequence. The legacy
spellings `Vector<String>` and `Map<String, Vector<Order>>` remain accepted,
but `grid fmt` emits `List of String` and `Map of String to List of Order`.

Tensor follows the same progression. Shape, semantic element type, and
precision are independent, optional contracts:

```grid
samples IS Tensor = A1:D1000
points IS Tensor[?, 3] = A1:C1000
distances IS Tensor[?] of unit:m = A1:A1000
weights IS Tensor[2, 2] with precision 32 = PARAMETER([[0.1, 0], [0, 0.1]])
score IS Tensor[] = AVERAGE(weights)
```

Omitted precision uses ordinary Grid `Number` semantics. Precision 32 is an
explicit storage/computation policy; precision 64 is available when authors
want to pin the default. Low-bit 2/4/8 storage is quantization, not floating
precision, and is deliberately not disguised as a numeric type.

Tensor contracts are persisted independently from backend promotion. Shape,
precision, and semantic element metadata therefore survive compilation even
when a formula takes a general evaluation path; runtime publication restores
the semantic element envelope without copying resident Tensor storage.

### 4.3 Dynamic cast inside expressions

Declaration-level `IS` attaches a tag to the assignment target. To tag a
value inside an expression — for intermediate results or nested assignments —
use infix `INTO`:

```text
B1 = A1 * fx_rate INTO currency
C1 = ROUND(amount, 2) INTO currency
```

This lowers to `TYPE_TAG(value, "<tag>")` and applies the same semantic overlay
as `target IS <tag> = …`. Use `IS` on the target when the whole cell carries
the tag; use `INTO` when only part of the expression should be tagged.

---

### 4.1 Tags On Range Targets

The tag applies to every cell in the range:

```text
A1:A10 is currency = 0
```

### 4.2 Tags Are Sticky

Once a cell carries a type tag, the tag persists across recomputations
until a different assignment overwrites it (or removes it by omitting
the tag).

```text
A1 is currency = 100              # A1 is currency
A1 = A1 + 1                     # A1 is still currency
A1 is percentage = 0.5            # A1 is now percentage
A1 = "hello"                    # A1 is now plain string (tag removed)
```

### 4.3 Unary predicates

Where a type tag says *what a value is*, a **predicate** says *what is true
about the symbol*. Colon predicates form an open, multi-valued set orthogonal
to the type. Fieldless predicates are ad hoc and need no declaration. Use
`predicate` to declare a reusable typed field schema shared by unary
attachments and explicit model-graph relationship payloads:

```text
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
x is a :reviewed(reviewer, 0.95) USD = 1.00
reviewer1 = PREDICATES(x).reviewed.by
reviewer2 = x.predicates.reviewed.by  # equivalent
```

The `a` or `an` after declaration-side `is` is optional surface sugar; the
leading colon identifies a predicate. A declaration is single-valued by
default, with an optional explicit `one` designation. A multi-valued predicate
declares the field tuple that distinguishes its instances:

```text
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  # [:r1, :r2]
reviewers = x.predicates.reviewed.by    # [:ada, :lin]
```

The declared field count and types apply wherever `:reviewed(...)` is attached.
An undeclared predicate may not carry fields. Field access is schema-checked;
`PREDICATES(x).reviewed` and `x.predicates.reviewed` query Boolean presence,
while a field of an absent predicate is `BLANK`. A many-valued field returns an array in
stable first-insertion order. Declared field types fail closed with `#TYPE!`
even in loose mode. For a single predicate, `+= :reviewed(...)` replaces its
atomic state and bare `-= :reviewed` removes it. For `many`, `+=` supplies the
full payload and upserts by the declared identity; `-= :reviewed(key, ...)`
supplies keys in `many by` order and removes that instance; bare
`-= :reviewed` removes all instances. These mutations are reactive inside
`WHEN`, `EVERY`, and `AT` rules; a predicate introduced only by a rule starts
absent until an add action fires. Unary predicates describe the symbol, not the
value, so they don't flow through arithmetic. See the
[predicate guide](./predicates.md) and
[`reference.md`](./reference.md#55-unary-predicates--is-a-predicate).

Reverse lookup is available through the unified reactive Graph receiver
`MODEL.PREDICATES`:

```text
all_reviewed = MODEL.PREDICATES.NEIGHBORS(:reviewed)
predicates_on_x = MODEL.PREDICATES.IN_NEIGHBORS("x")
```

Within this view, colon symbols are predicate vertices and quoted canonical
names are model-symbol vertices. The colon therefore prevents `:reviewed` from
colliding with a model symbol named `reviewed`. Each directed edge has kind
`:applies`; structured fields live on that edge. Authored binary Predicate
facts occupy the same view. A `many` unary predicate has one parallel edge
per identity, though `NEIGHBORS` still returns each target symbol once. Rule
mutations revise the view automatically. It is deliberately separate from
`MODEL.GRAPH`, so unary-predicate membership does not change model dependency
or dependency algorithms. The carrier-owned membership ID is an opaque
versioned string, currently `tag-membership:v2:<sha256>`, and stays stable when
an upsert changes payload fields without changing its typed identity.
`TAGS(x)`, `x.tags`, and the unary-only `MODEL.TAGS` view remain accepted for
source compatibility; new source should use the Predicate spellings above.

---

## 5. Ownership, State, And Model Boundaries

Every writable binding has two conceptual value layers:

- The **base** is the value produced by its top-level declaration.
- A **sticky override** is a committed write from an admitted external writer
  or a rule action. While present, it wins over the base.

The value authors read is the **effective value**: the override when one is
present, otherwise the base. `RESET` removes an override and reveals the base;
`CLEAR` installs a blank override. Aliases share these layers because they are
additional addresses for the same binding.

Declarations state who is supposed to write:

| Form | Intended owner | Base value | Other writers |
|---|---|---|---|
| `A1 = expr` | formula | `expr` | none; a rule `set` earns `GRID_MIXED_OWNERSHIP` |
| `input A1 = expr` | external caller | `expr` | admitted external writes |
| `default A1 = expr` | formula plus override layer | `expr` | rules; external writes when also declared `input` |
| `STATE A1 = expr` | model rules | initial/reset value `expr` | rule actions |
| rule-only target | model rules | `BLANK` | rule actions |

`output` does not grant write authority. It publishes a binding on the model's
read surface.

Two optional decorator keywords may appear at the **start** of an
assignment:

```text
input  <target>[IS <tag>] = <expression>
output <target>[IS <tag>] <op> <expression>
```

They label bindings as part of the model's external **write surface**
(`input`) and **read surface** (`output`).

### 5.1 `input` Decorator

`input` marks a binding as externally writable.

```text
input price = 0
input qty   IS integer = 0
input items = []
```

**Restrictions** — `input` may only appear on a plain eager `=`
assignment with a default value. The parser rejects:

| Form | Why |
|---|---|
| `input A1 ~= …` | Lazy bindings have no initial value to act as a default |
| `input A1 ?= …` | Conditional bindings skip on error — undefined initial state |
| `input A1 += 1` (and `-=`, `*=`, `/=`) | Arithmetic compound assignment belongs in a rule action, not a declaration |
| `input A1 = … ONCE` (and `EVERY`, `AT`) | Scheduled bindings are driven by time, not external writes |
| Inside `WHEN … THEN … END` rule actions | Rule actions are model-owned; declare `input` on the top-level binding instead |

The default value (the right-hand side of `=`) is the value the binding
holds before any external write arrives. It can be any expression —
including a function call — as long as it does not depend on other
declared inputs that have no default of their own.

### 5.2 `output` Decorator

`output` marks a binding as externally readable.

```text
output total       = price * qty
output history     ~= EXPENSIVE_AGGREGATE(transactions)
output last_seen   = NOW() ONCE
output default running_log = []
```

`output` may decorate top-level single-cell and named assignments. Rule actions
do not declare model boundaries; declare the target as an output at the top
level, or publish a semantic alias for it. Use outputs deliberately to define
what the model promises to consumers.

### 5.3 Combining Decorators

`input` and `output` may appear together, in either order:

```text
input output A1 = 0            # writable input that also surfaces as a read
output input A1 = 0            # equivalent
```

Each keyword may appear at most once per assignment. Decorators
are case-insensitive (`input`, `INPUT`, `Input` all parse the same).

### 5.4 `default` Decorator

`default` marks a formula as an **overridable base value**. The formula
computes normally, but a rule action (or an external input) may install
an override on top; the override wins and is *sticky* (it persists until
cleared), and [`RESET`](./rules-and-schedules.md#62-override-layer-actions--reset--clear)
restores the base.

```text
default price = model_estimate     # computed default, may be overridden
WHEN incident THEN price = manual_number END   # override wins, sticky
WHEN resolved THEN RESET price          END    # base governs again
```

Without `default`, a cell is owned by **either** its formula **or** its
rule actions, never both: a rule that `set`s a plain formula cell shadows
it silently and earns the `GRID_MIXED_OWNERSHIP` warning (see
[`errors.md`](./errors.md)). `default` is the opt-in that makes the
override intentional and inspectable — `WHY` reports the value as
*overridden* rather than silently tracing the shadowed formula.

`default` applies to single-cell and named formula targets. It is not
supported on range or table-mutation targets. It composes with type tags
(`default B1 IS currency = …`). Like `input`/`output`, it is
case-insensitive and appears at the start of the assignment.

It may compose with `input` when both external callers and model rules are
intentional writers:

```text
input default price = model_estimate
```

### 5.5 `STATE` Declaration

`STATE` declares model-owned mutable state. Its right-hand side is the initial
value and the value restored by `RESET`; rule actions install sticky revisions
on top of it.

```text
STATE retry_count = 0

WHEN request_failed THEN retry_count += 1 END
WHEN request_succeeded THEN RESET retry_count END

output retries = retry_count
```

`STATE` is distinct from an ordinary formula. A formula describes what a value
is now from its dependencies; state remembers what prior rule waves did. It is
also distinct from `input`: rules own state, while callers own inputs.

A state declaration uses a single-cell or named target with eager `=`. It
cannot itself be decorated `input` or `output`. Publish state through an
`output` alias or a derived output, as above. `STATE` uses the same preserved
base and override machinery as `default`, but keeps its authored state identity
for formatting and tooling.

### 5.6 Declared Surface

Use decorators to make the model boundary explicit:

- **Inputs** are the cells a caller or collaborator is expected to change.
- **Outputs** are the cells a caller or collaborator is expected to read.
- Any referenced but undeclared cell is just an implementation detail of the
  model.

Even when a workspace does not enforce the boundary, the declarations remain
valuable documentation for people and tools.

### 5.7 Common Mistakes

```text
input A1 += 1     # rejected: input requires plain `=` with a default
input A1 ~= 0     # rejected: input requires plain `=` with a default
A1 = input        # rejected: `input` is a reserved word
input input A1=0  # rejected: duplicate `input` decorator
STATE input A1=0  # rejected: state is model-owned, not an external input
```

To attach `input` to a counter you intend to increment from rules,
declare the input separately:

```text
input default counter = 0        # external surface plus intentional rule writes
WHEN trigger > 0 THEN
  counter += 1
END
output current_counter = counter  # publish the binding at top level
```

---

## 6. Schedule Modifiers

A single-action assignment can carry a schedule modifier. This is
equivalent to wrapping the assignment in a single-action rule block.

```text
G1 = NOW() EVERY cron"0 * * * *" BACKFILL
G2 = TRUE AT dt"2026-12-31T23:59:00Z"
G3 = NOW() ONCE
```

Schedule modifiers are sugar for:

```text
EVERY cron"0 * * * *" BACKFILL THEN
  G1 = NOW()
END

AT dt"2026-12-31T23:59:00Z" SKIP MISSED THEN
  G2 = TRUE
END
```

Three forms are accepted:

| Form | Behavior |
|---|---|
| `<expr> EVERY <duration\|cron> [SKIP MISSED\|BACKFILL]` | Wraps as an `EVERY` rule block |
| `<expr> AT <datetime> [SKIP MISSED\|BACKFILL]` | Wraps as an `AT` rule block |
| `<expr> ONCE` | **Capture the first successful evaluation and do not recompute it.** Useful for snapshot values such as `B1 = NOW() ONCE`. An unchanged assignment keeps its captured value when the same resident model is replaced; changing the expression starts a new capture. |

### 6.1 Restrictions

Schedule modifiers attach only to **eager `=`** assignments. Grid rejects:

- `~=` lazy assignments
- `?=` conditional eager assignments
- `+=` / `-=` / `*=` / `/=` compound assignments

A model that combines any schedule modifier with `~=`, `?=`, or a compound
operator builds with `GRID_ASSIGNMENT_SCHEDULE_OPERATOR`. The diagnostic names
the attempted scheduled form, explains why schedules require eager writes,
identifies an eager top-level assignment as the valid context, and shows an
`=` rewrite. See [contextual grammar diagnostics](./errors.md#61-contextual-grammar-diagnostics).

### 6.2 Semantics

The canonical style prefers explicit rule blocks for shared models; modifiers
are a convenience for short single-action cases. An inline `EVERY` or `AT`
target exists as `BLANK` before its first firing. Its RHS is evaluated only by
the scheduler, not as an ordinary base formula.

### 6.3 ONCE semantics

`ONCE` cells follow these rules:

1. The expression evaluates on the first evaluation wave that includes the
   cell (normally model application or its first read).
2. After the first successful evaluation, invalidations from upstream
   inputs or calculations **do not** trigger recomputation. The cell
   stays at its captured value.
3. Same-id replacement or runtime-host restart preserves the prior `ONCE`
   value if the cell's expression is unchanged. If the expression changes, the
   cell recomputes once with the new expression.
4. If the first attempt errors (e.g. `#DIV/0!`), the result is **not**
   sticky — the cell retries on the next invalidation wave.

---

## 7. Nested External Calls

When an external call is nested inside a larger formula, Grid tracks that call
separately so the fetched value can be cached and retried independently:

```text
A1 = ROUND(FX_RATE("EUR", "USD") + 0.01, 4)
```

For readability, prefer naming the external boundary when it is reused:

```text
eur_usd = FX_RATE("EUR", "USD") DEFAULT 1.08
A1 = ROUND(eur_usd + 0.01, 4)
```

---

## 8. Restrictions Summary

| Constraint | Where it applies |
|---|---|
| Range targets must be finite rectangles | Top-level and rule-action assignments |
| Arithmetic compound `+=` / `-=` / `*=` / `/=` is valid only on a single-cell or named target inside a rule action | Rule action body |
| Value-first `<value> AS <parameter>` binds call arguments by name; it is not assignment | All contexts |
| Inside rule actions, `=`, `?=`, and compound (`+=`/`-=`/`*=`/`/=`) are allowed; `~=` and nested rules are rejected | Rule action body |
| Rule action RHS may not contain external async calls because rule actions must commit atomically with the rule wave | Rule action body |
| Namespace specifiers use single quotes for spaces; identifiers don't need them | All contexts |
| `input` decorator requires a plain `=` assignment with a default value; rejected on `~=`, `?=`, compound, scheduled, and rule-action targets | All contexts |
| `output` decorates top-level single-cell or named assignments; rule actions do not declare the model boundary | Top level |
| `STATE` uses an eager `=` single-cell or named declaration and cannot also be `input` or `output` | Top level |
| Each decorator (`input`, `output`) may appear at most once per assignment | All contexts |

---

## 9. Common Mistakes

### 9.1 Missing Operator

```text
A1 10            # parse error
A1 = 10          # correct
```

### 9.2 Single-Quoted String

```text
A1 = 'hello'     # 'hello' is parsed as a namespace specifier, not a string
A1 = "hello"     # correct
```

### 9.3 Range Target Too Large

```text
A:A = 0          # rejected — full-column ranges not allowed as targets
A1:A1000 = 0    # works, but explodes the dependency graph
A1:A1000 ~= 0   # ~= not allowed in range target broadcasting
```

Use an anchor assignment with an array-valued RHS instead. For
constant fills the dedicated helpers are clearest:

```text
A1 = ZEROS(1000, 1)         # column of zeros
A1 = ONES(1000, 1)          # column of ones
A1 = FILL("TBD", 1000, 1)   # column of any constant value
A1 = REPEAT(0, 1000)        # equivalent shorthand for column of zeros
```

When each output element depends on its row/column index, prefer an array
comprehension — it lowers to a single `MAKEARRAY` binding but reads more
naturally:

```text
A1 = [r * 2 FOR r IN 1..1000]
A1 = [r * c FOR r IN 1..100, c IN 1..10]
```

Reach for `MAKEARRAY` directly when you need three or more index
variables, want to share heavy intermediates via `LET`, or are computing
the dimensions dynamically. For constant fills, prefer
`FILL` / `ZEROS` / `ONES` / `REPEAT`.

### 9.4 Incompatible Type Tag

```text
A1 is currency = "hello"      # #TYPE! — string can't be currency
A1 is currency = "100"        # OK — string coerces to number
A1 is currency = 100          # canonical
```

### 9.5 Cycle

```text
A1 = A2 + 1
A2 = A1 + 1     # both cells become #CIRCULAR_REF!
```

Cycles are detected and reported as `#CIRCULAR_REF!` unless iterative
calculation is enabled for a convergent cycle.
Lazy bindings (`~=`) participate in cycle detection.

---

## 10. Examples

A complete, well-typed model:

```text
MODEL "Pricing"
VERSION "1.0.0"

# Declared inputs (writable surface in protected mode)
input  A1 is currency   = 100000    # base price
input  A2 is percentage = 7pct      # discount
input  A3 is percentage = 21pct     # tax rate

# Declared outputs (readable surface in protected mode)
output B1 is currency = A1 * (1 - A2)      # discounted price
output B2 is currency = B1 * (1 + A3)      # gross price

# Lazy expensive lookup, exposed as output
output C1 ~= FX_RATE("USD", "EUR")

# Conditional output (only if FX succeeds)
output D1 is currency ?= B2 * C1

# Internal counter (not part of the external surface)
default E1 = 0
WHEN increment_requested THEN E1 += 1 END

# Range broadcast (every cell carries the `output` decorator)
output F1:F5 = 0

# Destructuring (decorator broadcasts to each binding)
output [min, max, avg] = SUMMARY([100, 200, 300, 400])

END MODEL
```

This model is safe to deploy with `protectedMode: true`: every
implicit input is declared (the model has no implicit inputs), and
the model declares both an `input` and an `output`.

---

## 11. SOLVE Statements

A `SOLVE` statement declares a goal-seek or bounded optimization over
workbook cells. The result cell receives the value of the variable cell
that satisfies the goal; the variable cell itself is never mutated, so the
sheet stays consistent and auditable.

```text
SOLVE Z9 = A2 IN [0, 1] GOAL A3 = 200000
SOLVE B9 = A1 IN [0, 5] MINIMIZE A2
SOLVE B9 = A1 IN [0, 5] MAXIMIZE A2
```

- `Z9` — result cell (receives the solved variable value).
- `A2` — variable cell the solver varies inside the bounds `[lo, hi]`
  (fractional bounds are fine).
- `GOAL A3 = 200000` drives cell `A3` to the target by root finding;
  `MINIMIZE` / `MAXIMIZE` optimize the objective cell instead.
- The goal may reach the variable through intermediate cells (chains and
  diamonds compose by the chain rule); other referenced cells participate
  as data parameters at their current values.
- Solves are reactive: editing an upstream input re-solves on the next
  resolve.

Current limits: one variable per statement, default-sheet addresses, and
expression-backed chains (a chain crossing a descriptor-compacted range
refuses). A statement the compiler cannot lower reports
`#CALC! (SOLVE_NOT_COMPILED)`; a solve that fails at runtime (no root in
the bounds, non-smooth objective) reports `#CALC! (SOLVE_FAILED)` on the
result cell without failing the rest of the model.

Solves execute on the solver-region lane and write back unconditionally —
the result cell has no other evaluator. The resolve response carries a
structured `solverWrites` report (cell, values, `boundDuals`), where
`boundDuals` is the goal's marginal at the solution (`∂goal/∂variable`) —
the shadow-price view of the solve; the same values appear in the solve's
diagnostics.

---

## 12. See Also

- [`reference.md`](./reference.md#5-type-system) for the type system.
- [`rules-and-schedules.md`](./rules-and-schedules.md) for rule blocks.
- [`external-functions.md`](./external-functions.md) for `~=` deep dive.
- [`errors.md`](./errors.md) for `#TYPE!`, `#CIRCULAR_REF!`, etc.
