Repository navigation
Stop an endless counted loop as TradingView does, count float steps by TradingView's measured end rule, accept more keyword and constant forms, and continue a statement after an operator line - #317
Conversation
…ngView does A counted for loop whose end is affine in its own counter can be endless: for i = 0 to i never fails its test (the end grows with the counter). We emitted a loop that never ends, so the run hung. TradingView does not hang: it keeps the script only when the loop's result is never used, and otherwise stops the run on the first bar with "Loop takes too long to execute". Measured on TradingView (entry quantity and entry count of every probe; tape and source digests are in tests/fixtures/endless_for/tradingview.json): - An endless loop whose result reaches an effect (a plotted or traded value, directly or through other variables) stops the script on bar 0 with the error above: no trade is entered. - An endless loop whose result is never read is dropped: the strategy trades as if the loop were absent (a 205-entry tape, identical to the same script without the loop), including when the loop body has a conditional break and when the start is not zero. - A chain of variables that never reaches an effect is dead as a whole, and a loop inside a function that is never called does not run. What changes in generated code (chart context only): - A loop is classed endless only when its end is affine in its own counter with constant coefficients, its start is constant, its step is positive or absent, and its body has no reachable break or return (a dead loop is omitted whether or not it has a break). For a non-zero start both candidate first end reads must agree. - A loop classed endless whose result reaches an effect (a transitive reading of what the loop writes, where the conditions of control constructs that contain effects count and a call we cannot see into counts as an effect) emits the run stop "Error on bar N: Loop takes too long to execute (> 2000 ms)". The limit is a named constant (kLoopTimeLimitMs) emitted only when such a loop exists. - A loop classed endless whose result reaches no effect is omitted. - Loops in function bodies and in request.security helpers are untouched; every other loop emits the same C++ as before. Known gaps: only statically endless loops are handled; a loop that is merely slow, or whose end depends on data, still runs to its natural end (a runtime time limit is a separate change); the effect analysis is conservative, so a loop whose result only looks live is stopped. Tests: tests/test_for_endless_loops.py (classification and emission, including dead chains and a function that is never called) and tests/test_e2e_for_endless_loops.py (the generated C++ compiled and run; the stop text, the omitted loop's entry count). The one existing test whose loop this rule changes (for i = 0 to i, now endless) is given a terminating end (10 - i) so it still checks the counter shadowing an outer series. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd count inexact float steps as TradingView does (PF-E2079)
Two loop behaviours differed from TradingView for a counted for loop with a float step.
1. A float step against an int counter. TradingView refuses a v6 loop whose counter is an int and whose step is a float. We accepted it and narrowed the step to an int (`by 0.5` became `by 0`, which stopped the run at the first bar, or in earlier releases was replaced with 1), so the loop ran with a step the author never wrote. A v6 loop whose start has positive int evidence (an int literal, an int-typed declaration, array.size and the like) and whose step has positive float evidence is now refused at the step expression with PF-E2079 ("The step of a 'for' loop has type float, but counter 'i' has type int"; hint: use an int step or a float counter), in the chart and in the request.security helper, whatever the end's type: TradingView refuses `for i = 0 to 1.0 by 0.25` too. The decision uses the same single loop-counter query that chooses the counter's C++ type (pineforge_codegen/loop_types.py), so analysis and emission cannot disagree. A start whose type is unknown (an untyped function parameter, a start read from a value we cannot type) with a float step is not refused: TradingView runs those loops with a float counter and the float step when the argument is a float, and so do we. v5 library bodies keep their int counters and existing step handling. A step that is zero or negative still stops the run as before.
2. Inexact steps. TradingView counts `0.0 to 0.3 by 0.1` as four iterations, as an accumulation x += step that continues while x is within a small allowance of the end; plain accumulation gives three here. A float-counter loop now accumulates the counter and continues while `x <= end + kFloatStepTolerance` (the mirrored test for a descending step). kFloatStepTolerance = 1e-12 is a named constant in the units of the counter, emitted only when a float-counter loop exists. It is an absolute allowance: measurements at a very large step (1e6) and a very small step (1e-6) rule out an allowance relative to the step or to the end.
Measured on TradingView (iteration count of each probe; the export digests, source digests and counts are in tests/fixtures/for_counter_headers/tradingview_quantities.json): 0.0 to 1.0 by 0.1 counts eleven; 0.0 to 0.3 by 0.1 and 0.0 to 0.6 by 0.2 count four; a tenth step to seven tenths counts eight; a descending tenth step counts four; ends short of 0.3 by 1e-3, 1e-5 and 1e-10 count three, so those shortfalls are not absorbed; with a step of 1e6 an end short of the fourth counter value by 1 or by 1e-7 counts three; with a step of 1e3 an end short by 1e-10 counts three; with a step of 1e-6 an end short by 1e-18 counts four. An absolute allowance fits all 18 rows for any value from about 5.6e-17 to about 9.98e-11; 1e-12 is a choice inside that bracket and is not a TradingView constant.
Known gaps: TradingView decides an unknown (parameter) start by the call argument's type: the same helper called with an int argument and a float step is refused by TradingView and runs here (a call-site type is not visible to us, and refusing it would also refuse the valid float-argument call); a float counter passed to a user helper without a declared parameter type is truncated to an int by the shared type inference (TradingView passes the fractional value: accumulating twice(x) for x from 0.0 to 1.0 by 0.25 gives 5 there and 2 here), and array.from(x) with a float counter builds an int array; the last two are recorded in the tests as current values against the measured ones.
Tests: tests/test_for_float_step.py (the refusal and its position, the allowance constant emitted once for a double counter and never for an int counter, a text check that the allowance reads neither the step nor the end, in the chart and request.security routes); the loop-counter tests that pinned the run stop for an int start with a fractional step now pin the refusal (code, severity, message, hint, step location); the 17 float-step rows with their measured counts, run through the generated C++ in tests/test_e2e_for_counter_headers.py, including the rows that fail for an allowance of 1e-10 and of 1e-17.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Recognize primitive type keywords before equals in call arguments while preserving declaration grammar, argument binding and diagnostic locations. Compare named calls with positional emission and retain grammar controls. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ames math.pow names its second parameter exponent. The registry called it exp, so a keyword exponent was dropped and the call lowered to a one-argument std::pow that does not compile, while the spelling exp compiled and ran. Callables whose declared parameter names are their complete keyword set now carry keyword_names_verified. For such a record an undeclared keyword is refused at its name through the catalog (PF-E2027), in the support check and in the shared merge helper. Records without the flag keep their existing keyword handling byte for byte. math.pow is the first record to carry it. Tests cover the named, mixed and reversed forms against the positional call, the refusal and its span, a flag-driven generic family built at test time, registry coverage of the lookup, and the absence of evidence fields in the registry. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Prove typed constant carriers through the input default contract and emit their packed values before script initialization. Share the existing color encoding and preserve direct input lowering and metadata. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ly when its indent is not a multiple of four, at every block depth Measured on TradingView: multiples of four are refused (top level and inside bodies), non-multiples and parenthesised wraps are accepted. Replaces the 0/4/8 width list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e, as TradingView does; pin every context from the measured tapes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… leave the previous token pending as a line-end operator Found by the lab: the generalised continuation refusal fired on a line whose last lexeme (a non-ASCII character) emitted no token, because the line-end check read the last token of an earlier line. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tep check from the counter's storage In Pine, `/` always produces a float value, so a loop such as `for i = n / 2 to n / 2 + 2` visits 2.5, 3.5, 4.5 when n is 5. The loop-counter query typed the start by joining its operands, so `n / 2` with int operands was an int and the generated loop declared an int counter and truncated the start: the loop visited 2, 3, 4. TradingView keeps the fraction. TradingView's compile check is a separate matter: it still treats a division of two ints as an int when it decides whether a float step is compatible with the counter (`for i = 4 / 2 to 6 by 0.5` is refused for a float step, in the direct, named and helper forms). So the counter query now answers two questions. The check type is unchanged, so the float-step refusal keeps refusing exactly what TradingView refuses. A new value type treats a division as a float value and selects the float counter, so the fraction of a division start is kept. Both answers come from one recursion, the shared type inference is untouched, and a division only in the loop end does not change the counter. Tests cover the refused float-step forms, the double counter for division starts with the default and an explicit int step, native runs whose sums are 10.5 for the input form, 10.5 and 12 for the series form and 10.5 for a constant start, and the unchanged int controls. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ir three loop-type rows A division that keeps a fraction selects the float counter, but a constant division with an exact integer result (`4 / 2`, `8 / 4`, `-4 / 2`, or a name bound once to such a constant) is an integer value, so its loop keeps the int counter and its emitted C++ does not change. The proof uses only integer literals, unary plus and minus, parentheses, `+ - *`, exact `/` and `%`, and names bound once to such constants; a division by zero, an input, a series, a call, a float literal or a name that is written anywhere else is not proved and keeps the fraction. Two rows built a request.security helper loop that holds a user function call, which is not a legal source; they now use legal forms. The comparison with the float twin no longer relies on a text substitution over the whole C++: it compares the complete output against the frozen reference for each form. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ingView-tape rows and carrier rows for the fractional start TradingView accepts `ta.highest(bar_index, 3)` and `ta.lowest(bar_index, 3)` as the start of a loop with a float step (their result is a float), but the check typed the call from its source argument, so an int source made it refuse a valid script. highest and lowest now give the float type for every overload. `ta.change` and `ta.valuewhen` stay typed from their source, which TradingView refuses on an int source. `ta.median`, `ta.mode`, `ta.max`, `ta.min`, `ta.range` and `ta.percentile_nearest_rank` have no TradingView receipt yet and are unknown for an int source until one is recorded. The measured quantity of each fractional-start fixture is now compared with the recorded TradingView entry quantity, exactly, from the measurement fixture: the five division forms, the fractional input and the fractional series. Eight carrier forms (reassignment, ternary, if and switch arms, helper return, helper parameter, tuple element, array element) and six integer controls are pinned. Two carriers still drop the fraction (a reassigned name and a helper parameter fed the division): they are named gap tests that assert the current value and flip when the gap closes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ta calls to the receipts TradingView returned TradingView accepts a descending bound `n / 2 to 0`, a fractional array index (truncated) and `str.tostring` of the fractional counter. Three fixtures copied from those scripts are compared with the recorded TradingView entry counts and quantities (205 entries of 4.5, 205 of 120, 615 of 1) and must translate with a double counter. The string fixture also pins the entry ids; our formatter prints 15 places where TradingView prints the 18 asked for, so that row is a named gap test that asserts the ids we produce today and flips when the formatter is fixed. The ta calls in the loop-start check follow TradingView's compile verdicts for an int source with a float step: `ta.median`, `ta.mode`, `ta.range` and `ta.change` give an int and the float step is refused; `ta.max`, `ta.min`, `ta.percentile_nearest_rank`, `ta.highest` and `ta.lowest` give a float and the loop translates with the double counter. The temporary unknown for the six calls without a receipt is removed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
TradingView types ta.max and ta.min as float for an int source, so a loop that starts at one of them gets a double counter. Two baseline rows that listed them as int-counter sources, and the three tests that read that list, now expect the double counter and a changed emitted loop. The int rows for ta.median and ta.mode are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… a tolerance of ours A counted loop with a float step continued while the counter was within 1e-12 of the end, a constant chosen inside a bracket rather than measured. TradingView's own rule was measured on 72 pre-registered loops (the counter is repeated double addition; the end comparison is the double difference): a counter whose difference to the end is below 1e-10 is still within the loop, counted up to a 9.999e-11 excess and not counted from 1.0e-10, measured at the 0.3 scale and at counter magnitudes of about 1, 1e3, 1e6 and 1e9. The loop now continues while `counter - end < 1e-10` ascending and `end - counter < 1e-10` descending, with the constant named for what it is. No magnitude-independent constant is claimed. The tests read the recorded TradingView counts and run each loop natively, asserting that every entry quantity equals the count TradingView produced; the old structural assertions that spelled the previous constant are removed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 29 changed function(s) (3 sampled, not proven, 26 not verified).
Formal verification. PR-changed functions: 3/29 verified (0 proven, 3 may-equivalent, 0 distinguished) · 26 not verified (3 vacuous, 19 unsupported, 4 over cap).
Not verified on this run: \_visit\_ForStmt (unsupported), \_\_init\_\_ (unsupported), \_generate\_cpp (unsupported), \_emit\_includes (unsupported), \_emit\_on\_bar (unsupported), \_input\_default\_expression (unsupported), \_literal\_color\_value (unsupported), \_build\_security\_math\_call (unsupported), \_emit\_security\_linear\_helper\_call\_scoped (unsupported), \_color\_arg\_nodes (unsupported), and 12 more; 4 changed function(s) beyond the cap and 0 skipped when the time budget ran out.
Graphify review — findings
Adds TradingView-matching counted-loop behaviour: float counters stop by the measured 1e-10 end-excess rule (kTvFloatStepEnd), a v6 float step against an int-evidenced start is refused with PF-E2079, and affine loops proved endless stop with the loop-time error once their result reaches an effect. Endless loops in functions, methods and request.security still never end. Registry builtins with verified keyword names now validate their full keyword set before lowering and at the keyword merge (unknown names report PF-E2027), and the parser accepts int/float/bool/string followed by = as a call-argument label. Resolves explicit const color input defaults to a packed value before on_bar, using color_literal_to_int64 to turn the literal into the integer that goes into the emitted C++ getter and settings descriptor. Reassigned, persistent and inferred carriers stay PF-E1006 compile gaps.
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 — 5859 functions depend on the 2605 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
Parser— 180 callers, 42 callees - worse:
Lexer— 208 callers, 6 callees - worse:
Analyzer— 88 callers, 14 callees - worse:
_CounterQuery— 6 callers, 22 callees - new:
_analyzed_counter_start()— 6 callers, 9 callees - new:
plan_endless_for_loops()— 6 callers, 8 callees - worse:
LoopTypeSources— 8 callers, 6 callees - new:
intrinsic_keyword_diagnostics()— 3 callers, 4 callees - …and 9 more — each is listed as a finding
Verification — 5859 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: 5396 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)
README.md§ Releases and this README (lines 42-69): references changed symbolssourceREADME.md§ Upgrading from 0.10.4 (lines 98-116): references changed symbolssourceREADME.md§ What this owns (lines 117-135): references changed symbolssourceREADME.md§ Install (lines 136-152): references changed symbolssourceREADME.md§ Quick start (lines 153-179): references changed symbolssourceREADME.md§ The transpile() function (lines 182-208): references changed symbolssourceREADME.md§ The transpile_full() function (lines 209-267): references changed symbolssourceREADME.md§ Diagnostic codes (lines 268-310): references changed symbolssourceREADME.md§ List the other symbols a script requests (lines 311-356): references changed symbolsexpressionREADME.md§ Handle unsupported features (lines 368-404): references changed symbolsruns
…and 10 more.
Formal verification
Could not verify: Could not verify \_visit\_ForStmt.
The verifier did not have enough to check \_visit\_ForStmt, 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 \_\_init\_\_.
The verifier did not have enough to check \_\_init\_\_, 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 \_generate\_cpp.
The verifier did not have enough to check \_generate\_cpp, 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\_includes.
The verifier did not have enough to check \_emit\_includes, 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)
Could not verify: Could not verify \_input\_default\_expression.
The verifier did not have enough to check \_input\_default\_expression, 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 \_literal\_color\_value.
The verifier did not have enough to check \_literal\_color\_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 \_build\_security\_math\_call.
The verifier did not have enough to check \_build\_security\_math\_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 \_emit\_security\_linear\_helper\_call\_scoped.
The verifier did not have enough to check \_emit\_security\_linear\_helper\_call\_scoped, 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 \_merge\_kwargs (not a proof).
The verifier ran both versions of \_merge\_kwargs 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.
Could not verify: Could not verify \_color\_arg\_nodes.
The verifier did not have enough to check \_color\_arg\_nodes, 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 \_resolve\_func\_args.
The verifier did not have enough to check \_resolve\_func\_args, 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\_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\_math\_call.
The verifier did not have enough to check \_visit\_math\_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 \_emit\_counter\_bound\_loop.
The verifier did not have enough to check \_emit\_counter\_bound\_loop, 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\_for.
The verifier did not have enough to check \_visit\_for, 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 \_tokenize\_line (not a proof).
The verifier ran both versions of \_tokenize\_line 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 \_\_init\_\_.
The verifier did not have enough to check \_\_init\_\_, 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 call.
The verifier did not have enough to check 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 counter\_spec.
The verifier did not have enough to check counter\_spec, 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 for\_counter\_type (not a proof).
The verifier ran both versions of for\_counter\_type 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.
Could not verify: Could not verify \_parse\_call\_args.
The verifier did not have enough to check \_parse\_call\_args, 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: not verifiable: all 3 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_parse\_for\_stmt.
The verifier did not have enough to check \_parse\_for\_stmt, 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: not verifiable: all 3 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_func.
The verifier did not have enough to check \_func, 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: not verified: a runtime divergence was observed, but the two versions' package snapshots loaded differently in the sandbox (old: {'fallback': {'pineforge_codegen': 'ImportError', 'pineforge_codegen.codegen': 'ImportError', 'pineforge_codegen.codegen.base': 'ModuleNotFoundError', 'pineforge_codegen.codegen.helpers': 'ModuleNotFoundError'}, 'missing': ['pineforge_codegen.codegen.ascii', 'pineforge_codegen.codegen.collection_history', 'pineforge_codegen.codegen.constant_fold', 'pineforge_codegen.codegen.emit_top', 'pineforge_codegen.codegen.host_members', 'pineforge_codegen.codegen.input', 'pineforge_codegen.codegen.security', 'pineforge_codegen.codegen.ta', 'pineforge_codegen.codegen.types', 'pineforge_codegen.codegen.visit_call', 'pineforge_codegen.codegen.visit_expr', 'pineforge_codegen.codegen.visit_stmt']}, new: {'fallback': {'pineforge_codegen': 'ImportError', 'pineforge_codegen.codegen': 'ImportError', 'pineforge_cod…
· 12 grounded finding(s) anchored inline below; 5 more finding(s) on lines outside this diff (see the check run).
| return None | ||
|
|
||
|
|
||
| def literal_color_value(node) -> int | None: |
There was a problem hiding this comment.
literal_color_value()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return True | ||
|
|
||
|
|
||
| def plan_endless_for_loops(program: Program) -> dict[int, str]: |
There was a problem hiding this comment.
plan_endless_for_loops()
fans out to 8 callees (efferent coupling); 6 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| from .signatures import IntrinsicFunc | ||
|
|
||
|
|
||
| def intrinsic_keyword_diagnostics( |
There was a problem hiding this comment.
intrinsic_keyword_diagnostics()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return None | ||
|
|
||
|
|
||
| class _CounterValueQuery(_CounterQuery): |
There was a problem hiding this comment.
_CounterValueQuery
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| (False, "function_local"), (False, "switch"), (False, "if"), (False, "loop"), | ||
| ]) | ||
| @pytest.mark.parametrize("member", ["blue", "red"]) | ||
| def test_native_channel_trades_and_overrides_match_the_direct_default( |
There was a problem hiding this comment.
test_native_channel_trades_and_overrides_match_the_direct_default()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| for name, (prefix, start, _end, _twin, _twin_end, family) | ||
| in sorted(DIVISION_VALUE_STARTS.items())), | ||
| ]) | ||
| def test_division_start_has_distinct_check_and_value_specs(prefix, start, family): |
There was a problem hiding this comment.
test_division_start_has_distinct_check_and_value_specs()
fans out to 11 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| pytest.param("", "4.0 / 2", "double", id="float-literal"), | ||
| pytest.param("float n = 4\n", "n / 2", "double", id="float-alias"), | ||
| ]) | ||
| def test_division_integrality_requires_constant_integer_operands(prefix, start, family): |
There was a problem hiding this comment.
test_division_integrality_requires_constant_integer_operands()
fans out to 12 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| "method measure(float sampleValue, float increment = acc) => sampleValue + increment\n", | ||
| "unusedCopy = acc + 1\nmeasure(float sampleValue = unusedCopy) => sampleValue\n", | ||
| ]) | ||
| def test_default_reads_preclude_omission_before_support_checks(tail): |
There was a problem hiding this comment.
test_default_reads_preclude_omission_before_support_checks()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
|
|
||
| @pytest.mark.parametrize("callee", ["unknownCall", "array.unknownCall", "math.unknownCall"]) | ||
| def test_a_call_without_a_purity_proof_is_an_effect(callee): |
There was a problem hiding this comment.
test_a_call_without_a_purity_proof_is_an_effect()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert set(plan_endless_for_loops(program).values()) == {"stop"} | ||
|
|
||
|
|
||
| def test_an_unknown_effect_in_an_unused_statement_keeps_its_other_inputs_live(): |
There was a problem hiding this comment.
test_an_unknown_effect_in_an_unused_statement_keeps_its_other_inputs_live()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
What changes
Endless counted loops. A counted
forloop whose end is affine in its own counter can never end (for i = 0 to i). We emitted a loop that never ended. TradingView keeps such a script only when the loop's result is never used, and otherwise stops the run on bar 0 with "Loop takes too long to execute (> 2000 ms)". A loop that is statically endless and whose result reaches an effect now stops the run with that text, located; an unused loop statement is omitted. The cases measured on TradingView (entry quantity and entry count per probe) are pinned in the tests. The omission is limited to the measured shape; any other flagged loop stops, so a shape that was not measured never runs and trades silently.Float counters count to TradingView's measured end. A float-step loop adds the step to the counter by repeated double addition, and continues while the counter's excess beyond the end, computed as one double subtraction in the loop's direction, is strictly below
1e-10(kTvFloatStepEnd, one named value, emitted only for float-counter loops). So0.0 to 0.3 by 0.1counts four iterations and0.0 to 2999.9999999999 by 1000.0counts three, as TradingView does. This replaces the absolute tolerance we had chosen ourselves. How it was found: 72 loop counts were taken on TradingView, candidate end rules were written down with their predictions before the counts were read, and the one rule that agreed with every count was implemented (the counts are pinned as data, with the native rows that read them). The end rule is measured at the 0.3 scale (dense, counted up to a 9.999e-11 shortfall and not from 1.0e-10) and at counter magnitudes of about 1, 1e3, 1e6 and 1e9 (sparse). No magnitude-independent claim is made, and1e-10is not a constant TradingView publishes.A float step against a counter with int start evidence is refused (
PF-E2079), as TradingView refuses it (CE10106). An unknown start with a positively float step selects a float counter.A division start keeps its fraction.
for i = n / 2 to ...with an intnand the default step started the counter at the truncated value (2 forn = 5); TradingView starts at 2.5. The loop counter now keeps the fraction of a start that can carry one: a division whose result is not proved integral selects the double counter, while the float-step check still types the start the way TradingView does (an int division with a float step is refused there too). A constant division with an exact integer result (4 / 2) keeps the int counter and its C++ unchanged. The measured quantities (the five division forms, a fractional input and a fractional series) are compared with TradingView's recorded entry quantities exactly. Measured on TradingView and pinned: a descending boundn / 2 to 0, a fractional array index (truncated, as TradingView does) andstr.tostringof the counter. The native tests are intests/test_e2e_for_counter_headers.py:test_tradingview_entry_quantities,test_division_tape_fixture_keeps_its_counter_family_and_twin, andtest_fractional_start_carrier_matches_the_measured_loop_shape; they were measured against TradingView on the stage head, and the final head is covered by the same tests passing in the engine-paired run (the stage-head measurements themselves were not repeated on the final head).Float step against a call. TradingView types
ta.highest,ta.lowest,ta.max,ta.minandta.percentile_nearest_rankas float and accepts them as the start of a float-step loop (they are no longer refused); it refusesta.change,ta.median,ta.mode,ta.rangeandta.valuewhenover an int source, andinput.timeas a start. Every call in the table was measured on TradingView (80 starts and 10tacarriers; the receipts are cited in the tests).More accepted forms.
math.powkeyword arguments bind by parameter name and unknown names are refused.int,float,bool,string) can label a named argument.Known gaps (none of them is fixed here)
The changelog entries behind these gaps are in
CHANGELOG.md, "Unreleased (next release)": the endless-loop entry (lines 25-32), thePF-E2079entry (36-39), the division-start entry (58-60), the float-counter end rule (61-72) and the "Known loop gaps" entry (73-85: the untyped helper,array.from, thef(0)/f(0.0)case). The other gaps below are stated here only.twice(x)accumulating over0.0 to 1.0 by 0.25gives 2 here and 5 on TradingView.array.from(x)can also truncate a float counter through inferredarray<int>storage.f(x)containingfor v = x to 1.0 by 0.25, TradingView refusesf(0)(CE10106) and acceptsf(0.0); PineForge accepts and runs both with a float counter, because call-argument types are not visible to this inference and refusing the unknown start would also refuse the valid float-argument call.na, or whose step is a strategy constant (a commission percentage, a direction, an OCA constant), is refused by TradingView (CE10097 / CE10104) and translated here, as on main; this change does not touch it.str.tostringof a float counter prints 15 fractional places here; TradingView prints 18.test_nonpositive_step_stops_without_tradesintests/test_e2e_for_counter_headers.py; the float-counter descending form is the unmeasured one.)s = n; s := s / 2, thenfor i = s to s + 2), and a helper parameter fed the division (count_from(n / 2)). TradingView gives quantity 10.5 on every entry; the engine gives 9.0. The cause is the variable's storage (an int initializer makes the variable an int, so the quotient is cut when stored, wherever it is read), a wider rule than the loop counter, with its own change after this one. Each case has a test that states the gap and will flip when it closes:test_known_gap_reassignment_fractionandtest_known_gap_helper_parameter_fractionintests/test_e2e_for_counter_headers.py(the same file holdstest_documented_untyped_helper_fraction_gapandtest_known_gap_counter_tostringfor the two gaps above). The 10.5 / 9.0 figures were measured on the stage head and on main; on the final head they are INFERENCE (the later commits do not touch that path), and the tests above are what shows the engine side on this head.request.securitystill never end here while TradingView stops them; data-dependent endless loops still hang. A flagged loop whose result is read in a shape that was not measured is stopped here, where TradingView may drop it.Verification
f(0)/f(0.0)gap above, asx_method_intandx_param_float); 9 with another TradingView error code (CE10097, CE10104, CE10123; 1 start kind, 8 registry probes): this head refuses 5 of them and translates 4 (a start kindna, and three probes that pass a strategy constant as the step), exactly as main does, so this change neither adds nor removes those four.8d0f9e75481c, against the active baseline: verdict 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.🤖 Generated with Claude Code