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, value, 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 held as a vector, `$list (len i32, items (array anyref))` (lib/compiler/emitter.warp): its first
100/// field is the i32 length, where a cons cell's first item is a $Node, whose first field is the i64 kind
101pub fn vector_items<T>(data: &Val, store: &mut StoreContextMut<'_, T>) -> Option<Vec<Val>> {
102	let vector = data.unwrap_anyref()?.unwrap_struct(&*store).ok()?;
103	let Val::I32(length) = vector.field(&mut *store, 0).ok()? else {
104		return None;
105	};
106	let items = vector.field(&mut *store, 1).ok()?.unwrap_anyref()?.as_array(&*store).ok()??;
107	let items: Vec<Val> = items.elems(&mut *store).ok()?.take(length as usize).collect();
108	Some(items)
109}
110
111/// The items of a list from its node's data and value: a vector's items, else those of its cons cells, each cell's
112/// first item, then its rest, a cell or a last single item
113pub fn items_of<T>(mut first: Val, mut rest: Val, store: &mut StoreContextMut<'_, T>) -> Result<Vec<Val>> {
114	if let Some(items) = vector_items(&first, store) {
115		return Ok(items);
116	}
117	let mut items = vec![];
118	loop {
119		if first.unwrap_anyref().is_some() {
120			items.push(first);
121		}
122		match fields(&rest, store) {
123			Some((kind, data, value)) if matches!((kind & KIND_MASK) as u8, LIST | BLOCK) => {
124				(first, rest) = (data, value)
125			}
126			Some((kind, ..)) if (kind & KIND_MASK) as u8 == EMPTY => return Ok(items),
127			Some(_) => return Ok([items, vec![rest]].concat()),
128			None => return Ok(items),
129		}
130	}
131}
132
133/// The parts of a node: a list's items, ø none, anything else itself (a call's arguments)
134pub fn parts_of<T>(node: &Val, store: &mut StoreContextMut<'_, T>) -> Result<Vec<Val>> {
135	match fields(node, store) {
136		Some((kind, data, value)) if matches!((kind & KIND_MASK) as u8, LIST | BLOCK) => items_of(data, value, store),
137		Some((kind, ..)) if (kind & KIND_MASK) as u8 != EMPTY => Ok(vec![*node]),
138		_ => Ok(vec![]),
139	}
140}
141
142/// A curly list: its keys as entries, other items numbered by their place
143fn object_of<T>(items: Vec<Val>, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<Value> {
144	let mut object = Map::new();
145	for item in items {
146		match fields(&item, store) {
147			Some((kind, key, value)) if (kind & KIND_MASK) as u8 == KEY => {
148				let (key, value) = entry_of(&key, &value, store, memory)?;
149				object.insert(key, value);
150			}
151			_ => {
152				object.insert(object.len().to_string(), json_of(&item, store, memory)?);
153			}
154		}
155	}
156	Ok(Value::Object(object))
157}
158
159fn entry_of<T>(key: &Val, value: &Val, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<(String, Value)> {
160	let name = match json_of(key, store, memory)? {
161		Value::String(name) => name,
162		other => other.to_string(),
163	};
164	Ok((name, json_of(value, store, memory)?))
165}
166
167/// The integer of an Int payload `$i64box`, when it is one
168fn plain_integer<T>(payload: &Val, store: &mut StoreContextMut<'_, T>) -> Option<i64> {
169	match as_struct(payload, store)?.field(&mut *store, 0).ok()? {
170		Val::I64(integer) => Some(integer),
171		_ => None,
172	}
173}
174
175/// An Int payload as a float: `$i64box`, `$BigInt(negative, limbs)` base 2^32, or a `$Ratio` of two payloads
176fn integer_of<T>(payload: &Val, store: &mut StoreContextMut<'_, T>) -> Result<f64> {
177	let payload = as_struct(payload, store).ok_or_else(|| wasmtime::Error::msg("an Int without payload"))?;
178	Ok(match payload.field(&mut *store, 0)? {
179		Val::I64(integer) => integer as f64,
180		numerator @ Val::AnyRef(_) => {
181			integer_of(&numerator, store)? / integer_of(&payload.field(&mut *store, 1)?, store)?
182		}
183		Val::I32(negative) => {
184			let limbs = payload.field(&mut *store, 1)?;
185			let limbs = limbs
186				.unwrap_anyref()
187				.ok_or_else(|| wasmtime::Error::msg("a BigInt without limbs"))?
188				.unwrap_array(&*store)?;
189			let magnitude = limbs
190				.elems(&mut *store)?
191				.collect::<Vec<_>>()
192				.iter()
193				.rev()
194				.fold(0.0, |high, limb| high * 2f64.powi(LIMB_BITS as i32) + f64::from(limb.unwrap_i32() as u32));
195			if negative != 0 {
196				-magnitude
197			} else {
198				magnitude
199			}
200		}
201		other => wasmtime::bail!("an unexpected Int payload {other:?}"),
202	})
203}
204
205/// The text of a Text node from its data, after the texts before it when it is a rope (text_append: data the last
206/// piece, the value field the text before it)
207fn text_of<T>(data: &Val, before: Val, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<String> {
208	let mut pieces = vec![bytes_of(data, store, memory)?];
209	let mut before = before;
210	while let Some((_, data, earlier)) = fields(&before, store) {
211		pieces.push(bytes_of(&data, store, memory)?);
212		before = earlier;
213	}
214	Ok(String::from_utf8_lossy(&pieces.into_iter().rev().flatten().collect::<Vec<u8>>()).into_owned())
215}
216
217/// The bytes of a text: a GC byte array `$Text`, or a `$String(ptr, len)` in linear memory
218fn bytes_of<T>(string: &Val, store: &mut StoreContextMut<'_, T>, memory: Memory) -> Result<Vec<u8>> {
219	if let Some(Ok(array)) = string.unwrap_anyref().map(|reference| reference.unwrap_array(&*store)) {
220		return Ok(array.elems(&mut *store)?.map(|byte| byte.unwrap_i32() as u8).collect());
221	}
222	let Some(string) = as_struct(string, store) else {
223		return Ok(vec![]);
224	};
225	let pointer = string.field(&mut *store, 0)?.unwrap_i32() as u32 as usize;
226	let length = string.field(&mut *store, 1)?.unwrap_i32() as u32 as usize;
227	let bytes = memory
228		.data(&*store)
229		.get(pointer..pointer + length)
230		.ok_or_else(|| wasmtime::Error::msg("a text beyond memory"))?;
231	Ok(bytes.to_vec())
232}
233
234/// The constructors, memory and text heap of the program: what JSON is built into a node with
235pub struct Builders {
236	functions: Vec<(&'static str, Func)>,
237	memory: Memory,
238	heap: Option<Global>,
239}
240
241impl Builders {
242	pub fn of(lookup: &mut dyn FnMut(&str) -> Option<wasmtime::Extern>) -> Result<Builders> {
243		let functions = BUILDERS.iter().filter_map(|&name| Some((name, lookup(name)?.into_func()?))).collect();
244		let memory = lookup(MEMORY)
245			.and_then(|export| export.into_memory())
246			.ok_or_else(|| wasmtime::Error::msg("the program exports no memory"))?;
247		let heap = lookup(TEXT_HEAP).and_then(|export| export.into_global());
248		Ok(Builders { functions, memory, heap })
249	}
250
251	fn call<T>(&self, name: &str, arguments: &[Val], result: Val, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
252		let (_, function) = self
253			.functions
254			.iter()
255			.find(|(known, _)| *known == name)
256			.ok_or_else(|| wasmtime::Error::msg(format!("the program exports no {name}")))?;
257		let mut results = [result];
258		function.call(&mut *store, arguments, &mut results)?;
259		Ok(results[0])
260	}
261
262	fn node<T>(&self, name: &str, arguments: &[Val], store: &mut StoreContextMut<'_, T>) -> Result<Val> {
263		self.call(name, arguments, Val::AnyRef(None), store)
264	}
265
266	fn int_operation<T>(&self, name: &str, a: i64, b: i64, store: &mut StoreContextMut<'_, T>) -> Result<i64> {
267		Ok(self.call(name, &[Val::I64(a), Val::I64(b)], Val::I64(0), store)?.unwrap_i64())
268	}
269
270	/// JSON as a node of the program: null ø, booleans 1/0, objects `{key:value …}`, `{$int: digits}` an exact integer,
271	/// `{$char: "b"}` a codepoint
272	pub fn build<T>(&self, value: &Value, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
273		match value {
274			Value::Null => self.node(NEW_EMPTY, &[], store),
275			Value::Bool(truth) => self.node(NEW_INT, &[Val::I64(i64::from(*truth))], store),
276			Value::Number(number) => match number.as_i64() {
277				Some(integer) => self.integer(&integer.to_string(), store),
278				None => self.node(NEW_FLOAT, &[Val::F64(number.as_f64().unwrap_or(f64::NAN).to_bits())], store),
279			},
280			Value::String(text) => self.text(NEW_TEXT, text.as_bytes(), store),
281			Value::Array(items) => {
282				self.list(items.iter().map(|item| self.build(item, store)).collect::<Result<_>>()?, SQUARE, store)
283			}
284			Value::Object(entries) => match single_text(entries) {
285				Some((BIG_INTEGER_KEY, digits)) if is_integer(digits) => self.integer(digits, store),
286				Some((CODEPOINT_KEY, letter)) if letter.chars().count() == 1 => {
287					self.node(NEW_CODEPOINT, &[Val::I32(letter.chars().next().unwrap_or('\0') as i32)], store)
288				}
289				_ => {
290					let mut keys = vec![];
291					for (key, value) in entries {
292						let (key, value) = (self.text(NEW_SYMBOL, key.as_bytes(), store)?, self.build(value, store)?);
293						keys.push(self.node(NEW_KEY, &[key, value, Val::I64(COLON)], store)?);
294					}
295					self.list(keys, CURLY, store)
296				}
297			},
298		}
299	}
300
301	/// A text of `bytes` as they are (a C function's text needs no valid UTF-8), ø for none
302	pub fn text_node<T>(&self, bytes: Option<&[u8]>, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
303		match bytes {
304			Some(bytes) => self.text(NEW_TEXT, bytes, store),
305			None => self.node(NEW_EMPTY, &[], store),
306		}
307	}
308
309	fn text<T>(&self, constructor: &str, text: &[u8], store: &mut StoreContextMut<'_, T>) -> Result<Val> {
310		let heap = self.heap.ok_or_else(|| wasmtime::Error::msg(format!("the program exports no {TEXT_HEAP}")))?;
311		let (pointer, length) = write_to_heap(&self.memory, heap, store, text)?;
312		self.node(constructor, &[Val::I32(pointer as i32), Val::I32(length as i32)], store)
313	}
314
315	/// ø for no items, as the emitter makes `[]`
316	fn list<T>(&self, items: Vec<Val>, bracket: i64, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
317		if items.is_empty() {
318			return self.node(NEW_EMPTY, &[], store);
319		}
320		items
321			.into_iter()
322			.rev()
323			.try_fold(Val::AnyRef(None), |rest, first| self.node(NEW_LIST, &[first, rest, Val::I64(bracket)], store))
324	}
325
326	/// The Int of decimal digits: a fixnum as it is, a larger integer composed from 32-bit limbs
327	fn integer<T>(&self, digits: &str, store: &mut StoreContextMut<'_, T>) -> Result<Val> {
328		let handle = match digits.parse::<i64>() {
329			Ok(small) if (FIXNUM_MIN..=FIXNUM_MAX).contains(&small) => small,
330			_ => {
331				let (negative, magnitude) = digits.strip_prefix('-').map_or((false, digits), |rest| (true, rest));
332				let mut handle = 0;
333				for limb in limbs_of(magnitude).iter().rev() {
334					handle = self.int_operation(SHIFT_LEFT, handle, LIMB_BITS, store)?;
335					handle = self.int_operation(ADD, handle, i64::from(*limb), store)?;
336				}
337				if negative {
338					handle = self.int_operation(SUBTRACT, 0, handle, store)?;
339				}
340				handle
341			}
342		};
343		self.node(NEW_INT, &[Val::I64(handle)], store)
344	}
345}
346
347fn single_text(entries: &Map<String, Value>) -> Option<(&str, &str)> {
348	match entries.iter().next() {
349		Some((key, Value::String(text))) if entries.len() == 1 => Some((key.as_str(), text.as_str())),
350		_ => None,
351	}
352}
353
354fn is_integer(digits: &str) -> bool {
355	let magnitude = digits.strip_prefix('-').unwrap_or(digits);
356	!magnitude.is_empty() && magnitude.bytes().all(|digit| digit.is_ascii_digit())
357}
358
359/// Decimal digits as base-2^32 limbs, least significant first
360fn limbs_of(digits: &str) -> Vec<u32> {
361	let mut limbs: Vec<u32> = vec![];
362	for digit in digits.bytes() {
363		let mut carry = u64::from(digit - b'0');
364		for limb in limbs.iter_mut() {
365			let product = u64::from(*limb) * 10 + carry;
366			*limb = product as u32;
367			carry = product >> LIMB_BITS;
368		}
369		if carry > 0 {
370			limbs.push(carry as u32);
371		}
372	}
373	limbs
374}
375
376/// Copy bytes into a module's memory at its text heap, for any store: fresh pages past the current memory when the heap
377/// is unset or full, the module's own emit_text_allocation rule
378pub fn write_to_heap<T>(
379	memory: &Memory,
380	heap: Global,
381	store: &mut StoreContextMut<'_, T>,
382	bytes: &[u8],
383) -> Result<(u32, u32)> {
384	let length = bytes.len() as u32;
385	let top = heap.get(&mut *store).i32().unwrap_or(0) as u32;
386	let memory_end = (memory.size(&*store) as u32) << PAGE_BITS;
387	let pointer = if top == 0 || top + length > memory_end {
388		memory.grow(&mut *store, ((length >> PAGE_BITS) + 1) as u64)?;
389		memory_end
390	} else {
391		top
392	};
393	let written = write_at(memory, store, pointer, bytes)?;
394	heap.set(&mut *store, Val::I32((pointer + length) as i32))?;
395	Ok(written)
396}
397
398/// The bytes at `pointer`, the memory grown to hold them
399pub fn write_at<T>(
400	memory: &Memory,
401	store: &mut StoreContextMut<'_, T>,
402	pointer: u32,
403	bytes: &[u8],
404) -> Result<(u32, u32)> {
405	let length = bytes.len() as u32;
406	let pages_needed = ((pointer + length) as usize).div_ceil(1 << PAGE_BITS);
407	let current_pages = memory.size(&*store) as usize;
408	if pages_needed > current_pages {
409		memory.grow(&mut *store, (pages_needed - current_pages) as u64)?;
410	}
411	memory.write(&mut *store, pointer as usize, bytes)?;
412	Ok((pointer, length))
413}
414
415/// The host side of foreign_call(runtime, module, member, call, arguments) in an executable: python and js through the
416/// bridge (foreign.rs), the answer built in the program; components and warp functions refused loudly
417pub fn foreign_call<T>(
418	caller: &mut wasmtime::Caller<'_, T>,
419	parts: [Option<Rooted<AnyRef>>; 5],
420) -> Result<Option<Rooted<AnyRef>>> {
421	let builders = Builders::of(&mut |name| caller.get_export(name))?;
422	let memory = builders.memory;
423	let mut store = caller.as_context_mut();
424	let [runtime, module, member, call, arguments] = parts.map(Val::AnyRef);
425	let mut text = |value: &Val| match json_of(value, &mut store, memory)? {
426		Value::String(text) => Ok(text),
427		other => Ok::<_, wasmtime::Error>(other.to_string()),
428	};
429	let (runtime, member) = (text(&runtime)?, text(&member)?);
430	if !crate::foreign::is_interpreted(&runtime) {
431		wasmtime::bail!("{runtime} {member}: an executable calls python and js only; run it with warp");
432	}
433	let module = json_of(&module, &mut store, memory)?;
434	let module_name = module.as_str().map_or_else(|| module.to_string(), str::to_string);
435	let arguments = match json_of(&call, &mut store, memory)? {
436		Value::Number(called) if called.as_i64() == Some(1) => Value::Array(
437			parts_of(&arguments, &mut store)?
438				.iter()
439				.map(|argument| json_of(argument, &mut store, memory))
440				.collect::<Result<_>>()?,
441		),
442		_ => Value::Null,
443	};
444	let mut no_callbacks = |_: usize, _: Value| Err("an executable cannot call a warp function back yet".to_string());
445	let answer = crate::foreign::call(&runtime, &module_name, module, &member, arguments, &mut no_callbacks)
446		.map_err(wasmtime::Error::msg)?;
447	Ok(builders.build(&answer, &mut store)?.unwrap_anyref().copied())
448}