1use tidepool_codegen::jit_machine::JitEffectMachine;
7pub use tidepool_codegen::jit_machine::JitError;
8pub use tidepool_effect::dispatch::DispatchEffect;
9pub use tidepool_eval::value::Value;
10use tidepool_repr::serial::{read_cbor, read_metadata, ReadError};
11use tidepool_repr::{CoreExpr, DataConTable};
12use std::fmt;
13use std::io;
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use tempfile::TempDir;
17
18mod cache;
19
20pub type CompileResult = (CoreExpr, DataConTable);
22
23#[derive(Debug)]
25pub enum CompileError {
26 Io(io::Error),
28 ExtractFailed(String),
30 ReadError(ReadError),
32 MissingOutput(PathBuf),
34}
35
36impl fmt::Display for CompileError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 CompileError::Io(e) => write!(f, "I/O error: {}", e),
40 CompileError::ExtractFailed(msg) => write!(f, "Haskell compilation failed:\n{}", msg),
41 CompileError::ReadError(e) => write!(f, "CBOR deserialization error: {}", e),
42 CompileError::MissingOutput(path) => {
43 write!(f, "Missing output file from extractor: {}", path.display())
44 }
45 }
46 }
47}
48
49impl std::error::Error for CompileError {
50 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51 match self {
52 CompileError::Io(e) => Some(e),
53 CompileError::ReadError(e) => Some(e),
54 _ => None,
55 }
56 }
57}
58
59impl From<io::Error> for CompileError {
60 fn from(e: io::Error) -> Self {
61 CompileError::Io(e)
62 }
63}
64
65impl From<ReadError> for CompileError {
66 fn from(e: ReadError) -> Self {
67 CompileError::ReadError(e)
68 }
69}
70
71#[derive(Debug)]
73pub enum RuntimeError {
74 Compile(CompileError),
75 Jit(JitError),
76}
77
78impl fmt::Display for RuntimeError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 RuntimeError::Compile(e) => write!(f, "{}", e),
82 RuntimeError::Jit(e) => write!(f, "{}", e),
83 }
84 }
85}
86
87impl std::error::Error for RuntimeError {
88 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89 match self {
90 RuntimeError::Compile(e) => Some(e),
91 RuntimeError::Jit(e) => Some(e),
92 }
93 }
94}
95
96impl From<CompileError> for RuntimeError {
97 fn from(e: CompileError) -> Self {
98 Self::Compile(e)
99 }
100}
101
102impl From<JitError> for RuntimeError {
103 fn from(e: JitError) -> Self {
104 Self::Jit(e)
105 }
106}
107
108pub fn compile_haskell(
127 source: &str,
128 target: &str,
129 include: &[&Path],
130) -> Result<CompileResult, CompileError> {
131 let key = cache::cache_key(source, target, include);
132 if let Some((expr_bytes, meta_bytes)) = cache::cache_load(&key) {
133 if let (Ok(expr), Ok(table)) = (read_cbor(&expr_bytes), read_metadata(&meta_bytes)) {
136 return Ok((expr, table));
137 }
138 }
139
140 let temp_dir = TempDir::new()?;
142 let input_path = temp_dir.path().join("input.hs");
143 std::fs::write(&input_path, source)?;
144
145 let mut cmd = Command::new("tidepool-extract");
148 cmd.arg(&input_path);
149 cmd.arg("--output-dir").arg(temp_dir.path());
150 cmd.arg("--target").arg(target);
151
152 for path in include {
153 cmd.arg("--include").arg(path);
154 }
155
156 let output = cmd.output().map_err(|e| {
157 if e.kind() == io::ErrorKind::NotFound {
158 io::Error::new(
159 io::ErrorKind::NotFound,
160 "tidepool-extract not found on PATH. Ensure the Tidepool harness is installed.",
161 )
162 } else {
163 e
164 }
165 })?;
166
167 if !output.status.success() {
168 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
169 return Err(CompileError::ExtractFailed(stderr));
170 }
171
172 let expr_path = temp_dir.path().join(format!("{}.cbor", target));
174 let meta_path = temp_dir.path().join("meta.cbor");
175
176 if !expr_path.exists() {
177 return Err(CompileError::MissingOutput(expr_path));
178 }
179 if !meta_path.exists() {
180 return Err(CompileError::MissingOutput(meta_path));
181 }
182
183 let expr_bytes = std::fs::read(&expr_path)?;
184 let meta_bytes = std::fs::read(&meta_path)?;
185
186 let expr = read_cbor(&expr_bytes)?;
187 let table = read_metadata(&meta_bytes)?;
188
189 cache::cache_store(&key, &expr_bytes, &meta_bytes);
191
192 Ok((expr, table))
193}
194
195const DEFAULT_NURSERY_SIZE: usize = 1 << 20; pub fn compile_and_run_with_nursery_size<U, H: DispatchEffect<U>>(
212 source: &str,
213 target: &str,
214 include: &[&Path],
215 handlers: &mut H,
216 user: &U,
217 nursery_size: usize,
218) -> Result<Value, RuntimeError> {
219 let (expr, table) = compile_haskell(source, target, include)?;
220 let mut machine = JitEffectMachine::compile(&expr, &table, nursery_size)?;
221 let value = machine.run(&table, handlers, user)?;
222 Ok(value)
223}
224
225pub fn compile_and_run<U, H: DispatchEffect<U>>(
239 source: &str,
240 target: &str,
241 include: &[&Path],
242 handlers: &mut H,
243 user: &U,
244) -> Result<Value, RuntimeError> {
245 compile_and_run_with_nursery_size(source, target, include, handlers, user, DEFAULT_NURSERY_SIZE)
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 #[ignore] fn test_compile_identity() {
255 let source = "module Test where\nidentity x = x";
256 let (expr, _table) = compile_haskell(source, "identity", &[])
257 .expect("Failed to compile identity");
258
259 assert_eq!(expr.nodes.len(), 2);
261 }
262
263 #[test]
264 #[ignore] fn test_compile_error() {
266 let source = "module Test where\nfoo = garbage";
267 let res = compile_haskell(source, "foo", &[]);
268 assert!(res.is_err());
269 if let Err(CompileError::ExtractFailed(msg)) = res {
270 assert!(msg.contains("Variable not in scope: garbage") || msg.contains("not in scope: garbage"));
271 } else {
272 panic!("Expected ExtractFailed error, got {:?}", res);
273 }
274 }
275}