Repository navigation
Refuse the shapes TradingView refuses, and keep na and int ternary typing as before - #290
Conversation
…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]>
…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]>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 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
nato 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)
AGENTS.md§ Pipeline (lines 83-122): references changed symbolsspecialize_security_contextsAGENTS.md§ File map (lines 123-300): references changed symbols_infer_type,numeric,order,parserAGENTS.md§ Support contracts (lines 344-476): references changed symbolsconstant,order,parser,specialize_security_contextsAGENTS.md§ Known codegen quirks (read before changing) (lines 477-935): references changed symbols_infer_type,_nodes,_type_spec_from_expr,orderAGENTS.md§ Conventions (lines 1004-1155): references changed symbols_infer_typeCHANGELOG.md§ Changes since 1.7.0 (lines 137-258): references changed symbolsorderCHANGELOG.md§ Compatibility and migration (lines 352-370): references changed symbolsorderCHANGELOG.md§ Compatibility and migration (lines 528-590): references changed symbolsconstantCHANGELOG.md§ Collection lowering and refusals (lines 761-772): references changed symbolsorderCHANGELOG.md§ Compatibility and migration (lines 862-908): references changed symbols__init__,constant
…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): |
There was a problem hiding this comment.
visit()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| ) | ||
|
|
||
|
|
||
| class _ScalarProof: |
There was a problem hiding this comment.
_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: |
There was a problem hiding this comment.
_Binder
fans out to 16 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return None | ||
|
|
||
|
|
||
| class Lexer: |
There was a problem hiding this comment.
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: | |||
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
_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): |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
test_close_processing_expression_uses_signal_closes()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Adds one Unreleased CHANGELOG entry: since 1.10.0 (#290), identifiers and import paths in Pine v6 scripts must be ASCII. Older sections unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
What this changes
89 commits on top of
main(c3ae98eebe44), the last two a merge ofmainand the narrowing of the statement-comma refusal described below. They do three things.request.securitypayloads, overloaded functions, tuples named like built-in variables and the color cast.naand int ternaries equal tomainwherever 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-engineasmain.Behaviour changes
New refusals (Pine v6 only, unless stated)
New catalog entries (46):
PF-E0038toPF-E0045(lexer and parser),PF-E2040toPF-E2073(analysis),PF-E5080toPF-E5082, and the warningPF-W1553. They are listed inpineforge_codegen/diagnostics_catalog.json(kept in step with the emitters byscripts/gen_diagnostics_catalog.py) and pinned intests/fixtures/diagnostic_codes_pin.json.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 inwhile, and Pine's operator precedence (a doublednotis refused only in the two-consecutive-tokens form).f(x) => ...) go through the same path as an indented block, including aniforswitchopened after the arrow.pineforge_codegen/analyzer/base.py): a barenawhere aboolis required (ternary arms, declarations, assignments, arguments, defaults) and anastored into amapofboolvalues; a string source ofnzandfixnan; an untypednadeclaration; a comparison withnawhere the other operand is provably numeric; a numericifcondition where the analyzer can prove the value is numeric; an unimplemented call in aswitcharm.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.PF-E2040(no resolver reason) andPF-E2041(with one) instead ofPF-E2018andPF-E2019; the message text and named arguments are unchanged andPF-E2018/PF-E2019stay in the catalog for the forms without a hint.docs/PUBLIC_CONTRACT.mdis updated.Lowering fixes
initial_capitalandreplace; 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.tostringof 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.PF-W1553names the case).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).pineforge_codegen/function_overloads.py);mathcalls are typed by signature; string arrays stay string vectors.na and int ternaries
nais resolved against the concrete type it is paired with, and an arm type is derived once per query instead of once per ancestor.maingave it: a string arm gives string, a float arm gives float, otherwise the true arm's type (soc ? na : 1has the type ofna, emitted as a double, andc ? 1 : nastays int). The unification above is kept for the other families and overridden only where it would say int andmain's rule does not, so a script without such a ternary emits the same code as before.intthat receives a nullable numeric ternary is stored as adouble(missing valueNaN) for globals and persistent variables, as before. Function-scope shapes keepmain's emission.naand an int array element returnsdouble, as onmain.CHANGELOG entry (for the next release)
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 againstmainatc3ae98eebe44; 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 ofmain, which only brings inmain'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 aremain's own new workflow tests that the merge brings in).ok255 of 255,err13 refused as asserted).main: exactly six differ. Five are the hinted-import refusals whose code changes fromPF-E2018toPF-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 istests/fixtures/tail_f_tv/arm_commas.pine(stringna). Every corpus script and every gate fixture is byte-identical tomain.naand int ternaries (function tails,nafirst and second, array-element arms, declared ints, nested ternaries, float parameters, string arms, a loop tail) compile to C++ that is byte-identical tomain's.mainaccepted) are now refused; of the 54 shapes TradingView accepts (andmainrefused), 53 now compile and one is still refused with a clean diagnostic (see the gaps below). No crash on any of the 426.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 aboolholdingnaor 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
mainOver the 1409 distinct strategy sources of a private probe corpus (8989 registered probes), the generated C++ was compared between
mainand the head. 1399 sources compile on both and 10 are refused on both (the same 10; the hinted-import code changes fromPF-E2018toPF-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 tomain's. The 41 differences, one line per kind (sources / probes):naarm is typed as a color, with the integer missing value.initial_capitalis renamed in the output (pf_safe_initial_capital) because the host class now has a member of that name; no change in behaviour.1e-10equality tolerance wheremainemitted the plain comparison.na(it wasfalse).doubletoint, withna<int>as its missing value.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
main, not changed here). A function whose body declares anintfrom a ternary with anaarm and returns it (for example a loop body that declaresint local = choose ? 1 : naand returnslocal) is emitted with adoublereturn type, and the int local keeps its missing-value sentinel, which becomes a finite number when it is assigned to the double return. TradingView givesna.mainemits 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.iforwhilecondition whose type the analyzer cannot prove; a comparison withnawhose other operand is not provably numeric; a doublednotthat is not two consecutive tokens; the function-scope and untyped-narefusals for a version 5 source; a string source ofnzorfixnanin a version 5 source;strategy.entry(qty = c ? 1 : na)and other na-valued quantity arguments.switcharm that declares a variable fromtimeframe.from_seconds(an unimplemented call); it is refused with a diagnostic, not a Python error.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