plum

#treesitter#compiler#wasm

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

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


73e4ff1Peter John 2026-09-04T19:31:38+05:30
feat(plum): make Str a real class instead of a special-cased primitive
examples/basics.plum CHANGED
@@ -1,7 +1,7 @@
1
1
  module basics
2
2
 
3
3
  MAX_RETRIES = 3
4
- PI = 3.14159
4
+ GOLDEN_RATIO = 1.61803
5
5
  GREETING = "hello"
6
6
 
7
7
  fun main() =
@@ -11,7 +11,7 @@ fun main() =
11
11
  big := 1_000_000
12
12
  flt := 3.14
13
13
  flt2 := 12.0f
14
- exp := 6.022e23
14
+ avogadro := 6.022e23
15
15
  name := "plum"
16
16
  yes := True
17
17
  no := False
examples/match.plum CHANGED
@@ -1,12 +1,10 @@
1
+ import std/option
2
+
1
3
  enum Color =
2
4
  | Red
3
5
  | Green
4
6
  | Blue
5
7
 
6
- enum Option =
7
- | Some[Int]
8
- | None
9
-
10
8
  fun describeNumber(n: Int) -> Str =
11
9
  match n
12
10
  0 => "zero"
@@ -28,24 +26,38 @@ fun describeColor(c: Color) -> Str =
28
26
  Green => "green"
29
27
  Blue => "blue"
30
28
 
29
+ # A dedicated, non-generic "maybe an Int" — as opposed to the real, generic
30
+ # `Option[T]` (`import std/option` above, used elsewhere in this file):
31
+ # constructing a bare payload-free variant (`Absent` here, `None` for a real
32
+ # generic enum) outside of a `match` pattern can't be disambiguated between
33
+ # multiple concrete instantiations of ITS enum from that expression alone
34
+ # (see the README's Generics section) — and since this file's forced-in
35
+ # stdlib prelude (`plum-core::loader::loadAndMerge`) uses `Option` at several
36
+ # OTHER concrete types internally, a bare `None` here genuinely IS
37
+ # ambiguous. `IntOpt` sidesteps that entirely by only ever having ONE
38
+ # possible instantiation to begin with.
39
+ enum IntOpt =
40
+ | Present[Int]
41
+ | Absent
42
+
31
- fun describeOption(opt: Option) -> Int =
43
+ fun describeOption(opt: IntOpt) -> Int =
32
44
  match opt
33
- Some(v) => v
45
+ Present(v) => v
34
- None => 0
46
+ Absent => 0
35
47
 
36
48
  fun main() -> Int =
37
- describeOption(Some(5))
49
+ describeOption(Present(5))
38
50
 
39
51
  # ---- pattern matching regression tests ----
40
52
 
41
53
  enum Nested =
42
- | Wrap[Option]
54
+ | Wrap[IntOpt]
43
55
  | Empty
44
56
 
45
57
  fun unwrapNested(n: Nested) -> Int =
46
58
  match n
47
- Wrap(Some(v)) => v
59
+ Wrap(Present(v)) => v
48
- Wrap(None) => -1
60
+ Wrap(Absent) => -1
49
61
  Empty => 0
50
62
 
51
63
  enum GenericOption =
@@ -164,10 +176,10 @@ test "match example computes correctly"
164
176
  assert main() == 5
165
177
 
166
178
  test "nested constructor pattern matches and binds runs correctly"
167
- assert unwrapNested(Wrap(Some(5))) == 5
179
+ assert unwrapNested(Wrap(Present(5))) == 5
168
180
 
169
181
  test "nested constructor pattern mismatch falls through to next case runs correctly"
170
- assert unwrapNested(Wrap(None)) == -1
182
+ assert unwrapNested(Wrap(Absent)) == -1
171
183
 
172
184
  test "nested constructor pattern against a specialized generic enum runs correctly"
173
185
  assert unwrapGenericBox(GFull(GSome(7))) == 7
@@ -188,7 +200,7 @@ test "constructor pattern wildcard field runs correctly"
188
200
  assert isSome(Some(99)) == 1
189
201
 
190
202
  test "constructor pattern does not misfire on payload free sibling"
191
- assert describeOption(None) == 0
203
+ assert describeOption(Absent) == 0
192
204
 
193
205
  test "multi subject match with enum tags runs correctly"
194
206
  assert andOrCheck() == 1
examples/types.plum CHANGED
@@ -1,3 +1,5 @@
1
+ import std/option
2
+
1
3
  type Point =
2
4
  x: Int
3
5
  y: Int
@@ -5,6 +7,9 @@ type Point =
5
7
  type Named(ToStr) =
6
8
  name: Str
7
9
 
10
+ fun toStr(self) -> Str =
11
+ self.name
12
+
8
13
  type Box[T] =
9
14
  value: T
10
15
 
@@ -12,7 +17,15 @@ trait Shape =
12
17
  area() -> Float
13
18
  perimeter() -> Float
14
19
 
20
+ # Named distinctly from the real `Comparable`/`Ord` traits `libs/std/str.plum`
21
+ # claims to (but never actually implements — see its own header comment) —
22
+ # both are now genuinely reachable in the SAME merged program (`str.plum` is
23
+ # an always-implicit prelude, see `plum-core::loader::loadAndMerge`), and
24
+ # `checkTraitConformance` matches purely by bare trait name, so reusing
25
+ # "Comparable" here would make ITS unrelated demo declaration the thing that
26
+ # suddenly enforces (and fails) `Str`'s own long-standing, deliberately
27
+ # unenforced claim.
15
- trait Comparable[T: Ord] =
28
+ trait DemoComparable[T: Ord] =
16
29
  compareTo(other: T) -> Int
17
30
 
18
31
  enum Color =
@@ -20,10 +33,6 @@ enum Color =
20
33
  | Green
21
34
  | Blue
22
35
 
23
- enum Option =
24
- | Some[Int]
25
- | None
26
-
27
36
  fun makeIntBox() -> Box =
28
37
  Box(value: 5)
29
38
 
@@ -76,7 +85,7 @@ fun stepToNumber(s: Step) -> Int =
76
85
  ReadMin => 1
77
86
  ReadMax => 2
78
87
 
79
- fun unwrapOptionOr(o: Option, default: Int) -> Int =
88
+ fun unwrapOptionOr(o: Option[Int], default: Int) -> Int =
80
89
  match o
81
90
  Some(v) =>
82
91
  return v
@@ -95,7 +104,7 @@ fun area(s: ShapeKind) -> Int =
95
104
  return r * r
96
105
 
97
106
  type OptionBox =
98
- value: Option
107
+ value: Option[Int]
99
108
 
100
109
  fun unwrap(default: Int) -> Int =
101
110
  match self.value
libs/std/bytes.plum CHANGED
@@ -4,11 +4,14 @@ import std/str
4
4
 
5
5
  # ByteSlice is `[]Byte` — Plum's counterpart to Go's byte slice: a
6
6
  # fixed-length, mutable sequence of raw bytes backed directly by a wasm-gc
7
- # `array<i8>`, the SAME underlying array type `Str` itself already uses (see
8
- # `plum-wasm-codegen`'s `PlumType::TByteSlice`). Every method here, and every
7
+ # `array<i8>` (see `plum-wasm-codegen`'s `PlumType::TByteSlice`) the same
8
+ # raw array type `Buffer`'s own `data` field uses underneath (`Str` itself is
9
+ # now a real two-level struct wrapping a `Buffer`, not this array directly;
10
+ # see `str.plum`'s header comment). Every method here, and `makeBytes`/
9
- # free function below, is a compiler intrinsic (see `compileIntrinsicFnBody`)
11
+ # `copyBytes` below, is a compiler intrinsic (see `compileIntrinsicFnBody`)
10
12
  # — there's no way to express raw array length/indexing/construction/copying
11
- # in Plum source itself, exactly like `Str.length`/`byteAt`/`byteToStr`.
13
+ # in Plum source itself; `copyStrToBytes`/`bytesToStr` build on top of them
14
+ # instead of needing intrinsics of their own.
12
15
  type ByteSlice =
13
16
  # Number of bytes in the slice.
14
17
  fun length(self) -> Int =
