Skip to main content

warp_runtime/
foreign.rs

1//! The bridge of foreign_call(runtime, module, member, arguments) (warp's lowering/foreign/foreign_modules.rs,
2//! notes/stdlib_connectors.md), shared by warp and its executables (card exe-foreign): one long-lived child per runtime
3//! and process, started on first use, speaking one JSON line per request and per answer. Python: `python3 -u` running
4//! FOREIGN_PYTHON_LOOP (`WARP_PYTHON` names another interpreter); JavaScript: `node` running FOREIGN_JS_LOOP
5//! (`WARP_NODE`), a global (`Math`) or a module it requires.
6//! A read gives the member itself, a call gives its result (a promise awaited); an exception is an error with the
7//! runtime's message. A value without a JSON form stays in its runtime behind an id, a handle: it crosses as the record
8//! `{$handle: 7, type: "date", text: "datetime.date(2020, 1, 2)"}` and is that object again when it comes back, as an
9//! argument or as the receiver of `d.isoformat()` (the module position). Handles live until the process ends.
10//! A warp function given as an argument crosses as `{$warp_function: 0}`, its index among the call's functions; node
11//! calls it back with the line `{callback: 0, arguments: […]}` and waits for its `{value}` or `{error}` (card js-callbacks).
12
13use serde_json::Value;
14use std::cell::RefCell;
15use std::io::{BufRead, BufReader, Write};
16use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
17use std::sync::{Condvar, Mutex};
18
19/// A runtime: its name in `use <runtime> …`, the variable naming its interpreter, the default interpreter, and how it
20/// runs its loop script
21struct Interpreter {
22	runtime: &'static str,
23	variable: &'static str,
24	default: &'static str,
25	arguments: [&'static str; 2],
26	script: &'static str,
27}
28
29const INTERPRETERS: [Interpreter; 2] = [
30	Interpreter {
31		runtime: "python",
32		variable: "WARP_PYTHON",
33		default: "python3",
34		arguments: ["-u", "-c"],
35		script: FOREIGN_PYTHON_LOOP,
36	},
37	Interpreter {
38		runtime: "js",
39		variable: "WARP_NODE",
40		default: "node",
41		arguments: ["--no-warnings", "-e"],
42		script: FOREIGN_JS_LOOP,
43	},
44];
45/// An integer beyond 64 bits, sent as its digits: `{"$int": "15511210043330985984000000"}`
46pub const BIG_INTEGER_KEY: &str = "$int";
47/// A codepoint (a WIT `char`, P94), which JSON would make a text: `{"$char": "b"}`
48pub const CODEPOINT_KEY: &str = "$char";
49/// A warp function among a call's arguments: `{"$warp_function": 0}`
50pub const WARP_FUNCTION_KEY: &str = "$warp_function";
51/// How a call's warp functions are called back: by their index, with the arguments the runtime gave them as a list
52pub type Callback<'a> = &'a mut dyn FnMut(usize, Value) -> Result<Value, String>;
53thread_local! {
54	/// The children this thread's calls hold while they call a warp function back: a foreign call that function makes
55	/// goes to the same child, which answers it while it waits
56	static HELD: RefCell<Vec<(&'static str, Runtime)>> = const { RefCell::new(Vec::new()) };
57}
58/// The request loop the child runs: {"module", "member", "arguments"} → {"value"} or {"error"}; the playground's
59/// Pyodide runs the same file
60const FOREIGN_PYTHON_LOOP: &str = include_str!("foreign_python.py"); // a link to web/playground/foreign_python.py: the packaged crate carries the file
61
62/// The same loop for node: a global (`Math`, `JSON`) or a module it requires or imports; a method keeps its object
63const FOREIGN_JS_LOOP: &str = r#"
64const handles = [];
65const handle = value => (handles.push(value), { "$handle": handles.length, type: value?.constructor?.name ?? typeof value, text: String(value).slice(0, 200) });
66const unplain = value => Array.isArray(value) ? value.map(unplain)
67	: value !== null && typeof value === "object" ? ("$handle" in value ? handles[value.$handle - 1] : "$warp_function" in value ? warpFunction(value.$warp_function)
68		: Object.fromEntries(Object.entries(value).map(([key, item]) => [key, unplain(item)])))
69	: value;
70const plain = value => {
71	if (typeof value === "bigint") return value >= -(2n ** 63n) && value < 2n ** 63n ? Number(value) : { "$int": value.toString() };
72	if (value === undefined || value === null || typeof value === "symbol") return null; // ø, not a handle of null
73	if (Array.isArray(value)) return value.map(plain);
74	const prototype = value !== null && typeof value === "object" ? Object.getPrototypeOf(value) : undefined;
75	if (prototype === Object.prototype || prototype === null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]));
76	if (typeof value === "function" || typeof value === "object") return handle(value); // a Date, a Map, an instance, a function
77	return value;
78};
79// what warp's operators on a value of this runtime forward to (lowering/foreign/foreign_modules.rs)
80const 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,
81	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,
82	getitem: (a, i) => typeof a.get === "function" ? a.get(i) : a[i], len: a => a.length ?? a.size, list: a => Array.from(a) };
83// `use js self` / `use js window`: node mirrors a page, its global object is globalThis
84const load = name => name === "operator" ? operator : name === "self" || name === "window" ? globalThis : name in globalThis ? globalThis[name] : (() => { try { return require(name); } catch (failure) { return import(name); } })();
85// the answers own stdout: what a module prints (console.log) goes to stderr
86const reply = answer => require("fs").writeSync(1, JSON.stringify(answer) + "\n");
87process.stdout.write = process.stderr.write.bind(process.stderr);
88// stdin read synchronously, so a warp function called back answers before the JavaScript calling it goes on
89const decoder = new (require("string_decoder").StringDecoder)("utf8");
90let buffered = "";
91const readLine = () => {
92	const chunk = Buffer.alloc(65536);
93	while (!buffered.includes("\n")) {
94		let count;
95		try { count = require("fs").readSync(0, chunk, 0, chunk.length, null); } catch (failure) { if (failure.code === "EAGAIN") continue; throw failure; }
96		if (count === 0) return null;
97		buffered += decoder.write(chunk.subarray(0, count));
98	}
99	const line = buffered.slice(0, buffered.indexOf("\n"));
100	buffered = buffered.slice(line.length + 1);
101	return line;
102};
103// a request's answer; each `yield` waits for what may be a promise (lowering/foreign/foreign_modules.rs CONSTRUCTOR_MEMBER: member
104// "" constructs the module, `new URL(…)`; SETTER_PREFIX: "set innerHTML" sets the member to the one argument)
105const SETTER = /^set (.+)$/;
106function* answerOf(request) {
107	try {
108		let owner = null, value = typeof request.module === "object" ? unplain(request.module) : yield load(request.module);
109		const set = request.member.match(SETTER);
110		if (request.member === "") value = new value(...unplain(request.arguments));
111		else if (set) value = value[set[1]] = unplain(request.arguments)[0];
112		else {
113			for (const part of request.member.split(".")) {
114				if (value?.[part] === undefined) throw new ReferenceError(`${request.module} has no ${request.member}`);
115				[owner, value] = [value, value[part]];
116			}
117			if (request.arguments !== null) value = yield value.apply(owner, unplain(request.arguments));
118		}
119		return { value: plain(value) };
120	} catch (failure) {
121		return { error: failure instanceof Error ? failure.name + ": " + failure.message : String(failure) };
122	}
123}
124// a request of the program: its promises awaited
125const awaited = async steps => {
126	let step = steps.next();
127	while (!step.done) step = await Promise.resolve(step.value).then(value => steps.next(value), failure => steps.throw(failure));
128	return step.value;
129};
130// a request of a warp function called back, which the JavaScript calling it waits for: no promise can be awaited there
131const immediate = steps => {
132	let step = steps.next();
133	while (!step.done) {
134		const promised = typeof step.value?.then === "function";
135		if (promised) step.value.then(undefined, () => {}); // its failure is not the program's: no unhandled rejection
136		step = promised ? steps.throw(new Error("gives a promise, which a warp function called back cannot wait for")) : steps.next(step.value);
137	}
138	return step.value;
139};
140// a warp function given as an argument: called back through warp, answering the foreign calls it makes meanwhile
141const warpFunction = index => (...values) => {
142	reply({ callback: index, arguments: values.map(plain) });
143	for (let line; (line = readLine()) !== null;) {
144		const message = JSON.parse(line);
145		if ("module" in message) reply(immediate(answerOf(message)));
146		else if ("error" in message) throw new Error(message.error);
147		else return unplain(message.value);
148	}
149	throw new Error("warp ended");
150};
151(async () => {
152	for (let line; (line = readLine()) !== null;) reply(await awaited(answerOf(JSON.parse(line))));
153})();
154"#;
155
156struct Runtime {
157	_child: Child,
158	input: ChildStdin,
159	output: BufReader<ChildStdout>,
160}
161
162/// The running children, by runtime; None while a call holds its child, which another thread waits for (RETURNED)
163static RUNNING: Mutex<Vec<(&'static str, Option<Runtime>)>> = Mutex::new(Vec::new());
164static RETURNED: Condvar = Condvar::new();
165
166/// Is `runtime` one an interpreter runs (python, js)
167pub fn is_interpreted(runtime: &str) -> bool {
168	interpreter(runtime).is_some()
169}
170
171fn interpreter(runtime: &str) -> Option<&'static Interpreter> {
172	INTERPRETERS.iter().find(|interpreter| interpreter.runtime == runtime)
173}
174
175/// `runtime.module.member(arguments…)` (arguments null: the member itself, read): the value the runtime answered, or
176/// its error as `runtime module.member: error`; `module` is a module's name or a handle, `module_name` how errors name it
177pub fn call(
178	runtime: &str,
179	module_name: &str,
180	module: Value,
181	member: &str,
182	arguments: Value,
183	callback: Callback,
184) -> Result<Value, String> {
185	let interpreter = interpreter(runtime).ok_or_else(|| format!("no foreign runtime {runtime}"))?;
186	let request = serde_json::json!({"module": module, "member": member, "arguments": arguments});
187	let mut answer =
188		exchange(interpreter, &request.to_string(), callback).map_err(|failure| format!("{runtime}: {failure}"))?;
189	match (answer.get_mut("value").map(Value::take), answer.get("error")) {
190		(_, Some(Value::String(error))) => Err(format!("{runtime} {module_name}.{member}: {error}")),
191		(Some(value), _) => Ok(value),
192		_ => Err(format!("{runtime} answered neither a value nor an error: {answer}")),
193	}
194}
195
196/// One request line out, one answer line back, each callback line before it answered; the child is started on first
197/// use and again after it ended
198fn exchange(interpreter: &'static Interpreter, request: &str, callback: Callback) -> Result<Value, String> {
199	let held = take_held(interpreter.runtime);
200	let nested = held.is_some();
201	let child = match held {
202		Some(child) => child,
203		None => borrow(interpreter)?,
204	};
205	let (answer, child) = talk(interpreter.runtime, child, request, callback);
206	match (nested, child) {
207		(true, Some(child)) => HELD.with_borrow_mut(|held| held.push((interpreter.runtime, child))),
208		(true, None) => {}
209		(false, child) => give_back(interpreter.runtime, child),
210	}
211	answer
212}
213
214/// The request to the child and its answer; a callback line calls the warp function back meanwhile, the child held by
215/// this thread for the foreign calls it makes. The child is gone when it ended
216fn talk(
217	runtime: &'static str,
218	mut child: Runtime,
219	request: &str,
220	callback: Callback,
221) -> (Result<Value, String>, Option<Runtime>) {
222	let mut line = request.to_string();
223	loop {
224		let mut answer = String::new();
225		let sent = writeln!(child.input, "{line}").and_then(|_| child.input.flush());
226		if sent.is_err() || child.output.read_line(&mut answer).unwrap_or(0) == 0 {
227			return (Err("the interpreter ended".into()), None);
228		}
229		let mut answer: Value = match serde_json::from_str(&answer) {
230			Ok(answer) => answer,
231			Err(failure) => return (Err(format!("answered no JSON: {failure}")), Some(child)),
232		};
233		let Some(function) = answer.get("callback").and_then(Value::as_u64) else {
234			return (Ok(answer), Some(child));
235		};
236		HELD.with_borrow_mut(|held| held.push((runtime, child)));
237		let arguments = answer.get_mut("arguments").map(Value::take).unwrap_or(Value::Null);
238		let result = callback(function as usize, arguments);
239		let Some(held) = take_held(runtime) else {
240			return (Err("the interpreter ended".into()), None);
241		};
242		child = held;
243		line = match result {
244			Ok(value) => serde_json::json!({ "value": value }),
245			Err(failure) => serde_json::json!({ "error": failure }),
246		}
247		.to_string();
248	}
249}
250
251/// The child of `runtime` this thread holds, while a warp function is called back
252fn take_held(runtime: &str) -> Option<Runtime> {
253	HELD.with_borrow_mut(|held| held.iter().rposition(|(name, _)| *name == runtime).map(|index| held.remove(index).1))
254}
255
256/// The child of the interpreter for one call: started when there is none, waited for while another thread's call holds it
257fn borrow(interpreter: &'static Interpreter) -> Result<Runtime, String> {
258	let mut running = RUNNING.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
259	loop {
260		let Some((_, slot)) = running.iter_mut().find(|(runtime, _)| *runtime == interpreter.runtime) else {
261			running.push((interpreter.runtime, None));
262			drop(running);
263			return start(interpreter).inspect_err(|_| give_back(interpreter.runtime, None));
264		};
265		if let Some(child) = slot.take() {
266			return Ok(child);
267		}
268		running = RETURNED.wait(running).unwrap_or_else(|poisoned| poisoned.into_inner());
269	}
270}
271
272/// The child back for the next call, or none: it ended, the next call starts another
273fn give_back(runtime: &'static str, child: Option<Runtime>) {
274	let mut running = RUNNING.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
275	match child {
276		Some(child) => {
277			if let Some((_, slot)) = running.iter_mut().find(|(name, _)| *name == runtime) {
278				*slot = Some(child);
279			}
280		}
281		None => running.retain(|(name, _)| *name != runtime),
282	}
283	RETURNED.notify_all();
284}
285
286fn start(interpreter: &Interpreter) -> Result<Runtime, String> {
287	let program = std::env::var(interpreter.variable).unwrap_or_else(|_| interpreter.default.to_string());
288	let mut child = Command::new(&program)
289		.args(interpreter.arguments)
290		.arg(interpreter.script)
291		.stdin(Stdio::piped())
292		.stdout(Stdio::piped())
293		.stderr(Stdio::inherit())
294		.spawn()
295		.map_err(|failure| {
296			format!("cannot start {program} ({failure}); set {} to an interpreter", interpreter.variable)
297		})?;
298	let input = child.stdin.take().ok_or("no stdin")?;
299	let output = BufReader::new(child.stdout.take().ok_or("no stdout")?);
300	Ok(Runtime { _child: child, input, output })
301}