use crate::node::{Node, NodeHash};
use crate::store::Store;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use walrus::ir::{BinaryOp, InstrSeqType, LoadKind, MemArg, StoreKind, UnaryOp, Value};
use walrus::{
ConstExpr, ElementItems, ElementKind, FunctionBuilder, FunctionId, GlobalId, InstrSeqBuilder,
LocalId, MemoryId, Module, RefType, TableId, TypeId, ValType,
};
#[derive(Debug)]
pub enum LowerError {
Store(crate::store::Error),
NotAModule,
Hole,
Effectful(String),
UnknownCallee(String),
UnresolvedRef(String),
Unsupported(&'static str),
}
impl std::fmt::Display for LowerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LowerError::Store(e) => write!(f, "store error: {e}"),
LowerError::NotAModule => write!(f, "lowering root must be a Module"),
LowerError::Hole => write!(f, "cannot lower an incomplete program (hole)"),
LowerError::Effectful(n) => {
write!(f, "function `{n}` declares effects; step 3 lowers pure functions only")
}
LowerError::UnknownCallee(n) => write!(f, "call to unknown function `{n}`"),
LowerError::UnresolvedRef(n) => write!(f, "unresolved reference: `{n}`"),
LowerError::Unsupported(w) => write!(f, "not lowerable yet: {w}"),
}
}
}
impl std::error::Error for LowerError {}
impl From<crate::store::Error> for LowerError {
fn from(e: crate::store::Error) -> Self {
LowerError::Store(e)
}
}
type LowerResult<T> = std::result::Result<T, LowerError>;
struct Ctx<'a> {
store: &'a Store,
fns: &'a HashMap<String, FunctionId>,
fn_table_idx: &'a HashMap<String, i64>,
lambdas: &'a HashMap<NodeHash, (i64, Vec<String>)>,
func_table: TableId,
indirect_types: &'a HashMap<usize, TypeId>,
recs: &'a HashMap<String, Vec<String>>,
vars: &'a HashMap<String, Vec<(String, Vec<String>)>>,
ftags: &'a HashMap<String, i64>,
fallible: &'a HashSet<String>,
locals: &'a RefCell<&'a mut walrus::ModuleLocals>,
alloc: FunctionId,
set: FunctionId,
get: FunctionId,
map_get: FunctionId,
map_try_get: FunctionId,
now: FunctionId,
log: FunctionId,
publish: FunctionId,
set_header: FunctionId,
rand: FunctionId,
disk_write: FunctionId,
disk_read: FunctionId,
net_get: FunctionId,
db_query: FunctionId,
str_concat: FunctionId,
str_slice: FunctionId,
str_lower: FunctionId,
str_from_code: FunctionId,
str_eq: FunctionId,
str_contains: FunctionId,
str_starts_with: FunctionId,
str_index_of: FunctionId,
num_to_str: FunctionId,
str_to_num: FunctionId,
str_to_num_opt: FunctionId,
list_cons: FunctionId,
list_get: FunctionId,
list_try_get: FunctionId,
}
#[derive(Clone, Copy)]
enum Slot {
Local(LocalId),
Member { base: LocalId, off: i64 },
}
fn build_alloc(wasm: &mut Module, bump: GlobalId, mem: MemoryId) -> FunctionId {
let mut fb = FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
let size = wasm.locals.add(ValType::I64);
let old = wasm.locals.add(ValType::I64);
let new = wasm.locals.add(ValType::I64);
let cur = wasm.locals.add(ValType::I64);
{
let mut b = fb.func_body();
b.global_get(bump).local_set(old);
b.local_get(old)
.local_get(size)
.binop(BinaryOp::I64Add)
.local_set(new);
b.local_get(new).global_set(bump);
b.memory_size(mem)
.unop(UnaryOp::I64ExtendUI32)
.i64_const(65536)
.binop(BinaryOp::I64Mul)
.local_set(cur);
b.local_get(new).local_get(cur).binop(BinaryOp::I64GtS);
b.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(new)
.local_get(cur)
.binop(BinaryOp::I64Sub)
.i64_const(65535)
.binop(BinaryOp::I64Add)
.i64_const(65536)
.binop(BinaryOp::I64DivS)
.unop(UnaryOp::I32WrapI64);
t.memory_grow(mem);
t.drop(); },
|_| {},
);
b.local_get(old); }
fb.finish(vec![size], &mut wasm.funcs)
}
fn build_set(wasm: &mut Module, mem: MemoryId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64, ValType::I64],
&[ValType::I64],
);
let p = wasm.locals.add(ValType::I64);
let off = wasm.locals.add(ValType::I64);
let v = wasm.locals.add(ValType::I64);
{
let mut b = fb.func_body();
b.local_get(p)
.local_get(off)
.binop(BinaryOp::I64Add)
.unop(UnaryOp::I32WrapI64) .local_get(v)
.store(
mem,
StoreKind::I64 { atomic: false },
MemArg { align: 3, offset: 0 },
)
.local_get(p); }
fb.finish(vec![p, off, v], &mut wasm.funcs)
}
fn build_get(wasm: &mut Module, mem: MemoryId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let p = wasm.locals.add(ValType::I64);
let off = wasm.locals.add(ValType::I64);
{
let mut b = fb.func_body();
b.local_get(p)
.local_get(off)
.binop(BinaryOp::I64Add)
.unop(UnaryOp::I32WrapI64)
.load(
mem,
LoadKind::I64 { atomic: false },
MemArg { align: 3, offset: 0 },
);
}
fb.finish(vec![p, off], &mut wasm.funcs)
}
fn build_map_get(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let p = wasm.locals.add(ValType::I64);
let key = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let count = wasm.locals.add(ValType::I64);
let k = wasm.locals.add(ValType::I64);
let val = wasm.locals.add(ValType::I64);
{
let mut b = fb.func_body();
b.local_get(p).i64_const(0).call(get).local_set(count);
b.i64_const(0).local_set(i);
b.i64_const(0).local_set(val);
b.block(InstrSeqType::Simple(None), |outer| {
let oid = outer.id();
outer.loop_(InstrSeqType::Simple(None), |lp| {
let lid = lp.id();
lp.local_get(i).local_get(count).binop(BinaryOp::I64GeS);
lp.br_if(oid);
lp.local_get(p);
lp.local_get(i)
.i64_const(16)
.binop(BinaryOp::I64Mul)
.i64_const(8)
.binop(BinaryOp::I64Add);
lp.call(get);
lp.local_set(k);
lp.local_get(k).local_get(key).binop(BinaryOp::I64Eq);
lp.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(p);
t.local_get(i)
.i64_const(16)
.binop(BinaryOp::I64Mul)
.i64_const(16)
.binop(BinaryOp::I64Add);
t.call(get);
t.local_set(val);
t.br(oid);
},
|_| {},
);
lp.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
lp.br(lid);
});
});
b.local_get(val);
}
fb.finish(vec![p, key], &mut wasm.funcs)
}
fn build_map_try_get(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let p = wasm.locals.add(ValType::I64);
let key = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let count = wasm.locals.add(ValType::I64);
let k = wasm.locals.add(ValType::I64);
let val = wasm.locals.add(ValType::I64);
let found = wasm.locals.add(ValType::I64);
let opt = wasm.locals.add(ValType::I64);
{
let mut b = fb.func_body();
b.local_get(p).i64_const(0).call(get).local_set(count);
b.i64_const(0).local_set(i);
b.i64_const(0).local_set(found);
b.i64_const(0).local_set(val);
b.block(InstrSeqType::Simple(None), |outer| {
let oid = outer.id();
outer.loop_(InstrSeqType::Simple(None), |lp| {
let lid = lp.id();
lp.local_get(i).local_get(count).binop(BinaryOp::I64GeS);
lp.br_if(oid);
lp.local_get(p);
lp.local_get(i)
.i64_const(16)
.binop(BinaryOp::I64Mul)
.i64_const(8)
.binop(BinaryOp::I64Add);
lp.call(get);
lp.local_set(k);
lp.local_get(k).local_get(key).binop(BinaryOp::I64Eq);
lp.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(p);
t.local_get(i)
.i64_const(16)
.binop(BinaryOp::I64Mul)
.i64_const(16)
.binop(BinaryOp::I64Add);
t.call(get);
t.local_set(val);
t.i64_const(1).local_set(found);
t.br(oid);
},
|_| {},
);
lp.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
lp.br(lid);
});
});
b.i64_const(16).call(alloc).local_set(opt);
b.local_get(found).i64_const(1).binop(BinaryOp::I64Eq);
b.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(opt).i64_const(0).i64_const(1).call(set).drop();
t.local_get(opt).i64_const(8).local_get(val).call(set).drop();
},
|e| {
e.local_get(opt).i64_const(0).i64_const(0).call(set).drop();
},
);
b.local_get(opt);
}
fb.finish(vec![p, key], &mut wasm.funcs)
}
fn build_str_concat(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let a = wasm.locals.add(ValType::I64);
let b = wasm.locals.add(ValType::I64);
let la = wasm.locals.add(ValType::I64);
let lb = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let dst = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(a).i64_const(0).call(get).local_set(la);
bd.local_get(b).i64_const(0).call(get).local_set(lb);
bd.local_get(la)
.local_get(lb)
.binop(BinaryOp::I64Add)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul)
.call(alloc)
.local_set(dst);
bd.local_get(dst)
.i64_const(0)
.local_get(la)
.local_get(lb)
.binop(BinaryOp::I64Add)
.call(set)
.drop();
bd.i64_const(0).local_set(i);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(la).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(dst);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.local_get(a);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.call(set);
l.drop();
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.i64_const(0).local_set(i);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(lb).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(dst);
l.local_get(la)
.local_get(i)
.binop(BinaryOp::I64Add)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.local_get(b);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.call(set);
l.drop();
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.local_get(dst);
}
fb.finish(vec![a, b], &mut wasm.funcs)
}
fn build_str_slice(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64, ValType::I64],
&[ValType::I64],
);
let s = wasm.locals.add(ValType::I64);
let start = wasm.locals.add(ValType::I64);
let n = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let dst = wasm.locals.add(ValType::I64);
let ls = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(s).i64_const(0).call(get).local_set(ls);
bd.local_get(start).i64_const(0).binop(BinaryOp::I64LtS);
bd.local_get(n).i64_const(0).binop(BinaryOp::I64LtS);
bd.binop(BinaryOp::I32Or);
bd.local_get(start)
.local_get(n)
.binop(BinaryOp::I64Add)
.local_get(ls)
.binop(BinaryOp::I64GtS);
bd.binop(BinaryOp::I32Or);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.unreachable();
},
|_| {},
);
bd.local_get(n)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul)
.call(alloc)
.local_set(dst);
bd.local_get(dst)
.i64_const(0)
.local_get(n)
.call(set)
.drop();
bd.i64_const(0).local_set(i);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(n).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(dst);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.local_get(s);
l.local_get(start)
.local_get(i)
.binop(BinaryOp::I64Add)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.call(set);
l.drop();
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.local_get(dst);
}
fb.finish(vec![s, start, n], &mut wasm.funcs)
}
fn build_str_lower(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64],
&[ValType::I64],
);
let s = wasm.locals.add(ValType::I64);
let n = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let dst = wasm.locals.add(ValType::I64);
let ch = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(s).i64_const(0).call(get).local_set(n);
bd.local_get(n)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul)
.call(alloc)
.local_set(dst);
bd.local_get(dst)
.i64_const(0)
.local_get(n)
.call(set)
.drop();
bd.i64_const(0).local_set(i);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(n).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(s);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get).local_set(ch);
l.local_get(ch).i64_const(65).binop(BinaryOp::I64GeS);
l.local_get(ch).i64_const(90).binop(BinaryOp::I64LeS);
l.binop(BinaryOp::I32And);
l.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(ch)
.i64_const(32)
.binop(BinaryOp::I64Add)
.local_set(ch);
},
|_| {},
);
l.local_get(dst);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.local_get(ch);
l.call(set);
l.drop();
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.local_get(dst);
}
fb.finish(vec![s], &mut wasm.funcs)
}
fn build_str_from_code(
wasm: &mut Module,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64],
&[ValType::I64],
);
let code = wasm.locals.add(ValType::I64);
let dst = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.i64_const(16).call(alloc).local_set(dst);
bd.local_get(dst)
.i64_const(0)
.i64_const(1)
.call(set)
.drop();
bd.local_get(dst)
.i64_const(8)
.local_get(code)
.call(set)
.drop();
bd.local_get(dst);
}
fb.finish(vec![code], &mut wasm.funcs)
}
fn build_list_get(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let ptr = wasm.locals.add(ValType::I64);
let idx = wasm.locals.add(ValType::I64);
let len = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(ptr).i64_const(0).call(get).local_set(len);
bd.local_get(idx).i64_const(0).binop(BinaryOp::I64LtS);
bd.local_get(idx).local_get(len).binop(BinaryOp::I64GeS);
bd.binop(BinaryOp::I32Or);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.unreachable();
},
|_| {},
);
bd.local_get(ptr);
bd.local_get(idx)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
bd.call(get); }
fb.finish(vec![ptr, idx], &mut wasm.funcs)
}
fn build_list_try_get(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let ptr = wasm.locals.add(ValType::I64);
let idx = wasm.locals.add(ValType::I64);
let len = wasm.locals.add(ValType::I64);
let opt = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(ptr).i64_const(0).call(get).local_set(len);
bd.i64_const(16).call(alloc).local_set(opt);
bd.local_get(idx).i64_const(0).binop(BinaryOp::I64LtS);
bd.local_get(idx).local_get(len).binop(BinaryOp::I64GeS);
bd.binop(BinaryOp::I32Or);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(opt).i64_const(0).i64_const(0).call(set).drop();
},
|e| {
e.local_get(opt).i64_const(0).i64_const(1).call(set).drop();
e.local_get(opt).i64_const(8);
e.local_get(ptr);
e.local_get(idx)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
e.call(get);
e.call(set).drop();
},
);
bd.local_get(opt);
}
fb.finish(vec![ptr, idx], &mut wasm.funcs)
}
fn build_str_eq(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let a = wasm.locals.add(ValType::I64);
let b = wasm.locals.add(ValType::I64);
let la = wasm.locals.add(ValType::I64);
let lb = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let res = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(a).i64_const(0).call(get).local_set(la);
bd.local_get(b).i64_const(0).call(get).local_set(lb);
bd.i64_const(0).local_set(i);
bd.local_get(la).local_get(lb).binop(BinaryOp::I64Ne);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(0).local_set(res);
},
|e| {
e.i64_const(1).local_set(res);
e.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(la).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(a);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.local_get(b);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.binop(BinaryOp::I64Ne);
l.if_else(
InstrSeqType::Simple(None),
|t2| {
t2.i64_const(0).local_set(res);
t2.br(oid);
},
|_| {},
);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
},
);
bd.local_get(res);
}
fb.finish(vec![a, b], &mut wasm.funcs)
}
fn build_str_contains(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let h = wasm.locals.add(ValType::I64);
let n = wasm.locals.add(ValType::I64);
let lh = wasm.locals.add(ValType::I64);
let ln = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let j = wasm.locals.add(ValType::I64);
let m = wasm.locals.add(ValType::I64);
let res = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(h).i64_const(0).call(get).local_set(lh);
bd.local_get(n).i64_const(0).call(get).local_set(ln);
bd.local_get(ln).i64_const(0).binop(BinaryOp::I64Eq);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(1).local_set(res);
},
|e| {
e.local_get(ln).local_get(lh).binop(BinaryOp::I64LeS);
e.if_else(
InstrSeqType::Simple(None),
|s2| {
s2.i64_const(0).local_set(i);
s2.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i)
.local_get(ln)
.binop(BinaryOp::I64Add)
.local_get(lh)
.binop(BinaryOp::I64GtS);
l.br_if(oid);
l.i64_const(1).local_set(m);
l.i64_const(0).local_set(j);
l.block(InstrSeqType::Simple(None), |ib| {
let ibid = ib.id();
ib.loop_(InstrSeqType::Simple(None), |il| {
let ilid = il.id();
il.local_get(j)
.local_get(ln)
.binop(BinaryOp::I64GeS);
il.br_if(ibid);
il.local_get(h);
il.local_get(i)
.local_get(j)
.binop(BinaryOp::I64Add)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
il.call(get);
il.local_get(n);
il.local_get(j)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
il.call(get);
il.binop(BinaryOp::I64Ne);
il.if_else(
InstrSeqType::Simple(None),
|mm| {
mm.i64_const(0).local_set(m);
mm.br(ibid);
},
|_| {},
);
il.local_get(j)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(j);
il.br(ilid);
});
});
l.local_get(m).i64_const(1).binop(BinaryOp::I64Eq);
l.if_else(
InstrSeqType::Simple(None),
|fm| {
fm.i64_const(1).local_set(res);
fm.br(oid);
},
|_| {},
);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
},
|_| {},
);
},
);
bd.local_get(res);
}
fb.finish(vec![h, n], &mut wasm.funcs)
}
fn build_str_index_of(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let h = wasm.locals.add(ValType::I64);
let n = wasm.locals.add(ValType::I64);
let lh = wasm.locals.add(ValType::I64);
let ln = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let j = wasm.locals.add(ValType::I64);
let m = wasm.locals.add(ValType::I64);
let res = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(h).i64_const(0).call(get).local_set(lh);
bd.local_get(n).i64_const(0).call(get).local_set(ln);
bd.i64_const(-1).local_set(res); bd.local_get(ln).i64_const(0).binop(BinaryOp::I64Eq);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(0).local_set(res);
},
|e| {
e.local_get(ln).local_get(lh).binop(BinaryOp::I64LeS);
e.if_else(
InstrSeqType::Simple(None),
|s2| {
s2.i64_const(0).local_set(i);
s2.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i)
.local_get(ln)
.binop(BinaryOp::I64Add)
.local_get(lh)
.binop(BinaryOp::I64GtS);
l.br_if(oid);
l.i64_const(1).local_set(m);
l.i64_const(0).local_set(j);
l.block(InstrSeqType::Simple(None), |ib| {
let ibid = ib.id();
ib.loop_(InstrSeqType::Simple(None), |il| {
let ilid = il.id();
il.local_get(j)
.local_get(ln)
.binop(BinaryOp::I64GeS);
il.br_if(ibid);
il.local_get(h);
il.local_get(i)
.local_get(j)
.binop(BinaryOp::I64Add)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
il.call(get);
il.local_get(n);
il.local_get(j)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
il.call(get);
il.binop(BinaryOp::I64Ne);
il.if_else(
InstrSeqType::Simple(None),
|mm| {
mm.i64_const(0).local_set(m);
mm.br(ibid);
},
|_| {},
);
il.local_get(j)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(j);
il.br(ilid);
});
});
l.local_get(m).i64_const(1).binop(BinaryOp::I64Eq);
l.if_else(
InstrSeqType::Simple(None),
|fm| {
fm.local_get(i).local_set(res);
fm.br(oid);
},
|_| {},
);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
},
|_| {},
);
},
);
bd.local_get(res);
}
fb.finish(vec![h, n], &mut wasm.funcs)
}
fn build_str_starts_with(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let s = wasm.locals.add(ValType::I64);
let p = wasm.locals.add(ValType::I64);
let ls = wasm.locals.add(ValType::I64);
let lp = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let res = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(s).i64_const(0).call(get).local_set(ls);
bd.local_get(p).i64_const(0).call(get).local_set(lp);
bd.i64_const(0).local_set(i);
bd.local_get(lp).local_get(ls).binop(BinaryOp::I64GtS);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(0).local_set(res);
},
|e| {
e.i64_const(1).local_set(res);
e.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(lp).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(s);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.local_get(p);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.binop(BinaryOp::I64Ne);
l.if_else(
InstrSeqType::Simple(None),
|t2| {
t2.i64_const(0).local_set(res);
t2.br(oid);
},
|_| {},
);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
},
);
bd.local_get(res);
}
fb.finish(vec![s, p], &mut wasm.funcs)
}
fn build_num_to_str(
wasm: &mut Module,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb =
FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
let n = wasm.locals.add(ValType::I64);
let neg = wasm.locals.add(ValType::I64);
let m = wasm.locals.add(ValType::I64);
let cnt = wasm.locals.add(ValType::I64);
let len = wasm.locals.add(ValType::I64);
let dst = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let q = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(n).i64_const(0).binop(BinaryOp::I64LtS);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(1).local_set(neg);
},
|e| {
e.i64_const(0).local_set(neg);
},
);
bd.local_get(n).i64_const(0).binop(BinaryOp::I64GtS);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(0)
.local_get(n)
.binop(BinaryOp::I64Sub)
.local_set(m);
},
|e| {
e.local_get(n).local_set(m);
},
);
bd.local_get(m).i64_const(0).binop(BinaryOp::I64Eq);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(16).call(alloc).local_set(dst);
t.local_get(dst).i64_const(0).i64_const(1).call(set).drop();
t.local_get(dst).i64_const(8).i64_const(48).call(set).drop();
},
|e| {
e.i64_const(0).local_set(cnt);
e.local_get(m).local_set(q);
e.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(cnt)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(cnt);
l.local_get(q)
.i64_const(10)
.binop(BinaryOp::I64DivS)
.local_set(q);
l.local_get(q).i64_const(0).binop(BinaryOp::I64Eq);
l.br_if(oid);
l.br(lid);
});
});
e.local_get(cnt)
.local_get(neg)
.binop(BinaryOp::I64Add)
.local_set(len);
e.local_get(len)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul)
.call(alloc)
.local_set(dst);
e.local_get(dst).i64_const(0).local_get(len).call(set).drop();
e.local_get(len)
.i64_const(1)
.binop(BinaryOp::I64Sub)
.local_set(i);
e.local_get(m).local_set(q);
e.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(dst);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.local_get(q)
.i64_const(10)
.binop(BinaryOp::I64RemS)
.i64_const(-1)
.binop(BinaryOp::I64Mul)
.i64_const(48)
.binop(BinaryOp::I64Add);
l.call(set).drop();
l.local_get(q)
.i64_const(10)
.binop(BinaryOp::I64DivS)
.local_set(q);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Sub)
.local_set(i);
l.local_get(q).i64_const(0).binop(BinaryOp::I64Eq);
l.br_if(oid);
l.br(lid);
});
});
e.local_get(neg).i64_const(0).binop(BinaryOp::I64Ne);
e.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(dst)
.i64_const(8)
.i64_const(45)
.call(set)
.drop();
},
|_| {},
);
},
);
bd.local_get(dst);
}
fb.finish(vec![n], &mut wasm.funcs)
}
fn build_str_to_num(wasm: &mut Module, get: FunctionId) -> FunctionId {
let mut fb =
FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
let s = wasm.locals.add(ValType::I64);
let ls = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let neg = wasm.locals.add(ValType::I64);
let acc = wasm.locals.add(ValType::I64);
let ch = wasm.locals.add(ValType::I64);
let res = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(s).i64_const(0).call(get).local_set(ls);
bd.i64_const(0).local_set(i);
bd.i64_const(0).local_set(neg);
bd.i64_const(0).local_set(acc);
bd.local_get(ls).i64_const(0).binop(BinaryOp::I64GtS);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(s).i64_const(8).call(get).i64_const(45);
t.binop(BinaryOp::I64Eq);
t.if_else(
InstrSeqType::Simple(None),
|t2| {
t2.i64_const(1).local_set(neg);
t2.i64_const(1).local_set(i);
},
|_| {},
);
},
|_| {},
);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(ls).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(s);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get).local_set(ch);
l.local_get(ch).i64_const(48).binop(BinaryOp::I64LtS);
l.local_get(ch).i64_const(57).binop(BinaryOp::I64GtS);
l.binop(BinaryOp::I32Or); l.br_if(oid);
l.local_get(acc).i64_const(10).binop(BinaryOp::I64Mul);
l.local_get(ch).i64_const(48).binop(BinaryOp::I64Sub);
l.binop(BinaryOp::I64Add).local_set(acc);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.local_get(neg).i64_const(1).binop(BinaryOp::I64Eq);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(0)
.local_get(acc)
.binop(BinaryOp::I64Sub)
.local_set(res);
},
|e| {
e.local_get(acc).local_set(res);
},
);
bd.local_get(res);
}
fb.finish(vec![s], &mut wasm.funcs)
}
fn build_str_to_num_opt(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb =
FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
let s = wasm.locals.add(ValType::I64);
let ls = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
let neg = wasm.locals.add(ValType::I64);
let acc = wasm.locals.add(ValType::I64);
let ch = wasm.locals.add(ValType::I64);
let ok = wasm.locals.add(ValType::I64);
let dc = wasm.locals.add(ValType::I64);
let res = wasm.locals.add(ValType::I64);
let p = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(s).i64_const(0).call(get).local_set(ls);
bd.i64_const(0).local_set(i);
bd.i64_const(0).local_set(neg);
bd.i64_const(0).local_set(acc);
bd.i64_const(1).local_set(ok);
bd.i64_const(0).local_set(dc);
bd.local_get(ls).i64_const(0).binop(BinaryOp::I64GtS);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(s).i64_const(8).call(get).i64_const(45);
t.binop(BinaryOp::I64Eq);
t.if_else(
InstrSeqType::Simple(None),
|t2| {
t2.i64_const(1).local_set(neg);
t2.i64_const(1).local_set(i);
},
|_| {},
);
},
|_| {},
);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(ls).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(s);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get).local_set(ch);
l.local_get(ch).i64_const(48).binop(BinaryOp::I64LtS);
l.local_get(ch).i64_const(57).binop(BinaryOp::I64GtS);
l.binop(BinaryOp::I32Or);
l.if_else(
InstrSeqType::Simple(None),
|bad| {
bad.i64_const(0).local_set(ok);
bad.br(oid);
},
|_| {},
);
l.local_get(acc).i64_const(10).binop(BinaryOp::I64Mul);
l.local_get(ch).i64_const(48).binop(BinaryOp::I64Sub);
l.binop(BinaryOp::I64Add).local_set(acc);
l.local_get(dc).i64_const(1).binop(BinaryOp::I64Add).local_set(dc);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.local_get(dc).i64_const(0).binop(BinaryOp::I64Eq);
bd.if_else(
InstrSeqType::Simple(None),
|z| {
z.i64_const(0).local_set(ok);
},
|_| {},
);
bd.local_get(neg).i64_const(1).binop(BinaryOp::I64Eq);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.i64_const(0)
.local_get(acc)
.binop(BinaryOp::I64Sub)
.local_set(res);
},
|e| {
e.local_get(acc).local_set(res);
},
);
bd.i64_const(16).call(alloc).local_set(p);
bd.local_get(ok).i64_const(0).binop(BinaryOp::I64Ne);
bd.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(p).i64_const(0).i64_const(1).call(set).drop();
t.local_get(p).i64_const(8).local_get(res).call(set).drop();
},
|e| {
e.local_get(p).i64_const(0).i64_const(0).call(set).drop();
},
);
bd.local_get(p);
}
fb.finish(vec![s], &mut wasm.funcs)
}
fn build_list_cons(
wasm: &mut Module,
get: FunctionId,
set: FunctionId,
alloc: FunctionId,
) -> FunctionId {
let mut fb = FunctionBuilder::new(
&mut wasm.types,
&[ValType::I64, ValType::I64],
&[ValType::I64],
);
let head = wasm.locals.add(ValType::I64);
let tail = wasm.locals.add(ValType::I64);
let tl = wasm.locals.add(ValType::I64);
let nl = wasm.locals.add(ValType::I64);
let dst = wasm.locals.add(ValType::I64);
let i = wasm.locals.add(ValType::I64);
{
let mut bd = fb.func_body();
bd.local_get(tail).i64_const(0).call(get).local_set(tl);
bd.local_get(tl).i64_const(1).binop(BinaryOp::I64Add).local_set(nl);
bd.local_get(nl)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul)
.call(alloc)
.local_set(dst);
bd.local_get(dst).i64_const(0).local_get(nl).call(set).drop();
bd.local_get(dst).i64_const(8).local_get(head).call(set).drop();
bd.i64_const(0).local_set(i);
bd.block(InstrSeqType::Simple(None), |o| {
let oid = o.id();
o.loop_(InstrSeqType::Simple(None), |l| {
let lid = l.id();
l.local_get(i).local_get(tl).binop(BinaryOp::I64GeS);
l.br_if(oid);
l.local_get(dst);
l.local_get(i)
.i64_const(2)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.local_get(tail);
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.i64_const(8)
.binop(BinaryOp::I64Mul);
l.call(get);
l.call(set);
l.drop();
l.local_get(i)
.i64_const(1)
.binop(BinaryOp::I64Add)
.local_set(i);
l.br(lid);
});
});
bd.local_get(dst);
}
fb.finish(vec![head, tail], &mut wasm.funcs)
}
fn free_vars(
store: &Store,
hash: &NodeHash,
bound: &HashSet<String>,
acc: &mut Vec<String>,
) -> LowerResult<()> {
let Some(node) = store.get(hash)? else {
return Ok(());
};
match node {
Node::Ref(n) => {
if !bound.contains(&n) && !acc.contains(&n) {
acc.push(n);
}
}
Node::Lambda { params, body } => {
let mut b2 = bound.clone();
for p in ¶ms {
b2.insert(p.name.clone());
}
free_vars(store, &body, &b2, acc)?;
}
Node::Match {
scrutinee, arms, ..
} => {
free_vars(store, &scrutinee, bound, acc)?;
for arm in &arms {
let mut b2 = bound.clone();
for x in &arm.bindings {
b2.insert(x.clone());
}
free_vars(store, &arm.body, &b2, acc)?;
}
}
other => {
for ch in crate::check::child_hashes(&other) {
free_vars(store, ch, bound, acc)?;
}
}
}
Ok(())
}
struct LamInfo {
hash: NodeHash,
params: Vec<crate::node::Param>,
body: NodeHash,
captures: Vec<String>,
}
fn collect_lambdas(
store: &Store,
hash: &NodeHash,
seen: &mut HashSet<NodeHash>,
out: &mut Vec<LamInfo>,
) -> LowerResult<()> {
let Some(node) = store.get(hash)? else {
return Ok(());
};
if let Node::Lambda { params, body } = &node {
if seen.insert(hash.clone()) {
let mut bound = HashSet::new();
for p in params {
bound.insert(p.name.clone());
}
let mut caps = Vec::new();
free_vars(store, body, &bound, &mut caps)?;
out.push(LamInfo {
hash: hash.clone(),
params: params.clone(),
body: body.clone(),
captures: caps,
});
}
return collect_lambdas(store, body, seen, out);
}
for ch in crate::check::child_hashes(&node) {
collect_lambdas(store, ch, seen, out)?;
}
Ok(())
}
pub fn lower(store: &Store, module_hash: &NodeHash) -> LowerResult<Vec<u8>> {
let Some(Node::Module {
types, functions, ..
}) = store.get(module_hash)?
else {
return Err(LowerError::NotAModule);
};
let mut wasm = Module::default();
let mut recs: HashMap<String, Vec<String>> = HashMap::new();
let mut vars: HashMap<String, Vec<(String, Vec<String>)>> = HashMap::new();
for th in &types {
match store.get(th)? {
Some(Node::RecordDef { name, fields }) => {
recs.insert(name, fields.into_iter().map(|(n, _)| n).collect());
}
Some(Node::VariantDef { name, cases }) => {
vars.insert(
name,
cases
.into_iter()
.map(|(c, p)| (c, p.into_iter().map(|(n, _)| n).collect()))
.collect(),
);
}
_ => {}
}
}
let mem = wasm.memories.add_local(false, false, 1, None, None);
let bump = wasm.globals.add_local(
ValType::I64,
true,
false,
ConstExpr::Value(Value::I64(16)),
);
let alloc = build_alloc(&mut wasm, bump, mem);
let set = build_set(&mut wasm, mem);
let get = build_get(&mut wasm, mem);
let map_get = build_map_get(&mut wasm, get);
let map_try_get = build_map_try_get(&mut wasm, get, set, alloc);
let str_concat = build_str_concat(&mut wasm, get, set, alloc);
let str_slice = build_str_slice(&mut wasm, get, set, alloc);
let str_lower = build_str_lower(&mut wasm, get, set, alloc);
let str_from_code = build_str_from_code(&mut wasm, set, alloc);
let str_eq = build_str_eq(&mut wasm, get);
let str_contains = build_str_contains(&mut wasm, get);
let str_starts_with = build_str_starts_with(&mut wasm, get);
let str_index_of = build_str_index_of(&mut wasm, get);
let num_to_str = build_num_to_str(&mut wasm, set, alloc);
let str_to_num = build_str_to_num(&mut wasm, get);
let str_to_num_opt = build_str_to_num_opt(&mut wasm, get, set, alloc);
let list_cons = build_list_cons(&mut wasm, get, set, alloc);
let list_get = build_list_get(&mut wasm, get);
let list_try_get = build_list_try_get(&mut wasm, get, set, alloc);
wasm.exports.add("mem", mem);
wasm.exports.add("__alloc", alloc);
let now_ty = wasm.types.add(&[], &[ValType::I64]);
let (now, _) = wasm.add_import_func("host", "now", now_ty);
let log_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
let (log, _) = wasm.add_import_func("host", "log", log_ty);
let publish_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
let (publish, _) = wasm.add_import_func("host", "publish", publish_ty);
let set_header_ty =
wasm.types.add(&[ValType::I64, ValType::I64], &[ValType::I64]);
let (set_header, _) =
wasm.add_import_func("host", "set_header", set_header_ty);
let rand_ty = wasm.types.add(&[], &[ValType::I64]);
let (rand, _) = wasm.add_import_func("host", "rand", rand_ty);
let dw_ty = wasm.types.add(&[ValType::I64, ValType::I64], &[ValType::I64]);
let (disk_write, _) = wasm.add_import_func("host", "disk_write", dw_ty);
let dr_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
let (disk_read, _) = wasm.add_import_func("host", "disk_read", dr_ty);
let ng_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
let (net_get, _) = wasm.add_import_func("host", "net_get", ng_ty);
let dq_ty = wasm.types.add(&[ValType::I64, ValType::I64], &[ValType::I64]);
let (db_query, _) = wasm.add_import_func("host", "db_query", dq_ty);
let mut ftags: HashMap<String, i64> = HashMap::new();
let mut fallible: HashSet<String> = HashSet::new();
for fh in &functions {
if let Some(Node::Function {
name, on_failure, ..
}) = store.get(fh)?
{
if !on_failure.is_empty() {
fallible.insert(name.clone());
}
for f in on_failure {
let next = (ftags.len() as i64) + 1;
ftags.entry(f).or_insert(next);
}
}
}
struct Pending {
is_fallible: bool,
body: Vec<NodeHash>,
result: NodeHash,
scope: HashMap<String, Slot>,
id: FunctionId,
}
let mut fn_ids: HashMap<String, FunctionId> = HashMap::new();
let mut table_ids: Vec<FunctionId> = Vec::new();
let mut fn_table_idx: HashMap<String, i64> = HashMap::new();
let mut lam_table: HashMap<NodeHash, (i64, Vec<String>)> = HashMap::new();
let mut pending: Vec<Pending> = Vec::with_capacity(functions.len());
let mut top_level: Vec<(String, FunctionId, usize)> = Vec::new();
let mut lam_infos: Vec<LamInfo> = Vec::new();
{
let mut seen = HashSet::new();
for fh in &functions {
if let Some(Node::Function { body, result, .. }) = store.get(fh)? {
for s in &body {
collect_lambdas(store, s, &mut seen, &mut lam_infos)?;
}
collect_lambdas(store, &result, &mut seen, &mut lam_infos)?;
}
}
}
for fh in &functions {
let Some(Node::Function {
name,
params,
on_failure,
body,
result,
..
}) = store.get(fh)?
else {
continue;
};
let param_tys = vec![ValType::I64; params.len()];
let fb = FunctionBuilder::new(&mut wasm.types, ¶m_tys, &[ValType::I64]);
let mut scope: HashMap<String, Slot> = HashMap::new();
let mut param_locals = Vec::with_capacity(params.len());
for p in ¶ms {
let l = wasm.locals.add(ValType::I64);
scope.insert(p.name.clone(), Slot::Local(l));
param_locals.push(l);
}
let id = fb.finish(param_locals, &mut wasm.funcs);
wasm.exports.add(&name, id);
top_level.push((name.clone(), id, params.len()));
fn_ids.insert(name, id);
pending.push(Pending {
is_fallible: !on_failure.is_empty(),
body,
result,
scope,
id,
});
}
let mut max_arity = 0usize;
for (name, real_id, arity) in &top_level {
max_arity = max_arity.max(*arity);
let sig: Vec<ValType> = vec![ValType::I64; arity + 1];
let fb = FunctionBuilder::new(&mut wasm.types, &sig, &[ValType::I64]);
let arg_locals: Vec<LocalId> =
(0..arity + 1).map(|_| wasm.locals.add(ValType::I64)).collect();
let tid = {
let mut b = fb;
{
let mut body = b.func_body();
for l in arg_locals.iter().take(*arity) {
body.local_get(*l);
}
body.call(*real_id); }
b.finish(arg_locals, &mut wasm.funcs)
};
let idx = table_ids.len() as i64;
table_ids.push(tid);
fn_table_idx.insert(name.clone(), idx);
}
for li in &lam_infos {
let arity = li.params.len();
max_arity = max_arity.max(arity);
let sig: Vec<ValType> = vec![ValType::I64; arity + 1];
let fb = FunctionBuilder::new(&mut wasm.types, &sig, &[ValType::I64]);
let mut scope: HashMap<String, Slot> = HashMap::new();
let mut locals = Vec::with_capacity(arity + 1);
for p in &li.params {
let l = wasm.locals.add(ValType::I64);
scope.insert(p.name.clone(), Slot::Local(l));
locals.push(l);
}
let env = wasm.locals.add(ValType::I64);
locals.push(env);
for (j, cap) in li.captures.iter().enumerate() {
scope.insert(
cap.clone(),
Slot::Member {
base: env,
off: (j as i64) * 8,
},
);
}
let id = fb.finish(locals, &mut wasm.funcs);
let idx = table_ids.len() as i64;
table_ids.push(id);
lam_table.insert(li.hash.clone(), (idx, li.captures.clone()));
pending.push(Pending {
is_fallible: false,
body: vec![],
result: li.body.clone(),
scope,
id,
});
}
let tlen = table_ids.len() as u64;
let func_table =
wasm.tables
.add_local(false, tlen, Some(tlen), RefType::Funcref);
if !table_ids.is_empty() {
wasm.elements.add(
ElementKind::Active {
table: func_table,
offset: ConstExpr::Value(Value::I32(0)),
},
ElementItems::Functions(table_ids.clone()),
);
}
let mut indirect_types: HashMap<usize, TypeId> = HashMap::new();
for k in 0..=max_arity {
let sig = vec![ValType::I64; k + 1];
let ty = wasm.types.add(&sig, &[ValType::I64]);
indirect_types.insert(k, ty);
}
for mut p in pending {
let locals_cell = RefCell::new(&mut wasm.locals);
let ctx = Ctx {
store,
fns: &fn_ids,
fn_table_idx: &fn_table_idx,
lambdas: &lam_table,
func_table,
indirect_types: &indirect_types,
recs: &recs,
vars: &vars,
ftags: &ftags,
fallible: &fallible,
locals: &locals_cell,
alloc,
set,
get,
map_get,
map_try_get,
now,
log,
publish,
set_header,
rand,
disk_write,
disk_read,
net_get,
db_query,
str_concat,
str_slice,
str_lower,
str_from_code,
str_eq,
str_contains,
str_starts_with,
str_index_of,
num_to_str,
str_to_num,
str_to_num_opt,
list_cons,
list_get,
list_try_get,
};
let lf = wasm.funcs.get_mut(p.id).kind.unwrap_local_mut();
let mut seq = lf.builder_mut().func_body();
for step_hash in &p.body {
match store.get(step_hash)? {
Some(Node::Step { binding, value }) => {
lower_expr(&ctx, &p.scope, &mut seq, &value)?;
let l = locals_cell.borrow_mut().add(ValType::I64);
seq.local_set(l);
p.scope.insert(binding, Slot::Local(l));
}
Some(Node::Hole { .. }) => return Err(LowerError::Hole),
_ => return Err(LowerError::Unsupported("non-step in function body")),
}
}
lower_expr(&ctx, &p.scope, &mut seq, &p.result)?;
if p.is_fallible {
let tmp = locals_cell.borrow_mut().add(ValType::I64);
seq.local_set(tmp);
seq.i64_const(16);
seq.call(alloc); seq.i64_const(0);
seq.i64_const(0);
seq.call(set); seq.i64_const(8);
seq.local_get(tmp);
seq.call(set); }
}
Ok(wasm.emit_wasm())
}
fn lower_expr(
c: &Ctx,
scope: &HashMap<String, Slot>,
seq: &mut InstrSeqBuilder,
hash: &NodeHash,
) -> LowerResult<()> {
match c.store.get(hash)? {
Some(Node::Lit(v)) => {
seq.i64_const(v);
}
Some(Node::FloatLit(bits)) => {
seq.i64_const(bits as i64);
}
Some(Node::FloatOp { op, lhs, rhs }) => {
use crate::node::BinOp::*;
lower_expr(c, scope, seq, &lhs)?;
seq.unop(UnaryOp::F64ReinterpretI64);
lower_expr(c, scope, seq, &rhs)?;
seq.unop(UnaryOp::F64ReinterpretI64);
match op {
Add => {
seq.binop(BinaryOp::F64Add)
.unop(UnaryOp::I64ReinterpretF64);
}
Sub => {
seq.binop(BinaryOp::F64Sub)
.unop(UnaryOp::I64ReinterpretF64);
}
Mul => {
seq.binop(BinaryOp::F64Mul)
.unop(UnaryOp::I64ReinterpretF64);
}
Div => {
seq.binop(BinaryOp::F64Div)
.unop(UnaryOp::I64ReinterpretF64);
}
Eq => {
seq.binop(BinaryOp::F64Eq).unop(UnaryOp::I64ExtendUI32);
}
Lt => {
seq.binop(BinaryOp::F64Lt).unop(UnaryOp::I64ExtendUI32);
}
Le => {
seq.binop(BinaryOp::F64Le).unop(UnaryOp::I64ExtendUI32);
}
Gt => {
seq.binop(BinaryOp::F64Gt).unop(UnaryOp::I64ExtendUI32);
}
Ge => {
seq.binop(BinaryOp::F64Ge).unop(UnaryOp::I64ExtendUI32);
}
Mod | Neq | And | Or => {
return Err(LowerError::Unsupported(
"operator not defined on Float",
));
}
}
}
Some(Node::IntToFloat(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.unop(UnaryOp::F64ConvertSI64)
.unop(UnaryOp::I64ReinterpretF64);
}
Some(Node::FloatToInt(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.unop(UnaryOp::F64ReinterpretI64)
.unop(UnaryOp::I64TruncSF64);
}
Some(Node::DecimalLit(v)) => {
seq.i64_const(v);
}
Some(Node::DecimalOp { op, lhs, rhs }) => {
use crate::node::BinOp::*;
match op {
Mul => {
lower_expr(c, scope, seq, &lhs)?;
lower_expr(c, scope, seq, &rhs)?;
seq.binop(BinaryOp::I64Mul)
.i64_const(10_000)
.binop(BinaryOp::I64DivS);
}
Div => {
lower_expr(c, scope, seq, &lhs)?;
seq.i64_const(10_000).binop(BinaryOp::I64Mul);
lower_expr(c, scope, seq, &rhs)?;
seq.binop(BinaryOp::I64DivS);
}
Add => {
lower_expr(c, scope, seq, &lhs)?;
lower_expr(c, scope, seq, &rhs)?;
seq.binop(BinaryOp::I64Add);
}
Sub => {
lower_expr(c, scope, seq, &lhs)?;
lower_expr(c, scope, seq, &rhs)?;
seq.binop(BinaryOp::I64Sub);
}
Eq | Neq | Lt | Le | Gt | Ge => {
lower_expr(c, scope, seq, &lhs)?;
lower_expr(c, scope, seq, &rhs)?;
match op {
Eq => seq.binop(BinaryOp::I64Eq),
Neq => seq.binop(BinaryOp::I64Ne),
Lt => seq.binop(BinaryOp::I64LtS),
Le => seq.binop(BinaryOp::I64LeS),
Gt => seq.binop(BinaryOp::I64GtS),
Ge => seq.binop(BinaryOp::I64GeS),
_ => unreachable!(),
};
seq.unop(UnaryOp::I64ExtendUI32);
}
Mod | And | Or => {
return Err(LowerError::Unsupported(
"operator not defined on Decimal",
));
}
}
}
Some(Node::IntToDecimal(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.i64_const(10_000).binop(BinaryOp::I64Mul);
}
Some(Node::DecimalToInt(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.i64_const(10_000).binop(BinaryOp::I64DivS);
}
Some(Node::DecimalRaw(a)) => {
lower_expr(c, scope, seq, &a)?;
}
Some(Node::Bool(b)) => {
seq.i64_const(if b { 1 } else { 0 });
}
Some(Node::Not(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.unop(UnaryOp::I64Eqz).unop(UnaryOp::I64ExtendUI32);
}
Some(Node::Ref(name)) => {
match scope
.get(&name)
.ok_or(LowerError::UnresolvedRef(name.clone()))?
{
Slot::Local(l) => {
seq.local_get(*l);
}
Slot::Member { base, off } => {
seq.local_get(*base);
seq.i64_const(*off);
seq.call(c.get);
}
}
}
Some(Node::Call { func, args }) => {
for a in &args {
lower_expr(c, scope, seq, a)?;
}
let id = *c
.fns
.get(&func)
.ok_or(LowerError::UnknownCallee(func.clone()))?;
seq.call(id);
if c.fallible.contains(&func) {
let r = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(r);
seq.local_get(r);
seq.i64_const(0);
seq.call(c.get); seq.i64_const(0);
seq.binop(BinaryOp::I64Ne); seq.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(r);
t.return_();
},
|_| {},
);
seq.local_get(r);
seq.i64_const(8);
seq.call(c.get); }
}
Some(Node::FuncRef(name)) => {
let idx = *c
.fn_table_idx
.get(&name)
.ok_or(LowerError::UnknownCallee(name.clone()))?;
let blk = c.locals.borrow_mut().add(ValType::I64);
seq.i64_const(16);
seq.call(c.alloc);
seq.local_set(blk);
seq.local_get(blk).i64_const(0).i64_const(idx).call(c.set);
seq.drop();
seq.local_get(blk).i64_const(8).i64_const(0).call(c.set);
seq.drop();
seq.local_get(blk);
}
Some(Node::Lambda { .. }) => {
let (idx, captures) = c
.lambdas
.get(hash)
.ok_or(LowerError::Unsupported("lambda not lifted"))?;
let env = c.locals.borrow_mut().add(ValType::I64);
if captures.is_empty() {
seq.i64_const(0);
seq.local_set(env);
} else {
seq.i64_const((8 * captures.len()) as i64);
seq.call(c.alloc);
seq.local_set(env);
for (j, cap) in captures.iter().enumerate() {
seq.local_get(env);
seq.i64_const((j * 8) as i64);
match scope
.get(cap)
.ok_or(LowerError::UnresolvedRef(cap.clone()))?
{
Slot::Local(l) => {
seq.local_get(*l);
}
Slot::Member { base, off } => {
seq.local_get(*base);
seq.i64_const(*off);
seq.call(c.get);
}
}
seq.call(c.set);
seq.drop();
}
}
let idx = *idx;
let blk = c.locals.borrow_mut().add(ValType::I64);
seq.i64_const(16);
seq.call(c.alloc);
seq.local_set(blk);
seq.local_get(blk).i64_const(0).i64_const(idx).call(c.set);
seq.drop();
seq.local_get(blk).i64_const(8).local_get(env).call(c.set);
seq.drop();
seq.local_get(blk);
}
Some(Node::CallValue { callee, args }) => {
lower_expr(c, scope, seq, &callee)?;
let p = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(p);
for a in &args {
lower_expr(c, scope, seq, a)?;
}
seq.local_get(p).i64_const(8).call(c.get); seq.local_get(p).i64_const(0).call(c.get); seq.unop(UnaryOp::I32WrapI64);
let ty = *c.indirect_types.get(&args.len()).ok_or(
LowerError::Unsupported("call_indirect arity not prepared"),
)?;
seq.call_indirect(ty, c.func_table);
}
Some(Node::Step { value, .. }) => {
lower_expr(c, scope, seq, &value)?;
}
Some(Node::BinOp { op, lhs, rhs }) => {
use crate::node::BinOp::*;
match op {
And | Or => {
lower_expr(c, scope, seq, &lhs)?;
seq.unop(UnaryOp::I32WrapI64);
let is_and = matches!(op, And);
let mut terr: Option<LowerError> = None;
let mut eerr: Option<LowerError> = None;
seq.if_else(
InstrSeqType::Simple(Some(ValType::I64)),
|t| {
if is_and {
if let Err(e) = lower_expr(c, scope, t, &rhs) {
terr = Some(e);
}
} else {
t.i64_const(1);
}
},
|e| {
if is_and {
e.i64_const(0);
} else if let Err(er) =
lower_expr(c, scope, e, &rhs)
{
eerr = Some(er);
}
},
);
if let Some(e) = terr.or(eerr) {
return Err(e);
}
}
_ => {
lower_expr(c, scope, seq, &lhs)?;
lower_expr(c, scope, seq, &rhs)?;
match op {
Add => seq.binop(BinaryOp::I64Add),
Sub => seq.binop(BinaryOp::I64Sub),
Mul => seq.binop(BinaryOp::I64Mul),
Div => seq.binop(BinaryOp::I64DivS),
Mod => seq.binop(BinaryOp::I64RemS),
Eq => seq
.binop(BinaryOp::I64Eq)
.unop(UnaryOp::I64ExtendUI32),
Neq => seq
.binop(BinaryOp::I64Ne)
.unop(UnaryOp::I64ExtendUI32),
Lt => seq
.binop(BinaryOp::I64LtS)
.unop(UnaryOp::I64ExtendUI32),
Le => seq
.binop(BinaryOp::I64LeS)
.unop(UnaryOp::I64ExtendUI32),
Gt => seq
.binop(BinaryOp::I64GtS)
.unop(UnaryOp::I64ExtendUI32),
Ge => seq
.binop(BinaryOp::I64GeS)
.unop(UnaryOp::I64ExtendUI32),
And | Or => unreachable!("handled above"),
};
}
}
}
Some(Node::If {
cond,
then_branch,
else_branch,
}) => {
lower_expr(c, scope, seq, &cond)?;
seq.unop(UnaryOp::I32WrapI64);
let mut terr: Option<LowerError> = None;
let mut eerr: Option<LowerError> = None;
seq.if_else(
InstrSeqType::Simple(Some(ValType::I64)),
|t| {
if let Err(e) = lower_expr(c, scope, t, &then_branch) {
terr = Some(e);
}
},
|e| {
if let Err(er) = lower_expr(c, scope, e, &else_branch) {
eerr = Some(er);
}
},
);
if let Some(e) = terr {
return Err(e);
}
if let Some(e) = eerr {
return Err(e);
}
}
Some(Node::Record { type_name, fields }) => {
let order = c
.recs
.get(&type_name)
.ok_or(LowerError::Unsupported("unknown record type in lowering"))?
.clone();
seq.i64_const((order.len() as i64) * 8);
seq.call(c.alloc);
for (i, fname) in order.iter().enumerate() {
let fexpr = fields
.iter()
.find(|(n, _)| n == fname)
.map(|(_, h)| h.clone())
.ok_or(LowerError::Unsupported("missing record field in lowering"))?;
seq.i64_const((i as i64) * 8);
lower_expr(c, scope, seq, &fexpr)?;
seq.call(c.set);
}
}
Some(Node::Field {
base,
type_name,
field,
}) => {
let order = c
.recs
.get(&type_name)
.ok_or(LowerError::Unsupported("unknown record type in lowering"))?;
let idx = order
.iter()
.position(|n| n == &field)
.ok_or(LowerError::Unsupported("unknown field in lowering"))?;
lower_expr(c, scope, seq, &base)?; seq.i64_const((idx as i64) * 8); seq.call(c.get); }
Some(Node::Fail(name)) => {
let tag = *c
.ftags
.get(&name)
.ok_or(LowerError::Unsupported("unknown failure in lowering"))?;
seq.i64_const(16);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(tag);
seq.call(c.set); seq.i64_const(8);
seq.i64_const(0);
seq.call(c.set); seq.return_(); }
Some(Node::Handle { body, handlers }) => {
let Some(Node::Call { func, args }) = c.store.get(&body)? else {
return Err(LowerError::Unsupported(
"Handle body must be a fallible call (v0.2)",
));
};
if !c.fallible.contains(&func) {
return Err(LowerError::Unsupported(
"Handle body must be a fallible call (v0.2)",
));
}
for a in &args {
lower_expr(c, scope, seq, a)?;
}
let id = *c
.fns
.get(&func)
.ok_or(LowerError::UnknownCallee(func.clone()))?;
seq.call(id);
let hb = c.locals.borrow_mut().add(ValType::I64);
let tg = c.locals.borrow_mut().add(ValType::I64);
let res = c.locals.borrow_mut().add(ValType::I64);
let handled = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(hb);
seq.local_get(hb);
seq.i64_const(0);
seq.call(c.get);
seq.local_set(tg); seq.i64_const(0);
seq.local_set(handled);
seq.local_get(tg);
seq.i64_const(0);
seq.binop(BinaryOp::I64Eq);
seq.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(hb);
t.i64_const(8);
t.call(c.get);
t.local_set(res);
t.i64_const(1);
t.local_set(handled);
},
|_| {},
);
for (fname, recover) in &handlers {
let tag = *c
.ftags
.get(fname)
.ok_or(LowerError::Unsupported("unknown handled failure"))?;
seq.local_get(tg);
seq.i64_const(tag);
seq.binop(BinaryOp::I64Eq);
let mut herr: Option<LowerError> = None;
seq.if_else(
InstrSeqType::Simple(None),
|t| match lower_expr(c, scope, t, recover) {
Ok(()) => {
t.local_set(res);
t.i64_const(1);
t.local_set(handled);
}
Err(e) => herr = Some(e),
},
|_| {},
);
if let Some(e) = herr {
return Err(e);
}
}
seq.local_get(handled);
seq.i64_const(0);
seq.binop(BinaryOp::I64Eq);
seq.if_else(
InstrSeqType::Simple(None),
|t| {
t.local_get(hb);
t.return_();
},
|_| {},
);
seq.local_get(res);
}
Some(Node::Variant {
type_name,
case,
fields,
}) => {
let cases = c
.vars
.get(&type_name)
.ok_or(LowerError::Unsupported("unknown variant type in lowering"))?
.clone();
let k = cases
.iter()
.position(|(cn, _)| cn == &case)
.ok_or(LowerError::Unsupported("unknown case in lowering"))?;
let payload = cases[k].1.clone();
seq.i64_const(((1 + payload.len()) as i64) * 8);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(k as i64);
seq.call(c.set); for (i, fname) in payload.iter().enumerate() {
let fe = fields
.iter()
.find(|(n, _)| n == fname)
.map(|(_, h)| h.clone())
.ok_or(LowerError::Unsupported("missing variant field in lowering"))?;
seq.i64_const(((1 + i) as i64) * 8);
lower_expr(c, scope, seq, &fe)?;
seq.call(c.set); }
}
Some(Node::Match {
scrutinee,
type_name,
arms,
}) => {
let cases = c
.vars
.get(&type_name)
.ok_or(LowerError::Unsupported("unknown variant type in lowering"))?
.clone();
lower_expr(c, scope, seq, &scrutinee)?; let sc = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(sc);
seq.local_get(sc);
seq.i64_const(0);
seq.call(c.get);
let tg = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(tg);
let res = c.locals.borrow_mut().add(ValType::I64);
for arm in &arms {
let k = cases
.iter()
.position(|(cn, _)| cn == &arm.case)
.ok_or(LowerError::Unsupported("unknown case in match lowering"))?;
let mut s2 = scope.clone();
for (i, b) in arm.bindings.iter().enumerate() {
s2.insert(
b.clone(),
Slot::Member {
base: sc,
off: ((1 + i) as i64) * 8,
},
);
}
seq.local_get(tg);
seq.i64_const(k as i64);
seq.binop(BinaryOp::I64Eq); let mut berr: Option<LowerError> = None;
seq.if_else(
InstrSeqType::Simple(None),
|t| match lower_expr(c, &s2, t, &arm.body) {
Ok(()) => {
t.local_set(res);
}
Err(e) => berr = Some(e),
},
|_| {},
);
if let Some(e) = berr {
return Err(e);
}
}
seq.local_get(res);
}
Some(Node::Str(s)) => {
let bytes = s.as_bytes();
seq.i64_const(((1 + bytes.len()) as i64) * 8);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(bytes.len() as i64);
seq.call(c.set); for (i, b) in bytes.iter().enumerate() {
seq.i64_const(((1 + i) as i64) * 8);
seq.i64_const(*b as i64);
seq.call(c.set);
}
}
Some(Node::StrLen(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.i64_const(0);
seq.call(c.get); }
Some(Node::StrLower(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.call(c.str_lower); }
Some(Node::StrFromCode(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.call(c.str_from_code); }
Some(Node::StrConcat(a, b)) => {
lower_expr(c, scope, seq, &a)?;
lower_expr(c, scope, seq, &b)?;
seq.call(c.str_concat);
}
Some(Node::StrSlice { s, start, len }) => {
lower_expr(c, scope, seq, &s)?;
lower_expr(c, scope, seq, &start)?;
lower_expr(c, scope, seq, &len)?;
seq.call(c.str_slice);
}
Some(Node::StrEq(a, b)) => {
lower_expr(c, scope, seq, &a)?;
lower_expr(c, scope, seq, &b)?;
seq.call(c.str_eq); }
Some(Node::StrContains { haystack, needle }) => {
lower_expr(c, scope, seq, &haystack)?;
lower_expr(c, scope, seq, &needle)?;
seq.call(c.str_contains); }
Some(Node::StrStartsWith { s, prefix }) => {
lower_expr(c, scope, seq, &s)?;
lower_expr(c, scope, seq, &prefix)?;
seq.call(c.str_starts_with); }
Some(Node::StrIndexOf { haystack, needle }) => {
lower_expr(c, scope, seq, &haystack)?;
lower_expr(c, scope, seq, &needle)?;
seq.call(c.str_index_of); }
Some(Node::NumberToStr(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.call(c.num_to_str); }
Some(Node::StrToNumberOpt(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.call(c.str_to_num_opt); }
Some(Node::StrToNumber(a)) => {
lower_expr(c, scope, seq, &a)?;
seq.call(c.str_to_num); }
Some(Node::Now) => {
seq.call(c.now); }
Some(Node::List(elems)) => {
seq.i64_const(((1 + elems.len()) as i64) * 8);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(elems.len() as i64);
seq.call(c.set); for (i, e) in elems.iter().enumerate() {
seq.i64_const(((1 + i) as i64) * 8);
lower_expr(c, scope, seq, e)?;
seq.call(c.set);
}
}
Some(Node::ListEmpty { .. }) => {
seq.i64_const(8);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(0);
seq.call(c.set); }
Some(Node::ListCons { head, tail }) => {
lower_expr(c, scope, seq, &head)?;
lower_expr(c, scope, seq, &tail)?;
seq.call(c.list_cons); }
Some(Node::OptionSome(v)) => {
lower_expr(c, scope, seq, &v)?;
let tmp = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(tmp);
seq.i64_const(16);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(1);
seq.call(c.set); seq.i64_const(8);
seq.local_get(tmp);
seq.call(c.set); }
Some(Node::OptionNone { .. }) => {
seq.i64_const(16);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(0);
seq.call(c.set); }
Some(Node::OptionElse { opt, default }) => {
lower_expr(c, scope, seq, &opt)?; let pl = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(pl);
seq.local_get(pl);
seq.i64_const(0);
seq.call(c.get); seq.i64_const(1);
seq.binop(BinaryOp::I64Eq); let mut eerr: Option<LowerError> = None;
seq.if_else(
InstrSeqType::Simple(Some(ValType::I64)),
|t| {
t.local_get(pl).i64_const(8).call(c.get); },
|e| {
if let Err(er) = lower_expr(c, scope, e, &default) {
eerr = Some(er);
}
},
);
if let Some(e) = eerr {
return Err(e);
}
}
Some(Node::OptionMatch {
opt,
some_bind,
some_body,
none_body,
}) => {
lower_expr(c, scope, seq, &opt)?; let pl = c.locals.borrow_mut().add(ValType::I64);
seq.local_set(pl);
seq.local_get(pl);
seq.i64_const(0);
seq.call(c.get); seq.i64_const(1);
seq.binop(BinaryOp::I64Eq); let mut s2 = scope.clone();
s2.insert(some_bind.clone(), Slot::Member { base: pl, off: 8 });
let mut terr: Option<LowerError> = None;
let mut eerr: Option<LowerError> = None;
seq.if_else(
InstrSeqType::Simple(Some(ValType::I64)),
|t| {
if let Err(e) = lower_expr(c, &s2, t, &some_body) {
terr = Some(e);
}
},
|e| {
if let Err(er) = lower_expr(c, scope, e, &none_body) {
eerr = Some(er);
}
},
);
if let Some(e) = terr.or(eerr) {
return Err(e);
}
}
Some(Node::ListTryGet { list, index }) => {
lower_expr(c, scope, seq, &list)?;
lower_expr(c, scope, seq, &index)?;
seq.call(c.list_try_get); }
Some(Node::ListLen(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.i64_const(0);
seq.call(c.get); }
Some(Node::ListGet { list, index }) => {
lower_expr(c, scope, seq, &list)?; lower_expr(c, scope, seq, &index)?; seq.call(c.list_get); }
Some(Node::Map(pairs)) => {
seq.i64_const(((1 + 2 * pairs.len()) as i64) * 8);
seq.call(c.alloc); seq.i64_const(0);
seq.i64_const(pairs.len() as i64);
seq.call(c.set); for (i, (k, v)) in pairs.iter().enumerate() {
seq.i64_const(((1 + 2 * i) as i64) * 8);
lower_expr(c, scope, seq, k)?;
seq.call(c.set);
seq.i64_const(((2 + 2 * i) as i64) * 8);
lower_expr(c, scope, seq, v)?;
seq.call(c.set);
}
}
Some(Node::MapGet { map, key }) => {
lower_expr(c, scope, seq, &map)?; lower_expr(c, scope, seq, &key)?; seq.call(c.map_get); }
Some(Node::MapTryGet { map, key }) => {
lower_expr(c, scope, seq, &map)?; lower_expr(c, scope, seq, &key)?; seq.call(c.map_try_get); }
Some(Node::MapLen(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.i64_const(0);
seq.call(c.get); }
Some(Node::Log(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.call(c.log); }
Some(Node::Publish(arg)) => {
lower_expr(c, scope, seq, &arg)?; seq.call(c.publish); }
Some(Node::SetHeader { name, value }) => {
lower_expr(c, scope, seq, &name)?; lower_expr(c, scope, seq, &value)?; seq.call(c.set_header); }
Some(Node::Rand) => {
seq.call(c.rand); }
Some(Node::DiskWrite { path, content }) => {
lower_expr(c, scope, seq, &path)?;
lower_expr(c, scope, seq, &content)?;
seq.call(c.disk_write); }
Some(Node::DiskRead(path)) => {
lower_expr(c, scope, seq, &path)?;
seq.call(c.disk_read); }
Some(Node::NetGet(url)) => {
lower_expr(c, scope, seq, &url)?;
seq.call(c.net_get); }
Some(Node::DbQuery { sql, params }) => {
lower_expr(c, scope, seq, &sql)?;
lower_expr(c, scope, seq, ¶ms)?;
seq.call(c.db_query); }
Some(Node::MutNew(v)) => {
seq.i64_const(8);
seq.call(c.alloc); seq.i64_const(0);
lower_expr(c, scope, seq, &v)?;
seq.call(c.set); }
Some(Node::MutGet(cell)) => {
lower_expr(c, scope, seq, &cell)?; seq.i64_const(0);
seq.call(c.get); }
Some(Node::MutSet { cell, value }) => {
lower_expr(c, scope, seq, &cell)?; seq.i64_const(0);
lower_expr(c, scope, seq, &value)?; seq.call(c.set); seq.i64_const(0);
seq.call(c.get); }
Some(Node::RecordDef { .. }) | Some(Node::VariantDef { .. }) => {
return Err(LowerError::Unsupported("type definition is not an expression"));
}
Some(Node::Hole { .. }) => return Err(LowerError::Hole),
Some(Node::Function { .. }) | Some(Node::Module { .. }) => {
return Err(LowerError::Unsupported("nested function or module"));
}
None => return Err(LowerError::Unsupported("missing node")),
}
Ok(())
}
#[derive(Debug)]
pub enum RunError {
Wasmtime(String),
BadExport(String),
}
impl std::fmt::Display for RunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RunError::Wasmtime(e) => write!(f, "wasmtime: {e}"),
RunError::BadExport(n) => write!(f, "no callable i64 export `{n}`"),
}
}
}
impl std::error::Error for RunError {}
type WtStore = wasmtime::Store<()>;
fn write_wasm_string(caller: &mut wasmtime::Caller<'_, ()>, s: &str) -> i64 {
let bytes = s.as_bytes();
let size = ((1 + bytes.len()) as i64) * 8;
let Some(alloc) = caller
.get_export("__alloc")
.and_then(wasmtime::Extern::into_func)
else {
return -1;
};
let mut out = [wasmtime::Val::I64(0)];
if alloc
.call(&mut *caller, &[wasmtime::Val::I64(size)], &mut out)
.is_err()
{
return -1;
}
let wasmtime::Val::I64(ptr) = out[0] else {
return -1;
};
let ptr = ptr as usize;
let Some(mem) = caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
else {
return -1;
};
let data = mem.data_mut(&mut *caller);
let put = |data: &mut [u8], off: usize, v: i64| {
data[off..off + 8].copy_from_slice(&v.to_le_bytes());
};
put(data, ptr, bytes.len() as i64);
for (i, b) in bytes.iter().enumerate() {
put(data, ptr + (1 + i) * 8, *b as i64);
}
ptr as i64
}
fn read_wasm_string(data: &[u8], ptr: usize) -> String {
let rd = |off: usize| -> i64 {
let mut b = [0u8; 8];
b.copy_from_slice(&data[off..off + 8]);
i64::from_le_bytes(b)
};
let len = rd(ptr).max(0) as usize;
let mut bytes = Vec::with_capacity(len);
for i in 0..len {
bytes.push((rd(ptr + (1 + i) * 8) & 0xff) as u8);
}
String::from_utf8_lossy(&bytes).into_owned()
}
thread_local! {
static PUBLISHED: std::cell::RefCell<Vec<String>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn drain_published() -> Vec<String> {
PUBLISHED.with(|p| std::mem::take(&mut *p.borrow_mut()))
}
fn record_published(topic: String) {
PUBLISHED.with(|p| p.borrow_mut().push(topic));
}
thread_local! {
static RESP_HEADERS: std::cell::RefCell<Vec<(String, String)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn drain_resp_headers() -> Vec<(String, String)> {
RESP_HEADERS.with(|h| std::mem::take(&mut *h.borrow_mut()))
}
fn record_resp_header(name: String, value: String) {
RESP_HEADERS.with(|h| h.borrow_mut().push((name, value)));
}
fn read_wasm_str_list(data: &[u8], ptr: usize) -> Vec<String> {
let rd = |off: usize| -> i64 {
let mut b = [0u8; 8];
b.copy_from_slice(&data[off..off + 8]);
i64::from_le_bytes(b)
};
let len = rd(ptr).max(0) as usize;
(0..len)
.map(|i| read_wasm_string(data, rd(ptr + (1 + i) * 8) as usize))
.collect()
}
fn run_sql(conn: &rusqlite::Connection, sql: &str, params: &[String]) -> String {
use rusqlite::types::Value;
fn esc(s: &str) -> String {
let mut o = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => o.push_str("\\\\"),
'\t' => o.push_str("\\t"),
'\n' => o.push_str("\\n"),
c => o.push(c),
}
}
o
}
fn cell(v: Value) -> String {
match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Real(f) => f.to_string(),
Value::Text(s) => esc(&s),
Value::Blob(b) => esc(&String::from_utf8_lossy(&b)),
}
}
let go = || -> rusqlite::Result<String> {
let bound = rusqlite::params_from_iter(params.iter());
let mut stmt = conn.prepare(sql)?;
let ncol = stmt.column_count();
if ncol == 0 {
drop(stmt);
conn.execute(sql, bound)?;
return Ok(conn.last_insert_rowid().to_string());
}
let mut rows = stmt.query(bound)?;
let mut out: Vec<String> = Vec::new();
while let Some(row) = rows.next()? {
let mut cols: Vec<String> = Vec::with_capacity(ncol);
for c in 0..ncol {
cols.push(cell(row.get::<_, Value>(c)?));
}
out.push(cols.join("\t"));
}
Ok(out.join("\n"))
};
match go() {
Ok(s) => s,
Err(e) => {
eprintln!("cairn db_query error: {e}");
String::new()
}
}
}
fn instantiate(
wasm: &[u8],
now: Option<i64>,
db: Option<&str>,
) -> std::result::Result<(WtStore, wasmtime::Instance), RunError> {
use wasmtime::{Engine, Linker, Module as WtModule};
let engine = Engine::default();
let module = WtModule::new(&engine, wasm).map_err(|e| RunError::Wasmtime(e.to_string()))?;
let mut store = wasmtime::Store::new(&engine, ());
let mut linker = Linker::new(&engine);
linker
.func_wrap("host", "now", move || -> i64 {
match now {
Some(v) => v,
None => std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0),
}
})
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap("host", "log", |v: i64| -> i64 {
eprintln!("[cairn log] {v}");
v
})
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap(
"host",
"publish",
|mut caller: wasmtime::Caller<'_, ()>, topic_ptr: i64| -> i64 {
let topic = {
match caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
{
Some(mem) => read_wasm_string(
mem.data(&caller),
topic_ptr as usize,
),
None => return 0,
}
};
record_published(topic);
0
},
)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap(
"host",
"set_header",
|mut caller: wasmtime::Caller<'_, ()>,
name_ptr: i64,
value_ptr: i64|
-> i64 {
let (name, value) = {
match caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
{
Some(mem) => {
let data = mem.data(&caller);
(
read_wasm_string(data, name_ptr as usize),
read_wasm_string(
data,
value_ptr as usize,
),
)
}
None => return 0,
}
};
record_resp_header(name, value);
0
},
)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap("host", "rand", || -> i64 {
let mut b = [0u8; 8];
getrandom::getrandom(&mut b).expect("OS entropy");
i64::from_le_bytes(b)
})
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap(
"host",
"disk_write",
|mut caller: wasmtime::Caller<'_, ()>, p: i64, cptr: i64| -> i64 {
let Some(mem) = caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
else {
return -1;
};
let data = mem.data(&caller);
let path = read_wasm_string(data, p as usize);
let content = read_wasm_string(data, cptr as usize);
match std::fs::write(&path, content.as_bytes()) {
Ok(()) => content.len() as i64,
Err(_) => -1,
}
},
)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap(
"host",
"disk_read",
|mut caller: wasmtime::Caller<'_, ()>, p: i64| -> i64 {
let path = {
let Some(mem) = caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
else {
return -1;
};
read_wasm_string(mem.data(&caller), p as usize)
};
let contents = std::fs::read_to_string(&path).unwrap_or_default();
write_wasm_string(&mut caller, &contents)
},
)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
linker
.func_wrap(
"host",
"net_get",
|mut caller: wasmtime::Caller<'_, ()>, u: i64| -> i64 {
let url = {
let Some(mem) = caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
else {
return -1;
};
read_wasm_string(mem.data(&caller), u as usize)
};
match ureq::get(&url).call() {
Ok(resp) => resp.status() as i64,
Err(ureq::Error::Status(code, _)) => code as i64,
Err(_) => -1,
}
},
)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let conn = match db {
Some(p) => rusqlite::Connection::open(p),
None => rusqlite::Connection::open_in_memory(),
}
.map_err(|e| RunError::Wasmtime(format!("sqlite open: {e}")))?;
let conn = std::sync::Mutex::new(conn);
linker
.func_wrap(
"host",
"db_query",
move |mut caller: wasmtime::Caller<'_, ()>, q: i64, pp: i64| -> i64 {
let (sql, params) = {
let Some(mem) = caller
.get_export("mem")
.and_then(wasmtime::Extern::into_memory)
else {
return -1;
};
let data = mem.data(&caller);
(
read_wasm_string(data, q as usize),
read_wasm_str_list(data, pp as usize),
)
};
let result = {
let conn = conn.lock().expect("db mutex");
run_sql(&conn, &sql, ¶ms)
};
write_wasm_string(&mut caller, &result)
},
)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let instance = linker
.instantiate(&mut store, &module)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
Ok((store, instance))
}
pub fn run_i64(wasm: &[u8], func: &str, args: &[i64]) -> std::result::Result<i64, RunError> {
run_with(wasm, func, args, None)
}
pub fn run_effectful_i64(
wasm: &[u8],
func: &str,
args: &[i64],
now_value: i64,
) -> std::result::Result<i64, RunError> {
run_with(wasm, func, args, Some(now_value))
}
fn run_with(
wasm: &[u8],
func: &str,
args: &[i64],
now: Option<i64>,
) -> std::result::Result<i64, RunError> {
use wasmtime::Val;
let (mut store, instance) = instantiate(wasm, now, None)?;
let f = instance
.get_func(&mut store, func)
.ok_or_else(|| RunError::BadExport(func.to_string()))?;
let params: Vec<Val> = args.iter().map(|a| Val::I64(*a)).collect();
let mut results = [Val::I64(0)];
f.call(&mut store, ¶ms, &mut results)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
match results[0] {
Val::I64(v) => Ok(v),
_ => Err(RunError::BadExport(func.to_string())),
}
}
pub fn run_fallible(
wasm: &[u8],
func: &str,
args: &[i64],
) -> std::result::Result<std::result::Result<i64, i64>, RunError> {
use wasmtime::Val;
let (mut store, instance) = instantiate(wasm, None, None)?;
let f = instance
.get_func(&mut store, func)
.ok_or_else(|| RunError::BadExport(func.to_string()))?;
let memory = instance
.get_memory(&mut store, "mem")
.ok_or_else(|| RunError::BadExport("mem".to_string()))?;
let params: Vec<Val> = args.iter().map(|a| Val::I64(*a)).collect();
let mut results = [Val::I64(0)];
f.call(&mut store, ¶ms, &mut results)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let ptr = match results[0] {
Val::I64(p) => p as usize,
_ => return Err(RunError::BadExport(func.to_string())),
};
let data = memory.data(&store);
let read = |off: usize| -> i64 {
let mut b = [0u8; 8];
b.copy_from_slice(&data[ptr + off..ptr + off + 8]);
i64::from_le_bytes(b)
};
let tag = read(0);
if tag == 0 {
Ok(Ok(read(8)))
} else {
Ok(Err(tag))
}
}
fn write_string_via_export(
store: &mut WtStore,
instance: &wasmtime::Instance,
s: &str,
) -> std::result::Result<i64, RunError> {
use wasmtime::Val;
let bytes = s.as_bytes();
let size = ((1 + bytes.len()) as i64) * 8;
let alloc = instance
.get_func(&mut *store, "__alloc")
.ok_or_else(|| RunError::BadExport("__alloc".to_string()))?;
let mut out = [Val::I64(0)];
alloc
.call(&mut *store, &[Val::I64(size)], &mut out)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let Val::I64(ptr) = out[0] else {
return Err(RunError::BadExport("__alloc".to_string()));
};
let mem = instance
.get_memory(&mut *store, "mem")
.ok_or_else(|| RunError::BadExport("mem".to_string()))?;
let data = mem.data_mut(&mut *store);
let p = ptr as usize;
data[p..p + 8].copy_from_slice(&(bytes.len() as i64).to_le_bytes());
for (i, b) in bytes.iter().enumerate() {
let off = p + (1 + i) * 8;
data[off..off + 8].copy_from_slice(&(*b as i64).to_le_bytes());
}
Ok(ptr)
}
pub fn serve_once(
wasm: &[u8],
handler: &str,
request: &str,
) -> std::result::Result<String, RunError> {
use wasmtime::Val;
let (mut store, instance) = instantiate(wasm, None, None)?;
let req_ptr = write_string_via_export(&mut store, &instance, request)?;
let f = instance
.get_func(&mut store, handler)
.ok_or_else(|| RunError::BadExport(handler.to_string()))?;
let mut out = [Val::I64(0)];
f.call(&mut store, &[Val::I64(req_ptr)], &mut out)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let Val::I64(resp_ptr) = out[0] else {
return Err(RunError::BadExport(handler.to_string()));
};
let mem = instance
.get_memory(&mut store, "mem")
.ok_or_else(|| RunError::BadExport("mem".to_string()))?;
Ok(read_wasm_string(mem.data(&store), resp_ptr as usize))
}
fn write_record_via_export(
store: &mut WtStore,
instance: &wasmtime::Instance,
slots: &[i64],
) -> std::result::Result<i64, RunError> {
use wasmtime::Val;
let size = (slots.len() as i64) * 8;
let alloc = instance
.get_func(&mut *store, "__alloc")
.ok_or_else(|| RunError::BadExport("__alloc".to_string()))?;
let mut out = [Val::I64(0)];
alloc
.call(&mut *store, &[Val::I64(size)], &mut out)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let Val::I64(ptr) = out[0] else {
return Err(RunError::BadExport("__alloc".to_string()));
};
let mem = instance
.get_memory(&mut *store, "mem")
.ok_or_else(|| RunError::BadExport("mem".to_string()))?;
let data = mem.data_mut(&mut *store);
for (i, v) in slots.iter().enumerate() {
let off = ptr as usize + i * 8;
data[off..off + 8].copy_from_slice(&v.to_le_bytes());
}
Ok(ptr)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpResponse {
pub status: i64,
pub body: String,
}
pub fn serve_request(
wasm: &[u8],
handler: &str,
method: &str,
path: &str,
body: &str,
) -> std::result::Result<HttpResponse, RunError> {
serve_request_with(wasm, handler, method, path, body, "", None)
}
pub fn serve_request_h(
wasm: &[u8],
handler: &str,
method: &str,
path: &str,
body: &str,
headers: &str,
) -> std::result::Result<HttpResponse, RunError> {
serve_request_with(wasm, handler, method, path, body, headers, None)
}
pub fn serve_request_db(
wasm: &[u8],
handler: &str,
db_path: &str,
method: &str,
path: &str,
body: &str,
) -> std::result::Result<HttpResponse, RunError> {
serve_request_with(wasm, handler, method, path, body, "", Some(db_path))
}
pub fn serve_request_db_h(
wasm: &[u8],
handler: &str,
db_path: &str,
method: &str,
path: &str,
body: &str,
headers: &str,
) -> std::result::Result<HttpResponse, RunError> {
serve_request_with(
wasm,
handler,
method,
path,
body,
headers,
Some(db_path),
)
}
fn serve_request_with(
wasm: &[u8],
handler: &str,
method: &str,
path: &str,
body: &str,
headers: &str,
db: Option<&str>,
) -> std::result::Result<HttpResponse, RunError> {
use wasmtime::Val;
let (mut store, instance) = instantiate(wasm, None, db)?;
let m = write_string_via_export(&mut store, &instance, method)?;
let p = write_string_via_export(&mut store, &instance, path)?;
let b = write_string_via_export(&mut store, &instance, body)?;
let hh = write_string_via_export(&mut store, &instance, headers)?;
let req_ptr =
write_record_via_export(&mut store, &instance, &[m, p, b, hh])?;
let f = instance
.get_func(&mut store, handler)
.ok_or_else(|| RunError::BadExport(handler.to_string()))?;
let mut out = [Val::I64(0)];
f.call(&mut store, &[Val::I64(req_ptr)], &mut out)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
let Val::I64(resp_ptr) = out[0] else {
return Err(RunError::BadExport(handler.to_string()));
};
let mem = instance
.get_memory(&mut store, "mem")
.ok_or_else(|| RunError::BadExport("mem".to_string()))?;
let data = mem.data(&store);
let slot = |i: usize| -> i64 {
let off = resp_ptr as usize + i * 8;
let mut x = [0u8; 8];
x.copy_from_slice(&data[off..off + 8]);
i64::from_le_bytes(x)
};
Ok(HttpResponse {
status: slot(0),
body: read_wasm_string(data, slot(1) as usize),
})
}
pub(crate) fn reason_phrase(status: i64) -> &'static str {
match status {
200 => "OK",
201 => "Created",
204 => "No Content",
400 => "Bad Request",
404 => "Not Found",
500 => "Internal Server Error",
s if (200..300).contains(&s) => "OK",
s if (400..500).contains(&s) => "Client Error",
_ => "Error",
}
}
pub fn serve_http(
wasm: &[u8],
handler: &str,
addr: &str,
) -> std::result::Result<(), RunError> {
serve_http_with(wasm, handler, addr, None)
}
pub fn serve_http_db(
wasm: &[u8],
handler: &str,
addr: &str,
db_path: &str,
) -> std::result::Result<(), RunError> {
serve_http_with(wasm, handler, addr, Some(db_path))
}
fn serve_http_with(
wasm: &[u8],
handler: &str,
addr: &str,
db: Option<&str>,
) -> std::result::Result<(), RunError> {
use std::io::{BufRead, BufReader, Read, Write};
let listener = std::net::TcpListener::bind(addr)
.map_err(|e| RunError::Wasmtime(e.to_string()))?;
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();
if reader.read_line(&mut request_line).is_err() {
continue;
}
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or("GET").to_string();
let path = parts.next().unwrap_or("/").to_string();
let mut content_length = 0usize;
let mut header_lines: Vec<String> = Vec::new();
loop {
let mut h = String::new();
if reader.read_line(&mut h).is_err() {
break;
}
let h = h.trim_end();
if h.is_empty() {
break;
}
if let Some((name, value)) = h.split_once(':') {
if name.eq_ignore_ascii_case("content-length") {
content_length = value.trim().parse().unwrap_or(0);
}
}
header_lines.push(h.to_string());
}
let headers = header_lines.join("\n");
let mut req_body = String::new();
if content_length > 0 {
let mut buf = vec![0u8; content_length];
if reader.read_exact(&mut buf).is_ok() {
req_body = String::from_utf8_lossy(&buf).into_owned();
}
}
drop(reader);
let (status, body) = match serve_request_with(
wasm, handler, &method, &path, &req_body, &headers, db,
) {
Ok(r) => (r.status, r.body),
Err(e) => (500, format!("cairn handler error: {e}")),
};
let extra: String = drain_resp_headers()
.into_iter()
.map(|(n, v)| format!("{n}: {v}\r\n"))
.collect();
let resp = format!(
"HTTP/1.1 {status} {}\r\nContent-Length: {}\r\n\
Content-Type: text/html; charset=utf-8\r\n{extra}\r\n{}",
reason_phrase(status),
body.len(),
body
);
let _ = stream.write_all(resp.as_bytes());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::{Param, Produces};
use crate::ty::{Confidence, Type};
use std::collections::BTreeSet;
fn func(
s: &Store,
name: &str,
params: &[&str],
body: Vec<NodeHash>,
result: NodeHash,
) -> NodeHash {
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params: params
.iter()
.map(|p| Param {
name: (*p).into(),
ty: Type::Number,
min_confidence: Confidence::External,
})
.collect(),
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec![],
body,
result,
})
.unwrap()
}
fn module(s: &Store, fns: Vec<NodeHash>) -> NodeHash {
s.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: fns,
})
.unwrap()
}
#[test]
fn a_constant_function_runs_correctly() {
let s = Store::open_in_memory().unwrap();
let lit = s.put(&Node::Lit(42)).unwrap();
let answer = func(&s, "answer", &[], vec![], lit);
let m = module(&s, vec![answer]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "answer", &[]).unwrap(), 42);
}
#[test]
fn publish_performs_the_live_effect_observably() {
let s = Store::open_in_memory().unwrap();
let topic = s.put(&Node::Str("items".into())).unwrap();
let notify = func(
&s,
"notify",
&[],
vec![],
s.put(&Node::Publish(topic)).unwrap(),
);
let m = module(&s, vec![notify]);
let wasm = lower(&s, &m).unwrap();
let _ = super::drain_published(); assert_eq!(run_i64(&wasm, "notify", &[]).unwrap(), 0);
assert!(
super::drain_published().contains(&"items".to_string()),
"host::publish must record the topic"
);
}
#[test]
fn num_to_str_handles_full_i64_range() {
let s = Store::open_in_memory().unwrap();
let rt = |name: &str, v: i64| {
let lit = s.put(&Node::Lit(v)).unwrap();
let n2s = s.put(&Node::NumberToStr(lit)).unwrap();
func(&s, name, &[], vec![], s.put(&Node::StrToNumber(n2s)).unwrap())
};
let vals = [
("mn", i64::MIN),
("mx", i64::MAX),
("neg", -1),
("zero", 0),
("c", 100),
("near", i64::MIN + 7),
];
let fns: Vec<_> = vals.iter().map(|(n, v)| rt(n, *v)).collect();
let lit = s.put(&Node::Lit(i64::MIN)).unwrap();
let n2s = s.put(&Node::NumberToStr(lit)).unwrap();
let mut all = fns;
all.push(func(&s, "mnlen", &[], vec![], s.put(&Node::StrLen(n2s)).unwrap()));
let m = module(&s, all);
let wasm = lower(&s, &m).unwrap();
for (n, v) in vals {
assert_eq!(run_i64(&wasm, n, &[]).unwrap(), v, "round-trip {n}");
}
assert_eq!(run_i64(&wasm, "mnlen", &[]).unwrap(), 20);
}
#[test]
fn boolean_and_comparison_operators_run() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let b = |v: bool| s.put(&Node::Bool(v)).unwrap();
let bin = |op, l: NodeHash, r: NodeHash| {
s.put(&Node::BinOp {
op,
lhs: l,
rhs: r,
})
.unwrap()
};
use crate::node::BinOp::*;
let rmod = func(&s, "rmod", &[], vec![], bin(Mod, n(7), n(3))); let le = func(&s, "le", &[], vec![], bin(Le, n(3), n(3))); let gt = func(&s, "gt", &[], vec![], bin(Gt, n(5), n(2))); let ge = func(&s, "ge", &[], vec![], bin(Ge, n(2), n(5))); let ne = func(&s, "ne", &[], vec![], bin(Neq, n(4), n(4))); let notf = func(
&s,
"notf",
&[],
vec![],
s.put(&Node::Not(b(false))).unwrap(),
); let andt = func(
&s,
"andt",
&[],
vec![],
bin(And, b(true), bin(Lt, n(1), n(2))),
);
let sc_or =
func(&s, "sc_or", &[], vec![], bin(Or, b(true), bin(Div, n(1), n(0))));
let sc_and = func(
&s,
"sc_and",
&[],
vec![],
bin(And, b(false), bin(Div, n(1), n(0))),
);
let m = module(
&s,
vec![rmod, le, gt, ge, ne, notf, andt, sc_or, sc_and],
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "rmod", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "le", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "gt", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "ge", &[]).unwrap(), 0);
assert_eq!(run_i64(&wasm, "ne", &[]).unwrap(), 0);
assert_eq!(run_i64(&wasm, "notf", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "andt", &[]).unwrap(), 1);
assert_eq!(
run_i64(&wasm, "sc_or", &[]).unwrap(),
1,
"Or must short-circuit past a trapping right operand"
);
assert_eq!(
run_i64(&wasm, "sc_and", &[]).unwrap(),
0,
"And must short-circuit past a trapping right operand"
);
}
#[test]
fn function_values_pass_and_call_indirect() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let r = |name: &str| s.put(&Node::Ref(name.into())).unwrap();
let double = func(
&s,
"double",
&["n"],
vec![],
s.put(&Node::BinOp {
op: crate::node::BinOp::Mul,
lhs: r("n"),
rhs: n(2),
})
.unwrap(),
);
let apply = func(
&s,
"apply",
&["f", "x"],
vec![],
s.put(&Node::CallValue {
callee: r("f"),
args: vec![r("x")],
})
.unwrap(),
);
let via_param = func(
&s,
"via_param",
&[],
vec![],
s.put(&Node::Call {
func: "apply".into(),
args: vec![
s.put(&Node::FuncRef("double".into())).unwrap(),
n(21),
],
})
.unwrap(),
);
let direct = func(
&s,
"direct",
&[],
vec![],
s.put(&Node::CallValue {
callee: s.put(&Node::FuncRef("double".into())).unwrap(),
args: vec![n(19)],
})
.unwrap(),
);
let m = module(&s, vec![double, apply, via_param, direct]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "via_param", &[]).unwrap(), 42);
assert_eq!(run_i64(&wasm, "direct", &[]).unwrap(), 38);
}
#[test]
fn closures_capture_and_run() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let r = |name: &str| s.put(&Node::Ref(name.into())).unwrap();
let par = |name: &str| Param {
name: name.into(),
ty: Type::Number,
min_confidence: Confidence::External,
};
let add = |a: NodeHash, b: NodeHash| {
s.put(&Node::BinOp {
op: crate::node::BinOp::Add,
lhs: a,
rhs: b,
})
.unwrap()
};
let lam = s
.put(&Node::Lambda {
params: vec![par("x")],
body: add(r("x"), r("k")),
})
.unwrap();
let mk = func(&s, "mk", &["k"], vec![], lam);
let apply = func(
&s,
"apply",
&["f", "v"],
vec![],
s.put(&Node::CallValue {
callee: r("f"),
args: vec![r("v")],
})
.unwrap(),
);
let via_apply = func(
&s,
"via_apply",
&[],
vec![],
s.put(&Node::Call {
func: "apply".into(),
args: vec![
s.put(&Node::Call {
func: "mk".into(),
args: vec![n(10)],
})
.unwrap(),
n(5),
],
})
.unwrap(),
);
let direct = func(
&s,
"direct",
&[],
vec![],
s.put(&Node::CallValue {
callee: s
.put(&Node::Call {
func: "mk".into(),
args: vec![n(10)],
})
.unwrap(),
args: vec![n(7)],
})
.unwrap(),
);
let inner = s
.put(&Node::Lambda {
params: vec![par("y")],
body: add(add(r("x"), r("y")), r("k")),
})
.unwrap();
let outer = s
.put(&Node::Lambda {
params: vec![par("x")],
body: inner,
})
.unwrap();
let mk2 = func(&s, "mk2", &["k"], vec![], outer);
let nested = func(
&s,
"nested",
&[],
vec![],
s.put(&Node::CallValue {
callee: s
.put(&Node::CallValue {
callee: s
.put(&Node::Call {
func: "mk2".into(),
args: vec![n(100)],
})
.unwrap(),
args: vec![n(20)],
})
.unwrap(),
args: vec![n(3)],
})
.unwrap(),
);
let m = module(
&s,
vec![mk, apply, via_apply, direct, mk2, nested],
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "via_apply", &[]).unwrap(), 15);
assert_eq!(run_i64(&wasm, "direct", &[]).unwrap(), 17);
assert_eq!(
run_i64(&wasm, "nested", &[]).unwrap(),
123,
"transitive capture: (x+y)+k = (20+3)+100"
);
}
#[test]
fn option_match_drives_control_flow() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let xs = s.put(&Node::List(vec![n(10), n(20), n(30)])).unwrap();
let at = |i: i64| {
s.put(&Node::OptionMatch {
opt: s
.put(&Node::ListTryGet {
list: xs.clone(),
index: n(i),
})
.unwrap(),
some_bind: "v".into(),
some_body: s.put(&Node::Ref("v".into())).unwrap(),
none_body: n(-1),
})
.unwrap()
};
let hit = func(&s, "hit", &[], vec![], at(1)); let miss = func(&s, "miss", &[], vec![], at(9)); let m = module(&s, vec![hit, miss]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "hit", &[]).unwrap(), 20);
assert_eq!(run_i64(&wasm, "miss", &[]).unwrap(), -1);
}
#[test]
fn map_try_get_yields_an_option() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let m = s
.put(&Node::Map(vec![(n(1), n(100)), (n(2), n(200))]))
.unwrap();
let look = |key: i64| {
s.put(&Node::OptionMatch {
opt: s
.put(&Node::MapTryGet {
map: m.clone(),
key: n(key),
})
.unwrap(),
some_bind: "v".into(),
some_body: s.put(&Node::Ref("v".into())).unwrap(),
none_body: n(-1),
})
.unwrap()
};
let hit = func(&s, "hit", &[], vec![], look(2)); let miss = func(&s, "miss", &[], vec![], look(9)); let md = module(&s, vec![hit, miss]);
let wasm = lower(&s, &md).unwrap();
assert_eq!(run_i64(&wasm, "hit", &[]).unwrap(), 200);
assert_eq!(run_i64(&wasm, "miss", &[]).unwrap(), -1);
}
#[test]
fn float_arithmetic_is_real_ieee754() {
use crate::node::BinOp;
let s = Store::open_in_memory().unwrap();
let f = |v: f64| s.put(&Node::FloatLit(v.to_bits())).unwrap();
let op = |o, a: NodeHash, b: NodeHash| {
s.put(&Node::FloatOp {
op: o,
lhs: a,
rhs: b,
})
.unwrap()
};
let prod = func(
&s,
"prod",
&[],
vec![],
s.put(&Node::FloatToInt(op(BinOp::Mul, f(2.5), f(4.0)))).unwrap(),
);
let lt = func(&s, "lt", &[], vec![], op(BinOp::Lt, f(0.1), f(0.2)));
let inexact = func(
&s,
"inexact",
&[],
vec![],
op(BinOp::Eq, op(BinOp::Add, f(0.1), f(0.2)), f(0.3)),
);
let raw = func(&s, "raw", &[], vec![], f(3.5));
let i2f2i = func(
&s,
"i2f2i",
&[],
vec![],
s.put(&Node::FloatToInt(
s.put(&Node::IntToFloat(s.put(&Node::Lit(7)).unwrap()))
.unwrap(),
))
.unwrap(),
);
let m = module(&s, vec![prod, lt, inexact, raw, i2f2i]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "prod", &[]).unwrap(), 10);
assert_eq!(run_i64(&wasm, "lt", &[]).unwrap(), 1);
assert_eq!(
run_i64(&wasm, "inexact", &[]).unwrap(),
0,
"0.1+0.2 != 0.3 — genuine IEEE-754"
);
assert_eq!(
run_i64(&wasm, "raw", &[]).unwrap(),
3.5f64.to_bits() as i64,
"Float travels as its f64 bit pattern in the i64 slot"
);
assert_eq!(run_i64(&wasm, "i2f2i", &[]).unwrap(), 7);
}
#[test]
fn decimal_is_exact() {
use crate::node::BinOp;
let s = Store::open_in_memory().unwrap();
let d = |v: f64| {
s.put(&Node::DecimalLit((v * 10_000.0).round() as i64))
.unwrap()
};
let op = |o, a: NodeHash, b: NodeHash| {
s.put(&Node::DecimalOp {
op: o,
lhs: a,
rhs: b,
})
.unwrap()
};
let to_int = |x: NodeHash| s.put(&Node::DecimalToInt(x)).unwrap();
let prod = func(
&s,
"prod",
&[],
vec![],
to_int(op(BinOp::Mul, d(1.25), d(4.0))),
);
let exact = func(
&s,
"exact",
&[],
vec![],
op(BinOp::Eq, op(BinOp::Add, d(0.10), d(0.20)), d(0.30)),
);
let money = func(
&s,
"money",
&[],
vec![],
to_int(op(BinOp::Add, d(19.99), d(0.01))),
);
let i2d2i = func(
&s,
"i2d2i",
&[],
vec![],
to_int(s.put(&Node::IntToDecimal(s.put(&Node::Lit(7)).unwrap())).unwrap()),
);
let m = module(&s, vec![prod, exact, money, i2d2i]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "prod", &[]).unwrap(), 5);
assert_eq!(
run_i64(&wasm, "exact", &[]).unwrap(),
1,
"0.10+0.20 == 0.30 exactly — the point of Decimal"
);
assert_eq!(run_i64(&wasm, "money", &[]).unwrap(), 20);
assert_eq!(run_i64(&wasm, "i2d2i", &[]).unwrap(), 7);
}
#[test]
fn out_of_bounds_list_get_and_str_slice_trap() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let list = s.put(&Node::List(vec![n(10), n(20), n(30)])).unwrap();
let good = func(
&s,
"good",
&[],
vec![],
s.put(&Node::ListGet {
list: list.clone(),
index: n(1),
})
.unwrap(),
);
let oob = func(
&s,
"oob",
&[],
vec![],
s.put(&Node::ListGet {
list,
index: n(5),
})
.unwrap(),
);
let bad = func(
&s,
"bad",
&[],
vec![],
s.put(&Node::StrLen(
s.put(&Node::StrSlice {
s: s.put(&Node::Str("ab".into())).unwrap(),
start: n(0),
len: n(5),
})
.unwrap(),
))
.unwrap(),
);
let m = module(&s, vec![good, oob, bad]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "good", &[]).unwrap(), 20);
assert!(run_i64(&wasm, "oob", &[]).is_err(), "OOB index must trap");
assert!(
run_i64(&wasm, "bad", &[]).is_err(),
"OOB slice must trap"
);
}
#[test]
fn str_to_number_opt_rejects_invalid_input() {
let s = Store::open_in_memory().unwrap();
let mk = |name: &str, input: &str| -> NodeHash {
let body = s
.put(&Node::OptionElse {
opt: s
.put(&Node::StrToNumberOpt(
s.put(&Node::Str(input.into())).unwrap(),
))
.unwrap(),
default: s.put(&Node::Lit(-999)).unwrap(),
})
.unwrap();
func(&s, name, &[], vec![], body)
};
let fns = vec![
mk("a", "42"),
mk("b", "-7"),
mk("c", "0"),
mk("d", "12x"),
mk("e", ""),
mk("f", "-"),
mk("g", "3.5"),
];
let m = module(&s, fns);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "a", &[]).unwrap(), 42);
assert_eq!(run_i64(&wasm, "b", &[]).unwrap(), -7);
assert_eq!(run_i64(&wasm, "c", &[]).unwrap(), 0);
assert_eq!(run_i64(&wasm, "d", &[]).unwrap(), -999); assert_eq!(run_i64(&wasm, "e", &[]).unwrap(), -999); assert_eq!(run_i64(&wasm, "f", &[]).unwrap(), -999); assert_eq!(run_i64(&wasm, "g", &[]).unwrap(), -999); }
#[test]
fn option_recovers_from_out_of_bounds_without_trapping() {
let s = Store::open_in_memory().unwrap();
let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
let list = s.put(&Node::List(vec![n(10), n(20), n(30)])).unwrap();
let body = s
.put(&Node::OptionElse {
opt: s
.put(&Node::ListTryGet {
list,
index: s.put(&Node::Ref("i".into())).unwrap(),
})
.unwrap(),
default: n(-1),
})
.unwrap();
let at = func(&s, "at", &["i"], vec![], body);
let some = func(
&s,
"som",
&[],
vec![],
s.put(&Node::OptionElse {
opt: s.put(&Node::OptionSome(n(7))).unwrap(),
default: n(-1),
})
.unwrap(),
);
let none = func(
&s,
"non",
&[],
vec![],
s.put(&Node::OptionElse {
opt: s
.put(&Node::OptionNone {
elem: Type::Number,
})
.unwrap(),
default: n(-1),
})
.unwrap(),
);
let m = module(&s, vec![at, some, none]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "at", &[0]).unwrap(), 10);
assert_eq!(run_i64(&wasm, "at", &[2]).unwrap(), 30);
assert_eq!(run_i64(&wasm, "at", &[5]).unwrap(), -1); assert_eq!(run_i64(&wasm, "at", &[-1]).unwrap(), -1); assert_eq!(run_i64(&wasm, "som", &[]).unwrap(), 7);
assert_eq!(run_i64(&wasm, "non", &[]).unwrap(), -1);
}
#[test]
fn allocation_grows_past_the_initial_page() {
let s = Store::open_in_memory().unwrap();
let r = |n: &str| s.put(&Node::Ref(n.into())).unwrap();
let num = |v: i64| s.put(&Node::Lit(v)).unwrap();
let binop = |op, a: NodeHash, b: NodeHash| {
s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
};
let p = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let ext = |ty: Type| Produces {
ty,
confidence: Confidence::External,
};
let num_list = || Type::List(Box::new(Type::Number));
let mk = s
.put(&Node::Function {
name: "mk".into(),
type_params: vec![],
params: vec![p("n", Type::Number)],
produces: ext(num_list()),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: s
.put(&Node::If {
cond: binop(crate::node::BinOp::Eq, r("n"), num(0)),
then_branch: s
.put(&Node::ListEmpty {
elem: Type::Number,
})
.unwrap(),
else_branch: s
.put(&Node::ListCons {
head: r("n"),
tail: s
.put(&Node::Call {
func: "mk".into(),
args: vec![binop(
crate::node::BinOp::Sub,
r("n"),
num(1),
)],
})
.unwrap(),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let sum = s
.put(&Node::Function {
name: "sum".into(),
type_params: vec![],
params: vec![p("xs", num_list()), p("i", Type::Number)],
produces: ext(Type::Number),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![s
.put(&Node::Step {
binding: "len".into(),
value: s.put(&Node::ListLen(r("xs"))).unwrap(),
})
.unwrap()],
result: s
.put(&Node::If {
cond: binop(crate::node::BinOp::Eq, r("i"), r("len")),
then_branch: num(0),
else_branch: binop(
crate::node::BinOp::Add,
s.put(&Node::ListGet {
list: r("xs"),
index: r("i"),
})
.unwrap(),
s.put(&Node::Call {
func: "sum".into(),
args: vec![
r("xs"),
binop(crate::node::BinOp::Add, r("i"), num(1)),
],
})
.unwrap(),
),
})
.unwrap(),
})
.unwrap();
let total = s
.put(&Node::Function {
name: "total".into(),
type_params: vec![],
params: vec![p("n", Type::Number)],
produces: ext(Type::Number),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![s
.put(&Node::Step {
binding: "xs".into(),
value: s
.put(&Node::Call {
func: "mk".into(),
args: vec![r("n")],
})
.unwrap(),
})
.unwrap()],
result: s
.put(&Node::Call {
func: "sum".into(),
args: vec![r("xs"), num(0)],
})
.unwrap(),
})
.unwrap();
let m = module(&s, vec![total, mk, sum]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "total", &[1500]).unwrap(), 1_125_750);
}
#[test]
fn real_net_get_returns_the_http_status() {
use std::io::{Read, Write};
let listener =
std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 256];
let _ = stream.read(&mut buf); let _ = stream.write_all(
b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n",
);
}
});
let s = Store::open_in_memory().unwrap();
let url = s
.put(&Node::Str(format!("http://127.0.0.1:{port}/")))
.unwrap();
let get = s.put(&Node::NetGet(url)).unwrap();
let mut net = BTreeSet::new();
net.insert(crate::ty::Effect::Net);
let ping = s
.put(&Node::Function {
name: "ping".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: net,
on_failure: vec![],
body: vec![],
result: get,
})
.unwrap();
let m = module(&s, vec![ping]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "ping", &[]).unwrap(), 204);
server.join().unwrap();
}
#[test]
fn a_function_returns_its_parameter() {
let s = Store::open_in_memory().unwrap();
let nref = s.put(&Node::Ref("n".into())).unwrap();
let id = func(&s, "id", &["n"], vec![], nref);
let m = module(&s, vec![id]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "id", &[7]).unwrap(), 7);
}
#[test]
fn a_call_through_a_binding_runs() {
let s = Store::open_in_memory().unwrap();
let nref = s.put(&Node::Ref("n".into())).unwrap();
let id = func(&s, "id", &["n"], vec![], nref);
let seven = s.put(&Node::Lit(7)).unwrap();
let call = s
.put(&Node::Call {
func: "id".into(),
args: vec![seven],
})
.unwrap();
let step = s
.put(&Node::Step {
binding: "x".into(),
value: call,
})
.unwrap();
let xref = s.put(&Node::Ref("x".into())).unwrap();
let use_id = func(&s, "use_id", &[], vec![step], xref);
let m = module(&s, vec![id, use_id]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "use_id", &[]).unwrap(), 7);
}
#[test]
fn a_hole_cannot_be_lowered() {
let s = Store::open_in_memory().unwrap();
let hole = s
.put(&Node::Hole {
expects: "Number".into(),
})
.unwrap();
let f = func(&s, "f", &[], vec![], hole);
let m = module(&s, vec![f]);
assert!(matches!(lower(&s, &m), Err(LowerError::Hole)));
}
#[test]
fn an_effectful_function_runs_via_a_host_import() {
let s = Store::open_in_memory().unwrap();
let now = s.put(&Node::Now).unwrap();
let mut time = BTreeSet::new();
time.insert(crate::ty::Effect::Time);
let clock = s
.put(&Node::Function {
name: "clock".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: time,
on_failure: vec![],
body: vec![],
result: now,
})
.unwrap();
let m = module(&s, vec![clock]);
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(report.ok(), "unexpected: {:?}", report.violations);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_effectful_i64(&wasm, "clock", &[], 42).unwrap(), 42);
assert_eq!(run_effectful_i64(&wasm, "clock", &[], 7).unwrap(), 7);
}
#[test]
fn using_an_effect_without_declaring_it_is_a_principle_5_violation() {
let s = Store::open_in_memory().unwrap();
let now = s.put(&Node::Now).unwrap();
let bad = s
.put(&Node::Function {
name: "bad".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(), on_failure: vec![],
body: vec![],
result: now,
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&bad).unwrap();
assert!(report.violations.iter().any(|v| v.principle == 5));
}
#[test]
fn records_construct_in_memory_and_fields_load() {
let s = Store::open_in_memory().unwrap();
let pd = s
.put(&Node::RecordDef {
name: "Point".into(),
fields: vec![
("x".into(), Type::Number),
("y".into(), Type::Number),
],
})
.unwrap();
let field_fn = |s: &Store, name: &str, field: &str| -> NodeHash {
let nref = s.put(&Node::Ref("n".into())).unwrap();
let n2 = s.put(&Node::Ref("n".into())).unwrap();
let one = s.put(&Node::Lit(1)).unwrap();
let yexpr = s
.put(&Node::BinOp {
op: crate::node::BinOp::Add,
lhs: n2,
rhs: one,
})
.unwrap();
let rec = s
.put(&Node::Record {
type_name: "Point".into(),
fields: vec![("x".into(), nref), ("y".into(), yexpr)],
})
.unwrap();
let step = s
.put(&Node::Step {
binding: "pt".into(),
value: rec,
})
.unwrap();
let ptref = s.put(&Node::Ref("pt".into())).unwrap();
let fx = s
.put(&Node::Field {
base: ptref,
type_name: "Point".into(),
field: field.into(),
})
.unwrap();
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params: vec![Param {
name: "n".into(),
ty: Type::Number,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![step],
result: fx,
})
.unwrap()
};
let mk_x = field_fn(&s, "mk_x", "x");
let mk_y = field_fn(&s, "mk_y", "y");
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![pd],
functions: vec![mk_x, mk_y],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "mk_x", &[7]).unwrap(), 7);
assert_eq!(run_i64(&wasm, "mk_y", &[7]).unwrap(), 8);
}
#[test]
fn variants_construct_and_match_dispatches() {
let s = Store::open_in_memory().unwrap();
let vd = s
.put(&Node::VariantDef {
name: "Status".into(),
cases: vec![
("Active".into(), vec![]),
("Closed".into(), vec![("reason".into(), Type::Number)]),
],
})
.unwrap();
let n = s.put(&Node::Ref("n".into())).unwrap();
let ctor = s
.put(&Node::Variant {
type_name: "Status".into(),
case: "Closed".into(),
fields: vec![("reason".into(), n)],
})
.unwrap();
let step = s
.put(&Node::Step {
binding: "s".into(),
value: ctor,
})
.unwrap();
let sref = s.put(&Node::Ref("s".into())).unwrap();
let zero = s.put(&Node::Lit(0)).unwrap();
let rref = s.put(&Node::Ref("reason".into())).unwrap();
let m_expr = s
.put(&Node::Match {
scrutinee: sref,
type_name: "Status".into(),
arms: vec![
crate::node::MatchArm {
case: "Active".into(),
bindings: vec![],
body: zero,
},
crate::node::MatchArm {
case: "Closed".into(),
bindings: vec!["reason".into()],
body: rref,
},
],
})
.unwrap();
let closed = s
.put(&Node::Function {
name: "closed".into(),
type_params: vec![],
params: vec![Param {
name: "n".into(),
ty: Type::Number,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![step],
result: m_expr,
})
.unwrap();
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![vd],
functions: vec![closed],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "closed", &[7]).unwrap(), 7);
assert_eq!(run_i64(&wasm, "closed", &[42]).unwrap(), 42);
}
#[test]
fn a_fallible_function_runs_and_fails() {
let s = Store::open_in_memory().unwrap();
let a = s.put(&Node::Ref("a".into())).unwrap();
let b = s.put(&Node::Ref("b".into())).unwrap();
let zero = s.put(&Node::Lit(0)).unwrap();
let is_zero = s
.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: b.clone(),
rhs: zero,
})
.unwrap();
let boom = s.put(&Node::Fail("DivByZero".into())).unwrap();
let div = s
.put(&Node::BinOp {
op: crate::node::BinOp::Div,
lhs: a,
rhs: b,
})
.unwrap();
let iff = s
.put(&Node::If {
cond: is_zero,
then_branch: boom,
else_branch: div,
})
.unwrap();
let f = s
.put(&Node::Function {
name: "safe_div".into(),
type_params: vec![],
params: vec![
Param {
name: "a".into(),
ty: Type::Number,
min_confidence: Confidence::External,
},
Param {
name: "b".into(),
ty: Type::Number,
min_confidence: Confidence::External,
},
],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec!["DivByZero".into()],
body: vec![],
result: iff,
})
.unwrap();
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![f],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_fallible(&wasm, "safe_div", &[6, 2]).unwrap(), Ok(3));
assert_eq!(run_fallible(&wasm, "safe_div", &[1, 0]).unwrap(), Err(1));
}
#[test]
fn handle_recovers_a_failure_and_passes_ok_through() {
let s = Store::open_in_memory().unwrap();
let mkfn = |s: &Store, name: &str, result: NodeHash| -> NodeHash {
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec!["Boom".into()],
body: vec![],
result,
})
.unwrap()
};
let boom = s.put(&Node::Fail("Boom".into())).unwrap();
let risky = mkfn(&s, "risky", boom); let seven = s.put(&Node::Lit(7)).unwrap();
let okfn = mkfn(&s, "okfn", seven);
let handled_caller = |s: &Store, name: &str, callee: &str| -> NodeHash {
let call = s
.put(&Node::Call {
func: callee.into(),
args: vec![],
})
.unwrap();
let zero = s.put(&Node::Lit(0)).unwrap();
let h = s
.put(&Node::Handle {
body: call,
handlers: vec![("Boom".into(), zero)],
})
.unwrap();
let step = s
.put(&Node::Step {
binding: "x".into(),
value: h,
})
.unwrap();
let xref = s.put(&Node::Ref("x".into())).unwrap();
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec![], body: vec![step],
result: xref,
})
.unwrap()
};
let recovered = handled_caller(&s, "recovered", "risky");
let passed = handled_caller(&s, "passed", "okfn");
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![risky, okfn, recovered, passed],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "recovered", &[]).unwrap(), 0); assert_eq!(run_i64(&wasm, "passed", &[]).unwrap(), 7); }
#[test]
fn a_generic_function_runs_at_a_scalar_and_a_pointer_type() {
let s = Store::open_in_memory().unwrap();
let pd = s
.put(&Node::RecordDef {
name: "Box".into(),
fields: vec![("v".into(), Type::Number)],
})
.unwrap();
let x = s.put(&Node::Ref("x".into())).unwrap();
let identity = s
.put(&Node::Function {
name: "identity".into(),
type_params: vec!["T".into()],
params: vec![Param {
name: "x".into(),
ty: Type::Var("T".into()),
min_confidence: Confidence::Structural,
}],
produces: Produces {
ty: Type::Var("T".into()),
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: x,
})
.unwrap();
let seven = s.put(&Node::Lit(7)).unwrap();
let cn = s
.put(&Node::Call {
func: "identity".into(),
args: vec![seven],
})
.unwrap();
let use_num = mono_fn(&s, "use_num", cn);
let five = s.put(&Node::Lit(5)).unwrap();
let rec = s
.put(&Node::Record {
type_name: "Box".into(),
fields: vec![("v".into(), five)],
})
.unwrap();
let cb = s
.put(&Node::Call {
func: "identity".into(),
args: vec![rec],
})
.unwrap();
let step = s
.put(&Node::Step {
binding: "b".into(),
value: cb,
})
.unwrap();
let bref = s.put(&Node::Ref("b".into())).unwrap();
let fv = s
.put(&Node::Field {
base: bref,
type_name: "Box".into(),
field: "v".into(),
})
.unwrap();
let use_box = s
.put(&Node::Function {
name: "use_box".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![step],
result: fv,
})
.unwrap();
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![pd],
functions: vec![identity, use_num, use_box],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "use_num", &[]).unwrap(), 7);
assert_eq!(run_i64(&wasm, "use_box", &[]).unwrap(), 5);
}
fn mono_fn(s: &Store, name: &str, result: NodeHash) -> NodeHash {
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result,
})
.unwrap()
}
#[test]
fn strings_lower_and_str_len_runs() {
let s = Store::open_in_memory().unwrap();
let hello = s.put(&Node::Str("hello".into())).unwrap();
let sl = s.put(&Node::StrLen(hello)).unwrap();
let size = mono_fn(&s, "size", sl);
let empty = s.put(&Node::Str(String::new())).unwrap();
let sl0 = s.put(&Node::StrLen(empty)).unwrap();
let zero = mono_fn(&s, "zero", sl0);
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![size, zero],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "size", &[]).unwrap(), 5);
assert_eq!(run_i64(&wasm, "zero", &[]).unwrap(), 0);
}
#[test]
fn lists_construct_and_index_and_measure() {
let s = Store::open_in_memory().unwrap();
let list = |s: &Store| -> NodeHash {
let a = s.put(&Node::Lit(10)).unwrap();
let b = s.put(&Node::Lit(20)).unwrap();
let cc = s.put(&Node::Lit(30)).unwrap();
s.put(&Node::List(vec![a, b, cc])).unwrap()
};
let two = s.put(&Node::Lit(2)).unwrap();
let third = s
.put(&Node::ListGet {
list: list(&s),
index: two,
})
.unwrap();
let get_third = mono_fn(&s, "third", third);
let len = s.put(&Node::ListLen(list(&s))).unwrap();
let len3 = mono_fn(&s, "len3", len);
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![get_third, len3],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "third", &[]).unwrap(), 30);
assert_eq!(run_i64(&wasm, "len3", &[]).unwrap(), 3);
}
#[test]
fn maps_construct_lookup_and_measure() {
let s = Store::open_in_memory().unwrap();
let map = |s: &Store| -> NodeHash {
let k1 = s.put(&Node::Lit(1)).unwrap();
let v1 = s.put(&Node::Lit(10)).unwrap();
let k2 = s.put(&Node::Lit(2)).unwrap();
let v2 = s.put(&Node::Lit(20)).unwrap();
let k3 = s.put(&Node::Lit(3)).unwrap();
let v3 = s.put(&Node::Lit(30)).unwrap();
s.put(&Node::Map(vec![(k1, v1), (k2, v2), (k3, v3)]))
.unwrap()
};
let two = s.put(&Node::Lit(2)).unwrap();
let hit = s
.put(&Node::MapGet {
map: map(&s),
key: two,
})
.unwrap();
let lookup = mono_fn(&s, "lookup", hit);
let nine = s.put(&Node::Lit(9)).unwrap();
let miss = s
.put(&Node::MapGet {
map: map(&s),
key: nine,
})
.unwrap();
let missing = mono_fn(&s, "missing", miss);
let sz = s.put(&Node::MapLen(map(&s))).unwrap();
let size = mono_fn(&s, "size", sz);
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![lookup, missing, size],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "lookup", &[]).unwrap(), 20); assert_eq!(run_i64(&wasm, "missing", &[]).unwrap(), 0); assert_eq!(run_i64(&wasm, "size", &[]).unwrap(), 3);
}
#[test]
fn the_log_effect_runs_and_is_pass_through() {
let s = Store::open_in_memory().unwrap();
let n = s.put(&Node::Ref("n".into())).unwrap();
let logged = s.put(&Node::Log(n)).unwrap();
let mut log_eff = BTreeSet::new();
log_eff.insert(crate::ty::Effect::Log);
let f = s
.put(&Node::Function {
name: "trace".into(),
type_params: vec![],
params: vec![Param {
name: "n".into(),
ty: Type::Number,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: log_eff,
on_failure: vec![],
body: vec![],
result: logged,
})
.unwrap();
let m = module(&s, vec![f]);
assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "trace", &[99]).unwrap(), 99);
}
#[test]
fn logging_without_declaring_log_is_a_principle_5_violation() {
let s = Store::open_in_memory().unwrap();
let lit = s.put(&Node::Lit(1)).unwrap();
let logged = s.put(&Node::Log(lit)).unwrap();
let f = s
.put(&Node::Function {
name: "bad".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(), on_failure: vec![],
body: vec![],
result: logged,
})
.unwrap();
let r = crate::check::Checker::new(&s).check(&f).unwrap();
assert!(r.violations.iter().any(|v| v.principle == 5));
}
#[test]
fn the_rand_effect_runs() {
let s = Store::open_in_memory().unwrap();
let r = s.put(&Node::Rand).unwrap();
let mut rand_eff = BTreeSet::new();
rand_eff.insert(crate::ty::Effect::Rand);
let f = s
.put(&Node::Function {
name: "pick".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: rand_eff,
on_failure: vec![],
body: vec![],
result: r,
})
.unwrap();
let m = module(&s, vec![f]);
assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
let wasm = lower(&s, &m).unwrap();
let draws: std::collections::HashSet<i64> = (0..5)
.map(|_| run_i64(&wasm, "pick", &[]).unwrap())
.collect();
assert!(draws.len() > 1, "rand returned a constant: {draws:?}");
}
#[test]
fn a_mutable_cell_creates_sets_and_reads() {
let s = Store::open_in_memory().unwrap();
let ten = s.put(&Node::Lit(10)).unwrap();
let cell = s.put(&Node::MutNew(ten)).unwrap();
let step_c = s
.put(&Node::Step {
binding: "c".into(),
value: cell,
})
.unwrap();
let cref1 = s.put(&Node::Ref("c".into())).unwrap();
let twenty = s.put(&Node::Lit(20)).unwrap();
let set = s
.put(&Node::MutSet {
cell: cref1,
value: twenty,
})
.unwrap();
let step_set = s
.put(&Node::Step {
binding: "_".into(),
value: set,
})
.unwrap();
let cref2 = s.put(&Node::Ref("c".into())).unwrap();
let getc = s.put(&Node::MutGet(cref2)).unwrap();
let mut mut_eff = BTreeSet::new();
mut_eff.insert(crate::ty::Effect::Mut);
let f = s
.put(&Node::Function {
name: "counter".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: mut_eff,
on_failure: vec![],
body: vec![step_c, step_set],
result: getc,
})
.unwrap();
let m = module(&s, vec![f]);
assert!(
crate::check::Checker::new(&s).check(&m).unwrap().ok(),
"checker rejected the cell program"
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "counter", &[]).unwrap(), 20);
}
#[test]
fn mutating_without_declaring_mut_is_a_principle_5_violation() {
let s = Store::open_in_memory().unwrap();
let one = s.put(&Node::Lit(1)).unwrap();
let cell = s.put(&Node::MutNew(one)).unwrap();
let f = s
.put(&Node::Function {
name: "bad".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Cell(Box::new(Type::Number)),
confidence: Confidence::Structural,
},
requires: BTreeSet::new(), on_failure: vec![],
body: vec![],
result: cell,
})
.unwrap();
let r = crate::check::Checker::new(&s).check(&f).unwrap();
assert!(r.violations.iter().any(|v| v.principle == 5));
}
#[test]
fn the_disk_effect_writes_and_reads_a_real_file() {
let s = Store::open_in_memory().unwrap();
let path = std::env::temp_dir()
.join(format!("cairn_disk_{}.txt", std::process::id()))
.to_string_lossy()
.into_owned();
let _ = std::fs::remove_file(&path);
let p1 = s.put(&Node::Str(path.clone())).unwrap();
let content = s.put(&Node::Str("hello".into())).unwrap();
let w = s
.put(&Node::DiskWrite {
path: p1,
content,
})
.unwrap();
let step = s
.put(&Node::Step {
binding: "_".into(),
value: w,
})
.unwrap();
let p2 = s.put(&Node::Str(path.clone())).unwrap();
let rd = s.put(&Node::DiskRead(p2)).unwrap();
let expect = s.put(&Node::Str("hello".into())).unwrap();
let rd = s.put(&Node::StrEq(rd, expect)).unwrap();
let mut disk = BTreeSet::new();
disk.insert(crate::ty::Effect::Disk);
let f = s
.put(&Node::Function {
name: "io".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Bool, confidence: Confidence::External,
},
requires: disk,
on_failure: vec![],
body: vec![step],
result: rd,
})
.unwrap();
let m = module(&s, vec![f]);
assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
let wasm = lower(&s, &m).unwrap();
let got = run_i64(&wasm, "io", &[]).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(got, 1);
}
#[test]
fn net_without_declaring_it_is_a_principle_5_violation() {
let s = Store::open_in_memory().unwrap();
let url = s.put(&Node::Str("u".into())).unwrap();
let g = s.put(&Node::NetGet(url)).unwrap();
let f = s
.put(&Node::Function {
name: "bad".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(), on_failure: vec![],
body: vec![],
result: g,
})
.unwrap();
let r = crate::check::Checker::new(&s).check(&f).unwrap();
assert!(r.violations.iter().any(|v| v.principle == 5));
}
#[test]
fn the_db_effect_returns_a_string_result() {
let s = Store::open_in_memory().unwrap();
let sql = s.put(&Node::Str("SELECT 1".into())).unwrap();
let q = s
.put(&Node::DbQuery {
sql,
params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
})
.unwrap();
let expect = s.put(&Node::Str("1".into())).unwrap();
let eq = s.put(&Node::StrEq(q, expect)).unwrap();
let mut db = BTreeSet::new();
db.insert(crate::ty::Effect::Db);
let f = s
.put(&Node::Function {
name: "query".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Bool,
confidence: Confidence::Structural,
},
requires: db,
on_failure: vec![],
body: vec![],
result: eq,
})
.unwrap();
let m = module(&s, vec![f]);
assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "query", &[]).unwrap(), 1);
}
#[test]
fn string_concat_slice_and_eq_run_correctly() {
let s = Store::open_in_memory().unwrap();
let lit = |s: &Store, t: &str| s.put(&Node::Str(t.into())).unwrap();
let cc = s
.put(&Node::StrConcat(lit(&s, "ab"), lit(&s, "c")))
.unwrap();
let cc_eq = s.put(&Node::StrEq(cc, lit(&s, "abc"))).unwrap();
let concat_ok = mono_fn(&s, "concat_ok", cc_eq);
let one = s.put(&Node::Lit(1)).unwrap();
let three = s.put(&Node::Lit(3)).unwrap();
let sl = s
.put(&Node::StrSlice {
s: lit(&s, "hello"),
start: one,
len: three,
})
.unwrap();
let sl_eq = s.put(&Node::StrEq(sl, lit(&s, "ell"))).unwrap();
let slice_ok = mono_fn(&s, "slice_ok", sl_eq);
let ne = s
.put(&Node::StrEq(lit(&s, "abc"), lit(&s, "abd")))
.unwrap();
let neq = mono_fn(&s, "neq", ne);
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![concat_ok, slice_ok, neq],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "concat_ok", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "slice_ok", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "neq", &[]).unwrap(), 0);
}
#[test]
fn str_contains_runs() {
let s = Store::open_in_memory().unwrap();
let lit = |s: &Store, t: &str| s.put(&Node::Str(t.into())).unwrap();
let mk = |s: &Store, name: &str, h: &str, n: &str| -> NodeHash {
let node = s
.put(&Node::StrContains {
haystack: lit(s, h),
needle: lit(s, n),
})
.unwrap();
mono_fn(s, name, node)
};
let hit = mk(&s, "hit", "hello world", "o w"); let miss = mk(&s, "miss", "hello", "xyz"); let empty = mk(&s, "empty", "abc", ""); let edge = mk(&s, "edge", "abc", "abcd");
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![hit, miss, empty, edge],
})
.unwrap();
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "hit", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "miss", &[]).unwrap(), 0);
assert_eq!(run_i64(&wasm, "empty", &[]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "edge", &[]).unwrap(), 0);
}
#[test]
fn cairn_as_an_http_handler_round_trips_request_and_response() {
let s = Store::open_in_memory().unwrap();
let prefix = s.put(&Node::Str("echo:".into())).unwrap();
let req = s.put(&Node::Ref("req".into())).unwrap();
let body = s.put(&Node::StrConcat(prefix, req)).unwrap();
let handle = s
.put(&Node::Function {
name: "handle".into(),
type_params: vec![],
params: vec![Param {
name: "req".into(),
ty: Type::String,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::String,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: body,
})
.unwrap();
let m = module(&s, vec![handle]);
assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_once(&wasm, "handle", "/hello").unwrap(),
"echo:/hello"
);
assert_eq!(serve_once(&wasm, "handle", "").unwrap(), "echo:");
}
#[test]
fn a_minimal_cairn_web_app_routes_and_renders_html() {
let s = Store::open_in_memory().unwrap();
let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let req_param = || Param {
name: "req".into(),
ty: Type::String,
min_confidence: Confidence::External,
};
let home_body = str_lit("<h1>Cairn</h1>");
let home = s
.put(&Node::Function {
name: "home".into(),
type_params: vec![],
params: vec![req_param()],
produces: Produces {
ty: Type::String,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: home_body,
})
.unwrap();
let q = s
.put(&Node::DbQuery {
sql: str_lit("SELECT 'Acme'"),
params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
})
.unwrap();
let inner = s.put(&Node::StrConcat(q, str_lit("</ul>"))).unwrap();
let cust_body = s
.put(&Node::StrConcat(str_lit("<ul>"), inner))
.unwrap();
let mut db = BTreeSet::new();
db.insert(crate::ty::Effect::Db);
let customers = s
.put(&Node::Function {
name: "customers".into(),
type_params: vec![],
params: vec![req_param()],
produces: Produces {
ty: Type::String,
confidence: Confidence::Structural,
},
requires: db.clone(),
on_failure: vec![],
body: vec![],
result: cust_body,
})
.unwrap();
let is_root = s
.put(&Node::StrEq(
s.put(&Node::Ref("req".into())).unwrap(),
str_lit("/"),
))
.unwrap();
let call_home = s
.put(&Node::Call {
func: "home".into(),
args: vec![s.put(&Node::Ref("req".into())).unwrap()],
})
.unwrap();
let has_cust = s
.put(&Node::StrContains {
haystack: s.put(&Node::Ref("req".into())).unwrap(),
needle: str_lit("/customers"),
})
.unwrap();
let call_cust = s
.put(&Node::Call {
func: "customers".into(),
args: vec![s.put(&Node::Ref("req".into())).unwrap()],
})
.unwrap();
let inner_if = s
.put(&Node::If {
cond: has_cust,
then_branch: call_cust,
else_branch: str_lit("<h1>404</h1>"),
})
.unwrap();
let route = s
.put(&Node::If {
cond: is_root,
then_branch: call_home,
else_branch: inner_if,
})
.unwrap();
let router = s
.put(&Node::Function {
name: "router".into(),
type_params: vec![],
params: vec![req_param()],
produces: Produces {
ty: Type::String,
confidence: Confidence::External,
},
requires: db,
on_failure: vec![],
body: vec![],
result: route,
})
.unwrap();
let m = s
.put(&Node::Module {
name: "app".into(),
types: vec![],
functions: vec![home, customers, router],
})
.unwrap();
assert!(
crate::check::Checker::new(&s).check(&m).unwrap().ok(),
"the web app did not type-check"
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_once(&wasm, "router", "/").unwrap(),
"<h1>Cairn</h1>"
);
assert_eq!(
serve_once(&wasm, "router", "/customers").unwrap(),
"<ul>Acme</ul>"
);
assert_eq!(
serve_once(&wasm, "router", "/nope").unwrap(),
"<h1>404</h1>"
);
}
#[test]
fn a_function_can_call_itself_recursively() {
let s = Store::open_in_memory().unwrap();
let nref = s.put(&Node::Ref("n".into())).unwrap();
let zero = s.put(&Node::Lit(0)).unwrap();
let cond = s
.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: nref.clone(),
rhs: zero.clone(),
})
.unwrap();
let one = s.put(&Node::Lit(1)).unwrap();
let nm1 = s
.put(&Node::BinOp {
op: crate::node::BinOp::Sub,
lhs: nref.clone(),
rhs: one,
})
.unwrap();
let rec = s
.put(&Node::Call {
func: "sum".into(),
args: vec![nm1],
})
.unwrap();
let n_plus_rec = s
.put(&Node::BinOp {
op: crate::node::BinOp::Add,
lhs: nref,
rhs: rec,
})
.unwrap();
let body = s
.put(&Node::If {
cond,
then_branch: zero,
else_branch: n_plus_rec,
})
.unwrap();
let sum = func(&s, "sum", &["n"], vec![], body);
let m = module(&s, vec![sum]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "sum", &[5]).unwrap(), 15); assert_eq!(run_i64(&wasm, "sum", &[0]).unwrap(), 0);
}
#[test]
fn mutually_recursive_functions_lower_regardless_of_order() {
let s = Store::open_in_memory().unwrap();
let mk = |name: &str, other: &str, base: i64| -> NodeHash {
let nref = s.put(&Node::Ref("n".into())).unwrap();
let zero = s.put(&Node::Lit(0)).unwrap();
let cond = s
.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: nref.clone(),
rhs: zero,
})
.unwrap();
let base_lit = s.put(&Node::Lit(base)).unwrap();
let one = s.put(&Node::Lit(1)).unwrap();
let nm1 = s
.put(&Node::BinOp {
op: crate::node::BinOp::Sub,
lhs: nref,
rhs: one,
})
.unwrap();
let rec = s
.put(&Node::Call {
func: other.into(),
args: vec![nm1],
})
.unwrap();
let body = s
.put(&Node::If {
cond,
then_branch: base_lit,
else_branch: rec,
})
.unwrap();
func(&s, name, &["n"], vec![], body)
};
let is_even = mk("is_even", "is_odd", 1);
let is_odd = mk("is_odd", "is_even", 0);
let m = module(&s, vec![is_even, is_odd]);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "is_even", &[10]).unwrap(), 1);
assert_eq!(run_i64(&wasm, "is_even", &[7]).unwrap(), 0);
assert_eq!(run_i64(&wasm, "is_odd", &[7]).unwrap(), 1);
}
#[test]
fn a_typed_element_tree_renders_to_html() {
let s = Store::open_in_memory().unwrap();
let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let cat = |a: NodeHash, b: NodeHash| -> NodeHash {
s.put(&Node::StrConcat(a, b)).unwrap()
};
let elem_ty = Type::Named("Element".into());
let str_out = || Produces {
ty: Type::String,
confidence: Confidence::External,
};
let param = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let element_def = s
.put(&Node::VariantDef {
name: "Element".into(),
cases: vec![
("Text".into(), vec![("content".into(), Type::String)]),
(
"El".into(),
vec![
("tag".into(), Type::String),
("kids".into(), Type::List(Box::new(elem_ty.clone()))),
],
),
],
})
.unwrap();
let kids_ref = s.put(&Node::Ref("kids".into())).unwrap();
let i_ref = s.put(&Node::Ref("i".into())).unwrap();
let len_step = s
.put(&Node::Step {
binding: "n".into(),
value: s.put(&Node::ListLen(kids_ref.clone())).unwrap(),
})
.unwrap();
let at_end = s
.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: i_ref.clone(),
rhs: s.put(&Node::Ref("n".into())).unwrap(),
})
.unwrap();
let head = s
.put(&Node::Call {
func: "render_html".into(),
args: vec![s
.put(&Node::ListGet {
list: kids_ref.clone(),
index: i_ref.clone(),
})
.unwrap()],
})
.unwrap();
let tail = s
.put(&Node::Call {
func: "render_kids".into(),
args: vec![
kids_ref,
s.put(&Node::BinOp {
op: crate::node::BinOp::Add,
lhs: i_ref,
rhs: s.put(&Node::Lit(1)).unwrap(),
})
.unwrap(),
],
})
.unwrap();
let kids_body = s
.put(&Node::If {
cond: at_end,
then_branch: str_lit(""),
else_branch: cat(head, tail),
})
.unwrap();
let render_kids = s
.put(&Node::Function {
name: "render_kids".into(),
type_params: vec![],
params: vec![
param("kids", Type::List(Box::new(elem_ty.clone()))),
param("i", Type::Number),
],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![len_step],
result: kids_body,
})
.unwrap();
let tag_ref = s.put(&Node::Ref("tag".into())).unwrap();
let open = cat(cat(str_lit("<"), tag_ref.clone()), str_lit(">"));
let close = cat(cat(str_lit("</"), tag_ref), str_lit(">"));
let inner = s
.put(&Node::Call {
func: "render_kids".into(),
args: vec![
s.put(&Node::Ref("kids".into())).unwrap(),
s.put(&Node::Lit(0)).unwrap(),
],
})
.unwrap();
let el_body = cat(open, cat(inner, close));
let html_match = s
.put(&Node::Match {
scrutinee: s.put(&Node::Ref("e".into())).unwrap(),
type_name: "Element".into(),
arms: vec![
crate::node::MatchArm {
case: "Text".into(),
bindings: vec!["content".into()],
body: s.put(&Node::Ref("content".into())).unwrap(),
},
crate::node::MatchArm {
case: "El".into(),
bindings: vec!["tag".into(), "kids".into()],
body: el_body,
},
],
})
.unwrap();
let render_html = s
.put(&Node::Function {
name: "render_html".into(),
type_params: vec![],
params: vec![param("e", elem_ty.clone())],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: html_match,
})
.unwrap();
let li = |text: &str| -> NodeHash {
let t = s
.put(&Node::Variant {
type_name: "Element".into(),
case: "Text".into(),
fields: vec![("content".into(), str_lit(text))],
})
.unwrap();
s.put(&Node::Variant {
type_name: "Element".into(),
case: "El".into(),
fields: vec![
("tag".into(), str_lit("li")),
("kids".into(), s.put(&Node::List(vec![t])).unwrap()),
],
})
.unwrap()
};
let ul = s
.put(&Node::Variant {
type_name: "Element".into(),
case: "El".into(),
fields: vec![
("tag".into(), str_lit("ul")),
(
"kids".into(),
s.put(&Node::List(vec![li("Hello"), li("World")]))
.unwrap(),
),
],
})
.unwrap();
let home = s
.put(&Node::Function {
name: "home".into(),
type_params: vec![],
params: vec![param("req", Type::String)],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: s
.put(&Node::Call {
func: "render_html".into(),
args: vec![ul],
})
.unwrap(),
})
.unwrap();
let m = s
.put(&Node::Module {
name: "view".into(),
types: vec![element_def],
functions: vec![home, render_html, render_kids],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"typed view model did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_once(&wasm, "home", "/").unwrap(),
"<ul><li>Hello</li><li>World</li></ul>"
);
}
#[test]
fn a_structured_request_routes_to_a_typed_response() {
let s = Store::open_in_memory().unwrap();
let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let resp = |status: i64, body: &str| -> NodeHash {
s.put(&Node::Record {
type_name: "Response".into(),
fields: vec![
("status".into(), s.put(&Node::Lit(status)).unwrap()),
("body".into(), str_lit(body)),
],
})
.unwrap()
};
let field = |binding: &str, f: &str| -> NodeHash {
s.put(&Node::Step {
binding: binding.into(),
value: s
.put(&Node::Field {
base: s.put(&Node::Ref("req".into())).unwrap(),
type_name: "Request".into(),
field: f.into(),
})
.unwrap(),
})
.unwrap()
};
let path_is = |p: &str| -> NodeHash {
s.put(&Node::StrEq(
s.put(&Node::Ref("path".into())).unwrap(),
str_lit(p),
))
.unwrap()
};
let is_post = s
.put(&Node::StrEq(
s.put(&Node::Ref("method".into())).unwrap(),
str_lit("POST"),
))
.unwrap();
let customers = s
.put(&Node::If {
cond: is_post,
then_branch: resp(201, "created"),
else_branch: resp(200, "list"),
})
.unwrap();
let body = s
.put(&Node::If {
cond: path_is("/"),
then_branch: resp(200, "home"),
else_branch: s
.put(&Node::If {
cond: path_is("/customers"),
then_branch: customers,
else_branch: resp(404, "not found"),
})
.unwrap(),
})
.unwrap();
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![Param {
name: "req".into(),
ty: Type::Named("Request".into()),
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Named("Response".into()),
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![field("method", "method"), field("path", "path")],
result: body,
})
.unwrap();
let m = s
.put(&Node::Module {
name: "app".into(),
types: vec![request_def, response_def],
functions: vec![route],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"structured router did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request(&wasm, "route", "GET", "/", "").unwrap(),
HttpResponse {
status: 200,
body: "home".into()
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/customers", "").unwrap(),
HttpResponse {
status: 200,
body: "list".into()
}
);
assert_eq!(
serve_request(&wasm, "route", "POST", "/customers", "").unwrap(),
HttpResponse {
status: 201,
body: "created".into()
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/nope", "").unwrap(),
HttpResponse {
status: 404,
body: "not found".into()
}
);
}
#[test]
fn a_deckhand_customers_page_end_to_end() {
let s = Store::open_in_memory().unwrap();
let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let cat = |a: NodeHash, b: NodeHash| -> NodeHash {
s.put(&Node::StrConcat(a, b)).unwrap()
};
let elem_ty = Type::Named("Element".into());
let str_out = || Produces {
ty: Type::String,
confidence: Confidence::External,
};
let param = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let element_def = s
.put(&Node::VariantDef {
name: "Element".into(),
cases: vec![
("Text".into(), vec![("content".into(), Type::String)]),
(
"El".into(),
vec![
("tag".into(), Type::String),
("kids".into(), Type::List(Box::new(elem_ty.clone()))),
],
),
],
})
.unwrap();
let kids_ref = s.put(&Node::Ref("kids".into())).unwrap();
let i_ref = s.put(&Node::Ref("i".into())).unwrap();
let len_step = s
.put(&Node::Step {
binding: "n".into(),
value: s.put(&Node::ListLen(kids_ref.clone())).unwrap(),
})
.unwrap();
let at_end = s
.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: i_ref.clone(),
rhs: s.put(&Node::Ref("n".into())).unwrap(),
})
.unwrap();
let head = s
.put(&Node::Call {
func: "render_html".into(),
args: vec![s
.put(&Node::ListGet {
list: kids_ref.clone(),
index: i_ref.clone(),
})
.unwrap()],
})
.unwrap();
let tail = s
.put(&Node::Call {
func: "render_kids".into(),
args: vec![
kids_ref,
s.put(&Node::BinOp {
op: crate::node::BinOp::Add,
lhs: i_ref,
rhs: s.put(&Node::Lit(1)).unwrap(),
})
.unwrap(),
],
})
.unwrap();
let render_kids = s
.put(&Node::Function {
name: "render_kids".into(),
type_params: vec![],
params: vec![
param("kids", Type::List(Box::new(elem_ty.clone()))),
param("i", Type::Number),
],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![len_step],
result: s
.put(&Node::If {
cond: at_end,
then_branch: str_lit(""),
else_branch: cat(head, tail),
})
.unwrap(),
})
.unwrap();
let tag_ref = s.put(&Node::Ref("tag".into())).unwrap();
let open = cat(cat(str_lit("<"), tag_ref.clone()), str_lit(">"));
let close = cat(cat(str_lit("</"), tag_ref), str_lit(">"));
let inner = s
.put(&Node::Call {
func: "render_kids".into(),
args: vec![
s.put(&Node::Ref("kids".into())).unwrap(),
s.put(&Node::Lit(0)).unwrap(),
],
})
.unwrap();
let render_html = s
.put(&Node::Function {
name: "render_html".into(),
type_params: vec![],
params: vec![param("e", elem_ty.clone())],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: s
.put(&Node::Match {
scrutinee: s.put(&Node::Ref("e".into())).unwrap(),
type_name: "Element".into(),
arms: vec![
crate::node::MatchArm {
case: "Text".into(),
bindings: vec!["content".into()],
body: s.put(&Node::Ref("content".into())).unwrap(),
},
crate::node::MatchArm {
case: "El".into(),
bindings: vec!["tag".into(), "kids".into()],
body: cat(open, cat(inner, close)),
},
],
})
.unwrap(),
})
.unwrap();
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let el = |tag: &str, kids: Vec<NodeHash>| -> NodeHash {
s.put(&Node::Variant {
type_name: "Element".into(),
case: "El".into(),
fields: vec![
("tag".into(), str_lit(tag)),
("kids".into(), s.put(&Node::List(kids)).unwrap()),
],
})
.unwrap()
};
let rows = s
.put(&Node::DbQuery {
sql: str_lit("SELECT 'Acme'"),
params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
})
.unwrap();
let row_text = s
.put(&Node::Variant {
type_name: "Element".into(),
case: "Text".into(),
fields: vec![("content".into(), rows)],
})
.unwrap();
let page = el("ul", vec![el("li", vec![row_text])]);
let customers = s
.put(&Node::Record {
type_name: "Response".into(),
fields: vec![
("status".into(), s.put(&Node::Lit(200)).unwrap()),
(
"body".into(),
s.put(&Node::Call {
func: "render_html".into(),
args: vec![page],
})
.unwrap(),
),
],
})
.unwrap();
let not_found = s
.put(&Node::Record {
type_name: "Response".into(),
fields: vec![
("status".into(), s.put(&Node::Lit(404)).unwrap()),
("body".into(), str_lit("not found")),
],
})
.unwrap();
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![param("req", Type::Named("Request".into()))],
produces: Produces {
ty: Type::Named("Response".into()),
confidence: Confidence::External,
},
requires: {
let mut e = BTreeSet::new();
e.insert(crate::ty::Effect::Db);
e
},
on_failure: vec![],
body: vec![s
.put(&Node::Step {
binding: "path".into(),
value: s
.put(&Node::Field {
base: s.put(&Node::Ref("req".into())).unwrap(),
type_name: "Request".into(),
field: "path".into(),
})
.unwrap(),
})
.unwrap()],
result: s
.put(&Node::If {
cond: s
.put(&Node::StrEq(
s.put(&Node::Ref("path".into())).unwrap(),
str_lit("/customers"),
))
.unwrap(),
then_branch: customers,
else_branch: not_found,
})
.unwrap(),
})
.unwrap();
let m = s
.put(&Node::Module {
name: "deckhand".into(),
types: vec![element_def, request_def, response_def],
functions: vec![route, render_html, render_kids],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"the Deckhand page did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request(&wasm, "route", "GET", "/customers", "").unwrap(),
HttpResponse {
status: 200,
body: "<ul><li>Acme</li></ul>".into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/", "").unwrap(),
HttpResponse {
status: 404,
body: "not found".into(),
}
);
}
#[test]
fn reason_phrase_covers_framework_codes_and_falls_back_by_class() {
assert_eq!(reason_phrase(200), "OK");
assert_eq!(reason_phrase(201), "Created");
assert_eq!(reason_phrase(404), "Not Found");
assert_eq!(reason_phrase(500), "Internal Server Error");
assert_eq!(reason_phrase(299), "OK");
assert_eq!(reason_phrase(418), "Client Error");
assert_eq!(reason_phrase(0), "Error");
}
#[test]
fn prefix_routing_extracts_a_path_param() {
let s = Store::open_in_memory().unwrap();
let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let resp = |status: i64, body: NodeHash| -> NodeHash {
s.put(&Node::Record {
type_name: "Response".into(),
fields: vec![
("status".into(), s.put(&Node::Lit(status)).unwrap()),
("body".into(), body),
],
})
.unwrap()
};
let path_ref = || s.put(&Node::Ref("path".into())).unwrap();
let plen_ref = || s.put(&Node::Ref("plen".into())).unwrap();
let id = s
.put(&Node::StrSlice {
s: path_ref(),
start: plen_ref(),
len: s
.put(&Node::BinOp {
op: crate::node::BinOp::Sub,
lhs: s.put(&Node::StrLen(path_ref())).unwrap(),
rhs: plen_ref(),
})
.unwrap(),
})
.unwrap();
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![Param {
name: "req".into(),
ty: Type::Named("Request".into()),
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Named("Response".into()),
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
s.put(&Node::Step {
binding: "path".into(),
value: s
.put(&Node::Field {
base: s.put(&Node::Ref("req".into())).unwrap(),
type_name: "Request".into(),
field: "path".into(),
})
.unwrap(),
})
.unwrap(),
s.put(&Node::Step {
binding: "plen".into(),
value: s.put(&Node::StrLen(str_lit("/customers/"))).unwrap(),
})
.unwrap(),
],
result: s
.put(&Node::If {
cond: s
.put(&Node::StrStartsWith {
s: path_ref(),
prefix: str_lit("/customers/"),
})
.unwrap(),
then_branch: resp(
200,
s.put(&Node::StrConcat(str_lit("customer "), id))
.unwrap(),
),
else_branch: resp(404, str_lit("not found")),
})
.unwrap(),
})
.unwrap();
assert!(crate::render::render(&s, &route)
.unwrap()
.contains("str_starts_with("));
let m = s
.put(&Node::Module {
name: "app".into(),
types: vec![request_def, response_def],
functions: vec![route],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"prefix router did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request(&wasm, "route", "GET", "/customers/42", "").unwrap(),
HttpResponse {
status: 200,
body: "customer 42".into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/about", "").unwrap(),
HttpResponse {
status: 404,
body: "not found".into(),
}
);
}
#[test]
fn number_and_string_conversions_round_trip() {
let s = Store::open_in_memory().unwrap();
let req = s.put(&Node::Ref("req".into())).unwrap();
let parsed = s.put(&Node::StrToNumber(req)).unwrap();
let back = s.put(&Node::NumberToStr(parsed)).unwrap();
let conv = s
.put(&Node::Function {
name: "conv".into(),
type_params: vec![],
params: vec![Param {
name: "req".into(),
ty: Type::String,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::String,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: back,
})
.unwrap();
let rendered = crate::render::render(&s, &conv).unwrap();
assert!(rendered.contains("number_to_str("));
assert!(rendered.contains("str_to_number("));
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![conv],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"conversions did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
for (input, expected) in [
("42", "42"),
("-7", "-7"),
("0", "0"),
("-1000000", "-1000000"),
("12ab", "12"), ("x", "0"), ("", "0"), ] {
assert_eq!(
serve_once(&wasm, "conv", input).unwrap(),
expected,
"conv({input:?})"
);
}
}
#[test]
fn numeric_path_param_routes() {
let s = Store::open_in_memory().unwrap();
let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let resp = |status: i64, body: NodeHash| -> NodeHash {
s.put(&Node::Record {
type_name: "Response".into(),
fields: vec![
("status".into(), s.put(&Node::Lit(status)).unwrap()),
("body".into(), body),
],
})
.unwrap()
};
let path_ref = || s.put(&Node::Ref("path".into())).unwrap();
let plen_ref = || s.put(&Node::Ref("plen".into())).unwrap();
let id_str = s
.put(&Node::StrSlice {
s: path_ref(),
start: plen_ref(),
len: s
.put(&Node::BinOp {
op: crate::node::BinOp::Sub,
lhs: s.put(&Node::StrLen(path_ref())).unwrap(),
rhs: plen_ref(),
})
.unwrap(),
})
.unwrap();
let echoed = s
.put(&Node::StrConcat(
str_lit("id="),
s.put(&Node::NumberToStr(
s.put(&Node::StrToNumber(id_str)).unwrap(),
))
.unwrap(),
))
.unwrap();
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![Param {
name: "req".into(),
ty: Type::Named("Request".into()),
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Named("Response".into()),
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
s.put(&Node::Step {
binding: "path".into(),
value: s
.put(&Node::Field {
base: s.put(&Node::Ref("req".into())).unwrap(),
type_name: "Request".into(),
field: "path".into(),
})
.unwrap(),
})
.unwrap(),
s.put(&Node::Step {
binding: "plen".into(),
value: s.put(&Node::StrLen(str_lit("/customers/"))).unwrap(),
})
.unwrap(),
],
result: s
.put(&Node::If {
cond: s
.put(&Node::StrStartsWith {
s: path_ref(),
prefix: str_lit("/customers/"),
})
.unwrap(),
then_branch: resp(200, echoed),
else_branch: resp(404, str_lit("not found")),
})
.unwrap(),
})
.unwrap();
let m = s
.put(&Node::Module {
name: "app".into(),
types: vec![request_def, response_def],
functions: vec![route],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"numeric router did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request(&wasm, "route", "GET", "/customers/42", "").unwrap(),
HttpResponse {
status: 200,
body: "id=42".into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/customers/abc", "").unwrap(),
HttpResponse {
status: 200,
body: "id=0".into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/x", "").unwrap(),
HttpResponse {
status: 404,
body: "not found".into(),
}
);
}
#[test]
fn a_rails_style_blog_app() {
let s = Store::open_in_memory().unwrap();
let lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
let cats = |parts: &[NodeHash]| -> NodeHash {
let mut it = parts.iter().cloned();
let first = it.next().unwrap();
it.fold(first, |acc, p| s.put(&Node::StrConcat(acc, p)).unwrap())
};
let param = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let str_out = || Produces {
ty: Type::String,
confidence: Confidence::External,
};
let post_ty = Type::Named("Post".into());
let post_def = s
.put(&Node::RecordDef {
name: "Post".into(),
fields: vec![
("id".into(), Type::Number),
("title".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let post = |id: i64, title: &str, body: &str| -> NodeHash {
s.put(&Node::Record {
type_name: "Post".into(),
fields: vec![
("id".into(), num(id)),
("title".into(), lit(title)),
("body".into(), lit(body)),
],
})
.unwrap()
};
let posts = s
.put(&Node::Function {
name: "posts".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::List(Box::new(post_ty.clone())),
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: s
.put(&Node::List(vec![
post(1, "Hello World", "The first post."),
post(2, "On Cairn", "Programs as reasoning chains."),
]))
.unwrap(),
})
.unwrap();
let field = |f: &str| -> NodeHash {
s.put(&Node::Field {
base: s
.put(&Node::ListGet {
list: r("ps"),
index: r("i"),
})
.unwrap(),
type_name: "Post".into(),
field: f.into(),
})
.unwrap()
};
let len_step = || {
s.put(&Node::Step {
binding: "n".into(),
value: s.put(&Node::ListLen(r("ps"))).unwrap(),
})
.unwrap()
};
let at_end = || {
s.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: r("i"),
rhs: r("n"),
})
.unwrap()
};
let next_i = || {
s.put(&Node::BinOp {
op: crate::node::BinOp::Add,
lhs: r("i"),
rhs: num(1),
})
.unwrap()
};
let posts_html = s
.put(&Node::Function {
name: "posts_html".into(),
type_params: vec![],
params: vec![
param("ps", Type::List(Box::new(post_ty.clone()))),
param("i", Type::Number),
],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![len_step()],
result: s
.put(&Node::If {
cond: at_end(),
then_branch: lit(""),
else_branch: cats(&[
lit("<li>"),
s.put(&Node::NumberToStr(field("id"))).unwrap(),
lit(": "),
field("title"),
lit("</li>"),
s.put(&Node::Call {
func: "posts_html".into(),
args: vec![r("ps"), next_i()],
})
.unwrap(),
]),
})
.unwrap(),
})
.unwrap();
let show_html = s
.put(&Node::Function {
name: "show_html".into(),
type_params: vec![],
params: vec![
param("ps", Type::List(Box::new(post_ty.clone()))),
param("id", Type::Number),
param("i", Type::Number),
],
produces: str_out(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![len_step()],
result: s
.put(&Node::If {
cond: at_end(),
then_branch: lit("<p>not found</p>"),
else_branch: s
.put(&Node::If {
cond: s
.put(&Node::BinOp {
op: crate::node::BinOp::Eq,
lhs: field("id"),
rhs: r("id"),
})
.unwrap(),
then_branch: cats(&[
lit("<h1>"),
field("title"),
lit("</h1><p>"),
field("body"),
lit("</p>"),
]),
else_branch: s
.put(&Node::Call {
func: "show_html".into(),
args: vec![r("ps"), r("id"), next_i()],
})
.unwrap(),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let resp = |status: i64, body: NodeHash| -> NodeHash {
s.put(&Node::Record {
type_name: "Response".into(),
fields: vec![("status".into(), num(status)), ("body".into(), body)],
})
.unwrap()
};
let id_from_path = s
.put(&Node::StrToNumber(
s.put(&Node::StrSlice {
s: r("path"),
start: r("plen"),
len: s
.put(&Node::BinOp {
op: crate::node::BinOp::Sub,
lhs: s.put(&Node::StrLen(r("path"))).unwrap(),
rhs: r("plen"),
})
.unwrap(),
})
.unwrap(),
))
.unwrap();
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![param("req", Type::Named("Request".into()))],
produces: Produces {
ty: Type::Named("Response".into()),
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
s.put(&Node::Step {
binding: "path".into(),
value: s
.put(&Node::Field {
base: r("req"),
type_name: "Request".into(),
field: "path".into(),
})
.unwrap(),
})
.unwrap(),
s.put(&Node::Step {
binding: "ps".into(),
value: s
.put(&Node::Call {
func: "posts".into(),
args: vec![],
})
.unwrap(),
})
.unwrap(),
s.put(&Node::Step {
binding: "plen".into(),
value: s.put(&Node::StrLen(lit("/posts/"))).unwrap(),
})
.unwrap(),
],
result: s
.put(&Node::If {
cond: s
.put(&Node::StrEq(r("path"), lit("/posts")))
.unwrap(),
then_branch: resp(
200,
cats(&[
lit("<ul>"),
s.put(&Node::Call {
func: "posts_html".into(),
args: vec![r("ps"), num(0)],
})
.unwrap(),
lit("</ul>"),
]),
),
else_branch: s
.put(&Node::If {
cond: s
.put(&Node::StrStartsWith {
s: r("path"),
prefix: lit("/posts/"),
})
.unwrap(),
then_branch: resp(
200,
s.put(&Node::Call {
func: "show_html".into(),
args: vec![
r("ps"),
id_from_path,
num(0),
],
})
.unwrap(),
),
else_branch: resp(404, lit("not found")),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let m = s
.put(&Node::Module {
name: "blog".into(),
types: vec![post_def, request_def, response_def],
functions: vec![route, posts_html, show_html, posts],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"the blog app did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request(&wasm, "route", "GET", "/posts", "").unwrap(),
HttpResponse {
status: 200,
body: "<ul><li>1: Hello World</li><li>2: On Cairn</li></ul>"
.into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/posts/2", "").unwrap(),
HttpResponse {
status: 200,
body: "<h1>On Cairn</h1><p>Programs as reasoning chains.</p>"
.into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/posts/9", "").unwrap(),
HttpResponse {
status: 200,
body: "<p>not found</p>".into(),
}
);
assert_eq!(
serve_request(&wasm, "route", "GET", "/", "").unwrap(),
HttpResponse {
status: 404,
body: "not found".into(),
}
);
}
#[test]
fn runtime_lists_via_cons() {
let s = Store::open_in_memory().unwrap();
let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
let bin = |op, a: NodeHash, b: NodeHash| -> NodeHash {
s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
};
let param = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let num_list = || Type::List(Box::new(Type::Number));
let ext = |ty: Type| Produces {
ty,
confidence: Confidence::External,
};
let mk = s
.put(&Node::Function {
name: "mk".into(),
type_params: vec![],
params: vec![param("n", Type::Number), param("i", Type::Number)],
produces: ext(num_list()),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: s
.put(&Node::If {
cond: bin(crate::node::BinOp::Eq, r("i"), r("n")),
then_branch: s
.put(&Node::ListEmpty {
elem: Type::Number,
})
.unwrap(),
else_branch: s
.put(&Node::ListCons {
head: r("i"),
tail: s
.put(&Node::Call {
func: "mk".into(),
args: vec![
r("n"),
bin(
crate::node::BinOp::Add,
r("i"),
num(1),
),
],
})
.unwrap(),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let sum = s
.put(&Node::Function {
name: "sum".into(),
type_params: vec![],
params: vec![param("xs", num_list()), param("i", Type::Number)],
produces: ext(Type::Number),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![s
.put(&Node::Step {
binding: "len".into(),
value: s.put(&Node::ListLen(r("xs"))).unwrap(),
})
.unwrap()],
result: s
.put(&Node::If {
cond: bin(crate::node::BinOp::Eq, r("i"), r("len")),
then_branch: num(0),
else_branch: bin(
crate::node::BinOp::Add,
s.put(&Node::ListGet {
list: r("xs"),
index: r("i"),
})
.unwrap(),
s.put(&Node::Call {
func: "sum".into(),
args: vec![
r("xs"),
bin(crate::node::BinOp::Add, r("i"), num(1)),
],
})
.unwrap(),
),
})
.unwrap(),
})
.unwrap();
let total = s
.put(&Node::Function {
name: "total".into(),
type_params: vec![],
params: vec![param("n", Type::Number)],
produces: ext(Type::Number),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![s
.put(&Node::Step {
binding: "xs".into(),
value: s
.put(&Node::Call {
func: "mk".into(),
args: vec![r("n"), num(0)],
})
.unwrap(),
})
.unwrap()],
result: s
.put(&Node::Call {
func: "sum".into(),
args: vec![r("xs"), num(0)],
})
.unwrap(),
})
.unwrap();
let rendered = crate::render::render(&s, &mk).unwrap();
assert!(rendered.contains("list_empty<Number>()"), "{rendered}");
assert!(rendered.contains("cons("), "{rendered}");
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![total, mk, sum],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"runtime lists did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(run_i64(&wasm, "total", &[5]).unwrap(), 10); assert_eq!(run_i64(&wasm, "total", &[0]).unwrap(), 0); assert_eq!(run_i64(&wasm, "total", &[1]).unwrap(), 0);
assert_eq!(run_i64(&wasm, "total", &[4]).unwrap(), 6); }
#[test]
fn str_index_of_finds_substring() {
let s = Store::open_in_memory().unwrap();
let strf = |name: &str, needle: &str| -> NodeHash {
let call = s
.put(&Node::StrIndexOf {
haystack: s.put(&Node::Ref("req".into())).unwrap(),
needle: s.put(&Node::Str(needle.into())).unwrap(),
})
.unwrap();
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params: vec![Param {
name: "req".into(),
ty: Type::String,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::String,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: s.put(&Node::NumberToStr(call)).unwrap(),
})
.unwrap()
};
let idx = strf("idx", "::");
let idx0 = strf("idx0", "");
assert!(crate::render::render(&s, &idx)
.unwrap()
.contains("str_index_of("));
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![idx, idx0],
})
.unwrap();
assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
let wasm = lower(&s, &m).unwrap();
assert_eq!(serve_once(&wasm, "idx", "ab::cd").unwrap(), "2");
assert_eq!(serve_once(&wasm, "idx", "::x").unwrap(), "0");
assert_eq!(serve_once(&wasm, "idx", "abc").unwrap(), "-1");
assert_eq!(serve_once(&wasm, "idx", "").unwrap(), "-1");
assert_eq!(serve_once(&wasm, "idx0", "anything").unwrap(), "0");
}
#[test]
fn a_db_backed_blog_index() {
let s = Store::open_in_memory().unwrap();
let lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
let bin = |op, a: NodeHash, b: NodeHash| -> NodeHash {
s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
};
let step = |binding: &str, value: NodeHash| -> NodeHash {
s.put(&Node::Step {
binding: binding.into(),
value,
})
.unwrap()
};
let slice = |str_: NodeHash, start: NodeHash, len: NodeHash| -> NodeHash {
s.put(&Node::StrSlice {
s: str_,
start,
len,
})
.unwrap()
};
let call = |f: &str, args: Vec<NodeHash>| -> NodeHash {
s.put(&Node::Call {
func: f.into(),
args,
})
.unwrap()
};
let cats = |parts: &[NodeHash]| -> NodeHash {
let mut it = parts.iter().cloned();
let first = it.next().unwrap();
it.fold(first, |acc, p| s.put(&Node::StrConcat(acc, p)).unwrap())
};
let param = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let ext = |ty: Type| Produces {
ty,
confidence: Confidence::External,
};
let post_ty = Type::Named("Post".into());
let post_list = || Type::List(Box::new(Type::Named("Post".into())));
let idx = |hay: NodeHash, needle: &str| -> NodeHash {
s.put(&Node::StrIndexOf {
haystack: hay,
needle: lit(needle),
})
.unwrap()
};
let post_def = s
.put(&Node::RecordDef {
name: "Post".into(),
fields: vec![
("id".into(), Type::Number),
("title".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let t1p1 = bin(crate::node::BinOp::Add, r("t1"), num(1));
let t2p1 = bin(crate::node::BinOp::Add, r("t2"), num(1));
let parse_row = s
.put(&Node::Function {
name: "parse_row".into(),
type_params: vec![],
params: vec![param("row", Type::String)],
produces: ext(post_ty.clone()),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
step("rlen", s.put(&Node::StrLen(r("row"))).unwrap()),
step("t1", idx(r("row"), "\t")),
step("ids", slice(r("row"), num(0), r("t1"))),
step(
"after1",
slice(
r("row"),
t1p1.clone(),
bin(crate::node::BinOp::Sub, r("rlen"), t1p1),
),
),
step("alen", s.put(&Node::StrLen(r("after1"))).unwrap()),
step("t2", idx(r("after1"), "\t")),
step("title", slice(r("after1"), num(0), r("t2"))),
step(
"pbody",
slice(
r("after1"),
t2p1.clone(),
bin(crate::node::BinOp::Sub, r("alen"), t2p1),
),
),
],
result: s
.put(&Node::Record {
type_name: "Post".into(),
fields: vec![
(
"id".into(),
s.put(&Node::StrToNumber(r("ids"))).unwrap(),
),
("title".into(), r("title")),
("body".into(), r("pbody")),
],
})
.unwrap(),
})
.unwrap();
let nlp1 = bin(crate::node::BinOp::Add, r("nl"), num(1));
let parse_posts = s
.put(&Node::Function {
name: "parse_posts".into(),
type_params: vec![],
params: vec![param("rows", Type::String)],
produces: ext(post_list()),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
step("rl", s.put(&Node::StrLen(r("rows"))).unwrap()),
step("nl", idx(r("rows"), "\n")),
],
result: s
.put(&Node::If {
cond: bin(crate::node::BinOp::Eq, r("rl"), num(0)),
then_branch: s
.put(&Node::ListEmpty {
elem: post_ty.clone(),
})
.unwrap(),
else_branch: s
.put(&Node::If {
cond: bin(
crate::node::BinOp::Eq,
r("nl"),
num(-1),
),
then_branch: s
.put(&Node::ListCons {
head: call(
"parse_row",
vec![r("rows")],
),
tail: s
.put(&Node::ListEmpty {
elem: post_ty.clone(),
})
.unwrap(),
})
.unwrap(),
else_branch: s
.put(&Node::ListCons {
head: call(
"parse_row",
vec![slice(
r("rows"),
num(0),
r("nl"),
)],
),
tail: call(
"parse_posts",
vec![slice(
r("rows"),
nlp1.clone(),
bin(
crate::node::BinOp::Sub,
r("rl"),
nlp1,
),
)],
),
})
.unwrap(),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let pf = |f: &str| -> NodeHash {
s.put(&Node::Field {
base: s
.put(&Node::ListGet {
list: r("ps"),
index: r("i"),
})
.unwrap(),
type_name: "Post".into(),
field: f.into(),
})
.unwrap()
};
let posts_html = s
.put(&Node::Function {
name: "posts_html".into(),
type_params: vec![],
params: vec![param("ps", post_list()), param("i", Type::Number)],
produces: ext(Type::String),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![step("n", s.put(&Node::ListLen(r("ps"))).unwrap())],
result: s
.put(&Node::If {
cond: bin(crate::node::BinOp::Eq, r("i"), r("n")),
then_branch: lit(""),
else_branch: cats(&[
lit("<li>"),
s.put(&Node::NumberToStr(pf("id"))).unwrap(),
lit(": "),
pf("title"),
lit("</li>"),
call(
"posts_html",
vec![
r("ps"),
bin(crate::node::BinOp::Add, r("i"), num(1)),
],
),
]),
})
.unwrap(),
})
.unwrap();
let mut db = BTreeSet::new();
db.insert(crate::ty::Effect::Db);
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![param("req", Type::Named("Request".into()))],
produces: ext(Type::Named("Response".into())),
requires: db,
on_failure: vec![],
body: {
let none = || {
s.put(&Node::ListEmpty { elem: Type::String }).unwrap()
};
let dbq = |sql: &str| {
s.put(&Node::DbQuery {
sql: lit(sql),
params: none(),
})
.unwrap()
};
vec![
step(
"_c",
dbq(
"CREATE TABLE IF NOT EXISTS posts \
(id INTEGER PRIMARY KEY, title TEXT, body TEXT)",
),
),
step(
"_i1",
dbq(
"INSERT INTO posts (title, body) \
VALUES ('Hello World', 'The first post.')",
),
),
step(
"_i2",
dbq(
"INSERT INTO posts (title, body) VALUES \
('On Cairn', 'Programs as reasoning chains.')",
),
),
step(
"rows",
dbq("SELECT id, title, body FROM posts ORDER BY id"),
),
step("ps", call("parse_posts", vec![r("rows")])),
]
},
result: s
.put(&Node::Record {
type_name: "Response".into(),
fields: vec![
("status".into(), num(200)),
(
"body".into(),
cats(&[
lit("<ul>"),
call("posts_html", vec![r("ps"), num(0)]),
lit("</ul>"),
]),
),
],
})
.unwrap(),
})
.unwrap();
let m = s
.put(&Node::Module {
name: "blog".into(),
types: vec![post_def, request_def, response_def],
functions: vec![route, parse_posts, parse_row, posts_html],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"db-backed blog did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request(&wasm, "route", "GET", "/posts", "").unwrap(),
HttpResponse {
status: 200,
body: "<ul><li>1: Hello World</li><li>2: On Cairn</li></ul>"
.into(),
}
);
}
#[test]
fn a_persistent_sqlite_blog() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let db = std::env::temp_dir()
.join(format!("cairn_sqlite_blog_{}_{nanos}.db", std::process::id()));
let dbs = db.to_str().unwrap().to_string();
let _ = std::fs::remove_file(&db);
let s = Store::open_in_memory().unwrap();
let lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
let bin = |op, a: NodeHash, b: NodeHash| -> NodeHash {
s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
};
let step = |binding: &str, value: NodeHash| -> NodeHash {
s.put(&Node::Step {
binding: binding.into(),
value,
})
.unwrap()
};
let slice = |str_: NodeHash, start: NodeHash, len: NodeHash| -> NodeHash {
s.put(&Node::StrSlice {
s: str_,
start,
len,
})
.unwrap()
};
let call = |f: &str, args: Vec<NodeHash>| -> NodeHash {
s.put(&Node::Call {
func: f.into(),
args,
})
.unwrap()
};
let cats = |parts: &[NodeHash]| -> NodeHash {
let mut it = parts.iter().cloned();
let first = it.next().unwrap();
it.fold(first, |acc, p| s.put(&Node::StrConcat(acc, p)).unwrap())
};
let param = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let ext = |ty: Type| Produces {
ty,
confidence: Confidence::External,
};
let post_ty = Type::Named("Post".into());
let post_list = || Type::List(Box::new(Type::Named("Post".into())));
let idx = |hay: NodeHash, needle: &str| -> NodeHash {
s.put(&Node::StrIndexOf {
haystack: hay,
needle: lit(needle),
})
.unwrap()
};
let dbq = |sql: &str| -> NodeHash {
s.put(&Node::DbQuery {
sql: lit(sql),
params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
})
.unwrap()
};
let post_def = s
.put(&Node::RecordDef {
name: "Post".into(),
fields: vec![
("id".into(), Type::Number),
("title".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let request_def = s
.put(&Node::RecordDef {
name: "Request".into(),
fields: vec![
("method".into(), Type::String),
("path".into(), Type::String),
("body".into(), Type::String),
],
})
.unwrap();
let response_def = s
.put(&Node::RecordDef {
name: "Response".into(),
fields: vec![
("status".into(), Type::Number),
("body".into(), Type::String),
],
})
.unwrap();
let t1p1 = bin(crate::node::BinOp::Add, r("t1"), num(1));
let t2p1 = bin(crate::node::BinOp::Add, r("t2"), num(1));
let parse_row = s
.put(&Node::Function {
name: "parse_row".into(),
type_params: vec![],
params: vec![param("row", Type::String)],
produces: ext(post_ty.clone()),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
step("rlen", s.put(&Node::StrLen(r("row"))).unwrap()),
step("t1", idx(r("row"), "\t")),
step("ids", slice(r("row"), num(0), r("t1"))),
step(
"after1",
slice(
r("row"),
t1p1.clone(),
bin(crate::node::BinOp::Sub, r("rlen"), t1p1),
),
),
step("alen", s.put(&Node::StrLen(r("after1"))).unwrap()),
step("t2", idx(r("after1"), "\t")),
step("title", slice(r("after1"), num(0), r("t2"))),
step(
"pbody",
slice(
r("after1"),
t2p1.clone(),
bin(crate::node::BinOp::Sub, r("alen"), t2p1),
),
),
],
result: s
.put(&Node::Record {
type_name: "Post".into(),
fields: vec![
(
"id".into(),
s.put(&Node::StrToNumber(r("ids"))).unwrap(),
),
("title".into(), r("title")),
("body".into(), r("pbody")),
],
})
.unwrap(),
})
.unwrap();
let nlp1 = bin(crate::node::BinOp::Add, r("nl"), num(1));
let parse_posts = s
.put(&Node::Function {
name: "parse_posts".into(),
type_params: vec![],
params: vec![param("rows", Type::String)],
produces: ext(post_list()),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![
step("rl", s.put(&Node::StrLen(r("rows"))).unwrap()),
step("nl", idx(r("rows"), "\n")),
],
result: s
.put(&Node::If {
cond: bin(crate::node::BinOp::Eq, r("rl"), num(0)),
then_branch: s
.put(&Node::ListEmpty {
elem: post_ty.clone(),
})
.unwrap(),
else_branch: s
.put(&Node::If {
cond: bin(
crate::node::BinOp::Eq,
r("nl"),
num(-1),
),
then_branch: s
.put(&Node::ListCons {
head: call(
"parse_row",
vec![r("rows")],
),
tail: s
.put(&Node::ListEmpty {
elem: post_ty.clone(),
})
.unwrap(),
})
.unwrap(),
else_branch: s
.put(&Node::ListCons {
head: call(
"parse_row",
vec![slice(
r("rows"),
num(0),
r("nl"),
)],
),
tail: call(
"parse_posts",
vec![slice(
r("rows"),
nlp1.clone(),
bin(
crate::node::BinOp::Sub,
r("rl"),
nlp1,
),
)],
),
})
.unwrap(),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let pf = |f: &str| -> NodeHash {
s.put(&Node::Field {
base: s
.put(&Node::ListGet {
list: r("ps"),
index: r("i"),
})
.unwrap(),
type_name: "Post".into(),
field: f.into(),
})
.unwrap()
};
let posts_html = s
.put(&Node::Function {
name: "posts_html".into(),
type_params: vec![],
params: vec![param("ps", post_list()), param("i", Type::Number)],
produces: ext(Type::String),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![step("n", s.put(&Node::ListLen(r("ps"))).unwrap())],
result: s
.put(&Node::If {
cond: bin(crate::node::BinOp::Eq, r("i"), r("n")),
then_branch: lit(""),
else_branch: cats(&[
lit("<li>"),
s.put(&Node::NumberToStr(pf("id"))).unwrap(),
lit(": "),
pf("title"),
lit("</li>"),
call(
"posts_html",
vec![
r("ps"),
bin(crate::node::BinOp::Add, r("i"), num(1)),
],
),
]),
})
.unwrap(),
})
.unwrap();
let resp = |status: i64, body: NodeHash| -> NodeHash {
s.put(&Node::Record {
type_name: "Response".into(),
fields: vec![("status".into(), num(status)), ("body".into(), body)],
})
.unwrap()
};
let mut dbset = BTreeSet::new();
dbset.insert(crate::ty::Effect::Db);
let route = s
.put(&Node::Function {
name: "route".into(),
type_params: vec![],
params: vec![param("req", Type::Named("Request".into()))],
produces: ext(Type::Named("Response".into())),
requires: dbset,
on_failure: vec![],
body: vec![step(
"path",
s.put(&Node::Field {
base: r("req"),
type_name: "Request".into(),
field: "path".into(),
})
.unwrap(),
)],
result: s
.put(&Node::If {
cond: s
.put(&Node::StrEq(r("path"), lit("/seed")))
.unwrap(),
then_branch: resp(
200,
cats(&[
dbq("CREATE TABLE IF NOT EXISTS posts \
(id INTEGER PRIMARY KEY, title TEXT, body TEXT)"),
dbq("INSERT INTO posts (title, body) \
VALUES ('Hello World', 'The first post.')"),
dbq("INSERT INTO posts (title, body) VALUES \
('On Cairn', 'Programs as reasoning chains.')"),
]),
),
else_branch: s
.put(&Node::If {
cond: s
.put(&Node::StrEq(r("path"), lit("/posts")))
.unwrap(),
then_branch: resp(
200,
cats(&[
lit("<ul>"),
call(
"posts_html",
vec![
call(
"parse_posts",
vec![dbq(
"SELECT id, title, body \
FROM posts ORDER BY id",
)],
),
num(0),
],
),
lit("</ul>"),
]),
),
else_branch: resp(404, lit("not found")),
})
.unwrap(),
})
.unwrap(),
})
.unwrap();
let m = s
.put(&Node::Module {
name: "blog".into(),
types: vec![post_def, request_def, response_def],
functions: vec![route, parse_posts, parse_row, posts_html],
})
.unwrap();
let report = crate::check::Checker::new(&s).check(&m).unwrap();
assert!(
report.ok(),
"persistent blog did not type-check: {:?}",
report.violations
);
let wasm = lower(&s, &m).unwrap();
assert_eq!(
serve_request_db(&wasm, "route", &dbs, "POST", "/seed", "").unwrap(),
HttpResponse {
status: 200,
body: "012".into(),
}
);
assert_eq!(
serve_request_db(&wasm, "route", &dbs, "GET", "/posts", "").unwrap(),
HttpResponse {
status: 200,
body: "<ul><li>1: Hello World</li><li>2: On Cairn</li></ul>"
.into(),
}
);
assert!(db.exists(), "the SQLite file should persist on disk");
let _ = std::fs::remove_file(&db);
let _ = std::fs::remove_file(format!("{dbs}-journal"));
let _ = std::fs::remove_file(format!("{dbs}-wal"));
let _ = std::fs::remove_file(format!("{dbs}-shm"));
}
}