use crate::node::{Node, NodeHash};
use crate::store::{Result, Store};
use crate::ty::{Confidence, Effect, Type};
use serde::Serialize;
use std::cell::Cell;
use std::collections::HashMap;
const LB: char = '\u{1}'; const SEP: char = '\u{2}'; const RB: char = '\u{3}';
thread_local! {
static MARK: Cell<bool> = const { Cell::new(false) };
}
struct MarkGuard;
impl MarkGuard {
fn on() -> Self {
MARK.with(|m| m.set(true));
MarkGuard
}
}
impl Drop for MarkGuard {
fn drop(&mut self) {
MARK.with(|m| m.set(false));
}
}
fn wrap(hash: &NodeHash, s: String) -> String {
if MARK.with(Cell::get) {
format!("{LB}{hash}{SEP}{s}{RB}")
} else {
s
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Span {
pub hash: NodeHash,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Addressed {
pub text: String,
pub spans: Vec<Span>,
}
pub fn render_addressed(store: &Store, root: &NodeHash) -> Result<Addressed> {
let marked = {
let _g = MarkGuard::on();
render(store, root)?
};
let mut text = String::with_capacity(marked.len());
let mut stack: Vec<(NodeHash, usize)> = Vec::new();
let mut spans: Vec<Span> = Vec::new();
let mut chars = marked.chars();
while let Some(c) = chars.next() {
match c {
LB => {
let mut h = String::new();
for hc in chars.by_ref() {
if hc == SEP {
break;
}
h.push(hc);
}
stack.push((NodeHash::parse(&h), text.len()));
}
RB => {
if let Some((hash, start)) = stack.pop() {
spans.push(Span {
hash,
start,
end: text.len(),
});
}
}
_ => text.push(c),
}
}
Ok(Addressed { text, spans })
}
pub fn node_at(a: &Addressed, offset: usize) -> Option<NodeHash> {
a.spans
.iter()
.filter(|s| offset >= s.start && offset < s.end)
.min_by_key(|s| s.end - s.start)
.map(|s| s.hash.clone())
}
pub fn render(store: &Store, root: &NodeHash) -> Result<String> {
let params = param_names(store, root)?;
let Some(node) = store.get(root)? else {
return Ok("<missing>".to_string());
};
Ok(match node {
Node::Module {
name,
types,
functions,
} => {
let mut parts = vec![format!("module {name}"), String::new()];
for th in &types {
parts.push(render_typedef(store, th)?);
parts.push(String::new());
}
for fh in &functions {
parts.push(render_function(store, fh, ¶ms)?);
parts.push(String::new());
}
parts.push("end".to_string());
wrap(root, parts.join("\n"))
}
Node::Function { .. } => render_function(store, root, ¶ms)?,
Node::RecordDef { .. } | Node::VariantDef { .. } => render_typedef(store, root)?,
_ => expr(store, root, ¶ms),
})
}
fn render_typedef(store: &Store, hash: &NodeHash) -> Result<String> {
match store.get(hash)? {
Some(Node::RecordDef { name, fields }) => {
let mut out = vec![format!("type {name} = record")];
for (fname, fty) in &fields {
out.push(format!(" {fname}: {}", ty(fty)));
}
out.push("end".to_string());
Ok(wrap(hash, out.join("\n")))
}
Some(Node::VariantDef { name, cases }) => {
let mut out = vec![format!("type {name} = variant")];
for (cname, payload) in &cases {
if payload.is_empty() {
out.push(format!(" {cname}"));
} else {
let fs: Vec<String> = payload
.iter()
.map(|(fn_, ft)| format!("{fn_}: {}", ty(ft)))
.collect();
out.push(format!(" {cname}({})", fs.join(", ")));
}
}
out.push("end".to_string());
Ok(wrap(hash, out.join("\n")))
}
_ => Ok(wrap(hash, "<missing>".to_string())),
}
}
fn param_names(store: &Store, root: &NodeHash) -> Result<HashMap<String, Vec<String>>> {
let mut map = HashMap::new();
let mut add = |n: &Node| {
if let Node::Function { name, params, .. } = n {
map.insert(
name.clone(),
params.iter().map(|p| p.name.clone()).collect(),
);
}
};
match store.get(root)? {
Some(Node::Module { functions, .. }) => {
for fh in &functions {
if let Some(f) = store.get(fh)? {
add(&f);
}
}
}
Some(f @ Node::Function { .. }) => add(&f),
_ => {}
}
Ok(map)
}
fn render_function(
store: &Store,
fh: &NodeHash,
pmap: &HashMap<String, Vec<String>>,
) -> Result<String> {
let Some(Node::Function {
name,
type_params,
params,
produces,
requires,
on_failure,
body,
result,
}) = store.get(fh)?
else {
return Ok(wrap(fh, "<missing>".to_string()));
};
let header = if type_params.is_empty() {
format!("function {name}")
} else {
format!("function {name}<{}>", type_params.join(", "))
};
let mut out: Vec<String> = vec![header];
if !params.is_empty() {
out.push(" given".to_string());
for p in ¶ms {
out.push(format!(
" {}: {}{}",
p.name,
ty(&p.ty),
conf_suffix(p.min_confidence)
));
}
}
out.push(format!(
" produces {}{}",
ty(&produces.ty),
conf_suffix(produces.confidence)
));
if requires.is_empty() {
out.push(" pure".to_string());
} else {
let names: Vec<String> = requires.iter().map(|e| effect(*e).to_string()).collect();
out.push(format!(" requires {}", names.join(", ")));
}
if !on_failure.is_empty() {
out.push(" on_failure".to_string());
for f in &on_failure {
out.push(format!(" {f}"));
}
}
out.push("do".to_string());
for step_hash in &body {
match store.get(step_hash)? {
Some(Node::Step { binding, value }) => match store.get(&value)? {
Some(Node::Handle { body, handlers }) => {
out.push(format!(
" step {} = {}",
binding,
expr(store, &body, pmap)
));
for (v, r) in &handlers {
out.push(format!(" on {} -> {}", v, expr(store, r, pmap)));
}
}
_ => {
out.push(format!(
" step {} = {}",
binding,
expr(store, &value, pmap)
));
}
},
Some(Node::Hole { expects }) => {
out.push(format!(" <hole: {expects}>"));
}
_ => out.push(" <missing>".to_string()),
}
}
out.push(format!(" yield {}", expr(store, &result, pmap)));
out.push("end".to_string());
Ok(wrap(fh, out.join("\n")))
}
fn expr(store: &Store, hash: &NodeHash, pmap: &HashMap<String, Vec<String>>) -> String {
wrap(hash, expr_body(store, hash, pmap))
}
fn expr_body(store: &Store, hash: &NodeHash, pmap: &HashMap<String, Vec<String>>) -> String {
match store.get(hash) {
Ok(Some(Node::Lit(v))) => v.to_string(),
Ok(Some(Node::FloatLit(bits))) => format!("{}f", f64::from_bits(bits)),
Ok(Some(Node::FloatOp { op, lhs, rhs })) => format!(
"{} {} {}",
operand(store, &lhs, pmap),
op.symbol(),
operand(store, &rhs, pmap)
),
Ok(Some(Node::IntToFloat(a))) => {
format!("to_float({})", expr(store, &a, pmap))
}
Ok(Some(Node::FloatToInt(a))) => {
format!("to_int({})", expr(store, &a, pmap))
}
Ok(Some(Node::DecimalLit(v))) => {
format!("{}.{:04}d", v / 10000, (v % 10000).abs())
}
Ok(Some(Node::DecimalOp { op, lhs, rhs })) => format!(
"{} {} {}",
operand(store, &lhs, pmap),
op.symbol(),
operand(store, &rhs, pmap)
),
Ok(Some(Node::IntToDecimal(a))) => {
format!("to_decimal({})", expr(store, &a, pmap))
}
Ok(Some(Node::DecimalToInt(a))) => {
format!("decimal_to_int({})", expr(store, &a, pmap))
}
Ok(Some(Node::DecimalRaw(a))) => {
format!("decimal_raw({})", expr(store, &a, pmap))
}
Ok(Some(Node::Bool(b))) => b.to_string(),
Ok(Some(Node::Not(a))) => format!("!{}", operand(store, &a, pmap)),
Ok(Some(Node::Str(s))) => format!("{s:?}"),
Ok(Some(Node::Now)) => "now()".to_string(),
Ok(Some(Node::List(es))) => {
let parts: Vec<String> =
es.iter().map(|e| expr(store, e, pmap)).collect();
format!("[{}]", parts.join(", "))
}
Ok(Some(Node::ListEmpty { elem })) => {
format!("list_empty<{}>()", ty(&elem))
}
Ok(Some(Node::ListCons { head, tail })) => format!(
"cons({}, {})",
expr(store, &head, pmap),
expr(store, &tail, pmap)
),
Ok(Some(Node::OptionSome(v))) => {
format!("some({})", expr(store, &v, pmap))
}
Ok(Some(Node::OptionNone { elem })) => {
format!("none<{}>()", ty(&elem))
}
Ok(Some(Node::OptionElse { opt, default })) => format!(
"{} else {}",
expr(store, &opt, pmap),
expr(store, &default, pmap)
),
Ok(Some(Node::OptionMatch {
opt,
some_bind,
some_body,
none_body,
})) => format!(
"match {} {{ Some({some_bind}) -> {}, None -> {} }}",
expr(store, &opt, pmap),
expr(store, &some_body, pmap),
expr(store, &none_body, pmap)
),
Ok(Some(Node::ListTryGet { list, index })) => format!(
"list_try_get({}, {})",
expr(store, &list, pmap),
expr(store, &index, pmap)
),
Ok(Some(Node::ListLen(a))) => {
format!("list_len({})", expr(store, &a, pmap))
}
Ok(Some(Node::ListGet { list, index })) => format!(
"list_get({}, {})",
expr(store, &list, pmap),
expr(store, &index, pmap)
),
Ok(Some(Node::Map(pairs))) => {
let parts: Vec<String> = pairs
.iter()
.map(|(k, v)| {
format!("{}: {}", expr(store, k, pmap), expr(store, v, pmap))
})
.collect();
format!("{{{}}}", parts.join(", "))
}
Ok(Some(Node::MapGet { map, key })) => format!(
"map_get({}, {})",
expr(store, &map, pmap),
expr(store, &key, pmap)
),
Ok(Some(Node::MapTryGet { map, key })) => format!(
"map_try_get({}, {})",
expr(store, &map, pmap),
expr(store, &key, pmap)
),
Ok(Some(Node::MapLen(a))) => format!("map_len({})", expr(store, &a, pmap)),
Ok(Some(Node::Log(a))) => format!("log({})", expr(store, &a, pmap)),
Ok(Some(Node::Publish(a))) => {
format!("publish({})", expr(store, &a, pmap))
}
Ok(Some(Node::SetHeader { name, value })) => format!(
"set_header({}, {})",
expr(store, &name, pmap),
expr(store, &value, pmap)
),
Ok(Some(Node::Rand)) => "rand()".to_string(),
Ok(Some(Node::MutNew(v))) => format!("cell({})", expr(store, &v, pmap)),
Ok(Some(Node::MutGet(cl))) => {
format!("cell_get({})", expr(store, &cl, pmap))
}
Ok(Some(Node::MutSet { cell, value })) => format!(
"cell_set({}, {})",
expr(store, &cell, pmap),
expr(store, &value, pmap)
),
Ok(Some(Node::DiskWrite { path, content })) => format!(
"disk_write({}, {})",
expr(store, &path, pmap),
expr(store, &content, pmap)
),
Ok(Some(Node::DiskRead(p))) => {
format!("disk_read({})", expr(store, &p, pmap))
}
Ok(Some(Node::NetGet(u))) => {
format!("net_get({})", expr(store, &u, pmap))
}
Ok(Some(Node::DbQuery { sql, params })) => format!(
"db_query({}, {})",
expr(store, &sql, pmap),
expr(store, ¶ms, pmap)
),
Ok(Some(Node::StrLen(a))) => {
format!("str_len({})", expr(store, &a, pmap))
}
Ok(Some(Node::StrLower(a))) => {
format!("str_lower({})", expr(store, &a, pmap))
}
Ok(Some(Node::StrFromCode(a))) => {
format!("str_from_code({})", expr(store, &a, pmap))
}
Ok(Some(Node::NumberToStr(a))) => {
format!("number_to_str({})", expr(store, &a, pmap))
}
Ok(Some(Node::StrToNumber(a))) => {
format!("str_to_number({})", expr(store, &a, pmap))
}
Ok(Some(Node::StrToNumberOpt(a))) => {
format!("str_to_number_opt({})", expr(store, &a, pmap))
}
Ok(Some(Node::StrConcat(a, b))) => format!(
"str_concat({}, {})",
expr(store, &a, pmap),
expr(store, &b, pmap)
),
Ok(Some(Node::StrSlice { s, start, len })) => format!(
"str_slice({}, {}, {})",
expr(store, &s, pmap),
expr(store, &start, pmap),
expr(store, &len, pmap)
),
Ok(Some(Node::StrEq(a, b))) => format!(
"str_eq({}, {})",
expr(store, &a, pmap),
expr(store, &b, pmap)
),
Ok(Some(Node::StrContains { haystack, needle })) => format!(
"str_contains({}, {})",
expr(store, &haystack, pmap),
expr(store, &needle, pmap)
),
Ok(Some(Node::StrStartsWith { s, prefix })) => format!(
"str_starts_with({}, {})",
expr(store, &s, pmap),
expr(store, &prefix, pmap)
),
Ok(Some(Node::StrIndexOf { haystack, needle })) => format!(
"str_index_of({}, {})",
expr(store, &haystack, pmap),
expr(store, &needle, pmap)
),
Ok(Some(Node::Ref(name))) => name,
Ok(Some(Node::Hole { expects })) => format!("<hole: {expects}>"),
Ok(Some(Node::Step { value, .. })) => expr(store, &value, pmap),
Ok(Some(Node::BinOp { op, lhs, rhs })) => {
format!(
"{} {} {}",
operand(store, &lhs, pmap),
op.symbol(),
operand(store, &rhs, pmap)
)
}
Ok(Some(Node::If {
cond,
then_branch,
else_branch,
})) => format!(
"if {} then {} else {} end",
expr(store, &cond, pmap),
expr(store, &then_branch, pmap),
expr(store, &else_branch, pmap)
),
Ok(Some(Node::Fail(v))) => format!("fail {v}"),
Ok(Some(Node::Handle { body, handlers })) => {
let mut s = expr(store, &body, pmap);
for (v, r) in &handlers {
s.push_str(&format!(" on {v} -> {}", expr(store, r, pmap)));
}
s
}
Ok(Some(Node::Call { func, args })) => {
let rendered: Vec<String> =
args.iter().map(|a| expr(store, a, pmap)).collect();
match pmap.get(&func) {
Some(names) if names.len() == rendered.len() => {
let pairs: Vec<String> = names
.iter()
.zip(&rendered)
.map(|(n, a)| format!("{n}: {a}"))
.collect();
format!("{func}({})", pairs.join(", "))
}
_ => format!("{func}({})", rendered.join(", ")),
}
}
Ok(Some(Node::Lambda { params, body })) => {
let ps: Vec<String> = params.iter().map(|p| p.name.clone()).collect();
format!("|{}| {}", ps.join(", "), expr(store, &body, pmap))
}
Ok(Some(Node::FuncRef(name))) => format!("&{name}"),
Ok(Some(Node::CallValue { callee, args })) => {
let rendered: Vec<String> =
args.iter().map(|a| expr(store, a, pmap)).collect();
format!(
"{}({})",
operand(store, &callee, pmap),
rendered.join(", ")
)
}
Ok(Some(Node::Record { type_name, fields })) => {
let parts: Vec<String> = fields
.iter()
.map(|(n, h)| format!("{n}: {}", expr(store, h, pmap)))
.collect();
format!("{type_name} {{ {} }}", parts.join(", "))
}
Ok(Some(Node::Field { base, field, .. })) => {
format!("{}.{field}", operand(store, &base, pmap))
}
Ok(Some(Node::Variant {
type_name,
case,
fields,
})) => {
if fields.is_empty() {
format!("{type_name}.{case}")
} else {
let parts: Vec<String> = fields
.iter()
.map(|(n, h)| format!("{n}: {}", expr(store, h, pmap)))
.collect();
format!("{type_name}.{case} {{ {} }}", parts.join(", "))
}
}
Ok(Some(Node::Match {
scrutinee, arms, ..
})) => {
let parts: Vec<String> = arms
.iter()
.map(|a| {
let binds = if a.bindings.is_empty() {
String::new()
} else {
format!("({})", a.bindings.join(", "))
};
format!(
"{}{} -> {}",
a.case,
binds,
expr(store, &a.body, pmap)
)
})
.collect();
format!(
"match {} {{ {} }}",
expr(store, &scrutinee, pmap),
parts.join(", ")
)
}
Ok(Some(Node::Function { .. }))
| Ok(Some(Node::Module { .. }))
| Ok(Some(Node::RecordDef { .. }))
| Ok(Some(Node::VariantDef { .. })) => "<nested>".to_string(),
Ok(None) => "<missing>".to_string(),
Err(_) => "<error>".to_string(),
}
}
fn operand(store: &Store, hash: &NodeHash, pmap: &HashMap<String, Vec<String>>) -> String {
let inner = expr(store, hash, pmap);
if matches!(
store.get(hash),
Ok(Some(Node::BinOp { .. }))
| Ok(Some(Node::FloatOp { .. }))
| Ok(Some(Node::DecimalOp { .. }))
| Ok(Some(Node::Not(_)))
| Ok(Some(Node::FuncRef(_)))
| Ok(Some(Node::Lambda { .. }))
| Ok(Some(Node::CallValue { .. }))
) {
format!("({inner})")
} else {
inner
}
}
fn ty(t: &Type) -> String {
match t {
Type::Number => "Number".into(),
Type::Float => "Float".into(),
Type::Decimal => "Decimal".into(),
Type::String => "String".into(),
Type::Bool => "Bool".into(),
Type::Bytes => "Bytes".into(),
Type::List(e) => format!("List<{}>", ty(e)),
Type::Map(k, v) => format!("Map<{}, {}>", ty(k), ty(v)),
Type::Option(e) => format!("Option<{}>", ty(e)),
Type::Result(o, e) => format!("Result<{}, {}>", ty(o), ty(e)),
Type::Named(n) => n.clone(),
Type::Cell(e) => format!("Cell<{}>", ty(e)),
Type::Fn { params, ret, .. } => {
let ps: Vec<String> = params.iter().map(ty).collect();
format!("Fn({}) -> {}", ps.join(", "), ty(ret))
}
Type::Var(v) => v.clone(),
Type::Never => "Never".into(),
}
}
fn conf_suffix(c: Confidence) -> &'static str {
match c {
Confidence::Structural => "",
Confidence::External => " @ external",
Confidence::Validated => " @ validated",
Confidence::Persisted => " @ persisted",
}
}
fn effect(e: Effect) -> &'static str {
match e {
Effect::Net => "Net",
Effect::Disk => "Disk",
Effect::Db => "Db",
Effect::Mut => "Mut",
Effect::Time => "Time",
Effect::Rand => "Rand",
Effect::Log => "Log",
Effect::Live => "Live",
Effect::Resp => "Resp",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::{Param, Produces};
use std::collections::BTreeSet;
#[allow(clippy::too_many_arguments)]
fn f(
s: &Store,
name: &str,
params: Vec<Param>,
prod: Produces,
requires: BTreeSet<Effect>,
on_failure: Vec<&str>,
body: Vec<NodeHash>,
result: NodeHash,
) -> NodeHash {
s.put(&Node::Function {
name: name.into(),
type_params: vec![],
params,
produces: prod,
requires,
on_failure: on_failure.into_iter().map(String::from).collect(),
body,
result,
})
.unwrap()
}
fn p(name: &str, ty: Type, c: Confidence) -> Param {
Param {
name: name.into(),
ty,
min_confidence: c,
}
}
#[test]
fn pure_constant_function() {
let s = Store::open_in_memory().unwrap();
let lit = s.put(&Node::Lit(42)).unwrap();
let answer = f(
&s,
"answer",
vec![],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
BTreeSet::new(),
vec![],
vec![],
lit,
);
let expected = "\
function answer
produces Number
pure
do
yield 42
end";
assert_eq!(render(&s, &answer).unwrap(), expected);
}
#[test]
fn param_with_non_baseline_confidence_renders_at_suffix() {
let s = Store::open_in_memory().unwrap();
let nref = s.put(&Node::Ref("n".into())).unwrap();
let id = f(
&s,
"id",
vec![p("n", Type::Number, Confidence::External)],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
BTreeSet::new(),
vec![],
vec![],
nref,
);
let expected = "\
function id
given
n: Number @ external
produces Number
pure
do
yield n
end";
assert_eq!(render(&s, &id).unwrap(), expected);
}
#[test]
fn effects_and_failures_render_in_the_header() {
let s = Store::open_in_memory().unwrap();
let zero = s.put(&Node::Lit(0)).unwrap();
let mut req = BTreeSet::new();
req.insert(Effect::Db);
req.insert(Effect::Net);
let risky = f(
&s,
"risky",
vec![],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
req,
vec!["Boom"],
vec![],
zero,
);
let expected = "\
function risky
produces Number
requires Net, Db
on_failure
Boom
do
yield 0
end";
assert_eq!(render(&s, &risky).unwrap(), expected);
}
#[test]
fn calls_render_with_named_arguments_from_the_module() {
let s = Store::open_in_memory().unwrap();
let nref = s.put(&Node::Ref("n".into())).unwrap();
let id = f(
&s,
"id",
vec![p("n", Type::Number, Confidence::Structural)],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
BTreeSet::new(),
vec![],
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 = f(
&s,
"use_id",
vec![],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
BTreeSet::new(),
vec![],
vec![step],
xref,
);
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![],
functions: vec![id, use_id],
})
.unwrap();
let out = render(&s, &m).unwrap();
assert!(out.contains("module m"));
assert!(out.contains("step x = id(n: 7)"), "got:\n{out}");
assert!(out.trim_end().ends_with("end"));
}
#[test]
fn a_hole_renders_as_a_hole_marker() {
let s = Store::open_in_memory().unwrap();
let hole = s
.put(&Node::Hole {
expects: "Number".into(),
})
.unwrap();
let g = f(
&s,
"g",
vec![],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
BTreeSet::new(),
vec![],
vec![],
hole,
);
assert!(render(&s, &g).unwrap().contains("yield <hole: Number>"));
}
#[test]
fn a_record_def_and_field_access_render() {
let s = Store::open_in_memory().unwrap();
let pd = s
.put(&Node::RecordDef {
name: "Point".into(),
fields: vec![("x".into(), Type::Number)],
})
.unwrap();
let pref = s.put(&Node::Ref("pt".into())).unwrap();
let fx = s
.put(&Node::Field {
base: pref,
type_name: "Point".into(),
field: "x".into(),
})
.unwrap();
let get = f(
&s,
"get",
vec![p("pt", Type::Named("Point".into()), Confidence::Structural)],
Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
BTreeSet::new(),
vec![],
vec![],
fx,
);
let m = s
.put(&Node::Module {
name: "m".into(),
types: vec![pd],
functions: vec![get],
})
.unwrap();
let out = render(&s, &m).unwrap();
assert!(out.contains("type Point = record"), "got:\n{out}");
assert!(out.contains(" x: Number"));
assert!(out.contains("yield pt.x"));
}
#[test]
fn a_generic_function_header_renders() {
let s = Store::open_in_memory().unwrap();
let x = s.put(&Node::Ref("x".into())).unwrap();
let id = 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 out = render(&s, &id).unwrap();
assert!(out.contains("function identity<T>"), "got:\n{out}");
assert!(out.contains("x: T"));
}
}
#[cfg(test)]
mod v04_review {
use super::*;
use crate::node::{BinOp, Node, Param, Produces};
use crate::store::Store;
use crate::ty::{Confidence, Type};
#[test]
fn v04_callee_projection_is_unambiguous() {
let s = Store::open_in_memory().unwrap();
let p = |v: i64| s.put(&Node::Lit(v)).unwrap();
let r = |n: &str| s.put(&Node::Ref(n.into())).unwrap();
let par = |n: &str, t: Type| Param { name: n.into(), ty: t, min_confidence: Confidence::External };
let dbl = s.put(&Node::Lambda {
params: vec![par("x", Type::Number)],
body: s.put(&Node::BinOp { op: BinOp::Mul, lhs: r("x"), rhs: p(2) }).unwrap(),
}).unwrap();
let g = s.put(&Node::FuncRef("helper".into())).unwrap();
let applied = s.put(&Node::CallValue { callee: g.clone(), args: vec![r("n")] }).unwrap();
let inline = s.put(&Node::CallValue { callee: dbl.clone(), args: vec![r("n")] }).unwrap();
let opt = s.put(&Node::OptionSome(r("n"))).unwrap();
let picked = s.put(&Node::OptionMatch {
opt,
some_bind: "v".into(),
some_body: s.put(&Node::BinOp { op: BinOp::Add, lhs: r("v"), rhs: p(1) }).unwrap(),
none_body: p(0),
}).unwrap();
let money = s.put(&Node::DecimalOp {
op: BinOp::Add,
lhs: s.put(&Node::DecimalLit(199900)).unwrap(),
rhs: s.put(&Node::DecimalLit(100)).unwrap(),
}).unwrap();
let ratio = s.put(&Node::FloatOp {
op: BinOp::Mul,
lhs: s.put(&Node::FloatLit(1.5f64.to_bits())).unwrap(),
rhs: s.put(&Node::IntToFloat(r("n"))).unwrap(),
}).unwrap();
let logic = s.put(&Node::BinOp {
op: BinOp::Or,
lhs: s.put(&Node::BinOp {
op: BinOp::And,
lhs: s.put(&Node::BinOp { op: BinOp::Eq, lhs: r("n"), rhs: p(0) }).unwrap(),
rhs: s.put(&Node::BinOp { op: BinOp::Lt, lhs: r("n"), rhs: p(0) }).unwrap(),
}).unwrap(),
rhs: s.put(&Node::BinOp { op: BinOp::Ge, lhs: r("n"), rhs: p(1) }).unwrap(),
}).unwrap();
let notlogic = s.put(&Node::Not(logic)).unwrap();
let body = vec![
s.put(&Node::Step { binding: "applied".into(), value: applied }).unwrap(),
s.put(&Node::Step { binding: "inline".into(), value: inline }).unwrap(),
s.put(&Node::Step { binding: "picked".into(), value: picked }).unwrap(),
s.put(&Node::Step { binding: "money".into(), value: money }).unwrap(),
s.put(&Node::Step { binding: "ratio".into(), value: ratio }).unwrap(),
];
let yield_ = s.put(&Node::BinOp {
op: BinOp::Add,
lhs: r("applied"),
rhs: s.put(&Node::BinOp {
op: BinOp::Add,
lhs: r("picked"),
rhs: s.put(&Node::DecimalToInt(r("money"))).unwrap(),
}).unwrap(),
}).unwrap();
let f = s.put(&Node::Function {
name: "demo".into(),
type_params: vec![],
params: vec![par("n", Type::Number)],
produces: Produces { ty: Type::Number, confidence: Confidence::External },
requires: Default::default(),
on_failure: vec![],
body,
result: s.put(&Node::BinOp { op: BinOp::Add, lhs: yield_, rhs: notlogic }).unwrap(),
}).unwrap();
let out = render(&s, &f).unwrap();
assert!(
out.contains("step applied = (&helper)(n)"),
"FuncRef callee must render as `(&helper)(n)`, not `&helper(n)`:\n{out}"
);
assert!(
out.contains("step inline = (|x| x * 2)(n)"),
"Lambda callee must render parenthesized so the body is \
delimited, not `|x| x * 2(n)`:\n{out}"
);
assert!(!out.contains("&helper(n)") || out.contains("(&helper)(n)"));
assert!(!out.contains("step inline = |x|"));
assert!(out.contains("match some(n) { Some(v) -> v + 1, None -> 0 }"));
assert!(out.contains("19.9900d + 0.0100d") && out.contains("1.5f"));
}
#[test]
fn render_addressed_is_byte_identical_and_addressable() {
let s = Store::open_in_memory().unwrap();
let two = s.put(&Node::Lit(2)).unwrap();
let three = s.put(&Node::Lit(3)).unwrap();
let add = s
.put(&Node::BinOp {
op: BinOp::Add,
lhs: two.clone(),
rhs: three.clone(),
})
.unwrap();
let g = s
.put(&Node::Function {
name: "g".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: std::collections::BTreeSet::new(),
on_failure: vec![],
body: vec![],
result: add.clone(),
})
.unwrap();
let canon = render(&s, &g).unwrap();
let a = render_addressed(&s, &g).unwrap();
assert_eq!(a.text, canon);
assert!(!a.text.contains(['\u{1}', '\u{2}', '\u{3}']));
let root = a.spans.iter().find(|x| x.hash == g).unwrap();
assert_eq!((root.start, root.end), (0, a.text.len()));
for sp in &a.spans {
assert!(sp.start <= sp.end && sp.end <= a.text.len());
assert!(root.start <= sp.start && sp.end <= root.end);
}
let i2 = a.text.find("2 + 3").unwrap();
let ip = a.text.find('+').unwrap();
let i3 = a.text.find("3\n").unwrap(); assert_eq!(node_at(&a, i2), Some(two.clone()));
assert_eq!(node_at(&a, i3), Some(three.clone()));
assert_eq!(node_at(&a, ip), Some(add.clone()));
assert_eq!(node_at(&a, 0), Some(g.clone()));
assert_eq!(node_at(&a, a.text.len()), None);
let sp = |h: &NodeHash| a.spans.iter().find(|x| &x.hash == h).unwrap();
let (s2, sa) = (sp(&two), sp(&add));
assert!(sa.start <= s2.start && s2.end <= sa.end);
assert!(sa.end - sa.start > s2.end - s2.start);
assert!(root.start <= sa.start && sa.end <= root.end);
}
}