@@ -32,14 +35,17 @@ fun makeBytes(n: Int) -> []Byte =
32
35
  fun copyBytes(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit =
33
36
  todo
34
37
 
35
- # Like `copyBytes`, but the source is a `Str` — `Str` and `[]Byte` share the
38
+ # Like `copyBytes`, but the source is a `Str` — ordinary Plum glue over
36
- # same underlying byte-array representation, so this is a plain bulk copy,
39
+ # `copyBytes` itself, reaching through `Str`'s real `data: Buffer` field to
37
- # not a conversion.
40
+ # its own `data: []Byte` (see `str.plum`'s header comment on `Str`'s shape).
38
41
  fun copyStrToBytes(dst: []Byte, dstStart: Int, src: Str, srcStart: Int, n: Int) -> Unit =
39
- todo
42
+ copyBytes(dst, dstStart, src.data.data, srcStart, n)
40
43
 
41
44
  # Copies out `n` bytes of `src` starting at `start` into a fresh, independent
42
45
  # `Str` — never aliases `src`, so mutating `src` afterwards can't
43
- # retroactively change an already-returned `Str`.
46
+ # retroactively change an already-returned `Str`. Ordinary Plum: allocate a
47
+ # right-sized `[]Byte`, copy into it, wrap it in a fresh `Buffer`/`Str` pair.
44
48
  fun bytesToStr(src: []Byte, start: Int, n: Int) -> Str =
45
- todo
49
+ data := makeBytes(n)
50
+ copyBytes(data, 0, src, start, n)
51
+ return Str(data: Buffer(data: data, len: n))
libs/std/str.plum CHANGED
@@ -1,26 +1,44 @@
1
1
  module std
2
2
 
3
3
  import std/list
4
+ import std/buffer
4
5
 
5
6
  # Any type that can be converted to a str needs to implement this trait
6
7
  trait ToStr =
7
8
  toStr() -> Str
8
9
 
9
- # A Str is an array of contiguous data stored in memory with a null termination using hex 0x00 or ASCII 0x00.
10
- # It is immutable and cannot be modified. It is copied for any changes and saved to a new memory location.
10
+ # A Str is an immutable byte sequence, backed by a real `Buffer` a genuine
11
+ # `type`/`data` field, not a compiler special case: `Str` is an ordinary
12
+ # wasm-gc struct wrapping a `Buffer` (itself a wasm-gc struct wrapping a raw
11
- # The previous str is freed if its reference count is 0 within the block.
13
+ # `[]Byte`), the same generic struct-of-fields codegen every other class
14
+ # gets. Only `byteToStr` (build a length-1 `Str` from a raw byte) and the
15
+ # string-literal/concatenation/equality/int-to-string runtime helpers still
16
+ # need to know that shape directly (see `plum-wasm-codegen`'s
17
+ # `compileIntrinsicFnBody`/`registerStringConcatHelper`/etc) — everything
18
+ # else, including `length`/`byteAt` below, is expressed by just calling
19
+ # through to `Buffer`.
20
+ #
21
+ # `Str` values are never actually MUTATED in place — a `Buffer` is mutable,
22
+ # but nothing here ever reassigns an existing `Str`'s `data` field or writes
23
+ # into its `Buffer` after construction; building a "changed" string always
24
+ # means constructing a brand new `Str`/`Buffer` pair (`Buffer.write`'s own
25
+ # callers are always building up a FRESH buffer before wrapping it, never
26
+ # mutating an already-published `Str`'s backing storage).
12
27
  type Str(Comparable, ToStr, Readable, Writable) =
13
28
  data: Buffer
14
29
 
15
- # Number of bytes in the string. Every other method below is built on top of
30
+ # Number of bytes in the string `self.data` (a `Buffer`) already tracks
16
- # this and `byteAt`/`byteToStr` (all three are compiler intrinsics — there is
31
+ # this, so unlike `byteAt`/`byteToStr` this needs no compiler intrinsic of
17
- # no way to express array length/indexing/construction in Plum source itself).
32
+ # its own.
18
33
  fun length(self) -> Int =
19
- todo
34
+ self.data.length()
20
35
 
21
- # The raw byte value (0-255) at index `i`. Traps if `i` is out of range.
36
+ # The raw byte value (0-255) at index `i`. Traps if `i` is out of range
37
+ # `self.data.data` (the `Buffer`'s own `[]Byte`) is a compiler intrinsic
38
+ # (see `libs/std/bytes.plum`'s `ByteSlice.get`), converted from `Byte` to
39
+ # a plain `Int` since that's this method's established contract.
22
40
  fun byteAt(self, i: Int) -> Int =
23
- todo
41
+ self.data.data.get(i).toInt()
24
42
 
25
43
  # FNV-1a over the raw UTF-8 bytes — used by `Map[K: Hashable, V]` (see
26
44
  # `map.plum`) to bucket `Str` keys. Not cryptographic, just a well-known,
plum-checker/src/lib.rs CHANGED
@@ -55,6 +55,14 @@ pub fn unify(t1: &PlumType, t2: &PlumType) -> Result<(), String> {
55
55
  (PlumType::TFloat, PlumType::TFloat) => Ok(()),
56
56
  (PlumType::TBool, PlumType::TBool) => Ok(()),
57
57
  (PlumType::TStr, PlumType::TStr) => Ok(()),
58
+ // `Str` is an ordinary class (`type Str(...) = data: Buffer ...` in
59
+ // `str.plum`) — a bare `Str(...)` construction (e.g. `bytesToStr`'s own
60
+ // body) infers as `TNamed("Str")` via `inferClassCallRaw`, while a `Str`
61
+ // type ANNOTATION (a param/return type, a string literal) resolves via
62
+ // `plumTypeFromName` to the dedicated `TStr` variant instead — these are
63
+ // the same real type spelled two different ways depending on how the
64
+ // checker reached it, so they must unify with each other too.
65
+ (PlumType::TStr, PlumType::TNamed(n)) | (PlumType::TNamed(n), PlumType::TStr) if n == "Str" => Ok(()),
58
66
  (PlumType::TByte, PlumType::TByte) => Ok(()),
59
67
  (PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
60
68
  (PlumType::TUnit, PlumType::TUnit) => Ok(()),
@@ -1224,20 +1232,31 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1224
1232
  let obj_ty = inferExpr(&attr.object, env, ctx)?;
1225
1233
  match &attr.attr {
1226
1234
  ast::AttrKind::Field(field_name) => match &obj_ty {
1235
+ // `Str` is an ordinary class (`type Str(...) = data: Buffer
1236
+ // ...` in `str.plum`) with a real field, unlike historically
1237
+ // (see the `other` hard-error arm's comment below) — resolve
1238
+ // it the same way `TNamed` does, just keyed by the literal
1239
+ // name "Str" since `TStr` itself carries no name.
1240
+ PlumType::TNamed(_) | PlumType::TStr => {
1241
+ let class_name: &str = match &obj_ty {
1242
+ PlumType::TNamed(n) => n.as_str(),
1243
+ _ => "Str",
1244
+ };
1227
- PlumType::TNamed(class_name) => match ctx.classes.get(class_name) {
1245
+ match ctx.classes.get(class_name) {
1228
- Some(fields) => fields.iter()
1246
+ Some(fields) => fields.iter()
1229
- .find(|(n, _)| n == field_name)
1230
- .map(|(_, t)| t.clone())
1231
- .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1232
- None => match ctx.enum_params.get(class_name) {
1233
- Some(params) => params.iter()
1234
1247
  .find(|(n, _)| n == field_name)
1235
1248
  .map(|(_, t)| t.clone())
1236
1249
  .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1250
+ None => match ctx.enum_params.get(class_name) {
1251
+ Some(params) => params.iter()
1252
+ .find(|(n, _)| n == field_name)
1253
+ .map(|(_, t)| t.clone())
1254
+ .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1237
- // Unmodeled type: allow, codegen will catch.
1255
+ // Unmodeled type: allow, codegen will catch.
1238
- None => Ok(PlumType::TVar("_".to_string())),
1256
+ None => Ok(PlumType::TVar("_".to_string())),
1239
- },
1257
+ },
1240
- },
1258
+ }
1259
+ }
1241
1260
  // An unresolved generic method return (e.g. `Result[T, E].unwrap()`'s
1242
1261
  // bare `T`, which this checker's `PlumType` erases entirely — see
1243
1262
  // `types.rs`'s `TNamed` having no type-argument slot) reaches here as
plum-cli/src/main.rs CHANGED
@@ -201,7 +201,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Re
201
201
  let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
202
202
  return Err(wasmtime::Error::msg("printLn expects a Str argument"));
203
203
  };
204
- let arr = s.unwrap_array(&caller)?;
204
+ let arr = unwrapStrToArray(&mut caller, s)?;
205
205
  let len = arr.len(&caller)?;
206
206
  let mut buf = vec![0u8; len as usize];
207
207
  arr.copy_to_i8_slice(&mut caller, &mut buf)?;
@@ -462,26 +462,47 @@ fn randomI64() -> i64 {
462
462
  x as i64
463
463
  }
464
464
 
465
+ /// `Str` is now a real two-level wasm-gc struct — `Str{data: Buffer{data:
466
+ /// []Byte, len: Int}}` (see `libs/std/str.plum`'s header comment on `Str`'s
467
+ /// shape) — not a raw `array<i8>` directly. Unwraps a `Str` `AnyRef` down to
468
+ /// that raw array, for host-side code (`printLn`, `readStrArg`,
469
+ /// `readStrResult`) that needs the actual bytes.
470
+ fn unwrapStrToArray(
471
+ mut store: impl wasmtime::AsContextMut,
472
+ s: &wasmtime::Rooted<wasmtime::AnyRef>,
473
+ ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
474
+ let str_struct = s.unwrap_struct(&store)?;
475
+ let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
476
+ return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
477
+ };
478
+ let buffer_struct = buffer_ref.unwrap_struct(&store)?;
479
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
480
+ return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference"));
481
+ };
482
+ bytes_ref.unwrap_array(&store)
483
+ }
484
+
465
- /// Reads a `Str` (wasm-gc `array<i8>`) argument's bytes into a Rust `String`,
485
+ /// Reads a `Str` argument's bytes into a Rust `String`, for the
466
- /// for the `rawReadFile`/`rawWriteFile`/`rawExists`/`rawMkdir`/`rawRemove` host
486
+ /// `rawReadFile`/`rawWriteFile`/`rawExists`/`rawMkdir`/`rawRemove` host
467
- /// imports below — the same array-unwrapping `printLn` already does, factored
487
+ /// imports below — factored out since every filesystem import needs to read
468
- /// out since every filesystem import needs to read at least one `Str` arg.
488
+ /// at least one `Str` arg.
469
489
  fn readStrArg(caller: &mut wasmtime::Caller<'_, ()>, val: &wasmtime::Val) -> wasmtime::Result<String> {
470
490
  let wasmtime::Val::AnyRef(Some(s)) = val else {
471
491
  return Err(wasmtime::Error::msg("expected a Str argument"));
472
492
  };
473
- let arr = s.unwrap_array(&caller)?;
493
+ let arr = unwrapStrToArray(&mut *caller, s)?;
474
494
  let len = arr.len(&caller)?;
475
495
  let mut buf = vec![0u8; len as usize];
476
496
  arr.copy_to_i8_slice(caller, &mut buf)?;
477
497
  Ok(String::from_utf8_lossy(&buf).into_owned())
478
498
  }
479
499
 
480
- /// Builds a new `Str` (wasm-gc `array<i8>`) value from raw bytes, to return
500
+ /// Builds a new `Str` value from raw bytes, to return from `rawReadFile` —
481
- /// from `rawReadFile` — the host-side counterpart to `readStrArg`. Needs the
501
+ /// the host-side counterpart to `readStrArg`. Allocates the raw `array<i8>`
482
- /// import's own reported `FuncType` to find the concrete array type to
502
+ /// (needs the import's own reported `FuncType` to find the concrete nested
483
- /// allocate (same reasoning as this function's caller's doc comment: a
484
- /// generic `ArrayRef` type would be the wrong, unrelated top array type).
503
+ /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
504
+ /// array type), then wraps it in a fresh `Buffer` struct and a `Str` struct
505
+ /// around that, mirroring `str.plum`'s real shape.
485
506
  fn makeStrResult(
486
507
  mut caller: impl wasmtime::AsContextMut,
487
508
  func_ty: &wasmtime::FuncType,
@@ -492,12 +513,35 @@ fn makeStrResult(
492
513
  let wasmtime::ValType::Ref(ref_ty) = result_ty else {
493
514
  return Err(wasmtime::Error::msg("expected a Str (ref) return type"));
494
515
  };
495
- let array_ty = ref_ty.heap_type().as_concrete_array()
516
+ let str_ty = ref_ty.heap_type().as_concrete_struct()
496
- .ok_or_else(|| wasmtime::Error::msg("expected a concrete array return type"))?
517
+ .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
497
518
  .clone();
519
+ let buffer_ty = str_ty.field(0)
520
+ .and_then(|f| match f.element_type() {
521
+ wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_struct().cloned(),
522
+ _ => None,
523
+ })
524
+ .ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type"))?;
525
+ let array_ty = buffer_ty.field(0)
526
+ .and_then(|f| match f.element_type() {
527
+ wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_array().cloned(),
528
+ _ => None,
529
+ })
530
+ .ok_or_else(|| wasmtime::Error::msg("expected Buffer.data to be a concrete []Byte array type"))?;
531
+
498
- let pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
532
+ let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
499
- let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &pre, bytes)?;
533
+ let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
534
+
535
+ let buffer_pre = wasmtime::StructRefPre::new(&mut caller, buffer_ty);
536
+ let buffer = wasmtime::StructRef::new(
537
+ &mut caller,
538
+ &buffer_pre,
539
+ &[wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)],
540
+ )?;
541
+
542
+ let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
543
+ let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?;
500
- Ok(arr.to_anyref())
544
+ Ok(str_val.to_anyref())
501
545
  }
502
546
 
503
547
  fn nowMillis() -> i64 {
@@ -642,7 +686,7 @@ fn readStrResult(store: &mut wasmtime::Store<()>, val: &wasmtime::Val) -> Result
642
686
  let wasmtime::Val::AnyRef(Some(s)) = val else {
643
687
  anyhow::bail!("internal error: expected a Str result");
644
688
  };
645
- let arr = s.unwrap_array(&mut *store)?;
689
+ let arr = unwrapStrToArray(&mut *store, s)?;
646
690
  let len = arr.len(&mut *store)?;
647
691
  let mut buf = vec![0u8; len as usize];
648
692
  arr.copy_to_i8_slice(&mut *store, &mut buf)?;
plum-core/src/loader.rs CHANGED
@@ -27,6 +27,27 @@ pub fn loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
27
27
  loadImport(import, lib_path, &mut visited, &mut items, &mut names)?;
28
28
  }
29
29
 
30
+ // `Str` is an ordinary class (`type Str(...) = data: Buffer ...` in
31
+ // `str.plum`, see its header comment) — unlike a true compiler primitive
32
+ // (`Int`/`Bool`), its wasm-gc representation now depends on that real
33
+ // declaration actually being present in the merged program. Loading it
34
+ // implicitly here, exactly as if every file `import`ed it, keeps string
35
+ // literals/`==`/`+`/`byteAt`/etc. usable with zero imports, matching how
36
+ // they behaved before this was a real class (and how `Int`/`Bool` still
37
+ // behave, being genuine primitives). `loadImport`'s own `visited` guard
38
+ // makes this a no-op for a file that already imports `std/str` itself
39
+ // (directly or transitively), so there's no duplicate-registration risk.
40
+ //
41
+ // Tolerates a `lib_path` that has no `std/str.plum` at all (e.g. a
42
+ // minimal, deliberately stdlib-free fixture directory used to test
43
+ // import resolution in isolation) — silently skipped rather than a hard
44
+ // error, since a program compiled against such a `lib_path` was never
45
+ // going to be able to use `Str` anyway; it'll get the same "Str must be
46
+ // registered" error at the point it actually tries to.
47
+ if lib_path.join("std/str.plum").exists() {
48
+ loadImport(&Import { path: "std/str".to_string() }, lib_path, &mut visited, &mut items, &mut names)?;
49
+ }
50
+
30
51
  Ok(Source {
31
52
  module: entry_source.module,
32
53
  imports: entry_source.imports,
plum-runtime/src/main.rs CHANGED
@@ -23,7 +23,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
23
23
  let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
24
24
  return Err(wasmtime::Error::msg("printLn expects a Str argument"));
25
25
  };
26
- let arr = s.unwrap_array(&caller)?;
26
+ let arr = unwrapStrToArray(&mut caller, s)?;
27
27
  let len = arr.len(&caller)?;
28
28
  let mut buf = vec![0u8; len as usize];
29
29
  arr.copy_to_i8_slice(&mut caller, &mut buf)?;
@@ -259,17 +259,41 @@ fn randomI64() -> i64 {
259
259
  /// See the identical helpers in `plum-cli/src/main.rs` — this crate is built
260
260
  /// fresh per `plum build` invocation, not linked against `plum-cli`, so the
261
261
  /// logic is duplicated rather than shared.
262
+ ///
263
+ /// `Str` is a real two-level wasm-gc struct — `Str{data: Buffer{data: []Byte,
264
+ /// len: Int}}` (see `libs/std/str.plum`'s header comment) — not a raw
265
+ /// `array<i8>` directly. Unwraps a `Str` `AnyRef` down to that raw array.
266
+ fn unwrapStrToArray(
267
+ mut store: impl wasmtime::AsContextMut,
268
+ s: &wasmtime::Rooted<wasmtime::AnyRef>,
269
+ ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
270
+ let str_struct = s.unwrap_struct(&store)?;
271
+ let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
272
+ return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
273
+ };
274
+ let buffer_struct = buffer_ref.unwrap_struct(&store)?;
275
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
276
+ return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference"));
277
+ };
278
+ bytes_ref.unwrap_array(&store)
279
+ }
280
+
262
281
  fn readStrArg(caller: &mut wasmtime::Caller<'_, ()>, val: &wasmtime::Val) -> wasmtime::Result<String> {
263
282
  let wasmtime::Val::AnyRef(Some(s)) = val else {
264
283
  return Err(wasmtime::Error::msg("expected a Str argument"));
265
284
  };
266
- let arr = s.unwrap_array(&caller)?;
285
+ let arr = unwrapStrToArray(&mut *caller, s)?;
267
286
  let len = arr.len(&caller)?;
268
287
  let mut buf = vec![0u8; len as usize];
269
288
  arr.copy_to_i8_slice(caller, &mut buf)?;
270
289
  Ok(String::from_utf8_lossy(&buf).into_owned())
271
290
  }
272
291
 
292
+ /// Builds a new `Str` value from raw bytes — allocates the raw `array<i8>`
293
+ /// (needs the import's own reported `FuncType` to find the concrete nested
294
+ /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
295
+ /// array type), then wraps it in a fresh `Buffer` struct and a `Str` struct
296
+ /// around that, mirroring `str.plum`'s real shape.
273
297
  fn makeStrResult(
274
298
  mut caller: impl wasmtime::AsContextMut,
275
299
  func_ty: &wasmtime::FuncType,
@@ -280,12 +304,35 @@ fn makeStrResult(
280
304
  let wasmtime::ValType::Ref(ref_ty) = result_ty else {
281
305
  return Err(wasmtime::Error::msg("expected a Str (ref) return type"));
282
306
  };
283
- let array_ty = ref_ty.heap_type().as_concrete_array()
307
+ let str_ty = ref_ty.heap_type().as_concrete_struct()
284
- .ok_or_else(|| wasmtime::Error::msg("expected a concrete array return type"))?
308
+ .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
285
309
  .clone();
310
+ let buffer_ty = str_ty.field(0)
311
+ .and_then(|f| match f.element_type() {
312
+ wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_struct().cloned(),
313
+ _ => None,
314
+ })
315
+ .ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type"))?;
316
+ let array_ty = buffer_ty.field(0)
317
+ .and_then(|f| match f.element_type() {
318
+ wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_array().cloned(),
319
+ _ => None,
320
+ })
321
+ .ok_or_else(|| wasmtime::Error::msg("expected Buffer.data to be a concrete []Byte array type"))?;
322
+
286
- let pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
323
+ let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
287
- let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &pre, bytes)?;
324
+ let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
325
+
326
+ let buffer_pre = wasmtime::StructRefPre::new(&mut caller, buffer_ty);
327
+ let buffer = wasmtime::StructRef::new(
328
+ &mut caller,
329
+ &buffer_pre,
330
+ &[wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)],
331
+ )?;
332
+
333
+ let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
334
+ let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?;
288
- Ok(arr.to_anyref())
335
+ Ok(str_val.to_anyref())
289
336
  }
290
337
 
291
338
  fn nowMillis() -> i64 {
plum-wasm-codegen/src/lib.rs CHANGED
@@ -567,14 +567,24 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
567
567
  PlumType::TInt => ValType::I64,
568
568
  PlumType::TFloat => ValType::F64,
569
569
  PlumType::TBool => withGcTypes(|r| gcRef(*r.enum_super_type_idx.get("Bool").expect("Bool must be registered"))),
570
+ // `Str` is an ORDINARY class (`type Str(...) = data: Buffer ...` in
571
+ // `str.plum`) — its own real struct type, registered exactly like any
572
+ // other class (see `buildGcTypeRegistry`'s `Slot::Class` handling),
573
+ // NOT the raw `byte_array_type_idx` (that's `Buffer`'s own `data`
574
+ // field's type, two levels down) — except when `Str` was never
575
+ // actually declared at all (an isolated snippet/unit test that never
576
+ // went through `plum-core::loader::loadAndMerge`'s `std/str` prelude),
577
+ // where `strGcValtypeOrFallback` falls back to treating `Str` as
578
+ // that raw array directly, matching this function's pre-refactor
579
+ // behavior.
570
- PlumType::TStr => withGcTypes(|r| gcRef(r.str_type_idx)),
580
+ PlumType::TStr => withGcTypes(strGcValtypeOrFallback),
571
581
  PlumType::TByte => ValType::I32,
572
582
  // `[]Byte` is represented by the EXACT SAME wasm-gc array type as `Str`
573
583
  // (a mutable `array<i8>`) — they're structurally identical, and nothing
574
584
  // in this codegen needs to distinguish them at the wasm-type level
575
585
  // (no runtime `ref.test`/dynamic dispatch keys off it), so reusing
576
- // `str_type_idx` avoids a second, redundant GC type-section entry.
586
+ // `byte_array_type_idx` avoids a second, redundant GC type-section entry.
577
- PlumType::TByteSlice => withGcTypes(|r| gcRef(r.str_type_idx)),
587
+ PlumType::TByteSlice => withGcTypes(|r| gcRef(r.byte_array_type_idx)),
578
588
  PlumType::TNamed(name) => withGcTypes(|r| {
579
589
  match r.class_type_idx.get(name).or_else(|| r.enum_super_type_idx.get(name)) {
580
590
  Some(idx) => gcRef(*idx),
@@ -612,7 +622,7 @@ pub struct GcTypeRegistry {
612
622
  /// subtype `struct` type index.
613
623
  pub variant_type_idx: HashMap<String, u32>,
614
624
  /// The single shared `array<i8>` type index every `Str` value uses.
615
- pub str_type_idx: u32,
625
+ pub byte_array_type_idx: u32,
616
626
  /// The single shared closure-value struct type index — `{table_idx: i32, env:
617
627
  /// anyref}` — every closure (and zero-capture "trampoline") uses regardless of
618
628
  /// its own captures; only assigned once closures are discovered (see
@@ -654,11 +664,15 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
654
664
  let idx = *registry.enum_super_type_idx.get("Bool").expect("internal codegen error: Bool must be registered in the GC type registry");
655
665
  ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(idx) })
656
666
  }
667
+ // See the matching comment in `plumTypeToValtype` — `Str` is an
668
+ // ordinary class's own real struct type, not the raw
669
+ // `byte_array_type_idx`, except when `Str` was never actually
670
+ // declared at all (`strGcValtypeOrFallback`'s fallback case).
657
- PlumType::TStr => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.str_type_idx) }),
671
+ PlumType::TStr => strGcValtypeOrFallback(registry),
658
672
  PlumType::TByte => ValType::I32,
