Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save xeioex/2bf44a8e83a004a696c58302bffa5455 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.

Lowered AST Plan B

Scope

This document turns the design from LOWERED_AST.md into a concrete implementation plan.

Plan B means:

  • keep one AST
  • keep the current pipeline
    • parser
    • AST
    • generator
    • bytecode
  • do a balanced semantic AST upgrade
  • keep most work in:
    • src/njs_parser.c
    • src/njs_parser.h
    • src/njs_generator.c

Project Goals

Primary goals

  • reduce generator-side AST shape recovery
  • make call/property/optional-chain lowering explicit
  • make future codegen changes cheaper and less fragile
  • fix known semantics gaps like parenthesized optional-call receiver preservation in a clean way

Non-goals

  • no full parser-to-bytecode rewrite
  • no second IR/lowering tree
  • no large runtime architecture rewrite
  • no immediate rewrite of every AST node family

Strategy

Use a phased migration.

Each phase should:

  • introduce a small semantic improvement
  • preserve testability
  • leave the tree in a shippable state
  • shrink generator inference logic

The plan is organized around one core primitive first:

  • PROPERTY_REF

Then a second core upgrade:

  • explicit call mode

Design Summary

Plan B should converge toward these semantic forms:

Core semantic primitives

  1. PROPERTY_REF
  2. CALL with explicit mode
  3. OPTIONAL_CHAIN with explicit ownership contract
  4. assignment nodes constrained to lowered targets

Recommended bias

  • add explicit AST-only node kinds where semantics are materially different
  • use mode flags only where variants are truly close

That means:

  • prefer explicit PROPERTY_REF
  • likely unify calls under one semantic call node later
  • keep optional-chain as its own node family

Parser Algorithm

The lowered AST project should use a retroactive-upgrade parser algorithm.

This is the key rule:

  • parse property access as PROPERTY
  • upgrade it in place to PROPERTY_REF when a consuming context later proves that reference semantics are required

This is the right fit for the current parser architecture because the parser usually does not know the consuming context at the first a.b parse step.

Why retroactive upgrade is required

When the parser first sees a.b, it does not yet know whether the surrounding construct will become:

  • value read: x = a.b
  • method call: a.b()
  • assignment target: a.b = 1
  • update target: a.b++
  • delete target: delete a.b
  • tagged template call: a.b`x`

So the plan should not rely on first-pass parse-time prediction.

Mutation points

The parser should upgrade PROPERTY to PROPERTY_REF at the moment the consuming construct becomes known.

Expected upgrade points:

  • call creation for receiver-aware calls
  • tagged template creation for receiver-aware tagged calls
  • assignment target creation
  • increment/decrement target creation
  • delete target lowering
  • for (... in ...) / for (... of ...) lvalue lowering if property targets are accepted there

Chain invariant

Only the final property access in a reference-consuming chain becomes PROPERTY_REF.

Examples:

  • a.b.c.d = 1
    • a.b is PROPERTY
    • a.b.c is PROPERTY
    • a.b.c.d is upgraded to PROPERTY_REF
  • (a.b)()
    • a.b is upgraded to PROPERTY_REF
  • (0, a.b)()
    • comma produces a value, so the result is not PROPERTY_REF

This keeps reference semantics local to the actual consuming edge.

Proposed Node Model

This is the target model, not necessarily the immediate first patch.

Node 1: PROPERTY_REF

Purpose:

  • a property access that still carries enough information for:
    • get
    • set
    • delete
    • call with receiver

Suggested shape:

  • left = base/object expression
  • right = key expression or name node
  • u.operation for:
    • atom property
    • computed property

Struct layout rule

PROPERTY_REF should fit the existing njs_parser_node_t layout with minimal change.

Recommended rule:

  • PROPERTY_REF uses the same child layout as PROPERTY
    • left = base
    • right = key/name
  • PROPERTY_REF uses u.operation the same way PROPERTY does
  • new semantics should come from token_type, not from a new collection of boolean flags

This avoids broad struct churn and keeps the design closer to existing njs style.

Node 2: CALL

Purpose:

  • unified call node with explicit call mode

Suggested shape:

  • left = callee or reference-bearing source
  • right = args list
  • mode:
    • plain
    • with_receiver
    • constructor

