Files
rust_browser/_bmad-output/implementation-artifacts/3-7-weakref-finalizationregistry-and-strict-mode-edge-cases.md
Zachary D. Rowitsch fb64ca1d34
All checks were successful
ci / fast (linux) (push) Successful in 7m9s
Create story files for Epic 3 stories 3.5-3.10
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>
2026-03-16 08:06:03 -04:00

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

  1. WeakRef construction and deref: new WeakRef(target) creates a weak reference to an object. weakRef.deref() returns the target object if still alive, or undefined if collected. TypeError if target is not an object. Per ECMAScript §26.1.

  2. 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 with heldValue when target is collected. Per ECMAScript §26.2.

  3. 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. FinalizationRegistry cleanup callbacks are called during explicit cleanupSome() or at engine-defined points (e.g., between microtask checkpoints). This is spec-compliant -- the spec says collection timing is implementation-defined.

  4. Strict mode: eval and arguments as identifiers: var eval = 1 and var arguments = 2 throw SyntaxError in strict mode. Assignment to eval or arguments (eval = 1) throws SyntaxError. Using as function names, parameter names, or catch binding names also throws. Per ECMAScript §13.1.1.

  5. Strict mode: with statement rejection: with (obj) { ... } throws SyntaxError in strict mode. In sloppy mode, with executes the body with obj pushed onto the scope chain. Per ECMAScript §14.11.

  6. 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.

  7. 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.

  8. 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.)

  9. Test262 tests promoted: All relevant Test262 tests for WeakRef, FinalizationRegistry, and strict mode edge cases are promoted. docs/JavaScript_Implementation_Checklist.md and docs/js_feature_matrix.md updated. just ci passes.

What NOT to Implement

  • No cycle-detecting garbage collector -- Rc<RefCell<>> reference counting is the existing memory model. WeakRef/FinalizationRegistry are implemented using Rust std::rc::Weak<> for genuine weak references, but cleanup timing is implementation-defined per spec. True cycle collection deferred.
  • No with statement in sloppy mode -- Parse with only to reject it in strict mode with a clear SyntaxError. Full sloppy-mode with semantics (scope chain manipulation) are out of scope due to complexity and the feature being deprecated. Document as known limitation.
  • No arguments object mutations -- Strict mode arguments is a snapshot (not linked to parameters). Full sloppy-mode arguments aliasing (where arguments[0] and the first parameter are linked) is not required.
  • No caller/callee restrictions -- arguments.caller and arguments.callee throwing TypeError in strict mode are deferred.
  • No Object.defineProperty strict 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 -- eval and arguments restrictions (AC: #4)

    • 1.1 Add eval and arguments to strict identifier checks in crates/js_parser/src/parser/mod.rs:
      • Extend check_strict_identifier() (or create new check) to reject eval and arguments as:
        • 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)
      • Error: "'{}' cannot be used as identifier in strict mode" (ECMAScript §13.1.1)
    • 1.2 Add validation in parse_var_declaration() in statements.rs:
      • After extracting binding name, check if strict and name is eval or arguments
    • 1.3 Add validation in function parameter parsing in statements.rs:
      • In parse_params() or equivalent, check each parameter name
    • 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
    • 1.5 Add parser tests in strict_mode_tests.rs:
      • var eval = 1 in strict → SyntaxError
      • var arguments = 1 in strict → SyntaxError
      • eval = 1 in strict → SyntaxError
      • arguments = 1 in strict → SyntaxError
      • function eval() {} in strict → SyntaxError
      • function f(eval) {} in strict → SyntaxError
      • function f(arguments) {} in strict → SyntaxError
      • catch (eval) {} in strict → SyntaxError
      • All above work fine in sloppy mode (no error)
  • Task 2: Strict mode -- with statement (AC: #5)

    • 2.1 Add with keyword recognition in crates/js_parser/src/token.rs:
      • Add With to TokenKind and keyword_from_str() (if not already present)
    • 2.2 Add with statement parsing in parse_statement() in statements.rs:
      • When TokenKind::With encountered:
        • 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)
      • Parse just enough to give a good error: consume with, (, expression, ), statement
    • 2.3 Add parser tests:
      • "use strict"; with (obj) { x; } → SyntaxError
      • with (obj) { x; } in sloppy → error (unsupported, not SyntaxError -- different message)
  • 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 0 followed 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)
      • 0o10 (ES2015 explicit octal) allowed in both modes
      • 0x10 (hex) allowed in both modes
      • 0b10 (binary) allowed in both modes
      • IMPORTANT: The lexer needs access to strict mode flag. Either pass it as parameter or check after lexing.
    • 3.2 Add octal escape sequence rejection in string literals:
      • In string literal parsing, when \ followed by [1-7] or \0 followed by [0-9]:
        • If strict mode → CompileError: "Octal escape sequences are not allowed in strict mode" (ECMAScript §B.1.2)
      • \0 alone (null character escape, NOT followed by digit) is always allowed
      • \x41 (hex escape) and \u0041 (unicode escape) always allowed
    • 3.3 Handle strict mode flag in lexer:
      • Option A: Lexer has strict: bool field, 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) or OctalEscapeSequence tokens, parser rejects in strict mode
    • 3.4 Add tests:
      • "use strict"; var x = 010; → SyntaxError
      • "use strict"; var s = "\1"; → SyntaxError
      • var 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
  • 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 SetGlobal or SetVar targets 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)
    • 4.2 Add explicit test in crates/js_vm/src/interpreter/tests/strict_mode_tests.rs:
      • "use strict"; undeclaredVar = 42; → ReferenceError
      • undeclaredVar = 42; in sloppy → creates global (or throws if not supported)
    • 4.3 Verify strict flag propagation in bytecode compiler:
      • FunctionBlueprint.strict: bool must be set correctly from Program.strict and Function.strict
      • Check crates/js_vm/src/bytecode/compiler/ for strict flag handling
  • 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 to JsObjectData
      • Add init_weakref(target: &JsObject), is_weakref(), deref_weakref() -> Option<JsObject> helpers
      • JsObject is Rc<RefCell<JsObjectData>> -- use Rc::downgrade() to create Weak
      • deref_weakref() calls Weak::upgrade() -- returns Some if target still alive, None if all strong refs dropped
    • 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. Store Rc::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) → return JsValue::Object(JsObject { inner })
      • If None → return JsValue::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.
    • 5.4 Wire WeakRef dispatch in calls.rs and add setup_weakref_builtins() in mod.rs
    • 5.5 Add unit tests in crates/js_vm/src/interpreter/tests/weakref_tests.rs:
      • new WeakRef({}) → object created
      • weakRef.deref() returns target when still referenced
      • TypeError on primitive target
      • TypeError when called without new
      • .deref() returns undefined after target is unreachable (test by dropping all strong refs in Rust test harness -- may not be testable from JS)
  • Task 6: FinalizationRegistry implementation (AC: #2, #3)

    • 6.1 Add FinalizationRegistry infrastructure to crates/js_vm/src/value.rs:
      • FinalizationRegistryData struct:
        cleanup_callback: JsValue,  // The callback function
        registrations: Vec<FinalizationEntry>,
        
      • FinalizationEntry struct:
        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> to JsObjectData
      • 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 target is not an object
        • TypeError if target === unregisterToken (same object)
        • Store weak reference to target, heldValue, and unregisterToken identity
      • .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).
    • 6.5 Wire dispatch in calls.rs and setup in mod.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)
  • 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 -- --nocapture
      • just js262-status promote --id <test-id>
    • 7.2 Run full Test262 suite and triage:
      • just test262-full
      • just triage-test262-full
    • 7.3 Run all existing JS test suites to verify no regressions:
      • cargo test -p js_vm
      • cargo test -p rust_browser --test js_tests
      • cargo test -p rust_browser --test js_dom_tests
      • cargo test -p rust_browser --test js_events
      • cargo test -p rust_browser --test js_scheduling
      • cargo test -p rust_browser --test js_async
      • cargo 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

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 identifier prohibition (parse-time)
  • this is undefined in non-constructor strict calls
  • .call() does not coerce non-object this

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:

  • 0 alone is decimal zero, NOT octal -- don't reject
  • 08, 09 are decimal (invalid octal digits) -- don't reject as octal
  • 010 is legacy octal = 8 -- reject in strict
  • 0o10 is 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.rs data fields, *_builtins.rs file, setup_*_builtins() call, dispatch in calls.rs
  • indexmap available as dependency
  • No unsafe code 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 ci after 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 unsafe code needed
  • No new external dependencies

References

Dev Agent Record

Agent Model Used

{{agent_model_name_version}}

Debug Log References

Completion Notes List

File List