plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
29ad747
— Peter John
2026-10-25T16:06:45+05:30
feat(codegen): $Tag runtime representation, tag patterns, ? rewrap, crash import
- plum-wasm-codegen/src/lib.rs +554 -8
- plum-wasm-codegen/tests/codegen_tests.rs +205 -0
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -454,6 +454,12 @@ struct LocalCtx<'a> {
|
|
|
454
454
|
/// through to the next statement instead of skipping/exiting — see the
|
|
455
455
|
/// `plum_compiler_bugs_found_2026_08` memory.
|
|
456
456
|
loop_extra_depth: Cell<u32>,
|
|
457
|
+
/// The declared return type of the function being compiled (`None` inside a
|
|
458
|
+
/// closure body) — `?` builds the OUTER function's `Err` variant from it.
|
|
459
|
+
ret_type: Option<PlumType>,
|
|
460
|
+
/// Match-subject scratch local -> its tag-union type, for subjects whose
|
|
461
|
+
/// static type is a `TTags` (their `Name`/`Class` arms test `$Tag.id`).
|
|
462
|
+
match_subject_types: RefCell<HashMap<u32, PlumType>>,
|
|
457
463
|
}
|
|
458
464
|
|
|
459
465
|
fn fnKey(f: &ast::Fn) -> String {
|
|
@@ -517,6 +523,138 @@ thread_local! {
|
|
|
517
523
|
static CURRENT_CONSTS: RefCell<HashMap<String, ast::Expr>> = RefCell::new(HashMap::new());
|
|
518
524
|
}
|
|
519
525
|
|
|
526
|
+
thread_local! {
|
|
527
|
+
/// Tag name -> runtime `$Tag.id`. Seeded (sorted) from every tag appearing in
|
|
528
|
+
/// the program's signatures/variants at the top of `compileSourceInner`;
|
|
529
|
+
/// `tagId` appends any stray tag literal never mentioned in a type.
|
|
530
|
+
static CURRENT_TAG_IDS: RefCell<std::collections::BTreeMap<String, i32>> =
|
|
531
|
+
const { RefCell::new(std::collections::BTreeMap::new()) };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// Import key of the `plum::crash(Str)` host function in `func_ids`.
|
|
535
|
+
const CRASH_FN_KEY: &str = "__crash";
|
|
536
|
+
|
|
537
|
+
fn tagId(name: &str) -> i32 {
|
|
538
|
+
CURRENT_TAG_IDS.with(|c| {
|
|
539
|
+
let mut m = c.borrow_mut();
|
|
540
|
+
if let Some(id) = m.get(name) {
|
|
541
|
+
return *id;
|
|
542
|
+
}
|
|
543
|
+
let id = m.len() as i32;
|
|
544
|
+
m.insert(name.to_string(), id);
|
|
545
|
+
id
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/// Collects every tag name inside every `[...]` tag-union segment of `s`
|
|
550
|
+
/// (a canonical type or mangled name), recursing into payload types.
|
|
551
|
+
fn collectTagNames(s: &str, out: &mut std::collections::BTreeSet<String>) {
|
|
552
|
+
let chars: Vec<char> = s.chars().collect();
|
|
553
|
+
let mut i = 0;
|
|
554
|
+
while i < chars.len() {
|
|
555
|
+
if chars[i] == '[' && chars.get(i + 1) != Some(&']') {
|
|
556
|
+
let start = i;
|
|
557
|
+
let mut depth = 0;
|
|
558
|
+
while i < chars.len() {
|
|
559
|
+
match chars[i] {
|
|
560
|
+
'[' => depth += 1,
|
|
561
|
+
']' => depth -= 1,
|
|
562
|
+
_ => {}
|
|
563
|
+
}
|
|
564
|
+
i += 1;
|
|
565
|
+
if depth == 0 {
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
let inner: String = chars[start + 1..i.saturating_sub(1)].iter().collect();
|
|
570
|
+
let mut item = String::new();
|
|
571
|
+
let mut d = 0;
|
|
572
|
+
let mut items = Vec::new();
|
|
573
|
+
for ch in inner.chars() {
|
|
574
|
+
match ch {
|
|
575
|
+
'(' | '[' => d += 1,
|
|
576
|
+
')' | ']' => d -= 1,
|
|
577
|
+
_ => {}
|
|
578
|
+
}
|
|
579
|
+
if ch == ',' && d == 0 {
|
|
580
|
+
items.push(std::mem::take(&mut item));
|
|
581
|
+
} else {
|
|
582
|
+
item.push(ch);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
items.push(item);
|
|
586
|
+
for it in items {
|
|
587
|
+
let name = it.split('(').next().unwrap_or("").trim().to_string();
|
|
588
|
+
if !name.is_empty() {
|
|
589
|
+
out.insert(name);
|
|
590
|
+
}
|
|
591
|
+
if let Some(pos) = it.find('(') {
|
|
592
|
+
collectTagNames(&it[pos + 1..], out);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
i += 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// Pushes the boxed form of the payload value on top of the stack (typed `ty`)
|
|
602
|
+
/// as an `anyref`: `Int`/`Float`/`Byte` get a `$BoxI64`/`$BoxF64`/`$BoxI32`,
|
|
603
|
+
/// reference values are stored as-is.
|
|
604
|
+
fn boxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
|
|
605
|
+
match plumTypeToValtype(ty) {
|
|
606
|
+
ValType::I64 => Instruction::StructNew(ctx.gc_types.box_i64_idx).encode(body),
|
|
607
|
+
ValType::F64 => Instruction::StructNew(ctx.gc_types.box_f64_idx).encode(body),
|
|
608
|
+
ValType::I32 => Instruction::StructNew(ctx.gc_types.box_i32_idx).encode(body),
|
|
609
|
+
_ => {}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// Inverse of `boxTagPayload`: an `anyref` payload on the stack becomes a `ty` value.
|
|
614
|
+
fn unboxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
|
|
615
|
+
let unbox = |idx: u32, body: &mut Vec<u8>| {
|
|
616
|
+
Instruction::RefCastNonNull(HeapType::Concrete(idx)).encode(body);
|
|
617
|
+
Instruction::StructGet {
|
|
618
|
+
struct_type_index: idx,
|
|
619
|
+
field_index: 0,
|
|
620
|
+
}
|
|
621
|
+
.encode(body);
|
|
622
|
+
};
|
|
623
|
+
match plumTypeToValtype(ty) {
|
|
624
|
+
ValType::I64 => unbox(ctx.gc_types.box_i64_idx, body),
|
|
625
|
+
ValType::F64 => unbox(ctx.gc_types.box_f64_idx, body),
|
|
626
|
+
ValType::I32 => unbox(ctx.gc_types.box_i32_idx, body),
|
|
627
|
+
ValType::Ref(rt) => {
|
|
628
|
+
if let HeapType::Concrete(_) = rt.heap_type {
|
|
629
|
+
Instruction::RefCastNullable(rt.heap_type).encode(body);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
_ => {}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/// Builds a tag value (`NotFound`, `Bad(x)`): `id` plus the optional payload,
|
|
637
|
+
/// boxed by `boxTagPayload`, as a `$Tag` struct.
|
|
638
|
+
fn compileTagLiteral(
|
|
639
|
+
name: &str,
|
|
640
|
+
payload: Option<&ast::Expr>,
|
|
641
|
+
body: &mut Vec<u8>,
|
|
642
|
+
ctx: &LocalCtx,
|
|
643
|
+
state: &mut ModuleState,
|
|
644
|
+
) -> Result<(), String> {
|
|
645
|
+
Instruction::I32Const(tagId(name)).encode(body);
|
|
646
|
+
match payload {
|
|
647
|
+
Some(e) => {
|
|
648
|
+
let ty = inferLocalType(e, ctx);
|
|
649
|
+
compileExpr(e, body, ctx, state)?;
|
|
650
|
+
boxTagPayload(&ty, body, ctx);
|
|
651
|
+
}
|
|
652
|
+
None => Instruction::RefNull(RefType::ANYREF.heap_type).encode(body),
|
|
653
|
+
}
|
|
654
|
+
Instruction::StructNew(ctx.gc_types.tag_type_idx).encode(body);
|
|
655
|
+
Ok(())
|
|
656
|
+
}
|
|
657
|
+
|
|
520
658
|
fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
|
|
521
659
|
CURRENT_GC_TYPES.with(|c| {
|
|
522
660
|
let borrow = c.borrow();
|
|
@@ -570,6 +708,10 @@ fn astTypeToWasm(name: &str) -> Option<ValType> {
|
|
|
570
708
|
// this name, orphaned/unused) class struct type instead of the shared
|
|
571
709
|
// `array<i8>` ref every `[]Byte` value actually is.
|
|
572
710
|
"ByteSlice" => Some(plumTypeToValtype(&PlumType::TByteSlice)),
|
|
711
|
+
// A structural tag union `[A,B(Str)]` (but not the `[]Byte` slice spelling).
|
|
712
|
+
other if other.starts_with('[') && !other.starts_with("[]") => {
|
|
713
|
+
Some(withGcTypes(|r| gcRef(r.tag_type_idx)))
|
|
714
|
+
}
|
|
573
715
|
other => Some(plumTypeToValtype(&PlumType::TNamed(other.to_string()))),
|
|
574
716
|
}
|
|
575
717
|
}
|
|
@@ -649,7 +791,8 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
|
|
|
649
791
|
// where a closure's exact struct type isn't being constructed/destructured
|
|
650
792
|
// directly (e.g. deciding a local's storage class), matching `TVariadic`'s
|
|
651
793
|
// existing "not modeled as a concrete shape here" treatment.
|
|
794
|
+
PlumType::TTags(_) => withGcTypes(|r| gcRef(r.tag_type_idx)),
|
|
652
|
-
PlumType::TFun(_, _) | PlumType::TVariadic(_)
|
|
795
|
+
PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
|
|
653
796
|
PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
|
|
654
797
|
}
|
|
655
798
|
}
|
|
@@ -689,6 +832,13 @@ pub struct GcTypeRegistry {
|
|
|
689
832
|
/// harmlessly never read as such) until/unless the program actually
|
|
690
833
|
/// instantiates `Array` at least once.
|
|
691
834
|
pub array_type_idx: u32,
|
|
835
|
+
/// The single `$Tag {id: i32, payload: anyref}` struct every structural
|
|
836
|
+
/// tag-union value (`NotFound`, `Bad(x)`) is represented as.
|
|
837
|
+
pub tag_type_idx: u32,
|
|
838
|
+
/// Boxes for non-reference tag payloads (`Int`/`Float`/`Byte`).
|
|
839
|
+
pub box_i64_idx: u32,
|
|
840
|
+
pub box_f64_idx: u32,
|
|
841
|
+
pub box_i32_idx: u32,
|
|
692
842
|
}
|
|
693
843
|
|
|
694
844
|
/// Whether `name` is `Array` itself or one of its monomorphized specializations
|
|
@@ -755,7 +905,8 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
|
|
|
755
905
|
// TFun (closures) and TVariadic get their own concrete representation once
|
|
756
906
|
// Task 2 (closures/variadic calls) lands — `anyref` is a safe, valid-but-not-
|
|
757
907
|
// yet-meaningful placeholder in the meantime, since nothing consumes it yet.
|
|
908
|
+
PlumType::TTags(_) => gcRef(registry.tag_type_idx),
|
|
758
|
-
PlumType::TFun(_, _) | PlumType::TVariadic(_)
|
|
909
|
+
PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
|
|
759
910
|
PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
|
|
760
911
|
}
|
|
761
912
|
}
|
|
@@ -781,6 +932,10 @@ fn buildGcTypeRegistry(
|
|
|
781
932
|
/// The single shared `array<anyref>` type every `Array[T]`
|
|
782
933
|
/// specialization aliases — see `isArraySpecialization`.
|
|
783
934
|
ArrayRef,
|
|
935
|
+
Tag,
|
|
936
|
+
BoxI64,
|
|
937
|
+
BoxF64,
|
|
938
|
+
BoxI32,
|
|
784
939
|
}
|
|
785
940
|
|
|
786
941
|
// Pass 1: assign every entry a slot (and therefore a type index) up front, before
|
|
@@ -790,6 +945,8 @@ fn buildGcTypeRegistry(
|
|
|
790
945
|
let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
|
|
791
946
|
let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
|
|
792
947
|
let mut array_ref_slot: Option<u32> = None;
|
|
948
|
+
let mut erased_enum_super: HashMap<String, u32> = HashMap::new();
|
|
949
|
+
let mut erased_variant_idx: HashMap<String, u32> = HashMap::new();
|
|
793
950
|
|
|
794
951
|
// `Bool` is an ordinary enum now (`type Bool = | True | False` in
|
|
795
952
|
// `libs/std/Bool.plum`) — registered exactly like any other enum
|
|
@@ -824,10 +981,31 @@ fn buildGcTypeRegistry(
|
|
|
824
981
|
}
|
|
825
982
|
continue;
|
|
826
983
|
}
|
|
984
|
+
// Specializations that differ only in their tag-union segments
|
|
985
|
+
// (`Try$Int$[A]` / `Try$Int$[A,B]`) share ONE set of wasm types — every
|
|
986
|
+
// tag value is the same `$Tag` struct — so a later one is aliased onto
|
|
987
|
+
// the first one's slots instead of getting its own.
|
|
988
|
+
let erased_enum = plum_checker::erasedEnumName(enum_name);
|
|
989
|
+
if let Some(existing_super) = erased_enum_super.get(&erased_enum) {
|
|
990
|
+
enum_super_type_idx.insert(enum_name.clone(), *existing_super);
|
|
991
|
+
for vname in variant_names {
|
|
992
|
+
let ev = plum_checker::erasedEnumName(vname);
|
|
993
|
+
if let Some(idx) = erased_variant_idx.get(&ev) {
|
|
994
|
+
variant_type_idx.insert(vname.clone(), *idx);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
variant_type_idx.insert(vname.clone(), slots.len() as u32);
|
|
998
|
+
erased_variant_idx.insert(ev, slots.len() as u32);
|
|
999
|
+
slots.push(Slot::Variant(vname.clone()));
|
|
1000
|
+
}
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
827
1003
|
enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
|
|
1004
|
+
erased_enum_super.insert(erased_enum, slots.len() as u32);
|
|
828
1005
|
slots.push(Slot::EnumSuper(enum_name.clone()));
|
|
829
1006
|
for vname in variant_names {
|
|
830
1007
|
variant_type_idx.insert(vname.clone(), slots.len() as u32);
|
|
1008
|
+
erased_variant_idx.insert(plum_checker::erasedEnumName(vname), slots.len() as u32);
|
|
831
1009
|
slots.push(Slot::Variant(vname.clone()));
|
|
832
1010
|
}
|
|
833
1011
|
// A RECORD-shaped enum (the `type X = ...` replacement, e.g. `enum Cat =
|
|
@@ -842,6 +1020,15 @@ fn buildGcTypeRegistry(
|
|
|
842
1020
|
}
|
|
843
1021
|
}
|
|
844
1022
|
|
|
1023
|
+
let tag_type_idx = slots.len() as u32;
|
|
1024
|
+
slots.push(Slot::Tag);
|
|
1025
|
+
let box_i64_idx = slots.len() as u32;
|
|
1026
|
+
slots.push(Slot::BoxI64);
|
|
1027
|
+
let box_f64_idx = slots.len() as u32;
|
|
1028
|
+
slots.push(Slot::BoxF64);
|
|
1029
|
+
let box_i32_idx = slots.len() as u32;
|
|
1030
|
+
slots.push(Slot::BoxI32);
|
|
1031
|
+
|
|
845
1032
|
// The registry is fully index-complete after pass 1 (every name has an assigned
|
|
846
1033
|
// slot) even though no field lists exist yet — safe to hand to `plumTypeToGcValtype`
|
|
847
1034
|
// for pass 2's field-type resolution.
|
|
@@ -853,6 +1040,10 @@ fn buildGcTypeRegistry(
|
|
|
853
1040
|
closure_type_idx: 0,
|
|
854
1041
|
variadic_array_type_idx: HashMap::new(),
|
|
855
1042
|
array_type_idx: array_ref_slot.unwrap_or(0),
|
|
1043
|
+
tag_type_idx,
|
|
1044
|
+
box_i64_idx,
|
|
1045
|
+
box_f64_idx,
|
|
1046
|
+
box_i32_idx,
|
|
856
1047
|
};
|
|
857
1048
|
|
|
858
1049
|
// Pass 2: build the real SubType for every slot, now that every cross-reference
|
|
@@ -939,6 +1130,29 @@ fn buildGcTypeRegistry(
|
|
|
939
1130
|
},
|
|
940
1131
|
}
|
|
941
1132
|
}
|
|
1133
|
+
Slot::Tag => SubType {
|
|
1134
|
+
is_final: true,
|
|
1135
|
+
supertype_idx: None,
|
|
1136
|
+
composite_type: CompositeType {
|
|
1137
|
+
inner: CompositeInnerType::Struct(StructType {
|
|
1138
|
+
fields: vec![
|
|
1139
|
+
FieldType {
|
|
1140
|
+
element_type: StorageType::Val(ValType::I32),
|
|
1141
|
+
mutable: false,
|
|
1142
|
+
},
|
|
1143
|
+
FieldType {
|
|
1144
|
+
element_type: StorageType::Val(ValType::Ref(RefType::ANYREF)),
|
|
1145
|
+
mutable: false,
|
|
1146
|
+
},
|
|
1147
|
+
]
|
|
1148
|
+
.into(),
|
|
1149
|
+
}),
|
|
1150
|
+
shared: false,
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
Slot::BoxI64 => boxSubType(ValType::I64),
|
|
1154
|
+
Slot::BoxF64 => boxSubType(ValType::F64),
|
|
1155
|
+
Slot::BoxI32 => boxSubType(ValType::I32),
|
|
942
1156
|
Slot::ArrayRef => SubType {
|
|
943
1157
|
is_final: true,
|
|
944
1158
|
supertype_idx: None,
|
|
@@ -958,6 +1172,23 @@ fn buildGcTypeRegistry(
|
|
|
958
1172
|
registry
|
|
959
1173
|
}
|
|
960
1174
|
|
|
1175
|
+
fn boxSubType(vt: ValType) -> SubType {
|
|
1176
|
+
SubType {
|
|
1177
|
+
is_final: true,
|
|
1178
|
+
supertype_idx: None,
|
|
1179
|
+
composite_type: CompositeType {
|
|
1180
|
+
inner: CompositeInnerType::Struct(StructType {
|
|
1181
|
+
fields: vec![FieldType {
|
|
1182
|
+
element_type: StorageType::Val(vt),
|
|
1183
|
+
mutable: false,
|
|
1184
|
+
}]
|
|
1185
|
+
.into(),
|
|
1186
|
+
}),
|
|
1187
|
+
shared: false,
|
|
1188
|
+
},
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
961
1192
|
/// Maps an `ast::ParamType::Fn(params, ret)` to the wasm signature of the *closure
|
|
962
1193
|
/// function* it compiles to: an implicit leading `env: anyref`, then one param per
|
|
963
1194
|
/// declared param type, returning `ret`'s val type (or nothing for `Unit`).
|
|
@@ -1232,6 +1463,31 @@ fn compileSourceInner(
|
|
|
1232
1463
|
let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
|
|
1233
1464
|
let min_required = plum_checker::buildMinRequiredArgs(source);
|
|
1234
1465
|
|
|
1466
|
+
// Intern every tag name the program's types mention, in sorted order.
|
|
1467
|
+
{
|
|
1468
|
+
let mut names = std::collections::BTreeSet::new();
|
|
1469
|
+
for (k, info) in &enum_variants {
|
|
1470
|
+
collectTagNames(k, &mut names);
|
|
1471
|
+
collectTagNames(&info.enum_name, &mut names);
|
|
1472
|
+
for t in &info.field_types {
|
|
1473
|
+
collectTagNames(&t.to_string(), &mut names);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
for scheme in global_env.values() {
|
|
1477
|
+
collectTagNames(&scheme.body.to_string(), &mut names);
|
|
1478
|
+
}
|
|
1479
|
+
for t in methods.values() {
|
|
1480
|
+
collectTagNames(&t.to_string(), &mut names);
|
|
1481
|
+
}
|
|
1482
|
+
CURRENT_TAG_IDS.with(|c| {
|
|
1483
|
+
*c.borrow_mut() = names
|
|
1484
|
+
.into_iter()
|
|
1485
|
+
.enumerate()
|
|
1486
|
+
.map(|(i, n)| (n, i as i32))
|
|
1487
|
+
.collect();
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1235
1491
|
let mut module = WasmModule::new();
|
|
1236
1492
|
|
|
1237
1493
|
let mut gc_types = buildGcTypeRegistry(&mut module, source, &enum_variants, &enum_params);
|
|
@@ -1317,6 +1573,21 @@ fn compileSourceInner(
|
|
|
1317
1573
|
}
|
|
1318
1574
|
}
|
|
1319
1575
|
|
|
1576
|
+
// `plum::crash(Str)`: same wasm type as `printLn`; imported only when the
|
|
1577
|
+
// program actually contains a `crash` statement.
|
|
1578
|
+
{
|
|
1579
|
+
let dbg = format!("{:?}", source.items);
|
|
1580
|
+
let uses_crash = dbg
|
|
1581
|
+
.match_indices("Crash(")
|
|
1582
|
+
.any(|(i, _)| i == 0 || matches!(dbg.as_bytes()[i - 1], b'[' | b'(' | b' '));
|
|
1583
|
+
if uses_crash {
|
|
1584
|
+
let str_vt = plumTypeToValtype(&PlumType::TStr);
|
|
1585
|
+
let type_idx = module.addType(&[str_vt], &[]);
|
|
1586
|
+
let func_idx = module.addImport("plum", "crash", type_idx);
|
|
1587
|
+
func_ids.insert(CRASH_FN_KEY.to_string(), func_idx);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1320
1591
|
// Pre-allocate one instance of every payload-free variant (True/False/None/...)
|
|
1321
1592
|
// as a global, populated once by a `start` function rather than reconstructed on
|
|
1322
1593
|
// every reference — see this migration plan's Decision 2. `struct.new` isn't
|
|
@@ -3094,6 +3365,18 @@ impl<'a> Collector<'a> {
|
|
|
3094
3365
|
/// sub-pattern found *nested* inside another constructor pattern's fields (the
|
|
3095
3366
|
/// outermost, per-subject pattern doesn't need one — see `nested_class_scratch`).
|
|
3096
3367
|
fn collectPattern(&mut self, pat: &ast::CasePattern, ty: &PlumType) {
|
|
3368
|
+
// Tag-union subject/field: uppercase names are tags (no binding, no
|
|
3369
|
+
// narrowing scratch slot); a tag's payload sub-pattern binds to its type.
|
|
3370
|
+
if let PlumType::TTags(tags) = ty {
|
|
3371
|
+
if isTagPattern(pat) {
|
|
3372
|
+
if let ast::CasePattern::Class { name, fields } = pat {
|
|
3373
|
+
if let (Some(Some(pty)), Some(f)) = (tags.get(name), fields.first()) {
|
|
3374
|
+
self.collectPattern(f, pty);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3097
3380
|
match pat {
|
|
3098
3381
|
ast::CasePattern::Name(n) => {
|
|
3099
3382
|
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
@@ -3473,6 +3756,8 @@ fn compileFnBody(
|
|
|
3473
3756
|
type_env: RefCell::new(base_env),
|
|
3474
3757
|
closure_local_sigs: RefCell::new(HashMap::new()),
|
|
3475
3758
|
loop_extra_depth: Cell::new(0),
|
|
3759
|
+
ret_type: f.returns.as_ref().map(|r| plum_checker::plumTypeFromAst(r)),
|
|
3760
|
+
match_subject_types: RefCell::new(HashMap::new()),
|
|
3476
3761
|
};
|
|
3477
3762
|
|
|
3478
3763
|
let result_vt = retTypeToWasm(f.returns.as_ref());
|
|
@@ -3561,7 +3846,9 @@ fn compileStmtInValuePosition(
|
|
|
3561
3846
|
) -> Result<(), String> {
|
|
3562
3847
|
match stmt {
|
|
3563
3848
|
ast::Stmt::Expr(e, ..) => compileExpr(e, body, ctx, state),
|
|
3564
|
-
ast::Stmt::Return(_) | ast::Stmt::Todo
|
|
3849
|
+
ast::Stmt::Return(_) | ast::Stmt::Todo | ast::Stmt::Crash(_) => {
|
|
3850
|
+
compileStmt(stmt, body, ctx, state)
|
|
3851
|
+
}
|
|
3565
3852
|
ast::Stmt::If(if_) => compileIf(if_, Some(result_vt), body, ctx, state),
|
|
3566
3853
|
ast::Stmt::Match(m) => compileMatch(m, body, ctx, state, Some(result_vt)),
|
|
3567
3854
|
_ => Err(
|
|
@@ -3993,8 +4280,18 @@ fn compileStmt(
|
|
|
3993
4280
|
Instruction::Unreachable.encode(body);
|
|
3994
4281
|
Instruction::End.encode(body);
|
|
3995
4282
|
}
|
|
3996
|
-
//
|
|
4283
|
+
// `crash msg` — hand the message to the host's `plum::crash(Str)`
|
|
4284
|
+
// import (registered only when the program uses `crash`, see
|
|
4285
|
+
// `CRASH_FN_KEY`), then trap.
|
|
4286
|
+
ast::Stmt::Crash(msg) => {
|
|
4287
|
+
compileExpr(msg, body, ctx, state)?;
|
|
4288
|
+
let crash_idx = *ctx
|
|
4289
|
+
.func_ids
|
|
4290
|
+
.get(CRASH_FN_KEY)
|
|
4291
|
+
.ok_or_else(|| "internal codegen error: crash import missing".to_string())?;
|
|
4292
|
+
Instruction::Call(crash_idx).encode(body);
|
|
3997
|
-
|
|
4293
|
+
Instruction::Unreachable.encode(body);
|
|
4294
|
+
}
|
|
3998
4295
|
ast::Stmt::Todo => {
|
|
3999
4296
|
// Marks an unimplemented body — trap rather than silently continuing.
|
|
4000
4297
|
Instruction::Unreachable.encode(body);
|
|
@@ -4131,6 +4428,83 @@ fn matchCoversEveryEnumVariant(
|
|
|
4131
4428
|
!tuples_seen.is_empty() && tuples_seen.len() == total_combinations
|
|
4132
4429
|
}
|
|
4133
4430
|
|
|
4431
|
+
/// An uppercase `Name`/`Class` pattern — against a tag-union subject, a tag arm.
|
|
4432
|
+
fn isTagPattern(pat: &ast::CasePattern) -> bool {
|
|
4433
|
+
let upper = |n: &str| n.chars().next().is_some_and(|c| c.is_uppercase());
|
|
4434
|
+
match pat {
|
|
4435
|
+
ast::CasePattern::Name(n) => upper(n),
|
|
4436
|
+
ast::CasePattern::Class { name, .. } => upper(name),
|
|
4437
|
+
_ => false,
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
/// Compiles one tag arm (`A =>` / `B(s) =>`) against a `$Tag` value that
|
|
4442
|
+
/// `load` pushes: tests `id`, binds/unboxes the payload, then runs `on_match`,
|
|
4443
|
+
/// else `on_fail`.
|
|
4444
|
+
#[allow(clippy::too_many_arguments)]
|
|
4445
|
+
fn compileTagPattern(
|
|
4446
|
+
pat: &ast::CasePattern,
|
|
4447
|
+
tags: &std::collections::BTreeMap<String, Option<Box<PlumType>>>,
|
|
4448
|
+
load: &dyn Fn(&mut Vec<u8>),
|
|
4449
|
+
result_vt: Option<ValType>,
|
|
4450
|
+
body: &mut Vec<u8>,
|
|
4451
|
+
ctx: &LocalCtx,
|
|
4452
|
+
state: &mut ModuleState,
|
|
4453
|
+
on_match: &mut dyn FnMut(&mut Vec<u8>, &mut ModuleState) -> Result<(), String>,
|
|
4454
|
+
on_fail: &mut dyn FnMut(&mut Vec<u8>, &mut ModuleState) -> Result<(), String>,
|
|
4455
|
+
) -> Result<(), String> {
|
|
4456
|
+
let (name, sub) = match pat {
|
|
4457
|
+
ast::CasePattern::Name(n) => (n.as_str(), None),
|
|
4458
|
+
ast::CasePattern::Class { name, fields } => (name.as_str(), fields.first()),
|
|
4459
|
+
_ => unreachable!("isTagPattern checked"),
|
|
4460
|
+
};
|
|
4461
|
+
let payload_ty = tags
|
|
4462
|
+
.get(name)
|
|
4463
|
+
.ok_or_else(|| format!("codegen: tag '{}' is not part of the matched tag union", name))?
|
|
4464
|
+
.as_ref()
|
|
4465
|
+
.map(|b| (**b).clone());
|
|
4466
|
+
let tag_idx = ctx.gc_types.tag_type_idx;
|
|
4467
|
+
load(body);
|
|
4468
|
+
Instruction::StructGet {
|
|
4469
|
+
struct_type_index: tag_idx,
|
|
4470
|
+
field_index: 0,
|
|
4471
|
+
}
|
|
4472
|
+
.encode(body);
|
|
4473
|
+
Instruction::I32Const(tagId(name)).encode(body);
|
|
4474
|
+
Instruction::I32Eq.encode(body);
|
|
4475
|
+
Instruction::If(blockTypeFor(result_vt)).encode(body);
|
|
4476
|
+
match (sub, &payload_ty) {
|
|
4477
|
+
(None, _) | (Some(ast::CasePattern::Wildcard), _) => {}
|
|
4478
|
+
(Some(ast::CasePattern::Name(n)), Some(pty)) if !isTagPattern(&ast::CasePattern::Name(n.clone())) => {
|
|
4479
|
+
let idx = ctx.locals.get(n).copied().ok_or_else(|| {
|
|
4480
|
+
format!("internal codegen error: missing binding local '{}'", n)
|
|
4481
|
+
})?;
|
|
4482
|
+
load(body);
|
|
4483
|
+
Instruction::StructGet {
|
|
4484
|
+
struct_type_index: tag_idx,
|
|
4485
|
+
field_index: 1,
|
|
4486
|
+
}
|
|
4487
|
+
.encode(body);
|
|
4488
|
+
unboxTagPayload(pty, body, ctx);
|
|
4489
|
+
Instruction::LocalSet(idx).encode(body);
|
|
4490
|
+
ctx.type_env
|
|
4491
|
+
.borrow_mut()
|
|
4492
|
+
.insert(n.clone(), TypeScheme::mono(pty.clone()));
|
|
4493
|
+
}
|
|
4494
|
+
(Some(_), _) => {
|
|
4495
|
+
return Err(format!(
|
|
4496
|
+
"codegen: unsupported pattern inside tag '{}' payload (use a name or '_')",
|
|
4497
|
+
name
|
|
4498
|
+
))
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
on_match(body, state)?;
|
|
4502
|
+
Instruction::Else.encode(body);
|
|
4503
|
+
on_fail(body, state)?;
|
|
4504
|
+
Instruction::End.encode(body);
|
|
4505
|
+
Ok(())
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4134
4508
|
fn compileMatch(
|
|
4135
4509
|
m: &ast::Match,
|
|
4136
4510
|
body: &mut Vec<u8>,
|
|
@@ -4151,6 +4525,11 @@ fn compileMatch(
|
|
|
4151
4525
|
let subject_ty = inferLocalType(subject, ctx);
|
|
4152
4526
|
let subject_vt = plumTypeToValtype(&subject_ty);
|
|
4153
4527
|
let scratch_local = ctx.match_scratch_base + base_slot + i as u32;
|
|
4528
|
+
if matches!(subject_ty, PlumType::TTags(_)) {
|
|
4529
|
+
ctx.match_subject_types
|
|
4530
|
+
.borrow_mut()
|
|
4531
|
+
.insert(scratch_local, subject_ty.clone());
|
|
4532
|
+
}
|
|
4154
4533
|
compileExpr(subject, body, ctx, state)?;
|
|
4155
4534
|
Instruction::LocalSet(scratch_local).encode(body);
|
|
4156
4535
|
all_subjects.push((subject_vt, scratch_local));
|
|
@@ -4166,8 +4545,14 @@ fn compileMatch(
|
|
|
4166
4545
|
// would reallocate every nested `CasePattern::Class` node at a new address and
|
|
4167
4546
|
// break `nested_class_scratch`'s pointer-identity-keyed lookup.
|
|
4168
4547
|
let subject_vts: Vec<ValType> = all_subjects.iter().map(|(vt, _)| *vt).collect();
|
|
4548
|
+
// A match over a tag union with no catch-all was proven exhaustive by the checker.
|
|
4549
|
+
let tag_union_match = m.subjects.len() == 1
|
|
4550
|
+
&& ctx
|
|
4551
|
+
.match_subject_types
|
|
4552
|
+
.borrow()
|
|
4553
|
+
.contains_key(&all_subjects[0].1);
|
|
4169
|
-
let exhaustive_fallback =
|
|
4554
|
+
let exhaustive_fallback = result_vt.is_some()
|
|
4170
|
-
|
|
4555
|
+
&& (tag_union_match || matchCoversEveryEnumVariant(&m.cases, &subject_vts, ctx));
|
|
4171
4556
|
|
|
4172
4557
|
compileMatchArmsMulti(
|
|
4173
4558
|
&m.cases,
|
|
@@ -4277,6 +4662,44 @@ fn compileCasePositions(
|
|
|
4277
4662
|
}
|
|
4278
4663
|
let pat = &case.patterns[pos];
|
|
4279
4664
|
let (subject_vt, scratch_local) = all_subjects[pos];
|
|
4665
|
+
let tag_subject = ctx.match_subject_types.borrow().get(&scratch_local).cloned();
|
|
4666
|
+
if let Some(PlumType::TTags(tags)) = &tag_subject {
|
|
4667
|
+
if isTagPattern(pat) {
|
|
4668
|
+
return compileTagPattern(
|
|
4669
|
+
pat,
|
|
4670
|
+
tags,
|
|
4671
|
+
&|b: &mut Vec<u8>| Instruction::LocalGet(scratch_local).encode(b),
|
|
4672
|
+
result_vt,
|
|
4673
|
+
body,
|
|
4674
|
+
ctx,
|
|
4675
|
+
state,
|
|
4676
|
+
&mut |body, state| {
|
|
4677
|
+
compileCasePositions(
|
|
4678
|
+
case,
|
|
4679
|
+
pos + 1,
|
|
4680
|
+
all_subjects,
|
|
4681
|
+
rest,
|
|
4682
|
+
result_vt,
|
|
4683
|
+
exhaustive_fallback,
|
|
4684
|
+
body,
|
|
4685
|
+
ctx,
|
|
4686
|
+
state,
|
|
4687
|
+
)
|
|
4688
|
+
},
|
|
4689
|
+
&mut |body, state| {
|
|
4690
|
+
compileMatchArmsMulti(
|
|
4691
|
+
rest,
|
|
4692
|
+
all_subjects,
|
|
4693
|
+
result_vt,
|
|
4694
|
+
exhaustive_fallback,
|
|
4695
|
+
body,
|
|
4696
|
+
ctx,
|
|
4697
|
+
state,
|
|
4698
|
+
)
|
|
4699
|
+
},
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4280
4703
|
match pat {
|
|
4281
4704
|
ast::CasePattern::Wildcard => {
|
|
4282
4705
|
// Always matches this position; move on to the next one.
|
|
@@ -4318,7 +4741,9 @@ fn compileCasePositions(
|
|
|
4318
4741
|
Instruction::LocalSet(idx).encode(body);
|
|
4319
4742
|
ctx.type_env.borrow_mut().insert(
|
|
4320
4743
|
n.clone(),
|
|
4744
|
+
TypeScheme::mono(
|
|
4321
|
-
|
|
4745
|
+
tag_subject.unwrap_or_else(|| plumTypeFromValtypeHint(subject_vt)),
|
|
4746
|
+
),
|
|
4322
4747
|
);
|
|
4323
4748
|
// A binding always matches this position; move on to the next one.
|
|
4324
4749
|
compileCasePositions(
|
|
@@ -4588,6 +5013,55 @@ fn compileFieldPatterns(
|
|
|
4588
5013
|
let pat = &fields[fpos];
|
|
4589
5014
|
let field_ty = &field_types[fpos];
|
|
4590
5015
|
|
|
5016
|
+
if let PlumType::TTags(tags) = field_ty {
|
|
5017
|
+
if isTagPattern(pat) {
|
|
5018
|
+
return compileTagPattern(
|
|
5019
|
+
pat,
|
|
5020
|
+
tags,
|
|
5021
|
+
&|b: &mut Vec<u8>| {
|
|
5022
|
+
Instruction::LocalGet(container_local).encode(b);
|
|
5023
|
+
Instruction::StructGet {
|
|
5024
|
+
struct_type_index: container_type_idx,
|
|
5025
|
+
field_index: fpos as u32,
|
|
5026
|
+
}
|
|
5027
|
+
.encode(b);
|
|
5028
|
+
},
|
|
5029
|
+
result_vt,
|
|
5030
|
+
body,
|
|
5031
|
+
ctx,
|
|
5032
|
+
state,
|
|
5033
|
+
&mut |body, state| {
|
|
5034
|
+
compileFieldPatterns(
|
|
5035
|
+
fields,
|
|
5036
|
+
field_types,
|
|
5037
|
+
fpos + 1,
|
|
5038
|
+
container_local,
|
|
5039
|
+
container_type_idx,
|
|
5040
|
+
rest,
|
|
5041
|
+
all_subjects,
|
|
5042
|
+
result_vt,
|
|
5043
|
+
exhaustive_fallback,
|
|
5044
|
+
body,
|
|
5045
|
+
ctx,
|
|
5046
|
+
state,
|
|
5047
|
+
on_match,
|
|
5048
|
+
)
|
|
5049
|
+
},
|
|
5050
|
+
&mut |body, state| {
|
|
5051
|
+
compileMatchArmsMulti(
|
|
5052
|
+
rest,
|
|
5053
|
+
all_subjects,
|
|
5054
|
+
result_vt,
|
|
5055
|
+
exhaustive_fallback,
|
|
5056
|
+
body,
|
|
5057
|
+
ctx,
|
|
5058
|
+
state,
|
|
5059
|
+
)
|
|
5060
|
+
},
|
|
5061
|
+
);
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
|
|
4591
5065
|
match pat {
|
|
4592
5066
|
ast::CasePattern::Wildcard => compileFieldPatterns(
|
|
4593
5067
|
fields,
|
|
@@ -4951,6 +5425,35 @@ fn compileExpr(
|
|
|
4951
5425
|
Instruction::If(BlockType::Result(success_vt)).encode(body);
|
|
4952
5426
|
Instruction::LocalGet(scratch_local).encode(body);
|
|
4953
5427
|
Instruction::RefCastNonNull(HeapType::Concrete(failure_idx)).encode(body);
|
|
5428
|
+
// Failure branch: take the inner `Err` payload and build the OUTER
|
|
5429
|
+
// function's `Err` variant from it (the Ok types may differ).
|
|
5430
|
+
let outer_err = match &ctx.ret_type {
|
|
5431
|
+
Some(PlumType::TNamed(outer_enum)) if *outer_enum != enum_name => {
|
|
5432
|
+
plum_checker::tryOperatorVariants(outer_enum, &cctx)
|
|
5433
|
+
.map(|(_, _, fname, _)| fname)
|
|
5434
|
+
}
|
|
5435
|
+
_ => None,
|
|
5436
|
+
};
|
|
5437
|
+
if let Some(outer_err_name) = outer_err {
|
|
5438
|
+
let outer_idx = *ctx
|
|
5439
|
+
.gc_types
|
|
5440
|
+
.variant_type_idx
|
|
5441
|
+
.get(outer_err_name)
|
|
5442
|
+
.ok_or_else(|| {
|
|
5443
|
+
format!(
|
|
5444
|
+
"codegen: variant '{}' missing from the GC type registry",
|
|
5445
|
+
outer_err_name
|
|
5446
|
+
)
|
|
5447
|
+
})?;
|
|
5448
|
+
if outer_idx != failure_idx {
|
|
5449
|
+
Instruction::StructGet {
|
|
5450
|
+
struct_type_index: failure_idx,
|
|
5451
|
+
field_index: 0,
|
|
5452
|
+
}
|
|
5453
|
+
.encode(body);
|
|
5454
|
+
Instruction::StructNew(outer_idx).encode(body);
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
4954
5457
|
Instruction::Return.encode(body);
|
|
4955
5458
|
Instruction::Else.encode(body);
|
|
4956
5459
|
Instruction::LocalGet(scratch_local).encode(body);
|
|
@@ -5116,6 +5619,30 @@ fn compileExpr(
|
|
|
5116
5619
|
}
|
|
5117
5620
|
ast::Expr::Compare(c) => {
|
|
5118
5621
|
let left_ty = inferLocalType(&c.left, ctx);
|
|
5622
|
+
// Tag unions compare by `$Tag.id` (the checker rejects payload tags).
|
|
5623
|
+
if matches!(left_ty, PlumType::TTags(_)) {
|
|
5624
|
+
let tag_idx = ctx.gc_types.tag_type_idx;
|
|
5625
|
+
for side in [&c.left, &c.right] {
|
|
5626
|
+
compileExpr(side, body, ctx, state)?;
|
|
5627
|
+
Instruction::StructGet {
|
|
5628
|
+
struct_type_index: tag_idx,
|
|
5629
|
+
field_index: 0,
|
|
5630
|
+
}
|
|
5631
|
+
.encode(body);
|
|
5632
|
+
}
|
|
5633
|
+
match &c.op {
|
|
5634
|
+
ast::CmpOp::Eq => Instruction::I32Eq.encode(body),
|
|
5635
|
+
ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I32Ne.encode(body),
|
|
5636
|
+
other => {
|
|
5637
|
+
return Err(format!(
|
|
5638
|
+
"codegen: '{:?}' is not supported between tag unions",
|
|
5639
|
+
other
|
|
5640
|
+
))
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
pushBoolRefFromI32Flag(body, ctx);
|
|
5644
|
+
return Ok(());
|
|
5645
|
+
}
|
|
5119
5646
|
compileExpr(&c.left, body, ctx, state)?;
|
|
5120
5647
|
compileExpr(&c.right, body, ctx, state)?;
|
|
5121
5648
|
match left_ty {
|
|
@@ -5282,6 +5809,18 @@ fn compileExpr(
|
|
|
5282
5809
|
compileExpr(&synthetic, body, ctx, state)?;
|
|
5283
5810
|
} else if let Some(info) = ctx.enum_variants.get(&call.name) {
|
|
5284
5811
|
compileVariantConstruction(info, call, expr, body, ctx, state)?;
|
|
5812
|
+
} else if !ctx.func_ids.contains_key(&call.name)
|
|
5813
|
+
&& call.args.len() <= 1
|
|
5814
|
+
&& call.name.chars().next().is_some_and(|c| c.is_uppercase())
|
|
5815
|
+
&& matches!(inferLocalType(expr, ctx), PlumType::TTags(_))
|
|
5816
|
+
{
|
|
5817
|
+
// `Bad(x)` / `Bad()` — an undeclared uppercase name is a tag literal.
|
|
5818
|
+
let payload = call.args.first().map(|a| match a {
|
|
5819
|
+
ast::Arg::Positional(e) => e,
|
|
5820
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
5821
|
+
ast::Arg::Pair { value, .. } => value,
|
|
5822
|
+
});
|
|
5823
|
+
compileTagLiteral(&call.name, payload, body, ctx, state)?;
|
|
5285
5824
|
} else {
|
|
5286
5825
|
fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
|
|
5287
5826
|
match arg {
|
|
@@ -5382,6 +5921,11 @@ fn compileExpr(
|
|
|
5382
5921
|
let const_value = CURRENT_CONSTS.with(|c| c.borrow().get(n).cloned());
|
|
5383
5922
|
match const_value {
|
|
5384
5923
|
Some(value) => compileExpr(&value, body, ctx, state)?,
|
|
5924
|
+
None if n.chars().next().is_some_and(|c| c.is_uppercase())
|
|
5925
|
+
&& matches!(inferLocalType(expr, ctx), PlumType::TTags(_)) =>
|
|
5926
|
+
{
|
|
5927
|
+
compileTagLiteral(n, None, body, ctx, state)?
|
|
5928
|
+
}
|
|
5385
5929
|
None => {
|
|
5386
5930
|
return Err(format!(
|
|
5387
5931
|
"codegen: type name '{}' is not yet supported as a value",
|
|
@@ -6200,6 +6744,8 @@ fn compileClosureBody(
|
|
|
6200
6744
|
type_env: RefCell::new(base_env),
|
|
6201
6745
|
closure_local_sigs: RefCell::new(HashMap::new()),
|
|
6202
6746
|
loop_extra_depth: Cell::new(0),
|
|
6747
|
+
ret_type: None,
|
|
6748
|
+
match_subject_types: RefCell::new(HashMap::new()),
|
|
6203
6749
|
};
|
|
6204
6750
|
|
|
6205
6751
|
compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, info.ret_vt)?;
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -1032,3 +1032,208 @@ fn wasmtimeGcConfigSupportsWideningConcreteStructRefToAnyrefAndCastingBack() {
|
|
|
1032
1032
|
"concrete struct ref should widen to anyref implicitly and cast back correctly"
|
|
1033
1033
|
);
|
|
1034
1034
|
}
|
|
1035
|
+
|
|
1036
|
+
// ---------------------------------------------------------------------------
|
|
1037
|
+
// Roc-style structural error tags (`Try[T, E]` with `E = [A, B(Str)]`).
|
|
1038
|
+
// ---------------------------------------------------------------------------
|
|
1039
|
+
|
|
1040
|
+
const TRY_PRELUDE: &str = "\
|
|
1041
|
+
enum Bool =
|
|
1042
|
+
| True
|
|
1043
|
+
| False
|
|
1044
|
+
|
|
1045
|
+
enum Try[T, E] =
|
|
1046
|
+
| Ok(T)
|
|
1047
|
+
| Err(E)
|
|
1048
|
+
|
|
1049
|
+
";
|
|
1050
|
+
|
|
1051
|
+
/// Compiles `TRY_PRELUDE + body`, validates it, runs `main() -> Int` and
|
|
1052
|
+
/// returns its result.
|
|
1053
|
+
fn runTryMain(body: &str) -> i64 {
|
|
1054
|
+
let src = format!("{}{}", TRY_PRELUDE, body);
|
|
1055
|
+
let bytes = assertValid(&src);
|
|
1056
|
+
let engine = gcEngine();
|
|
1057
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
1058
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
1059
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
|
|
1060
|
+
let main = instance
|
|
1061
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
1062
|
+
.unwrap();
|
|
1063
|
+
main.call(&mut store, ()).unwrap()
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
#[test]
|
|
1067
|
+
fn int_payload_roundtrip() {
|
|
1068
|
+
let got = runTryMain(
|
|
1069
|
+
"\
|
|
1070
|
+
fun make() -> Try[Int, [Big(Int), NotFound]] =
|
|
1071
|
+
Err(Big(9007199254740993))
|
|
1072
|
+
|
|
1073
|
+
fun main() -> Int =
|
|
1074
|
+
match make()
|
|
1075
|
+
Ok(v) => v
|
|
1076
|
+
Err(NotFound) => 0
|
|
1077
|
+
Err(Big(n)) => n
|
|
1078
|
+
",
|
|
1079
|
+
);
|
|
1080
|
+
assert_eq!(got, 9007199254740993);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
#[test]
|
|
1084
|
+
fn float_payload_roundtrip() {
|
|
1085
|
+
let got = runTryMain(
|
|
1086
|
+
"\
|
|
1087
|
+
fun make() -> Try[Int, [Frac(Float), NotFound]] =
|
|
1088
|
+
Err(Frac(0.1))
|
|
1089
|
+
|
|
1090
|
+
fun main() -> Int =
|
|
1091
|
+
match make()
|
|
1092
|
+
Ok(v) => v
|
|
1093
|
+
Err(NotFound) => 0
|
|
1094
|
+
Err(Frac(f)) =>
|
|
1095
|
+
if f == 0.1
|
|
1096
|
+
return 1
|
|
1097
|
+
return 2
|
|
1098
|
+
",
|
|
1099
|
+
);
|
|
1100
|
+
assert_eq!(got, 1);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
#[test]
|
|
1104
|
+
fn nested_err_tag_pattern() {
|
|
1105
|
+
let prog = |ctor: &str| {
|
|
1106
|
+
format!(
|
|
1107
|
+
"\
|
|
1108
|
+
fun make() -> Try[Int, [Bad(Int), NotFound]] =
|
|
1109
|
+
{}
|
|
1110
|
+
|
|
1111
|
+
fun main() -> Int =
|
|
1112
|
+
match make()
|
|
1113
|
+
Ok(v) => v
|
|
1114
|
+
Err(NotFound) => 0
|
|
1115
|
+
Err(Bad(n)) => n
|
|
1116
|
+
",
|
|
1117
|
+
ctor
|
|
1118
|
+
)
|
|
1119
|
+
};
|
|
1120
|
+
assert_eq!(runTryMain(&prog("Ok(7)")), 7);
|
|
1121
|
+
assert_eq!(runTryMain(&prog("Err(NotFound)")), 0);
|
|
1122
|
+
assert_eq!(runTryMain(&prog("Err(Bad(42))")), 42);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
#[test]
|
|
1126
|
+
fn question_mark_different_ok_types() {
|
|
1127
|
+
// Inner Try[Int,[A]] inside fn returning Try[Str,[A,B]]: failure rewraps
|
|
1128
|
+
// into the OUTER Err variant (different Ok type), success continues.
|
|
1129
|
+
let prog = |inner: &str| {
|
|
1130
|
+
format!(
|
|
1131
|
+
"\
|
|
1132
|
+
fun inner() -> Try[Int, [A]] =
|
|
1133
|
+
{}
|
|
1134
|
+
|
|
1135
|
+
fun outer() -> Try[Str, [A, B]] =
|
|
1136
|
+
x := inner()?
|
|
1137
|
+
Ok(\"fine\")
|
|
1138
|
+
|
|
1139
|
+
fun main() -> Int =
|
|
1140
|
+
match outer()
|
|
1141
|
+
Ok(_) => 1
|
|
1142
|
+
Err(A) => 2
|
|
1143
|
+
Err(B) => 3
|
|
1144
|
+
",
|
|
1145
|
+
inner
|
|
1146
|
+
)
|
|
1147
|
+
};
|
|
1148
|
+
assert_eq!(runTryMain(&prog("Err(A)")), 2);
|
|
1149
|
+
assert_eq!(runTryMain(&prog("Ok(5)")), 1);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#[test]
|
|
1153
|
+
fn tag_eq() {
|
|
1154
|
+
let got = runTryMain(
|
|
1155
|
+
"\
|
|
1156
|
+
fun main() -> Int =
|
|
1157
|
+
a := A == A
|
|
1158
|
+
b := A == B
|
|
1159
|
+
if a
|
|
1160
|
+
if b
|
|
1161
|
+
return 0
|
|
1162
|
+
return 1
|
|
1163
|
+
return 2
|
|
1164
|
+
",
|
|
1165
|
+
);
|
|
1166
|
+
assert_eq!(got, 1);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
#[test]
|
|
1170
|
+
fn match_on_bare_tag_union() {
|
|
1171
|
+
let got = runTryMain(
|
|
1172
|
+
"\
|
|
1173
|
+
fun pick(n: Int) -> [Small, Big(Int)] =
|
|
1174
|
+
if n < 10
|
|
1175
|
+
return Small
|
|
1176
|
+
return Big(n)
|
|
1177
|
+
|
|
1178
|
+
fun main() -> Int =
|
|
1179
|
+
match pick(77)
|
|
1180
|
+
Small => 0
|
|
1181
|
+
Big(n) => n
|
|
1182
|
+
",
|
|
1183
|
+
);
|
|
1184
|
+
assert_eq!(got, 77);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
#[test]
|
|
1188
|
+
fn crash_traps_and_calls_import() {
|
|
1189
|
+
let src = format!(
|
|
1190
|
+
"{}fun main() -> Int =\n crash \"boom\"\n",
|
|
1191
|
+
TRY_PRELUDE
|
|
1192
|
+
);
|
|
1193
|
+
let bytes = assertValid(&src);
|
|
1194
|
+
let engine = gcEngine();
|
|
1195
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
1196
|
+
assert!(
|
|
1197
|
+
module.imports().any(|i| i.module() == "plum" && i.name() == "crash"),
|
|
1198
|
+
"crash import must be emitted when crash is used"
|
|
1199
|
+
);
|
|
1200
|
+
let plain = assertValid("fun main() -> Int =\n 1\n");
|
|
1201
|
+
let plain_module = wasmtime::Module::new(&engine, &plain).unwrap();
|
|
1202
|
+
assert!(
|
|
1203
|
+
plain_module.imports().all(|i| i.name() != "crash"),
|
|
1204
|
+
"crash import must NOT be emitted when unused"
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
fn countTypes(bytes: &[u8]) -> usize {
|
|
1209
|
+
let mut n = 0;
|
|
1210
|
+
for payload in wasmparser::Parser::new(0).parse_all(bytes) {
|
|
1211
|
+
if let wasmparser::Payload::TypeSection(s) = payload.unwrap() {
|
|
1212
|
+
for g in s {
|
|
1213
|
+
n += g.unwrap().types().count();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
n
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
#[test]
|
|
1221
|
+
fn specializations_share_gc_type() {
|
|
1222
|
+
let prog = |second: &str| {
|
|
1223
|
+
format!(
|
|
1224
|
+
"{}\
|
|
1225
|
+
fun f() -> Try[Int, [A]] =
|
|
1226
|
+
Err(A)
|
|
1227
|
+
|
|
1228
|
+
fun g() -> Try[Int, {}] =
|
|
1229
|
+
Err(A)
|
|
1230
|
+
|
|
1231
|
+
fun main() -> Int = 1
|
|
1232
|
+
",
|
|
1233
|
+
TRY_PRELUDE, second
|
|
1234
|
+
)
|
|
1235
|
+
};
|
|
1236
|
+
let same = assertValid(&prog("[A]"));
|
|
1237
|
+
let wider = assertValid(&prog("[A, B]"));
|
|
1238
|
+
assert_eq!(countTypes(&same), countTypes(&wider));
|
|
1239
|
+
}
|