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();
for &ns in &ns_order {
let Some(file_info) = program_data.get(ns) else {
continue;
};
for (def_name, compiled) in &file_info.defs {
if let crate::calcit::Calcit::List(xs) = &compiled.preprocessed_code {
if matches!(
xs.first(),
Some(crate::calcit::Calcit::Syntax(crate::calcit::CalcitSyntax::Defatom, _))
) {
let qualified = format!("{ns}/{def_name}");
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 mut lazy_defs: Vec<(String, String, Calcit)> = Vec::new(); 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::LazyValue {
continue;
}
if let crate::calcit::Calcit::List(xs) = &compiled.preprocessed_code {
if matches!(
xs.first(),
Some(crate::calcit::Calcit::Syntax(crate::calcit::CalcitSyntax::Defatom, _))
) {
continue;
}
}
lazy_defs.push((ns.to_string(), def_name.to_string(), compiled.preprocessed_code.clone()));
}
}
let atom_count = atom_initial_values.len() as u32;
let lazy_count = lazy_defs.len() as u32;
let lazy_val_global_base = 1 + atom_count;
let lazy_flag_global_base = lazy_val_global_base + lazy_count;
let getter_base_idx = num_imports + runtime_fn_count + fn_defs.len() as u32;
let mut lazy_globals: HashMap<String, u32> = HashMap::new();
for (i, (ns, def_name, _)) in lazy_defs.iter().enumerate() {
let qualified = format!("{ns}/{def_name}");
let getter_fn_idx = getter_base_idx + i as u32;
lazy_globals.insert(qualified.clone(), getter_fn_idx);
fn_index.insert(format!("__lazy_getter_{i}"), getter_fn_idx);
}
let env = WasmCompileEnv {
fn_index,
fn_arity,
fn_has_rest,
runtime_fn_index,
tag_index,
record_field_tags,
string_pool,
atom_globals,
fn_table_index,
lazy_globals,
};
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 mut init_getter_calls: Vec<Instruction<'static>> = Vec::new();
for (i, (ns, def_name, init_expr)) in lazy_defs.iter().enumerate() {
let val_global_idx = lazy_val_global_base + i as u32;
let flag_global_idx = lazy_flag_global_base + i as u32;
let getter_fn = build_lazy_getter_fn(ns, def_name, val_global_idx, flag_global_idx, init_expr, &env);
let getter_idx = num_imports + compiled_fns.len() as u32;
init_getter_calls.push(Instruction::Call(getter_idx));
init_getter_calls.push(Instruction::Drop);
compiled_fns.push(getter_fn);
}
if !lazy_defs.is_empty() {
init_getter_calls.push(f64_const(0.0)); compiled_fns.push(CompiledFn {
export_name: Some("__init".to_string()),
params: vec![],
results: vec![ValType::F64],
locals: vec![],
instructions: init_getter_calls,
});
}
let wasm_bytes = build_wasm_module(
&compiled_fns,
heap_start,
&string_data_segment,
&atom_initial_values,
lazy_count,
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>,
fn_table_index: HashMap<String, u32>,
lazy_globals: 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>,
fn_table_index: HashMap<String, u32>,
lambda_locals: HashMap<String, (Vec<String>, Vec<Calcit>)>,
lazy_globals: HashMap<String, u32>,
}
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,
fn_table_index: env.fn_table_index,
lambda_locals: HashMap::new(),
lazy_globals: env.lazy_globals,
}
}
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);
}
}
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 build_lazy_getter_fn(
_ns: &str,
def_name: &str,
val_global_idx: u32,
flag_global_idx: u32,
init_expr: &Calcit,
env: &WasmCompileEnv,
) -> CompiledFn {
let mut probe_ctx = WasmGenCtx::new(0, env.clone());
match emit_expr(&mut probe_ctx, init_expr) {
Ok(()) => {
let mut ctx = WasmGenCtx::new(0, env.clone());
ctx.emit(Instruction::GlobalGet(flag_global_idx));
ctx.emit(Instruction::I32Eqz);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Empty));
if emit_expr(&mut ctx, init_expr).is_ok() {
ctx.emit(Instruction::GlobalSet(val_global_idx));
ctx.emit(Instruction::I32Const(1));
ctx.emit(Instruction::GlobalSet(flag_global_idx));
} else {
ctx.emit(Instruction::Drop);
}
ctx.emit(Instruction::End);
ctx.emit(Instruction::GlobalGet(val_global_idx));
CompiledFn {
export_name: None,
params: vec![],
results: vec![ValType::F64],
locals: ctx.extra_locals,
instructions: ctx.instructions,
}
}
Err(e) => {
eprintln!("[wasm] lazy getter for {def_name} failed to compile: {e}");
CompiledFn {
export_name: None,
params: vec![],
results: vec![ValType::F64],
locals: vec![],
instructions: vec![f64_const(0.0)],
}
}
}
}
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(&getter_idx) = ctx.lazy_globals.get(&qualified) {
ctx.emit(Instruction::Call(getter_idx));
} 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, &[])?;
}
Calcit::Symbol { sym, info, .. } => {
let name = sym.as_ref();
if let Some(&idx) = ctx.locals.get(name) {
ctx.emit(Instruction::LocalGet(idx));
return Ok(());
}
let qualified = format!("{}/{}", info.at_ns.as_ref(), name);
if let Some(&getter_idx) = ctx.lazy_globals.get(&qualified).or_else(|| ctx.lazy_globals.get(name)) {
ctx.emit(Instruction::Call(getter_idx));
return Ok(());
}
if let Some(&slot) = ctx.fn_table_index.get(&qualified).or_else(|| ctx.fn_table_index.get(name)) {
ctx.emit(f64_const(slot as f64));
return Ok(());
}
return Err(format!("unsupported WASM expression: {expr}"));
}
_ => 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" 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),
"split" if args_list.len() == 2 => return emit_str_split(ctx, &args_list),
"pairs-map" if args_list.len() == 1 => return emit_pairs_map(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),
"&>=" if args_list.len() == 2 => return emit_cmp(ctx, Instruction::F64Ge, &args_list),
"&<=" if args_list.len() == 2 => return emit_cmp(ctx, Instruction::F64Le, &args_list),
"&>" if args_list.len() == 2 => return emit_cmp(ctx, Instruction::F64Gt, &args_list),
"&<" if args_list.len() == 2 => return emit_cmp(ctx, Instruction::F64Lt, &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" 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" 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 | CalcitProc::Identical => {
if args.len() != 2 {
return Err(format!("&= expects 2 args, got {}", args.len()));
}
let fn_idx = *ctx
.runtime_fn_index
.get("__rt_generic_eq")
.ok_or_else(|| "runtime helper __rt_generic_eq not found".to_string())?;
emit_expr(ctx, &args[0])?;
emit_expr(ctx, &args[1])?;
ctx.emit(Instruction::Call(fn_idx));
ctx.emit(Instruction::F64ConvertI32U);
Ok(())
}
CalcitProc::NativeCompare => {
if args.len() != 2 {
return Err(format!("&compare expects 2 args, got {}", args.len()));
}
emit_expr(ctx, &args[0])?;
emit_expr(ctx, &args[1])?;
let fn_idx = *ctx
.runtime_fn_index
.get("__rt_generic_compare")
.ok_or("__rt_generic_compare not found")?;
ctx.emit(Instruction::Call(fn_idx));
Ok(())
}
CalcitProc::Not => {
if args.len() != 1 {
return Err("not expects 1 arg".into());
}
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::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 => {
if args.len() != 1 {
return Err(format!("nil? expects 1 arg, got {}", args.len()));
}
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("Record mutation (assoc/with) not yet supported in WASM codegen".into())
}
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 => {
for arg in args {
emit_expr(ctx, arg)?;
ctx.emit(Instruction::Drop);
}
ctx.emit(f64_const(0.0)); Ok(())
}
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 => {
if args.len() != 1 {
return Err("bit-not expects 1 arg".into());
}
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::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::NativeListDissoc => emit_list_dissoc(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::NativeMapDestruct => emit_map_destruct(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 => {
if args.len() == 1 {
emit_turn_string(ctx, args)
} else {
emit_str_variadic(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 => {
for arg in args {
emit_expr(ctx, arg)?;
ctx.emit(Instruction::Drop);
}
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitProc::GetEnv => {
for arg in args {
emit_expr(ctx, arg)?;
ctx.emit(Instruction::Drop);
}
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitProc::AtomDeref => {
if args.len() != 1 {
return Err(format!("&atom:deref expects 1 arg, got {}", args.len()));
}
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 => {
if args.len() != 1 {
return Err(format!("turn-tag expects 1 arg, got {}", args.len()));
}
emit_expr(ctx, &args[0])
}
CalcitProc::NativeStructImplTraits | CalcitProc::NativeEnumImplTraits => {
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitProc::RegisterCalcitBuiltinImpls => {
ctx.emit(f64_const(0.0));
Ok(())
}
CalcitProc::Sort => {
if args.is_empty() {
ctx.emit(f64_const(0.0));
} else {
emit_expr(ctx, &args[0])?;
}
Ok(())
}
CalcitProc::Split => emit_str_split(ctx, args),
_ => 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 nil_tag = get_type_tag(ctx, "nil");
let v_local = ctx.alloc_local_typed(ValType::F64);
emit_expr(ctx, &args[0])?;
ctx.emit(Instruction::LocalSet(v_local));
ctx.emit(Instruction::LocalGet(v_local));
ctx.emit(f64_const(0.0));
ctx.emit(Instruction::F64Eq);
ctx.emit(Instruction::If(wasm_encoder::BlockType::Result(ValType::F64)));
ctx.emit(f64_const(nil_tag));
ctx.emit(Instruction::Else);
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);
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_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
}
fn collect_strings_from_expr(expr: &Calcit, strings: &mut Vec<String>) {
match expr {
Calcit::Str(s) => {
strings.push(s.to_string());
}
Calcit::List(xs) => {
for x in xs.iter() {
collect_strings_from_expr(x, strings);
}
}
_ => {}
}
}