plum

#treesitter#compiler#wasm

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

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


142a4b5 — Peter John 2026-11-01T21:48:09+05:30
perf: Str wraps an exact-length []Byte directly (was Str{Buffer{arr,len}})
plum-checker/src/lib.rs CHANGED
@@ -768,7 +768,7 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
768
768
  (PlumType::TFloat, PlumType::TFloat) => Ok(()),
769
769
  (PlumType::TBool, PlumType::TBool) => Ok(()),
770
770
  (PlumType::TStr, PlumType::TStr) => Ok(()),
771
- // `Str` is an ordinary class (`type Str(...) = data: Buffer ...` in
771
+ // `Str` is an ordinary class (`type Str(...) = bytes: []Byte ...` in
772
772
  // `str.plum`) — a bare `Str(...)` construction (e.g. `bytesToStr`'s own
773
773
  // body) infers as `TNamed("Str")` via `inferClassCallRaw`, while a `Str`
774
774
  // type ANNOTATION (a param/return type, a string literal) resolves via
plum-cli/src/main.rs CHANGED
@@ -655,8 +655,7 @@ fn randomI64() -> i64 {
655
655
  (z ^ (z >> 31)) as i64
656
656
  }
657
657
 
658
- /// `Str` is now a real two-level wasm-gc struct — `Str{data: Buffer{data:
659
- /// []Byte, len: Int}}` (see `libs/std/str.plum`'s header comment on `Str`'s
658
+ /// `Str` is now a one-field wasm-gc struct — `Str{bytes: []Byte}` (see `libs/std/str.plum`'s header comment on `Str`'s
660
659
  /// shape) — not a raw `array<i8>` directly. Unwraps a `Str` `AnyRef` down to
661
660
  /// that raw array, for host-side code (`printLn`, `readStrArg`,
662
661
  /// `readStrResult`) that needs the actual bytes.
@@ -665,14 +664,8 @@ fn unwrapStrToArray(
665
664
  s: &wasmtime::Rooted<wasmtime::AnyRef>,
666
665
  ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
667
666
  let str_struct = s.unwrap_struct(&store)?;
668
- let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
667
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = str_struct.field(&mut store, 0)? else {
669
- return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
668
+ return Err(wasmtime::Error::msg("Str.bytes is not a []Byte reference"));
670
- };
671
- let buffer_struct = buffer_ref.unwrap_struct(&store)?;
672
- let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
673
- return Err(wasmtime::Error::msg(
674
- "Buffer.data is not a []Byte reference",
675
- ));
676
669
  };
677
670
  bytes_ref.unwrap_array(&store)
678
671
  }
@@ -699,8 +692,7 @@ fn readStrArg(
699
692
  /// the host-side counterpart to `readStrArg`. Allocates the raw `array<i8>`
700
693
  /// (needs the import's own reported `FuncType` to find the concrete nested
701
694
  /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
702
- /// array type), then wraps it in a fresh `Buffer` struct and a `Str` struct
695
+ /// array type), then wraps it in a fresh `Str` struct, mirroring `str.plum`'s real shape.
703
- /// around that, mirroring `str.plum`'s real shape.
704
696
  fn makeStrResult(
705
697
  mut caller: impl wasmtime::AsContextMut,
706
698
  func_ty: &wasmtime::FuncType,
@@ -718,18 +710,7 @@ fn makeStrResult(
718
710
  .as_concrete_struct()
719
711
  .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
720
712
  .clone();
721
- let buffer_ty = str_ty
722
- .field(0)
723
- .and_then(|f| match f.element_type() {
724
- wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
725
- rt.heap_type().as_concrete_struct().cloned()
726
- }
727
- _ => None,
728
- })
729
- .ok_or_else(|| {
730
- wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type")
731
- })?;
732
- let array_ty = buffer_ty
713
+ let array_ty = str_ty
733
714
  .field(0)
734
715
  .and_then(|f| match f.element_type() {
735
716
  wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
@@ -737,28 +718,16 @@ fn makeStrResult(
737
718
  }
738
719
  _ => None,
739
720
  })
740
- .ok_or_else(|| {
741
- wasmtime::Error::msg("expected Buffer.data to be a concrete []Byte array type")
721
+ .ok_or_else(|| wasmtime::Error::msg("expected Str.bytes to be a concrete []Byte array type"))?;
742
- })?;
743
722
 
744
723
  let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
745
724
  let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
746
725
 
747
- let buffer_pre = wasmtime::StructRefPre::new(&mut caller, buffer_ty);
748
- let buffer = wasmtime::StructRef::new(
749
- &mut caller,
750
- &buffer_pre,
751
- &[
752
- wasmtime::Val::AnyRef(Some(arr.to_anyref())),
753
- wasmtime::Val::I64(bytes.len() as i64),
754
- ],
755
- )?;
756
-
757
726
  let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
758
727
  let str_val = wasmtime::StructRef::new(
759
728
  &mut caller,
760
729
  &str_pre,
761
- &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))],
730
+ &[wasmtime::Val::AnyRef(Some(arr.to_anyref()))],
762
731
  )?;
