plum

#treesitter#compiler#wasm

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

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


d959708 — Peter John 2026-11-01T22:10:50+05:30
refactor: byteToStr in Plum; Int interpolation via Int.toStr; int_to_string wasm helper removed
plum-checker/src/lib.rs CHANGED
@@ -3376,10 +3376,6 @@ fn inferMethodCall(
3376
3376
  _ => Err(format!("a variadic parameter has no method '{}'", name)),
3377
3377
  };
3378
3378
  }
3379
- // `Bool.toStr` is a compiler intrinsic ("true"/"false"), not a source method.
3380
- if matches!(obj_ty, PlumType::TBool) && name == "toStr" && args.is_empty() {
3381
- return Ok(PlumType::TStr);
3382
- }
3383
3379
  // A method on a generic parameter: duck-typed — checked when the enclosing generic body is
3384
3380
  // checked per concrete instantiation.
3385
3381
  if let PlumType::TParam(_) = obj_ty {
@@ -3440,7 +3436,8 @@ fn inferMethodCall(
3440
3436
  // A fully concrete generic receiver (`Map[Str, Int]`) is checked in the
3441
3437
  // erased pass too, so a typo'd method is a positioned checker error
3442
3438
  // rather than a late codegen failure.
3443
- if !hasFreeVars(obj_ty) && (INST_MODE.with(|m| m.get()) || matches!(obj_ty, PlumType::TApp(..))) {
3439
+ if !hasFreeVars(obj_ty) && (INST_MODE.with(|m| m.get())
3440
+ || matches!(obj_ty, PlumType::TApp(..) | PlumType::TInt | PlumType::TBool)) {
3444
3441
  return Err(format!("{} has no method '{}'", obj_ty, name));
3445
3442
  }
3446
3443
  // Otherwise (erased pass): allow, the per-instance pass decides.
plum-std/Str.plum CHANGED
@@ -393,10 +393,11 @@ record Str(bytes: []Byte) =
393
393
  fun words(self) -> Str =
394
394
  caseConvert(self, " ", 0)
395
395
 
396
- # A new 1-byte Str holding `b`'s low 8 bits — a compiler intrinsic (there's no
396
+ # A new 1-byte Str holding `b`'s low 8 bits, the counterpart to `byteAt`.
397
- # way to build a new array from Plum source), the counterpart to `byteAt`.
398
397
  fun byteToStr(b: Int) -> Str =
398
+ out := makeBytes(1)
399
+ out.set(0, Byte(b))
399
- todo
400
+ return Str(bytes: out)
400
401
 
401
402
  fun isAsciiUpper(b: Int) -> Bool =
402
403
  b >= 65 && b <= 90 # 'A'..'Z'
plum-wasm-codegen/src/lib.rs CHANGED
@@ -413,7 +413,6 @@ pub struct CompileCtx<'a> {
413
413
  pub string_eq_func: u32,
414
414
  /// Shared runtime helper `(n: i64) -> ref Str`: allocates a new `array<i8>`
415
415
  /// holding `n`'s decimal representation, for interpolating an `Int`.
416
- pub int_to_string_func: u32,
417
416
  /// wasm-gc type-section indices for this program's enums/Str/closures —
418
417
  /// every value's real representation (see `docs/superpowers/plans/2026-07-25-wasm-gc-migration.md`).
419
418
  pub gc_types: GcTypeRegistry,
@@ -462,7 +461,6 @@ struct LocalCtx<'a> {
462
461
  named_fn_values: &'a HashMap<(String, PlumType), u32>,
463
462
  string_concat_func: u32,
464
463
  string_eq_func: u32,
465
- int_to_string_func: u32,
466
464
  methods: &'a MethodEnv,
467
465
  enum_variants: &'a EnumVariants,
468
466
  enum_params: &'a EnumParams,
@@ -2493,7 +2491,6 @@ fn compileSourceInner(
2493
2491
 
2494
2492
  let string_concat_func = registerStringConcatHelper(&mut module, gc_types.byte_array_type_idx);
2495
2493
  let string_eq_func = registerStringEqHelper(&mut module, gc_types.byte_array_type_idx);
2496
- let int_to_string_func = registerIntToStringHelper(&mut module, gc_types.byte_array_type_idx);
2497
2494
 
2498
2495
  let ctx = CompileCtx {
2499
2496
  func_ids,
@@ -2515,7 +2512,6 @@ fn compileSourceInner(
2515
2512
  named_fn_values,
2516
2513
  string_concat_func,
2517
2514
  string_eq_func,
2518
- int_to_string_func,
2519
2515
  gc_types,
2520
2516
  singleton_globals,
2521
2517
  };
@@ -2822,138 +2818,6 @@ fn registerStringEqHelper(module: &mut WasmModule, byte_array_type_idx: u32) ->
2822
2818
  module.addFunction(type_idx, &body)
2823
2819
  }
2824
2820
 
2825
- /// Registers `__int_to_string(n: i64) -> ref Str`, a hand-written runtime helper
2826
- /// backing string interpolation: allocates a new `array<i8>` holding `n`'s decimal
2827
- /// representation (handling a leading `-` for negatives, and `0` correctly via a
2828
- /// do-while digit count that always runs at least once).
2829
- fn registerIntToStringHelper(module: &mut WasmModule, byte_array_type_idx: u32) -> u32 {
2830
- let byte_array_ref = gcRef(byte_array_type_idx);
2831
- let str_ref = withGcTypes(strGcValtypeOrFallback);
2832
- let type_idx = module.addType(&[ValType::I64], &[str_ref]);
2833
-
2834
- // locals: 0=n (param, i64), 1=is_neg (i32), 2=count (i32), 3=total_len (i32),
2835
- // 4=pos (i32), 5=result (ref), 6=abs_n (i64), 7=temp (i64)
2836
- // Locals are declared as one group of 4 `i32`s, one group of 1 `ref`, then one
2837
- // group of 2 `i64`s (see below), so indices must stay grouped by type in that
2838
- // same order — NOT in whatever order reads best logically.
2839
- const N: u32 = 0;
2840
- const IS_NEG: u32 = 1;
2841
- const COUNT: u32 = 2;
2842
- const TOTAL_LEN: u32 = 3;
2843
- const POS: u32 = 4;
2844
- const RESULT: u32 = 5;
2845
- const ABS_N: u32 = 6;
2846
- const TEMP: u32 = 7;
2847
-
2848
- let mut body = Vec::new();
2849
- body.extend(encodeLeb128U32(3)); // three locals groups
2850
- body.extend(encodeLeb128U32(4)); // is_neg, count, total_len, pos: i32
2851
- ValType::I32.encode(&mut body);
2852
- body.extend(encodeLeb128U32(1)); // result: ref
2853
- byte_array_ref.encode(&mut body);
2854
- body.extend(encodeLeb128U32(2)); // abs_n, temp: i64
2855
- ValType::I64.encode(&mut body);
2856
-
2857
- // is_neg = n < 0
2858
- Instruction::LocalGet(N).encode(&mut body);
2859
- Instruction::I64Const(0).encode(&mut body);
2860
- Instruction::I64LtS.encode(&mut body);
2861
- Instruction::LocalSet(IS_NEG).encode(&mut body);
2862
-
2863
- // abs_n = if is_neg then (0 - n) else n
2864
- Instruction::LocalGet(IS_NEG).encode(&mut body);
2865
- Instruction::If(BlockType::Result(ValType::I64)).encode(&mut body);
2866
- Instruction::I64Const(0).encode(&mut body);
2867
- Instruction::LocalGet(N).encode(&mut body);
2868
- Instruction::I64Sub.encode(&mut body);
2869
- Instruction::Else.encode(&mut body);
2870
- Instruction::LocalGet(N).encode(&mut body);
2871
- Instruction::End.encode(&mut body);
2872
- Instruction::LocalSet(ABS_N).encode(&mut body);
2873
-
2874
- // count digits: do { temp /= 10; count++ } while (temp != 0); temp starts as abs_n
2875
- Instruction::I32Const(0).encode(&mut body);
2876
- Instruction::LocalSet(COUNT).encode(&mut body);
2877
- Instruction::LocalGet(ABS_N).encode(&mut body);
2878
- Instruction::LocalSet(TEMP).encode(&mut body);
2879
- Instruction::Loop(BlockType::Empty).encode(&mut body);
2880
- Instruction::LocalGet(TEMP).encode(&mut body);
2881
- Instruction::I64Const(10).encode(&mut body);
2882
- Instruction::I64DivS.encode(&mut body);
2883
- Instruction::LocalSet(TEMP).encode(&mut body);
2884
- Instruction::LocalGet(COUNT).encode(&mut body);
2885
- Instruction::I32Const(1).encode(&mut body);
2886
- Instruction::I32Add.encode(&mut body);
2887
- Instruction::LocalSet(COUNT).encode(&mut body);
2888
- Instruction::LocalGet(TEMP).encode(&mut body);
2889
- Instruction::I64Const(0).encode(&mut body);
2890
- Instruction::I64Ne.encode(&mut body);
2891
- Instruction::BrIf(0).encode(&mut body);
2892
- Instruction::End.encode(&mut body);
2893
-
2894
- // total_len = count + is_neg
2895
- Instruction::LocalGet(COUNT).encode(&mut body);
2896
- Instruction::LocalGet(IS_NEG).encode(&mut body);
2897
- Instruction::I32Add.encode(&mut body);
2898
- Instruction::LocalSet(TOTAL_LEN).encode(&mut body);
2899
-
2900
- // result = array.new_default(byte_array_type_idx, total_len) — no length prefix needed,
2901
- // `array.len` reads it back natively.
2902
- Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
2903
- Instruction::ArrayNewDefault(byte_array_type_idx).encode(&mut body);
2904
- Instruction::LocalSet(RESULT).encode(&mut body);
2905
-
2906
- // pos = total_len; temp = abs_n
2907
- Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
2908
- Instruction::LocalSet(POS).encode(&mut body);
2909
- Instruction::LocalGet(ABS_N).encode(&mut body);
2910
- Instruction::LocalSet(TEMP).encode(&mut body);
2911
-
2912
- // do { pos--; result[pos] = '0' + temp % 10; temp /= 10 } while (pos > is_neg)
2913
- Instruction::Loop(BlockType::Empty).encode(&mut body);
2914
- Instruction::LocalGet(POS).encode(&mut body);
2915
- Instruction::I32Const(1).encode(&mut body);
2916
- Instruction::I32Sub.encode(&mut body);
2917
- Instruction::LocalSet(POS).encode(&mut body);
2918
- // array.set(result, pos, value) — stack order [array_ref, index, value]
2919
- Instruction::LocalGet(RESULT).encode(&mut body);
2920
- Instruction::LocalGet(POS).encode(&mut body);
2921
- // value = '0' + (temp % 10)
2922
- Instruction::LocalGet(TEMP).encode(&mut body);
2923
- Instruction::I64Const(10).encode(&mut body);
2924
- Instruction::I64RemS.encode(&mut body);
2925
- Instruction::I64Const(48).encode(&mut body);
2926
- Instruction::I64Add.encode(&mut body);
2927
- Instruction::I32WrapI64.encode(&mut body);
2928
- Instruction::ArraySet(byte_array_type_idx).encode(&mut body);
2929
- // temp /= 10
2930
- Instruction::LocalGet(TEMP).encode(&mut body);
2931
- Instruction::I64Const(10).encode(&mut body);
2932
- Instruction::I64DivS.encode(&mut body);
2933
- Instruction::LocalSet(TEMP).encode(&mut body);
2934
- // while (pos > is_neg)
2935
- Instruction::LocalGet(POS).encode(&mut body);
2936
- Instruction::LocalGet(IS_NEG).encode(&mut body);
2937
- Instruction::I32GtU.encode(&mut body);
2938
- Instruction::BrIf(0).encode(&mut body);
2939
- Instruction::End.encode(&mut body);
2940
-
2941
- // if (is_neg) result[0] = '-'
2942
- Instruction::LocalGet(IS_NEG).encode(&mut body);
2943
- Instruction::If(BlockType::Empty).encode(&mut body);
2944
- Instruction::LocalGet(RESULT).encode(&mut body);
2945
- Instruction::I32Const(0).encode(&mut body);
2946
- Instruction::I32Const(45).encode(&mut body); // '-'
2947
- Instruction::ArraySet(byte_array_type_idx).encode(&mut body);
2948
- Instruction::End.encode(&mut body);
2949
-
2950
- Instruction::LocalGet(RESULT).encode(&mut body);
2951
- wrapBytesAsStr(&mut body);
2952
- Instruction::End.encode(&mut body);
2953
-
2954
- module.addFunction(type_idx, &body)
2955
- }
2956
-
2957
2821
  /// Hand-written bodies for a handful of `libs/std/str.plum` primitives that can't
2958
2822
  /// be expressed in Plum source at all (byte-level array access, building a new
2959
2823
  /// one-element array) — these three are the whole reason every other `Str` method
@@ -2970,22 +2834,6 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2970
2834
  }
2971
2835
  let byte_array_type_idx = withGcTypes(|r| r.byte_array_type_idx);
2972
2836
  match (f.type_param.as_deref(), f.name.as_str()) {
2973
- // byteToStr(b: Int) -> Str — a new length-1 Str holding `b`'s low 8 bits,
2974
- // wrapped into `Str`'s real `{bytes: []Byte}`
2975
- // shape (see `wrapBytesAsStr`).
2976
- (None, "byteToStr") => {
2977
- let mut body = vec![0u8];
2978
- Instruction::LocalGet(0).encode(&mut body); // b
2979
- Instruction::I32WrapI64.encode(&mut body);
2980
- Instruction::ArrayNewFixed {
2981
- array_type_index: byte_array_type_idx,
2982
- array_size: 1,
2983
- }
2984
- .encode(&mut body);
2985
- wrapBytesAsStr(&mut body);
2986
- Instruction::End.encode(&mut body);
2987
- Some(body)
2988
- }
2989
2837
  // ByteSlice.length(self) -> Int — `[]Byte` reuses `Str`'s array<i8> wasm
2990
2838
  // type (see the comment on `PlumType::TByteSlice` in `plumTypeToValtype`),
2991
2839
  // so this is byte-for-byte identical to `Str.length` above.
@@ -3057,7 +2905,7 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
3057
2905
  /// best-effort `std/str` prelude), falling back to the raw `array<i8>` type
3058
2906
  /// instead of panicking. Safe specifically because this is only used by the
3059
2907
  /// three string runtime helpers (`registerStringConcatHelper`/
3060
- /// `registerStringEqHelper`/`registerIntToStringHelper`), which are
2908
+ /// `registerStringEqHelper`), which are
3061
2909
  /// registered UNCONDITIONALLY in every compiled program regardless of
3062
2910
  /// whether it actually uses `Str` — a `Str`-less program (which, by
3063
2911
  /// definition, never calls them) doesn't care what type they claim.
@@ -4265,7 +4113,6 @@ fn compileFnBodyImpl(
4265
4113
  named_fn_values: &ctx.named_fn_values,
4266
4114
  string_concat_func: ctx.string_concat_func,
4267
4115
  string_eq_func: ctx.string_eq_func,
4268
- int_to_string_func: ctx.int_to_string_func,
4269
4116
  methods: &ctx.methods,
4270
4117
  enum_variants: &ctx.enum_variants,
4271
4118
  enum_params: &ctx.enum_params,
@@ -7479,8 +7326,11 @@ fn compileInterpolatedString(
7479
7326
  compileExpr(expr, body, ctx, state)?;
7480
7327
  }
7481
7328
  PlumType::TInt => {
7329
+ // Ordinary `Int.toStr` (`plum-std/Int.plum`), like Float below.
7482
7330
  compileExpr(expr, body, ctx, state)?;
7331
+ let func_idx = *ctx.func_ids.get("Int::toStr")
7332
+ .ok_or_else(|| "codegen: interpolating an Int value requires `Int.toStr` (import std/Int)".to_string())?;
7483
- Instruction::Call(ctx.int_to_string_func).encode(body);
7333
+ Instruction::Call(func_idx).encode(body);
7484
7334
  }
7485
7335
  PlumType::TBool => {
7486
7336
  let str_ref = withGcTypes(|r| plumTypeToGcValtype(&PlumType::TStr, r));
@@ -7771,7 +7621,6 @@ fn compileClosureBody(
7771
7621
  named_fn_values: &ctx.named_fn_values,
7772
7622
  string_concat_func: ctx.string_concat_func,
7773
7623
  string_eq_func: ctx.string_eq_func,
7774
- int_to_string_func: ctx.int_to_string_func,
7775
7624
  methods: &ctx.methods,
7776
7625
  enum_variants: &ctx.enum_variants,
7777
7626
  enum_params: &ctx.enum_params,
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -79,9 +79,14 @@ fn readStrResult(store: &mut wasmtime::Store<()>, val: &wasmtime::Val) -> String
79
79
  let wasmtime::Val::AnyRef(Some(s)) = val else {
80
80
  panic!("expected a Str result");
81
81
  };
82
+ // `Str` is a record wrapping one `[]Byte` in field 0.
83
+ let st = s.unwrap_struct(&*store).expect("Str result should be a struct");
84
+ let wasmtime::Val::AnyRef(Some(bytes)) = st.field(&mut *store, 0).expect("bytes field") else {
85
+ panic!("Str.bytes should be a reference");
86
+ };
82
- let arr = s
87
+ let arr = bytes
83
- .unwrap_array(&mut *store)
88
+ .unwrap_array(&*store)
84
- .expect("Str result should be an array");
89
+ .expect("Str.bytes should be an array");
85
90
  let len = arr.len(&mut *store).expect("should read array length");
86
91
  let mut buf = vec![0u8; len as usize];
87
92
  arr.copy_to_i8_slice(&mut *store, &mut buf)
@@ -97,9 +102,17 @@ fn readStrResult(store: &mut wasmtime::Store<()>, val: &wasmtime::Val) -> String
97
102
  #[test]
98
103
  fn testBlocksCompileAndRun() {
99
104
  let src = "\
105
+ import std/Str
106
+ import std/Int
107
+ import std/Float
108
+
100
109
  fun add(a: Int, b: Int) -> Int =
101
110
  a + b
102
111
 
112
+ fun hardAssertTrap() -> Int =
113
+ assert add(1, 1) == 99
114
+ 0
115
+
103
116
  test \"all pass\"
104
117
  assert add(1, 2) == 3
105
118
  assert add(2, 2) == 4
@@ -109,31 +122,29 @@ test \"one fails, rest still runs\"
109
122
  assert add(1, 1) == 99
110
123
  assert add(2, 2) == 4
111
124
 
112
- fun hardAssertTrap() -> Int =
113
- assert add(1, 1) == 99
114
- 0
115
-
116
125
  test \"hard assert traps\"
117
126
  assert hardAssertTrap() == 0
118
127
  ";
128
+ let dir = std::env::temp_dir().join(format!("plum_t3_testblocks_{}", std::process::id()));
129
+ std::fs::create_dir_all(&dir).unwrap();
130
+ let path = dir.join("main.plum");
119
- let source = parse(src);
131
+ std::fs::write(&path, src).unwrap();
132
+ let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
120
133
  let (bytes, names) = compileTestSource(&source).expect("test source should compile");
121
134
  let result = wasmparser::validate(&bytes);
122
135
  assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
136
+ // The imported std modules contribute their own tests after this file's.
123
137
  assert_eq!(
124
- names,
138
+ names[..3],
125
- vec![
126
- "all pass",
127
- "one fails, rest still runs",
139
+ ["all pass", "one fails, rest still runs", "hard assert traps"]
128
- "hard assert traps"
129
- ]
130
140
  );
131
141
 
132
142
  let engine = gcEngine();
133
143
  let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
134
144
  let mut store = wasmtime::Store::new(&engine, ());
135
- let instance =
145
+ let mut linker = wasmtime::Linker::new(&engine);
146
+ linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
136
- wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
147
+ let instance = linker.instantiate(&mut store, &module).expect("module should instantiate");
137
148
 
138
149
  let allPass = instance
139
150
  .get_func(&mut store, "test#all pass")
@@ -1057,6 +1068,10 @@ fn crash_import_not_triggered_by_string_literal() {
1057
1068
  #[test]
1058
1069
  fn catch_inside_string_interpolation_compiles() {
1059
1070
  let src = "\
1071
+ import std/Str
1072
+ import std/Int
1073
+ import std/Float
1074
+
1060
1075
  fun make() -> error{Nope}!Int =
1061
1076
  return error.Nope
1062
1077
 
@@ -1064,7 +1079,7 @@ fun main() -> Int =
1064
1079
  s := \"v={make() catch 5}\"
1065
1080
  return 1
1066
1081
  ";
1067
- assert_eq!(runInt(src), 1);
1082
+ assert_eq!(runMerged::<i64>("catch_interp", src), 1);
1068
1083
  }
1069
1084
 
1070
1085
  // ---- mixed types via enums (T4) ----
@@ -1626,9 +1641,27 @@ fun main() -> Int =
1626
1641
  let path = dir.join("main.plum");
1627
1642
  std::fs::write(&path, src).unwrap();
1628
1643
  let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
1629
- // Same as any other method of a std type that was not imported: rejected by the checker or by codegen.
1644
+ // Same as any other method of a std type that was not imported: rejected by the checker.
1645
+ assert!(
1630
- let rejected = plum_checker::checkSource(&source).is_err() || compileSource(&source).is_err();
1646
+ plum_checker::checkSource(&source).is_err(),
1631
- assert!(rejected, "Bool.toStr without import std/Bool must be rejected");
1647
+ "Bool.toStr without import std/Bool must be rejected by the checker"
1648
+ );
1649
+ }
1650
+
1651
+ #[test]
1652
+ fn int_tostr_without_import_is_rejected() {
1653
+ // Unmerged source: nothing from std is loaded, so `Int` has no `toStr`.
1654
+ // (The loader itself also refuses a file that uses Int without importing it.)
1655
+ let src = "\
1656
+ fun main() -> Str =
1657
+ n := 5
1658
+ return n.toStr()
1659
+ ";
1660
+ let source = parse(src);
1661
+ assert!(
1662
+ plum_checker::checkSource(&source).is_err(),
1663
+ "Int.toStr without import std/Int must be rejected by the checker"
1664
+ );
1632
1665
  }
1633
1666
 
1634
1667
  #[test]