plum

#treesitter#compiler#wasm

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

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


5e131a3Peter John 2026-07-20T12:14:54+05:30
fix(plum-wasm-codegen): guard constructor-pattern match against payload-free sibling values
plum-wasm-codegen/src/lib.rs CHANGED
@@ -978,10 +978,22 @@ fn compile_variant_constructor_arm(
978
978
  let tag = info.tag;
979
979
  let field_types = info.field_types.clone();
980
980
 
981
+ // A constructor pattern can only match if the runtime subject is actually
982
+ // a heap pointer (payload variants are always >= HEAP_BASE); a
983
+ // payload-free sibling variant is a small int tag, and loading i32 from
984
+ // that address would read unrelated/zeroed memory instead of a real tag.
985
+ // Guard with a range check before doing the I32Load.
986
+ Instruction::LocalGet(scratch_local).encode(body);
987
+ Instruction::I32Const(HEAP_BASE as i32).encode(body);
988
+ Instruction::I32GeU.encode(body);
989
+ Instruction::If(BlockType::Result(ValType::I32)).encode(body);
981
990
  Instruction::LocalGet(scratch_local).encode(body);
982
991
  Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
983
992
  Instruction::I32Const(tag).encode(body);
984
993
  Instruction::I32Eq.encode(body);
994
+ Instruction::Else.encode(body);
995
+ Instruction::I32Const(0).encode(body);
996
+ Instruction::End.encode(body);
985
997
  Instruction::If(BlockType::Empty).encode(body);
986
998
  for (i, (pat, field_ty)) in fields.iter().zip(field_types.iter()).enumerate() {
987
999
  let bind_name = match pat {
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -159,6 +159,36 @@ fn match_string_pattern_is_a_clear_error() {
159
159
  assert!(err.contains("string match"), "got: {}", err);
160
160
  }
161
161
 
162
+ #[test]
163
+ fn nested_constructor_pattern_is_a_clear_error() {
164
+ let src = "\
165
+ enum Option =
166
+ | Some(Int)
167
+ | None
168
+
169
+ enum Nested =
170
+ | Wrap(Option)
171
+ | Empty
172
+
173
+ f(n: Nested) -> Int =
174
+ match n
175
+ Wrap(Some(v)) =>
176
+ return v
177
+ Empty =>
178
+ return 0
179
+
180
+ main() -> Int =
181
+ f(Empty)
182
+ ";
183
+ let source = parse(src);
184
+ let err = compile_source(&source).expect_err("nested constructor patterns are not yet supported");
185
+ assert!(
186
+ err.contains("only bare bindings or '_' are supported inside a constructor pattern"),
187
+ "got: {}",
188
+ err
189
+ );
190
+ }
191
+
162
192
  /// Runs `main`'s wasm bytes and returns its i64 result. wasm's own validator
163
193
  /// (via wasmparser, above) only proves the module is well-formed — it can't catch
164
194
  /// wrong *values*, so these tests actually execute the compiled output.
@@ -469,3 +499,56 @@ main() -> Int =
469
499
  let bytes = compile_source(&source).expect("compile failed");
470
500
  assert_eq!(run_main(&bytes), 1);
471
501
  }
502
+
503
+ #[test]
504
+ fn constructor_pattern_does_not_misfire_on_payload_free_sibling() {
505
+ // Regression test: `None` is a small-int tag, not a heap pointer. The
506
+ // constructor-pattern arm for `Some(v)` must not treat a payload-free
507
+ // sibling value as if it were a pointer to a `Some` payload.
508
+ let src = "\
509
+ enum Option =
510
+ | Some(Int)
511
+ | None
512
+
513
+ unwrapOr(o: Option, default: Int) -> Int =
514
+ match o
515
+ Some(v) =>
516
+ return v
517
+ None =>
518
+ return default
519
+
520
+ main() -> Int =
521
+ unwrapOr(None, 5)
522
+ ";
523
+ let source = parse(src);
524
+ let bytes = compile_source(&source).expect("compile failed");
525
+ assert_eq!(run_main(&bytes), 5);
526
+ }
527
+
528
+ #[test]
529
+ fn enum_class_field_construct_and_destructure_runs_correctly() {
530
+ // Interop check: a class with a field of an enum type, constructed with a
531
+ // payload variant, then matched via the class field.
532
+ let src = "\
533
+ enum Option =
534
+ | Some(Int)
535
+ | None
536
+
537
+ type Box =
538
+ value: Option
539
+
540
+ unwrap<Box>(default: Int) -> Int =
541
+ match self.value
542
+ Some(v) =>
543
+ return v
544
+ None =>
545
+ return default
546
+
547
+ main() -> Int =
548
+ b = Box(value: Some(42))
549
+ b.unwrap(0)
550
+ ";
551
+ let source = parse(src);
552
+ let bytes = compile_source(&source).expect("compile failed");
553
+ assert_eq!(run_main(&bytes), 42);
554
+ }