use std::path::{Path, PathBuf};
use std::process::Command;
pub fn kernel_json_to_js(kernel_json: &str, source_path: &Path) -> Result<String, String> {
let tmp_dir = std::env::temp_dir();
let tmp_file = tmp_dir.join("lykn_kernel.json");
std::fs::write(&tmp_file, kernel_json).map_err(|e| format!("error writing temp file: {e}"))?;
let project_root = find_project_root(source_path)
.ok_or_else(|| "cannot find lykn project root (need src/compiler.js)".to_string())?;
let script = build_deno_script(&tmp_file);
let output = Command::new("deno")
.arg("eval")
.arg("--ext=js")
.arg(&script)
.current_dir(&project_root)
.output()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
"lykn compile requires Deno — install from https://deno.land".to_string()
} else {
format!("error running Deno: {e}")
}
})?;
let _ = std::fs::remove_file(&tmp_file);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("JS kernel compiler error:\n{stderr}"));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn build_deno_script(tmp_file: &Path) -> String {
format!(
r#"
import {{ compile }} from "./src/compiler.js";
const kernelJson = Deno.readTextFileSync("{tmp_path}");
const kernel = JSON.parse(kernelJson);
function fromJson(val) {{
if (val && typeof val === "object" && !Array.isArray(val)) {{
if (val.type === "list") return {{ type: "list", values: val.values.map(fromJson) }};
if (val.type === "cons") return {{ type: "cons", car: fromJson(val.car), cdr: fromJson(val.cdr) }};
return val; // atom, string, number already in correct format
}}
// Fallback for any legacy flat format
if (Array.isArray(val)) return {{ type: "list", values: val.map(fromJson) }};
if (typeof val === "string") return {{ type: "atom", value: val }};
if (typeof val === "number") return {{ type: "number", value: val }};
if (typeof val === "boolean") return {{ type: "atom", value: String(val) }};
if (val === null) return {{ type: "atom", value: "null" }};
return val;
}}
const ast = kernel.map(fromJson);
console.log(compile(ast));
"#,
tmp_path = tmp_file.display()
)
}
pub(crate) fn find_project_root(start: &Path) -> Option<PathBuf> {
let start = if start.is_file() {
start.parent()?
} else {
start
};
let mut current = start.canonicalize().ok()?;
loop {
if current.join("deno.json").exists() || current.join("src/compiler.js").exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn build_deno_script_contains_import_and_path() {
let path = Path::new("/tmp/kernel.json");
let script = build_deno_script(path);
assert!(script.contains("import { compile }"));
assert!(script.contains("/tmp/kernel.json"));
assert!(script.contains("fromJson"));
assert!(script.contains("console.log(compile(ast))"));
}
#[test]
fn find_project_root_with_deno_json() {
let tmp = std::env::temp_dir().join("lykn_test_find_root");
let _ = fs::remove_dir_all(&tmp);
let sub = tmp.join("a").join("b");
fs::create_dir_all(&sub).unwrap();
fs::write(tmp.join("deno.json"), "{}").unwrap();
let found = find_project_root(&sub).unwrap();
assert_eq!(found, tmp.canonicalize().unwrap());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn find_project_root_with_compiler_js() {
let tmp = std::env::temp_dir().join("lykn_test_find_root_cjs");
let _ = fs::remove_dir_all(&tmp);
let sub = tmp.join("child");
fs::create_dir_all(&sub).unwrap();
let src_dir = tmp.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("compiler.js"), "").unwrap();
let found = find_project_root(&sub).unwrap();
assert_eq!(found, tmp.canonicalize().unwrap());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn find_project_root_from_file() {
let tmp = std::env::temp_dir().join("lykn_test_find_root_file");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("deno.json"), "{}").unwrap();
let file = tmp.join("example.lykn");
fs::write(&file, "(+ 1 2)").unwrap();
let found = find_project_root(&file).unwrap();
assert_eq!(found, tmp.canonicalize().unwrap());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn find_project_root_none_when_missing() {
let tmp = std::env::temp_dir().join("lykn_test_find_root_none");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let result = find_project_root(&tmp);
let _ = result;
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn find_project_root_nonexistent_path() {
let result = find_project_root(Path::new("/nonexistent/path/here"));
assert!(result.is_none());
}
fn deno_available() -> bool {
Command::new("deno")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
#[test]
fn test_kernel_json_to_js_simple() {
if !deno_available() {
eprintln!("skipping: deno not found");
return;
}
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("should have workspace root");
let source_path = workspace_root.join("deno.json");
let kernel_json = "[]";
let result = kernel_json_to_js(kernel_json, &source_path);
match &result {
Ok(js) => {
let _ = js;
}
Err(e) => {
assert!(
e.contains("JS kernel compiler error"),
"expected JS compiler error, got: {e}"
);
}
}
}
#[test]
fn test_kernel_json_to_js_no_project_root() {
if !deno_available() {
eprintln!("skipping: deno not found");
return;
}
let tmp = std::env::temp_dir().join("lykn_test_bridge_no_root");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let source_path = tmp.join("test.lykn");
fs::write(&source_path, "").unwrap();
let result = kernel_json_to_js("[]", &source_path);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("cannot find lykn project root"),
"expected project root error, got: {err}"
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_kernel_json_to_js_invalid_json() {
if !deno_available() {
eprintln!("skipping: deno not found");
return;
}
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("should have workspace root");
let source_path = workspace_root.join("deno.json");
let result = kernel_json_to_js("{{{not valid json", &source_path);
let _ = result;
}
}