Repository navigation
Classify UDF-wrapped and var-scalar stable expressions as TA lengths - #54
Merged
Merged
Conversation
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
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The stable-TA-length guard hard-rejected two bar-invariant shapes:
ta.lowest(low[1], f_bars(mins))withf_bars(m) => math.max(1, math.round(m*60/tfSec))refused to transpile.varscalar with a stable per-run initializer (e.g.var tfSec = timeframe.in_seconds()) — allvarnames 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/varipscalars 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)
Unsupported TA constructor length, pass post-fix; 4 conservative guards (series-dynamic, reassigned-var body, recursive, multi-statement) pass on both sides.🤖 Generated with Claude Code