Skip to content

Refuse the shapes TradingView refuses, and keep na and int ternary typing as before - #290

Merged
luisleo526 merged 89 commits into
mainfrom
pr-merge-main
Nov 1, 2026
Merged

luisleo526 merged 89 commits into
mainfrom
pr-merge-main

Conversation

@luisleo526

@luisleo526 luisleo526 commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

89 commits on top of main (c3ae98eebe44), the last two a merge of main and the narrowing of the statement-comma refusal described below. They do three things.

  1. Refuse, with a stable diagnostic code, the Pine v6 shapes TradingView itself refuses. Before this change a script using one of these shapes compiled and ran with behaviour TradingView never produces. Each refusal was added only for a form that was measured on TradingView; a form that was not measured stays accepted (see "Known gaps").
  2. Fix lowering defects in host-member names, enum text conversion, user methods, request.security payloads, overloaded functions, tuples named like built-in variables and the color cast.
  3. Keep the emitted C++ of na and int ternaries equal to main wherever the new typing rules would have changed it, and say so in tests.

Only the Python and JSON contract and the emitted C++ change. The engine pairing does not change: the tests below ran against the same pineforge-engine as main.

Behaviour changes

New refusals (Pine v6 only, unless stated)

New catalog entries (46): PF-E0038 to PF-E0045 (lexer and parser), PF-E2040 to PF-E2073 (analysis), PF-E5080 to PF-E5082, and the warning PF-W1553. They are listed in pineforge_codegen/diagnostics_catalog.json (kept in step with the emitters by scripts/gen_diagnostics_catalog.py) and pinned in tests/fixtures/diagnostic_codes_pin.json.

  • Lexer and parser (pineforge_codegen/lexer.py, pineforge_codegen/parser.py): a continuation line whose indentation is not a multiple of four outside brackets, mixed indentation, a statement-ending comma followed by a more deeply indented line (the form TradingView refuses; a comma at the end of a line followed by a line at the same or a lesser indentation, or by the end of the source, is accepted and parsed exactly as the same script without the comma), non-ASCII identifier and import-path names, a numeric condition in while, and Pine's operator precedence (a doubled not is refused only in the two-consecutive-tokens form).
  • Single-line function bodies (f(x) => ...) go through the same path as an indented block, including an if or switch opened after the arrow.
  • Typing (pineforge_codegen/analyzer/base.py): a bare na where a bool is required (ternary arms, declarations, assignments, arguments, defaults) and a na stored into a map of bool values; a string source of nz and fixnan; an untyped na declaration; a comparison with na where the other operand is provably numeric; a numeric if condition where the analyzer can prove the value is numeric; an unimplemented call in a switch arm.
  • Function definitions: the function-scope shapes TradingView refuses (for example a global read before its declaration, PF-E2053) are refused for version 6 sources; imported version 5 library definitions keep their earlier behaviour. A method called before its definition is refused only within one source file (PF-E2060).
  • strategy() declaration settings (pineforge_codegen/codegen/strategy_declaration.py) are read as constants in the language's positional order, with catalog entries for their errors.
  • Unresolved imports: the refusal now carries a hint about supplying or inlining the library source. The hinted forms use PF-E2040 (no resolver reason) and PF-E2041 (with one) instead of PF-E2018 and PF-E2019; the message text and named arguments are unchanged and PF-E2018 / PF-E2019 stay in the catalog for the forms without a hint. docs/PUBLIC_CONTRACT.md is updated.

Lowering fixes

  • The host class has members named initial_capital and replace; a script variable with either name hid the member. Both names are reserved (pineforge_codegen/codegen/host_members.py), as are the log, time and string helper names the emitter calls (about 30 names), so a user function of the same name no longer replaces a built-in.
  • str.tostring of an enum variable emitted a call to a helper the generated source did not declare, so every such program failed to build. The helper is now declared, and the call prints the enum member's title text.
  • A color cast keeps its value and the missing-value sentinel instead of emitting zero (warning PF-W1553 names the case).
  • A user method is lowered on a call or parenthesised receiver; method bindings are recorded for primitive expression receivers; a built-in namespace call stays off the user-method receiver fallback.
  • request.security: a method or function parameter is bound inside the payload, a chart array read and its array initializers are evaluated in the requested context, and a later global that is only passed as a call argument into a helper's payload is now accepted (it was refused before; this is a newly accepted shape).
  • Overloaded scalar functions resolve by their declared parameter types (pineforge_codegen/function_overloads.py); math calls are typed by signature; string arrays stay string vectors.
  • A tuple binding named like a calendar built-in variable is read as the binding and keeps one history slot per bar.
  • The order-shapes host reads leave out a member of another object and a helper class's own method.

na and int ternaries

  • A bare na is resolved against the concrete type it is paired with, and an arm type is derived once per query instead of once per ancestor.
  • A ternary of the int family keeps the type main gave it: a string arm gives string, a float arm gives float, otherwise the true arm's type (so c ? na : 1 has the type of na, emitted as a double, and c ? 1 : na stays int). The unification above is kept for the other families and overridden only where it would say int and main's rule does not, so a script without such a ternary emits the same code as before.
  • A declared int that receives a nullable numeric ternary is stored as a double (missing value NaN) for globals and persistent variables, as before. Function-scope shapes keep main's emission.
  • A function ending in a ternary of na and an int array element returns double, as on main.

