plum

#treesitter#compiler#wasm

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

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


ac67871 — Peter John 2026-10-26T11:04:01+05:30
feat: narrow Int to i32 at extern boundaries; remove Bool.plum
README.md CHANGED
@@ -471,7 +471,7 @@ It's sugar for `obj.map(|v| v.field)` — nothing more than that; chain it as ma
471
471
 
472
472
  ## Standard library highlights
473
473
 
474
- - **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
474
+ - **`Bool`** — built in (no import); `True`/`False`, `toStr` is a compiler intrinsic, `"true".parseBool()` (in `std/Str`) parses it
475
475
  - **`Try[T, E]`** (`import std/Try`) — `Ok(T)` or `Err(E)`; `isOk`/`isErr`/`unwrap`/`unwrapOr`/`unwrapOrElse`/`map`/`mapErr`/`andThen`
476
476
  - **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
477
477
  - **`Byte`** / **`[]Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
plum-cli/src/main.rs CHANGED
@@ -255,15 +255,15 @@ fn hostImports(
255
255
  },
256
256
  )))
257
257
  }
258
- ("plum", "rawRandomInt") => {
258
+ ("plum", "rawRandomHalf") => {
259
259
  let func_ty = imp.ty().func().cloned().ok_or_else(|| {
260
- anyhow::anyhow!("plum::rawRandomInt import must be a function")
260
+ anyhow::anyhow!("plum::rawRandomHalf import must be a function")
261
261
  })?;
262
262
  Ok(wasmtime::Extern::Func(wasmtime::Func::new(
263
263
  &mut *store,
264
264
  func_ty,
265
265
  |_caller, _params, results| {
266
- results[0] = wasmtime::Val::I64(randomI64());
266
+ results[0] = wasmtime::Val::I32((randomI64() >> 32) as i32);
267
267
  Ok(())
268
268
  },
269
269
  )))
@@ -276,7 +276,7 @@ fn hostImports(
276
276
  &mut *store,
277
277
  func_ty,
278
278
  |_caller, _params, results| {
279
- results[0] = wasmtime::Val::I64(nowMillis());
279
+ results[0] = wasmtime::Val::F64((nowMillis() as f64).to_bits());
280
280
  Ok(())
281
281
  },
282
282
  )))
@@ -311,7 +311,7 @@ fn hostImports(
311
311
  let path = readStrArg(&mut caller, &params[0])?;
312
312
  let data = readStrArg(&mut caller, &params[1])?;
313
313
  results[0] =
314
- wasmtime::Val::I64(std::fs::write(&path, &data).is_ok() as i64);
314
+ wasmtime::Val::I32(std::fs::write(&path, &data).is_ok() as i32);
315
315
  Ok(())
316
316
  },
317
317
  )))
@@ -330,7 +330,7 @@ fn hostImports(
330
330
  // otherwise `remove` on a broken symlink would report "no such file"
331
331
  // and never get to call `rawRemove` on the symlink itself.
332
332
  results[0] =
333
- wasmtime::Val::I64(std::fs::symlink_metadata(&path).is_ok() as i64);
333
+ wasmtime::Val::I32(std::fs::symlink_metadata(&path).is_ok() as i32);
334
334
  Ok(())
335
335
  },
336
336
  )))
@@ -345,7 +345,7 @@ fn hostImports(
345
345
  |mut caller, params, results| {
346
346
  let path = readStrArg(&mut caller, &params[0])?;
347
347
  results[0] =
348
- wasmtime::Val::I64(std::fs::create_dir_all(&path).is_ok() as i64);
348
+ wasmtime::Val::I32(std::fs::create_dir_all(&path).is_ok() as i32);
349
349
  Ok(())
350
350
  },
351
351
  )))
@@ -360,7 +360,7 @@ fn hostImports(
360
360
  |mut caller, params, results| {
361
361
  let path = readStrArg(&mut caller, &params[0])?;
362
362
  results[0] =
363
- wasmtime::Val::I64(std::fs::remove_file(&path).is_ok() as i64);
363
+ wasmtime::Val::I32(std::fs::remove_file(&path).is_ok() as i32);
364
364
  Ok(())
365
365
  },
366
366
  )))
@@ -375,7 +375,7 @@ fn hostImports(
375
375
  |mut caller, params, results| {
376
376
  let path = readStrArg(&mut caller, &params[0])?;
377
377
  results[0] =
378
- wasmtime::Val::I64(std::fs::remove_dir(&path).is_ok() as i64);
378
+ wasmtime::Val::I32(std::fs::remove_dir(&path).is_ok() as i32);
379
379
  Ok(())
380
380
  },
381
381
  )))
@@ -390,8 +390,8 @@ fn hostImports(
390
390
  |mut caller, params, results| {
391
391
  let old_path = readStrArg(&mut caller, &params[0])?;
392
392
  let new_path = readStrArg(&mut caller, &params[1])?;
393
- results[0] = wasmtime::Val::I64(
393
+ results[0] = wasmtime::Val::I32(
394
- std::fs::rename(&old_path, &new_path).is_ok() as i64,
394
+ std::fs::rename(&old_path, &new_path).is_ok() as i32,
395
395
  );
396
396
  Ok(())
397
397
  },
@@ -406,7 +406,7 @@ fn hostImports(
406
406
  func_ty,
407
407
  |mut caller, params, results| {
408
408
  let path = readStrArg(&mut caller, &params[0])?;
409
- let wasmtime::Val::I64(len) = params[1] else {
409
+ let wasmtime::Val::I32(len) = params[1] else {
410
410
  return Err(wasmtime::Error::msg(
411
411
  "rawTruncate expects an Int length argument",
412
412
  ));
@@ -416,7 +416,7 @@ fn hostImports(
416
416
  .open(&path)
417
417
  .and_then(|f| f.set_len(len.max(0) as u64))
418
418
  .is_ok();
419
- results[0] = wasmtime::Val::I64(ok as i64);
419
+ results[0] = wasmtime::Val::I32(ok as i32);
420
420
  Ok(())
421
421
  },
422
422
  )))
@@ -431,8 +431,8 @@ fn hostImports(
431
431
  |mut caller, params, results| {
432
432
  let existing_path = readStrArg(&mut caller, &params[0])?;
433
433
  let new_path = readStrArg(&mut caller, &params[1])?;
434
- results[0] = wasmtime::Val::I64(
434
+ results[0] = wasmtime::Val::I32(
435
- std::fs::hard_link(&existing_path, &new_path).is_ok() as i64,
435
+ std::fs::hard_link(&existing_path, &new_path).is_ok() as i32,
436
436
  );
437
437
  Ok(())
438
438
  },
@@ -452,7 +452,7 @@ fn hostImports(
452
452
  let ok = std::os::unix::fs::symlink(&target, &path).is_ok();
453
453
  #[cfg(not(unix))]
454
454
  let ok = false;
455
- results[0] = wasmtime::Val::I64(ok as i64);
455
+ results[0] = wasmtime::Val::I32(ok as i32);
456
456
  Ok(())
457
457
  },
458
458
  )))
@@ -510,7 +510,7 @@ fn hostImports(
510
510
  func_ty,
511
511
  |mut caller, params, results| {
512
512
  let path = readStrArg(&mut caller, &params[0])?;
513
- let wasmtime::Val::I64(mode) = params[1] else {
513
+ let wasmtime::Val::I32(mode) = params[1] else {
514
514
  return Err(wasmtime::Error::msg(
515
515
  "rawChmod expects an Int mode argument",
516
516
  ));
@@ -526,7 +526,7 @@ fn hostImports(
526
526
  };
527
527
  #[cfg(not(unix))]
528
528
  let ok = false;
529
- results[0] = wasmtime::Val::I64(ok as i64);
529
+ results[0] = wasmtime::Val::I32(ok as i32);
530
530
  Ok(())
531
531
  },
532
532
  )))
plum-core/src/loader.rs CHANGED
@@ -57,6 +57,8 @@ pub fn loadAndMergeWithOverrides(
57
57
  )?;
58
58
  }
59
59
 
60
+ injectBoolIntrinsics(&mut items);
61
+
60
62
  Ok(Source {
61
63
  module: entry_source.module,
62
64
  imports: entry_source.imports,
@@ -65,6 +67,32 @@ pub fn loadAndMergeWithOverrides(
65
67
  })
66
68
  }
67
69
 
70
+ /// `Bool` is a built-in i32 with no source file; its one method, `toStr`
71
+ /// ("True"/"False" — the same text string interpolation emits), is a compiler
72
+ /// intrinsic. It is supplied as a synthetic method item so the checker, `ToStr`
73
+ /// trait conformance, dyn dispatch and codegen all see an ordinary method. Only
74
+ /// injected once `Str` is in the program (the method returns `Str`).
75
+ fn injectBoolIntrinsics(items: &mut Vec<Item>) {
76
+ let has_str = items
77
+ .iter()
78
+ .any(|i| matches!(i, Item::Enum(e) if e.name == "Str"));
79
+ if !has_str {
80
+ return;
81
+ }
82
+ const SNIPPET: &str = "enum Bool =\n | Bool\n\n fun toStr(self) -> Str =\n self ? \"True\" : \"False\"\n";
83
+ let mut parser = tree_sitter::Parser::new();
84
+ parser
85
+ .set_language(&tree_sitter_plum::LANGUAGE.into())
86
+ .expect("plum grammar loads");
87
+ let tree = parser.parse(SNIPPET, None).expect("intrinsic snippet parses");
88
+ let source = AstParser::new(SNIPPET).parseSource(tree.root_node());
89
+ for item in source.items {
90
+ if let Item::Fn(f) = item {
91
+ items.push(Item::Fn(f));
92
+ }
93
+ }
94
+ }
95
+
68
96
  fn canonicalizeMaybeMissing(path: &Path) -> Result<PathBuf, String> {
69
97
  if let Ok(canon) = std::fs::canonicalize(path) {
70
98
  return Ok(canon);
plum-examples/basics.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  module basics
2
- import std/Bool
3
2
  import std/Number
4
3
  import std/Str
5
4
 
plum-examples/closures.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Str
2
- import std/Bool
3
2
  import std/Number
4
3
 
5
4
  fun each(cb: fn(Int) -> Int) -> Int =
plum-examples/control_flow.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Str
2
- import std/Bool
3
2
  import std/Number
4
3
 
5
4
  fun loopSum(limit: Int) -> Int =
plum-examples/dop_visitor.plum CHANGED
@@ -37,7 +37,6 @@
37
37
  # for anything that needs to treat the shapes polymorphically.
38
38
 
39
39
  import std/Os
40
- import std/Bool
41
40
  import std/Number
42
41
  import std/Str
43
42
 
plum-examples/error_propagation.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Try
2
- import std/Bool
3
2
  import std/Number
4
3
  import std/Str
5
4
  import std/List
plum-examples/functions.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Str
2
- import std/Bool
3
2
  import std/Number
4
3
 
5
4
  fun addInts(a: Int, b: Int) -> Int =
plum-examples/generics.plum CHANGED
@@ -1,7 +1,6 @@
1
1
  module generics
2
2
  import std/Str
3
3
  import std/Number
4
- import std/Bool
5
4
  import std/List
6
5
  import std/Map
7
6
  import std/Try
plum-examples/io.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Str
2
- import std/Bool
3
2
 
4
3
  # `printLn` mirrors `libs/std/os.plum`'s declaration; redeclared locally
5
4
  # (rather than `import std/os`) so this stays the SMALLEST possible example
plum-examples/match.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Try
2
- import std/Bool
3
2
  import std/Number
4
3
  import std/Str
5
4
 
plum-examples/methods.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Str
2
- import std/Bool
3
2
  import std/Number
4
3
 
5
4
  enum Cat =
plum-examples/numbers.plum CHANGED
@@ -1,4 +1,3 @@
1
- import std/Bool
2
1
  import std/Str
3
2
  import std/Number
4
3
 
plum-examples/oop_visitor.plum CHANGED
@@ -30,7 +30,6 @@
30
30
  # which is a real cost specific to this language.
31
31
 
32
32
  import std/Os
33
- import std/Bool
34
33
  import std/Number
35
34
  import std/Str
36
35
 
plum-examples/safe_navigation.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Try
2
- import std/Bool
3
2
  import std/Number
4
3
  import std/Str
5
4
  # `obj?.field` / `obj?.method(args)` — Groovy/Kotlin-style safe navigation.
plum-examples/strings.plum CHANGED
@@ -1,4 +1,3 @@
1
- import std/Bool
2
1
  import std/Number
3
2
  import std/Str
4
3
 
plum-examples/testing.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Str
2
- import std/Bool
3
2
  import std/Number
4
3
 
5
4
  fun add(a: Int, b: Int) -> Int =
plum-examples/traits.plum CHANGED
@@ -1,4 +1,3 @@
1
- import std/Bool
2
1
  import std/Str
3
2
  import std/Number
4
3
  import std/Try
plum-examples/types.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  import std/Try
2
- import std/Bool
3
2
  import std/Str
4
3
  import std/Number
5
4
 
plum-runtime/src/main.rs CHANGED
@@ -45,17 +45,17 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
45
45
  Ok(())
46
46
  }))
47
47
  }
48
- ("plum", "rawRandomInt") => {
48
+ ("plum", "rawRandomHalf") => {
49
- let func_ty = imp.ty().func().cloned().expect("plum::rawRandomInt import should be a function");
49
+ let func_ty = imp.ty().func().cloned().expect("plum::rawRandomHalf import should be a function");
50
50
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| {
51
- results[0] = wasmtime::Val::I64(randomI64());
51
+ results[0] = wasmtime::Val::I32((randomI64() >> 32) as i32);
52
52
  Ok(())
53
53
  }))
54
54
  }
55
55
  ("plum", "rawNowMillis") => {
56
56
  let func_ty = imp.ty().func().cloned().expect("plum::rawNowMillis import should be a function");
57
57
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| {
58
- results[0] = wasmtime::Val::I64(nowMillis());
58
+ results[0] = wasmtime::Val::F64((nowMillis() as f64).to_bits());
59
59
  Ok(())
60
60
  }))
61
61
  }
@@ -73,7 +73,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
73
73
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
74
74
  let path = readStrArg(&mut caller, &params[0])?;
75
75
  let data = readStrArg(&mut caller, &params[1])?;
76
- results[0] = wasmtime::Val::I64(std::fs::write(&path, &data).is_ok() as i64);
76
+ results[0] = wasmtime::Val::I32(std::fs::write(&path, &data).is_ok() as i32);
77
77
  Ok(())
78
78
  }))
79
79
  }
@@ -83,7 +83,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
83
83
  let path = readStrArg(&mut caller, &params[0])?;
84
84
  // See the identical `rawExists` in plum-cli for why this uses
85
85
  // `symlink_metadata` rather than `Path::exists`.
86
- results[0] = wasmtime::Val::I64(std::fs::symlink_metadata(&path).is_ok() as i64);
86
+ results[0] = wasmtime::Val::I32(std::fs::symlink_metadata(&path).is_ok() as i32);
87
87
  Ok(())
88
88
  }))
89
89
  }
@@ -91,7 +91,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
91
91
  let func_ty = imp.ty().func().cloned().expect("plum::rawMkdir import should be a function");
92
92
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
93
93
  let path = readStrArg(&mut caller, &params[0])?;
94
- results[0] = wasmtime::Val::I64(std::fs::create_dir_all(&path).is_ok() as i64);
94
+ results[0] = wasmtime::Val::I32(std::fs::create_dir_all(&path).is_ok() as i32);
95
95
  Ok(())
96
96
  }))
97
97
  }
@@ -99,7 +99,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
99
99
  let func_ty = imp.ty().func().cloned().expect("plum::rawRemove import should be a function");
100
100
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
101
101
  let path = readStrArg(&mut caller, &params[0])?;
102
- results[0] = wasmtime::Val::I64(std::fs::remove_file(&path).is_ok() as i64);
102
+ results[0] = wasmtime::Val::I32(std::fs::remove_file(&path).is_ok() as i32);
103
103
  Ok(())
104
104
  }))
105
105
  }
@@ -107,7 +107,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
107
107
  let func_ty = imp.ty().func().cloned().expect("plum::rawRemoveDir import should be a function");
108
108
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
109
109
  let path = readStrArg(&mut caller, &params[0])?;
110
- results[0] = wasmtime::Val::I64(std::fs::remove_dir(&path).is_ok() as i64);
110
+ results[0] = wasmtime::Val::I32(std::fs::remove_dir(&path).is_ok() as i32);
111
111
  Ok(())
112
112
  }))
113
113
  }
@@ -116,7 +116,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
116
116
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
117
117
  let old_path = readStrArg(&mut caller, &params[0])?;
118
118
  let new_path = readStrArg(&mut caller, &params[1])?;
119
- results[0] = wasmtime::Val::I64(std::fs::rename(&old_path, &new_path).is_ok() as i64);
119
+ results[0] = wasmtime::Val::I32(std::fs::rename(&old_path, &new_path).is_ok() as i32);
120
120
  Ok(())
121
121
  }))
122
122
  }
@@ -124,12 +124,12 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
124
124
  let func_ty = imp.ty().func().cloned().expect("plum::rawTruncate import should be a function");
125
125
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
126
126
  let path = readStrArg(&mut caller, &params[0])?;
127
- let wasmtime::Val::I64(len) = params[1] else {
127
+ let wasmtime::Val::I32(len) = params[1] else {
128
128
  return Err(wasmtime::Error::msg("rawTruncate expects an Int length argument"));
129
129
  };
130
130
  let ok = std::fs::OpenOptions::new().write(true).open(&path)
131
131
  .and_then(|f| f.set_len(len.max(0) as u64)).is_ok();
132
- results[0] = wasmtime::Val::I64(ok as i64);
132
+ results[0] = wasmtime::Val::I32(ok as i32);
133
133
  Ok(())
134
134
  }))
135
135
  }
@@ -138,7 +138,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
138
138
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
139
139
  let existing_path = readStrArg(&mut caller, &params[0])?;
140
140
  let new_path = readStrArg(&mut caller, &params[1])?;
141
- results[0] = wasmtime::Val::I64(std::fs::hard_link(&existing_path, &new_path).is_ok() as i64);
141
+ results[0] = wasmtime::Val::I32(std::fs::hard_link(&existing_path, &new_path).is_ok() as i32);
142
142
  Ok(())
143
143
  }))
144
144
  }
@@ -151,7 +151,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
151
151
  let ok = std::os::unix::fs::symlink(&target, &path).is_ok();
152
152
  #[cfg(not(unix))]
153
153
  let ok = false;
154
- results[0] = wasmtime::Val::I64(ok as i64);
154
+ results[0] = wasmtime::Val::I32(ok as i32);
155
155
  Ok(())
156
156
  }))
157
157
  }
@@ -179,7 +179,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
179
179
  let func_ty = imp.ty().func().cloned().expect("plum::rawChmod import should be a function");
180
180
  wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
181
181
  let path = readStrArg(&mut caller, &params[0])?;
182
- let wasmtime::Val::I64(mode) = params[1] else {
182
+ let wasmtime::Val::I32(mode) = params[1] else {
183
183
  return Err(wasmtime::Error::msg("rawChmod expects an Int mode argument"));
184
184
  };
185
185
  #[cfg(unix)]
@@ -189,7 +189,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Ve
189
189
  };
190
190
  #[cfg(not(unix))]
191
191
  let ok = false;
192
- results[0] = wasmtime::Val::I64(ok as i64);
192
+ results[0] = wasmtime::Val::I32(ok as i32);
193
193
  Ok(())
194
194
  }))
195
195
  }
plum-std/Array.plum CHANGED
@@ -1,5 +1,4 @@
1
1
  module std
2
- import std/Bool
3
2
  import std/Number
4
3
 
5
4
  # A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
plum-std/Base64.plum CHANGED
@@ -2,7 +2,6 @@ module std
2
2
  import std/Str
3
3
  import std/Buffer
4
4
  import std/Try
5
- import std/Bool
6
5
  import std/Number
7
6
 
8
7
  # The Base64 package contains support for doing Base64 binary-to-text encodings.
plum-std/Bool.plum DELETED
@@ -1,27 +0,0 @@
1
- module std
2
- import std/Try
3
- import std/Str
4
-
5
- # `Bool` is a built-in primitive (a wasm i32, 0/1); `True`/`False` are always
6
- # available literals and need no import. This file only provides the METHODS
7
- # whose receiver is a `Bool` (the single-variant enum is a pure marker, exactly
8
- # like `Byte.plum`/`Number.plum`).
9
- enum Bool =
10
- | Bool
11
-
12
- fun parse(v: Str) -> Try[Bool, [BadBoolStr]] =
13
- if v == "true"
14
- Ok(True)
15
- else if v == "false"
16
- Ok(False)
17
- else
18
- Err(BadBoolStr)
19
-
20
- fun and(self, o: Bool) -> Bool =
21
- self && o
22
-
23
- fun or(self, o: Bool) -> Bool =
24
- self || o
25
-
26
- fun toStr(self) -> Str =
27
- self ? "True" : "False"
plum-std/Buffer.plum CHANGED
@@ -1,7 +1,6 @@
1
1
  module std
2
2
  import std/ByteSlice
3
3
  import std/Str
4
- import std/Bool
5
4
  import std/Number
6
5
 
7
6
  # A Buffer is a growable, mutable sequence of bytes for efficiently building
plum-std/Byte.plum CHANGED
@@ -1,6 +1,5 @@
1
1
  module std
2
2
  import std/Number
3
- import std/Bool
4
3
  import std/Str
5
4
 
6
5
  # Byte is an unsigned 8-bit value (0-255) — Plum's counterpart to Go's
plum-std/ByteSlice.plum CHANGED
@@ -1,7 +1,6 @@
1
1
  module std
2
2
  import std/Byte
3
3
  import std/Str
4
- import std/Bool
5
4
  import std/Number
6
5
 
7
6
  # ByteSlice is `[]Byte` — Plum's counterpart to Go's byte slice: a
plum-std/Http.plum CHANGED
@@ -6,7 +6,6 @@ import std/List
6
6
  import std/Map
7
7
  import std/Try
8
8
  import std/Str
9
- import std/Bool
10
9
  import std/Number
11
10
 
12
11
  # An HTTP response: status code, response headers, and the raw response
plum-std/Json.plum CHANGED
@@ -6,7 +6,6 @@ import std/Map
6
6
  import std/Number
7
7
  import std/Str
8
8
  import std/Buffer
9
- import std/Bool
10
9
 
11
10
  # A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
12
11
  # distinguish integers from floats the way Plum does), and `JsonList`/
plum-std/List.plum CHANGED
@@ -4,7 +4,6 @@ import std/Try
4
4
  import std/Array
5
5
  import std/Buffer
6
6
  import std/Number
7
- import std/Bool
8
7
  import std/Str
9
8
 
10
9
  # A growable list: `data` is a backing `Array[T]` whose length is the list's
plum-std/Map.plum CHANGED
@@ -2,7 +2,6 @@ module std
2
2
  import std/List
3
3
  import std/Try
4
4
  import std/Array
5
- import std/Bool
6
5
  import std/Number
7
6
  import std/Str
8
7
 
plum-std/Number.plum CHANGED
@@ -1,6 +1,5 @@
1
1
  module std
2
2
  import std/Str
3
- import std/Bool
4
3
  import std/Try
5
4
 
6
5
  MIN_VALUE = -0x8000_0000_0000_0000 # Lowest value of Int
@@ -361,7 +360,14 @@ fun parseFloat(s: Str) -> Try[Float, [BadNumStr]] =
361
360
  # A raw, host-provided pseudo-random 64-bit Int (xorshift64*, seeded from the
362
361
  # wall clock — not cryptographically secure). Every other random-number
363
362
  # function in std is built on top of this one host extern.
364
- extern fun rawRandomInt() -> Int
363
+ extern fun rawRandomHalf() -> Int
364
+
365
+ # 64 random bits, assembled from two i32 halves (`Int` is narrowed to i32 at the
366
+ # extern boundary, so the host can only hand back 32 bits per call).
367
+ fun rawRandomInt() -> Int =
368
+ hi := rawRandomHalf()
369
+ lo := rawRandomHalf() & 0xFFFFFFFF
370
+ return { hi << 32 } | lo
365
371
 
366
372
  # A random Float uniformly distributed in [0.0, 1.0).
367
373
  fun random() -> Float =
@@ -546,3 +552,9 @@ fun cbrt(x: Float) -> Float =
546
552
  guess = {2.0f * guess + v / {guess * guess}} / 3.0f
547
553
  i = i + 1
548
554
  return neg ? -guess : guess
555
+
556
+ test "random stays in range with 64-bit assembled from two i32 halves"
557
+ r := random()
558
+ assert r >= 0.0f && r < 1.0f
559
+ assert randomInt(10) >= 0 && randomInt(10) < 10
560
+ assert rawRandomInt() != rawRandomInt()
plum-std/Os.plum CHANGED
@@ -3,7 +3,6 @@ import std/List
3
3
  import std/Try
4
4
  import std/Str
5
5
  import std/Uuid
6
- import std/Bool
7
6
  import std/Number
8
7
 
9
8
  # NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
plum-std/Str.plum CHANGED
@@ -2,8 +2,8 @@ module std
2
2
 
3
3
  import std/List
4
4
  import std/Buffer
5
- import std/Bool
6
5
  import std/Number
6
+ import std/Try
7
7
 
8
8
  # Any type that can be converted to a str needs to implement this trait
9
9
  trait ToStr =
@@ -146,6 +146,15 @@ enum Str(Comparable, ToStr, Readable, Writable) =
146
146
  fun toStr(self) -> Str =
147
147
  self
148
148
 
149
+ # Parses exactly `"true"` or `"false"`.
150
+ fun parseBool(self) -> Try[Bool, [BadBoolStr]] =
151
+ if self == "true"
152
+ Ok(True)
153
+ else if self == "false"
154
+ Ok(False)
155
+ else
156
+ Err(BadBoolStr)
157
+
149
158
  fun matchPattern(self, pattern: Regex) -> List[Str] =
150
159
  todo
151
160
 
@@ -648,3 +657,14 @@ test "reverse reverses by character, not by raw byte"
648
657
  test "escape/unescape round-trip HTML-sensitive characters"
649
658
  assert "<a href=\"x\">&'</a>".escape() == "&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;"
650
659
  assert "&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;".unescape() == "<a href=\"x\">&'</a>"
660
+
661
+ test "Bool.toStr is a compiler intrinsic: True/False, also through interpolation"
662
+ t := True
663
+ assert t.toStr() == "True"
664
+ assert False.toStr() == "False"
665
+ assert "{t}-{!t}" == "True-False"
666
+
667
+ test "Str.parseBool accepts true/false and rejects everything else"
668
+ assert "true".parseBool() ?? False
669
+ assert !{"false".parseBool() ?? True}
670
+ assert !{"yes".parseBool() ?? False}
plum-std/Testing.plum CHANGED
@@ -2,7 +2,6 @@ module std
2
2
 
3
3
  import std/Try
4
4
  import std/List
5
- import std/Bool
6
5
  import std/Number
7
6
  import std/Str
8
7
 
plum-std/Time.plum CHANGED
@@ -1,10 +1,10 @@
1
1
  module std
2
2
  import std/Number
3
3
  import std/Str
4
- import std/Bool
5
4
 
6
5
  # A raw, host-provided wall-clock reading: milliseconds since the Unix epoch.
6
+ # Returned as Float (exact up to 2^53) because `Int` externs are narrowed to i32.
7
- extern fun rawNowMillis() -> Int
7
+ extern fun rawNowMillis() -> Float
8
8
 
9
9
  # A single point in time, stored as milliseconds since the Unix epoch.
10
10
  enum Time(ToStr) =
@@ -75,7 +75,7 @@ enum Time(ToStr) =
75
75
  + "." + self.millisecond().toStr().padStart("0", 3) + "Z"
76
76
 
77
77
  fun now() -> Time =
78
- return Time(value: rawNowMillis())
78
+ return Time(value: Int(rawNowMillis()))
79
79
 
80
80
  # A Gregorian calendar date, decomposed from a day count by `civilFromDays`.
81
81
  enum Civil =
@@ -190,3 +190,6 @@ test "Duration arithmetic and unit constructors compute correctly"
190
190
  assert Duration(value: 0).isZero() == True
191
191
  assert Duration.seconds(1).isBefore(Duration.seconds(2)) == True
192
192
  assert Duration.seconds(2).isAfter(Duration.seconds(1)) == True
193
+
194
+ test "now() reads a wall clock beyond 2^31 ms through the Float extern"
195
+ assert now().toMillis() > 1600000000000
plum-std/Try.plum CHANGED
@@ -1,7 +1,6 @@
1
1
  module std
2
2
 
3
3
  import std/Str
4
- import std/Bool
5
4
  import std/Number
6
5
  import std/List
7
6
  import std/Map
plum-std/Uuid.plum CHANGED
@@ -2,7 +2,6 @@ module std
2
2
  import std/Number
3
3
  import std/Try
4
4
  import std/Str
5
- import std/Bool
6
5
 
7
6
  # A UUID, stored as its canonical 36-character
8
7
  # `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form.
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1852,6 +1852,7 @@ fn compileSourceInner(
1852
1852
  // always its unique key.
1853
1853
  let mut func_ids: HashMap<String, u32> = HashMap::new();
1854
1854
  let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
1855
+ let mut extern_wrappers: Vec<(String, u32, Vec<ValType>, Option<ValType>)> = Vec::new();
1855
1856
  for item in &source.items {
1856
1857
  if let ast::Item::Fn(f) = item {
1857
1858
  if !f.is_extern {
@@ -1859,10 +1860,17 @@ fn compileSourceInner(
1859
1860
  }
1860
1861
  let param_types = fnWasmParamTypes(f, &gc_types);
1861
1862
  let ret = retTypeToWasm(f.returns.as_ref());
1863
+ // `Int` in an extern signature is narrowed to wasm i32 at the host
1864
+ // boundary (the browser host never sees a BigInt). The import gets the
1865
+ // narrowed type; a same-named wrapper with the plain i64 signature (added
1866
+ // below, once all imports exist) range-checks/wraps args and sign-extends
1867
+ // the result, so every Plum call site is unchanged.
1868
+ let narrow = |v: &ValType| if *v == ValType::I64 { ValType::I32 } else { *v };
1869
+ let imp_params: Vec<ValType> = param_types.iter().map(narrow).collect();
1862
- let results_vec: Vec<ValType> = ret.into_iter().collect();
1870
+ let imp_results: Vec<ValType> = ret.iter().map(narrow).collect();
1863
- let type_idx = module.addType(&param_types, &results_vec);
1871
+ let type_idx = module.addType(&imp_params, &imp_results);
1864
1872
  let func_idx = module.addImport("plum", &f.name, type_idx);
1865
- func_ids.insert(f.name.clone(), func_idx);
1873
+ extern_wrappers.push((f.name.clone(), func_idx, param_types.clone(), ret));
1866
1874
  func_sigs.insert(
1867
1875
  f.name.clone(),
1868
1876
  FuncSig {
@@ -1892,6 +1900,40 @@ fn compileSourceInner(
1892
1900
  }
1893
1901
  }
1894
1902
 
1903
+ // Extern `Int` narrowing wrappers (see the import loop above). The i32 range
1904
+ // check is unconditional (one wrap/extend/compare per Int arg; the CLI has no
1905
+ // separate debug mode): an `Int` that doesn't fit in i32 traps.
1906
+ for (name, imp_idx, params, ret) in &extern_wrappers {
1907
+ let mut b = vec![0u8];
1908
+ for (i, vt) in params.iter().enumerate() {
1909
+ if *vt == ValType::I64 {
1910
+ Instruction::LocalGet(i as u32).encode(&mut b);
1911
+ Instruction::LocalGet(i as u32).encode(&mut b);
1912
+ Instruction::I32WrapI64.encode(&mut b);
1913
+ Instruction::I64ExtendI32S.encode(&mut b);
1914
+ Instruction::I64Ne.encode(&mut b);
1915
+ Instruction::If(BlockType::Empty).encode(&mut b);
1916
+ Instruction::Unreachable.encode(&mut b);
1917
+ Instruction::End.encode(&mut b);
1918
+ }
1919
+ }
1920
+ for (i, vt) in params.iter().enumerate() {
1921
+ Instruction::LocalGet(i as u32).encode(&mut b);
1922
+ if *vt == ValType::I64 {
1923
+ Instruction::I32WrapI64.encode(&mut b);
1924
+ }
1925
+ }
1926
+ Instruction::Call(*imp_idx).encode(&mut b);
1927
+ if *ret == Some(ValType::I64) {
1928
+ Instruction::I64ExtendI32S.encode(&mut b);
1929
+ }
1930
+ Instruction::End.encode(&mut b);
1931
+ let results_vec: Vec<ValType> = ret.iter().cloned().collect();
1932
+ let t = module.addType(params, &results_vec);
1933
+ let w = module.addFunction(t, &b);
1934
+ func_ids.insert(name.clone(), w);
1935
+ }
1936
+
1895
1937
  // Pre-allocate one instance of every payload-free variant (True/False/End/...)
1896
1938
  // as a global, populated once by a `start` function rather than reconstructed on
1897
1939
  // every reference — see this migration plan's Decision 2. `struct.new` isn't
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1598,7 +1598,6 @@ fn runMerged<R: wasmtime::WasmResults>(name: &str, src: &str) -> R {
1598
1598
  #[test]
1599
1599
  fn heterogeneous_list_dispatch() {
1600
1600
  let src = "\
1601
- import std/Bool
1602
1601
  import std/Str
1603
1602
  import std/Number
1604
1603
  import std/Try
@@ -1661,7 +1660,6 @@ fun main() -> Float =
1661
1660
  #[test]
1662
1661
  fn boxed_int_dispatch() {
1663
1662
  let src = "\
1664
- import std/Bool
1665
1663
  import std/Str
1666
1664
  import std/Number
1667
1665
 
@@ -1681,7 +1679,6 @@ fun main() -> Int =
1681
1679
  #[test]
1682
1680
  fn boxed_i64_extremes_through_generic() {
1683
1681
  let src = "\
1684
- import std/Bool
1685
1682
  import std/Str
1686
1683
  import std/Number
1687
1684
 
@@ -1704,7 +1701,6 @@ fun main() -> Int =
1704
1701
  #[test]
1705
1702
  fn generic_closure_thunk() {
1706
1703
  let src = "\
1707
- import std/Bool
1708
1704
  import std/Str
1709
1705
  import std/Number
1710
1706
  import std/Try
@@ -1720,7 +1716,6 @@ fun main() -> Int =
1720
1716
  #[test]
1721
1717
  fn try_of_list_of_try() {
1722
1718
  let src = "\
1723
- import std/Bool
1724
1719
  import std/Str
1725
1720
  import std/Number
1726
1721
  import std/Try
@@ -1750,7 +1745,6 @@ fun main() -> Int =
1750
1745
  #[test]
1751
1746
  fn one_copy_per_generic() {
1752
1747
  let src = "\
1753
- import std/Bool
1754
1748
  import std/Str
1755
1749
  import std/Number
1756
1750
  import std/Try
@@ -1862,10 +1856,94 @@ fun chain(ok: Bool) -> Try[Bool, [Nope]] =
1862
1856
  fun main() -> Int =
1863
1857
  a := chain(True) ?? True
1864
1858
  b := chain(False) ?? True
1865
- p := Bool.parse(\"true\") ?? False
1859
+ p := \"true\".parseBool() ?? False
1866
1860
  if !a && b && p
1867
1861
  return 1
1868
1862
  return 0
1869
1863
  ";
1870
1864
  assert_eq!(runMerged::<i64>("bool_try", src), 1);
1871
1865
  }
1866
+
1867
+ // ---- extern `Int` narrowing (T1): i32 at the host boundary ----
1868
+
1869
+ /// Compiles a program whose `main` returns `echo(<arg>)` through a host `echo`
1870
+ /// import that is `(i32) -> i32`; returns the module plus `main`'s outcome.
1871
+ fn externEcho(arg: &str) -> (wasmtime::Module, Result<i64, String>) {
1872
+ let src = format!(
1873
+ "extern fun echo(x: Int) -> Int\n\nfun main() -> Int =\n echo({})\n",
1874
+ arg
1875
+ );
1876
+ let source = parse(&src);
1877
+ let bytes = compileSource(&source).expect("compile failed");
1878
+ wasmparser::validate(&bytes).expect("valid wasm");
1879
+ let engine = gcEngine();
1880
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
1881
+ let mut store = wasmtime::Store::new(&engine, ());
1882
+ let mut linker = wasmtime::Linker::new(&engine);
1883
+ linker.func_wrap("plum", "echo", |x: i32| -> i32 { x }).unwrap();
1884
+ let instance = linker.instantiate(&mut store, &module).unwrap();
1885
+ let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
1886
+ let r = main.call(&mut store, ()).map_err(|e| e.to_string());
1887
+ (module, r)
1888
+ }
1889
+
1890
+ #[test]
1891
+ fn extern_int_is_i32() {
1892
+ let (module, _) = externEcho("1");
1893
+ let imp = module
1894
+ .imports()
1895
+ .find(|i| i.module() == "plum" && i.name() == "echo")
1896
+ .expect("echo import");
1897
+ let ty = imp.ty().func().cloned().expect("func import");
1898
+ let params: Vec<_> = ty.params().collect();
1899
+ let results: Vec<_> = ty.results().collect();
1900
+ assert!(matches!(params.as_slice(), [wasmtime::ValType::I32]), "{:?}", params);
1901
+ assert!(matches!(results.as_slice(), [wasmtime::ValType::I32]), "{:?}", results);
1902
+ }
1903
+
1904
+ #[test]
1905
+ fn extern_int_roundtrip_negative() {
1906
+ assert_eq!(externEcho("0 - 5").1.unwrap(), -5);
1907
+ assert_eq!(externEcho("2147483647").1.unwrap(), 2147483647);
1908
+ assert_eq!(externEcho("0 - 2147483647 - 1").1.unwrap(), -2147483648);
1909
+ }
1910
+
1911
+ #[test]
1912
+ fn extern_int_overflow_traps() {
1913
+ assert!(externEcho("2147483648").1.is_err(), "Int > i32::MAX must trap");
1914
+ assert!(externEcho("1 << 40").1.is_err(), "Int that doesn't fit must trap");
1915
+ assert!(externEcho("0 - 2147483649").1.is_err(), "Int < i32::MIN must trap");
1916
+ }
1917
+
1918
+ #[test]
1919
+ fn bool_tostr_intrinsic() {
1920
+ let src = "\
1921
+ import std/Number
1922
+ import std/Str
1923
+
1924
+ fun main() -> Int =
1925
+ t := True
1926
+ if t.toStr() == \"True\" && False.toStr() == \"False\" && \"{t}\" == \"True\"
1927
+ return 1
1928
+ return 0
1929
+ ";
1930
+ assert_eq!(runMerged::<i64>("bool_tostr_intrinsic", src), 1);
1931
+ }
1932
+
1933
+ #[test]
1934
+ fn parse_bool() {
1935
+ let src = "\
1936
+ import std/Number
1937
+ import std/Str
1938
+ import std/Try
1939
+
1940
+ fun main() -> Int =
1941
+ a := \"true\".parseBool() ?? False
1942
+ b := \"false\".parseBool() ?? True
1943
+ c := \"nope\".parseBool() ?? False
1944
+ if a && !b && !c
1945
+ return 1
1946
+ return 0
1947
+ ";
1948
+ assert_eq!(runMerged::<i64>("parse_bool", src), 1);
1949
+ }
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -157,7 +157,7 @@ fn ioExampleCompilesAndRunsCorrectly() {
157
157
  },
158
158
  );
159
159
 
160
- // `io.plum` now also transitively pulls in `rawRandomInt` (via the
160
+ // `io.plum` now also transitively pulls in `rawRandomHalf` (via the
161
161
  // always-implicit `std/str` prelude — `str` -> `list` -> `int`, which
162
162
  // declares it for `List.sample`/`shuffle` — see
163
163
  // `plum-core::loader::loadAndMerge`), even though this test never
@@ -168,17 +168,17 @@ fn ioExampleCompilesAndRunsCorrectly() {
168
168
  .imports()
169
169
  .map(|imp| match (imp.module(), imp.name()) {
170
170
  ("plum", "printLn") => wasmtime::Extern::Func(print_ln),
171
- ("plum", "rawRandomInt") => {
171
+ ("plum", "rawRandomHalf") => {
172
172
  let func_ty = imp
173
173
  .ty()
174
174
  .func()
175
175
  .cloned()
176
- .expect("rawRandomInt import should be a function");
176
+ .expect("rawRandomHalf import should be a function");
177
177
  wasmtime::Extern::Func(wasmtime::Func::new(
178
178
  &mut store,
179
179
  func_ty,
180
180
  |_caller, _params, results| {
181
- results[0] = wasmtime::Val::I64(0);
181
+ results[0] = wasmtime::Val::I32(0);
182
182
  Ok(())
183
183
  },
184
184
  ))