659
- // See the matching comment in `plumTypeToValtype` — `[]Byte` reuses `Str`'s
673
+ // See the matching comment in `plumTypeToValtype` — `[]Byte` reuses
660
- // `array<i8>` GC type index rather than getting its own.
674
+ // `Buffer`'s own `array<i8>` GC type index rather than getting its own.
661
- PlumType::TByteSlice => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.str_type_idx) }),
675
+ PlumType::TByteSlice => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.byte_array_type_idx) }),
662
676
  PlumType::TNamed(name) => match registry.class_type_idx.get(name).or_else(|| registry.enum_super_type_idx.get(name)) {
663
677
  Some(idx) => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(*idx) }),
664
678
  // A field typed as a generic enum/class (e.g. `Node.next: Option[Node]`)
@@ -769,7 +783,7 @@ fn buildGcTypeRegistry(
769
783
  class_type_idx,
770
784
  enum_super_type_idx,
771
785
  variant_type_idx,
772
- str_type_idx: 0,
786
+ byte_array_type_idx: 0,
773
787
  closure_type_idx: 0,
774
788
  variadic_array_type_idx: HashMap::new(),
775
789
  array_type_idx: array_ref_slot.unwrap_or(0),
@@ -1257,7 +1271,17 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
1257
1271
  // (Runs on the monomorphized source, so any generic types in a closure's context
1258
1272
  // are already concrete.) Registers each closure as its own wasm function + table
1259
1273
  // element and records the free variables it must capture.
1274
+ // Only genuine free functions (`type_param.is_none()`) — a bare `FnCall`
1275
+ // (`each(...)`) can only ever resolve to a free function, never a method
1276
+ // (those always dispatch through `Attribute`/`AttrKind::Method`, keyed by
1277
+ // `(receiver, name)` instead — see that arm's own comment below). Keying
1278
+ // this by bare name across free functions AND methods would let a
1279
+ // monomorphized method silently SHADOW a same-named free function here
1280
+ // (e.g. a user's own top-level `each` losing to `List$Str.each` once
1281
+ // `List` happens to be in scope) — same-named methods on unrelated
1282
+ // classes are already ambiguous by bare name, so methods have no business
1283
+ // being in a bare-name lookup table at all.
1260
- let fn_decls: HashMap<String, &ast::Fn> = fns.iter().map(|f| (f.name.clone(), *f)).collect();
1284
+ let fn_decls: HashMap<String, &ast::Fn> = fns.iter().filter(|f| f.type_param.is_none()).map(|f| (f.name.clone(), *f)).collect();
1261
1285
  let mut raw_closures: Vec<RawClosure> = Vec::new();
1262
1286
  let mut named_fn_refs: HashSet<String> = HashSet::new();
1263
1287
  for f in &fns {
@@ -1409,9 +1433,9 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
1409
1433
 
1410
1434
  // Runtime helpers backing string interpolation (`"{expr}"`): always registered
1411
1435
  // (unconditionally, for simplicity) since they're cheap and self-contained.
1412
- let string_concat_func = registerStringConcatHelper(&mut module, gc_types.str_type_idx);
1436
+ let string_concat_func = registerStringConcatHelper(&mut module, gc_types.byte_array_type_idx);
1413
- let string_eq_func = registerStringEqHelper(&mut module, gc_types.str_type_idx);
1437
+ let string_eq_func = registerStringEqHelper(&mut module, gc_types.byte_array_type_idx);
1414
- let int_to_string_func = registerIntToStringHelper(&mut module, gc_types.str_type_idx);
1438
+ let int_to_string_func = registerIntToStringHelper(&mut module, gc_types.byte_array_type_idx);
1415
1439
 
1416
1440
  let ctx = CompileCtx {
1417
1441
  func_ids, func_sigs, classes, methods, enum_variants, enum_params, min_required, global_env,
@@ -1471,60 +1495,83 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
1471
1495
  }
1472
1496
 
1473
1497
  /// Registers `__string_concat(a: ref Str, b: ref Str) -> ref Str`, a hand-written
1474
- /// runtime helper backing string interpolation. `Str` is a wasm-gc `array<i8>`,
1498
+ /// runtime helper backing string interpolation. Unwraps both `Str` params down
1475
- /// which tracks its own length (`array.len`) — building the concatenation is:
1499
+ /// to their raw `array<i8>`s (`unwrapStrToBytes`) — which each track their own
1476
- /// allocate a new array sized `len(a) + len(b)`, then two `array.copy`s (a
1500
+ /// length via `array.len` — allocates a new array sized `len(a) + len(b)`,
1477
- /// whole-array-in-one-instruction bulk copy).
1501
+ /// does two `array.copy`s (a whole-array-in-one-instruction bulk copy), then
1502
+ /// wraps the result back into a real `Str` (`wrapBytesAsStr`).
1478
- fn registerStringConcatHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
1503
+ fn registerStringConcatHelper(module: &mut WasmModule, byte_array_type_idx: u32) -> u32 {
1479
- let str_ref = gcRef(str_type_idx);
1504
+ let byte_array_ref = gcRef(byte_array_type_idx);
1505
+ let str_ref = withGcTypes(strGcValtypeOrFallback);
1480
1506
  let type_idx = module.addType(&[str_ref, str_ref], &[str_ref]);
1481
1507
 
1482
- // locals: 0=a (param), 1=b (param), 2=len_a, 3=len_b, 4=result
1508
+ // locals: 0=a (param), 1=b (param), 2=arr_a, 3=arr_b, 4=len_a, 5=len_b,
1509
+ // 6=total_len, 7=result
1483
1510
  const A: u32 = 0;
1484
1511
  const B: u32 = 1;
1512
+ const ARR_A: u32 = 2;
1513
+ const ARR_B: u32 = 3;
1485
- const LEN_A: u32 = 2;
1514
+ const LEN_A: u32 = 4;
1486
- const LEN_B: u32 = 3;
1515
+ const LEN_B: u32 = 5;
1516
+ const TOTAL_LEN: u32 = 6;
1487
- const RESULT: u32 = 4;
1517
+ const RESULT: u32 = 7;
1488
1518
 
1489
1519
  let mut body = Vec::new();
1490
- body.extend(encodeLeb128U32(2)); // two locals groups
1520
+ body.extend(encodeLeb128U32(3)); // three locals groups
1491
- body.extend(encodeLeb128U32(2)); // len_a, len_b: i32
1521
+ body.extend(encodeLeb128U32(2)); // arr_a, arr_b: ref
1522
+ byte_array_ref.encode(&mut body);
1523
+ body.extend(encodeLeb128U32(3)); // len_a, len_b, total_len: i32
1492
1524
  ValType::I32.encode(&mut body);
1493
1525
  body.extend(encodeLeb128U32(1)); // result: ref
1494
- str_ref.encode(&mut body);
1526
+ byte_array_ref.encode(&mut body);
1495
1527
 
1496
- // len_a = array.len(a); len_b = array.len(b)
1528
+ // arr_a = a.data.data; arr_b = b.data.data
1497
1529
  Instruction::LocalGet(A).encode(&mut body);
1530
+ unwrapStrToBytes(&mut body);
1531
+ Instruction::LocalSet(ARR_A).encode(&mut body);
1532
+ Instruction::LocalGet(B).encode(&mut body);
1533
+ unwrapStrToBytes(&mut body);
1534
+ Instruction::LocalSet(ARR_B).encode(&mut body);
1535
+
1536
+ // len_a = array.len(arr_a); len_b = array.len(arr_b)
1537
+ Instruction::LocalGet(ARR_A).encode(&mut body);
1498
1538
  Instruction::ArrayLen.encode(&mut body);
1499
1539
  Instruction::LocalSet(LEN_A).encode(&mut body);
1500
- Instruction::LocalGet(B).encode(&mut body);
1540
+ Instruction::LocalGet(ARR_B).encode(&mut body);
1501
1541
  Instruction::ArrayLen.encode(&mut body);
1502
1542
  Instruction::LocalSet(LEN_B).encode(&mut body);
1503
1543
 
1504
- // result = array.new_default(str_type_idx, len_a + len_b)
1544
+ // total_len = len_a + len_b; result = array.new_default(byte_array_type_idx, total_len)
1505
1545
  Instruction::LocalGet(LEN_A).encode(&mut body);
1506
1546
  Instruction::LocalGet(LEN_B).encode(&mut body);
1507
1547
  Instruction::I32Add.encode(&mut body);
1548
+ Instruction::LocalSet(TOTAL_LEN).encode(&mut body);
1549
+ Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
1508
- Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
1550
+ Instruction::ArrayNewDefault(byte_array_type_idx).encode(&mut body);
1509
1551
  Instruction::LocalSet(RESULT).encode(&mut body);
1510
1552
 
1511
- // array.copy(dst: result, dst_offset: 0, src: a, src_offset: 0, len: len_a)
1553
+ // array.copy(dst: result, dst_offset: 0, src: arr_a, src_offset: 0, len: len_a)
1512
1554
  Instruction::LocalGet(RESULT).encode(&mut body);
1513
1555
  Instruction::I32Const(0).encode(&mut body);
1514
- Instruction::LocalGet(A).encode(&mut body);
1556
+ Instruction::LocalGet(ARR_A).encode(&mut body);
1515
1557
  Instruction::I32Const(0).encode(&mut body);
1516
1558
  Instruction::LocalGet(LEN_A).encode(&mut body);
1517
- Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
1559
+ Instruction::ArrayCopy { array_type_index_dst: byte_array_type_idx, array_type_index_src: byte_array_type_idx }.encode(&mut body);
1518
1560
 
1519
- // array.copy(dst: result, dst_offset: len_a, src: b, src_offset: 0, len: len_b)
1561
+ // array.copy(dst: result, dst_offset: len_a, src: arr_b, src_offset: 0, len: len_b)
1520
1562
  Instruction::LocalGet(RESULT).encode(&mut body);
1521
1563
  Instruction::LocalGet(LEN_A).encode(&mut body);
1522
- Instruction::LocalGet(B).encode(&mut body);
1564
+ Instruction::LocalGet(ARR_B).encode(&mut body);
1523
1565
  Instruction::I32Const(0).encode(&mut body);
1524
1566
  Instruction::LocalGet(LEN_B).encode(&mut body);
1525
- Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
1567
+ Instruction::ArrayCopy { array_type_index_dst: byte_array_type_idx, array_type_index_src: byte_array_type_idx }.encode(&mut body);
1526
1568
 
1569
+ // wrap result (the raw array) back into a real Str
1527
1570
  Instruction::LocalGet(RESULT).encode(&mut body);
1571
+ wrapBytesAsStr(&mut body, |b| {
1572
+ Instruction::LocalGet(TOTAL_LEN).encode(b);
1573
+ Instruction::I64ExtendI32U.encode(b);
1574
+ });
1528
1575
  Instruction::End.encode(&mut body);
1529
1576
 
1530
1577
  module.addFunction(type_idx, &body)
@@ -1540,26 +1587,39 @@ fn registerStringConcatHelper(module: &mut WasmModule, str_type_idx: u32) -> u32
1540
1587
  /// unequal — silently breaking the extremely common `someStr == "literal"`
1541
1588
  /// pattern (e.g. `Map`'s own `Str`-keyed `get`/`set` comparing keys). This does
1542
1589
  /// a real byte-for-byte comparison instead: same length, then every byte equal.
1543
- fn registerStringEqHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
1590
+ fn registerStringEqHelper(module: &mut WasmModule, byte_array_type_idx: u32) -> u32 {
1544
- let str_ref = gcRef(str_type_idx);
1591
+ let byte_array_ref = gcRef(byte_array_type_idx);
1592
+ let str_ref = withGcTypes(strGcValtypeOrFallback);
1545
1593
  let type_idx = module.addType(&[str_ref, str_ref], &[ValType::I32]);
1546
1594
 
1547
- // locals: 0=a (param), 1=b (param), 2=len_a, 3=len_b, 4=i
1595
+ // locals: 0=a (param), 1=b (param), 2=arr_a, 3=arr_b, 4=len_a, 5=len_b, 6=i
1548
1596
  const A: u32 = 0;
1549
1597
  const B: u32 = 1;
1598
+ const ARR_A: u32 = 2;
1599
+ const ARR_B: u32 = 3;
1550
- const LEN_A: u32 = 2;
1600
+ const LEN_A: u32 = 4;
1551
- const LEN_B: u32 = 3;
1601
+ const LEN_B: u32 = 5;
1552
- const I: u32 = 4;
1602
+ const I: u32 = 6;
1553
1603
 
1554
1604
  let mut body = Vec::new();
1555
- body.extend(encodeLeb128U32(1)); // one locals group
1605
+ body.extend(encodeLeb128U32(2)); // two locals groups
1606
+ body.extend(encodeLeb128U32(2)); // arr_a, arr_b: ref
1607
+ byte_array_ref.encode(&mut body);
1556
1608
  body.extend(encodeLeb128U32(3)); // len_a, len_b, i: i32
1557
1609
  ValType::I32.encode(&mut body);
1558
1610
 
1611
+ // arr_a = a.data.data; arr_b = b.data.data
1559
1612
  Instruction::LocalGet(A).encode(&mut body);
1613
+ unwrapStrToBytes(&mut body);
1614
+ Instruction::LocalSet(ARR_A).encode(&mut body);
1615
+ Instruction::LocalGet(B).encode(&mut body);
1616
+ unwrapStrToBytes(&mut body);
1617
+ Instruction::LocalSet(ARR_B).encode(&mut body);
1618
+
1619
+ Instruction::LocalGet(ARR_A).encode(&mut body);
1560
1620
  Instruction::ArrayLen.encode(&mut body);
1561
1621
  Instruction::LocalSet(LEN_A).encode(&mut body);
1562
- Instruction::LocalGet(B).encode(&mut body);
1622
+ Instruction::LocalGet(ARR_B).encode(&mut body);
1563
1623
  Instruction::ArrayLen.encode(&mut body);
1564
1624
  Instruction::LocalSet(LEN_B).encode(&mut body);
1565
1625
 
@@ -1584,12 +1644,12 @@ fn registerStringEqHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
1584
1644
  Instruction::I32GeU.encode(&mut body);
1585
1645
  Instruction::BrIf(1).encode(&mut body);
1586
1646
 
1587
- Instruction::LocalGet(A).encode(&mut body);
1647
+ Instruction::LocalGet(ARR_A).encode(&mut body);
1588
1648
  Instruction::LocalGet(I).encode(&mut body);
1589
- Instruction::ArrayGetU(str_type_idx).encode(&mut body);
1649
+ Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
1590
- Instruction::LocalGet(B).encode(&mut body);
1650
+ Instruction::LocalGet(ARR_B).encode(&mut body);
1591
1651
  Instruction::LocalGet(I).encode(&mut body);
1592
- Instruction::ArrayGetU(str_type_idx).encode(&mut body);
1652
+ Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
1593
1653
  Instruction::I32Ne.encode(&mut body);
1594
1654
  Instruction::If(BlockType::Empty).encode(&mut body);
1595
1655
  Instruction::I32Const(0).encode(&mut body);
@@ -1614,8 +1674,9 @@ fn registerStringEqHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
1614
1674
  /// backing string interpolation: allocates a new `array<i8>` holding `n`'s decimal
1615
1675
  /// representation (handling a leading `-` for negatives, and `0` correctly via a
1616
1676
  /// do-while digit count that always runs at least once).
1617
- fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
1677
+ fn registerIntToStringHelper(module: &mut WasmModule, byte_array_type_idx: u32) -> u32 {
1618
- let str_ref = gcRef(str_type_idx);
1678
+ let byte_array_ref = gcRef(byte_array_type_idx);
1679
+ let str_ref = withGcTypes(strGcValtypeOrFallback);
1619
1680
  let type_idx = module.addType(&[ValType::I64], &[str_ref]);
1620
1681
 
1621
1682
  // locals: 0=n (param, i64), 1=is_neg (i32), 2=count (i32), 3=total_len (i32),
@@ -1637,7 +1698,7 @@ fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32
1637
1698
  body.extend(encodeLeb128U32(4)); // is_neg, count, total_len, pos: i32
1638
1699
  ValType::I32.encode(&mut body);
1639
1700
  body.extend(encodeLeb128U32(1)); // result: ref
1640
- str_ref.encode(&mut body);
1701
+ byte_array_ref.encode(&mut body);
1641
1702
  body.extend(encodeLeb128U32(2)); // abs_n, temp: i64
1642
1703
  ValType::I64.encode(&mut body);
1643
1704
 
@@ -1684,10 +1745,10 @@ fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32
1684
1745
  Instruction::I32Add.encode(&mut body);
1685
1746
  Instruction::LocalSet(TOTAL_LEN).encode(&mut body);
1686
1747
 
1687
- // result = array.new_default(str_type_idx, total_len) — no length prefix needed,
1748
+ // result = array.new_default(byte_array_type_idx, total_len) — no length prefix needed,
1688
1749
  // `array.len` reads it back natively.
1689
1750
  Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
1690
- Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
1751
+ Instruction::ArrayNewDefault(byte_array_type_idx).encode(&mut body);
1691
1752
  Instruction::LocalSet(RESULT).encode(&mut body);
1692
1753
 
1693
1754
  // pos = total_len; temp = abs_n
@@ -1712,7 +1773,7 @@ fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32
1712
1773
  Instruction::I64Const(48).encode(&mut body);
1713
1774
  Instruction::I64Add.encode(&mut body);
1714
1775
  Instruction::I32WrapI64.encode(&mut body);
1715
- Instruction::ArraySet(str_type_idx).encode(&mut body);
1776
+ Instruction::ArraySet(byte_array_type_idx).encode(&mut body);
1716
1777
  // temp /= 10
1717
1778
  Instruction::LocalGet(TEMP).encode(&mut body);
1718
1779
  Instruction::I64Const(10).encode(&mut body);
@@ -1731,10 +1792,14 @@ fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32
1731
1792
  Instruction::LocalGet(RESULT).encode(&mut body);
1732
1793
  Instruction::I32Const(0).encode(&mut body);
1733
1794
  Instruction::I32Const(45).encode(&mut body); // '-'
1734
- Instruction::ArraySet(str_type_idx).encode(&mut body);
1795
+ Instruction::ArraySet(byte_array_type_idx).encode(&mut body);
1735
1796
  Instruction::End.encode(&mut body);
1736
1797
 
1737
1798
  Instruction::LocalGet(RESULT).encode(&mut body);
1799
+ wrapBytesAsStr(&mut body, |b| {
1800
+ Instruction::LocalGet(TOTAL_LEN).encode(b);
1801
+ Instruction::I64ExtendI32U.encode(b);
1802
+ });
1738
1803
  Instruction::End.encode(&mut body);
1739
1804
 
1740
1805
  module.addFunction(type_idx, &body)
@@ -1754,34 +1819,17 @@ fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
1754
1819
  return compileArrayIntrinsicFnBody(f);
1755
1820
  }
1756
1821
  }
1757
- let str_type_idx = withGcTypes(|r| r.str_type_idx);
1822
+ let byte_array_type_idx = withGcTypes(|r| r.byte_array_type_idx);
1758
1823
  match (f.type_param.as_deref(), f.name.as_str()) {
1759
- // Str.length(self) -> Int
1760
- (Some("Str"), "length") => {
1761
- let mut body = vec![0u8]; // no locals
1762
- Instruction::LocalGet(0).encode(&mut body); // self
1763
- Instruction::ArrayLen.encode(&mut body);
1764
- Instruction::I64ExtendI32U.encode(&mut body);
1765
- Instruction::End.encode(&mut body);
1766
- Some(body)
1767
- }
1768
- // Str.byteAt(self, i: Int) -> Int — the byte's unsigned value (0-255).
1769
- (Some("Str"), "byteAt") => {
1770
- let mut body = vec![0u8];
1771
- Instruction::LocalGet(0).encode(&mut body); // self
1772
- Instruction::LocalGet(1).encode(&mut body); // i
1773
- Instruction::I32WrapI64.encode(&mut body);
1774
- Instruction::ArrayGetU(str_type_idx).encode(&mut body);
1775
- Instruction::I64ExtendI32U.encode(&mut body);
1776
- Instruction::End.encode(&mut body);
1777
- Some(body)
1778
- }
1779
- // byteToStr(b: Int) -> Str — a new 1-byte Str holding `b`'s low 8 bits.
1824
+ // byteToStr(b: Int) -> Str — a new length-1 Str holding `b`'s low 8 bits,
1825
+ // wrapped into `Str`'s real `{data: Buffer{data: []Byte, len: Int}}`
1826
+ // shape (see `wrapBytesAsStr`).
1780
1827
  (None, "byteToStr") => {
1781
1828
  let mut body = vec![0u8];
1782
1829
  Instruction::LocalGet(0).encode(&mut body); // b
1783
1830
  Instruction::I32WrapI64.encode(&mut body);
1784
- Instruction::ArrayNewFixed { array_type_index: str_type_idx, array_size: 1 }.encode(&mut body);
1831
+ Instruction::ArrayNewFixed { array_type_index: byte_array_type_idx, array_size: 1 }.encode(&mut body);
1832
+ wrapBytesAsStr(&mut body, |b| { Instruction::I64Const(1).encode(b); });
1785
1833
  Instruction::End.encode(&mut body);
1786
1834
  Some(body)
1787
1835
  }
@@ -1803,7 +1851,7 @@ fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
1803
1851
  Instruction::LocalGet(0).encode(&mut body); // self
1804
1852
  Instruction::LocalGet(1).encode(&mut body); // i
1805
1853
  Instruction::I32WrapI64.encode(&mut body);
1806
- Instruction::ArrayGetU(str_type_idx).encode(&mut body);
1854
+ Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
1807
1855
  Instruction::End.encode(&mut body);
1808
1856
  Some(body)
1809
1857
  }
@@ -1814,7 +1862,7 @@ fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
1814
1862
  Instruction::LocalGet(1).encode(&mut body); // i
1815
1863
  Instruction::I32WrapI64.encode(&mut body);
1816
1864
  Instruction::LocalGet(2).encode(&mut body); // b (already i32)
1817
- Instruction::ArraySet(str_type_idx).encode(&mut body);
1865
+ Instruction::ArraySet(byte_array_type_idx).encode(&mut body);
1818
1866
  Instruction::End.encode(&mut body);
1819
1867
  Some(body)
1820
1868
  }
@@ -1823,17 +1871,12 @@ fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
1823
1871
  let mut body = vec![0u8];
1824
1872
  Instruction::LocalGet(0).encode(&mut body); // n
1825
1873
  Instruction::I32WrapI64.encode(&mut body);
1826
- Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
1874
+ Instruction::ArrayNewDefault(byte_array_type_idx).encode(&mut body);
1827
1875
  Instruction::End.encode(&mut body);
1828
1876
  Some(body)
1829
1877
  }
1830
1878
  // copyBytes(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit
1831
- // copyStrToBytes(dst: []Byte, dstStart: Int, src: Str, srcStart: Int, n: Int) -> Unit
1832
- // Both compile to the exact same `array.copy` — `[]Byte` and `Str` share
1833
- // one underlying wasm-gc array type, so a bulk copy between them needs no
1834
- // conversion, just the one instruction. Two Plum-level names exist only so
1835
- // the checker can enforce each argument's declared type.
1836
- (None, "copyBytes") | (None, "copyStrToBytes") => {
1879
+ (None, "copyBytes") => {
1837
1880
  let mut body = vec![0u8];
1838
1881
  Instruction::LocalGet(0).encode(&mut body); // dst
1839
1882
  Instruction::LocalGet(1).encode(&mut body); // dstStart
@@ -1843,47 +1886,68 @@ fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
1843
1886
  Instruction::I32WrapI64.encode(&mut body);
1844
1887
  Instruction::LocalGet(4).encode(&mut body); // n
1845
1888
  Instruction::I32WrapI64.encode(&mut body);
1846
- Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
1889
+ Instruction::ArrayCopy { array_type_index_dst: byte_array_type_idx, array_type_index_src: byte_array_type_idx }.encode(&mut body);
1847
1890
  Instruction::End.encode(&mut body);
1848
1891
  Some(body)
1849
1892
  }
1850
- // bytesToStr(src: []Byte, start: Int, n: Int) -> Str — copies out `n` bytes
1851
- // starting at `start` into a fresh `Str`, rather than aliasing `src`
1852
- // directly, so a later mutation of `src` (e.g. `Buffer` reusing/growing its
1853
- // backing slice) can never retroactively change an already-returned `Str`.
1854
- (None, "bytesToStr") => {
1855
- const SRC: u32 = 0;
1856
- const START: u32 = 1;
1857
- const N: u32 = 2;
1893
+ _ => None,
1858
- const RESULT: u32 = 3;
1894
+ }
1859
- let str_ref = gcRef(str_type_idx);
1860
- let mut body = Vec::new();
1895
+ }
1861
- body.extend(encodeLeb128U32(1)); // one locals group
1862
- body.extend(encodeLeb128U32(1)); // result: ref
1863
- str_ref.encode(&mut body);
1864
1896
 
1897
+ /// `Str`'s wasm-gc valtype, exactly like `plumTypeToGcValtype(&PlumType::TStr,
1898
+ /// r)` — EXCEPT it tolerates a program compiled against a `lib_path` that
1899
+ /// never loaded `str.plum` at all (see `plum-core::loader::loadAndMerge`'s
1900
+ /// best-effort `std/str` prelude), falling back to the raw `array<i8>` type
1901
+ /// instead of panicking. Safe specifically because this is only used by the
1902
+ /// three string runtime helpers (`registerStringConcatHelper`/
1903
+ /// `registerStringEqHelper`/`registerIntToStringHelper`), which are
1904
+ /// registered UNCONDITIONALLY in every compiled program regardless of
1905
+ /// whether it actually uses `Str` — a `Str`-less program (which, by
1865
- Instruction::LocalGet(N).encode(&mut body);
1906
+ /// definition, never calls them) doesn't care what type they claim.
1866
- Instruction::I32WrapI64.encode(&mut body);
1867
- Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
1907
+ fn strGcValtypeOrFallback(r: &GcTypeRegistry) -> ValType {
1868
- Instruction::LocalSet(RESULT).encode(&mut body);
1908
+ match r.class_type_idx.get("Str") {
1909
+ Some(idx) => gcRef(*idx),
1910
+ None => gcRef(r.byte_array_type_idx),
1911
+ }
1912
+ }
1869
1913
 
1914
+ /// Wraps a just-pushed raw `array<i8>` (top of the value stack) — together
1915
+ /// with `push_len`, which must push its length as an `i64` — into `Str`'s
1916
+ /// real two-level `{data: Buffer{data: []Byte, len: Int}}` representation:
1917
+ /// `struct.new Buffer(arr, len)`, then `struct.new Str(buffer)`. Every codegen
1918
+ /// site that builds a `Str` value directly from raw bytes (`byteToStr`,
1919
+ /// string literals, the string-concat/int-to-string runtime helpers) goes
1920
+ /// through this, so the wrapping logic stays in one place.
1921
+ fn wrapBytesAsStr(body: &mut Vec<u8>, push_len: impl FnOnce(&mut Vec<u8>)) {
1922
+ let types = withGcTypes(|r| {
1923
+ r.class_type_idx.get("Buffer").copied().zip(r.class_type_idx.get("Str").copied())
1924
+ });
1925
+ // No real `Str`/`Buffer` registered at all (see `strGcValtypeOrFallback`'s
1926
+ // doc comment) — a raw `array<i8>` already IS this program's "Str", so
1927
+ // there's nothing to wrap; the array left on the stack by the caller is
1928
+ // the whole value.
1929
+ let Some((buffer_type_idx, str_type_idx)) = types else { return };
1930
+ push_len(body);
1931
+ Instruction::StructNew(buffer_type_idx).encode(body);
1870
- Instruction::LocalGet(RESULT).encode(&mut body);
1932
+ Instruction::StructNew(str_type_idx).encode(body);
1871
- Instruction::I32Const(0).encode(&mut body);
1872
- Instruction::LocalGet(SRC).encode(&mut body);
1873
- Instruction::LocalGet(START).encode(&mut body);
1874
- Instruction::I32WrapI64.encode(&mut body);
1933
+ }
1875
- Instruction::LocalGet(N).encode(&mut body);
1876
- Instruction::I32WrapI64.encode(&mut body);
1877
- Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
1878
1934
 
1935
+ /// The inverse of `wrapBytesAsStr`: given a `Str` value already on the stack,
1936
+ /// leaves its raw `array<i8>` (`self.data.data`) on the stack instead —
1937
+ /// `struct.get Str 0` (unwrap to the `Buffer`), then `struct.get Buffer 0`
1938
+ /// (unwrap to the `[]Byte`). A no-op in the same "Str isn't really
1939
+ /// registered" fallback case `wrapBytesAsStr` documents — the value already
1879
- Instruction::LocalGet(RESULT).encode(&mut body);
1940
+ /// on the stack IS the raw array in that case.
1880
- Instruction::End.encode(&mut body);
1941
+ fn unwrapStrToBytes(body: &mut Vec<u8>) {
1881
- Some(body)
1942
+ let types = withGcTypes(|r| {
1943
+ r.class_type_idx.get("Buffer").copied().zip(r.class_type_idx.get("Str").copied())
1882
- }
1944
+ });
1883
- _ => None,
1945
+ let Some((buffer_type_idx, str_type_idx)) = types else { return };
1884
- }
1946
+ Instruction::StructGet { struct_type_index: str_type_idx, field_index: 0 }.encode(body);
1947
+ Instruction::StructGet { struct_type_index: buffer_type_idx, field_index: 0 }.encode(body);
1885
1948
  }
