use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
pub use tidepool_codegen::host_fns::{drain_diagnostics, push_diagnostic};
use tidepool_codegen::jit_machine::JitEffectMachine;
pub use tidepool_codegen::jit_machine::JitError;
pub use tidepool_effect::dispatch::DispatchEffect;
pub use tidepool_eval::value::Value;
use tidepool_repr::serial::{read_cbor, read_metadata, MetaWarnings, ReadError};
use tidepool_repr::{CoreExpr, DataConTable};
mod cache;
mod render;
pub use render::{value_to_json, EvalResult};
pub type CompileResult = (CoreExpr, DataConTable, MetaWarnings);
#[derive(Debug)]
pub enum CompileError {
Io(io::Error),
ExtractFailed(String),
ReadError(ReadError),
MissingOutput(PathBuf),
IOTypeDetected,
}
impl fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompileError::Io(e) => write!(f, "I/O error: {}", e),
CompileError::ExtractFailed(msg) => write!(f, "Haskell compilation failed:\n{}", msg),
CompileError::ReadError(e) => write!(f, "CBOR deserialization error: {}", e),
CompileError::MissingOutput(path) => {
write!(f, "Missing output file from extractor: {}", path.display())
}
CompileError::IOTypeDetected => {
write!(f, "IO type detected in result binding. IO operations (unsafePerformIO, etc.) are not supported in the Tidepool sandbox.")
}
}
}
}
impl std::error::Error for CompileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CompileError::Io(e) => Some(e),
CompileError::ReadError(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for CompileError {
fn from(e: io::Error) -> Self {
CompileError::Io(e)
}
}
impl From<ReadError> for CompileError {
fn from(e: ReadError) -> Self {
CompileError::ReadError(e)
}
}
#[derive(Debug)]
pub enum RuntimeError {
Compile(CompileError),
Jit(JitError),
}
impl fmt::Display for RuntimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RuntimeError::Compile(e) => write!(f, "{}", e),
RuntimeError::Jit(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for RuntimeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
RuntimeError::Compile(e) => Some(e),
RuntimeError::Jit(e) => Some(e),
}
}
}
impl From<CompileError> for RuntimeError {
fn from(e: CompileError) -> Self {
Self::Compile(e)
}
}
impl From<JitError> for RuntimeError {
fn from(e: JitError) -> Self {
Self::Jit(e)
}
}
fn extract_module_name(source: &str) -> Option<String> {
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("module ") {
let name: String = rest
.trim_start()
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_')
.collect();
if !name.is_empty() {
return Some(name);
}
}
}
None
}
pub fn compile_haskell(
source: &str,
target: &str,
include: &[&Path],
) -> Result<CompileResult, CompileError> {
let key = cache::cache_key(source, target, include);
if let Some((expr_bytes, meta_bytes)) = cache::cache_load(&key) {
if let (Ok(expr), Ok((table, warnings))) =
(read_cbor(&expr_bytes), read_metadata(&meta_bytes))
{
return Ok((expr, table, warnings));
}
}
let temp_dir = TempDir::new()?;
let filename = extract_module_name(source)
.map(|m| format!("{}.hs", m))
.unwrap_or_else(|| "Input.hs".to_string());
let input_path = temp_dir.path().join(&filename);
std::fs::write(&input_path, source)?;
let extract_bin =
std::env::var("TIDEPOOL_EXTRACT").unwrap_or_else(|_| "tidepool-extract".to_string());
let mut cmd = Command::new(&extract_bin);
cmd.arg(&input_path);
cmd.arg("--output-dir").arg(temp_dir.path());
cmd.arg("--target").arg(target);
for path in include {
cmd.arg("--include").arg(path);
}
let output = cmd.output().map_err(|e| {
if e.kind() == io::ErrorKind::NotFound {
io::Error::new(
io::ErrorKind::NotFound,
"tidepool-extract not found on PATH. Ensure the Tidepool harness is installed.",
)
} else {
e
}
})?;
let stderr_str = String::from_utf8_lossy(&output.stderr);
if !stderr_str.is_empty() {
eprintln!("[tidepool-extract stderr]\n{}", stderr_str);
}
if !output.status.success() {
return Err(CompileError::ExtractFailed(stderr_str.into_owned()));
}
let expr_path = temp_dir.path().join(format!("{}.cbor", target));
let meta_path = temp_dir.path().join("meta.cbor");
if !expr_path.exists() {
return Err(CompileError::MissingOutput(expr_path));
}
if !meta_path.exists() {
return Err(CompileError::MissingOutput(meta_path));
}
let expr_bytes = std::fs::read(&expr_path)?;
let meta_bytes = std::fs::read(&meta_path)?;
let expr = read_cbor(&expr_bytes)?;
let (table, warnings) = read_metadata(&meta_bytes)?;
cache::cache_store(&key, &expr_bytes, &meta_bytes);
Ok((expr, table, warnings))
}
const DEFAULT_NURSERY_SIZE: usize = 1 << 26;
pub fn compile_and_run_with_nursery_size<U, H: DispatchEffect<U>>(
source: &str,
target: &str,
include: &[&Path],
handlers: &mut H,
user: &U,
nursery_size: usize,
) -> Result<EvalResult, RuntimeError> {
let (expr, mut table, warnings) = compile_haskell(source, target, include)?;
if warnings.has_io {
return Err(RuntimeError::Compile(CompileError::IOTypeDetected));
}
table.populate_siblings_from_expr(&expr);
let mut machine = JitEffectMachine::compile(&expr, &table, nursery_size)?;
let value = machine.run(&table, handlers, user)?;
Ok(EvalResult::new(value, table))
}
pub fn compile_and_run_pure(
source: &str,
target: &str,
include: &[&Path],
) -> Result<EvalResult, RuntimeError> {
let (expr, mut table, warnings) = compile_haskell(source, target, include)?;
if warnings.has_io {
return Err(RuntimeError::Compile(CompileError::IOTypeDetected));
}
table.populate_siblings_from_expr(&expr);
let mut machine = JitEffectMachine::compile(&expr, &table, DEFAULT_NURSERY_SIZE)?;
let value = machine.run_pure()?;
Ok(EvalResult::new(value, table))
}
pub fn compile_and_run<U, H: DispatchEffect<U>>(
source: &str,
target: &str,
include: &[&Path],
handlers: &mut H,
user: &U,
) -> Result<EvalResult, RuntimeError> {
compile_and_run_with_nursery_size(
source,
target,
include,
handlers,
user,
DEFAULT_NURSERY_SIZE,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn ensure_extract_available() -> bool {
if std::env::var("TIDEPOOL_EXTRACT").is_err() {
let bin = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("haskell")
.join("tidepool-extract");
if bin.exists() {
std::env::set_var("TIDEPOOL_EXTRACT", &bin);
}
}
std::process::Command::new("ghc")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[test]
fn test_compile_identity() {
if !ensure_extract_available() {
eprintln!("Skipping: GHC not available (run inside `nix develop`)");
return;
}
let source = "module Test where\nidentity x = x";
let (expr, _table, _warnings) =
compile_haskell(source, "identity", &[]).expect("Failed to compile identity");
assert!(expr.nodes.len() >= 2);
}
#[test]
fn test_compile_error() {
if !ensure_extract_available() {
eprintln!("Skipping: GHC not available (run inside `nix develop`)");
return;
}
let source = "module Test where\nfoo = garbage";
let res = compile_haskell(source, "foo", &[]);
assert!(res.is_err());
if let Err(CompileError::ExtractFailed(msg)) = res {
assert!(
msg.contains("Variable not in scope: garbage")
|| msg.contains("not in scope: garbage")
);
} else {
panic!("Expected ExtractFailed error, got {:?}", res);
}
}
}