Skip to content

Use the documented str.format_time defaults, add PF-E2081 and PF-E2087, reserve the generator's own member names, test helper-chosen request.security timeframes, and give function locals spelled like built-in variables their own type and history - #355

Merged
luisleo526 merged 37 commits into
mainfrom
train/codegen-20261109a
Nov 9, 2026

Conversation

@luisleo526

@luisleo526 luisleo526 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This pull request combines separate branches on top of main with plain merges. Each section below describes one branch: what changes for a user, how it was tested, and what the tests do not claim. The branches touch different features; where they share a file (CHANGELOG.md, the diagnostic catalog and its pin, the support-contract page) the merge keeps every entry of every branch.

The numbers below describe the current head of this pull request. It was opened with sections 1 to 5; section 6 was merged into it afterwards, and the numbers of the verification section were measured again on the new head. If another branch is merged into this one before it lands, the description is updated and the numbers are measured again on that head.

1. str.format_time follows the documented defaults for an omitted format and timezone

When a str.format_time call leaves out the format, the translation now uses the ISO 8601 pattern yyyy-MM-dd'T'HH:mm:ssZ; when it leaves out the timezone, it uses the exchange time zone (syminfo.timezone). The Pine Script User Manual documents both defaults (Time, Formatting dates and times). Before, the omitted slots were filled with yyyy-MM-dd and UTC, so str.format_time(time) was a ten-character string where the documented result has 24 characters. A call that gives both arguments, by position or by keyword, translates exactly as before; passing "yyyy-MM-dd" and "UTC" explicitly restores the previous output. The change is two literals in _visit_format_time_call.

Tests (tests/test_e2e_keyword_hole_binding.py, 17 rows):

  • three text rows for the emitted call (both slots omitted, only the timezone omitted, only the format omitted), each also requiring the keyword spelling to emit the same text;
  • four rows that compile and run a script on fixed timestamps, in the default zone and in New York: they print the strings and their lengths and compare them with literal expectations;
  • three compile rows for an omitted slot inside a user function, a method and a request.security payload;
  • six rows that keep explicit calls and unrelated scripts unchanged against the previous main (no changed line may name format_time, an explicit call keeps its exact text, a script without the call emits none);
  • the existing keyword-hole row.
    With the two literals reverted, 10 of the 17 rows fail (the three text rows, the three compile rows and the four running rows) and 7 pass; with the change all 17 pass. The expected strings of the four running rows are derived by hand from the documented format and time-zone rules; they were not measured on TradingView.

Compatibility: a script that omits the format or the timezone of str.format_time gets the documented string instead of the old one; every other script translates as before. A translation comparison of 1,216 sources (856 tracked Pine sources, 312 corpus strategies, 48 probes) between main and this branch alone changed exactly the five probes that omit a slot and none of the other 1,211 sources; the same comparison of main against itself changed none.

2. A removed when argument has its own diagnostic, PF-E2081

A strategy order call that passes when in a Pine v6 source is still refused at the keyword, but now with the dedicated code PF-E2081: the message says that Pine v6 removed the argument, and the hint shows the call placed inside an if statement. The generic PF-E2027 and its pinned template are unchanged and still report every other unknown name, on these functions and elsewhere; a source of another Pine version keeps its existing refusal. The catalog, the pin, the public contract, the support contract page, the known-quirks page and the changelog describe the new code.

Tests (tests/test_public_issue_286.py, tests/test_diagnostic_codes.py, tests/test_diagnostic_notes.py): with the old translator the updated rows fail (96 failed: 91 rows expect PF-E2081 where PF-E2027 is reported, and 5 rows compare the declared catalog code set); with the change the same files pass (1,393 passed, 22 skipped). The pin digest of PF-E2027 and the pin of PF-E2081 were checked to pass; they were not seen to fail on the old translator.

3. A call or a variable is refused as the default of a user-defined type field (PF-E2087)

Summary

TradingView refuses a function call, or a reference to a variable the script declares, as the default value of a field of a user-defined type (compile error CE10133, "The default value cannot be a function, variable or calculation."). PineForge accepted both and generated code that evaluated them when the type was constructed.

This change reports the new diagnostic PF-E2087, "The default value cannot be a function, variable or calculation.", at the first token of the default expression, for types declared in Pine v6 code.

What is refused and what is not

Refused: a field default that contains a function call (a user function, a method, or a builtin such as array.new<int>(), map.new<string, int>() or array.from(...)) or an identifier bound to a variable the script declares. This includes such a call or variable inside arithmetic, comparison, logical, unary and ternary expressions.