763
732
  Ok(str_val.to_anyref())
764
733
  }
plum-runtime/src/main.rs CHANGED
@@ -289,20 +289,15 @@ fn randomI64() -> i64 {
289
289
  /// fresh per `plum build` invocation, not linked against `plum-cli`, so the
290
290
  /// logic is duplicated rather than shared.
291
291
  ///
292
- /// `Str` is a real two-level wasm-gc struct — `Str{data: Buffer{data: []Byte,
292
+ /// `Str` is a one-field wasm-gc struct — `Str{bytes: []Byte}` — not a raw
293
- /// len: Int}}` (see `libs/std/str.plum`'s header comment) — not a raw
294
293
  /// `array<i8>` directly. Unwraps a `Str` `AnyRef` down to that raw array.
295
294
  fn unwrapStrToArray(
296
295
  mut store: impl wasmtime::AsContextMut,
297
296
  s: &wasmtime::Rooted<wasmtime::AnyRef>,
298
297
  ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
299
298
  let str_struct = s.unwrap_struct(&store)?;
300
- let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
299
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = str_struct.field(&mut store, 0)? else {
301
- return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
302
- };
303
- let buffer_struct = buffer_ref.unwrap_struct(&store)?;
304
- let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
305
- return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference"));
300
+ return Err(wasmtime::Error::msg("Str.bytes is not a []Byte reference"));
306
301
  };
307
302
  bytes_ref.unwrap_array(&store)
308
303
  }
