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}