use serde_json::Value;
use std::cell::RefCell;
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::{Condvar, Mutex};
struct Interpreter {
runtime: &'static str,
variable: &'static str,
default: &'static str,
arguments: [&'static str; 2],
script: &'static str,
}
const INTERPRETERS: [Interpreter; 2] = [
Interpreter {
runtime: "python",
variable: "WARP_PYTHON",
default: "python3",
arguments: ["-u", "-c"],
script: FOREIGN_PYTHON_LOOP,
},
Interpreter {
runtime: "js",
variable: "WARP_NODE",
default: "node",
arguments: ["--no-warnings", "-e"],
script: FOREIGN_JS_LOOP,
},
];
pub const BIG_INTEGER_KEY: &str = "$int";
pub const CODEPOINT_KEY: &str = "$char";
pub const WARP_FUNCTION_KEY: &str = "$warp_function";
pub type Callback<'a> = &'a mut dyn FnMut(usize, Value) -> Result<Value, String>;
thread_local! {
static HELD: RefCell<Vec<(&'static str, Runtime)>> = const { RefCell::new(Vec::new()) };
}
const FOREIGN_PYTHON_LOOP: &str = include_str!("foreign_python.py");
const FOREIGN_JS_LOOP: &str = r#"
const handles = [];
const handle = value => (handles.push(value), { "$handle": handles.length, type: value?.constructor?.name ?? typeof value, text: String(value).slice(0, 200) });
const unplain = value => Array.isArray(value) ? value.map(unplain)
: value !== null && typeof value === "object" ? ("$handle" in value ? handles[value.$handle - 1] : "$warp_function" in value ? warpFunction(value.$warp_function)
: Object.fromEntries(Object.entries(value).map(([key, item]) => [key, unplain(item)])))
: value;
const plain = value => {
if (typeof value === "bigint") return value >= -(2n ** 63n) && value < 2n ** 63n ? Number(value) : { "$int": value.toString() };
if (value === undefined || value === null || typeof value === "symbol") return null; // ø, not a handle of null
if (Array.isArray(value)) return value.map(plain);
const prototype = value !== null && typeof value === "object" ? Object.getPrototypeOf(value) : undefined;
if (prototype === Object.prototype || prototype === null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]));
if (typeof value === "function" || typeof value === "object") return handle(value); // a Date, a Map, an instance, a function
return value;
};
// what warp's operators on a value of this runtime forward to (lowering/foreign/foreign_modules.rs)
const operator = { add: (a, b) => a + b, sub: (a, b) => a - b, mul: (a, b) => a * b, truediv: (a, b) => a / b, mod: (a, b) => a % b, pow: (a, b) => a ** b,
lt: (a, b) => a < b, gt: (a, b) => a > b, le: (a, b) => a <= b, ge: (a, b) => a >= b, eq: (a, b) => a === b, ne: (a, b) => a !== b, neg: a => -a,
getitem: (a, i) => typeof a.get === "function" ? a.get(i) : a[i], len: a => a.length ?? a.size, list: a => Array.from(a) };
// `use js self` / `use js window`: node mirrors a page, its global object is globalThis
const load = name => name === "operator" ? operator : name === "self" || name === "window" ? globalThis : name in globalThis ? globalThis[name] : (() => { try { return require(name); } catch (failure) { return import(name); } })();
// the answers own stdout: what a module prints (console.log) goes to stderr
const reply = answer => require("fs").writeSync(1, JSON.stringify(answer) + "\n");
process.stdout.write = process.stderr.write.bind(process.stderr);
// stdin read synchronously, so a warp function called back answers before the JavaScript calling it goes on
const decoder = new (require("string_decoder").StringDecoder)("utf8");
let buffered = "";
const readLine = () => {
const chunk = Buffer.alloc(65536);
while (!buffered.includes("\n")) {
let count;
try { count = require("fs").readSync(0, chunk, 0, chunk.length, null); } catch (failure) { if (failure.code === "EAGAIN") continue; throw failure; }
if (count === 0) return null;
buffered += decoder.write(chunk.subarray(0, count));
}
const line = buffered.slice(0, buffered.indexOf("\n"));
buffered = buffered.slice(line.length + 1);
return line;
};
// a request's answer; each `yield` waits for what may be a promise (lowering/foreign/foreign_modules.rs CONSTRUCTOR_MEMBER: member
// "" constructs the module, `new URL(…)`; SETTER_PREFIX: "set innerHTML" sets the member to the one argument)
const SETTER = /^set (.+)$/;
function* answerOf(request) {
try {
let owner = null, value = typeof request.module === "object" ? unplain(request.module) : yield load(request.module);
const set = request.member.match(SETTER);
if (request.member === "") value = new value(...unplain(request.arguments));
else if (set) value = value[set[1]] = unplain(request.arguments)[0];
else {
for (const part of request.member.split(".")) {
if (value?.[part] === undefined) throw new ReferenceError(`${request.module} has no ${request.member}`);
[owner, value] = [value, value[part]];
}
if (request.arguments !== null) value = yield value.apply(owner, unplain(request.arguments));
}
return { value: plain(value) };
} catch (failure) {
return { error: failure instanceof Error ? failure.name + ": " + failure.message : String(failure) };
}
}
// a request of the program: its promises awaited
const awaited = async steps => {
let step = steps.next();
while (!step.done) step = await Promise.resolve(step.value).then(value => steps.next(value), failure => steps.throw(failure));
return step.value;
};
// a request of a warp function called back, which the JavaScript calling it waits for: no promise can be awaited there
const immediate = steps => {
let step = steps.next();
while (!step.done) {
const promised = typeof step.value?.then === "function";
if (promised) step.value.then(undefined, () => {}); // its failure is not the program's: no unhandled rejection
step = promised ? steps.throw(new Error("gives a promise, which a warp function called back cannot wait for")) : steps.next(step.value);
}
return step.value;
};
// a warp function given as an argument: called back through warp, answering the foreign calls it makes meanwhile
const warpFunction = index => (...values) => {
reply({ callback: index, arguments: values.map(plain) });
for (let line; (line = readLine()) !== null;) {
const message = JSON.parse(line);
if ("module" in message) reply(immediate(answerOf(message)));
else if ("error" in message) throw new Error(message.error);
else return unplain(message.value);
}
throw new Error("warp ended");
};
(async () => {
for (let line; (line = readLine()) !== null;) reply(await awaited(answerOf(JSON.parse(line))));
})();
"#;
struct Runtime {
_child: Child,
input: ChildStdin,
output: BufReader<ChildStdout>,
}
static RUNNING: Mutex<Vec<(&'static str, Option<Runtime>)>> = Mutex::new(Vec::new());
static RETURNED: Condvar = Condvar::new();
pub fn is_interpreted(runtime: &str) -> bool {
interpreter(runtime).is_some()
}
fn interpreter(runtime: &str) -> Option<&'static Interpreter> {
INTERPRETERS.iter().find(|interpreter| interpreter.runtime == runtime)
}
pub fn call(
runtime: &str,
module_name: &str,
module: Value,
member: &str,
arguments: Value,
callback: Callback,
) -> Result<Value, String> {
let interpreter = interpreter(runtime).ok_or_else(|| format!("no foreign runtime {runtime}"))?;
let request = serde_json::json!({"module": module, "member": member, "arguments": arguments});
let mut answer =
exchange(interpreter, &request.to_string(), callback).map_err(|failure| format!("{runtime}: {failure}"))?;
match (answer.get_mut("value").map(Value::take), answer.get("error")) {
(_, Some(Value::String(error))) => Err(format!("{runtime} {module_name}.{member}: {error}")),
(Some(value), _) => Ok(value),
_ => Err(format!("{runtime} answered neither a value nor an error: {answer}")),
}
}
fn exchange(interpreter: &'static Interpreter, request: &str, callback: Callback) -> Result<Value, String> {
let held = take_held(interpreter.runtime);
let nested = held.is_some();
let child = match held {
Some(child) => child,
None => borrow(interpreter)?,
};
let (answer, child) = talk(interpreter.runtime, child, request, callback);
match (nested, child) {
(true, Some(child)) => HELD.with_borrow_mut(|held| held.push((interpreter.runtime, child))),
(true, None) => {}
(false, child) => give_back(interpreter.runtime, child),
}
answer
}
fn talk(
runtime: &'static str,
mut child: Runtime,
request: &str,
callback: Callback,
) -> (Result<Value, String>, Option<Runtime>) {
let mut line = request.to_string();
loop {
let mut answer = String::new();
let sent = writeln!(child.input, "{line}").and_then(|_| child.input.flush());
if sent.is_err() || child.output.read_line(&mut answer).unwrap_or(0) == 0 {
return (Err("the interpreter ended".into()), None);
}
let mut answer: Value = match serde_json::from_str(&answer) {
Ok(answer) => answer,
Err(failure) => return (Err(format!("answered no JSON: {failure}")), Some(child)),
};
let Some(function) = answer.get("callback").and_then(Value::as_u64) else {
return (Ok(answer), Some(child));
};
HELD.with_borrow_mut(|held| held.push((runtime, child)));
let arguments = answer.get_mut("arguments").map(Value::take).unwrap_or(Value::Null);
let result = callback(function as usize, arguments);
let Some(held) = take_held(runtime) else {
return (Err("the interpreter ended".into()), None);
};
child = held;
line = match result {
Ok(value) => serde_json::json!({ "value": value }),
Err(failure) => serde_json::json!({ "error": failure }),
}
.to_string();
}
}
fn take_held(runtime: &str) -> Option<Runtime> {
HELD.with_borrow_mut(|held| held.iter().rposition(|(name, _)| *name == runtime).map(|index| held.remove(index).1))
}
fn borrow(interpreter: &'static Interpreter) -> Result<Runtime, String> {
let mut running = RUNNING.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
loop {
let Some((_, slot)) = running.iter_mut().find(|(runtime, _)| *runtime == interpreter.runtime) else {
running.push((interpreter.runtime, None));
drop(running);
return start(interpreter).inspect_err(|_| give_back(interpreter.runtime, None));
};
if let Some(child) = slot.take() {
return Ok(child);
}
running = RETURNED.wait(running).unwrap_or_else(|poisoned| poisoned.into_inner());
}
}
fn give_back(runtime: &'static str, child: Option<Runtime>) {
let mut running = RUNNING.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
match child {
Some(child) => {
if let Some((_, slot)) = running.iter_mut().find(|(name, _)| *name == runtime) {
*slot = Some(child);
}
}
None => running.retain(|(name, _)| *name != runtime),
}
RETURNED.notify_all();
}
fn start(interpreter: &Interpreter) -> Result<Runtime, String> {
let program = std::env::var(interpreter.variable).unwrap_or_else(|_| interpreter.default.to_string());
let mut child = Command::new(&program)
.args(interpreter.arguments)
.arg(interpreter.script)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|failure| {
format!("cannot start {program} ({failure}); set {} to an interpreter", interpreter.variable)
})?;
let input = child.stdin.take().ok_or("no stdin")?;
let output = BufReader::new(child.stdout.take().ok_or("no stdout")?);
Ok(Runtime { _child: child, input, output })
}