seqc/
config.rs

1//! Compiler configuration for extensibility
2//!
3//! This module provides configuration types that allow external projects
4//! to extend the Seq compiler with additional builtins without modifying
5//! the core compiler.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use seqc::{CompilerConfig, ExternalBuiltin};
11//!
12//! // Define builtins provided by your runtime extension
13//! let config = CompilerConfig::new()
14//!     .with_builtin(ExternalBuiltin::new(
15//!         "journal-append",
16//!         "my_runtime_journal_append",
17//!     ))
18//!     .with_builtin(ExternalBuiltin::new(
19//!         "actor-send",
20//!         "my_runtime_actor_send",
21//!     ));
22//!
23//! // Compile with extended builtins
24//! compile_file_with_config(source_path, output_path, false, &config)?;
25//! ```
26
27use crate::types::Effect;
28use std::path::PathBuf;
29
30/// Definition of an external builtin function
31///
32/// External builtins are functions provided by a runtime extension
33/// (like an actor system) that should be callable from Seq code.
34///
35/// # Type Safety
36///
37/// External builtins can optionally specify their stack effect for type checking.
38/// This affects how the type checker validates code using the builtin:
39///
40/// - **With effect**: The type checker enforces the declared signature.
41///   Use this when you know the exact stack effect (recommended).
42///
43/// - **Without effect (`None`)**: The type checker assigns a maximally polymorphic
44///   signature `( ..a -- ..b )`, meaning "accepts any stack, produces any stack".
45///   This **disables type checking** for calls to this builtin - type errors
46///   involving this builtin will only be caught at runtime.
47///
48/// For best type safety, always provide an explicit effect when possible.
49#[derive(Debug, Clone)]
50pub struct ExternalBuiltin {
51    /// The name used in Seq code (e.g., "journal-append")
52    pub seq_name: String,
53
54    /// The symbol name for linking (e.g., "seq_actors_journal_append")
55    ///
56    /// Must contain only alphanumeric characters, underscores, and periods.
57    /// This is validated at construction time to prevent LLVM IR injection.
58    pub symbol: String,
59
60    /// Optional stack effect for type checking.
61    ///
62    /// - `Some(effect)`: Type checker enforces this signature
63    /// - `None`: Type checker uses maximally polymorphic `( ..a -- ..b )`,
64    ///   effectively disabling type checking for this builtin
65    ///
66    /// **Warning**: Using `None` can hide type errors until runtime.
67    /// Prefer providing an explicit effect when the signature is known.
68    pub effect: Option<Effect>,
69}
70
71impl ExternalBuiltin {
72    /// Validate that a symbol name is safe for LLVM IR
73    ///
74    /// Valid symbols contain only: alphanumeric characters, underscores, and periods.
75    /// This prevents injection of arbitrary LLVM IR directives.
76    fn validate_symbol(symbol: &str) -> Result<(), String> {
77        if symbol.is_empty() {
78            return Err("Symbol name cannot be empty".to_string());
79        }
80        for c in symbol.chars() {
81            if !c.is_alphanumeric() && c != '_' && c != '.' {
82                return Err(format!(
83                    "Invalid character '{}' in symbol '{}'. \
84                     Symbols may only contain alphanumeric characters, underscores, and periods.",
85                    c, symbol
86                ));
87            }
88        }
89        Ok(())
90    }
91
92    /// Create a new external builtin with just name and symbol
93    ///
94    /// # Panics
95    ///
96    /// Panics if the symbol contains invalid characters for LLVM IR.
97    /// Valid symbols contain only alphanumeric characters, underscores, and periods.
98    pub fn new(seq_name: impl Into<String>, symbol: impl Into<String>) -> Self {
99        let symbol = symbol.into();
100        Self::validate_symbol(&symbol).expect("Invalid symbol name");
101        ExternalBuiltin {
102            seq_name: seq_name.into(),
103            symbol,
104            effect: None,
105        }
106    }
107
108    /// Create a new external builtin with a stack effect
109    ///
110    /// # Panics
111    ///
112    /// Panics if the symbol contains invalid characters for LLVM IR.
113    pub fn with_effect(
114        seq_name: impl Into<String>,
115        symbol: impl Into<String>,
116        effect: Effect,
117    ) -> Self {
118        let symbol = symbol.into();
119        Self::validate_symbol(&symbol).expect("Invalid symbol name");
120        ExternalBuiltin {
121            seq_name: seq_name.into(),
122            symbol,
123            effect: Some(effect),
124        }
125    }
126}
127
128/// Configuration for the Seq compiler
129///
130/// Allows external projects to extend the compiler with additional
131/// builtins and configuration options.
132#[derive(Debug, Clone, Default)]
133pub struct CompilerConfig {
134    /// External builtins to include in compilation
135    pub external_builtins: Vec<ExternalBuiltin>,
136
137    /// Additional library paths for linking
138    pub library_paths: Vec<String>,
139
140    /// Additional libraries to link
141    pub libraries: Vec<String>,
142
143    /// External FFI manifest paths to load
144    ///
145    /// These manifests are loaded in addition to any `include ffi:*` statements
146    /// in the source code. Use this to provide custom FFI bindings without
147    /// embedding them in the compiler.
148    pub ffi_manifest_paths: Vec<PathBuf>,
149
150    /// Pure inline test mode: bypass scheduler, return top of stack as exit code.
151    /// Only supports inline operations (integers, arithmetic, stack ops).
152    /// Used for testing and benchmarking pure computation without FFI overhead.
153    pub pure_inline_test: bool,
154}
155
156impl CompilerConfig {
157    /// Create a new empty configuration
158    pub fn new() -> Self {
159        CompilerConfig::default()
160    }
161
162    /// Add an external builtin (builder pattern)
163    pub fn with_builtin(mut self, builtin: ExternalBuiltin) -> Self {
164        self.external_builtins.push(builtin);
165        self
166    }
167
168    /// Add multiple external builtins
169    pub fn with_builtins(mut self, builtins: impl IntoIterator<Item = ExternalBuiltin>) -> Self {
170        self.external_builtins.extend(builtins);
171        self
172    }
173
174    /// Add a library path for linking
175    pub fn with_library_path(mut self, path: impl Into<String>) -> Self {
176        self.library_paths.push(path.into());
177        self
178    }
179
180    /// Add a library to link
181    pub fn with_library(mut self, lib: impl Into<String>) -> Self {
182        self.libraries.push(lib.into());
183        self
184    }
185
186    /// Add an external FFI manifest path
187    ///
188    /// The manifest will be loaded and its functions made available
189    /// during compilation, in addition to any `include ffi:*` statements.
190    pub fn with_ffi_manifest(mut self, path: impl Into<PathBuf>) -> Self {
191        self.ffi_manifest_paths.push(path.into());
192        self
193    }
194
195    /// Add multiple external FFI manifest paths
196    pub fn with_ffi_manifests(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
197        self.ffi_manifest_paths.extend(paths);
198        self
199    }
200
201    /// Get seq names of all external builtins (for AST validation)
202    pub fn external_names(&self) -> Vec<&str> {
203        self.external_builtins
204            .iter()
205            .map(|b| b.seq_name.as_str())
206            .collect()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_external_builtin_new() {
216        let builtin = ExternalBuiltin::new("my-func", "runtime_my_func");
217        assert_eq!(builtin.seq_name, "my-func");
218        assert_eq!(builtin.symbol, "runtime_my_func");
219        assert!(builtin.effect.is_none());
220    }
221
222    #[test]
223    fn test_config_builder() {
224        let config = CompilerConfig::new()
225            .with_builtin(ExternalBuiltin::new("func-a", "sym_a"))
226            .with_builtin(ExternalBuiltin::new("func-b", "sym_b"))
227            .with_library_path("/custom/lib")
228            .with_library("myruntime");
229
230        assert_eq!(config.external_builtins.len(), 2);
231        assert_eq!(config.library_paths, vec!["/custom/lib"]);
232        assert_eq!(config.libraries, vec!["myruntime"]);
233    }
234
235    #[test]
236    fn test_external_names() {
237        let config = CompilerConfig::new()
238            .with_builtin(ExternalBuiltin::new("func-a", "sym_a"))
239            .with_builtin(ExternalBuiltin::new("func-b", "sym_b"));
240
241        let names = config.external_names();
242        assert_eq!(names, vec!["func-a", "func-b"]);
243    }
244
245    #[test]
246    fn test_symbol_validation_valid() {
247        // Valid symbols: alphanumeric, underscores, periods
248        let _ = ExternalBuiltin::new("test", "valid_symbol");
249        let _ = ExternalBuiltin::new("test", "valid.symbol.123");
250        let _ = ExternalBuiltin::new("test", "ValidCamelCase");
251        let _ = ExternalBuiltin::new("test", "seq_actors_journal_append");
252    }
253
254    #[test]
255    #[should_panic(expected = "Invalid symbol name")]
256    fn test_symbol_validation_rejects_hyphen() {
257        // Hyphens are not valid in LLVM symbols
258        let _ = ExternalBuiltin::new("test", "invalid-symbol");
259    }
260
261    #[test]
262    #[should_panic(expected = "Invalid symbol name")]
263    fn test_symbol_validation_rejects_at() {
264        // @ could be used for LLVM IR injection
265        let _ = ExternalBuiltin::new("test", "@malicious");
266    }
267
268    #[test]
269    #[should_panic(expected = "Invalid symbol name")]
270    fn test_symbol_validation_rejects_empty() {
271        let _ = ExternalBuiltin::new("test", "");
272    }
273}