Unchanged: literals, na, negative literals, calculations over literals, builtin members and builtin variables such as close (the rule looks at syntax and at the script's own declarations only, so they keep the handling they had), constructor arguments such as Book.new(total = counter), and local initializers. A default that reads a variable declared later in the script keeps its existing PF-E2075 refusal.

Pine versions

The refusal applies to a type declared in //@version=6 code: a v6 script, or a type that an imported v6 library exports. It is decided from the version of the declaration itself (the version recorded on an imported type, otherwise the script's). A type exported by an imported //@version=5 library keeps the handling it had before this change, including the library checker's own refusals and, where it applied, the existing session-history reason (PF-E5048). A v5 main script is refused earlier by the version check, before any type default is read.

Diagnostic code

PF-E2087 is a new catalog entry, with its pin line written by hand. PF-E2083 to PF-E2086 belong to the strategy.default_entry_qty argument refusals and are not touched.

Tests

  • New tests/test_field_default_refusal.py: thirteen refusal rows that assert the code, arguments, message, absence of a hint, level, phase and the exact line and column, and paired controls whose complete C++ equals the C++ of the parent commit (literal defaults, na, a negative literal, constructor arguments, local initializers).
  • New tests/test_field_default_versions.py: the version rows. A v5 main script stops at the version check; a type exported by a v5 library with a call default or a variable default is still accepted, with complete C++ equal to the reference commit's; the session-history reason is still reported for a v5 library default that needs it; the same libraries declared as v6 are refused in the library file; and one source is refused under v6 but not under v5.
  • Existing tests whose scripts used a call or a variable as a field default now pass it to the constructor instead, or expect the refusal: test_codegen_ta_stable_var_literal.py, test_codegen_validation_fixes.py, test_e2e_session_history.py, test_session_history_in_method.py. None is dropped or skipped.
  • The tests of each step are committed before the change they test and fail before it for the right reason (the refusal does not happen, or it happens for a v5 library type).

Compatibility

A script that used a call or a declared variable as a field default in v6 code stops compiling, as it does not compile in TradingView. The generated C++ of every script that still compiles is unchanged. A population of 1507 translation contexts (all of them accepted) was translated on main and on this branch with identical generated C++ and no new refusal. A second set of 188 sources gave the same result on both: 78 translate with identical C++, and the other 110 are refused with the same unrelated message on both, so it is not an independent population for this rule. The full test suite was run on main and on this branch on a machine without the engine library or the corpus: the same two tests fail on both (their inputs are missing there), no test fails only on the branch, and the branch passes 86 more tests (its new ones).

Known limit

The behaviour of a type exported by a //@version=5 library is the behaviour before this change, not a TradingView measurement.

4. Tests: a request.security timeframe chosen through a helper follows the choice on every bar

Tests only; no product file changes. A script can choose the timeframe of request.security with a condition that reads a chart value, and pass that timeframe through the parameter of a helper function, through a local alias, through a tuple helper, through several call sites of one helper, through nested helpers, or let the helper choose its own timeframe. The new tests pin that each such form gives the same traces and trades, on every bar and at every call site, as the equivalent script that requests each timeframe once at the top level and selects the value on the chart. They compare PineForge with PineForge; they are not a comparison with a TradingView export.

New tests/test_e2e_security_helper_timeframe.py:

  • nine native rows. Each runs the helper form and its top-level equivalent on the same chart feed (the corpus 15-minute feed, a window of about 31 days, so the daily context publishes about 31 values), with an entry quantity that carries the requested value and traces of the requested values, and requires identical traces and trades. Each row requires that both choices occur during the run, that the requested values change, and that trades are made, so a comparison cannot pass with one fixed timeframe. The forms are: a timeframe passed as a helper parameter, through a local alias, a tuple helper, two call sites with different choices, one fixed and one chosen call site, nested helpers, a helper that chooses its own timeframe, and two controls (a helper called with literals and a helper called with input timeframes, including changed input values);
  • nine structural rows over the same sources that need no native engine: every call site and every arm registers exactly one request, the choice is made per bar and not at setup, and a fixed call site keeps the original emission path.
    The native rows skip when the native inputs are missing, like the neighbouring native test modules; in the combined test run below they ran.

5. A Pine name spelled like a generated state member or helper is escaped

A Pine variable whose name is spelled like one of the generator's own state members or helper names (for example _security_helper_series_) made the translator raise an internal AssertionError from its duplicate-member check instead of producing C++ (the case that was reported: a local of a function that request.security calls), or hid the generated member. The script is valid Pine. The names that the generator declares itself, and the numbered families that keep a fixed numbering, now take the same C++ escape as any other name that collides with generated C++. Ordinary names keep their spelling, and the duplicate-member check stays as the last line of defence.

What changes: pineforge_codegen/codegen/helpers.py reserves, for the safe-name allocator, the fixed spellings of the generator's state members, helper types and exported methods, the fixed temporaries, and the numbered families that do not allocate against authored names (request results, precalculated vectors, per-request state, technical-analysis state, history members, closed-trade fold members, loop temporaries). docs/PUBLIC_CONTRACT.md, docs/reference/conventions.md and CHANGELOG.md state the rule.

Tests: the new tests/test_generated_member_names.py has a census read from the codegen sources (every fixed or numbered name that a generated declaration introduces) that must be reserved, and behavioural rows that put each census name through a global variable, a helper local and a function parameter, numeric and string, and compare the complete C++ with the C++ of the same script under an ordinary name, after the escaped spelling is replaced by text. tests/test_helper_series_words.py adds the two store names to the words used as helper locals; tests/test_e2e_session_history.py maps the two escaped spellings back before it compares complete C++. The tests were written first and fail without the change (103 rows).

Compatibility: scripts whose names do not collide emit identical C++. A population of 1,507 translation contexts (all accepted) and a second set of 188 sources (78 translate identically, 110 are refused with the same message on both trees) were translated on the parent commit of this branch and on the branch, before the branches were combined; the combined head is covered by the comparison and the test run below. The committed list of host members is unchanged (172 names) with the headers of the engine main at the time, and test_committed_host_members_are_the_derived_set passes on the combined head.

Limits: the census lists the numbered families by hand, so it will not notice a new family; one family is not reserved yet: a script local spelled _tuple_result_<N>_elem_<M> still collides with the generator's tuple bindings in a tuple assignment with series or global targets (a compile error, a spelling no one writes).

6. A function or method local spelled like a built-in variable keeps its own type, history and reads

This section describes one branch of this pull request, merged in after the pull request was opened. Its numbers were measured on that branch against its own base, codegen main at 7d7681dc, before it was combined with sections 1 to 5. Where the text below speaks of the open fix for #350, that fix is section 5 of this pull request and the combination is the head that the verification section below covers.

Summary

Reported in #351. A variable declared inside a function or a method whose name is also a built-in variable (second, hour, close, bar_index, ...) was typed, stored and read as the built-in variable in several shapes. Inside a function that request.security calls, a string variable named second kept its history in the numeric helper store. A variable in a nested block, a variable reassigned with :=, a loop variable and a variable without any history read took the series type of the built-in variable, and the history of a variable named hl2, hlc3, hlcc4 or ohlc4 received the built-in variable's expression.

One early pass now gives every such declaration its own private name before the type, storage and history inventories are built, so the declared variable wins everywhere. It covers all 26 built-in variable names that the analyzer or the generator treats specially; the names come from the existing tables and the pass holds no list of its own. The declaration shapes that have test rows are: a plain local, a local in a nested block, a var or varip local reassigned with :=, a for or for ... in variable (all three binder shapes), a tuple element, a local in a switch or a while body, an int, bool, color, array, label or user-defined-type local, in functions and in methods, typed or untyped, with or without a history read. Parameters, globals and reads of the built-in variable keep their meaning, and a function that only reads the built-in variable is unchanged. Scripts that declare no such variable emit identical C++. One group keeps main's behaviour on purpose: see "Compatibility".

This does not make the script from #351 compile, and it does not make every shape work: see "Known limits". The main reason is a separate defect: a string variable with a history read, used in a concatenation, is emitted as std::to_string applied to a string, and that C++ does not compile for any variable name, ordinary names included, on main at 7d7681dc (the base of this branch) and on this branch: #354. The reporter's script still fails for that reason.

What changes

  • pineforge_codegen/analyzer/callable_history_names.py: the new pass separate_builtin_callable_locals selects the declarations in functions and methods whose spelling is one of the built-in variable names, allocates <name>__pfhist<N> in source order so that it clashes neither with an authored name nor with a generated clone name, and renames each declaration together with every use that the analyzer's value-binding resolver bound to it. The authored spelling is kept as an annotation, and authored_callable_text maps a private name in a message back to it. The set of names is built by one function, _builtin_callable_local_names(), from the existing tables. The earlier separation of the history of direct callable locals stays.
  • pineforge_codegen/analyzer/base.py: runs the pass right after the value bindings are resolved; the inventories that depend on names (the reassigned names, the method signatures) are built after the rename; analyzer diagnostics name the variable as the author wrote it; the refusal of a namespace name used for an object keeps deciding on the authored spelling, and its message text is unchanged. The history of such a variable is recorded as its own. The analyzer's name-only shortcuts for price and bar fields also step aside when an identifier is bound to a declaration of the script; after the early pass has renamed the declaration, that case does not arise in a function or a method, so these hunks are kept as a safeguard and no test row exercises them.
  • pineforge_codegen/analyzer/value_bindings.py: keeps the table of declaration bindings in step with the rename (a tuple element read on the right of := needs it: a row covers that) and charges the analysis budget.
  • pineforge_codegen/codegen/types.py and pineforge_codegen/codegen/visit_expr.py: the declaration that a name is bound to inside its own function is recovered and its type is used; the lookup by name after the function's scope has closed let the built-in variable's type win. The same step-aside rule applies to the price and bar field reads of the emitted code.
  • pineforge_codegen/codegen/base.py and pineforge_codegen/codegen/input.py: generator diagnostics and the key of an untitled input use the authored spelling, so no private name reaches a message or an input key.
  • pineforge_codegen/codegen/security.py and pineforge_codegen/codegen/tables.py: the narrow rule that keeps main's refusal (see "Kept as on main"). One constant, SECURITY_PRICE_LOCAL_NAMES, holds the nine price-field names; one method, _security_check_price_named_local_history, is called at the two places where a history read of a helper local meets its binding (the prepass walker and the expression builder), which is where main composed the refusal. It applies only to a declaration that the early pass renamed (it reads the pass's own record of the authored spelling, not the bare name), that is not declared var, and whose type is string; it raises the diagnostic that main gives, with the same code and message, and changes nothing else. A global, a parameter and a library local are untouched.
  • CHANGELOG.md: one bullet.

