Skip to content

feat(parser): chained assignment with attribute and subscript targets - #142

Merged
ivarvong merged 1 commit into
mainfrom
feat/chained-assign-targets
Jul 2, 2026
Merged

feat(parser): chained assignment with attribute and subscript targets#142
ivarvong merged 1 commit into
mainfrom
feat/chained-assign-targets

Conversation

@ivarvong

@ivarvong ivarvong commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Real Python chains assignments through any valid target — self.prev = self.next = None, v = d['k'] = compute(), a[0] = a[1] = False — but pyex only chained bare names (x = y = 1). Everything else died with unexpected token at the second =. This is the parser gap that broke an idiomatic LRU cache (Node.__init__ with self.prev = self.next = None) in the WasmGC demo.

  • Parser (lib/pyex/parser.ex): collect_chained_assign now accepts any valid target (name, getattr, subscript) at each link, and a shared parse_assign_value helper lets every assignment entry point — attr, subscript, slice, nested-subscript, expression-statement — continue a chain from its own target. Single-target assignments still emit their exact old AST nodes, so nothing downstream sees a new shape unless a chain is present.
  • Interpreter (lib/pyex/interpreter/bindings.ex): eval_chained_assign generalizes from [String.t()] to mixed unpack_target() lists, evaluating the RHS exactly once and binding left-to-right by reusing bind_assignment_pairs (the tuple-unpack machinery). A new slice-key clause keeps a[:2] = b = x routed through the name-form slice splicer.
  • Deleted the old parse_subscript_assign_value block-desugar, which duplicated the RHS AST across the block — a[0] = b = f() used to call f() twice. The unified path fixes that class of bug.
  • One-line spec widening in assignments.ex (eval_name_subscript_assign accepts {:__evaluated__, v}), demanded by Dialyzer.

Semantics verified

RHS evaluated exactly once across the chain; a = b = [] aliases (a is bTrue); assignment order is left-to-right; x = 1 == 2 still parses as comparison; f() = x = 1 is still a syntax error. Eleven new tests in test/pyex/interpreter_test.exs under "chained assignment", including a linked-list pointer-splice case mirroring the LRU's _remove.

Test plan

  • mix format --check-formatted
  • mix compile --warnings-as-errors (modulo the three pre-existing 1.20.2-local warnings on main)
  • mix test — 6512/6513 (the one failure is the known local-OTP zipfile CRC-message artifact, present on main)
  • mix dialyzer — clean
  • CI green (watching)

🤖 Generated with Claude Code

https://claude.ai/code/session_01KuxYKoh8pXEna5ohZUgJsn

…rgets

The parser only chained bare names (x = y = 1); attribute targets
(self.prev = self.next = None), name/subscript mixes (v = d['k'] = 1),
and nested subscripts all failed with "unexpected token" at the second
'='. A separate half-implementation for subscript chains desugared to a
block while duplicating the RHS AST, so a[0] = b = f() called f() twice.

Generalize collect_chained_assign to accept any valid target (name,
getattr, subscript) at each link, and route every assignment entry
point (attr, subscript, slice, nested-subscript, expression-statement)
through a shared parse_assign_value helper so any of them can continue
a chain. Single-target assignments still emit their exact old AST
nodes. Chains emit one :chained_assign node with mixed targets;
eval_chained_assign evaluates the RHS exactly once and binds left to
right by reusing bind_assignment_pairs, with a slice-key clause so
a[:2] = b = x keeps routing through the name-form slice splicer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuxYKoh8pXEna5ohZUgJsn
@ivarvong
ivarvong merged commit c407f82 into main Jul 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant