plum

#treesitter#compiler#wasm

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

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


5993bb7 — Peter John 2026-11-03T19:09:47+05:30
trmc_test: guard that std List.map stays within 3x of a build with a presized GC heap
Files changed (1) hide show
  1. plum-core/tests/trmc_test.rs +89 -0
plum-core/tests/trmc_test.rs CHANGED
@@ -367,3 +367,92 @@ fun main() -> Int =
367
367
  assert!(isRewritten("nth1", &p, "replaceNth"));
368
368
  assert_eq!(runInt("nth2", &p), 1);
369
369
  }
370
+
371
+ /// Whether the wasm fn named `name` (or a specialised copy of it) contains an op
372
+ /// matching `pred`.
373
+ fn hasOp(wasm: &[u8], name: &str, pred: fn(&wasmparser::Operator) -> bool) -> bool {
374
+ let mut names = std::collections::HashMap::new();
375
+ let mut imports = 0u32;
376
+ for p in wasmparser::Parser::new(0).parse_all(wasm) {
377
+ match p.unwrap() {
378
+ wasmparser::Payload::ImportSection(r) => imports = r.count(),
379
+ wasmparser::Payload::CustomSection(c) => {
380
+ if let wasmparser::KnownCustom::Name(r) = c.as_known() {
381
+ for n in r {
382
+ if let Ok(wasmparser::Name::Function(m)) = n {
383
+ for e in m.into_iter().flatten() {
384
+ names.insert(e.index, e.name.to_string());
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ _ => {}
391
+ }
392
+ }
393
+ let mut idx = imports;
394
+ let mut seen = false;
395
+ let mut hit = false;
396
+ for p in wasmparser::Parser::new(0).parse_all(wasm) {
397
+ if let wasmparser::Payload::CodeSectionEntry(b) = p.unwrap() {
398
+ if names.get(&idx).is_some_and(|n| n == name || n.starts_with(&format!("{name}[")) || n.starts_with(&format!("{name}$c"))) {
399
+ seen = true;
400
+ let mut r = b.get_operators_reader().unwrap();
401
+ while !r.eof() {
402
+ hit |= pred(&r.read().unwrap());
403
+ }
404
+ }
405
+ idx += 1;
406
+ }
407
+ }
408
+ assert!(seen, "no fn {name}");
409
+ hit
410
+ }
411
+
412
+ /// F-T2-1: std `List.map` (TRMC `mapCells`) looked ~10x slower than building a
413
+ /// list. The loop is not the cause: one `struct.new` per cell, closure inlined.
414
+ /// The time was wasmtime's copying GC copying every live cell on each heap
415
+ /// growth, landing on whichever phase grew the heap (`build` gets the same
416
+ /// spikes). With the GC heap pre-sized so no collection runs, a 1M map must cost
417
+ /// at most 3x a 1M build.
418
+ #[test]
419
+ fn std_map_1m_within_3x_of_build() {
420
+ let head = "import std/Int\nimport std/List\n\nfun build(n: Int, acc: List[Int]) -> List[Int] = if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))\n\n";
421
+ let buildSrc = format!("{head}fun main() -> Int =\n nil: List[Int] := Nil\n xs := build(1000000, nil)\n xs.length()\n");
422
+ let mapSrc = format!("{head}fun main() -> Int =\n nil: List[Int] := Nil\n xs := build(1000000, nil)\n ys := xs.map(|x| x * 2)\n xs.length() + ys.length()\n");
423
+ let mapBytes = compileSource(&load("stdmap", &mapSrc)).unwrap();
424
+ assert!(
425
+ !hasOp(&mapBytes, "mapCells", |o| matches!(o, wasmparser::Operator::CallRef { .. })),
426
+ "closure not inlined into mapCells"
427
+ );
428
+ assert_eq!(structNewCount(&mapBytes, "mapCells"), 1);
429
+
430
+ let mut config = wasmtime::Config::new();
431
+ config.wasm_gc(true);
432
+ config.wasm_function_references(true);
433
+ config.wasm_tail_call(true);
434
+ config.gc_heap_initial_size(512 << 20);
435
+ let engine = wasmtime::Engine::new(&config).unwrap();
436
+ let time = |name: &str, src: &str, want: i64| -> std::time::Duration {
437
+ let bytes = compileSource(&load(name, src)).unwrap();
438
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
439
+ (0..3)
440
+ .map(|_| {
441
+ let mut store = wasmtime::Store::new(&engine, ());
442
+ let mut linker = wasmtime::Linker::new(&engine);
443
+ linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
444
+ let instance = linker.instantiate(&mut store, &module).unwrap();
445
+ let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
446
+ let t = std::time::Instant::now();
447
+ assert_eq!(main.call(&mut store, ()).unwrap(), want);
448
+ t.elapsed()
449
+ })
450
+ .min()
451
+ .unwrap()
452
+ };
453
+ let tBuild = time("stdbuild", &buildSrc, 1_000_000);
454
+ let tBoth = time("stdmap2", &mapSrc, 2_000_000);
455
+ let tMap = tBoth.saturating_sub(tBuild);
456
+ eprintln!("build {tBuild:?} build+map {tBoth:?} map {tMap:?}");
457
+ assert!(tMap <= tBuild * 3, "map {tMap:?} vs build {tBuild:?}");
458
+ }