The JIT that runs makepad apps from source: lexer, front end, type checker, lazy borrow checker (Gandalf; `--check-first` opts in), codegen, session and warm/cold caches keyed by compiler stamp, provided crates bound late, startup images, live patching. User-facing name is `makepad run` (binary `makepad`, MAKEPAD_RUN_* env, ~/.makepad-run); libs/relax stays the internal library name. `makepad check` reports rustc-style diagnostics with rustc error codes and checks a whole workspace (`--workspace`). Runs on macOS, Linux, Windows and Android. makepad-remote: one tool schema for apps and the WM (app/1): grabs, captures, traces, frame profiles and tweaks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
136 lines
6.6 KiB
Rust
136 lines
6.6 KiB
Rust
// Run-time modules (hotreload.md §11, modules.rs, std::relax): a running program loads source
|
|
// files it has never seen, calls their exports through checked fn pointers, edits them live and
|
|
// unloads them; errors and panics in modules never take the program down.
|
|
// Run: `makepad test tests/modules/host.rs` (it finds its modules next to itself).
|
|
|
|
pub struct FxIn {
|
|
pub t: f32,
|
|
pub kick: f32,
|
|
}
|
|
|
|
/// Part of the host's API the modules call.
|
|
pub fn host_scale(x: f32) -> f32 {
|
|
x * 10.0
|
|
}
|
|
|
|
fn dir() -> String {
|
|
let f = file!();
|
|
match f.rfind('/') {
|
|
Some(i) => f[..i].to_string(),
|
|
None => ".".to_string(),
|
|
}
|
|
}
|
|
|
|
fn read(name: &str) -> String {
|
|
std::fs::read_to_string(format!("{}/{}", dir(), name)).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn modules() {
|
|
use std::relax::{Edit, Module};
|
|
let inp = FxIn { t: 1.5, kick: 0.25 };
|
|
// 1. load + call
|
|
let t0 = std::time::Instant::now();
|
|
let m = Module::load("fx_a", &format!("{}/fx_a.rs", dir()), &["test_crate"]).unwrap();
|
|
let ms = t0.elapsed().as_secs_f64() * 1e3;
|
|
eprintln!("cold load of fx_a: {:.2} ms", ms);
|
|
let frame: fn(&FxIn) -> f32 = m.get("frame").unwrap();
|
|
println!("out a {}", frame(&inp));
|
|
// 2. a wrongly typed export is refused
|
|
match m.get::<fn(f32) -> f32>("frame") {
|
|
Ok(_) => println!("out wrong type accepted"),
|
|
Err(e) => println!("out wrong type refused: {}", e.contains("is `fn(")),
|
|
}
|
|
match m.get::<fn(&FxIn) -> f32>("nope") {
|
|
Ok(_) => println!("out missing accepted"),
|
|
Err(e) => println!("out missing refused: {}", e.contains("no export")),
|
|
}
|
|
// 3. a body edit applies in place (the fn pointer taken before runs the new code)
|
|
match m.replace(&read("fx_a_v1.rs")) {
|
|
Ok(e) => println!("out edit {:?} -> {}", e, frame(&inp)),
|
|
Err(e) => println!("out edit refused: {}", e),
|
|
}
|
|
// 4. an edit with a type error is refused, the module keeps running
|
|
match m.replace(&read("fx_a_bad.rs")) {
|
|
Ok(e) => println!("out bad edit applied {:?}", e),
|
|
Err(e) => println!("out bad edit refused at line: {} -> {}", e.contains("fx_a.rs:"), frame(&inp)),
|
|
}
|
|
// 5. a module with a type error does not load
|
|
match Module::load("fx_bad", &format!("{}/fx_bad.rs", dir()), &["test_crate"]) {
|
|
Ok(_) => println!("out bad module loaded"),
|
|
Err(e) => println!("out bad module refused: {}", e.contains("fx_bad.rs:")),
|
|
}
|
|
// 6. a panic in a module is caught by the caller
|
|
let p = Module::load("fx_panic", &format!("{}/fx_panic.rs", dir()), &["test_crate"]).unwrap();
|
|
let pf: fn(&FxIn) -> f32 = p.get("frame").unwrap();
|
|
let r = std::panic::catch_unwind(|| pf(&inp));
|
|
println!("out panic caught: {}", r.is_err());
|
|
// 7. two modules at once; unload one
|
|
let b = Module::load("fx_b", &format!("{}/fx_b.rs", dir()), &["test_crate"]).unwrap();
|
|
let fb: fn(&FxIn) -> f32 = b.get("frame").unwrap();
|
|
println!("out a {} b {}", frame(&inp), fb(&inp));
|
|
b.unload();
|
|
match b.get::<fn(&FxIn) -> f32>("frame") {
|
|
Ok(_) => println!("out unloaded handle works"),
|
|
Err(e) => println!("out unloaded handle refused: {}", e.contains("unloaded")),
|
|
}
|
|
// 8. a layout edit asks for a fresh load
|
|
match m.replace(&read("fx_a_layout.rs")) {
|
|
Ok(e) => println!("out layout edit {:?}", e),
|
|
Err(e) => println!("out layout edit refused: {}", e),
|
|
}
|
|
// 9. guarded calls: a panic, a call that never returns (deadline) and unbounded recursion
|
|
// each end the call with an error; the program keeps running
|
|
let g = std::relax::guard(0, || pf(&inp));
|
|
println!("out guard panic: {}", g.is_err());
|
|
let h = Module::load("fx_hang", &format!("{}/fx_hang.rs", dir()), &["test_crate"]).unwrap();
|
|
let hf: fn(&FxIn) -> f32 = h.get("frame").unwrap();
|
|
let g = std::relax::guard(200, || hf(&inp));
|
|
println!("out guard hang: {}", g.is_err());
|
|
let d = Module::load("fx_deep", &format!("{}/fx_deep.rs", dir()), &["test_crate"]).unwrap();
|
|
let df: fn(&FxIn) -> f32 = d.get("frame").unwrap();
|
|
let g = std::thread::Builder::new().stack_size(1 << 20).spawn(move || std::relax::guard(0, || df(&FxIn { t: 1.0, kick: 0.0 })).is_err()).unwrap().join();
|
|
println!("out guard overflow: {:?}", g);
|
|
println!("out still running {}", frame(&inp));
|
|
// 10. load from text (embedded presets, AI edits): errors name the given path; modules can
|
|
// use each other (a kit module and an effect on top of it)
|
|
let kit = Module::load_source("kit_k", "presets/kit_k.rs", "pub fn boost(x: f32) -> f32 { x * 3.0 }\n", &[]).unwrap();
|
|
let fx = Module::load_source("fx_k", "presets/fx_k.rs", "use test_crate::FxIn;\npub fn frame(i: &FxIn) -> f32 { kit_k::boost(i.t) }\n", &["test_crate", "kit_k"]).unwrap();
|
|
let kf: fn(&FxIn) -> f32 = fx.get("frame").unwrap();
|
|
println!("out text module {}", kf(&inp));
|
|
match Module::load_source("fx_kbad", "presets/fx_kbad.rs", "pub fn frame(x: f32) -> f32 {\n x + \"a\"\n}\n", &[]) {
|
|
Ok(_) => println!("out bad text loaded"),
|
|
Err(e) => println!("out bad text refused at its path: {}", e.contains("presets/fx_kbad.rs:2:")),
|
|
}
|
|
let _ = kit;
|
|
match Module::load_source("fx_rr", "presets/fx_rr.rx", "pub fn frame(x: f32) -> f32 {\n let y = x * 2.0;\n y + 1.0\n}\n", &[]) {
|
|
Ok(m) => {
|
|
let f: fn(f32) -> f32 = m.get("frame").unwrap();
|
|
println!("out rr module {}", f(2.0));
|
|
}
|
|
Err(e) => println!("out rr module refused: {}", e),
|
|
}
|
|
// 11. escape attempts: every file in escape/ is refused at load with a sandbox error
|
|
let mut names: Vec<String> = std::fs::read_dir(format!("{}/escape", dir())).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect();
|
|
names.sort();
|
|
let mut refused = 0;
|
|
for n in &names {
|
|
match Module::load(&format!("esc_{}", n.trim_end_matches(".rs")), &format!("{}/escape/{}", dir(), n), &[]) {
|
|
Ok(_) => println!("out ESCAPE LOADED: {}", n),
|
|
Err(e) => {
|
|
if e.contains("sandbox") {
|
|
refused += 1;
|
|
} else {
|
|
println!("out escape {} refused without a sandbox reason: {}", n, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
println!("out escapes refused: {} of {}", refused, names.len());
|
|
// 12. the orphan rule: an impl of another crate's trait for another crate's type is refused
|
|
match Module::load("fx_orphan", &format!("{}/fx_orphan.rs", dir()), &[]) {
|
|
Ok(_) => println!("out ORPHAN IMPL LOADED"),
|
|
Err(e) => println!("out orphan impl refused: {}", e.contains("orphan rule")),
|
|
}
|
|
let _ = Edit::Patched;
|
|
}
|