@@ -11855,6 +11855,107 @@ type CompileImpl = unsafe extern "C" fn(
1185511855static COMPILE_IMPL: std::sync::atomic::AtomicPtr<c_void> =
1185611856 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
1185711857
11858+/// The successful result of running a compile closure: the value plus the
11859+/// optional body-type descriptor and the snippet's declared relations, to hand
11860+/// back through the FFI out-params.
11861+pub struct CompileOutcome {
11862+ pub value: *mut Value,
11863+ pub ty: Option<String>,
11864+ pub relations: Vec<String>,
11865+}
11866+
11867+/// Null the three out-param pairs so a compile error leaves them well-defined.
11868+///
11869+/// # Safety
11870+/// Each non-null pointer must be valid for writes of its payload.
11871+pub unsafe fn compile_out_params_init(pairs: [(*mut *mut u8, *mut usize); 3]) {
11872+ unsafe {
11873+ for (p, l) in pairs {
11874+ if !p.is_null() {
11875+ *p = std::ptr::null_mut();
11876+ }
11877+ if !l.is_null() {
11878+ *l = 0;
11879+ }
11880+ }
11881+ }
11882+}
11883+
11884+/// Hand a string back as a malloc'd buffer (C ABI — the caller frees it).
11885+///
11886+/// # Safety
11887+/// `ptr`/`len`, if non-null, must be valid for writes.
11888+pub unsafe fn compile_write_out(s: &str, ptr: *mut *mut u8, len: *mut usize) {
11889+ if ptr.is_null() || len.is_null() {
11890+ return;
11891+ }
11892+ let buf = unsafe { libc::malloc(s.len().max(1)) as *mut u8 };
11893+ if buf.is_null() {
11894+ return;
11895+ }
11896+ unsafe {
11897+ std::ptr::copy_nonoverlapping(s.as_ptr(), buf, s.len());
11898+ *ptr = buf;
11899+ *len = s.len();
11900+ }
11901+}
11902+
11903+/// Run a `base.compile` compile+run closure behind the FFI boundary, containing
11904+/// any panic. `compile` is the compiler- and backend-specific work; it returns
11905+/// `Ok(Some(outcome))` on success, `Ok(None)` on a clean compile error, and any
11906+/// panic is caught here. On success the outcome's ty/relations are written to
11907+/// the out-params and the value returned; on error or panic the (already
11908+/// nulled) out-params get the message in `out_err` and null is returned, so the
11909+/// runtime surfaces a `Result.Err` instead of unwinding into the host.
11910+///
11911+/// This is the single containment point shared by every `knot_compile_impl`
11912+/// (the compiler binary's and the `knot-compile-rt` one a compiled program
11913+/// links), so the two cannot drift apart.
11914+///
11915+/// # Safety
11916+/// `db` must be a live db handle; the out-param pointers, if non-null, must be
11917+/// valid for writes of their payload. They must already be nulled via
11918+/// [`compile_out_params_init`] by the caller.
11919+#[allow(clippy::too_many_arguments)]
11920+pub unsafe fn run_compile_contained(
11921+ db: *mut c_void,
11922+ out_ty_ptr: *mut *mut u8,
11923+ out_ty_len: *mut usize,
11924+ out_rels_ptr: *mut *mut u8,
11925+ out_rels_len: *mut usize,
11926+ out_err_ptr: *mut *mut u8,
11927+ out_err_len: *mut usize,
11928+ compile: impl FnOnce(*mut c_void) -> Option<CompileOutcome>,
11929+) -> *mut Value {
11930+ // The raw pointers are !UnwindSafe, so assert it; the closure only reads
11931+ // them and the out-params were nulled by the caller, so no broken state is
11932+ // observable across a caught panic.
11933+ let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| compile(db)));
11934+ match r {
11935+ Ok(Some(outcome)) => {
11936+ if let Some(ty) = &outcome.ty {
11937+ unsafe { compile_write_out(ty, out_ty_ptr, out_ty_len) };
11938+ }
11939+ let csv = outcome.relations.join(",");
11940+ unsafe { compile_write_out(&csv, out_rels_ptr, out_rels_len) };
11941+ outcome.value
11942+ }
11943+ Ok(None) => std::ptr::null_mut(),
11944+ Err(payload) => {
11945+ let msg = payload
11946+ .downcast_ref::<&str>()
11947+ .map(|s| (*s).to_string())
11948+ .or_else(|| payload.downcast_ref::<String>().cloned())
11949+ .unwrap_or_else(|| "unknown panic".to_string());
11950+ unsafe {
11951+ compile_write_out(&format!("compile panicked: {msg}"), out_err_ptr, out_err_len)
11952+ };
11953+ std::ptr::null_mut()
11954+ }
11955+ }
11956+}
11957+
11958+
1185811959/// Registered by knot-compile-rt at program startup to install the compile
1185911960/// implementation. Called from the generated program's `main`.
1186011961#[unsafe(no_mangle)]
@@ -24986,3 +25087,126 @@ mod show_tests {
2498625087 );
2498725088 }
2498825089}
25090+
25091+#[cfg(test)]
25092+mod compile_containment_tests {
25093+ //! `base.compile` runs in the host's process; a panic in compilation or the
25094+ //! generated snippet must surface as an Err, never unwind across `extern
25095+ //! "C"`. These exercise the shared containment point both `knot_compile_impl`
25096+ //! entry points delegate to.
25097+ use super::*;
25098+
25099+ /// Out-param slots for a contained run, mirroring the FFI caller.
25100+ struct Outs {
25101+ ty: *mut u8,
25102+ ty_len: usize,
25103+ rels: *mut u8,
25104+ rels_len: usize,
25105+ err: *mut u8,
25106+ err_len: usize,
25107+ }
25108+ impl Outs {
25109+ fn new() -> Self {
25110+ Outs {
25111+ ty: std::ptr::null_mut(),
25112+ ty_len: 0,
25113+ rels: std::ptr::null_mut(),
25114+ rels_len: 0,
25115+ err: std::ptr::null_mut(),
25116+ err_len: 0,
25117+ }
25118+ }
25119+ unsafe fn init(&mut self) {
25120+ unsafe {
25121+ compile_out_params_init([
25122+ (&mut self.ty, &mut self.ty_len),
25123+ (&mut self.rels, &mut self.rels_len),
25124+ (&mut self.err, &mut self.err_len),
25125+ ]);
25126+ }
25127+ }
25128+ fn err_string(&self) -> Option<String> {
25129+ if self.err.is_null() {
25130+ return None;
25131+ }
25132+ let bytes = unsafe { std::slice::from_raw_parts(self.err, self.err_len) };
25133+ let s = String::from_utf8_lossy(bytes).into_owned();
25134+ unsafe { libc::free(self.err as *mut c_void) };
25135+ Some(s)
25136+ }
25137+ }
25138+
25139+ #[test]
25140+ fn success_writes_out_params_and_returns_value() {
25141+ let mut o = Outs::new();
25142+ unsafe { o.init() };
25143+ let sentinel = 0xdeadbeefusize as *mut Value;
25144+ let v = unsafe {
25145+ run_compile_contained(
25146+ std::ptr::null_mut(),
25147+ &mut o.ty,
25148+ &mut o.ty_len,
25149+ &mut o.rels,
25150+ &mut o.rels_len,
25151+ &mut o.err,
25152+ &mut o.err_len,
25153+ |_| Some(CompileOutcome {
25154+ value: sentinel,
25155+ ty: Some("Int".to_string()),
25156+ relations: vec!["a\t{..}".to_string()],
25157+ }),
25158+ )
25159+ };
25160+ assert_eq!(v, sentinel);
25161+ assert!(!o.ty.is_null(), "ty written");
25162+ assert!(!o.rels.is_null(), "relations written");
25163+ assert!(o.err.is_null(), "no error on success");
25164+ unsafe {
25165+ libc::free(o.ty as *mut c_void);
25166+ libc::free(o.rels as *mut c_void);
25167+ }
25168+ }
25169+
25170+ #[test]
25171+ fn clean_compile_error_returns_null() {
25172+ let mut o = Outs::new();
25173+ unsafe { o.init() };
25174+ let v = unsafe {
25175+ run_compile_contained(
25176+ std::ptr::null_mut(),
25177+ &mut o.ty,
25178+ &mut o.ty_len,
25179+ &mut o.rels,
25180+ &mut o.rels_len,
25181+ &mut o.err,
25182+ &mut o.err_len,
25183+ |_| None,
25184+ )
25185+ };
25186+ assert!(v.is_null(), "compile error returns null");
25187+ }
25188+
25189+ #[test]
25190+ fn panic_is_contained_and_surfaced_as_err() {
25191+ let mut o = Outs::new();
25192+ unsafe { o.init() };
25193+ let v = unsafe {
25194+ run_compile_contained(
25195+ std::ptr::null_mut(),
25196+ &mut o.ty,
25197+ &mut o.ty_len,
25198+ &mut o.rels,
25199+ &mut o.rels_len,
25200+ &mut o.err,
25201+ &mut o.err_len,
25202+ |_| -> Option<CompileOutcome> { panic!("boom in codegen") },
25203+ )
25204+ };
25205+ assert!(v.is_null(), "panic returns null, does not unwind");
25206+ let err = o.err_string().expect("panic message written to out_err");
25207+ assert!(
25208+ err.contains("compile panicked") && err.contains("boom in codegen"),
25209+ "panic payload surfaced: {err}"
25210+ );
25211+ }
25212+}