@@ -20,12 +20,19 @@ fn db() -> *mut std::ffi::c_void {
2020 knot_runtime::knot_db_open(PATH.as_ptr(), PATH.len())
2121}
2222
23+/// The runtime's code-free scan, passed so the JIT frees modules whose result
24+/// is pure data. These tests read the result back as a value (never call a
25+/// returned lambda), so a freed module is never re-entered.
26+unsafe extern "C" fn scan(v: *mut std::ffi::c_void) -> i32 {
27+ knot_runtime::knot_value_is_code_free(v.cast())
28+}
29+
2330#[test]
2431fn jit_compiles_and_runs_pure_int() {
2532 init_compile_rt();
2633 let db = db();
2734 let out =
28− compile_and_run("base.println (base.show (40 + 2))", db, None).expect("JIT compile failed");
35+ compile_and_run("base.println (base.show (40 + 2))", db, None, Some(scan)).expect("JIT compile failed");
2936 assert!(!out.value.is_null(), "JIT produced a null Value");
3037}
3138
@@ -33,7 +40,7 @@ fn jit_compiles_and_runs_pure_int() {
3340fn jit_compiles_and_runs_closure() {
3441 init_compile_rt();
3542 let db = db();
36− let out = compile_and_run("base.println (base.show ((\\x -> x * 2 + 1) 10))", db, None)
43+ let out = compile_and_run("base.println (base.show ((\\x -> x * 2 + 1) 10))", db, None, Some(scan))
3744 .expect("JIT compile failed");
3845 assert!(!out.value.is_null());
3946}
@@ -42,7 +49,7 @@ fn jit_compiles_and_runs_closure() {
4249fn jit_rejects_type_error() {
4350 init_compile_rt();
4451 let db = db();
45− let res = compile_and_run("1 + \"not an int\"", db, None);
52+ let res = compile_and_run("1 + \"not an int\"", db, None, Some(scan));
4653 assert!(res.is_err(), "type error should fail compilation");
4754}
4855
@@ -53,7 +60,7 @@ fn jit_rejects_type_error() {
5360fn jit_surfaces_scalar_body_type() {
5461 init_compile_rt();
5562 let db = db();
56− let out = compile_and_run("42", db, None).expect("JIT compile failed");
63+ let out = compile_and_run("42", db, None, Some(scan)).expect("JIT compile failed");
5764 assert_eq!(out.ty.as_deref(), Some("Int"));
5865}
5966
@@ -61,7 +68,7 @@ fn jit_surfaces_scalar_body_type() {
6168fn jit_surfaces_polymorphic_lambda_type() {
6269 init_compile_rt();
6370 let db = db();
64− let out = compile_and_run("\\x -> x", db, None).expect("JIT compile failed");
71+ let out = compile_and_run("\\x -> x", db, None, Some(scan)).expect("JIT compile failed");
6572 // Identity is polymorphic: a type variable on both sides.
6673 assert_eq!(out.ty.as_deref(), Some("a -> a"));
6774}
@@ -70,10 +77,51 @@ fn jit_surfaces_polymorphic_lambda_type() {
7077fn jit_surfaces_text_body_type() {
7178 init_compile_rt();
7279 let db = db();
73− let out = compile_and_run("\"hello\"", db, None).expect("JIT compile failed");
80+ let out = compile_and_run("\"hello\"", db, None, Some(scan)).expect("JIT compile failed");
7481 assert_eq!(out.ty.as_deref(), Some("Text"));
7582}
7683
84+/// A pure-data result lets the JIT free the module (the scan reports
85+/// code-free). The value must stay valid after the free — its content is pure
86+/// data, not code. This exercises the `free_memory` path directly.
87+#[test]
88+fn jit_frees_module_for_pure_data_result() {
89+ use knot_runtime::Value;
90+ init_compile_rt();
91+ let db = db();
92+ let out = compile_and_run("{n 42 label \"answer\"}", db, None, Some(scan))
93+ .expect("JIT compile failed");
94+ // Read the record AFTER the module was freed inside compile_and_run.
95+ match unsafe { &*out.value.cast::<Value>() } {
96+ Value::Record(fields) => {
97+ assert_eq!(fields.len(), 2);
98+ let n = fields.iter().find(|f| &*f.name == "n").expect("n field");
99+ match unsafe { &*n.value } {
100+ Value::Int(n) => assert_eq!(*n, 42),
101+ _ => panic!("n is not an Int"),
102+ }
103+ }
104+ _ => panic!("expected a record"),
105+ }
106+}
107+
108+/// A function-returning result keeps the module alive: the scan reports the
109+/// value holds code, so the JIT must NOT free it. The surfaced value is a
110+/// callable Function whose `fn_ptr` points into the (still-mapped) module.
111+#[test]
112+fn jit_keeps_module_for_function_result() {
113+ use knot_runtime::Value;
114+ init_compile_rt();
115+ let db = db();
116+ let out = compile_and_run("\\x -> x + 1", db, None, Some(scan)).expect("JIT compile failed");
117+ // The scan saw a Function, so the module was kept (leaked) — and the value
118+ // is a real Function value carrying a non-null code pointer.
119+ match unsafe { &*out.value.cast::<Value>() } {
120+ Value::Function(f) => assert!(!f.fn_ptr.is_null(), "function must hold a code ptr"),
121+ _ => panic!("expected a Function"),
122+ }
123+}
124+
77125/// A user ADT extracted in the JIT renders self-contained: the declaration is
78126/// inlined (`with {…} (Type.Ctor …)`), matching the compiled-binary path.
79127/// Regression: the ctor-decl registration lived in the C `main` prologue,
@@ -89,6 +137,7 @@ fn jit_extract_user_adt_is_self_contained() {
89137 "with {\nPriority Low {} High {}\n}\n(base.extract (Priority.Low {}))",
90138 db,
91139 None,
140+ Some(scan),
92141 )
93142 .expect("JIT compile failed");
94143 let text = match unsafe { &*out.value.cast::<Value>() } {