Tests

  • New tests/test_helper_builtin_named_locals.py: every built-in variable name that the analyzer or the generator treats specially, as a numeric and as a string function variable, with and without an explicit history capacity, plain and declared with var; each row compares the store references of the emitted code with the same function whose variable has an ordinary name and checks which helper store holds the history.
  • New tests/test_builtin_named_locals_scope.py: the 26 names come from the same tables as the pass, and a row compares the pass's own name set with them (and checks that no name contains the clone markers _cs or __ni). For every name the complete C++ is compared with the C++ of the same script whose variable has an ordinary name, after the variable's own storage names are mapped back; the embedded legacy capability manifest is part of that comparison and nothing is masked. The shapes are: history-free, typed, reassigned with :=, nested block, two call sites, loop variable, numeric tuple element (each as a numeric and as a string variable where that applies), in a function called through request.security, in a chart-path function and in a method; varip; the three for ... in binder shapes; int, bool, color and array locals; a drawing local and a user-defined-type local read with history; switch and while bodies; method bodies with a nested block, a typed local and a reassigned local; a tuple element read on the right of := to an int variable. A string tuple element read with history is compared by its refusal text. Further rows:
    • a request that sits inside the declaring function and reads the local: the request list, the diagnostics and the legacy embedded manifest equal the ordinary name's, and so does the C++ once the local's own private spelling is mapped back (that mapping also hides the spelling inside the confirmed-bar receipt, so the receipt is read separately, below); the confirmed-bar receipt that is embedded in the C++ is read raw, for a request with a renderable payload (nz(<name>[1]) and <name>[1]) and for each of the 26 names: its expression field is exactly nz(<name>__pfhist1[1]) or <name>__pfhist1[1] (the ordinary name's: nz(aa[1]) and aa[1]), every other key and value of the receipt equals the ordinary name's, and the private spelling occurs nowhere else in it. So the embedded confirmed-bar receipt is not equal to the ordinary name's: the spelling of that one field differs, on purpose, and the rows pin it (see "Known limits");
    • diagnostics: 30 script shapes with each of the 26 names give the same diagnostics (message and hint) as the ordinary name, and none contains a private name. The ordinary name's diagnostics hold only the plot note in every shape except the string tuple element, so these rows cannot see an unrestored name in a shape whose diagnostics never name the local; the restoration rows below cover the shapes where a diagnostic does. The support check's "diverges from TradingView semantics" warning is required exactly for the three names that have it (bar_index, last_bar_index, timenow) and forbidden for the other 23, in this test as in the test of the request that sits inside the declaring function;
    • allocation: eight scripts, among them authored <name>__pfhist<N> spellings and their clone spellings, show that private names clash neither with authored names nor with each other, in source order, and the compile file compiles the same eight;
    • restoration: a direct row for authored_callable_text (exact bindings, both clone suffixes, unmapped spellings, a missing text), a generator refusal of a float local used as a matrix receiver, a typed local's refusal, and the key of an untitled input all carry the authored name;
    • a function local that holds an instance of a user-defined type and is spelled like a name that is both a built-in variable and a namespace (dayofweek, time, timenow) is still refused with the namespace message, without any private name in it;
    • library functions with a local spelled like each name (version 5 and version 6 called plainly, and version 6 called through request.security): the inliner qualifies a library local with its module and function names, so the pass has nothing to rename there and the C++ equals the ordinary name's after the qualified spelling is mapped;
    • preservation: the complete C++ equals main's (7d7681dc) for a global and a parameter spelled like a built-in variable, for functions that only read built-in variables (history reads included, also hl2, hlc3, hlcc4, ohlc4), and for a function that declares the variable beside one that reads the built-in variable.
  • New tests/test_builtin_named_locals_price_refusal.py (138 rows): for each of the nine price-field names, a string variable read with history inside a function called through request.security is refused with the diagnostic main gives (one error, code PF-E3011, the message below, located at the history read). Nine rows (one per name, the typed shape) compare level, code, message, line and column with what main's own tree gives for the same script, and 18 more rows (the nested block and the function called at two sites) check the diagnostic and its position. 111 rows show that nothing else is refused: the other 17 names as string variables in the same three shapes, the nine names as numeric variables and the nine names as string variables without a history read, each equal to the ordinary name's translation (87 rows), and the shapes that main accepted (24 rows: the nine names reassigned with := and in a chart-path function, second and time in three shapes). Four existing tests that compared these pairs with the ordinary name now expect the refusal for exactly those pairs (27 rows of the requested-variants test, 27 rows of the diagnostics test, 18 rows of the helper-history test and the 3 close pairs of the string compile rows); no row and no assertion was removed.
  • New tests/test_builtin_named_locals_compile.py: 173 compile attempts (a syntax check of the emitted C++ against the engine headers, with the repository's compile helper; the rows run where the helper finds an engine include tree and skip otherwise, like the other compile tests): the numeric tuple, typed and chart shapes for an ordinary name and for each of the 26 names (81); 20 further shapes for an ordinary name and for second (40); the eight allocation scripts (8); a string variable without a history read for an ordinary name and for each of the 26 names (27); the string-with-history shapes for an ordinary name and for second, close and time (17 attempts in 5 rows; the pairs of close in the typed, nested and two-site shapes check the refusal instead of compiling), where the rows pin that renaming keeps the compile outcome and the first compiler error, because those shapes do not compile for any name (see A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354 above).
  • tests/test_helper_series_words.py: the string variable named second is no longer left out of the rows.
  • No row runs the emitted code or checks a computed value: the checks are C++ equality with the ordinary-name control, diagnostics, compile outcome and refusal text.
  • Red first. On the parent of the narrow rule (this change without that rule), the 27 rows of tests/test_builtin_named_locals_price_refusal.py that pin the refusal fail with DID NOT RAISE CompileError: the script is accepted, and the C++ it gives does not compile (A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354). The 111 other rows of that file pass there, as they must. In the first round, on main at 7d7681dc with that round's test files added (a throwaway commit that was never pushed), 116 ids failed in tests/test_helper_builtin_named_locals.py and tests/test_helper_series_words.py; tests/test_helper_series_words.py is unchanged since, and tests/test_helper_builtin_named_locals.py has gained the refusal branch for 18 rows, so that count is not re-measured.
  • On this branch the 12 test files that this change adds, touches or depends on give 3,456 tests with none failing (6 skipped: they need a built engine library), and no compile row skipped; the rows that read main's refusal through the reference commit ran. A merge with current main (437b9069) is clean, and its merged tree gives the same 3,456 tests with none failing (the same 6 skipped for the engine library, no compile row skipped). A merge with the open fix for A Pine variable named like a generated script-state member ends in an uncaught AssertionError #350 conflicts in the same two files as before (CHANGELOG.md, tests/test_helper_series_words.py) and needs a hand resolution; that combination is not part of this change's tests. Tests of that other change need private fixtures and skip on a machine without them; they must run wherever the fixtures exist.
  • The whole suite was not run for this revision of the change; the files named above were, and the sets of sources and contexts under "Compatibility" were translated on both trees.

Compatibility

  • Scripts that declare no variable named like a built-in variable inside a function or a method emit identical C++. A population of 1,507 translation contexts (1,384 distinct sources; the archive holds 8,006 index entries and is 19,568,640 bytes, sha256 0971c4b6...) was translated on main at 7d7681dc and on this branch: all 1,507 translate on both, to identical C++ in every context, and none is newly refused. A second set of 188 sources gave the same on both: 110 are refused on both, the other 78 translate to identical C++, and none is newly refused. A count over every node below each function and method body (library sources parsed with the library parser) finds no such declaration in any of the 188 sources, in the 1,384 distinct sources of the population, or in the 7 distinct library sources, so these sets guard scripts without a collision and say nothing about scripts with one; the rows and the compile rows above do.
  • Scripts that do declare such a variable change on purpose. A numeric variable in a nested block, reassigned with :=, in a loop, in a tuple or without a history read now behaves exactly like a variable with an ordinary name instead of taking the built-in variable's series type or expression (a tuple element named second with a history read was stored as Series<int>, it is now Series<double>). A string variable without a history read now compiles for each of the 26 names (a compile row per name); on main it did not compile for the eleven names measured (the nine price-field names, second and time).
  • Kept as on main: a string variable named open, high, low, close, hl2, hlc3, hlcc4, ohlc4 or volume that is read with history inside a function called through request.security is still refused, with the message it always got ("request.security helper-parameter history currently requires a direct OHLC/time bar-series binding", code PF-E3011): the early pass would otherwise turn that refusal into accepted C++ that does not compile (A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354). Rows pin the refusal for each of the nine names in three shapes (a typed variable, a variable in a nested block, a function called at two sites), compare its code and message with what main gives for the same script, and pin that nothing else is refused. A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354 is not fixed by this change and stays open for 1.11.1.
  • One change of acceptance toward the ordinary name: a string tuple element named like one of these nine names that is read with history was accepted and stored as the numeric built-in variable; it is now refused with the diagnostic that every other string tuple element read with history already gets.
  • One acceptance difference in the other direction, measured outside the suite: a program that combines the callable shapes above (a chart function, a typed local, a tuple element, a loop variable, varip, for ... in, a method), all declaring the same name, is refused for an ordinary name by the existing check "A callable history local ... shares raw generated Series state with another lexical declaration", and is accepted, with C++ that compiles, for each of the 26 built-in names, because each declaration now has its own private storage. The check itself is unchanged.

Known limits

  • A string variable with a history read, used in a concatenation, does not compile for any name (A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354). The string shapes with a history read (typed, reassigned, nested, two call sites, chart-path) give the same compile outcome and the same first compiler error as the ordinary name (for the nine price-field names in the shapes listed under "Kept as on main" the translation is refused instead), and a string tuple element read with history is refused for every name alike; the compile rows pin that, and no row checks a value.
  • For bar_index, last_bar_index and timenow the support check still reports "diverges from TradingView semantics in PineForge." at every read of a local with that name, as it does for a global or a parameter with that name: it runs before scoping and looks at names only, and this change does not touch it. The rows require exactly those warnings for exactly those three names, in the shapes that read the name, and forbid them for the other 23.
  • A version 5 library function reached from a request.security payload is refused for every local name and for an ordinary name alike (a library rule, unchanged).
  • A request that sits inside the declaring function and reads the local gives the same request list, the same diagnostics and the same legacy embedded manifest as the ordinary name, and the same C++ once the local's private spelling is mapped back; the confirmed-bar receipt embedded in the C++ is the one difference, described next. For a callable local with a built-in name that is read inside request.security in the declaring function, the confirmed-bar receipt's expression field carries the internal storage spelling when the payload can be rendered (nz(second__pfhist1[1]) for nz(second[1])); rows pin it for all 26 names. Restoring the authored spelling there is a 1.11.1 item, next to A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354. Measured outside the suite on seven such scripts, for the compile outcome and the first compiler error only (no C++ equality was measured for these seven): four compile (a history read of the local under nz, a plain history read, a history read in a method, a history read in a nested block) and three do not, for the ordinary name too (the local passed to a built-in call or to a user function inside the request expression, or a tuple element: the request code does not see the function's local, '<name>' was not declared in this scope, a separate limit that this change does not touch). The four built-in names tried (second, close, hl2, bar_index) give the same compile outcome and the same first compiler error as the ordinary name in all seven.
  • Not covered by rows: map locals, and matrix locals beyond one row that pins the matrix refusal for second (both are refused for every name alike, measured outside the suite); user-defined-type and drawing locals beyond the namespace-object rows, the object control and the rows for one drawing local and one user-defined-type local read with history inside a function; three more shapes of a string local of the nine price-field names, measured outside the suite on main and on this branch, without a row: a varip local read with history in the requested helper is refused on both with the same error (the rule applies to it, as it did on main); a local passed as an argument to a second helper that reads its parameter with history is refused on both, but main gave the refusal described above (code PF-E3011) and this branch gives the error that an ordinary name gets (code PF-E3097, "derived helper-parameter history requires dependency-aware helper evaluation for this body"); a local read with history directly in a request.security payload inside its own function is accepted on both, with different C++ because the local's history is now its own: for the nine names and for second the C++ of main does not compile (the history is read from the built-in series), and the C++ of this branch compiles for all of them and for an ordinary name (a syntax check against the engine headers; nothing was run); and the analyzer's name-only shortcuts that this change keeps as a safeguard.

Combined verification

The head was tested as one tree on a clean checkout with the engine built from main: 91 test files, 14,009 passed, 56 skipped, 0 failed. The 91 files are every test file that this pull request changes (18) and the test files that sit next to the changed code. Every skip is a private fixture that is not present on the machine. The new test modules of sections 3, 4, 5 and 6 and the keyword-hole module of section 1 ran without a skip (test_field_default_refusal.py 46 rows, test_field_default_versions.py 39 rows, test_e2e_security_helper_timeframe.py 18 rows, test_generated_member_names.py 799 rows, test_e2e_keyword_hole_binding.py 17 rows, test_helper_builtin_named_locals.py 214 rows, test_builtin_named_locals_scope.py 1784 rows, test_builtin_named_locals_compile.py 161 rows, test_builtin_named_locals_price_refusal.py 138 rows and test_builtin_named_locals_receipt.py 52 rows).

Section 6 was merged into the head that the pull request was opened with. The merge has two conflicts, resolved by hand: CHANGELOG.md keeps every bullet of both sides, in order; tests/test_helper_series_words.py keeps the reference words that section 5 adds and the rows for the string variable named second that section 6 adds back, with a combined comment. Apart from those two files, the merge changes the same files, with the same changed lines, as the branch of section 6 does.

A run of the whole tests directory on the head before section 6 was merged in, on a machine with the engine library built from main, gave 25,929 passed, 188 skipped and 0 failed. Of the skips, 179 are private fixtures that are not present on that machine, 6 are population sources that need an environment variable to be set, and 3 are empty parameter sets; the last nine are in test files that this pull request does not change. The whole directory has not been run on the combined head; hosted CI is reported by this pull request itself.

A translation comparison of 1,227 sources (856 tracked Pine sources, 312 corpus strategies, 59 probes) between main and this head changed exactly nine sources, all of them probes written for these branches: the five probes of section 1 that omit a slot, one source that made the translator crash before and now translates (section 5), one refusal that now reports PF-E2081 (section 2), one call default that is now refused with PF-E2087 (section 3), and one probe of section 6, a string variable named second in a function, which now keeps its own string history instead of the numeric history of the built-in variable. None of the 856 tracked sources and none of the 312 corpus strategies changed. The same comparison of main against itself changed none.

Known limits

  • The expected strings of the four running str.format_time rows are derived by hand from the documented rules; they are not TradingView measurements.
  • The type-field default refusal applies to types declared in Pine v6 code; the behaviour for a type exported by a //@version=5 library is the behaviour before this change, not a TradingView measurement.
  • The helper-timeframe tests compare PineForge with itself.
  • The reservation of generated names does not yet cover _tuple_result_<N>_elem_<M> locals in a tuple assignment with series or global targets (section 5).
  • The limits listed in section 6 apply to this head unchanged. In particular a string variable with a history read that is used in a concatenation does not compile for any name (A string local with a history read, used in a concatenation, is emitted as std::to_string on a string (the C++ does not compile) #354, which stays open), and the confirmed-bar receipt of a request inside the declaring function names such a variable by its internal storage spelling.
  • Tests that need private fixtures skip on a machine without them. Every skip in the run above is of that kind.

🤖 Generated with Claude Code

luisleo526 and others added 30 commits November 8, 2026 13:16
TradingView refuses a call, or a reference to a user-declared variable, as the
default of a field of a user-defined type (compile error CE10133). The new
tests/test_field_default_refusal.py pins that refusal with located-error rows
for calls, variables and operators or ternaries over them, and with paired
controls whose complete C++ equals the C++ of the parent commit: literal
defaults, na, a negative literal, constructor arguments and local initializers
stay accepted.

The existing tests whose scripts used such a default now pass the call or the
variable to the constructor instead, or expect the refusal, and keep what they
check.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A field default of a user-defined type that contains a function call, or an
identifier bound to a user-declared variable, now raises PF-E2087 at the first
token of the default expression, as TradingView does (compile error CE10133).
Builtin members, builtin variables, calculations over literals, constructor
arguments and local initializers are unaffected, and a default that reads a
variable declared later keeps its PF-E2075 refusal.

The code is added to the diagnostics catalog and its pin. The reference guide
documents the rule and says that only a callable parameter default that names
an earlier global is still accepted; the changelog lists the new refusal.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A script whose type field default called a function that reads a session
flag's history used to reach PF-E5048. The analyzer now refuses every call in
a type field default with PF-E2087, so that script no longer gets as far as
the history check. The row leaves the parametrized PF-E5048 test, which is
renamed for the method paths it still covers, and a new test pins that the
same source is refused with exactly PF-E2087.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The paragraph added with the PF-E2087 refusal broke two of its lines at
uneven widths. Rewrap it to the width of the paragraphs around it; no words
change.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
An omitted format and an omitted timezone of str.format_time are documented
to be the ISO 8601 pattern yyyy-MM-dd'T'HH:mm:ssZ and the exchange time zone
(syminfo.timezone). The new translation rows pin the emitted C++ of the three
call shapes that leave a slot out; the preservation rows pin that fully
explicit calls and scripts without the call keep their translation byte for
byte; the native rows pin the complete printed strings for known instants in
a UTC and a New York exchange zone. The rows that change behaviour fail on
the current translation, which still fills the slots with a date-only pattern
and UTC.

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

When a str.format_time call leaves out the format, the translation now uses
the ISO 8601 pattern yyyy-MM-dd'T'HH:mm:ssZ, and when it leaves out the
timezone it uses the exchange time zone (syminfo.timezone), as the Pine User
Manual documents. Before, the slots were filled with yyyy-MM-dd and UTC, so
str.format_time(time) was a 10-character string where the documented result
has 24 characters. Calls that give both arguments, by position or by keyword,
translate exactly as before; passing "yyyy-MM-dd" and "UTC" explicitly
restores the previous output. The changelog and the known-quirks page say so.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…hoice on every bar

A script can pick the timeframe of request.security with a condition that
reads a chart value and pass it through the parameter of a helper function,
through a local alias, through a tuple helper, through two call sites of one
helper, through nested helpers, or let the helper pick its own timeframe. The
new native rows run each form and the equivalent script that requests every
timeframe once at the top level and selects the value on the chart, on the
same feed, with an entry quantity that carries the requested value, and
require identical traces and trades. Each row also requires that both choices
occur during the run, so the comparison cannot pass with one fixed timeframe.
A helper called with a literal and one called with an input timeframe are
controls. Structural rows check that every call site and arm registers one
request.

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

A local declared inside a function that a request.security expression calls
may be spelled like a built-in variable (second, hour, close, bar_index, ...).
It must keep the type its own declaration gives it, and its history must live
in the helper store that matches that type: the string store for a string
local, the numeric store for a numeric one. The new rows cover every built-in
variable name the analyzer and the generator treat specially, as a numeric and
a string local, with and without an explicit history capacity, plain and
declared with var, and compare the store references with the same helper whose
local has an ordinary name. The existing words test no longer leaves out the
string local named second. Controls check that a helper which only reads the
built-in calendar variables emits the same complete C++ as before.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…g for typing and history

A string local named second in a requested helper kept its history in the
numeric helper store, because the type of the declaration was looked up by
name after the callable's scope had closed and the built-in variable's integer
type won over the declaration's string type. The same lookup, and the analyzer
and emitter rules that test a bare name against the built-in tables, also
treated a local named like a price, bar or calendar variable as the built-in.

The generator now recovers the declaration a name is bound to inside its own
callable and uses that declaration's type, the analyzer records the history of
such a local as the local's, and the name-based built-in shortcuts step aside
when the identifier is bound to a declaration of the script. A built-in
variable that a callable only reads, and a global that shares a built-in's
spelling, keep the handling they had.

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

Pine v6 removed the when argument of the strategy order functions. The
refusal of a source that still passes it should say so instead of repeating
the generic unknown keyword text. The updated rows expect a dedicated code,
PF-E2081, with its own arguments and wording for when on the seven order
functions in a version 6 source, at the same location as before; every other
unknown name keeps PF-E2027 with its generic message, a version 5 source
keeps its existing refusal, and valid order calls still translate. The
catalog tests expect the new entry and an unchanged pin for PF-E2027. The
rows fail on the current translator, which reports PF-E2027 for when.

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

A strategy order call that passes when in a version 6 source is still refused
at the keyword, but now with the dedicated code PF-E2081: the message says Pine
v6 removed the argument and the hint shows the call placed inside an if
statement. The generic PF-E2027 and its pinned template are unchanged and
still report every other unknown name, on these functions and elsewhere; a
source of another version keeps its existing refusal. The catalog, the pin,
the public contract, the support contract page, the known-quirks page and
the changelog describe the new code.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d, not an internal error

A local of a requested helper spelled like one of the generator's own state
members (_security_helper_series_, _security_helper_series_str_) made
transpile() raise an AssertionError from the duplicate-member check instead
of producing C++. The script is valid Pine. The words test now uses both
store names as helper locals, numeric and string, and compares the store
references with a helper whose local has an ordinary name. A new census test
reads the codegen sources for every fixed or numbered name that a generated
declaration introduces and requires that an authored name of that spelling is
escaped, with the families the emitter already allocates against authored
names listed together with their reason; diagnostic prose and statement
keywords are not declarations and are left out of the census. Behavioural
rows run a global, a helper local and a function parameter of those
spellings through transpile and compare the whole C++ with the control.
Controls compare the complete C++ of scripts with ordinary names with the
C++ of the parent commit.

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

The safe-name allocator renames an authored identifier only when its spelling
is in the reserved sets. The generator's own state members and helper types
were missing from them, and several numbered member families (request
members, precalculation members, per-request state, technical-analysis
state, history members, closed-trade fold members, loop temporaries) keep a
fixed numbering instead of allocating against authored names. A helper
local spelled like one of them ended in the emitter's duplicate-member
assertion, or hid a generated member.

The fixed spellings are added to the emitter names and the temporaries, and
the numbered families to the temporary pattern, so that such a name takes the
same escape as any other name that collides with generated C++. Ordinary
names keep their spelling. The duplicate-member assertion stays as the last
line of defence. The history-member test that compares complete C++ now maps
the two escaped spellings back before comparing. The public contract and the
conventions guide state the rule, and the changelog lists the change.

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

The refusal of a call or a variable as a type field default is a rule of
Pine v6 code. The new rows pin the outcomes around it. A v5 main script is
refused by the version check before any default is visited. A type exported
by an imported v5 library keeps the outcome it had: call defaults and
variable defaults are accepted, and a default that calls once for every new
instance still reports the session history reason. The same library
declared as v6 refuses the same defaults in the library file, and the
same main script refuses a call default under v6 but not under v5. The
reference rows compare the complete C++, or the diagnostic codes, with the
reference commit that the other field default tests use.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The call and variable refusal (PF-E2087) now applies to a type declaration
whose own Pine version is 6: the version recorded on a type that an imported
library brings in, or the version of the script otherwise. A type exported by
an imported v5 library keeps the handling it had before the rule. The
library checker's own refusals still apply, other call defaults are
accepted, and the session history reason (PF-E5048) is reported where it was
reported. The support contracts guide, the pipeline page and the changelog
say so.

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

The note in the known-quirks page now cites the test file that covers the
PF-E2081 wording (tests/test_public_issue_286.py) and keeps the list of files
that cover the general keyword rule as it was. The comment in the test that
explains why a function body that nothing calls is asked only with the
support checker now says what was observed (nothing is raised there on main
either) and no longer names a cause that was not shown.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ntity rows to the call

The tests now say that the expected strings of the str.format_time rows are
hand-derived from the documented format and time-zone rules and were not
measured on TradingView.

Three new rows translate an omitted format and timezone inside a user
function, a method and a request.security payload, check the documented
defaults in the emitted call, and compile the result, which shows that the
exchange zone member is in scope in each of them.

The six rows that compare a translation with the one of an earlier commit no
longer compare the whole translation unit. They fail when a changed line
names format_time, they require the explicit call to be in both translations,
and they require scripts without the call to emit none, so a change elsewhere
in the translation no longer turns them red.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the omitted-format and omitted-timezone defaults of str.format_time
(an omitted format is the ISO 8601 pattern, an omitted timezone is the
exchange zone). No conflict.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the dedicated diagnostic PF-E2081 for the removed when argument of
the strategy order functions in a version 6 source. The one conflict is in
CHANGELOG.md, where both branches add a line at the top of the unreleased
list; both lines are kept, in the order of the two merges.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the diagnostic PF-E2087 for a field of a user-defined type whose
default is a function call or a variable that the script declares, in Pine v6
code, as TradingView refuses it. Literals, na, calculations over literals and
builtin members keep their handling. No conflict.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in one new test module and no product file: a script can choose the
timeframe of request.security with a condition that reads a chart value and
pass it through the parameter of a helper function, a local alias, a tuple
helper, several call sites, nested helpers or a helper that chooses its own
timeframe; the rows pin that each form follows the choice on every bar. No
conflict.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the fix for a Pine variable spelled like one of the generator's own
state members or helper names (for example _security_helper_series_), which
raised an internal assertion or hid the generated member: those spellings are
now reserved for the safe-name allocator. The one conflict is in CHANGELOG.md,
where both sides add a line at the top of the unreleased list; both lines are
kept, in the order of the merges.

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

Add rows for the places the earlier change must leave alone or must change on purpose: a global and a parameter spelled like a built-in variable, built-in history reads in a helper and in a chart-path function, a sibling function that only reads the built-in variable, and the chart-path functions whose own local now wins over the built-in variable.

Add census variants (history-free, typed, reassigned, declared in a block, loop variable, two call sites, tuple) that compare the emitted C++ with the same script written with a neutral local name. A string tuple element read with history stays refused, with the same diagnostic for every name.

These rows compare emitted C++ only; nothing is compiled or run.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The first version of the comparison rewrote a map iterator member, a quoted text that the control also holds, and the escaped spelling of some names, and it assigned a value that referred to the local itself. Rewrite only recognised unqualified local identifiers (including the generated storage and escaped spellings), change a quoted text only where it equals the control's text in the same place, and assign constants in the reassignment variant.

Add rows for the normaliser itself: it is the identity on the control, it keeps qualified members and shared text, it maps the escaped spellings, and it still reports a different series type, a missing call, a read of the built-in variable and a manifest note.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ignore the manifest note

The comparison of a local spelled like a built-in variable with its neutral-named control now renames the local's own generated storage (this->x and GeneratedStrategy::x) like its other generated spellings, and leaves every other qualified member alone. The embedded manifest lists a note about the built-in variable when a script only declares a local of that name; that is a separate known difference, so the comparison with a control ignores the notes in that list and nothing else.

Add rows for both rules, and a census row for a numeric tuple element read with history, which compares the complete C++ with the neutral-named control for every name.

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

The embedded capability document lists its open facts under the key unresolved, not solved, so the mask added in the previous commit never matched the real text and six rows still failed on a note about the built-in variable. Use the real key in the mask and in the rows that check it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…iable its own private name

The earlier fix only separated direct function-body locals that carry a history read, so the same collision still produced wrong series types, wrong history stores or undeclared storage for locals declared in nested blocks, locals reassigned with :=, loop variables, tuple elements and locals without any history read.

Add one early pass that runs right after the analyzer resolves value bindings. It selects every function-local and method-local declaration whose spelling is a built-in variable name (taken from the existing built-in tables, no name list), gives it a private name in source order that cannot clash with an authored name or a clone name, and renames the declaration together with every use bound to it. Parameters, globals and reads that precede the declaration keep their bindings. A script without such a local is returned before anything is walked or changed.

The authored spelling is kept as an annotation so that analyzer and generator diagnostics and the key of an untitled input still name the local as written. The name-dependent inventories are now built after the rename. The original late history separation stays for callable locals that need it.

Add rows for method locals, persistent locals, the three for-in binder shapes, typed-local refusals and untitled inputs. Replace the changelog line.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… on the class fix

The row for an untitled input on a persistent local looked for the float getter, while the generator emits the double getter for an input.float value; it failed for every name for that reason alone.

The persistent-reassignment row used the built-in close as the assigned value, which for a local that is itself spelled close reads the local; it now assigns a constant, like the other reassignment rows.

A numeric tuple element spelled like a built-in is no longer a preservation row. On the previous main its history was stored in the built-in's series type (Series<int> for second); the new pass gives it the ordinary floating storage. The shape stays pinned against the renamed control by the existing tuple rows.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ames that are both a variable and a namespace

The earlier change respelled the placeholder of the refusal "Invalid object name" from node.name to a local name. The diagnostics catalog is keyed by the spelled template, so the catalog test found a template without a code. Restore the exact spelling. The refusal still decides on the authored spelling, because the rename gives the local a private name, and the restoration of authored names in diagnostics that already runs after the analysis puts the author's name back into the message.

Add rows at the end of the scoped-locals tests for the three names that are built-in variables and also namespace names (dayofweek, time, timenow): a function-local that holds a user-defined-type instance is refused with exactly the message of the unrenamed script, and no private name appears in the message or in the exception text. An inventory row keeps the three names in step with the two name sets. A name that is not a namespace name (second) is not refused and matches the renamed control.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the documentation pull request that landed on main after the
previous merge (the clarification of the unreleased fixes in the changelog
and the numeric helper documentation, #353). No conflict and no hand edit.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
luisleo526 and others added 3 commits November 9, 2026 01:55
…put of built-in-named locals

Drop the manifest-note mask from the scoped-locals comparisons. Every comparison row now includes the embedded capability manifest; the mask and its self-tests are removed.

Add rows for the private storage names: an authored name that looks like a private storage name, or like its call-site or nested-call clone, is reserved; allocation follows source order across several colliding declarations; a refusal that names an authored private-looking spelling keeps it unchanged.

Add rows for diagnostics that name a renamed local: a generator refusal over all 26 names, an analyzer refusal for a matrix local read with history, and a request inside the declaring function (complete C++, diagnostics, requests and the embedded manifest).

Add a compile test file built from the same source builders: the numeric tuple, typed and chart shapes for all 26 names and a neutral name, the remaining shapes for one name, the authored-collision scripts, and the string-with-history shapes, which do not compile for any name and are pinned to the compile outcome of the neutral name.

Add rows for int, bool, color and array locals, switch and while bodies, and method locals in nested, typed and reassigned form, and a preservation row for history of the derived price variables.

Expose the set of built-in spellings that the pass uses as a function and compare it with the test file's own inventory. The pass is unchanged.

Reword the changelog line to say what is checked.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…tured-request comparison

The first run of the captured-request rows found that for the three built-in names the support check flags as divergent (bar_index, last_bar_index, timenow) a function-local of that name gets the check's "diverges from TradingView semantics" warning at every read. The support check runs before scoping and reads names only, so a global or a parameter of that name gets the same warning; this change does not touch it.

Drop exactly those warnings from the subject's list before comparing it with the neutral name's list, and require that they occur for the names in the check's table and for no other name.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nd library functions

Add a direct row for the helper that restores authored spellings in diagnostic text: exact bindings, both clone suffixes, spellings that are not bindings, a missing text, and empty or absent annotations.

Compare the diagnostics (messages and hints) of every script shape of the scoped-locals tests, for all 26 built-in names, with those of the same script whose local has a neutral name; the support check's by-name divergence warning is the only difference allowed, and no diagnostic may carry a private spelling.

Add rows for a library function, version 5 and version 6, that declares a local spelled like a built-in variable, called directly and inside a request. The inliner qualifies the local with the module and function names, so the pass has nothing to rename there and the rows pin that.

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 6 changed function(s) (6 not verified).

Formal verification. PR-changed functions: 0/6 verified (0 proven, 0 may-equivalent, 0 distinguished) · 6 not verified (6 unsupported).

Not verified on this run: \_visit\_TypeDecl (unsupported), \_resolve\_func\_args (unsupported), \_visit\_format\_time\_call (unsupported), intrinsic\_keyword\_diagnostics (unsupported), validate\_intrinsic\_keywords (unsupported), \_visit\_FuncCall (unsupported).


Graphify review — findings

Adds a Pine v6 PF-E2081 diagnostic for the removed when argument on the seven strategy order and cancel calls, pointing users to an if around the call, while other unknown keywords keep PF-E2027. The support-contract wording for what v5 sources report in this case is the part most exposed to drift from the documented v5 behaviour. Makes str.format_time fall back to TradingView's defaults when arguments are omitted: "yyyy-MM-dd'T'HH:mm:ssZ" for the format and syminfo.timezone for the timezone. Passing "yyyy-MM-dd" and "UTC" explicitly restores the old output, and the change needs C++ regeneration and a relink. Rejects type field defaults containing calls or user-declared variable references with PF-E2087 in v6 scripts and libraries, while v5 sources keep the earlier handling. Fixed generated state members, helper types and numbered member families, including history-bearing locals in requested helpers, now go through the C++ name allocator.

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 — 4963 functions depend on the 1096 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: Analyzer — 105 callers, 15 callees
  • new: _assert_unchanged() — 3 callers, 6 callees
  • worse: _codegen_for() — 3 callers, 4 callees
  • new: _spelling() — 1 callers, 8 callees

Verification — 4963 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: 4294 function(s) in the blast radius were not formally verified this run

Cross-repo impact

Cross-repo impact

This change impacts 2 symbol(s) across 1 peer repo(s) — blast radius over derived cross-repo edges:

  • pineforge-4pass/pineforge-hpo — python/pineforge_hpo/transpile.py (via import_moniker, heuristic), tests/python/test_transpile.py (via import_moniker, heuristic)

1 peer repo(s) in this group are not yet indexed — cross-repo impact may be incomplete.

Cross-repo linkage is derived (SCIP-moniker matches are sound; name/contract matches are heuristic) and ADVISORY — it never affects this check's conclusion.

Docs that may be stale (advisory)

…and 10 more.

Formal verification

Could not verify: Could not verify \_visit\_TypeDecl.

The verifier did not have enough to check \_visit\_TypeDecl, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)

Could not verify: Could not verify \_resolve\_func\_args.

The verifier did not have enough to check \_resolve\_func\_args, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)

Could not verify: Could not verify \_visit\_format\_time\_call.

The verifier did not have enough to check \_visit\_format\_time\_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 intrinsic\_keyword\_diagnostics.

The verifier did not have enough to check intrinsic\_keyword\_diagnostics, 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: parameter `intrinsic` is annotated `IntrinsicFunc | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify validate\_intrinsic\_keywords.

The verifier did not have enough to check validate\_intrinsic\_keywords, 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: parameter `intrinsic` is annotated `IntrinsicFunc | None` — outside the synthesizable primitive/collection set

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)

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

return tree


def _assert_unchanged(source, reference, tmp_path, *, libraries=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 — _assert_unchanged()

fans out to 6 callees (efferent coupling).

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

)


def _spelling(source, name):

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

fans out to 8 callees (efferent coupling).

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

luisleo526 and others added 4 commits November 9, 2026 06:32
…arning control and the pinned receipt

A control for the by-name divergence warning in the diagnostics rows: the warning must be present exactly for the three names that have it (in the shapes that read the name) and absent for the other 23, and the table of those names is pinned. A row for a tuple element read on the right of := to an int variable (it needs the table of declaration bindings to follow the rename). Rows for a drawing local and a user-defined-type local read with history. An invariant that no built-in name contains the clone markers. Rows that pin the confirmed-bar request receipt of a request inside the declaring function, which carries the private spelling of the local in its expression field. A compile row for a string variable without history for each of the 26 names. The changelog bullet no longer says that the local behaves like a local with a neutral name in every respect and lists the exceptions.

Tests and one changelog bullet only.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ory in a requested helper

A string variable named open, high, low, close, hl2, hlc3, hlcc4, ohlc4 or volume that is read with history inside a function called through request.security is refused on main with one error (request.security helper-parameter history currently requires a direct OHLC/time bar-series binding). The rows compare that refusal, its code and its message, with what main gives for the same script, for each of the nine names, and pin that nothing else is refused: the other 17 names, the nine names as numeric variables and without a history read, and the shapes that main accepted.

Tests only. On the previous commit the refusal rows fail, because the built-in-named locals now take the path of an ordinary name.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d with history in a requested helper

A string variable named open, high, low, close, hl2, hlc3, hlcc4, ohlc4 or volume that is read with history inside a function called through request.security was refused on main with one error (request.security helper-parameter history currently requires a direct OHLC/time bar-series binding). The early pass that gives a function local spelled like a built-in variable a private name turned that refusal into accepted C++ that does not compile (a separate known defect: a string with a history read, used in a concatenation, is emitted as std::to_string applied to a string). The refusal is kept for exactly these nine names and the shapes where main refused; nothing else changes. Four existing tests compared these pairs with the neutral control and now expect the refusal for them (a few lines each, the pairs are named in the diff; no row and no assertion is removed), and the refusal rows of the previous commit now pass.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings in the fix for a declared local whose name is also a built-in
variable: it keeps its own type, history storage and reads. Two files
conflict with the earlier merges. CHANGELOG.md is resolved by keeping every
bullet of both sides, in merge order. tests/test_helper_series_words.py is
resolved by keeping the reference-word list of the generated-name merge (the
two store names are valid locals there) with the combined comment.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526 luisleo526 changed the title Use the documented str.format_time defaults, add PF-E2081 and PF-E2087, reserve the generator's own member names, and test helper-chosen request.security timeframes Use the documented str.format_time defaults, add PF-E2081 and PF-E2087, reserve the generator's own member names, test helper-chosen request.security timeframes, and give function locals spelled like built-in variables their own type and history Nov 9, 2026
@luisleo526
luisleo526 merged commit 4690e39 into main Nov 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant