Create comprehensive implementation-ready story files for the remaining Epic 3 (JavaScript Engine Maturity) stories and update sprint status from backlog to ready-for-dev: - 3.5: Built-in Completeness (Array/String/Object) - 3.6: Built-in Completeness (Date/RegExp/Map/Set) - 3.7: WeakRef, FinalizationRegistry & Strict Mode Edge Cases - 3.8: DOM Bindings via web_api - 3.9: Event Dispatch Completeness - 3.10: Web API Exposure (fetch, Math, setInterval, rAF) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
25 KiB
Story 3.7: WeakRef, FinalizationRegistry & Strict Mode Edge Cases
Status: ready-for-dev
Story
As a web developer using JavaScript, I want advanced memory management APIs and complete strict mode support, So that all ECMAScript edge cases are handled correctly.
Acceptance Criteria
-
WeakRef construction and deref:
new WeakRef(target)creates a weak reference to an object.weakRef.deref()returns the target object if still alive, orundefinedif collected. TypeError iftargetis not an object. Per ECMAScript §26.1. -
FinalizationRegistry with cleanup callbacks:
new FinalizationRegistry(callback)creates a registry..register(target, heldValue, unregisterToken?)registers a target..unregister(unregisterToken)removes registrations. Cleanup callback is called withheldValuewhen target is collected. Per ECMAScript §26.2. -
GC-awareness caveat: Since the engine uses
Rc<RefCell<>>(reference counting, no cycle collection),WeakRef.deref()always returns the target as long as any strong reference exists.FinalizationRegistrycleanup callbacks are called during explicitcleanupSome()or at engine-defined points (e.g., between microtask checkpoints). This is spec-compliant -- the spec says collection timing is implementation-defined. -
Strict mode:
evalandargumentsas identifiers:var eval = 1andvar arguments = 2throw SyntaxError in strict mode. Assignment toevalorarguments(eval = 1) throws SyntaxError. Using as function names, parameter names, or catch binding names also throws. Per ECMAScript §13.1.1. -
Strict mode:
withstatement rejection:with (obj) { ... }throws SyntaxError in strict mode. In sloppy mode,withexecutes the body withobjpushed onto the scope chain. Per ECMAScript §14.11. -
Strict mode: octal literal rejection: Legacy octal literals (
010,077) throw SyntaxError in strict mode.0o10(ES2015 octal) is allowed in both modes. Octal escape sequences in strings ("\01","\77") throw SyntaxError in strict mode."\0"alone (null character, not followed by digit) is allowed. Per ECMAScript §B.1.1. -
Strict mode: assignment to undeclared variables:
"use strict"; x = 42;throws ReferenceError at runtime (not creating a global). Verify this works correctly in the bytecode VM path. -
Strict mode: read-only property assignment: In strict mode, assignment to a non-writable property throws TypeError. Assignment to a getter-only accessor property throws TypeError. Assignment to a new property on a non-extensible object throws TypeError. (Depends on property descriptor system from Story 3.5.)
-
Test262 tests promoted: All relevant Test262 tests for WeakRef, FinalizationRegistry, and strict mode edge cases are promoted.
docs/JavaScript_Implementation_Checklist.mdanddocs/js_feature_matrix.mdupdated.just cipasses.
What NOT to Implement
- No cycle-detecting garbage collector --
Rc<RefCell<>>reference counting is the existing memory model. WeakRef/FinalizationRegistry are implemented using Ruststd::rc::Weak<>for genuine weak references, but cleanup timing is implementation-defined per spec. True cycle collection deferred. - No
withstatement in sloppy mode -- Parsewithonly to reject it in strict mode with a clear SyntaxError. Full sloppy-modewithsemantics (scope chain manipulation) are out of scope due to complexity and the feature being deprecated. Document as known limitation. - No
argumentsobject mutations -- Strict modeargumentsis a snapshot (not linked to parameters). Full sloppy-modeargumentsaliasing (wherearguments[0]and the first parameter are linked) is not required. - No
caller/calleerestrictions --arguments.callerandarguments.calleethrowing TypeError in strict mode are deferred. - No
Object.definePropertystrict enforcement -- AC #8 depends on Story 3.5's property descriptor system. If Story 3.5 is not yet landed, skip AC #8 and note it as a dependency.
Files to Modify
| File | Change |
|---|---|
crates/js_vm/src/value.rs |
Add Weak<RefCell<JsObjectData>> support for WeakRef. Add weak_ref_target: Option<Weak<RefCell<JsObjectData>>> field to JsObjectData. Add finalization_registry_data field. |
crates/js_vm/src/interpreter/weakref_builtins.rs |
New file -- WeakRef constructor, .deref() method, setup function. |
crates/js_vm/src/interpreter/finalization_registry_builtins.rs |
New file -- FinalizationRegistry constructor, .register(), .unregister(), cleanup scheduling. |
crates/js_vm/src/interpreter/mod.rs |
Add setup_weakref_builtins(), setup_finalization_registry_builtins() calls. |
crates/js_vm/src/interpreter/expressions/calls.rs |
Add WeakRef/FinalizationRegistry method dispatch. |
crates/js_parser/src/parser/mod.rs |
Add eval/arguments identifier checks in strict mode via check_strict_identifier(). |
crates/js_parser/src/parser/statements.rs |
Add with statement parsing: reject in strict mode with SyntaxError, reject in sloppy mode with "not supported" error. Add parameter name validation for eval/arguments in strict functions. |
crates/js_parser/src/parser/expressions.rs |
Add eval/arguments assignment validation in strict mode. Validate octal escape sequences in string literals when in strict mode. |
crates/js_parser/src/lexer.rs |
Add legacy octal literal (0[0-7]+) detection and rejection in strict mode. Ensure 0o octal works in both modes. |
crates/js_vm/src/interpreter/bytecode_exec.rs |
Verify assignment to undeclared variable throws ReferenceError in strict mode (bytecode path). |
crates/js_parser/src/parser/tests/strict_mode_tests.rs |
Add tests for eval/arguments restrictions, with rejection, octal rejection. |
crates/js_vm/src/interpreter/tests/strict_mode_tests.rs |
Add runtime strict mode tests. |
crates/js_vm/src/interpreter/tests/weakref_tests.rs |
New file -- WeakRef unit tests. |
crates/js_vm/src/interpreter/tests/finalization_registry_tests.rs |
New file -- FinalizationRegistry unit tests. |
tests/external/js262/js262_manifest.toml |
Promote passing tests. |
docs/JavaScript_Implementation_Checklist.md |
Check off WeakRef/FinalizationRegistry items. Update strict mode items. |
docs/old/js_feature_matrix.md |
Update coverage. |
Tasks / Subtasks
-
Task 1: Strict mode --
evalandargumentsrestrictions (AC: #4)- 1.1 Add
evalandargumentsto strict identifier checks incrates/js_parser/src/parser/mod.rs:- Extend
check_strict_identifier()(or create new check) to rejectevalandargumentsas:- Variable declarations:
var eval = 1,let arguments = 2,const eval = 3 - Function names:
function eval() {},function arguments() {} - Parameter names:
function f(eval) {},function f(arguments) {} - Catch binding:
catch (eval) {} - Assignment targets:
eval = 1,arguments = 2(in assignment expression)
- Variable declarations:
- Error:
"'{}' cannot be used as identifier in strict mode"(ECMAScript §13.1.1)
- Extend
- 1.2 Add validation in
parse_var_declaration()instatements.rs:- After extracting binding name, check if strict and name is
evalorarguments
- After extracting binding name, check if strict and name is
- 1.3 Add validation in function parameter parsing in
statements.rs:- In
parse_params()or equivalent, check each parameter name
- In
- 1.4 Add validation in assignment expressions in
expressions.rs:- In
parse_assignment_expression(), if LHS is Identifier("eval") or Identifier("arguments") and strict → SyntaxError
- In
- 1.5 Add parser tests in
strict_mode_tests.rs:var eval = 1in strict → SyntaxErrorvar arguments = 1in strict → SyntaxErroreval = 1in strict → SyntaxErrorarguments = 1in strict → SyntaxErrorfunction eval() {}in strict → SyntaxErrorfunction f(eval) {}in strict → SyntaxErrorfunction f(arguments) {}in strict → SyntaxErrorcatch (eval) {}in strict → SyntaxError- All above work fine in sloppy mode (no error)
- 1.1 Add
-
Task 2: Strict mode --
withstatement (AC: #5)- 2.1 Add
withkeyword recognition incrates/js_parser/src/token.rs:- Add
WithtoTokenKindandkeyword_from_str()(if not already present)
- Add
- 2.2 Add
withstatement parsing inparse_statement()instatements.rs:- When
TokenKind::Withencountered:- If strict mode →
CompileError: "'with' statement not allowed in strict mode"(ECMAScript §14.11.1) - If sloppy mode →
CompileError: "'with' statement is not supported"(known limitation, not spec-compliant but acceptable)
- If strict mode →
- Parse just enough to give a good error: consume
with,(, expression,), statement
- When
- 2.3 Add parser tests:
"use strict"; with (obj) { x; }→ SyntaxErrorwith (obj) { x; }in sloppy → error (unsupported, not SyntaxError -- different message)
- 2.1 Add
-
Task 3: Strict mode -- octal literal and escape rejection (AC: #6)
- 3.1 Add legacy octal literal detection in
crates/js_parser/src/lexer.rs:- When number starts with
0followed by[0-7](e.g.,010,077):- If strict mode →
CompileError: "Octal literals are not allowed in strict mode"(ECMAScript §B.1.1) - If sloppy mode → parse as octal number (or decimal -- implementation choice)
- If strict mode →
0o10(ES2015 explicit octal) allowed in both modes0x10(hex) allowed in both modes0b10(binary) allowed in both modes- IMPORTANT: The lexer needs access to strict mode flag. Either pass it as parameter or check after lexing.
- When number starts with
- 3.2 Add octal escape sequence rejection in string literals:
- In string literal parsing, when
\followed by[1-7]or\0followed by[0-9]:- If strict mode →
CompileError: "Octal escape sequences are not allowed in strict mode"(ECMAScript §B.1.2)
- If strict mode →
\0alone (null character escape, NOT followed by digit) is always allowed\x41(hex escape) and\u0041(unicode escape) always allowed
- In string literal parsing, when
- 3.3 Handle strict mode flag in lexer:
- Option A: Lexer has
strict: boolfield, set by parser when"use strict"is detected, then re-lex - Option B: Lexer always lexes octals, parser validates after receiving token
- Recommendation: Option B is simpler -- lexer produces
OctalLiteral(value)orOctalEscapeSequencetokens, parser rejects in strict mode
- Option A: Lexer has
- 3.4 Add tests:
"use strict"; var x = 010;→ SyntaxError"use strict"; var s = "\1";→ SyntaxErrorvar x = 010;in sloppy → OK (value 8)var x = 0o10;→ OK in both modes (value 8)"use strict"; var s = "\0";→ OK (null character, no digit following)"use strict"; var s = "\01";→ SyntaxError
- 3.1 Add legacy octal literal detection in
-
Task 4: Strict mode -- undeclared variable assignment (AC: #7)
- 4.1 Verify bytecode VM path in
crates/js_vm/src/interpreter/bytecode_exec.rs:- When
SetGlobalorSetVartargets an undeclared variable in strict mode:- Must throw ReferenceError, NOT silently create a global
- Check
Environment::set()behavior -- it may already throw for undeclared - The AST interpreter path already works (env.set() throws ReferenceError)
- When
- 4.2 Add explicit test in
crates/js_vm/src/interpreter/tests/strict_mode_tests.rs:"use strict"; undeclaredVar = 42;→ ReferenceErrorundeclaredVar = 42;in sloppy → creates global (or throws if not supported)
- 4.3 Verify strict flag propagation in bytecode compiler:
FunctionBlueprint.strict: boolmust be set correctly fromProgram.strictandFunction.strict- Check
crates/js_vm/src/bytecode/compiler/for strict flag handling
- 4.1 Verify bytecode VM path in
-
Task 5: WeakRef implementation (AC: #1, #3)
- 5.1 Add WeakRef infrastructure to
crates/js_vm/src/value.rs:- Add
weak_ref_target: Option<std::rc::Weak<RefCell<JsObjectData>>>field toJsObjectData - Add
init_weakref(target: &JsObject),is_weakref(),deref_weakref() -> Option<JsObject>helpers JsObjectisRc<RefCell<JsObjectData>>-- useRc::downgrade()to createWeakderef_weakref()callsWeak::upgrade()-- returnsSomeif target still alive,Noneif all strong refs dropped
- Add
- 5.2 Create
crates/js_vm/src/interpreter/weakref_builtins.rs:setup_weakref_builtins(env: &mut Environment)-- register WeakRef constructor and prototype- Constructor:
new WeakRef(target)-- TypeError if target is not an object. StoreRc::downgrade(&target.inner). - TypeError if called without
new
- 5.3 Implement
WeakRef.prototype.deref()(ECMAScript §26.1.3.2):- Call
Weak::upgrade()on stored weak reference - If
Some(inner)→ returnJsValue::Object(JsObject { inner }) - If
None→ returnJsValue::Undefined - Note: With
Rc<>, this will almost always return the target unless all other strong references have been dropped (e.g., variable went out of scope). This is spec-compliant -- timing is implementation-defined.
- Call
- 5.4 Wire WeakRef dispatch in
calls.rsand addsetup_weakref_builtins()inmod.rs - 5.5 Add unit tests in
crates/js_vm/src/interpreter/tests/weakref_tests.rs:new WeakRef({})→ object createdweakRef.deref()returns target when still referenced- TypeError on primitive target
- TypeError when called without
new .deref()returnsundefinedafter target is unreachable (test by dropping all strong refs in Rust test harness -- may not be testable from JS)
- 5.1 Add WeakRef infrastructure to
-
Task 6: FinalizationRegistry implementation (AC: #2, #3)
- 6.1 Add FinalizationRegistry infrastructure to
crates/js_vm/src/value.rs:FinalizationRegistryDatastruct:cleanup_callback: JsValue, // The callback function registrations: Vec<FinalizationEntry>,FinalizationEntrystruct:target: Weak<RefCell<JsObjectData>>, // Weak reference to watched object held_value: JsValue, // Value passed to callback unregister_token: Option<usize>, // Pointer identity of unregister token- Add
finalization_registry_data: Option<FinalizationRegistryData>toJsObjectData - Add helpers:
init_finalization_registry(),is_finalization_registry()
- 6.2 Create
crates/js_vm/src/interpreter/finalization_registry_builtins.rs:setup_finalization_registry_builtins(env: &mut Environment)- Constructor:
new FinalizationRegistry(callback)-- TypeError if callback is not callable - TypeError if called without
new
- 6.3 Implement FinalizationRegistry prototype methods:
.register(target, heldValue, unregisterToken?)(ECMAScript §26.2.3.2):- TypeError if
targetis not an object - TypeError if
target === unregisterToken(same object) - Store weak reference to target, heldValue, and unregisterToken identity
- TypeError if
.unregister(unregisterToken)(ECMAScript §26.2.3.3):- Remove all registrations with matching unregisterToken
- Return boolean (true if any removed)
- 6.4 Implement cleanup scheduling:
- Add
cleanup_finalization_registries()method to interpreter - For each registry: iterate entries, check if
target.upgrade()returns None - If target is dead: call cleanup callback with
heldValue, remove entry - Scheduling: Call
cleanup_finalization_registries()at microtask checkpoint (after each script execution, same point where microtasks are drained) - Note: With Rc<>, targets are only "dead" when all strong refs are dropped. In practice, most JS objects will have strong refs until the VM shuts down. This is acceptable per spec (cleanup timing is implementation-defined).
- Add
- 6.5 Wire dispatch in
calls.rsand setup inmod.rs - 6.6 Add unit tests in
crates/js_vm/src/interpreter/tests/finalization_registry_tests.rs:- Constructor with callback
.register()and.unregister()basic operations- TypeError on primitive target
- TypeError on non-callable callback
- Cleanup callback invocation (test by manually dropping strong refs in Rust test harness)
- 6.1 Add FinalizationRegistry infrastructure to
-
Task 7: Testing and validation (AC: #9)
- 7.1 Run vendored Test262 suite and promote passing tests:
cargo test -p rust_browser --test js262_harness js262_suite_matches_manifest_expectations -- --nocapturejust js262-status promote --id <test-id>
- 7.2 Run full Test262 suite and triage:
just test262-fulljust triage-test262-full
- 7.3 Run all existing JS test suites to verify no regressions:
cargo test -p js_vmcargo test -p rust_browser --test js_testscargo test -p rust_browser --test js_dom_testscargo test -p rust_browser --test js_eventscargo test -p rust_browser --test js_schedulingcargo test -p rust_browser --test js_asynccargo test -p rust_browser --test js_modules
- 7.4 Update
docs/JavaScript_Implementation_Checklist.md:- Check off WeakRef/FinalizationRegistry
- Update strict mode section with new checks
- 7.5 Update
docs/old/js_feature_matrix.md - 7.6 Run
just ci-- full validation pass
- 7.1 Run vendored Test262 suite and promote passing tests:
Dev Notes
Key Architecture Decisions
WeakRef uses std::rc::Weak<RefCell<JsObjectData>>. This is the natural Rust equivalent of a weak reference in an Rc<>-based system. Rc::downgrade() creates a Weak, and Weak::upgrade() returns Some if any strong refs remain. This gives us real weak reference semantics within Rust's reference counting model -- objects that are truly unreachable (no more Rc clones anywhere) will return None from deref().
FinalizationRegistry cleanup is best-effort. The spec explicitly says cleanup timing is implementation-defined (ECMAScript §9.10.3). Our implementation checks for dead targets during microtask checkpoints. In practice, with Rc<>, objects are rarely "dead" while JS code still runs (some Rc clone usually exists). This is acceptable and spec-compliant.
with statement is parsed only to reject. Implementing full with semantics (runtime scope chain manipulation) is complex and the feature is deprecated. Parse enough to give a clear error in both strict (SyntaxError) and sloppy (unsupported) modes.
Strict mode octal handling needs lexer-parser coordination. The lexer doesn't currently know about strict mode. Two approaches: (1) lexer always produces tokens, parser validates based on strict flag, or (2) parser feeds strict flag back to lexer. Approach (1) is simpler -- have the lexer tag octal literals and escape sequences, then the parser rejects them in strict context.
Current Strict Mode State
Already implemented (Phase 9 complete):
"use strict"directive prologue detection with escape sequence guard- Function-scoped and inherited strict mode
- Reserved word enforcement (7 words + yield)
- Duplicate parameter rejection
- Non-simple parameter +
"use strict"rejection delete identifierprohibition (parse-time)thisisundefinedin non-constructor strict calls.call()does not coerce non-objectthis
Key files:
- Parser strict checks:
crates/js_parser/src/parser/mod.rs:148-163(reserved words),mod.rs:360-382(directive) - Runtime strict:
crates/js_vm/src/interpreter/expressions/calls.rs:821-839(this binding),calls.rs:666-682(.call coercion) - Parser tests:
crates/js_parser/src/parser/tests/strict_mode_tests.rs(381 lines) - VM tests:
crates/js_vm/src/interpreter/tests/strict_mode_tests.rs(338 lines)
Implementation Patterns
WeakMap/WeakSet dispatch pattern (in calls.rs):
if obj.is_weakmap() {
match method_name.as_str() {
"get" => { ... }
"set" => { ... }
_ => {}
}
}
WeakRef and FinalizationRegistry follow this same dispatch pattern.
Strict identifier check (in parser/mod.rs):
fn check_strict_identifier(&self, name: &str) -> Result<(), CompileError> {
if self.strict && is_strict_reserved_word(name) {
Err(CompileError::new(...))
} else { Ok(()) }
}
Extend this to also check for "eval" and "arguments".
Critical Implementation Details
Weak<RefCell<JsObjectData>> requires JsObject to expose its Rc. Currently JsObject wraps Rc<RefCell<JsObjectData>> as inner. WeakRef needs Rc::downgrade(&obj.inner). This should work directly since inner is accessible within the js_vm crate.
Octal literal edge cases:
0alone is decimal zero, NOT octal -- don't reject08,09are decimal (invalid octal digits) -- don't reject as octal010is legacy octal = 8 -- reject in strict0o10is ES2015 octal = 8 -- allowed in strict"\0"is null char escape -- always allowed"\01"is octal escape -- reject in strict"\8"and"\9"are non-octal -- per spec, these are identity escapes (allowed even in strict per ES2021 Annex B revision)
FinalizationRegistry target === unregisterToken check. The spec says register(target, heldValue, unregisterToken) must throw if target is the same as unregisterToken. Use Rc::ptr_eq() for object identity comparison.
Dependencies
Story 3.5 (property descriptors): AC #8 (strict mode property assignment errors) depends on the property descriptor system from Story 3.5. If Story 3.5 is not yet complete, skip AC #8 and document it as a follow-up.
No dependency on Story 3.6. WeakRef/FinalizationRegistry are independent of Date/Map/Set.
Previous Story Intelligence
From Story 3.6 (Date/RegExp/Map/Set):
- New builtin types follow consistent pattern:
value.rsdata fields,*_builtins.rsfile,setup_*_builtins()call, dispatch incalls.rs indexmapavailable as dependency- No
unsafecode needed
From Story 3.4 (ES modules) review lessons:
- Don't leave stub code -- wire everything into execution paths
- Tests must test actual code paths
- Run
just ciafter each task
Risk Assessment
LOW: WeakRef. Straightforward wrapper around Rc::downgrade()/Weak::upgrade(). Small API surface.
LOW: Strict mode eval/arguments checks. Simple parse-time validation. Well-defined spec behavior. Existing check_strict_identifier() pattern to follow.
MEDIUM: FinalizationRegistry cleanup scheduling. Need to integrate cleanup checks into the microtask/event loop. Finding the right hook point requires understanding the execution pipeline in app_browser/browser_runtime.
MEDIUM: Octal literal/escape handling. Lexer-parser coordination for strict mode. Multiple edge cases (\0 vs \01, 08 vs 010). Need careful testing.
LOW: with statement. Parse-only, reject in all modes. No runtime semantics to implement.
Phased Implementation Strategy
Phase A -- Strict Mode Fixes (Tasks 1-4): Quick wins. Parse-time checks with existing infrastructure. Can be completed independently.
Phase B -- WeakRef (Task 5): Small, focused implementation using Rc::downgrade().
Phase C -- FinalizationRegistry (Task 6): More complex due to cleanup scheduling. Depends on understanding microtask checkpoint locations.
Phase D -- Testing + Validation (Task 7): After all implementations.
Project Structure Notes
- Parser changes in
crates/js_parser/src/(Layer 1) -- no layer violations - VM changes in
crates/js_vm/src/interpreter/(Layer 1) -- new files for WeakRef, FinalizationRegistry - Value type changes in
crates/js_vm/src/value.rs(Layer 1) - No
unsafecode needed - No new external dependencies
References
- ECMAScript §26.1 -- WeakRef Objects -- WeakRef constructor and prototype
- ECMAScript §26.2 -- FinalizationRegistry Objects -- FinalizationRegistry
- ECMAScript §9.10 -- CleanupFinalizationRegistry -- Cleanup abstract operation
- ECMAScript §13.1.1 -- Static Semantics: Early Errors -- eval/arguments restrictions
- ECMAScript §14.11 -- The with Statement -- with statement and strict mode
- ECMAScript §B.1.1 -- Numeric Literals -- Legacy octal
- ECMAScript §B.1.2 -- String Literals -- Octal escape sequences
- [Source: crates/js_parser/src/parser/mod.rs:148-163] -- Existing strict reserved word check
- [Source: crates/js_parser/src/parser/mod.rs:360-382] -- "use strict" directive detection
- [Source: crates/js_vm/src/interpreter/expressions/calls.rs:821-839] -- Strict this binding
- [Source: crates/js_vm/src/interpreter/weakmap_builtins.rs] -- WeakMap pattern to follow
- [Source: crates/js_vm/src/value.rs] -- JsObject is Rc<RefCell>
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.7] -- Story requirements
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}