Skip to main content

seqc/
script.rs

1//! Script mode for running .seq files directly
2//!
3//! Enables `.seq` files to run directly with shebangs:
4//! ```bash
5//! #!/usr/bin/env seqc
6//! : main ( -- Int ) "Hello from script!" io.write-line 0 ;
7//! ```
8//!
9//! Running `seqc script.seq arg1 arg2` or `./script.seq` (with shebang) will:
10//! 1. Detect script mode (first arg is a `.seq` file)
11//! 2. Compile with `-O0` for fast compilation
12//! 3. Cache compiled binary (keyed by source + include hashes)
13//! 4. Run cached binary or compile -> cache -> run
14//! 5. Pass remaining argv to the script
15
16use crate::CompilerConfig;
17use crate::config::OptimizationLevel;
18use crate::parser::Parser;
19use crate::resolver::{Resolver, find_stdlib};
20use crate::stdlib_embed;
21use sha2::{Digest, Sha256};
22use std::ffi::OsString;
23use std::fs;
24use std::path::{Path, PathBuf};
25
26/// Get cache directory: $XDG_CACHE_HOME/seq/ or ~/.cache/seq/
27pub fn get_cache_dir() -> Option<PathBuf> {
28    // Try XDG_CACHE_HOME first
29    if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
30        let path = PathBuf::from(xdg_cache);
31        if path.is_absolute() {
32            return Some(path.join("seq"));
33        }
34    }
35
36    // Fall back to ~/.cache/seq/
37    if let Ok(home) = std::env::var("HOME") {
38        return Some(PathBuf::from(home).join(".cache").join("seq"));
39    }
40
41    None
42}
43
44/// Compute cache key from source + all transitive includes
45///
46/// Algorithm:
47/// 1. Hash main source file content
48/// 2. Sort and hash all filesystem includes
49/// 3. Sort and hash all embedded stdlib modules
50/// 4. Combine into final SHA-256 hex string
51pub fn compute_cache_key(
52    source_path: &Path,
53    source_files: &[PathBuf],
54    embedded_modules: &[String],
55) -> Result<String, String> {
56    let mut hasher = Sha256::new();
57
58    // Hash the main source file content
59    let main_content =
60        fs::read(source_path).map_err(|e| format!("Failed to read source file: {}", e))?;
61    hasher.update(&main_content);
62
63    // Sort and hash all filesystem includes
64    let mut sorted_files: Vec<_> = source_files.iter().collect();
65    sorted_files.sort();
66    for file in sorted_files {
67        if file != source_path {
68            // Don't double-hash the main file
69            let content = fs::read(file)
70                .map_err(|e| format!("Failed to read included file '{}': {}", file.display(), e))?;
71            hasher.update(&content);
72        }
73    }
74
75    // Sort and hash all embedded stdlib modules
76    let mut sorted_modules: Vec<_> = embedded_modules.iter().collect();
77    sorted_modules.sort();
78    for module_name in sorted_modules {
79        if let Some(content) = stdlib_embed::get_stdlib(module_name) {
80            hasher.update(content.as_bytes());
81        }
82    }
83
84    let hash = hasher.finalize();
85    Ok(hex::encode(hash))
86}
87
88/// Strip shebang line from source if present
89///
90/// Replaces the first line with a comment if it starts with `#!`
91/// so that line numbers in error messages remain correct.
92fn strip_shebang(source: &str) -> std::borrow::Cow<'_, str> {
93    if source.starts_with("#!") {
94        // Replace shebang with comment of same length to preserve line numbers
95        if let Some(newline_pos) = source.find('\n') {
96            let mut result = String::with_capacity(source.len());
97            result.push('#');
98            result.push_str(&" ".repeat(newline_pos - 1));
99            result.push_str(&source[newline_pos..]);
100            std::borrow::Cow::Owned(result)
101        } else {
102            // Single line file with just shebang
103            std::borrow::Cow::Borrowed("#")
104        }
105    } else {
106        std::borrow::Cow::Borrowed(source)
107    }
108}
109
110/// Prepare a script for execution: parse, resolve includes, and compile if needed.
111/// Returns the path to the cached binary.
112///
113/// # Symlink Behavior
114///
115/// The source path is canonicalized, which resolves symlinks to their target.
116/// This means the same script accessed via different symlinks will share one
117/// cache entry (based on the resolved path's content hash).
118fn prepare_script(source_path: &Path) -> Result<PathBuf, String> {
119    // Canonicalize the source path
120    let source_path = source_path.canonicalize().map_err(|e| {
121        format!(
122            "Failed to find source file '{}': {}",
123            source_path.display(),
124            e
125        )
126    })?;
127
128    // Get cache directory
129    let cache_dir =
130        get_cache_dir().ok_or_else(|| "Could not determine cache directory".to_string())?;
131
132    // Parse the source to find includes (strip shebang if present)
133    let source_raw = fs::read_to_string(&source_path)
134        .map_err(|e| format!("Failed to read source file: {}", e))?;
135    let source = strip_shebang(&source_raw);
136
137    let mut parser = Parser::new(&source);
138    let program = parser.parse()?;
139
140    // Resolve includes to get list of dependencies
141    let (source_files, embedded_modules) = if !program.includes.is_empty() {
142        let stdlib_path = find_stdlib();
143        let mut resolver = Resolver::new(stdlib_path);
144        let result = resolver.resolve(&source_path, program)?;
145        (result.source_files, result.embedded_modules)
146    } else {
147        (vec![source_path.clone()], Vec::new())
148    };
149
150    // Compute cache key (use raw source for consistent hashing)
151    let cache_key = compute_cache_key(&source_path, &source_files, &embedded_modules)?;
152    let cached_binary = cache_dir.join(&cache_key);
153
154    // Check if cached binary exists
155    if cached_binary.exists() {
156        return Ok(cached_binary);
157    }
158
159    // Create cache directory if needed
160    fs::create_dir_all(&cache_dir)
161        .map_err(|e| format!("Failed to create cache directory: {}", e))?;
162
163    // Use process ID in temp file name to avoid collisions between parallel compilations
164    let pid = std::process::id();
165    let temp_binary = cache_dir.join(format!("{}.{}.tmp", cache_key, pid));
166    let temp_source = cache_dir.join(format!("{}.{}.seq", cache_key, pid));
167
168    // Write preprocessed source to a temp file for compilation
169    fs::write(&temp_source, source.as_ref())
170        .map_err(|e| format!("Failed to write temp source: {}", e))?;
171
172    // Compile with -O0 for fast compilation
173    let config = CompilerConfig::new().with_optimization_level(OptimizationLevel::O0);
174
175    let compile_result =
176        crate::compile_file_with_config(&temp_source, &temp_binary, false, &config);
177
178    // Clean up temp source file
179    fs::remove_file(&temp_source).ok();
180
181    // Handle compilation result
182    if let Err(e) = compile_result {
183        // Clean up temp binary on compilation failure
184        fs::remove_file(&temp_binary).ok();
185        return Err(e);
186    }
187
188    // Try to atomically move to final location
189    // If another process already created the cached binary, that's fine - use it
190    if fs::rename(&temp_binary, &cached_binary).is_err() {
191        // Rename failed - check if cached binary now exists (race with another process)
192        if cached_binary.exists() {
193            // Another process won the race, clean up our temp and use theirs
194            fs::remove_file(&temp_binary).ok();
195        } else {
196            // Rename failed for another reason, clean up and report error
197            fs::remove_file(&temp_binary).ok();
198            return Err("Failed to cache compiled binary".to_string());
199        }
200    }
201
202    Ok(cached_binary)
203}
204
205/// Run a .seq script (compile if needed, then exec)
206///
207/// This function does not return on success - it execs the compiled binary.
208/// On error, it returns an Err with the error message.
209#[cfg(unix)]
210pub fn run_script(
211    source_path: &Path,
212    args: &[OsString],
213) -> Result<std::convert::Infallible, String> {
214    use std::os::unix::process::CommandExt;
215
216    let cached_binary = prepare_script(source_path)?;
217
218    // Exec the cached binary with script args
219    let err = std::process::Command::new(&cached_binary).args(args).exec();
220
221    // If we get here, exec failed
222    Err(format!("Failed to execute script: {}", err))
223}
224
225/// Run a .seq script on non-Unix platforms (spawn + wait instead of exec)
226#[cfg(not(unix))]
227pub fn run_script(
228    source_path: &Path,
229    args: &[OsString],
230) -> Result<std::convert::Infallible, String> {
231    let cached_binary = prepare_script(source_path)?;
232
233    // Spawn the cached binary and wait for it
234    let status = std::process::Command::new(&cached_binary)
235        .args(args)
236        .status()
237        .map_err(|e| format!("Failed to execute script: {}", e))?;
238
239    std::process::exit(status.code().unwrap_or(1));
240}
241
242#[cfg(test)]
243mod tests;