seqc/
ffi.rs

1//! FFI (Foreign Function Interface) Support
2//!
3//! This module handles parsing of FFI manifests and generating the LLVM IR
4//! for calling external C functions from Seq code.
5//!
6//! FFI is purely a compiler/linker concern - the runtime remains free of
7//! external dependencies.
8//!
9//! # Usage
10//!
11//! ```seq
12//! include ffi:libedit
13//!
14//! : repl ( -- )
15//!   "prompt> " readline
16//!   dup string-empty not if
17//!     dup add-history
18//!     process-input
19//!     repl
20//!   else
21//!     drop
22//!   then
23//! ;
24//! ```
25
26use crate::types::{Effect, StackType, Type};
27use serde::Deserialize;
28use std::collections::HashMap;
29
30/// FFI type mapping for C interop
31#[derive(Debug, Clone, Deserialize, PartialEq)]
32#[serde(rename_all = "snake_case")]
33pub enum FfiType {
34    /// C int/long mapped to Seq Int (i64)
35    Int,
36    /// C char* mapped to Seq String
37    String,
38    /// C void* as raw pointer (represented as Int)
39    Ptr,
40    /// C void - no return value
41    Void,
42}
43
44/// Argument passing mode
45#[derive(Debug, Clone, Deserialize, PartialEq)]
46#[serde(rename_all = "snake_case")]
47pub enum PassMode {
48    /// Convert Seq String to null-terminated char*
49    CString,
50    /// Pass raw pointer value
51    Ptr,
52    /// Pass as C integer
53    Int,
54    /// Pass pointer to value (for out parameters)
55    ByRef,
56}
57
58/// Memory ownership annotation for return values
59#[derive(Debug, Clone, Deserialize, PartialEq)]
60#[serde(rename_all = "snake_case")]
61pub enum Ownership {
62    /// C function allocated memory, caller must free
63    CallerFrees,
64    /// Library owns the memory, don't free
65    Static,
66    /// Valid only during call, copy immediately
67    Borrowed,
68}
69
70/// An argument to an FFI function
71#[derive(Debug, Clone, Deserialize)]
72pub struct FfiArg {
73    /// The type of the argument
74    #[serde(rename = "type")]
75    pub arg_type: FfiType,
76    /// How to pass the argument to C
77    #[serde(default = "default_pass_mode")]
78    pub pass: PassMode,
79    /// Fixed value (for parameters like NULL callbacks)
80    pub value: Option<String>,
81}
82
83fn default_pass_mode() -> PassMode {
84    PassMode::CString
85}
86
87/// Return value specification
88#[derive(Debug, Clone, Deserialize)]
89pub struct FfiReturn {
90    /// The type of the return value
91    #[serde(rename = "type")]
92    pub return_type: FfiType,
93    /// Memory ownership
94    #[serde(default = "default_ownership")]
95    pub ownership: Ownership,
96}
97
98fn default_ownership() -> Ownership {
99    Ownership::Borrowed
100}
101
102/// A function binding in an FFI manifest
103#[derive(Debug, Clone, Deserialize)]
104pub struct FfiFunction {
105    /// C function name (e.g., "sqlite3_open")
106    pub c_name: String,
107    /// Seq word name (e.g., "db-open")
108    pub seq_name: String,
109    /// Stack effect annotation (e.g., "( String -- String )")
110    pub stack_effect: String,
111    /// Function arguments
112    #[serde(default)]
113    pub args: Vec<FfiArg>,
114    /// Return value specification
115    #[serde(rename = "return")]
116    pub return_spec: Option<FfiReturn>,
117}
118
119/// A library binding in an FFI manifest
120#[derive(Debug, Clone, Deserialize)]
121pub struct FfiLibrary {
122    /// Library name for reference
123    pub name: String,
124    /// Linker flag (e.g., "sqlite3" for -lsqlite3)
125    pub link: String,
126    /// Function bindings
127    #[serde(rename = "function", default)]
128    pub functions: Vec<FfiFunction>,
129}
130
131/// Top-level FFI manifest structure
132#[derive(Debug, Clone, Deserialize)]
133pub struct FfiManifest {
134    /// Library definitions (usually just one per manifest)
135    #[serde(rename = "library")]
136    pub libraries: Vec<FfiLibrary>,
137}
138
139impl FfiManifest {
140    /// Parse an FFI manifest from TOML content
141    ///
142    /// Validates the manifest after parsing to catch:
143    /// - Empty library names or linker flags
144    /// - Empty function names (c_name or seq_name)
145    /// - Malformed stack effects
146    pub fn parse(content: &str) -> Result<Self, String> {
147        let manifest: Self =
148            toml::from_str(content).map_err(|e| format!("Failed to parse FFI manifest: {}", e))?;
149        manifest.validate()?;
150        Ok(manifest)
151    }
152
153    /// Validate the manifest for common errors
154    fn validate(&self) -> Result<(), String> {
155        if self.libraries.is_empty() {
156            return Err("FFI manifest must define at least one library".to_string());
157        }
158
159        for (lib_idx, lib) in self.libraries.iter().enumerate() {
160            // Validate library name
161            if lib.name.trim().is_empty() {
162                return Err(format!("FFI library {} has empty name", lib_idx + 1));
163            }
164
165            // Validate linker flag (security: prevent injection of arbitrary flags)
166            if lib.link.trim().is_empty() {
167                return Err(format!("FFI library '{}' has empty linker flag", lib.name));
168            }
169            // Only allow safe characters in linker flag: alphanumeric, dash, underscore, dot
170            for c in lib.link.chars() {
171                if !c.is_alphanumeric() && c != '-' && c != '_' && c != '.' {
172                    return Err(format!(
173                        "FFI library '{}' has invalid character '{}' in linker flag '{}'. \
174                         Only alphanumeric, dash, underscore, and dot are allowed.",
175                        lib.name, c, lib.link
176                    ));
177                }
178            }
179
180            // Validate each function
181            for (func_idx, func) in lib.functions.iter().enumerate() {
182                // Validate c_name
183                if func.c_name.trim().is_empty() {
184                    return Err(format!(
185                        "FFI function {} in library '{}' has empty c_name",
186                        func_idx + 1,
187                        lib.name
188                    ));
189                }
190
191                // Validate seq_name
192                if func.seq_name.trim().is_empty() {
193                    return Err(format!(
194                        "FFI function '{}' in library '{}' has empty seq_name",
195                        func.c_name, lib.name
196                    ));
197                }
198
199                // Validate stack_effect is not empty
200                if func.stack_effect.trim().is_empty() {
201                    return Err(format!(
202                        "FFI function '{}' has empty stack_effect",
203                        func.seq_name
204                    ));
205                }
206
207                // Validate stack_effect parses correctly
208                if let Err(e) = func.effect() {
209                    return Err(format!(
210                        "FFI function '{}' has malformed stack_effect '{}': {}",
211                        func.seq_name, func.stack_effect, e
212                    ));
213                }
214            }
215        }
216
217        Ok(())
218    }
219
220    /// Get all linker flags needed for this manifest
221    pub fn linker_flags(&self) -> Vec<String> {
222        self.libraries.iter().map(|lib| lib.link.clone()).collect()
223    }
224
225    /// Get all function bindings from this manifest
226    pub fn functions(&self) -> impl Iterator<Item = &FfiFunction> {
227        self.libraries.iter().flat_map(|lib| lib.functions.iter())
228    }
229}
230
231impl FfiFunction {
232    /// Parse the stack effect string into an Effect
233    pub fn effect(&self) -> Result<Effect, String> {
234        parse_stack_effect(&self.stack_effect)
235    }
236}
237
238/// Parse a stack effect string like "( String -- String )" into an Effect
239fn parse_stack_effect(s: &str) -> Result<Effect, String> {
240    // Strip parentheses and trim
241    let s = s.trim();
242    let s = s
243        .strip_prefix('(')
244        .ok_or("Stack effect must start with '('")?;
245    let s = s
246        .strip_suffix(')')
247        .ok_or("Stack effect must end with ')'")?;
248    let s = s.trim();
249
250    // Split on "--"
251    let parts: Vec<&str> = s.split("--").collect();
252    if parts.len() != 2 {
253        return Err(format!(
254            "Stack effect must contain exactly one '--', got: {}",
255            s
256        ));
257    }
258
259    let inputs_str = parts[0].trim();
260    let outputs_str = parts[1].trim();
261
262    // Parse input types
263    let mut inputs = StackType::RowVar("a".to_string());
264    for type_name in inputs_str.split_whitespace() {
265        let ty = parse_type_name(type_name)?;
266        inputs = inputs.push(ty);
267    }
268
269    // Parse output types
270    let mut outputs = StackType::RowVar("a".to_string());
271    for type_name in outputs_str.split_whitespace() {
272        let ty = parse_type_name(type_name)?;
273        outputs = outputs.push(ty);
274    }
275
276    Ok(Effect::new(inputs, outputs))
277}
278
279/// Parse a type name string into a Type
280fn parse_type_name(name: &str) -> Result<Type, String> {
281    match name {
282        "Int" => Ok(Type::Int),
283        "Float" => Ok(Type::Float),
284        "Bool" => Ok(Type::Bool),
285        "String" => Ok(Type::String),
286        _ => Err(format!("Unknown type '{}' in stack effect", name)),
287    }
288}
289
290// ============================================================================
291// Embedded FFI Manifests
292// ============================================================================
293
294/// Embedded libedit FFI manifest (BSD-licensed)
295pub const LIBEDIT_MANIFEST: &str = include_str!("../ffi/libedit.toml");
296
297/// Get an embedded FFI manifest by name
298pub fn get_ffi_manifest(name: &str) -> Option<&'static str> {
299    match name {
300        "libedit" => Some(LIBEDIT_MANIFEST),
301        _ => None,
302    }
303}
304
305/// Check if an FFI manifest exists
306pub fn has_ffi_manifest(name: &str) -> bool {
307    get_ffi_manifest(name).is_some()
308}
309
310/// List all available embedded FFI manifests
311pub fn list_ffi_manifests() -> &'static [&'static str] {
312    &["libedit"]
313}
314
315// ============================================================================
316// FFI Code Generation
317// ============================================================================
318
319/// Resolved FFI bindings ready for code generation
320#[derive(Debug, Clone)]
321pub struct FfiBindings {
322    /// Map from Seq word name to C function info
323    pub functions: HashMap<String, FfiFunctionInfo>,
324    /// Linker flags to add
325    pub linker_flags: Vec<String>,
326}
327
328/// Information about an FFI function for code generation
329#[derive(Debug, Clone)]
330pub struct FfiFunctionInfo {
331    /// C function name
332    pub c_name: String,
333    /// Seq word name
334    pub seq_name: String,
335    /// Stack effect for type checking
336    pub effect: Effect,
337    /// Arguments
338    pub args: Vec<FfiArg>,
339    /// Return specification
340    pub return_spec: Option<FfiReturn>,
341}
342
343impl FfiBindings {
344    /// Create empty bindings
345    pub fn new() -> Self {
346        FfiBindings {
347            functions: HashMap::new(),
348            linker_flags: Vec::new(),
349        }
350    }
351
352    /// Add bindings from a manifest
353    pub fn add_manifest(&mut self, manifest: &FfiManifest) -> Result<(), String> {
354        // Add linker flags
355        self.linker_flags.extend(manifest.linker_flags());
356
357        // Add function bindings
358        for func in manifest.functions() {
359            let effect = func.effect()?;
360            let info = FfiFunctionInfo {
361                c_name: func.c_name.clone(),
362                seq_name: func.seq_name.clone(),
363                effect,
364                args: func.args.clone(),
365                return_spec: func.return_spec.clone(),
366            };
367
368            if self.functions.contains_key(&func.seq_name) {
369                return Err(format!(
370                    "FFI function '{}' is already defined",
371                    func.seq_name
372                ));
373            }
374
375            self.functions.insert(func.seq_name.clone(), info);
376        }
377
378        Ok(())
379    }
380
381    /// Check if a word is an FFI function
382    pub fn is_ffi_function(&self, name: &str) -> bool {
383        self.functions.contains_key(name)
384    }
385
386    /// Get FFI function info
387    pub fn get_function(&self, name: &str) -> Option<&FfiFunctionInfo> {
388        self.functions.get(name)
389    }
390
391    /// Get all FFI function names for AST validation
392    pub fn function_names(&self) -> Vec<&str> {
393        self.functions.keys().map(|s| s.as_str()).collect()
394    }
395}
396
397impl Default for FfiBindings {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn test_parse_manifest() {
409        let content = r#"
410[[library]]
411name = "example"
412link = "example"
413
414[[library.function]]
415c_name = "example_func"
416seq_name = "example-func"
417stack_effect = "( String -- String )"
418args = [
419  { type = "string", pass = "c_string" }
420]
421return = { type = "string", ownership = "caller_frees" }
422"#;
423
424        let manifest = FfiManifest::parse(content).unwrap();
425        assert_eq!(manifest.libraries.len(), 1);
426        assert_eq!(manifest.libraries[0].name, "example");
427        assert_eq!(manifest.libraries[0].link, "example");
428        assert_eq!(manifest.libraries[0].functions.len(), 1);
429
430        let func = &manifest.libraries[0].functions[0];
431        assert_eq!(func.c_name, "example_func");
432        assert_eq!(func.seq_name, "example-func");
433        assert_eq!(func.args.len(), 1);
434        assert_eq!(func.args[0].arg_type, FfiType::String);
435        assert_eq!(func.args[0].pass, PassMode::CString);
436    }
437
438    #[test]
439    fn test_parse_stack_effect() {
440        let effect = parse_stack_effect("( String -- String )").unwrap();
441        // Input: ( ..a String )
442        let (rest, top) = effect.inputs.clone().pop().unwrap();
443        assert_eq!(top, Type::String);
444        assert_eq!(rest, StackType::RowVar("a".to_string()));
445        // Output: ( ..a String )
446        let (rest, top) = effect.outputs.clone().pop().unwrap();
447        assert_eq!(top, Type::String);
448        assert_eq!(rest, StackType::RowVar("a".to_string()));
449    }
450
451    #[test]
452    fn test_parse_stack_effect_void() {
453        let effect = parse_stack_effect("( String -- )").unwrap();
454        // Input: ( ..a String )
455        let (rest, top) = effect.inputs.clone().pop().unwrap();
456        assert_eq!(top, Type::String);
457        assert_eq!(rest, StackType::RowVar("a".to_string()));
458        // Output: ( ..a )
459        assert_eq!(effect.outputs, StackType::RowVar("a".to_string()));
460    }
461
462    #[test]
463    fn test_ffi_bindings() {
464        let content = r#"
465[[library]]
466name = "example"
467link = "example"
468
469[[library.function]]
470c_name = "example_read"
471seq_name = "example-read"
472stack_effect = "( String -- String )"
473args = [{ type = "string", pass = "c_string" }]
474return = { type = "string", ownership = "caller_frees" }
475
476[[library.function]]
477c_name = "example_store"
478seq_name = "example-store"
479stack_effect = "( String -- )"
480args = [{ type = "string", pass = "c_string" }]
481return = { type = "void" }
482"#;
483
484        let manifest = FfiManifest::parse(content).unwrap();
485        let mut bindings = FfiBindings::new();
486        bindings.add_manifest(&manifest).unwrap();
487
488        assert!(bindings.is_ffi_function("example-read"));
489        assert!(bindings.is_ffi_function("example-store"));
490        assert!(!bindings.is_ffi_function("not-defined"));
491
492        assert_eq!(bindings.linker_flags, vec!["example"]);
493    }
494
495    // Validation tests
496
497    #[test]
498    fn test_validate_empty_library_name() {
499        let content = r#"
500[[library]]
501name = ""
502link = "example"
503
504[[library.function]]
505c_name = "example_func"
506seq_name = "example-func"
507stack_effect = "( String -- String )"
508"#;
509
510        let result = FfiManifest::parse(content);
511        assert!(result.is_err());
512        assert!(result.unwrap_err().contains("empty name"));
513    }
514
515    #[test]
516    fn test_validate_empty_link() {
517        let content = r#"
518[[library]]
519name = "example"
520link = "  "
521
522[[library.function]]
523c_name = "example_func"
524seq_name = "example-func"
525stack_effect = "( String -- String )"
526"#;
527
528        let result = FfiManifest::parse(content);
529        assert!(result.is_err());
530        assert!(result.unwrap_err().contains("empty linker flag"));
531    }
532
533    #[test]
534    fn test_validate_empty_c_name() {
535        let content = r#"
536[[library]]
537name = "mylib"
538link = "mylib"
539
540[[library.function]]
541c_name = ""
542seq_name = "my-func"
543stack_effect = "( -- Int )"
544"#;
545
546        let result = FfiManifest::parse(content);
547        assert!(result.is_err());
548        assert!(result.unwrap_err().contains("empty c_name"));
549    }
550
551    #[test]
552    fn test_validate_empty_seq_name() {
553        let content = r#"
554[[library]]
555name = "mylib"
556link = "mylib"
557
558[[library.function]]
559c_name = "my_func"
560seq_name = ""
561stack_effect = "( -- Int )"
562"#;
563
564        let result = FfiManifest::parse(content);
565        assert!(result.is_err());
566        assert!(result.unwrap_err().contains("empty seq_name"));
567    }
568
569    #[test]
570    fn test_validate_empty_stack_effect() {
571        let content = r#"
572[[library]]
573name = "mylib"
574link = "mylib"
575
576[[library.function]]
577c_name = "my_func"
578seq_name = "my-func"
579stack_effect = ""
580"#;
581
582        let result = FfiManifest::parse(content);
583        assert!(result.is_err());
584        assert!(result.unwrap_err().contains("empty stack_effect"));
585    }
586
587    #[test]
588    fn test_validate_malformed_stack_effect_no_parens() {
589        let content = r#"
590[[library]]
591name = "mylib"
592link = "mylib"
593
594[[library.function]]
595c_name = "my_func"
596seq_name = "my-func"
597stack_effect = "String -- Int"
598"#;
599
600        let result = FfiManifest::parse(content);
601        assert!(result.is_err());
602        let err = result.unwrap_err();
603        assert!(err.contains("malformed stack_effect"));
604    }
605
606    #[test]
607    fn test_validate_malformed_stack_effect_no_separator() {
608        let content = r#"
609[[library]]
610name = "mylib"
611link = "mylib"
612
613[[library.function]]
614c_name = "my_func"
615seq_name = "my-func"
616stack_effect = "( String Int )"
617"#;
618
619        let result = FfiManifest::parse(content);
620        assert!(result.is_err());
621        let err = result.unwrap_err();
622        assert!(err.contains("malformed stack_effect"));
623        assert!(err.contains("--"));
624    }
625
626    #[test]
627    fn test_validate_malformed_stack_effect_unknown_type() {
628        let content = r#"
629[[library]]
630name = "mylib"
631link = "mylib"
632
633[[library.function]]
634c_name = "my_func"
635seq_name = "my-func"
636stack_effect = "( UnknownType -- Int )"
637"#;
638
639        let result = FfiManifest::parse(content);
640        assert!(result.is_err());
641        let err = result.unwrap_err();
642        assert!(err.contains("malformed stack_effect"));
643        assert!(err.contains("Unknown type"));
644    }
645
646    #[test]
647    fn test_validate_no_libraries() {
648        // TOML requires the `library` field to be present since it's not marked with #[serde(default)]
649        // An empty manifest will fail TOML parsing, not our custom validation
650        // But we can test with an explicit empty array
651        let content = r#"
652library = []
653"#;
654
655        let result = FfiManifest::parse(content);
656        assert!(result.is_err());
657        assert!(result.unwrap_err().contains("at least one library"));
658    }
659
660    #[test]
661    fn test_validate_linker_flag_injection() {
662        // Security: reject linker flags with potentially dangerous characters
663        let content = r#"
664[[library]]
665name = "evil"
666link = "evil -Wl,-rpath,/malicious"
667
668[[library.function]]
669c_name = "func"
670seq_name = "func"
671stack_effect = "( -- )"
672"#;
673
674        let result = FfiManifest::parse(content);
675        assert!(result.is_err());
676        let err = result.unwrap_err();
677        assert!(err.contains("invalid character"));
678    }
679
680    #[test]
681    fn test_validate_linker_flag_valid() {
682        // Valid linker flags: alphanumeric, dash, underscore, dot
683        let content = r#"
684[[library]]
685name = "test"
686link = "my-lib_2.0"
687
688[[library.function]]
689c_name = "func"
690seq_name = "func"
691stack_effect = "( -- )"
692"#;
693
694        let result = FfiManifest::parse(content);
695        assert!(result.is_ok());
696    }
697}