Skip to main content

logicaffeine_compile/codegen/
bindings.rs

1use std::fmt::Write;
2
3use crate::analysis::registry::TypeRegistry;
4use crate::ast::stmt::{Stmt, TypeExpr};
5use crate::intern::Interner;
6use super::ffi::{CAbiClass, classify_type_for_c_abi};
7
8/// Generate Python ctypes bindings for all C-exported functions.
9pub fn generate_python_bindings(
10    stmts: &[Stmt],
11    module_name: &str,
12    interner: &Interner,
13    registry: &TypeRegistry,
14) -> String {
15    let mut out = String::new();
16
17    writeln!(out, "\"\"\"Auto-generated Python bindings for {}.\"\"\"", module_name).unwrap();
18    writeln!(out, "import ctypes").unwrap();
19    writeln!(out, "from ctypes import c_int64, c_uint64, c_double, c_bool, c_char_p, c_void_p, c_size_t, POINTER").unwrap();
20    writeln!(out, "import os").unwrap();
21    writeln!(out, "import sys\n").unwrap();
22
23    writeln!(out, "class LogosError(Exception):").unwrap();
24    writeln!(out, "    pass\n").unwrap();
25
26    writeln!(out, "class LogosRefinementError(LogosError):").unwrap();
27    writeln!(out, "    pass\n").unwrap();
28
29    writeln!(out, "def _lib_ext():").unwrap();
30    writeln!(out, "    if sys.platform == \"darwin\":").unwrap();
31    writeln!(out, "        return \".dylib\"").unwrap();
32    writeln!(out, "    elif sys.platform == \"win32\":").unwrap();
33    writeln!(out, "        return \".dll\"").unwrap();
34    writeln!(out, "    else:").unwrap();
35    writeln!(out, "        return \".so\"\n").unwrap();
36
37    let class_name = module_name.chars().next().unwrap_or('M').to_uppercase().to_string()
38        + &module_name[1..];
39
40    writeln!(out, "class {}:", class_name).unwrap();
41    writeln!(out, "    OK = 0").unwrap();
42    writeln!(out, "    ERROR = 1").unwrap();
43    writeln!(out, "    REFINEMENT_VIOLATION = 2").unwrap();
44    writeln!(out, "    NULL_POINTER = 3").unwrap();
45    writeln!(out, "    OUT_OF_BOUNDS = 4\n").unwrap();
46
47    writeln!(out, "    def __init__(self, path=None):").unwrap();
48    writeln!(out, "        if path is None:").unwrap();
49    writeln!(out, "            path = os.path.join(os.path.dirname(__file__), \"lib{}\" + _lib_ext())", module_name).unwrap();
50    writeln!(out, "        self._lib = ctypes.CDLL(path)").unwrap();
51    writeln!(out, "        self._setup()\n").unwrap();
52
53    writeln!(out, "    def _check(self, status):").unwrap();
54    writeln!(out, "        if status != self.OK:").unwrap();
55    writeln!(out, "            err = self._lib.logos_get_last_error()").unwrap();
56    writeln!(out, "            msg = err.decode(\"utf-8\") if err else \"Unknown error\"").unwrap();
57    writeln!(out, "            self._lib.logos_clear_error()").unwrap();
58    writeln!(out, "            if status == self.REFINEMENT_VIOLATION:").unwrap();
59    writeln!(out, "                raise LogosRefinementError(msg)").unwrap();
60    writeln!(out, "            raise LogosError(msg)\n").unwrap();
61
62    // _setup method
63    writeln!(out, "    def _setup(self):").unwrap();
64    writeln!(out, "        self._lib.logos_get_last_error.restype = c_char_p").unwrap();
65    writeln!(out, "        self._lib.logos_clear_error.restype = None").unwrap();
66    writeln!(out, "        self._lib.logos_free_string.argtypes = [c_char_p]").unwrap();
67    writeln!(out, "        self._lib.logos_free_string.restype = None").unwrap();
68
69    // Per-function setup
70    for stmt in stmts {
71        if let Stmt::FunctionDef { name, is_exported: true, export_target, params, return_type, .. } = stmt {
72            let is_c = match export_target {
73                None => true,
74                Some(t) => interner.resolve(*t).eq_ignore_ascii_case("c"),
75            };
76            if !is_c { continue; }
77
78            let func_name = format!("logos_{}", interner.resolve(*name));
79            let mut argtypes = Vec::new();
80            for (_, ptype) in params.iter() {
81                argtypes.push(python_ctypes_type(ptype, interner, registry));
82            }
83            let restype = return_type
84                .map(|ty| python_ctypes_type(ty, interner, registry))
85                .unwrap_or_else(|| "None".to_string());
86
87            writeln!(out, "        self._lib.{}.argtypes = [{}]", func_name, argtypes.join(", ")).unwrap();
88            writeln!(out, "        self._lib.{}.restype = {}", func_name, restype).unwrap();
89        }
90    }
91    writeln!(out).unwrap();
92
93    // Per-function wrapper methods
94    for stmt in stmts {
95        if let Stmt::FunctionDef { name, is_exported: true, export_target, params, return_type, .. } = stmt {
96            let is_c = match export_target {
97                None => true,
98                Some(t) => interner.resolve(*t).eq_ignore_ascii_case("c"),
99            };
100            if !is_c { continue; }
101
102            let raw_name = interner.resolve(*name);
103            let c_func_name = format!("logos_{}", raw_name);
104            let param_names: Vec<String> = params.iter()
105                .map(|(pname, _)| interner.resolve(*pname).to_string())
106                .collect();
107            let type_hints: Vec<String> = params.iter()
108                .map(|(pname, ptype)| {
109                    format!("{}: {}", interner.resolve(*pname), python_type_hint(ptype, interner))
110                })
111                .collect();
112            let ret_hint = return_type
113                .map(|ty| format!(" -> {}", python_type_hint(ty, interner)))
114                .unwrap_or_default();
115
116            // Python method uses the raw name for ergonomic API; delegates to prefixed C symbol
117            writeln!(out, "    def {}(self, {}){}:", raw_name, type_hints.join(", "), ret_hint).unwrap();
118            writeln!(out, "        return self._lib.{}({})", c_func_name, param_names.join(", ")).unwrap();
119            writeln!(out).unwrap();
120        }
121    }
122
123    out
124}
125
126fn python_ctypes_type(ty: &TypeExpr, interner: &Interner, registry: &TypeRegistry) -> String {
127    match classify_type_for_c_abi(ty, interner, registry) {
128        CAbiClass::ReferenceType => "c_void_p".to_string(),
129        CAbiClass::ValueType => {
130            match ty {
131                TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
132                    let name = interner.resolve(*sym);
133                    match name {
134                        "Int" => "c_int64".to_string(),
135                        "Nat" => "c_uint64".to_string(),
136                        "Real" | "Float" => "c_double".to_string(),
137                        "Bool" | "Boolean" => "c_bool".to_string(),
138                        "Text" | "String" => "c_char_p".to_string(),
139                        _ => "c_void_p".to_string(),
140                    }
141                }
142                _ => "c_void_p".to_string(),
143            }
144        }
145    }
146}
147
148fn python_type_hint(ty: &TypeExpr, interner: &Interner) -> String {
149    match ty {
150        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
151            let name = interner.resolve(*sym);
152            match name {
153                "Int" | "Nat" => "int".to_string(),
154                "Real" | "Float" => "float".to_string(),
155                "Bool" | "Boolean" => "bool".to_string(),
156                "Text" | "String" => "str".to_string(),
157                other => other.to_string(),
158            }
159        }
160        _ => "object".to_string(),
161    }
162}
163
164/// Generate TypeScript type declarations (.d.ts) and FFI bindings (.js).
165pub fn generate_typescript_bindings(
166    stmts: &[Stmt],
167    module_name: &str,
168    interner: &Interner,
169    registry: &TypeRegistry,
170) -> (String, String) {
171    let mut dts = String::new();
172    let mut js = String::new();
173
174    // .d.ts
175    writeln!(dts, "// Auto-generated TypeScript definitions for {}", module_name).unwrap();
176    let mut ffi_entries = Vec::new();
177
178    for stmt in stmts {
179        if let Stmt::FunctionDef { name, is_exported: true, export_target, params, return_type, .. } = stmt {
180            let is_c = match export_target {
181                None => true,
182                Some(t) => interner.resolve(*t).eq_ignore_ascii_case("c"),
183            };
184            if !is_c { continue; }
185
186            let raw_name = interner.resolve(*name);
187            let c_symbol = format!("logos_{}", raw_name);
188            let ts_params: Vec<String> = params.iter()
189                .map(|(pname, ptype)| format!("{}: {}", interner.resolve(*pname), typescript_type(ptype, interner)))
190                .collect();
191            let ts_ret = return_type
192                .map(|ty| typescript_type(ty, interner))
193                .unwrap_or_else(|| "void".to_string());
194            writeln!(dts, "export declare function {}({}): {};", raw_name, ts_params.join(", "), ts_ret).unwrap();
195
196            // Collect FFI entries for .js (raw_name for JS API, c_symbol for C FFI)
197            let ffi_params: Vec<String> = params.iter()
198                .map(|(_, ptype)| ffi_napi_type(ptype, interner, registry))
199                .collect();
200            let ffi_ret = return_type
201                .map(|ty| ffi_napi_type(ty, interner, registry))
202                .unwrap_or_else(|| "'void'".to_string());
203            ffi_entries.push((raw_name.to_string(), c_symbol, ffi_ret, ffi_params));
204        }
205    }
206
207    // .js — uses koffi (pure JS, no native deps)
208    writeln!(js, "const koffi = require('koffi');").unwrap();
209    writeln!(js, "const path = require('path');\n").unwrap();
210    writeln!(js, "const libPath = path.join(__dirname, 'lib{}');", module_name).unwrap();
211    writeln!(js, "const lib = koffi.load(libPath);\n").unwrap();
212
213    // Declare runtime functions
214    writeln!(js, "const logos_get_last_error = lib.func('const char* logos_get_last_error()');").unwrap();
215    writeln!(js, "const logos_clear_error = lib.func('void logos_clear_error()');").unwrap();
216    writeln!(js, "const logos_free_string = lib.func('void logos_free_string(void* ptr)');\n").unwrap();
217
218    // Declare user-exported functions (C symbols use logos_ prefix)
219    for (raw_name, c_symbol, ffi_ret, ffi_params) in &ffi_entries {
220        let koffi_ret = ffi_napi_to_koffi(ffi_ret);
221        let koffi_params: Vec<String> = ffi_params.iter()
222            .enumerate()
223            .map(|(i, p)| format!("{} arg{}", ffi_napi_to_koffi(p), i))
224            .collect();
225        writeln!(js, "const _{} = lib.func('{} {}({})');\n", raw_name, koffi_ret, c_symbol, koffi_params.join(", ")).unwrap();
226    }
227
228    writeln!(js, "function checkStatus(status) {{").unwrap();
229    writeln!(js, "  if (status !== 0) {{").unwrap();
230    writeln!(js, "    const err = logos_get_last_error();").unwrap();
231    writeln!(js, "    logos_clear_error();").unwrap();
232    writeln!(js, "    throw new Error(err || 'Unknown LogicAffeine error');").unwrap();
233    writeln!(js, "  }}").unwrap();
234    writeln!(js, "}}\n").unwrap();
235
236    for (raw_name, _, _, _) in &ffi_entries {
237        let params_from_stmts = stmts.iter().find_map(|s| {
238            if let Stmt::FunctionDef { name, is_exported: true, params, .. } = s {
239                if interner.resolve(*name) == raw_name.as_str() {
240                    Some(params)
241                } else {
242                    None
243                }
244            } else {
245                None
246            }
247        });
248        if let Some(params) = params_from_stmts {
249            let param_names: Vec<String> = params.iter()
250                .map(|(pname, _)| interner.resolve(*pname).to_string())
251                .collect();
252            writeln!(js, "module.exports.{} = ({}) => _{}({});", raw_name, param_names.join(", "), raw_name, param_names.join(", ")).unwrap();
253        }
254    }
255
256    (js, dts)
257}
258
259fn typescript_type(ty: &TypeExpr, interner: &Interner) -> String {
260    match ty {
261        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
262            let name = interner.resolve(*sym);
263            match name {
264                "Int" | "Nat" | "Real" | "Float" | "Byte" => "number".to_string(),
265                "Bool" | "Boolean" => "boolean".to_string(),
266                "Text" | "String" | "Char" => "string".to_string(),
267                "Unit" => "void".to_string(),
268                other => other.to_string(),
269            }
270        }
271        TypeExpr::Generic { base, params } => {
272            let base_name = interner.resolve(*base);
273            match base_name {
274                "Seq" | "List" | "Vec" if !params.is_empty() => {
275                    format!("{}[]", typescript_type(&params[0], interner))
276                }
277                "Option" | "Maybe" if !params.is_empty() => {
278                    format!("{} | null", typescript_type(&params[0], interner))
279                }
280                _ => "any".to_string(),
281            }
282        }
283        _ => "any".to_string(),
284    }
285}
286
287/// Convert ffi-napi type strings to koffi type strings for TypeScript bindings.
288fn ffi_napi_to_koffi(ffi_type: &str) -> &str {
289    match ffi_type {
290        "'int64'" => "int64_t",
291        "'uint64'" => "uint64_t",
292        "'double'" => "double",
293        "'bool'" => "bool",
294        "'string'" => "const char*",
295        "'pointer'" => "void*",
296        "'void'" => "void",
297        _ => "void*",
298    }
299}
300
301fn ffi_napi_type(ty: &TypeExpr, interner: &Interner, registry: &TypeRegistry) -> String {
302    match classify_type_for_c_abi(ty, interner, registry) {
303        CAbiClass::ReferenceType => "'pointer'".to_string(),
304        CAbiClass::ValueType => {
305            match ty {
306                TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
307                    let name = interner.resolve(*sym);
308                    match name {
309                        "Int" => "'int64'".to_string(),
310                        "Nat" => "'uint64'".to_string(),
311                        "Real" | "Float" => "'double'".to_string(),
312                        "Bool" | "Boolean" => "'bool'".to_string(),
313                        "Text" | "String" => "'string'".to_string(),
314                        _ => "'pointer'".to_string(),
315                    }
316                }
317                _ => "'pointer'".to_string(),
318            }
319        }
320    }
321}