plum
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 +1 -1
- plum-cli/src/main.rs +7 -38
- plum-runtime/src/main.rs +14 -26
- plum-std/Buffer.plum +12 -0
- plum-std/ByteSlice.plum +6 -8
- plum-std/Str.plum +15 -26
- plum-wasm-codegen/src/lib.rs +24 -71
- plum-wasm-codegen/tests/codegen_tests.rs +60 -5
- plum-wasm-codegen/tests/examples_test.rs +4 -12
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(...) =
|
|
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
|
-
///
|
|
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(
|
|
667
|
+
let wasmtime::Val::AnyRef(Some(bytes_ref)) = str_struct.field(&mut store, 0)? else {
|
|
669
|
-
return Err(wasmtime::Error::msg("Str.
|
|
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 `
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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("
|
|
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 `
|
|
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 =
|
|
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)) =>
|
|
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
|
|
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
|
|
346
|
+
let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
|
|
356
|
-
let
|
|
347
|
+
let str_val = wasmtime::StructRef::new(
|
|
357
348
|
&mut caller,
|
|
358
|
-
&
|
|
349
|
+
&str_pre,
|
|
359
|
-
&[wasmtime::Val::AnyRef(Some(arr.to_anyref()))
|
|
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`
|
|
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 `
|
|
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
|
|
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.
|
|
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 `
|
|
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(
|
|
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
|
|
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`)
|
|
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
|
-
# `
|
|
12
|
+
# compiler special case. Only `byteToStr` and the string-literal/concat/
|
|
13
|
+
# equality/int-to-string runtime helpers know that shape directly (see
|
|
18
|
-
#
|
|
14
|
+
# `plum-wasm-codegen`'s `wrapBytesAsStr`/`unwrapStrToBytes`).
|
|
19
|
-
# through to `Buffer`.
|
|
20
15
|
#
|
|
21
|
-
# `Str` values are never
|
|
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
|
-
#
|
|
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(
|
|
18
|
+
record Str(bytes: []Byte) =
|
|
28
|
-
|
|
19
|
+
|
|
29
|
-
# Number of bytes in the string
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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 `{
|
|
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
|
|
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
|
|
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
|
-
///
|
|
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`
|
|
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
|
|
3077
|
+
/// through this.
|
|
3097
|
-
fn wrapBytesAsStr(body: &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`
|
|
3079
|
+
// No real `Str` registered at all (see `strGcValtypeOrFallback`'s doc
|
|
3105
|
-
//
|
|
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(
|
|
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
|
|
3087
|
+
/// The inverse of `wrapBytesAsStr`: given a `Str` value on the stack, leaves
|
|
3118
|
-
///
|
|
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
|
-
///
|
|
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
|
-
|
|
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
|
|
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
|
|
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)) = ¶ms[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)) =
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
137
|
+
let wasmtime::Val::AnyRef(Some(bytes_ref)) = str_struct.field(&mut caller, 0)? else {
|
|
139
|
-
return Err(wasmtime::Error::msg("Str.
|
|
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)?;
|