Lines
95.4 %
Functions
48 %
Branches
100 %
//! Wasm module section-level registration: types, imports, functions,
//! exports, data segments, plus the local-pool allocator.
//!
//! Every helper that mutates the wasm encoder sections lives here so
//! the other context submodules (types, pair, ratio, commodity, ...)
//! are pure consumers of stable indices.
use super::{CompileContext, LOCAL_POOL_BASE};
use crate::ast::WasmType;
use crate::error::{Error, Result};
use tracing::debug;
use wasm_encoder::{
ArrayType, CompositeInnerType, CompositeType, EntityType as WasmEntityType, ExportKind,
FieldType, StorageType, StructType, SubType, ValType,
};
fn bump(counter: &mut u32, kind: &'static str) -> Result<u32> {
let idx = *counter;
*counter = counter
.checked_add(1)
.ok_or_else(|| Error::Compile(format!("wasm {kind} index space exhausted")))?;
Ok(idx)
}
impl CompileContext {
pub fn alloc_local(&mut self, ty: WasmType) -> Result<u32> {
let idx = bump(&mut self.next_local, "local")?;
self.local_types.push((ty, idx));
pub fn reset_locals(&mut self) {
self.next_local = LOCAL_POOL_BASE;
self.local_types.clear();
self.closure_bodies.clear();
self.serializer =
super::super::layout::OutputSerializer::new(super::super::expr::LOCAL_OUTPUT_BASE);
pub fn build_locals_declaration(&self) -> Vec<(u32, ValType)> {
let preallocated: [(u32, ValType); 6] = [
(1, self.string_ref()),
(1, ValType::I32),
(1, self.ratio_ref()),
];
let mut locals: Vec<(u32, ValType)> = preallocated.to_vec();
for &(ty, _) in &self.local_types {
locals.push((1, self.wasm_val_type(ty)));
locals
pub fn register_type(&mut self) -> Result<u32> {
let idx = bump(&mut self.type_count, "type")?;
self.types.ty().subtype(&SubType {
is_final: true,
supertype_idx: None,
composite_type: CompositeType {
inner: CompositeInnerType::Array(ArrayType(FieldType {
element_type: StorageType::I8,
mutable: true,
})),
shared: false,
describes: None,
descriptor: None,
},
});
/// Registers a mutable `(array (mut i64))` type. Used for the commodity
/// unit-term (ADR-0028): a flat array of sorted `(hi, lo, exp)` i64 triples.
pub fn register_i64_array_type(&mut self) -> Result<u32> {
element_type: StorageType::Val(ValType::I64),
pub fn register_struct_type(&mut self, fields: &[ValType]) -> Result<u32> {
let struct_fields: Vec<FieldType> = fields
.iter()
.map(|vt| FieldType {
element_type: StorageType::Val(*vt),
mutable: false,
})
.collect();
inner: CompositeInnerType::Struct(StructType {
fields: struct_fields.into_boxed_slice(),
}),
/// Errors if `name` is already bound in `func_names`. The name space is a
/// closed universe (built-in helpers + reserved imports `__nomi_raise` /
/// `log` / `__nomi_catch_each` + user host fns + unique monomorph/lambda
/// helper names), and a silent overwrite would re-point an already-resolved
/// caller at the wrong index. Notably this rejects a user `HostFnSpec` whose
/// `import_name` collides with a reserved built-in (e.g. `"log"`), which
/// would otherwise misroute PRINT/DISPLAY to the user fn or emit invalid
/// wasm on a signature mismatch — surfaced as a structured `Error::Compile`.
fn reject_duplicate_func_name(&self, name: &str) -> Result<()> {
if self.func_names.contains_key(name) {
return Err(Error::Compile(format!(
"wasm function name '{name}' is already registered (reserved built-in or duplicate host fn)"
)));
Ok(())
pub fn register_import(
&mut self,
module: &str,
name: &str,
params: &[ValType],
results: &[ValType],
) -> Result<u32> {
self.reject_duplicate_func_name(name)?;
let type_idx = self.get_or_create_func_type(params, results)?;
self.imports
.import(module, name, WasmEntityType::Function(type_idx));
let func_idx = bump(&mut self.import_func_count, "imported function")?;
self.func_names.insert(name.to_string(), func_idx);
Ok(func_idx)
/// Reserves a stable wasm function index for `name` *before* its
/// body is emitted, and returns the new index. The function
/// section already accounts for the slot at this point, so any
/// body emitted afterward can `call $name` (including itself for
/// recursion) by looking the index up via [`Self::declared_func_index`].
/// The body must land on `pending_helpers` in the order matching the
/// reservation sequence; later phases drain that queue into the code
/// section.
pub fn register_function(
self.functions.function(type_idx);
let local_idx = bump(&mut self.local_func_count, "local function")?;
let func_idx = self
.import_func_count
.checked_add(local_idx)
.ok_or_else(|| Error::Compile("wasm function index space exhausted".to_string()))?;
/// Exports a previously-declared function by name. The name set is the
/// closed, static universe registered by the constructor, so a miss is a
/// compiler bug — surfaced as a structured `Error::Compile` rather than a
/// `HashMap[key]` panic (CLAUDE.md).
pub fn export_func(&mut self, name: &str) -> Result<()> {
let idx = self.func_names.get(name).copied().ok_or_else(|| {
Error::Compile(format!(
"internal: cannot export unregistered wasm function '{name}'"
))
})?;
self.exports.export(name, ExportKind::Func, idx);
/// Looks up a declared function index by name, erroring (never panicking)
/// on a miss. For the rare construction-time helper not covered by a typed
/// `WasmIds` field (e.g. `unit_div`); emit-time code uses `ctx.ids.*`.
pub(super) fn declared_func_index(&self, name: &str) -> Result<u32> {
self.func_names.get(name).copied().ok_or_else(|| {
"internal: wasm function '{name}' was not registered during context construction"
pub fn serializer(&mut self) -> &mut super::super::layout::OutputSerializer {
&mut self.serializer
pub(super) fn get_or_create_func_type(
if let Some(inner) = self.type_cache.get(params)
&& let Some(&idx) = inner.get(results)
{
return Ok(idx);
self.types
.ty()
.function(params.iter().copied(), results.iter().copied());
self.type_cache
.entry(params.to_vec())
.or_default()
.insert(results.to_vec(), idx);
pub fn add_data(&mut self, bytes: &[u8]) -> Result<u32> {
let idx = bump(&mut self.data_count, "data segment")?;
debug!(idx, len = bytes.len(), "adding data segment");
self.data.passive(bytes.iter().copied());