pub(crate) mod aggregate;
mod binary_op;
mod block;
mod call;
mod control;
mod literal;
mod optional;
mod reference;
mod unary_op;
use std::cell::Cell;
use std::collections::HashMap;
use formalang::ast::PrimitiveType;
use formalang::ir::{
BindingId, FunctionId, ImplId, IrExpr, IrModule, MethodIdx, ResolvedType, StructId, TraitId,
};
use thiserror::Error;
use wasm_encoder::{InstructionSink, ValType};
use crate::layout::LayoutError;
use crate::types::TypeMapError;
pub use aggregate::{
lower_array, lower_closure_ref, lower_dict_access, lower_dict_literal, lower_enum_inst,
lower_field_access, lower_range, lower_self_field_ref, lower_struct_inst, lower_tuple,
};
pub use binary_op::lower_binary_op;
pub use block::{lower_block, lower_function_body, lower_function_body_in_module};
pub use call::{lower_call_closure, lower_function_call, lower_method_call};
pub use control::{lower_for, lower_if, lower_match};
pub use literal::lower_literal;
pub use reference::{lower_let_ref, lower_reference};
pub use unary_op::lower_unary_op;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LowerError {
#[error("lowering for {what} is not yet implemented")]
NotYetImplemented {
what: String,
},
#[error("numeric literal {payload} cannot be lowered as {target:?}")]
LiteralOutOfRange {
payload: String,
target: PrimitiveType,
},
#[error("literal kind {kind} does not match declared type {ty:?}")]
LiteralTypeMismatch {
kind: String,
ty: ResolvedType,
},
#[error("BindingId {0:?} is not registered in the binding map")]
UnknownBinding(BindingId),
#[error(
"Reference target is Unresolved — ResolveReferencesPass must run before WasmBackend::generate"
)]
UnresolvedReference,
#[error("operator {op} on {operand:?} is not supported in this phase")]
UnsupportedOperator {
op: String,
operand: PrimitiveType,
},
#[error(transparent)]
TypeMap(#[from] TypeMapError),
#[error("let binding '{name}' has zero-sized type {ty:?} and cannot be stored in a wasm local")]
ZeroSizedLetBinding {
name: String,
ty: ResolvedType,
},
#[error("FunctionId {0:?} is not registered in the function map")]
UnknownFunction(FunctionId),
#[error("FunctionCall path {path:?} is unresolved (function_id = None)")]
UnresolvedFunctionCall {
path: Vec<String>,
},
#[error("LowerContext is missing the {what} field required for this lowering")]
MissingContext {
what: &'static str,
},
#[error("StructInst targets an external struct (struct_id = None) — Phase 4")]
ExternalStructInst,
#[error(transparent)]
Layout(#[from] LayoutError),
#[error(
"field index {field_idx} is out of range for struct '{struct_name}' ({field_count} fields)"
)]
FieldIndexOutOfRange {
struct_name: String,
field_count: usize,
field_idx: u32,
},
#[error("field access on non-aggregate type {ty:?} — type-checker should have rejected this")]
FieldAccessOnNonAggregate {
ty: ResolvedType,
},
#[error("StructId {0:?} is not present in IrModule.structs")]
UnknownStruct(formalang::ir::StructId),
#[error("EnumInst targets an external enum (enum_id = None) — Phase 4")]
ExternalEnumInst,
#[error("EnumId {0:?} is not present in IrModule.enums")]
UnknownEnum(formalang::ir::EnumId),
#[error("variant '{variant}' is not declared on enum '{enum_name}'")]
UnknownVariant {
enum_name: String,
variant: String,
},
#[error("SelfFieldRef encountered in a function with no `self` struct context")]
MissingSelfStruct,
#[error("static method (impl {impl_id:?}, method {method_idx:?}) is not registered")]
UnknownMethod {
impl_id: ImplId,
method_idx: MethodIdx,
},
#[error("virtual method dispatch is not yet supported (Phase 3)")]
VirtualMethodCall,
#[error("no vtable registered for trait {trait_id:?} on target tag={}, id={}", target.0, target.1)]
UnknownVtable {
trait_id: TraitId,
target: ImplTargetKey,
},
#[error("no call_indirect type registered for trait {trait_id:?} method {method_idx:?}")]
UnknownVirtualMethodType {
trait_id: TraitId,
method_idx: MethodIdx,
},
#[error("virtual method dispatch on receiver type {ty:?} is not supported")]
UnsupportedVirtualReceiver {
ty: ResolvedType,
},
}
#[derive(Debug, Default, Clone)]
pub struct FunctionMap {
by_id: HashMap<FunctionId, u32>,
}
impl FunctionMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, id: FunctionId, wasm_index: u32) {
self.by_id.insert(id, wasm_index);
}
#[must_use]
pub fn get(&self, id: FunctionId) -> Option<u32> {
self.by_id.get(&id).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.by_id.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
}
#[derive(Debug, Default, Clone)]
pub struct MethodMap {
by_id: HashMap<(ImplId, MethodIdx), u32>,
}
impl MethodMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: (ImplId, MethodIdx), wasm_index: u32) {
self.by_id.insert(key, wasm_index);
}
pub fn iter(&self) -> impl Iterator<Item = (&(ImplId, MethodIdx), &u32)> {
self.by_id.iter()
}
#[must_use]
pub fn get(&self, key: (ImplId, MethodIdx)) -> Option<u32> {
self.by_id.get(&key).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.by_id.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
}
#[derive(Debug, Default, Clone)]
pub struct BindingMap {
by_id: HashMap<BindingId, u32>,
}
impl BindingMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, id: BindingId, local_index: u32) {
self.by_id.insert(id, local_index);
}
#[must_use]
pub fn get(&self, id: BindingId) -> Option<u32> {
self.by_id.get(&id).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.by_id.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct LowerContext<'a> {
pub bindings: &'a BindingMap,
pub functions: &'a FunctionMap,
pub methods: Option<&'a MethodMap>,
pub module: Option<&'a IrModule>,
pub bump_allocator: Option<u32>,
pub scratch_locals: Option<&'a ScratchAllocator>,
pub self_struct_id: Option<StructId>,
pub closure_table: Option<u32>,
pub closure_funcref_indices: Option<&'a HashMap<FunctionId, u32>>,
pub closure_type_indices: Option<&'a HashMap<ResolvedType, u32>>,
pub string_pool: Option<&'a HashMap<String, u32>>,
pub str_eq: Option<u32>,
pub str_concat: Option<u32>,
pub method_table: Option<u32>,
pub vtable_offsets: Option<&'a HashMap<(TraitId, ImplTargetKey), u32>>,
pub virtual_call_type_indices: Option<&'a HashMap<(TraitId, MethodIdx), u32>>,
}
impl<'a> LowerContext<'a> {
#[must_use]
pub const fn new(bindings: &'a BindingMap, functions: &'a FunctionMap) -> Self {
Self {
bindings,
functions,
methods: None,
module: None,
bump_allocator: None,
scratch_locals: None,
self_struct_id: None,
closure_table: None,
closure_funcref_indices: None,
closure_type_indices: None,
string_pool: None,
str_eq: None,
str_concat: None,
method_table: None,
vtable_offsets: None,
virtual_call_type_indices: None,
}
}
#[must_use]
pub const fn with_methods(mut self, methods: &'a MethodMap) -> Self {
self.methods = Some(methods);
self
}
#[must_use]
pub const fn with_module(mut self, module: &'a IrModule) -> Self {
self.module = Some(module);
self
}
#[must_use]
pub const fn with_bump_allocator(mut self, idx: u32) -> Self {
self.bump_allocator = Some(idx);
self
}
#[must_use]
pub const fn with_scratch_locals(mut self, allocator: &'a ScratchAllocator) -> Self {
self.scratch_locals = Some(allocator);
self
}
#[must_use]
pub const fn with_self_struct_id(mut self, id: StructId) -> Self {
self.self_struct_id = Some(id);
self
}
#[must_use]
pub const fn with_closure_table(mut self, idx: u32) -> Self {
self.closure_table = Some(idx);
self
}
#[must_use]
pub const fn with_closure_funcref_indices(
mut self,
indices: &'a HashMap<FunctionId, u32>,
) -> Self {
self.closure_funcref_indices = Some(indices);
self
}
#[must_use]
pub const fn with_closure_type_indices(
mut self,
indices: &'a HashMap<ResolvedType, u32>,
) -> Self {
self.closure_type_indices = Some(indices);
self
}
#[must_use]
pub const fn with_string_pool(mut self, pool: &'a HashMap<String, u32>) -> Self {
self.string_pool = Some(pool);
self
}
#[must_use]
pub const fn with_str_eq(mut self, idx: u32) -> Self {
self.str_eq = Some(idx);
self
}
pub fn str_eq_index(&self) -> Result<u32, LowerError> {
self.str_eq
.ok_or(LowerError::MissingContext { what: "str_eq" })
}
#[must_use]
pub const fn with_str_concat(mut self, idx: u32) -> Self {
self.str_concat = Some(idx);
self
}
pub fn str_concat_index(&self) -> Result<u32, LowerError> {
self.str_concat
.ok_or(LowerError::MissingContext { what: "str_concat" })
}
pub fn string_header_offset(&self, text: &str) -> Result<u32, LowerError> {
let pool = self.string_pool.ok_or(LowerError::MissingContext {
what: "string_pool",
})?;
pool.get(text)
.copied()
.ok_or_else(|| LowerError::NotYetImplemented {
what: format!("string literal {text:?} was not pre-interned"),
})
}
pub fn closure_table_index(&self) -> Result<u32, LowerError> {
self.closure_table.ok_or(LowerError::MissingContext {
what: "closure_table",
})
}
pub fn closure_type_index(&self, ty: &ResolvedType) -> Result<u32, LowerError> {
let map = self
.closure_type_indices
.ok_or(LowerError::MissingContext {
what: "closure_type_indices",
})?;
map.get(ty)
.copied()
.ok_or_else(|| LowerError::NotYetImplemented {
what: format!("call_indirect type signature for {ty:?} (not pre-registered)"),
})
}
pub fn closure_funcref_index(&self, id: FunctionId) -> Result<u32, LowerError> {
let map = self
.closure_funcref_indices
.ok_or(LowerError::MissingContext {
what: "closure_funcref_indices",
})?;
map.get(&id).copied().ok_or(LowerError::UnknownFunction(id))
}
#[must_use]
pub const fn with_method_table(mut self, idx: u32) -> Self {
self.method_table = Some(idx);
self
}
#[must_use]
pub const fn with_vtable_offsets(
mut self,
offsets: &'a HashMap<(TraitId, ImplTargetKey), u32>,
) -> Self {
self.vtable_offsets = Some(offsets);
self
}
#[must_use]
pub const fn with_virtual_call_type_indices(
mut self,
indices: &'a HashMap<(TraitId, MethodIdx), u32>,
) -> Self {
self.virtual_call_type_indices = Some(indices);
self
}
pub fn method_table_index(&self) -> Result<u32, LowerError> {
self.method_table.ok_or(LowerError::MissingContext {
what: "method_table",
})
}
pub fn vtable_offset(
&self,
trait_id: TraitId,
target: ImplTargetKey,
) -> Result<u32, LowerError> {
let map = self.vtable_offsets.ok_or(LowerError::MissingContext {
what: "vtable_offsets",
})?;
map.get(&(trait_id, target))
.copied()
.ok_or(LowerError::UnknownVtable { trait_id, target })
}
pub fn virtual_call_type_index(
&self,
trait_id: TraitId,
method_idx: MethodIdx,
) -> Result<u32, LowerError> {
let map = self
.virtual_call_type_indices
.ok_or(LowerError::MissingContext {
what: "virtual_call_type_indices",
})?;
map.get(&(trait_id, method_idx))
.copied()
.ok_or(LowerError::UnknownVirtualMethodType {
trait_id,
method_idx,
})
}
pub fn module(&self) -> Result<&'a IrModule, LowerError> {
self.module
.ok_or(LowerError::MissingContext { what: "module" })
}
#[must_use]
pub const fn module_opt(&self) -> Option<&'a IrModule> {
self.module
}
pub fn bump_allocator(&self) -> Result<u32, LowerError> {
self.bump_allocator.ok_or(LowerError::MissingContext {
what: "bump_allocator",
})
}
pub fn next_scratch_local(&self, ty: ValType) -> Result<u32, LowerError> {
let allocator = self.scratch_locals.ok_or(LowerError::MissingContext {
what: "scratch_locals",
})?;
allocator.allocate(ty)
}
}
#[expect(
clippy::exhaustive_structs,
reason = "plain bundle consumed by the function-body planner"
)]
#[derive(Debug, Clone, Copy)]
pub struct ClosureCallContext<'a> {
pub table_idx: u32,
pub funcref_indices: &'a HashMap<FunctionId, u32>,
pub type_indices: &'a HashMap<ResolvedType, u32>,
}
pub type ImplTargetKey = (u32, u32);
#[expect(
clippy::exhaustive_structs,
reason = "plain bundle consumed by the function-body planner"
)]
#[derive(Debug, Clone, Copy)]
pub struct VTableContext<'a> {
pub table_idx: u32,
pub vtable_offsets: &'a HashMap<(TraitId, ImplTargetKey), u32>,
pub call_type_indices: &'a HashMap<(TraitId, MethodIdx), u32>,
}
#[derive(Debug)]
pub struct ScratchAllocator {
i32_base: u32,
i32_next: Cell<u32>,
i32_count: u32,
i64_base: u32,
i64_next: Cell<u32>,
i64_count: u32,
f32_base: u32,
f32_next: Cell<u32>,
f32_count: u32,
f64_base: u32,
f64_next: Cell<u32>,
f64_count: u32,
}
#[expect(
clippy::exhaustive_structs,
reason = "plain layout record consumed by the function-body planner"
)]
#[derive(Debug, Clone, Copy)]
pub struct ScratchRegions {
pub i32: (u32, u32),
pub i64: (u32, u32),
pub f32: (u32, u32),
pub f64: (u32, u32),
}
impl ScratchAllocator {
#[must_use]
pub const fn new(regions: ScratchRegions) -> Self {
let (i32_base, i32_count) = regions.i32;
let (i64_base, i64_count) = regions.i64;
let (f32_base, f32_count) = regions.f32;
let (f64_base, f64_count) = regions.f64;
Self {
i32_base,
i32_next: Cell::new(0),
i32_count,
i64_base,
i64_next: Cell::new(0),
i64_count,
f32_base,
f32_next: Cell::new(0),
f32_count,
f64_base,
f64_next: Cell::new(0),
f64_count,
}
}
pub fn allocate(&self, ty: ValType) -> Result<u32, LowerError> {
let (next, base, capacity, tag) = match ty {
ValType::I32 => (&self.i32_next, self.i32_base, self.i32_count, "i32"),
ValType::I64 => (&self.i64_next, self.i64_base, self.i64_count, "i64"),
ValType::F32 => (&self.f32_next, self.f32_base, self.f32_count, "f32"),
ValType::F64 => (&self.f64_next, self.f64_base, self.f64_count, "f64"),
ValType::V128 | ValType::Ref(_) => {
return Err(LowerError::NotYetImplemented {
what: format!("scratch local of type {ty:?}"),
});
}
};
let used = next.get();
if used >= capacity {
return Err(LowerError::NotYetImplemented {
what: format!(
"scratch-local pre-walk under-counted {tag} slots (reserved {capacity}, asked for {})",
used.saturating_add(1)
),
});
}
let local_index = base
.checked_add(used)
.ok_or_else(|| LowerError::NotYetImplemented {
what: format!("scratch local index overflow for {tag}"),
})?;
next.set(used.saturating_add(1));
Ok(local_index)
}
}
pub fn lower_expr(
expr: &IrExpr,
sink: &mut InstructionSink<'_>,
ctx: &LowerContext<'_>,
) -> Result<(), LowerError> {
match expr {
IrExpr::Literal { .. } => lower_literal(expr, sink, ctx),
IrExpr::Reference { .. } => lower_reference(expr, sink, ctx),
IrExpr::LetRef { .. } => lower_let_ref(expr, sink, ctx),
IrExpr::BinaryOp { .. } => lower_binary_op(expr, sink, ctx),
IrExpr::UnaryOp { .. } => lower_unary_op(expr, sink, ctx),
IrExpr::Block { .. } => lower_block(expr, sink, ctx),
IrExpr::FunctionCall { .. } => lower_function_call(expr, sink, ctx),
IrExpr::CallClosure { .. } => lower_call_closure(expr, sink, ctx),
IrExpr::If { .. } => lower_if(expr, sink, ctx),
IrExpr::StructInst { .. } => lower_struct_inst(expr, sink, ctx),
IrExpr::FieldAccess { .. } => lower_field_access(expr, sink, ctx),
IrExpr::Tuple { .. } => lower_tuple(expr, sink, ctx),
IrExpr::EnumInst { .. } => lower_enum_inst(expr, sink, ctx),
IrExpr::Match { .. } => lower_match(expr, sink, ctx),
IrExpr::SelfFieldRef { .. } => lower_self_field_ref(expr, sink, ctx),
IrExpr::MethodCall { .. } => lower_method_call(expr, sink, ctx),
IrExpr::ClosureRef { .. } => lower_closure_ref(expr, sink, ctx),
IrExpr::Array { .. } => lower_array(expr, sink, ctx),
IrExpr::For { .. } => lower_for(expr, sink, ctx),
IrExpr::DictAccess { .. } => lower_dict_access(expr, sink, ctx),
IrExpr::DictLiteral { .. } => lower_dict_literal(expr, sink, ctx),
IrExpr::Closure { .. } => Err(LowerError::NotYetImplemented {
what: format!("IrExpr::{}", expr_variant_name(expr)),
}),
}
}
pub(crate) const fn expr_variant_name(expr: &IrExpr) -> &'static str {
match expr {
IrExpr::Literal { .. } => "Literal",
IrExpr::Reference { .. } => "Reference",
IrExpr::LetRef { .. } => "LetRef",
IrExpr::SelfFieldRef { .. } => "SelfFieldRef",
IrExpr::FieldAccess { .. } => "FieldAccess",
IrExpr::StructInst { .. } => "StructInst",
IrExpr::EnumInst { .. } => "EnumInst",
IrExpr::Tuple { .. } => "Tuple",
IrExpr::Array { .. } => "Array",
IrExpr::BinaryOp { .. } => "BinaryOp",
IrExpr::UnaryOp { .. } => "UnaryOp",
IrExpr::If { .. } => "If",
IrExpr::For { .. } => "For",
IrExpr::Match { .. } => "Match",
IrExpr::FunctionCall { .. } => "FunctionCall",
IrExpr::CallClosure { .. } => "CallClosure",
IrExpr::MethodCall { .. } => "MethodCall",
IrExpr::Closure { .. } => "Closure",
IrExpr::ClosureRef { .. } => "ClosureRef",
IrExpr::DictLiteral { .. } => "DictLiteral",
IrExpr::DictAccess { .. } => "DictAccess",
IrExpr::Block { .. } => "Block",
}
}