use crate::typing::domain::Trees;
use crate::typing::rule::{Conclusion, Judgment, Premise, TypingRule};
use crate::typing::{TyExpr, TypeExpr};
use std::collections::HashMap;
use std::fmt;
use std::ops::Range;
pub type Reg = usize;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Instr {
Eval { dst: Reg, expr: TyExpr },
Ascribe { binding: String, expected: Reg },
Equate { left: Reg, right: Reg },
Member { binding: String },
PushScope,
PopScope,
Extend { binding: String, ty: Reg },
Emit { ty: Reg },
Effect { binding: String, ty: Reg },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Program {
pub name: String,
pub instrs: Vec<Instr>,
pub splices: HashMap<String, Range<usize>>,
}
impl Program {
#[must_use]
pub fn splice(&self, b: &str) -> Option<&[Instr]> {
self.splices.get(b).map(|r| &self.instrs[r.clone()])
}
}
#[must_use]
pub fn compile(rule: &TypingRule, trees: &Trees) -> Program {
let mut c = Compiler {
trees,
instrs: Vec::new(),
splices: HashMap::new(),
next: 0,
};
for premise in &rule.premises {
c.premise(premise);
}
c.conclusion(&rule.conclusion);
Program {
name: rule.name.clone(),
instrs: c.instrs,
splices: c.splices,
}
}
struct Compiler<'a> {
trees: &'a Trees,
instrs: Vec<Instr>,
splices: HashMap<String, Range<usize>>,
next: Reg,
}
impl Compiler<'_> {
fn fresh(&mut self) -> Reg {
let r = self.next;
self.next += 1;
r
}
fn eval(&mut self, expr: &TypeExpr) -> Reg {
let ty = self.trees.get(expr).cloned().unwrap_or(TyExpr::Top);
let dst = self.fresh();
self.instrs.push(Instr::Eval { dst, expr: ty });
dst
}
fn premise(&mut self, p: &Premise) {
let scoped = !p.extensions.is_empty();
if scoped {
self.instrs.push(Instr::PushScope);
}
let setting_start = self.instrs.len();
for (name, ext) in &p.extensions {
let r = self.eval(ext);
self.instrs.push(Instr::Extend {
binding: name.clone(),
ty: r,
});
}
match &p.judgment {
Judgment::Ascription { binding, ty } => {
let ascribe_eval_start = self.instrs.len();
let r = self.eval(ty);
self.splices
.insert(binding.clone(), setting_start..ascribe_eval_start);
self.instrs.push(Instr::Ascribe {
binding: binding.clone(),
expected: r,
});
}
Judgment::Membership { binding } => {
let member_start = self.instrs.len();
self.instrs.push(Instr::Member {
binding: binding.clone(),
});
self.splices
.insert(binding.clone(), setting_start..member_start);
}
Judgment::Equation { left, right } => {
let l = self.eval(left);
let r = self.eval(right);
self.instrs.push(Instr::Equate { left: l, right: r });
}
}
if scoped {
self.instrs.push(Instr::PopScope);
}
}
fn conclusion(&mut self, c: &Conclusion) {
for (var, ty) in &c.effects {
let r = self.eval(ty);
self.instrs.push(Instr::Effect {
binding: var.clone(),
ty: r,
});
}
let r = self.eval(&c.ty);
self.instrs.push(Instr::Emit { ty: r });
}
}
impl fmt::Display for Instr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Instr::Eval { dst, expr } => write!(f, "r{dst} = {expr}"),
Instr::Ascribe { binding, expected } => write!(f, "ascribe {binding} : r{expected}"),
Instr::Equate { left, right } => write!(f, "equate r{left} = r{right}"),
Instr::Member { binding } => write!(f, "member {binding}"),
Instr::PushScope => write!(f, "push_scope"),
Instr::PopScope => write!(f, "pop_scope"),
Instr::Extend { binding, ty } => write!(f, "extend {binding} := r{ty}"),
Instr::Emit { ty } => write!(f, "emit r{ty}"),
Instr::Effect { binding, ty } => write!(f, "effect {binding} := r{ty}"),
}
}
}
impl fmt::Display for Program {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}:", self.name)?;
let mut indent = 1usize;
for instr in &self.instrs {
if matches!(instr, Instr::PopScope) {
indent = indent.saturating_sub(1);
}
writeln!(f, "{}{instr}", " ".repeat(indent))?;
if matches!(instr, Instr::PushScope) {
indent += 1;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::grammar::SPG;
use crate::typing::TypingRule;
fn stlc() -> SPG {
SPG::load(include_str!("../../examples/stlc.auf")).unwrap()
}
fn trees(g: &SPG, rule: &TypingRule) -> Trees {
let bindings = g.rule_bindings(&rule.name);
rule.type_exprs()
.into_iter()
.filter_map(|te| {
TyExpr::build(g, te, &bindings)
.ok()
.map(|ty| (te.clone(), ty))
})
.collect()
}
fn compile_src(g: &SPG, premises: &str, conclusion: &str, name: &str) -> Program {
let rule = TypingRule::new(premises.into(), conclusion.into(), name.into()).unwrap();
compile(&rule, &trees(g, &rule))
}
#[test]
fn app_lowers_to_two_ascriptions_and_an_emit() {
let g = stlc();
let prog = compile_src(&g, "Γ ⊢ l : ?A -> ?B, Γ ⊢ r : ?A", "?B", "app");
let kinds: Vec<_> = prog
.instrs
.iter()
.map(|i| match i {
Instr::Eval { .. } => "eval",
Instr::Ascribe { .. } => "ascribe",
Instr::Emit { .. } => "emit",
_ => "other",
})
.collect();
assert_eq!(
kinds,
vec!["eval", "ascribe", "eval", "ascribe", "eval", "emit"]
);
assert!(
matches!(&prog.instrs[0], Instr::Eval { expr: TyExpr::Con(label, _), .. } if label == "FunctionType")
);
assert!(matches!(&prog.instrs[1], Instr::Ascribe { binding, .. } if binding == "l"));
}
#[test]
fn lambda_scopes_its_context_extension() {
let g = stlc();
let prog = compile_src(&g, "Γ[a:τ] ⊢ e : ?B", "τ -> ?B", "lambda");
assert!(prog.instrs.contains(&Instr::PushScope));
assert!(prog.instrs.contains(&Instr::PopScope));
let push = prog
.instrs
.iter()
.position(|i| *i == Instr::PushScope)
.unwrap();
let pop = prog
.instrs
.iter()
.position(|i| *i == Instr::PopScope)
.unwrap();
let ascribe = prog
.instrs
.iter()
.position(|i| matches!(i, Instr::Ascribe { binding, .. } if binding == "e"))
.unwrap();
let emit = prog
.instrs
.iter()
.position(|i| matches!(i, Instr::Emit { .. }))
.unwrap();
assert!(
push < ascribe && ascribe < pop,
"ascription is inside the scope"
);
assert!(pop < emit, "conclusion is emitted after the scope closes");
}
#[test]
fn var_lowers_to_member_and_ctx_emit() {
let g = stlc();
let prog = compile_src(&g, "x ∈ Γ", "Γ(x)", "var");
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Member { binding } if binding == "x"))
);
assert!(matches!(prog.instrs.last(), Some(Instr::Emit { .. })));
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Eval { expr: TyExpr::Ctx(v), .. } if v == "x"))
);
}
#[test]
fn display_is_readable() {
let g = stlc();
let prog = compile_src(&g, "Γ[a:τ] ⊢ e : ?B", "τ -> ?B", "lambda");
let s = prog.to_string();
assert!(s.starts_with("lambda:\n"));
assert!(s.contains("push_scope"));
assert!(s.contains("ascribe e : r"));
}
#[test]
fn splice_for_app_separates_l_and_r() {
let g = stlc();
let prog = compile_src(&g, "Γ ⊢ l : ?A -> ?B, Γ ⊢ r : ?A", "?B", "app");
let l = prog.splice("l").unwrap();
let r = prog.splice("r").unwrap();
assert!(l.is_empty(), "expected empty splice for `l`, got {l:?}");
assert!(r.is_empty(), "expected empty splice for `r`, got {r:?}");
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Ascribe { binding, .. } if binding == "l"))
);
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Ascribe { binding, .. } if binding == "r"))
);
}
#[test]
fn setting_is_premise_local_across_siblings() {
let g = stlc();
let prog = compile_src(&g, "Γ[a:τ] ⊢ l : ?A -> ?B, Γ ⊢ r : ?A", "?B", "app_set");
let l_splice = prog.splice("l").unwrap();
assert!(
l_splice
.iter()
.any(|i| matches!(i, Instr::Extend { binding, .. } if binding == "a")),
"splice for `l` should include setting extension `a`, got {l_splice:?}"
);
let r_splice = prog.splice("r").unwrap();
assert!(
r_splice
.iter()
.all(|i| !matches!(i, Instr::Extend { binding, .. } if binding == "a")),
"splice for `r` must not include sibling setting `a`, got {r_splice:?}"
);
assert!(prog.splice("a").is_none());
}
#[test]
fn splice_for_lambda_includes_setting_not_ascribe() {
let g = stlc();
let prog = compile_src(&g, "Γ[a:τ] ⊢ e : ?B", "τ -> ?B", "lambda");
let s_e = prog.splice("e").unwrap();
assert!(
s_e.iter()
.any(|i| matches!(i, Instr::Extend { binding, .. } if binding == "a"))
);
assert!(
s_e.iter()
.all(|i| !matches!(i, Instr::Ascribe { binding, .. } if binding == "e"))
);
assert!(s_e.iter().all(|i| !matches!(i, Instr::PushScope)));
assert!(s_e.iter().all(|i| !matches!(i, Instr::PopScope)));
assert!(prog.splice("a").is_none());
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Ascribe { binding, .. } if binding == "e"))
);
}
#[test]
fn premise_term_splice_covers_all_its_settings() {
let g = stlc();
let prog = compile_src(&g, "Γ[a:τ][b:σ] ⊢ e : ?A", "?A", "two_set");
let s_e = prog.splice("e").unwrap();
assert_eq!(
s_e.iter()
.filter(|i| matches!(i, Instr::Extend { .. }))
.count(),
2,
"e's descent applies both settings, got {s_e:?}"
);
assert!(prog.splice("a").is_none());
assert!(prog.splice("b").is_none());
assert!(
prog.instrs
.iter()
.any(|i| matches!(i, Instr::Ascribe { binding, .. } if binding == "e"))
);
assert!(s_e.iter().all(|i| !matches!(i, Instr::Ascribe { .. })));
assert!(s_e.iter().all(|i| !matches!(i, Instr::PushScope)));
assert!(s_e.iter().all(|i| !matches!(i, Instr::PopScope)));
}
#[test]
fn splice_for_var_is_setting_only() {
let g = stlc();
let prog = compile_src(&g, "x ∈ Γ", "Γ(x)", "var");
let s = prog.splice("x").unwrap();
assert!(s.iter().all(|i| !matches!(i, Instr::Member { .. })));
}
}