Skip to main content

warp_runtime/
gc_values.rs

1//! Warp's GC Nodes (CLAUDE.md "Serialization", warp's type_kinds.rs) as JSON and back, for an executable's foreign calls
2//! (card exe-foreign): what warp does with json_of(node_in(…)) and Builders::build(TaskValue::of(node_of(…))), without
3//! warp's Node. warp's tests/wasm/test_executable_foreign.rs holds these codes equal to warp's.
4use crate::foreign::{BIG_INTEGER_KEY, CODEPOINT_KEY};
5use serde_json::{Map, Value};
6use wasmtime::{AnyRef, AsContextMut, Func, Global, Memory, Result, Rooted, StoreContextMut, StructRef, Val};
7
8pub const KIND_MASK: i64 = 0xFF;
9pub const KIND_BITS: i64 = 8;
10pub const EMPTY: u8 = 0;
11pub const INT: u8 = 1;
12pub const FLOAT: u8 = 2;
13pub const TEXT: u8 = 3;
14pub const CODEPOINT: u8 = 4;
15pub const SYMBOL: u8 = 5;
16pub const KEY: u8 = 6;
17pub const BLOCK: u8 = 7;
18pub const LIST: u8 = 8;
19pub const ERROR: u8 = 11;
20pub const FUNCTION: u8 = 16;
21/// The high bits of a bool's Int kind
22pub const BOOL_INFO: i64 = 1;
23/// Op::Colon's code, `key: value`
24pub const COLON: i64 = 1;
25pub const CURLY: i64 = 0;
26pub const SQUARE: i64 = 1;
27const FIELD_KIND: usize = 0;
28const FIELD_DATA: usize = 1;
29const FIELD_VALUE: usize = 2;
30/// The exported constructors a value is built with in the program
31const NEW_EMPTY: &str = "new_empty";
32const NEW_INT: &str = "new_int";
33const NEW_FLOAT: &str = "new_float";
34const NEW_CODEPOINT: &str = "new_codepoint";
35const NEW_TEXT: &str = "new_text";
36const NEW_SYMBOL: &str = "new_symbol";
37const NEW_KEY: &str = "new_key";
38const NEW_LIST: &str = "new_list";
39/// The exports an integer beyond the fixnums is composed with: shift left, add, subtract (warp's EXACT_BUILDERS)
40const SHIFT_LEFT: &str = "int_shift_left";
41const ADD: &str = "exact_add";
42const SUBTRACT: &str = "exact_sub";
43pub const BUILDERS: [&str; 11] =
44	[NEW_EMPTY, NEW_INT, NEW_FLOAT, NEW_CODEPOINT, NEW_TEXT, NEW_SYMBOL, NEW_KEY, NEW_LIST, SHIFT_LEFT, ADD, SUBTRACT];
45/// The global holding the end of the program's texts (warp's TEXT_HEAP_EXPORT)
46pub const TEXT_HEAP: &str = "text_heap";
47pub const MEMORY: &str = "memory";
48const PAGE_BITS: u32 = 16;
49const LIMB_BITS: i64 = 32;
50/// Integers in [FIXNUM_MIN, FIXNUM_MAX] stand for themselves in an Int, others are handles (warp's big_int.rs)
51const FIXNUM_MIN: i64 = -((1 << 62) - 1);
52const FIXNUM_MAX: i64 = 1 << 62;
53
54/// A node's parts: its kind, data and value fields
55fn fields<T>(node: &Val, store: &mut StoreContextMut<'_, T>) -> Option<(i64, Val, Val)> {
56	let node = as_struct(node, store)?;
57	let mut field = |index| node.field(&mut *store, index).ok();
58	Some((field(FIELD_KIND)?.unwrap_i64(), field(FIELD_DATA)?, field(FIELD_VALUE)?))
59}
60
61fn as_struct<T>(value: &Val, store: &StoreContextMut<'_, T>) -> Option<Rooted<StructRef>> {
62	value.unwrap_anyref()?.unwrap_struct(store).ok()
63}
64
65/// A program's node as JSON: lists as arrays, a curly list of keys as an object, big integers and ratios as floats
66pub fn json_of<T>(node: &Val, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<Value> {
67	let Some((kind, data, value)) = fields(node, store) else {
68		return Ok(Value::Null);
69	};
70	let high_bits = (kind >> KIND_BITS) & 0xFF;
71	Ok(match (kind & KIND_MASK) as u8 {
72		EMPTY => Value::Null,
73		INT if high_bits == BOOL_INFO => Value::Bool(integer_of(&data, store)? != 0.0),
74		INT => match plain_integer(&data, store) {
75			Some(integer) => Value::from(integer),
76			None => Value::from(integer_of(&data, store)?),
77		},
78		FLOAT => Value::from(
79			as_struct(&data, store).map_or(Ok(Val::F64(0)), |boxed| boxed.field(&mut *store, 0))?.unwrap_f64(),
80		),
81		CODEPOINT => {
82			let code = data.unwrap_anyref().and_then(|reference| reference.unwrap_i31(&*store).ok());
83			Value::String(code.and_then(|code| char::from_u32(code.get_u32())).unwrap_or('\0').to_string())
84		}
85		TEXT | SYMBOL | ERROR => Value::String(text_of(&data, store, memory)?),
86		KEY => Value::Object(Map::from_iter([entry_of(&data, &value, store, memory)?])),
87		BLOCK => object_of(items_of(data, value, store)?, store, memory)?,
88		LIST if high_bits == CURLY => object_of(items_of(data, value, store)?, store, memory)?,
89		LIST => Value::Array(
90			items_of(data, value, store)?.iter().map(|item| json_of(item, store, memory)).collect::<Result<_>>()?,
91		),
92		FUNCTION => {
93			wasmtime::bail!("an executable cannot give a warp function to a foreign call yet; run it with warp")
94		}
95		other => wasmtime::bail!("a value of kind {other} cannot cross to a foreign call from an executable yet"),
96	})
97}
98
99/// The items of a list of cons cells: each cell's first item, then its rest, a cell or a last single item
100pub fn items_of<T>(mut first: Val, mut rest: Val, store: &mut StoreContextMut<'_, T>) -> Result<Vec<Val>> {
101	let mut items = vec![];
102	loop {
103		if first.unwrap_anyref().is_some() {
104			items.push(first);
105		}
106		match fields(&rest, store) {
107			Some((kind, data, value)) if matches!((kind & KIND_MASK) as u8, LIST | BLOCK) => {
108				(first, rest) = (data, value)
109			}
110			Some((kind, ..)) if (kind & KIND_MASK) as u8 == EMPTY => return Ok(items),
111			Some(_) => return Ok([items, vec![rest]].concat()),
112			None => return Ok(items),
113		}
114	}
115}
116
117/// The parts of a node: a list's items, ø none, anything else itself (a call's arguments)
118pub fn parts_of<T>(node: &Val, store: &mut StoreContextMut<'_, T>) -> Result<Vec<Val>> {
119	match fields(node, store) {
120		Some((kind, data, value)) if matches!((kind & KIND_MASK) as u8, LIST | BLOCK) => items_of(data, value, store),
121		Some((kind, ..)) if (kind & KIND_MASK) as u8 != EMPTY => Ok(vec![*node]),
122		_ => Ok(vec![]),
123	}
124}
125
126/// A curly list: its keys as entries, other items numbered by their place
127fn object_of<T>(items: Vec<Val>, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<Value> {
128	let mut object = Map::new();
129	for item in items {
130		match fields(&item, store) {
131			Some((kind, key, value)) if (kind & KIND_MASK) as u8 == KEY => {
132				let (key, value) = entry_of(&key, &value, store, memory)?;
133				object.insert(key, value);
134			}
135			_ => {
136				object.insert(object.len().to_string(), json_of(&item, store, memory)?);
137			}
138		}
139	}
140	Ok(Value::Object(object))
141}
142
143fn entry_of<T>(key: &Val, value: &Val, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<(String, Value)> {
144	let name = match json_of(key, store, memory)? {
145		Value::String(name) => name,
146		other => other.to_string(),
147	};
148	Ok((name, json_of(value, store, memory)?))
149}
150
151/// The integer of an Int payload `$i64box`, when it is one
152fn plain_integer<T>(payload: &Val, store: &mut StoreContextMut<'_, T>) -> Option<i64> {
153	match as_struct(payload, store)?.field(&mut *store, 0).ok()? {
154		Val::I64(integer) => Some(integer),
155		_ => None,
156	}
157}
158
159/// An Int payload as a float: `$i64box`, `$BigInt(negative, limbs)` base 2^32, or a `$Ratio` of two payloads
160fn integer_of<T>(payload: &Val, store: &mut StoreContextMut<'_, T>) -> Result<f64> {
161	let payload = as_struct(payload, store).ok_or_else(|| wasmtime::Error::msg("an Int without payload"))?;
162	Ok(match payload.field(&mut *store, 0)? {
163		Val::I64(integer) => integer as f64,
164		numerator @ Val::AnyRef(_) => {
165			integer_of(&numerator, store)? / integer_of(&payload.field(&mut *store, 1)?, store)?
166		}
167		Val::I32(negative) => {
168			let limbs = payload.field(&mut *store, 1)?;
169			let limbs = limbs
170				.unwrap_anyref()
171				.ok_or_else(|| wasmtime::Error::msg("a BigInt without limbs"))?
172				.unwrap_array(&*store)?;
173			let magnitude = limbs
174				.elems(&mut *store)?
175				.collect::<Vec<_>>()
176				.iter()
177				.rev()
178				.fold(0.0, |high, limb| high * 2f64.powi(LIMB_BITS as i32) + f64::from(limb.unwrap_i32() as u32));
179			if negative != 0 {
180				-magnitude
181			} else {
182				magnitude
183			}
184		}
185		other => wasmtime::bail!("an unexpected Int payload {other:?}"),
186	})
187}
188
189/// The letters of a `$String(ptr, len)` in linear memory
190fn text_of<T>(string: &Val, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<String> {
191	let Some(string) = as_struct(string, store) else {
192		return Ok(String::new());
193	};
194	let pointer = string.field(&mut *store, 0)?.unwrap_i32() as u32 as usize;
195	let length = string.field(&mut *store, 1)?.unwrap_i32() as u32 as usize;
196	let bytes = memory
197		.data(&*store)
198		.get(pointer..pointer + length)
199		.ok_or_else(|| wasmtime::Error::msg("a text beyond memory"))?;
200	Ok(String::from_utf8_lossy(bytes).into_owned())
201}
202
203/// The constructors, memory and text heap of the program: what JSON is built into a node with
204pub struct Builders {
205	functions: Vec<(&'static str, Func)>,
206	memory: Memory,
207	heap: Option<Global>,
208}
209
210impl Builders {
211	pub fn of(lookup: &mut dyn FnMut(&str) -> Option<wasmtime::Extern>) -> Result<Builders> {
212		let functions = BUILDERS.iter().filter_map(|&name| Some((name, lookup(name)?.into_func()?))).collect();
213		let memory = lookup(MEMORY)
214			.and_then(|export| export.into_memory())
215			.ok_or_else(|| wasmtime::Error::msg("the program exports no memory"))?;
216		let heap = lookup(TEXT_HEAP).and_then(|export| export.into_global());
217		Ok(Builders { functions, memory, heap })
218	}
219
220	fn call<T>(&self, name: &str, arguments: &[Val], result: Val, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
221		let (_, function) = self
222			.functions
223			.iter()
224			.find(|(known, _)| *known == name)
225			.ok_or_else(|| wasmtime::Error::msg(format!("the program exports no {name}")))?;
226		let mut results = [result];
227		function.call(&mut *store, arguments, &mut results)?;
228		Ok(results[0])
229	}
230
231	fn node<T>(&self, name: &str, arguments: &[Val], store: &mut StoreContextMut<'_, T>) -> Result<Val> {
232		self.call(name, arguments, Val::AnyRef(None), store)
233	}
234
235	fn int_operation<T>(&self, name: &str, a: i64, b: i64, store: &mut StoreContextMut<'_, T>) -> Result<i64> {
236		Ok(self.call(name, &[Val::I64(a), Val::I64(b)], Val::I64(0), store)?.unwrap_i64())
237	}
238
239	/// JSON as a node of the program: null ø, booleans 1/0, objects `{key:value …}`, `{$int: digits}` an exact integer,
240	/// `{$char: "b"}` a codepoint
241	pub fn build<T>(&self, value: &Value, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
242		match value {
243			Value::Null => self.node(NEW_EMPTY, &[], store),
244			Value::Bool(truth) => self.node(NEW_INT, &[Val::I64(i64::from(*truth))], store),
245			Value::Number(number) => match number.as_i64() {
246				Some(integer) => self.integer(&integer.to_string(), store),
247				None => self.node(NEW_FLOAT, &[Val::F64(number.as_f64().unwrap_or(f64::NAN).to_bits())], store),
248			},
249			Value::String(text) => self.text(NEW_TEXT, text, store),
250			Value::Array(items) => {
251				self.list(items.iter().map(|item| self.build(item, store)).collect::<Result<_>>()?, SQUARE, store)
252			}
253			Value::Object(entries) => match single_text(entries) {
254				Some((BIG_INTEGER_KEY, digits)) if is_integer(digits) => self.integer(digits, store),
255				Some((CODEPOINT_KEY, letter)) if letter.chars().count() == 1 => {
256					self.node(NEW_CODEPOINT, &[Val::I32(letter.chars().next().unwrap_or('\0') as i32)], store)
257				}
258				_ => {
259					let mut keys = vec![];
260					for (key, value) in entries {
261						let (key, value) = (self.text(NEW_SYMBOL, key, store)?, self.build(value, store)?);
262						keys.push(self.node(NEW_KEY, &[key, value, Val::I64(COLON)], store)?);
263					}
264					self.list(keys, CURLY, store)
265				}
266			},
267		}
268	}
269
270	fn text<T>(&self, constructor: &str, text: &str, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
271		let heap = self.heap.ok_or_else(|| wasmtime::Error::msg(format!("the program exports no {TEXT_HEAP}")))?;
272		let (pointer, length) = write_to_heap(&self.memory, heap, store, text.as_bytes())?;
273		self.node(constructor, &[Val::I32(pointer as i32), Val::I32(length as i32)], store)
274	}
275
276	/// ø for no items, as the emitter makes `[]`
277	fn list<T>(&self, items: Vec<Val>, bracket: i64, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
278		if items.is_empty() {
279			return self.node(NEW_EMPTY, &[], store);
280		}
281		items
282			.into_iter()
283			.rev()
284			.try_fold(Val::AnyRef(None), |rest, first| self.node(NEW_LIST, &[first, rest, Val::I64(bracket)], store))
285	}
286
287	/// The Int of decimal digits: a fixnum as it is, a larger integer composed from 32-bit limbs
288	fn integer<T>(&self, digits: &str, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
289		let handle = match digits.parse::<i64>() {
290			Ok(small) if (FIXNUM_MIN..=FIXNUM_MAX).contains(&small) => small,
291			_ => {
292				let (negative, magnitude) = digits.strip_prefix('-').map_or((false, digits), |rest| (true, rest));
293				let mut handle = 0;
294				for limb in limbs_of(magnitude).iter().rev() {
295					handle = self.int_operation(SHIFT_LEFT, handle, LIMB_BITS, store)?;
296					handle = self.int_operation(ADD, handle, i64::from(*limb), store)?;
297				}
298				if negative {
299					handle = self.int_operation(SUBTRACT, 0, handle, store)?;
300				}
301				handle
302			}
303		};
304		self.node(NEW_INT, &[Val::I64(handle)], store)
305	}
306}
307
308fn single_text(entries: &Map<String, Value>) -> Option<(&str, &str)> {
309	match entries.iter().next() {
310		Some((key, Value::String(text))) if entries.len() == 1 => Some((key.as_str(), text.as_str())),
311		_ => None,
312	}
313}
314
315fn is_integer(digits: &str) -> bool {
316	let magnitude = digits.strip_prefix('-').unwrap_or(digits);
317	!magnitude.is_empty() && magnitude.bytes().all(|digit| digit.is_ascii_digit())
318}
319
320/// Decimal digits as base-2^32 limbs, least significant first
321fn limbs_of(digits: &str) -> Vec<u32> {
322	let mut limbs: Vec<u32> = vec![];
323	for digit in digits.bytes() {
324		let mut carry = u64::from(digit - b'0');
325		for limb in limbs.iter_mut() {
326			let product = u64::from(*limb) * 10 + carry;
327			*limb = product as u32;
328			carry = product >> LIMB_BITS;
329		}
330		if carry > 0 {
331			limbs.push(carry as u32);
332		}
333	}
334	limbs
335}
336
337/// Copy bytes into a module's memory at its text heap, for any store: fresh pages past the current memory when the heap
338/// is unset or full, the module's own emit_text_allocation rule
339pub fn write_to_heap<T>(
340	memory: &Memory,
341	heap: Global,
342	store: &mut StoreContextMut<'_, T>,
343	bytes: &[u8],
344) -> Result<(u32, u32)> {
345	let length = bytes.len() as u32;
346	let top = heap.get(&mut *store).i32().unwrap_or(0) as u32;
347	let memory_end = (memory.size(&*store) as u32) << PAGE_BITS;
348	let pointer = if top == 0 || top + length > memory_end {
349		memory.grow(&mut *store, ((length >> PAGE_BITS) + 1) as u64)?;
350		memory_end
351	} else {
352		top
353	};
354	let written = write_at(memory, store, pointer, bytes)?;
355	heap.set(&mut *store, Val::I32((pointer + length) as i32))?;
356	Ok(written)
357}
358
359/// The bytes at `pointer`, the memory grown to hold them
360pub fn write_at<T>(
361	memory: &Memory,
362	store: &mut StoreContextMut<'_, T>,
363	pointer: u32,
364	bytes: &[u8],
365) -> Result<(u32, u32)> {
366	let length = bytes.len() as u32;
367	let pages_needed = ((pointer + length) as usize).div_ceil(1 << PAGE_BITS);
368	let current_pages = memory.size(&*store) as usize;
369	if pages_needed > current_pages {
370		memory.grow(&mut *store, (pages_needed - current_pages) as u64)?;
371	}
372	memory.write(&mut *store, pointer as usize, bytes)?;
373	Ok((pointer, length))
374}
375
376/// The host side of foreign_call(runtime, module, member, call, arguments) in an executable: python and js through the
377/// bridge (foreign.rs), the answer built in the program; components and warp functions refused loudly
378pub fn foreign_call<T>(
379	caller: &mut wasmtime::Caller<'_, T>,
380	parts: [Option<Rooted<AnyRef>>; 5],
381) -> Result<Option<Rooted<AnyRef>>> {
382	let builders = Builders::of(&mut |name| caller.get_export(name))?;
383	let memory = builders.memory;
384	let mut store = caller.as_context_mut();
385	let [runtime, module, member, call, arguments] = parts.map(Val::AnyRef);
386	let mut text = |value: &Val| match json_of(value, &mut store, memory)? {
387		Value::String(text) => Ok(text),
388		other => Ok::<_, wasmtime::Error>(other.to_string()),
389	};
390	let (runtime, member) = (text(&runtime)?, text(&member)?);
391	if !crate::foreign::is_interpreted(&runtime) {
392		wasmtime::bail!("{runtime} {member}: an executable calls python and js only; run it with warp");
393	}
394	let module = json_of(&module, &mut store, memory)?;
395	let module_name = module.as_str().map_or_else(|| module.to_string(), str::to_string);
396	let arguments = match json_of(&call, &mut store, memory)? {
397		Value::Number(called) if called.as_i64() == Some(1) => Value::Array(
398			parts_of(&arguments, &mut store)?
399				.iter()
400				.map(|argument| json_of(argument, &mut store, memory))
401				.collect::<Result<_>>()?,
402		),
403		_ => Value::Null,
404	};
405	let mut no_callbacks = |_: usize, _: Value| Err("an executable cannot call a warp function back yet".to_string());
406	let answer = crate::foreign::call(&runtime, &module_name, module, &member, arguments, &mut no_callbacks)
407		.map_err(wasmtime::Error::msg)?;
408	Ok(builders.build(&answer, &mut store)?.unwrap_anyref().copied())
409}