Skip to content

Let scenario values be arithmetic expressions #327

Closed

Description

@LeadcodeDev

Part of #326. Batch 1 — #328, #329 and #332 depend on it.

A scenario cannot compute. Reproducing the reel meant writing 316 lines of Python to emit the JSON, and every position in the result is a frozen literal: unreadable in the studio, uneditable by hand, and divorced from the intent that produced it. Placing eight badges on a circle, shading twenty-four facets by their angle to a light, or floating an element on a sine are all one line in the reference reels and impossible here.

Owned files

  • crates/rustmotion-core/src/expr/ (new: lexer.rs, parser.rs, ast.rs, eval.rs, builtins.rs, mod.rs)
  • crates/rustmotion/src/loader.rs (the static-folding pass)

Do NOT write: anything under engine/ (#328 owns the dependency graph), schema/scenario.rs (#336).

Deliverables

  1. A value may be an expression — a string prefixed with =:

    { "x": "= $W/2 + cos($i / $count * TAU - PI/2) * 700 - 110" }

    Allowed wherever a number or a colour is: style properties, root numeric fields, keyframe values, and at/delay/duration where the result is read as a TimePoint.

  2. A deliberately non-Turing-complete grammar. Literals, + - * / %, unary minus, parentheses, comparisons, a ternary, function calls. No loops, no user-defined functions, no recursion. Evaluation stays bounded, which is what makes it validatable and what keeps it out of the class of things that need a sandbox.

  3. Builtins — sin cos tan atan2 sqrt pow abs min max clamp floor ceil round sign exp log lerp smoothstep, plus rand(seed) and noise(x, seed). Both are pure functions of their arguments with no global state, so two renders of a file are identical. Constants PI, TAU, E.

  4. Scope — $t scene time, $T absolute time, $beat, $i/$index/$item/$count inside for-each, $W/$H/$fps/$duration. Component params keep the existing $name sigil and the existing resolution order.

  5. Two evaluation tiers, and this is the decision that matters. An expression whose free variables are all known at load time is evaluated once during resolution and replaced by a literal, at zero per-frame cost. This tier alone removes the generator script. An expression reading $t, $T, $beat, a variable or a node reference compiles once to a stack machine over f64 and evaluates per frame, allocation-free in the hot loop.

  6. expression_error as a hard validation error covering parse failures, unknown identifiers and arity mismatches, named and located the way the existing for-each errors are.

  7. --fix refuses a file using expressions, as it already refuses for-each, use and include: path indices no longer match the source.

Acceptance

  • for-each over eight items with "x": "= cos($i / $count * TAU) * 700" places them on a circle, folded to literals at load
  • A hostile expression (deep nesting, huge exponent) is rejected at load rather than hanging a render
  • <= 15% render-time regression on examples/mega-showcase.json with per-frame expressions in play

Activity

  1. self-assigned this
    on Sep 25, 2026
  2. changed the title [-]WS-1 timeline: absolute scene placement and a beat grid[/-] [+]WS-2 expressions: an arithmetic layer for scenario values[/+] on Sep 25, 2026
  3. changed the title [-]WS-2 expressions: an arithmetic layer for scenario values[/-] [+]Let scenario values be arithmetic expressions[/+] on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions