* Expressions, Scripting: a layer's property numbers follow After Effects
In After Effects a layer's top-level number reaches the same group on every layer of a kind:
Marker is 1, and slots the layer has not used yet (Time Remap, Motion Trackers, Layer Styles,
Data, Sets...) keep their place. EffectCraft numbered only the groups a layer holds, without
Marker, so `layer(n)` and `layer.property(n)` reached a different group, and the numbers moved
as groups were added. Rigs that read a palette by number, such as
`comp("Palette").layer(1)(4)(n)(1)` on a shape layer, read Transform instead of Effects; their
`try` fell back to the stored value and every linked colour rendered near-black, with no
expression error.
`effectcraft_project::ae_order` holds After Effects' slot order per layer kind, observed in
After Effects 26.3 from a script (`layer.property(i).matchName`) and from an expression
(`thisComp.layer(..)(i).name`) on new layers of each kind. The expression host and the
scripting model resolve a layer's numbers through it: 1 is the markers, a used slot is its
group, an unused one is an empty group (never null, so loops over `numProperties` are safe).
`numProperties` at layer level and `propertyIndex` of top-level groups are After Effects'
numbers too. 3D model and primitive layers keep EffectCraft's own numbers.
* Expressions: a layer's top-level groups by After Effects match name
`thisLayer("ADBE Effect Parade")`, as scripts such as Karton write their expressions, reaches the
group through the same table as the layer's numbers: Effects, or an empty group for a slot the
layer has not used, and `("ADBE Marker")` the markers.
* Expressions, Scripting: review follow-ups for After Effects layer numbers
- An enabled Time Remap (a top-level property) reports After Effects' number 2 as its propertyIndex.
- In scripting, an unused slot resolves to the same empty group by match name
(`property("ADBE Layer Styles")`) as by number (`property(7)`).
- The empty group is callable like any group: `layer.property(7)(1)` is null, not a TypeError.
Add bezier(t, tMin, tMax, v1, v2, [x1, y1, x2, y2]) and the short bezier(t, v1, v2, curve) form: a CSS cubic-bezier easing curve solved with Newton-Raphson and a bisection fallback.
Scalars and arrays work like linear/ease; x1/x2 clamp to 0..1, y1/y2 may overshoot, t clamps to tMin..tMax, and a curve that is not four finite numbers falls back to linear instead of throwing.
Tests cover known curves (ease is [1/3, 0, 2/3, 1]), clamps, arrays, the short form, invalid curves and overshoot, plus two fuzz seeds.
Closes#1884.
An array replacer now copies each source object once per call, so a cycle stays a cycle and the native check throws TypeError, while shared objects still serialize. Documents which threads evaluate expressions, what the 256 KiB guard covers (tested on a 512 KiB thread), that it does not apply on wasm32, and why eval is indirect.
eval, Function and the generator/async function constructors now check the nesting of their source like the expression text. join, toLocaleString, flat and JSON.stringify check the run's stack use per level, results nested past the detached levels become [object], and Proxy/Reflect are no longer available to expressions. Each of these overflowed the stack and aborted the app.
The After Effects stagger idiom
id = thisProperty.propertyGroup(2).propertyIndex;
failed with "not a callable function": the expression object model had
no propertyGroup() and property groups had no propertyIndex.
- Property.propertyGroup(countUp = 1) and PropertyGroup.propertyGroup()
return the group countUp levels up; from a top-level group they reach
the layer. A count below 1, not a number, or above the layer is an
error naming propertyGroup, never a crash.
- PropertyGroup.propertyIndex: the group's 1-based index among its
siblings (Child requests now carry it).
- One new host request (Ancestor) walks up from the node's full uid
path, so it also works for the property being evaluated.
- Tests for the stagger expression, effect parameters, reaching the
layer, other layers' properties and groups and out-of-range counts;
the fuzzer seeds and tokens include the new members.
fanning_out_reads_stop_at_a_budget expects the run-count error, but under heavy load
the 5 s wall-clock budget could fire first (different error). Add a test-only
per-thread time limit and lift it in that test. Wall-clock assertions in the other
new tests are now only generous hang guards (>=10x), the fuzzer's hang deadline is
24x MAX_TIME and the perf bound is 10x looser.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: expression text is input we don't control, and nothing fuzzed the
expression parser and evaluator. A seeded mutation fuzzer found stack
overflows (a few hundred `?:`/`=>`/`if`, a few thousand `.`/`<`/`()` links,
75 nested template substitutions or regex groups, a chain inside one
template), hangs (an expression reading its own layer's transform nested
2^16 times; fanning-out reads; 10M-iteration loops re-run per read; loops
calling looping functions), an i64 overflow panic in wiggle noise, and NaN
path vertices tripping the rasterizer's clipping assertion.
Fix: a dependency-free seeded fuzzer (crates/expr/src/tests/fuzz.rs: real
AE-style seeds, char/token mutations, worker thread with a 2 MiB stack,
catch_unwind, deadline, env vars for long runs); a weighted nesting
estimate covering chains without brackets and template/regex insides;
cycle detection per (property, time, text); a per-evaluation budget of
20 000 script runs and 5 s (runs are interrupted through boa's budgeted
evaluation; native only); loop limit 1M; wrapping noise lattice indices;
non-finite path results rejected. Each fix has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>