Skip to main content

logicaffeine_compile/codegen/
marshal.rs

1use std::collections::HashSet;
2use std::fmt::Write;
3
4use crate::analysis::registry::TypeRegistry;
5use crate::analysis::types::RustNames;
6use crate::ast::stmt::{Stmt, TypeExpr};
7use crate::intern::{Interner, Symbol};
8
9use super::context::{RefinementContext, analyze_variable_capabilities, replace_word};
10use super::detection::{is_result_type, collect_mutable_vars};
11use super::types::codegen_type_expr;
12use super::{
13    CAbiClass, classify_type_for_c_abi,
14    codegen_assertion, codegen_stmt,
15    try_emit_vec_fill_pattern, try_emit_for_range_pattern, try_emit_swap_pattern,
16    try_emit_prefix_reverse,
17};
18
19pub(super) fn is_text_type(ty: &TypeExpr, interner: &Interner) -> bool {
20    match ty {
21        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
22            matches!(interner.resolve(*sym), "Text" | "String")
23        }
24        TypeExpr::Refinement { base, .. } => is_text_type(base, interner),
25        _ => false,
26    }
27}
28
29/// FFI: `Char` is declared `uint32_t` in the C header, so it must cross the
30/// boundary as `u32` (validated via `char::from_u32`), never as a bare Rust
31/// `char` — an out-of-range `u32` from C materialized as a `char` is UB.
32pub(super) fn is_char_type(ty: &TypeExpr, interner: &Interner) -> bool {
33    match ty {
34        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
35            interner.resolve(*sym) == "Char"
36        }
37        TypeExpr::Refinement { base, .. } => is_char_type(base, interner),
38        _ => false,
39    }
40}
41
42/// FFI: Map a TypeExpr to its C ABI representation.
43/// Primitives pass through; Text becomes raw pointer.
44pub(super) fn map_type_to_c_abi(ty: &TypeExpr, interner: &Interner, is_return: bool) -> String {
45    if is_text_type(ty, interner) {
46        if is_return {
47            "*mut std::os::raw::c_char".to_string()
48        } else {
49            "*const std::os::raw::c_char".to_string()
50        }
51    } else {
52        codegen_type_expr(ty, interner)
53    }
54}
55
56/// Whether `ty` is a scalar that crosses the AOT-native boundary BY VALUE — the sound,
57/// hazard-free subset (no `Rc`/pointer marshaling). `Seq`/`Map`/`Text` are deferred:
58/// they would cross `*mut LogosSeq<T>` etc., which needs the borrow-not-own protocol
59/// (HOTSWAP §Axis-3 B4). A function outside this subset gets no shim and falls through.
60pub(super) fn is_native_scalar(ty: &TypeExpr, interner: &Interner) -> bool {
61    match ty {
62        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
63            matches!(interner.resolve(*sym), "Int" | "Float" | "Bool")
64        }
65        TypeExpr::Refinement { base, .. } => is_native_scalar(base, interner),
66        _ => false,
67    }
68}
69
70/// Emit the AOT-native export shim (HOTSWAP §Axis-3): a thin `#[no_mangle] extern "C"`
71/// calling-convention wrapper over the inner Rust fn that crosses our ACTUAL values —
72/// scalars BY VALUE — with NO `CString`/handle marshaling (unlike
73/// [`codegen_c_export_with_marshaling`]). The interpreter marshals VM `Value`s to these
74/// scalar args, calls the loaded symbol, and re-boxes the scalar result.
75///
76/// Returns `None` unless every param AND the return are in the sound scalar subset
77/// ([`is_native_scalar`]): the caller then falls through to VM+JIT, so a function the
78/// shim cannot represent simply isn't AOT-native — no gap at the seam.
79pub fn codegen_native_tier_export(
80    name: Symbol,
81    params: &[(Symbol, &TypeExpr)],
82    return_type: Option<&TypeExpr>,
83    interner: &Interner,
84) -> Option<String> {
85    if !params.iter().all(|(_, t)| is_native_scalar(t, interner)) {
86        return None;
87    }
88    if let Some(rt) = return_type {
89        if !is_native_scalar(rt, interner) {
90            return None;
91        }
92    }
93    let names = RustNames::new(interner);
94    let inner = names.ident(name);
95    let export = format!("logos_native_{}", names.raw(name));
96    let params_sig = params
97        .iter()
98        .map(|(p, t)| format!("{}: {}", interner.resolve(*p), codegen_type_expr(t, interner)))
99        .collect::<Vec<_>>()
100        .join(", ");
101    let args = params
102        .iter()
103        .map(|(p, _)| interner.resolve(*p).to_string())
104        .collect::<Vec<_>>()
105        .join(", ");
106    let ret = return_type
107        .map(|t| codegen_type_expr(t, interner))
108        .unwrap_or_else(|| "()".to_string());
109    let ret_sig = if ret == "()" { String::new() } else { format!(" -> {ret}") };
110    let mut out = String::new();
111    let _ = writeln!(out, "#[no_mangle]");
112    let _ = writeln!(out, "pub extern \"C\" fn {export}({params_sig}){ret_sig} {{");
113    let _ = writeln!(out, "    {inner}({args})");
114    let _ = writeln!(out, "}}");
115    Some(out)
116}
117
118/// FFI: Generate a C-exported function with Universal ABI marshaling.
119///
120/// Produces: 1) an inner function with normal Rust types, 2) a #[no_mangle] extern "C" wrapper.
121///
122/// The wrapper handles:
123/// - Text param/return marshaling (*const c_char <-> String)
124/// - Reference type params/returns via opaque LogosHandle
125/// - Result<T, E> returns via status code + out-parameter
126/// - Refinement type boundary guards
127pub(super) fn codegen_c_export_with_marshaling(
128    name: Symbol,
129    params: &[(Symbol, &TypeExpr)],
130    body: &[Stmt],
131    return_type: Option<&TypeExpr>,
132    interner: &Interner,
133    lww_fields: &HashSet<(String, String)>,
134    mv_fields: &HashSet<(String, String)>,
135    async_functions: &HashSet<Symbol>,
136    boxed_fields: &HashSet<(String, String, String)>,
137    registry: &crate::analysis::registry::TypeRegistry,
138    type_env: &crate::analysis::types::TypeEnv,
139) -> String {
140    let mut output = String::new();
141    let names = RustNames::new(interner);
142    let raw_name = names.raw(name);
143    // All exported C ABI symbols use the `logos_` prefix to avoid keyword
144    // collisions in target languages (C, Python, JS, etc.) and to provide
145    // a consistent namespace for the generated library.
146    let func_name = format!("logos_{}", raw_name);
147    let inner_name = names.ident(name);
148
149    // Classify return type
150    let has_ref_return = return_type.map_or(false, |ty| {
151        classify_type_for_c_abi(ty, interner, registry) == CAbiClass::ReferenceType
152    });
153    let has_result_return = return_type.map_or(false, |ty| is_result_type(ty, interner));
154    let has_text_return = return_type.map_or(false, |t| is_text_type(t, interner));
155
156    // Determine if we need status-code return pattern
157    // Status code is needed when the return value requires an out-parameter (ref/text/result)
158    // or when refinement parameters need validation error paths.
159    // Ref-type parameters do NOT force status code — catch_unwind handles invalid handle panics.
160    let uses_status_code = has_ref_return || has_result_return || has_text_return
161        || params.iter().any(|(_, ty)| matches!(ty, TypeExpr::Refinement { .. }));
162
163    // 1) Emit the inner function with normal Rust types
164    let inner_params: Vec<String> = params.iter()
165        .map(|(pname, ptype)| {
166            format!("{}: {}", interner.resolve(*pname), codegen_type_expr(ptype, interner))
167        })
168        .collect();
169    let inner_ret = return_type.map(|t| codegen_type_expr(t, interner));
170
171    let inner_sig = if let Some(ref ret) = inner_ret {
172        if ret != "()" {
173            format!("fn {}({}) -> {}", inner_name, inner_params.join(", "), ret)
174        } else {
175            format!("fn {}({})", inner_name, inner_params.join(", "))
176        }
177    } else {
178        format!("fn {}({})", inner_name, inner_params.join(", "))
179    };
180
181    writeln!(output, "{} {{", inner_sig).unwrap();
182    let func_mutable_vars = collect_mutable_vars(body);
183    let mut func_ctx = RefinementContext::new();
184    let mut func_synced_vars = HashSet::new();
185    let func_var_caps = analyze_variable_capabilities(body, interner);
186    for (param_name, param_type) in params {
187        let type_name = codegen_type_expr(param_type, interner);
188        func_ctx.register_variable_type(*param_name, type_name);
189    }
190    let func_pipe_vars = HashSet::new();
191    {
192        let stmt_refs: Vec<&Stmt> = body.iter().collect();
193        let mut si = 0;
194        while si < stmt_refs.len() {
195            if let Some((code, skip)) = try_emit_vec_fill_pattern(&stmt_refs, si, interner, 1, &mut func_ctx) {
196                output.push_str(&code);
197                si += 1 + skip;
198                continue;
199            }
200            if let Some((code, skip)) = try_emit_for_range_pattern(&stmt_refs, si, interner, 1, &func_mutable_vars, &mut func_ctx, lww_fields, mv_fields, &mut func_synced_vars, &func_var_caps, async_functions, &func_pipe_vars, boxed_fields, registry, type_env) {
201                output.push_str(&code);
202                si += 1 + skip;
203                continue;
204            }
205            if let Some((code, skip)) = try_emit_prefix_reverse(&stmt_refs, si, interner, 1, func_ctx.get_variable_types()) {
206                output.push_str(&code);
207                si += 1 + skip;
208                continue;
209            }
210            if let Some((code, skip)) = try_emit_swap_pattern(&stmt_refs, si, interner, 1, func_ctx.get_variable_types(), func_ctx.oracle()) {
211                output.push_str(&code);
212                si += 1 + skip;
213                continue;
214            }
215            output.push_str(&codegen_stmt(stmt_refs[si], interner, 1, &func_mutable_vars, &mut func_ctx, lww_fields, mv_fields, &mut func_synced_vars, &func_var_caps, async_functions, &func_pipe_vars, boxed_fields, registry, type_env));
216            si += 1;
217        }
218    }
219    writeln!(output, "}}\n").unwrap();
220
221    // 2) Build the C ABI wrapper parameters
222    let mut c_params: Vec<String> = Vec::new();
223
224    for (pname, ptype) in params.iter() {
225        let pn = names.ident(*pname);
226        if classify_type_for_c_abi(ptype, interner, registry) == CAbiClass::ReferenceType {
227            c_params.push(format!("{}: LogosHandle", pn));
228        } else if is_text_type(ptype, interner) {
229            c_params.push(format!("{}: *const std::os::raw::c_char", pn));
230        } else if is_char_type(ptype, interner) {
231            // Char crosses the ABI as uint32_t; validated to a `char` in the body.
232            c_params.push(format!("{}: u32", pn));
233        } else {
234            c_params.push(format!("{}: {}", pn, codegen_type_expr(ptype, interner)));
235        }
236    }
237
238    // Add out-parameter if using status-code pattern with return value
239    if uses_status_code {
240        if let Some(ret_ty) = return_type {
241            if has_result_return {
242                // Result<T, E>: out param for the Ok(T) type
243                if let TypeExpr::Generic { params: ref rparams, .. } = ret_ty {
244                    if !rparams.is_empty() {
245                        let ok_ty = &rparams[0];
246                        if classify_type_for_c_abi(ok_ty, interner, registry) == CAbiClass::ReferenceType {
247                            c_params.push("out: *mut LogosHandle".to_string());
248                        } else if is_text_type(ok_ty, interner) {
249                            c_params.push("out: *mut *mut std::os::raw::c_char".to_string());
250                        } else {
251                            let ty_str = codegen_type_expr(ok_ty, interner);
252                            c_params.push(format!("out: *mut {}", ty_str));
253                        }
254                    }
255                }
256            } else if has_ref_return {
257                c_params.push("out: *mut LogosHandle".to_string());
258            } else if has_text_return {
259                c_params.push("out: *mut *mut std::os::raw::c_char".to_string());
260            }
261        }
262    }
263
264    // Build the wrapper signature
265    let c_sig = if uses_status_code {
266        format!("pub extern \"C\" fn {}({}) -> LogosStatus", func_name, c_params.join(", "))
267    } else if has_text_return {
268        format!("pub extern \"C\" fn {}({}) -> *mut std::os::raw::c_char", func_name, c_params.join(", "))
269    } else if let Some(ret_ty) = return_type {
270        // Char returns cross the ABI as uint32_t (see is_char_type).
271        let ret_str = if is_char_type(ret_ty, interner) {
272            "u32".to_string()
273        } else {
274            codegen_type_expr(ret_ty, interner)
275        };
276        if ret_str != "()" {
277            format!("pub extern \"C\" fn {}({}) -> {}", func_name, c_params.join(", "), ret_str)
278        } else {
279            format!("pub extern \"C\" fn {}({})", func_name, c_params.join(", "))
280        }
281    } else {
282        format!("pub extern \"C\" fn {}({})", func_name, c_params.join(", "))
283    };
284
285    writeln!(output, "#[no_mangle]").unwrap();
286    writeln!(output, "{} {{", c_sig).unwrap();
287
288    // 3) Marshal parameters
289    let call_args: Vec<String> = params.iter()
290        .map(|(pname, ptype)| {
291            let pname_str = names.ident(*pname);
292            if classify_type_for_c_abi(ptype, interner, registry) == CAbiClass::ReferenceType {
293                // Look up handle in registry, dereference, and clone for inner.
294                // A NULL/stale handle must surface as an error and return
295                // gracefully — NOT panic via `.expect()` OUTSIDE the catch_unwind
296                // boundary, which would unwind across the `extern "C"` frame (UB /
297                // abort). Mirrors the graceful accessors (codegen/ffi.rs).
298                let rust_ty = codegen_type_expr(ptype, interner);
299                let err_return = if uses_status_code {
300                    "return LogosStatus::InvalidHandle;".to_string()
301                } else if return_type.map_or(false, |t| codegen_type_expr(t, interner) != "()") {
302                    "return Default::default();".to_string()
303                } else {
304                    "return;".to_string()
305                };
306                writeln!(output, "    let {pn} = {{", pn = pname_str).unwrap();
307                writeln!(output, "        let __id = {pn} as u64;", pn = pname_str).unwrap();
308                writeln!(output, "        let __reg = logos_handle_registry().lock().unwrap_or_else(|e| e.into_inner());").unwrap();
309                writeln!(output, "        match __reg.deref(__id) {{").unwrap();
310                writeln!(output, "            Some(__ptr) => {{ let __v = unsafe {{ &*(__ptr as *const {ty}) }}.clone(); drop(__reg); __v }}", ty = rust_ty).unwrap();
311                writeln!(output, "            None => {{ drop(__reg); logos_set_last_error(\"InvalidHandle: handle '{pn}' not found in registry\".to_string()); {ret} }}", pn = pname_str, ret = err_return).unwrap();
312                writeln!(output, "        }}").unwrap();
313                writeln!(output, "    }};").unwrap();
314            } else if is_text_type(ptype, interner) {
315                // Null-safety: check for NULL *const c_char before CStr::from_ptr
316                if uses_status_code {
317                    writeln!(output, "    if {pn}.is_null() {{ logos_set_last_error(\"NullPointer: text parameter '{pn}' is null\".to_string()); return LogosStatus::NullPointer; }}",
318                        pn = pname_str).unwrap();
319                    writeln!(output, "    let {pn} = unsafe {{ std::ffi::CStr::from_ptr({pn}).to_string_lossy().into_owned() }};",
320                        pn = pname_str).unwrap();
321                } else {
322                    // Non-status-code function: substitute empty string for NULL
323                    writeln!(output, "    let {pn} = if {pn}.is_null() {{ String::new() }} else {{ unsafe {{ std::ffi::CStr::from_ptr({pn}).to_string_lossy().into_owned() }} }};",
324                        pn = pname_str).unwrap();
325                }
326            } else if is_char_type(ptype, interner) {
327                // Validate the incoming uint32_t scalar; an out-of-range value is
328                // UB if materialized as a Rust `char`. Use U+FFFD on invalid input.
329                writeln!(output, "    let {pn} = char::from_u32({pn}).unwrap_or('\\u{{FFFD}}');",
330                    pn = pname_str).unwrap();
331            }
332            pname_str.to_string()
333        })
334        .collect();
335
336    // 4) Emit refinement guards for parameters
337    for (pname, ptype) in params.iter() {
338        if let TypeExpr::Refinement { base: _, var, predicate } = ptype {
339            let pname_str = interner.resolve(*pname);
340            let bound = interner.resolve(*var);
341            let assertion = codegen_assertion(predicate, interner);
342            let check = if bound == pname_str {
343                assertion
344            } else {
345                replace_word(&assertion, bound, pname_str)
346            };
347            writeln!(output, "    if !({}) {{", check).unwrap();
348            writeln!(output, "        logos_set_last_error(format!(\"Refinement violation: expected {check}, got {pn} = {{}}\", {pn}));",
349                check = check, pn = pname_str).unwrap();
350            writeln!(output, "        return LogosStatus::RefinementViolation;").unwrap();
351            writeln!(output, "    }}").unwrap();
352        }
353    }
354
355    // 4b) Null out-parameter check (before catch_unwind to avoid calling inner fn)
356    if uses_status_code && (has_ref_return || has_text_return || has_result_return) {
357        writeln!(output, "    if out.is_null() {{ logos_set_last_error(\"NullPointer: output parameter is null\".to_string()); return LogosStatus::NullPointer; }}").unwrap();
358    }
359
360    // 5) Determine panic default for catch_unwind error arm
361    let panic_default = if uses_status_code {
362        "LogosStatus::ThreadPanic"
363    } else if has_text_return {
364        "std::ptr::null_mut()"
365    } else if return_type.map_or(false, |t| codegen_type_expr(t, interner) != "()") {
366        "Default::default()"
367    } else {
368        "" // void function
369    };
370
371    // 6) Open catch_unwind panic boundary
372    writeln!(output, "    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {{").unwrap();
373
374    // 7) Call inner and marshal return (inside catch_unwind closure)
375    if uses_status_code {
376        if has_result_return {
377            // Result<T, E>: match on Ok/Err
378            writeln!(output, "    match {}({}) {{", inner_name, call_args.join(", ")).unwrap();
379            writeln!(output, "        Ok(val) => {{").unwrap();
380
381            if let Some(TypeExpr::Generic { params: ref rparams, .. }) = return_type {
382                if !rparams.is_empty() {
383                    let ok_ty = &rparams[0];
384                    if classify_type_for_c_abi(ok_ty, interner, registry) == CAbiClass::ReferenceType {
385                        writeln!(output, "            let __ptr = Box::into_raw(Box::new(val)) as usize;").unwrap();
386                        writeln!(output, "            let mut __reg = logos_handle_registry().lock().unwrap_or_else(|e| e.into_inner());").unwrap();
387                        writeln!(output, "            let (__id, _) = __reg.register(__ptr);").unwrap();
388                        writeln!(output, "            unsafe {{ *out = __id as LogosHandle; }}").unwrap();
389                    } else if is_text_type(ok_ty, interner) {
390                        writeln!(output, "            match std::ffi::CString::new(val) {{").unwrap();
391                        writeln!(output, "                Ok(cstr) => unsafe {{ *out = cstr.into_raw(); }},").unwrap();
392                        writeln!(output, "                Err(_) => {{").unwrap();
393                        writeln!(output, "                    logos_set_last_error(\"Return value contains null byte\".to_string());").unwrap();
394                        writeln!(output, "                    return LogosStatus::ContainsNullByte;").unwrap();
395                        writeln!(output, "                }}").unwrap();
396                        writeln!(output, "            }}").unwrap();
397                    } else {
398                        writeln!(output, "            unsafe {{ *out = val; }}").unwrap();
399                    }
400                }
401            }
402
403            writeln!(output, "            LogosStatus::Ok").unwrap();
404            writeln!(output, "        }}").unwrap();
405            writeln!(output, "        Err(e) => {{").unwrap();
406            writeln!(output, "            logos_set_last_error(format!(\"{{}}\", e));").unwrap();
407            writeln!(output, "            LogosStatus::Error").unwrap();
408            writeln!(output, "        }}").unwrap();
409            writeln!(output, "    }}").unwrap();
410        } else if has_ref_return {
411            // Reference type return -> box, register in handle registry, and write to out-parameter
412            writeln!(output, "    let result = {}({});", inner_name, call_args.join(", ")).unwrap();
413            writeln!(output, "    let __ptr = Box::into_raw(Box::new(result)) as usize;").unwrap();
414            writeln!(output, "    let mut __reg = logos_handle_registry().lock().unwrap_or_else(|e| e.into_inner());").unwrap();
415            writeln!(output, "    let (__id, _) = __reg.register(__ptr);").unwrap();
416            writeln!(output, "    unsafe {{ *out = __id as LogosHandle; }}").unwrap();
417            writeln!(output, "    LogosStatus::Ok").unwrap();
418        } else if has_text_return {
419            // Text return with status code -> write to out-parameter
420            writeln!(output, "    let result = {}({});", inner_name, call_args.join(", ")).unwrap();
421            writeln!(output, "    match std::ffi::CString::new(result) {{").unwrap();
422            writeln!(output, "        Ok(cstr) => {{").unwrap();
423            writeln!(output, "            unsafe {{ *out = cstr.into_raw(); }}").unwrap();
424            writeln!(output, "            LogosStatus::Ok").unwrap();
425            writeln!(output, "        }}").unwrap();
426            writeln!(output, "        Err(_) => {{").unwrap();
427            writeln!(output, "            logos_set_last_error(\"Return value contains null byte\".to_string());").unwrap();
428            writeln!(output, "            LogosStatus::ContainsNullByte").unwrap();
429            writeln!(output, "        }}").unwrap();
430            writeln!(output, "    }}").unwrap();
431        } else {
432            // No return value but status code (e.g., refinement-only)
433            writeln!(output, "    {}({});", inner_name, call_args.join(", ")).unwrap();
434            writeln!(output, "    LogosStatus::Ok").unwrap();
435        }
436    } else if has_text_return {
437        // Text-only marshaling (legacy path, no status code)
438        writeln!(output, "    let result = {}({});", inner_name, call_args.join(", ")).unwrap();
439        writeln!(output, "    match std::ffi::CString::new(result) {{").unwrap();
440        writeln!(output, "        Ok(cstr) => cstr.into_raw(),").unwrap();
441        writeln!(output, "        Err(_) => {{ logos_set_last_error(\"Return value contains null byte\".to_string()); std::ptr::null_mut() }}").unwrap();
442        writeln!(output, "    }}").unwrap();
443    } else if let Some(ret_ty) = return_type {
444        // A Char return crosses the ABI as uint32_t.
445        if is_char_type(ret_ty, interner) {
446            writeln!(output, "    {}({}) as u32", inner_name, call_args.join(", ")).unwrap();
447        } else {
448            writeln!(output, "    {}({})", inner_name, call_args.join(", ")).unwrap();
449        }
450    } else {
451        writeln!(output, "    {}({})", inner_name, call_args.join(", ")).unwrap();
452    }
453
454    // 8) Close catch_unwind with panic handler
455    writeln!(output, "    }})) {{").unwrap();
456    writeln!(output, "        Ok(__v) => __v,").unwrap();
457    writeln!(output, "        Err(__panic) => {{").unwrap();
458    writeln!(output, "            let __msg = if let Some(s) = __panic.downcast_ref::<String>() {{ s.clone() }} else if let Some(s) = __panic.downcast_ref::<&str>() {{ s.to_string() }} else {{ \"Unknown panic\".to_string() }};").unwrap();
459    writeln!(output, "            logos_set_last_error(__msg);").unwrap();
460    if !panic_default.is_empty() {
461        writeln!(output, "            {}", panic_default).unwrap();
462    }
463    writeln!(output, "        }}").unwrap();
464    writeln!(output, "    }}").unwrap();
465
466    writeln!(output, "}}\n").unwrap();
467
468    output
469}