@@ -321,8 +316,7 @@ fn readStrArg(caller: &mut wasmtime::Caller<'_, ()>, val: &wasmtime::Val) -> was
321
316
  /// Builds a new `Str` value from raw bytes — allocates the raw `array<i8>`
322
317
  /// (needs the import's own reported `FuncType` to find the concrete nested
323
318
  /// array type — a generic `ArrayRef` type would be the wrong, unrelated top
324
- /// array type), then wraps it in a fresh `Buffer` struct and a `Str` struct
319
+ /// array type), then wraps it in a fresh `Str` struct, mirroring `str.plum`'s real shape.
325
- /// around that, mirroring `str.plum`'s real shape.
326
320
  fn makeStrResult(
327
321
  mut caller: impl wasmtime::AsContextMut,
328
322
  func_ty: &wasmtime::FuncType,
@@ -336,31 +330,25 @@ fn makeStrResult(
336
330
  let str_ty = ref_ty.heap_type().as_concrete_struct()
337
331
  .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
338
332
  .clone();
339
- let buffer_ty = str_ty.field(0)
340
- .and_then(|f| match f.element_type() {
341
- wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_struct().cloned(),
342
- _ => None,
343
- })
344
- .ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type"))?;
345
- let array_ty = buffer_ty.field(0)
333
+ let array_ty = str_ty
334
+ .field(0)
346
335
  .and_then(|f| match f.element_type() {
347
- wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_array().cloned(),
336
+ wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
337
+ rt.heap_type().as_concrete_array().cloned()
338
+ }
348
339
  _ => None,
349
340
  })
350
- .ok_or_else(|| wasmtime::Error::msg("expected Buffer.data to be a concrete []Byte array type"))?;
341
+ .ok_or_else(|| wasmtime::Error::msg("expected Str.bytes to be a concrete []Byte array type"))?;
351
342
 
352
343
  let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
353
344
  let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
354
345
 
355
- let buffer_pre = wasmtime::StructRefPre::new(&mut caller, buffer_ty);
346
+ let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
356
- let buffer = wasmtime::StructRef::new(
347
+ let str_val = wasmtime::StructRef::new(
357
348
  &mut caller,
358
- &buffer_pre,
349
+ &str_pre,
359
- &[wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)],
350
+ &[wasmtime::Val::AnyRef(Some(arr.to_anyref()))],
360
351
  )?;
361
-
362
- let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
363
- let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?;
364
352
  Ok(str_val.to_anyref())
365
353
  }
366
354
 
plum-std/Buffer.plum CHANGED
@@ -79,3 +79,15 @@ fun exerciseBufferInit() -> Str =
79
79
 
80
80
  test "Buffer() with no args runs init() and works immediately"
81
81
  assert exerciseBufferInit() == "hello world"
82
+
83
+ test "toStr length equals bytes written, even after capacity doubling"
84
+ b := Buffer()
85
+ i := 0
86
+ while i < 100
87
+ b.write("ab")
88
+ i = i + 1
89
+ b.writeByte(Byte(33))
90
+ s := b.toStr()
91
+ assert s.length() == 201
92
+ assert b.length() == 201
93
+ assert s.byteAt(200) == 33
plum-std/ByteSlice.plum CHANGED
@@ -7,9 +7,8 @@ import std/Float
7
7
  # ByteSlice is `[]Byte` — Plum's counterpart to Go's byte slice: a
8
8
  # fixed-length, mutable sequence of raw bytes backed directly by a wasm-gc
9
9
  # `array<i8>` (see `plum-wasm-codegen`'s `PlumType::TByteSlice`) — the same
10
- # raw array type `Buffer`'s own `data` field uses underneath (`Str` itself is
10
+ # raw array type `Buffer`'s own `data` field uses underneath (`Str` is a record wrapping one of these;
11
- # now a real two-level struct wrapping a `Buffer`, not this array directly;
12
- # see `str.plum`'s header comment). Every method here, and `makeBytes`/
11
+ # see `Str.plum`). Every method here, and `makeBytes`/
13
12
  # `copyBytes` below, is a compiler intrinsic (see `compileIntrinsicFnBody`)
14
13
  # — there's no way to express raw array length/indexing/construction/copying
15
14
  # in Plum source itself; `copyStrToBytes`/`bytesToStr` build on top of them
@@ -39,16 +38,15 @@ fun copyBytes(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) ->
39
38
  todo
40
39
 
41
40
  # Like `copyBytes`, but the source is a `Str` — ordinary Plum glue over
42
- # `copyBytes` itself, reaching through `Str`'s real `data: Buffer` field to
41
+ # `copyBytes` itself, reaching through `Str`'s `bytes: []Byte` field.
43
- # its own `data: []Byte` (see `str.plum`'s header comment on `Str`'s shape).
44
42
  fun copyStrToBytes(dst: []Byte, dstStart: Int, src: Str, srcStart: Int, n: Int) -> Unit =
45
- copyBytes(dst, dstStart, src.data.data, srcStart, n)
43
+ copyBytes(dst, dstStart, src.bytes, srcStart, n)
46
44
 
47
45
  # Copies out `n` bytes of `src` starting at `start` into a fresh, independent
48
46
  # `Str` — never aliases `src`, so mutating `src` afterwards can't
49
47
  # retroactively change an already-returned `Str`. Ordinary Plum: allocate a
50
- # right-sized `[]Byte`, copy into it, wrap it in a fresh `Buffer`/`Str` pair.
48
+ # right-sized `[]Byte`, copy into it, wrap it in a fresh `Str`.
51
49
  fun bytesToStr(src: []Byte, start: Int, n: Int) -> Str =
52
50
  data := makeBytes(n)
53
51
  copyBytes(data, 0, src, start, n)
54
- return Str(data: Buffer(data: data, len: n))
52
+ return Str(bytes: data)
plum-std/Str.plum CHANGED
@@ -7,37 +7,26 @@ import std/Float
7
7
  import std/Byte
8
8
  import std/ByteSlice
9
9
 
10
- # A Str is an immutable byte sequence, backed by a real `Buffer` in its
10
+ # A Str is an immutable byte sequence: an ordinary record wrapping one raw
11
- # `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
13
- # `[]Byte`), the same generic struct-of-fields codegen every other class
11
+ # `[]Byte` (`bytes`) whose length is exactly the string's length. Not a
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
12
+ # compiler special case. Only `byteToStr` and the string-literal/concat/
13
+ # equality/int-to-string runtime helpers know that shape directly (see
18
- # else, including `length`/`byteAt` below, is expressed by just calling
14
+ # `plum-wasm-codegen`'s `wrapBytesAsStr`/`unwrapStrToBytes`).
19
- # through to `Buffer`.
20
15
  #
21
- # `Str` values are never actually MUTATED in place — a `Buffer` is mutable,
16
+ # `Str` values are never mutated: nothing may write into a published Str's
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
17
+ # `bytes`. Building a "changed" string always means a fresh array and Str.
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).
27
- record Str(data: Buffer) =
18
+ record Str(bytes: []Byte) =
28
-
19
+
29
- # Number of bytes in the string — `self.data` (a `Buffer`) already tracks
20
+ # Number of bytes in the string (the array's exact length).
30
- # this, so unlike `byteAt`/`byteToStr` this needs no compiler intrinsic of
31
- # its own.
32
21
  fun length(self) -> Int =