CHANGELOG entry (for the next release)

Unreleased

  • Translation refuses, with stable diagnostic codes, the Pine v6 shapes TradingView refuses: continuation and indentation forms (including a statement-ending comma followed by a more deeply indented line), non-ASCII identifiers, operator-precedence and na/bool shapes, string sources of nz and fixnan, function-definition scope shapes and a method called before its definition in the same file. 46 catalog entries are added (PF-E0038 to PF-E0045, PF-E2040 to PF-E2073, PF-E5080 to PF-E5082, PF-W1553).
  • A refused import of an unresolved library now carries a hint and the codes PF-E2040 / PF-E2041 instead of PF-E2018 / PF-E2019.
  • The names initial_capital and replace, and the emitter's helper names (about 30), are reserved; a script variable of either name is renamed in the output.
  • str.tostring of an enum variable now builds and prints the member's title text.
  • A color cast keeps its value and missing sentinel.
  • A later global that is only passed as a call argument into a helper's request.security payload is now accepted.
  • Measured on the 835 sources of the test corpus and fixtures, emitted C++ differs from the build before this change in exactly one accepted script (tests/fixtures/tail_f_tv/arm_commas.pine, a different string na); the other five differences are refusals whose code changed.

Verification

Run on a cloud build host from a clean checkout of the head, against the engine pairing of main. The suite, corpus compile, gate and Pyodide gate ran on the head itself against main at c3ae98eebe44; the 835-source, 46-program and distinct-strategy-source comparisons ran on a tree whose product files are identical to the head's (the head differs from it only in one test file); the 426-shape probe ran earlier still (before the merge of main, which only brings in main's three workflow files, and before the comma narrowing):

  • pytest: 14853 passed, 221 skipped, 0 failures (15074 tests in the junit file; 30 of them are main's own new workflow tests that the merge brings in).
  • Corpus: 312 of 312 strategies compile.
  • Gate: 268 fixtures, parity OK (ok 255 of 255, err 13 refused as asserted).
  • Pyodide gate: passed.
  • Byte comparison of the generated output of 835 sources against main: exactly six differ. Five are the hinted-import refusals whose code changes from PF-E2018 to PF-E2040 (tests/fixtures/library_scripts/arm_line_import.pine, overloads_import.pine, signals_import.pine, v5rules_import.pine, w11_color_v5_import.pine); the sixth is tests/fixtures/tail_f_tv/arm_commas.pine (string na). Every corpus script and every gate fixture is byte-identical to main.
  • 46 small programs that probe na and int ternaries (function tails, na first and second, array-element arms, declared ints, nested ternaries, float parameters, string arms, a loop tail) compile to C++ that is byte-identical to main's.
  • A probe over 426 shapes on which TradingView and the previous build disagreed: all 372 shapes TradingView refuses (and main accepted) are now refused; of the 54 shapes TradingView accepts (and main refused), 53 now compile and one is still refused with a clean diagnostic (see the gaps below). No crash on any of the 426.
  • The new tests are text- and compile-based: they assert the diagnostic code, line and column or the emitted declaration, and several compile and run the emitted C++. Over the 87 commits tests/ gains 12827 lines and loses 286 in 35 existing files (largest: tests/test_codegen_ascii.py); the removed lines are edits of existing rows, for example tests that built a bool holding na or pinned a shape version 6 now refuses, which were moved to the refused form or to an imported version 5 definition, and each commit message says why.

Known change set: strategy sources whose emitted C++ differs from main

Over the 1409 distinct strategy sources of a private probe corpus (8989 registered probes), the generated C++ was compared between main and the head. 1399 sources compile on both and 10 are refused on both (the same 10; the hinted-import code changes from PF-E2018 to PF-E2040). Of the 1399, 1358 emit byte-identical C++; 41 sources emit different C++ (they cover 405 of the registered probes). The failing case of an earlier head of this pull request (a declaration list whose line ends in a comma and continues at the same indentation) now emits C++ byte-identical to main's. The 41 differences, one line per kind (sources / probes):

  • 29 / 324: a variable that holds a color is stored as a 64-bit integer (it was a double), and a ternary of colors with a na arm is typed as a color, with the integer missing value.
  • 5 / 23: a script variable named initial_capital is renamed in the output (pf_safe_initial_capital) because the host class now has a member of that name; no change in behaviour.
  • 3 / 16: a comparison is emitted in the double form with the 1e-10 equality tolerance where main emitted the plain comparison.
  • 2 / 12: the value a function returns when no branch returns one is na (it was false).
  • 1 / 18: the return type of one function changes from double to int, with na<int> as its missing value.
  • 1 / 12: the settings receipt of strategy() no longer lists the title fields as unresolved (they are read as constants).

The per-probe table (one row per probe with its kind; the kind is the same for every probe of a source) is kept outside the repository because probe ids carry third-party strategy names; its sha256 is f60ab7315988dda9477e4e69301932c7746f12d0b317df7042798c8c14c92dd2 (the file is in the evidence store under that digest). A parity sweep over those probes judges these changes by trades; the kinds above are read from the emitted text and say what changed, not whether a result moves.

Known limitations and gaps

  • Finite sentinel in function scope (behaviour of main, not changed here). A function whose body declares an int from a ternary with a na arm and returns it (for example a loop body that declares int local = choose ? 1 : na and returns local) is emitted with a double return type, and the int local keeps its missing-value sentinel, which becomes a finite number when it is assigned to the double return. TradingView gives na. main emits the same code; the test row that covers it (tests/test_na_numeric_selection.py) pins that emission so a later change is deliberate. It is not Pine behaviour and the expectation should not be copied to other rows.
  • Refusals cover only measured forms. The following stay accepted although TradingView may refuse some of their variants, because those variants were not measured: a numeric if or while condition whose type the analyzer cannot prove; a comparison with na whose other operand is not provably numeric; a doubled not that is not two consecutive tokens; the function-scope and untyped-na refusals for a version 5 source; a string source of nz or fixnan in a version 5 source; strategy.entry(qty = c ? 1 : na) and other na-valued quantity arguments.
  • One shape TradingView accepts is still refused: a switch arm that declares a variable from timeframe.from_seconds (an unimplemented call); it is refused with a diagnostic, not a Python error.
  • The set that records which ternary nodes fell back to the earlier typing is keyed by id(node). No pass creates a ternary node during analysis (the library lowering and finite-length expansion run before the analyzer, and each retry builds a fresh analyzer), so no collision is known; storing the nodes themselves is planned with the next change in that area.

🤖 Generated with Claude Code

luisleo526 and others added 30 commits October 30, 2026 22:20
…d block

The inline body of a function or method was parsed by a separate helper that
returned after its first statement unless that statement was an assignment, so
the statements after the first comma were attached to the surrounding program
and read the function's parameters outside their scope.

Parse the inline body with the statement parser and the statement-list
flattening that the indented block already uses: every comma-separated statement
stays in the callable's body, in source order, sees all parameters and the
locals of the earlier statements, and the last statement is the return value.
The inline-only restriction on assignment targets goes away with the helper; an
assignment to an undefined name is now refused by the analyzer's own diagnostic
instead of by the removed syntax rule.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e-line body

The two rows that now expect the analyzer's refusal also assert its catalog
code, so a change of the code, not only of the text, fails them.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…in-name locals, unregistered function

Three rows of the matrix asked for something the single-line path is not
responsible for. A user method on a color value created by color.rgb has no
receiver type in either body layout, so those parameter combinations are
removed rather than skipped. Two rows named a local after the second-of-minute
builtin, which the emitter resolves before locals in both layouts, so the
locals are renamed (the expected values are unchanged). The trailing-comma row
defined a function the script never called, so no member was emitted; it now
checks the parsed body of the source that ends in the comma against the body of
a runnable twin, then builds and runs the twin.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A selection, assignment, argument, comparison or function return that pairs na
with a concrete value took the family of the first branch or of the double
fallback, so a function returning a string on one path and na on the other was
declared to return double. A concrete type now wins over na in both the coarse
type join and the structured type join, numeric and string promotion keep their
rule, and two na values keep the existing fallback. The resolved type reaches
every boundary that can carry na (ternary arms, target values, user-function
arguments, comparison operands, array reference arguments), and an array na
uses the vector type of its context.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The first version joined types wherever two expressions met, so history-reading
method arguments, conservative terminal return typing, concrete array arms and
integer widths changed in code that has no bare na. Apply the structured join,
the terminal recovery, the argument target typing and the array-reference
staging only when a selected value contains a bare or implicit na, keep the
history bridge for history parameters, keep the integer width of a selected
time value, and drop the global color-literal additions. The expectations of
the new test file that mixed authored and generated type names or omitted an
existing initializer are corrected.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d a contract note

Libraries are inlined only when the library source is supplied. For every other
import the refusal keeps its message and now carries one hint, defined once next
to the refusal builder and used by both places that raise it: libraries are
supported only when the library source is supplied; remove the import or paste
the library code into the script. The diagnostic catalog entries that carry the
hint and the pins that mirror them move with it, and the public contract lists
the requirement.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…estor

Typing a ternary asked for the structured type of its arms and, when that
failed, retyped the false arm and its suffix, and each join then scanned the
selected values again for a bare na. For a chain nested through the false arm
that made the work cubic in the depth, and a 500-link chain exceeded the
transpile time limit. Build one table per query by an iterative post-order walk
of both arms, holding each node's structured result and its nullability, and
let the join and the fallback consume it. The table is local to the query, no
caller's expected type is stored, and the chosen types are unchanged. Rows pin
the operation counts on both typing helpers and a deep string-and-na chain.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…old ones as they were

A hint is part of what a diagnostic code means, so adding one to the existing
unresolved-import entries repurposed them. Restore those entries, their pins and
the historical snapshots to their previous content, add two new entries for the
same messages with the plain hint (the next free codes of the analysis area,
pinned as new codes are), let the classifier pick the new codes by the hint it
already keys on, and name the two additions in the historical-reconstruction
test so any other unlisted addition still fails. The control row now supplies
both libraries of its resolved chain, and the contract lists the new identities.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…dented function uses

A function or method whose body starts on the arrow line with an if expression
was kept as a single expression holding a conditional statement, a shape the
expression emitter has no case for, so the generated function returned a
placeholder and did not compile. The inline body now keeps that conditional as
the bare if statement, the representation an indented body already has, so
analysis types the branch value and the emitter writes the ordinary conditional
tail with the family's missing value for the unmatched branch.

Expression type inference also looked a method call's receiver up on its own,
and for a scalar receiver held in a global binding it could fall back to double,
so an integer method result whose branch was unmatched reached the caller as the
integer missing sentinel read as a finite number. Inference now uses the
typed-method resolver that call emission uses.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…line to that line

The lexer floors space indentation to four-space levels, so a line indented by
two or three spaces had the level of the line before it. After most tokens that
meant a new statement, but after a function header's arrow the parser required
an indented block and refused a body written on the next line at two spaces. A
tab-free line one to three spaces beyond the previous logical line's level now
continues that line, after any token, header included. A function whose body is
one expression at two spaces is therefore the single-line function it joins
into; a conditional, loop or switch header joined to its short body is a syntax
error, and so is a function body of several statements at two spaces. Block
bodies at four spaces or a tab are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…rms, declarations, assignments, arguments, defaults

In Pine v6 a bool has no na, and the reference behaviour refuses a bare na in
these places: an arm of a bool ternary, the initializer of a bool declaration,
an assignment to a bool name, an argument for a parameter typed bool, a bool
parameter default, the initial value of array.new<bool>, the default of
input.bool, and the value of a function whose only returned value is an
untyped na read as a condition. Each is now an analyzer error at the na token
(or the statement for a declaration or assignment): PF-E2042 for the
declaration, PF-E2043 for the ternary arm and PF-E2044 for the other members.
A function that returns a bool on some path and na on another (an if without
else) stays accepted, and so do na for int, float, string, color and user
types; Pine v5 library code keeps its previous behaviour. Rows pin the line
and column of every refusal and the accepted shapes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…indented function body uses

A function or method whose body starts on the arrow line with a switch was
wrapped back into an expression statement, a shape the expression emitter has
no case for, so the generated function returned a placeholder and did not
compile. The inline body now keeps that switch as the bare switch statement,
the representation an indented body already has, so the analyzer types the arm
value and the emitter writes the ordinary selection tail with the family's
missing value for an unmatched selector. A header-line switch and a header-line
if now take the same path as their indented forms.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… Pine v6

Six existing tests and four rows of the new refusal file built a script in which
a bool receives a bare na and expected it to translate. Pine v6 refuses all of
them. A test whose purpose was a bool-na behaviour now asserts the refusal with
its code and source location; a test that mixed several types keeps every
assertion and loses only the bool-na part. The historical catalog
reconstruction test names the three new bool diagnostics, so any other
unlisted addition still fails.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… v6 refuses

Outside parentheses and brackets, a tab-free line whose indentation is not a
multiple of four spaces now continues the previous logical line whether it is
deeper or shallower; a trailing operator must be followed by a line indented by
a number of spaces that is not a multiple of four; a block may add exactly one
indentation level. Inside an open parenthesis or bracket the indentation of the
following lines is cosmetic. Two analyzer checks join them: an int variable
declared with a float literal, and an if condition of numeric type, are
refused. The four new diagnostics have their own codes (PF-E0038, PF-E0039,
PF-E2045, PF-E2046) with pins added; no old entry changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…he time builtin

Two rows named a local after the second-of-minute builtin, which the emitter
resolves before locals in both layouts, so the arm read the builtin instead of
the local and the selector 1 value was 0.0. The locals are renamed; the
arithmetic and the expected values are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
# Conflicts:
#	pineforge_codegen/diagnostics_catalog.json
#	tests/fixtures/diagnostic_codes_pin.json
#	tests/test_diagnostic_notes.py
The continuation, indentation and numeric-condition refusals are not in this
head yet, so their four codes are not additions here.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nguage's positional order

The declaration's settings were read as literals only, so a const name, an
arithmetic expression or a boolean expression was ignored, and the positional
arguments after the title never reached the settings (the seventh positional,
pyramiding, was dropped; a long positional list landed a value in the wrong
setting). One shared path now binds every positional with the repository's own
ordered signature table, merges named arguments without overwriting a bound
slot, and evaluates each setting as a compile-time constant (a literal, an
earlier const, a named constant, arithmetic, comparisons, boolean operators,
ternaries and closed pure math). A value that is not a constant, a value given
twice and a positional beyond the list are located declaration errors.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…alue is numeric

The first version refused any condition whose analyzed type was int or float,
but many bool-valued expressions (built-in calls, member reads, history and
collection reads, selections) reach that type only through the analyzer's
float fallback, so valid conditions such as ta.crossover(...) were refused. A
condition is now refused only when it is a numeric literal, a numeric built-in
series from the built-in tables, arithmetic or unary minus over such operands,
a binding or alias of such an expression that is never reassigned, or an
explicitly typed numeric parameter. Everything reached through the fallback is
accepted as before. A trailing-operator continuation keeps the new indentation
rule only where it applies outside parentheses and block context shows it is a
continuation, and the catalog-history test names the four new codes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d drop two unmeasured rows

Two existing tests wrapped a long expression after a trailing operator with the
next line indented by exactly four spaces at the top level, the one shape the
indentation rule refuses (a wrapped line must not be indented by a multiple of
four spaces outside parentheses). Indent that line by six spaces, which keeps
each test's purpose (line endings; a nullable selection) unchanged. Two rows of
the new rule file pinned a refusal for a wrapped operand inside a function body
whose indentation the reference behaviour does not rule out, so they are
removed rather than asserted either way.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…re-aim the tests of the replaced behaviour

The three declaration errors are now raised through an emitter form the catalog
generator scans, so it derives their templates, allocates their codes and pins
them itself (the hand-added entries are removed). The tests that expected a
non-constant setting to stay silently unresolved now assert the located error,
the tests that expected a constant name or expression to be ignored now assert
that it is honoured, and the two journal tests use sources that remain
ineligible once constant expressions are read.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…gy() declaration errors

The three templates (too many positional arguments, an argument given both
positionally and by keyword, a setting that is not a compile-time constant) are
now extracted from the source, so the catalog generator's write mode added their
codes PF-E5080 to PF-E5082 and their pins. The two files are the generator's
own output; no old entry or pin changed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…rated head

# Conflicts:
#	pineforge_codegen/diagnostics_catalog.json
#	tests/fixtures/diagnostic_codes_pin.json
#	tests/test_diagnostic_notes.py
# Conflicts:
#	tests/test_diagnostic_notes.py
The bare color(x) cast was emitted as the literal 0 for every argument and
skipped the argument entirely. A color is held as a 64-bit integer whose missing
value is that type's minimum, so zero is a real color and na(color(na)) was
false, while color(c) of a real color always became zero. The cast now binds its
argument (positional or x=) and converts it through the integer-slot conversion
the emitter already uses, so a literal or bound na stays the color na and a real
color passes through unchanged. The construct still warns that visual output is
not rendered, with text that says what it now preserves.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The warning added for the bare color() cast is pinned with the digest the
catalog generator derives for it (its write mode found the entry already
consistent with the source and only added the pin), and the historical
reconstruction test names it among the additions after the old snapshot.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
# Conflicts:
#	tests/test_diagnostic_notes.py
The engine's host class now has accessible members named initial_capital
and replace, and generated code spells them, so a script variable of either
name hid the member. Regenerate codegen/host_members.py with
scripts/gen_host_members.py (clang 18 on a CI runner, against the engine
that added them): two names added, nothing removed.

Tests: the committed-set check now asserts both names are derived; the
rename test gains a variable of each name (renamed to pf_safe_<name> and
compiled); the loud probe runs with both names against its neutral-name
twin; the lane composition row that used "replace" as an unreserved Pine
name now asserts the rename and the same trace as its reference.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
luisleo526 and others added 19 commits October 31, 2026 20:09
…d pin fixture

The fixture gains one digest line for each of PF-E0043 to PF-E0045 and
PF-E2062 to PF-E2073, produced by the catalog generator. No existing line
changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…elper's request.security payload

The refusal "Global variable 'X' is read before its declaration in a function"
fired for an identifier that a call argument substitutes into the copied
payload of a helper defined above the global. That identifier is evaluated at
the call site, below the declaration, and TradingView accepts such a program
(two recorded probes of it ran with 336 trades each). The analyzer now skips
the refusal for an identifier that carries the payload provenance marker; a
read written in the function body itself is refused exactly as before.

The two replay tests run the unmodified recorded probes against the recorded
trades again (they had been made to move the global declarations above the
helper), the four older refusal pins of the same copied-argument shape become
compile regressions, and two boundary tests pin the body read (refused, with
line and column) and the argument at a call below the declaration (accepted).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ith na

The doubled-not refusal matched the tree shape not(not x), but the parser
drops parentheses, so the parenthesised operand, which TradingView accepts,
was refused as well. The parser now marks a not whose very next token is
another not, and only that spelling is refused, with the same code, message,
line and column as before.

The comparison of a value with a bare na was refused for every operand type,
but it was measured only for a number. It is now refused only when the other
operand is proven to be an int or a float; a string, an object field, an
array or an operand of unproven type keeps its ordinary lowering.

New tests pin the refused spellings beside the accepted ones, with the lines
and columns derived by hand.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…entinel

A bare na next to an int operand of a binary operator was rendered with the
int type of its neighbour, so "int n = 5" and "y = n + na" gave the int
missing-value sentinel plus five instead of NaN, although arithmetic with na
is a float in Pine. The neighbour's int type does not prove the type of the
na, so the na is rendered as a double there; the contextual typing of a
proven target (a typed declaration, a string or bool peer) is unchanged.

Tests: emitted-code rows for + - * / with the na on either side and a native
run that reads the value back, plus rows showing a double peer and the proven
contexts unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A call such as math.abs(x) was bound to a user method named abs on float
whenever the script defined one: the receiver type is inferred for any
receiver that has no declared type, and a namespace prefix is not a value.
The inference is now skipped for a built-in namespace name that is not also a
variable, parameter, local or loop variable of the script; value receivers
(a variable, a parenthesised expression, a call result) bind user methods as
before.

Tests: a user method abs beside math.abs keeps the built-in, the same method
still binds on a float variable, a parenthesised receiver and a call receiver,
and another namespace shows the same.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The guard that keeps a namespace call such as math.abs(x) off the user-method
receiver fallback used the whole built-in namespace table, which also holds
names that are values in Pine (the bar time, the execution time and the
weekday number). A user method called on one of those values stopped binding.
The guard now skips names the emitter already lowers as bare built-in values,
so only a pure namespace prefix is kept off the fallback.

Tests: a user method on the bar time and on the execution time binds, and the
math.abs control keeps the built-in.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
"float y = cond ? 1 : na" selected an int and the int missing-value sentinel
on the na arm, and converting that int to the float variable gave
-2147483648.0 instead of NaN, so na(y) was false. A ternary that contains a
bare na and reaches a float variable, a float call argument, a float array
element, an explicit float() cast or an arithmetic operand of a double now
goes through the conversion that maps the int sentinel to NaN; a ternary
assigned to an int variable keeps the int sentinel.

The tests that checked the bare na beside an int operand are corrected to the
text the declarations lower to and to the values a native run prints, and new
rows pin the ternary shapes in emitted code and in a native run that reads
na(y) back on a bear bar.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A script variable named math is emitted under the escaped C++ name
pf_safe_math, so the tests that check the emitted receiver and argument of a
user method on such a variable now expect that spelling. No product code
changes: the binding itself was already correct.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A ternary with an int arm and a bare na arm was typed int, so its na arm was
the int missing-value sentinel, and every consumer that is not a converting
float variable (a builtin argument such as plot, a ta source or math.max, an
untyped variable later used in arithmetic or a comparison, a local of a
function) read that sentinel as a number. The inference now keeps such a
ternary a float, as before, so its na arm is NaN everywhere; only a declared
int variable or an int-typed return target supplies the int representation
and keeps the int sentinel. A bare na added to a string stays the string
concatenation it was.

Tests: emitted-code rows and native runs for the builtin arguments, untyped
variables, function locals, nested ternaries, the int target that keeps the
sentinel, an integer plus na and a string plus na.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ile; restore the edited tests

The refusal of a method called before its definition compared the position of
a script call with the position of a method declared in an imported library,
so an ordinary top-level call of an exported method was refused whenever the
library declared it on a higher line than the script called it. It now
compares only a call and a declaration of the same source file.

A tuple declared in a top-level block under the name of a built-in variable no
longer makes a later read outside the block read the block-local name: the
built-in is kept once the block has ended.

The tests and recorded sources that had been changed to avoid these refusals
are restored: the library call sites at the top level, the two recorded
request.security sources to the text their pinned hashes describe, the held
order of the global-history rows, the value assertions of the later-global
rows, the mixed-callsite helper test and the three recursion receipt rows
(now written with accepted mutually recursive and imported recursive
functions). New rows pin an imported method called at the top level, the
refusal inside one file, and an int declaration of an integer plus a bare na.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…uble

A variable declared int that was assigned a ternary with a bare na arm was
stored as an int holding the int missing-value sentinel, so every later use
(an addition, a comparison, a builtin argument, a text conversion) read the
sentinel as a number, while the earlier behaviour stored it as a double whose
missing value is NaN. The value flow of such a declaration is followed through
globals, persistent variables, locals, function parameters (arguments and
defaults) and function returns, and the storage of each of them is a double
again; a script without such a flow emits the same code as before. The earlier
special casing that kept the int sentinel for a declared int target is
removed.

The tests that pinned the int sentinel for these variables are replaced by
stricter checks of the new behaviour (double storage, NaN on the missing arm),
and native runs read back the sum with one, the comparison, the text, the
maximum and the plotted value on a bear and a bull bar.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ple block

A bare read of the calendar variable second after a block that declared a
tuple of the same name is lowered to the engine calendar helper, not to the
raw time-structure field the first version of the test expected. The three
block rows now assert that call, and a fourth row (an else block) asserts that
the read inside the block and the read after it are different texts. No
product code changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The value flow that stores a declared int fed by a nullable numeric ternary as
a double also reached function scope (a local declared int, an int parameter
and its default, an int function return), where the earlier code emits an int
with a missing-value-preserving wrapper and where existing tests record that
emission. The flow is now limited to the scopes whose storage was already a
double (globals and persistent variables); function-scope shapes are emitted
as before, and the helpers that only served the parameter and return
propagation are removed.

The new rows that pinned double storage for function-scope shapes now pin the
earlier emission instead; the global and persistent rows are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e function

A function whose loop body declares an int from a ternary with a na arm and
returns it is emitted with a double return type; the int local keeps its
missing-value sentinel, which becomes a finite number when it is assigned to
the double return. The row for this shape expected an int function and NaN
consumers; it now expects the double signature, the double result slot and the
values this emission produces (the finite sentinel on the missing arm), each
derived in a comment, so a later change to this behaviour is deliberate.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…int array element

A function whose last expression is a ternary with a bare na arm and an
element read from an int array (a shift or a get) was typed int, so it
returned the int missing-value sentinel as a number; the earlier behaviour
types it as a double whose missing arm is NaN. The analyzer now keeps the
floating return type for that terminal ternary when the other arm is a call on
a variable the type tables know as an array; literal arms, the time and bar
index built-ins and every float or string row are unchanged.

The two rows that pinned the int return now pin the double return with the
strict body checks of the float rows (the missing arm is a double NaN and no
int sentinel appears), and a native run reads the missing arm back as NaN.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The fixture that checks an int function return keeps guarded int storage used
a ternary of na and an int array element as its tail, a shape that now returns
a double like the earlier behaviour. Its tail is a literal arm, which is an int
return in both the earlier behaviour and now, so the fixture keeps checking
exactly the integer-target storage it was written for; every assertion of the
test is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…m's type

A ternary with a bare na arm took the type of its concrete arm, which made
"c ? na : 1", "c ? na : time", a na arm beside a local number, an element read
from an array held in a field, a call result or a map or matrix, a method
result, and the same ternary reached through a variable or an if branch an int
(with the int missing-value sentinel) where the earlier code typed them as a
double with a NaN missing arm. The analyzer now computes the earlier result for
every ternary (a string arm wins, then a float arm, otherwise the true arm's
type) and, wherever the new typing would say int while that result is not int,
keeps the earlier one, also for the exact return type recorded for a function
and for the call-site query that reads it. Other families (string, bool
refusal, objects, drawings, arrays, floats) and the ternaries that were already
int (a literal, the time and bar index built-ins) are unchanged. This replaces
the earlier fix that handled one shape of the problem.

Tests: rows for the na-first function tails, the globals, the array,
field, call, method, variable and branch shapes (emitted signature and tail
text), a native run reading the missing arm back as NaN, and a regression row
that a genuine int ternary stays int.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Main gained two commits that touch only the promote-baseline workflow
(.github/workflows/promote-baseline.yml), its documentation (docs/ci.md) and
its tests (tests/test_pr_workflows.py). None of those files is changed on this
branch, so the merge has no conflict and no resolution: the result is main's
three files plus this branch's changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…eeper

A statement that ends a line with a comma was refused (PF-E0040) in every block
body, whatever the next line looked like. TradingView accepts a comma at the end
of a line when the next code line starts at the same indentation (at the top
level, in a function body and in an if body), and when the next line is
indented less (a comma on the last line of a function body); it refuses the
form whose next line is indented deeper (measured at the top level), with a
mismatched-input error at that line. A comma at the end of the source was not
measured and stays accepted, as it was before the refusal existed. A strategy whose
function body starts with a declaration list that continues on the next line at
the same indentation therefore compiled on TradingView and stopped compiling
here.

The lexer now restores the line boundary of the comma-less script when the next
code line has the same or less indentation, so the following lines are parsed
exactly as they are without the comma (blank and comment lines in between do
not change that). A deeper next line keeps the earlier token stream and the
refusal keeps its code, message and location. The call-argument comma rule, the
arrow-body continuation paths and every script without a statement-ending comma
are unchanged.

Tests: rows for the function body, top level, if, for, while and switch bodies,
a comma before a dedent, before else and after comments, an operator-first
continuation, a tab-indented body and a comma at the end of the source, each
asserting that the emitted C++ equals the C++ of the same script written
without the comma; the measured refusal row and two more deeper-line rows stay
refused with the same code, message, line and column.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526
luisleo526 marked this pull request as ready for review November 1, 2026 09:14

@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.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; formal verification proved 0 of 115 changed function(s) (1 sampled, not proven, 114 not verified).

Formal verification. PR-changed functions: 1/115 verified (0 proven, 1 may-equivalent, 0 distinguished) · 114 not verified (1 vacuous, 23 unsupported, 90 over cap).

Not verified on this run: \_generate (vacuous: never exercised), \_\_init\_\_ (unsupported), \_populate\_builtins (unsupported), \_visit\_Assignment (unsupported), \_visit\_BinOp (unsupported), \_visit\_FuncCall (unsupported), \_visit\_FuncDef (unsupported), \_visit\_Identifier (unsupported), \_visit\_IfStmt (unsupported), \_visit\_ImportStmt (unsupported), and 14 more; 90 changed function(s) beyond the cap and 0 skipped when the time budget ran out.


Graphify review — findings

Adds a hint to unresolved-import errors about supplying or inlining the library source, reported as PF-E2040 without a resolver reason and PF-E2041 with one, while the message text stays the same. Binds scalar function overloads before analysis, and after analysis re-specializes method security payloads using the resolved receiver types, then re-runs the analyzer on the rewritten AST. Tightens analyzer typing by refusing na for bool values, with only imported v5 library definitions keeping the old nullable-bool lowering, and resolves strategy declaration settings after the whole program's reassignments are known.

Worth a look

  • Assigning na to a bool variable now errors in Pine v5 scripts — pineforge_codegen/analyzer/base.py:4885 · Escalate · high
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Bool-na declaration refusal is not gated to Pine v6, so valid v5 scripts are rejected — pineforge_codegen/analyzer/base.py · Escalate · high
    • NOT verified (no proof, no reproducing execution) — needs human review
  • v6 nz/fixnan bool refusal contradicts its own documented contract — pineforge_codegen/analyzer/base.py:4350 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Int-from-float literal diagnostic ignores v5 library compatibility exemption — pineforge_codegen/analyzer/base.py:4596 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Method parameter reassignment is not rejected — pineforge_codegen/analyzer/base.py:4875 · 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 — 6159 functions depend on the 4065 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: Parser — 138 callers, 42 callees
  • worse: Lexer — 161 callers, 6 callees
  • worse: Analyzer — 48 callers, 12 callees
  • worse: ScriptIndex — 16 callers, 22 callees
  • worse: _generate() — 5 callers, 21 callees
  • worse: walk() — 15 callers, 5 callees
  • worse: resolve() — 21 callers, 3 callees
  • worse: _specialize() — 1 callers, 20 callees
  • …and 17 more — each is listed as a finding

Verification — 6159 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: 6150 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 \_generate.

The verifier did not have enough to check \_generate, 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 181 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly CompileError — names the real obstacle, not a sampling gap)

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 \_populate\_builtins.

