use crate::ast::{Ast, Pattern};
use crate::eval::{available_fields, eval_error, match_pattern, EvalError, Interp};
use crate::quoted;
use crate::value::{BaseEnv, Env, Value};
use rustyfi_syntax::RustyfiVersion;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
#[derive(Clone)]
pub(crate) struct CompiledExpr(Rc<dyn Fn(&Env, &mut Interp<'_>) -> Result<Value, EvalError>>);
impl CompiledExpr {
fn new(
f: impl Fn(&Env, &mut Interp<'_>) -> Result<Value, EvalError> + 'static,
) -> CompiledExpr {
CompiledExpr(Rc::new(f))
}
pub(crate) fn run(&self, env: &Env, interp: &mut Interp<'_>) -> Result<Value, EvalError> {
(self.0)(env, interp)
}
}
impl std::fmt::Debug for CompiledExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<compiled>")
}
}
#[derive(Clone, Default)]
struct Globals(Rc<RefCell<Vec<Value>>>);
impl Globals {
#[inline]
fn get(&self, slot: usize) -> Value {
self.0.borrow()[slot].clone()
}
#[inline]
fn set(&self, slot: usize, v: Value) {
self.0.borrow_mut()[slot] = v;
}
fn finish(&self, len: usize) {
self.0.borrow_mut().resize(len, Value::Unit);
}
}
enum Scope {
Frame(Vec<String>),
Global(String, usize),
}
enum Binding {
Local(u16, u16),
Global(usize),
}
struct Compiler<'b> {
scopes: Vec<Scope>,
n_globals: usize,
globals: Globals,
globals_v01: Option<&'b BaseEnv>,
globals_v006: Option<&'b BaseEnv>,
current_version: RustyfiVersion,
}
impl<'b> Compiler<'b> {
fn new(globals: Option<&'b BaseEnv>) -> Compiler<'b> {
Compiler {
scopes: Vec::new(),
n_globals: 0,
globals: Globals::default(),
globals_v01: globals,
globals_v006: None,
current_version: RustyfiVersion::V0_1,
}
}
fn new_xver(env_v01: &'b BaseEnv, env_v006: &'b BaseEnv) -> Compiler<'b> {
Compiler {
scopes: Vec::new(),
n_globals: 0,
globals: Globals::default(),
globals_v01: Some(env_v01),
globals_v006: Some(env_v006),
current_version: RustyfiVersion::V0_1,
}
}
fn globals_for(&self, version: RustyfiVersion) -> Option<&'b BaseEnv> {
match version {
RustyfiVersion::V0_1 => self.globals_v01,
RustyfiVersion::V0_0 => self.globals_v006,
_ => None,
}
}
fn resolve(&self, name: &str) -> Option<Binding> {
let mut depth = 0u16;
for entry in self.scopes.iter().rev() {
match entry {
Scope::Frame(names) => {
if let Some(index) = names.iter().rposition(|n| n == name) {
return Some(Binding::Local(depth, index as u16));
}
depth += 1;
}
Scope::Global(n, slot) => {
if n == name {
return Some(Binding::Global(*slot));
}
}
}
}
None
}
fn is_bound(&self, name: &str) -> bool {
self.resolve(name).is_some()
}
fn alloc_global(&mut self, name: &str) -> usize {
let slot = self.n_globals;
self.n_globals += 1;
self.scopes.push(Scope::Global(name.to_string(), slot));
slot
}
fn in_frame<R>(
&mut self,
names: impl IntoIterator<Item = String>,
body: impl FnOnce(&mut Compiler<'b>) -> R,
) -> R {
let mark = self.scopes.len();
self.scopes.push(Scope::Frame(names.into_iter().collect()));
let r = body(self);
self.scopes.truncate(mark);
r
}
fn try_saturated_prim(&mut self, ast: &Ast) -> Option<CompiledExpr> {
let (head, args) = unfold_spine(ast);
let Ast::Var(name, _) = head else {
return None;
};
if self.is_bound(name) {
return None;
}
let Value::Prim { def, applied } = self
.globals_for(self.current_version)
.and_then(|g| g.lookup(name))?
else {
return None;
};
if !applied.is_empty() || def.arity != args.len() {
return None;
}
let run = def.run;
let cargs: Vec<CompiledExpr> = args.iter().map(|a| self.compile(a)).collect();
Some(CompiledExpr::new(move |env, interp| {
let mut vals = Vec::with_capacity(cargs.len());
for c in &cargs {
vals.push(c.run(env, interp)?);
}
run(interp, vals)
}))
}
fn compile(&mut self, ast: &Ast) -> CompiledExpr {
match ast {
Ast::Unit => CompiledExpr::new(|_, _| Ok(Value::Unit)),
Ast::Bool(b) => {
let b = *b;
CompiledExpr::new(move |_, _| Ok(Value::Bool(b)))
}
Ast::Int(n) => {
let n = *n;
CompiledExpr::new(move |_, _| Ok(Value::Int(n)))
}
Ast::Float(x) => {
let x = *x;
CompiledExpr::new(move |_, _| Ok(Value::Float(x)))
}
Ast::Length(l) => {
let l = *l;
CompiledExpr::new(move |_, _| Ok(Value::Length(l)))
}
Ast::Str(s) => {
let s = s.clone();
CompiledExpr::new(move |_, _| Ok(Value::Str(s.clone())))
}
Ast::Var(name, span) => self.compile_var_read(name, *span, "variable"),
Ast::Apply(f, arg) => {
if let Some(special) = self.try_saturated_prim(ast) {
special
} else {
let cf = self.compile(f);
let ca = self.compile(arg);
CompiledExpr::new(move |env, interp| {
let func = cf.run(env, interp)?;
let arg = ca.run(env, interp)?;
interp.apply(func, arg)
})
}
}
Ast::Lambda(param, body) => {
let cbody = self.in_frame([param.clone()], |c| c.compile(body));
CompiledExpr::new(move |env, _| {
Ok(Value::CompiledClosure {
opt_labels: Vec::new(),
body: cbody.clone(),
env: env.clone(),
})
})
}
Ast::LambdaOpt { opts, param, body } => {
let binders: Vec<String> = opts
.iter()
.map(|(_, b)| b.clone())
.chain(std::iter::once(param.clone()))
.collect();
let cbody = self.in_frame(binders, |c| c.compile(body));
let opt_labels: Vec<String> = opts.iter().map(|(l, _)| l.clone()).collect();
CompiledExpr::new(move |env, _| {
Ok(Value::CompiledClosure {
opt_labels: opt_labels.clone(),
body: cbody.clone(),
env: env.clone(),
})
})
}
Ast::ApplyOpt { func, opts, arg } => {
let cf = self.compile(func);
let copts: Vec<(String, CompiledExpr)> = opts
.iter()
.map(|(l, e)| (l.clone(), self.compile(e)))
.collect();
let ca = self.compile(arg);
CompiledExpr::new(move |env, interp| {
let func = cf.run(env, interp)?;
let mut opt_vals = Vec::with_capacity(copts.len());
for (l, ce) in &copts {
opt_vals.push((l.clone(), ce.run(env, interp)?));
}
let arg = ca.run(env, interp)?;
interp.apply_with_opts(func, opt_vals, arg)
})
}
Ast::LetIn(name, value, rest) => {
let cvalue = self.compile(value);
let crest = self.in_frame([name.clone()], |c| c.compile(rest));
CompiledExpr::new(move |env, interp| {
let v = cvalue.run(env, interp)?;
crest.run(&env.child(vec![v]), interp)
})
}
Ast::LetMathIn(name, value, rest) => {
let cvalue = self.compile(value);
let crest = self.in_frame([name.clone()], |c| c.compile(rest));
CompiledExpr::new(move |env, interp| {
let v = cvalue.run(env, interp)?;
crest.run(&env.child(vec![v]), interp)
})
}
Ast::LetRecIn(bindings, body) => {
let names: Vec<String> = bindings.iter().map(|(n, _)| n.clone()).collect();
let (cbindings, cbody) = self.in_frame(names.clone(), |c| {
let cbindings: Vec<(std::rc::Rc<str>, CompiledExpr)> = bindings
.iter()
.map(|(n, value_ast)| (n.as_str().into(), c.compile(value_ast)))
.collect();
let cbody = c.compile(body);
(cbindings, cbody)
});
let_rec_frame(cbindings, cbody)
}
Ast::IfThenElse(cond, then_e, else_e) => {
let ccond = self.compile(cond);
let cthen = self.compile(then_e);
let celse = self.compile(else_e);
CompiledExpr::new(move |env, interp| match ccond.run(env, interp)? {
Value::Bool(true) => cthen.run(env, interp),
Value::Bool(false) => celse.run(env, interp),
other => eval_error(format!(
"if-then-else condition must be bool, got {}",
other.type_name()
)),
})
}
Ast::Record(fields) => {
let cfields: Vec<(String, CompiledExpr)> = fields
.iter()
.map(|(name, e)| (name.clone(), self.compile(e)))
.collect();
CompiledExpr::new(move |env, interp| {
let mut map = BTreeMap::new();
for (name, ce) in &cfields {
map.insert(name.clone(), ce.run(env, interp)?);
}
Ok(Value::Record(map))
})
}
Ast::List(items) => {
let citems: Vec<CompiledExpr> = items.iter().map(|e| self.compile(e)).collect();
CompiledExpr::new(move |env, interp| {
let mut out = Vec::with_capacity(citems.len());
for ce in &citems {
out.push(ce.run(env, interp)?);
}
Ok(Value::List(out))
})
}
Ast::Tuple(items) => {
let citems: Vec<CompiledExpr> = items.iter().map(|e| self.compile(e)).collect();
CompiledExpr::new(move |env, interp| {
let mut out = Vec::with_capacity(citems.len());
for ce in &citems {
out.push(ce.run(env, interp)?);
}
Ok(Value::Tuple(out))
})
}
Ast::Ctor(name, arg) => {
let name = name.clone();
let carg = arg.as_ref().map(|a| self.compile(a));
CompiledExpr::new(move |env, interp| {
let payload = match &carg {
Some(ce) => Some(Box::new(ce.run(env, interp)?)),
None => None,
};
Ok(Value::Ctor(name.clone(), payload))
})
}
Ast::InlineText(elems) => {
let elems = Rc::new(elems.iter().map(|e| self.compile_itext(e)).collect());
CompiledExpr::new(move |env, _| {
Ok(Value::InlineText {
elems: Rc::clone(&elems),
env: env.clone(),
})
})
}
Ast::BlockText(elems) => {
let elems = Rc::new(elems.iter().map(|e| self.compile_btext(e)).collect());
CompiledExpr::new(move |env, _| {
Ok(Value::BlockText {
elems: Rc::clone(&elems),
env: env.clone(),
})
})
}
Ast::MathText(elems) => {
let elems = Rc::new(elems.iter().map(|e| self.compile_melem(e)).collect());
CompiledExpr::new(move |env, _| {
Ok(Value::MathText {
elems: Rc::clone(&elems),
env: env.clone(),
})
})
}
Ast::LetMutableIn(name, init, body) => {
let cinit = self.compile(init);
let cbody = self.in_frame([name.clone()], |c| c.compile(body));
CompiledExpr::new(move |env, interp| {
let v = cinit.run(env, interp)?;
let cell = Value::Ref(Rc::new(RefCell::new(v)));
cbody.run(&env.child(vec![cell]), interp)
})
}
Ast::Overwrite(name, span, value) => {
let cell_of = self.compile_var_read(name, *span, "mutable variable");
let name = name.clone();
let span = *span;
let cvalue = self.compile(value);
CompiledExpr::new(move |env, interp| {
let cell = cell_of.run(env, interp)?;
match cell {
Value::Ref(cell) => {
let v = cvalue.run(env, interp)?;
*cell.borrow_mut() = v;
Ok(Value::Unit)
}
other => Err(EvalError {
span: Some(span),
msg: format!(
"cannot overwrite an immutable variable '{name}' (got a value of type {})",
other.type_name()
),
}),
}
})
}
Ast::WhileDo(cond, body) => {
let ccond = self.compile(cond);
let cbody = self.compile(body);
CompiledExpr::new(move |env, interp| loop {
match ccond.run(env, interp)? {
Value::Bool(true) => {
cbody.run(env, interp)?;
}
Value::Bool(false) => break Ok(Value::Unit),
other => {
return eval_error(format!(
"while-do condition must be bool, got {}",
other.type_name()
))
}
}
})
}
Ast::Sequential(e1, e2) => {
let ce1 = self.compile(e1);
let ce2 = self.compile(e2);
CompiledExpr::new(move |env, interp| {
ce1.run(env, interp)?;
ce2.run(env, interp)
})
}
Ast::AccessField(e, label, span) => {
let ce = self.compile(e);
let label = label.clone();
let span = *span;
CompiledExpr::new(move |env, interp| {
let v = ce.run(env, interp)?;
match v {
Value::Record(map) => map.get(&label).cloned().ok_or_else(|| EvalError {
span: Some(span),
msg: format!(
"record has no field '{label}' (available fields: {})",
available_fields(&map)
),
}),
other => Err(EvalError {
span: Some(span),
msg: format!(
"cannot access field '{label}' of a non-record value (got {})",
other.type_name()
),
}),
}
})
}
Ast::UpdateField(e, label, value) => {
let ce = self.compile(e);
let label = label.clone();
let cvalue = self.compile(value);
CompiledExpr::new(move |env, interp| {
let v = ce.run(env, interp)?;
let new_v = cvalue.run(env, interp)?;
match v {
Value::Record(mut map) => {
if !map.contains_key(&label) {
return eval_error(format!(
"cannot update field '{label}': record has no such field \
(available fields: {})",
available_fields(&map)
));
}
map.insert(label.clone(), new_v);
Ok(Value::Record(map))
}
other => eval_error(format!(
"cannot update field '{label}' of a non-record value (got {})",
other.type_name()
)),
}
})
}
Ast::Match(scrutinee, arms) => {
let cscrut = self.compile(scrutinee);
let carms: Vec<CompiledArm> = arms
.iter()
.map(|arm| {
let mut vars = Vec::new();
pattern_vars(&arm.pat, &mut vars);
self.in_frame(vars, |c| CompiledArm {
pat: arm.pat.clone(),
guard: arm.guard.as_ref().map(|g| c.compile(g)),
body: c.compile(&arm.body),
})
})
.collect();
CompiledExpr::new(move |env, interp| {
let v = cscrut.run(env, interp)?;
for arm in &carms {
let mut bindings = Vec::new();
if !match_pattern(&arm.pat, &v, &mut bindings) {
continue;
}
let inner = env.child(bindings);
if let Some(guard) = &arm.guard {
match guard.run(&inner, interp)? {
Value::Bool(true) => {}
Value::Bool(false) => continue,
other => {
return eval_error(format!(
"match guard must be bool, got {}",
other.type_name()
))
}
}
}
return arm.body.run(&inner, interp);
}
eval_error(format!(
"non-exhaustive match: no arm matched a value of type {}",
v.type_name()
))
})
}
Ast::VersionScope(v, body) => {
let prev = std::mem::replace(&mut self.current_version, *v);
let c = self.compile(body);
self.current_version = prev;
c
}
Ast::ModuleScope(_, body) => self.compile(body),
Ast::StageScope(_, body) => self.compile(body),
Ast::Next(inner) => {
let body = self.compile(inner);
CompiledExpr::new(move |env, _| {
Ok(Value::Code {
body: body.clone(),
env: env.clone(),
})
})
}
Ast::Prev(inner) => {
let inner = self.compile(inner);
CompiledExpr::new(move |env, interp| {
match inner.run(env, interp)? {
Value::Code { body, env: quoted_env } => body.run("ed_env, interp),
other => eval_error(format!(
"`~` expects a code value (from `&`), got {}",
other.type_name()
)),
}
})
}
}
}
fn compile_var_read(
&mut self,
name: &str,
span: rustyfi_syntax::Span,
what: &'static str,
) -> CompiledExpr {
match self.resolve(name) {
Some(Binding::Local(depth, index)) => {
return CompiledExpr::new(move |env: &Env, _| Ok(env.slot(depth, index)))
}
Some(Binding::Global(slot)) => {
let globals = self.globals.clone();
return CompiledExpr::new(move |_, _| Ok(globals.get(slot)));
}
None => {}
}
if let Some(v) = self
.globals_for(self.current_version)
.and_then(|g| g.lookup(name))
{
return CompiledExpr::new(move |_, _| Ok(v.clone()));
}
let name = name.to_string();
CompiledExpr::new(move |_, _| {
Err(EvalError {
span: Some(span),
msg: format!("unbound {what} '{name}' at run time"),
})
})
}
fn compile_cmd_name(
&mut self,
name: &str,
span: rustyfi_syntax::Span,
kind: &'static str,
) -> CompiledExpr {
self.compile_var_read(name, span, kind)
}
fn compile_cmd_arg(&mut self, a: &crate::ast::CmdArg) -> quoted::CmdArg {
quoted::CmdArg {
opts: a
.opts
.iter()
.map(|(l, e)| (l.clone(), self.compile(e)))
.collect(),
arg: self.compile(&a.arg),
}
}
fn compile_itext(&mut self, e: &crate::ast::IText) -> quoted::IText {
use crate::ast::IText as A;
match e {
A::Text(s) => quoted::IText::Text(s.clone()),
A::CodeText(s) => quoted::IText::CodeText(s.clone()),
A::Cmd { name, span, args } => quoted::IText::Cmd {
cmd: self.compile_cmd_name(name, *span, "inline command"),
args: args.iter().map(|a| self.compile_cmd_arg(a)).collect(),
},
A::Embed { expr, span } => quoted::IText::Embed {
expr: self.compile(expr),
span: *span,
},
A::EmbedMath { elems, span } => quoted::IText::EmbedMath {
elems: Rc::new(elems.iter().map(|m| self.compile_melem(m)).collect()),
span: *span,
},
}
}
fn compile_btext(&mut self, e: &crate::ast::BText) -> quoted::BText {
use crate::ast::BText as A;
match e {
A::Cmd { name, span, args } => quoted::BText::Cmd {
cmd: self.compile_cmd_name(name, *span, "block command"),
args: args.iter().map(|a| self.compile_cmd_arg(a)).collect(),
},
A::Embed { expr, span } => quoted::BText::Embed {
expr: self.compile(expr),
span: *span,
},
}
}
fn compile_melem(&mut self, e: &crate::ast::MathElem) -> quoted::MathElem {
use crate::ast::MathElem as A;
match e {
A::Chars(s) => quoted::MathElem::Chars(s.clone()),
A::Group(es) => {
quoted::MathElem::Group(es.iter().map(|x| self.compile_melem(x)).collect())
}
A::Sub(b, s) => quoted::MathElem::Sub(
Box::new(self.compile_melem(b)),
s.iter().map(|x| self.compile_melem(x)).collect(),
),
A::Sup(b, s) => quoted::MathElem::Sup(
Box::new(self.compile_melem(b)),
s.iter().map(|x| self.compile_melem(x)).collect(),
),
A::Primes(b, n) => quoted::MathElem::Primes(Box::new(self.compile_melem(b)), *n),
A::Cmd { name, span, args } => quoted::MathElem::Cmd {
cmd: self.compile_cmd_name(name, *span, "math command"),
name: name.as_str().into(),
span: *span,
args: args.iter().map(|a| self.compile_cmd_arg(a)).collect(),
},
A::Embed { expr, span } => quoted::MathElem::Embed {
expr: self.compile(expr),
span: *span,
},
}
}
fn compile_spine(&mut self, ast: &Ast) -> CompiledExpr {
match ast {
Ast::LetIn(name, value, rest) => self.spine_let(name, value, rest, false),
Ast::LetMathIn(name, value, rest) => self.spine_let(name, value, rest, false),
Ast::LetMutableIn(name, init, body) => self.spine_let(name, init, body, true),
Ast::LetRecIn(bindings, body) => self.spine_let_rec(bindings, body),
other => self.compile(other),
}
}
fn spine_let(&mut self, name: &str, value: &Ast, rest: &Ast, mutable: bool) -> CompiledExpr {
let cvalue = self.compile(value);
let slot = self.alloc_global(name);
let crest = self.compile_spine(rest);
let globals = self.globals.clone();
CompiledExpr::new(move |env, interp| {
let v = cvalue.run(env, interp)?;
globals.set(
slot,
if mutable {
Value::Ref(Rc::new(RefCell::new(v)))
} else {
v
},
);
crest.run(env, interp)
})
}
fn spine_let_rec(&mut self, bindings: &[(String, Rc<Ast>)], body: &Ast) -> CompiledExpr {
let all_lambda = bindings
.iter()
.all(|(_, v)| matches!(**v, Ast::Lambda(..) | Ast::LambdaOpt { .. }));
if !all_lambda {
let names: Vec<String> = bindings.iter().map(|(n, _)| n.clone()).collect();
let (cbindings, cbody) = self.in_frame(names, |c| {
let cbindings: Vec<(Rc<str>, CompiledExpr)> = bindings
.iter()
.map(|(n, value_ast)| (n.as_str().into(), c.compile(value_ast)))
.collect();
let cbody = c.compile_spine(body);
(cbindings, cbody)
});
return let_rec_frame(cbindings, cbody);
}
let slots: Vec<usize> = bindings.iter().map(|(n, _)| self.alloc_global(n)).collect();
let cbindings: Vec<(Rc<str>, CompiledExpr)> = bindings
.iter()
.map(|(n, value_ast)| (n.as_str().into(), self.compile(value_ast)))
.collect();
let cbody = self.compile_spine(body);
let globals = self.globals.clone();
CompiledExpr::new(move |env, interp| {
for ((name, cval), slot) in cbindings.iter().zip(slots.iter()) {
let v = cval.run(env, interp)?;
if !matches!(v, Value::CompiledClosure { .. }) {
return eval_error(format!(
"let-rec binding '{name}' must be a function, got {}",
v.type_name()
));
}
globals.set(*slot, v);
}
cbody.run(env, interp)
})
}
}
fn let_rec_frame(cbindings: Vec<(Rc<str>, CompiledExpr)>, cbody: CompiledExpr) -> CompiledExpr {
CompiledExpr::new(move |env, interp| {
let inner = env.child(vec![Value::Unit; cbindings.len()]);
for (i, (name, cval)) in cbindings.iter().enumerate() {
let v = cval.run(&inner, interp)?;
if !matches!(v, Value::CompiledClosure { .. }) {
return eval_error(format!(
"let-rec binding '{name}' must be a function, got {}",
v.type_name()
));
}
inner.set_slot(0, i as u16, v);
}
cbody.run(&inner, interp)
})
}
struct CompiledArm {
pat: Pattern,
guard: Option<CompiledExpr>,
body: CompiledExpr,
}
fn unfold_spine(ast: &Ast) -> (&Ast, Vec<&Ast>) {
let mut args = Vec::new();
let mut head = ast;
while let Ast::Apply(f, a) = head {
args.push(a.as_ref());
head = f.as_ref();
}
args.reverse();
(head, args)
}
fn pattern_vars(pat: &Pattern, out: &mut Vec<String>) {
match pat {
Pattern::Wild
| Pattern::Unit
| Pattern::Bool(_)
| Pattern::Int(_)
| Pattern::Str(_)
| Pattern::EmptyList => {}
Pattern::Var(name) => out.push(name.clone()),
Pattern::As(inner, name) => {
pattern_vars(inner, out);
out.push(name.clone());
}
Pattern::Tuple(ps) => {
for p in ps {
pattern_vars(p, out);
}
}
Pattern::Cons(h, t) => {
pattern_vars(h, out);
pattern_vars(t, out);
}
Pattern::Ctor(_, Some(p)) => pattern_vars(p, out),
Pattern::Ctor(_, None) => {}
}
}
pub(crate) fn compile_program(ast: &Ast, base_env: &BaseEnv) -> CompiledExpr {
let mut c = Compiler::new(Some(base_env));
let compiled = c.compile_spine(ast);
c.globals.finish(c.n_globals);
compiled
}
pub(crate) fn compile_program_xver(
ast: &Ast,
base_env: &BaseEnv,
base_env_v006: &BaseEnv,
) -> CompiledExpr {
let mut c = Compiler::new_xver(base_env, base_env_v006);
let compiled = c.compile_spine(ast);
c.globals.finish(c.n_globals);
compiled
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{MatchArm, Pattern};
use crate::eval::Interp;
use crate::value::{BaseEnv, Env, Value};
use rustyfi_backend::{FontKey, FontMetrics, Length};
use rustyfi_syntax::Span;
struct Mono;
impl FontMetrics for Mono {
fn advance(&self, _f: FontKey, c: char, size: Length) -> Option<Length> {
if c.is_ascii() {
Some(size * 0.5)
} else {
None
}
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
fn var(name: &str) -> Ast {
Ast::Var(name.to_string(), Span::default())
}
fn app1(f: Ast, a: Ast) -> Ast {
Ast::Apply(Box::new(f), Box::new(a))
}
fn app2(name: &str, a: Ast, b: Ast) -> Ast {
app1(app1(var(name), a), b)
}
fn fib_program(n: i64) -> Ast {
let body = Ast::IfThenElse(
Box::new(app2("<", var("n"), Ast::Int(2))),
Box::new(var("n")),
Box::new(app2(
"+",
app1(var("fib"), app2("-", var("n"), Ast::Int(1))),
app1(var("fib"), app2("-", var("n"), Ast::Int(2))),
)),
);
let fib_lambda = Rc::new(Ast::Lambda("n".to_string(), Rc::new(body)));
Ast::LetRecIn(
vec![("fib".to_string(), fib_lambda)],
Box::new(app1(var("fib"), Ast::Int(n))),
)
}
fn eval_compiled(base: &BaseEnv, ast: &Ast) -> Result<Value, EvalError> {
let mono = Mono;
let mut interp = Interp::new(&mono);
compile_program(ast, base).run(&Env::root(), &mut interp)
}
fn assert_deterministic(ast: &Ast) {
let env_a = crate::primitives::base_env();
let env_b = crate::primitives::base_env();
match (eval_compiled(&env_a, ast), eval_compiled(&env_b, ast)) {
(Ok(a), Ok(b)) => assert_eq!(
format!("{a:?}"),
format!("{b:?}"),
"two independent compiles produced different values"
),
(Err(a), Err(b)) => assert_eq!(
a.to_string(),
b.to_string(),
"two independent compiles produced different errors"
),
(a, b) => panic!("ok/err mismatch between two runs: {a:?} vs {b:?}"),
}
}
#[test]
fn deterministic_labeled_optionals() {
use std::rc::Rc;
let body = app2(
"+",
var("x"),
Ast::Match(
Box::new(var("b")),
vec![
MatchArm {
pat: Pattern::Ctor("None".to_string(), None),
guard: None,
body: Ast::Int(0),
},
MatchArm {
pat: Pattern::Ctor(
"Some".to_string(),
Some(Box::new(Pattern::Var("v".to_string()))),
),
guard: None,
body: var("v"),
},
],
),
);
let lam = Ast::LambdaOpt {
opts: vec![("bias".to_string(), "b".to_string())],
param: "x".to_string(),
body: Rc::new(body),
};
assert_deterministic(&Ast::ApplyOpt {
func: Box::new(lam.clone()),
opts: vec![("bias".to_string(), Ast::Int(40))],
arg: Box::new(Ast::Int(2)),
});
assert_deterministic(&app1(lam, Ast::Int(2)));
}
#[test]
fn deterministic_literals_and_arithmetic() {
assert_deterministic(&Ast::Int(42));
assert_deterministic(&Ast::Str("hi".to_string()));
assert_deterministic(&Ast::Bool(true));
assert_deterministic(&app2("+", Ast::Int(2), Ast::Int(3)));
assert_deterministic(&app2("*", Ast::Int(7), Ast::Int(6)));
assert_deterministic(&app2("<", Ast::Int(2), Ast::Int(3)));
assert_deterministic(&app2("^", Ast::Str("foo".into()), Ast::Str("bar".into())));
assert_deterministic(&app2("/", Ast::Int(1), Ast::Int(0)));
}
#[test]
fn deterministic_let_lambda_and_capture() {
assert_deterministic(&Ast::LetIn(
"id".into(),
Box::new(Ast::Lambda("x".into(), Rc::new(var("x")))),
Box::new(app1(var("id"), Ast::Int(7))),
));
assert_deterministic(&Ast::LetIn(
"a".into(),
Box::new(Ast::Int(5)),
Box::new(app1(
Ast::Lambda("x".into(), Rc::new(app2("+", var("a"), var("x")))),
Ast::Int(3),
)),
));
}
#[test]
fn deterministic_let_rec_fib_and_mutual() {
assert_deterministic(&fib_program(15));
let even_body = Ast::IfThenElse(
Box::new(app2("==", var("n"), Ast::Int(0))),
Box::new(Ast::Bool(true)),
Box::new(app1(var("odd"), app2("-", var("n"), Ast::Int(1)))),
);
let odd_body = Ast::IfThenElse(
Box::new(app2("==", var("n"), Ast::Int(0))),
Box::new(Ast::Bool(false)),
Box::new(app1(var("even"), app2("-", var("n"), Ast::Int(1)))),
);
let bindings = vec![
(
"even".to_string(),
Rc::new(Ast::Lambda("n".into(), Rc::new(even_body))),
),
(
"odd".to_string(),
Rc::new(Ast::Lambda("n".into(), Rc::new(odd_body))),
),
];
assert_deterministic(&Ast::LetRecIn(
bindings,
Box::new(Ast::Tuple(vec![
app1(var("even"), Ast::Int(10)),
app1(var("odd"), Ast::Int(7)),
])),
));
assert_deterministic(&Ast::LetRecIn(
vec![("x".into(), Rc::new(Ast::Int(1)))],
Box::new(var("x")),
));
}
#[test]
fn deterministic_records_lists_tuples_and_fields() {
assert_deterministic(&Ast::Record(vec![
("a".into(), Ast::Int(1)),
("b".into(), Ast::Str("x".into())),
]));
assert_deterministic(&Ast::List(vec![Ast::Int(1), Ast::Int(2), Ast::Int(3)]));
assert_deterministic(&Ast::Tuple(vec![Ast::Int(1), Ast::Bool(true)]));
let rec = Ast::Record(vec![("a".into(), Ast::Int(9)), ("b".into(), Ast::Int(8))]);
assert_deterministic(&Ast::AccessField(
Box::new(rec.clone()),
"a".into(),
Span::default(),
));
assert_deterministic(&Ast::AccessField(
Box::new(rec.clone()),
"zzz".into(),
Span::default(),
));
assert_deterministic(&Ast::UpdateField(
Box::new(rec.clone()),
"a".into(),
Box::new(Ast::Int(100)),
));
assert_deterministic(&Ast::UpdateField(
Box::new(rec),
"nope".into(),
Box::new(Ast::Int(1)),
));
}
#[test]
fn deterministic_match_arms_guards_and_ctors() {
assert_deterministic(&Ast::Match(
Box::new(Ast::Int(3)),
vec![
MatchArm {
pat: Pattern::Int(1),
guard: None,
body: Ast::Str("one".into()),
},
MatchArm {
pat: Pattern::Wild,
guard: None,
body: Ast::Str("other".into()),
},
],
));
assert_deterministic(&Ast::Match(
Box::new(Ast::Int(4)),
vec![
MatchArm {
pat: Pattern::Var("x".into()),
guard: Some(app2(">", var("x"), Ast::Int(10))),
body: Ast::Str("big".into()),
},
MatchArm {
pat: Pattern::Var("x".into()),
guard: Some(app2(">", var("x"), Ast::Int(0))),
body: Ast::Str("small".into()),
},
MatchArm {
pat: Pattern::Wild,
guard: None,
body: Ast::Str("np".into()),
},
],
));
assert_deterministic(&Ast::Match(
Box::new(Ast::List(vec![Ast::Int(1), Ast::Int(2)])),
vec![
MatchArm {
pat: Pattern::EmptyList,
guard: None,
body: Ast::Int(-1),
},
MatchArm {
pat: Pattern::Cons(
Box::new(Pattern::Var("h".into())),
Box::new(Pattern::Var("t".into())),
),
guard: None,
body: var("h"),
},
],
));
assert_deterministic(&Ast::Match(
Box::new(Ast::Ctor("Some".into(), Some(Box::new(Ast::Int(5))))),
vec![
MatchArm {
pat: Pattern::Ctor("None".into(), None),
guard: None,
body: Ast::Int(0),
},
MatchArm {
pat: Pattern::Ctor("Some".into(), Some(Box::new(Pattern::Var("x".into())))),
guard: None,
body: var("x"),
},
],
));
assert_deterministic(&Ast::Match(
Box::new(Ast::Int(5)),
vec![MatchArm {
pat: Pattern::Int(1),
guard: None,
body: Ast::Int(0),
}],
));
}
#[test]
fn deterministic_mutable_while_and_sequential() {
let deref = |n: &str| app1(var("!"), var(n));
let loop_body = Ast::Sequential(
Box::new(Ast::Overwrite(
"acc".into(),
Span::default(),
Box::new(app2("+", deref("acc"), deref("i"))),
)),
Box::new(Ast::Overwrite(
"i".into(),
Span::default(),
Box::new(app2("+", deref("i"), Ast::Int(1))),
)),
);
let while_loop = Ast::WhileDo(
Box::new(app2("<", deref("i"), Ast::Int(5))),
Box::new(loop_body),
);
let prog = Ast::LetMutableIn(
"acc".into(),
Box::new(Ast::Int(0)),
Box::new(Ast::LetMutableIn(
"i".into(),
Box::new(Ast::Int(0)),
Box::new(Ast::Sequential(
Box::new(while_loop),
Box::new(deref("acc")),
)),
)),
);
assert_deterministic(&prog); }
fn prepare_document(src: &str) -> (BaseEnv, Ast) {
let lib_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../lib-rustyfi/dist/packages/stdja-mini.satyh");
let lib_src = std::fs::read_to_string(&lib_path).unwrap();
let lib_file = rustyfi_syntax::parse_file(&lib_src).unwrap();
let doc_file = rustyfi_syntax::parse_file(src).unwrap();
let mut prelude = lib_file.prelude;
prelude.extend(doc_file.prelude);
let merged = rustyfi_syntax::cst::File {
headers: Vec::new(),
prelude,
in_kw: doc_file.in_kw,
body: doc_file.body,
eoi: doc_file.eoi,
};
let env = crate::primitives::base_env();
let store = crate::symbol::SymbolStore::new();
let scope = crate::elaborate::Scope::new(&store, env.names());
let program = crate::elaborate::elaborate_program(&merged, &scope).unwrap();
crate::typecheck::typecheck(&program).unwrap();
(env, crate::ast::debrand(&program.body, &store))
}
fn many_paragraph_src(n: usize) -> String {
let mut body = String::new();
for i in 0..n {
body.push_str(&format!(
"+p {{ paragraph number {i} with a few \\emph{{words}} to typeset here }}\n"
));
}
format!("document (||) '< {body} >")
}
#[test]
fn deterministic_document_many_paragraphs() {
let (env_a, body_a) = prepare_document(&many_paragraph_src(12));
let doc_a = eval_compiled(&env_a, &body_a).unwrap();
let (env_b, body_b) = prepare_document(&many_paragraph_src(12));
let doc_b = eval_compiled(&env_b, &body_b).unwrap();
assert_eq!(
format!("{doc_a:?}"),
format!("{doc_b:?}"),
"two independent runs produced different documents"
);
assert!(matches!(doc_a, Value::Document(_)));
}
fn bench_ns<F: FnMut()>(iters: u32, mut f: F) -> f64 {
f();
let start = std::time::Instant::now();
for _ in 0..iters {
f();
}
start.elapsed().as_nanos() as f64 / iters as f64
}
#[test]
#[ignore = "benchmark; run with --release -- --ignored --nocapture"]
fn bench_fib() {
const N: i64 = 28;
let calls = {
let (mut a, mut b) = (0u64, 1u64);
for _ in 0..=N + 1 {
let t = a + b;
a = b;
b = t;
}
2 * a - 1
};
let prog = fib_program(N);
let env = crate::primitives::base_env();
let build = bench_ns(20, || {
let _ = compile_program(&prog, &env);
});
let compiled = compile_program(&prog, &env);
let mono = Mono;
let mut interp = Interp::new(&mono);
let root = Env::root();
let run = bench_ns(20, || {
let _ = compiled.run(&root, &mut interp).unwrap();
});
println!("\n== fib({N}) : {calls} calls/eval ==");
println!(" compile : {build:>9.0} ns (one-off)");
println!(
" run : {:>9.0} ns/eval ({:>5.1} ns/call)",
run,
run / calls as f64
);
}
#[test]
#[ignore = "benchmark; run with --release -- --ignored --nocapture"]
fn bench_many_paragraph_document() {
const PARAS: usize = 300;
let (env, body) = prepare_document(&many_paragraph_src(PARAS));
let build = bench_ns(20, || {
let _ = compile_program(&body, &env);
});
let compiled = compile_program(&body, &env);
let mono = Mono;
let mut interp = Interp::new(&mono);
let root = Env::root();
let run = bench_ns(20, || {
let _ = compiled.run(&root, &mut interp).unwrap();
});
println!("\n== document with {PARAS} paragraphs ==");
println!(" compile : {build:>10.0} ns (one-off)");
println!(" run : {run:>10.0} ns/doc");
}
}