use crate::types::{Effect, StackType, Type};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum FfiType {
Int,
String,
Ptr,
Void,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PassMode {
CString,
Ptr,
Int,
ByRef,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Ownership {
CallerFrees,
Static,
Borrowed,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FfiArg {
#[serde(rename = "type")]
pub arg_type: FfiType,
#[serde(default = "default_pass_mode")]
pub pass: PassMode,
pub value: Option<String>,
}
fn default_pass_mode() -> PassMode {
PassMode::CString
}
#[derive(Debug, Clone, Deserialize)]
pub struct FfiReturn {
#[serde(rename = "type")]
pub return_type: FfiType,
#[serde(default = "default_ownership")]
pub ownership: Ownership,
}
fn default_ownership() -> Ownership {
Ownership::Borrowed
}
#[derive(Debug, Clone, Deserialize)]
pub struct FfiFunction {
pub c_name: String,
pub seq_name: String,
pub stack_effect: String,
#[serde(default)]
pub args: Vec<FfiArg>,
#[serde(rename = "return")]
pub return_spec: Option<FfiReturn>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FfiLibrary {
pub name: String,
pub link: String,
#[serde(rename = "function", default)]
pub functions: Vec<FfiFunction>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FfiManifest {
#[serde(rename = "library")]
pub libraries: Vec<FfiLibrary>,
}
impl FfiManifest {
pub fn parse(content: &str) -> Result<Self, String> {
let manifest: Self =
toml::from_str(content).map_err(|e| format!("Failed to parse FFI manifest: {}", e))?;
manifest.validate()?;
Ok(manifest)
}
fn validate(&self) -> Result<(), String> {
if self.libraries.is_empty() {
return Err("FFI manifest must define at least one library".to_string());
}
for (lib_idx, lib) in self.libraries.iter().enumerate() {
if lib.name.trim().is_empty() {
return Err(format!("FFI library {} has empty name", lib_idx + 1));
}
if lib.link.trim().is_empty() {
return Err(format!("FFI library '{}' has empty linker flag", lib.name));
}
for c in lib.link.chars() {
if !c.is_alphanumeric() && c != '-' && c != '_' && c != '.' {
return Err(format!(
"FFI library '{}' has invalid character '{}' in linker flag '{}'. \
Only alphanumeric, dash, underscore, and dot are allowed.",
lib.name, c, lib.link
));
}
}
for (func_idx, func) in lib.functions.iter().enumerate() {
if func.c_name.trim().is_empty() {
return Err(format!(
"FFI function {} in library '{}' has empty c_name",
func_idx + 1,
lib.name
));
}
if func.seq_name.trim().is_empty() {
return Err(format!(
"FFI function '{}' in library '{}' has empty seq_name",
func.c_name, lib.name
));
}
if func.stack_effect.trim().is_empty() {
return Err(format!(
"FFI function '{}' has empty stack_effect",
func.seq_name
));
}
if let Err(e) = func.effect() {
return Err(format!(
"FFI function '{}' has malformed stack_effect '{}': {}",
func.seq_name, func.stack_effect, e
));
}
}
}
Ok(())
}
pub fn linker_flags(&self) -> Vec<String> {
self.libraries.iter().map(|lib| lib.link.clone()).collect()
}
pub fn functions(&self) -> impl Iterator<Item = &FfiFunction> {
self.libraries.iter().flat_map(|lib| lib.functions.iter())
}
}
impl FfiFunction {
pub fn effect(&self) -> Result<Effect, String> {
parse_stack_effect(&self.stack_effect)
}
}
fn parse_stack_effect(s: &str) -> Result<Effect, String> {
let s = s.trim();
let s = s
.strip_prefix('(')
.ok_or("Stack effect must start with '('")?;
let s = s
.strip_suffix(')')
.ok_or("Stack effect must end with ')'")?;
let s = s.trim();
let parts: Vec<&str> = s.split("--").collect();
if parts.len() != 2 {
return Err(format!(
"Stack effect must contain exactly one '--', got: {}",
s
));
}
let inputs_str = parts[0].trim();
let outputs_str = parts[1].trim();
let mut inputs = StackType::RowVar("a".to_string());
for type_name in inputs_str.split_whitespace() {
let ty = parse_type_name(type_name)?;
inputs = inputs.push(ty);
}
let mut outputs = StackType::RowVar("a".to_string());
for type_name in outputs_str.split_whitespace() {
let ty = parse_type_name(type_name)?;
outputs = outputs.push(ty);
}
Ok(Effect::new(inputs, outputs))
}
fn parse_type_name(name: &str) -> Result<Type, String> {
match name {
"Int" => Ok(Type::Int),
"Float" => Ok(Type::Float),
"Bool" => Ok(Type::Bool),
"String" => Ok(Type::String),
_ => Err(format!("Unknown type '{}' in stack effect", name)),
}
}
pub const LIBEDIT_MANIFEST: &str = include_str!("../ffi/libedit.toml");
pub fn get_ffi_manifest(name: &str) -> Option<&'static str> {
match name {
"libedit" => Some(LIBEDIT_MANIFEST),
_ => None,
}
}
pub fn has_ffi_manifest(name: &str) -> bool {
get_ffi_manifest(name).is_some()
}
pub fn list_ffi_manifests() -> &'static [&'static str] {
&["libedit"]
}
#[derive(Debug, Clone)]
pub struct FfiBindings {
pub functions: HashMap<String, FfiFunctionInfo>,
pub linker_flags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct FfiFunctionInfo {
pub c_name: String,
pub seq_name: String,
pub effect: Effect,
pub args: Vec<FfiArg>,
pub return_spec: Option<FfiReturn>,
}
impl FfiBindings {
pub fn new() -> Self {
FfiBindings {
functions: HashMap::new(),
linker_flags: Vec::new(),
}
}
pub fn add_manifest(&mut self, manifest: &FfiManifest) -> Result<(), String> {
self.linker_flags.extend(manifest.linker_flags());
for func in manifest.functions() {
let effect = func.effect()?;
let info = FfiFunctionInfo {
c_name: func.c_name.clone(),
seq_name: func.seq_name.clone(),
effect,
args: func.args.clone(),
return_spec: func.return_spec.clone(),
};
if self.functions.contains_key(&func.seq_name) {
return Err(format!(
"FFI function '{}' is already defined",
func.seq_name
));
}
self.functions.insert(func.seq_name.clone(), info);
}
Ok(())
}
pub fn is_ffi_function(&self, name: &str) -> bool {
self.functions.contains_key(name)
}
pub fn function_names(&self) -> Vec<&str> {
self.functions.keys().map(|s| s.as_str()).collect()
}
}
impl Default for FfiBindings {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_manifest() {
let content = r#"
[[library]]
name = "example"
link = "example"
[[library.function]]
c_name = "example_func"
seq_name = "example-func"
stack_effect = "( String -- String )"
args = [
{ type = "string", pass = "c_string" }
]
return = { type = "string", ownership = "caller_frees" }
"#;
let manifest = FfiManifest::parse(content).unwrap();
assert_eq!(manifest.libraries.len(), 1);
assert_eq!(manifest.libraries[0].name, "example");
assert_eq!(manifest.libraries[0].link, "example");
assert_eq!(manifest.libraries[0].functions.len(), 1);
let func = &manifest.libraries[0].functions[0];
assert_eq!(func.c_name, "example_func");
assert_eq!(func.seq_name, "example-func");
assert_eq!(func.args.len(), 1);
assert_eq!(func.args[0].arg_type, FfiType::String);
assert_eq!(func.args[0].pass, PassMode::CString);
}
#[test]
fn test_parse_stack_effect() {
let effect = parse_stack_effect("( String -- String )").unwrap();
let (rest, top) = effect.inputs.clone().pop().unwrap();
assert_eq!(top, Type::String);
assert_eq!(rest, StackType::RowVar("a".to_string()));
let (rest, top) = effect.outputs.clone().pop().unwrap();
assert_eq!(top, Type::String);
assert_eq!(rest, StackType::RowVar("a".to_string()));
}
#[test]
fn test_parse_stack_effect_void() {
let effect = parse_stack_effect("( String -- )").unwrap();
let (rest, top) = effect.inputs.clone().pop().unwrap();
assert_eq!(top, Type::String);
assert_eq!(rest, StackType::RowVar("a".to_string()));
assert_eq!(effect.outputs, StackType::RowVar("a".to_string()));
}
#[test]
fn test_ffi_bindings() {
let content = r#"
[[library]]
name = "example"
link = "example"
[[library.function]]
c_name = "example_read"
seq_name = "example-read"
stack_effect = "( String -- String )"
args = [{ type = "string", pass = "c_string" }]
return = { type = "string", ownership = "caller_frees" }
[[library.function]]
c_name = "example_store"
seq_name = "example-store"
stack_effect = "( String -- )"
args = [{ type = "string", pass = "c_string" }]
return = { type = "void" }
"#;
let manifest = FfiManifest::parse(content).unwrap();
let mut bindings = FfiBindings::new();
bindings.add_manifest(&manifest).unwrap();
assert!(bindings.is_ffi_function("example-read"));
assert!(bindings.is_ffi_function("example-store"));
assert!(!bindings.is_ffi_function("not-defined"));
assert_eq!(bindings.linker_flags, vec!["example"]);
}
#[test]
fn test_validate_empty_library_name() {
let content = r#"
[[library]]
name = ""
link = "example"
[[library.function]]
c_name = "example_func"
seq_name = "example-func"
stack_effect = "( String -- String )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty name"));
}
#[test]
fn test_validate_empty_link() {
let content = r#"
[[library]]
name = "example"
link = " "
[[library.function]]
c_name = "example_func"
seq_name = "example-func"
stack_effect = "( String -- String )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty linker flag"));
}
#[test]
fn test_validate_empty_c_name() {
let content = r#"
[[library]]
name = "mylib"
link = "mylib"
[[library.function]]
c_name = ""
seq_name = "my-func"
stack_effect = "( -- Int )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty c_name"));
}
#[test]
fn test_validate_empty_seq_name() {
let content = r#"
[[library]]
name = "mylib"
link = "mylib"
[[library.function]]
c_name = "my_func"
seq_name = ""
stack_effect = "( -- Int )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty seq_name"));
}
#[test]
fn test_validate_empty_stack_effect() {
let content = r#"
[[library]]
name = "mylib"
link = "mylib"
[[library.function]]
c_name = "my_func"
seq_name = "my-func"
stack_effect = ""
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty stack_effect"));
}
#[test]
fn test_validate_malformed_stack_effect_no_parens() {
let content = r#"
[[library]]
name = "mylib"
link = "mylib"
[[library.function]]
c_name = "my_func"
seq_name = "my-func"
stack_effect = "String -- Int"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("malformed stack_effect"));
}
#[test]
fn test_validate_malformed_stack_effect_no_separator() {
let content = r#"
[[library]]
name = "mylib"
link = "mylib"
[[library.function]]
c_name = "my_func"
seq_name = "my-func"
stack_effect = "( String Int )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("malformed stack_effect"));
assert!(err.contains("--"));
}
#[test]
fn test_validate_malformed_stack_effect_unknown_type() {
let content = r#"
[[library]]
name = "mylib"
link = "mylib"
[[library.function]]
c_name = "my_func"
seq_name = "my-func"
stack_effect = "( UnknownType -- Int )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("malformed stack_effect"));
assert!(err.contains("Unknown type"));
}
#[test]
fn test_validate_no_libraries() {
let content = r#"
library = []
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("at least one library"));
}
#[test]
fn test_validate_linker_flag_injection() {
let content = r#"
[[library]]
name = "evil"
link = "evil -Wl,-rpath,/malicious"
[[library.function]]
c_name = "func"
seq_name = "func"
stack_effect = "( -- )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("invalid character"));
}
#[test]
fn test_validate_linker_flag_valid() {
let content = r#"
[[library]]
name = "test"
link = "my-lib_2.0"
[[library.function]]
c_name = "func"
seq_name = "func"
stack_effect = "( -- )"
"#;
let result = FfiManifest::parse(content);
assert!(result.is_ok());
}
}