Skip to content

Read-position run: judge calls and member/offset reads at every position they are written #1050

Closed

Description

@zonuexe

Parent issue for the read-position run, from the #895 triage and the walker run's split. It covers #935, #936 and #949. The architect's design notes are collapsed below; they include root causes R1–R7, witness tables, slice file lists and risks.

Root causes, in short

  • Existence, arity, inaccessible and on-null checks. These checks see only the call that the statement kind spells (checkable_calls). A call nested anywhere else is never judged, and its ArgValue::Call carries no span (R1, R2).
  • Member and offset reads. These are judged only in Assign and Return values (R3). The lowering already scans every expression in those positions and drops the data (R4).
  • Headers. Header conditions reach the IR only as escape data (R5).
  • First-class callables. These are refused by design (R6).
  • print(expr);. This lowers to a Barrier (R7).

Sibling default-surface FPs found while witnessing (fixed first, in S0b–S0d)

Decisions

  • D1 (diagnostic column): the read's own span, with one conscious fp-gate reseed. ADR-level; awaits the owner.
  • D2 (@): silence at the default floor, with a strict twin left open.
  • D3 (conditional positions): env-independent families judge there; value-dependent families decline.
  • D4: headers are in this run.
  • D5 (S5, by-ref oracle for method and ctor arguments): decide after S3's A/B.

ADR

ADR-0104 "A read is judged at every position it is written, through its carrier", written with S1. ADR-level; awaits the owner.

Slices

Architect's design notes

Read-position run — tables (master d543c7c, PHP 8.5.11)

Base binary: scratchpad/i1048-base/target/release/steins (steins --version → revision d543c7c,
built from the clean worktree i1048-base/wt). Probes: scratchpad/fable-rp-probes/w*/*.php;
PHP witnesses by scratchpad/fable-rp-phpwit.sh <file> (one process per function, handler installed).
Every "today" cell below is that binary's output on check --no-cache (default; strict adds nothing
for these ids except untyped.*). All line numbers are d543c7c.

0. Root causes (verified)

# Mechanism Where Effect
R1 The existence/arity/inaccessible-method/on-null checks iterate checkable_calls, which returns only the CallExpr the statement kind spells: Call, Assign.call, Return.call, PropAssign.value_call, Echo's operand calls crates/steins-infer/src/descent/call_sweep.rs:16-24; consumer crates/steins-infer/src/stmt_calls.rs:74-200 a call nested in an argument, operand, array element, new argument, throw/exit operand, ctor arg, named-arg value, literal spread, instanceof lhs, condition, foreach subject, static initializer is never judged (#935 s2–s11, s13–s20, s25, s29; w0d th/th2/ex/ar/ne/inst/spread/named)
R2 A nested call survives only as ArgValue::Call(String, Vec<ArgValue>) / ArgValue::MethodCall{callee,args,named} — no span, no callee_ref, no arg spans, no positional_only crates/steins-syntax/src/ast.rs:1403,1423; built at lower_expr.rs:559-600, 606-614 even a consumer that descended ArgValue could not emit at the call or run arity
R3 Member/offset reads are judged by pattern-matching Assign{value: X} / Return{value: X} for X ∈ {OffsetRead, PropFetch, ClassConst, Coalesce} (+ Destructure.source) crates/steins-infer/src/stmt_checks.rs:45-47, 70-71, 81-82, 103-119 echo, operand, argument, cast, interpolation, comparison, array element, ?: lhs, nested level, compound read-half are silent (#936 c2–c8, c12–c20, c23; props p2,p3,p8–p11,p13–p15,p20,p22,p25,p28? (p28 return $c->nope is reported); #949 r2–r5, r10–r19, r24–r28, r33, r34, r36)
R4 The lowering already walks every expression of the four entry-env statement kinds for string_contexts (lower_stmt.rs:750-779, 796-841), guards (lower_guards.rs:28-50), invalidated (lower_stmt.rs:1983-2034), runs, hoisted matches (lower_stmt.rs:1164-1191) — but none of these records read positions with spans — the data exists at lowering; it is dropped
R5 Header expressions (if/elseif/while/for/do-while cond, foreach subject, match/switch subject) are lowered to CondExpr/CondOperand; a call that is the operand survives as CondOperand::Other{call}/CondExpr::Call (ast.rs:2853-2857, 2931), used for escape/sweep only (branch.rs:97-105) lower_stmt.rs:974-990 (Foreach.subject: Option<String>), branch.rs:62-93 header calls/reads are never judged; a decided-true if marks only bodies dead (branch.rs:84-93), not later conditions
R6 First-class callables: named_call has no PartialApplication arm (lower_stmt.rs:850-872), and both ladders refuse the shape on purpose: leg (l) absence.rs:67-72, leg (d) absence.rs:1117-1122 — $c = missing_fn(...), $f = $c->gone(...), C::gone(...) silent; PHP throws at creation (witnessed s10, s11, m4, m8)
R7 print(expr) as a statement falls to _ => Barrier (lower_stmt.rs:1851) — env cleared after every print (w0d pr: dumpType($x) → unknown) and the operand's calls unjudged

1. PHP semantics table: carriers (witnessed, PHP 8.5.11, handler installed)

Carrier (innermost construct around the read) offset $a['nope'] property $c->nope private const A::P / undefined A::NOPE call nope_fn() / $c->gone()
Plain operand (+, ., ===, !, -, cast, interpolation, echo, print, return, = rhs, array element/key, new arg, throw/exit operand, instanceof lhs, yield, match subject, ?: lhs) warns (r2–r5, r10–r16, r24–r27, r33, r36) warns (p2, p8, p9, p13–p15, p22, p25) Error (c2–c5, c9, c12–c20, c23) Error (every silent row of #935)
Argument, by-value callee (strlen, strtoupper) warns (r28, r42) warns (p3) Error (c12) Error (s2, s7, s13, s14)
Argument, by-ref callee (byref(&$v), sort, settype, preg_match) silent — key created (r9; r31 is a later TypeError) silent — property created, deprecation only (p7, h1, h7, h8) n/a (not an lvalue) Error (a call is always evaluated)
isset/empty/?? lhs/unset/??= lhs silent (r6, r7, r8, r20, r23) silent (p4, p5, p6, p26 private, p27 private) Error — A::NOPE ?? 1, A::P ?? 1 throw; isset(A::X) is a compile error n/a
?? final arm today's ladder (r43 silent when lhs settles) same Error Error
&&/` /?:` rhs, ternary arm, under an undecided test may run (r34 warns) may run
…under a decided guard (function_exists, defined, method_exists, property_exists, class_exists, version cmp) dead arm silent (r29, r30 by isset) dead arm silent (p12, p24) dead arm silent (c10, c11) dead arm silent (s21–s24, m10, m11)
@ operand warns, and the handler runs (error_reporting()=4437 inside it; r32, p23) same n/a n/a
compound lhs (++, .=, +=) warns (r17–r19; A7 "whitelist-eligible") warns + deprecation (q17, q18) — silenced project-wide by the write obstacle n/a n/a
write lhs, & operand, foreach/destructure target silent silent (deprecation only) n/a n/a
first-class callable f(...) / $o->m(...) / C::m(...) n/a n/a n/a Error at creation (s10, s11, m4, m8)
same statement, base passed by-ref before the read (f2($a) . $a['nope']) silent (r41) silent n/a n/a
same statement, read before the by-ref pass ($a['nope'] . f2($a)) warns (r40) warns n/a n/a
header: if/while/match subject/foreach subject warns (r14, r37, r12, r22) warns (p13, p20) Error (c14, c15) Error (s3, s8, s25)
static $x = f() — — — Error (s9)
closure / arrow body own scope (r39 reports via the closure's own trace) own scope own scope own scope (s27 reported today)

2. The rule (full table, by family class × position class)

Family classes:

  • E (env-independent): call.undefined-function, call.undefined-method (receiver class from the entry store; a test cannot change an allocation's class), call.inaccessible-method, phpdoc.undefined-method (declared lane; conservative without in-statement narrowing), call.too-few-arguments, call.unknown-named-argument, call.printf-too-few-arguments (literal format only), class-const.undefined, class-const.inaccessible, property.inaccessible.
  • V (env/value-dependent): offset.missing, offset.on-unsupported, offset.undeclared, offset.maybe-missing, property.undefined, property.maybe-undefined, property.on-non-object, call.on-null, call.on-non-object, preg.invalid-pattern (folds the pattern from env).
  • Not positional: type families (reach nested calls through the value-lane descent already — s28 reports at 33:31), dumps/asserts, discarded-call (statement only), descents/summaries, escape/sweep (escape_nested_calls already recurses).
Position class E V
Plain, entry-env statement kinds (expression stmt incl. throw/exit/print, return, echo, <?=) judge judge, unless the read's base name has an InvalidatedVar entry in this statement that is opaque or not provably by-value (by_value_survivors) — decline (covers r40/r41 soundly, loses r40)
Argument position (the read is an argument) judge (a call) judge iff arg_is_by_value(callee, pos); unresolved/method/ctor callee → decline (A7 unchanged; S5 extends the oracle)
Conditional (ternary arms, ?: rhs, &&/` /and/orrhs,??` rhs non-final)
isset/empty/?? lhs/unset/??= lhs operand n/a silence for offsets and properties; not a carrier for class constants (Error) — judge E there
@ operand judge (an Error is not suppressible) owner D2; recommend silence at the default floor
compound lhs read-half n/a offsets: judge (A7); properties: judged but the write obstacle silences by construction — row documented, no code
write / & / foreach / destructure target n/a silence
first-class callable existence only (call.undefined-function/-method, class.undefined already covers the class); not arity/type/descent/no-effect/dumps n/a
hoisted structured match arm the arm's own trace (today) same
refused (unstructured) match arm, switch case body that is Opaque silence (first-match: not a position the walk can claim is reached) silence
closure/arrow/anonymous-class/class body own scope own scope
static $x = <expr> judge (scope is poisoned anyway) decline (poisoned)
header: if cond, while cond, match/switch subject, foreach subject (first evaluation = entry env) judge at evaluation (S4) judge at evaluation (first iteration is in the env the walker holds)
header: elseif cond judge at evaluation; a cond after a decided-true earlier branch is dead (fixes w0b e1 for constant.undefined/class.undefined too) decline (conditional)
header: for cond (after init), do-while cond (after body) judge at evaluation with the env the walker has there (S4) decline in S4 (sound; precision later)
declaration constant expressions (param default, property default, const/enum initializer) out of this run (own root: evaluated at declaration/first use, no walk) n/a
attribute arguments never (evaluated only on reflection) never

Double-judging guard: the lowering emits every position (uniform, testable); the walk builds
judged_calls = checkable_calls ∪ positions deduped by CallExpr.span, and check_read_positions
iterates positions only (the Assign/Return arms are deleted), so one mechanism remains.

3. Per-issue witness tables (today = d543c7c)

#935 functions (w935/w935.php)

fn PHP today after
s1 nope_fn(); Error 4:23 same
s2 id(nope_fn()), s7 id(two(1)), s13 echo strlen(nope_fn()), s14 return_me(nope_fn()), s15 return id(two(1)), s16 return two(1)+1, s4 nope_fn()+1, s5 [nope_fn()], s29 [two(1)], s17 (string)nope_fn(), s18 "a".nope_fn(), s20 nope_fn() ?? 1 Error / ArgumentCountError silent S2 reports at the nested call
s19 nope_fn() ? 1 : 2 (cond is Plain) Error silent S2
s21–s24 (function_exists/isset guards) ok silent silent (dead extent / conditional)
s3 if (nope_fn()), s8 foreach (nope_fn() as …), s25 while (nope_fn()) Error silent S4
s9 static $x = nope_fn(); Error silent S2 (static initializer row)
s10 missing_fn(...), s11 (new X)->nope(...) Error at creation silent S2 (FCC row)
s12 echo nope_fn(), s26 match arm, s27 arrow body, s28 type nested — 16:29, 31:54, 32:37, 33:31 reported unchanged (dedupe)
w0d: print(nope_fn()), throw new E(1) (ctor arity), throw nope_fn(), exit(nope_fn()), [nope_fn() => 1], new E(nope_fn(), 2), nope_fn() instanceof E, strlen(...[nope_fn()]), two(b: nope_fn(), a: 1) Error silent (print also clears the env: 3:67 debug.type: unknown) S2

#935 methods (w935/w935m.php)

fn PHP today after
m1 $c->gone();, m5 echo $c->gone() Error 3:37, 7:42 same
m2 strlen($c->gone()), m3 [$c->gone()], m7 $c->gone()+1, m12 (new C)->gone()+1 Error silent S2
m6 if ($c->gone()) Error silent S4
m4 $c->gone(...), m8 C::gone(...) Error at creation silent S2 (FCC)
m9 $c->m(1, 2) (extra args), m10/m11 method_exists guards ok silent silent (m10/m11: the guard folds No → dead extent; pin)

#936 class constants (w936/w936.php) — today only c1 (3:22) and c21 (24:24)

rows PHP after
c2 echo A::NOPE, c3 1 === A::NOPE, c4 +1, c5 [A::NOPE], c8 ?? 1, c9 === null, c12 strlen(A::NOPE), c13 concat, c16 ? : cond, c17 cast, c18 array key, c23 !A::NOPE Error S3
c6 echo A::P, c19 A::P + 1, c20 A::P === 2 Error (private) S3 (class-const.inaccessible)
c7 $s === Suit::Clubs (return operand, comparison) Error S3
c14 if (A::NOPE), c15 match (A::NOPE) Error S4
c10 defined('A::NOPE') ? A::NOPE : 1, c11 defined(…) && A::NOPE ok silent, via S0c (today defined('A::X') folds Maybe, see w0c)
c22 echo A::class ok silent (::class excluded at collection)

#936 properties (w936pr/reads.php) — today p1 (3:38), p16 (17:38), p28 return $c->nope (25:44)

rows PHP after
p2 echo, p8 interpolation, p9 +1, p14 cast, p15 […], p22 $c->nope['k'] (outer read), p25 === null warns S3
p10 echo $c->priv, p11 $c->priv + 1 Error (private) S3 (property.inaccessible)
p3 strlen($c->nope) warns S3 if arg_is_by_value (builtin: yes) — but see S0b: with the conservative obstacle, nope passed as an argument anywhere becomes an obstacle, so this row is silence until S5
p4 isset, p5 ??, p6 empty, p26 $c->priv ?? 1, p27 isset($c->priv) ok silent (carrier)
p12 property_exists(…) ? :, p24 isset(…) && … ok silent (decided guard / conditional)
p13 if ($c->nope), p20 foreach ($c->nope …) warns S4
p23 @$c->nope warns, handler runs owner D2

#949 offsets (w949r/reads.php) — today only r1 (3:40)

rows PHP after
r2 echo, r3 strlen((string)…), r4 $a["n"]["zz"] (nested level: base $a["n"] resolves through the same fact), r5 1 + …, r10 concat, r11 interpolation, r13 cast, r15 ? : cond, r16 […], r24 -, r25 !, r26 === 1, r27 return … + 1, r33 $a["nope"]["deeper"] (outer level), r36 ?: 1 lhs warns S3
r17 ++, r18 .=, r19 += warns (A7) S3 (compound read-half: the OffsetWrite/Barrier statement kinds carry the position)
r28 strlen($a["nope"]), r42 warns S3 (arg_is_by_value builtin)
r9 byref($a["nope"]), r31 sort($a["nope"]) silent / TypeError silent (by-ref carrier)
r6 isset, r7 ??, r8 empty, r20 ??=, r21 &, r23 unset, r35 array_key_exists ok silent
r29, r30 (decided isset guards), r34 count($a) > 0 && $a["nope"] (undecided) ok / warns silent (conditional, V family) — r34 is a known false negative, recorded
r40 $a["nope"] . f2($a) / r41 f2($a) . $a["nope"] warns / ok both silent (same-statement by-ref rule); r40 recorded as a false negative
r12 match (…), r14 if, r22 foreach, r37 while warns S4
r32 @$a["nope"] warns, handler runs owner D2
r43 $a["k"] ?? $a["nope"] ok silent (today's coalesce_arm_settles)

Sibling holes found while witnessing (all default-surface, all on d543c7c)

probe PHP Steins today root slice
w0a/holes.php h1 byref($c->viaref); $x = $c->viaref; silent (property created by ref) property.undefined 4:56 — FP property_writes collects assignment/inc/foreach lvalues only (lower_decl.rs:224-238); call-argument positions, &$o->p operands and destructure targets (tree.rs:783-796 does not recurse into array patterns) are not writes S0b
h7 settype($c->st, "int"); $w = $c->st;, h8 preg_match(…, $c->pm); $w = $c->pm; silent FP 9:61, 10:69 same S0b
w0c/defined.php d4 if (defined('A::P')) { return A::P; } (P private) ok — defined() returns false for an inaccessible constant class-const.inaccessible 6:52 — FP constant_defined_verdict (existence.rs:333-346) knows only global constants; a C::K spelling answers Maybe S0c
d5 if (defined('A::NOPE')) { $x = A::NOPE; … }, d6 if (!defined('A::NOPE')) return 0; $x = A::NOPE; ok class-const.undefined 7:48, 8:61 — FP same S0c
w0b/elseif.php e1 if (true) {…} elseif (NOPE_CONST) {…} ok (cond never evaluated) constant.undefined 2:53 — FP walk_if marks only bodies dead (branch.rs:84-93); elseifs: Vec<(CondExpr, Vec<Stmt>)> carries no cond span S4 (needs the header record)
w0d/misc.php pr print(nope_fn()); dumpType($x) Error debug.type: unknown 3:67 (env cleared), call unjudged print → Barrier (lower_stmt.rs:1851) S2
#297 [$a, $b] = f($bad) — call not in checkable_calls call_sweep.rs:16-24 lacks Destructure S0d

4. Slices

Slice Issue(s) Files Depends on Parallel with
S0 (no PR) baselines public strict+default JSON, effect-diff --set-baseline, the three dry-run transforms, cargo xtask nsrt, cargo xtask perf corpus/guzzle__guzzle --no-php --bless, private fp-gate under lockf with --deadline — —
S0b new issue: property-creating argument/reference/destructure positions are write obstacles crates/steins-syntax/src/lower_decl.rs (collector arms for call-argument property fetches, & operands, foreach by-ref targets), crates/steins-syntax/src/tree.rs (push_lvalue recurses into Array/List/LegacyArray patterns), tests crates/steins-infer/tests/it/member_absence.rs; no shard type change ((HashSet<String>, bool) keeps its type, so no SCHEMA_VERSION bump; verify shard.rs:149,470) — S0c, S0d, S1
S0c new issue: defined('C::K') folds against the class-constant reach and the scope's visibility crates/steins-infer/src/existence.rs (constant_defined_verdict gains a :: arm: No when absent from enumerate_const_reach with a clear A2i/A2ii, or declared but private_invisible/protected-blocked from w.scope's class; Yes when declared, accessible and unconditionally declared; else Maybe), tests existence_guard.rs, reference_suite_pins.rs, inaccessible_members.rs — S0b, S0d, S1
S0d #297 crates/steins-infer/src/descent/call_sweep.rs:16-24 adds Destructure { call: Some(c), .. }; tests destructure_source_read.rs, arity.rs; the walker arm (walk.rs:661-665) ignores the summary, so parity with Assign is the only change — S0b, S0c, S1
S1 IR crates/steins-syntax/src/ast.rs (ReadPosition { kind: Call(CallExpr) | Offset{base,key} | Prop{var,prop} | ClassConst(sc,name), span, carrier: Carrier }, Carrier::{Plain, Argument{callee: Option<NameRef>, position}, Conditional, IssetLike, Suppressed, Write, CompoundRead, FirstClassCallable}, Stmt.read_positions), new crates/steins-syntax/src/lower_read.rs (one CST scan over the four entry-env statement kinds + Statement::Static; stops at function-likes/class-likes and at a hoisted structured match; mirrors scan_string_contexts's boundary), lower_stmt.rs central fill, docs/internal-spec/trace-ir.md, syntax tests (new read_positions.rs). Byte-identical check output (no consumer). Trace payloads need no SCHEMA_VERSION bump (decoded past the gate; analyzer-version hash refuses old generations) — S0b, S0c, S0d
S2 #935 (+ FCC, print, static, ctor/throw/exit/named/spread positions) crates/steins-infer/src/stmt_calls.rs (iterate judged_calls, E/V split per position class), absence.rs (legs (l) :67-72 and (d) :1117-1122 flip: an FCC is an existence site; the comment goes), arity.rs (FCC stays declined — the shape is argument-less), crates/steins-syntax/src/lower_stmt.rs:1851 (print → Echo-like with its calls and invalidation), tests undefined_function.rs, undefined_method.rs, arity.rs, inaccessible_members.rs, on_non_object.rs, reference_suite_pins.rs, changelog changed/ S1 S3 (different functions in absence.rs; otherwise disjoint)
S3 #936, #949 crates/steins-infer/src/stmt_checks.rs (iterate positions; delete the Assign/Return arms), offsets.rs (span = the read; doc block §A7 rewrite), absence.rs (property/const doc), inaccessible.rs, non_object.rs, tests offset_family.rs, shape_strict_leg.rs, shape_reads.rs, member_absence.rs, inaccessible_members.rs, on_non_object.rs, coalesce_value.rs, destructure_source_read.rs, changelog S1 (and D1) S2
S4 headers (#935 s3/s8/s25, m6; #936 c14/c15; #949 r12/r14/r22/r37; the dead-elseif-cond FP) ast.rs (Header { span, read_positions } on If.cond/each elseif, While, For, DoWhile, Foreach.subject, Match.subject, switch-true labels), lower_stmt.rs (fill), branch.rs (walk_if judges each cond's positions where it evaluates it and mark_dead_spans the conds after a decided-true one; walk_match subject), loops.rs (while/for/do-while/foreach), tests branch_analysis.rs, loop_bodies.rs, match_switch.rs, constant_undefined.rs, class_undefined.rs S1, S2, S3 —
S5 (optional, decide after S3's A/B) by-ref evidence for method/static/ctor arguments; unlocks the "declined" argument rows for properties/offsets, ADR-0070 survival through method calls, and #947 d/e/g ast.rs (InvalidatedVar.sites carries a Callee-shaped site), lower_stmt.rs:1983-2034, by_value.rs (arg_is_by_value resolves an exact-receiver method / named ctor through resolve_call_target with the store), walk.rs by_value_survivors callers, S0b's obstacle reads the same oracle (shard type change → SCHEMA_VERSION 26 + generation-schema.md row) S1–S3 —

Landing order: S0 → {S0b, S0c, S0d, S1} (file-disjoint, any order) → S2 ∥ S3 → S4 → (S5).

5. Expected movement and exposure

Public corpus today (head JSONs scratchpad/i1037-*-head-*.json, 2026-10-11 00:49, 10 packages):
default total 31 findings, strict 17,185; of the affected ids: default call.on-non-object 1; strict
offset.maybe-missing 2 (composer, console), call.on-non-object 1; every other affected id 0.
So the public A/B is a non-regression check; the libraries run, so new TPs are expected to be rare
and each one is witnessed on PHP. Column moves under D1 are classified "same finding, moved to the read".

Exposure (rough rg over corpus/, 6,670 files): class-constant fetches 4,988 of which 417 are
whole-assignment/return (the whitelist covers ~8%); offset reads ~6,456 of which 619 whole
(~10%); lines with a call nested in an argument ~64k; first-class callables 72; if conditions with
a call 9,121; foreach subjects with a call 214; echo/print with a call/member 63.

Private fp-gate: the only place volume shows. Run per slice with --deadline, lockf -k, and report
counts only; the fp-gate key includes the column (xtask/src/gate.rs:887), so D1 forces one reseed.

Reference suites: re-run the mago/mir fixture sets named in #935/#936/#949 per the survey memo
(~/.claude/projects/.../memory/steins-reference-suite-survey.md); every fixture that flips lands as a
row in reference_suite_pins.rs.

6. Soundness risks, by slice

  • S0b: recall loss for property names ever passed as an argument (conservative obstacle). Measured: property.undefined fires 0 times on the public corpus; private count before/after.
  • S0c: a Yes fold marks the !defined() branch dead — only when the class and constant are unconditionally declared and the dam is clear (A2i/A2ii legs reused). Risk otherwise nil (today is Maybe).
  • S1: lowering cost — a sixth per-statement CST scan in a crate that is 45% of wall time (docs/agents/profiling.md); measure cargo xtask perf corpus/guzzle__guzzle --no-php; if cold analyze moves >3%, merge the scan into scan_string_contexts (same boundary) before S2.
  • S2: (a) volume — nested positions are ~10× statement positions, so any extension the analyzing PHP lacks (mb_*, ctype_*) now fires ~10× more call.undefined-function; same posture as today (ADR-0049 A6/A2ii, [runtime] declarations), different volume — the private corpus decides whether a [runtime] nudge belongs in the message; (b) conditional positions under an undecided test are judged (D3) — the same rule as an if body; the guard sweep's dead extents and mark_dead_operand must be consulted per position; (c) FCC: arity/type/descent/no-effect must never see one — tests pin every consumer of checkable_calls; (d) dedupe by span, or echo nope_fn() reports twice.
  • S3: (a) a Conditional position must decline for V families even when the test does not mention the place (simplest sound rule; r34 is a recorded false negative); (b) the same-statement by-ref rule reads stmt.invalidated through by_value_survivors — the opaque entry (method call anywhere in the statement) declines every V read of that base; (c) D1 moves every existing finding's column; (d) property.inaccessible at isset/??/empty is silent in PHP (p26, p27) while class-const.inaccessible is an Error there (A::P ?? 1) — the carrier table is per family, not per construct.
  • S4: judging a header against the entry env is sound only for the first evaluation (while), the entry (if, match/switch subject, foreach subject), after init (for) and after the body (do-while); each walker already holds that env; elseif conds are Conditional for V.
  • S5: a wrong by-value answer for a method would let a by-ref created key/property be reported — the oracle refuses anything but an exact-receiver, non-overridable resolution (final/private or exact allocation with a complete descendant closure).

Activity

  1. zonuexe commented on Oct 11, 2026

    @zonuexe
    ContributorAuthor

    Owner decision 2026-10-11: D1 adopted — a member/offset read's finding sits at the read's own span (ADR-0104); the column move gets one conscious fp-gate reseed.

  2. 44 remaining items

  3. zonuexe commented on Oct 14, 2026

    @zonuexe
    ContributorAuthor

    S5 (by-ref evidence for method/static/ctor arguments) is deferred per D5 after S3's A/B: on the public corpus the method/ctor argument declines cost no finding, and on the private corpus S3's +51/+4 movement came from other positions. Revisit if a corpus shows a by-ref-method decline hiding a real finding.

  4. zonuexe commented on Oct 14, 2026

    @zonuexe
    ContributorAuthor

    Run complete

    slice PR outcome
    S0b #1054 property-creating argument, & and destructure positions are write obstacles
    S0c #1053 defined('C::K') folds against class-constant reach and visibility
    S0d #1052 a destructure's source call is checkable (#297)
    S1 #1062 Stmt.read_positions IR with carriers, ADR-0104 (40-byte positions, phpunit RSS +2.6%)
    S2 #1066 nested calls judged at every position; FCC is an existence site; stored guards; print
    S3 #1069 member, offset and class-constant reads judged at every position
    S4 #1077 header reads judged where PHP evaluates them; a condition after a decided-true one is dead
    S5 — deferred per D5 (no measured loss from the method/ctor by-ref decline)

    The owner decision D1 (findings at the read's own span) landed with one conscious reseed of the private ledger.

    Follow-ups filed during the run: #1065 (polynomial residue, including new chains), #1067 (anonymous-class constructor arguments; static inaccessible at conditional positions), #1068 (by-ref alias across statements and implicit __toString), #1072 (guards do not reach closures in the guarded arm), #1079 (optional-key narrowing by empty() / === [] / ?? null).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions