fix(compiler): track callee seed reads beside write effects - #102
fix(compiler): track callee seed reads beside write effects#102thiremani wants to merge 3 commits into
Conversation
A body whose last statement always writes can still depend on the incoming output seed (`y = x > 0 x` then `y = y + 1`). The CFG treated every all-MustWrite callee as reading no seed, so `a = 20` followed by `a = MaybeIncrement(-1)` was rejected as an unused overwrite although lowering already produced 21. Publish a per-output SeedEffect (NoSeedRead/MaySeedRead) beside BodyOutputEffects, derived by an order-sensitive fold: explicit reads, resolved format markers, and calls to seed-reading callees count until a raw MustWrite replaces the output, and the fact is sticky. Call sites record CalleeReadsSeed for every named target whatever the ABI; the CFG turns it into a pre-write read of a defined destination. ReadsSeed keeps its boundary-resolution meaning, so the write fold still discounts boundary-manufactured MustWrites and accumulators stay MustWrite. SCC settlement requeues callers on seed growth as well as write weakening, and an unpublished seed fact at a call site is an ICE. Overwrite-before-read callees and fresh targets keep their existing diagnostics. Lowering, public symbols, and prototypes are unchanged; PIR is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tick the section 1 acceptance criteria, point the backlog row at the PR, and defer the canonical description to PIR plan section 15. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An indirect callee reads its destination only when lowering seeds the staging slot from it, which makeCallOutputAdapters does only for identical storage types. A StrH destination for a StrG output gets an ABI-typed zero seed, so the callee never observes the caller's value, yet CalleeReadsSeed recorded a read and suppressed the true unused-overwrite diagnostic. Gate the composed read on TypeEqual between the destination's slot type and the callee output, the same authority destSlotType consults; direct scalar returns always match. Un-tick the PIR half of the consumption criterion: PIR does not route calls yet, so its consumption of the seed facts is owed by Step 4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Closing as superseded. Review of the seeded-output semantics concluded that the analysis this PR adds should not exist: the language rule will be that declared outputs are write-only inside their template (no reads in values, conditions, arguments, prints, or format markers), so a body can never observe its incoming seed and The replacement PR starts from master and keeps the ABI unchanged: the hidden seed parameter and destination-seeded staging slots stay as an unobservable keep-old carrier, and the existing write-effect and boundary-resolution ( The branch stays for reference. |
Implements §1 of
docs/Pluto Effects and Follow-up Plan.md: the seed dependency analysis fix queued ahead of PIR call routing. PIR is untouched.Problem
The CFG treated an all-
MustWritecallee as reading no seed. A body whose last statement always writes can still depend on the incoming output value:Before (master
c253781): the script is rejected, even though lowering already computes 21 from the seed. Inserting a print ofabefore the call made it compile and print20then21.After: the script compiles and prints
21; the fresh-target variant (b = MaybeIncrement(-1)) prints1.Three facts, kept separate
FuncInfo.BodyOutputEffects(MustWrite/MayWrite, unchanged)FuncInfo.BodySeedEffects(NoSeedRead/MaySeedRead) and per-call-siteStatementEffect.CalleeReadsSeedStatementEffect.ReadsSeed(unchanged meaning: directMayWriteresult resolved at an existing target)=ReadsSeedwas not broadened: the body fold still discounts boundary-manufacturedMustWrites, so a seed-dependent accumulator staysMustWrite.deriveBodySeedEffects): walks statements in order. Explicit reads (conditions, values, print arguments, resolved format markers and their dynamic width/precision operands) and implicit reads through aMaySeedReadcallee happen before the statement's writes; a rawMustWritereplaces the output, a boundary-resolved write does not. The fact is sticky, so copying the seed to a local first keeps the dependency, and reads that feed another output count.CalleeReadsSeedwhatever the ABI, but only when the seed reaches the callee. Indirect outputs read their destination-seeded staging slot just as direct returns read the hidden seed parameter, and lowering seeds that slot from the destination only for identical storage types (makeCallOutputAdapters). AStrHdestination for aStrGoutput gets an ABI-typed zero seed, so no read is recorded and the prior value stays a true unused-overwrite (review round 1 regression, with CFG, effect, and end-to-end tests). Only boundary resolution stays behind the direct-ABI check.x = MaybeIncrement(-1)alone is still a dead store. Unused-write diagnostics still fire where the prior value is genuinely unread, e.g. a callee that overwrites before reading (y = xtheny = y + 1), or one that resets its output to 0 before a nested seed-reading call.Public symbols, prototypes, and lowering are unchanged; every public direct scalar return keeps its hidden seed parameter.
Docs
docs/Pluto IR Plan.md§15: corrects the assertion that an all-MustWritecallee reads no seed and adds theSeedEffectsubsection, fold rules, SCC step wording, and CFG event order.docs/Pluto Effects and Follow-up Plan.md§1: acceptance criteria ticked and the resolving PR recorded, except the PIR half of the consumption criterion: PIR does not route calls yet, so its consumption of the seed facts is owed by Step 4 call routing.Verification
go test -race ./lexer ./parser ./compiler: pass.python3 test.py --leak-check(full suite): 78 passed, 0 failed, no leaks reported.MustWrite, definite vs conditional overwrite, local copy, condition/print/marker/width reads, nested calls and reset-before-call, cross-output reads, recursive SCC growth, indirect callee composition, function-owned range domain with zero/nonempty/fresh calls), CFG valid and error cases, andtests/seed/end-to-end fixtures with a second script compiled warm from the shared cache.🤖 Generated with Claude Code