use std::collections::HashMap;
use std::fs;
use std::path::Path;
use wasm_encoder::{
CodeSection, ConstExpr, ElementSection, Elements, ExportKind, ExportSection, Function, FunctionSection, GlobalSection, GlobalType,
Ieee64, Instruction, MemorySection, MemoryType, Module, RefType, TableSection, TableType, TypeSection, ValType,
};
use crate::builtins::syntax::get_raw_args_fn;
use crate::calcit::{
Calcit, CalcitArgLabel, CalcitFnArgs, CalcitImport, CalcitLocal, CalcitProc, CalcitStruct, CalcitSyntax, MethodKind,
};
use crate::program;
#[path = "emit_wasm/methods.rs"]
mod methods;
#[path = "emit_wasm/records.rs"]
mod records;
#[path = "emit_wasm/runtime.rs"]
mod runtime;
use methods::{emit_call_args, emit_method_invoke};
use records::{
emit_enum_tuple_new, emit_record_contains, emit_record_count, emit_record_field_tag, emit_record_get, emit_record_get_name,
emit_record_matches, emit_record_new, emit_record_nth, emit_record_struct, emit_record_to_map, emit_tuple_assoc, emit_tuple_count,
emit_tuple_new, emit_tuple_nth, resolve_struct_ref, try_parse_defrecord_form,
};
use runtime::{HOST_IMPORTS, build_runtime_fns, build_wasm_module};
const HEAP_BASE: i32 = 16;
const HEAP_PTR_GLOBAL: u32 = 0;
const HEAP_MAGIC: i32 = 0xCA1C_17A9u32 as i32;
fn f64_const(v: f64) -> Instruction<'static> {
Instruction::F64Const(Ieee64::from(v))
}
fn mem_arg_f64(offset: u64) -> wasm_encoder::MemArg {
wasm_encoder::MemArg {
offset,
align: 3, memory_index: 0,
}
}
fn mem_arg_i32(offset: u64) -> wasm_encoder::MemArg {
wasm_encoder::MemArg {
offset,
align: 2, memory_index: 0,
}
}
fn mem_arg_byte(offset: u64) -> wasm_encoder::MemArg {
wasm_encoder::MemArg {
offset,
align: 0, memory_index: 0,
}
}
#[path = "emit_wasm/heap.rs"]
mod heap;
#[path = "emit_wasm/hof.rs"]
mod hof;
#[path = "emit_wasm/lists.rs"]
mod lists;
#[path = "emit_wasm/maps.rs"]
mod maps;
#[path = "emit_wasm/sets.rs"]
mod sets;
#[path = "emit_wasm/strings.rs"]
mod strings;
#[allow(unused_imports)]
pub(super) use heap::*; use hof::*;
use lists::*;
use maps::*;
use sets::*;
use strings::*;
pub fn emit_wasm(init_ns: &str, emit_path: &str) -> Result<(), String> {
let program_data = program::clone_compiled_program_snapshot()?;
let mut fn_defs: Vec<(String, String, CalcitFnArgs, Vec<Calcit>)> = Vec::new();
let mut ns_order: Vec<&str> = Vec::new();
if program_data.contains_key(init_ns) {
ns_order.push(init_ns);
}
for ns in program_data.keys() {
if ns.as_ref() != init_ns {
ns_order.push(ns);
}
}
for &ns in &ns_order {
let Some(file_info) = program_data.get(ns) else {
continue;
};
for (def_name, compiled) in &file_info.defs {
if compiled.kind != program::CompiledDefKind::Fn {
continue;
}
match extract_fn_parts(&compiled.preprocessed_code) {
Ok((args, body)) => {
fn_defs.push((ns.to_string(), def_name.to_string(), args, body));
}
Err(e) => {
eprintln!("[wasm] skipping {ns}/{def_name}: {e}");
}
}
}
}
if fn_defs.is_empty() {
return Err(format!("namespace not found or no functions: {init_ns}"));
}
let num_imports = HOST_IMPORTS.len() as u32;
let tag_index = collect_all_tags_from(&fn_defs);
println!("TAG INDEX: {tag_index:?}");
let (mut compiled_fns, mut runtime_fn_index) = build_runtime_fns(
num_imports,
*tag_index.get("map").expect("map tag must exist") as i32,
*tag_index.get("list").expect("list tag must exist") as i32,
*tag_index.get("string").expect("string tag must exist") as i32,
);
let str_tag_id = *tag_index.get("string").expect("string tag must exist") as i32;
let str_new_idx = num_imports + compiled_fns.len() as u32;
runtime_fn_index.insert("__str_new".to_string(), str_new_idx);
compiled_fns.push(build_str_new_fn(str_tag_id));
let str_pad_left_idx = num_imports + compiled_fns.len() as u32;
runtime_fn_index.insert("__rt_str_pad_left".to_string(), str_pad_left_idx);
compiled_fns.push(build_str_pad_left_fn(str_tag_id));
let str_pad_right_idx = num_imports + compiled_fns.len() as u32;
runtime_fn_index.insert("__rt_str_pad_right".to_string(), str_pad_right_idx);
compiled_fns.push(build_str_pad_right_fn(str_tag_id));
let runtime_fn_count = compiled_fns.len() as u32;
let mut export_name_counts: HashMap<String, usize> = HashMap::new();
for (_, name, _, _) in &fn_defs {
*export_name_counts.entry(name.clone()).or_insert(0) += 1;
}
let mut fn_index: HashMap<String, u32> = HashMap::new();
let mut fn_arity: HashMap<String, u32> = HashMap::new();
let mut fn_has_rest: HashMap<String, u32> = HashMap::new();
let mut fn_table_index: HashMap<String, u32> = HashMap::new();
for (i, (ns, name, args, _)) in fn_defs.iter().enumerate() {
let idx = num_imports + runtime_fn_count + i as u32;
let qualified = format!("{ns}/{name}");
fn_index.insert(qualified.clone(), idx);
fn_index.insert(name.clone(), idx);
let (arity, rest_fixed) = compute_fn_arity(args);
fn_arity.insert(qualified.clone(), arity);
fn_arity.insert(name.clone(), arity);
if let Some(fixed) = rest_fixed {
fn_has_rest.insert(qualified.clone(), fixed);
fn_has_rest.insert(name.clone(), fixed);
}
fn_table_index.insert(qualified, i as u32);
fn_table_index.insert(name.clone(), i as u32);
}
let record_field_tags = collect_record_field_tags_from_program(&program_data, &tag_index);
let (string_pool, string_data_segment, heap_start) = build_string_pool(&fn_defs, &tag_index);
let mut atom_initial_values: Vec<f64> = Vec::new();
let mut atom_globals: HashMap<String, u32> = HashMap::new();
let mut value_imports: HashMap<String, Calcit> = HashMap::new();
for &ns in &ns_order {
let Some(file_info) = program_data.get(ns) else {
continue;
};
for (def_name, compiled) in &file_info.defs {
let qualified = format!("{ns}/{def_name}");
if matches!(compiled.kind, program::CompiledDefKind::Value | program::CompiledDefKind::LazyValue) {
value_imports.insert(qualified.clone(), compiled.preprocessed_code.to_owned());
}
if let crate::calcit::Calcit::List(xs) = &compiled.preprocessed_code {
if matches!(
xs.first(),
Some(crate::calcit::Calcit::Syntax(crate::calcit::CalcitSyntax::Defatom, _))
) {
let global_idx = atom_initial_values.len() as u32;
atom_globals.insert(qualified, global_idx);
let init_val = match xs.get(2) {
Some(crate::calcit::Calcit::Bool(true)) => 1.0,
Some(crate::calcit::Calcit::Number(n)) => *n,
_ => 0.0, };
atom_initial_values.push(init_val);
}
}
}
}
let env = WasmCompileEnv {
fn_index,
fn_arity,
fn_has_rest,
runtime_fn_index,
tag_index,
record_field_tags,
string_pool,
atom_globals,
value_imports,
fn_table_index,
};
for (ns, def_name, args, body) in &fn_defs {
let export_name = if export_name_counts.get(def_name).copied().unwrap_or(0) > 1 {
format!("{ns}/{def_name}")
} else {
def_name.clone()
};
let result = try_custom_def_impl(ns, def_name, &export_name, args, &env)
.unwrap_or_else(|| compile_fn(def_name, &export_name, args, body, &env));
match result {
Ok(func) => compiled_fns.push(func),
Err(e) => {
eprintln!("[wasm] skipping {ns}/{def_name}: {e}");
let (arity, _) = compute_fn_arity(args);
compiled_fns.push(CompiledFn {
export_name: Some(export_name),
params: vec![ValType::F64; arity as usize],
results: vec![ValType::F64],
locals: vec![],
instructions: vec![f64_const(0.0)],
});
}
}
}
if compiled_fns.is_empty() {
return Err("no functions could be compiled to WASM".into());
}
let wasm_bytes = build_wasm_module(
&compiled_fns,
heap_start,
&string_data_segment,
&atom_initial_values,
runtime_fn_count,
)?;
let out_path = Path::new(emit_path);
if !out_path.exists() {
fs::create_dir_all(out_path).map_err(|e| format!("failed to create dir: {e}"))?;
}
let wasm_file = out_path.join("program.wasm");
fs::write(&wasm_file, &wasm_bytes).map_err(|e| format!("failed to write WASM: {e}"))?;
println!("wrote WASM to: {}", wasm_file.display());
Ok(())
}
struct CompiledFn {
export_name: Option<String>,
params: Vec<ValType>,
results: Vec<ValType>,
locals: Vec<ValType>,
instructions: Vec<Instruction<'static>>,
}
#[derive(Clone)]
struct WasmCompileEnv {
fn_index: HashMap<String, u32>,
fn_arity: HashMap<String, u32>,
fn_has_rest: HashMap<String, u32>,
runtime_fn_index: HashMap<String, u32>,
tag_index: HashMap<String, u32>,
record_field_tags: HashMap<u32, Vec<u32>>,
string_pool: HashMap<String, u32>,
atom_globals: HashMap<String, u32>,
value_imports: HashMap<String, Calcit>,
fn_table_index: HashMap<String, u32>,
}
fn extract_fn_parts(code: &Calcit) -> Result<(CalcitFnArgs, Vec<Calcit>), String> {
let Calcit::List(items) = code else {
return Err(format!("expected preprocessed defn list, got: {code}"));
};
match (items.first(), items.get(1), items.get(2)) {
(Some(Calcit::Syntax(CalcitSyntax::Defn, _)), Some(Calcit::Symbol { .. }), Some(Calcit::List(args))) => {
let raw_args = get_raw_args_fn(args)?;
Ok((raw_args, items.drop_left().drop_left().drop_left().to_vec()))
}
_ => Err(format!("expected preprocessed defn form, got: {code}")),
}
}
struct WasmGenCtx {
locals: HashMap<String, u32>,
extra_locals: Vec<ValType>,
next_local: u32,
uses_recur: bool,
arg_indices: Vec<u32>,
instructions: Vec<Instruction<'static>>,
fn_index: HashMap<String, u32>,
fn_arity: HashMap<String, u32>,
fn_has_rest: HashMap<String, u32>,
runtime_fn_index: HashMap<String, u32>,
tag_index: HashMap<String, u32>,
record_field_tags: HashMap<u32, Vec<u32>>,
block_depth: u32,
string_pool: HashMap<String, u32>,
atom_globals: HashMap<String, u32>,
value_imports: HashMap<String, Calcit>,
fn_table_index: HashMap<String, u32>,
lambda_locals: HashMap<String, (Vec<String>, Vec<Calcit>)>,
}
impl WasmGenCtx {
fn new(num_params: u32, env: WasmCompileEnv) -> Self {
WasmGenCtx {
locals: HashMap::new(),
extra_locals: Vec::new(),
next_local: num_params,
uses_recur: false,
arg_indices: Vec::new(),
instructions: Vec::new(),
fn_index: env.fn_index,
fn_arity: env.fn_arity,
fn_has_rest: env.fn_has_rest,
runtime_fn_index: env.runtime_fn_index,
tag_index: env.tag_index,
record_field_tags: env.record_field_tags,
block_depth: 0,
string_pool: env.string_pool,
atom_globals: env.atom_globals,
value_imports: env.value_imports,
fn_table_index: env.fn_table_index,
lambda_locals: HashMap::new(),
}
}
fn alloc_local(&mut self) -> u32 {
self.alloc_local_typed(ValType::F64)
}
fn alloc_local_typed(&mut self, vt: ValType) -> u32 {
let idx = self.next_local;
self.next_local += 1;
self.extra_locals.push(vt);
idx
}
fn declare_local(&mut self, name: &str) -> u32 {
let idx = self.alloc_local();
self.locals.insert(name.to_owned(), idx);
idx
}
fn emit(&mut self, instr: Instruction<'static>) {
self.instructions.push(instr);
}
pub(super) fn call_rt(&mut self, name: &str) {
let fn_idx = *self
.runtime_fn_index
.get(name)
.unwrap_or_else(|| panic!("runtime helper missing: {name}"));
self.emit(Instruction::Call(fn_idx));
}
pub(super) fn stub_proc(&mut self, args: &[Calcit]) -> Result<(), String> {
for arg in args {
emit_expr(self, arg)?;
self.emit(Instruction::Drop);
}
self.emit(f64_const(0.0));
Ok(())
}
pub(super) fn silent_nil(&mut self) -> Result<(), String> {
self.emit(f64_const(0.0));
Ok(())
}
pub(super) fn i32_inc(&mut self, local: u32) {
self.emit(Instruction::LocalGet(local));
self.emit(Instruction::I32Const(1));
self.emit(Instruction::I32Add);
self.emit(Instruction::LocalSet(local));
}
pub(super) fn i32_dec(&mut self, local: u32) {
self.emit(Instruction::LocalGet(local));
self.emit(Instruction::I32Const(1));
self.emit(Instruction::I32Sub);
self.emit(Instruction::LocalSet(local));
}
#[allow(dead_code)]
pub(super) fn i32_add_into(&mut self, a: u32, b: u32, dst: u32) {
self.emit(Instruction::LocalGet(a));
self.emit(Instruction::LocalGet(b));
self.emit(Instruction::I32Add);
self.emit(Instruction::LocalSet(dst));
}
#[allow(dead_code)]
pub(super) fn i32_sub_into(&mut self, a: u32, b: u32, dst: u32) {
self.emit(Instruction::LocalGet(a));
self.emit(Instruction::LocalGet(b));
self.emit(Instruction::I32Sub);
self.emit(Instruction::LocalSet(dst));
}
pub(super) fn begin_block(&mut self) {
self.emit(Instruction::Block(wasm_encoder::BlockType::Empty));
}
pub(super) fn begin_loop(&mut self) {
self.emit(Instruction::Loop(wasm_encoder::BlockType::Empty));
}
pub(super) fn begin_block_if(&mut self) {
self.emit(Instruction::If(wasm_encoder::BlockType::Empty));
}
#[allow(dead_code)]
pub(super) fn end_one(&mut self) {
self.emit(Instruction::End);
}
pub(super) fn end_block_loop(&mut self) {
self.emit(Instruction::End); self.emit(Instruction::End); }
pub(super) fn br_loop(&mut self) {
self.emit(Instruction::Br(0));
}
pub(super) fn br_if_exit(&mut self) {
self.emit(Instruction::BrIf(1));
}
pub(super) fn loop_exit_if_ge(&mut self, i: u32, count: u32) {
self.emit(Instruction::LocalGet(i));
self.emit(Instruction::LocalGet(count));
self.emit(Instruction::I32GeU);
self.br_if_exit();
}
pub(super) fn loop_exit_if_neg(&mut self, i: u32) {
self.emit(Instruction::LocalGet(i));
self.emit(Instruction::I32Const(0));
self.emit(Instruction::I32LtS);
self.br_if_exit();
}
pub(super) fn ptr_to_f64(&mut self, local: u32) {
self.emit(Instruction::LocalGet(local));
self.emit(Instruction::F64ConvertI32U);
}
pub(super) fn store_i32_as_f64(&mut self, ptr: u32, val: u32, byte_offset: u64) {
self.emit(Instruction::LocalGet(ptr));
self.emit(Instruction::LocalGet(val));
self.emit(Instruction::F64ConvertI32U);
self.emit(Instruction::F64Store(mem_arg_f64(byte_offset)));
}
pub(super) fn alloc_i32(&mut self, val: i32) -> u32 {
let local = self.alloc_local_typed(ValType::I32);
self.emit(Instruction::I32Const(val));
self.emit(Instruction::LocalSet(local));
local
}
pub(super) fn i32_offset(&mut self, src: u32, delta: i32) -> u32 {
let local = self.alloc_local_typed(ValType::I32);
self.emit(Instruction::LocalGet(src));
self.emit(Instruction::I32Const(delta.unsigned_abs() as i32));
if delta >= 0 {
self.emit(Instruction::I32Add);
} else {
self.emit(Instruction::I32Sub);
}
self.emit(Instruction::LocalSet(local));
local
}
}
#[inline]
fn expect_arity(n: usize, args: &[Calcit], proc_name: &str) -> Result<(), String> {
if args.len() != n {
return Err(format!("{proc_name} expects {n} arg(s), got {}", args.len()));
}
Ok(())
}
fn compute_fn_arity(args: &CalcitFnArgs) -> (u32, Option<u32>) {
match args {
CalcitFnArgs::Args(v) => (v.len() as u32, None),
CalcitFnArgs::MarkedArgs(labels) => {
let mut fixed: u32 = 0;
let mut rest_param_count: u32 = 0;
let mut rest_seen = false;
for label in labels {
match label {
CalcitArgLabel::Idx(_) => {
if rest_seen {
rest_param_count += 1;
} else {
fixed += 1;
}
}
CalcitArgLabel::OptionalMark => {}
CalcitArgLabel::RestMark => {
rest_seen = true;
}
}
}
if rest_seen && rest_param_count > 0 {
(fixed + 1, Some(fixed))
} else {
(fixed, None)
}
}
}
}
fn try_custom_def_impl(
ns: &str,
def_name: &str,
export_name: &str,
_args: &CalcitFnArgs,
env: &WasmCompileEnv,
) -> Option<Result<CompiledFn, String>> {
if ns != "calcit.core" {
return None;
}
type BodyFn = fn(&mut WasmGenCtx) -> Result<(), String>;
let arity: u32 = 2;
let build = |body_fn: BodyFn, en: &str, env: &WasmCompileEnv| -> Result<CompiledFn, String> {
let mut ctx = WasmGenCtx::new(arity, env.clone());
ctx.locals.insert("__p0__".into(), 0);
ctx.locals.insert("__p1__".into(), 1);
ctx.arg_indices.push(0);
ctx.arg_indices.push(1);
body_fn(&mut ctx)?;
Ok(CompiledFn {
export_name: Some(en.to_owned()),
params: vec![ValType::F64; arity as usize],
results: vec![ValType::F64],
locals: ctx.extra_locals,
instructions: ctx.instructions,
})
};
match def_name {
"repeat" => Some(build(|ctx| emit_repeat_from_locals(ctx, 0, 1), export_name, env)),
"interleave" => Some(build(|ctx| emit_interleave_from_locals(ctx, 0, 1), export_name, env)),
"zipmap" => Some(build(|ctx| emit_zipmap_from_locals(ctx, 0, 1), export_name, env)),
"join" => Some(build(|ctx| emit_join_from_locals(ctx, 0, 1), export_name, env)),
"join-str" => Some(build(|ctx| emit_join_str_from_locals(ctx, 0, 1), export_name, env)),
_ => None,
}
}
fn compile_fn(
_name: &str,
export_name: &str,
args: &CalcitFnArgs,
body: &[Calcit],
env: &WasmCompileEnv,
) -> Result<CompiledFn, String> {
let mut param_names = Vec::new();
match args {
CalcitFnArgs::Args(idxs) => {
for idx in idxs {
param_names.push(CalcitLocal::read_name(*idx));
}
}
CalcitFnArgs::MarkedArgs(labels) => {
let mut seen_rest = false;
for label in labels {
match label {
CalcitArgLabel::Idx(idx) => {
param_names.push(CalcitLocal::read_name(*idx));
if seen_rest {
seen_rest = false;
}
}
CalcitArgLabel::OptionalMark => {
}
CalcitArgLabel::RestMark => {
seen_rest = true;
}
}
}
}
}
let arity = param_names.len();
let mut ctx = WasmGenCtx::new(arity as u32, env.clone());
for (i, pname) in param_names.iter().enumerate() {
ctx.locals.insert(pname.clone(), i as u32);
ctx.arg_indices.push(i as u32);
}
ctx.uses_recur = body.iter().any(check_uses_recur);
if ctx.uses_recur {
ctx.emit(Instruction::Loop(wasm_encoder::BlockType::Result(ValType::F64)));
emit_body(&mut ctx, body)?;
ctx.emit(Instruction::End); } else {
emit_body(&mut ctx, body)?;
}
Ok(CompiledFn {
export_name: Some(export_name.to_owned()),
params: vec![ValType::F64; arity],
results: vec![ValType::F64],
locals: ctx.extra_locals,
instructions: ctx.instructions,
})
}
fn check_uses_recur(expr: &Calcit) -> bool {
match expr {
Calcit::Proc(CalcitProc::Recur) => true,
Calcit::List(xs) => {
if let Some(Calcit::Syntax(CalcitSyntax::Defn, _)) = xs.first() {
return false;
}
xs.iter().any(check_uses_recur)
}
_ => false,
}
}
fn emit_body(ctx: &mut WasmGenCtx, exprs: &[Calcit]) -> Result<(), String> {
if exprs.is_empty() {
ctx.emit(f64_const(0.0));
return Ok(());
}
for (i, expr) in exprs.iter().enumerate() {
emit_expr(ctx, expr)?;
if i < exprs.len() - 1 {
ctx.emit(Instruction::Drop);
}
}
Ok(())
}
fn emit_inline_iife(ctx: &mut WasmGenCtx, params: &[String], body: &[Calcit], init_args: &[Calcit]) -> Result<(), String> {
let mut param_locals: Vec<u32> = Vec::new();
for (i, _param_name) in params.iter().enumerate() {
let tmp = ctx.alloc_local();
if i < init_args.len() {
emit_expr(ctx, &init_args[i])?;
} else {
ctx.emit(f64_const(0.0)); }
ctx.emit(Instruction::LocalSet(tmp));
param_locals.push(tmp);
}
let mut saved: Vec<(String, Option<u32>)> = Vec::new();
for (i, name) in params.iter().enumerate() {
saved.push((name.clone(), ctx.locals.get(name).copied()));
ctx.locals.insert(name.clone(), param_locals[i]);
}
let uses_recur = body.iter().any(check_uses_recur);
if uses_recur {
let old_arg_indices = std::mem::replace(&mut ctx.arg_indices, param_locals);
let old_block_depth = ctx.block_depth;
ctx.block_depth = 0;
ctx.emit(Instruction::Loop(wasm_encoder::BlockType::Result(ValType::F64)));
emit_body(ctx, body)?;
ctx.emit(Instruction::End);
ctx.block_depth = old_block_depth;
ctx.arg_indices = old_arg_indices;
} else {
emit_body(ctx, body)?;
}
for (name, old) in saved {
match old {
Some(idx) => {
ctx.locals.insert(name, idx);
}
None => {
ctx.locals.remove(&name);
}
}
}
Ok(())
}
fn emit_expr(ctx: &mut WasmGenCtx, expr: &Calcit) -> Result<(), String> {
match expr {
Calcit::Number(n) => {
ctx.emit(f64_const(*n));
}
Calcit::Bool(true) => {
ctx.emit(f64_const(1.0));
}
Calcit::Bool(false) | Calcit::Nil => {
ctx.emit(f64_const(0.0));
}
Calcit::List(xs) if xs.is_empty() => {
emit_list_new(ctx, &[])?;
}
Calcit::Tag(t) => {
let tag_str = t.to_string();
let id = *ctx
.tag_index
.get(&tag_str)
.ok_or_else(|| format!("unknown tag in WASM codegen: {tag_str}"))?;
ctx.emit(f64_const(id as f64));
}
Calcit::Struct(s) => {
let tag_str = s.name.to_string();
let id = *ctx
.tag_index
.get(&tag_str)
.ok_or_else(|| format!("unknown struct tag in WASM codegen: {tag_str}"))?;
ctx.emit(f64_const(id as f64));
}
Calcit::Local(local) => {
let name = &*local.sym;
let idx = *ctx.locals.get(name).ok_or_else(|| format!("undefined local variable: {name}"))?;
ctx.emit(Instruction::LocalGet(idx));
}
Calcit::List(xs) if !xs.is_empty() => {
emit_call_expr(ctx, xs)?;
}
Calcit::Import(import) if import.def.as_ref() == "do" => {
ctx.emit(f64_const(0.0));
}
Calcit::Import(import) => {
let qualified = format!("{}/{}", import.ns, import.def);
if let Some(&global_idx) = ctx.atom_globals.get(&qualified) {
ctx.emit(Instruction::GlobalGet(global_idx));
} else if import.def.as_ref() == "{}" {
emit_map_new(ctx, &[])?;
} else if import.def.as_ref() == "[]" {
emit_list_new(ctx, &[])?;
} else if let Ok(struct_def) = resolve_struct_ref(expr) {
let tag_str = struct_def.name.to_string();
let id = *ctx
.tag_index
.get(&tag_str)
.ok_or_else(|| format!("unknown struct tag in WASM codegen: {tag_str}"))?;
ctx.emit(f64_const(id as f64));
} else if let Some(&slot) = ctx
.fn_table_index
.get(&qualified)
.or_else(|| ctx.fn_table_index.get(import.def.as_ref()))
{
ctx.emit(f64_const(slot as f64));
} else if let Some(value_expr) = ctx.value_imports.get(&qualified).cloned() {
emit_expr(ctx, &value_expr)?;
} else {
ctx.emit(f64_const(0.0));
}
}
Calcit::Str(s) => {
let ptr = ctx
.string_pool
.get(s.as_ref())
.ok_or_else(|| format!("string literal not found in pool: {s}"))?;
ctx.emit(f64_const(*ptr as f64));
}
Calcit::Record(_) => return Err("Record literals not supported in WASM codegen (use constructor)".into()),
Calcit::Tuple(_) => return Err("Tuple literals not supported in WASM codegen (use constructor)".into()),
Calcit::Fn { info, .. } => {
if let Some(def_ref) = &info.def_ref {
let qualified = format!("{}/{}", def_ref.def_ns, def_ref.def_name);
let slot = ctx
.fn_table_index
.get(&qualified)
.or_else(|| ctx.fn_table_index.get(def_ref.def_name.as_ref()))
.copied()
.ok_or_else(|| format!("fn value not in table: {qualified}"))?;
ctx.emit(f64_const(slot as f64));
} else {
eprintln!("[wasm] anonymous closure (no def_ref): emitting nil placeholder");
ctx.emit(f64_const(0.0));
}
}
Calcit::Proc(CalcitProc::List) => {
emit_list_new(ctx, &[])?;
}
Calcit::Proc(CalcitProc::NativeMap) => {
emit_map_new(ctx, &[])?;
}
_ => return Err(format!("unsupported WASM expression: {expr}")),
}
Ok(())
}
fn emit_call_expr(ctx: &mut WasmGenCtx, xs: &crate::calcit::CalcitList) -> Result<(), String> {
let head = &xs[0];
let args_list: Vec<Calcit> = xs.drop_left().to_vec();
match head {
Calcit::Syntax(syn, _) => match syn {
CalcitSyntax::CallSpread => emit_call_spread(ctx, &args_list),
CalcitSyntax::If => emit_if(ctx, &args_list),
CalcitSyntax::CoreLet => emit_let(ctx, &args_list),
CalcitSyntax::Match => emit_match(ctx, &args_list),
CalcitSyntax::HintFn => {
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitSyntax::AssertType => {
if args_list.is_empty() {
return Err("assert-type expects at least 1 arg".into());
}
emit_expr(ctx, &args_list[0])
}
CalcitSyntax::Defn => {
eprintln!("[wasm] closure-as-value: emitting nil placeholder for nested fn/defn");
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitSyntax::Quote | CalcitSyntax::Quasiquote => {
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitSyntax::Reset => {
if args_list.len() != 2 {
return Err(format!("reset! expects 2 args, got {}", args_list.len()));
}
let qualified = match &args_list[0] {
Calcit::Import(import) => format!("{}/{}", import.ns, import.def),
_ => return Err(format!("reset! first arg must be an atom import, got: {}", args_list[0])),
};
let global_idx = *ctx
.atom_globals
.get(&qualified)
.ok_or_else(|| format!("unknown atom in reset!: {qualified}"))?;
emit_expr(ctx, &args_list[1])?;
let tmp = ctx.alloc_local();
ctx.emit(Instruction::LocalTee(tmp));
ctx.emit(Instruction::GlobalSet(global_idx));
ctx.emit(Instruction::LocalGet(tmp));
Ok(())
}
_ => Err(format!("unsupported syntax in WASM: {syn}")),
},
Calcit::Proc(proc) => emit_proc_call(ctx, proc, &args_list),
Calcit::Method(name, kind) => match kind {
MethodKind::Invoke(_) => emit_method_invoke(ctx, name.as_ref(), &args_list),
_ => Err(format!("unsupported method in WASM: .{name}")),
},
Calcit::Import(import) => {
if import.def.as_ref() == "do" {
return emit_body(ctx, &args_list);
}
if import.ns.as_ref() == "calcit.core" {
match import.def.as_ref() {
"union" => return emit_set_op_variadic(ctx, &args_list, SetOpKind::Union),
"difference" => return emit_set_op_variadic(ctx, &args_list, SetOpKind::Difference),
"include" => return emit_set_op_variadic(ctx, &args_list, SetOpKind::Include),
"reduce" if args_list.len() == 3 => return emit_foldl(ctx, &args_list),
"foldl-compare" if args_list.len() == 3 => return emit_foldl_compare(ctx, &args_list),
"map" | "&list:map" if args_list.len() == 2 => return emit_map(ctx, &args_list),
"map-indexed" | "&list:map-indexed" if args_list.len() == 2 => return emit_map_indexed(ctx, &args_list),
"each" if args_list.len() == 2 => return emit_each(ctx, &args_list),
"filter" | "&list:filter" | "&set:filter" if args_list.len() == 2 => return emit_filter(ctx, &args_list),
"any?" if args_list.len() == 2 => return emit_any(ctx, &args_list),
"every?" if args_list.len() == 2 => return emit_every(ctx, &args_list),
"find" if args_list.len() == 2 => return emit_find(ctx, &args_list),
"find-index" if args_list.len() == 2 => return emit_find_index(ctx, &args_list),
"concat" => return emit_list_concat(ctx, &args_list),
"deref" if args_list.len() == 1 => return emit_expr(ctx, &args_list[0]),
"str" if !args_list.is_empty() => return emit_str_variadic(ctx, &args_list),
"str-spaced" if !args_list.is_empty() => return emit_str_spaced(ctx, &args_list),
"foldl'" if args_list.len() == 3 => return emit_foldl(ctx, &args_list),
"filter-not" if args_list.len() == 2 => return emit_filter_not(ctx, &args_list),
"slice" if args_list.len() >= 2 && args_list.len() <= 3 => return emit_list_slice(ctx, &args_list),
"dissoc" if args_list.len() == 2 => return emit_map_dissoc(ctx, &args_list),
"conj" if args_list.len() >= 2 => return emit_conj(ctx, &args_list),
"update" if args_list.len() == 3 => return emit_update(ctx, &args_list),
"mapcat" if args_list.len() == 2 => return emit_mapcat(ctx, &args_list),
"repeat" if args_list.len() == 2 => return emit_repeat(ctx, &args_list),
"interleave" if args_list.len() == 2 => return emit_interleave(ctx, &args_list),
"zipmap" if args_list.len() == 2 => return emit_zipmap(ctx, &args_list),
"join" if args_list.len() == 2 => return emit_join(ctx, &args_list),
"join-str" if args_list.len() == 2 => return emit_join_str(ctx, &args_list),
"let" if !args_list.is_empty() => return emit_let_multi(ctx, &args_list),
"map-kv" if args_list.len() == 2 => return emit_map_kv(ctx, &args_list),
"'" => return emit_list_new(ctx, &args_list),
_ => {}
}
}
let qualified = format!("{}/{}", import.ns, import.def);
let fn_idx = ctx
.fn_index
.get(&qualified)
.or_else(|| ctx.fn_index.get(import.def.as_ref()))
.ok_or_else(|| format!("unknown function: {qualified}"))?;
let fn_idx = *fn_idx;
let target_arity = ctx
.fn_arity
.get(&qualified)
.or_else(|| ctx.fn_arity.get(import.def.as_ref()))
.copied()
.unwrap_or(args_list.len() as u32);
let rest_fixed = ctx
.fn_has_rest
.get(&qualified)
.or_else(|| ctx.fn_has_rest.get(import.def.as_ref()))
.copied();
emit_call_args(ctx, &args_list, target_arity, rest_fixed)?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
Calcit::Symbol { sym, .. } => {
let name = sym.as_ref();
if matches!(name, "println" | "eprintln" | "echo") {
let log_idx = HOST_IMPORTS
.iter()
.position(|imp| imp.module == "io" && imp.name == "log_value")
.expect("log_value host import") as u32;
for arg in &args_list {
emit_expr(ctx, arg)?;
ctx.emit(Instruction::Call(log_idx));
ctx.emit(Instruction::Drop); }
ctx.emit(f64_const(0.0)); return Ok(());
}
match name {
"map" if args_list.len() == 2 => return emit_map(ctx, &args_list),
"filter" | "&list:filter" | "&set:filter" if args_list.len() == 2 => return emit_filter(ctx, &args_list),
"filter-not" if args_list.len() == 2 => return emit_filter_not(ctx, &args_list),
"each" if args_list.len() == 2 => return emit_each(ctx, &args_list),
"any?" if args_list.len() == 2 => return emit_any(ctx, &args_list),
"every?" if args_list.len() == 2 => return emit_every(ctx, &args_list),
"find" if args_list.len() == 2 => return emit_find(ctx, &args_list),
"find-index" if args_list.len() == 2 => return emit_find_index(ctx, &args_list),
"map-indexed" if args_list.len() == 2 => return emit_map_indexed(ctx, &args_list),
"mapcat" if args_list.len() == 2 => return emit_mapcat(ctx, &args_list),
"reduce" if args_list.len() == 3 => return emit_foldl(ctx, &args_list),
"foldl'" if args_list.len() == 3 => return emit_foldl(ctx, &args_list),
"update" if args_list.len() == 3 => return emit_update(ctx, &args_list),
_ => {}
}
if let Some((params, body)) = ctx.lambda_locals.get(name).cloned() {
if params.len() == args_list.len() {
for (param, arg) in params.iter().zip(args_list.iter()) {
let arg_lambda = match arg {
Calcit::Local(a) => ctx.lambda_locals.get(a.sym.as_ref()).cloned(),
Calcit::Symbol { sym: s, .. } => ctx.lambda_locals.get(s.as_ref()).cloned(),
_ => None,
};
if let Some(captured) = arg_lambda {
ctx.lambda_locals.insert(param.clone(), captured);
} else {
emit_expr(ctx, arg)?;
let idx = ctx.declare_local(param);
ctx.emit(Instruction::LocalSet(idx));
}
}
return emit_body(ctx, &body);
}
}
let fn_idx = *ctx.fn_index.get(name).ok_or_else(|| format!("unknown function: {sym}"))?;
let target_arity = ctx.fn_arity.get(name).copied().unwrap_or(args_list.len() as u32);
let rest_fixed = ctx.fn_has_rest.get(name).copied();
emit_call_args(ctx, &args_list, target_arity, rest_fixed)?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
Calcit::Registered(name) => {
let name = name.as_ref();
if matches!(name, "println" | "eprintln" | "echo") {
let log_idx = HOST_IMPORTS
.iter()
.position(|imp| imp.module == "io" && imp.name == "log_value")
.expect("log_value host import") as u32;
for arg in &args_list {
emit_expr(ctx, arg)?;
ctx.emit(Instruction::Call(log_idx));
ctx.emit(Instruction::Drop);
}
ctx.emit(f64_const(0.0)); return Ok(());
}
Err(format!("unsupported registered proc in WASM: {name}"))
}
Calcit::Fn { info, .. } => {
let def_ref = info.def_ref.as_ref().ok_or_else(|| {
format!(
"function literal without def reference is not supported in WASM: {}/{}",
info.def_ns, info.name
)
})?;
if def_ref.def_ns.as_ref() == "calcit.core" {
match def_ref.def_name.as_ref() {
"map" if args_list.len() == 2 => return emit_map(ctx, &args_list),
"filter" | "&list:filter" | "&set:filter" if args_list.len() == 2 => return emit_filter(ctx, &args_list),
"filter-not" if args_list.len() == 2 => return emit_filter_not(ctx, &args_list),
"each" if args_list.len() == 2 => return emit_each(ctx, &args_list),
"any?" if args_list.len() == 2 => return emit_any(ctx, &args_list),
"every?" if args_list.len() == 2 => return emit_every(ctx, &args_list),
"find" if args_list.len() == 2 => return emit_find(ctx, &args_list),
"find-index" if args_list.len() == 2 => return emit_find_index(ctx, &args_list),
"map-indexed" if args_list.len() == 2 => return emit_map_indexed(ctx, &args_list),
"mapcat" if args_list.len() == 2 => return emit_mapcat(ctx, &args_list),
"reduce" if args_list.len() == 3 => return emit_foldl(ctx, &args_list),
"foldl'" if args_list.len() == 3 => return emit_foldl(ctx, &args_list),
"update" if args_list.len() == 3 => return emit_update(ctx, &args_list),
_ => {}
}
}
let qualified = format!("{}/{}", def_ref.def_ns, def_ref.def_name);
let fn_idx = ctx
.fn_index
.get(&qualified)
.or_else(|| ctx.fn_index.get(def_ref.def_name.as_ref()))
.copied()
.ok_or_else(|| format!("unknown function literal target in WASM: {qualified}"))?;
let target_arity = ctx
.fn_arity
.get(&qualified)
.or_else(|| ctx.fn_arity.get(def_ref.def_name.as_ref()))
.copied()
.unwrap_or(args_list.len() as u32);
let rest_fixed = ctx
.fn_has_rest
.get(&qualified)
.or_else(|| ctx.fn_has_rest.get(def_ref.def_name.as_ref()))
.copied();
emit_call_args(ctx, &args_list, target_arity, rest_fixed)?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
Calcit::List(iife_items) if matches!(iife_items.first(), Some(Calcit::Syntax(CalcitSyntax::Defn, _))) => {
let params = match iife_items.get(2) {
Some(Calcit::List(param_list)) => param_list
.iter()
.filter_map(|p| match p {
Calcit::Local(CalcitLocal { sym, .. }) => Some(sym.as_ref().to_owned()),
Calcit::Symbol { sym, .. } => Some(sym.as_ref().to_owned()),
_ => None,
})
.collect::<Vec<_>>(),
other => return Err(format!("IIFE defn: params list expected, got: {other:?}")),
};
let body: Vec<Calcit> = iife_items.iter().skip(3).cloned().collect();
emit_inline_iife(ctx, ¶ms, &body, &args_list)
}
Calcit::Local(local) => {
let local_name = local.sym.as_ref().to_string();
if let Some((params, body)) = ctx.lambda_locals.get(&local_name).cloned() {
if params.len() == args_list.len() {
for (param, arg) in params.iter().zip(args_list.iter()) {
let arg_lambda = match arg {
Calcit::Local(a) => ctx.lambda_locals.get(a.sym.as_ref()).cloned(),
Calcit::Symbol { sym, .. } => ctx.lambda_locals.get(sym.as_ref()).cloned(),
_ => None,
};
if let Some(captured) = arg_lambda {
ctx.lambda_locals.insert(param.clone(), captured);
} else {
emit_expr(ctx, arg)?;
let idx = ctx.declare_local(param);
ctx.emit(Instruction::LocalSet(idx));
}
}
return emit_body(ctx, &body);
}
}
let local_idx = *ctx
.locals
.get(&local_name)
.ok_or_else(|| format!("undefined local used as function: {}", local.sym))?;
for arg in &args_list {
emit_expr(ctx, arg)?;
}
ctx.emit(Instruction::LocalGet(local_idx));
ctx.emit(Instruction::I32TruncF64S);
ctx.emit(Instruction::CallIndirect {
type_index: args_list.len() as u32,
table_index: 0,
});
Ok(())
}
_ => Err(format!("unsupported call head in WASM: {head}")),
}
}
fn emit_call_spread(ctx: &mut WasmGenCtx, args_list: &[Calcit]) -> Result<(), String> {
if args_list.is_empty() {
return Err("&call-spread expects at least a callee".into());
}
let head = &args_list[0];
let call_args = &args_list[1..];
match head {
Calcit::Import(import) => {
let qualified = format!("{}/{}", import.ns, import.def);
let fn_idx = ctx
.fn_index
.get(&qualified)
.or_else(|| ctx.fn_index.get(import.def.as_ref()))
.copied()
.ok_or_else(|| format!("unknown function: {qualified}"))?;
let target_arity = ctx
.fn_arity
.get(&qualified)
.or_else(|| ctx.fn_arity.get(import.def.as_ref()))
.copied()
.unwrap_or(call_args.len() as u32);
let rest_fixed = ctx
.fn_has_rest
.get(&qualified)
.or_else(|| ctx.fn_has_rest.get(import.def.as_ref()))
.copied();
emit_call_spread_args(ctx, call_args, target_arity, rest_fixed)?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
Calcit::Symbol { sym, .. } => {
let name = sym.as_ref();
let fn_idx = *ctx.fn_index.get(name).ok_or_else(|| format!("unknown function: {sym}"))?;
let target_arity = ctx.fn_arity.get(name).copied().unwrap_or(call_args.len() as u32);
let rest_fixed = ctx.fn_has_rest.get(name).copied();
emit_call_spread_args(ctx, call_args, target_arity, rest_fixed)?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
Calcit::Fn { info, .. } => {
let def_ref = info.def_ref.as_ref().ok_or_else(|| {
format!(
"function literal without def reference is not supported in WASM: {}/{}",
info.def_ns, info.name
)
})?;
let qualified = format!("{}/{}", def_ref.def_ns, def_ref.def_name);
let fn_idx = ctx
.fn_index
.get(&qualified)
.or_else(|| ctx.fn_index.get(def_ref.def_name.as_ref()))
.copied()
.ok_or_else(|| format!("unknown function literal target in WASM: {qualified}"))?;
let target_arity = ctx
.fn_arity
.get(&qualified)
.or_else(|| ctx.fn_arity.get(def_ref.def_name.as_ref()))
.copied()
.unwrap_or(call_args.len() as u32);
let rest_fixed = ctx
.fn_has_rest
.get(&qualified)
.or_else(|| ctx.fn_has_rest.get(def_ref.def_name.as_ref()))
.copied();
emit_call_spread_args(ctx, call_args, target_arity, rest_fixed)?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
Calcit::Proc(proc) => {
if matches!(proc, CalcitProc::Recur | CalcitProc::NativeListDissoc | CalcitProc::NativeMapDissoc) {
emit_proc_call(ctx, proc, call_args)
} else {
emit_call_spread_args_as_regular(ctx, proc, call_args)
}
}
Calcit::Method(_name, MethodKind::Invoke(_)) => {
ctx.emit(Instruction::Unreachable);
Ok(())
}
Calcit::Local(_) => {
ctx.emit(Instruction::Unreachable);
Ok(())
}
_ => Err(format!("unsupported call head in WASM: {head}")),
}
}
fn emit_call_spread_args_as_regular(ctx: &mut WasmGenCtx, proc: &CalcitProc, call_args: &[Calcit]) -> Result<(), String> {
let mut real_args: Vec<Calcit> = vec![];
let mut i = 0;
while i < call_args.len() {
if matches!(call_args[i], Calcit::Syntax(CalcitSyntax::ArgSpread, _)) {
i += 1; } else {
real_args.push(call_args[i].clone());
}
i += 1;
}
emit_proc_call(ctx, proc, &real_args)
}
fn emit_call_spread_args(ctx: &mut WasmGenCtx, call_args: &[Calcit], target_arity: u32, rest_fixed: Option<u32>) -> Result<(), String> {
let Some(fixed) = rest_fixed else {
return Err("&call-spread in WASM currently requires the target function to accept rest args".into());
};
let fixed = fixed as usize;
let spread_pos = call_args
.iter()
.position(|a| matches!(a, Calcit::Syntax(CalcitSyntax::ArgSpread, _)));
let (explicit_args, spread_list_opt): (&[Calcit], Option<&Calcit>) = if let Some(pos) = spread_pos {
(&call_args[..pos], call_args.get(pos + 1))
} else if call_args.len() == fixed + 1 {
(&call_args[..fixed], Some(&call_args[fixed]))
} else {
return Err(format!(
"&call-spread in WASM expects {} fixed args plus `& spread-list`, got {} args",
fixed,
call_args.len()
));
};
let Some(spread_list_expr) = spread_list_opt else {
return Err("&call-spread missing spread list expression".into());
};
let n_explicit = explicit_args.len();
let n_from_spread = fixed.saturating_sub(n_explicit);
for arg in explicit_args {
emit_expr(ctx, arg)?;
}
if n_from_spread == 0 {
emit_expr(ctx, spread_list_expr)?;
} else {
emit_expr(ctx, spread_list_expr)?;
let spread_f64 = ctx.alloc_local();
ctx.emit(Instruction::LocalSet(spread_f64));
let spread_i32 = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(spread_f64));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(spread_i32));
for i in 0..n_from_spread {
ctx.emit(Instruction::LocalGet(spread_i32));
ctx.emit(Instruction::I32Const(((1 + i) * 8) as i32));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
}
emit_list_slice_from_i32_local(ctx, spread_i32, n_from_spread)?;
}
let emitted_args = fixed + 1;
for _ in emitted_args..(target_arity as usize) {
ctx.emit(f64_const(0.0));
}
Ok(())
}
fn emit_list_slice_from_i32_local(ctx: &mut WasmGenCtx, src_i32: u32, from_idx: usize) -> Result<(), String> {
let old_count = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(src_i32));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(old_count));
let new_count = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(old_count));
ctx.emit(Instruction::I32Const(from_idx as i32));
ctx.emit(Instruction::I32Sub);
ctx.emit(Instruction::LocalSet(new_count));
let total_slots = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(new_count));
ctx.emit(Instruction::I32Const(1));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalSet(total_slots));
let dst = emit_alloc_with_count(ctx, new_count, total_slots, "list");
let src_base = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(src_i32));
ctx.emit(Instruction::I32Const((8 + from_idx * 8) as i32));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalSet(src_base));
let dst_base = emit_addr_offset(ctx, dst, 8);
emit_copy_f64_loop(ctx, dst_base, src_base, new_count);
ctx.emit(Instruction::LocalGet(dst));
ctx.emit(Instruction::F64ConvertI32U);
Ok(())
}
fn emit_proc_call(ctx: &mut WasmGenCtx, proc: &CalcitProc, args: &[Calcit]) -> Result<(), String> {
match proc {
CalcitProc::NativeAdd => emit_binary(ctx, Instruction::F64Add, args),
CalcitProc::NativeMinus => emit_binary(ctx, Instruction::F64Sub, args),
CalcitProc::NativeMultiply => emit_binary(ctx, Instruction::F64Mul, args),
CalcitProc::NativeDivide => emit_binary(ctx, Instruction::F64Div, args),
CalcitProc::NativeNumberRem => {
if args.len() != 2 {
return Err("rem expects 2 args".into());
}
emit_expr(ctx, &args[0])?; emit_expr(ctx, &args[0])?; emit_expr(ctx, &args[1])?; ctx.emit(Instruction::F64Div);
ctx.emit(Instruction::F64Trunc);
emit_expr(ctx, &args[1])?; ctx.emit(Instruction::F64Mul);
ctx.emit(Instruction::F64Sub);
Ok(())
}
CalcitProc::NativeLessThan => emit_cmp(ctx, Instruction::F64Lt, args),
CalcitProc::NativeGreaterThan => emit_cmp(ctx, Instruction::F64Gt, args),
CalcitProc::NativeEquals => emit_equals(ctx, args),
CalcitProc::Identical => emit_cmp(ctx, Instruction::F64Eq, args),
CalcitProc::NativeCompare => {
expect_arity(2, args, "&compare")?;
let a = ctx.alloc_local();
let b = ctx.alloc_local();
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::LocalSet(a));
emit_expr(ctx, &args[1])?;
ctx.emit(Instruction::LocalSet(b));
ctx.emit(Instruction::LocalGet(a));
ctx.emit(Instruction::LocalGet(b));
ctx.emit(Instruction::F64Lt);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.emit(f64_const(-1.0));
ctx.emit(Instruction::Else);
ctx.emit(Instruction::LocalGet(a));
ctx.emit(Instruction::LocalGet(b));
ctx.emit(Instruction::F64Gt);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.emit(f64_const(1.0));
ctx.emit(Instruction::Else);
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::End);
ctx.emit(Instruction::End);
Ok(())
}
CalcitProc::Not => {
expect_arity(1, args, "not")?;
ctx.emit(f64_const(1.0)); ctx.emit(f64_const(0.0)); emit_expr(ctx, &args[0])?;
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq); ctx.emit(Instruction::Select);
Ok(())
}
CalcitProc::Floor => emit_unary(ctx, Instruction::F64Floor, args),
CalcitProc::Ceil => emit_unary(ctx, Instruction::F64Ceil, args),
CalcitProc::Round => emit_unary(ctx, Instruction::F64Nearest, args),
CalcitProc::Sqrt => emit_unary(ctx, Instruction::F64Sqrt, args),
CalcitProc::IsRound => {
expect_arity(1, args, "round?")?;
let v = ctx.alloc_local();
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::LocalSet(v));
ctx.emit(Instruction::LocalGet(v));
ctx.emit(Instruction::F64Floor);
ctx.emit(Instruction::LocalGet(v));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::F64ConvertI32U);
Ok(())
}
CalcitProc::NativeNumberFract => {
expect_arity(1, args, "&number:fract")?;
let v = ctx.alloc_local();
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::LocalSet(v));
ctx.emit(Instruction::LocalGet(v));
ctx.emit(Instruction::LocalGet(v));
ctx.emit(Instruction::F64Floor);
ctx.emit(Instruction::F64Sub);
Ok(())
}
CalcitProc::Sin => emit_host_call(ctx, "sin", args),
CalcitProc::Cos => emit_host_call(ctx, "cos", args),
CalcitProc::Pow => emit_host_call(ctx, "pow", args),
CalcitProc::TypeOf => emit_type_of(ctx, args),
CalcitProc::ListQuestion => emit_type_predicate(ctx, "list", args),
CalcitProc::TagQuestion => emit_type_predicate(ctx, "tag", args),
CalcitProc::SymbolQuestion => emit_type_predicate(ctx, "symbol", args),
CalcitProc::NilQuestion => {
expect_arity(1, args, "nil?")?;
ctx.emit(f64_const(1.0));
ctx.emit(f64_const(0.0));
emit_expr(ctx, &args[0])?;
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::Select);
Ok(())
}
CalcitProc::StringQuestion => emit_type_predicate(ctx, "string", args),
CalcitProc::MapQuestion => emit_type_predicate(ctx, "map", args),
CalcitProc::NumberQuestion => emit_type_predicate(ctx, "number", args),
CalcitProc::BoolQuestion => emit_type_predicate(ctx, "bool", args),
CalcitProc::SetQuestion => emit_type_predicate(ctx, "set", args),
CalcitProc::TupleQuestion => emit_type_predicate(ctx, "tuple", args),
CalcitProc::RecordQuestion => emit_type_predicate(ctx, "record", args),
CalcitProc::FnQuestion => emit_type_predicate(ctx, "fn", args),
CalcitProc::Recur => {
let spread_pos = args.iter().position(|a| matches!(a, Calcit::Syntax(CalcitSyntax::ArgSpread, _)));
if let Some(pos) = spread_pos {
let explicit = &args[..pos];
let rest_expr = args.get(pos + 1).ok_or("recur spread: missing list after &")?;
let total = ctx.arg_indices.len();
let n_explicit = explicit.len();
if n_explicit >= total {
return Err(format!("recur spread: too many explicit args ({n_explicit} >= {total})"));
}
let n_remaining = total - n_explicit;
emit_expr(ctx, rest_expr)?;
let spread_f64 = ctx.alloc_local();
ctx.emit(Instruction::LocalSet(spread_f64));
let spread_i32 = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(spread_f64));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(spread_i32));
let mut temps = Vec::new();
for arg in explicit {
let tmp = ctx.alloc_local();
emit_expr(ctx, arg)?;
ctx.emit(Instruction::LocalSet(tmp));
temps.push(tmp);
}
for i in 0..(n_remaining - 1) {
let tmp = ctx.alloc_local();
ctx.emit(Instruction::LocalGet(spread_i32));
ctx.emit(Instruction::I32Const(((1 + i) * 8) as i32));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::LocalSet(tmp));
temps.push(tmp);
}
let rest_slice_local = ctx.alloc_local();
emit_list_slice_from_i32_local(ctx, spread_i32, n_remaining - 1)?;
ctx.emit(Instruction::LocalSet(rest_slice_local));
temps.push(rest_slice_local);
if temps.len() != total {
return Err(format!("recur spread: computed {} temps but need {}", temps.len(), total));
}
for (i, &tmp) in temps.iter().enumerate() {
ctx.emit(Instruction::LocalGet(tmp));
ctx.emit(Instruction::LocalSet(ctx.arg_indices[i]));
}
ctx.emit(Instruction::Br(ctx.block_depth));
ctx.emit(Instruction::Unreachable);
Ok(())
} else {
if args.len() != ctx.arg_indices.len() {
return Err(format!(
"recur arity mismatch: expected {}, got {}",
ctx.arg_indices.len(),
args.len()
));
}
let mut temps = Vec::new();
for arg in args {
let tmp = ctx.alloc_local();
emit_expr(ctx, arg)?;
ctx.emit(Instruction::LocalSet(tmp));
temps.push(tmp);
}
for (i, &tmp) in temps.iter().enumerate() {
ctx.emit(Instruction::LocalGet(tmp));
ctx.emit(Instruction::LocalSet(ctx.arg_indices[i]));
}
ctx.emit(Instruction::Br(ctx.block_depth)); ctx.emit(Instruction::Unreachable);
Ok(())
}
}
CalcitProc::NativeRecord => emit_record_new(ctx, args),
CalcitProc::NativeRecordNth => emit_record_nth(ctx, args),
CalcitProc::NativeRecordGet => emit_record_get(ctx, args),
CalcitProc::NativeRecordCount => emit_record_count(ctx, args),
CalcitProc::NativeRecordFieldTag => emit_record_field_tag(ctx, args),
CalcitProc::NativeRecordStruct => emit_record_struct(ctx, args),
CalcitProc::NativeRecordGetName => emit_record_get_name(ctx, args),
CalcitProc::NativeRecordToMap => emit_record_to_map(ctx, args),
CalcitProc::NativeRecordAssoc | CalcitProc::NativeRecordAssocAt | CalcitProc::NativeRecordWith => {
Err(format!("{proc} not yet supported in WASM codegen"))
}
CalcitProc::NativeRecordFromMap
| CalcitProc::NativeRecordExtendAs
| CalcitProc::NativeRecordPartial
| CalcitProc::NativeRecordImpls
| CalcitProc::NativeRecordWithAt
| CalcitProc::NativeLooseRecord => Err(format!("Record operation {proc} not yet supported in WASM codegen")),
CalcitProc::NativeRecordContains => emit_record_contains(ctx, args),
CalcitProc::NativeRecordMatches => emit_record_matches(ctx, args),
CalcitProc::NativeTuple => emit_tuple_new(ctx, args),
CalcitProc::NativeTupleNth => emit_tuple_nth(ctx, args),
CalcitProc::NativeTupleCount => emit_tuple_count(ctx, args),
CalcitProc::NativeTupleValidateEnum => ctx.stub_proc(args), CalcitProc::NativeEnumTupleNew => emit_enum_tuple_new(ctx, args),
CalcitProc::NativeTupleImpls
| CalcitProc::NativeTupleParams
| CalcitProc::NativeTupleEnum
| CalcitProc::NativeTupleImplTraits
| CalcitProc::NativeTupleEnumHasVariant
| CalcitProc::NativeTupleEnumVariantArity => Err(format!("Tuple operation {proc} not yet supported in WASM codegen")),
CalcitProc::NativeTupleAssoc => emit_tuple_assoc(ctx, args),
CalcitProc::BitShl => emit_bitwise_binary(ctx, Instruction::I32Shl, args),
CalcitProc::BitShr => emit_bitwise_binary(ctx, Instruction::I32ShrS, args),
CalcitProc::BitAnd => emit_bitwise_binary(ctx, Instruction::I32And, args),
CalcitProc::BitOr => emit_bitwise_binary(ctx, Instruction::I32Or, args),
CalcitProc::BitXor => emit_bitwise_binary(ctx, Instruction::I32Xor, args),
CalcitProc::BitNot => {
expect_arity(1, args, "bit-not")?;
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::I32TruncF64S);
ctx.emit(Instruction::I32Const(-1)); ctx.emit(Instruction::I32Xor);
ctx.emit(Instruction::F64ConvertI32S);
Ok(())
}
CalcitProc::Raise => {
for arg in args {
emit_expr(ctx, arg)?;
ctx.emit(Instruction::Drop);
}
ctx.emit(Instruction::Unreachable);
Ok(())
}
CalcitProc::List => emit_list_new(ctx, args),
CalcitProc::Append => emit_list_append(ctx, args),
CalcitProc::Prepend => emit_list_prepend(ctx, args),
CalcitProc::Butlast => emit_list_butlast(ctx, args),
CalcitProc::NativeListAppend => emit_list_append(ctx, args),
CalcitProc::NativeListPrepend => emit_list_prepend(ctx, args),
CalcitProc::NativeListButlast => emit_list_butlast(ctx, args),
CalcitProc::NativeListLast => emit_list_last(ctx, args),
CalcitProc::NativeListSort => {
if args.is_empty() {
ctx.emit(f64_const(0.0));
} else {
emit_expr(ctx, &args[0])?;
}
Ok(())
}
CalcitProc::NativeListRange => emit_range(ctx, args),
CalcitProc::NativeListFoldl => emit_foldl(ctx, args),
CalcitProc::NativeListFoldlShortcut => emit_foldl_shortcut(ctx, args),
CalcitProc::NativeListCount => emit_ds_count(ctx, args),
CalcitProc::NativeListNth => emit_list_nth(ctx, args),
CalcitProc::NativeListFirst => emit_list_first(ctx, args),
CalcitProc::NativeListRest => emit_list_rest(ctx, args),
CalcitProc::NativeListEmpty => emit_ds_empty(ctx, args),
CalcitProc::NativeListSlice => emit_list_slice(ctx, args),
CalcitProc::NativeListReverse => emit_list_reverse(ctx, args),
CalcitProc::NativeListConcat => emit_list_concat(ctx, args),
CalcitProc::NativeListAssoc => emit_list_assoc(ctx, args),
CalcitProc::NativeListAssocBefore => emit_list_assoc_before(ctx, args),
CalcitProc::NativeListAssocAfter => emit_list_assoc_after(ctx, args),
CalcitProc::NativeListDissoc => emit_list_dissoc(ctx, args),
CalcitProc::NativeListToSet => emit_list_to_set(ctx, args),
CalcitProc::NativeListContains => emit_list_contains(ctx, args),
CalcitProc::NativeListIncludes => emit_list_includes(ctx, args),
CalcitProc::NativeListQ => emit_list_q(ctx, args),
CalcitProc::NativeBufListNew => emit_buf_list_new(ctx, args),
CalcitProc::NativeBufListPush => emit_buf_list_push(ctx, args),
CalcitProc::NativeBufListConcat => emit_buf_list_concat(ctx, args),
CalcitProc::NativeBufListToList => emit_buf_list_to_list(ctx, args),
CalcitProc::NativeBufListCount => emit_buf_list_count(ctx, args),
CalcitProc::NativeMap => emit_map_new(ctx, args),
CalcitProc::NativeMapGet => emit_map_get_op(ctx, args),
CalcitProc::NativeMapAssoc => emit_map_assoc(ctx, args),
CalcitProc::NativeMapDissoc => emit_map_dissoc(ctx, args),
CalcitProc::NativeMapCount => emit_ds_count(ctx, args),
CalcitProc::NativeMapEmpty => emit_ds_empty(ctx, args),
CalcitProc::NativeMapContains => emit_map_contains(ctx, args),
CalcitProc::NativeMapIncludes => emit_map_includes(ctx, args),
CalcitProc::ToPairs => emit_map_to_pairs(ctx, args),
CalcitProc::NativeMapToList => emit_map_to_list(ctx, args),
CalcitProc::Set => emit_set_new(ctx, args),
CalcitProc::NativeInclude => emit_set_include(ctx, args),
CalcitProc::NativeExclude => emit_set_exclude(ctx, args),
CalcitProc::NativeSetCount => emit_ds_count(ctx, args),
CalcitProc::NativeSetEmpty => emit_ds_empty(ctx, args),
CalcitProc::NativeSetIncludes => emit_set_includes(ctx, args),
CalcitProc::NativeSetToList => emit_set_to_list(ctx, args),
CalcitProc::NativeDifference => emit_set_difference(ctx, args),
CalcitProc::NativeUnion => emit_set_union(ctx, args),
CalcitProc::NativeSetIntersection => emit_set_intersection(ctx, args),
CalcitProc::NativeSetDestruct => emit_set_destruct(ctx, args),
CalcitProc::NativeMerge => emit_map_merge(ctx, args),
CalcitProc::NativeMergeNonNil => emit_map_merge_non_nil(ctx, args),
CalcitProc::NativeMapDiffNew => emit_map_diff_new(ctx, args),
CalcitProc::NativeMapDiffKeys => emit_map_diff_keys(ctx, args),
CalcitProc::NativeMapCommonKeys => emit_map_common_keys(ctx, args),
CalcitProc::NativeMapDiffTriple => maps::emit_map_diff_triple(ctx, args),
CalcitProc::NativeMapDestruct => emit_map_destruct(ctx, args),
CalcitProc::NativeMapKeys => emit_map_keys(ctx, args),
CalcitProc::NativeMapVals => emit_map_vals(ctx, args),
CalcitProc::Range => emit_range(ctx, args),
CalcitProc::NativeHash => emit_hash_proc(ctx, args),
CalcitProc::NativeStrCount => emit_str_count(ctx, args),
CalcitProc::NativeStrEmpty => emit_str_empty(ctx, args),
CalcitProc::NativeStrConcat => emit_str_concat(ctx, args),
CalcitProc::NativeStrNth => emit_str_nth(ctx, args),
CalcitProc::NativeStrFirst => emit_str_first(ctx, args),
CalcitProc::NativeStrRest => emit_str_rest(ctx, args),
CalcitProc::NativeStrSlice => emit_str_slice(ctx, args),
CalcitProc::NativeStrCompare => emit_str_compare(ctx, args),
CalcitProc::NativeStrContains => emit_str_contains(ctx, args),
CalcitProc::NativeStrIncludes => emit_str_includes(ctx, args),
CalcitProc::NativeStrFindIndex => emit_str_find_index(ctx, args),
CalcitProc::NativeStrPadLeft => emit_str_pad_left(ctx, args),
CalcitProc::NativeStrPadRight => emit_str_pad_right(ctx, args),
CalcitProc::StartsWith => emit_str_starts_with(ctx, args),
CalcitProc::EndsWith => emit_str_ends_with(ctx, args),
CalcitProc::TurnString | CalcitProc::NativeStr => emit_turn_string(ctx, args),
CalcitProc::Trim => emit_trim(ctx, args),
CalcitProc::IsBlank => emit_blank(ctx, args),
CalcitProc::GetCharCode => emit_get_char_code(ctx, args),
CalcitProc::ParseFloat => emit_parse_float(ctx, args),
CalcitProc::CharFromCode => emit_char_from_code(ctx, args),
CalcitProc::NativeStrReplace => emit_str_replace(ctx, args),
CalcitProc::NativeStrEscape => emit_str_escape(ctx, args),
CalcitProc::Split => emit_split(ctx, args),
CalcitProc::SplitLines => emit_split_lines(ctx, args),
CalcitProc::NativeListDistinct => emit_list_distinct(ctx, args),
CalcitProc::Foldl => emit_foldl(ctx, args),
CalcitProc::FoldlShortcut => emit_foldl_shortcut(ctx, args),
CalcitProc::FoldrShortcut => emit_foldr_shortcut(ctx, args),
CalcitProc::FormatToLisp => emit_format_to_lisp(ctx, args),
CalcitProc::PrStr => ctx.stub_proc(args),
CalcitProc::GetEnv => ctx.stub_proc(args),
CalcitProc::AtomDeref => {
expect_arity(1, args, "&atom:deref")?;
emit_expr(ctx, &args[0])
}
CalcitProc::Quit => {
ctx.emit(Instruction::Unreachable);
ctx.emit(f64_const(0.0)); Ok(())
}
CalcitProc::NativeGetCalcitBackend => {
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitProc::TurnTag => {
expect_arity(1, args, "turn-tag")?;
emit_expr(ctx, &args[0])
}
CalcitProc::NativeStructImplTraits | CalcitProc::NativeEnumImplTraits => {
eprintln!("[wasm warning] trait registration via impl-traits is not supported in WASM");
ctx.silent_nil()
}
CalcitProc::RegisterCalcitBuiltinImpls => {
eprintln!("[wasm warning] RegisterCalcitBuiltinImpls is ignored in WASM (builtin impls already registered)");
ctx.silent_nil()
}
CalcitProc::NativeImplNew => {
eprintln!("[wasm warning] &impl::new is not supported in WASM; trait impls are ignored");
ctx.silent_nil()
}
CalcitProc::NativeAssertTraits => {
eprintln!("[wasm warning] &assert-traits is not enforced in WASM (trait checking disabled)");
ctx.silent_nil()
}
CalcitProc::NativeGetOs => ctx.stub_proc(args),
CalcitProc::NativeNumberDisplayBy => {
if args.len() != 2 {
return Err("&number:display-by expects 2 args".into());
}
emit_expr(ctx, &args[0])?; emit_expr(ctx, &args[1])?; ctx.call_rt("__rt_display_by");
Ok(())
}
CalcitProc::NativeNumberFormat => ctx.stub_proc(args),
CalcitProc::Sort => {
if args.is_empty() {
ctx.emit(f64_const(0.0));
} else {
emit_expr(ctx, &args[0])?;
}
Ok(())
}
_ => Err(format!("unsupported proc in WASM: {proc}")),
}
}
fn emit_unary(ctx: &mut WasmGenCtx, instr: Instruction<'static>, args: &[Calcit]) -> Result<(), String> {
if args.len() != 1 {
return Err(format!("{instr:?} expects 1 arg, got {}", args.len()));
}
emit_expr(ctx, &args[0])?;
ctx.emit(instr);
Ok(())
}
fn emit_type_of(ctx: &mut WasmGenCtx, args: &[Calcit]) -> Result<(), String> {
if args.len() != 1 {
return Err(format!("type-of expects 1 arg, got {}", args.len()));
}
let number_tag = get_type_tag(ctx, "number");
let v_local = ctx.alloc_local_typed(ValType::F64);
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::LocalSet(v_local));
let is_valid_ptr = ctx.alloc_local_typed(ValType::I32);
let raw_base = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(v_local));
ctx.emit(Instruction::LocalGet(v_local));
ctx.emit(Instruction::F64Trunc);
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::LocalGet(v_local));
ctx.emit(f64_const((HEAP_BASE + 8) as f64));
ctx.emit(Instruction::F64Ge);
ctx.emit(Instruction::I32And);
ctx.emit(Instruction::LocalGet(v_local));
ctx.emit(Instruction::GlobalGet(HEAP_PTR_GLOBAL));
ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::F64Lt);
ctx.emit(Instruction::I32And);
ctx.emit(Instruction::LocalSet(is_valid_ptr));
ctx.emit(Instruction::LocalGet(v_local));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Sub);
ctx.emit(Instruction::LocalSet(raw_base));
ctx.emit(Instruction::LocalGet(is_valid_ptr));
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.emit(Instruction::LocalGet(raw_base));
ctx.emit(Instruction::I32Load(mem_arg_i32(0)));
ctx.emit(Instruction::I32Const(HEAP_MAGIC));
ctx.emit(Instruction::I32Eq);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.emit(Instruction::LocalGet(raw_base));
ctx.emit(Instruction::I32Load(mem_arg_i32(4)));
ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::Else);
ctx.emit(f64_const(number_tag));
ctx.emit(Instruction::End);
ctx.emit(Instruction::Else);
ctx.emit(f64_const(number_tag));
ctx.emit(Instruction::End);
Ok(())
}
fn emit_type_predicate(ctx: &mut WasmGenCtx, type_name: &str, args: &[Calcit]) -> Result<(), String> {
if args.len() != 1 {
return Err(format!("{}? expects 1 arg, got {}", type_name, args.len()));
}
let expected_tag = get_type_tag(ctx, type_name);
emit_type_of(ctx, args)?;
ctx.emit(f64_const(expected_tag));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::F64ConvertI32U);
Ok(())
}
fn emit_host_call(ctx: &mut WasmGenCtx, name: &str, args: &[Calcit]) -> Result<(), String> {
let import_idx = HOST_IMPORTS
.iter()
.position(|imp| imp.name == name)
.ok_or_else(|| format!("unknown host import: {name}"))?;
let expected_arity = HOST_IMPORTS[import_idx].arity;
if args.len() != expected_arity {
return Err(format!("{name} expects {expected_arity} args, got {}", args.len()));
}
for arg in args {
emit_expr(ctx, arg)?;
}
ctx.emit(Instruction::Call(import_idx as u32));
Ok(())
}
fn emit_binary(ctx: &mut WasmGenCtx, instr: Instruction<'static>, args: &[Calcit]) -> Result<(), String> {
if args.len() != 2 {
return Err(format!("{instr:?} expects 2 args, got {}", args.len()));
}
emit_expr(ctx, &args[0])?;
emit_expr(ctx, &args[1])?;
ctx.emit(instr);
Ok(())
}
fn emit_cmp(ctx: &mut WasmGenCtx, instr: Instruction<'static>, args: &[Calcit]) -> Result<(), String> {
if args.len() != 2 {
return Err(format!("{instr:?} expects 2 args, got {}", args.len()));
}
ctx.emit(f64_const(1.0));
ctx.emit(f64_const(0.0));
emit_expr(ctx, &args[0])?;
emit_expr(ctx, &args[1])?;
ctx.emit(instr);
ctx.emit(Instruction::Select);
Ok(())
}
fn emit_equals(ctx: &mut WasmGenCtx, args: &[Calcit]) -> Result<(), String> {
if args.len() != 2 {
return Err(format!("= expects 2 args, got {}", args.len()));
}
let a = ctx.alloc_local();
let b = ctx.alloc_local();
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::LocalSet(a));
emit_expr(ctx, &args[1])?;
ctx.emit(Instruction::LocalSet(b));
let result = emit_equals_core(ctx, a, b)?;
ctx.emit(Instruction::LocalGet(result));
Ok(())
}
#[allow(private_interfaces)]
pub(super) fn emit_equals_core(ctx: &mut WasmGenCtx, a: u32, b: u32) -> Result<u32, String> {
emit_equals_core_impl(ctx, a, b, true)
}
#[allow(private_interfaces)]
pub(super) fn emit_equals_core_shallow(ctx: &mut WasmGenCtx, a: u32, b: u32) -> Result<u32, String> {
emit_equals_core_impl(ctx, a, b, false)
}
fn emit_equals_core_impl(ctx: &mut WasmGenCtx, a: u32, b: u32, structural_sets: bool) -> Result<u32, String> {
let result = ctx.alloc_local();
let string_tag = *ctx.tag_index.get("string").ok_or("string tag not found")? as i32;
let list_tag = *ctx.tag_index.get("list").ok_or("list tag not found")? as i32;
let set_tag = *ctx.tag_index.get("set").ok_or("set tag not found")? as i32;
let map_tag = *ctx.tag_index.get("map").ok_or("map tag not found")? as i32;
let rt_str_compare = *ctx
.runtime_fn_index
.get("__rt_str_compare")
.ok_or("runtime helper __rt_str_compare not found")?;
let rt_set_find_elem = *ctx
.runtime_fn_index
.get("__rt_set_find_elem")
.ok_or("runtime helper __rt_set_find_elem not found")?;
let rt_map_equal = *ctx
.runtime_fn_index
.get("__rt_map_equal")
.ok_or("runtime helper __rt_map_equal not found")?;
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::LocalSet(result));
ctx.emit(Instruction::LocalGet(a));
ctx.emit(Instruction::LocalGet(b));
ctx.emit(Instruction::F64Eq);
ctx.begin_block_if();
ctx.emit(f64_const(1.0));
ctx.emit(Instruction::LocalSet(result));
ctx.emit(Instruction::End);
ctx.emit(Instruction::LocalGet(result));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq);
ctx.begin_block_if();
let ptr_a = ctx.alloc_local_typed(ValType::I32);
let ptr_b = ctx.alloc_local_typed(ValType::I32);
let both_heap = ctx.alloc_local_typed(ValType::I32);
let mem_size_f64 = ctx.alloc_local();
ctx.emit(Instruction::MemorySize(0));
ctx.emit(Instruction::I32Const(16)); ctx.emit(Instruction::I32Shl);
ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::LocalSet(mem_size_f64));
ctx.emit(Instruction::LocalGet(a));
ctx.emit(f64_const((HEAP_BASE + 8) as f64));
ctx.emit(Instruction::F64Ge);
ctx.emit(Instruction::LocalGet(a));
ctx.emit(Instruction::LocalGet(mem_size_f64));
ctx.emit(Instruction::F64Lt);
ctx.emit(Instruction::I32And);
ctx.emit(Instruction::LocalGet(b));
ctx.emit(f64_const((HEAP_BASE + 8) as f64));
ctx.emit(Instruction::F64Ge);
ctx.emit(Instruction::LocalGet(b));
ctx.emit(Instruction::LocalGet(mem_size_f64));
ctx.emit(Instruction::F64Lt);
ctx.emit(Instruction::I32And);
ctx.emit(Instruction::I32And);
ctx.emit(Instruction::LocalSet(both_heap));
ctx.emit(Instruction::LocalGet(both_heap));
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(a));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(ptr_a));
ctx.emit(Instruction::LocalGet(b));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(ptr_b));
let tag_a = ctx.alloc_local_typed(ValType::I32);
let tag_b = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::I32Const(4));
ctx.emit(Instruction::I32Sub);
ctx.emit(Instruction::I32Load(mem_arg_i32(0)));
ctx.emit(Instruction::LocalSet(tag_a));
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::I32Const(4));
ctx.emit(Instruction::I32Sub);
ctx.emit(Instruction::I32Load(mem_arg_i32(0)));
ctx.emit(Instruction::LocalSet(tag_b));
ctx.emit(Instruction::LocalGet(tag_a));
ctx.emit(Instruction::LocalGet(tag_b));
ctx.emit(Instruction::I32Eq);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(tag_a));
ctx.emit(Instruction::I32Const(string_tag));
ctx.emit(Instruction::I32Eq);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::Call(rt_str_compare));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq); ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::LocalSet(result));
ctx.emit(Instruction::End);
ctx.emit(Instruction::LocalGet(result));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::LocalGet(tag_a));
ctx.emit(Instruction::I32Const(string_tag));
ctx.emit(Instruction::I32Ne);
ctx.emit(Instruction::I32And);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(tag_a));
ctx.emit(Instruction::I32Const(list_tag));
ctx.emit(Instruction::I32Eq);
ctx.begin_block_if();
{
let cnt_a = ctx.alloc_local_typed(ValType::I32);
let cnt_b = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(cnt_a));
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(cnt_b));
ctx.emit(Instruction::LocalGet(cnt_a));
ctx.emit(Instruction::LocalGet(cnt_b));
ctx.emit(Instruction::I32Eq);
ctx.begin_block_if();
{
let all_eq = ctx.alloc_i32(1); let li = ctx.alloc_i32(0);
ctx.begin_block();
ctx.begin_loop();
ctx.loop_exit_if_ge(li, cnt_a);
let offset_a = ctx.alloc_local_typed(ValType::I32);
let offset_b = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalGet(li));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Mul);
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalSet(offset_a));
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalGet(li));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Mul);
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalSet(offset_b));
let elem_a = ctx.alloc_local(); let elem_b = ctx.alloc_local(); let elems_eq = ctx.alloc_i32(0); let elem_a_i32 = ctx.alloc_local_typed(ValType::I32);
let elem_b_i32 = ctx.alloc_local_typed(ValType::I32);
let elem_tag_a = ctx.alloc_local_typed(ValType::I32);
let elem_tag_b = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(offset_a));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::LocalSet(elem_a));
ctx.emit(Instruction::LocalGet(offset_b));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::LocalSet(elem_b));
ctx.emit(Instruction::LocalGet(elem_a));
ctx.emit(Instruction::LocalGet(elem_b));
ctx.emit(Instruction::F64Eq);
ctx.begin_block_if();
ctx.emit(Instruction::I32Const(1));
ctx.emit(Instruction::LocalSet(elems_eq));
ctx.emit(Instruction::Else);
let heap_min_elem = (HEAP_BASE + 8) as f64;
ctx.emit(Instruction::LocalGet(elem_a));
ctx.emit(f64_const(heap_min_elem));
ctx.emit(Instruction::F64Ge);
ctx.emit(Instruction::LocalGet(elem_b));
ctx.emit(f64_const(heap_min_elem));
ctx.emit(Instruction::F64Ge);
ctx.emit(Instruction::I32And);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(elem_a));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(elem_a_i32));
ctx.emit(Instruction::LocalGet(elem_b));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(elem_b_i32));
ctx.emit(Instruction::LocalGet(elem_a_i32));
ctx.emit(Instruction::I32Const(4));
ctx.emit(Instruction::I32Sub);
ctx.emit(Instruction::I32Load(mem_arg_i32(0)));
ctx.emit(Instruction::LocalSet(elem_tag_a));
ctx.emit(Instruction::LocalGet(elem_b_i32));
ctx.emit(Instruction::I32Const(4));
ctx.emit(Instruction::I32Sub);
ctx.emit(Instruction::I32Load(mem_arg_i32(0)));
ctx.emit(Instruction::LocalSet(elem_tag_b));
ctx.emit(Instruction::LocalGet(elem_tag_a));
ctx.emit(Instruction::I32Const(string_tag));
ctx.emit(Instruction::I32Eq);
ctx.emit(Instruction::LocalGet(elem_tag_b));
ctx.emit(Instruction::I32Const(string_tag));
ctx.emit(Instruction::I32Eq);
ctx.emit(Instruction::I32And);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(elem_a_i32));
ctx.emit(Instruction::LocalGet(elem_b_i32));
ctx.emit(Instruction::Call(rt_str_compare));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq); ctx.emit(Instruction::LocalSet(elems_eq));
ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::LocalGet(elems_eq));
ctx.emit(Instruction::I32Eqz);
ctx.begin_block_if();
ctx.emit(Instruction::I32Const(0));
ctx.emit(Instruction::LocalSet(all_eq));
ctx.emit(Instruction::Br(2)); ctx.emit(Instruction::End);
ctx.i32_inc(li);
ctx.emit(Instruction::Br(0));
ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::LocalGet(all_eq));
ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::LocalSet(result));
}
ctx.emit(Instruction::End); }
ctx.emit(Instruction::End);
ctx.emit(Instruction::LocalGet(tag_a));
ctx.emit(Instruction::I32Const(set_tag));
ctx.emit(Instruction::I32Eq);
ctx.begin_block_if();
{
let cnt_a = ctx.alloc_local_typed(ValType::I32);
let cnt_b = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(cnt_a));
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::LocalSet(cnt_b));
ctx.emit(Instruction::LocalGet(cnt_a));
ctx.emit(Instruction::LocalGet(cnt_b));
ctx.emit(Instruction::I32Eq);
ctx.begin_block_if();
{
let all_eq = ctx.alloc_i32(1);
let si = ctx.alloc_i32(0);
ctx.begin_block();
ctx.begin_loop();
ctx.loop_exit_if_ge(si, cnt_a);
let offset_a = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalGet(si));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Mul);
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalSet(offset_a));
ctx.emit(Instruction::LocalGet(offset_a));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
let elem_a = ctx.alloc_local();
ctx.emit(Instruction::LocalSet(elem_a));
let elem_not_found = if structural_sets {
let bj = ctx.alloc_i32(0);
let found_b = ctx.alloc_i32(-1);
ctx.begin_block();
ctx.begin_loop();
ctx.loop_exit_if_ge(bj, cnt_b);
let b_off = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalGet(bj));
ctx.emit(Instruction::I32Const(8));
ctx.emit(Instruction::I32Mul);
ctx.emit(Instruction::I32Add);
ctx.emit(Instruction::LocalSet(b_off));
let b_elem = ctx.alloc_local();
ctx.emit(Instruction::LocalGet(b_off));
ctx.emit(Instruction::F64Load(mem_arg_f64(0)));
ctx.emit(Instruction::LocalSet(b_elem));
let eq_r = emit_equals_core_impl(ctx, elem_a, b_elem, false)?;
ctx.emit(Instruction::LocalGet(eq_r));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Ne);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(bj));
ctx.emit(Instruction::LocalSet(found_b));
ctx.emit(Instruction::Br(2));
ctx.emit(Instruction::End);
ctx.i32_inc(bj);
ctx.emit(Instruction::Br(0));
ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::LocalGet(found_b));
ctx.emit(Instruction::I32Const(-1));
ctx.emit(Instruction::I32Eq); let not_found_flag = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalSet(not_found_flag));
not_found_flag
} else {
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::LocalGet(elem_a));
ctx.emit(Instruction::Call(rt_set_find_elem));
ctx.emit(Instruction::I32Const(-1));
ctx.emit(Instruction::I32Eq); let not_found_flag = ctx.alloc_local_typed(ValType::I32);
ctx.emit(Instruction::LocalSet(not_found_flag));
not_found_flag
};
ctx.emit(Instruction::LocalGet(elem_not_found));
ctx.begin_block_if();
ctx.emit(Instruction::I32Const(0));
ctx.emit(Instruction::LocalSet(all_eq));
ctx.emit(Instruction::Br(2)); ctx.emit(Instruction::End);
ctx.i32_inc(si);
ctx.emit(Instruction::Br(0));
ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::LocalGet(all_eq));
ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::LocalSet(result));
}
ctx.emit(Instruction::End); }
ctx.emit(Instruction::End);
ctx.emit(Instruction::LocalGet(result));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::LocalGet(tag_a));
ctx.emit(Instruction::I32Const(map_tag));
ctx.emit(Instruction::I32Eq);
ctx.emit(Instruction::I32And);
ctx.begin_block_if();
ctx.emit(Instruction::LocalGet(ptr_a));
ctx.emit(Instruction::LocalGet(ptr_b));
ctx.emit(Instruction::Call(rt_map_equal));
ctx.emit(Instruction::F64ConvertI32U);
ctx.emit(Instruction::LocalSet(result));
ctx.emit(Instruction::End);
ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::End); ctx.emit(Instruction::End);
Ok(result)
}
fn emit_bitwise_binary(ctx: &mut WasmGenCtx, instr: Instruction<'static>, args: &[Calcit]) -> Result<(), String> {
if args.len() != 2 {
return Err(format!("{instr:?} expects 2 args, got {}", args.len()));
}
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::I32TruncF64S);
emit_expr(ctx, &args[1])?;
ctx.emit(Instruction::I32TruncF64S);
ctx.emit(instr);
ctx.emit(Instruction::F64ConvertI32S);
Ok(())
}
fn emit_if(ctx: &mut WasmGenCtx, args: &[Calcit]) -> Result<(), String> {
if args.len() < 2 || args.len() > 3 {
return Err(format!("if expects 2-3 args, got {}", args.len()));
}
emit_expr(ctx, &args[0])?;
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Ne);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.block_depth += 1;
emit_expr(ctx, &args[1])?;
ctx.emit(Instruction::Else);
if args.len() == 3 {
emit_expr(ctx, &args[2])?;
} else {
ctx.emit(f64_const(0.0));
}
ctx.block_depth -= 1;
ctx.emit(Instruction::End);
Ok(())
}
fn emit_let_multi(ctx: &mut WasmGenCtx, args: &[Calcit]) -> Result<(), String> {
if args.is_empty() {
ctx.emit(f64_const(0.0));
return Ok(());
}
let Calcit::List(pairs_list) = &args[0] else {
return Err(format!("let expects a list of binding pairs, got: {}", args[0]));
};
let body = &args[1..];
emit_let_pairs(ctx, &pairs_list.to_vec(), body)
}
fn emit_let_pairs(ctx: &mut WasmGenCtx, pairs: &[Calcit], body: &[Calcit]) -> Result<(), String> {
if pairs.is_empty() {
return emit_body(ctx, body);
}
let pair = &pairs[0];
let Calcit::List(xs) = pair else {
return Err(format!("let binding expects a pair, got: {pair}"));
};
if xs.len() != 2 {
return Err(format!("let binding pair must have 2 elements, got {}", xs.len()));
}
let var_name = match &xs[0] {
Calcit::Local(CalcitLocal { sym, .. }) => sym.to_string(),
Calcit::Symbol { sym, .. } => sym.to_string(),
other => return Err(format!("let binding expected symbol, got: {other}")),
};
emit_expr(ctx, &xs[1])?;
let idx = ctx.declare_local(&var_name);
ctx.emit(Instruction::LocalSet(idx));
emit_let_pairs(ctx, &pairs[1..], body)
}
fn emit_let(ctx: &mut WasmGenCtx, body: &[Calcit]) -> Result<(), String> {
if body.is_empty() {
ctx.emit(f64_const(0.0));
return Ok(());
}
let pair = &body[0];
let rest = &body[1..];
match pair {
Calcit::Nil => emit_body(ctx, rest),
Calcit::List(xs) if xs.is_empty() => emit_body(ctx, rest),
Calcit::List(xs) if xs.len() == 2 => {
let var_name = match &xs[0] {
Calcit::Local(CalcitLocal { sym, .. }) => sym.to_string(),
Calcit::Symbol { sym, .. } => sym.to_string(),
other => return Err(format!("let binding expected symbol, got: {other}")),
};
if let Some((params, body)) = try_extract_inline_lambda(&xs[1]) {
ctx.lambda_locals.insert(var_name.clone(), (params, body));
ctx.declare_local(&var_name);
if rest.len() == 1 {
if let Calcit::List(inner) = &rest[0] {
if let Some(Calcit::Syntax(CalcitSyntax::CoreLet, _)) = inner.first() {
let inner_body: Vec<Calcit> = inner.drop_left().to_vec();
return emit_let(ctx, &inner_body);
}
}
}
return emit_body(ctx, rest);
}
emit_expr(ctx, &xs[1])?;
let idx = ctx.declare_local(&var_name);
ctx.emit(Instruction::LocalSet(idx));
if rest.len() == 1 {
if let Calcit::List(inner) = &rest[0] {
if let Some(Calcit::Syntax(CalcitSyntax::CoreLet, _)) = inner.first() {
let inner_body: Vec<Calcit> = inner.drop_left().to_vec();
return emit_let(ctx, &inner_body);
}
}
}
emit_body(ctx, rest)
}
_ => Err(format!("unsupported let binding form: {pair}")),
}
}
fn emit_match(ctx: &mut WasmGenCtx, args: &[Calcit]) -> Result<(), String> {
if args.is_empty() {
return Err("match requires a value and branches".into());
}
emit_expr(ctx, &args[0])?;
let ptr_f64 = ctx.alloc_local();
ctx.emit(Instruction::LocalSet(ptr_f64));
let tag_local = ctx.alloc_local();
ctx.emit(Instruction::LocalGet(ptr_f64));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::F64Load(mem_arg_f64(8)));
ctx.emit(Instruction::LocalSet(tag_local));
let branches = &args[1..];
let mut tag_branches: Vec<(&Calcit, &Calcit)> = Vec::new(); let mut wildcard_body: Option<&Calcit> = None;
for branch in branches {
let Calcit::List(pair) = branch else {
return Err(format!("match branch expected a pair, got: {branch}"));
};
if pair.len() != 2 {
return Err(format!("match branch expected 2 elements, got {}", pair.len()));
}
let pattern = &pair[0];
let body = &pair[1];
match pattern {
Calcit::Symbol { sym, .. } | Calcit::Local(CalcitLocal { sym, .. }) if sym.as_ref() == "_" => {
wildcard_body = Some(body);
}
Calcit::List(_) => {
tag_branches.push((pattern, body));
}
other => return Err(format!("unsupported match pattern: {other}")),
}
}
let num_tag_branches = tag_branches.len();
if num_tag_branches == 0 {
if let Some(body) = wildcard_body {
emit_expr(ctx, body)?;
} else {
ctx.emit(f64_const(0.0));
}
return Ok(());
}
for (i, (pattern, body)) in tag_branches.iter().enumerate() {
let Calcit::List(pat_xs) = pattern else {
return Err(format!("match pattern expected list, got: {pattern}"));
};
let tag_str = match &pat_xs[0] {
Calcit::Tag(t) => t.to_string(),
other => return Err(format!("match pattern expected tag, got: {other}")),
};
let tag_id = *ctx
.tag_index
.get(&tag_str)
.ok_or_else(|| format!("unknown tag in match pattern: {tag_str}"))?;
ctx.emit(Instruction::LocalGet(tag_local));
ctx.emit(f64_const(tag_id as f64));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.block_depth += 1;
let binding_count = pat_xs.len() - 1;
for bind_idx in 0..binding_count {
let binding = &pat_xs[1 + bind_idx];
let bind_name = match binding {
Calcit::Local(CalcitLocal { sym, .. }) => sym.to_string(),
Calcit::Symbol { sym, .. } => sym.to_string(),
other => return Err(format!("match binding expected symbol, got: {other}")),
};
let offset = ((2 + bind_idx) * 8) as u64;
ctx.emit(Instruction::LocalGet(ptr_f64));
ctx.emit(Instruction::I32TruncF64U);
ctx.emit(Instruction::F64Load(mem_arg_f64(offset)));
let idx = ctx.declare_local(&bind_name);
ctx.emit(Instruction::LocalSet(idx));
}
emit_expr(ctx, body)?;
ctx.emit(Instruction::Else);
if i == num_tag_branches - 1 {
if let Some(wb) = wildcard_body {
emit_expr(ctx, wb)?;
} else {
ctx.emit(f64_const(0.0));
}
}
}
for _ in 0..num_tag_branches {
ctx.block_depth -= 1;
ctx.emit(Instruction::End);
}
Ok(())
}
const BUILTIN_TYPE_TAGS: &[&str] = &[
"buf-list", "list", "map", "set", "tuple", "record", "number", "bool", "nil", "tag", "fn", "string", "symbol",
];
fn collect_all_tags_from(fn_defs: &[(String, String, CalcitFnArgs, Vec<Calcit>)]) -> HashMap<String, u32> {
let mut tags: Vec<String> = Vec::new();
for t in BUILTIN_TYPE_TAGS {
tags.push((*t).to_string());
}
for (_, _, _, body) in fn_defs {
for expr in body {
collect_tags_from_expr(expr, &mut tags);
}
}
tags.sort();
tags.dedup();
tags.into_iter().enumerate().map(|(i, t)| (t, (i + 1) as u32)).collect()
}
fn collect_tags_from_expr(expr: &Calcit, tags: &mut Vec<String>) {
match expr {
Calcit::Tag(t) => {
tags.push(t.to_string());
}
Calcit::List(xs) => {
for x in xs.iter() {
collect_tags_from_expr(x, tags);
}
}
Calcit::Struct(s) => {
tags.push(s.name.to_string());
for f in s.fields.iter() {
tags.push(f.to_string());
}
}
Calcit::Import(CalcitImport { ns, def, .. }) => {
if let Ok(struct_def) = resolve_struct_ref(expr) {
tags.push(struct_def.name.to_string());
for f in struct_def.fields.iter() {
tags.push(f.to_string());
}
}
let _ = (ns, def); }
_ => {}
}
}
fn build_string_pool(
fn_defs: &[(String, String, CalcitFnArgs, Vec<Calcit>)],
tag_index: &HashMap<String, u32>,
) -> (HashMap<String, u32>, Vec<u8>, i32) {
let mut strings: Vec<String> = Vec::new();
for (_, _, _, body) in fn_defs {
for expr in body {
collect_strings_from_expr(expr, &mut strings);
}
}
strings.sort();
strings.dedup();
if strings.is_empty() {
return (HashMap::new(), Vec::new(), HEAP_BASE);
}
let string_tag_id = *tag_index.get("string").expect("string type tag must exist") as i32;
let mut pool: HashMap<String, u32> = HashMap::new();
let mut data: Vec<u8> = Vec::new();
let mut offset = HEAP_BASE as u32;
for s in &strings {
let byte_len = s.len() as u32;
data.extend_from_slice(&(HEAP_MAGIC as u32).to_le_bytes());
data.extend_from_slice(&(string_tag_id as u32).to_le_bytes());
let logical_ptr = offset + 8;
pool.insert(s.clone(), logical_ptr);
data.extend_from_slice(&(byte_len as f64).to_le_bytes());
data.extend_from_slice(s.as_bytes());
let padded_len = (byte_len + 7) & !7;
data.extend(std::iter::repeat_n(0u8, (padded_len - byte_len) as usize));
offset += 8 + 8 + padded_len;
}
let heap_start = offset as i32;
(pool, data, heap_start)
}
fn collect_record_field_tags_from_program(
program_data: &program::CompiledProgram,
tag_index: &HashMap<String, u32>,
) -> HashMap<u32, Vec<u32>> {
let mut result = HashMap::new();
for file_info in program_data.values() {
for compiled in file_info.defs.values() {
let struct_def =
try_parse_defrecord_form(&compiled.preprocessed_code).or_else(|| try_parse_defrecord_form(&compiled.codegen_form));
let Some(struct_def) = struct_def else {
continue;
};
let Some(struct_tag_id) = tag_index.get(struct_def.name.ref_str()) else {
continue;
};
let field_tag_ids = struct_def
.fields
.iter()
.filter_map(|field| tag_index.get(field.ref_str()).copied())
.collect::<Vec<_>>();
result.insert(*struct_tag_id, field_tag_ids);
}
}
result
}
pub(crate) fn try_format_tuple_literal(expr: &Calcit) -> Option<String> {
if let Calcit::List(list) = expr {
if !list.is_empty() {
if let Calcit::Proc(p) = &list[0] {
if *p == CalcitProc::NativeTuple {
let mut s = String::from("(:: ");
for (i, item) in list.iter().skip(1).enumerate() {
if i > 0 {
s.push(' ');
}
match item {
Calcit::Tag(_) | Calcit::Str(_) | Calcit::Number(_) | Calcit::Bool(_) | Calcit::Nil => {
use std::fmt::Write;
write!(s, "{item}").ok()?;
}
_ => return None,
}
}
s.push(')');
return Some(s);
}
}
}
}
None
}
fn collect_strings_from_expr(expr: &Calcit, strings: &mut Vec<String>) {
match expr {
Calcit::Str(s) => {
strings.push(s.to_string());
}
Calcit::Tag(t) => {
strings.push(t.to_string());
}
Calcit::List(xs) => {
if xs.len() == 2 {
if let Calcit::Proc(p) = &xs[0] {
if matches!(p.as_ref(), "format-to-lisp") {
if let Calcit::List(inner) = &xs[1] {
if inner.len() >= 2 {
if let Calcit::Syntax(CalcitSyntax::Quote, _) = &inner[0] {
let s = crate::calcit::format_to_lisp(&inner[1]);
strings.push(s);
}
}
}
}
}
}
if let Some(tuple_str) = try_format_tuple_literal(expr) {
strings.push(tuple_str);
}
for x in xs.iter() {
collect_strings_from_expr(x, strings);
}
}
_ => {}
}
}