Repository navigation
Request-position if/switch: evaluate each arm in its consuming context - #306
luisleo526 wants to merge 3 commits into
Conversation
…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>
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 3 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 29 changed function(s) (3 sampled, not proven, 26 not verified).
Formal verification. PR-changed functions: 3/29 verified (0 proven, 3 may-equivalent, 0 distinguished) · 26 not verified (3 vacuous, 19 unsupported, 4 over cap).
Not verified on this run: \_generate (vacuous: never exercised), \_visit\_MemberAccess (unsupported), \_\_init\_\_ (unsupported), \_generate\_cpp (unsupported), \_build\_security\_expr (unsupported), \_emit\_security\_evaluator\_body (unsupported), \_emit\_security\_linear\_helper\_call\_scoped (unsupported), \_security\_fallback\_owns (unsupported), \_security\_helper\_call\_plan (unsupported), \_security\_lazy\_ta\_keys (unsupported), and 12 more; 4 changed function(s) beyond the cap and 0 skipped when the time budget ran out.
Graphify review — findings
Adds support for block-bodied if/switch selections as request.security timeframe arguments, lowering them to runtime lambdas keyed on a __selection_value_N slot. The arm walk, history-index collection and local-history checks have to handle selections with no else/default, and that case is where the lowering leaves the slot at its initial value. Rejects empty-string arms with PF-E3105 unless a measured chart timeframe backs them, which now also covers scalar switch arms; selection-local names are dropped from global members, and timeframe.* variables take their types from TIMEFRAME_VARIABLES with a STRING fallback. Converts CodegenInvariantError into a located CompileError diagnostic that tells the user to report it to PineForge, and falls back to a plain RuntimeError when the node carries no location.
Worth a look
- Block-arm if/switch with no else/default falls back to slot initial instead of the chart timeframe —
pineforge_codegen/codegen/security.py:488· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Local-history check crashes on if/switch without an else or default —
pineforge_codegen/codegen/security.py· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- TA history-index walk crashes on IfStmt with no else body —
pineforge_codegen/codegen/security.py:6652· 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 — 4948 functions depend on the 1687 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
Parser— 151 callers, 42 callees - worse:
Analyzer— 61 callers, 12 callees - worse:
CodeGen— 44 callers, 12 callees - new:
expression()— 3 callers, 8 callees - new:
_generator()— 3 callers, 7 callees - new:
walk()— 1 callers, 9 callees - new:
walk()— 1 callers, 8 callees - new:
block()— 0 callers, 9 callees - …and 5 more — each is listed as a finding
Verification — 4948 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: 4933 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-482): references changed symbolsarm,block,check,emit,expressionAGENTS.md§ Known codegen quirks (read before changing) (lines 483-941): references changed symbolsblock,expression,sourceAGENTS.md§ How to add a new Pine v6 function (lines 942-981): references changed symbolssourceAGENTS.md§ How to run tests (lines 982-1009): references changed symbolsbuild,check,sourceAGENTS.md§ Conventions (lines 1010-1161): references changed symbolscheck,expression,sourceAGENTS.md§ Parity campaign gate (applies on EVERY harness) (lines 1179-1200): 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 \_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 \_\_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)
Could not verify: Could not verify \_security\_fallback\_owns.
The verifier did not have enough to check \_security\_fallback\_owns, 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 \_security\_helper\_call\_plan.
The verifier did not have enough to check \_security\_helper\_call\_plan, 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 \_security\_lazy\_ta\_keys.
The verifier did not have enough to check \_security\_lazy\_ta\_keys, 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 \_security\_needs\_string\_series.
The verifier did not have enough to check \_security\_needs\_string\_series, 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 \_security\_switch\_as\_ternary.
The verifier did not have enough to check \_security\_switch\_as\_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 \_security\_tf\_runtime\_expr.
The verifier did not have enough to check \_security\_tf\_runtime\_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)
Could not verify: Could not verify \_security\_tf\_switch\_runtime\_expr.
The verifier did not have enough to check \_security\_tf\_switch\_runtime\_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 \_substitute\_tf\_input\_reads (not a proof).
The verifier ran both versions of \_substitute\_tf\_input\_reads 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.
No difference found (not proven): No behavior difference found in \_infer\_type (not a proof).
The verifier ran both versions of \_infer\_type 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 \_visit\_expr.
The verifier did not have enough to check \_visit\_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)
Could not verify: Could not verify \_emit\_block\_with\_assign.
The verifier did not have enough to check \_emit\_block\_with\_assign, 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\_body\_with\_assign.
The verifier did not have enough to check \_emit\_body\_with\_assign, 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\_block\_statements.
The verifier did not have enough to check \_visit\_block\_statements, 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\_stmt.
The verifier did not have enough to check \_visit\_stmt, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: 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 \_parse\_arm\_line.
The verifier did not have enough to check \_parse\_arm\_line, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 3 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_parse\_enum\_decl.
The verifier did not have enough to check \_parse\_enum\_decl, 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 118 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
· 10 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).
| 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.
|
|
||
|
|
||
| @pytest.mark.parametrize("name", PROBES) | ||
| def test_request_block_construction_only_exposes_compile_errors(name): |
There was a problem hiding this comment.
test_request_block_construction_only_exposes_compile_errors()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert isinstance(generator, CodeGen) | ||
|
|
||
|
|
||
| def test_registration_never_emits_statements(monkeypatch): |
There was a problem hiding this comment.
test_registration_never_emits_statements()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| CodeGen(Analyzer(ast).analyze()) | ||
|
|
||
|
|
||
| def test_expression_arm_timeframe_if_registers_a_typed_lambda(monkeypatch): |
There was a problem hiding this comment.
test_expression_arm_timeframe_if_registers_a_typed_lambda()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert registration(transpile(source("security-timeframe-switch"))) == expected | ||
|
|
||
|
|
||
| def test_scalar_timeframe_switch_is_dispatched_before_substitution(monkeypatch): |
There was a problem hiding this comment.
test_scalar_timeframe_switch_is_dispatched_before_substitution()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert "_security_helper_series_str_" in transpile(pine) | ||
|
|
||
|
|
||
| def test_requested_arm_average_has_one_lazy_site(): |
There was a problem hiding this comment.
test_requested_arm_average_has_one_lazy_site()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert run_emitted_tu(cpp, driver, opt="-O2", label=name).split() == expected.split() | ||
|
|
||
|
|
||
| def _generator(source: str) -> tuple[CodeGen, object]: |
There was a problem hiding this comment.
_generator()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Superseded by #308, which carries this change together with the related one on current main. |
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).
Request-position if/switch: declare the refusal codes PF-E3102 to PF-E3105 and PF-E5083 in the catalog and the digest pin
Request-position if/switch: value rows from formulas and tapes, refusal rows, timeframe member types, grammar slots
Expected quantities come from TradingView tapes and formulas, not from engine output.
🤖 Generated with Claude Code