Skip to content

Classify UDF-wrapped and var-scalar stable expressions as TA lengths - #54

Merged
luisleo526 merged 1 commit into
mainfrom
fix/stable-ta-length-udf-var
Jul 10, 2026
Merged

luisleo526 merged 1 commit into
mainfrom
fix/stable-ta-length-udf-var

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

What

The stable-TA-length guard hard-rejected two bar-invariant shapes:

  1. A TA constructor length computed through a pure single-expression user-defined function — any UDF call was classified as series-valued, so e.g. ta.lowest(low[1], f_bars(mins)) with f_bars(m) => math.max(1, math.round(m*60/tfSec)) refused to transpile.
  2. A never-reassigned var scalar with a stable per-run initializer (e.g. var tfSec = timeframe.in_seconds()) — all var names were treated as mutable state.

The stability classifier and the reset-expression renderer now inline single-ExprStmt-body UDFs structurally (parameter substitution over the arithmetic/call grammar; cycle guard + depth cap at the shared choke-point, so recursive UDFs are refused without hanging), and never-reassigned top-level var/varip scalars with stable initializers register as stable derived expressions (no use-site inlining). Genuinely series-dynamic lengths (ternary over an ATR-derived ratio) are still rejected with the same error — pinned by a guard test and an end-to-end true-positive sentinel.

Gates (author + independent fresh-context R7, both green)

  • 3 RED tests fail pre-fix with Unsupported TA constructor length, pass post-fix; 4 conservative guards (series-dynamic, reassigned-var body, recursive, multi-statement) pass on both sides.
  • Full pytest 1473 passed / 1 skipped; corpus compile sweep 254 passed.
  • End-to-end: the false-positive exemplar transpiles and runs (89.5% match, 100% price parity); the true-positive sentinel still errors on its series-dynamic length.
  • Private validation full sweep (412): transpile-error inventory 8→7 (exactly the unblocked exemplar), remaining errors byte-identical, 0 tier moves.

🤖 Generated with Claude Code

The stable-TA-length guard hard-rejected two bar-invariant shapes:
a TA constructor length computed through a pure single-expression
user-defined function (any UDF call was treated as series-valued), and
a never-reassigned var scalar whose initializer is a stable per-run
expression (all var names were treated as mutable state).

The stability classifier and the reset-expression renderer now inline
single-ExprStmt-body UDFs structurally (parameter substitution over the
arithmetic/call grammar; cycle guard and depth cap at the shared
choke-point, so recursive UDFs are refused without hanging), and a
first pass registers never-reassigned top-level var/varip scalars with
stable initializers as stable derived expressions without inlining
their use-sites. Genuinely series-dynamic lengths (e.g. a ternary over
an ATR-derived ratio) are still rejected with the same error.

Note: block-form single-expression UDFs parse with is_single_expr set
false; the inliner keys off the body being exactly one expression
statement, not that flag.

Co-Authored-By: Claude Fable 5 <[email protected]>
@luisleo526
luisleo526 merged commit 097f499 into main Jul 10, 2026
9 checks passed
@luisleo526
luisleo526 deleted the fix/stable-ta-length-udf-var branch July 10, 2026 19:38
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 added a commit that referenced this pull request Sep 23, 2026
…the member and the manifest (C4 W4)

TradingView: "If not specified, the variable name is used as the input's
title" (Pine v6 reference, input.int title). The manifest and the member
followed that rule, but several getters re-derived the key from a
re-spelled call or from whichever name they were handed, so an override
set the member and never reached the TA size or the security feed:

- tf = input.timeframe("60") as a request.security timeframe registered the
  evaluator with get_input_string("", ...) -- since 6545234 (#29), which
  renders the bound call with no enclosing name;
- len = input.int(9) with a stable `len := ...` reassignment: the TA reset
  read the folded `(cond ? 30 : input.int(9))` under the default's text "9"
  from 8390364 on, under "" since C2's spelling;
- var n = input.int(9) * 2 (or math.max(input.int(9), 2), or a float
  feeding ta.bb): the reset re-read the nested call under "" -- 097f499 (#54);
- a = input.int(9) then b = a: the reset read it under "b" -- since the
  initial public release 5b92828.

One key per input, from the call node itself:
pine_spelling.input_binding_names() maps each global-scope input call a
declaration names -- the call bound straight to `v = input.*()` and any call
in a var initializer, the manifest's rule -- to that name.
_get_input_title() consults it before the name its caller passes, so the
member, the TA reset, a security timeframe and an alias's read all agree
with the manifest; and a call re-spelled for a derived length (codegen
_arith_expr_to_str, analyzer _expr_to_str) carries the name as title=, since
the re-parsed node has lost its declaration. The manifest walker moves to
pine_spelling.global_input_calls() so both share one definition of "global
scope".

E2E (tests/test_e2e_untitled_input_keys.py): each untitled shape runs next
to the same strategy with an explicit title, at the defaults and under the
same override keyed by each one's manifest title; the trades must be
identical and the override must move them. Before (on d0ae5be): 7 failed --
security_timeframe(_alias), stable_reassignment, var_nested(_call,_float),
alias -- each "override left the trades unchanged"; the direct binding,
input.source, a security length and a ta.change length through an alias
passed. After: 11 passed (e.g. security_timeframe override {"tf": "240"}
5508 trades == the "TF"-titled twin).

Re-transpile: no C++ or manifest change in the 312 probes, the corpus or
the 101 closed strategies -- none uses these shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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