1886
1949
 
1950
+
1887
1951
  /// `Array[T]`'s `init`/`get`/`set`/`length` — every specialization (`Array$Int`,
1888
1952
  /// `Array$List$Pair$Str$Int`, ...) shares ONE wasm-gc `array<anyref>` type
1889
1953
  /// (`GcTypeRegistry::array_type_idx`, see `isArraySpecialization`), so these
@@ -4381,8 +4445,12 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4381
4445
  let obj_ty = inferLocalType(&attr.object, ctx);
4382
4446
  match &attr.attr {
4383
4447
  ast::AttrKind::Field(field_name) => {
4448
+ // `Str` is an ordinary class (`data: Buffer`) with no name of
4449
+ // its own carried in `PlumType::TStr` — resolve it via the
4450
+ // literal name "Str", same as `TNamed` does via its own name.
4384
4451
  let class_name = match &obj_ty {
4385
4452
  PlumType::TNamed(n) => n.clone(),
4453
+ PlumType::TStr => "Str".to_string(),
4386
4454
  other => return Err(format!("codegen: cannot access field '{}' on non-class type {}", field_name, other)),
4387
4455
  };
4388
4456
  match ctx.classes.get(&class_name) {
@@ -4603,17 +4671,19 @@ fn compileVariantConstruction(
4603
4671
  }
4604
4672
 
4605
4673
  /// Emits a static (compile-time-known) string literal as a fresh passive data
4606
- /// segment, pushing a `Str` array built from it via `array.new_data`. `Str`'s wasm-gc
4674
+ /// segment, building the raw `array<i8>` via `array.new_data` (no length prefix
4607
- /// representation is a plain `array<i8>` (see Decision 4 of the wasm-gc migration
4608
- /// plan)no length prefix needed, unlike the old bump-allocator layout.
4675
+ /// needed`array.len` reads it back natively), then wrapping it into a real
4676
+ /// `Str` (`wrapBytesAsStr` — see `str.plum`'s header comment on `Str`'s shape).
4609
4677
  fn compileStaticString(text: &str, body: &mut Vec<u8>, state: &mut ModuleState) {
4610
4678
  let bytes = text.as_bytes();
4611
4679
  let data_index = state.passive_segments.len() as u32;
4612
4680
  state.passive_segments.push(bytes.to_vec());
4613
- let str_type_idx = withGcTypes(|r| r.str_type_idx);
4681
+ let byte_array_type_idx = withGcTypes(|r| r.byte_array_type_idx);
4614
4682
  Instruction::I32Const(0).encode(body);
4615
4683
  Instruction::I32Const(bytes.len() as i32).encode(body);
4616
- Instruction::ArrayNewData { array_type_index: str_type_idx, array_data_index: data_index }.encode(body);
4684
+ Instruction::ArrayNewData { array_type_index: byte_array_type_idx, array_data_index: data_index }.encode(body);
4685
+ let len = bytes.len() as i64;
4686
+ wrapBytesAsStr(body, |b| { Instruction::I64Const(len).encode(b); });
4617
4687
  }
4618
4688
 
4619
4689
  /// Lowers a string literal that contains at least one `{expr}` interpolation.
@@ -4644,7 +4714,7 @@ fn compileInterpolatedString(
4644
4714
  Instruction::Call(ctx.int_to_string_func).encode(body);
4645
4715
  }
4646
4716
  PlumType::TBool => {
4647
- let str_ref = withGcTypes(|r| gcRef(r.str_type_idx));
4717
+ let str_ref = withGcTypes(|r| plumTypeToGcValtype(&PlumType::TStr, r));
4648
4718
  compileBoolConditionAsI32(expr, body, ctx, state)?;
4649
4719
  Instruction::If(BlockType::Result(str_ref)).encode(body);
4650
4720
  compileStaticString("True", body, state);
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -1,20 +1,24 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_wasm_codegen::compileSource;
3
- use plum_core::AstParser;
4
3
 
5
4
  fn examplesDir() -> std::path::PathBuf {
6
5
  std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples")
7
6
  }
8
7
 
8
+ fn libPath() -> std::path::PathBuf {
9
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../libs")
10
+ }
11
+
12
+ /// Loads `name` via the REAL compilation path (`loadAndMerge`, resolving its
13
+ /// own `import`s against `../libs` and merging in `Str`'s always-implicit
14
+ /// `std/str` prelude — see `plum-core::loader::loadAndMerge`'s doc comment)
15
+ /// rather than parsing it in isolation — several example files now genuinely
16
+ /// `import` real stdlib types (e.g. `std/option`), so a bare single-file
17
+ /// parse would leave those names undeclared.
9
18
  fn parseFile(name: &str) -> plum_core::ast::Source {
10
19
  let path = examplesDir().join(name);
20
+ plum_core::loadAndMerge(&path, &libPath())
11
- let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e));
21
+ .unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e))
12
- let mut parser = tree_sitter::Parser::new();
13
- parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
14
- let tree = parser.parse(&src, None).unwrap_or_else(|| panic!("failed to parse {}", path.display()));
15
- assert!(!tree.root_node().has_error(), "{} has a parse error", path.display());
16
- let ap = AstParser::new(&src);
17
- ap.parseSource(tree.root_node())
18
22
  }
19
23
 
20
24
  /// See the identically-named helper in codegen_tests.rs for why both features
@@ -114,11 +118,23 @@ fn ioExampleCompilesAndRunsCorrectly() {
114
118
  .func()
115
119
  .cloned()
116
120
  .expect("plum::printLn import should be a function");
121
+ // `Str` is a real two-level struct now — `Str{data: Buffer{data: []Byte,
122
+ // len: Int}}` (see `libs/std/str.plum`'s header comment) — not a raw
123
+ // `array<i8>` directly, so unwrap through both `struct.get`s before
124
+ // reading the actual bytes (mirrors `plum-cli`'s `unwrapStrToArray`).
117
125
  let print_ln = wasmtime::Func::new(&mut store, import_ty, move |mut caller, params, _results| {
118
126
  let wasmtime::Val::AnyRef(Some(s)) = &params[0] else {
119
127
  return Err(wasmtime::Error::msg("printLn expects a Str argument"));
120
128
  };
129
+ let str_struct = s.unwrap_struct(&caller)?;
130
+ let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut caller, 0)? else {
131
+ return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
132
+ };
133
+ let buffer_struct = buffer_ref.unwrap_struct(&caller)?;
134
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut caller, 0)? else {
135
+ return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference"));
136
+ };
121
- let arr = s.unwrap_array(&caller)?;
137
+ let arr = bytes_ref.unwrap_array(&caller)?;
122
138
  let len = arr.len(&caller)?;
123
139
  let mut buf = vec![0u8; len as usize];
124
140
  arr.copy_to_i8_slice(&mut caller, &mut buf)?;
@@ -126,7 +142,29 @@ fn ioExampleCompilesAndRunsCorrectly() {
126
142
  Ok(())
127
143
  });
