plum

#treesitter#compiler#wasm

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

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


35ca120 — Peter John 2026-11-01T22:18:05+05:30
refactor: Str + and == are ordinary Plum methods; concat/eq wasm helpers removed
plum-checker/src/lib.rs CHANGED
@@ -3498,7 +3498,8 @@ fn startVariant(
3498
3498
  /// True for an operand whose operators are user methods (`add`, `eq`, ...): a
3499
3499
  /// declared class/enum, not a primitive, `Str`, `Bool` or an error name.
3500
3500
  pub fn isOperatorOperand(t: &PlumType) -> bool {
3501
+ matches!(t, PlumType::TStr)
3501
- matches!(t, PlumType::TNamed(n) | PlumType::TApp(n, _) if n != "Str" && n != "Int" && n != "Float" && n != "Bool" && n != "Byte")
3502
+ || matches!(t, PlumType::TNamed(n) | PlumType::TApp(n, _) if n != "Int" && n != "Float" && n != "Bool" && n != "Byte")
3502
3503
  }
3503
3504
 
3504
3505
  /// Resolves operator `sym` on a non-primitive left operand to `left.method(right)`,
plum-std/Str.plum CHANGED
@@ -137,6 +137,34 @@ record Str(bytes: []Byte) =
137
137
  fun concat(self, other: Str) -> Str =
138
138
  self + other
139
139
 
140
+ # `a + b`: one exact-length allocation and two bulk copies. Returns an
141
+ # operand unchanged when the other is empty (Strs are immutable).
142
+ fun add(self, o: Str) -> Str =
143
+ n := self.length()
144
+ m := o.length()
145
+ if m == 0
146
+ return self
147
+ if n == 0
148
+ return o
149
+ out := makeBytes(n + m)
150
+ copyBytes(out, 0, self.bytes, 0, n)
151
+ copyBytes(out, n, o.bytes, 0, m)
152
+ return Str(bytes: out)
153
+
154
+ # `a == b` / `a != b`: CONTENT equality (never reference equality).
155
+ fun eq(self, o: Str) -> Bool =
156
+ n := self.length()
157
+ if n != o.length()
158
+ return false
159
+ a := self.bytes
160
+ b := o.bytes
161
+ i := 0
162
+ while i < n
163
+ if a.get(i) != b.get(i)
164
+ return false
165
+ i = i + 1
166
+ return true
167
+
140
168
  fun toStr(self) -> Str =
141
169
  self
142
170
 
plum-wasm-codegen/src/lib.rs CHANGED
@@ -406,11 +406,9 @@ pub struct CompileCtx<'a> {
406
406
  /// Shared runtime helper `(a: ref Str, b: ref Str) -> ref Str`: allocates a new
407
407
  /// `array<i8>` exactly long enough to hold `a`'s bytes followed by `b`'s, for
408
408
  /// lowering string interpolation (`"{expr}"`).
409
- pub string_concat_func: u32,
410
409
  /// Shared runtime helper `(a: ref Str, b: ref Str) -> i32`: byte-for-byte
411
410
  /// content equality (1/0) — see the comment on its registration site for
412
411
  /// why `==`/`!=` on `Str` needs this instead of plain `ref.eq`.
413
- pub string_eq_func: u32,
414
412
  /// Shared runtime helper `(n: i64) -> ref Str`: allocates a new `array<i8>`
415
413
  /// holding `n`'s decimal representation, for interpolating an `Int`.
416
414
  /// wasm-gc type-section indices for this program's enums/Str/closures —
@@ -459,8 +457,6 @@ struct LocalCtx<'a> {
459
457
  closures: &'a HashMap<(usize, usize), ClosureInfo>,
460
458
  closure_call_types: &'a HashMap<ClosureSigKey, u32>,
461
459
  named_fn_values: &'a HashMap<(String, PlumType), u32>,
462
- string_concat_func: u32,
463
- string_eq_func: u32,
464
460
  methods: &'a MethodEnv,
465
461
  enum_variants: &'a EnumVariants,
466
462
  enum_params: &'a EnumParams,
@@ -2489,8 +2485,6 @@ fn compileSourceInner(
2489
2485
  let start_slot = (start_func_idx - module.func_import_count) as usize;
2490
2486
  module.functions[start_slot].1 = final_start_body;
2491
2487
 
2492
- let string_concat_func = registerStringConcatHelper(&mut module, gc_types.byte_array_type_idx);
2493
- let string_eq_func = registerStringEqHelper(&mut module, gc_types.byte_array_type_idx);
2494
2488
 
2495
2489
  let ctx = CompileCtx {
2496
2490
  func_ids,
@@ -2510,8 +2504,6 @@ fn compileSourceInner(
2510
2504
  closure_asts,
2511
2505
  closure_call_types,
2512
2506
  named_fn_values,
2513
- string_concat_func,
2514
- string_eq_func,
2515
2507
  gc_types,
2516
2508
  singleton_globals,
2517
2509
  };
@@ -2637,187 +2629,6 @@ fn getClosureCallType(module: &mut WasmModule, m: &mut HashMap<ClosureSigKey, u3
2637
2629
  t
2638
2630
  }
2639
2631
 
2640
- /// Registers `__string_concat(a: ref Str, b: ref Str) -> ref Str`, a hand-written
2641
- /// runtime helper backing string interpolation. Unwraps both `Str` params down
2642
- /// to their raw `array<i8>`s (`unwrapStrToBytes`) — which each track their own
2643
- /// length via `array.len` — allocates a new array sized `len(a) + len(b)`,
2644
- /// does two `array.copy`s (a whole-array-in-one-instruction bulk copy), then
2645
- /// wraps the result back into a real `Str` (`wrapBytesAsStr`).
2646
- fn registerStringConcatHelper(module: &mut WasmModule, byte_array_type_idx: u32) -> u32 {
2647
- let byte_array_ref = gcRef(byte_array_type_idx);
2648
- let str_ref = withGcTypes(strGcValtypeOrFallback);
2649
- let type_idx = module.addType(&[str_ref, str_ref], &[str_ref]);
2650
-
2651
- // locals: 0=a (param), 1=b (param), 2=arr_a, 3=arr_b, 4=len_a, 5=len_b,
2652
- // 6=total_len, 7=result
2653
- const A: u32 = 0;
2654
- const B: u32 = 1;
2655
- const ARR_A: u32 = 2;
2656
- const ARR_B: u32 = 3;
2657
- const LEN_A: u32 = 4;
2658
- const LEN_B: u32 = 5;
2659
- const TOTAL_LEN: u32 = 6;
2660
- const RESULT: u32 = 7;
2661
-
2662
- let mut body = Vec::new();
2663
- body.extend(encodeLeb128U32(3)); // three locals groups
2664
- body.extend(encodeLeb128U32(2)); // arr_a, arr_b: ref
2665
- byte_array_ref.encode(&mut body);
2666
- body.extend(encodeLeb128U32(3)); // len_a, len_b, total_len: i32
2667
- ValType::I32.encode(&mut body);
2668
- body.extend(encodeLeb128U32(1)); // result: ref
2669
- byte_array_ref.encode(&mut body);
2670
-
2671
- // arr_a = a.bytes; arr_b = b.bytes
2672
- Instruction::LocalGet(A).encode(&mut body);
2673
- unwrapStrToBytes(&mut body);
2674
- Instruction::LocalSet(ARR_A).encode(&mut body);
2675
- Instruction::LocalGet(B).encode(&mut body);
2676
- unwrapStrToBytes(&mut body);
2677
- Instruction::LocalSet(ARR_B).encode(&mut body);
2678
-
2679
- // len_a = array.len(arr_a); len_b = array.len(arr_b)
2680
- Instruction::LocalGet(ARR_A).encode(&mut body);
2681
- Instruction::ArrayLen.encode(&mut body);
2682
- Instruction::LocalSet(LEN_A).encode(&mut body);
2683
- Instruction::LocalGet(ARR_B).encode(&mut body);
2684
- Instruction::ArrayLen.encode(&mut body);
2685
- Instruction::LocalSet(LEN_B).encode(&mut body);
2686
-
2687
- // total_len = len_a + len_b; result = array.new_default(byte_array_type_idx, total_len)
2688
- Instruction::LocalGet(LEN_A).encode(&mut body);
2689
- Instruction::LocalGet(LEN_B).encode(&mut body);
2690
- Instruction::I32Add.encode(&mut body);
2691
- Instruction::LocalSet(TOTAL_LEN).encode(&mut body);
2692
- Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
2693
- Instruction::ArrayNewDefault(byte_array_type_idx).encode(&mut body);
2694
- Instruction::LocalSet(RESULT).encode(&mut body);
2695
-
2696
- // array.copy(dst: result, dst_offset: 0, src: arr_a, src_offset: 0, len: len_a)
2697
- Instruction::LocalGet(RESULT).encode(&mut body);
2698
- Instruction::I32Const(0).encode(&mut body);
2699
- Instruction::LocalGet(ARR_A).encode(&mut body);
2700
- Instruction::I32Const(0).encode(&mut body);
2701
- Instruction::LocalGet(LEN_A).encode(&mut body);
2702
- Instruction::ArrayCopy {
2703
- array_type_index_dst: byte_array_type_idx,
2704
- array_type_index_src: byte_array_type_idx,
2705
- }
2706
- .encode(&mut body);
2707
-
2708
- // array.copy(dst: result, dst_offset: len_a, src: arr_b, src_offset: 0, len: len_b)
2709
- Instruction::LocalGet(RESULT).encode(&mut body);
2710
- Instruction::LocalGet(LEN_A).encode(&mut body);
2711
- Instruction::LocalGet(ARR_B).encode(&mut body);
2712
- Instruction::I32Const(0).encode(&mut body);
2713
- Instruction::LocalGet(LEN_B).encode(&mut body);
2714
- Instruction::ArrayCopy {
2715
- array_type_index_dst: byte_array_type_idx,
2716
- array_type_index_src: byte_array_type_idx,
2717
- }
2718
- .encode(&mut body);
2719
-
2720
- // wrap result (the raw array) back into a real Str
2721
- Instruction::LocalGet(RESULT).encode(&mut body);
2722
- wrapBytesAsStr(&mut body);
2723
- Instruction::End.encode(&mut body);
2724
-
2725
- module.addFunction(type_idx, &body)
2726
- }
2727
-
2728
- /// Registers `__string_eq(a: ref Str, b: ref Str) -> i32` (1/0), a hand-written
2729
- /// runtime helper backing `Str`'s `==`/`!=` operator. `Str` is a wasm-gc `ref` —
2730
- /// the general `Compare` codegen's default for reference-typed operands is
2731
- /// `ref.eq` (identity), correct for a payload-free singleton (`End`) or
2732
- /// class-instance identity, but WRONG for `Str`: two independently-heap-allocated
2733
- /// `array<i8>` values holding identical bytes (e.g. two `"a"` literals, or one
2734
- /// literal and a runtime-built string) are different objects and would compare
2735
- /// unequal — silently breaking the extremely common `someStr == "literal"`
2736
- /// pattern (e.g. `Map`'s own `Str`-keyed `get`/`set` comparing keys). This does
2737
- /// a real byte-for-byte comparison instead: same length, then every byte equal.
2738
- fn registerStringEqHelper(module: &mut WasmModule, byte_array_type_idx: u32) -> u32 {
2739
- let byte_array_ref = gcRef(byte_array_type_idx);
2740
- let str_ref = withGcTypes(strGcValtypeOrFallback);
2741
- let type_idx = module.addType(&[str_ref, str_ref], &[ValType::I32]);
2742
-
2743
- // locals: 0=a (param), 1=b (param), 2=arr_a, 3=arr_b, 4=len_a, 5=len_b, 6=i
2744
- const A: u32 = 0;
2745
- const B: u32 = 1;
2746
- const ARR_A: u32 = 2;
2747
- const ARR_B: u32 = 3;
2748
- const LEN_A: u32 = 4;
2749
- const LEN_B: u32 = 5;
2750
- const I: u32 = 6;
2751
-
2752
- let mut body = Vec::new();
2753
- body.extend(encodeLeb128U32(2)); // two locals groups
2754
- body.extend(encodeLeb128U32(2)); // arr_a, arr_b: ref
2755
- byte_array_ref.encode(&mut body);
2756
- body.extend(encodeLeb128U32(3)); // len_a, len_b, i: i32
2757
- ValType::I32.encode(&mut body);
2758
-
2759
- // arr_a = a.bytes; arr_b = b.bytes
2760
- Instruction::LocalGet(A).encode(&mut body);
2761
- unwrapStrToBytes(&mut body);
2762
- Instruction::LocalSet(ARR_A).encode(&mut body);
2763
- Instruction::LocalGet(B).encode(&mut body);
2764
- unwrapStrToBytes(&mut body);
2765
- Instruction::LocalSet(ARR_B).encode(&mut body);
2766
-
2767
- Instruction::LocalGet(ARR_A).encode(&mut body);
2768
- Instruction::ArrayLen.encode(&mut body);
2769
- Instruction::LocalSet(LEN_A).encode(&mut body);
2770
- Instruction::LocalGet(ARR_B).encode(&mut body);
2771
- Instruction::ArrayLen.encode(&mut body);
2772
- Instruction::LocalSet(LEN_B).encode(&mut body);
2773
-
2774
- // if len_a != len_b: return 0
2775
- Instruction::LocalGet(LEN_A).encode(&mut body);
2776
- Instruction::LocalGet(LEN_B).encode(&mut body);
2777
- Instruction::I32Ne.encode(&mut body);
2778
- Instruction::If(BlockType::Empty).encode(&mut body);
2779
- Instruction::I32Const(0).encode(&mut body);
2780
- Instruction::Return.encode(&mut body);
2781
- Instruction::End.encode(&mut body);
2782
-
2783
- // i = 0
2784
- Instruction::I32Const(0).encode(&mut body);
2785
- Instruction::LocalSet(I).encode(&mut body);
2786
-
2787
- // block { loop { if i >= len_a: br 1 (exit loop); if a[i] != b[i]: return 0; i++; br 0 } }
2788
- Instruction::Block(BlockType::Empty).encode(&mut body);
2789
- Instruction::Loop(BlockType::Empty).encode(&mut body);
2790
- Instruction::LocalGet(I).encode(&mut body);
2791
- Instruction::LocalGet(LEN_A).encode(&mut body);
2792
- Instruction::I32GeU.encode(&mut body);
2793
- Instruction::BrIf(1).encode(&mut body);
2794
-
2795
- Instruction::LocalGet(ARR_A).encode(&mut body);
2796
- Instruction::LocalGet(I).encode(&mut body);
2797
- Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
2798
- Instruction::LocalGet(ARR_B).encode(&mut body);
2799
- Instruction::LocalGet(I).encode(&mut body);
2800
- Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
2801
- Instruction::I32Ne.encode(&mut body);
2802
- Instruction::If(BlockType::Empty).encode(&mut body);
2803
- Instruction::I32Const(0).encode(&mut body);
2804
- Instruction::Return.encode(&mut body);
2805
- Instruction::End.encode(&mut body);
2806
-
2807
- Instruction::LocalGet(I).encode(&mut body);
2808
- Instruction::I32Const(1).encode(&mut body);
2809
- Instruction::I32Add.encode(&mut body);
2810
- Instruction::LocalSet(I).encode(&mut body);
2811
- Instruction::Br(0).encode(&mut body);
2812
- Instruction::End.encode(&mut body); // end loop
2813
- Instruction::End.encode(&mut body); // end block
2814
-
2815
- Instruction::I32Const(1).encode(&mut body);
2816
- Instruction::End.encode(&mut body); // end function
2817
-
2818
- module.addFunction(type_idx, &body)
2819
- }
2820
-
2821
2632
  /// Hand-written bodies for a handful of `libs/std/str.plum` primitives that can't
2822
2633
  /// be expressed in Plum source at all (byte-level array access, building a new
2823
2634
  /// one-element array) — these three are the whole reason every other `Str` method
@@ -2904,11 +2715,8 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2904
2715
  /// never loaded `str.plum` at all (see `plum-core::loader::loadAndMerge`'s
2905
2716
  /// best-effort `std/str` prelude), falling back to the raw `array<i8>` type
2906
2717
  /// instead of panicking. Safe specifically because this is only used by the
2907
- /// three string runtime helpers (`registerStringConcatHelper`/
2908
- /// `registerStringEqHelper`), which are
2718
+ /// unconditionally registered string runtime helpers, which every compiled
2909
- /// registered UNCONDITIONALLY in every compiled program regardless of
2910
- /// whether it actually uses `Str` — a `Str`-less program (which, by
2911
- /// definition, never calls them) doesn't care what type they claim.
2719
+ /// program gets regardless of whether it uses `Str`.
2912
2720
  fn strGcValtypeOrFallback(r: &GcTypeRegistry) -> ValType {
2913
2721
  match r.class_type_idx.get("Str") {
2914
2722
  Some(idx) => gcRef(*idx),
@@ -2929,20 +2737,6 @@ fn wrapBytesAsStr(body: &mut Vec<u8>) {
2929
2737
  Instruction::Call(mk).encode(body);
2930
2738
  }
2931
2739
 
2932
- /// The inverse of `wrapBytesAsStr`: given a `Str` value on the stack, leaves
2933
- /// its raw `array<i8>` (`self.bytes`) there instead: one `struct.get Str 0`.
2934
- /// A no-op when `Str` isn't registered (the value already IS the array).
2935
- fn unwrapStrToBytes(body: &mut Vec<u8>) {
2936
- let Some(str_type_idx) = withGcTypes(|r| r.class_type_idx.get("Str").copied()) else {
2937
- return;
2938
- };
2939
- Instruction::StructGet {
2940
- struct_type_index: str_type_idx,
2941
- field_index: 0,
2942
- }
2943
- .encode(body);
2944
- }
2945
-
2946
2740
  /// `Array[T]`'s `init`/`get`/`set`/`length` — the intrinsic is backed by ONE wasm-gc `array<anyref>` type
2947
2741
  /// (`GcTypeRegistry::array_type_idx`, see `isArraySpecialization`), so these
2948
2742
  /// compile identically regardless of the concrete `T`.
@@ -4111,8 +3905,6 @@ fn compileFnBodyImpl(
4111
3905
  closures: &ctx.closures,
4112
3906
  closure_call_types: &ctx.closure_call_types,
4113
3907
  named_fn_values: &ctx.named_fn_values,
4114
- string_concat_func: ctx.string_concat_func,
4115
- string_eq_func: ctx.string_eq_func,
4116
3908
  methods: &ctx.methods,
4117
3909
  enum_variants: &ctx.enum_variants,
4118
3910
  enum_params: &ctx.enum_params,
@@ -6360,15 +6152,9 @@ fn compileExprCore(
6360
6152
  ast::Expr::Binary(b) => {
6361
6153
  let left_ty = inferLocalType(&b.left, ctx);
6362
6154
  let is_float = matches!(left_ty, PlumType::TFloat);
6363
- let is_str = matches!(left_ty, PlumType::TStr);
6364
6155
  compileExpr(&b.left, body, ctx, state)?;
6365
6156
  compileExpr(&b.right, body, ctx, state)?;
6366
6157
  match b.op {
6367
- // `Str + Str` (e.g. `libs/std/str.plum`'s `concat`) allocates a new
6368
- // array holding both operands' bytes via the same runtime helper
6369
- // string interpolation uses — there's no native wasm "add" for a
6370
- // GC ref.
6371
- ast::BinOp::Add if is_str => Instruction::Call(ctx.string_concat_func).encode(body),
6372
6158
  ast::BinOp::Add => if is_float {
6373
6159
  Instruction::F64Add
6374
6160
  } else {
@@ -6482,25 +6268,6 @@ fn compileExprCore(
6482
6268
  }
6483
6269
  .encode(body);
6484
6270
  }
6485
- // `Str`'s `==`/`!=` needs real byte-for-byte comparison, not
6486
- // reference identity — see `registerStringEqHelper`'s doc
6487
- // comment for why plain `ref.eq` silently breaks the (very
6488
- // common) `someStr == "literal"` pattern.
6489
- PlumType::TStr => match &c.op {
6490
- ast::CmpOp::Eq => {
6491
- Instruction::Call(ctx.string_eq_func).encode(body);
6492
- }
6493
- ast::CmpOp::Neq | ast::CmpOp::NotEq2 => {
6494
- Instruction::Call(ctx.string_eq_func).encode(body);
6495
- Instruction::I32Eqz.encode(body);
6496
- }
6497
- other => {
6498
- return Err(format!(
6499
- "codegen: '{:?}' is not supported between Str values",
6500
- other
6501
- ))
6502
- }
6503
- },
6504
6271
  PlumType::TBool => match &c.op {
6505
6272
  ast::CmpOp::Eq => Instruction::I32Eq.encode(body),
6506
6273
  ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I32Ne.encode(body),
@@ -7306,7 +7073,7 @@ fn compileStaticString(text: &str, body: &mut Vec<u8>, state: &mut ModuleState)
7306
7073
  /// `compileStaticString`; `Str`/`Int`/`Bool`/`Float` interpolated values
7307
7074
  /// converted at runtime — `Float` via `Float.toStr`, see the `TFloat` arm
7308
7075
  /// below), then all parts are left-folded together with the
7309
- /// `__string_concat` runtime helper.
7076
+ /// `Str.add` method (chained, left to right).
7310
7077
  fn compileInterpolatedString(
7311
7078
  s: &ast::StringExpr,
7312
7079
  body: &mut Vec<u8>,
@@ -7375,7 +7142,10 @@ fn compileInterpolatedString(
7375
7142
  }
7376
7143
  }
7377
7144
  if !first {
7145
+ let add = *ctx.func_ids.get("Str::add").ok_or_else(|| {
7146
+ "codegen: string interpolation requires `Str.add` (import std/Str)".to_string()
7147
+ })?;
7378
- Instruction::Call(ctx.string_concat_func).encode(body);
7148
+ Instruction::Call(add).encode(body);
7379
7149
  }
7380
7150
  first = false;
7381
7151
  }
@@ -7619,8 +7389,6 @@ fn compileClosureBody(
7619
7389
  closures: &ctx.closures,
7620
7390
  closure_call_types: &ctx.closure_call_types,
7621
7391
  named_fn_values: &ctx.named_fn_values,
7622
- string_concat_func: ctx.string_concat_func,
7623
- string_eq_func: ctx.string_eq_func,
7624
7392
  methods: &ctx.methods,
7625
7393
  enum_variants: &ctx.enum_variants,
7626
7394
  enum_params: &ctx.enum_params,
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -3029,7 +3029,8 @@ fun main() -> Int =
3029
3029
  a + b + c + {if d == \"none\" then 4 else 0} + f
3030
3030
  ";
3031
3031
  // a = 2, b = 10, c = 7, d = 4, e = 3
3032
+ // Str `==` is the ordinary `Str.eq` method, so Str.plum must really load.
3032
- assert_eq!(runInt(&format!("{}{}", STD_HEAD, src)), 26);
3033
+ assert_eq!(runWithCrash("error_slots", &format!("{}{}", STD_HEAD, src)).0, Ok(26));
3033
3034
  }
3034
3035
 
3035
3036
  // ---- associated consts, statics and `primitive` ----