Skip to main content

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 all FFI function names for AST validation
387    pub fn function_names(&self) -> Vec<&str> {
388        self.functions.keys().map(|s| s.as_str()).collect()
389    }
390}
391
392impl Default for FfiBindings {
393    fn default() -> Self {
394        Self::new()
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn test_parse_manifest() {
404        let content = r#"
405[[library]]
406name = "example"
407link = "example"
408
409[[library.function]]
410c_name = "example_func"
411seq_name = "example-func"
412stack_effect = "( String -- String )"
413args = [
414  { type = "string", pass = "c_string" }
415]
416return = { type = "string", ownership = "caller_frees" }
417"#;
418
419        let manifest = FfiManifest::parse(content).unwrap();
420        assert_eq!(manifest.libraries.len(), 1);
421        assert_eq!(manifest.libraries[0].name, "example");
422        assert_eq!(manifest.libraries[0].link, "example");
423        assert_eq!(manifest.libraries[0].functions.len(), 1);
424
425        let func = &manifest.libraries[0].functions[0];
426        assert_eq!(func.c_name, "example_func");
427        assert_eq!(func.seq_name, "example-func");
428        assert_eq!(func.args.len(), 1);
429        assert_eq!(func.args[0].arg_type, FfiType::String);
430        assert_eq!(func.args[0].pass, PassMode::CString);
431    }
432
433    #[test]
434    fn test_parse_stack_effect() {
435        let effect = parse_stack_effect("( String -- String )").unwrap();
436        // Input: ( ..a String )
437        let (rest, top) = effect.inputs.clone().pop().unwrap();
438        assert_eq!(top, Type::String);
439        assert_eq!(rest, StackType::RowVar("a".to_string()));
440        // Output: ( ..a String )
441        let (rest, top) = effect.outputs.clone().pop().unwrap();
442        assert_eq!(top, Type::String);
443        assert_eq!(rest, StackType::RowVar("a".to_string()));
444    }
445
446    #[test]
447    fn test_parse_stack_effect_void() {
448        let effect = parse_stack_effect("( String -- )").unwrap();
449        // Input: ( ..a String )
450        let (rest, top) = effect.inputs.clone().pop().unwrap();
451        assert_eq!(top, Type::String);
452        assert_eq!(rest, StackType::RowVar("a".to_string()));
453        // Output: ( ..a )
454        assert_eq!(effect.outputs, StackType::RowVar("a".to_string()));
455    }
456
457    #[test]
458    fn test_ffi_bindings() {
459        let content = r#"
460[[library]]
461name = "example"
462link = "example"
463
464[[library.function]]
465c_name = "example_read"
466seq_name = "example-read"
467stack_effect = "( String -- String )"
468args = [{ type = "string", pass = "c_string" }]
469return = { type = "string", ownership = "caller_frees" }
470
471[[library.function]]
472c_name = "example_store"
473seq_name = "example-store"
474stack_effect = "( String -- )"
475args = [{ type = "string", pass = "c_string" }]
476return = { type = "void" }
477"#;
478
479        let manifest = FfiManifest::parse(content).unwrap();
480        let mut bindings = FfiBindings::new();
481        bindings.add_manifest(&manifest).unwrap();
482
483        assert!(bindings.is_ffi_function("example-read"));
484        assert!(bindings.is_ffi_function("example-store"));
485        assert!(!bindings.is_ffi_function("not-defined"));
486
487        assert_eq!(bindings.linker_flags, vec!["example"]);
488    }
489
490    // Validation tests
491
492    #[test]
493    fn test_validate_empty_library_name() {
494        let content = r#"
495[[library]]
496name = ""
497link = "example"
498
499[[library.function]]
500c_name = "example_func"
501seq_name = "example-func"
502stack_effect = "( String -- String )"
503"#;
504
505        let result = FfiManifest::parse(content);
506        assert!(result.is_err());
507        assert!(result.unwrap_err().contains("empty name"));
508    }
509
510    #[test]
511    fn test_validate_empty_link() {
512        let content = r#"
513[[library]]
514name = "example"
515link = "  "
516
517[[library.function]]
518c_name = "example_func"
519seq_name = "example-func"
520stack_effect = "( String -- String )"
521"#;
522
523        let result = FfiManifest::parse(content);
524        assert!(result.is_err());
525        assert!(result.unwrap_err().contains("empty linker flag"));
526    }
527
528    #[test]
529    fn test_validate_empty_c_name() {
530        let content = r#"
531[[library]]
532name = "mylib"
533link = "mylib"
534
535[[library.function]]
536c_name = ""
537seq_name = "my-func"
538stack_effect = "( -- Int )"
539"#;
540
541        let result = FfiManifest::parse(content);
542        assert!(result.is_err());
543        assert!(result.unwrap_err().contains("empty c_name"));
544    }
545
546    #[test]
547    fn test_validate_empty_seq_name() {
548        let content = r#"
549[[library]]
550name = "mylib"
551link = "mylib"
552
553[[library.function]]
554c_name = "my_func"
555seq_name = ""
556stack_effect = "( -- Int )"
557"#;
558
559        let result = FfiManifest::parse(content);
560        assert!(result.is_err());
561        assert!(result.unwrap_err().contains("empty seq_name"));
562    }
563
564    #[test]
565    fn test_validate_empty_stack_effect() {
566        let content = r#"
567[[library]]
568name = "mylib"
569link = "mylib"
570
571[[library.function]]
572c_name = "my_func"
573seq_name = "my-func"
574stack_effect = ""
575"#;
576
577        let result = FfiManifest::parse(content);
578        assert!(result.is_err());
579        assert!(result.unwrap_err().contains("empty stack_effect"));
580    }
581
582    #[test]
583    fn test_validate_malformed_stack_effect_no_parens() {
584        let content = r#"
585[[library]]
586name = "mylib"
587link = "mylib"
588
589[[library.function]]
590c_name = "my_func"
591seq_name = "my-func"
592stack_effect = "String -- Int"
593"#;
594
595        let result = FfiManifest::parse(content);
596        assert!(result.is_err());
597        let err = result.unwrap_err();
598        assert!(err.contains("malformed stack_effect"));
599    }
600
601    #[test]
602    fn test_validate_malformed_stack_effect_no_separator() {
603        let content = r#"
604[[library]]
605name = "mylib"
606link = "mylib"
607
608[[library.function]]
609c_name = "my_func"
610seq_name = "my-func"
611stack_effect = "( String Int )"
612"#;
613
614        let result = FfiManifest::parse(content);
615        assert!(result.is_err());
616        let err = result.unwrap_err();
617        assert!(err.contains("malformed stack_effect"));
618        assert!(err.contains("--"));
619    }
620
621    #[test]
622    fn test_validate_malformed_stack_effect_unknown_type() {
623        let content = r#"
624[[library]]
625name = "mylib"
626link = "mylib"
627
628[[library.function]]
629c_name = "my_func"
630seq_name = "my-func"
631stack_effect = "( UnknownType -- Int )"
632"#;
633
634        let result = FfiManifest::parse(content);
635        assert!(result.is_err());
636        let err = result.unwrap_err();
637        assert!(err.contains("malformed stack_effect"));
638        assert!(err.contains("Unknown type"));
639    }
640
641    #[test]
642    fn test_validate_no_libraries() {
643        // TOML requires the `library` field to be present since it's not marked with #[serde(default)]
644        // An empty manifest will fail TOML parsing, not our custom validation
645        // But we can test with an explicit empty array
646        let content = r#"
647library = []
648"#;
649
650        let result = FfiManifest::parse(content);
651        assert!(result.is_err());
652        assert!(result.unwrap_err().contains("at least one library"));
653    }
654
655    #[test]
656    fn test_validate_linker_flag_injection() {
657        // Security: reject linker flags with potentially dangerous characters
658        let content = r#"
659[[library]]
660name = "evil"
661link = "evil -Wl,-rpath,/malicious"
662
663[[library.function]]
664c_name = "func"
665seq_name = "func"
666stack_effect = "( -- )"
667"#;
668
669        let result = FfiManifest::parse(content);
670        assert!(result.is_err());
671        let err = result.unwrap_err();
672        assert!(err.contains("invalid character"));
673    }
674
675    #[test]
676    fn test_validate_linker_flag_valid() {
677        // Valid linker flags: alphanumeric, dash, underscore, dot
678        let content = r#"
679[[library]]
680name = "test"
681link = "my-lib_2.0"
682
683[[library.function]]
684c_name = "func"
685seq_name = "func"
686stack_effect = "( -- )"
687"#;
688
689        let result = FfiManifest::parse(content);
690        assert!(result.is_ok());
691    }
692}