Massive feature drop

- pattern matching seems to be correct
- dynamic dispatch works with the to_string example
- template strings as a last-minute addition
- interpreter revamp, virtual stack for abort safety
This commit is contained in:
2024-01-29 18:26:56 +00:00
parent a8887227e5
commit c279301583
71 changed files with 947 additions and 932 deletions

View File

@@ -1,24 +1,10 @@
use std::collections::VecDeque;
use std::mem;
use never::Never;
use super::context::RunContext;
use super::error::RunError;
use super::nort::{Clause, ClauseInst, Expr};
use super::path_set::{PathSet, Step};
use super::run::run;
use crate::location::CodeLocation;
/// Information about a function call presented to an external function
pub struct CallData<'a> {
/// Location of the function expression
pub location: CodeLocation,
/// The argument the function was called on. Functions are curried
pub arg: Expr,
/// Information relating to this interpreter run
pub ctx: RunContext<'a>,
}
use crate::foreign::atom::CallData;
/// Process the clause at the end of the provided path. Note that paths always
/// point to at least one target. Note also that this is not cached as a
@@ -42,15 +28,12 @@ fn map_at<E>(
_ => (),
}
Ok(match (source, path.next()) {
(Clause::Lambda { .. } | Clause::Identity(_), _) =>
unreachable!("Handled above"),
(Clause::Lambda { .. } | Clause::Identity(_), _) => unreachable!("Handled above"),
// If the path ends and this isn't a lambda, process it
(val, None) => mapper(val)?,
// If it's an Apply, execute the next step in the path
(Clause::Apply { f, x }, Some(head)) => {
let proc = |x: &Expr| {
Ok(map_at(path, &x.clause.cls(), mapper)?.to_expr(x.location()))
};
let proc = |x: &Expr| Ok(map_at(path, &x.cls(), mapper)?.to_expr(x.location()));
match head {
None => Clause::Apply { f: proc(f)?, x: x.clone() },
Some(n) => {
@@ -69,7 +52,12 @@ fn map_at<E>(
/// with the value in the body. Note that a path may point to multiple
/// placeholders.
#[must_use]
fn substitute(paths: &PathSet, value: ClauseInst, body: &Clause) -> Clause {
pub fn substitute(
paths: &PathSet,
value: ClauseInst,
body: &Clause,
on_sub: &mut impl FnMut(),
) -> Clause {
let PathSet { steps, next } = paths;
map_at(steps.iter().cloned(), body, &mut |chkpt| -> Result<Clause, Never> {
match (chkpt, next) {
@@ -80,18 +68,20 @@ fn substitute(paths: &PathSet, value: ClauseInst, body: &Clause) -> Clause {
let mut argv = x.clone();
let f = match conts.get(&None) {
None => f.clone(),
Some(sp) => substitute(sp, value.clone(), &f.clause.cls())
.to_expr(f.location()),
Some(sp) => substitute(sp, value.clone(), &f.cls(), on_sub).to_expr(f.location()),
};
for (i, old) in argv.iter_mut().rev().enumerate() {
if let Some(sp) = conts.get(&Some(i)) {
let tmp = substitute(sp, value.clone(), &old.clause.cls());
let tmp = substitute(sp, value.clone(), &old.cls(), on_sub);
*old = tmp.to_expr(old.location());
}
}
Ok(Clause::Apply { f, x: argv })
},
(Clause::LambdaArg, None) => Ok(Clause::Identity(value.clone())),
},
(Clause::LambdaArg, None) => {
on_sub();
Ok(Clause::Identity(value.clone()))
},
(_, None) => panic!("Argument path must point to LambdaArg"),
(_, Some(_)) => panic!("Argument path can only fork at Apply"),
}
@@ -99,11 +89,7 @@ fn substitute(paths: &PathSet, value: ClauseInst, body: &Clause) -> Clause {
.unwrap_or_else(|e| match e {})
}
pub(super) fn apply_as_atom(
f: Expr,
arg: Expr,
ctx: RunContext,
) -> Result<Clause, RunError> {
pub(super) fn apply_as_atom(f: Expr, arg: Expr, ctx: RunContext) -> Result<Clause, RunError> {
let call = CallData { location: f.location(), arg, ctx };
match f.clause.try_unwrap() {
Ok(clause) => match clause {
@@ -116,73 +102,3 @@ pub(super) fn apply_as_atom(
},
}
}
/// Apply a function-like expression to a parameter.
pub(super) fn apply(
mut f: Expr,
mut argv: VecDeque<Expr>,
mut ctx: RunContext,
) -> Result<(Option<usize>, Clause), RunError> {
// allow looping but break on the main path so that `continue` functions as a
// trampoline
loop {
if argv.is_empty() {
return Ok((ctx.gas, f.clause.into_cls()));
} else if ctx.gas == Some(0) {
return Ok((Some(0), Clause::Apply { f, x: argv }));
}
let mut f_cls = f.clause.cls_mut();
match &mut *f_cls {
// apply an ExternFn or an internal function
Clause::Atom(_) => {
mem::drop(f_cls);
// take a step in expanding atom
let halt = run(f, ctx.clone())?;
ctx.gas = halt.gas;
if halt.inert && halt.state.clause.is_atom() {
let arg = argv.pop_front().expect("checked above");
let loc = halt.state.location();
f = apply_as_atom(halt.state, arg, ctx.clone())?.to_expr(loc)
} else {
f = halt.state
}
},
Clause::Lambda { args, body } => {
match args {
None => *f_cls = body.clause.clone().into_cls(),
Some(args) => {
let arg = argv.pop_front().expect("checked above").clause.clone();
let cls = substitute(args, arg, &body.clause.cls());
// cost of substitution
// XXX: should this be the number of occurrences instead?
ctx.use_gas(1);
mem::drop(f_cls);
f = cls.to_expr(f.location());
},
}
},
Clause::Constant(name) => {
let name = name.clone();
mem::drop(f_cls);
f = (ctx.symbols.get(&name).cloned())
.ok_or_else(|| RunError::MissingSymbol(name, f.location()))?;
ctx.use_gas(1);
},
Clause::Apply { f: fun, x } => {
for item in x.drain(..).rev() {
argv.push_front(item)
}
let tmp = fun.clone();
mem::drop(f_cls);
f = tmp;
},
Clause::Identity(f2) => {
let tmp = f2.clone();
mem::drop(f_cls);
f.clause = tmp
},
Clause::Bottom(bottom) => return Err(bottom.clone()),
Clause::LambdaArg => panic!("Leftover argument marker"),
}
}
}

View File

@@ -10,6 +10,8 @@ pub struct RunContext<'a> {
pub symbols: &'a HashMap<Sym, Expr>,
/// The number of reduction steps the interpreter can take before returning
pub gas: Option<usize>,
/// The limit of recursion
pub stack_size: usize,
}
impl<'a> RunContext<'a> {
/// Consume some gas if it is being counted

View File

@@ -1,34 +1,66 @@
use std::fmt::{Debug, Display};
use std::sync::Arc;
use std::fmt::{self, Debug, Display};
use crate::foreign::error::ExternError;
use itertools::Itertools;
use super::nort::Expr;
use super::run::Interrupted;
use crate::foreign::error::{ExternError, ExternErrorObj};
use crate::location::CodeLocation;
use crate::name::Sym;
use super::run::Interrupted;
/// Print a stack trace
pub fn strace(stack: &[Expr]) -> String {
stack.iter().rev().map(|x| format!("{x}\n at {}", x.location)).join("\n")
}
/// Problems in the process of execution
#[derive(Debug, Clone)]
pub enum RunError {
/// A Rust function encountered an error
Extern(Arc<dyn ExternError>),
/// Symbol not in context
MissingSymbol(Sym, CodeLocation),
Extern(ExternErrorObj),
/// Ran out of gas
Interrupted(Interrupted)
Interrupted(Interrupted),
}
impl From<Arc<dyn ExternError>> for RunError {
fn from(value: Arc<dyn ExternError>) -> Self { Self::Extern(value) }
impl<T: ExternError + 'static> From<T> for RunError {
fn from(value: T) -> Self { Self::Extern(value.rc()) }
}
impl From<ExternErrorObj> for RunError {
fn from(value: ExternErrorObj) -> Self { Self::Extern(value) }
}
impl Display for RunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Extern(e) => write!(f, "Error in external function: {e}"),
Self::MissingSymbol(sym, loc) => {
write!(f, "{sym}, called at {loc} is not loaded")
Self::Interrupted(i) => {
write!(f, "Ran out of gas:\n{}", strace(&i.stack))
},
Self::Extern(e) => write!(f, "Program fault: {e}"),
}
}
}
#[derive(Clone)]
pub(crate) struct StackOverflow {
pub stack: Vec<Expr>,
}
impl ExternError for StackOverflow {}
impl Display for StackOverflow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let limit = self.stack.len() - 2; // 1 for failed call, 1 for current
write!(f, "Stack depth exceeded {limit}:\n{}", strace(&self.stack))
}
}
#[derive(Clone)]
pub(crate) struct MissingSymbol {
pub sym: Sym,
pub loc: CodeLocation,
}
impl ExternError for MissingSymbol {}
impl Display for MissingSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, called at {} is not loaded", self.sym, self.loc)
}
}

View File

@@ -75,12 +75,13 @@ impl<'a> HandlerTable<'a> {
pub fn run_handler(
mut state: Expr,
handlers: &mut HandlerTable,
RunContext { mut gas, symbols }: RunContext,
mut ctx: RunContext,
) -> Result<Halt, RunError> {
loop {
let inert;
Halt { gas, inert, state } = run(state, RunContext { gas, symbols })?;
let state_cls = state.clause.cls();
let halt = run(state, ctx.clone())?;
state = halt.state;
ctx.use_gas(halt.gas.unwrap_or(0));
let state_cls = state.cls();
if let Clause::Atom(Atom(a)) = &*state_cls {
if let Some(res) = handlers.dispatch(a.as_ref(), state.location()) {
drop(state_cls);
@@ -88,9 +89,9 @@ pub fn run_handler(
continue;
}
}
if inert || gas == Some(0) {
if halt.inert || ctx.no_gas() {
drop(state_cls);
break Ok(Halt { gas, inert, state });
break Ok(Halt { gas: ctx.gas, inert: halt.inert, state });
}
}
}

View File

@@ -97,6 +97,28 @@ impl Expr {
| Clause::Bottom(_) => None,
})
}
/// Clone the refcounted [ClauseInst] out of the expression
#[must_use]
pub fn clsi(&self) -> ClauseInst { self.clause.clone() }
/// Readonly access to the [Clause]
///
/// # Panics
///
/// if the clause is already borrowed
#[must_use]
pub fn cls(&self) -> impl Deref<Target = Clause> + '_ { self.clause.cls() }
/// Read-Write access to the [Clause]
///
/// # Panics
///
/// if the clause is already borrowed
#[must_use]
pub fn cls_mut(&self) -> impl DerefMut<Target = Clause> + '_ {
self.clause.cls_mut()
}
}
impl Debug for Expr {
@@ -160,15 +182,21 @@ impl ClauseInst {
/// Call a normalization function on the expression. The expr is
/// updated with the new clause which affects all copies of it
/// across the tree.
///
/// This function bypasses and collapses identities, but calling it in a plain
/// loop intermittently re-acquires the mutex, and looping inside of it breaks
/// identity collapsing. [ClauseInst::try_normalize_trampoline] solves these
/// problems.
pub fn try_normalize<T>(
&self,
mapper: impl FnOnce(Clause) -> Result<(Clause, T), RunError>,
) -> Result<(ClauseInst, T), RunError> {
) -> Result<(Self, T), RunError> {
enum Report<T> {
Nested(ClauseInst, T),
Plain(T),
}
let ret = take_with_output(&mut *self.cls_mut(), |clause| match &clause {
// don't modify identities, instead update and return the nested clause
Clause::Identity(alt) => match alt.try_normalize(mapper) {
Ok((nested, t)) => (clause, Ok(Report::Nested(nested, t))),
Err(e) => (Clause::Bottom(e.clone()), Err(e)),
@@ -184,6 +212,32 @@ impl ClauseInst {
})
}
/// Repeatedly call a normalization function on the held clause, switching
/// [ClauseInst] values as needed to ensure that
pub fn try_normalize_trampoline<T>(
mut self,
mut mapper: impl FnMut(Clause) -> Result<(Clause, Option<T>), RunError>,
) -> Result<(Self, T), RunError> {
loop {
let (next, exit) = self.try_normalize(|mut cls| {
loop {
if matches!(cls, Clause::Identity(_)) {
break Ok((cls, None));
}
let (next, exit) = mapper(cls)?;
if let Some(exit) = exit {
break Ok((next, Some(exit)));
}
cls = next;
}
})?;
if let Some(exit) = exit {
break Ok((next, exit));
}
self = next
}
}
/// Call a predicate on the clause, returning whatever the
/// predicate returns. This is a convenience function for reaching
/// through the [Mutex]. The clause will never be [Clause::Identity].
@@ -320,11 +374,11 @@ impl Display for Clause {
Clause::Apply { f: fun, x } =>
write!(f, "({fun} {})", x.iter().join(" ")),
Clause::Lambda { args, body } => match args {
Some(path) => write!(f, "\\{path:?}.{body}"),
None => write!(f, "\\_.{body}"),
Some(path) => write!(f, "[\\{path}.{body}]"),
None => write!(f, "[\\_.{body}]"),
},
Clause::Constant(t) => write!(f, "{t}"),
Clause::Identity(other) => write!(f, "({other})"),
Clause::Identity(other) => write!(f, "{{{other}}}"),
}
}
}

View File

@@ -67,8 +67,7 @@ impl<'a, T: ?Sized, U: ?Sized> NortBuilder<'a, T, U> {
}
fn non_app_step<V>(self, f: impl FnOnce(NortBuilder<T, U>) -> V) -> V {
if let Some(IntGenData::Apply(_)) = self.stack.value() {
let prev = self.pop(1);
f(prev.push(IntGenData::AppF))
f(self.pop(1).push(IntGenData::AppF))
} else {
f(self)
}
@@ -80,14 +79,17 @@ impl<'a, T: ?Sized, U: ?Sized> NortBuilder<'a, T, U> {
pub fn arg_logic(self, name: &'a U) {
let mut lambda_chk = (self.lambda_picker)(name);
self.non_app_step(|ctx| {
let opt = ctx.stack.rfold(None, |path, item| match item {
let res = ctx.stack.iter().try_fold(vec![], |path, item| match item {
IntGenData::Apply(_) => panic!("This is removed after handling"),
IntGenData::Lambda(n, rc) =>
lambda_chk(n).then(|| (vec![], *rc)).or(path),
IntGenData::AppArg(n) => path.map(|(p, rc)| (pushed(p, Some(*n)), rc)),
IntGenData::AppF => path.map(|(p, rc)| (pushed(p, None), rc)),
IntGenData::Lambda(n, rc) => match lambda_chk(n) {
false => Ok(path),
true => Err((path, *rc))
},
IntGenData::AppArg(n) => Ok(pushed(path, Some(*n))),
IntGenData::AppF => Ok(pushed(path, None)),
});
let (path, slot) = opt.expect("Argument not wrapped in matching lambda");
let (mut path, slot) = res.expect_err("Argument not wrapped in matching lambda");
path.reverse();
match &mut *slot.borrow_mut() {
slot @ None => *slot = Some(PathSet::end(path)),
Some(slot) => take_mut::take(slot, |p| p.overlay(PathSet::end(path))),

View File

@@ -1,101 +1,129 @@
use std::collections::VecDeque;
use std::mem;
use hashbrown::HashMap;
use super::apply::apply;
use super::context::{Halt, RunContext};
use super::error::RunError;
use super::nort::{Clause, Expr};
use crate::foreign::atom::AtomicReturn;
use crate::foreign::error::ExternResult;
use crate::location::CodeLocation;
use crate::name::Sym;
use crate::foreign::atom::{AtomicReturn, RunData};
use crate::foreign::error::ExternError;
use crate::interpreter::apply::{apply_as_atom, substitute};
use crate::interpreter::error::{strace, MissingSymbol, StackOverflow};
use crate::utils::pure_seq::pushed;
/// Information about a normalization run presented to an atom
#[derive(Clone)]
pub struct RunData<'a> {
/// Location of the atom
pub location: CodeLocation,
/// Information about the execution
pub ctx: RunContext<'a>,
}
#[derive(Debug)]
/// Interpreter state when processing was interrupted
#[derive(Debug, Clone)]
pub struct Interrupted {
stack: Vec<Expr>,
/// Cached soft stack to save the interpreter having to rebuild it from the
/// bottom.
pub stack: Vec<Expr>,
}
impl Interrupted {
pub fn resume(self, ctx: RunContext) -> Result<Halt, RunError> {
run_stack(self.stack, ctx)
}
/// Continue processing where it was interrupted
pub fn resume(self, ctx: RunContext) -> Result<Halt, RunError> { run_stack(self.stack, ctx) }
}
/// Normalize an expression using beta reduction with memoization
pub fn run(mut expr: Expr, mut ctx: RunContext) -> Result<Halt, RunError> {
run_stack(vec![expr], ctx)
pub fn run(expr: Expr, ctx: RunContext) -> Result<Halt, RunError> {
let mut v = Vec::with_capacity(1000);
v.push(expr);
run_stack(v, ctx)
}
fn run_stack(
mut stack: Vec<Expr>,
mut ctx: RunContext,
) -> Result<Halt, RunError> {
fn run_stack(mut stack: Vec<Expr>, mut ctx: RunContext) -> Result<Halt, RunError> {
let mut expr = stack.pop().expect("Empty stack");
let mut popped = false;
loop {
// print!("Now running {expr}");
// let trace = strace(&stack);
// if trace.is_empty() {
// println!("\n")
// } else {
// println!("\n{trace}\n")
// };
if ctx.no_gas() {
return Err(RunError::Interrupted(Interrupted {
stack: pushed(stack, expr),
}));
return Err(RunError::Interrupted(Interrupted { stack: pushed(stack, expr) }));
}
let (next_clsi, inert) = expr.clause.try_normalize(|mut cls| {
loop {
if ctx.no_gas() {
return Ok((cls, false));
ctx.use_gas(1);
enum Res {
Inert,
Cont,
Push(Expr),
}
let (next_clsi, res) = expr.clause.try_normalize(|cls| match cls {
Clause::Identity(_) => panic!("Passed by try_normalize"),
Clause::LambdaArg => panic!("Unbound argument"),
Clause::Lambda { .. } => Ok((cls, Res::Inert)),
Clause::Bottom(b) => Err(b),
Clause::Constant(n) => match ctx.symbols.get(&n) {
Some(expr) => Ok((Clause::Identity(expr.clsi()), Res::Cont)),
None => Err(RunError::Extern(MissingSymbol { sym: n.clone(), loc: expr.location() }.rc())),
},
Clause::Atom(mut a) => {
if !popped {
if let Some(delegate) = a.0.redirect() {
let next = delegate.clone();
return Ok((Clause::Atom(a), Res::Push(next)));
}
}
match cls {
cls @ Clause::Identity(_) => return Ok((cls, false)),
// TODO:
// - unfuck nested loop
// - inline most of [apply] to eliminate recursion step
Clause::Apply { f, x } => {
if x.is_empty() {
return Ok((f.clause.into_cls(), false));
}
let (gas, clause) = apply(f, x, ctx.clone())?;
if ctx.gas.is_some() {
ctx.gas = gas;
}
cls = clause;
let rd = RunData { ctx: ctx.clone(), location: expr.location() };
match a.run(rd)? {
AtomicReturn::Inert(c) => Ok((c, Res::Inert)),
AtomicReturn::Change(gas, c) => {
ctx.use_gas(gas);
Ok((c, Res::Cont))
},
Clause::Atom(data) => {
let run = RunData { ctx: ctx.clone(), location: expr.location() };
let atomic_ret = data.run(run)?;
if ctx.gas.is_some() {
ctx.gas = atomic_ret.gas;
}
},
Clause::Apply { f, mut x } => {
if x.is_empty() {
return Ok((Clause::Identity(f.clsi()), Res::Cont));
}
match &*f.cls() {
Clause::Identity(f2) =>
return Ok((Clause::Apply { f: f2.clone().to_expr(f.location()), x }, Res::Cont)),
Clause::Apply { f, x: x2 } => {
for item in x2.iter().rev() {
x.push_front(item.clone())
}
if atomic_ret.inert {
return Ok((atomic_ret.clause, true));
}
cls = atomic_ret.clause;
return Ok((Clause::Apply { f: f.clone(), x }, Res::Cont));
},
Clause::Constant(c) => {
let symval = (ctx.symbols.get(&c)).ok_or_else(|| {
RunError::MissingSymbol(c.clone(), expr.location())
})?;
ctx.gas = ctx.gas.map(|g| g - 1); // cost of lookup
cls = Clause::Identity(symval.clause.clone());
_ => (),
}
if !popped {
return Ok((Clause::Apply { f: f.clone(), x }, Res::Push(f)));
}
let f_cls = f.cls();
let arg = x.pop_front().expect("checked above");
let loc = f.location();
let f = match &*f_cls {
Clause::Atom(_) => {
mem::drop(f_cls);
apply_as_atom(f, arg, ctx.clone())?
},
// non-reducible
c => return Ok((c, true)),
Clause::Lambda { args, body } => match args {
None => body.clsi().into_cls(),
Some(args) => substitute(args, arg.clsi(), &body.cls(), &mut || ctx.use_gas(1)),
},
c => panic!("Run should never settle on {c}"),
};
}
Ok((Clause::Apply { f: f.to_expr(loc), x }, Res::Cont))
},
})?;
expr.clause = next_clsi;
if inert {
match stack.pop() {
Some(e) => expr = e,
None => return Ok(Halt { state: expr, gas: ctx.gas, inert }),
}
popped = matches!(res, Res::Inert);
match res {
Res::Cont => continue,
Res::Inert => match stack.pop() {
None => return Ok(Halt { state: expr, gas: ctx.gas, inert: true }),
Some(prev) => expr = prev,
},
Res::Push(next) => {
if stack.len() == ctx.stack_size {
stack.extend([expr, next]);
return Err(RunError::Extern(StackOverflow { stack }.rc()));
}
stack.push(expr);
expr = next;
},
}
}
}