Skip to content

Drop receiver narrowings for []= writes in content write-back positions - #391

Merged
zonuexe merged 1 commit into
masterfrom
claude/issue-388-block-index-opwrite
Oct 9, 2026
Merged

zonuexe merged 1 commit into
masterfrom
claude/issue-388-block-index-opwrite

Conversation

@zonuexe

@zonuexe zonuexe commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes #388

Summary

h[:a] ||= "t" inside [1].each { … } (or &&=/op=, a plain
h[k] = v, a masgn/for/rescue index target) left the pre-block
h[:a] narrowing live: apply_mutation_effects ran only
invalidate_after_call semantics — a keyed slot drop for a stable
[]= key, nothing for the compound forms — so the post-block
h[:a].frobnicate fired for "s" | 1 where the oracle emits nothing.

The oracle drops the record because it REBINDS the captured receiver:
content_writeback_block_captures (invoke_call) for a literal
block/lambda, loop_content_writeback for a while/until body. This
ports that positional fact: path_content_writeback walks the
flow_children descent to the write and reports a crossed
FlowEdge::Barrier (deferred body) or a while/until body edge —
and apply_mutation_effects then drops every narrowing rooted at the
receiver, whatever drop_key carried.

Boundary (all probe-verified against e59b7b89)

Drops (silent on ref): literal block (each, loop do, unknown
m {…}), lambda/proc/->() {} bodies, while/until bodies —
including nested (while under if, block under for), and any []=
form inside them (compound, plain, different slot, index target).

Keeps + joins (fires "s" | "t" | 1 on ref): if/case/when
arms, &&/||/?: operands, begin/rescue/else/ensure,
rescue-modifier operands, for bodies, while/until predicates.

for is the trap: it lowers to Node::Loop with predicate = the collection, so the discriminator is index.is_empty() && index_writes.is_empty() (a for always has an index target — except
targets binding no local like for @a in xs, which misread as while;
that direction declines to silence, never a new fire).

Measured

  • Fresh-dir probes on ~35 rows (all forms × all positions): every
    pre-existing FP silent now; joined positions still fire.
  • harness/gate.sh: docs_check, cargo test -p rigor-cli,
    cargo test -p rigor-infer, run_snapshot.rb — all pass.
  • Standing sweep fp_audit --gaps --sweep: 0 FP / 815 gaps on the
    standing corpora — identical to master's count, no gap delta.
  • New regression tests: writeback_index_write_drops_indexed_narrowing
    (14 silent rows) + joined_index_write_keeps_indexed_narrowing
    (9 firing rows + the straight-line "s" | "t" | 1 union control).

Known non-parity left behind (all safe-side)

  • Joined positions report for "s" | 1 where the oracle says
    for "s" | "t" | 1 — the pre-existing missing stored-slot union on
    conditional arms (both fire; message drift only).
  • An in-body h[k] read where the body also contains an h[k] op= v
    now reads the widened-Dynamic receiver and declines — e.g.
    each { h[:a].m; h[:a] ||= "t" } loses an oracle for "s" | 1 in-body
    diagnostic (coverage loss; the descend env is the end-of-file flat env
    and cannot hold both the write-back drop and the pre-write record).
  • for with a target that binds nothing (for @a in xs,
    for A in xs, for ::A in xs, for $g in xs, for a.b in xs,
    for *w in xs, for * in xs) misreads as while/until — both
    index and index_writes are empty — so a h[k] op= v in its body
    drops the record where the oracle joins it: silent vs
    for "s" | "t" | 1 (coverage loss on a rare shape; tightening the
    discriminator needs a Node::Loop kind flag, deferred).
  • h&.[]=:a arity (wrong-arity on Hash#[]=) is a pre-existing port
    gap on master — unrelated rule, untouched here.

Generated with Devin

Review (fd7e07d)

Opus 5.5 + Grok 4.6, independent probes on both engines: Approved ×2 —
headline and every boundary row verified, no new false positives.

Filed follow-ups: #392 (for discriminator hole — the full
binding-less-target list above), #393 (retry operand leaking an
indexed write — pre-existing on master).

…ions

A `local[k]` store replayed through a literal block/lambda body or a
`while`/`until` body rebinds the receiver on the reference —
`content_writeback_block_captures` for the non-escaping-block call,
`loop_content_writeback` for the iterated body — and the rebind drops
every indexed narrowing rooted at it. The port's `[]=` mutation applied
only `invalidate_after_call` semantics (a literal-key slot drop, nothing
for the compound `h[k] op= v`), so `h[:a] ||= "t"` inside `each {}` left
the pre-block `h[:a]` -> `"s" | 1` record live and a post-block
`h[:a].frobnicate` fired a diagnostic the oracle never emits
(rigor-rs#388).

`path_content_writeback` walks the `flow_children` descent to the write
and reports whether it crosses a Barrier edge (a deferred body — the
capture-writeback rebind) or a `while`/`until` body edge. `for` joins
instead of writing back, so its body stays excluded; joined positions —
`if`/`case`/`rescue`/`ensure` arms, `&&`/`?:` operands, rescue-modifier
operands — keep the record their arm's own eval re-recorded.

Closes #388

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@zonuexe
zonuexe marked this pull request as ready for review October 9, 2026 20:37
@zonuexe
zonuexe merged commit 2fb2950 into master Oct 9, 2026
5 checks passed
@zonuexe
zonuexe deleted the claude/issue-388-block-index-opwrite branch October 9, 2026 20:41
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.

FP: in-block index op-write (h[:a] ||= v) leaves a stale slot record for the post-block read

1 participant