* expr: an expression cannot rebind a prelude name for the next one (#2133)
One runtime serves every expression on a thread and user code runs as a script
at global scope, so `linear = function () {}` rebound the name for every later
expression, and for every later project in the session. #1387 closed the `__`
globals, the model constructors and the prelude's own prototypes; the rest of
the global surface is here: the prelude's own 104 helpers, the built-ins that an
expression and the prelude both call, and the prototypes those hang off.
Freezing a prototype costs an expression the right to shadow an inherited name
on its own object: once the inherited data property is non-writable, `o.x = …`
is dropped in sloppy mode rather than creating an own property, which is the
override mistake. Every name that was shadowable before is therefore replaced
by an accessor whose setter writes an own property on the receiver, emulating
OrdinarySetWithOwnDescriptor so the substitution does not show through.
The repair covers every own writable, configurable data property of every
prototype the block reaches, not a list of commonly shadowed names: a list
leaves `a.map = …` throwing and `a.push = …` and `a.join = …` silently running
the native method, which is worse, because the expression keeps working and
quietly computes something else.
`name` and `message` on an error's prototype are the one exception, and they
are frozen rather than repaired. The host renders a thrown error by reading
both off the object with no context to call a getter with, and nowhere safe to
call one: the time budget is checked between polls of the evaluation and
formatting runs after that returns, so a getter of the expression's own would
run outside it, bounded only by the loop and recursion limits at some hundreds
of millions of iterations. `e.name = 'MyError'` is dropped in silence as the
price of keeping user code out of the failure path.
The setup code reads every intrinsic it needs before the first prototype is
rewritten, and indexes arrays rather than calling their methods. Repairing
`Array.prototype` turns `concat` and `forEach` into accessors, and an earlier
attempt at this used both while walking the remaining globals, which left
every prototype after `Array.prototype` unfrozen and the prelude failing to
load at all.
Measured on macOS arm64, release, min of 3 x N, against this base: an
inherited-name read costs ~68 ns where it is now an accessor, which is +18%
on a loop doing a hundred of them and +2% to +7% on ordinary expressions.
Extending the repair past `Object.prototype` is what the array-method read
pays for; nothing else moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* expr: the prelude's lock answers delete and defineProperty too (#2133)
Tests for the two routes to a global binding the existing ones did not cover
against a prelude function, asked for in review. `writable: false` answers an
assignment and a `var`; it is `configurable: false` that answers these, and
they fail differently: a `delete` of a non-configurable property evaluates to
false in sloppy mode, while a `defineProperty` over one throws, so that is the
one route whose refusal the user sees.
Also records which mode decides that, because it was asked and the answer is
not the obvious one: an expression cannot reach strict mode at all.
`Runtime::script` wraps the text in `try{…}catch(__e){throw __e}`, so a
`"use strict"` the user writes is an expression statement inside a block
rather than a directive prologue and has no effect. A refused write is
therefore always the silent kind, never a TypeError.
No change to the prelude; these pass as it stands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* expr: freeze Math and JSON plainly instead of repairing them (#2176)
Math and JSON are namespaces, not prototypes: nothing inherits sin or
parse, so the accessor repair keeps no shadowing there and only puts a
getter call on every Math.* read of a per-frame expression. Freeze them
directly and lock the two bindings so Math = {} cannot replace them for
the next expression either. Prototypes keep the repair.
---------
Co-authored-by: Anubhav-Rai <adobearakap@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Text strokes were always drawn with round joins. The document gets a
Line Join (`lineJoin: miter|round|bevel` in layer.setText / newText),
Miter for new text as in After Effects, Round for documents saved before
it so their look is kept. The 2D and Advanced 3D (extruded) text strokes
use it, expressions' style copies carry it, and the Character panel has
Miter / Round / Bevel buttons under the stroke width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LayerInfo, sourceRectAtTime and Layer.source now resolve the effective footage item/size through the collapsed instance EvalCtx, matching the rendered pixels. Adds a regression test with different original and replacement footage sizes.
Carry property, Font and Media Replacement overrides into the collapsed nested EvalCtx so nested layers render beyond the precomp frame instead of falling back to a clipped, flattened image. Key the layer cache on instance overrides to keep distinct collapsed instances isolated.
A missing or unusable curve no longer consumes the last value. The long form reads argument 5 and the short form argument 3, with the other arities falling back to linear interpolation. Adds the missing-curve and four-element-array cases reported in review.
- accept only four-element arrays for bezier(): extra values make the curve
malformed, so it falls back to linear like the other invalid forms (#1884);
- stop Newton on the estimated y error (f * dy/dx) instead of a residual in x
alone: near the endpoints dy/dx is in the thousands, so a 1e-9 residual in x
could still move the returned value;
- tests: a five-element curve is linear, and three steep curves match a
bisection reference within 1e-6.
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.
Every undoable edit keeps the previous project for undo, so the comp it touches
is shared and Arc::make_mut deep-copies it: every layer and every keyframe. An
edit therefore costs time in proportion to the comp's size, and a script that
sets many keyframes in a big comp goes quadratic (a 1,600-layer build with
~100k sampled keys took ~45 minutes).
Comp::layers is now a Layers collection backed by Vec<Arc<Layer>>: copying the
stack copies pointers, and only the layer an edit changes is copied. The API
mirrors Vec<Layer>; serialized form is unchanged (a list of layers).
Comp::layer_mut and the iter_mut().filter/find call sites use find_mut /
iter_mut_where so they copy only the layers they actually change.
- prop.addKey in a 400-layer comp: 1.36 ms -> 0.16 ms per key (flat in comp size)
- the 1,600-layer script build: ~45 min -> 47 s, frames pixel-identical
- regression tests: an edit leaves the other layers shared with undo; layers
round-trip as a plain list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkpGcKfWyCudfSsQNiWAaG
A Layer is now a callable function with its own prototype, which costs
more to make than the plain object it was. __layer(c, l) makes each
layer once per evaluation (kept in __S, which __begin resets), so an
expression naming the same layer many times builds it once. That covers
thisComp.layer(...) in a loop, .parent, a Layer Control's value and
thisLayer.
The checks for a layer (the result, thisComp.layer(layer, offset), the
property proxy) read __isLayer only on functions. Numbers and other
primitives are no longer boxed to look the property up.
Timing (release, Windows, medians of 2000 evaluations, PR vs main):
- one evaluation building one layer: about 22 vs 16 us;
- thisLayer.transform.opacity: about 200 vs 170 us;
- 20000 thisComp.layer(1) lookups in one evaluation: 95 vs 138 ms.
In After Effects a layer is a property group: thisLayer("transform"),
thisComp.layer("Other")("effects")("Blur"), thisLayer(1). In
EffectCraft's expression runtime a Layer was a plain object, so those
failed with "TypeError: not a callable function", including the
references the pick whip writes with Compact English off
(thisComp.layer("Other")("effects")("ec.noise.fractal")(...)).
A Layer is now callable like a property group (built as __mkGroup
builds groups), looking up its own properties by match name, name or
index, and keeps its methods, name and index. The places that took a
layer for a plain object (the result reading as the layer's index,
thisComp.layer(layer, offset), a Layer Control's value handed through
the property proxy) accept it.
Test: layers_are_callable_property_groups (fails before).
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.
- scriptui.js: replace the self-comparison NaN check (o.max === o.max) with
the explicit Number.isNaN(o.max) (S1764).
- sw.js: mark the fire-and-forget cache.put() with void and a .catch() so the
promise rejection is handled (S9383).
- prelude.js: return __err(...) from the activeCamera getter so every path
yields a value (S4275); __err always throws, so behavior is unchanged.