The verifier did not have enough to check \_populate\_builtins, 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\_Assignment.

The verifier did not have enough to check \_visit\_Assignment, 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\_BinOp.

The verifier did not have enough to check \_visit\_BinOp, 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\_FuncCall.

The verifier did not have enough to check \_visit\_FuncCall, 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\_FuncDef.

The verifier did not have enough to check \_visit\_FuncDef, 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\_Identifier.

The verifier did not have enough to check \_visit\_Identifier, 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\_IfStmt.

The verifier did not have enough to check \_visit\_IfStmt, 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\_ImportStmt.

The verifier did not have enough to check \_visit\_ImportStmt, 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\_MethodDef.

The verifier did not have enough to check \_visit\_MethodDef, 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\_StrategyDecl.

The verifier did not have enough to check \_visit\_StrategyDecl, 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\_SwitchStmt.

The verifier did not have enough to check \_visit\_SwitchStmt, 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\_Ternary.

The verifier did not have enough to check \_visit\_Ternary, 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\_TupleAssign.

The verifier did not have enough to check \_visit\_TupleAssign, 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\_UnaryOp.

The verifier did not have enough to check \_visit\_UnaryOp, 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\_VarDecl.

The verifier did not have enough to check \_visit\_VarDecl, 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\_WhileStmt.

The verifier did not have enough to check \_visit\_WhileStmt, 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 analyze.

The verifier did not have enough to check analyze, 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 \_callsite\_primitive\_expr\_type.

The verifier did not have enough to check \_callsite\_primitive\_expr\_type, 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 \_handle\_fixnan\_call.

The verifier did not have enough to check \_handle\_fixnan\_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 \_handle\_input\_member\_call.

The verifier did not have enough to check \_handle\_input\_member\_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 \_handle\_user\_func\_call.

The verifier did not have enough to check \_handle\_user\_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)

No difference found (not proven): No behavior difference found in \_join\_callsite\_primitive\_types (not a proof).

The verifier ran both versions of \_join\_callsite\_primitive\_types 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 \_type\_spec\_from\_expr.

The verifier did not have enough to check \_type\_spec\_from\_expr, 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)

· 17 grounded finding(s) anchored inline below; 8 more finding(s) on lines outside this diff (see the check run).

raise ValueError("not a constant number")
return _numeric(value)

def visit(expr):

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 — visit()

fans out to 8 callees (efferent coupling).

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

)


class _ScalarProof:

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 — _ScalarProof

fans out to 11 callees (efferent coupling).

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

return Analyzer._certain_scalar_type(self, node)


class _Binder:

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 — _Binder

