Skip to content

Commit e32a326

Browse files
authored
Transmute integer to fieldless enum (#840)
This PR implements semantics to cast (transmute) an integer into an enum if that enum is known to have no fields in it's variants. Some things to consider that influence this PR: 1. Transmute is only possible between the same width bits (e.g. `u8 -> i8` fine, `u8 -> u16` not fine); 2. Discriminants are stored as `u128` in the type data even if they are unsigned at the source level; 3. An integer transmuted into an enum is wellformed as long as the bit pattern matches a discriminant; The combination of these points mean that the approach to soundly casting an integer into an enum is to treat the incoming integer as unsigned (converting if signed), and check if that value is in the discriminants. If yes, follow to the corresponding variant position; if not return `#UBErrorInvalidDisciminantsInEnumCast`. The added code has meant a work around with retrieving the discriminant of a thunked cast can be removed. All added tests that should pass do except for one which is failing, I have made an issue for this as it is related to decoding negative discriminant enum types from an allocation.
1 parent 153bce7 commit e32a326

9 files changed

+190
-11
lines changed

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

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,23 +1560,58 @@ Casting an integer to a `[u8; N]` array materialises its little-endian bytes.
15601560
rule #staticArrayLenBits(_OTHER) => 0 [owise]
15611561
```
15621562

1563-
Another specialisation is getting the discriminant of `enum`s without fields after converting some integer data to it
1564-
(see `#discriminant` and `rvalueDiscriminant`).
1565-
If none of the `enum` variants has any fields, the `Transmute` of a number to the `enum` data is necessarily just the discriminant itself., and can be returned as the integer value afgter adjusting to the byte length of the discriminant:
1563+
A transmutation from an integer to an enum is wellformed if:
1564+
- The bit width of the incoming integer is the same as the discriminant type of the enum
1565+
(e.g. `u8 -> i8` fine, `u8 -> u16` not fine) - this is guaranteed by the compiler;
1566+
- The incoming integer has a bit pattern that matches a discriminant of the enum
1567+
(e.g. `255_u8` and `-1_i8` fine iff `0b1111_1111` is a discriminant of the enum);
1568+
1569+
Note that discriminants are stored as `u128` in the type data even if they are signed
1570+
or unsigned at the source level. This means that our approach to soundly transmute an
1571+
integer into a enum is to treat the incoming integer as unsigned (converting if signed),
1572+
and check if the value is in the discriminants. If yes, find the corresponding variant
1573+
index; if not, return `#UBErrorInvalidDiscriminantsInEnumCast`.
15661574

15671575
```k
1568-
rule <k> #discriminant(
1569-
thunk(#cast (Integer(DATA, _, false), castKindTransmute, _, TY)),
1570-
TY
1571-
) => Integer(DATA, 0, false) // HACK: bit width 0 means "flexible"
1572-
...
1573-
</k>
1574-
requires #isEnumWithoutFields(lookupTy(TY))
1575-
15761576
syntax Bool ::= #isEnumWithoutFields ( TypeInfo ) [function, total]
15771577
// ----------------------------------------------------------------
15781578
rule #isEnumWithoutFields(typeInfoEnumType(_, _, _, FIELDSS, _)) => #noFields(FIELDSS)
15791579
rule #isEnumWithoutFields(_OTHER) => false [owise]
1580+
1581+
// TODO: Connect this with MirError
1582+
syntax Evaulation ::= "#UBErrorInvalidDiscriminantsInEnumCast"
1583+
rule <k>
1584+
#cast( Integer ( VAL , WIDTH , _SIGNED ) , castKindTransmute , _TY_FROM , TY_TO ) ~> _REST
1585+
=>
1586+
#UBErrorInvalidDiscriminantsInEnumCast
1587+
</k>
1588+
requires #isEnumWithoutFields(lookupTy(TY_TO))
1589+
andBool notBool #validDiscriminant( truncate(VAL, WIDTH, Unsigned) , lookupTy(TY_TO) )
1590+
1591+
rule <k>
1592+
#cast( Integer ( VAL , WIDTH , _SIGNED ) , castKindTransmute , _TY_FROM , TY_TO )
1593+
=>
1594+
Aggregate( #findVariantIdxFromTy( truncate(VAL, WIDTH, Unsigned), lookupTy(TY_TO) ) , .List )
1595+
...
1596+
</k>
1597+
requires #isEnumWithoutFields(lookupTy(TY_TO))
1598+
andBool #validDiscriminant( truncate(VAL, WIDTH, Unsigned) , lookupTy(TY_TO))
1599+
1600+
syntax VariantIdx ::= #findVariantIdxFromTy ( Int , TypeInfo ) [function, total]
1601+
//------------------------------------------------------------------------------
1602+
rule #findVariantIdxFromTy( VAL , typeInfoEnumType(_, _, DISCRIMINANTS, _, _) ) => #findVariantIdx( VAL, DISCRIMINANTS)
1603+
rule #findVariantIdxFromTy( _ , _ ) => err("NotAnEnum") [owise]
1604+
1605+
syntax Bool ::= #validDiscriminant ( Int , TypeInfo ) [function, total]
1606+
// ----------------------------------------------------------------------------
1607+
rule #validDiscriminant( VAL , typeInfoEnumType(_, _, DISCRIMINANTS, _, _) ) => #validDiscriminantAux( VAL , DISCRIMINANTS )
1608+
rule #validDiscriminant( _ , _ ) => false [owise]
1609+
1610+
syntax Bool ::= #validDiscriminantAux ( Int , Discriminants ) [function, total]
1611+
// ----------------------------------------------------------------------------
1612+
rule #validDiscriminantAux( VAL, discriminant(mirInt(DISCRIMINANT)) REST ) => VAL ==Int DISCRIMINANT orBool #validDiscriminantAux( VAL, REST )
1613+
rule #validDiscriminantAux( VAL, discriminant( DISCRIMINANT ) REST ) => VAL ==Int DISCRIMINANT orBool #validDiscriminantAux( VAL, REST )
1614+
rule #validDiscriminantAux( _VAL, .Discriminants ) => false
15801615
```
15811616

15821617

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
┌─ 1 (root, init)
3+
│ #execTerminator ( terminator ( ... kind: terminatorKindCall ( ... func: operandC
4+
│ span: 0
5+
6+
│ (100 steps)
7+
└─ 3 (stuck, leaf)
8+
#traverseProjection ( toLocal ( 2 ) , thunk ( #decodeConstant ( constantKindAllo
9+
span: 153
10+
11+
12+
┌─ 2 (root, leaf, target, terminal)
13+
│ #EndProgram ~> .K
14+
15+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
┌─ 1 (root, init)
3+
│ #execTerminator ( terminator ( ... kind: terminatorKindCall ( ... func: operandC
4+
│ span: 0
5+
6+
│ (13 steps)
7+
└─ 3 (stuck, leaf)
8+
#UBErrorInvalidDiscriminantsInEnumCast ~> .K
9+
function: main
10+
11+
12+
┌─ 2 (root, leaf, target, terminal)
13+
│ #EndProgram ~> .K
14+
15+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use std::convert::TryFrom;
2+
3+
pub enum ProgramError {
4+
InvalidAccountData
5+
}
6+
7+
#[repr(u8)]
8+
#[derive(Clone, Copy, Debug, PartialEq)]
9+
pub enum AccountState {
10+
Uninitialized,
11+
Initialized,
12+
Frozen,
13+
}
14+
15+
impl TryFrom<u8> for AccountState {
16+
type Error = ProgramError;
17+
18+
#[inline(always)]
19+
fn try_from(value: u8) -> Result<Self, Self::Error> {
20+
match value {
21+
// SAFETY: `value` is guaranteed to be in the range of the enum variants.
22+
0..=2 => Ok(unsafe { core::mem::transmute::<u8, AccountState>(value) }),
23+
_ => Err(ProgramError::InvalidAccountData),
24+
}
25+
}
26+
}
27+
28+
fn main() {
29+
num_into_state(0);
30+
}
31+
32+
fn num_into_state(arg: u8) {
33+
let state: Result<AccountState, ProgramError> = AccountState::try_from(arg);
34+
35+
if arg <= 2 {
36+
assert!(state.is_ok());
37+
} else {
38+
assert!(state.is_err());
39+
}
40+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#[repr(i8)]
2+
#[derive(Clone, Copy, Debug, PartialEq)]
3+
pub enum AccountState {
4+
Uninitialized = 0,
5+
Initialized = 1,
6+
Frozen = -1,
7+
}
8+
9+
fn main() {
10+
unsafe {
11+
assert_eq!(core::mem::transmute::<u8, AccountState>(0), AccountState::Uninitialized);
12+
assert_eq!(core::mem::transmute::<i8, AccountState>(0), AccountState::Uninitialized);
13+
14+
assert_eq!(core::mem::transmute::<u8, AccountState>(1), AccountState::Initialized);
15+
assert_eq!(core::mem::transmute::<i8, AccountState>(1), AccountState::Initialized);
16+
17+
assert_eq!(core::mem::transmute::<u8, AccountState>(255), AccountState::Frozen);
18+
assert_eq!(core::mem::transmute::<i8, AccountState>(-1), AccountState::Frozen);
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#[repr(u8)]
2+
#[derive(Clone, Copy, Debug, PartialEq)]
3+
pub enum AccountState {
4+
Uninitialized = 11,
5+
Initialized = 22,
6+
Frozen = 255,
7+
}
8+
9+
fn main() {
10+
unsafe {
11+
assert_eq!(core::mem::transmute::<u8, AccountState>(11), AccountState::Uninitialized);
12+
assert_eq!(core::mem::transmute::<u8, AccountState>(22), AccountState::Initialized);
13+
assert_eq!(core::mem::transmute::<u8, AccountState>(255), AccountState::Frozen);
14+
15+
// Compiler rejects transmuting between different width types
16+
// assert_eq!(core::mem::transmute::<u64, AccountState>(-1), AccountState::Frozen);
17+
18+
// The within the same width the type can be different, here an `-1_i8` is used instead
19+
// of a `255_u8`, but the bit pattern matches it creates the correct type
20+
assert_eq!(core::mem::transmute::<i8, AccountState>(-1), AccountState::Frozen);
21+
}
22+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#[repr(u8)]
2+
#[derive(Clone, Copy, Debug, PartialEq)]
3+
pub enum AccountState {
4+
Uninitialized,
5+
Initialized,
6+
Frozen,
7+
}
8+
9+
fn main() {
10+
unsafe {
11+
std::hint::black_box(
12+
core::mem::transmute::<u8, AccountState>(4)
13+
);
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#[repr(u8)]
2+
#[derive(Clone, Copy, Debug, PartialEq)]
3+
pub enum AccountState {
4+
Uninitialized,
5+
Initialized,
6+
Frozen,
7+
}
8+
9+
fn main() {
10+
unsafe {
11+
assert_eq!(core::mem::transmute::<u8, AccountState>(0), AccountState::Uninitialized);
12+
assert_eq!(core::mem::transmute::<u8, AccountState>(1), AccountState::Initialized);
13+
assert_eq!(core::mem::transmute::<u8, AccountState>(2), AccountState::Frozen);
14+
}
15+
}

kmir/src/tests/integration/test_integration.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
'niche-enum',
5353
'assume-cheatcode-conflict-fail',
5454
'raw-ptr-cast-fail',
55+
'transmute-u8-to-enum-fail',
56+
'transmute-u8-to-enum-changed-discriminant-signed-fail',
5557
]
5658

5759

0 commit comments

Comments
 (0)