Skip to content

Instantly share code, notes, and snippets.

@xeioex
Created March 3, 2026 08:09
Show Gist options
  • Select an option

  • Save xeioex/f5084952db8353d2558539abe752bbbb to your computer and use it in GitHub Desktop.

Select an option

Save xeioex/f5084952db8353d2558539abe752bbbb to your computer and use it in GitHub Desktop.

Lowered AST Phase 1 Closeout

Status

Phase 1 is complete.

Implemented commits:

  • 334e888a parser: lower method call property sources to PROPERTY_REF
  • d8fd6e2b parser: lower property targets to PROPERTY_REF
  • 7e169263 generator: accept PROPERTY_REF in property target paths

Verified AST Shapes

Representative AST dumps were checked with ./build/njs -a -c ....

Value read stays PROPERTY

Expression:

  • var o={a:1}; o.a

Observed:

  • final read node is PROPERTY

Receiver-bearing call uses PROPERTY_REF

Expression:

  • var o={a:function(){return 1}}; o.a()

Observed:

  • call node is METHOD_CALL
  • METHOD_CALL.left is PROPERTY_REF

Assignment target uses PROPERTY_REF

Expression:

  • var o={a:1}; o.a = 1

Observed:

  • assignment left-hand side is PROPERTY_REF

Update target uses PROPERTY_REF

Expression:

  • var o={a:1}; o.a++

Observed:

  • POST_INCREMENT.left is PROPERTY_REF

Delete uses lowered target path

Expression:

  • var o={a:1}; delete o.a

Observed:

  • final node is PROPERTY_DELETE
  • parser now reaches it through PROPERTY -> PROPERTY_REF -> PROPERTY_DELETE

Tagged template receiver path uses PROPERTY_REF

Expression:

  • var o={x:1,m:function(){return this.x}}; o.m\x``

Observed:

  • tagged call lowers to METHOD_CALL
  • METHOD_CALL.left is PROPERTY_REF

for-in property lvalue uses PROPERTY_REF

Expression:

  • var dst={}; for (dst.a in {x:1}) {}

Observed:

  • FOR_IN.left.left is PROPERTY_REF

Comma strips reference semantics

Expression:

  • var o={x:1,m:function(){return this.x}}; (0, o.m)()

Observed:

  • final call node is FUNCTION_CALL
  • comma right side remains plain PROPERTY

This matches the phase-1 chain invariant.

Runtime Verification

Verified with ./build/njs -c:

  • (o.f)() preserves receiver
  • o.t\!`` preserves receiver
  • (o.a) = 2
  • (o.a)++
  • delete (o.a)
  • for ((dst.a) in {x:1}) {}

Also verified:

  • make -j4 njs
  • make -j4 unit_test

Unit test total at closeout:

  • TOTAL: PASSED [5986/5986]

Remaining Deferred Items

These are intentionally deferred beyond phase 1:

  • remove METHOD_CALL.u.object optional-call back-reference hack
  • remove optional-chain shape recovery in parser/generator
  • unify call node family
  • solve (o?.m)() / preserved-this design gap
  • tighten generator splits further so fewer paths tolerate plain PROPERTY

Audit Result

Remaining plain PROPERTY assumptions are phase-1 acceptable:

  • initial member parsing still creates PROPERTY
  • parser upgrades to PROPERTY_REF only at consuming contexts
  • optional helpers still accept both PROPERTY and PROPERTY_REF transitively

No additional phase-1 correctness gaps were found in parser/generator after the closeout audit.

Lowered AST Phase 2 Closeout

Scope

Phase 2 was intended to normalize lowered call semantics on top of phase 1 PROPERTY_REF.

The target was:

  • make receiver-bearing calls explicit
  • reduce parser/generator shape recovery around calls
  • keep optional ownership on OPTIONAL_CHAIN
  • prepare a clean path for fixing grouped optional calls like (o?.m)()

Landed Work

Phase 2 delivered the following.

1. Call contract tightening

The parser/generator contract is stricter than it was before:

  • PROPERTY_REF is the direct source form for receiver-bearing calls
  • METHOD_CALL is the receiver-bearing call form
  • FUNCTION_CALL is the plain value-call form
  • comma still strips receiver semantics
  • grouping still preserves receiver semantics

This removed some of the earlier silent recovery from incidental AST shape.

2. Optional-chain helper cleanup

Optional-call and optional-target helper paths were split and localized.

The important result is that optional-chain state is no longer scattered across unrelated parser/generator code paths. The remaining transitional preserve-state handling is concentrated in a small set of helpers.

3. Narrow call-with-this runtime path

The grouped optional-call bug was fixed with a narrow runtime extension:

  • FUNCTION_FRAME_THIS

This lets codegen pass:

  • resolved callee
  • explicit this

for grouped optional plain-call cases without redesigning the entire call model.

This path now fixes:

  • (o?.m)()
  • (o?.[k])()
  • the full optional-call-preserves-this test262 matrix

4. Regression coverage

Local unit coverage now includes the receiver-preserving optional-call matrix that previously existed only in test262.

That matters because this area is easy to regress during parser/generator cleanup, especially around grouping and optional ownership boundaries.

Resulting Invariants

At phase-2 closeout, these invariants hold.

Call lowering

  • direct property call sources lower through PROPERTY_REF
  • METHOD_CALL.left is expected to be PROPERTY_REF
  • FUNCTION_CALL may carry explicit this only for the grouped optional-call preservation case

Grouping and value boundaries

  • grouping preserves receiver-bearing semantics
  • comma strips receiver-bearing semantics

Optional ownership

  • OPTIONAL_CHAIN remains the short-circuit owner
  • inner call mode remains plain vs receiver-bearing
  • optional ownership is not encoded as a call-mode variant

What Is Still Transitional

Phase 2 intentionally did not eliminate all transitional state.

The remaining transitional pieces are:

  • METHOD_CALL.u.object is still used as optional-call preserve state
  • FUNCTION_CALL.u.object is now also used for the narrow grouped optional-call this handoff
  • OBJECT_VALUE is still used as the preserve wrapper node for optional-chain state

This is acceptable for phase 2 because:

  • the state is now localized
  • the runtime behavior is correct
  • the remaining coupling is explicit and auditable

It is not yet the final lowered-AST design.

What Phase 2 Did Not Solve

Phase 2 did not attempt to solve all call modeling issues.

Specifically, it did not:

  • replace the current call node family with a unified call node
  • replace u.object transitional call metadata with a dedicated semantic field layout
  • remove OBJECT_VALUE preserve wrappers from optional chaining
  • redesign bytecode around a general call-with-explicit-receiver model

Those belong to a later phase if we still want a cleaner lowered call IR.

Recommended Phase 3 Ownership

If work continues, phase 3 should be treated as structural cleanup, not bug triage.

Recommended phase-3 targets:

  1. separate transitional call metadata from generic u.object reuse
  2. reduce or remove OBJECT_VALUE as the optional preserve wrapper
  3. decide whether FUNCTION_FRAME_THIS remains a narrow special path or becomes part of a more general explicit call-with-receiver model
  4. make the remaining optional-call preserve contract less dependent on METHOD_CALL back-references

The key point is:

  • phase 2 already achieved the needed behavior and normalization
  • phase 3 should only be started if the goal is architectural cleanup

Closeout Assessment

Phase 2 is complete enough to stop here.

Reasons:

  • the original grouped optional-call this bug is fixed
  • the behavior is pinned by local unit tests and test262 coverage
  • parser/generator recovery is materially reduced compared to the starting point
  • the remaining transitional state is localized rather than scattered

This is a valid end state for phase 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment