Skip to content

codegen: five fixes for 1.8.0 (late restore, statement-order var initialization, request.currency_rate, helper-local var color, one-line field assignment) - #261

Merged
luisleo526 merged 24 commits into
mainfrom
fix/codegen-180-integration
Oct 26, 2026
Merged

luisleo526 merged 24 commits into
mainfrom
fix/codegen-180-integration

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

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

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

luisleo526 and others added 24 commits October 26, 2026 11:36
Red-first regression tests: the reporter's script and the never-taken
trade branch must translate, an evaluated call must stop the run like
runtime.error, a skipped call must run like a literal replacement, and a
malformed call must still fail signature validation.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ted (#228)

PineForge has no currency-conversion data, so request.currency_rate is no
longer refused at analysis time. It translates to an expression that
evaluates its arguments, stops the run with pine_runtime_error and has the
float type, so a script whose call sits in a branch that never runs
translates and runs, and an evaluated call stops the run like
runtime.error.

The call binds its from/to/ignore_invalid_currency signature through the
existing builtin argument binder; the shared signature diagnostics name
the actual callee (scripts/gen_diagnostics_catalog.py; the generated catalog
follows in the next commit).

Fixes #228

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… errors (#228)

The short user_message of PF-E1088 / PF-W1088 now reads "The arguments to
{name} do not match its Pine signature." as scripts/gen_diagnostics_catalog.py
writes it, because request.currency_rate shares the nz / fixnan argument
binder.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…or half)

Red-first tests: the reporter's script must translate, explicit and
inferred color declarations are typed from the Pine type, the requested
value must match a helper without var, a color set on one requested bar
must persist when the next bar does not assign it, na colors must match
chart state, and epoch-width int, varip and collection state stay refused.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…color half)

A helper-local var of Pine type color now keeps its state in the helper's
double series: the packed 32-bit ARGB value is exactly representable, na is
preserved, and reads cast back to the ordinary color integer type. The type is
taken from the declaration (type hint, analyzed type, or color-valued
initializer), never from a name or a literal. The epoch-width int64 refusal
and the collection / UDT / drawing refusals are unchanged; their message for
non-int64 types now also names color (new code PF-E3093; PF-E3015 stays
pinned).

Part of #229

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… part 1)

