Skip to content

Inline input.*() as a TA length behaves like its bound spelling (fixes #132) - #133

Merged
luisleo526 merged 3 commits into
mainfrom
c2/inline-input-ta-length
Sep 23, 2026
Merged

luisleo526 merged 3 commits into
mainfrom
c2/inline-input-ta-length

Conversation

@luisleo526

@luisleo526 luisleo526 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #132.

What

Since v0.8.1 an inline input.*() call used as a TA constructor length (ta.ema(close, input.int(9, "fast"))) was refused with "neither a compile-time constant nor derived from an input", while binding the input to a variable first worked. Inline inputs are now one leaf of a TA length, exactly like their bound spelling, for every TA constructor:

  • pine_spelling.py (new) keeps an inline input call whole inside the constructor-arg strings; the analyzer spells input calls with their keyword arguments and escaped strings.
  • The codegen admits an inline call when its bound form v = <call> would be input-backed (not a source input, constant default), hides the call's argument text from the identifier scans, and uses the default for the placeholder and precalculation sizes; the runtime reset reads the live input by its title.
  • The input manifest now lists every input call at global scope, titled with the key the C++ reads.

Root cause: 6545234 ("resolve input-derived TA lengths", #29, first in v0.8.1) added the guard; the identifier scan read the inline call's own text (input, int, the title) as unknown names. v0.7.5–v0.8.0 "accepted" the pattern only by sizing both EMAs at 1 with no runtime reset — the issue's strategy could never trade on those versions either. 8390364 (v0.9.0) later let a bare input(15) through but still refused a titled call, and keyword arguments were dropped silently.

Evidence

  • End to end (tests/test_e2e_inline_input_ta_length.py, 79 cases): transpile through the app's glue (gate/glue.py transpile_json) → compile against the engine (main fc7aad62) → run on the ETH 15m feed (222,295 bars). Every TA constructor with a length (sma … vwap bands, 52 families), keyword/positional/every-parameter forms, arithmetic and math.* over inputs, request.security, user functions, loops: trades sha256-identical to the bound spelling at defaults and under an input override, the override changes the trades, and the input manifests are identical. Before the fix: 74 failed / 5 passed (68 refused with the issue's diagnostic, 5 silently miscompiled). After: 79 passed.
  • Full suite with the engine env: 2937 passed, 2 skipped (the two documented skips), 0 failed.
  • Corpus: re-transpiling all 312 corpus probes (gitlink 9182e3d) gives identical=312 changed=0 — the C++ is byte-identical; only two probes' input manifests gain entries their C++ already read by title.
  • Campaign sweep on this head with engine main fc7aad62: exp-r5-c2-inline-input-20260923, 72/72 cases, 0 failed; candidate 39c3092a…; offline band1 +0/−0 band2 +0/−0 (score 0, hard regressions 0, coverage lost 0); pr_gate pineforge-pr-gate-8m7nx → neutral, accepted at net 0.

Found on the way (not fixed here)

  • ta.change(src, n) ignores n (the length only bounds the history; the emitted compute(src) always looks back 1 bar) — an existing silent miscompile; no corpus probe uses n > 1. Tracked for a follow-up lane.
  • An input title containing " or \ produces invalid C++ (bound and inline alike).
  • var n = input.int(9) without a title: the member reads key "n" but the TA reset reads "", so an override does not resize the indicator.

A release that carries this fix is the owner's call.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LifAW9e5J6gFx7gatuxETE

luisleo526 and others added 3 commits September 23, 2026 17:01
… end to end (C2 W1)

Issue #132: since 0.8.1 `ta.ema(close, input.int(9, "fast"))` is refused
("Unsupported TA constructor length ... neither a compile-time constant nor
derived from an input") while `len = input.int(9, "fast")` + `ta.ema(close,
len)` is accepted.

tests/test_e2e_inline_input_ta_length.py drives each case through the
interfaces a client uses: pineforge-app's transpile_json glue (gate/glue.py)
in a subprocess, the emitted TU built as a strategy shared library against
the engine runtime (the corpus/CMakeLists.txt recipe), and the engine's
scripts/run_strategy.py C-ABI harness over the corpus ETH-USDT 1m feed,
resampled to 15m by the engine's own resampler into a temp dir. Every case
compares the inline spelling with its variable-bound twin: byte-identical
engine_trades.csv at the defaults and under an inputs.json override, which
must move the trades, and identical input manifests.

74 cases: one per TA constructor with constructor arguments (a guard test
ties the list to TA_CLASS_MAP - TA_NO_CTOR), the issue strategy verbatim,
every argument form of the inline call (named/reordered defval and title,
minval..display, options, plain input(), punctuation in the title, an empty
title), arithmetic and math.* over inline inputs, a derived binding, and
TA sites reached through request.security, user functions (keyword argument,
security inside the helper, a title equal to the parameter name) and a loop.
Four more pin that a length which is otherwise a series stays refused with
the same diagnostic when an inline input is one of its leaves.

At this commit all 74 inline-vs-bound cases fail: 68 are refused with the
issue's diagnostic, the `var` twin of one is refused too, and 5 transpile but
wrong -- `input.int(defval=9, title="fast")` emits
`ta::EMA(get_input_int("", 0))`, `input.int(9, title="fast")` reads key "",
a title spelling a derived name ("m", with `m = 2 * 3`) reads key
"(2 * 3)", and none of the inline inputs reaches the manifest. The refusal
pins and the registry guard pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ound spelling (C2 W2)

Root cause. 6545234 (#29, first shipped in 0.8.1) added the TA-constructor
guard in _emit_constructor: a ctor arg must fold to a literal or expand,
through _runtime_ctor_arg_for_reset, to a runtime expression whose every
identifier token is input-backed, a known constant or structural. Ctor args
travel as Pine source spellings, and the token scan read an inline call's
own argument text -- `input`, `int`, the title `fast` -- as unknown names,
so the length was refused. Before that commit 0.7.5-0.8.0 "accepted" it by
sizing both EMAs at 1 with no runtime reset (ta::EMA(1): fast == slow ==
close, a strategy that can never trade). 8390364 later let the bare word
`input` through, which admitted argument-free spellings such as input(15)
but not a title; and the analyzer's _expr_to_str dropped keyword arguments,
so `input.int(defval=9, title="fast")` reached codegen as `input.int()` and
silently became ta::EMA(get_input_int("", 0)).

Fix:
  * pine_spelling.py: string-literal-aware helpers -- the Pine literal of a
    string, identifier substitution that leaves string contents alone, the
    extent of each inline input call, and a full spelling of an input call.
  * analyzer _expr_to_str spells an input call with its keyword arguments,
    escapes string literals and spells list literals (options=[...]).
  * codegen: an inline input call is admitted exactly when its bound
    spelling `v = <call>` would make v input-backed (_is_stable_inline_input:
    not a source input, constant defval; _input_default_value is now the one
    rule for both). The reset gate masks each admitted call to the bare word
    `input`; the placeholder, precalc and prepare_script_run sizes read its
    defval (_fold_inline_input_defaults), as a bound name does through
    _known_vars; derived-length tracking (`n = input.int(9, "fast") * 2`,
    `var n = input.int(9, "fast")`) spells it in full and counts it as an
    input reference; _expand_derived no longer rewrites a title that happens
    to spell a derived name.
  * The input manifest lists every global-scope input call, inline ones too,
    one entry per call site in source order, titled with the key the C++
    reads the input by.

The inline spelling now emits the bound spelling's C++ except for the bound
twin's own member and its source-line call-site ids. A length that is
otherwise a series (a bar field, bar_index, a source input) is still refused
with the same diagnostic.

Evidence: tests/test_e2e_inline_input_ta_length.py 79 passed (was 74 failed);
all 312 corpus/validation generated.cpp re-transpile byte-identical; the
manifest of two corpus probes gains the inputs their C++ already reads
(analyzer-anvil-percent-costs-01: "Stop %", "Target %";
na-ledger-history-anchor-01: "Entry Deviation %").

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ounts (C2 W3)

Adds "Known codegen quirks" entry 10: an inline input.*() call is one leaf
of a TA length -- pine_spelling.py keeps it whole in the ctor-arg spelling,
the codegen masks its argument text out of every identifier scan, it is
admitted exactly when its bound spelling would be input-backed, a length
that is otherwise a series stays refused, and the manifest lists every
global-scope input call with the key the C++ reads it by. Lists
pine_spelling.py in the file map.

Re-measured on 2026-09-23 with the engine at engine-main fc7aad62 (corpus
gitlink 9182e3d), Eigen and a built runtime: 2937 passed, 2 skipped (the two
documented skips), 0 failed; without a compile environment 1952 passed,
674 skipped. The corpus sweep is unchanged at 314 passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@luisleo526
luisleo526 marked this pull request as ready for review September 23, 2026 09:31
@luisleo526
luisleo526 merged commit acf03a1 into main Sep 23, 2026
9 checks passed
luisleo526 added a commit that referenced this pull request Sep 23, 2026
…s the member's key (C3 W4)

var n = input.int(9) with no title: the member reads the input under "n" (a
var initializer lends its name to the untitled call; the manifest lists "n"),
but the TA runtime reset read it under "", so overriding n never resized
ta.ema(close, n). Since 097f499 (#54, which turned the shape from a refusal
into an accepted TA length), _collect_stable_var_scalars recorded the var's
initializer as a derived expression, and the reset re-lowered that spelling
as an inline input call with no enclosing name. #133 spells that call with
its title and keyword arguments, which fixed the titled form, but an
untitled call still has no name to key by.

A never-reassigned var bound straight to an input call holds the input's
value on every bar, exactly like n = input.int(9), so it is now registered
the same way (_collect_known_var): input-backed, read through its call node.
Every reader then keys it by _get_input_title(call, var_name=n), and the
constructor placeholder is the default (9) as for the plain binding instead
of 1. The titled var binding takes the same path; var n = input(9), refused
before, is admitted with it. An input whose default is not a constant keeps
the derived-expression path.

No corpus probe binds an input with var: the 312 re-transpile byte-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@luisleo526
luisleo526 deleted the c2/inline-input-ta-length branch October 5, 2026 02:39
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.

0.10.4: inline input.int() as a TA length is rejected (neither a compile-time constant nor derived from an input)

1 participant