# .ips LLM reference

Generated from the handbook, compiler export and registered examples. Do not edit this artifact.

Write .ips the way the first section shows: draw values with `lower to upper`, shape them with `with` and a distribution, draw colors as gradients (`dark to light with d`), name fixed sets in palettes, and make every seed alone a different prop. The later sections cover the whole language.

Product 0.7.1 · source language 0.7 · C API 0.4 · ABI 0.5
Commit: `196ff75804d67e4771675ac92219b31f19b5a824` · development / release candidate · dirty: true
Budget: 102400 UTF-8 bytes (no truncation). Sections: idioms, style, assets, syntax, types, control, helper, random, colors, palette, collections, samplers, part, surfaces, limits, grammar, builtins, diagnostics. Symbols: none.

[Full handbook](docs/language-v0.7.md) · [Compiler catalog](docs/language.json)

## Write idiomatic props (required style)

[Handbook section](docs/language/idioms.md#writing-idiomatic-props)

A prop is judged by how different, and how good, every seed looks. The rules
below are the house style of the examples and the showcase props. Follow them
when you write a new asset: they keep sources short, every roll visible and
every seed distinct. The complete example at the end applies all of them.

The smallest prop in this style draws its proportions and its color from one
shaped distribution and reports its collider:

### Draw values with `to`

Every random number is a range: `w = 20 to 30`, `tilt = -0.4 to 0.4`,
`lean = base - 2 to base + 2`. Do not build draws by hand from `unit()`,
`range()` or arithmetic such as `20 + int(unit() * 11)`: `to` says the same
thing in one readable step, picks `i32` or `f32` from the bounds and
includes both ends. Keep `index(n)` for selecting a palette element.

### Shape every proportion with `with`

Uniform draws make half of all seeds look extreme. Give sizes, lengths and
offsets a shape: `gaussian` for natural variation around a typical value,
`beta` to lean towards one end (rust that is mostly light, wear that is
mostly low) and `triangular` for a clear most-likely value. Declare a shared
description once, as in `distribution typical = gaussian(mean=0.5, sigma=0.18)`,
and reuse it for every proportion of one object.

Distribution parameters are in the units of the drawn value; they are not
rescaled to the range. `14.0 to 26.0 with gaussian(mean=0.5, sigma=0.18)` has
no probability inside its range and traps. Either describe the value in its
own units, as in `lean = -2.0 to 2.0 with gaussian(mean=0.0, sigma=0.8)`, or
draw a normalized roll and map it:
`height = lerp(14.0, 26.0, 0.0 to 1.0 with typical)`.

### Draw colors as gradients

Colors are drawn the same way as numbers. Pick the two ends of the look you
want and draw between them: `metal = #705845 to #c8c8c8`. Add `with` to skew
the result: `body = color.shade(base, -6) to base with beta(alpha=4.0, beta=2.0)`
mostly lands near `base`. One draw moves every channel together, so the
result never leaves the gradient. Do not draw red, green and blue
separately, and do not compute random channels by hand.

Derive the related tones of one material from its drawn color with
`color.shade` (lighter or darker steps), `color.mix` and `color.hsv`. Adjust a
channel with `rgba(c.r + 5, c.g, c.b, c.a)`; `rgb()` and `rgba()` clamp.

### Use palettes for fixed sets

A set of hand-picked colors or levels belongs in an asset-level `palette`:
`palette hues = [#6fd3ff, #c38bff, #7dffb0]`. Choose an element with
`hues[index(3)]` and blend a color or `f32` palette continuously with
`ramp(hues, t)`. Palettes are visible in every helper, so a material ramp is
declared once and used everywhere. Keep literal colors out of the drawing
code when a palette can name them.

### Choose with `pick` and `chance`

Pick one of several values with `pick(2, 3, 4)` or
`pick(#c0a060, #a0a0a8, #b87333)`; all choices share one type. Decide an
optional feature with `chance(p)`, as in `if (chance(0.3)) { ... }`. Both
consume one draw.

### Make every seed a different prop

With only a seed, and every input at its default, every seed must give a
visibly different result. Variety must never depend on inputs:

- An ordinary style input with default 0 rolls from the seed; values from 1 force one
  look. Derive the roll by hashing the seed, as in
  `look = style; if (style == 0) { look = 1 + (hash(seed ^ 0x2f6b9a13) & 1) }`, so a
  rolled style renders exactly like the same style chosen explicitly.
- Input defaults mean "roll it": `0` for `u32` style switches and a
  transparent color for color inputs, as in `input color tint = #00000000`
  read with `if (tint.a == 0) { base = hues[index(4)] }`.
- Roll every part of the look (shape, material, colors and details) from the
  seed; an input only overrides one of those rolls.

### Keep one silhouette across styles

Styles of one seed should read as the same object. Roll the shape inside
its own stream, `stream_push(hash(seed ^ 0x51ed2701))` ... `stream_pop()`,
before any style-dependent choice. Make every roll unconditionally, even
when a style does not use it, so a style never shifts the draws that come
after it.

### Pass inputs to helpers

Helpers see palettes and `seed`, but not inputs. Pass what
they need as typed parameters, as in `paint(base: color, look: i32)`. A local
keeps the type of its first assignment, so write `x = 0.0` when the value
becomes `f32` later.

### Paint runs, not pixels

Every executed instruction spends fuel from the asset's budget (5,000,000 by
default). Filling a 64×64 area costs about 4,200 fuel as one `rect`, 9,600 as
one `rect` per row, 71,000 as a `pixel` loop and 84,000 with a helper call per
pixel. Compute each row's extent once and paint it with `rect`; draw outlines
with `line`, `oval` and `poly`. Reserve per-pixel loops for small details.

### Before and after

Each program below compiles and runs, but breaks one rule. The correction
follows each one.

Write `w = 20 to 30`: one readable draw with both ends included.

A uniform height makes every second pillar look extreme. Write
`h = lerp(14.0, 26.0, 0.0 to 1.0 with gaussian(mean=0.5, sigma=0.18))`.

Three independent channel draws produce colors outside the intended family.
Draw the color as one gradient: `c = #2878b4 to #5ac8ff`.

The per-pixel loop spends about 43,000 fuel. Painting one `rect` per row from
the row's extent, `half = sqrt(r * r - dy * dy)`, draws the same disc for
about 2,200.

With only a seed this banner never changes. Make the default `0` mean "roll
it": `input u32 pattern = 0`, then
`kind = pattern; if (pattern == 0) { kind = 1 + (hash(seed ^ 0x6d2b79f5) & 1) }`
and branch on `kind`.

### Pitfalls

These compile errors and surprises catch most first drafts:

- `min`, `max` and `clamp` return `i32` for integer-only arguments. Use an
  explicit `float(...)` or one f32 argument before reassigning an f32 local.
- Colors cannot be compared with `==`. Compare channels, as in
  `tint.a == 0`.
- `for` bounds must be `i32`: write `for i in 0..int(count)`.
- There are no arrays besides palettes. Use a palette for a fixed list and
  compute repeated positions inside a loop.
- `part.outputs.name` reads the last call of that part. Read an output right
  after the call that produced it.
- A translucent color drawn inside a part over its empty canvas disappears.
  Draw glazes and shadows that cover other parts in the asset that places
  them.
- A local keeps the type of its first assignment: `x = 0` stays `i32`.

### A complete example

The crystal below applies every rule: a hashed variant roll, a transparent
color input that means "roll it", a shape in its own stream shaped by one
distribution, a skewed color gradient, palettes, `pick`, `chance`, a helper
that receives what it needs, and rows painted with `rect`.

[minimal-prop.ips](examples/language/minimal-prop.ips), executed with seed 7.

```ips
asset Boulder {
  size 32x24
  origin 16,22
  output rect collider

  draw {
    distribution typical = gaussian(mean=0.5, sigma=0.18)
    w = lerp(14.0, 26.0, 0.0 to 1.0 with typical)
    h = lerp(8.0, 16.0, 0.0 to 1.0 with typical)
    rock = #5b6170 to #8d94a3 with typical
    oval(16, 22.0 - h * 0.5, w * 0.5, h * 0.5, rock)
    oval(16.0 - w * 0.15, 22.0 - h * 0.7, w * 0.25, h * 0.2, color.shade(rock, 3))
    collider = {16.0 - w * 0.5, 22.0 - h, w, h}
  }
}
```

[idioms.ips](examples/language/idioms.ips), executed with seed 7.

```ips
asset Crystal { input i32 v = 0
  size 32x32
  origin 16,30
  // Variant 0 rolls 1 or 2 from the seed; 1 is plain, 2 has a glint.
  // Transparent means "roll a colour from the seed".
  input color tint = #00000000
  output rect collider
  palette stone = [#3b3f4a, #565b69, #767c8c]
  palette hues = [#6fd3ff, #c38bff, #7dffb0, #ff8fa3]
  palette glints = [#ffffff, #fff3b0, #ffd166]

  draw {
    // Hash the seed instead of drawing, so a rolled variant renders
    // exactly like the same variant chosen explicitly.
    look = v
    if (v == 0) { look = 1 + (hash(seed ^ 0x2f6b9a13) & 1) }

    // The shape draws from its own stream, so every variant keeps the
    // same silhouette and only the colours change.
    stream_push(hash(seed ^ 0x51ed2701))
    distribution typical = gaussian(mean=0.5, sigma=0.18)
    height = lerp(14.0, 26.0, 0.0 to 1.0 with typical)
    width = lerp(7.0, 12.0, 0.0 to 1.0 with typical)
    lean = -2.0 to 2.0 with gaussian(mean=0.0, sigma=0.8)
    facets = pick(2, 3, 4)
    stream_pop()

    // Colours are drawn, not computed: a gradient between two ends,
    // skewed towards the bright end.
    base = tint
    if (tint.a == 0) { base = hues[index(4)] }
    body = color.shade(base, -6) to base with beta(alpha=4.0, beta=2.0)
    glint = glints[index(3)]
    if (look == 2) { body = color.mix(body, glint, 0.35) }

    // Paint whole rows with rect(), never pixel by pixel.
    top = 30.0 - height
    rows = int(height)
    for r in 0..rows {
      t = float(r) / float(rows)
      half = width * 0.5 * smoothstep(0.0, 0.4, t)
      x = 16.0 + lean * (1.0 - t) - half
      rect(x, top + float(r), half, 1.0, color.shade(body, 2))
      rect(x + half, top + float(r), half, 1.0, color.shade(body, -3))
    }
    for f in 1..facets {
      fx = 16.0 + lean * 0.5 + (-3.0 to 3.0)
      line(fx, top + 4.0, 16.0, 29.0, 1, color.shade(body, 5))
    }
    pedestal(28.0)
    if (chance(0.6)) { pixel(int(16.0 + lean), int(top) + 2, glint) }
    collider = {16.0 - width * 0.5, top, width, height}
  }

  // Palettes are visible in helpers; inputs and v/a are not, so pass them.
  pedestal(y: f32) {
    for i in 0..3 {
      w = 18.0 - float(i) * 4.0 + (-1.0 to 1.0)
      rect(16.0 - w * 0.5, y - float(i), w, 2.0, stone[i])
    }
  }
}
```

## Before and after

Each program below compiles and runs but breaks one rule of the idioms; write the corrected form.

### Draw with `to`, not unit() arithmetic

[unit-arithmetic.ips](examples/language/style/unit-arithmetic.ips)

Avoid:
```ips
asset Plank {
  size 32x8
  draw {
    w = 20 + int(unit() * 11)
    rect(0, 2, w, 4, #8a5a33)
  }
}
```
One readable draw with both ends included.
Write:
```ips
asset Plank {
  size 32x8
  draw {
    w = 20 to 30
    rect(0, 2, w, 4, #8a5a33)
  }
}
```

### Shape proportions with a distribution

[uniform-proportion.ips](examples/language/style/uniform-proportion.ips)

Avoid:
```ips
asset Pillar {
  size 16x32
  draw {
    h = 14.0 to 26.0
    rect(4, 30.0 - h, 8, h, #9aa3ad)
  }
}
```
A uniform height makes every second pillar look extreme; a gaussian keeps most near the typical height.
Write:
```ips
asset Pillar {
  size 16x32
  draw {
    h = lerp(14.0, 26.0, 0.0 to 1.0 with gaussian(mean=0.5, sigma=0.18))
    rect(4, 30.0 - h, 8, h, #9aa3ad)
  }
}
```

### Draw a color as one gradient

[channel-draws.ips](examples/language/style/channel-draws.ips)

Avoid:
```ips
asset Gem {
  size 8x8
  draw {
    c = rgb(40 to 90, 120 to 200, 180 to 255)
    rect(1, 1, 6, 6, c)
  }
}
```
Independent channel draws leave the intended color family; one gradient draw stays inside it.
Write:
```ips
asset Gem {
  size 8x8
  draw {
    c = #2878b4 to #5ac8ff
    rect(1, 1, 6, 6, c)
  }
}
```

### Paint rows, not pixels

[pixel-loop.ips](examples/language/style/pixel-loop.ips)

Avoid:
```ips
asset Disc {
  size 32x32
  draw {
    r = 10.0 to 14.0
    for y in 0..32 {
      for x in 0..32 {
        dx = float(x) + 0.5 - 16.0
        dy = float(y) + 0.5 - 16.0
        if (dx * dx + dy * dy < r * r) { pixel(x, y, #c0a060) }
      }
    }
  }
}
```
The per-pixel loop spends about 43,000 fuel; one rect per row draws the same disc for about 2,200.
Write:
```ips
asset Disc {
  size 32x32
  draw {
    r = 10.0 to 14.0
    for y in 0..32 {
      dy = float(y) + 0.5 - 16.0
      if (dy * dy < r * r) {
        half = sqrt(r * r - dy * dy)
        rect(16.0 - half, y, half * 2.0, 1, #c0a060)
      }
    }
  }
}
```

### Let the seed alone vary the prop

[input-variety.ips](examples/language/style/input-variety.ips)

Avoid:
```ips
asset Banner {
  size 16x16
  input u32 pattern = 1
  draw {
    rect(0, 0, 16, 16, #203040)
    if (pattern == 1) { rect(0, 6, 16, 4, #e0c060) }
    else { rect(6, 0, 4, 16, #e0c060) }
  }
}
```
With only a seed the banner never changes. The default 0 now means roll it from the seed; 1 and 2 still force a look.
Write:
```ips
asset Banner {
  size 16x16
  input u32 pattern = 0
  draw {
    rect(0, 0, 16, 16, #203040)
    kind = pattern
    if (pattern == 0) { kind = 1 + (hash(seed ^ 0x6d2b79f5) & 1) }
    if (kind == 1) { rect(0, 6, 16, 4, #e0c060) }
    else { rect(6, 0, 4, 16, #e0c060) }
  }
}
```

## Compiler rules

Values you can store, pass and return: i32, f32, color, vec2, distribution, list<i32>, list<f32>, list<color>, list<vec2>, list<distribution>, sampler<i32>, sampler<f32>, sampler<color>, sampler<vec2>, list<sampler<i32>>, list<sampler<f32>>, list<sampler<color>>, list<sampler<vec2>>, stepper<i32>, stepper<f32>, stepper<color>, stepper<vec2>, list<stepper<i32>>, list<stepper<f32>>, list<stepper<color>>, list<stepper<vec2>>, layer, mask. Helper parameters take i32, f32, color, vec2, distribution, list<i32>, list<f32>, list<color>, list<vec2>, list<distribution>, sampler<i32>, sampler<f32>, sampler<color>, sampler<vec2>, list<sampler<i32>>, list<sampler<f32>>, list<sampler<color>>, list<sampler<vec2>>, stepper<i32>, stepper<f32>, stepper<color>, stepper<vec2>, list<stepper<i32>>, list<stepper<f32>>, list<stepper<color>>, list<stepper<vec2>>, layer, mask. `circle` and `rect` exist only as output kinds.
Only i32 to f32 converts implicitly; a local keeps the exact type of its first assignment.
`list<T>` is immutable and holds 0 to 256 elements; empty literals need a known element type. Elements evaluate once left to right; list indices are i32 and trap outside the length. Helpers share immutable data. `list.len` gives i32; `poly` accepts list<vec2> and color, evaluating points first. Direct legacy palette indexing keeps its last-element fallback; a palette passed as a list uses checked indexing and the 256-element limit.
Captured samplers evaluate lower, upper and optional distribution once, without an intrinsic draw. Copies/helpers share the immutable description. Receivers evaluate once before the count; batch pick checks count, handle and allocation before results. Constants and empty batches consume no RNG. Const samplers and range palettes store compile-time descriptions. Range palettes lift scalar entries to constant descriptions; indexing selects without RNG and keeps the last-element fallback. Pass them as list<sampler<T>> for checked indexing. Ramp accepts only constant color/f32 palettes.

| Receiver | Method | Result |
| --- | --- | --- |
| sampler<i32> | .pick() | i32 |
| sampler<i32> | .pick(n: i32) | list<i32> |
| sampler<f32> | .pick() | f32 |
| sampler<f32> | .pick(n: i32) | list<f32> |
| sampler<color> | .pick() | color |
| sampler<color> | .pick(n: i32) | list<color> |
| sampler<vec2> | .pick() | vec2 |
| sampler<vec2> | .pick(n: i32) | list<vec2> |

`stepper<T>` captures the same four endpoint types as a sampler, but `.pick(n)` requires an i32 count in 0-256 and returns deterministic list<T> quantiles without RNG. No cursor or implicit sampler conversion exists. Uniform positions are i/(n-1), with exact endpoints; n=0 is empty and n=1 returns the first step. Integer interpolation uses binary64 and rounds halves away from zero. Gaussian, triangular, beta, weighted and density use the truncated distribution support, with one shared coefficient for all color RGBA channels or vec2 components. Weighted steps may repeat. Captured operands still run eagerly in lower, upper, distribution order. Const descriptions and lists of steppers are supported; picks require generation, distinct compares descriptions, and sort rejects them. Empty picks still validate their handle but do not check probability mass.

| Receiver | Method | Result |
| --- | --- | --- |
| stepper<i32> | .pick(n: i32) | list<i32> |
| stepper<f32> | .pick(n: i32) | list<f32> |
| stepper<color> | .pick(n: i32) | list<color> |
| stepper<vec2> | .pick(n: i32) | list<vec2> |

Layer/mask `.blur(sigma)` requires finite f32 0-64 (i32 widens), in input pixels. Evaluate receiver then sigma once. Immutable results keep geometry/D4, never paint/use RNG. Zero validates/shares without allocation. Otherwise: Gaussian r=ceil(3*sigma), K=2*r+1, normalized pinnedExp weights, binary64 horizontal then vertical; repeat nearest edges. Premultiply stored RGBA, unpremultiply after both passes, round nearest ties up, zero-alpha RGB=0. Masks return A8 matching alpha. Clip applies on paint. Invalid dynamic sigma traps. Fuel 1+K+W*H*(1+2*K); output and 8*W*min(H,K)*C scratch share 64 MiB (C=4 layer,1 mask). Always release scratch; failures publish nothing.

| Receiver | Method | Result |
| --- | --- | --- |
| layer | .blur(sigma: f32) | layer |
| mask | .blur(sigma: f32) | mask |

`lower to upper` evaluates the lower bound, then the upper one, then draws once: i32 bounds give i32; f32 bounds give f32; mixed numbers widen to f32. It binds tighter than `<` and looser than `<<` and cannot be chained.
Between two color values or two vec2 values, `to` draws one shared position, so r, g, b, a or x, y move together.
`lower to upper with d` draws from `d` restricted to the range, never clamped; it evaluates lower, upper, distribution in that order, and i32 ranges round to the nearest integer, halves away from zero.
Point distribution literals: `weighted {key: value, ...}` (1–256 points); `density {key: value, ...}` (2–256 points). Numeric pairs evaluate once left to right; keys keep exact i32/f32 types, increase strictly, and values are finite/nonnegative with positive total mass. Descriptions snapshot without RNG; use `to ... with d` or capture a sampler. Weighted values are discrete masses; clipped draws consume one stream step, and missing mass traps. Density values are heights interpolated linearly, zero outside their support; draws integrate clipped trapezoids and invert the selected integral. Equal density bounds need only lie in the closed support. Color/vec2 use positions on 0–1.
Read-only members: vec2 .x, .y give f32; color .r, .g, .b, .a give i32.
A local is declared by its first assignment and is visible in the whole function; if that assignment never runs, it reads as zero of its type (there is no definite-assignment check).
Inputs are visible only in `draw` and are read-only; pass them to helpers as parameters.
Helper arguments evaluate in parameter declaration order; naming them does not change that order.
Not available: recursive helpers, let declarations, fn declarations, named geometry constructors.

## Assets, inputs and outputs

[Handbook section](docs/language/assets.md#assets-inputs-and-outputs)

A source file contains imports followed by exactly one `asset name { ... }`.
An asset requires `size` and one `draw` entry. It may declare an origin,
inputs, outputs, palettes, constants and helpers. Metadata
declarations are outside function bodies. `size 16x16` and `size 16 16` are
accepted. Origin coordinates are constant numeric expressions; an omitted
origin is `(0, 0)`.

The host supplies a seed, optional target dimensions and ordinary declared
input values. `draw` declares no source parameters or return type. There are
no implicit variant/archetype controls: `v` and `a` are ordinary names and
must be declared before use. Authors define any style or class selectors
as input values and decide which selections their asset supports.

### Asset constants

`const type name = expression` declares an immutable asset value with an
explicit `i32`, `f32`, `color`, `vec2`, `distribution`, [sampler<T>](docs/language/samplers.md),
[stepper<T>](docs/language/steppers.md) or [list<T>](docs/language/collections.md) type. Constants are
visible in `draw` and every helper of their defining asset. Imported parts
resolve their own constants; a parent cannot access a child's constants.
`i32` initializers may widen to declared `f32`; other types must match.
Integer arithmetic keeps its wrapping signed 32-bit rules.

Initializers may reference other constants, including declarations later in
the asset. The compiler evaluates the acyclic dependency graph once during
compilation. Arithmetic, component/channel reads and deterministic builtins
are allowed. `ipc --describe-language` marks each eligible overload with
`constant_evaluable`; seeded noise is allowed with a constant explicit seed.
Legacy palette indexing and `ramp` are also allowed, including the last-element
fallback for an invalid palette index.

The current RNG state `seed`, input uniforms, part outputs, RNG draws
and user helper or part calls are forbidden. This includes seeded selection
with `roll`/`choose` and equal-bound random ranges. The compiler checks every
declaration, including unused constants, and checks forbidden effects in
both branches of `&&`/`||`. Pure evaluation still short circuits: `false &&
(1 / 0 == 0)` is a valid `i32` constant, while `false && index(1)` is rejected.

Constant `gaussian`, `triangular`, `beta`, `weighted` and `density` constructors
validate parameters and points during compilation. An invalid unused description is a source
error; a dynamic constructor in `draw` retains its existing runtime trap.
Copying or reading a constant distribution performs no draw. Each `to/with`
still samples through the existing runtime kernel and stream.

Constant names cannot collide with asset declarations, imported aliases or
helper parameters, or use the reserved controls/placement names. Constants
cannot be assigned or used as loop counters. `const` is reserved; rename
existing helpers, locals and inputs with that spelling. Use descriptive names.

### Input uniforms

`input type name = constant` declares a host-settable, read-only uniform.

| Input type | Constant default | Value inside `draw` |
| --- | --- | --- |
| `color` | `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` | `color` |
| `f32` | integer or floating literal, optionally negative | `f32` |
| `u32` | nonnegative integer literal | `i32`, retaining its 32-bit pattern |
| `i32` | integer in `[-2147483648, 2147483647]`, optionally negative | `i32` |

Defaults permit a literal, parentheses and one numeric unary minus, such as
`input f32 amount = -(0.5)`. Arithmetic, names, calls and repeated negation
are rejected. Floating defaults preserve negative zero (`-0.0`); an integer
spelling used for an `f32` default is converted directly to binary32, without
first wrapping to an integer bit pattern. Root and part defaults use the
same conversion. For a `u32` bit pattern representing signed -1, use
`0xffffffff`; a negative `u32` default suggests `input i32`.
Boolean tokens are not integer input defaults.
Uniforms are visible in `draw`; pass their values explicitly to helpers.

Input names `seed`, `at`, `rot`, `mirror_x` and `mirror_y` are
reserved. Inputs and outputs cannot share a name; declarations of the same
kind must have unique names. Inputs cannot be assigned or used as loop counters.

### Structured outputs

`output kind name` declares a scalar slot. Assignment replaces its current
value; an unwritten slot remains unset. `output list kind name` appends each
assignment in execution order. Outputs are written by `draw` or a helper
belonging to the same asset, and returned to the host alongside the image.
`output mask name` and `output layer name` instead declare internal
[resource exports](docs/language/surfaces.md#export-regions-from-parts). They accept shared
immutable handles, cannot be lists, and never add host geometry slots.

| Kind | Assignment value |
| --- | --- |
| `point` | `{x, y}`, a `vec2` |
| `circle` | `{x, y, radius}` |
| `rect` | `{x, y, width, height}` |

Components must be numeric and widen to `f32`. Scalar part outputs can be
read and propagated; list outputs cannot be read as expressions. Outputs
are not ordinary readable local variables. See [composition](docs/language/composition.md)
for placement and transformation of part-owned outputs.

The VM validates the resulting executable and bounds its resources. Generation
is subject to the host's canvas limits and fuel budget; see
[execution limits](docs/language/tooling.md#execution-limits).

[assets.ips](examples/language/assets.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset UniformAsset { input i32 a = 0 input i32 v = 0
  size 16x16
  origin 8, 8
  input color tint = #9dc
  input f32 scale = 1.0
  input u32 dotCount = 2
  input u32 offset = 0
  output point anchor
  output rect bounds
  output circle area
  output list point dots
  draw {
    rect(0, 0, 16, 16, #123)
    rect(2 + offset, 2, 4 * scale + v, 4 + a, tint)
    anchor = {8, 8}
    bounds = {2, 2, 4 * scale + v, 4 + a}
    area = {8, 8, 3}
    for i in 0..dotCount { dots = {i, 8} }
  }
}
```

## Source syntax and names

[Handbook section](docs/language/syntax.md#source-syntax-and-names)

Source must be valid UTF-8. Whitespace separates tokens; newlines do not end
statements. `//` comments extend to the next newline and `/* ... */` comments
end at the first closing delimiter (they do not nest). Semicolons are optional
separators inside function blocks. One optional semicolon may follow each
import, `size`, `origin`, `input`, `output` or
`palette` or `const` declaration. Forms without semicolons remain valid; newline is
whitespace and never implicitly terminates a declaration or expression.

Ordinary identifiers match `[A-Za-z_][A-Za-z0-9_]*` and are case-sensitive.
Control/declaration keywords (`asset`, `input`, `output`, `import`, `from`,
`if`, `else`, `while`, `for`, `in`, `break`, `continue`, `return`,
`true`, `false`) and `const`, `to`, `with`, `distribution`, `sampler`, `stepper`,
`weighted`, `density`, `layer`, `mask` and `clip` are reserved. `draw`, `at`, scalar type names and geometry kind
names and `list` are soft keywords accepted in name positions; `draw` still selects
the entry block when used as an asset-level function name. Reserved words cannot
name assets, inputs, outputs, locals, helpers, parameters, aliases or members.
Newly introduced keywords have no contextual compatibility aliases. Asset names and
`for` counter names require an ordinary identifier. Asset-level `size`,
`origin` and `palette` introduce metadata.
`const type name = expression` introduces an [asset constant](docs/language/assets.md#asset-constants)
only in declaration position. The reserved word cannot be used as a name elsewhere.

Use descriptive names distinct from builtin functions and the namespaces
`color`, `vec2`, `dist`, `noise`, `cell`, `surface`, `mask` and `list.len`. Builtin call resolution takes
precedence over user helpers with the same name.

### Literals

- Integers: decimal digits or `0x`/`0X` plus hexadecimal digits. They become
  32-bit patterns, so `0xffffffff` is the expression value -1. Use ordinary
  decimal without leading zeroes; literal parsing also recognizes leading-zero
  octal forms. Values outside the literal parser's range are rejected.
- Floats: digits with a fractional part and/or exponent, such as `2.5`,
  `1e3`, `4.0e-2`. The fractional part requires digits on both sides of the
  dot; write `0.5`, not `.5`. Evaluation uses IEEE-754 binary32.
- Colors: `#RGB` and `#RGBA` repeat each digit; `#abc` is `#aabbcc`.
  Long forms are `#RRGGBB` and `#RRGGBBAA`. Missing alpha is 255.
- `true` and `false` are `i32` values 1 and 0.
- Strings are quoted import paths, with escapes `\n`, `\t`, `\r`, `\"` and
  `\\`. Strings are not runtime values.

There are no source literals for infinity or NaN; floating operations can
produce them. Numeric minus is an operator. Input defaults and palette
elements admit a small constant grammar: a literal, parentheses and one
numeric unary minus. They do not admit arithmetic, calls or RNG. Ordinary
runtime expressions retain their usual operator grammar and wrapping
integer semantics.

Trailing commas are accepted in call arguments, helper parameters, palette
elements, list literals and compound literals. Syntax is ASCII: Unicode text may appear only
in comments and string literals.

[syntax.ips](examples/language/syntax.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
// Comments can contain arbitrary valid UTF-8: współrzędne.
asset SourceSyntax {
  size 8x8;
  origin 0, 0;


  input i32 offset = -(0);
  output point anchor;
  palette colors = [#abc, #1234, #abcdef, #abcdef80,];
  draw {
    /* A semicolon separates statements; a newline is whitespace. */
    x = 0x1; y = 1e0
    point = {x, y,}
    pixel(int(point.x), int(point.y), colors[0],)
    value = -3 + int(2.5)
    if (true && !false) { pixel(value + 2, 2, #fff) }
    anchor = {offset, 0}
  }
}
```

## Types, expressions and variables

[Handbook section](docs/language/types.md#types-expressions-and-variables)

### Value types and conversions

| Type | Meaning | Can be stored, passed and returned? |
| --- | --- | --- |
| `i32` | signed interpretation of a 32-bit integer pattern | yes |
| `f32` | IEEE-754 binary32 | yes |
| `color` | RGBA8 channels, read as `i32` through `.r`, `.g`, `.b`, `.a` | yes |
| `vec2` | two `f32` components | yes |
| `distribution` | a [distribution](docs/language/random.md#distributions) family and its parameters | yes |
| `list<T>` | zero to 256 immutable values of a current element type | yes |
| `sampler<T>` | captured value/range for explicit picks; T is i32, f32, color or vec2 | yes |
| `stepper<T>` | deterministic range steps; T is i32, f32, color or vec2 | yes |
| `layer` | immutable [captured RGBA8 surface](docs/language/surfaces.md) | yes; no host input, const or collection |
| `mask` | immutable [coverage view or A8 result](docs/language/surfaces.md#masks-from-captured-alpha) | yes; same surface restrictions |
| `circle`, `rect` | three/four numeric components for outputs | output writes only |
| `void` | absence of a result | call statements only |

`u32` is an input declaration type, not a local/helper type. A point is
represented by `vec2`; there is no separate storable `point` type.

Numeric arithmetic mixes `i32` and `f32` by widening the integer to `f32`.
Builtin/helper arguments and return values also permit this widening.
Local reassignment requires an exact type match. Use `int(x)` or `float(x)`
when an explicit conversion is needed; no implicit float-to-integer conversion
is performed. Integer-to-float conversion can lose precision.

### Variables and scope

The first assignment to a fresh name declares a local and fixes its type.
`distribution name = value` also states the type and requires a
distribution value. `list<T> name = value` supplies the element context,
including for an empty literal. See [collections](docs/language/collections.md).
`sampler<T> name = value` declares a [sampling description](docs/language/samplers.md).
`stepper<T> name = value` declares a [deterministic step description](docs/language/steppers.md).
`layer name { ... }` captures a surface and binds the name after the block.
`layer copy = image` explicitly types a handle copy. A layer handle whose
capture did not execute is unset and traps when painted.
Sampler, stepper and list locals whose initializer does not run contain unset handles;
using them traps, including a sampler or stepper's empty batch pick.
Locals belong to the whole function, including those first assigned inside
a branch or loop; braces do not introduce shadowing scopes. Source is checked
in traversal order. Initialize a local before branching if it is needed later:
the language does not perform definite-assignment analysis, and untouched
runtime local slots have the default value of their type.

Helpers have their own locals. Parameters are read-only. `draw` also has
read-only inputs; helpers receive their values through parameters.
`seed` reads the current RNG state in either kind of function and cannot be
assigned. Palettes are asset-level constants visible to draw and helpers.
Typed [asset constants](docs/language/assets.md#asset-constants) are also visible in both
contexts. Their initializer type must match the declaration, allowing only
the ordinary i32-to-f32 widening; later assignment or shadowing is rejected.

### Operators

Binary operators associate leftward. The precedence below goes from lowest
to highest; parentheses explicitly select grouping.

| Precedence | Operators | Operands |
| --- | --- | --- |
| 1 | `\|\|` | `i32`, short-circuit |
| 2 | `&&` | `i32`, short-circuit |
| 3–5 | `\|`, `^`, `&` | `i32` |
| 6 | `==`, `!=` | numeric |
| 7 | `<`, `<=`, `>`, `>=` | numeric |
| 8 | `to` (random range, not chainable) | numbers, two colors or two vec2 values; see [random ranges](docs/language/random.md#random-ranges-with-to) |
| 9 | `<<`, `>>` | `i32` |
| 10 | `+`, `-` | numeric or vector/vector |
| 11 | `*`, `/`, `%` | numeric; `%` only `i32`; vector forms below |
| 12 | unary `-`, `!`, `~` | numeric/vector minus; `i32` logical/bitwise not |
| 13 | call, member access, palette index | postfix expressions |

Shifts bind more tightly than comparisons on either side:
`value < 1 << bits` means `value < (1 << bits)`, while
`value << bits < limit` means `(value << bits) < limit`. Comparison chains
are left-associative: `a < b < c` compares the normalized 0/1 result of
`a < b` with `c`. Write `a < b && b < c` when both comparisons must hold.
The [grammar](docs/language/grammar.md) records these rules.

Comparisons and logical operations return normalized `i32` 0/1. `&&` and
`||` skip the right operand when its value cannot affect the result, including
its RNG effects. Other binary operands evaluate left to right.
Integer arithmetic wraps modulo 2³². `/` and `%` use signed integer semantics
when both operands are integers; division by zero traps. Shift counts are
masked to five bits, and `>>` is logical (zero-filling). Floating division
uses IEEE-754 results, including NaN/infinity.
Signed division of `-2147483648` by `-1` also traps for overflow; signed
remainder for that pair is zero. These checks are distinct from the ordinary
wrapping behavior of integer addition, subtraction and multiplication.

Vectors use `{x, y}`, expose `.x` and `.y`, support componentwise `+`, `-`,
`*`, `/` with another vector, vector `*`/`/` scalar, scalar `*` vector and
unary minus. Scalar/vector `+`, `-`, `/`, vector comparisons and vector `%`
are rejected. Components are values rather than assignable fields.

Colors expose `.r`, `.g`, `.b` and `.a` as `i32` values in 0–255 on any color
expression: a local, input, palette element, helper result or color builtin.
Reading a channel evaluates its base once and draws no randomness itself.
Channels are not assignable either, and there is no implicit conversion between
a color and a number; build a new color with `rgb()`/`rgba()` and use
`float(c.a) / 255.0` for 0–1 arithmetic. See [color channels](docs/language/drawing.md#color-channels).

[types.ips](examples/language/types.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset ValueTypes {
  size 8x8
  draw {
    count = 2
    amount = float(count)
    count = int(amount + 0.5)
    selected = 0
    if (count > 1) { selected = 1 }
    p = {1, 2}
    q = -p + {4, 4}
    q = q * {1, 2} / {1, 2}
    q = 2 * q / 2
    bitMask = ((1 << count) | 1) ^ 2
    bitMask = (bitMask & 7) >> 1
    if (bitMask < (1 << 4) && selected) {
      pixel(int(q.x), int(q.y), #9dc)
    }
    notMask = ~bitMask
    remainder = count % 2
  }
}
```

## Control flow

[Handbook section](docs/language/control-flow.md#control-flow)

`if (condition) { ... }`, optional `else if` branches and an optional `else`
select blocks. `while (condition) { ... }` re-evaluates its condition before
each iteration. Conditions must be `i32`: zero is false and any nonzero
value is true. Compare a float or explicitly convert it instead of using it
directly as a condition.

`for i in start..end { ... }` iterates over a half-open integer range.
The start is evaluated, assigned to the counter, then the end is evaluated
once and retained. The counter must be an `i32` local; a fresh counter is
declared and remains visible after the loop. An existing `i32` local is reused.
The loop tests `i < end`, runs the body and increments `i`. Start ≥ end
produces no iterations. Assigning to the counter inside the body is allowed
and changes subsequent iteration behavior.

`break` exits the innermost loop in the current function. `continue` moves
to its next iteration; in a `for` loop it still executes the increment.
Neither statement can target a caller's loop. Inputs, outputs, palettes,
parameters and reserved names cannot serve as counters.

`return value` returns from a typed helper; `return;` returns from a void
helper or draw. Since newlines are whitespace, use the semicolon when a bare
return precedes another statement; before `}` a bare `return` is sufficient.
A value-returning helper must return on every reachable path, as required
by the bytecode validator.

When these statements leave a `layer` or `clip` block, exited scopes close in
reverse entry order. Layers retain their partial image; target, mode and clip
coverage are restored before the caller or loop resumes. Caller scopes remain
active during helper returns. See [surface scopes](docs/language/surfaces.md#clip-scopes-and-painting-masks).

See [execution limits](docs/language/tooling.md#execution-limits) for host control of fuel.

[control-flow.ips](examples/language/control-flow.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset Loops {
  size 8x8
  draw {
    rect(0, 0, 8, 8, #123)
    for i in 0..8 {
      if (i == 1) { continue }
      if (i >= 6) { break }
      pixel(i, 2, #9dc)
    }
    j = 0
    while (j < 3) {
      pixel(j, 4, #fc9)
      j = j + 1
    }
    if (j == 0) { pixel(0, 0, #f00) }
    else if (j == 3) { pixel(3, 3, #0f0) }
    else { pixel(0, 0, #00f) }
    return;
  }
}
```

## Helpers

[Handbook section](docs/language/functions.md#user-functions-and-argument-evaluation)

A helper is an asset-level `name(parameter: type, ...) { ... }` declaration.
All parameter types are explicit and use the [storable types](docs/language/types.md#value-types-and-conversions),
including scalars, lists, sampler/stepper descriptions and surface handles.
A value-returning helper declares `-> type`; omission means a void helper.
Arguments may be positional, named by the declared parameter, or mixed.
Every parameter is required, parameter names are unique, and `i32` may widen
to `f32`. For example, `paint(tint=#9dc, x=8)` binds to
`paint(x: i32, tint: color) { pixel(x, 0, tint); }`.
Forward calls are allowed. Direct and indirect recursion are rejected.

Pass uniforms to helpers explicitly. Helpers share the executing
asset's RNG stream and may draw or write its declared output slots. A helper
cannot access a caller's local variables or mutate its own parameters.
Asset [constants](docs/language/assets.md#asset-constants) are directly visible to helpers;
helper parameters cannot shadow their names. Const initializers cannot call helpers.
Calling a value-returning function as a statement discards its result.

### Argument binding and evaluation

Builtin, canvas and helper calls accept positional and named arguments, including
a mixture. Positional arguments fill signature slots from the start;
named arguments bind the indicated slot. Names do not advance the positional
counter. Duplicate, missing and unknown arguments are rejected.
Part placement (`at`) is required and named-only. Transform and uniform
overrides are optional and named-only; omitted uniforms keep their declared
typed defaults. Variadic builtin names are derived from the selected arity
(`x0`/`y0` for polygon vertices and `value0`, ... for choices).

Ordinary builtin arguments evaluate once, in signature order, independent
of written name order. `rect(color = #fff, h = 2, x = 1, w = 2, y = 1)` has
the same argument evaluation order as `rect(1, 1, 2, 2, #fff)`.
`poly` evaluates coordinate pairs before its color, although its positional
color argument comes first. Helpers evaluate every argument once in declaration
order, including named calls: `measure(y=unit(), x=unit())` evaluates `x`
before `y` when declared as `measure(x: f32, y: f32)`. Positional calls retain
their left-to-right order. Part calls have their [own order](docs/language/composition.md#part-call-order).

`pick`/`choose` evaluate all choices eagerly; only logical `&&`/`||` provide
short-circuit evaluation. RNG effects remain even when a selected result is
discarded. `roll`/`choose` isolate their stream after argument evaluation.

[functions.ips](examples/language/functions.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset Helpers {
  size 8x8
  input color tint = #9dc
  output point anchor
  draw {
    p = shifted(amount = 2, p = {1, 1})
    paint(tint = tint, p = p)
    amount = twice(2)
  }
  shifted(p: vec2, amount: f32) -> vec2 { return p + {amount, amount} }
  paint(p: vec2, tint: color) {
    pixel(int(p.x), int(p.y), tint)
    anchor = p
    return;
  }
  twice(n: i32) -> f32 { return n * 2 }
}
```

## Randomness: ranges, gradients and distributions

[Handbook section](docs/language/random.md#randomness-and-seeds)

Random operations use the current deterministic 32-bit stream. `seed` reads
its current state, so saving `saved = seed` before drawing preserves an
explicit seed value. Changing the number or order of stream draws changes
later results. Hash/noise operations with explicit seeds do not advance it.
They may still evaluate effectful arguments first. The shared compiler export
distinguishes `read_current`, `advance_current`, seeded computation, isolated
streams and stream push/restore; these summaries describe the operation itself,
separately from argument effects and execution-wide limits.

### Random ranges with `to`

`lower to upper` draws a value from the current stream every time it is
evaluated; it is not a list or a stored range. Both bounds are included:

| Bounds | Result | Draw |
| --- | --- | --- |
| `i32` and `i32` | `i32` | every integer in the range equally often |
| any `f32` bound | `f32` | uniform between the bounds; an `i32` bound widens |
| `color` and `color` | `color` | one position on the RGBA gradient ([gradients](docs/language/random.md#gradients-and-segments)) |
| `vec2` and `vec2` | `vec2` | one point of the segment ([segments](docs/language/random.md#gradients-and-segments)) |

The lower bound is evaluated once, then the upper bound, then the value is
drawn; RNG used inside a bound stays part of the current stream. Equal bounds
return that value and still consume a draw. A lower bound greater than the
upper one traps, as does a NaN or infinite `f32` bound; bounds are never
swapped. Integer draws discard the few raw values that would favour some
results, so every integer of the range is equally likely for any width, and
the full `i32` range is allowed. Redraws cost 1 fuel each; the exact algorithms
are in [random ranges](spec/iprop-v0.7.md#18-uniform-random-ranges).
Floating results never leave the bounds, even after rounding.

`to` binds more loosely than arithmetic and shifts and more tightly than
comparisons, so `base - 2 to base + 2` draws around `base`, and
`(0 to 100) < 10` compares a draw. Write `c.r + (-5 to 5)` to add a draw to
an expression. Ranges cannot be chained: `a to b to c` is rejected; use
`(1 to 3) to (5 to 8)` to draw the bounds themselves. The formatter writes
every range in parentheses.

### Gradients and segments

Between two colors or two `vec2` values, `to` draws one position `t` from 0
to 1 and moves every channel or component by it, so they never vary
independently. `#000000 to #ffffff` gives only grays, and
`{0.0, 0.0} to {1.0, 1.0}` only points with equal coordinates; write
`{0.0 to 1.0, 0.0 to 1.0}` for two independent draws.

With `with`, the distribution describes `t` on 0 to 1, as in
`0.0 to 1.0 with d`, so use a description in those units. Swapping the ends
reverses the direction a skewed distribution favours. One stream step is
drawn even for equal ends.

Colors mix their stored RGBA8 channels with the `color.mix` formula, alpha
included (unlike `color.mix`, which keeps the first alpha); there is no HSV
or linear-light conversion. Channels truncate as in `color.mix`: every byte
from a channel's smaller end up to one below its larger end is equally
likely, and the larger end itself appears only at the exact end of a
distribution's range. Vectors interpolate each component in `f32` and stay on
the segment, even for opposite large ends; an infinite or NaN end traps.

This example is intentionally rejected: write `(a to b) to c` to draw a bound.

### Distributions

`lower to upper with d` draws from the distribution `d` restricted to the
range: values outside `[lower, upper]` are never produced, and the shape
inside the range is kept, so nothing piles up at the bounds the way a
clamped draw would. Without `with` a range is uniform.

A `distribution` is a value: store it, copy it, pass it to a helper and
return it from one. `distribution d = ...` declares it explicitly; a plain
first assignment infers the type as usual. Its parameters are evaluated
once, when the constructor runs, and may come from inputs, earlier
computations or draws; changing those variables later does not change the
description. Constructing a distribution draws nothing; every `with` draw
takes its own value from the current stream, so two ranges using one
description are two independent draws. Distributions support no arithmetic,
comparison, members or conversion, and are not inputs, outputs or palette
elements.

Parameters describe the result's own units; they are not rescaled to the
range. `-5 to 5 with gaussian(mean=0.0, sigma=2.0)` clusters around 0, while
the same description with `20 to 30` samples its far tail. A range that
leaves no probability under the distribution traps, as does an equal range
outside its natural range. For an `i32` range the continuous draw rounds to
the nearest integer, halves away from zero; both bounds stay possible, with
the probability of their half intervals.

Evaluation order is the lower bound, the upper bound, the distribution, then
one stream step. Parameters are checked when the constructor runs: invalid
values trap there, even when the description is never used, and an
unassigned `distribution` local traps when drawn. Draws cost 1 fuel plus 1 per
iteration of the pinned numerical kernels, typically 20-150; the
[specification](spec/iprop-v0.7.md#19-analytic-distributions) gives the exact
algorithms. A helper named `gaussian`, `triangular` or `beta` takes
precedence over the constructor of the same name.

This example is intentionally rejected: `with` needs a distribution.

[point-distributions.ips](examples/language/point-distributions.ips), executed with seed 7.

```ips
asset density_columns {
  size 32x32
  output point material
  output point heights
  output point state

  const distribution finishes = weighted { 1: 50, 2: 20, 3: 20, 4: 10 }
  const distribution profile = density { 14.0: 0.0, 18.0: 1.0, 22.0: 0.6, 26.0: 0.0 }
  const sampler<f32> columnHeight = sampler(14.0 to 26.0 with profile)
  palette tones = [#8c969f, #8e9c70, #ad7750, #524640]

  pickHeights(spec: sampler<f32>) -> list<f32> { return spec.pick(3) }

  draw {
    style = 1 to 4 with finishes
    values = pickHeights(columnHeight)
    tint = tones[style - 1]
    for i in 0..3 {
      rect(2 + i * 10, 28.0 - values[i], 8, values[i], tint)
    }
    material = {style, style}
    heights = {values[0], values[2]}
    state = {seed & 65535, seed >> 16}
  }
}
```

## Drawing and colors

[Handbook section](docs/language/drawing.md#drawing-and-colors)

Raster calls paint the current canvas in call order. Pixel centers and edge
coverage are fixed by [canvas algorithms](spec/iprop-v0.7.md#11-canvas-operations--exact-algorithms).
Out-of-canvas coverage is clipped. Geometry arguments must be finite and
within the operation's representable domain; invalid geometry can trap.
The generated signatures distinguish integer pixels/thickness from floating
geometry.

The frozen compositor interpolates RGB for translucent sources while retaining
the destination alpha. An opaque source replaces all RGBA bytes. A translucent
source over initially transparent storage can leave nonzero RGB with alpha zero;
this is neither standard straight-alpha nor premultiplied-alpha blending.
Start with an opaque background for translucent glaze examples.

### Color helpers

A random color between two ends is `#705845ff to #c8c8c8ff`; see
[gradients](docs/language/random.md#gradients-and-segments).

### Color channels

`c.r`, `c.g`, `c.b` and `c.a` read the stored RGBA8 channels of any color
value as `i32` in 0–255; alpha is read as stored. The base is evaluated
exactly once per read, so `makeColor().r` calls the helper once, while
`makeColor().r` and `makeColor().g` are two calls. Store the color first to
read the channels of one result.

Channels are read-only. Build a modified color with `rgb()` or `rgba()`,
which clamp their channels, so `rgba(c.r + 5, c.g - 3, c.b, c.a)` needs no
extra `clamp()`. Convert explicitly for 0–1 arithmetic: `float(c.a) / 255.0`.

This example is intentionally rejected: a color has only the four channel
names, and the diagnostic lists them.

This example is intentionally rejected: a channel cannot be assigned.

[colors.ips](examples/language/colors.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset ColorHelpers {
  size 8x8
  draw {
    base = rgb(r = 32, g = 64, b = 96)
    glaze = rgba(128, 192, 160, 128)
    mixed = color.mix(base, #9dc, 0.5)
    alias = lerp(base, #9dc, 0.5)
    shaded = color.shade(mixed, 10)
    opaque = color.set.a(glaze, 255)
    multiplied = color.mul(opaque, #ccc)
    scaled = color.scale(multiplied, 0.8)
    adjusted = color.hsv(scaled, 0.25, 1.0, 1.0)
    warmer = rgba(adjusted.r + 5, adjusted.g - 3, adjusted.b, adjusted.a)
    metal = #705845ff to #c8c8c8ff
    bright = #705845ff to #e8e8e8ff with beta(alpha=5.0, beta=2.0)
    opacity = float(glaze.a) / 255.0
    rect(0, 0, 8, 8, base)
    rect(2, 2, 4, 4, glaze)
    pixel(1, 1, warmer)
    pixel(2, 1, metal)
    pixel(3, 1, bright)
    if (opacity > 0.5 && base.b == 96) {
      pixel(6, 6, #fff)
    }
  }
}
```

## Palettes

[Handbook section](docs/language/palettes.md#palettes)

`palette name = [constant, ...]` declares a non-empty, immutable asset-level
array. Elements permit literals, parentheses and one numeric unary minus,
such as `[-2, -(1), 0]` or `[-0.5, (-0.0), 1.0]`. The array has one resolved
element type: `i32`, `f32` or `color`; boolean literals count as `i32`.
Pure integer arrays remain `i32`. If any numeric element is `f32`, integer
elements widen to `f32`, so `[0, 0.5, 1]` is an `f32` palette. Colors cannot
be mixed with numbers. Integer negation
uses the ordinary wrapping 32-bit semantics and floating negation preserves
negative zero. Arithmetic, calls and repeated negation are rejected in these
legacy literal palettes.
Palettes are available in draw and helpers,
and cannot share a name with an input, output, helper or another palette.

`name[index]` requires an `i32` index and returns the element type. A negative
or out-of-range index selects the last element. The index runs once, and reading
the selected element makes no RNG draw. A bare palette is a first-class list
when it has at most 256 entries; lists use checked indices instead of fallback.

[palettes.ips](examples/language/palettes.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset PaletteHelpers {
  size 8x8
  palette colors = [#123, #9dc, #fc9]
  palette levels = [-(0.0), 0.5, 1]
  palette integers = [1, 2, 3]
  palette signed = [-2, -(1), 0]
  draw {
    c = colors[index(3)]
    last = integers[-1]
    continuous = ramp(colors, 0.5)
    intensity = ramp(t = 0.5, palette = levels)
    rect(0, 0, 8, 8, c)
    pixel(last + signed[1] + 1, 2, color.scale(continuous, intensity))
  }
}
```

## Immutable lists and polygon points

[Handbook section](docs/language/collections.md#literals-and-types)

### Literals and types

Square brackets construct a list. Elements evaluate once, from left to right,
in the caller's stream. `[10 to 15, 20 to 30]` contains two sampled integers;
reading either element again performs no draw. A distribution element is a
description, so `[gaussian(0.5, 0.18)]` itself performs no draw.

### Index and length

`items[index]` evaluates the list before its `i32` index, each once. An index
below zero or at least the length traps with `IndexRange` and `list index out
of range`. An unassigned list handle traps when used; it is different from
an assigned empty list. There is no implicit conversion of an `f32` index.

### List transformations

`items.distinct()` returns the first occurrence of each value in its original
order. `items.sort()` returns a stable ascending list of `i32`, `f32`, `color`
or `vec2`: numbers use numeric order, colors use lexicographic RGBA byte order,
and vectors use lexicographic x/y order. Both take no arguments, preserve the
element type, evaluate the receiver once and leave the original unchanged.
Use `items = items.distinct()` to replace a local, or chain
`items.distinct().sort()`. Sorting retains duplicates.

For scalar floats and vector components, signed zeros compare equal, and all
NaNs form one equal class after positive infinity. Stable sorting and distinct
retain the original bits of selected elements. `.distinct()` also accepts
distributions, samplers and steppers: it compares their stored descriptions, including
mode/family, bounds, parameter bits and original point types/bits, without
drawing or comparing handle addresses. `.sort()` rejects those description
types. Both methods work in pure asset constants and on typed empty lists.

[collections.ips](examples/language/collections.ips), executed with seed 7.

```ips
asset collections {
  size 32 24
  output point checks
  output point direction
  output point samples
  output point state

  const list<vec2> outline = [{3, 4}, {18, 2}, {25, 15}, {8, 20}]
  const list<color> steel = [#929ca8, #343a42]
  const list<distribution> shapes = [gaussian(0.5, 0.18), beta(2, 2)]
  palette legacy = [7, 11, 19]

  paintShape(points: list<vec2>, tint: color) {
    poly(points, tint)
  }

  echo(items: list<i32>) -> list<i32> {
    return items
  }

  empty() -> list<vec2> {
    return []
  }

  draw {
    paintShape(outline, steel[0])
    pixel(3, 4, steel[1])
    rolls = echo([10 to 15, 20 to 30])
    storedPalette = legacy
    checks = {list.len(outline), list.len(empty()) + storedPalette[2]}
    direction = outline[1]
    samples = {rolls[0], rolls[1]}
    shaped = 0.0 to 1.0 with shapes[0]
    state = {seed & 65535, seed >> 16}
  }
}
```

## Captured descriptions and explicit picks

[Handbook section](docs/language/samplers.md#capture-a-description)

### Capture a description

`sampler(value)` captures one value; `sampler(lower to upper with d)` captures
a range and optional distribution. `sampler<T>` supports i32, f32, color and
vec2. Construction evaluates lower, upper and distribution once in that order,
validates them and stores an immutable snapshot. It makes no intrinsic RNG
draw; nested draws in those expressions still run. Changing the original local
or input later does not change the description. Parentheses around the range
are allowed. Other value expressions are evaluated eagerly before capture;
capture is neither a lambda nor a general lazy expression.

### Pick one or several results

`spec.pick()` returns T, while `spec.pick(n)` always returns `list<T>`, including
when n is 0 or 1. n must be i32 in 0–256. The receiver is evaluated once before
n, which is also evaluated once. After those effects, the operation checks the
count, checks the handle and reserves the list before the first draw. An unset
sampler traps even for n=0. Reading or indexing the resulting immutable list
makes no further draws. The optional count name is `n`.

[sampling-palettes.ips](examples/language/sampling-palettes.ips), executed with seed 7.

```ips
asset samplingPalettes { input i32 v = 0
  size 32x24
  output point checks
  output point direction
  output point samples
  output point normalized
  output point state

  const distribution typical = gaussian(mean=25.0, sigma=2.0)
  const distribution finishCurve = gaussian(mean=0.5, sigma=0.18)
  const sampler<f32> position = sampler(0.0 to 1.0 with finishCurve)
  const sampler<color> finish = sampler(#343a42 to #d8dce2 with finishCurve)
  const list<sampler<i32>> fixed = [sampler(40)]

  palette widths = [10 to 15, 20 to 30 with typical, 40]
  palette tints = [finish, #929ca8]
  palette directions = [{3, 2} to {26, 2} with beta(2, 3), {3, 4}]

  description(items: list<sampler<i32>>, i: i32) -> sampler<i32> {
    return items[i]
  }

  draw {
    spec = description(widths, v)
    values = spec.pick(3)
    material = tints[0].pick()
    direction = directions[0].pick()
    normalized = {position.pick(), 0}
    checks = {list.len(values), fixed[0].pick(1)[0]}
    samples = {values[0], values[2]}
    for i in 0..3 {
      rect(2, 2 + i * 6, values[i], 4, material)
    }
    state = {seed & 65535, seed >> 16}
  }
}
```

## Parts

[Handbook section](docs/language/composition.md#imports-and-part-composition)

`import alias from "path.ips"` precedes the asset. The path is relative to
the file that contains the import, and an imported asset may import parts
of its own ([nested parts](docs/language/composition.md#nested-parts)). Duplicate aliases in one asset,
missing files and import cycles are errors. The compiler links every part
into one executable; generation does not load source files.
The alias `surface` is reserved for the [surface namespace](docs/language/surfaces.md);
rename an existing import using that alias when recompiling.
Parts can share immutable mask/layer snapshots through
[resource outputs](docs/language/surfaces.md#export-regions-from-parts). They follow each
hierarchy level's placement and D4 once; host indices still count geometry only.

`alias.draw(seed = seed, at = {x, y}, uniforms...)` requires one integer
seed and a named placement. Seed may be positional or named. Transform
arguments and input overrides are named only.

Placement `at`, transform arguments and optional input overrides are named
only. Extra positional arguments, unknown names and missing seed or
placement are errors. Input names `variant` and `archetype` remain ordinary
uniform names. Unspecified
uniforms use the part's defaults. `at` positions the part's declared origin.
Call order determines paint order; the parent's RNG stream is restored
after the part executes to its state after the dynamic argument expressions.
An override containing `unit()` therefore still advances the parent stream;
random draws made by the child use its isolated stream. A part may paint and
write its own outputs, and its conservative exported effect summary includes
callee/placement checks rather than assuming the child is pure.

Optional transform arguments are compile-time integers: `rot` is 0, 90, 180
or 270; `mirror_x`/`mirror_y` are 0 or 1, including boolean constants.
Each can occur once. Rotation is applied before the mirrors. Positive angles
are counterclockwise in the downward-y plane; transforms are exact D4 pixel
permutations, not filtered image rotation.

Scalar `alias.outputs.name` reads the last called part's output, including
point `.x`/`.y`. A read before that slot has been set traps. The value is in
the reading asset's coordinates: the child's transform and placement are
already applied, so it can place another part or be drawn directly.
Assigning it to a parent's output propagates it. List outputs are
host-facing and cannot be read this way. The host receives every output in
the root asset's coordinates, including points, rectangles, circles and
list elements written by part helpers; each enclosing call is applied once,
when the value is written. A singular output keeps its last written value: a
later call that does not write it neither replaces nor moves it.

### Part call order

Uniform overrides evaluate in the part's input declaration order. Then the
`seed` and placement expressions evaluate, regardless of the order
of named arguments in the source. Transform
arguments are compile-time constants. Each dynamic argument evaluates once.
See [parts and D4 transforms](spec/iprop-v0.7.md#105-part-transforms)
for the exact geometry mapping and rectangle bounds, and
[part instances](spec/iprop-v0.7.md#106-part-instances-section) for the output
coordinates.

The current example uses named arguments in a different written order from
their evaluation order. Omitted overrides use the part's resolved declaration
defaults, including signed integer and floating constants; no new runtime
input type is introduced for a signed source declaration.

### Nested parts

An imported asset may import parts itself, to any depth. Each import path is
resolved beside the importing file: a `hilt.ips` in `parts/hilt/` that
imports `"grip.ips"` loads `parts/hilt/grip.ips`, whichever asset imports
the hilt. `ipc` resolves paths on the file system, so `..` after a
symlinked directory leaves its target; embedding hosts resolve against
their own source store.

Aliases are local to their asset. An instance is named by its alias path
from the root, such as `hilt.grip`, and each alias path is a separate
instance with its own inputs, outputs and RNG stream, even when two branches
import the same file. That file is still loaded and parsed once. A parent
draws and reads only its direct children; to expose a grandchild's output,
the child copies it to an output of its own. At most 256 instances may
result from expanding the imports. A cycle, including one through an
equivalent path such as `"../dir/a.ips"`, is an error naming the whole
chain.

[part.ips](examples/language/part.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
asset Part {
  size 4x4
  origin 2, 2
  input color tint = #9dc
  output point anchor
  output list point dots
  draw {
    rect(0, 0, 4, 4, tint)
    anchor = {2, 1}
    dots = {1, 1}
  }
}
```

[composition.ips](examples/language/composition.ips), an API demo executed with seed 7: it shows the calls of this section on a tiny canvas, not the style of a prop.

```ips
import leaf from "part.ips";
asset Composition { size 16x16
  output point attached
  draw {
    leaf.draw(at = {8, 8}, tint = #fc9, seed = seed, rot = 90, mirror_x = 1)
    attached = leaf.outputs.anchor
    pixel(int(leaf.outputs.anchor.x), int(leaf.outputs.anchor.y), #fff)
  }
}
```

Part slots: seed positional required; at named-only required; rot named-only default=0; mirror_x named-only default=0; mirror_y named-only default=0; $uniform named-only default=input_declaration.
Part evaluation order: $uniform, seed, at, rot, mirror_x, mirror_y.

## Captured layers and surface painting

[Handbook section](docs/language/surfaces.md#layers-and-alpha-composition)

### Capture geometry once

`layer name { statements }` starts a transparent RGBA8 target with the current
target's dimensions and raster scale. Primitives, weather, helpers and part
calls write into that target. The block executes once in the current RNG stream;
called parts keep their isolated streams and existing argument order. Capture
does not automatically paint its parent.

After the block closes, `name` holds an immutable `layer`. Copying it, passing
it to a typed helper or returning it shares the buffer. A later capture with the
same name creates a new snapshot; earlier copies remain unchanged. `layer copy
= image` can state the local type explicitly. Lists and samplers of layers,
pixel reads, arithmetic, comparisons, host inputs and asset constants are
unsupported. An initializer skipped by a branch leaves an unset handle; using
it traps rather than silently producing an empty image.

The name becomes available after its block, including in the function's usual
scope outside a loop or conditional. `return`, `break` and `continue` close
every capture they exit with its current image and restore the previous target.
A loop inside a layer does not close that layer when it exits. Returning a
value evaluates it before scope cleanup. Outputs written in a capture publish
once, even if the captured image is never painted.

`layer` is a reserved keyword. Rename old locals, inputs and helpers with
that spelling before compiling their source. A capture result must be a writable local;
parameters, inputs, constants, palettes and output declarations cannot be its
destination.

### Paint a snapshot

Paints a captured layer into the current target using its saved mapping. The
image expression evaluates once, with no intrinsic RNG, rerendering or pixel
copy. The full source canvas is visited in row order, including transparent and
off-target pixels; transparent source pixels are skipped by legacy composition.
`surface.paint` adds no `at`, rotation or mirror arguments: placement and D4
come from the existing part mechanism, shared with captured surfaces.

Omitted `alpha` on a layer inherits the active target's composition mode.
The root always starts in `surface.legacy`. Select
`layer name(alpha=surface.source_over)` for correct straight RGBA8 alpha,
or `alpha=surface.legacy` for the frozen compositor. Mode arguments must be
literal namespace constants; a numeric variable or const alias is rejected.
`surface.legacy` and `surface.source_over` have scalar constant values 0 and 1,
but those integers cannot be passed as mode arguments.

Helpers share the active target and its mode. A part's temporary canvas and
its blit inherit that mode; ordinary part source therefore works with
source-over when called inside such a layer. An explicit layer inside the
part may override it. End, return, break and continue restore both the
caller's target and its mode. Weather still changes RGB on the active target
and preserves alpha.

Omitted `blend` on `surface.paint(image)` uses the **destination's active
mode**. The image's capture mode does not select its blend. An explicit
`blend=surface.source_over` or `blend=surface.legacy` overrides that one
paint operation, without changing the target's mode for subsequent draws.

### Exact RGBA8 source-over

Channels use their stored 0–255 values; composition does not convert to
linear light and does not change `color.mix`. For source `Cs, As` and
destination `Cd, Ad`, calculate with unsigned 64-bit integers:

All divisions are integer divisions. Each RGB channel rounds to nearest,
with exact halves rounding up. If `D == 0`, write `(0,0,0,0)` instead of
performing channel division. A zero-alpha source leaves a visible destination
unchanged; over a zero-alpha destination it canonicalizes hidden RGB to zero.
Full-source paint visits include those transparent pixels.

Every primitive, part blit and paint stores a rounded RGBA8 result before
the next operation. Layers are never flattened or fused; grouping and order
can change rounding. For example, `#11223380` on an empty source-over target
stores `(17,34,51,128)`; adding `#ff000080` stores `(176,11,17,192)`.

Legacy remains exact: opaque replacement, floor RGB division for partial
alpha, unchanged destination alpha and hidden RGB. Legacy part blits and
paint skip zero-alpha source pixels. Existing binaries retain this behavior.

The namespace leaves an ordinary `paint(...)` helper available. An import alias
named `surface` conflicts with the new namespace and receives a located
diagnostic; rename that alias when migrating its source.

### Gaussian blur

Both surface types provide the ordinary method `.blur(sigma)`:

### Resources and compatibility

At most 32 layer and clip scopes combined may be active across the complete helper/part call
stack. Each capture reserves `128 + 4 * width * height` bytes from the shared
64 MiB generation arena. Copies and mapping views charge no second buffer.
Root canvas allocation retains its existing limits. Extra part canvases use the
arena and release their allocation when the last handle expires. Existing part allocation, blit fuel,
pixel rules, output transforms and RNG stay unchanged.

A capture instruction costs one fuel plus one per target pixel cleared; paint
costs one plus one per source pixel visited. These costs are reserved before
allocation or scanning. Drawing inside a layer keeps the primitive's usual
fuel cost. Each surface buffer releases when its last handle is destroyed;
success or a trap clears the generation's remaining handles. A failed
generation never publishes a partial image. Default fuel remains 5,000,000.

Layers use the file's type descriptor table. They do not add
public C input/output enums or host layouts. Host outputs remain points,
rectangles and circles; resource exports use their own internal table.

### Masks from captured alpha

Returns an immutable `mask` view of a layer's alpha, including partial coverage.
It shares the RGBA8 buffer and mapping without scanning pixels or drawing again.
Use opaque geometry in a layer when the mask should represent shape alone;
legacy partial draws on an empty layer keep zero alpha. A mask can be copied,
passed to a helper or returned. `mask name = expression` is a typed copy.
Unassigned masks trap. Masks cannot be const, host inputs, geometry outputs or
list/sampler elements, and have no arithmetic, comparisons or pixel reads.
`mask` and `clip` are reserved in every name position.

### Coverage algebra

Takes the maximum of the two coverages at each pixel.

Takes the minimum of the two coverages at each pixel.

Uses `min(a, 255 - b)`, including partial alpha; this is not arithmetic subtraction.

Uses `255 - a` inside the input's rectangle and zero outside that rectangle.

Two-mask operations allocate the bounding rectangle of both input surfaces,
at the active target's raster scale. Inputs keep their buffers and coverage.
Masks are interpreted in the current asset's coordinates; exact D4 and existing
part raster scales are supported, with no arbitrary resampling. Reads outside
an input rectangle are zero. Nonfinite scales or unrepresentable bounds trap.
The result stores one A8 byte per pixel, without a duplicate RGBA image.

### Disk morphology and offsets

Takes the maximum coverage in the disk `dx*dx + dy*dy <= radius*radius`.
The result includes a border of `radius` pixels outside the original rectangle,
so an outline can extend beyond it.

Takes the minimum in the same disk, including zero outside the surface.
Its rectangle remains the input rectangle. Radius is `i32` in **0–64**, measured
in target pixels; radius zero preserves coverage. Constant bad radii reject at
compilation; dynamic bad radii trap before scanning.

Moves the mapping by `dx`, `dy` integer target pixels without copying the buffer.
Offsets may move the whole mask off-canvas. Algebra and morphology retain those
outside pixels, while painting only writes within the target.

### Clip scopes and painting masks

`clip(region) { ... }` evaluates its mask once and limits writes on the current
target. Helpers inherit the active clips. A layer capture, like a part, starts
its own canvas without the parent's clips; they apply once where
`surface.paint` paints the layer, even when it was captured inside the same
clip. The parent likewise applies its clips once at a part's blit.
The child's own clips can independently limit its image. Geometric outputs
remain complete even when the image is clipped.

Combine clip coverage from outer to inner, starting at 255, using
`(a*b + 127)/255` at each step. Scale primitive/paint source alpha by the combined
coverage with the same rounding, then use the active legacy/source-over kernel.
Zero coverage skips the write entirely, preserving hidden RGB in both modes.
`surface.paint(region, color=...)` first scales color alpha by mask coverage,
then applies combined clip coverage. A mask requires a color; omitted blend
inherits the destination mode. Image, color and static blend are evaluated in
signature order, regardless of named argument order, without implicit RNG.

Weather runs its usual algorithm; each proposed write interpolates the old and
proposed RGBA channels by combined clip coverage, rounding nearest with halves
up. Coverage zero preserves the old pixel, and weather still preserves alpha.
Normal scope exit, return, break and continue restore target, mode and clips in
reverse entry order. A trap releases generation resources and publishes no image.

An A8 result reserves `128 + width*height` bytes. Its instruction costs one fuel,
plus one per zeroed pixel and one per algebra output pixel, or one per logical
disk sample for morphology. This counts all disk samples even when an optimized
scan stops early. Radius 1 has five samples; radius 64 has 12,853. `mask.from`
and `offset` cost one without scanning; paint visits each source rectangle pixel.
Every in-target attempted write charges one per active clip level, including
zero coverage. Fuel is reserved before allocation or scanning. Shared views do
not charge the full buffer again.

### Export regions from parts

Declare `output mask region` or `output layer image` in a component and assign
a matching handle. Its parent reads `child.outputs.region` or
`child.outputs.image` after calling that direct child. Exporting does not paint:
explicitly paint a layer or use a mask for clipping. Capture an entire part
with `layer name { child.draw(...) }`; there is no `part.render`.

Each write freezes a snapshot in the owner's caller coordinates, including
that call's raster scale, placement and D4. Propagating through the parent's
own output applies the next hierarchy level once. Local copies retain their
buffer and mapping after slot replacements. A later call with no write retains
the previous snapshot **and its previous placement**. Reading requires this
frame's last-called part; an unwritten slot traps. A root resource output is
also internal and is not host-readable.

The format has a separate resource-export table. C API `output_count`, part
output ranges and host indices count geometry only; point/rect/circle tags
and ABI layouts do not change. Slots reserve 64 bytes each in the generation
arena. Views share a buffer, charge it once, and do not scan pixels. A write
or read costs one instruction fuel and consumes no RNG. The last handle frees
its surface; success and traps clear remaining generation resources. Handles
cannot escape a generation or survive in a cross-seed cache.

[captured-panels.ips](examples/language/captured-panels.ips), executed with seed 7.

```ips
asset captured_panels {
  size 32x24
  output point panelSize
  output point state

  draw {
    layer panel(alpha=surface.legacy) {
      width = 24 to 28
      frame(width)
      panelSize = {width, 18}
    }
    rect(0, 0, 32, 24, #243242)
    surface.paint(reuse(panel))
    pixel(16, 12, #ffffff)
    surface.paint(panel, blend=surface.legacy)
    state = {seed & 65535, seed >> 16}
  }

  reuse(image: layer) -> layer { return image }

  frame(width: i32) {
    rect(2, 3, width, 18, #343a42)
    rect(4, 5, width - 4, 14, #929ca8)
    pixel(4, 5, #c09060)
    pixel(width - 1, 18, #c09060)
  }
}
```

[blur.ips](examples/language/blur.ips), executed with seed 7.

```ips
asset softGlow {
    size 16x16
    draw {
        layer image(alpha = surface.source_over) { rect(5, 5, 6, 6, #ff804080) }
        softImage = image.blur(1.5).blur(0)
        softMask = mask.from(image).blur(sigma = 2)
        surface.paint(softMask, #40b0ff, blend = surface.source_over)
        surface.paint(softImage, blend = surface.source_over)
    }
}
```

## Execution limits

[Handbook section](docs/language/tooling.md#execution-limits)

### Execution limits

The compiler currently emits a default fuel budget of 5,000,000. The host
can supply its invocation budget; each executed instruction/drawing operation
retains its normative cost. Optimizations may reduce total fuel without
changing image/output bits. A loop need not be statically bounded, but
execution traps when fuel runs out.

Canvas dimensions are limited to 16384 per side in the reference runtime.
Hosts may impose smaller limits on dimensions or total pixel counts.
Binary pools/indices, function nesting, stream stacks
and structured outputs also have bounds; see
[validation and resource limits](spec/iprop-v0.7.md#14-validation-and-limits).
Compressed v0.7 files bound both encoded and decoded size to 64 MiB.
Host-specific limits and invalid input values can reject an
invocation before draw starts.

Integer division/remainder by zero traps; float division follows IEEE-754.
Noise coordinate checks, nonfinite HSV hue, invalid geometry, unset part
output reads and unbalanced stream pops can also trap. Traps must be handled
by the host; a partial image is not a successful generation result.

## Grammar

[Handbook section](docs/language/grammar.md#grammar)

This EBNF records the current parser. Braces in the grammar mean repetition,
square brackets mean optional syntax and quoted strings are literal tokens.
`identifier` is an ordinary identifier; `name` additionally accepts soft
keywords as described in [source syntax](docs/language/syntax.md). Semantic checks such as
types, restricted declaration constants, required declarations and acyclic calls
apply after parsing. Strings are only valid as import paths after typing.

In `return`, the optional expression is omitted when the next token is `;`
or `}`; newline alone does not terminate it. Numeric token conversion has
the [literal restrictions](docs/language/syntax.md#literals) described above. The parser
allows empty palette/compound syntax, but semantic checks reject empty
palettes and compounds whose arity is not 2, 3 or 4. Metadata keywords are
recognized contextually; their names cannot introduce helpers in that position.
Declaration typing rejects strings as values and permits unary minus only
on numeric literals. Palette elements use the same restricted constant
grammar after parsing; a general expression AST does not make it a valid
palette element.

## Builtins

Every builtin of this compiler, grouped by handbook chapter: exported signatures, then the first paragraph of its handbook description.

### Randomness and seeds

`gaussian(mean: f32, sigma: f32) -> distribution`
The normal distribution with centre `mean` and standard deviation `sigma`;
`sigma` must be positive and both finite. Its natural range is unbounded, so
any range with probability can use it.

`triangular(min: f32, peak: f32, max: f32) -> distribution`
A triangle from `min` to `max` whose most frequent values lie at `peak`.
Requires finite `min < max` and `min <= peak <= max`; a peak at an end makes
small (or large) values most common.

`beta(alpha: f32, beta: f32) -> distribution`
`beta(alpha: f32, beta: f32, min: f32, max: f32) -> distribution`
The beta distribution with shape parameters `alpha` and `beta`, both positive
and finite, on the natural range `min` to `max` (0 to 1 when omitted;
`min < max`). `alpha < beta` favours the low end, `alpha > beta` the high end,
equal values above 1 the middle, and values below 1 both ends.

`unit() -> f32` (draws from the stream)
Style: Prefer `0.0 to 1.0`, or `lower to upper` for the value itself.
Consumes one stream draw and returns an `f32` in `[0, 1)`. The frozen stream
advance and conversion are specified in [RNG](spec/iprop-v0.7.md#122-stream-advance-rngunit).

`range(a: f32, b: f32) -> f32` (draws from the stream)
Style: Prefer `lower to upper`, which includes both ends; `range` keeps its own draws for existing generators.
Consumes one draw and computes `a + (b - a) * unit()` with pinned binary32
steps. Bounds are not reordered. Ordinary ordered bounds describe a uniform
range; floating rounding follows the exact arithmetic contract.

`index(n: i32) -> i32` (draws from the stream)
Style: Use it to select a palette element; for a number in a range write `lower to upper`.
Consumes one draw and selects using the unsigned interpretation of `n`.
Zero returns zero; a negative `i32` is a large unsigned bound, not an error.
Use a positive bound for ordinary palette/array selection.

`pick(n: i32) -> i32` (draws from the stream)
`pick(value0: T, value1: T, ...) -> T` (draws from the stream)
`pick(n)` is exactly `index(n)`. With two or more arguments it eagerly
evaluates choices, then consumes one selection draw. Choices must share a
storable type; mixed numeric choices widen to `f32`. Named choice arguments
are `value0`, `value1`, etc. A single non-integer choice is rejected because
the single-argument overload is the index alias.

`chance(p: f32) -> i32` (draws from the stream)
Returns `i32` 0/1 for `unit() < p` after evaluating `p`. It consumes a draw
even when `p <= 0`, `p >= 1`, or `p` is NaN. NaN yields false.

`roll(seed: i32, a: f32, b: f32) -> f32` (isolated seeded stream)
Draws a range using an explicit `i32` seed. Arguments evaluate in signature
order before seed isolation. The isolated operation consumes one draw and
restores the outer stream to its state after argument evaluation.

`choose(seed: i32, value0: T, ...) -> T` (isolated seeded stream)
Selects from one or more eager typed choices using an explicit seed. Choice
type rules match multi-choice `pick`. A single choice still performs an
isolated draw. Nested isolated calls restore each caller's stream. Named
arguments are `seed`, `value0`, `value1`, etc.

`hash(x: i32) -> i32`
Returns the frozen splitmix32 hash of the argument's bit pattern as `i32`.
It does not change the current stream. It is useful for deriving independent
seeds from a saved seed and a coordinate or index.

`stream_push(seed: i32) -> void` (push seeded stream)
Saves the current stream and derives a nested stream from the supplied seed
according to the VM contract. Pair it with `stream_pop` before returning.
This is stream derivation, not the direct seed assignment used by `roll`.

`stream_pop() -> void` (restore stream)
Restores the most recently pushed stream. An unmatched pop traps. Nesting
is bounded by the VM's stream-stack limit. Balanced calls isolate a sequence
of draws while retaining the caller's stream.

### Numbers and interpolation

`int(x: i32) -> i32`
`int(x: f32) -> i32`
An `i32` is preserved exactly. An `f32` truncates toward zero with saturation
to the signed 32-bit bounds; NaN becomes 0. Use this conversion for integer
pixel coordinates and conditions.

`float(x: f32) -> f32`
`float(x: i32) -> f32`
Widens `i32` to binary32. An existing `f32` is preserved, including negative
zero. Integers beyond binary32 precision can round.

`i32trunc(x: f32) -> i32`
The explicit floating truncation operation: same saturated/NaN conversion
as the `f32` overload of `int`.

`round(x: f32) -> i32`
Returns an `i32`, rounding to the nearest integer with halfway values away
from zero, then applying saturated truncation. NaN becomes zero; infinities
saturate. An i32 argument widens to f32 before rounding. See the
[rounding contract](spec/iprop-v0.7.md#94-float).

`sqrt(x: f32) -> f32`
Binary32 square root. Negative arguments yield NaN; negative zero is
preserved according to the floating contract.

`sin(x: f32) -> f32`
Pinned sine approximation, with angle in radians. Exact range reduction,
nonfinite behavior and polynomial evaluation are specified in
[trigonometry](spec/iprop-v0.7.md#127-trigonometry).

`cos(x: f32) -> f32`
Pinned cosine approximation, with angle in radians and the same normative
trigonometry contract as `sin`.

`abs(x: f32) -> f32`
Floating magnitude. It accepts numeric widening and returns `f32`.

`floor(x: f32) -> f32`
Rounds a float downward and returns a float. Convert explicitly when an
integer result is required.

`ceil(x: f32) -> f32`
Rounds a float upward and returns a float.

`min(x: f32, y: f32) -> f32`
`min(x: i32, y: i32) -> i32`
Returns exact signed `i32` when both arguments are integers. If either argument
is `f32`, integers widen and the floating result uses pinned minNum semantics:
one NaN selects the
other argument, two NaNs produce canonical NaN, and negative zero precedes
positive zero.

`max(x: f32, y: f32) -> f32`
`max(x: i32, y: i32) -> i32`
Returns exact signed `i32` for two integers, otherwise `f32` with the
corresponding maxNum semantics; positive
zero succeeds negative zero.

`pow(x: f32, y: f32) -> f32`
Pinned floating exponentiation. Zero, negative bases, integer/noninteger
exponents, NaN and infinity cases follow
[the math contract](spec/iprop-v0.7.md#128-the-math-package).

`exp(x: f32) -> f32`
Pinned base-e exponential. Overflow/underflow and nonfinite handling are
defined by the same math contract.

`atan2(y: f32, x: f32) -> f32`
Returns the pinned angle in radians. The argument order is `y, x`; signed
zero and quadrant handling follow the math specification.

`smooth(t: f32) -> f32`
Clamps `t` to `[0, 1]` and applies cubic smoothstep `t*t*(3 - 2*t)` using
the frozen operation order.

`lerp(a: f32, b: f32, t: f32) -> f32`
`lerp(a: color, b: color, t: f32) -> color`
Interpolates numbers or colors with clamped `t`. Numeric inputs widen to
`f32`. Color interpolation has the same behavior as `color.mix`, including
preservation of the first color's alpha.

`gain(t: f32, k: f32) -> f32`
Schlick contrast curve. `t` clamps to `[0, 1]`, `k` to `[1e-4, 1]`.
`k = 0.5` is identity; `k = 1` returns 0.5 for interior positions and retains
endpoints. A NaN argument produces canonical NaN before clamping.

`quintic(t: f32) -> f32`
Clamped quintic smootherstep, also used by gradient noise's fade function.

`smoothstep(a: f32, b: f32, t: f32) -> f32`
Maps `t` between two edges through clamped smooth interpolation. Exact
equal-edge and reversed-edge behavior follows
[the curve toolkit](spec/iprop-v0.7.md#1214-the-curve-toolkit).

`remap(t: f32, a: f32, b: f32, c: f32, d: f32) -> f32`
Maps a value from interval `[a, b]` to `[c, d]`; this is a range transform,
not an automatic clamp. Degenerate intervals follow the frozen float rules.

`clamp(x: f32, min: f32, max: f32) -> f32`
`clamp(x: i32, min: i32, max: i32) -> i32`
Bounds a value between the two supplied limits, ordering them first, so reversed
or equal bounds are valid. All three integer arguments select exact signed
`i32`; any `f32` argument selects the existing floating operations. Parameter
names are `x`, `min`, `max`. Floating NaN and signed-zero behavior is unchanged.
See [0.7 migration](docs/language/migration-v0.7.md#keep-integer-and-floating-locals-consistent)
for assignments to existing f32 locals.

### Drawing and colors

`pixel(x: i32, y: i32, color: color) -> void`
Style: Prefer one `rect` per row for areas; keep `pixel` for single dots.
Paints one integer pixel with a color, subject to clipping and blending.

`rect(x: f32, y: f32, w: f32, h: f32, color: color) -> void`
Paints an axis-aligned rectangle using floating x/y/width/height and the
normative pixel-center fill rules.

`line(ax: f32, ay: f32, bx: f32, by: f32, thickness: i32, color: color) -> void`
Paints a segment using floating endpoints and an integer thickness. The
segment rasterizer's endpoint and cap rules are frozen.

`curve(a: vec2, b: vec2, c: vec2, thickness: f32, color: color) -> void`
`curve(a: vec2, b: vec2, c: vec2, d: vec2, thickness: f32, color: color) -> void`
Paints a quadratic or cubic Bézier stroke depending on whether three or
four control points are supplied. Thickness is a floating diameter in target
pixels; strokes have round caps. Nonfinite geometry is rejected. Distance
estimation and coverage follow the pinned bounded Bézier kernel.

`oval(x: f32, y: f32, rx: f32, ry: f32, color: color) -> void`
Paints an ellipse centered at x/y with radii rx/ry, using the exact raster
contract. Use valid positive radii for ordinary ellipses.

`poly(points: list<vec2>, color: color) -> void`
`poly(color: color, x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, ...) -> void`
Paints a filled polygon with at least three `(x, y)` pairs. Positional color
comes first. Named coordinates are `x0`, `y0`, `x1`, `y1`, etc., with `color`.
`poly(points, color)` also accepts an immutable `list<vec2>` of at least
three vertices. Its named argument is `points`; points evaluate before color.
Coordinates evaluate first in vertex order, then color, even for reordered
named arguments. The point-in-polygon rule is specified in the canvas contract.

`weather(seed: i32, amount: i32) -> void` (seeded computation)
Applies the pinned seeded wear operation to the current image. Amount is an
integer wear parameter; the explicit seed makes it independent of stream
draws. It operates on pixels already drawn, so place it after the base shapes.

`rgb(r: i32, g: i32, b: i32) -> color`
Style: For a random color, prefer a gradient draw (`dark to light with d`) to random channels.
Builds an opaque color from integer red/green/blue channels clamped to 0–255.

`rgba(r: i32, g: i32, b: i32, a: i32) -> color`
Style: For a random color, prefer a gradient draw (`dark to light with d`) to random channels.
Builds a color with an integer alpha channel; all four channels clamp to 0–255.

`color.mix(a: color, b: color, t: f32) -> color`
Style: When the mix amount is random, prefer a gradient draw (`dark to light with d`).
Interpolates RGB with clamped floating `t`, retaining the first argument's
alpha. The color overload of `lerp` is equivalent.

`color.shade(color: color, amount: i32) -> color`
Offsets RGB brightness by the integer amount with channel clamping. Alpha
is preserved. Bare `shade`/`mix` names are not builtins in the current language.

`color.set.a(color: color, alpha: i32) -> color`
Replaces alpha with a clamped integer channel while retaining RGB.

`color.mul(a: color, b: color) -> color`
Multiplies color channels using the pinned integer conversion. See
[color utilities](spec/iprop-v0.7.md#1212-the-colours) for alpha and rounding.

`color.scale(color: color, scale: f32) -> color`
Scales RGB by a floating factor with pinned conversion/clamping; preserves
alpha. Nonfinite behavior follows the color utility contract.

`color.hsv(color: color, hue: f32, saturation: f32, value: f32) -> color`
Adjusts hue by turns and scales saturation/value according to the frozen HSV
conversion, preserving alpha. Nonfinite hue shifts trap; this is a color
adjustment, not a constructor from three standalone HSV channels.

### Vectors and distance fields

`vec2.len(v: vec2) -> f32`
Returns vector length using the pinned multiply/add/square-root order.

`vec2.dot(a: vec2, b: vec2) -> f32`
Returns the scalar dot product of two vectors.

`vec2.normalize(v: vec2) -> vec2`
Returns a unit direction from a finite vector. Zero, including signed zero,
returns `{+0, +0}`. NaN or infinity in either component traps. The scaled
binary64 algorithm handles maximum and subnormal f32 components without
overflowing the length; it draws no randomness. See the
[normalization contract](spec/iprop-v0.7.md#22-stable-vector-normalization).

`vec2.rot(v: vec2, a: f32) -> vec2`
Rotates a vector by a floating angle in radians using pinned sine/cosine.
Positive angles are counterclockwise in the downward-y plane. This continuous
vector rotation is distinct from the exact discrete part transforms.

`dist.circle(p: vec2, center: vec2, radius: f32) -> f32`
Signed distance to a circle defined by a center and radius.

`dist.rect(p: vec2, center: vec2, half_size: vec2) -> f32`
Signed distance to a centered axis-aligned rectangle. `half_size` contains
half-width and half-height, not its full dimensions.

`dist.roundrect(p: vec2, center: vec2, half_size: vec2, radius: f32) -> f32`
Signed distance to a centered rounded rectangle with half-dimensions and a
corner radius. Supply dimensions/radius suitable for the shape being authored.

`dist.line(p: vec2, a: vec2, b: vec2) -> f32`
Distance to a finite segment. Degenerate segments and nonfinite inputs follow
the exact [distance-field contract](spec/iprop-v0.7.md#129-signed-distance-fields).

`dist.bezier(p: vec2, a: vec2, b: vec2, c: vec2) -> f32`
`dist.bezier(p: vec2, a: vec2, b: vec2, c: vec2, d: vec2) -> f32`
Distance estimate to a quadratic/cubic Bézier curve on `[0, 1]`, selected by
the control-point count. The kernel performs a fixed bounded search rather
than an arbitrary host-library approximation. Nonfinite geometry traps.

`fill(d: f32, w: f32) -> f32`
Converts signed distance and a transition width to floating coverage.
The exact zero/nonpositive width and NaN rules are in the distance-field spec.

`fold(v: vec2, n: i32) -> vec2`
Folds a vector into an n-sector kaleidoscope domain, returning a `vec2`.
The sector count is an integer. Valid counts and angular folding follow
[fold and polar](spec/iprop-v0.7.md#1213-the-dither-the-fold-and-the-polar).

`polar(v: vec2) -> vec2`
Returns `{radius, angle}` for a vector; angle is in radians under the same
downward-y convention as vector rotation. Signed-zero behavior is pinned.

### Noise and procedural utilities

`noise(x: f32, y: f32, seed: i32) -> f32` (seeded computation)
Frozen value noise over the lattice, with interpolation defined by
[the original noise contract](spec/iprop-v0.7.md#124-noisex-y-seed).

`fault(x: f32, y: f32, seed: i32) -> f32` (seeded computation)
Frozen triangular noise/fault primitive with explicit coordinates and seed;
its exact hashing and interpolation are in the adjacent fault specification.

`noise.grad(x: f32, y: f32, seed: i32) -> f32` (seeded computation)
Gradient noise. Coordinates must be finite, with each floor in
`[-2147483648, 2147483646]`. A failure traps with `noise coordinate out of range`.

`noise.fbm(x: f32, y: f32, octaves: i32, seed: i32) -> f32` (seeded computation)
Normalized octave sum of gradient noise. The integer octave count clamps to
1–8. The gradient coordinate domain is checked at every octave, after
frequency scaling.

`noise.cell(x: f32, y: f32, seed: i32) -> f32` (seeded computation)
Worley F1 distance using the complete pinned 5×5 search window. Coordinates
must be finite, with floors in `[-16777214, 16777214]`.

`noise.ridge(x: f32, y: f32, octaves: i32, seed: i32) -> f32` (seeded computation)
Normalized ridged octave sum. Octaves clamp to 1–8, with the same per-octave
gradient domain checks as `noise.fbm`.

`warp(x: f32, y: f32, amount: f32, seed: i32) -> f32` (seeded computation)
Domain-warped noise using the fixed inner/outer four-octave fbms. Amount must
be finite; all offset and warped samples must meet the gradient domains.

`cell.jitter(cx: i32, cy: i32, seed: i32) -> vec2` (seeded computation)
Returns an absolute point inside `[cx, cx+1) × [cy, cy+1)`. Integer cell
origins must be exactly representable as binary32. All integers within
±16777216 qualify; larger ones must also be representable. A rejected origin
traps with `cell index is not representable as f32`.

`dither(t: f32, x: i32, y: i32) -> f32`
Applies the pinned ordered Bayer threshold to `t` at integer pixel x/y.
It returns floating coverage. Matrix indexing and threshold comparisons are
specified in [dither](spec/iprop-v0.7.md#1213-the-dither-the-fold-and-the-polar).

### Palettes

`ramp(palette: palette<f32>, t: f32) -> f32`
`ramp(palette: palette<color>, t: f32) -> color`
Interpolates a `color` or `f32` palette with numeric `t` clamped to `[0, 1]`.
It returns the palette's element type. Pure integer palettes are rejected;
include a floating element when continuous numeric interpolation is wanted. A single
element returns that element, but still evaluates `t` exactly once, including
its RNG effects. Named arguments are `palette` and `t`.

### Immutable lists

`list.len(items: list<i32>) -> i32`
`list.len(items: list<f32>) -> i32`
`list.len(items: list<color>) -> i32`
`list.len(items: list<vec2>) -> i32`
`list.len(items: list<distribution>) -> i32`
`list.len(items: list<sampler<i32>>) -> i32`
`list.len(items: list<sampler<f32>>) -> i32`
`list.len(items: list<sampler<color>>) -> i32`
`list.len(items: list<sampler<vec2>>) -> i32`
`list.len(items: list<stepper<i32>>) -> i32`
`list.len(items: list<stepper<f32>>) -> i32`
`list.len(items: list<stepper<color>>) -> i32`
`list.len(items: list<stepper<vec2>>) -> i32`
Returns the list length as `i32`, without iteration or RNG. Its overloads,
like all builtin signatures, come from the compiler catalogue. It also accepts
a first-class palette of at most 256 elements.

### Layers and alpha composition

`surface.paint(image: layer) -> void`
`surface.paint(image: layer, blend: i32) -> void`
`surface.paint(image: mask, color: color) -> void`
`surface.paint(image: mask, color: color, blend: i32) -> void`
Paints a captured layer into the current target using its saved mapping. The
image expression evaluates once, with no intrinsic RNG, rerendering or pixel
copy. The full source canvas is visited in row order, including transparent and
off-target pixels; transparent source pixels are skipped by legacy composition.
`surface.paint` adds no `at`, rotation or mirror arguments: placement and D4
come from the existing part mechanism, shared with captured surfaces.

`mask.from(image: layer) -> mask`
Returns an immutable `mask` view of a layer's alpha, including partial coverage.
It shares the RGBA8 buffer and mapping without scanning pixels or drawing again.
Use opaque geometry in a layer when the mask should represent shape alone;
legacy partial draws on an empty layer keep zero alpha. A mask can be copied,
passed to a helper or returned. `mask name = expression` is a typed copy.
Unassigned masks trap. Masks cannot be const, host inputs, geometry outputs or
list/sampler elements, and have no arithmetic, comparisons or pixel reads.
`mask` and `clip` are reserved in every name position.

`mask.union(a: mask, b: mask) -> mask`
Takes the maximum of the two coverages at each pixel.

`mask.intersect(a: mask, b: mask) -> mask`
Takes the minimum of the two coverages at each pixel.

`mask.subtract(a: mask, b: mask) -> mask`
Uses `min(a, 255 - b)`, including partial alpha; this is not arithmetic subtraction.

`mask.invert(source: mask) -> mask`
Uses `255 - a` inside the input's rectangle and zero outside that rectangle.

`mask.expand(source: mask, radius: i32) -> mask`
Takes the maximum coverage in the disk `dx*dx + dy*dy <= radius*radius`.
The result includes a border of `radius` pixels outside the original rectangle,
so an outline can extend beyond it.

`mask.shrink(source: mask, radius: i32) -> mask`
Takes the minimum in the same disk, including zero outside the surface.
Its rectangle remains the input rectangle. Radius is `i32` in **0–64**, measured
in target pixels; radius zero preserves coverage. Constant bad radii reject at
compilation; dynamic bad radii trap before scanning.

`mask.offset(source: mask, dx: i32, dy: i32) -> mask`
Moves the mapping by `dx`, `dy` integer target pixels without copying the buffer.
Offsets may move the whole mask off-canvas. Algebra and morphology retain those
outside pixels, while painting only writes within the target.

## Rejection and repair

### Keep a local's fixed type

[local-type.ips](examples/language/errors/local-type.ips)

Wrong:
```ips
asset InvalidLocal {
  size 8x8
  draw { x = 1; x = 1.5 }
}
```
Diagnostic `IPS-TYPE-001`: `cannot assign f32`.

Infer f32 from the first assignment when later values are floating.
Corrected:
```ips
asset InvalidLocal {
  size 8x8
  draw { x = 1.0; x = 1.5 }
}
```

### Pass a uniform to a helper

[helper-uniform.ips](examples/language/errors/helper-uniform.ips)

Wrong:
```ips
asset InvalidUniform {
  size 8x8
  input color tint = #9dc
  draw { paint() }
  paint() { pixel(1, 1, tint) }
}
```
Diagnostic `IPS-TYPE-001`: `only visible inside 'draw'`.

Pass the uniform explicitly through a typed helper parameter.
Corrected:
```ips
asset InvalidUniform {
  size 8x8
  input color tint = #9dc
  draw { paint(tint) }
  paint(tint: color) { pixel(1, 1, tint) }
}
```

### Use the declared parameter name

[unknown-argument.ips](examples/language/errors/unknown-argument.ips)

Wrong:
```ips
asset InvalidArgument {
  size 8x8
  draw { pixel(x = 1, y = 1, colour = #fff) }
}
```
Diagnostic `IPS-TYPE-001`: `has no argument 'colour'`.

Use the parameter name exported by the compiler signature.
Corrected:
```ips
asset InvalidArgument {
  size 8x8
  draw { pixel(x = 1, y = 1, color = #fff) }
}
```
