Section 02: Parser — assignment_target + AssignTarget AST
Goal
See frontmatter. Proposal Phase 2. Parser accepts the extended LHS and emits a single chain-capturing node; all desugaring is deferred to the type checker (section-03).
Implementation Sketch
The current grammar restricts assignment LHS to a bare identifier. This section extends the assignment-target dispatcher to accept an identifier followed by any combination of [expr] (index) and .ident (field) suffixes, emitting one AssignTarget node. The parser does NOT decide spread-vs-updated (that needs types); it records the raw chain. The root must remain a bare identifier (mutability is checked in section-03).
Design Decisions
- DD-3 (
stepsstorage = flattened-arena range, NOT inlineVec):Expr+ExprKindderiveCopy(ori_ir/src/ast/expr.rs:35,91) and are size-pinned viastatic_assert_size!(ExprKind, 24)/static_assert_size!(Expr, 32)(expr.rs:518-523). An inlineVec<AccessStep>breaks BOTH (losesCopy, 24-byteVecfield exceeds theExprKindbudget). Store the chain in a parallelAccessSteparena addressed byAccessStepRange { start: u32, len: u16 }built with the existingdefine_range!macro — the same shape every variadic-child IR node already uses (ExprRange/ArmRange/CallArgRange,expr.rs:21-24; arena pattern perir.md §Arena Allocation+§Range Types).AccessStepis itself aCopyenum (Index(ExprId)/Field(Name)). This is mission-aligned: ori_ir’sFlatten Everything(expr.rs:13:No Box<Expr>, use ExprId(u32) indices) is theori_irper-crate mission’s load-bearing invariant. - DD-4 (root payload =
root: ExprId, NOTroot: Name+span): the grammar restricts the assignment-target root to a bare identifier (grammar.ebnfassignment_target = identifier { ... }), so aName+span representation is expressible.root: ExprIdis chosen instead because (a) the00-overview.mdArchitecture / Data Flow block already pinsroot: ExprIdand section-03’s desugar consumes that field as anExprId(right-to-left wrapping reads the root as an expression node); (b) it keepsAssignTargetuniform with the existingIndex { receiver: ExprId, ... }/Field { receiver: ExprId, ... }variants whose receivers areExprId(expr.rs:165,168), so section-03 reuses the same receiver-resolution path; (c) the rootExprIdresolves to anExprKind::Identnode, preserving its span for diagnostics without a parallelSpanfield. The parser validates the root IS a bare-identifier expression (rejectingf()[0] = x,(a+b)[0] = x,(a as T).f = x) — the syntactic restriction is enforced at parse time, not encoded in the field type. - DD-5 (zero-step boundary — bare
x = exprkeepsExprKind::Identtarget, neverAssignTarget { steps: [] }):grammar.ebnfassignment_target = identifier { "[" expression "]" | "." identifier }makes a bare identifier a valid zero-step assignment_target. The parser MUST guard against emitting an empty-chainAssignTargetnode: when the LHS resolves to a bare identifier with zero index/field suffixes,ExprKind::Assign { target, value }keepstargetresolving toExprKind::Identexactly as today (DD-1 leaves bare-identifier targets unchanged).AssignTargetis emitted ONLY when ≥1 access step is present. This preserves the unchanged-shape invariant for the overwhelmingly-common bare-identifier assignment and keeps section-03’s desugar from running on a degenerate empty chain. - DD-6 (
**=operator-table representation — verify before the matrix runs): success_criteria DD-2 claims ALLparse.md §PR-5compound operators (incl.**=) accept the extended LHS uniformly.**=(power-assign) must already tokenize/fuse in the operator tables (ori_ir/src/token/tag.rs,ori_ir/src/ast/operators.rs) for the PR-5 parse-time rewrite to reach it. The matrix’s**=row is gated on a precondition check: confirm**=is representable (the**operator desugars perori-syntax.md §Operatorsprecedence-2**right-assoc + compound**=per§Compound assignment); if a token/operator-fusion gap surfaces, that gap is a discovered bug — file via/add-bugand pull into scope per CLAUDE.md §Tests That Expose Bugs (NOT a silent matrix-row skip).
02.1 assignment_target Grammar + AssignTarget AST Node
- TDD-first: failing parser test matrix — target shapes
x/x[i]/x.f/x.f[i]/x[i].f/x.f.g[i].h× ALLparse.md §PR-5operators=/+=/-=/*=//=/%=/**=/@=/&=/|=/^=/<<=/>>=/&&=/||=. Negative (non-identifier root / non-target postfix rejection):f()[0] = x,(a+b)[0] = x,a?.b = x,a.m() = x,a.m()[0] = x,(a as T).f = x— method-call LHS and cast LHS rejected. Negatives live in BOTH the Rust parser tests (ori_parse/src/grammar/expr/mod.rs) ANDtests/spec/expressions/index_assignment_syntax.ori(#compile_failpins). - Add
AssignTargetAST node inori_ir/src/ast/expr.rsas an ADDITIVEExprKindvariant —ExprKind::AssignTarget { root: ExprId, steps: AccessStepRange }(DD-3 + DD-4).stepsis a flattened-arena range (NOTVec<AccessStep>— aVecfield breaksCopyonExprKind/Expratexpr.rs:35,91AND blowsstatic_assert_size!(ExprKind, 24)atexpr.rs:518-523). DefineAccessStep::Index(ExprId)/AccessStep::Field(Name)as aCopyelement enum, store it in a parallel arena, and address it viaAccessStepRange { start: u32, len: u16 }built with thedefine_range!macro (sibling toExprRange/ArmRange/CallArgRange). All three deriveCopy/Clone/Eq/PartialEq/Hash/Debugfor Salsa. Add theAssignTargetarm to theExprKindDebugimpl (expr.rs:377-512) and re-check the size assertion holds.ExprKind::Assign { target: ExprId, value: ExprId }field shape is UNCHANGED —targetresolves to the newExprKind::AssignTargetnode for chain targets, or staysExprKind::Identfor bare-identifier targets per DD-5 (DD-1 resolution; not a field-type change, not a parallel struct).- Rust Tests:
ori_ir/src/ast/tests.rs— assertExprKindsize unchanged (thestatic_assert_size!compile-time check),AccessStepRangeround-trips through the arena, andAssignTargetderivesCopy.
- Rust Tests:
- Keep
ExprKind::Assignconsumers + incremental copier compiling; close the cross-crate exhaustive-match gate (agy-F2): add anExprKind::AssignTargetarm toori_parse/src/incremental/copier/expr.rs(additive copier variant, mirrors the existing per-variant pattern near line 251). Walk EVERY exhaustiveExprKindmatch site that lacks a_catch-all —ori_irDebug impl (expr.rs:377-512) + visitor walks,ori_types,ori_canon,ori_eval,ori_arc,ori_fmt,oric— adding the explicitAssignTargetarm. Gate:cargo c(workspace) compiles green; the Rust non-exhaustive-match error IS the enforcement (impl-hygiene.md §IR Variant Exhaustiveness). Sites matchingAssign(shape unchanged) stay compiling untouched; chain-target routing sites handExprKind::AssignTargetto section-03’s desugar (no eval/canon/AIMS handling here — parser stays syntax-only). Banned: a_ => unreachable!()/_ => todo!()catch-all silently swallowing the new variant (deferred GAP).- Rust Tests:
ori_parse/src/incremental/copier/expr.rscopier round-trip tests
- Rust Tests:
- Extend parser to accept
assignment_targeton the LHS of=+ compound operators —ori_parse/src/grammar/expr/mod.rs(assignment-target dispatcher) +ori_parse/src/grammar/expr/postfix.rs(chain resolution). Emit ONEAssignTargetnode ONLY when ≥1 index/field step is present (DD-5 zero-step guard); a bare-identifier root with zero steps keepsExprKind::Assign { target: ExprKind::Ident }. Reject non-identifier roots + non-index/field postfix.- Rust Tests:
ori_parse/src/grammar/expr/mod.rsparser tests - Ori Tests:
tests/spec/expressions/index_assignment_syntax.ori
- Rust Tests:
- Zero-step boundary guard test (DD-5 / opencode-F1): pin a parser unit test that
x = expr(bare identifier, zero suffixes) producesExprKind::Assign { target: <ExprKind::Ident> }, NOTExprKind::AssignTarget { steps: <empty range> }. Asserts the dispatch never emits a degenerate empty-chain node for the common bare-identifier case.- Rust Tests:
ori_parse/src/grammar/expr/mod.rszero-step-boundary test
- Rust Tests:
-
**=operator-table precondition check (DD-6 / codex-F1): BEFORE the**=matrix row runs, confirm**=is representable in the operator/token tables (ori_ir/src/token/tag.rs,ori_ir/src/ast/operators.rs) — the PR-5 parse-time rewrite ofx[i] **= y->x[i] = x[i] ** yrequires**=to tokenize/fuse. If a token/operator-fusion gap surfaces, file via/add-bugand pull into scope (CLAUDE.md §Tests That Expose Bugs) — NOT a silent matrix-row skip. - Compound-operator LHS: verify ALL
parse.md §PR-5compound operators (+= -= *= /= %= **= @= &= |= ^= <<= >>= &&= ||=) accept the extended target (no subset deferral); compound expand stays the parse-time rewrite (parse.md §PR-5) over the extended target (pertypeck.md §EX-17— compound desugars tox = x op y, then section-03 desugars the index/field target). - Compound chain-target shared-identity invariant (AR-1 / DD-2 / opencode-F2): the PR-5 parse-time expansion of
x[i] op= rhs->x[i] = x[i] op rhsover a chain target MUST reuse the already-parsed chain for the RHS read-copy viaExprIdclone of the index/field-step expressions, NOT re-parse the LHS from token source. Re-parsing would resolve a side-effecting index key (x[f()] += 1) twice; cloning the parsedExprIds gives the read-copy and the write-copy (theAssignTarget) the SAME chain identity (structurally-identical steps), so section-03 desugars read-copy viaIndex(read) and write-copy viaIndexSet(write) into their correct roles, evaluatingf()exactly once (pertypeck.md §EX-17+parse.md §PR-5; the eval-once count-pinned test lives in section-05). Pin a parser test asserting both copies carry the structurally-identical chain (same stepExprIds) forstate.items[i] += 1; banned: re-parsing or two independently-resolved chain targets that section-03 could desugar inconsistently.- Rust Tests:
ori_parse/src/grammar/expr/mod.rscompound-chain-identity test
- Rust Tests:
- Verify: parser matrix green; no regression in
cargo t -p ori_parse+./test-all.sh.
Intelligence Reconnaissance
2026-06-01 — feature-mode scaffold; proposal is the research artifact. Verify the exact module against the shipped tree before editing — do NOT trust a guessed path.
scripts/intel-query.sh file-symbols "ori_parse/src/grammar/expr" --repo ori— assignment-target dispatcher + chain resolution[ori:ori_parse/src/grammar/expr/].scripts/intel-query.sh symbols "Assign" --repo ori --kind type— existing assignment AST node to extend[ori:ori_ir/src/ast/expr.rs].scripts/intel-query.sh callers "parse_assignment" --repo ori— assignment dispatch entry.
Spec References
- Proposal §Grammar Changes, §Implementation Note: Type-Directed Desugaring (parser is syntax-only).
grammar.ebnfproduction (synced in section-04):assignment_target = identifier { "[" expression "]" | "." identifier }.- Spec §13.6 Assignment Semantics — §13.6.1 field assignment, §13.6.2 index assignment (
13-variables.md); the surface this parser accepts. parse.md §PR-5(compound-assignment parse-time rewrite).ir.md §Arena Allocation+§Range Types+§DerivedTrait;ori_ir/src/ast/expr.rs(Copyderives +static_assert_size!budget) — AST-representation grounding for DD-3.
Tests
tests/spec/expressions/index_assignment_syntax.ori + Rust parser/AST unit tests per items.
HISTORY
- 2026-06-11 — Stale
review_pipeline:marker cleared by /continue-roadmap orchestrator: marker carriedstage: ?,next_step: ?,updated: ?. Per /review-plan SKILL.md §Step 1a stale-marker rule (reviewed: false+ marker present → STALE by definition), marker invalid; prior diagnosis preserved here for traceability. Cure rooted inscripts/plan_orchestrator/markers.py:clear_stale_marker_if_unreviewed.