plum
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
- 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
|
+
}
|