Skip to content

Commit 871e155

Browse files
Stevengredkcumming
andauthored
fix(rt): generalize direct-tag enum decoding to any variant count and discriminant (#955)
## Summary Replace the specialized \`#decodeOptionTag01\` rule (which only handled exactly 2 variants with discriminants 0/1 and the Option-like 0+1 field pattern) with a general \`#decodeEnumDirect\` decoder that handles: - Any number of variants (not just two) - Any discriminant values (not just 0 and 1) - Any unsigned integer tag width (u8, u16, etc.) - Any number of fields per variant New K helper functions (all total with `owise` fallbacks): - `#decodeEnumDirectFields`: given variant index, decode its fields using per-variant offsets - `#decodeEnumDirectTag`: read tag bytes as unsigned little-endian integer - `#nthTys`: index into per-variant field type lists - `#nthVariantOffsets`: extract field offsets from nth variant layout ## Context Before this change, direct-tag enum decoding was specialized to `discriminants: 0, 1` and single-byte tag checks (modeled after `Option`). Any enum with different discriminant values or more than two variants would produce `UnableToDecode`. ## Changes - `kmir/src/kmir/kdist/mir-semantics/rt/decoding.md`: replace `#decodeOptionTag01` with general `#decodeEnumDirect` rules - `kmir/src/tests/integration/data/prove-rs/enum-direct-tag-decode.rs`: add Rust test covering repr(u8) 0/1, repr(u8) 2/5, repr(u16) 0/256, and 3-variant enums - `kmir/src/tests/integration/test_integration.py`: register the new prove-rs test ## Testing - `make build` ✅ - `uv --directory kmir run pytest kmir/src/tests/integration/test_integration.py -k 'enum-direct-tag-decode' -vv` ✅ --------- Co-authored-by: Daniel Cumming <124537596+dkcumming@users.noreply.github.com>
1 parent 8ee9ccf commit 871e155

2 files changed

Lines changed: 132 additions & 24 deletions

File tree

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

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -297,27 +297,26 @@ If there are no fields, the enum can be decoded by using their data as the discr
297297
requires TAG =/=Int DISCRIMINANT
298298
```
299299

300-
#### Enums with two variants
300+
#### Enums with direct tag encoding and fields
301301

302-
Having two variants with possibly zero or one field each is a very common case,
303-
it includes a number of standard library types such as `Option` and `Result`.
304-
305-
The following rules are specialised to the case of encoding an `Option`.
306-
An important distinction here is whether or not the tag is niche-encoded.
307-
If the option holds data that has all-zero as a possible value, a separate tag is used, usually as the first field.
308-
In both cases we expect the tag to be in the single shared field, and the discriminant to be just 0 and 1.
302+
Decodes any enum with `tagEncodingDirect` and a `primitiveInt` tag,
303+
with arbitrary variant counts, discriminant values, and field counts per variant.
304+
The tag is read as an unsigned little-endian integer, matched against the discriminant
305+
list to find the variant index, and then the variant's fields are decoded using the
306+
per-variant layout offsets.
309307

310308
```k
309+
// General entry rule: direct-tag enum with at least one field somewhere.
311310
rule #decodeValue(
312311
BYTES
313312
, typeInfoEnumType(...
314313
name: _
315314
, adtDef: _
316-
, discriminants: discriminant(0) discriminant(1) .Discriminants
317-
, fields: (.Tys : (FIELD_TYPE .Tys) : .Tyss)
315+
, discriminants: DISCRIMINANTS
316+
, fields: FIELD_TYPESS
318317
, layout:
319318
someLayoutShape(layoutShape(...
320-
fields: fieldsShapeArbitrary(mk(... offsets: machineSize(0) .MachineSizes))
319+
fields: _FIELDS
321320
, variants:
322321
variantsShapeMultiple(
323322
mk(...
@@ -329,7 +328,7 @@ In both cases we expect the tag to be in the single shared field, and the discri
329328
)
330329
, tagEncoding: tagEncodingDirect
331330
, tagField: 0
332-
, variants: _VARIANTS
331+
, variants: VARIANT_LAYOUTS
333332
)
334333
)
335334
, abi: _ABI
@@ -338,22 +337,59 @@ In both cases we expect the tag to be in the single shared field, and the discri
338337
))
339338
) #as ENUM_TYPE
340339
)
341-
=> #decodeOptionTag01(BYTES, TAG_WIDTH, FIELD_TYPE, ENUM_TYPE)
342-
343-
syntax Evaluation ::= #decodeOptionTag01 ( Bytes , IntegerLength , Ty , TypeInfo ) [function, total]
344-
// --------------------------------------------------------------------------------------
345-
rule #decodeOptionTag01(BYTES, _LEN, _TY, _ENUM_TYPE)
346-
=> Aggregate(variantIdx(0), .List)
347-
requires 0 ==Int BYTES[0] // expect only 0 or 1 as tags, so higher bytes do not matter
348-
[preserves-definedness]
349-
rule #decodeOptionTag01(BYTES, LEN, TY, _ENUM_TYPE)
350-
=> Aggregate(variantIdx(1), ListItem(#decodeValue(substrBytes(BYTES, #byteLength(LEN), lengthBytes(BYTES)), lookupTy(TY))))
351-
requires 1 ==Int BYTES[0] // expect only 0 or 1 as tags, so higher bytes do not matter
340+
=> #decodeEnumDirectFields(
341+
BYTES,
342+
#findVariantIdx(#decodeEnumDirectTag(BYTES, TAG_WIDTH), DISCRIMINANTS),
343+
FIELD_TYPESS,
344+
VARIANT_LAYOUTS,
345+
ENUM_TYPE
346+
)
347+
requires notBool #noFields(FIELD_TYPESS)
348+
349+
// ---------------------------------------------------------------------------
350+
// #decodeEnumDirectFields: given the variant index, decode its fields
351+
// ---------------------------------------------------------------------------
352+
syntax Evaluation ::= #decodeEnumDirectFields ( Bytes , VariantIdx , Tyss , LayoutShapes , TypeInfo ) [function, total]
353+
// --------------------------------------------------------------------------------------------------------------------------
354+
rule #decodeEnumDirectFields(BYTES, variantIdx(IDX), FIELD_TYPESS, VARIANT_LAYOUTS, _ENUM_TYPE)
355+
=> Aggregate(
356+
variantIdx(IDX),
357+
#decodeFieldsWithOffsets(BYTES, #nthTys(FIELD_TYPESS, IDX), #nthVariantOffsets(VARIANT_LAYOUTS, IDX))
358+
)
359+
requires 0 <=Int IDX
352360
[preserves-definedness]
353-
rule #decodeOptionTag01(BYTES, _LEN, _TY, ENUM_TYPE)
361+
362+
// Error cases: variant not found or other failure
363+
rule #decodeEnumDirectFields(BYTES, _, _FIELD_TYPESS, _VARIANT_LAYOUTS, ENUM_TYPE)
354364
=> UnableToDecode(BYTES, ENUM_TYPE)
355365
[owise]
356366
367+
// ---------------------------------------------------------------------------
368+
// #decodeEnumDirectTag: read the tag bytes as an unsigned little-endian int
369+
// ---------------------------------------------------------------------------
370+
syntax Int ::= #decodeEnumDirectTag ( Bytes , IntegerLength ) [function, total]
371+
rule #decodeEnumDirectTag(BYTES, TAG_WIDTH)
372+
=> Bytes2Int(substrBytes(BYTES, 0, #byteLength(TAG_WIDTH)), LE, Unsigned)
373+
requires lengthBytes(BYTES) >=Int #byteLength(TAG_WIDTH)
374+
[preserves-definedness]
375+
rule #decodeEnumDirectTag(_, _) => -1 [owise]
376+
377+
// ---------------------------------------------------------------------------
378+
// List-indexing helpers
379+
// ---------------------------------------------------------------------------
380+
381+
// Index into a Tyss (list of per-variant field type lists)
382+
syntax Tys ::= #nthTys ( Tyss , Int ) [function, total]
383+
rule #nthTys(TYS : _REST, 0) => TYS
384+
rule #nthTys(_TYS : REST, N) => #nthTys(REST, N -Int 1) requires N >Int 0
385+
rule #nthTys(_, _) => .Tys [owise]
386+
387+
// Index into variant layouts and extract field offsets in one step (total)
388+
syntax MachineSizes ::= #nthVariantOffsets ( LayoutShapes , Int ) [function, total]
389+
rule #nthVariantOffsets(layoutShape(fieldsShapeArbitrary(mk(OFFSETS)), _, _, _, _) _REST, 0) => OFFSETS
390+
rule #nthVariantOffsets(_L REST, N) => #nthVariantOffsets(REST, N -Int 1) requires N >Int 0
391+
rule #nthVariantOffsets(_, _) => .MachineSizes [owise]
392+
357393
syntax Int ::= #byteLength ( IntegerLength ) [function, total]
358394
// -----------------------------------------------------------
359395
rule #byteLength(integerLengthI8 ) => 1
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/// Comprehensive test for direct-tag enum decoding.
2+
///
3+
/// Covers:
4+
/// - `#[repr(u8)]` with discriminants 0/1 (baseline, like Option)
5+
/// - `#[repr(u8)]` with discriminants 2/5 (non-zero start)
6+
/// - `#[repr(u16)]` with discriminants 0/256 (wide tag exceeding u8 range)
7+
/// - `#[repr(u8)]` with 3 variants (beyond two-variant restriction)
8+
9+
// --- Baseline: repr(u8), disc 0 / 1 -------------------------------------------
10+
11+
#[repr(u8)]
12+
enum TwoU8 {
13+
A(bool) = 0,
14+
B(bool) = 1,
15+
}
16+
17+
const TWO_U8_A: TwoU8 = TwoU8::A(true);
18+
const TWO_U8_B: TwoU8 = TwoU8::B(false);
19+
20+
// --- Non-zero discriminants: repr(u8), disc 2 / 5 -----------------------------
21+
22+
#[repr(u8)]
23+
enum NonZero {
24+
X(bool) = 2,
25+
Y(bool) = 5,
26+
}
27+
28+
const NZ_X: NonZero = NonZero::X(false);
29+
const NZ_Y: NonZero = NonZero::Y(true);
30+
31+
// --- Wide tag: repr(u16), disc 0 / 256 ----------------------------------------
32+
33+
#[repr(u16)]
34+
enum WideTag {
35+
Lo(bool) = 0,
36+
Hi(bool) = 256,
37+
}
38+
39+
const WIDE_LO: WideTag = WideTag::Lo(true);
40+
const WIDE_HI: WideTag = WideTag::Hi(false);
41+
42+
// --- Three variants: repr(u8), disc 0 / 1 / 2 ---------------------------------
43+
44+
#[repr(u8)]
45+
enum Three {
46+
First(bool) = 0,
47+
Second(bool) = 1,
48+
Third(bool) = 2,
49+
}
50+
51+
const THREE_A: Three = Three::First(true);
52+
const THREE_B: Three = Three::Second(false);
53+
const THREE_C: Three = Three::Third(true);
54+
55+
fn main() {
56+
// Baseline
57+
assert!(matches!(TWO_U8_A, TwoU8::A(true)));
58+
assert!(matches!(TWO_U8_B, TwoU8::B(false)));
59+
60+
// Non-zero discriminants
61+
assert!(matches!(NZ_X, NonZero::X(false)));
62+
assert!(matches!(NZ_Y, NonZero::Y(true)));
63+
64+
// Wide tag
65+
assert!(matches!(WIDE_LO, WideTag::Lo(true)));
66+
assert!(matches!(WIDE_HI, WideTag::Hi(false)));
67+
68+
// Three variants
69+
assert!(matches!(THREE_A, Three::First(true)));
70+
assert!(matches!(THREE_B, Three::Second(false)));
71+
assert!(matches!(THREE_C, Three::Third(true)));
72+
}

0 commit comments

Comments
 (0)