plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


b3bb7b5 — Peter John 2026-10-26T10:24:18+05:30
feat!: Bool is a built-in i32 primitive
plum-checker/src/lib.rs CHANGED
@@ -483,17 +483,6 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
483
483
  {
484
484
  Ok(())
485
485
  }
486
- // Same reasoning as `Str` above — `Bool` is an ordinary enum now
487
- // (`enum Bool = | True | False` in `libs/std/Bool.plum`), so a bare
488
- // `True`/`False`/`Ok(...)`-wrapped-variant reference can infer as
489
- // `TNamed("Bool")` (via `EnumVariantInfo`/`bareVariantWrapType`)
490
- // while a `Bool` type ANNOTATION resolves via `plumTypeFromName` to
491
- // the dedicated `TBool` variant instead.
492
- (PlumType::TBool, PlumType::TNamed(n)) | (PlumType::TNamed(n), PlumType::TBool)
493
- if n == "Bool" =>
494
- {
495
- Ok(())
496
- }
497
486
  (PlumType::TByte, PlumType::TByte) => Ok(()),
498
487
  (PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
499
488
  (PlumType::TUnit, PlumType::TUnit) => Ok(()),
@@ -2184,8 +2173,11 @@ fn checkPattern(
2184
2173
  ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt, ctx),
2185
2174
  ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat, ctx),
2186
2175
  ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr, ctx),
