Skip to content

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

Merged
luisleo526 merged 14 commits into
mainfrom
loop-end-rule-keyword-forms
Nov 2, 2026
Merged

luisleo526 merged 14 commits into
mainfrom
loop-end-rule-keyword-forms

Conversation

@luisleo526

@luisleo526 luisleo526 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

What changes

Endless counted loops. A counted for loop 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). So 0.0 to 0.3 by 0.1 counts four iterations and 0.0 to 2999.9999999999 by 1000.0 counts 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, and 1e-10 is 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 int n and the default step started the counter at the truncated value (2 for n = 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 bound n / 2 to 0, a fractional array index (truncated, as TradingView does) and str.tostring of the counter. The native tests are in tests/test_e2e_for_counter_headers.py: test_tradingview_entry_quantities, test_division_tape_fixture_keeps_its_counter_family_and_twin, and test_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.min and ta.percentile_nearest_rank as float and accepts them as the start of a float-step loop (they are no longer refused); it refuses ta.change, ta.median, ta.mode, ta.range and ta.valuewhen over an int source, and input.time as a start. Every call in the table was measured on TradingView (80 starts and 10 ta carriers; the receipts are cited in the tests).

More accepted forms.

  • math.pow keyword arguments bind by parameter name and unknown names are refused.
  • A primitive type keyword (int, float, bool, string) can label a named argument.
  • A constant colour passed as an input default resolves.
  • A line after an unfinished operator continues the statement only when its indent allows it, and the refusal for a bad continuation is reported at the end of the operator line, as TradingView reports it. Measured on TradingView: a four-space continuation is refused and three spaces accepted in each of 22 operator families at the top level; the rule is the same for every token and context, and a TradingView counter-example would come out. A non-ASCII last lexeme is found by our own checks and has no TradingView receipt.

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), the PF-E2079 entry (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, the f(0) / f(0.0) case). The other gaps below are stated here only.

  • An untyped helper that receives a float counter truncates it: twice(x) accumulating over 0.0 to 1.0 by 0.25 gives 2 here and 5 on TradingView. array.from(x) can also truncate a float counter through inferred array<int> storage.
  • For f(x) containing for v = x to 1.0 by 0.25, TradingView refuses f(0) (CE10106) and accepts f(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.
  • A float-step loop whose start is 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.tostring of a float counter prints 15 fractional places here; TradingView prints 18.
  • The float-counter end rule is measured at the 0.3 scale and at about 1, 1e3, 1e6 and 1e9; it makes no magnitude-independent claim.
  • What a counted loop with a negative step does at run time (TradingView's RE10021) is not measured for this change: three negative-step sources translate without refusal, and whether the engine stops at run time as TradingView does, or counts, has not been checked here. (The int-counter forms with a non-positive step are tested natively and stop before any trade: test_nonpositive_step_stops_without_trades in tests/test_e2e_for_counter_headers.py; the float-counter descending form is the unmeasured one.)
  • Two cases still lose a division start's fraction (UNCHANGED from main; measured on TradingView and on engine runs of main and of the last release): a name first stored from an int and then reassigned with a division (s = n; s := s / 2, then for 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_fraction and test_known_gap_helper_parameter_fraction in tests/test_e2e_for_counter_headers.py (the same file holds test_documented_untyped_helper_fraction_gap and test_known_gap_counter_tostring for 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.
  • Endless loops inside functions, methods and request.security still 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.
  • The lexer rule for operator continuations is pinned by TradingView receipts at the top level and in the contexts listed above; it is NOT covered by a receipt for methods, nesting depths 3 and 4, brackets, commas, ternaries, intervening comments, tabs, or all indent widths.
  • The series-chosen ternary/switch forms remain a known gap, tracked in A request.security timeframe chosen by a chart series value is computed once, not on every bar #310.

Verification

  • Full codegen suite on a host without the engine checkout, base main and this head, same command: base 13,047 passed, 0 failed; head 13,778 passed, 0 failed; nothing fails only on the head.
  • Engine-paired test run on the cloud runner (compile environment present): 19,196 passed, 235 skipped, 0 failed; compile corpus 312 passed; parity gate over 268 fixtures passes. The 72 float-counter end-rule rows, the 228 float-counter header rows and the nine keyword and type-keyword modules all pass by id with nothing skipped.
  • Translated before and after over 4,330 sources in four sets (the population 1,384, a second wave 1,939, the tests tree with the corpus 955, and 52 import pairs): two sources change. One contains a float-step counted loop, and its emitted C++ changes because the loop now uses the measured end rule. The other is a refused source with a unicode identifier: its diagnostic loses one continuation line and it stays refused. Nothing is newly refused, nothing is newly translated, and no compile verdict changes. Limit: only one of these sources contains a float-step loop, so this comparison shows the new rule does not disturb the other sources; the loop behaviour itself is covered by the TradingView counts pinned in the tests. Over 188 further sources (45 start kinds and 80 registry probes among them) joined with TradingView's verdicts: 0 sources where this head refuses and TradingView accepts; 2 where TradingView refuses and this head translates (the f(0) / f(0.0) gap above, as x_method_int and x_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 kind na, and three probes that pass a strategy constant as the step), exactly as main does, so this change neither adds nor removes those four.
  • On the 188-source comparison set, 110 sources that main translated are now refused, and each of them is refused by TradingView too (compile verdicts joined by source hash); none is accepted by TradingView.
  • Full-population parity sweep of this exact head, paired with engine main 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

luisleo526 and others added 14 commits November 3, 2026 04:40
…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>

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

…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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — intrinsic_keyword_diagnostics()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return None


class _CounterValueQuery(_CounterQuery):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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.

@luisleo526
luisleo526 merged commit daa5102 into main Nov 2, 2026
13 checks passed
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