1use std::fmt;
7use std::io;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use tempfile::TempDir;
11pub use tidepool_codegen::host_fns::{drain_diagnostics, push_diagnostic};
12use tidepool_codegen::jit_machine::JitEffectMachine;
13pub use tidepool_codegen::jit_machine::JitError;
14pub use tidepool_effect::dispatch::DispatchEffect;
15pub use tidepool_eval::value::Value;
16use tidepool_repr::serial::{read_cbor, read_metadata, MetaWarnings, ReadError};
17use tidepool_repr::{CoreExpr, DataConTable};
18
19mod cache;
20mod render;
21
22pub use render::{value_to_json, EvalResult};
23
24pub type CompileResult = (CoreExpr, DataConTable, MetaWarnings);
26
27#[derive(Debug)]
29pub enum CompileError {
30 Io(io::Error),
32 ExtractFailed(String),
34 ReadError(ReadError),
36 MissingOutput(PathBuf),
38 IOTypeDetected,
40}
41
42impl fmt::Display for CompileError {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 CompileError::Io(e) => write!(f, "I/O error: {}", e),
46 CompileError::ExtractFailed(msg) => write!(f, "Haskell compilation failed:\n{}", msg),
47 CompileError::ReadError(e) => write!(f, "CBOR deserialization error: {}", e),
48 CompileError::MissingOutput(path) => {
49 write!(f, "Missing output file from extractor: {}", path.display())
50 }
51 CompileError::IOTypeDetected => {
52 write!(f, "IO type detected in result binding. IO operations (unsafePerformIO, etc.) are not supported in the Tidepool sandbox.")
53 }
54 }
55 }
56}
57
58impl std::error::Error for CompileError {
59 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60 match self {
61 CompileError::Io(e) => Some(e),
62 CompileError::ReadError(e) => Some(e),
63 _ => None,
64 }
65 }
66}
67
68impl From<io::Error> for CompileError {
69 fn from(e: io::Error) -> Self {
70 CompileError::Io(e)
71 }
72}
73
74impl From<ReadError> for CompileError {
75 fn from(e: ReadError) -> Self {
76 CompileError::ReadError(e)
77 }
78}
79
80#[derive(Debug)]
82pub enum RuntimeError {
83 Compile(CompileError),
84 Jit(JitError),
85}
86
87impl fmt::Display for RuntimeError {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 RuntimeError::Compile(e) => write!(f, "{}", e),
91 RuntimeError::Jit(e) => write!(f, "{}", e),
92 }
93 }
94}
95
96impl std::error::Error for RuntimeError {
97 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98 match self {
99 RuntimeError::Compile(e) => Some(e),
100 RuntimeError::Jit(e) => Some(e),
101 }
102 }
103}
104
105impl From<CompileError> for RuntimeError {
106 fn from(e: CompileError) -> Self {
107 Self::Compile(e)
108 }
109}
110
111impl From<JitError> for RuntimeError {
112 fn from(e: JitError) -> Self {
113 Self::Jit(e)
114 }
115}
116
117fn extract_module_name(source: &str) -> Option<String> {
119 for line in source.lines() {
120 let trimmed = line.trim();
121 if let Some(rest) = trimmed.strip_prefix("module ") {
122 let name: String = rest
124 .trim_start()
125 .chars()
126 .take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_')
127 .collect();
128 if !name.is_empty() {
129 return Some(name);
130 }
131 }
132 }
133 None
134}
135
136pub fn compile_haskell(
155 source: &str,
156 target: &str,
157 include: &[&Path],
158) -> Result<CompileResult, CompileError> {
159 let key = cache::cache_key(source, target, include);
160 if let Some((expr_bytes, meta_bytes)) = cache::cache_load(&key) {
161 if let (Ok(expr), Ok((table, warnings))) =
164 (read_cbor(&expr_bytes), read_metadata(&meta_bytes))
165 {
166 return Ok((expr, table, warnings));
167 }
168 }
169
170 let temp_dir = TempDir::new()?;
174 let filename = extract_module_name(source)
175 .map(|m| format!("{}.hs", m))
176 .unwrap_or_else(|| "Input.hs".to_string());
177 let input_path = temp_dir.path().join(&filename);
178 std::fs::write(&input_path, source)?;
179
180 let extract_bin =
183 std::env::var("TIDEPOOL_EXTRACT").unwrap_or_else(|_| "tidepool-extract".to_string());
184 let mut cmd = Command::new(&extract_bin);
185 cmd.arg(&input_path);
186 cmd.arg("--output-dir").arg(temp_dir.path());
187 cmd.arg("--target").arg(target);
188
189 for path in include {
190 cmd.arg("--include").arg(path);
191 }
192
193 let output = cmd.output().map_err(|e| {
194 if e.kind() == io::ErrorKind::NotFound {
195 io::Error::new(
196 io::ErrorKind::NotFound,
197 "tidepool-extract not found on PATH. Ensure the Tidepool harness is installed.",
198 )
199 } else {
200 e
201 }
202 })?;
203
204 let stderr_str = String::from_utf8_lossy(&output.stderr);
206 if !stderr_str.is_empty() {
207 eprintln!("[tidepool-extract stderr]\n{}", stderr_str);
208 }
209
210 if !output.status.success() {
211 return Err(CompileError::ExtractFailed(stderr_str.into_owned()));
212 }
213
214 let expr_path = temp_dir.path().join(format!("{}.cbor", target));
216 let meta_path = temp_dir.path().join("meta.cbor");
217
218 if !expr_path.exists() {
219 return Err(CompileError::MissingOutput(expr_path));
220 }
221 if !meta_path.exists() {
222 return Err(CompileError::MissingOutput(meta_path));
223 }
224
225 let expr_bytes = std::fs::read(&expr_path)?;
226 let meta_bytes = std::fs::read(&meta_path)?;
227
228 let expr = read_cbor(&expr_bytes)?;
229 let (table, warnings) = read_metadata(&meta_bytes)?;
230
231 cache::cache_store(&key, &expr_bytes, &meta_bytes);
233
234 Ok((expr, table, warnings))
235}
236
237const DEFAULT_NURSERY_SIZE: usize = 1 << 26; pub fn compile_and_run_with_nursery_size<U, H: DispatchEffect<U>>(
254 source: &str,
255 target: &str,
256 include: &[&Path],
257 handlers: &mut H,
258 user: &U,
259 nursery_size: usize,
260) -> Result<EvalResult, RuntimeError> {
261 let (expr, mut table, warnings) = compile_haskell(source, target, include)?;
262 if warnings.has_io {
263 return Err(RuntimeError::Compile(CompileError::IOTypeDetected));
264 }
265 table.populate_siblings_from_expr(&expr);
269 let mut machine = JitEffectMachine::compile(&expr, &table, nursery_size)?;
270 let value = machine.run(&table, handlers, user)?;
271 Ok(EvalResult::new(value, table))
272}
273
274pub fn compile_and_run_pure(
279 source: &str,
280 target: &str,
281 include: &[&Path],
282) -> Result<EvalResult, RuntimeError> {
283 let (expr, mut table, warnings) = compile_haskell(source, target, include)?;
284 if warnings.has_io {
285 return Err(RuntimeError::Compile(CompileError::IOTypeDetected));
286 }
287 table.populate_siblings_from_expr(&expr);
288 let mut machine = JitEffectMachine::compile(&expr, &table, DEFAULT_NURSERY_SIZE)?;
289 let value = machine.run_pure()?;
290 Ok(EvalResult::new(value, table))
291}
292
293pub fn compile_and_run<U, H: DispatchEffect<U>>(
307 source: &str,
308 target: &str,
309 include: &[&Path],
310 handlers: &mut H,
311 user: &U,
312) -> Result<EvalResult, RuntimeError> {
313 compile_and_run_with_nursery_size(
314 source,
315 target,
316 include,
317 handlers,
318 user,
319 DEFAULT_NURSERY_SIZE,
320 )
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn ensure_extract_available() -> bool {
330 if std::env::var("TIDEPOOL_EXTRACT").is_err() {
331 let bin = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
332 .parent()
333 .unwrap()
334 .join("haskell")
335 .join("tidepool-extract");
336 if bin.exists() {
337 std::env::set_var("TIDEPOOL_EXTRACT", &bin);
338 }
339 }
340 std::process::Command::new("ghc")
342 .arg("--version")
343 .stdout(std::process::Stdio::null())
344 .stderr(std::process::Stdio::null())
345 .status()
346 .map(|s| s.success())
347 .unwrap_or(false)
348 }
349
350 #[test]
351 fn test_compile_identity() {
352 if !ensure_extract_available() {
353 eprintln!("Skipping: GHC not available (run inside `nix develop`)");
354 return;
355 }
356 let source = "module Test where\nidentity x = x";
357 let (expr, _table, _warnings) =
358 compile_haskell(source, "identity", &[]).expect("Failed to compile identity");
359
360 assert!(expr.nodes.len() >= 2);
362 }
363
364 #[test]
365 fn test_compile_error() {
366 if !ensure_extract_available() {
367 eprintln!("Skipping: GHC not available (run inside `nix develop`)");
368 return;
369 }
370 let source = "module Test where\nfoo = garbage";
371 let res = compile_haskell(source, "foo", &[]);
372 assert!(res.is_err());
373 if let Err(CompileError::ExtractFailed(msg)) = res {
374 assert!(
375 msg.contains("Variable not in scope: garbage")
376 || msg.contains("not in scope: garbage")
377 );
378 } else {
379 panic!("Expected ExtractFailed error, got {:?}", res);
380 }
381 }
382}