2176
+ ast::CasePattern::Name(n) if n == "True" || n == "False" => {
2177
+ unify(subject_ty, &PlumType::TBool, ctx)
2178
+ }
2187
2179
  ast::CasePattern::Name(n) => {
2188
- let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
2180
+ let is_known_variant =n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
2189
2181
  && ctx.enum_variants.contains_key(n);
2190
2182
  if is_known_variant {
2191
2183
  Ok(()) // equality check against a known enum tag, e.g. `True`
@@ -2425,7 +2417,7 @@ fn isTagLiteralName(n: &str, ctx: &CheckCtx) -> bool {
2425
2417
  n.chars().next().is_some_and(|c| c.is_uppercase())
2426
2418
  && !matches!(
2427
2419
  n,
2428
- "Int" | "Float" | "Bool" | "Str" | "Byte" | "Unit" | "Self"
2420
+ "Int" | "Float" | "Bool" | "Str" | "Byte" | "Unit" | "Self" | "True" | "False"
2429
2421
  )
2430
2422
  && !ctx.enum_variants.contains_key(n)
2431
2423
  && !ctx.enum_variants.values().any(|i| i.enum_name == n)
@@ -2738,12 +2730,11 @@ fn inferExprInner(
2738
2730
  // const (`PI`, `MAX_VALUE`, ...), since there's no separate "constant"
2739
2731
  // token. Prefer a matching const's real type over the enum-variant/
2740
2732
  // unmodeled-type-name fallbacks below.
2733
+ // `True`/`False` are built-in `Bool` literals, always available.
2734
+ ast::Expr::TypeName(n) if n == "True" || n == "False" => Ok(PlumType::TBool),
2741
2735
  ast::Expr::TypeName(n) => match lookup(env, n) {
2742
2736
  Ok(ty) if !matches!(ty, PlumType::TVar(_)) => Ok(ty),
2743
2737
  _ => match ctx.enum_variants.get(n) {
2744
- // `Bool` normalizes to the dedicated `TBool` variant (not
2745
- // `TNamed("Bool")`) — see the note on `unify`'s Bool arm.
2746
- Some(info) if info.enum_name == "Bool" => Ok(PlumType::TBool),
2747
2738
  Some(info) => {
2748
2739
  let (_, result, binds) = startVariant(info, &[], expected);
2749
2740
  Ok(substVars(&result, &binds))
plum-checker/tests/checker_tests.rs CHANGED
@@ -2239,3 +2239,38 @@ enum Box[T] =
2239
2239
  let result = checkSource(&parse(src));
2240
2240
  assert!(result.is_ok(), "got {:?}", result.err());
2241
2241
  }
2242
+
2243
+ // ---------- built-in Bool (no `import std/Bool`, no `enum Bool`) ----------
2244
+
2245
+ #[test]
2246
+ fn builtin_bool_needs_no_import() {
2247
+ let src = "\
2248
+ fun both(a: Bool, b: Bool) -> Bool =
2249
+ a && !b || False
2250
+
2251
+ fun main() -> Bool =
2252
+ x := True
2253
+ x == both(True, False)
2254
+ ";
2255
+ let r = checkSource(&parseClean(src));
2256
+ assert!(r.is_ok(), "got {:?}", r.err());
2257
+ }
2258
+
2259
+ #[test]
2260
+ fn builtin_bool_match_missing_false_is_rejected() {
2261
+ let r = errText(checkSource(&parseClean(
2262
+ "fun name(b: Bool) -> Str =\n match b\n True => \"y\"\n",
2263
+ )));
2264
+ assert!(r.contains("non-exhaustive match on Bool: missing False"), "got: {}", r);
2265
+ let ok = checkSource(&parseClean(
2266
+ "fun name(b: Bool) -> Str =\n match b\n True => \"y\"\n False => \"n\"\n",
2267
+ ));
2268
+ assert!(ok.is_ok(), "got {:?}", ok.err());
2269
+ }
2270
+
2271
+ #[test]
2272
+ fn true_false_are_never_tags() {
2273
+ let src = "fun f() -> Int =\n x := True\n y := False\n x == y ? 1 : 0\n";
2274
+ let r = checkSource(&parseClean(src));
2275
+ assert!(r.is_ok(), "got {:?}", r.err());
2276
+ }
plum-std/Bool.plum CHANGED
@@ -2,9 +2,12 @@ module std
2
2
  import std/Try
3
3
  import std/Str
4
4
 
5
+ # `Bool` is a built-in primitive (a wasm i32, 0/1); `True`/`False` are always
6
+ # available literals and need no import. This file only provides the METHODS
7
+ # whose receiver is a `Bool` (the single-variant enum is a pure marker, exactly
8
+ # like `Byte.plum`/`Number.plum`).
5
9
  enum Bool =
6
- | True
10
+ | Bool
7
- | False
8
11
 
9
12
  fun parse(v: Str) -> Try[Bool, [BadBoolStr]] =
10
13
  if v == "true"
@@ -15,20 +18,10 @@ enum Bool =
15
18
  Err(BadBoolStr)
16
19
 
17
20
  fun and(self, o: Bool) -> Bool =
18
- match self, o
19
- True, True => True
20
- True, False => False
21
- False, True => False
22
- False, False => False
21
+ self && o
23
22
 
24
23
  fun or(self, o: Bool) -> Bool =
25
- match self, o
24
+ self || o
26
- True, True => True
27
- True, False => True
28
- False, True => True
29
- False, False => False
30
25
 
31
26
  fun toStr(self) -> Str =
32
- match self
33
- True => "True"
34
- False => "False"
27
+ self ? "True" : "False"
plum-wasm-codegen/src/lib.rs CHANGED
@@ -643,6 +643,10 @@ fn collectTagNamesTy(ty: &PlumType, out: &mut std::collections::BTreeSet<String>
643
643
  /// as an `anyref`: `Int`/`Float`/`Byte` get a `$BoxI64`/`$BoxF64`/`$BoxI32`,
644
644
  /// reference values are stored as-is.
645
645
  fn boxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
646
+ if matches!(ty, PlumType::TBool) {
647
+ Instruction::Call(ctx.gc_types.box_bool_fn).encode(body);
648
+ return;
649
+ }
646
650
  match plumTypeToValtype(ty) {
647
651
  ValType::I64 => Instruction::Call(ctx.gc_types.box_i64_fn).encode(body),
648
652
  ValType::F64 => Instruction::Call(ctx.gc_types.box_f64_fn).encode(body),
@@ -651,6 +655,33 @@ fn boxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
651
655
  }
652
656
  }
653
657
 
658
+ /// Stand-in plum type for "any erased reference" in `coerceTy` calls.
659
+ fn anyTy() -> PlumType {
660
+ // A function type lowers to `anyref`, the closure ABI's erased slot type.
661
+ PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
662
+ }
663
+
664
+ /// `coerceVt` with static types: a `Bool` and a `Byte` are both wasm `i32`, so
665
+ /// boxing/unboxing across an erased edge must look at the plum type to pick
666
+ /// `$BoxBool` over `$BoxI32`.
667
+ fn coerceTy(from: &PlumType, to: &PlumType, body: &mut Vec<u8>, g: &GcTypeRegistry) {
668
+ let (fv, tv) = (plumTypeToGcValtype(from, g), plumTypeToGcValtype(to, g));
669
+ match (from, to) {
670
+ (PlumType::TBool, _) if matches!(tv, ValType::Ref(_)) => {
671
+ Instruction::Call(g.box_bool_fn).encode(body)
672
+ }
673
+ (_, PlumType::TBool) if matches!(fv, ValType::Ref(_)) => {
674
+ Instruction::RefCastNonNull(HeapType::Concrete(g.box_bool_idx)).encode(body);
675
+ Instruction::StructGet {
676
+ struct_type_index: g.box_bool_idx,
677
+ field_index: fld(0),
678
+ }
679
+ .encode(body);
680
+ }
681
+ _ => coerceVt(fv, tv, body, g),
682
+ }
683
+ }
684
+
654
685
  /// Converts the value on top of the stack from representation `from` to `to`
655
686
  /// (the erased-generics edge conversions): a primitive entering a reference slot
656
687
  /// (`ref null $Obj` / `anyref`) is boxed; a reference leaving such a slot for a
@@ -707,8 +738,12 @@ fn compileExprAs(
707
738
  ) -> Result<(), String> {
708
739
  compileExpr(expr, body, ctx, state)?;
709
740
  if let Some(vt) = expected {
710
- let own = plumTypeToValtype(&inferLocalType(expr, ctx));
741
+ let own_ty = inferLocalType(expr, ctx);
742
+ if matches!(own_ty, PlumType::TBool) && matches!(vt, ValType::Ref(_)) {
711
- coerceVt(own, vt, body, ctx.gc_types);
743
+ coerceTy(&own_ty, &anyTy(), body, ctx.gc_types);
744
+ } else {
745
+ coerceVt(plumTypeToValtype(&own_ty), vt, body, ctx.gc_types);
746
+ }
712
747
  }
713
748
  Ok(())
714
749
  }
@@ -762,6 +797,10 @@ fn unboxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
762
797
  }
763
798
  .encode(body);
764
799
  };
800
+ if matches!(ty, PlumType::TBool) {
801
+ unbox(ctx.gc_types.box_bool_idx, body);
802
+ return;
803
+ }
765
804
  match plumTypeToValtype(ty) {
766
805
  ValType::I64 => unbox(ctx.gc_types.box_i64_idx, body),
767
806
  ValType::F64 => unbox(ctx.gc_types.box_f64_idx, body),
@@ -892,13 +931,7 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
892
931
  PlumType::TApp(n, _) => plumTypeToValtype(&PlumType::TNamed(n.clone())),
893
932
  PlumType::TInt => ValType::I64,
894
933
  PlumType::TFloat => ValType::F64,
895
- PlumType::TBool => withGcTypes(|r| {
934
+ PlumType::TBool => ValType::I32,
896
- gcRef(
897
- *r.enum_super_type_idx
898
- .get("Bool")
899
- .expect("Bool must be registered"),
900
- )
901
- }),
902
935
  // `Str` is an ORDINARY class (`type Str(...) = data: Buffer ...` in
903
936
  // `str.plum`) — its own real struct type, registered exactly like any
904
937
  // other class (see `buildGcTypeRegistry`'s `Slot::Class` handling),
@@ -986,6 +1019,8 @@ pub struct GcTypeRegistry {
986
1019
  pub box_i64_idx: u32,
987
1020
  pub box_f64_idx: u32,
988
1021
  pub box_i32_idx: u32,
1022
+ /// `$BoxBool {typeId, i32}` — a `Bool` crossing an erased edge (`toStr` must say True/False).
1023
+ pub box_bool_idx: u32,
989
1024
  /// The `$Obj {typeId: i32}` supertype of every class/enum/tag/box struct.
990
1025
  pub obj_type_idx: u32,
991
1026
  /// Enum name and variant name -> the runtime `typeId` stored in field 0 of
@@ -996,6 +1031,7 @@ pub struct GcTypeRegistry {
996
1031
  pub box_i64_fn: u32,
997
1032
  pub box_f64_fn: u32,
998
1033
  pub box_i32_fn: u32,
1034
+ pub box_bool_fn: u32,
999
1035
  /// Helper `(arr: array<i8>, len: i64) -> Str` building `Str{Buffer{arr,len}}`
1000
1036
  /// with correct headers; `None` when the program has no `Str`/`Buffer`.
1001
1037
  pub mk_str_fn: Option<u32>,
@@ -1008,6 +1044,8 @@ pub const TYPE_ID_BOX_I64: i32 = 1;
1008
1044
  pub const TYPE_ID_BOX_F64: i32 = 2;
1009
1045
  pub const TYPE_ID_BOX_I32: i32 = 3;
1010
1046
  pub const TYPE_ID_TAG: i32 = 4;
1047
+ /// Box for a `Bool` (i32 payload); ids 5/6 are `Str`/`Buffer`.
1048
+ pub const TYPE_ID_BOX_BOOL: i32 = 7;
1011
1049
  /// `Str` and `Buffer` get fixed ids so hosts can build them without a lookup.
1012
1050
  pub const TYPE_ID_STR: i32 = 5;
1013
1051
  pub const TYPE_ID_BUFFER: i32 = 6;
@@ -1047,16 +1085,7 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
1047
1085
  PlumType::TApp(n, _) => plumTypeToGcValtype(&PlumType::TNamed(n.clone()), registry),
1048
1086
  PlumType::TInt => ValType::I64,
1049
1087
  PlumType::TFloat => ValType::F64,
1050
- PlumType::TBool => {
1088
+ PlumType::TBool => ValType::I32,
1051
- let idx = *registry
1052
- .enum_super_type_idx
1053
- .get("Bool")
1054
- .expect("internal codegen error: Bool must be registered in the GC type registry");
1055
- ValType::Ref(RefType {
1056
- nullable: true,
1057
- heap_type: HeapType::Concrete(idx),
1058
- })
1059
- }
1060
1089
  // See the matching comment in `plumTypeToValtype` — `Str` is an
1061
1090
  // ordinary class's own real struct type, not the raw
1062
1091
  // `byte_array_type_idx`, except when `Str` was never actually
@@ -1126,6 +1155,7 @@ fn buildGcTypeRegistry(
1126
1155
  BoxI64,
1127
1156
  BoxF64,
1128
1157
  BoxI32,
1158
+ BoxBool,
1129
1159
  }
1130
1160
 
1131
1161
  // Pass 1: assign every entry a slot (and therefore a type index) up front, before
@@ -1216,6 +1246,8 @@ fn buildGcTypeRegistry(
1216
1246
  slots.push(Slot::BoxF64);
1217
1247
  let box_i32_idx = slots.len() as u32;
1218
1248
  slots.push(Slot::BoxI32);
1249
+ let box_bool_idx = slots.len() as u32;
1250
+ slots.push(Slot::BoxBool);
1219
1251
 
1220
1252
  // The registry is fully index-complete after pass 1 (every name has an assigned
1221
1253
  // slot) even though no field lists exist yet — safe to hand to `plumTypeToGcValtype`
@@ -1232,11 +1264,13 @@ fn buildGcTypeRegistry(
1232
1264
  box_i64_idx,
1233
1265
  box_f64_idx,
1234
1266
  box_i32_idx,
1267
+ box_bool_idx,
1235
1268
  obj_type_idx,
1236
1269
  type_ids,
1237
1270
  box_i64_fn: 0,
1238
1271
  box_f64_fn: 0,
1239
1272
  box_i32_fn: 0,
1273
+ box_bool_fn: 0,
1240
1274
  mk_str_fn: None,
1241
1275
  dyn_fns: HashMap::new(),
1242
1276
  };
@@ -1354,6 +1388,7 @@ fn buildGcTypeRegistry(
1354
1388
  Slot::BoxI64 => boxSubType(ValType::I64, obj_type_idx),
1355
1389
  Slot::BoxF64 => boxSubType(ValType::F64, obj_type_idx),
1356
1390
  Slot::BoxI32 => boxSubType(ValType::I32, obj_type_idx),
1391
+ Slot::BoxBool => boxSubType(ValType::I32, obj_type_idx),
1357
1392
  Slot::ArrayRef => SubType {
1358
1393
  is_final: true,
1359
1394
  supertype_idx: None,
@@ -1908,6 +1943,8 @@ fn compileSourceInner(
1908
1943
  registerBoxHelper(&mut module, ValType::F64, TYPE_ID_BOX_F64, gc_types.box_f64_idx);
1909
1944
  gc_types.box_i32_fn =
1910
1945
  registerBoxHelper(&mut module, ValType::I32, TYPE_ID_BOX_I32, gc_types.box_i32_idx);
1946
+ gc_types.box_bool_fn =
1947
+ registerBoxHelper(&mut module, ValType::I32, TYPE_ID_BOX_BOOL, gc_types.box_bool_idx);
1911
1948
  if let (Some(&buffer_idx), Some(&str_idx)) = (
1912
1949
  gc_types.class_type_idx.get("Buffer"),
1913
1950
  gc_types.class_type_idx.get("Str"),
@@ -2135,10 +2172,18 @@ fn compileSourceInner(
2135
2172
  tbody.push(0u8); // no locals beyond the params already in the signature
2136
2173
  for (i, vt) in sig.params.iter().enumerate() {
2137
2174
  Instruction::LocalGet((i + 1) as u32).encode(&mut tbody); // local 0 is env, ignored
2175
+ match sig.decl_params.get(i) {
2176
+ Some(PlumType::TBool) => {
2177
+ coerceTy(&anyTy(), &PlumType::TBool, &mut tbody, &gc_types)
2178
+ }
2138
- coerceVt(ValType::Ref(RefType::ANYREF), *vt, &mut tbody, &gc_types);
2179
+ _ => coerceVt(ValType::Ref(RefType::ANYREF), *vt, &mut tbody, &gc_types),
2180
+ }
2139
2181
  }
2140
2182
  Instruction::Call(real_func_idx).encode(&mut tbody);
2141
2183
  match sig.ret {
2184
+ Some(_) if matches!(sig.decl_ret, PlumType::TBool) => {
2185
+ coerceTy(&PlumType::TBool, &anyTy(), &mut tbody, &gc_types)
2186
+ }
2142
2187
  Some(rvt) => coerceVt(rvt, ValType::Ref(RefType::ANYREF), &mut tbody, &gc_types),
2143
2188
  None => Instruction::RefNull(HeapType::ANY).encode(&mut tbody),
2144
2189
  }
@@ -2358,6 +2403,7 @@ fn registerDynDispatchers(
2358
2403
  ("Int", ValType::I64, gc_types.box_i64_idx, TYPE_ID_BOX_I64),
2359
2404
  ("Float", ValType::F64, gc_types.box_f64_idx, TYPE_ID_BOX_F64),
2360
2405
  ("Byte", ValType::I32, gc_types.box_i32_idx, TYPE_ID_BOX_I32),
2406
+ ("Bool", ValType::I32, gc_types.box_bool_idx, TYPE_ID_BOX_BOOL),
2361
2407
  ] {
2362
2408
  let direct = format!("{}::{}", prim, m.name);
2363
2409
  let mut b = Vec::new();
@@ -2481,6 +2527,7 @@ fn registerDynEqHelper(module: &mut WasmModule, g: &GcTypeRegistry, string_eq_fu
2481
2527
  (TYPE_ID_BOX_I64, g.box_i64_idx, Instruction::I64Eq),
2482
2528
  (TYPE_ID_BOX_F64, g.box_f64_idx, Instruction::F64Eq),
2483
2529
  (TYPE_ID_BOX_I32, g.box_i32_idx, Instruction::I32Eq),
2530
+ (TYPE_ID_BOX_BOOL, g.box_bool_idx, Instruction::I32Eq),
2484
2531
  ] {
2485
2532
  tid(&mut b, 0);
2486
2533
  Instruction::I32Const(id).encode(&mut b);
@@ -3461,7 +3508,7 @@ fn fvCollectBoundBlock(block: &ast::Block, bound: &mut std::collections::HashSet
3461
3508
 
3462
3509
  fn fvCollectPatternBindings(pat: &ast::CasePattern, bound: &mut std::collections::HashSet<String>) {
3463
3510
  match pat {
3464
- ast::CasePattern::Name(n) => {
3511
+ ast::CasePattern::Name(n) if n != "True" && n != "False" => {
3465
3512
  bound.insert(n.clone());
3466
3513
  }
3467
3514
  ast::CasePattern::Class { fields, .. } => {
@@ -3711,7 +3758,7 @@ impl<'a> Collector<'a> {
3711
3758
  match pat {
3712
3759
  ast::CasePattern::Name(n) => {
3713
3760
  let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
3714
- && self.enum_variants.contains_key(n);
3761
+ && (self.enum_variants.contains_key(n) || n == "True" || n == "False");
3715
3762
  if !is_variant {
3716
3763
  self.bind(n, ty.clone());
3717
3764
  }
@@ -4169,11 +4216,8 @@ fn compileStmtInValuePosition(
4169
4216
  /// (each branch is `BlockType::Empty`, nothing left on the stack) or `Some(vt)` when this
4170
4217
  /// `if` is in value position — every branch must then leave a `vt` value on the stack, which
4171
4218
  /// requires an `else` (a value can't be produced on a path that doesn't exist).
4172
- /// Compiles a `Bool`-typed expression, then a `ref.test` against the `True` variant's
4219
+ /// Compiles a `Bool`-typed expression (a plain wasm `i32`, 1/0) for use as a native
4173
- /// concrete type, leaving a plain `i32` (1/0) on the stack. Every place a `Bool` value
4174
- /// drives wasm's OWN native control flow (`if`/`br_if`, which require a raw `i32`
4175
- /// condition, not a `ref`) goes through this — see this migration plan's Decision 1
4176
- /// (Bool is a full wasm-gc struct, no special-casing).
4220
+ /// control-flow condition; `&&`/`||` short-circuit.
4177
4221
  fn compileBoolConditionAsI32(
4178
4222
  expr: &ast::Expr,
4179
4223
  body: &mut Vec<u8>,
@@ -4184,20 +4228,11 @@ fn compileBoolConditionAsI32(
4184
4228
  // alone executed) unless the left side's result already needs it — guarding
4185
4229
  // `i < len && s.byteAt(i) != ...` on the length check only works if `byteAt`
4186
4230
  // is never reached once `i < len` is false. Handled here (not just as a
4187
- // generic `Expr::Bool` case in `compileExpr` below) so a boolean used
4231
+ // generic `Expr::Bool` case in `compileExpr` below).
4188
- // directly as an `if`/`while` condition never pays for constructing a real
4189
- // `Bool` ref just to immediately `ref.test` it back into an `i32`.
4190
4232
  if let ast::Expr::Bool(b) = expr {
4191
4233
  return compileShortCircuitBoolI32(b, body, ctx, state);
4192
4234
  }
4193
- compileExpr(expr, body, ctx, state)?;
4235
+ compileExpr(expr, body, ctx, state)
4194
- let true_idx = *ctx
4195
- .gc_types
4196
- .variant_type_idx
4197
- .get("True")
4198
- .expect("internal codegen error: True must be registered in the GC type registry");
4199
- Instruction::RefTestNonNull(HeapType::Concrete(true_idx)).encode(body);
4200
- Ok(())
4201
4236
  }
4202
4237
 
4203
4238
  /// Leaves a short-circuited `i32` (1/0) on the stack for `b.left op b.right`:
@@ -4227,28 +4262,6 @@ fn compileShortCircuitBoolI32(
4227
4262
  Ok(())
4228
4263
  }
4229
4264
 
4230
- /// Given an `i32` boolean (1/0) already on the stack, converts it into a `Bool` ref by
4231
- /// selecting the pre-allocated `True`/`False` singleton (this migration plan's
4232
- /// Decision 2) — the reverse of `compileBoolConditionAsI32`. Used wherever a native
4233
- /// wasm comparison/logical-op instruction just left a raw `i32` predicate on the
4234
- /// stack that needs to become a proper `Bool` value.
4235
- fn pushBoolRefFromI32Flag(body: &mut Vec<u8>, ctx: &LocalCtx) {
4236
- let bool_ref_ty = plumTypeToValtype(&PlumType::TBool);
4237
- let true_global = *ctx
4238
- .singleton_globals
4239
- .get("True")
4240
- .expect("internal codegen error: True singleton global missing");
4241
- let false_global = *ctx
4242
- .singleton_globals
4243
- .get("False")
4244
- .expect("internal codegen error: False singleton global missing");
4245
- Instruction::If(BlockType::Result(bool_ref_ty)).encode(body);
4246
- Instruction::GlobalGet(true_global).encode(body);
4247
- Instruction::Else.encode(body);
4248
- Instruction::GlobalGet(false_global).encode(body);
4249
- Instruction::End.encode(body);
4250
- }
4251
-
4252
4265
  fn compileIf(
4253
4266
  if_: &ast::If,
4254
4267
  result_vt: Option<ValType>,
@@ -4322,12 +4335,7 @@ fn compileStmt(
4322
4335
  // Convert to the representation the local was declared with
4323
4336
  // (differs only across an erased/instantiated edge).
4324
4337
  if let Some(decl) = ctx.local_decl_types.get(name) {
4325
- coerceVt(
4326
- plumTypeToValtype(&vty),
4338
+ coerceTy(&vty, decl, body, ctx.gc_types);
4327
- plumTypeToValtype(decl),
4328
- body,
4329
- ctx.gc_types,
4330
- );
4331
4339
  }
4332
4340
  Instruction::LocalSet(idx).encode(body);
4333
4341
  ctx.type_env
@@ -4427,7 +4435,11 @@ fn compileStmt(
4427
4435
  // Closure ABI: the result leaves as an `anyref`.
4428
4436
  let rt_vt = ctx.ret_type.as_ref().map(plumTypeToValtype);
4429
4437
  if let Some(vt) = rt_vt {
4438
+ if let Some(rt @ PlumType::TBool) = ctx.ret_type.as_ref() {
4439
+ coerceTy(rt, &anyTy(), body, ctx.gc_types);
4440
+ } else {
4430
- coerceVt(vt, ValType::Ref(RefType::ANYREF), body, ctx.gc_types);
4441
+ coerceVt(vt, ValType::Ref(RefType::ANYREF), body, ctx.gc_types);
4442
+ }
4431
4443
  }
4432
4444
  }
4433
4445
  Instruction::Return.encode(body);
@@ -4759,6 +4771,48 @@ fn matchCoversEveryEnumVariant(
4759
4771
  !tuples_seen.is_empty() && tuples_seen.len() == total_combinations
4760
4772
  }
4761
4773
 
4774
+ /// True if unguarded `True`/`False` arms between them cover every combination of
4775
+ /// `n` `Bool` subjects (the checker has already proven exhaustiveness).
4776
+ fn matchCoversEveryBoolCombo(cases: &[ast::Case], n: usize) -> bool {
4777
+ let mut seen: std::collections::BTreeSet<Vec<bool>> = std::collections::BTreeSet::new();
4778
+ for case in cases {
4779
+ if case.guard.is_some() || case.patterns.len() != n {
4780
+ continue;
4781
+ }
4782
+ let tuple: Option<Vec<Vec<bool>>> = case
4783
+ .patterns
4784
+ .iter()
4785
+ .map(|p| match p {
4786
+ ast::CasePattern::Name(nm) if nm == "True" => Some(vec![true]),
4787
+ ast::CasePattern::Name(nm) if nm == "False" => Some(vec![false]),
4788
+ ast::CasePattern::Wildcard => Some(vec![true, false]),
4789
+ ast::CasePattern::Name(nm)
4790
+ if !nm.chars().next().is_some_and(|c| c.is_uppercase()) =>
4791
+ {
4792
+ Some(vec![true, false])
4793
+ }
4794
+ _ => None,
4795
+ })
4796
+ .collect();
4797
+ let Some(options) = tuple else { continue };
4798
+ let mut combos: Vec<Vec<bool>> = vec![Vec::new()];
4799
+ for opts in &options {
4800
+ combos = combos
4801
+ .into_iter()
4802
+ .flat_map(|c| {
4803
+ opts.iter().map(move |b| {
4804
+ let mut c = c.clone();
4805
+ c.push(*b);
4806
+ c
4807
+ })
4808
+ })
4809
+ .collect();
4810
+ }
4811
+ seen.extend(combos);
4812
+ }
4813
+ n < 8 && seen.len() == 1usize << n
4814
+ }
4815
+
4762
4816
  /// An uppercase `Name`/`Class` pattern — against a tag-union subject, a tag arm.
4763
4817
  fn isTagPattern(pat: &ast::CasePattern) -> bool {
4764
4818
  let upper = |n: &str| n.chars().next().is_some_and(|c| c.is_uppercase());
@@ -4885,8 +4939,14 @@ fn compileMatch(
4885
4939
  .match_subject_types
4886
4940
  .borrow()
4887
4941
  .contains_key(&all_subjects[0].1);
4942
+ let bool_match = !m.subjects.is_empty()
4943
+ && m.subjects
4944
+ .iter()
4945
+ .all(|e| matches!(inferLocalType(e, ctx), PlumType::TBool));
4888
4946
  let exhaustive_fallback = result_vt.is_some()
4947
+ && (tag_union_match
4948
+ || (bool_match && matchCoversEveryBoolCombo(&m.cases, m.subjects.len()))
4889
- && (tag_union_match || matchCoversEveryEnumVariant(&m.cases, &subject_vts, ctx));
4949
+ || matchCoversEveryEnumVariant(&m.cases, &subject_vts, ctx));
4890
4950
 
4891
4951
  compileMatchArmsMulti(
4892
4952
  &m.cases,
@@ -5049,6 +5109,37 @@ fn compileCasePositions(
5049
5109
  state,
5050
5110
  )
5051
5111
  }
5112
+ // `True`/`False` against a `Bool` (i32) subject: a plain i32 compare.
5113
+ ast::CasePattern::Name(n) if n == "True" || n == "False" => {
5114
+ Instruction::LocalGet(scratch_local).encode(body);
5115
+ if n == "False" {
5116
+ Instruction::I32Eqz.encode(body);
5117
+ }
5118
+ Instruction::If(blockTypeFor(result_vt)).encode(body);
5119
+ compileCasePositions(
5120
+ case,
5121
+ pos + 1,
5122
+ all_subjects,
5123
+ rest,
5124
+ result_vt,
5125
+ exhaustive_fallback,
5126
+ body,
5127
+ ctx,
5128
+ state,
5129
+ )?;
5130
+ Instruction::Else.encode(body);
5131
+ compileMatchArmsMulti(
5132
+ rest,
5133
+ all_subjects,
5134
+ result_vt,
5135
+ exhaustive_fallback,
5136
+ body,
5137
+ ctx,
5138
+ state,
5139
+ )?;
5140
+ Instruction::End.encode(body);
5141
+ Ok(())
5142
+ }
5052
5143
  ast::CasePattern::Name(n) => {
5053
5144
  let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
5054
5145
  && ctx.enum_variants.contains_key(n);
@@ -5371,7 +5462,7 @@ fn compileFieldPatterns(
5371
5462
  field_index: fld(fpos),
5372
5463
  }
5373
5464
  .encode(b);
5374
- coerceVt(decl_vt, inst_vt, b, ctx.gc_types);
5465
+ coerceTy(decl_ty, field_ty, b, ctx.gc_types);
5375
5466
  },
5376
5467
  result_vt,
5377
5468
  body,
@@ -5427,6 +5518,48 @@ fn compileFieldPatterns(
5427
5518
  state,
5428
5519
  on_match,
5429
5520
  ),
5521
+ // A nested `True`/`False` against a `Bool` field: i32 compare.
5522
+ ast::CasePattern::Name(n) if n == "True" || n == "False" => {
5523
+ Instruction::LocalGet(container_local).encode(body);
5524
+ Instruction::StructGet {
5525
+ struct_type_index: container_type_idx,
5526
+ field_index: fld(fpos),
5527
+ }
5528
+ .encode(body);
5529
+ coerceTy(decl_ty, &PlumType::TBool, body, ctx.gc_types);
5530
+ if n == "False" {
5531
+ Instruction::I32Eqz.encode(body);
5532
+ }
5533
+ Instruction::If(blockTypeFor(result_vt)).encode(body);
5534
+ compileFieldPatterns(
5535
+ fields,
5536
+ field_types,
5537
+ inst_types,
5538
+ fpos + 1,
5539
+ container_local,
5540
+ container_type_idx,
5541
+ rest,
5542
+ all_subjects,
5543
+ result_vt,
5544
+ exhaustive_fallback,
5545
+ body,
5546
+ ctx,
5547
+ state,
5548
+ on_match,
5549
+ )?;
5550
+ Instruction::Else.encode(body);
5551
+ compileMatchArmsMulti(
5552
+ rest,
5553
+ all_subjects,
5554
+ result_vt,
5555
+ exhaustive_fallback,
5556
+ body,
5557
+ ctx,
5558
+ state,
5559
+ )?;
5560
+ Instruction::End.encode(body);
5561
+ Ok(())
5562
+ }
5430
5563
  ast::CasePattern::Name(n)
5431
5564
  if !(n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
5432
5565
  && ctx.enum_variants.contains_key(n)) =>
@@ -5443,7 +5576,7 @@ fn compileFieldPatterns(
5443
5576
  field_index: fld(fpos),
5444
5577
  }
5445
5578
  .encode(body);
5446
- coerceVt(decl_vt, inst_vt, body, ctx.gc_types);
5579
+ coerceTy(decl_ty, field_ty, body, ctx.gc_types);
5447
5580
  Instruction::LocalSet(idx).encode(body);
5448
5581
  ctx.type_env
5449
5582
  .borrow_mut()
@@ -5527,7 +5660,7 @@ fn compileFieldPatterns(
5527
5660
  field_index: fld(fpos),
5528
5661
  }
5529
5662
  .encode(body);
5530
- coerceVt(decl_vt, inst_vt, body, ctx.gc_types);
5663
+ coerceTy(decl_ty, field_ty, body, ctx.gc_types);
5531
5664
  Instruction::I64Const(*n).encode(body);
5532
5665
  Instruction::I64Eq.encode(body);
5533
5666
  Instruction::If(blockTypeFor(result_vt)).encode(body);
@@ -5698,8 +5831,7 @@ fn compileExpr(
5698
5831
  // type may differ across an erased edge (a generic `T` slot read
5699
5832
  // at its instantiated type).
5700
5833
  if let Some(decl) = ctx.local_decl_types.get(name.as_str()) {
5701
- let want = plumTypeToValtype(&inferLocalType(expr, ctx));
5702
- coerceVt(plumTypeToValtype(decl), want, body, ctx.gc_types);
5834
+ coerceTy(decl, &inferLocalType(expr, ctx), body, ctx.gc_types);
5703
5835
  }
5704
5836
  }
5705
5837
  // Not a local: a bare reference to a top-level function used as a
@@ -5772,14 +5904,13 @@ fn compileExpr(
5772
5904
  })?;
5773
5905
  // The payload slot is declared at its generic type (`T` -> `$Obj`); the
5774
5906
  // value this expression yields is at its instantiated type.
5775
- let raw_success_vt = plumTypeToValtype(
5907
+ let raw_success_ty = success_info
5776
- &success_info
5777
- .field_types
5908
+ .field_types
5778
- .first()
5909
+ .first()
5779
- .cloned()
5910
+ .cloned()
5780
- .unwrap_or(PlumType::TUnit),
5911
+ .unwrap_or(PlumType::TUnit);
5781
- );
5912
+ let success_ty = inferLocalType(expr, ctx);
5782
- let success_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
5913
+ let success_vt = plumTypeToValtype(&success_ty);
5783
5914
 
5784
5915
  let key = expr as *const ast::Expr as usize;
5785
5916
  let slot = *ctx
@@ -5840,7 +5971,7 @@ fn compileExpr(
5840
5971
  field_index: fld(0),
5841
5972
  }
5842
5973
  .encode(body);
5843
- coerceVt(raw_success_vt, success_vt, body, ctx.gc_types);
5974
+ coerceTy(&raw_success_ty, &success_ty, body, ctx.gc_types);
5844
5975
  }
5845
5976
  Instruction::End.encode(body);
5846
5977
  }
@@ -5893,14 +6024,13 @@ fn compileExpr(
5893
6024
  failure_name
5894
6025
  )
5895
6026
  })?;
5896
- let raw_success_vt = plumTypeToValtype(
6027
+ let raw_success_ty = success_info
5897
- &success_info
5898
- .field_types
6028
+ .field_types
5899
- .first()
6029
+ .first()
5900
- .cloned()
6030
+ .cloned()
5901
- .unwrap_or(PlumType::TUnit),
6031
+ .unwrap_or(PlumType::TUnit);
5902
- );
6032
+ let success_ty = inferLocalType(expr, ctx);
5903
- let success_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
6033
+ let success_vt = plumTypeToValtype(&success_ty);
5904
6034
 
5905
6035
  let key = expr as *const ast::Expr as usize;
5906
6036
  let slot = *ctx
@@ -5923,7 +6053,7 @@ fn compileExpr(
5923
6053
  field_index: fld(0),
5924
6054
  }
5925
6055
  .encode(body);
5926
- coerceVt(raw_success_vt, success_vt, body, ctx.gc_types);
6056
+ coerceTy(&raw_success_ty, &success_ty, body, ctx.gc_types);
5927
6057
  }
5928
6058
  Instruction::End.encode(body);
5929
6059
  }
@@ -5989,12 +6119,10 @@ fn compileExpr(
5989
6119
  }
5990
6120
  ast::Expr::Bool(b) => {
5991
6121
  compileShortCircuitBoolI32(b, body, ctx, state)?;
5992
- pushBoolRefFromI32Flag(body, ctx);
5993
6122
  }
5994
6123
  ast::Expr::Not(inner) => {
5995
6124
  compileBoolConditionAsI32(inner, body, ctx, state)?;
5996
6125
  Instruction::I32Eqz.encode(body);
5997
- pushBoolRefFromI32Flag(body, ctx);
5998
6126
  }
5999
6127
  ast::Expr::Compare(c) => {
6000
6128
  let left_ty = inferLocalType(&c.left, ctx);
@@ -6019,7 +6147,6 @@ fn compileExpr(
6019
6147
  ))
6020
6148
  }
6021
6149
  }
6022
- pushBoolRefFromI32Flag(body, ctx);
6023
6150
  return Ok(());
6024
6151
  }
6025
6152
  compileExpr(&c.left, body, ctx, state)?;
@@ -6085,7 +6212,17 @@ fn compileExpr(
6085
6212
  ))
6086
6213
  }
6087
6214
  },
6215
+ PlumType::TBool => match &c.op {
6216
+ ast::CmpOp::Eq => Instruction::I32Eq.encode(body),
6217
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I32Ne.encode(body),
6218
+ other => {
6219
+ return Err(format!(
6220
+ "codegen: '{:?}' is not supported between Bool values",
6221
+ other
6222
+ ))
6223
+ }
6224
+ },
6088
- // Bool/class/enum values are wasm-gc refs — `==`/`!=` compares
6225
+ // Class/enum values are wasm-gc refs — `==`/`!=` compares
6089
6226
  // reference identity via `ref.eq`. That's exactly right for a
6090
6227
  // payload-free singleton (`End`/`True`/`False`, this migration
6091
6228
  // plan's Decision 2) and for class-instance identity.
@@ -6104,7 +6241,6 @@ fn compileExpr(
6104
6241
  }
6105
6242
  },
6106
6243
  }
6107
- pushBoolRefFromI32Flag(body, ctx);
6108
6244
  }
6109
6245
  ast::Expr::Ternary(t) => {
6110
6246
  let result_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
@@ -6243,6 +6379,9 @@ fn compileExpr(
6243
6379
  .ok_or_else(|| "codegen: 'self' used outside a method".to_string())?;
6244
6380
  Instruction::LocalGet(idx).encode(body);
6245
6381
  }
6382
+ ast::Expr::TypeName(n) if n == "True" || n == "False" => {
6383
+ Instruction::I32Const((n == "True") as i32).encode(body);
6384
+ }
6246
6385
  ast::Expr::TypeName(n) => {
6247
6386
  match ctx.enum_variants.get(n) {
6248
6387
  // A bare uppercase name the checker typed as a tag union is a tag
@@ -6445,12 +6584,7 @@ fn coerceCallResultTo(decl_ret: &PlumType, want: &PlumType, body: &mut Vec<u8>,
6445
6584
  if matches!(decl_ret, PlumType::TUnit) || matches!(want, PlumType::TUnit | PlumType::TVar(_)) {
6446
6585
  return;
6447
6586
  }
6448
- coerceVt(
6449
- plumTypeToValtype(decl_ret),
6587
+ coerceTy(decl_ret, want, body, ctx.gc_types);
6450
- plumTypeToValtype(want),
6451
- body,
6452
- ctx.gc_types,
6453
- );
6454
6588
  }
6455
6589
 
6456
6590
  /// Pushes the arguments of a call to the function described by `sig`: each fixed
@@ -6526,7 +6660,7 @@ fn compileFieldRead(
6526
6660
  ))
6527
6661
  }
6528
6662
  };
6529
- let want = plumTypeToValtype(&inferLocalType(expr, ctx));
6663
+ let want = &inferLocalType(expr, ctx);
6530
6664
  // A discriminant enum's shared params are declared directly on the enum's
6531
6665
  // SUPERTYPE (see `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to
6532
6666
  // any one variant needed.
@@ -6548,12 +6682,7 @@ fn compileFieldRead(
6548
6682
  field_index: fld(field_idx),
6549
6683
  }
6550
6684
  .encode(body);
6551
- coerceVt(
6552
- plumTypeToValtype(&params[field_idx].1),
6685
+ coerceTy(&params[field_idx].1, want, body, ctx.gc_types);
6553
- want,
6554
- body,
6555
- ctx.gc_types,
6556
- );
6557
6686
  return Ok(());
6558
6687
  }
6559
6688
  // Not a discriminant enum: `class_name` is an ordinary enum, and the checker
@@ -6590,12 +6719,7 @@ fn compileFieldRead(
6590
6719
  field_index: fld(field_idx),
6591
6720
  }
6592
6721
  .encode(body);
6593
- coerceVt(
6594
- plumTypeToValtype(&info.field_types[field_idx]),
6722
+ coerceTy(&info.field_types[field_idx], want, body, ctx.gc_types);
6595
- want,
6596
- body,
6597
- ctx.gc_types,
6598
- );
6599
6723
  Ok(())
6600
6724
  }
6601
6725
 
@@ -7025,12 +7149,8 @@ fn compileClosureLiteral(
7025
7149
  )
7026
7150
  })?;
7027
7151
  Instruction::LocalGet(local_idx).encode(body);
7028
- let have = ctx
7029
- .local_decl_types
7030
- .get(name)
7031
- .map(plumTypeToValtype)
7032
- .unwrap_or_else(|| plumTypeToValtype(cap_ty));
7152
+ let have = ctx.local_decl_types.get(name).unwrap_or(cap_ty);
7033
- coerceVt(have, plumTypeToValtype(cap_ty), body, ctx.gc_types);
7153
+ coerceTy(have, cap_ty, body, ctx.gc_types);
7034
7154
  }
7035
7155
  Instruction::StructNew(info.env_type_idx).encode(body);
7036
7156
  Instruction::StructNew(ctx.gc_types.closure_type_idx).encode(body);
@@ -7097,8 +7217,7 @@ fn compileClosureCall(
7097
7217
  if closureCallReturnsUnit(&call.name, ctx) {
7098
7218
  Instruction::Drop.encode(body);
7099
7219
  } else {
7100
- let want = plumTypeToValtype(&inferLocalType(call_expr, ctx));
7220
+ coerceTy(&anyTy(), &inferLocalType(call_expr, ctx), body, ctx.gc_types);
7101
- coerceVt(any, want, body, ctx.gc_types);
7102
7221
  }
7103
7222
  Ok(())
7104
7223
  }
@@ -7163,12 +7282,12 @@ fn compileClosureBody(
7163
7282
  let mut idx = 0u32;
7164
7283
 
7165
7284
  idx += 1 + cl.params.len() as u32; // env + raw params (all in the signature)
7166
- let mut param_prologue: Vec<(u32, u32, ValType)> = Vec::new(); // (raw, typed, vt)
7285
+ let mut param_prologue: Vec<(u32, u32, PlumType)> = Vec::new(); // (raw, typed, ptype)
7167
7286
  for (i, (name, pty)) in cl.params.iter().zip(info.param_ptypes.iter()).enumerate() {
7168
7287
  let vt = plumTypeToValtype(pty);
7169
7288
  locals.insert(name.clone(), idx);
7170
7289
  groups.push(vt);
7171
- param_prologue.push((i as u32 + 1, idx, vt));
7290
+ param_prologue.push((i as u32 + 1, idx, pty.clone()));
7172
7291
  idx += 1;
7173
7292
  }
7174
7293
  for (name, ty) in &info.free_vars {
@@ -7228,9 +7347,9 @@ fn compileClosureBody(
7228
7347
  }
7229
7348
 
7230
7349
  // Prologue: convert each raw `anyref` param to its real representation.
7231
- for (raw, typed, vt) in &param_prologue {
7350
+ for (raw, typed, pty) in &param_prologue {
7232
7351
  Instruction::LocalGet(*raw).encode(&mut body);
7233
- coerceVt(ValType::Ref(RefType::ANYREF), *vt, &mut body, &ctx.gc_types);
7352
+ coerceTy(&anyTy(), pty, &mut body, &ctx.gc_types);
7234
7353
  Instruction::LocalSet(*typed).encode(&mut body);
7235
7354
  }
7236
7355
  // Restore each captured free variable from the env struct (local 0, statically
@@ -7280,14 +7399,13 @@ fn compileClosureBody(
7280
7399
  subject_ptypes: RefCell::new(HashMap::new()),
7281
7400
  };
7282
7401
 
7283
- let any = ValType::Ref(RefType::ANYREF);
7284
7402
  if matches!(info.ret_ptype, PlumType::TUnit) {
7285
7403
  compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, None)?;
7286
7404
  Instruction::RefNull(HeapType::ANY).encode(&mut body);
7287
7405
  } else {
7288
7406
  let ret_vt = plumTypeToValtype(&info.ret_ptype);
7289
7407
  compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, Some(ret_vt))?;
7290
- coerceVt(ret_vt, any, &mut body, &ctx.gc_types);
7408
+ coerceTy(&info.ret_ptype, &anyTy(), &mut body, &ctx.gc_types);
7291
7409
  }
7292
7410
 
7293
7411
  Instruction::End.encode(&mut body);
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1772,3 +1772,100 @@ fun main() -> Int =
1772
1772
  assert_eq!(maps.len(), 1, "got {:?}", keys);
1773
1773
  assert!(!keys.iter().any(|k| k.contains('$')), "mangled names remain");
1774
1774
  }
1775
+
1776
+ // ---- built-in Bool (i32; no `import std/Bool`) ----
1777
+
1778
+ #[test]
1779
+ fn builtin_bool_without_import() {
1780
+ let src = "\
1781
+ import std/Str
1782
+ import std/Number
1783
+
1784
+ fun flip(b: Bool) -> Bool =
1785
+ !b
1786
+
1787
+ fun name(b: Bool) -> Str =
1788
+ match b
1789
+ True => \"yes\"
1790
+ False => \"no\"
1791
+
1792
+ fun main() -> Int =
1793
+ x := True
1794
+ if flip(False) && x == True && !False || False
1795
+ if name(x) == \"yes\" && name(False) == \"no\" && \"{x}\" == \"True\"
1796
+ return 1
1797
+ return 0
1798
+ ";
1799
+ assert_eq!(runMerged::<i64>("bool_noimport", src), 1);
1800
+ }
1801
+
1802
+ #[test]
1803
+ fn builtin_bool_list_round_trip() {
1804
+ let src = "\
1805
+ import std/Str
1806
+ import std/Number
1807
+ import std/List
1808
+ import std/Try
1809
+
1810
+ fun main() -> Int =
1811
+ l := List[Bool]()
1812
+ l.push(True)
1813
+ l.push(False)
1814
+ l.push(True)
1815
+ a := l.get(0) ?? False
1816
+ b := l.get(1) ?? True
1817
+ c := l.get(2) ?? False
1818
+ if a && !b && c && l.length() == 3
1819
+ return 1
1820
+ return 0
1821
+ ";
1822
+ assert_eq!(runMerged::<i64>("bool_list", src), 1);
1823
+ }
1824
+
1825
+ #[test]
1826
+ fn boxed_bool_to_str_and_eq() {
1827
+ let src = "\
1828
+ import std/Str
1829
+ import std/Number
1830
+
1831
+ fun show(x: ToStr) -> Str =
1832
+ x.toStr()
1833
+
1834
+ fun same(a: T, b: T) -> Bool =
1835
+ a == b
1836
+
1837
+ fun main() -> Int =
1838
+ if show(True) == \"True\" && show(False) == \"False\" && same(True, True) && !same(True, False)
1839
+ return 1
1840
+ return 0
1841
+ ";
1842
+ assert_eq!(runMerged::<i64>("bool_boxed", src), 1);
1843
+ }
1844
+
1845
+ #[test]
1846
+ fn builtin_bool_in_try_with_question_and_elvis() {
1847
+ let src = "\
1848
+ import std/Str
1849
+ import std/Number
1850
+ import std/Try
1851
+
1852
+ fun yes(ok: Bool) -> Try[Bool, [Nope]] =
1853
+ if ok
1854
+ Ok(True)
1855
+ else
1856
+ Err(Nope)
1857
+
1858
+ fun chain(ok: Bool) -> Try[Bool, [Nope]] =
1859
+ v := yes(ok)?
1860
+ Ok(!v)
1861
+
1862
+ fun main() -> Int =
1863
+ a := chain(True) ?? True
1864
+ b := chain(False) ?? True
1865
+ p := Bool.parse(\"true\") ?? False
1866
+ if !a && b && p
1867
+ return 1
1868
+ return 0
1869
+ ";
1870
+ assert_eq!(runMerged::<i64>("bool_try", src), 1);
1871
+ }