use crate::target_support::os_uses_macho;
use lamina_platform::TargetOperatingSystem;
use std::{collections::HashMap, env, error::Error, fmt};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RuntimeSymbol {
Malloc,
Free,
Exit,
Memcpy,
Memmove,
Memset,
Divti3,
Udivti3,
Modti3,
Umodti3,
Divdi3,
Udivdi3,
Moddi3,
Umoddi3,
Floatdidf,
Floatundidf,
Floatdisf,
Floatundisf,
Fixdfdi,
Fixunsdfdi,
Fixsfdi,
Fixunssfdi,
Floattidf,
Floatuntidf,
Fixdfti,
Fixunsdfti,
Floattisf,
Floatuntisf,
Fixsfti,
Fixunssfti,
}
impl RuntimeSymbol {
pub const ALL: [RuntimeSymbol; 30] = [
RuntimeSymbol::Malloc,
RuntimeSymbol::Free,
RuntimeSymbol::Exit,
RuntimeSymbol::Memcpy,
RuntimeSymbol::Memmove,
RuntimeSymbol::Memset,
RuntimeSymbol::Divti3,
RuntimeSymbol::Udivti3,
RuntimeSymbol::Modti3,
RuntimeSymbol::Umodti3,
RuntimeSymbol::Divdi3,
RuntimeSymbol::Udivdi3,
RuntimeSymbol::Moddi3,
RuntimeSymbol::Umoddi3,
RuntimeSymbol::Floatdidf,
RuntimeSymbol::Floatundidf,
RuntimeSymbol::Floatdisf,
RuntimeSymbol::Floatundisf,
RuntimeSymbol::Fixdfdi,
RuntimeSymbol::Fixunsdfdi,
RuntimeSymbol::Fixsfdi,
RuntimeSymbol::Fixunssfdi,
RuntimeSymbol::Floattidf,
RuntimeSymbol::Floatuntidf,
RuntimeSymbol::Fixdfti,
RuntimeSymbol::Fixunsdfti,
RuntimeSymbol::Floattisf,
RuntimeSymbol::Floatuntisf,
RuntimeSymbol::Fixsfti,
RuntimeSymbol::Fixunssfti,
];
pub const fn c_name(self) -> &'static str {
match self {
RuntimeSymbol::Malloc => "malloc",
RuntimeSymbol::Free => "free",
RuntimeSymbol::Exit => "exit",
RuntimeSymbol::Memcpy => "memcpy",
RuntimeSymbol::Memmove => "memmove",
RuntimeSymbol::Memset => "memset",
RuntimeSymbol::Divti3 => "__divti3",
RuntimeSymbol::Udivti3 => "__udivti3",
RuntimeSymbol::Modti3 => "__modti3",
RuntimeSymbol::Umodti3 => "__umodti3",
RuntimeSymbol::Divdi3 => "__divdi3",
RuntimeSymbol::Udivdi3 => "__udivdi3",
RuntimeSymbol::Moddi3 => "__moddi3",
RuntimeSymbol::Umoddi3 => "__umoddi3",
RuntimeSymbol::Floatdidf => "__floatdidf",
RuntimeSymbol::Floatundidf => "__floatundidf",
RuntimeSymbol::Floatdisf => "__floatdisf",
RuntimeSymbol::Floatundisf => "__floatundisf",
RuntimeSymbol::Fixdfdi => "__fixdfdi",
RuntimeSymbol::Fixunsdfdi => "__fixunsdfdi",
RuntimeSymbol::Fixsfdi => "__fixsfdi",
RuntimeSymbol::Fixunssfdi => "__fixunssfdi",
RuntimeSymbol::Floattidf => "__floattidf",
RuntimeSymbol::Floatuntidf => "__floatuntidf",
RuntimeSymbol::Fixdfti => "__fixdfti",
RuntimeSymbol::Fixunsdfti => "__fixunsdfti",
RuntimeSymbol::Floattisf => "__floattisf",
RuntimeSymbol::Floatuntisf => "__floatuntisf",
RuntimeSymbol::Fixsfti => "__fixsfti",
RuntimeSymbol::Fixunssfti => "__fixunssfti",
}
}
pub fn from_c_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|symbol| symbol.c_name() == name)
}
pub fn from_builtin_name(name: &str) -> Option<Self> {
match name {
"dealloc" => Some(RuntimeSymbol::Free),
_ => Self::from_c_name(name),
}
}
}
pub struct BuiltinLibrary {
target_os: TargetOperatingSystem,
overrides: HashMap<RuntimeSymbol, String>,
}
impl BuiltinLibrary {
pub fn new(
target_os: TargetOperatingSystem,
overrides: HashMap<RuntimeSymbol, String>,
) -> Self {
Self {
target_os,
overrides,
}
}
pub fn from_env(target_os: TargetOperatingSystem) -> Result<Self, BuiltinOverrideError> {
Ok(Self::new(target_os, builtin_overrides_from_env()?))
}
pub fn from_env_or_unset(target_os: TargetOperatingSystem) -> Self {
Self::new(target_os, builtin_overrides_from_env().unwrap_or_default())
}
pub fn has_overrides(&self) -> bool {
!self.overrides.is_empty()
}
pub fn c_name(&self, symbol: RuntimeSymbol) -> &str {
self.overrides
.get(&symbol)
.map_or(symbol.c_name(), String::as_str)
}
pub fn symbol(&self, symbol: RuntimeSymbol) -> String {
decorate_symbol(self.target_os, self.c_name(symbol))
}
pub fn resolve(&self, name: &str) -> Option<String> {
RuntimeSymbol::from_builtin_name(name).map(|symbol| self.symbol(symbol))
}
}
fn decorate_symbol(target_os: TargetOperatingSystem, name: &str) -> String {
if os_uses_macho(target_os) {
format!("_{name}")
} else {
name.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BuiltinOverrideError {
MalformedEntry(String),
UnknownBuiltin(String),
EmptySide(String),
Conflict {
builtin: &'static str,
first: String,
second: String,
},
}
impl fmt::Display for BuiltinOverrideError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BuiltinOverrideError::MalformedEntry(entry) => {
write!(f, "LAMINA_BUILTINS entry `{entry}` is not `name=symbol`")
}
BuiltinOverrideError::UnknownBuiltin(name) => write!(
f,
"LAMINA_BUILTINS names `{name}`, which is not a runtime helper this compiler calls; the names are {}",
builtin_name_list()
),
BuiltinOverrideError::EmptySide(entry) => {
write!(f, "LAMINA_BUILTINS entry `{entry}` leaves one side empty")
}
BuiltinOverrideError::Conflict {
builtin,
first,
second,
} => write!(
f,
"LAMINA_BUILTINS sets `{builtin}` twice, to `{first}` and to `{second}`"
),
}
}
}
impl Error for BuiltinOverrideError {}
fn builtin_name_list() -> String {
let mut names: Vec<&str> = RuntimeSymbol::ALL
.into_iter()
.map(RuntimeSymbol::c_name)
.collect();
names.push("dealloc");
names.sort_unstable();
names.join(", ")
}
pub fn parse_builtin_overrides(
value: &str,
) -> Result<HashMap<RuntimeSymbol, String>, BuiltinOverrideError> {
let mut overrides: HashMap<RuntimeSymbol, String> = HashMap::new();
for entry in value.split(',') {
if entry.trim().is_empty() {
continue;
}
let Some((name, symbol)) = entry.split_once('=') else {
return Err(BuiltinOverrideError::MalformedEntry(
entry.trim().to_owned(),
));
};
let name = name.trim();
let symbol = symbol.trim();
if name.is_empty() || symbol.is_empty() {
return Err(BuiltinOverrideError::EmptySide(entry.trim().to_owned()));
}
let Some(runtime) = RuntimeSymbol::from_builtin_name(name) else {
return Err(BuiltinOverrideError::UnknownBuiltin(name.to_owned()));
};
if let Some(first) = overrides.get(&runtime)
&& first != symbol
{
return Err(BuiltinOverrideError::Conflict {
builtin: runtime.c_name(),
first: first.clone(),
second: symbol.to_owned(),
});
}
overrides.insert(runtime, symbol.to_owned());
}
Ok(overrides)
}
fn builtin_overrides_from_env() -> Result<HashMap<RuntimeSymbol, String>, BuiltinOverrideError> {
match env::var("LAMINA_BUILTINS") {
Ok(value) => parse_builtin_overrides(&value),
Err(_) => Ok(HashMap::new()),
}
}
pub trait Abi {
fn target_os(&self) -> TargetOperatingSystem;
fn mangle_function_name(&self, name: &str) -> String;
fn builtin_library(&self) -> BuiltinLibrary {
BuiltinLibrary::from_env_or_unset(self.target_os())
}
fn call_stub(&self, name: &str) -> Option<String> {
self.builtin_library().resolve(name)
}
}
pub fn mangle_macos_name(name: &str) -> String {
if name == "main" {
"_main".to_string()
} else {
format!("_{name}")
}
}
pub fn get_printf_symbol(target_os: TargetOperatingSystem) -> &'static str {
match target_os {
os if os_uses_macho(os) => "_printf",
_ => "printf",
}
}
pub fn get_malloc_symbol(target_os: TargetOperatingSystem) -> &'static str {
match target_os {
os if os_uses_macho(os) => "_malloc",
_ => "malloc",
}
}
pub fn get_free_symbol(target_os: TargetOperatingSystem) -> &'static str {
match target_os {
os if os_uses_macho(os) => "_free",
_ => "free",
}
}
pub fn common_call_stub(name: &str, target_os: TargetOperatingSystem) -> Option<String> {
BuiltinLibrary::from_env_or_unset(target_os).resolve(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mangle_macos_name() {
assert_eq!(mangle_macos_name("main"), "_main");
assert_eq!(mangle_macos_name("foo"), "_foo");
assert_eq!(mangle_macos_name("my_function"), "_my_function");
}
#[test]
fn test_get_printf_symbol() {
assert_eq!(get_printf_symbol(TargetOperatingSystem::MacOS), "_printf");
assert_eq!(get_printf_symbol(TargetOperatingSystem::IOS), "_printf");
assert_eq!(
get_printf_symbol(TargetOperatingSystem::VisionOS),
"_printf"
);
assert_eq!(get_printf_symbol(TargetOperatingSystem::Linux), "printf");
assert_eq!(get_printf_symbol(TargetOperatingSystem::Windows), "printf");
}
#[test]
fn test_get_malloc_symbol() {
assert_eq!(get_malloc_symbol(TargetOperatingSystem::MacOS), "_malloc");
assert_eq!(get_malloc_symbol(TargetOperatingSystem::IOS), "_malloc");
assert_eq!(get_malloc_symbol(TargetOperatingSystem::Linux), "malloc");
assert_eq!(get_malloc_symbol(TargetOperatingSystem::Windows), "malloc");
}
#[test]
fn test_get_free_symbol() {
assert_eq!(get_free_symbol(TargetOperatingSystem::MacOS), "_free");
assert_eq!(get_free_symbol(TargetOperatingSystem::TvOS), "_free");
assert_eq!(get_free_symbol(TargetOperatingSystem::Linux), "free");
assert_eq!(get_free_symbol(TargetOperatingSystem::Windows), "free");
}
#[test]
fn test_builtin_library_default_symbols() {
let lib_macos = BuiltinLibrary::from_env_or_unset(TargetOperatingSystem::MacOS);
assert_eq!(lib_macos.resolve("malloc"), Some("_malloc".to_string()));
assert_eq!(lib_macos.resolve("dealloc"), Some("_free".to_string()));
let lib_linux = BuiltinLibrary::from_env_or_unset(TargetOperatingSystem::Linux);
assert_eq!(lib_linux.resolve("malloc"), Some("malloc".to_string()));
assert_eq!(lib_linux.resolve("dealloc"), Some("free".to_string()));
}
#[test]
fn test_builtin_library_unknown_symbol() {
let lib = BuiltinLibrary::from_env_or_unset(TargetOperatingSystem::Linux);
assert_eq!(lib.resolve("unknown_function"), None);
}
#[test]
fn test_common_call_stub() {
assert_eq!(
common_call_stub("malloc", TargetOperatingSystem::MacOS),
Some("_malloc".to_string())
);
assert_eq!(
common_call_stub("dealloc", TargetOperatingSystem::Linux),
Some("free".to_string())
);
assert_eq!(
common_call_stub("unknown", TargetOperatingSystem::Linux),
None
);
}
}
#[cfg(test)]
mod runtime_symbol_tests {
use super::*;
fn all_overridden(os: TargetOperatingSystem) -> BuiltinLibrary {
let overrides = RuntimeSymbol::ALL
.into_iter()
.map(|symbol| (symbol, format!("k_{}", symbol.c_name())))
.collect();
BuiltinLibrary::new(os, overrides)
}
#[test]
fn default_symbols_keep_c_names_and_platform_decoration() {
let linux = BuiltinLibrary::new(TargetOperatingSystem::Linux, HashMap::new());
let macos = BuiltinLibrary::new(TargetOperatingSystem::MacOS, HashMap::new());
assert!(!linux.has_overrides());
for symbol in RuntimeSymbol::ALL {
assert_eq!(linux.c_name(symbol), symbol.c_name());
assert_eq!(linux.symbol(symbol), symbol.c_name());
assert_eq!(macos.c_name(symbol), symbol.c_name());
assert_eq!(macos.symbol(symbol), format!("_{}", symbol.c_name()));
}
}
#[test]
fn overrides_replace_every_symbol() {
for os in [TargetOperatingSystem::Linux, TargetOperatingSystem::MacOS] {
let lib = all_overridden(os);
assert!(lib.has_overrides());
for symbol in RuntimeSymbol::ALL {
let expected_c = format!("k_{}", symbol.c_name());
assert_eq!(lib.c_name(symbol), expected_c);
assert_eq!(
lib.resolve(symbol.c_name()),
Some(decorate_symbol(os, &expected_c))
);
}
}
}
#[test]
fn resolve_maps_legacy_builtin_keys() {
let lib = BuiltinLibrary::new(
TargetOperatingSystem::Linux,
parse_builtin_overrides("dealloc=k_free").expect("a known builtin parses"),
);
assert_eq!(lib.resolve("dealloc"), Some("k_free".to_string()));
assert_eq!(lib.resolve("free"), Some("k_free".to_string()));
assert_eq!(lib.resolve("write"), None);
}
#[test]
fn parse_builtin_overrides_accepts_padded_known_entries() {
let parsed =
parse_builtin_overrides(" malloc = k_alloc ,exit=k_exit").expect("both are builtins");
assert_eq!(parsed.len(), 2);
assert_eq!(
parsed.get(&RuntimeSymbol::Malloc).map(String::as_str),
Some("k_alloc")
);
assert_eq!(
parsed.get(&RuntimeSymbol::Exit).map(String::as_str),
Some("k_exit")
);
}
#[test]
fn parse_builtin_overrides_refuses_what_it_cannot_apply() {
assert_eq!(
parse_builtin_overrides("malloc=k_alloc,bogus=x"),
Err(BuiltinOverrideError::UnknownBuiltin("bogus".to_owned()))
);
assert_eq!(
parse_builtin_overrides("memset="),
Err(BuiltinOverrideError::EmptySide("memset=".to_owned()))
);
assert_eq!(
parse_builtin_overrides("=y"),
Err(BuiltinOverrideError::EmptySide("=y".to_owned()))
);
assert_eq!(
parse_builtin_overrides("malloc"),
Err(BuiltinOverrideError::MalformedEntry("malloc".to_owned()))
);
assert_eq!(
parse_builtin_overrides("free=a,dealloc=b"),
Err(BuiltinOverrideError::Conflict {
builtin: "free",
first: "a".to_owned(),
second: "b".to_owned(),
})
);
assert!(parse_builtin_overrides("free=a,dealloc=a").is_ok());
assert!(parse_builtin_overrides("").is_ok());
}
#[test]
fn unknown_builtin_message_lists_the_names() {
let message = BuiltinOverrideError::UnknownBuiltin("bogus".to_owned()).to_string();
assert!(message.contains("bogus"), "{message}");
assert!(message.contains("malloc"), "{message}");
assert!(message.contains("__divti3"), "{message}");
}
#[test]
fn from_c_name_round_trips_every_symbol() {
for symbol in RuntimeSymbol::ALL {
assert_eq!(RuntimeSymbol::from_c_name(symbol.c_name()), Some(symbol));
assert_eq!(
RuntimeSymbol::from_builtin_name(symbol.c_name()),
Some(symbol)
);
}
assert_eq!(RuntimeSymbol::from_c_name("print"), None);
}
}