Skip to content

Request-position if/switch: evaluate each arm in its consuming context - #306

Closed
luisleo526 wants to merge 3 commits into
mainfrom
fix/request-position-if-switch
Closed

luisleo526 wants to merge 3 commits into
mainfrom
fix/request-position-if-switch

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor
  • Request-position if/switch: evaluate a block-valued selection in the 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).

  • 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

luisleo526 and others added 3 commits November 2, 2026 10:47
…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>
@luisleo526
luisleo526 marked this pull request as ready for review November 2, 2026 03:27

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

View in Graphify

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)

…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):

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 — 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):

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 — 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):

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 — 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):

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 — 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — test_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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — test_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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — test_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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — test_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():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — test_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]:

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

fans out to 7 callees (efferent coupling).

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

@luisleo526

Copy link
Copy Markdown
Contributor Author

Superseded by #308, which carries this change together with the related one on current main.

@luisleo526 luisleo526 closed this Nov 2, 2026
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