Red-first tests: the reporter's one-line `set(State s) => s.value := 1`, the
method form, a compound operator and comma-joined statements must translate
to exactly the C++ of the same body written on an indented line; the value
the script plots is 1 on every bar; a plain expression body stays one
ExprStmt with is_single_expr; a plain-name assignment keeps its parser error.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…n or method body (#230, part 1)

`set(State s) => s.value := 1` stopped at the colon-equals token because the
one-line body of a function or method was read as a single expression. The
body now takes the same expression-or-assignment shape as a switch arm's
line, restricted to field and element targets (and their compound operators,
comma-joined statements included). An assignment body is a real statement, so
it is represented as an ordinary block (is_single_expr false) and translates
exactly like the same body on its own indented line. A plain expression body
keeps its one-ExprStmt form, and a plain-name assignment keeps its parser
error.

Part of #230

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…keyword spelling from a test (#228)

The first lab run (rj-20261026t033717-8ad6d7) failed two tests of this branch:

- tests/test_diagnostic_notes.py requires the PF-W1088 user_message to differ
  from PF-E1088's and to say that a switch arm only warns; the catalog
  generator edit had given both the same sentence. PF-W1088 now keeps the
  switch-arm sentence after the callee-aware first sentence, in the generator
  and in the catalog.
- the all-keyword call spelled `to = 'JPY'`, but `to` is a keyword token of the
  lexer and fails to parse as an argument name (ParseError: Unexpected token
  TO). That is a front-end limit independent of this change; the test now
  spells the optional argument by name after two positional ones.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…F-E3093; AGENTS.md lists colour (#229, var color half)

The boundary test (epoch-width int64, varip, collection stay refused) no longer
carries the colour positive control, which is its own test, and its collection
case asserts the new code PF-E3093 instead of matching the old sentence by
prefix. AGENTS.md's helper `var` state row names colour (packed exact 32-bit
ARGB in the double series, na as NaN, read back as int64_t).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The stop is the repo's own request_stop(request_stop_marker(node)) shim, the
one request.financial / earnings / dividends / splits use, so a new engine
reports the same coded no-data failure for the currency request (function
request.currency_rate, the spelled call and its line) and an older engine
keeps the original pine_runtime_error with the same English text. Arguments
are still evaluated first and a skipped branch evaluates nothing.

The tests derive the expected stop from a transpiled sibling no-data request
instead of a runtime.error twin. AGENTS.md says request.currency_rate is not a
hard reject: it translates and stops where it is evaluated.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…jected shapes; changelog (#230, part 1)

A history subscript is not an assignable target in Pine, so the one-line
function or method body takes only a field (member access) target; an
assignment operator after any other target stays unconsumed and keeps the old
parser error. Two rows pin it: a plain-name second statement after a field
assignment (`s.value := 1, x := 2`) and a subscript target (`s[1] := 2`), both
rejected with the COLON_EQUALS parser diagnostic. CHANGELOG gets the entry.

Part of #230

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
)

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]>
…est.security evaluation

A lab probe built only from shipped features: calc_on_order_fills, two
same-symbol request.security calls (30m lookahead off, 5m lookahead on) whose
helper keeps a var counter, and a late margin-call liquidation after the
script walk. The requested counters are compared with a pure-Python count of
completed requested buckets on every bar, and with a control run without the
margin call. It asserts that the stressed run really executed the margin fill
and the restore. It fails on the current tree: the counter reads 11 where the
feed gives 13.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ss a late restore

tests/test_security_coof_restore_members.py transpiles scripts with a
finer lookahead request (numeric and string helper state, TA binding
variants, OHLC and TA histories with the bar-index clock, mutable globals,
expression and session histories) and asserts on the emitted rollback hook:
requested-progression members are copied but not restored, result cells and
chart members are restored, a chart-fallback payload keeps its members in the
restore, and tuple and lower-timeframe result cells still restore. Every
expectation is derived from the emitted code of the same script.

tests/test_e2e_security_coof_restore_history_reads.py runs the chart reading
offset history of a requested series across the late margin restore and
compares with a feed-derived oracle.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…-evaluation state

Under calc_on_order_fills the engine commits the script-state checkpoint
before it feeds the deferred requested evaluations of a chart bar. A later
fill recalculation (for example a late margin call) restored that older
checkpoint, so the generated state of the request.security context (helper
series and their var seeds, requested TA objects, OHLC and expression
histories, mutable copies, bar-index clocks) lost the deferred writes while
the engine's requested aggregators kept advancing: a silent wrong value.

The generated restore_script_state() now skips exactly those members, taken
from the same metadata that declares them, and still restores chart state and
the merged request results. Snapshot and commit are unchanged. A payload that
keeps user calls on the chart's bar (the chart-fallback warning) keeps its old
rollback, and so do the shared helper maps of a script that has one. No engine
or ABI change. Over the 555 corpus and fixture scripts the emitted C++ differs
from the parent revision in 72 of them, and only inside that one function.

Pins that hash the whole emission now hash it with the rollback hook body
removed, so they keep proving that everything else is unchanged:
- map source-order script 1: old (full emission) 8c6ee0d0..., new (hook body
  removed) 25368130..., reason: the hook body differs by design; the stripped
  emission of the parent revision gives the same 25368130...
- map source-order script 2: old acd85265..., new 074bda25..., same reason and
  the same equality with the parent revision.
The whole-C++ identity test against the parent compares with the hook body
removed on both sides (shared helper without_restore_script_state).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the 1.8.0 release notes. The release note for this fix is a
separate 1.8.1 (planned) section above the planned-1.8.0 section.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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]>
…ted-evaluation state

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The CHANGELOG entry of this change is carried by the combined 1.8.0 entry
written in the last commit of this branch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The CHANGELOG entry of this change is carried by the combined 1.8.0 entry
written in the last commit of this branch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The CHANGELOG entry of this change is carried by the combined 1.8.0 entry
written in the last commit of this branch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…r method