128
144
 
145
+ // `io.plum` now also transitively pulls in `rawRandomInt` (via the
146
+ // always-implicit `std/str` prelude — `str` -> `list` -> `int`, which
147
+ // declares it for `List.sample`/`shuffle` — see
148
+ // `plum-core::loader::loadAndMerge`), even though this test never
149
+ // actually calls anything that uses it. Wire up a harmless stub so the
150
+ // module can still instantiate; every import must be satisfied in
151
+ // declaration order.
152
+ let externs: Vec<wasmtime::Extern> = module
153
+ .imports()
154
+ .map(|imp| match (imp.module(), imp.name()) {
155
+ ("plum", "printLn") => wasmtime::Extern::Func(print_ln),
156
+ ("plum", "rawRandomInt") => {
157
+ let func_ty = imp.ty().func().cloned().expect("rawRandomInt import should be a function");
158
+ wasmtime::Extern::Func(wasmtime::Func::new(&mut store, func_ty, |_caller, _params, results| {
159
+ results[0] = wasmtime::Val::I64(0);
160
+ Ok(())
161
+ }))
162
+ }
163
+ (m, n) => panic!("io.plum test needs a stub for unexpected import {}::{}", m, n),
164
+ })
165
+ .collect();
166
+
129
- let instance = wasmtime::Instance::new(&mut store, &module, &[wasmtime::Extern::Func(print_ln)])
167
+ let instance = wasmtime::Instance::new(&mut store, &module, &externs)
130
168
  .expect("module should instantiate");
131
169
  let main = instance
132
170
  .get_typed_func::<(), ()>(&mut store, "main")