Repository navigation
Operator typing and request-position if/switch: refuse the pairs and forms TradingView refuses, evaluate block-valued selections in the request's own context - #308
Conversation
…perand One analyzer relation types every unary and binary operator, the ?: condition and the compound assignment. A pair is refused only when both operand types come from a certain source (a literal, a declared hint, a typed parameter, a structured type, a variant-resolved builtin call); coarse registry results, VOID and UNKNOWN stay unresolved and are accepted. The proofs live in a side table that only the refusal relation reads: the symbol types, callable return types and storage facts that code generation reads keep their previous values, so no emitted C++ changes. A direct comparison with na is refused from the comparison start. A history read of an array, matrix, map or user-type field used as an operator operand is refused, and twenty such sources are pinned with their TradingView code family. A proven operand that fits no overload of an operator is refused whatever the other operand is, so a collection read beside an unresolved operand no longer reaches code generation. A comparison always has bool type. The call-site check resolves a function's free names in its definition scope. A while condition of type na and a switch discriminator that fits no case type are refused as TradingView refuses them. The operand-pair refusal has its own catalog code, pinned in the digest fixture. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The positional-argument change keeps PF-E2074 on main; the operator typing codes stay PF-E2076 to PF-E2078. The catalog, its digest pin and the code-delta list carry both sets. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…request's own context, refuse the forms not measured against TradingView A global bound to an if or switch whose arms carry statements, used as a request expression, was rendered by the chart renderer inside the request evaluator: bar_index read the chart counter (one bar behind the request's own) and block locals were chart members written from the evaluator. The selection is now lowered through the request-context renderer with evaluator-local block locals; var and varip writes are persisted by one lowering shared by if and switch arms. An if or switch placed in an inline switch arm, a ternary operand, a for bound, a type field default or an enum title is refused at its first token, as TradingView refuses it, and an unlowerable value stops generation with a located refusal instead of emitting a placeholder. Refused with located catalog codes: a write to a plain binding declared outside a requested selection (assignment, compound assignment, collection mutation, or a helper write); a request expression that yields no value; a requested timeframe selection with an empty-string arm (its value against TradingView is not measured). A numeric arm is no longer typed as a string. The string series storage is registered only for a string var declared inside the selection being lowered, so a plain request.security call gains no member, checkpoint field or shifted checkpoint slot. A timeframe member read is typed from the signature table (multiplier int, the is* members bool). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…E3105 and PF-E5083 in the catalog and the digest pin Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…al rows, timeframe member types, grammar slots Expected quantities come from TradingView tapes and formulas, not from engine output. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> # Conflicts: # tests/test_diagnostic_notes.py
Three test comments pointed at probe directories that exist only on the machine that measured them. Each now states the reply itself. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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 4 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 48 changed function(s) (1 sampled, not proven, 47 not verified).
Formal verification. PR-changed functions: 1/48 verified (0 proven, 1 may-equivalent, 0 distinguished) · 47 not verified (1 vacuous, 23 unsupported, 23 over cap).
Not verified on this run: \_generate (vacuous: never exercised), \_\_init\_\_ (unsupported), \_check\_incompatible\_binary\_operands (unsupported), \_numeric\_condition\_type (unsupported), \_visit\_Assignment (unsupported), \_visit\_BinOp (unsupported), \_visit\_FuncCall (unsupported), \_visit\_FuncDef (unsupported), \_visit\_IfStmt (unsupported), \_visit\_MemberAccess (unsupported), and 14 more; 23 changed function(s) beyond the cap and 0 skipped when the time budget ran out.
Graphify review — findings
Adds a single operator-typing relation to the analyzer that checks ?:, compound assignments (+=, -=, …) and switch-case == operands from lexical type facts and written-call facts instead of a float fallback. Mismatches are reported at the operand's first token, and an unresolved declaration stays unresolved rather than recovering a storage type. The shared _check_operator_operands path now carries unary, not and binary mismatch reporting, and if and ternary conditions go through the same ?: lookup, so a missing (na) condition is rejected too and v5 library definitions are skipped. Converts a CodegenInvariantError raised during code generation into a CompileError with an "Internal error: no value lowering for AST node" diagnostic at the node's location. If the node has no location, it falls back to a plain RuntimeError.
Worth a look
- Unary operator mismatch crashes on unpacking operands[-2:] —
pineforge_codegen/analyzer/base.py· Escalate · high- NOT verified (no proof, no reproducing execution) — needs human review
- Specific binary diagnostics also emit the generic 'cannot combine' error —
pineforge_codegen/analyzer/base.py· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
noton a non-bool operand emits two contradictory errors —pineforge_codegen/analyzer/base.py· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Cached operand type is reused for Identifiers even when per-call parameter_types are supplied —
pineforge_codegen/analyzer/base.py· 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 — 5335 functions depend on the 2153 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
Parser— 160 callers, 42 callees - worse:
Analyzer— 72 callers, 13 callees - worse:
CodeGen— 48 callers, 12 callees - worse:
resolve()— 22 callers, 3 callees - new:
resolve_operator()— 16 callers, 3 callees - new:
expression()— 3 callers, 8 callees - new:
_generator()— 3 callers, 7 callees - new:
walk()— 1 callers, 9 callees - …and 13 more — each is listed as a finding
Verification — 5335 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: 5320 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§ REQUIRED before claiming any change is done (lines 13-56): references changed symbolsbuild,sourceAGENTS.md§ What this is (lines 57-82): references changed symbolssourceAGENTS.md§ File map (lines 129-306): references changed symbolsblock,build,check,registration,sourceAGENTS.md§ Support contracts (lines 350-489): references changed symbolsarm,block,check,emit,expressionAGENTS.md§ Known codegen quirks (read before changing) (lines 490-948): references changed symbols_type_spec_from_expr,block,expression,sourceAGENTS.md§ How to add a new Pine v6 function (lines 949-988): references changed symbolssourceAGENTS.md§ How to run tests (lines 989-1016): references changed symbolsbuild,check,sourceAGENTS.md§ Conventions (lines 1017-1168): references changed symbolscheck,expression,sourceAGENTS.md§ Parity campaign gate (applies on EVERY harness) (lines 1186-1207): references changed symbolsrecordCHANGELOG.md§ Changes since 1.6.3 (lines 288-335): references changed symbolssource,void
…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 241 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 \_check\_incompatible\_binary\_operands.
The verifier did not have enough to check \_check\_incompatible\_binary\_operands, 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 \_numeric\_condition\_type.
The verifier did not have enough to check \_numeric\_condition\_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 \_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\_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\_MemberAccess.
The verifier did not have enough to check \_visit\_MemberAccess, 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\_Subscript.
The verifier did not have enough to check \_visit\_Subscript, 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\_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 \_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)
Could not verify: Could not verify \_\_init\_\_.
The verifier did not have enough to check \_\_init\_\_, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_generate\_cpp.
The verifier did not have enough to check \_generate\_cpp, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_build\_security\_expr.
The verifier did not have enough to check \_build\_security\_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)
No difference found (not proven): No behavior difference found in \_check\_security\_tf\_arms (not a proof).
The verifier ran both versions of \_check\_security\_tf\_arms on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: the synth_receiver tier. A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_emit\_security\_evaluator\_body.
The verifier did not have enough to check \_emit\_security\_evaluator\_body, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
Could not verify: Could not verify \_emit\_security\_linear\_helper\_call\_scoped.
The verifier did not have enough to check \_emit\_security\_linear\_helper\_call\_scoped, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: method — first parameter is `self`, which needs a constructed instance (not synthesizable)
· 5 grounded finding(s) anchored inline below; 16 more finding(s) on lines outside this diff (see the check run).
| return TypeSpec(left.kind, name=left.name or right.name, **arguments) | ||
|
|
||
|
|
||
| def resolve_operator( |
There was a problem hiding this comment.
resolve_operator()
16 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| def fail(value, kind, detail): | ||
| self._security_selection_refusal(value, kind, detail) | ||
|
|
||
| def expression(value, env): |
There was a problem hiding this comment.
expression()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| loc=value.loc) for a, b in zip(left, right)] | ||
| fail(owner, "statement", "tuple declaration with unresolved elements") | ||
|
|
||
| def block(body, outer): |
There was a problem hiding this comment.
block()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| states = dict(saved["states"]) if saved is not None else {} | ||
| types = dict(saved["types"]) if saved is not None else {} | ||
|
|
||
| def walk(value, env): |
There was a problem hiding this comment.
walk()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| if stmt.target.name in local: | ||
| local[stmt.target.name] = origin(stmt.value, local) | ||
|
|
||
| def walk(value, local): |
There was a problem hiding this comment.
walk()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
This pull request combines two reviewed changes onto the current main. Each section below is one change; the two share the diagnostics catalog and its digest pin, so their codes are numbered together (PF-E2076 to PF-E2078, PF-E3102 to PF-E3105, PF-E5083).
Part 1. Operator typing: refuse an operator pair TradingView refuses, at the operand
What changes
One analyzer relation now types every unary and binary operator, the
?:condition and the compound assignment. A pair of operand types is refused when TradingView refuses it, at the operand TradingView names, and only when both types are proven by a certain source: a literal, a declared type, a typed parameter, a structured collection type, or a builtin call whose overload is resolved from its arguments. A coarse registry result,voidandunknownstay unresolved and are accepted, as before.str.split).whilecondition of type na, and aswitchdiscriminator that fits none of its case types, are refused.No emitted-C++ change from this part
The operator relation touches no file under
pineforge_codegen/codegen. Over the pinned population (1,384 sources), the second wave (1,939 sources) and 52 import pairs: no source changes its generated C++ because of it. The only status changes are sources TradingView also refuses: five (string compared with a number) were translated by main and failed to build; two keep their refusal and only the reported column moved.New diagnostic codes
PF-E2076 (numeric operand), PF-E2077 (
?:condition must be bool), PF-E2078 (operator cannot combine the two operand types). The message of PF-E2066 now names the operand roles.Pins that flipped from accepted to refused (22)
All are forms TradingView refuses (its reply is cited per row in the test fixtures).
1-13: a direct comparison with na, thirteen spellings (string, string field, array; both operand orders; a call result).
14-18:
if na,na ? true : false,na and true,na or true,not na(the valid runtime control now uses an explicit bool).19:
na ? 1 : 0.20: the na condition row of the numeric-condition test.
21: a bool source compared with
!= 0in the lazy-clock test (the valid control uses!= false).22: the shadowed builtin in the finite-length test: a bool compared with an int is now refused at the comparison instead of translated.
Also: the
a > b == cparameter row now isolates the equality after a valid numeric comparison, and the original bool source is restored as its own refusal row. Four refusal locations were re-pinned to the operand's first token (22 to 13, 31 to 26, 15 to 9 twice); they have no exact-form position reply and are not claimed as measured column matches.Newly refused without an exact reply of that form
Refused as unproven pairs, witnessed by the operator family replies: an operator on a user object returned by a call; box, table, map and matrix operands under arithmetic and ordering; a string or bool under
- * / %; a proven float?:condition; a switch whose discriminator and cases mismatch other than string against int; enums compared across types under==. Replies for most of these exist (string and bool arithmetic, box, table, map, matrix, float condition, switch int against string, enum across types), all refused with the same code.Not in part 1
math.abs(2): TradingView accepts it, PineForge refuses it (PF-E2073): a recorded compile gap.string s = na;s == ""),float x = "1"and its siblings, the missing-string concatenation lowering, assignability and the bool wrapper typing are not claimed and stay open.math.round(close) + 1.5,math.abs(x) * 2,math.max(a, b) > 3,math.max(1, 2.0) * 3,math.round(close) % 2 == 0) are accepted by TradingView and by the relation, and translate on both trees."a" + closeposition and the nesteda[1][0]location are not compared against an exact reply.Part 2. Request-position if/switch: evaluate a block-valued selection in the request's own context, refuse the forms not measured against TradingView
The defect
A global bound to an
iforswitchwhose arms carry statements and used as a request expression was rendered by the chart renderer inside the request evaluator:bar_indexread the chart's counter, one bar behind the request's own counter, and block locals were chart members written from the evaluator. The request therefore returned the previous bar's value for such a source (a silent wrong value for a source that compiled), and several block-armed forms failed in g++ or were refused although TradingView accepts them.What changes
varandvaripwrites are persisted by one lowering shared byifandswitcharms.if/switchin an inline switch arm, a ternary operand, aforbound, a type field default or an enum title is refused at its first token, as TradingView refuses it (its replies: inline arm CE10013,forbound CE10161, field default and enum title CE10133, ternary operand CE10150 for the keyword in an operand position).vardeclared inside the selection being lowered, so a plainrequest.securitycall gains no member, checkpoint field or shifted slot.timeframe.*member read is typed from the signature table (previously every member was a string):timeframe.multiplieris an int, theis*members are bool,periodandmain_periodstay strings. This changes the inferred type of every chart expression that reads one of those members.Measured results (TradingView replies and the engine on the same context)
ifwith a block arm, a single-expressionifbound to a global, and a global read through a second global (main returned a constant for the last).if.timeframe.*typing correction; its five probes grade excellent with identical trades on both sides. Second wave (1,939): no C++ change from this part; seven sources TradingView refuses are now refused at the parser instead of failing to build. 52 import pairs: no change.Not in part 2 (existing gaps, tracked as follow-ups)
ifoutside a request is accepted by this compiler although TradingView refuses it; it is unchanged by this change.Verification
Both parts were reviewed on their own heads and again as the union on this head (two merge commits, no rewritten commit).
🤖 Generated with Claude Code