If introducing a new CALL token is too disruptive at first, phase 2 can keep:

  • FUNCTION_CALL
  • METHOD_CALL

but add explicit mode fields and normalize their meaning.

Node 3: OPTIONAL_CHAIN

Purpose:

  • explicit short-circuit owner

Suggested shape:

  • left = tested base
  • right = continuation
  • result mode:
    • value
    • reference-bearing

Ownership rule

Optional ownership should stay orthogonal to call mode.

That means:

  • CALL mode only needs:
    • plain
    • with_receiver
    • constructor
  • optional behavior belongs to the surrounding OPTIONAL_CHAIN

This avoids duplicating:

  • optional_plain
  • optional_with_receiver

inside the call node family.

Timing rule

OPTIONAL_CHAIN result ownership is also determined retroactively.

At chain creation time, parser often still does not know whether the result will be consumed as:

  • plain value
  • receiver-preserving call source
  • delete target

So the optional-chain node should be updated when the consuming context becomes known, using the same retroactive-upgrade principle as PROPERTY_REF.

Node 4: ASSIGN

Purpose:

  • constrain assignment lowering to valid target shapes

Suggested shape:

  • left = lowered target
    • NAME
    • PROPERTY_REF
  • right = RHS
  • operator kind should remain primarily encoded in token type

The plan should not introduce a second assignment mode field unless it replaces assignment token distinctions entirely.

Recommended rule:

  • keep existing assignment token kinds
  • use lowered target shape to simplify generator branching

Project Phases

Phase 0: Preparation

Goals

  • document current parser/generator contracts
  • add focused tests before structural change
  • isolate hotspots where generator recovers semantics from AST shape

Files

  • src/njs_parser.c
  • src/njs_generator.c
  • src/test/njs_unit_test.c

Deliverables

  • targeted regression tests for:
    • parenthesized optional call receiver preservation
    • optional delete nesting
    • property assignment target edge cases
    • logical assignment on property refs
    • increment/decrement on property targets
    • tagged template method calls
    • comma operator stripping receiver semantics
  • code comments near current shape-recovery logic

Why this phase matters

It gives a stable baseline and prevents the project from drifting into silent semantic regressions.

Phase 1: Introduce PROPERTY_REF

Goals

  • distinguish value property access from reference-bearing property access
  • stop making generator decide late whether a property access is being used as a target or receiver-bearing call source

Files

  • src/njs_lexer.h
  • src/njs_parser.h
  • src/njs_parser.c
  • src/njs_generator.c
  • src/test/njs_unit_test.c

Recommended changes

1. AST node kind

Add a new AST-only token:

  • NJS_TOKEN_PROPERTY_REF

Do not replace all PROPERTY uses immediately.

Initial split:

  • PROPERTY value-producing property access
  • PROPERTY_REF target/reference-bearing property access

This split should be semantic only. For plain reads, PROPERTY_REF should still generate the same bytecode as current PROPERTY reads if the node is routed through a read path.

2. Parser lowering points

Parser should emit PROPERTY_REF when a property access is used in any context that may require reference semantics:

  • method-call source
  • assignment target
  • delete target
  • optional method-call source
  • pre/post increment target
  • pre/post decrement target
  • tagged template receiver source
  • for ... in / for ... of property lvalue, if supported

Parser can still emit plain PROPERTY for value-only access.

The parser should do this by retroactive in-place upgrade from PROPERTY to PROPERTY_REF, not by trying to predict the consuming context during first parse.

3. Generator dispatch

Add explicit generator handling for PROPERTY_REF.

Important rule:

  • generator should never need to inspect a plain PROPERTY and wonder whether it is really target-like

4. Serializer and side-effect helpers

Update:

  • AST serialization
  • njs_parser_has_side_effect() only as needed

PROPERTY_REF itself should remain side-effect free. Only call/update/ assignment/delete nodes remain side-effecting.

5. Delete interaction

Keep the existing mutation pattern.

Recommended rule:

  • PROPERTY used by delete first upgrades to PROPERTY_REF
  • PROPERTY_REF is then mutated to PROPERTY_DELETE

This keeps delete lowering consistent with the existing parser style and avoids introducing a separate delete-only reference model.

Expected generator cleanup

This phase should simplify:

  • method call input handling
  • delete handling
  • assignment target selection
  • increment/decrement target handling

Risk

  • medium

Success criteria

  • existing property read behavior unchanged
  • delete and assignment behavior unchanged
  • generator has fewer token_type == PROPERTY plus context-specific reinterpretation branches

Phase 2: Normalize Call Lowering

Goals

  • make call semantics explicit
  • remove call-mode recovery from AST shape
  • prepare for receiver-preserving optional call fixes

Files

  • src/njs_parser.h
  • src/njs_parser.c
  • src/njs_generator.c
  • maybe small updates in:
    • src/njs_vmcode.h
    • src/njs_vmcode.c
    • src/njs_disassembler.c

Options inside this phase

Option 2A: keep current call tokens initially

Keep:

  • NJS_TOKEN_FUNCTION_CALL
  • NJS_TOKEN_METHOD_CALL

But normalize them:

  • FUNCTION_CALL always means plain call
  • METHOD_CALL always means receiver-aware call
  • input shape must already be lowered and valid

This is the lower-risk first step.

Option 2B: unify under NJS_TOKEN_CALL

Introduce one call token with mode field.

This is cleaner, but larger.

Recommendation

Use Option 2A first.

Reason:

  • smaller parser churn
  • smaller serializer churn
  • still yields most of the benefit

Required parser behavior

The parser should only create:

  • METHOD_CALL from PROPERTY_REF
  • FUNCTION_CALL from value-only callees
  • receiver-aware tagged template call from PROPERTY_REF

That gives a clean invariant:

  • if a call needs receiver semantics, the AST already says so

Non-property expressions always produce value semantics, not PROPERTY_REF.

This is what makes:

  • (a.b)() preserve this
  • (0, a.b)() lose this

without special cases in later codegen.

Generator behavior target

njs_generate_method_call() should stop recovering hidden semantics and should assume:

  • its input already contains the correct receiver-bearing source

The transition plan should also explicitly remove the current u.object-backreference dependency used by optional method calls once PROPERTY_REF plus explicit optional ownership can encode the same meaning directly.

Optional VM improvement

Current frame split:

  • FUNCTION_FRAME
  • METHOD_FRAME

Short term:

  • keep both if needed

Medium term:

  • add explicit "call with known callee + known this" path

This VM improvement is recommended but not required to complete phase 2.

Risk

  • medium

Success criteria

  • no generator-side call ownership analysis beyond direct node fields
  • call behavior stable for:
    • plain function call
    • property method call
    • parenthesized non-optional method call

Phase 3: Rework Optional Chaining On Top Of PROPERTY_REF

Goals

  • make optional chaining a true short-circuit owner
  • preserve receiver semantics through parenthesized optional access when required
  • remove remaining optional-call AST recovery

Files

  • src/njs_parser.c
  • src/njs_parser.h
  • src/njs_generator.c
  • src/test/njs_unit_test.c

Parser target behavior

The parser should distinguish:

  • optional value access
  • optional reference-bearing access

This matters because:

  • o?.m may later be used as plain value
  • (o?.m)() must preserve receiver-aware call intent if the design chooses to preserve it semantically

Recommended model

OPTIONAL_CHAIN should carry enough information to say:

  • what base is tested
  • whether the continuation yields:
    • value
    • PROPERTY_REF
    • call node

This ownership should be assigned retroactively when the consuming context becomes known, not assumed at initial chain creation time.

Generator target behavior

Generator should no longer:

  • inspect optional-chain descendants trying to recover method ownership

Instead it should:

  • emit nullish short-circuit
  • seed continuation indexes from explicit lowered node meaning
  • preserve receiver temp only when node meaning says so

Key semantic cases to validate

  • o?.m()
  • o?.m?.()
  • (o?.m)()
  • (o?.["m"])()
  • o?.m\x``
  • delete o?.a?.b
  • a?.b.c
  • a.b?.c.d
  • (a?.b).c
  • a?.b?.c?.d
  • delete a?.b?.c?.d
  • o?.a = 1 remains invalid if grammar says so

Risk

  • high relative to prior phases

Success criteria

  • optional-chain code becomes local and declarative
  • parenthesized receiver-preserving optional call becomes fixable without parser/generator hacks

Phase 4: Rework Assignment Lowering

Goals

  • constrain assignment inputs to lowered target shapes
  • simplify simple, operation, and logical assignment families

Files

  • src/njs_parser.c
  • src/njs_generator.c
  • src/test/njs_unit_test.c

Parser target behavior

Assignment nodes should no longer accept an arbitrary "lvalue-like" expression and force generator to branch later.

The parser should already have normalized:

  • variable target
  • property reference target

This phase should explicitly include update expressions:

  • obj.prop++
  • ++obj.prop
  • obj.prop--
  • --obj.prop

Generator target behavior

njs_generate_assignment(), njs_generate_operation_assignment(), and njs_generate_logical_assignment() should branch on explicit target kind, not on arbitrary syntax node shape.

Benefits

  • clearer logical assignment lowering
  • simpler delete/assignment separation
  • fewer jump-placement mistakes caused by hidden target distinctions
  • increment/decrement lowering can use the same explicit target model

Risk

  • medium

Success criteria

  • assignment code paths are organized by target kind and operator kind
  • property-target logic is shared where possible

Phase 5: Remove Transitional Recovery Paths

Goals

  • delete compatibility code
  • tighten invariants
  • make the lowered AST contract explicit

Files

  • src/njs_parser.c
  • src/njs_generator.c
  • docs/tests as needed

Cleanup targets

  • AST shape recovery helpers that should no longer exist
  • parser fallbacks that create old ambiguous forms
  • generator comments describing legacy recovery behavior

Concrete cleanup checklist

  • njs_parser_create_call() no longer uses raw node->token_type == NJS_TOKEN_PROPERTY shape recovery
  • tagged template call lowering no longer uses raw property shape
  • njs_generate_method_call() no longer depends on njs_generate_optional_method_call() for semantic detection
  • njs_generate_assignment() no longer branches on arbitrary property-vs-name syntax shape
  • njs_generate_operation_assignment() no longer branches on arbitrary property-vs-name syntax shape
  • njs_generate_logical_assignment() no longer branches on arbitrary property-vs-name syntax shape
  • njs_generate_inc_dec_operation() no longer branches on raw NJS_TOKEN_PROPERTY shape
  • njs_parser_optional_chain_property() shape recovery is removed or reduced to transitional compatibility only
  • optional-call lowering no longer needs METHOD_CALL.u.object back-reference hacks
  • njs_generate_optional_chain() no longer uses hoist as a property-preservation trick
  • PROPERTY_REF and PROPERTY have explicit, documented routing invariants

Risk

  • low

Success criteria

  • the main semantic invariants are true by construction
  • new feature work naturally uses lowered forms

Core Invariants To Enforce

These should be documented and asserted mentally throughout the project.

Invariant 1

PROPERTY_REF is the only property node family allowed to feed:

  • assignment targets
  • delete targets
  • receiver-aware calls
  • tagged template receiver-aware calls
  • update-expression targets

Invariant 2

METHOD_CALL never depends on source-shape reconstruction.

If a node is METHOD_CALL, parser has already encoded the receiver relationship explicitly.

Invariant 3

OPTIONAL_CHAIN explicitly owns its short-circuit region.

Generator does not scan descendants to decide what the chain "really" means.

Invariant 4

Assignments are lowered targets plus existing assignment token kinds.

Generator does not re-validate arbitrary expression shapes as targets.

Invariant 5

For pure value reads, PROPERTY_REF must be able to generate the same bytecode as the current PROPERTY read path.

The distinction is semantic routing, not a required change in read-code quality.

Invariant 6

Non-property operators produce values, not references.

This includes:

  • comma
  • arithmetic operators
  • logical operators
  • conditional expressions

Grouping alone must not strip reference semantics.

Bytecode Plan

The project should separate AST work from bytecode improvements.

Stage 1

Reuse current VM forms as much as possible:

  • FUNCTION_FRAME
  • METHOD_FRAME
  • FUNCTION_CALL

This keeps risk down while AST semantics improve.

Stage 2

After phases 1-3 stabilize, consider a small VM call-path improvement:

  • explicit receiver-aware call frame

Possible forms:

  • new frame opcode
  • generalized frame opcode with mode

Recommendation

Do not start with VM opcode redesign.

Do it only after the semantic AST contract is stable.

Parser Work Breakdown

Main parser tasks

  1. Add new AST-only token(s)
  2. Update node creation helpers
  3. Lower property access by context
  4. Lower call creation from lowered inputs
  5. Lower optional chain ownership explicitly
  6. Lower assignment targets explicitly
  7. Update serializer
  8. Lower update-expression targets explicitly

Specific hotspots

  • njs_parser_create_call()
  • tagged template / postfix call creation paths
  • optional-chain parse states
  • unary delete
  • assignment operator parse states
  • update-expression parse states
  • for ... in / for ... of target parse states if property lvalues are supported
  • left-hand-side expression states

Generator Work Breakdown

Main generator tasks

  1. Add dispatch for lowered node kinds
  2. Stop overloading plain property generation
  3. Simplify method-call input assumptions
  4. Simplify optional-chain code ownership
  5. Simplify assignment target branching
  6. Simplify update-expression target branching
  7. Remove recovery helpers after each migration phase

Specific hotspots

  • property generation paths
  • function/method call generation
  • tagged template call generation
  • optional-chain generation
  • logical assignment generation
  • update-expression generation
  • target release/index release logic

Small Changes Outside Parser/Generator

These should remain limited.

src/njs_lexer.h

  • AST-only token additions

src/njs_vm.c

  • only AST debug output shape changes

src/njs_function.c

  • safe-mode AST whitelist updates if token sequences change

src/njs_vmcode.*

  • ideally none until the call-path cleanup step
  • later, a small explicit receiver-aware call path

src/njs_disassembler.c

  • only if bytecode changes later

Test Plan

Each phase should land with focused regression coverage.

Call/reference tests

  • o.m()
  • (o.m)()
  • var f = o.m; f()
  • (0, o.m)()
  • o?.m()
  • o?.m?.()
  • (o?.m)()
  • (o?.["m"])()
  • o.m`x`
  • o.m1().m2()
  • new o.Ctor()

Delete/reference tests

  • delete o.a
  • delete o?.a
  • delete o?.a?.b
  • delete o?.a?.b?.c

Assignment/reference tests

  • o.a = 1
  • o.a += 1
  • o.a ||= 1
  • o.a++
  • ++o.a
  • o.a--
  • --o.a
  • for (o.a in src) ; if property lvalues are supported there
  • readonly/getter-only/non-extensible short-circuit cases

Structural confidence tests

  • AST serialization for representative expressions
  • existing unit tests
  • test262 focused subsets for:
    • optional chaining
    • logical assignment
    • delete
    • update expressions

Recommended Commit Structure

This project should be split into small, reviewable chunks.

Suggested layering:

  1. preparatory tests and comments
  2. PROPERTY_REF introduction
  3. parser lowering for property refs
  4. generator support for property refs
  5. call normalization on top of property refs
  6. optional-chain lowering cleanup
  7. assignment lowering cleanup
  8. removal of transitional recovery code

Risk Assessment

Low-risk parts

  • AST serialization updates
  • explicit token additions
  • parser-side target normalization with tests

Medium-risk parts

  • property-ref introduction
  • call normalization
  • assignment lowering cleanup

High-risk parts

  • optional chaining with receiver preservation
  • any early VM opcode redesign

Recommended First Two Milestones

Milestone 1

Land PROPERTY_REF with enough parser/generator support to use it for:

  • method calls
  • delete
  • assignment targets

This is the foundation milestone.

Milestone 2

Normalize call creation so that:

  • plain calls and receiver-aware calls are explicit
  • generator no longer reconstructs hidden receiver ownership

Do not try to solve all optional-chaining edge cases until these two milestones are stable.

Final Recommendation

The project should proceed in this order:

  1. PROPERTY_REF
  2. explicit call lowering
  3. optional-chain ownership cleanup
  4. assignment lowering cleanup
  5. optional VM call-path improvement

That sequence best matches the goals:

  • most work in parser/generator
  • few other-file changes
  • real simplification of future codegen changes
  • lower risk than a more aggressive rewrite
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment