Repository navigation
Print true or false when a bool becomes text, and let a bool input be read through history - #319
Conversation
…all stored in a variable `str.tostring` of a built-in bool (the session and barstate members), of such a member kept in a variable, and of a variable assigned from a bool-returning ta call (`ta.crossover`, `ta.crossunder`, `ta.cross`, `ta.rising`, `ta.falling`) printed 0 or 1, where TradingView prints false or true. The type inference answered "double" for those expressions, so the text went through the numeric formatter. A bool built from a comparison, a literal, a `var bool`, a function over a comparison, a UDT field, an array element or a map value, and a ta call written directly inside `str.tostring`, were already typed bool and printed correctly. The inference now reads the builtin signature table: every builtin member typed bool is a bool, a ta call whose signature returns bool keeps that type in its variable and in its history, and a flag kept in a variable is a bool. The member table also lists `timeframe.isminutes` and `timeframe.isseconds`, which were already emitted. Numeric expressions keep their types, and the complete generated C++ of the numeric controls is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… changing any non-text generated C++ The first change typed built-in booleans and bool-returning ta calls as bool in the analyzer, which altered the C++ of conditions that never produce text. This change removes that storage override and adds a read-only text query that follows the registry declaration of the value: built-in members and calls, locals and parameters of user functions, callable returns, array elements built by `array.from`, request payloads, selections and history reads. `str.tostring`, `string()` and the `strategy.order` comment consume it; conditions, `and`/`or`, ternaries and numeric text keep their existing inference and C++. The registry now declares the missing bool results: the three array predicates, the ten matrix predicates, `ta.change` and `ta.valuewhen` over a bool source, a bool `request.security` payload, and `timeframe.change`. A stored `timeframe.isminutes` or `timeframe.isseconds` was typed as a string, so the C++ bool was stored as a character (a control byte or an empty string); the stored slot keeps its old type so non-text scripts emit the same C++, and only explicit bool text decodes it. The `strategy.order` call used to drop its comment argument; it now carries a comment that contains bool text. The tests derive their cases from the registry: every built-in bool member and bool call in seven shapes (direct, stored, argument, array element, history, function local, function return), with full-C++ controls for conditions and numeric text, and native rows for the stored timeframe flags and the order comment. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The adaptation of a bool argument for an untyped string parameter compares the parameter type with `PineType.STRING`, but `visit_call.py` did not import the name, so every script that called a user function failed with a NameError during translation. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ostring The analyzer does not visit the keyword arguments of `str.tostring`, so a variable read through `value = ok` had no declaration snapshot and the bool-text query fell back to float storage; the positional form was converted and the keyword form was not. The query now reads the authored declaration of such a read from the lexical binding pass when no snapshot exists, and keeps a visited read's snapshot authoritative. The tests derive the keyword shapes from the registry for stored, local and parameter bools. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…text natively A bool stored in a variable (for example `ti = timeframe.isminutes`) was typed as a string by the coarse inference, so converting it to text applied `.empty()` to a C++ `bool` and the script did not compile. The text boundary now reads the type the value is actually emitted with (parameter, series, local or hoisted global) and only treats a value as legacy bool-in-string storage when it really is a string. The shared string-history boundary gets the same bool-to-string carry. The NaN-safe cast stays; a sanitizer-flagged native test covers it. The bool-text tests now compile and run each case against the paired runtime and compare the printed text with a Pine `if` that prints a literal for the same condition, instead of reading generated source. The frozen-C++ controls that pin "no change for non-text scripts" are kept and labelled. Not fixed here: a bool read through input history (`input(true)[1]`, `input.bool(...)[1]`) fails to compile because of a separate defect in assigning a scalar input to a history series. Those two native rows are collected, not skipped, and stay red until that defect is fixed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ng guard, and give the call inventory a matrix receiver Two checks that main gained after this change was first written failed on it: - The determinism guard flags a `sorted()` over any mapping whose name is also used somewhere in the emitter for an id-keyed store. The bool-text query sorted its type bindings under the name `env`, which another module also uses for an id-keyed store. The sort covers (function, parameter) string pairs, so the order is deterministic; it now goes through a helper with its own name. - The security bar-dependency inventory builds one call per namespace from the registered signature tables. Declaring the matrix predicates (`matrix.is_*`) added a `matrix` namespace, and the generated call passed a number where a matrix is required. The inventory now declares a matrix for that namespace, as it already does for `map`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…te clone's parameter type, and let a bool input read through history reach its history writer
Three failures from the engine-paired run of the bool-text change:
- `strategy.order` with a comment that holds a bool conversion generated a call
with an eighth argument. The engine's `strategy_order` has seven parameters and
carries no comment, so a script that compiled before stopped compiling. The
call is again exactly what it was: the comment is not passed. The two native
tests that asserted a delivered order comment are removed; a frozen-C++ control
and a compile-and-run test now cover a bool text in an order comment.
- A function called once with a stored compatibility bool and once with a
numeric value is cloned per call site, and the clone takes a `double`. The
bridge that wraps a bool as a string for a legacy string parameter read the
base function's parameter type, so it wrapped the argument for the clone too
("cannot convert std::string to double"). It now reads the type of the
parameter the call actually binds to, and wraps only a `std::string` one.
- `input(true)[1]` and `input.bool(...)[1]` did not compile (a scalar assignment
to a `Series<bool>` member). An input read through history is no longer treated
as a static global input, so it reaches the existing per-bar history write.
Inputs that are not read through history keep their generated C++.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…hrough get_intrinsic The registry test that main gained with the keyword-binding change requires every record of every intrinsic registry to be what `get_intrinsic(namespace, name)` returns. The scalar predicate registries added for bool text (`array.includes`, `array.every`, `array.some`, the `matrix.is_*` predicates, and the bool overload of `request.security`) were not reachable, so that test failed. `get_intrinsic` now also looks in those registries. None of these records declares verified keyword names, and every keyword check returns early for such a record, so a call that reached `get_intrinsic` before behaves as it did. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 10 changed function(s) (3 sampled, not proven, 7 not verified).
Formal verification. PR-changed functions: 3/10 verified (0 proven, 3 may-equivalent, 0 distinguished) · 7 not verified (7 unsupported).
Not verified on this run: \_visit\_MemberAccess (unsupported), \_emit\_func\_def\_body (unsupported), \_emit\_on\_bar (unsupported), \_visit\_func\_call (unsupported), \_visit\_str\_call (unsupported), \_visit\_rhs\_value (unsupported), \_visit\_var\_decl (unsupported).
Graphify review — findings
Adds a BoolTextTypes query that decides whether an expression is a Pine bool for text conversion, so a bool stored in a legacy numeric slot no longer loses its type when it is turned into a string. It follows declarations, reassignments, user-function parameters and call-site arguments through exact bindings, and it never changes storage, parameter, history or condition types, so scripts that never produce text emit the same code. The query indexes every user function's body up front and joins argument types across call sites, which is where a function with no AST node, declared-versus-actual parameter types, and method-call default handling can go wrong. Also makes timeframe.* variable lookup in the analyzer consult VARIABLE_STORAGE_COMPATIBILITY before TIMEFRAME_VARIABLES, and adds ValueBindings.declaration_of to read an identifier's authored declaration without visiting the use.
Worth a look
- Crash on user function with node None —
pineforge_codegen/codegen/bool_text.py· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Call-site argument types override declared parameter types —
pineforge_codegen/codegen/bool_text.py:124· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 4992 functions depend on the 1038 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_assert_native_text()— 15 callers, 3 callees - new:
call_type()— 1 callers, 9 callees - new:
infer()— 0 callers, 7 callees - new:
test_v5_observers_keep_their_accepted_reference_cpp()— 0 callers, 7 callees
Verification — 4992 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 4146 function(s) in the blast radius were not formally verified this run
Cross-repo impact
Cross-repo impact
No cross-repo impact detected among the indexed peer repo(s).
1 peer repo(s) in this group are not yet indexed — cross-repo impact may be incomplete.
Cross-repo linkage is derived (SCIP-moniker matches are sound; name/contract matches are heuristic) and ADVISORY — it never affects this check's conclusion.
Docs that may be stale (advisory)
AGENTS.md§ REQUIRED before claiming any change is done (lines 13-56): references changed symbolscollectAGENTS.md§ Pipeline (lines 83-151): references changed symbolsidentifierAGENTS.md§ File map (lines 152-335): references changed symbolsparameterAGENTS.md§ How to run tests (lines 1157-1184): references changed symbolscollectCHANGELOG.md§ Compatibility and migration (lines 935-981): references changed symbols__init__CLAUDE.md§ REQUIRED before claiming any change is done (lines 13-56): references changed symbolscollectCLAUDE.md§ Pipeline (lines 83-151): references changed symbolsidentifierCLAUDE.md§ File map (lines 152-335): references changed symbolsparameterCLAUDE.md§ How to run tests (lines 1157-1184): references changed symbolscollectCONTRIBUTING.md§ Required checks (lines 74-141): references changed symbolscollect
…and 10 more.
Formal verification
Could not verify: Could not verify \_visit\_MemberAccess.
The verifier did not have enough to check \_visit\_MemberAccess, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_emit\_func\_def\_body.
The verifier did not have enough to check \_emit\_func\_def\_body, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_emit\_on\_bar.
The verifier did not have enough to check \_emit\_on\_bar, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
No difference found (not proven): No behavior difference found in \_infer\_type (not a proof).
The verifier ran both versions of \_infer\_type on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: the synth_receiver tier. A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_visit\_func\_call.
The verifier did not have enough to check \_visit\_func\_call, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_visit\_str\_call.
The verifier did not have enough to check \_visit\_str\_call, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_visit\_rhs\_value.
The verifier did not have enough to check \_visit\_rhs\_value, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_visit\_var\_decl.
The verifier did not have enough to check \_visit\_var\_decl, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
No difference found (not proven): No behavior difference found in get\_intrinsic (not a proof).
The verifier ran both versions of get\_intrinsic on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in is\_intrinsic\_variable (not a proof).
The verifier ran both versions of is\_intrinsic\_variable on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 4 grounded finding(s) anchored inline below.
| return symbol.type_spec or _PRIMITIVES.get(symbol.pine_type) | ||
| return None | ||
|
|
||
| def call_type(value, env): |
There was a problem hiding this comment.
call_type()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return TypeSpec.udt(namespace) | ||
| return None | ||
|
|
||
| def infer(value, env): |
There was a problem hiding this comment.
infer()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| ' log.info("Pine reference {0} false", bar_index)\n') | ||
|
|
||
|
|
||
| def _assert_native_text(tmp_path, native_env, body, expression, condition, |
There was a problem hiding this comment.
_assert_native_text()
15 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @pytest.mark.parametrize("value", ( | ||
| "session.isfirstbar_regular", "barstate.islast", | ||
| )) | ||
| def test_v5_observers_keep_their_accepted_reference_cpp(tmp_path, value): |
There was a problem hiding this comment.
test_v5_observers_keep_their_accepted_reference_cpp()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes the behaviour reported in #315.
str.tostringof a bool printed0or1(and, for some stored values, an empty text or a control byte); TradingView printstrueorfalse. The same text is now produced bystr.tostring(x),string(x)andstr.tostring(value = x)wherever the bool comes from.What changes
Bool text. A read-only query of the Pine value type (
codegen/bool_text.py) tells the text conversion that a value is a bool even when its storage is a legacy numeric or string slot. It is consulted only by the text conversion, never by a storage, parameter, history or condition decision, so a script that does not turn a bool into text gets the same generated C++ as before. It covers built-in bool variables (session.*,barstate.*,chart.*,timeframe.*,strategy.long/strategy.short), bool-returning calls (ta.crossover,ta.crossunder,ta.cross,ta.rising,ta.falling,str.contains/startswith/endswith,na,bool,map.contains,array.includes/every/some, the tenmatrix.is_*predicates,ta.changeandta.valuewhenover a bool, andrequest.securityof a bool), and every way such a value travels: used directly, stored in a variable, passed to a function, returned, held in an array or a history reference, and used inside a function.Registry. The signature registry now declares the bool results that were missing (
timeframe.isminutes,timeframe.isseconds, the array and matrix predicates, the bool overloads ofta.change,ta.valuewhenandrequest.security).get_intrinsicalso reaches the new records; none declares verified keyword names, so no keyword check changes.Two compile failures fixed on the way. A bool stored from
timeframe.isminutesortimeframe.issecondsand then converted to text did not produce true/false, and converted to text as a function argument or in history it did not compile. A function called with a stored bool of one kind and a numeric value elsewhere is cloned per call site; the bridge that carries a bool into a legacy string parameter now follows the type of the parameter the call actually binds to.A bool input read through history now compiles.
input(true)[1]andinput.bool(...)[1]failed to compile (a scalar assignment to a history series). An input that is read through history now takes the history path for every input type (int, float, bool, string, color, timeframe, symbol, also inside a function); avarinput and an input that is not read through history keep their generated C++.Known gaps (none is fixed here)
timeframe.isminutesandtimeframe.issecondsused as a bool VALUE (a function argument or a history read that drives a condition or a quantity) still do not compile, exactly as on main: only their text conversion is fixed here.strategy.order's comment is not carried by the engine yet: the engine's order call has no comment parameter, so a bool text in astrategy.ordercomment is unchanged (the script keeps compiling and running exactly as before).string(...)with keyword-only transport and namedstr.formatsubstitutions of a bool are not covered by the new rows.ifthat printstrueorfalsefor the same condition), not from newly measured TradingView tapes.var; nested functions, an input inside a loop or anif, and tuples are not measured.Verification
84120311: 20,490 passed, 235 skipped, 0 failed; compile corpus 312 passed; the parity gate step passes (268 fixtures).84120311(61 bool-producing registry members in the direct, stored, function-argument, array and history shapes, plus controls), compiled against the engine and run: 310 rows convert a bool to text and 2 rows are flag controls that are not text conversions (identical on both trees). Of the 310, on main 87 already print true/false, 210 print 0/1, 5 print another text (ta.changeof a bool), 2 print a control byte and 6 do not compile (87 + 210 + 5 + 2 + 6 = 310); on this head all 310 print true/false. A second set of 20 sources puts a bool text in astrategy.ordercomment: 19 compile and run on both trees with byte-equal generated C++, and the 20th (an input read through history) compiles only on this head.84120311, against the active baseline (engine84120311with codegen maind6a3107c): PASS, no improvement and no regression. 7,989 probes are comparable and every one is measured on both sides; the hard gate evaluates 1,009 with 0 moves and 0 regressions, the target gate evaluates 6,980 with 0 moves up or down, and no band gains or loses an entrant. The gate verdict is recorded for this engine and codegen pair and reads back as PASS, no-improve-no-regression; advancing the baseline after the merge is a separate step.input(true)and a titledinput.bool) put the value into an order quantity; TradingView accepts each, and on this head the exported quantities equal TradingView's on all 205 entries of each (the engine, with the inputs at their defaults, compared by entry time; the ten rows export three distinct tapes: quantity 2 on seven rows, 4 on two and 2.5 on one, so they are not ten independent datasets); on main none of the ten compiles. The text conversion oftimeframe.isminutes/issecondsthrough a function argument and through history is compared with TradingView twice: with the text compared to"true"and the result put into an order quantity (four small scripts, quantity equal on all 205 entries of each), and in the order comments (four small scripts: each flag through a function argument and through history, converted to text, in the entry and close comments): on this head the close-order text equals TradingView's on 204 of 205 trades in each script (trueforisminutes,falseforissecondson a 60-minute chart), and on the 205th trade the engine records no close comment and the exported TradingView text is blank; the entry comment was not read natively (the harness exposes only the close comment); on main none of the four compiles.🤖 Generated with Claude Code