fans out to 16 callees (efferent coupling).

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

return None


class Lexer:

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 — Lexer

fans out to 6 callees (efferent coupling); 161 callers depend on it (afferent coupling).

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

@@ -313,9 +313,16 @@ def _spell(node) -> str:
class ScriptIndex:

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 — ScriptIndex

fans out to 22 callees (efferent coupling); 16 callers depend on it (afferent coupling).

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

from tests.test_runtime_var_initialization import _compile_and_run


def _parse_expr(source: 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 — _parse_expr()

high coupling complexity (Ca·Ce = 16).

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



def test_price_history_and_later_globals_replay_tradingview_tape(tmp_path_factory):
def test_price_history_replays_tradingview_tape(tmp_path_factory):

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_price_history_replays_tradingview_tape()

fans out to 6 callees (efferent coupling).

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



@pytest.mark.parametrize("case", _CASES, ids=lambda case: case.name)
def test_comma_body_translates_and_compiles_like_indented_body(case: BodyCase) -> 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 — test_comma_body_translates_and_compiles_like_indented_body()

fans out to 6 callees (efferent coupling).

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

_compile_pair(_REPRODUCTION, indented, label="record-array-parameter")


def test_trailing_comma_at_end_of_source_compiles_and_returns_the_tail() -> 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 — test_trailing_comma_at_end_of_source_compiles_and_returns_the_tail()

fans out to 6 callees (efferent coupling).

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



@pytest.mark.parametrize("value", ["true", "not false"], ids=["literal", "expression"])
def test_close_processing_expression_uses_signal_closes(value):

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_close_processing_expression_uses_signal_closes()

fans out to 6 callees (efferent coupling).

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

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