Lines
100 %
Functions
45 %
Branches
//! `$pair` setup + entity allocators + string-equality helper.
//!
//! Three loosely-related setup chunks share this file: they all
//! register type + function pairs that the rest of the compiler treats
//! as opaque imports. Pair is the canonical list cell, entity
//! allocators expose typed `alloc_<kind>` exports for host-side
//! struct construction, and `string_eq` is the byte-by-byte array
//! equality helper.
use super::CompileContext;
use crate::error::{Error, Result};
use wasm_encoder::{
CompositeInnerType, CompositeType, FieldType, Function, HeapType, Instruction, RefType,
StorageType, StructType, SubType, ValType,
};
impl CompileContext {
/// Canonical list cell: `(struct (field anyref car) (field (ref null
/// $pair) cdr))`. The only list-cell type in the compiler. CONS boxes
/// the car (`ref.i31` for i32, implicit anyref subtype for Ratio /
/// Commodity / String); CAR downcasts using the compile-time
/// `PairElement` carried by `WasmType::PairRef`. Heterogeneous lists
/// are refused at the language layer, so the wasm shape is uniform
/// regardless of element type.
pub(super) fn register_pair_type(&mut self) -> Result<u32> {
let pair_idx = self.type_count;
let pair_nullable_ref = ValType::Ref(RefType {
nullable: true,
heap_type: HeapType::Concrete(pair_idx),
});
let anyref_field = ValType::Ref(RefType {
heap_type: HeapType::ANY,
let struct_fields = vec![
FieldType {
element_type: StorageType::Val(anyref_field),
mutable: false,
},
element_type: StorageType::Val(pair_nullable_ref),
];
self.types.ty().subtype(&SubType {
is_final: true,
supertype_idx: None,
composite_type: CompositeType {
inner: CompositeInnerType::Struct(StructType {
fields: struct_fields.into_boxed_slice(),
}),
shared: false,
describes: None,
descriptor: None,
self.type_count = self
.type_count
.checked_add(1)
.ok_or_else(|| Error::Compile("wasm type index space exhausted".to_string()))?;
Ok(pair_idx)
}
pub(super) fn declare_pair_helpers(&mut self) -> Result<()> {
let pair_ref = self.pair_ref();
let anyref = self.anyref();
self.register_function("pair_new", &[anyref, pair_ref], &[pair_ref])?;
// Host helpers (`scripting::runtime::alloc_pair_chain`)
// reach `pair_new` via `caller.get_export("pair_new")` to
// fold chains element-by-element. Re-entry through the
// guest's own allocator dodges engine type canonicalization
// for the `$pair` struct's `(ref null $pair)` cdr field.
self.export_func("pair_new")?;
Ok(())
pub(super) fn build_pair_helpers(&mut self) {
self.build_pair_new_body();
/// One exported `alloc_<kind>` per entity wasm struct. Body is a
/// straight `struct.new $<kind>` over the params. Host fns that
/// produce typed entity returns (`list-accounts`, `get-account`,
/// ...) re-enter the module via these exports — host-side
/// `StructType::new` can't reproduce the entity's reference-typed
/// field signature (`(ref null $i8_array)`) because engine
/// canonicalization compares the field's concrete type index, not
/// just the abstract heap class. Re-entry through the module's
/// own allocator dodges the issue: each call produces a struct ref
/// of the exact `$<kind>` type the guest's `ref.cast` then accepts.
/// Declares + exports one `alloc_<kind>` per entity wasm struct (no bodies).
/// Walks `ENTITY_SPECS` in order so [`Self::build_entity_allocators`] can
/// emit the matching bodies positionally.
pub(super) fn declare_entity_allocators(&mut self) -> Result<()> {
// Walks the same `ENTITY_SPECS` table that `new_skeleton`
// consults — single source of truth keeps the allocator
// signature aligned with the struct layout.
for spec in super::entity_registry::ENTITY_SPECS {
let params: Vec<ValType> = spec.fields.iter().map(|f| f.as_val_type(self)).collect();
let entity_ref = self.entity_ref(spec.kind);
let export_name = format!("alloc_{}", spec.type_name);
self.register_function(&export_name, ¶ms, &[entity_ref])?;
// Host helpers (`scripting::runtime::alloc_entity_via_export`)
// reach each allocator via `caller.get_export(...)` — the
// export must be present or the host fn traps at runtime.
self.export_func(&export_name)?;
/// Emits each `alloc_<kind>` body — a flat `local.get $i` × n +
/// `struct.new $kind` — in `ENTITY_SPECS` order, reading the struct index
/// from `self.ids.entity_type`.
pub(super) fn build_entity_allocators(&mut self) -> Result<()> {
let arity = spec.fields.len();
let struct_idx = self.ids.entity_type(spec.kind);
let mut f = Function::new([]);
for i in 0..arity {
let idx = u32::try_from(i)
.map_err(|_| Error::Compile("entity arity exceeds u32".to_string()))?;
f.instruction(&Instruction::LocalGet(idx));
f.instruction(&Instruction::StructNew(struct_idx));
f.instruction(&Instruction::End);
self.pending_helpers.push(f);
fn build_pair_new_body(&mut self) {
let pair_idx = self.ids.ty_pair;
// params: $car=0 (anyref), $cdr=1 (ref null $pair).
// body: push both, struct.new $pair.
f.instruction(&Instruction::LocalGet(0));
f.instruction(&Instruction::LocalGet(1));
f.instruction(&Instruction::StructNew(pair_idx));
pub(super) fn declare_string_eq_helper(&mut self) -> Result<()> {
let string_ref = self.string_ref();
self.register_function("string_eq", &[string_ref, string_ref], &[ValType::I32])?;
pub(super) fn build_string_eq_helper(&mut self) {
self.build_string_eq_body();
fn build_string_eq_body(&mut self) {
let i8_array_idx = self.ids.ty_i8_array;
// params: $a=0 (ref null $i8_array), $b=1 (ref null $i8_array)
// locals: $len=2 (i32), $i=3 (i32)
let mut f = Function::new([(1, ValType::I32), (1, ValType::I32)]);
// Null guard. A null `$i8_array` is the runtime shape of an absent
// string (nil), distinct from a present empty string "". `array.len`
// traps on null, so handle the null cases before touching length:
// null == null → 1 (both absent ≡ nil = nil)
// null == "..." → 0 (absent ≠ present, even ≠ "")
// Lispy nil≠"" semantics — see builtin_reference.org.
// if a is null: return (b is null) as i32
f.instruction(&Instruction::RefIsNull);
f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
f.instruction(&Instruction::Return);
// a is non-null here; if b is null they differ → return 0
f.instruction(&Instruction::I32Const(0));
// len = array.len(a)
f.instruction(&Instruction::ArrayLen);
f.instruction(&Instruction::LocalSet(2));
// if array.len(b) != len → return 0
f.instruction(&Instruction::LocalGet(2));
f.instruction(&Instruction::I32Ne);
// i = 0
f.instruction(&Instruction::LocalSet(3));
// block $exit
f.instruction(&Instruction::Block(wasm_encoder::BlockType::Empty));
// loop $cmp
f.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty));
// if i >= len → break (equal)
f.instruction(&Instruction::LocalGet(3));
f.instruction(&Instruction::I32GeU);
f.instruction(&Instruction::BrIf(1));
// if a[i] != b[i] → return 0
f.instruction(&Instruction::ArrayGetU(i8_array_idx));
// i++
f.instruction(&Instruction::I32Const(1));
f.instruction(&Instruction::I32Add);
f.instruction(&Instruction::Br(0));
f.instruction(&Instruction::End); // end loop
f.instruction(&Instruction::End); // end block
// return 1 (equal)