33
- self.data.length()
22
+ self.bytes.length()
34
23
 
35
24
  # The raw byte value (0-255) at index `i`. Traps if `i` is out of range —
36
- # `self.data.data` (the `Buffer`'s own `[]Byte`) is a compiler intrinsic
25
+ # `self.bytes` is a compiler intrinsic
37
26
  # (see `libs/std/bytes.plum`'s `ByteSlice.get`), converted from `Byte` to
38
27
  # a plain `Int` since that's this method's established contract.
39
28
  fun byteAt(self, i: Int) -> Int =
40
- self.data.data.get(i).toInt()
29
+ self.bytes.get(i).toInt()
41
30
 
42
31
  # FNV-1a over the raw UTF-8 bytes — used by `Map[K, V]` (see
43
32
  # `map.plum`) to bucket `Str` keys. Not cryptographic, just a well-known,
@@ -279,7 +268,7 @@ record Str(data: Buffer) =
279
268
  en := if e > len then len else e
280
269
  if s >= en
281
270
  return ""
282
- return bytesToStr(self.data.data, s, en - s)
271
+ return bytesToStr(self.bytes, s, en - s)
283
272
 
284
273
  fun toLower(self) -> Str =
285
274
  mapCase(self, false)
@@ -295,7 +284,7 @@ record Str(data: Buffer) =
295
284
  cl := self.codePointByteLength(i)
296
285
  if i + cl > n
297
286
  cl = n - i
298
- copyBytes(out, n - i - cl, self.data.data, i, cl)
287
+ copyBytes(out, n - i - cl, self.bytes, i, cl)
299
288
  i = i + cl
300
289
  return bytesToStr(out, 0, n)
301
290
 
plum-wasm-codegen/src/lib.rs CHANGED
@@ -919,7 +919,7 @@ pub struct GcTypeRegistry {
919
919
  pub known_enums: HashSet<String>,
920
920
  /// The single shared `array<i8>` type index `[]Byte`/`Buffer`'s own
921
921
  /// `data` field use — NOT `Str`'s own representation (`Str` is an
922
- /// ordinary class, `data: Buffer`; see `libs/std/Str.plum`'s header).
922
+ /// ordinary record, `bytes: []Byte`; see `libs/std/Str.plum`'s header).
923
923
  pub byte_array_type_idx: u32,
924
924
  /// The single shared closure-value struct type index — `{table_idx: i32, env:
925
925
  /// anyref}` — every closure (and zero-capture "trampoline") uses regardless of
@@ -937,7 +937,7 @@ pub struct GcTypeRegistry {
937
937
  /// harmlessly never read as such) until/unless the program actually
938
938
  /// instantiates `Array` at least once.
939
939
  pub array_type_idx: u32,
940
- /// Helper `(arr: array<i8>, len: i64) -> Str` building `Str{Buffer{arr,len}}`; `None` when the program has no `Str`/`Buffer`.
940
+ /// Helper `(arr: array<i8>) -> Str` building `Str{arr}`; `None` when the program has no `Str`.
941
941
  pub mk_str_fn: Option<u32>,
942
942
  /// `!T` (keyed by `T`) -> its heap box struct `{leaves of T..., code: i32}`.
943
943
  pub err_box: HashMap<PlumType, u32>,
@@ -2278,20 +2278,12 @@ fn compileSourceInner(
2278
2278
  module.nameFunction(start_func_idx, "start");
2279
2279
  module.setStartFunction(start_func_idx);
2280
2280
 
2281
- if let (Some(&buffer_idx), Some(&str_idx)) = (
2282
- gc_types.class_type_idx.get("Buffer"),
2283
- gc_types.class_type_idx.get("Str"),
2281
+ if let Some(&str_idx) = gc_types.class_type_idx.get("Str") {
2284
- ) {
2285
2282
  let mut b = vec![0u8];
2286
2283
  Instruction::LocalGet(0).encode(&mut b);
2287
- Instruction::LocalGet(1).encode(&mut b);
2288
- Instruction::StructNew(buffer_idx).encode(&mut b);
2289
2284
  Instruction::StructNew(str_idx).encode(&mut b);
2290
2285
  Instruction::End.encode(&mut b);
2291
- let t = module.addType(
2292
- &[gcRef(gc_types.byte_array_type_idx), ValType::I64],
2286
+ let t = module.addType(&[gcRef(gc_types.byte_array_type_idx)], &[gcRef(str_idx)]);
2293
- &[gcRef(str_idx)],
2294
- );
2295
2287
  gc_types.mk_str_fn = Some(module.addFunction(t, &b));
2296
2288
  }
2297
2289
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
@@ -2683,7 +2675,7 @@ fn registerStringConcatHelper(module: &mut WasmModule, byte_array_type_idx: u32)
2683
2675
  body.extend(encodeLeb128U32(1)); // result: ref
2684
2676
  byte_array_ref.encode(&mut body);
2685
2677
 
2686
- // arr_a = a.data.data; arr_b = b.data.data
2678
+ // arr_a = a.bytes; arr_b = b.bytes
2687
2679
  Instruction::LocalGet(A).encode(&mut body);
2688
2680
  unwrapStrToBytes(&mut body);
2689
2681
  Instruction::LocalSet(ARR_A).encode(&mut body);
@@ -2734,10 +2726,7 @@ fn registerStringConcatHelper(module: &mut WasmModule, byte_array_type_idx: u32)
2734
2726
 
2735
2727
  // wrap result (the raw array) back into a real Str
2736
2728
  Instruction::LocalGet(RESULT).encode(&mut body);
2737
- wrapBytesAsStr(&mut body, |b| {
2729
+ wrapBytesAsStr(&mut body);
2738
- Instruction::LocalGet(TOTAL_LEN).encode(b);
2739
- Instruction::I64ExtendI32U.encode(b);
2740
- });
2741
2730
  Instruction::End.encode(&mut body);
2742
2731
 
2743
2732
  module.addFunction(type_idx, &body)
@@ -2774,7 +2763,7 @@ fn registerStringEqHelper(module: &mut WasmModule, byte_array_type_idx: u32) ->
2774
2763
  body.extend(encodeLeb128U32(3)); // len_a, len_b, i: i32
2775
2764
  ValType::I32.encode(&mut body);
2776
2765
 
2777
- // arr_a = a.data.data; arr_b = b.data.data
2766
+ // arr_a = a.bytes; arr_b = b.bytes
2778
2767
  Instruction::LocalGet(A).encode(&mut body);
2779
2768
  unwrapStrToBytes(&mut body);
2780
2769
  Instruction::LocalSet(ARR_A).encode(&mut body);
@@ -2962,10 +2951,7 @@ fn registerIntToStringHelper(module: &mut WasmModule, byte_array_type_idx: u32)
2962
2951
  Instruction::End.encode(&mut body);
2963
2952
 
2964
2953
  Instruction::LocalGet(RESULT).encode(&mut body);
2965
- wrapBytesAsStr(&mut body, |b| {
2954
+ wrapBytesAsStr(&mut body);
2966
- Instruction::LocalGet(TOTAL_LEN).encode(b);
2967
- Instruction::I64ExtendI32U.encode(b);
2968
- });
2969
2955
  Instruction::End.encode(&mut body);
2970
2956
 
2971
2957
  module.addFunction(type_idx, &body)
@@ -2988,7 +2974,7 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2988
2974
  let byte_array_type_idx = withGcTypes(|r| r.byte_array_type_idx);
2989
2975
  match (f.type_param.as_deref(), f.name.as_str()) {
2990
2976
  // byteToStr(b: Int) -> Str — a new length-1 Str holding `b`'s low 8 bits,
2991
- // wrapped into `Str`'s real `{data: Buffer{data: []Byte, len: Int}}`
2977
+ // wrapped into `Str`'s real `{bytes: []Byte}`
2992
2978
  // shape (see `wrapBytesAsStr`).
2993
2979
  (None, "byteToStr") => {
2994
2980
  let mut body = vec![0u8];
@@ -2999,9 +2985,7 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2999
2985
  array_size: 1,
3000
2986
  }
3001
2987
  .encode(&mut body);
3002
- wrapBytesAsStr(&mut body, |b| {
2988
+ wrapBytesAsStr(&mut body);
3003
- Instruction::I64Const(1).encode(b);
3004
- });
3005
2989
  Instruction::End.encode(&mut body);
3006
2990
  Some(body)
3007
2991
  }
@@ -3087,47 +3071,24 @@ fn strGcValtypeOrFallback(r: &GcTypeRegistry) -> ValType {
3087
3071
  }
3088
3072
  }
3089
3073
 
3090
- /// Wraps a just-pushed raw `array<i8>` (top of the value stack) — together
3074
+ /// Wraps a just-pushed raw `array<i8>` (top of the value stack, whose length
3091
- /// with `push_len`, which must push its length as an `i64` — into `Str`'s
3092
- /// real two-level `{data: Buffer{data: []Byte, len: Int}}` representation:
3075
+ /// IS the string's length) into `Str`'s `{bytes: []Byte}` representation via
3093
- /// `struct.new Buffer(arr, len)`, then `struct.new Str(buffer)`. Every codegen
3094
- /// site that builds a `Str` value directly from raw bytes (`byteToStr`,
3076
+ /// `mk_str_fn`. Every codegen site that builds a `Str` from raw bytes goes
3095
- /// string literals, the string-concat/int-to-string runtime helpers) goes
3096
- /// through this, so the wrapping logic stays in one place.
3077
+ /// through this.
3097
- fn wrapBytesAsStr(body: &mut Vec<u8>, push_len: impl FnOnce(&mut Vec<u8>)) {
3078
+ fn wrapBytesAsStr(body: &mut Vec<u8>) {
3098
- let types = withGcTypes(|r| {
3099
- r.class_type_idx
3100
- .get("Buffer")
3101
- .copied()
3102
- .zip(r.class_type_idx.get("Str").copied())
3103
- });
3104
- // No real `Str`/`Buffer` registered at all (see `strGcValtypeOrFallback`'s
3079
+ // No real `Str` registered at all (see `strGcValtypeOrFallback`'s doc
3105
- // doc comment) — a raw `array<i8>` already IS this program's "Str", so
3080
+ // comment) — a raw `array<i8>` already IS this program's "Str".
3106
- // there's nothing to wrap; the array left on the stack by the caller is
3107
- // the whole value.
3108
- let Some((buffer_type_idx, str_type_idx)) = types else {
3081
+ let Some(mk) = withGcTypes(|r| r.mk_str_fn) else {
3109
3082
  return;
3110
3083
  };
3111
- let _ = (buffer_type_idx, str_type_idx);
3112
- push_len(body);
3113
- let mk = withGcTypes(|r| r.mk_str_fn).expect("mk_str_fn registered with Str/Buffer");
3114
3084
  Instruction::Call(mk).encode(body);
3115
3085
  }
3116
3086
 
3117
- /// The inverse of `wrapBytesAsStr`: given a `Str` value already on the stack,
3087
+ /// The inverse of `wrapBytesAsStr`: given a `Str` value on the stack, leaves
3118
- /// leaves its raw `array<i8>` (`self.data.data`) on the stack instead —
3088
+ /// its raw `array<i8>` (`self.bytes`) there instead: one `struct.get Str 0`.
3119
- /// `struct.get Str 0` (unwrap to the `Buffer`), then `struct.get Buffer 0`
3120
- /// (unwrap to the `[]Byte`). A no-op in the same "Str isn't really
3121
- /// registered" fallback case `wrapBytesAsStr` documents — the value already
3122
- /// on the stack IS the raw array in that case.
3089
+ /// A no-op when `Str` isn't registered (the value already IS the array).
3123
3090
  fn unwrapStrToBytes(body: &mut Vec<u8>) {
3124
- let types = withGcTypes(|r| {
3125
- r.class_type_idx
3126
- .get("Buffer")
3127
- .copied()
3128
- .zip(r.class_type_idx.get("Str").copied())
3091
+ let Some(str_type_idx) = withGcTypes(|r| r.class_type_idx.get("Str").copied()) else {
3129
- });
3130
- let Some((buffer_type_idx, str_type_idx)) = types else {
3131
3092
  return;
3132
3093
  };
3133
3094
  Instruction::StructGet {
@@ -3135,11 +3096,6 @@ fn unwrapStrToBytes(body: &mut Vec<u8>) {
3135
3096
  field_index: 0,
3136
3097
  }
3137
3098
  .encode(body);
3138
- Instruction::StructGet {
3139
- struct_type_index: buffer_type_idx,
3140
- field_index: 0,
3141
- }
3142
- .encode(body);
3143
3099
  }
3144
3100
 
3145
3101
  /// `Array[T]`'s `init`/`get`/`set`/`length` — the intrinsic is backed by ONE wasm-gc `array<anyref>` type
@@ -7139,7 +7095,7 @@ fn compileFieldRead(
7139
7095
  ctx: &LocalCtx,
7140
7096
  state: &mut ModuleState,
7141
7097
  ) -> Result<(), String> {
7142
- // `Str` is an ordinary class (`data: Buffer`) with no name of its own carried
7098
+ // `Str` is an ordinary record (`bytes: []Byte`) with no name of its own carried
7143
7099
  // in `PlumType::TStr` — resolve it via the literal name "Str", same as a
7144
7100
  // `TNamed`/`TApp` does via its own name.
7145
7101
  let class_name = match (plum_checker::nominalName(obj_ty), obj_ty) {
@@ -7498,10 +7454,7 @@ fn compileStaticString(text: &str, body: &mut Vec<u8>, state: &mut ModuleState)
7498
7454
  array_data_index: data_index,
7499
7455
  }
7500
7456
  .encode(body);
7501
- let len = bytes.len() as i64;
7502
- wrapBytesAsStr(body, |b| {
7457
+ wrapBytesAsStr(body);
7503
- Instruction::I64Const(len).encode(b);
7504
- });
7505
7458
  }
7506
7459
 
7507
7460
  /// Lowers a string literal that contains at least one `{expr}` interpolation.
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -2327,14 +2327,10 @@ fn runWithCrash(name: &str, src: &str) -> (Result<i64, String>, Vec<String>) {
2327
2327
  .func_new("plum", "crash", ty, |mut caller, params, _| {
2328
2328
  let wasmtime::Val::AnyRef(Some(s)) = &params[0] else { panic!("Str expected") };
2329
2329
  let st = s.unwrap_struct(&caller).unwrap();
2330
- let wasmtime::Val::AnyRef(Some(buf)) = st.field(&mut caller, 0).unwrap() else { panic!() };
2331
- let bs = buf.unwrap_struct(&caller).unwrap();
2332
- let wasmtime::Val::AnyRef(Some(arr)) = bs.field(&mut caller, 0).unwrap() else { panic!() };
2330
+ let wasmtime::Val::AnyRef(Some(arr)) = st.field(&mut caller, 0).unwrap() else { panic!() };
2333
- let wasmtime::Val::I64(len) = bs.field(&mut caller, 1).unwrap() else { panic!() };
2334
2331
  let arr = arr.unwrap_array(&caller).unwrap();
2335
2332
  let mut b = vec![0u8; arr.len(&caller).unwrap() as usize];
2336
2333
  arr.copy_to_i8_slice(&mut caller, &mut b).unwrap();
2337
- b.truncate(len as usize);
2338
2334
  caller.data_mut().push(String::from_utf8_lossy(&b).into_owned());
2339
2335
  Ok(())
2340
2336
  })
@@ -3134,3 +3130,62 @@ fun main() -> Int =
3134
3130
  ", STD_HEAD));
3135
3131
  assert!(msg.contains("can't take self"), "{msg}");
3136
3132
  }
3133
+
3134
+ /// `Str` is a record with ONE field, the exact-length `[]Byte`: the type
3135
+ /// section holds a struct whose single field is a ref to an `array<i8>`, and
3136
+ /// the only 2-field array-first struct is `Buffer` itself.
3137
+ #[test]
3138
+ fn strIsOneStructWithOneByteArrayField() {
3139
+ let src = "import std/Str\nimport std/Int\nimport std/Float\n\nfun main() -> Int =\n \"hello\".length()\n";
3140
+ let dir = std::env::temp_dir().join(format!("plum_strshape_{}", std::process::id()));
3141
+ std::fs::create_dir_all(&dir).unwrap();
3142
+ let path = dir.join("main.plum");
3143
+ std::fs::write(&path, src).unwrap();
3144
+ let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
3145
+ let bytes = compileSource(&source).expect("compile failed");
3146
+ let _ = std::fs::remove_dir_all(&dir);
3147
+ let mut byte_arrays = std::collections::HashSet::new();
3148
+ let (mut one_field, mut buffer_like) = (0, 0);
3149
+ let mut idx = 0u32;
3150
+ for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
3151
+ if let wasmparser::Payload::TypeSection(s) = payload.unwrap() {
3152
+ for g in s {
3153
+ for t in g.unwrap().types() {
3154
+ match &t.composite_type.inner {
3155
+ wasmparser::CompositeInnerType::Array(a) => {
3156
+ if matches!(a.0.element_type, wasmparser::StorageType::I8) {
3157
+ byte_arrays.insert(idx);
3158
+ }
3159
+ }
3160
+ wasmparser::CompositeInnerType::Struct(st) => {
3161
+ let is_arr = |f: &wasmparser::FieldType| match f.element_type {
3162
+ wasmparser::StorageType::Val(wasmparser::ValType::Ref(r)) => {
3163
+ matches!(r.heap_type(), wasmparser::HeapType::Concrete(u) if u.as_module_index().map_or(false, |i| byte_arrays.contains(&i)))
3164
+ }
3165
+ _ => false,
3166
+ };
3167
+ if st.fields.len() == 1 && is_arr(&st.fields[0]) {
3168
+ one_field += 1;
3169
+ }
3170
+ if st.fields.len() == 2 && is_arr(&st.fields[0]) {
3171
+ buffer_like += 1;
3172
+ }
3173
+ }
3174
+ _ => {}
3175
+ }
3176
+ idx += 1;
3177
+ }
3178
+ }
3179
+ }
3180
+ }
3181
+ assert_eq!(one_field, 1, "expected exactly Str{{bytes}}");
3182
+ // Buffer{data, len} still exists as its own record (Str imports it).
3183
+ assert_eq!(buffer_like, 1);
3184
+ assert_eq!(runWithCrash("strshape", src).0.unwrap(), 5);
3185
+ }
3186
+
3187
+ #[test]
3188
+ fn bufferToStrLengthExactAfterDoubling() {
3189
+ let src = "import std/Str\nimport std/Buffer\nimport std/Int\nimport std/Float\n\nfun main() -> Int =\n b := Buffer()\n i := 0\n while i < 37\n b.write(\"abc\")\n i = i + 1\n b.toStr().length() * 1000 + b.length()\n";
3190
+ assert_eq!(runWithCrash("bufexact", src).0.unwrap(), 111111);
3191
+ }
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -123,9 +123,8 @@ fn ioExampleCompilesAndRunsCorrectly() {
123
123
  .func()
124
124
  .cloned()
125
125
  .expect("plum::printLn import should be a function");
126
- // `Str` is a real two-level struct now — `Str{data: Buffer{data: []Byte,
127
- // len: Int}}` (see `libs/std/str.plum`'s header comment) — not a raw
126
+ // `Str` is a one-field struct now — `Str{bytes: []Byte}` (see `libs/std/str.plum`'s header comment) — not a raw
128
- // `array<i8>` directly, so unwrap through both `struct.get`s before
127
+ // `array<i8>` directly, so unwrap through the `struct.get` before
129
128
  // reading the actual bytes (mirrors `plum-cli`'s `unwrapStrToArray`).
130
129
  let print_ln = wasmtime::Func::new(
131
130
  &mut store,
@@ -135,15 +134,8 @@ fn ioExampleCompilesAndRunsCorrectly() {
135
134
  return Err(wasmtime::Error::msg("printLn expects a Str argument"));
136
135
  };
137
136
  let str_struct = s.unwrap_struct(&caller)?;
138
- let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut caller, 0)? else {
137
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = str_struct.field(&mut caller, 0)? else {
139
- return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
138
+ return Err(wasmtime::Error::msg("Str.bytes is not a []Byte reference"));
140
- };
141
- let buffer_struct = buffer_ref.unwrap_struct(&caller)?;
142
- let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut caller, 0)?
143
- else {
144
- return Err(wasmtime::Error::msg(
145
- "Buffer.data is not a []Byte reference",
146
- ));
147
139
  };
148
140
  let arr = bytes_ref.unwrap_array(&caller)?;
149
141
  let len = arr.len(&caller)?;