Skip to content

codegen: evaluate a dependent persistent aggregate initializer in statement order (Fixes #226) - #259

Closed
luisleo526 wants to merge 3 commits into
mainfrom
cx/iss-226
Closed

luisleo526 wants to merge 3 commits into
mainfrom
cx/iss-226

Conversation

@luisleo526

@luisleo526 luisleo526 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

codegen: initialize a dependent persistent aggregate at its declaration, in statement order (#226)

Part of #226 (the var form, which is the issue's script). The varip form of the same script has the same cause and is not changed here: it is tracked in #258.

The defect

An aggregate var (UDT, array, map or matrix, built directly or through a helper) whose initializer reads a value that only the statement walk computes (an earlier ordinary declaration, a reassignment or field write, a collection method, or a global read by a user helper) was initialized in the first-bar _var_initialized preamble, before those statements ran. The constructor read the member's zero default. Nothing fails; the value is silently wrong. On the issue's script the field holds 0 where the same script with the dependency written inline holds strategy.initial_capital / 100.

The change

Such declarations now use the existing per-declaration once guard (_pf_var_init_<name>) at their statement position.

  • base.py: statement_ordered_aggregate selects them from traversal positions and the analyzer's binding scopes, so helper locals and parameters cannot capture an unrelated global of the same spelling; a later aggregate that depends on a moved var follows it.
  • emit_top.py: skips the selected right-hand side in the preamble and pushes the typed na slot for a history-read UDT.
  • visit_stmt.py: supplies the authored UDT target type and avoids numeric coercion for the routed aggregates.
  • AGENTS.md / CLAUDE.md: the support contract in one paragraph. CHANGELOG.md: one bullet.

Independent constructors, aliases, literals, bar-only initializers, varip and callable-local declarations keep their existing routes. Existing tests and expected fixtures are not changed.

Evidence

  • New tests in tests/test_var_initializer_statement_order.py: every expectation comes from a control form of the same script (the dependency written inline, or var removed for the emission-order checks), never from captured codegen output. The issue's script is the first row.
  • Red first: on the base commit the new file has 23 failing rows, exactly the dependent var rows; the independent-constructor row and the varip known-gap row pass on base. Green on this head: full pytest 10,657 passed, 0 failed, 221 skipped (the same 221 as in the red run on base; the new file has no skip or xfail marker); compile-corpus 312 passed.
  • Emitted C++ across the engine corpus and all tests/*.pine fixtures (772 scripts that compile) is byte-identical to the base commit; this is a byte-neutral statement and does not show that the new route is exercised there. Three synthetic scripts were added to the same comparison: the issue's var script and a var aggregate reading a moved scalar var change as intended; the varip twin is byte-identical to base. The comparison only transpiles, so it does not depend on the host it ran on.
  • Limits: the runtime rows compare field values; the eight synthetic bars place no trade even in the control, so no trade-level claim is made from them.

Planned for codegen 1.9.0.

🤖 Generated with Claude Code

luisleo526 and others added 2 commits October 26, 2026 13:39
)

A `var` initializer that constructs a UDT (or array/map/matrix, directly or
through a helper) from a variable declared on an earlier line is emitted in
the first-bar preamble, before the statement that assigns that variable, so
it reads the member's zero default: the field holds 0 where the same script
with the dependency written inline holds the value.

These tests fail on the current head for that reason:
- the issue's script: the emitted assignment order of `base` and `state`
  must equal the order of the same script without `var`;
- the issue's script and its inline-expression control, run on eight fixed
  bars through the engine harness (static and dynamic): the positive flag and
  the observed field equal the control's. No trade-level claim is made: the
  eight flat bars place no trade even in the control;
- five dependent aggregate shapes (UDT, array, matrix, map and UDT built by a
  helper) plus persistence, reassignment and moved-scalar-`var` cases, all
  compared against a control form of the same script, never against captured
  codegen output;
- a dependent `varip` row that pins only that the `varip` emission is not
  touched by this change (known gap, not a claim of correctness).

Part of #226.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…tement order (#226)

An aggregate `var` (UDT, array, map, matrix, directly or through a helper)
whose initializer reads a value that only the statement walk computes (an
earlier ordinary declaration, a reassignment or field write, a collection
method, or a global read by a user helper) used to be initialized in the
first-bar `_var_initialized` preamble, before those statements ran, so it
read the member's zero default. Silent wrong value.

Such declarations now use the existing per-declaration once guard at their
statement position:
- base.py selects them (`statement_ordered_aggregate`) using traversal
  positions and the analyzer's binding scopes, so helper locals and
  parameters cannot capture an unrelated global of the same spelling;
  later aggregate dependencies on a moved `var` follow it;
- emit_top.py skips the selected RHS in the preamble and pushes the typed
  na slot for a history-read UDT;
- visit_stmt.py supplies the authored UDT target type and avoids numeric
  coercion for the routed aggregates.
Independent constructors, aliases, literals, bar-only initializers, `varip`
and callable-local declarations keep their existing routes; non-var
statements keep their order. Existing tests and expected fixtures are not
changed. AGENTS.md and CLAUDE.md state the support contract in one paragraph.

Part of #226.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526 luisleo526 changed the title codegen: evaluate a dependent persistent aggregate initializer in statement order (Part of #226) codegen: evaluate a dependent persistent aggregate initializer in statement order (Fixes #226) Oct 26, 2026
Resolve the CHANGELOG conflict with the planned 1.8.0 section: the entry for
the statement-ordered persistent initializer sits in a new "1.9.0 — planned"
section above it, and the 1.8.0 section is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 26, 2026
…ialization, request.currency_rate, helper-local var color, one-line field assignment) (#261)

## What

This branch carries five codegen fixes planned for 1.8.0. It is five merge commits on `main`, one per pull request, in this order, followed by one CHANGELOG commit.

| Merged pull request | Fix |
|---|---|
| #260 | A `calc_on_order_fills` recalculation after a fill no longer rolls back the state of a deferred `request.security` evaluation (a silent wrong value, for example after a late margin-call liquidation). |
| #259 | A persistent (`var`) UDT, array, map or matrix whose initializer reads an earlier computed value is initialized once, in statement order (Fixes #226). A `varip` declaration with the same dependency still reads the default; that gap is tracked in #258. |
| #254 | `request.currency_rate` raises its error only where the call is evaluated, not where it merely appears in a branch that never runs (Fixes #228). |
| #255 | `var color` state inside a `request.security` helper, explicit or inferred, with persistence, transparency and `na` (Part of #229). The refusal for any other unsupported helper-local `var` type has the new code `PF-E3093`. |
| #253 | A UDT field assignment as the body of a one-line function or method (Part of #230). |

The CHANGELOG commit puts all five entries in the planned 1.8.0 section, updates its summary and compatibility notes, and removes the separate planned-patch section one of the merged branches added.

## Scope and limits

- The five merge commits resolved no code conflict; the only textual conflicts were in `CHANGELOG.md`, replaced once by the last commit. The name-status against `main` is the union of the five pull requests' own files (28 files).
- Over the 555 corpus and fixture scripts the emitted C++ differs from `main` in 72 scripts, all from the #260 change and only inside `restore_script_state()`; the other 483 are byte-identical. The other four fixes change no script of the 555; their tests cover their shapes.
- The user-facing text of two diagnostics (`PF-E1088`, `PF-W1088`) is reworded and one code is added (`PF-E3093`); the catalog schema is unchanged.
- The second half of #229 (the history of a derived helper argument) and the `varip` gap (#258) are not in this branch; they follow as separate pull requests.

## Tests

- Full suite on the head of this branch with the paired engine: 10725 passed, 221 skipped (private fixtures and empty parameter sets, the same 221 as `main`), 0 failed.
- A parity sweep over the whole population (7989 graded probes): PASS, no regression and no improvement (hard gate: 1009 evaluated, no moves; coverage complete).
- Every pull request above keeps the tests it was reviewed with; this branch adds no test of its own.

Fixes #226, Fixes #228. Part of #229, Part of #230.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@luisleo526

Copy link
Copy Markdown
Contributor Author

Superseded by #261, which carried this fix into the 1.8.0 release.

@luisleo526 luisleo526 closed this Oct 26, 2026
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