Skip to content

Commit 63cd6a5

Browse files
authored
Hotfix pass singleton tuples as single elements to closures (#815)
This PR implements a special calling convention applied to _closures_: * The first argument is the closure's environment (currently not supported nor are types extracted in stable-mir-json) * The second argument is a _tuple_ that needs to be unpacked and the individual components stored as locals `_2` to `_N`
1 parent 7b09892 commit 63cd6a5

File tree

9 files changed

+4071
-27
lines changed

9 files changed

+4071
-27
lines changed

kmir/src/kmir/kdist/mir-semantics/kmir.md

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,6 @@ An operand may be a `Reference` (the only way a function could access another fu
367367
=>
368368
ListItem(newLocal(TY, MUT)) #reserveFor(REST)
369369
370-
371370
syntax KItem ::= #setArgsFromStack ( Int, Operands)
372371
| #setArgFromStack ( Int, Operand)
373372
| #execIntrinsic ( MonoItemKind, Operands, Place )
@@ -410,6 +409,108 @@ An operand may be a `Reference` (the only way a function could access another fu
410409
andBool isTypedValue(CALLERLOCALS[I])
411410
[preserves-definedness] // valid list indexing checked
412411
```
412+
413+
For closures (like `|x,y| { things using x and y }`), a special calling convention is in effect:
414+
The first argument of the closure is its environment.
415+
Its type is currently not extracted (KMIR does not currently support variable-capturing) and it is not initialised.
416+
The second argument is a _tuple_ of all the arguments, however the function body expects these arguments as single locals.
417+
418+
Using this calling convention should be indicated by the `spread_arg` field in the function body.[^spread_arg]
419+
However, this field is usually `None` for _closures_, it is only set for internal functions of the Rust execution mechanism.
420+
Therefore a heuristics is used here:
421+
* The function has two arguments,
422+
* the 1st argument has an unknown type (or refers to one),
423+
* and the 2nd argument is a tuple.
424+
425+
[^spread_arg]: https://doc.rust-lang.org/beta/nightly-rustc/rustc_public/mir/body/struct.Body.html#structfield.spread_arg
426+
427+
```k
428+
// reserve space for local variables and copy/move arguments from a tuple inside the old locals into their place
429+
rule [setupCalleeClosure]: <k> #setUpCalleeData(
430+
monoItemFn(_, _, someBody(body((FIRST:BasicBlock _) #as BLOCKS, NEWLOCALS, _, _, _, _))),
431+
operandMove(place(local(CLOSURE:Int), .ProjectionElems))
432+
operandMove(place(local(TUPLE), .ProjectionElems))
433+
.Operands
434+
)
435+
=>
436+
#setTupleArgs(2, getValue(LOCALS, TUPLE)) ~> #execBlock(FIRST)
437+
// arguments are tuple components, stored as _2 .. _n
438+
...
439+
</k>
440+
<currentFrame>
441+
<currentBody> _ => toKList(BLOCKS) </currentBody>
442+
<locals> LOCALS => #reserveFor(NEWLOCALS) </locals>
443+
<stack>
444+
(ListItem(CALLERFRAME => #updateStackLocal(#updateStackLocal(CALLERFRAME, TUPLE, Moved), CLOSURE, Moved)))
445+
_:List
446+
</stack>
447+
...
448+
</currentFrame>
449+
requires 0 <=Int CLOSURE andBool CLOSURE <Int size(LOCALS)
450+
andBool 0 <=Int TUPLE andBool TUPLE <Int size(LOCALS)
451+
andBool isTypedValue(LOCALS[TUPLE])
452+
andBool isTupleType(lookupTy(tyOfLocal({LOCALS[TUPLE]}:>TypedLocal)))
453+
andBool isTypedLocal(LOCALS[CLOSURE])
454+
andBool typeInfoVoidType ==K lookupTy(tyOfLocal({LOCALS[CLOSURE]}:>TypedLocal))
455+
// either the closure ref type is missing from type table
456+
[priority(40), preserves-definedness]
457+
458+
rule [setupCalleeClosure2]: <k> #setUpCalleeData(
459+
monoItemFn(_, _, someBody(body((FIRST:BasicBlock _) #as BLOCKS, NEWLOCALS, _, _, _, _))),
460+
operandMove(place(local(CLOSURE:Int), .ProjectionElems))
461+
operandMove(place(local(TUPLE), .ProjectionElems))
462+
.Operands
463+
)
464+
=>
465+
#setTupleArgs(2, getValue(LOCALS, TUPLE)) ~> #execBlock(FIRST)
466+
// arguments are tuple components, stored as _2 .. _n
467+
...
468+
</k>
469+
<currentFrame>
470+
<currentBody> _ => toKList(BLOCKS) </currentBody>
471+
<locals> LOCALS => #reserveFor(NEWLOCALS) </locals>
472+
<stack>
473+
(ListItem(CALLERFRAME => #updateStackLocal(#updateStackLocal(CALLERFRAME, TUPLE, Moved), CLOSURE, Moved)))
474+
_:List
475+
</stack>
476+
...
477+
</currentFrame>
478+
requires 0 <=Int CLOSURE andBool CLOSURE <Int size(LOCALS)
479+
andBool 0 <=Int TUPLE andBool TUPLE <Int size(LOCALS)
480+
andBool isTypedValue(LOCALS[TUPLE])
481+
andBool isTupleType(lookupTy(tyOfLocal({LOCALS[TUPLE]}:>TypedLocal)))
482+
andBool isTypedLocal(LOCALS[CLOSURE])
483+
// or the closure ref type pointee is missing from the type table
484+
andBool isRefType(lookupTy(tyOfLocal({LOCALS[CLOSURE]}:>TypedLocal)))
485+
andBool isTy(pointeeTy(lookupTy(tyOfLocal({LOCALS[CLOSURE]}:>TypedLocal))))
486+
andBool lookupTy({pointeeTy(lookupTy(tyOfLocal({LOCALS[CLOSURE]}:>TypedLocal)))}:>Ty) ==K typeInfoVoidType
487+
[priority(45), preserves-definedness]
488+
489+
syntax Bool ::= isTupleType ( TypeInfo ) [function, total]
490+
| isRefType ( TypeInfo ) [function, total]
491+
// -------------------------------------------------------
492+
rule isTupleType(typeInfoTupleType(_)) => true
493+
rule isTupleType( _ ) => false [owise]
494+
rule isRefType(typeInfoRefType(_)) => true
495+
rule isRefType( _ ) => false [owise]
496+
497+
syntax KItem ::= #setTupleArgs ( Int , Value )
498+
| #setTupleArgs ( Int , List )
499+
500+
// unpack tuple and set arguments individually
501+
rule <k> #setTupleArgs(IDX, Aggregate(variantIdx(0), ARGS)) => #setTupleArgs(IDX, ARGS) ... </k>
502+
503+
rule <k> #setTupleArgs(_, .List ) => .K ... </k>
504+
505+
rule <k> #setTupleArgs(IDX, ListItem(VAL) REST:List)
506+
=> #setLocalValue(place(local(IDX), .ProjectionElems), #incrementRef(VAL)) ~> #setTupleArgs(IDX +Int 1, REST)
507+
...
508+
</k>
509+
```
510+
511+
512+
#### Assert
513+
413514
The `Assert` terminator checks that an operand holding a boolean value (which has previously been computed, e.g., an overflow flag for arithmetic operations) has the expected value (e.g., that this overflow flag is `false` - a very common case).
414515
If the condition value is as expected, the program proceeds with the given `target` block.
415516
Otherwise the provided message is passed to a `panic!` call, ending the program with an error, modelled as an `AssertError` in the semantics.

kmir/src/kmir/kdist/mir-semantics/rt/data.md

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -445,30 +445,6 @@ This is done without consideration of the validity of the Downcast[^downcast].
445445
</k>
446446
```
447447

448-
If an Aggregate contains only one element and #traverseProjection becomes stuck, you can directly access this element.
449-
450-
Without this rule, the test `closure_access_struct-fail.rs` demonstrates the following behavior:
451-
- The execution gets stuck after 192 steps at `#traverseProjection ( toLocal ( 2 ) , Aggregate ( variantIdx ( 0 ) , ListItem ( ... ) ) , ... )`
452-
- The stuck state occurs because there's a Deref projection that needs to be applied to the single element of this Aggregate, but the existing Deref rules only handle pointers and references, not Aggregates
453-
454-
With this rule enabled:
455-
- The execution progresses further to 277 steps before getting stuck
456-
- It gets stuck at a different location: `#traverseProjection ( toLocal ( 19 ) , thunk ( #decodeConstant ( constantKindAll ... ) ) , ... )`
457-
- The rule automatically unwraps the single-element Aggregate, allowing the field access to proceed
458-
459-
This rule is essential for handling closures that access struct fields, as MIR represents certain struct accesses through single-element Aggregates that need to be unwrapped.
460-
461-
```k
462-
rule <k> #traverseProjection(
463-
DEST,
464-
Aggregate(_, ARGS),
465-
PROJS,
466-
CTXTS
467-
)
468-
=> #traverseProjection(DEST, getValue(ARGS, 0), PROJS, CTXTS) ... </k>
469-
requires size(ARGS) ==Int 1 [preserves-definedness, priority(100)]
470-
```
471-
472448
#### Ranges
473449

474450
An `Index` projection operates on an array or slice (`Range`) value, to access an element of the array.

kmir/src/tests/integration/data/crate-tests/two-crate-bin/crate2::main.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
┌─ 1 (root, init)
33
│ #execTerminator ( terminator ( ... kind: terminatorKindCall ( ... func: operandC
44
5-
│ (730 steps)
5+
│ (727 steps)
66
├─ 3 (terminal)
77
│ #EndProgram ~> .K
88
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
fn test_t(x: (u32,)) -> u32 { x.0 } // expects a singleton tuple
2+
3+
fn main() {
4+
let single: u32 = 32;
5+
let tuple: (u32,) = (single,);
6+
7+
test_t(tuple); // gets passed a singleton tuple
8+
9+
let identity = |x| x; // expects a single u32
10+
11+
identity(single); // gets passed a &closure and a singleton tuple!
12+
13+
let twice = (single, single);
14+
let is_equal = |a, b| { assert!(a == b); }; // expects and accesses its arguments as locals _2 and _3 (u32)
15+
16+
// is_equal(twice); // error
17+
is_equal(single, single); // gets passed a &closure and a singleton tuple !!!
18+
}

0 commit comments

Comments
 (0)