Skip to main content

lamina_codegen/
abi.rs

1//! Common ABI trait and utilities for code generation backends.
2//!
3//! A trait that all ABI implementations must follow,
4//! reducing code duplication and giving a consistent interface.
5//!
6//! # Application Binary Interface (ABI) Overview
7//!
8//! Lamina uses platform C ABIs for function calls. The ABI implementation is
9//! abstracted through the `Abi` trait, which gives a consistent interface
10//! across all backends.
11//!
12//! ## Common ABI Contract
13//!
14//! ### Function Name Mangling
15//!
16//! All backends apply platform-specific symbol mangling:
17//! - **macOS**: Functions are prefixed with underscore (e.g., `main` → `_main`)
18//! - **Linux/Windows**: Functions use their original names (e.g., `main` → `main`)
19//!
20//! The `mangle_function_name()` method handles this automatically.
21//!
22//! ### Argument Passing
23//!
24//! - Arguments are passed using the platform C ABI register order
25//! - Overflow arguments (beyond register capacity) are spilled to the stack
26//! - Integer arguments use 64-bit slots, even for narrower integer types (i8, i16, i32)
27//! - Floating-point arguments follow platform-specific rules
28//!
29//! **Example: Function call with 3 arguments (RISC-V)**
30//! ```text
31//! # Arguments: a0=arg1, a1=arg2, a2=arg3
32//! mv a0, t0        # Load arg1 to a0
33//! mv a1, t1        # Load arg2 to a1
34//! mv a2, t2        # Load arg3 to a2
35//! call my_function # Call function
36//! ```
37//!
38//! **Example: Function call with 10 arguments (x86_64 System V)**
39//! ```text
40//! # First 6 args in registers: rdi, rsi, rdx, rcx, r8, r9
41//! # Remaining 4 args on stack (16-byte aligned)
42//! mov %rdi, %rax   # arg1
43//! mov %rsi, %rbx   # arg2
44//! # ... args 3-6 in rdx, rcx, r8, r9
45//! push %r10        # arg7 on stack
46//! push %r11        # arg8 on stack
47//! push %r12        # arg9 on stack
48//! push %r13        # arg10 on stack
49//! call my_function
50//! add $32, %rsp    # Clean up stack
51//! ```
52//!
53//! ### Return Values
54//!
55//! - Integer return values use the C ABI integer return register
56//! - Floating-point return values use the FP return register (if applicable)
57//! - Void functions return nothing
58//!
59//! **Example: Return value handling**
60//! ```text
61//! # RISC-V: return value in a0 (integer) or fa0 (float)
62//! call my_function
63//! mv t0, a0        # Save integer return value
64//!
65//! # x86_64: return value in rax (integer) or xmm0 (float)
66//! call my_function
67//! mov %rax, %rbx   # Save integer return value
68//! ```
69//!
70//! ### Builtin Functions
71//!
72//! Builtin functions can be overridden via the `LAMINA_BUILTINS` environment variable:
73//!
74//! ```bash
75//! LAMINA_BUILTINS="print=printf,malloc=jemalloc_malloc,dealloc=jemalloc_free"
76//! ```
77//!
78//! Default builtins:
79//! - `print` → `printf` (or `_printf` on macOS)
80//! - `malloc` → `malloc` (or `_malloc` on macOS)
81//! - `dealloc` → `free` (or `_free` on macOS)
82//!
83//! ## Backend-Specific ABIs
84//!
85//! See the documentation in each backend's ABI module:
86//! - `x86_64::abi` - System V AMD64 and Microsoft x64
87//! - `aarch64::abi` - AAPCS64
88//! - `riscv::abi` - RISC-V calling convention
89//! - `wasm::abi` - WebAssembly stack-based calling
90
91use crate::target_support::os_uses_macho;
92use lamina_platform::TargetOperatingSystem;
93use std::{collections::HashMap, env};
94
95pub struct BuiltinLibrary {
96    target_os: TargetOperatingSystem,
97    overrides: HashMap<String, String>,
98}
99
100impl BuiltinLibrary {
101    pub fn from_env(target_os: TargetOperatingSystem) -> Self {
102        let overrides = builtin_overrides_from_env();
103        Self {
104            target_os,
105            overrides,
106        }
107    }
108
109    pub fn resolve(&self, name: &str) -> Option<String> {
110        self.overrides
111            .get(name)
112            .cloned()
113            .or_else(|| self.default_symbol(name))
114    }
115
116    fn default_symbol(&self, name: &str) -> Option<String> {
117        match name {
118            "print" => Some(get_printf_symbol(self.target_os).to_string()),
119            "malloc" => Some(get_malloc_symbol(self.target_os).to_string()),
120            "dealloc" => Some(get_free_symbol(self.target_os).to_string()),
121            _ => None,
122        }
123    }
124}
125
126fn builtin_overrides_from_env() -> HashMap<String, String> {
127    let mut overrides = HashMap::new();
128    if let Ok(value) = env::var("LAMINA_BUILTINS") {
129        for entry in value.split(',') {
130            let mut parts = entry.splitn(2, '=');
131            let name = parts.next().unwrap_or("").trim();
132            let symbol = parts.next().unwrap_or("").trim();
133            if !name.is_empty() && !symbol.is_empty() {
134                overrides.insert(name.to_string(), symbol.to_string());
135            }
136        }
137    }
138    overrides
139}
140
141/// Trait defining the common interface for all ABI implementations.
142///
143/// This contract covers name mangling, builtin symbol mapping, and the
144/// minimal calling convention expectations shared by MIR backends.
145///
146/// ## Calling Convention Assumptions
147/// - Arguments are passed using the platform C ABI register order, with
148///   overflow arguments spilled to the stack.
149/// - Integer arguments use 64-bit slots, even for narrower integer types.
150/// - Return values use the C ABI integer return register when present.
151///
152/// ## Builtins
153/// - Builtins are resolved by `call_stub`, which can be overridden with
154///   the `LAMINA_BUILTINS` environment variable.
155/// - The default builtin map recognizes `print`, `malloc`, and `dealloc`.
156///
157/// ## Naming
158/// - Backends must apply platform-specific symbol mangling, such as
159///   the underscore prefix on macOS.
160pub trait Abi {
161    /// Returns the target operating system for this ABI instance.
162    fn target_os(&self) -> TargetOperatingSystem;
163
164    /// Returns the mangled function name with platform-specific prefix.
165    ///
166    /// This method handles platform-specific symbol naming conventions,
167    /// such as the underscore prefix on macOS.
168    fn mangle_function_name(&self, name: &str) -> String;
169
170    fn builtin_library(&self) -> BuiltinLibrary {
171        BuiltinLibrary::from_env(self.target_os())
172    }
173
174    /// Maps well-known intrinsic/runtime names to platform symbol stubs.
175    ///
176    /// Returns `None` if the name is not a known intrinsic that needs
177    /// special handling. This lets backends map internal names
178    /// (like "print") to platform-specific symbols (like "_printf" on macOS).
179    ///
180    /// Default implementation returns `None` for all names, meaning
181    /// no special mapping is needed.
182    fn call_stub(&self, name: &str) -> Option<String> {
183        self.builtin_library().resolve(name)
184    }
185}
186
187/// Helper function to mangle function names for macOS (adds underscore prefix).
188///
189/// This is a common pattern used by multiple backends, so we provide
190/// it as a shared utility function.
191pub fn mangle_macos_name(name: &str) -> String {
192    if name == "main" {
193        "_main".to_string()
194    } else {
195        format!("_{name}")
196    }
197}
198
199/// Helper function to get the platform-specific printf symbol name.
200///
201/// Returns "_printf" on macOS, "printf" on other platforms.
202pub fn get_printf_symbol(target_os: TargetOperatingSystem) -> &'static str {
203    match target_os {
204        os if os_uses_macho(os) => "_printf",
205        _ => "printf",
206    }
207}
208
209/// Helper function to get the platform-specific malloc symbol name.
210///
211/// Returns "_malloc" on macOS, "malloc" on other platforms.
212pub fn get_malloc_symbol(target_os: TargetOperatingSystem) -> &'static str {
213    match target_os {
214        os if os_uses_macho(os) => "_malloc",
215        _ => "malloc",
216    }
217}
218
219/// Helper function to get the platform-specific free symbol name.
220///
221/// Returns "_free" on macOS, "free" on other platforms.
222pub fn get_free_symbol(target_os: TargetOperatingSystem) -> &'static str {
223    match target_os {
224        os if os_uses_macho(os) => "_free",
225        _ => "free",
226    }
227}
228
229/// Helper function to create a common call_stub implementation.
230///
231/// This handles the common pattern of mapping "print", "malloc", and "dealloc"
232/// to their platform-specific symbols. Backends can use this to reduce
233/// duplication in their `call_stub` implementations.
234pub fn common_call_stub(name: &str, target_os: TargetOperatingSystem) -> Option<String> {
235    BuiltinLibrary::from_env(target_os).resolve(name)
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_mangle_macos_name() {
244        assert_eq!(mangle_macos_name("main"), "_main");
245        assert_eq!(mangle_macos_name("foo"), "_foo");
246        assert_eq!(mangle_macos_name("my_function"), "_my_function");
247    }
248
249    #[test]
250    fn test_get_printf_symbol() {
251        assert_eq!(get_printf_symbol(TargetOperatingSystem::MacOS), "_printf");
252        assert_eq!(get_printf_symbol(TargetOperatingSystem::IOS), "_printf");
253        assert_eq!(
254            get_printf_symbol(TargetOperatingSystem::VisionOS),
255            "_printf"
256        );
257        assert_eq!(get_printf_symbol(TargetOperatingSystem::Linux), "printf");
258        assert_eq!(get_printf_symbol(TargetOperatingSystem::Windows), "printf");
259    }
260
261    #[test]
262    fn test_get_malloc_symbol() {
263        assert_eq!(get_malloc_symbol(TargetOperatingSystem::MacOS), "_malloc");
264        assert_eq!(get_malloc_symbol(TargetOperatingSystem::IOS), "_malloc");
265        assert_eq!(get_malloc_symbol(TargetOperatingSystem::Linux), "malloc");
266        assert_eq!(get_malloc_symbol(TargetOperatingSystem::Windows), "malloc");
267    }
268
269    #[test]
270    fn test_get_free_symbol() {
271        assert_eq!(get_free_symbol(TargetOperatingSystem::MacOS), "_free");
272        assert_eq!(get_free_symbol(TargetOperatingSystem::TvOS), "_free");
273        assert_eq!(get_free_symbol(TargetOperatingSystem::Linux), "free");
274        assert_eq!(get_free_symbol(TargetOperatingSystem::Windows), "free");
275    }
276
277    #[test]
278    fn test_builtin_library_default_symbols() {
279        let lib_macos = BuiltinLibrary::from_env(TargetOperatingSystem::MacOS);
280        assert_eq!(lib_macos.resolve("print"), Some("_printf".to_string()));
281        assert_eq!(lib_macos.resolve("malloc"), Some("_malloc".to_string()));
282        assert_eq!(lib_macos.resolve("dealloc"), Some("_free".to_string()));
283
284        let lib_linux = BuiltinLibrary::from_env(TargetOperatingSystem::Linux);
285        assert_eq!(lib_linux.resolve("print"), Some("printf".to_string()));
286        assert_eq!(lib_linux.resolve("malloc"), Some("malloc".to_string()));
287        assert_eq!(lib_linux.resolve("dealloc"), Some("free".to_string()));
288    }
289
290    #[test]
291    fn test_builtin_library_unknown_symbol() {
292        let lib = BuiltinLibrary::from_env(TargetOperatingSystem::Linux);
293        assert_eq!(lib.resolve("unknown_function"), None);
294    }
295
296    #[test]
297    fn test_common_call_stub() {
298        assert_eq!(
299            common_call_stub("print", TargetOperatingSystem::MacOS),
300            Some("_printf".to_string())
301        );
302        assert_eq!(
303            common_call_stub("print", TargetOperatingSystem::Linux),
304            Some("printf".to_string())
305        );
306        assert_eq!(
307            common_call_stub("malloc", TargetOperatingSystem::MacOS),
308            Some("_malloc".to_string())
309        );
310        assert_eq!(
311            common_call_stub("dealloc", TargetOperatingSystem::Linux),
312            Some("free".to_string())
313        );
314        assert_eq!(
315            common_call_stub("unknown", TargetOperatingSystem::Linux),
316            None
317        );
318    }
319}