Skip to content

fix(rt): resolve thunk(operandCopy) in projection traversal#956

Draft
Stevengre wants to merge 5 commits intomasterfrom
jh/fix-thunk-projection
Draft

fix(rt): resolve thunk(operandCopy) in projection traversal#956
Stevengre wants to merge 5 commits intomasterfrom
jh/fix-thunk-projection

Conversation

@Stevengre
Copy link
Contributor

@Stevengre Stevengre commented Feb 28, 2026

Summary

Add rules to resolve thunk(operandCopy(place(...))) values during projection traversal and borrow creation. Guard existing terminal rules to prevent premature matching on thunked values.

  • Add #isThunkOperandCopy predicate
  • Add 4 #traverseProjection rules for thunked copy-places (read, move-read, write, borrow)
  • Guard existing .ProjectionElems terminal rules with notBool #isThunkOperandCopy(VAL)
  • Add iter-copied-take-next-thunk-fail.rs and iter-map-eq-copied-take-thunk-fail.rs tests

Context

When closure or iterator type metadata is incomplete (e.g. missing from the SMIR type table), zero-sized constants are decoded as thunk(operandCopy(place(...))) instead of concrete values. When downstream code projects into such a thunked value — via field access, deref, or borrow creation — the #traverseProjection rules had no matching case, leaving the term stuck.

The fix composes remaining projections onto the thunked place and delegates to existing operand evaluation:

  • Copy-read: => operandCopy(place(local(I), appendP(PLACEPROJ, PROJS))) — re-evaluates the original place with composed projections
  • Move-read: emits #writeMoved then resolves as copy
  • Write: delegates to #setLocalValue on the composed place
  • Borrow (#forRef): materializes #mkRef directly to avoid re-traversal (re-entering via getValue(LOCALS, I) can reintroduce the same thunk, causing non-terminating projection growth)

This is a workaround for incomplete type metadata — miri does not have thunks since it always has complete type information. PR #957 (closure type metadata) addresses the root cause by providing the missing type info, but this PR handles thunks that still arise in other scenarios.

Without this fix, iterator patterns like keys.iter().take(n).copied() get stuck:

Leaf <k>:
  #traverseProjection(toLocal(1), thunk(operandCopy(place(local(1), ...))), ...)
    ~> #forRef(...)
  -- no matching rule, term is stuck

With this fix, the thunked place is composed and re-evaluated, allowing execution to continue.

Proof evidence

Without fix (RED): spl-multisig-iter-eq-copied-next-fail.rs gets stuck at #traverseProjection ~> #forRef

With fix (GREEN): Execution advances past the thunk-projection point (test still fails at a later point due to other unrelated issues, hence the -fail suffix).

Test plan

  • -fail tests (proof advances further with fix, still fails at later point)
  • make test-integration regression

…w creation

When a value is thunked as thunk(operandCopy(place(...))), projection
traversal and borrow creation had no rules to resolve the thunk, causing
stuck #traverseProjection ~> #readProjection / #forRef terms.

Add #isThunkOperandCopy predicate and four #traverseProjection rules
that compose remaining projections onto the thunked place and delegate
to existing operand evaluation:
- Copy-read: resolve to operandCopy with composed place
- Move-read: emit #writeMoved then resolve the copy
- Write: delegate to #setLocalValue on composed place
- Borrow (#forRef): materialize #mkRef directly to avoid re-traversal

Guard existing .ProjectionElems terminal rules with
notBool #isThunkOperandCopy(VAL) to prevent premature matching.

Tests: iter-copied-take-next-thunk-fail.rs,
       iter-map-eq-copied-take-thunk-fail.rs
Stevengre and others added 4 commits February 28, 2026 14:24
Add a rule for `aggregateKindClosure` to construct closures as
`Aggregate(variantIdx(0), ARGS)`, matching the existing tuple and ADT
aggregate handling.

Add an `[owise]` fallback to `#setTupleArgs` for `Value` arguments
that are not wrapped in an `Aggregate` tuple. This handles closure-call
paths where a single argument is passed directly.

Test: `closure-no-capture.rs` — a non-capturing closure passed through
a generic `FnOnce` call. Exercises the `#setTupleArgs` fallback for
unwrapped single-value arguments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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