The CHANGELOG entry of this change is carried by the combined 1.8.0 entry
written in the last commit of this branch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The planned-1.8.0 section now carries the five entries (late-restore rollback,
statement-order initialization of persistent declarations, request.currency_rate
evaluated only where it runs, helper-local var color, one-line field
assignment), updates its summary and its emitted-C++ and diagnostics notes, and
the separate planned-patch section added by one of the merged branches is
removed. The compatibility bullet scopes the restore-function figure to the
late-restore change and names both reworded diagnostics (PF-E1088, PF-W1088).
A test comment no longer names an internal system.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 26, 2026
… bound to a derived expression (#262)

## What

`request.security` now accepts a helper that reads the history of a parameter bound to a derived expression, as TradingView does. The reporter's script from #229 translates:

```pine
f(x) => x[2] - x
a = ta.sma(close, 3)
b = f(a)
c = request.security(syminfo.tickerid, '60', b)
```

Before, the transpiler refused it ("helper-parameter history currently requires a direct OHLC/time bar-series binding"). The helper's `x[n]` read the chart's bars; it now reads the argument's own values on the requested context's bars.

This is the second half of #229. The first half (`var color` helper state) is in #261.

## How it works

- Each call site that needs the history keeps its own ring of argument values, sampled on the requested context's bars, so `x[n]` returns the value the argument had `n` evaluations of that call ago.
- Admission is by the position of the call. Direct OHLC and time bindings, builtin forms and method envelopes keep their existing lowering.
- The ring is requested-evaluation state. The restore after a `calc_on_order_fills` fill (#260, in #261) leaves it alone, like the rest of the requested progression state.
- The history of a global that is fully overwritten before the request statement replays its last unconditional write.

## Refusals

Shapes that cannot be sampled are refused with located errors, `PF-E3094` through `PF-E3101`: a derived history offset that is not a non-negative constant integer, a ring capacity that is not representable, a call that is not unconditional, a helper that depends on other helpers' state, history composed with helper-result history, a mutable global that cannot be replayed, and a parameter whose type or width has no lossless history. The code numbers follow `PF-E3093` from #261; messages are unchanged by the numbering.

## Scope and limits

- Over the 555 corpus and fixture scripts the emitted C++ is identical to main: none of them contains a derived helper-argument history, so the change reaches only scripts of the shape above (the tests cover them).
- One diagnostic code range is added (`PF-E3094` to `PF-E3101`); the catalog schema is unchanged. The CHANGELOG entry is in the 1.8.0 section.

## Tests

- Full suite with the paired engine: 10837 passed, 221 skipped (private fixtures and empty parameter sets, the same 221 as main), 0 failed. The branch adds the spec test of the reporter's script (red on main), a compiled-and-run witness of the sampled values, a refusal test with one row per shape, and a witness that the ring survives a late fill recalculation.

Fixes #229

🤖 Generated with [Claude Code](https://claude.com/claude-code)
luisleo526 added a commit that referenced this pull request Oct 27, 2026
…s value at declaration time (#263)

## What

A `varip` aggregate (UDT, array, map, matrix) whose initializer reads a value computed by an earlier statement now initializes at its declaration, in statement order, like the `var` form fixed in #226 (merged in #261). Before, it was initialized in the first-bar preamble, before that statement, and read the zero default.

```pine
//@Version=6
strategy('varip gap', initial_capital = 100000)
type State
    float value
float base = strategy.initial_capital / 100
varip state = State.new(value=base)
observed = state.value
```

`observed` was `0`; it is now `1000`, the same as the inline form `varip state = State.new(value=strategy.initial_capital / 100)` and the `var` form.

## How

The `varip` declaration takes the same declaration-site route as `var` when its initializer depends on an earlier statement (a two-line change in the statement-order check). Independent initializers and callable-scoped declarations keep their routes and their emission. `varip` storage and its once flag stay outside the `calc_on_order_fills` rollback checkpoint, as before: a value set on the first bar survives a fill recalculation and the initializer runs once.

## Tests

- Emission order of the dependency and the declaration, the compiled run against the inline and `var` controls for each aggregate kind (UDT, array, map, matrix, UDT built by a helper), the issue's own script in the static and dynamic builds, and that the first value is kept when the dependency changes later. All of them fail on `main` (the value is `0` against `1000`, or the declaration comes first).
- A guard that independent `varip` initializers keep their preamble emission byte for byte.
- With a native engine: a `calc_on_order_fills` strategy with a dependent `varip` aggregate keeps its first value across fill recalculations (it reads `0` on `main`), and a static test that the `varip` storage and its once flag stay out of the rollback checkpoint and the restore.
- The test that pinned the old behaviour as a known gap is removed; its shapes are covered by the new tests.
- Full suite with the paired engine: 10881 passed, 221 skipped (private fixtures and empty parameter sets, the same 221 as `main`), 0 failed.
- Over the 555 corpus and fixture scripts the emitted C++ is identical to `main`: none of them has a dependent `varip` aggregate initializer.

The CHANGELOG entry of #226 now says the `varip` form takes the same route.

Fixes #258

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant