forked from Orchid/orchid
262 lines
10 KiB
Rust
262 lines
10 KiB
Rust
//! Adaptor trait to embed Rust values in Orchid expressions
|
|
|
|
use std::any::Any;
|
|
use std::fmt;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use never::Never;
|
|
|
|
use super::error::{RTError, RTResult};
|
|
use crate::interpreter::context::{RunEnv, RunParams};
|
|
use crate::interpreter::nort;
|
|
use crate::location::{CodeLocation, SourceRange};
|
|
use crate::name::NameLike;
|
|
use crate::parse::lexer::Lexeme;
|
|
use crate::parse::parsed;
|
|
use crate::utils::ddispatch::{request, Request, Responder};
|
|
|
|
/// Information returned by [Atomic::run].
|
|
pub enum AtomicReturn {
|
|
/// No work was done. If the atom takes an argument, it can be provided now
|
|
Inert(Atom),
|
|
/// Work was done, returns new clause and consumed gas. 1 gas is already
|
|
/// consumed by the virtual call, so nonzero values indicate expensive
|
|
/// operations.
|
|
Change(usize, nort::Clause),
|
|
}
|
|
impl AtomicReturn {
|
|
/// Report indicating that the value is inert. The result here is always [Ok],
|
|
/// it's meant to match the return type of [Atomic::run]
|
|
#[allow(clippy::unnecessary_wraps)]
|
|
pub fn inert<T: Atomic, E>(this: T) -> Result<Self, E> { Ok(Self::Inert(Atom::new(this))) }
|
|
}
|
|
|
|
/// Returned by [Atomic::run]
|
|
pub type AtomicResult = RTResult<AtomicReturn>;
|
|
|
|
/// General error produced when a non-function [Atom] is applied to something as
|
|
/// a function.
|
|
#[derive(Clone)]
|
|
pub struct NotAFunction(pub nort::Expr);
|
|
impl RTError for NotAFunction {}
|
|
impl fmt::Display for NotAFunction {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{:?} is not a function", self.0)
|
|
}
|
|
}
|
|
|
|
/// Information about a function call presented to an external function
|
|
pub struct CallData<'a, 'b> {
|
|
/// Location of the function expression
|
|
pub location: CodeLocation,
|
|
/// The argument the function was called on. Functions are curried
|
|
pub arg: nort::Expr,
|
|
/// Globally available information such as the list of all constants
|
|
pub env: &'a RunEnv<'b>,
|
|
/// Resource limits and other details specific to this interpreter run
|
|
pub params: &'a mut RunParams,
|
|
}
|
|
|
|
/// Information about a normalization run presented to an atom
|
|
pub struct RunData<'a, 'b> {
|
|
/// Location of the atom
|
|
pub location: CodeLocation,
|
|
/// Globally available information such as the list of all constants
|
|
pub env: &'a RunEnv<'b>,
|
|
/// Resource limits and other details specific to this interpreter run
|
|
pub params: &'a mut RunParams,
|
|
}
|
|
|
|
/// Functionality the interpreter needs to handle a value
|
|
///
|
|
/// # Lifecycle methods
|
|
///
|
|
/// Atomics expose the methods [Atomic::redirect], [Atomic::run],
|
|
/// [Atomic::apply] and [Atomic::apply_mut] to interact with the interpreter.
|
|
/// The interpreter first tries to call `redirect` to find a subexpression to
|
|
/// normalize. If it returns `None` or the subexpression is inert, `run` is
|
|
/// called. `run` takes ownership of the value and returns a new one.
|
|
///
|
|
/// If `run` indicated in its return value that the result is inert and the atom
|
|
/// is in the position of a function, `apply` or `apply_mut` is called depending
|
|
/// upon whether the atom is referenced elsewhere. `apply` falls back to
|
|
/// `apply_mut` so implementing it is considered an optimization to avoid
|
|
/// excessive copying.
|
|
///
|
|
/// Atoms don't generally have to be copyable because clauses are refcounted in
|
|
/// the interpreter, but Orchid code is always free to duplicate the references
|
|
/// and apply them as functions to multiple different arguments so atoms that
|
|
/// represent functions have to support application by-ref without consuming the
|
|
/// function itself.
|
|
pub trait Atomic: Any + fmt::Debug + Responder + Send
|
|
where Self: 'static
|
|
{
|
|
/// Casts this value to [Any] so that its original value can be salvaged
|
|
/// during introspection by other external code.
|
|
///
|
|
/// This function should be implemented in exactly one way:
|
|
///
|
|
/// ```ignore
|
|
/// fn as_any(self: Box<Self>) -> Box<dyn Any> { self }
|
|
/// ```
|
|
#[must_use]
|
|
fn as_any(self: Box<Self>) -> Box<dyn Any>;
|
|
/// See [Atomic::as_any], exactly the same but for references
|
|
#[must_use]
|
|
fn as_any_ref(&self) -> &dyn Any;
|
|
/// Print the atom's type name. Should only ever be implemented as
|
|
///
|
|
/// ```ignore
|
|
/// fn type_name(&self) -> &'static str { std::any::type_name::<Self>() }
|
|
/// ```
|
|
fn type_name(&self) -> &'static str;
|
|
|
|
/// Returns a reference to a possible expression held inside the atom which
|
|
/// can be reduced. For an overview of the lifecycle see [Atomic]
|
|
fn redirect(&mut self) -> Option<&mut nort::Expr>;
|
|
|
|
/// Attempt to normalize this value. If it wraps a value, this should report
|
|
/// inert. If it wraps a computation, it should execute one logical step of
|
|
/// the computation and return a structure representing the next.
|
|
///
|
|
/// For an overview of the lifecycle see [Atomic]
|
|
fn run(self: Box<Self>, run: RunData) -> AtomicResult;
|
|
|
|
/// Combine the function with an argument to produce a new clause. Falls back
|
|
/// to [Atomic::apply_mut] by default.
|
|
///
|
|
/// For an overview of the lifecycle see [Atomic]
|
|
fn apply(mut self: Box<Self>, call: CallData) -> RTResult<nort::Clause> { self.apply_mut(call) }
|
|
|
|
/// Combine the function with an argument to produce a new clause
|
|
///
|
|
/// For an overview of the lifecycle see [Atomic]
|
|
fn apply_mut(&mut self, call: CallData) -> RTResult<nort::Clause>;
|
|
|
|
/// Must return true for atoms parsed from identical source.
|
|
/// If the atom cannot be parsed from source, it can safely be ignored
|
|
#[allow(unused_variables)]
|
|
fn parser_eq(&self, other: &dyn Atomic) -> bool { false }
|
|
|
|
/// Wrap the atom in a clause to be placed in an [AtomicResult].
|
|
#[must_use]
|
|
fn atom_cls(self) -> nort::Clause
|
|
where Self: Sized {
|
|
nort::Clause::Atom(Atom(Box::new(self)))
|
|
}
|
|
|
|
/// Shorthand for `self.atom_cls().to_inst()`
|
|
fn atom_clsi(self) -> nort::ClauseInst
|
|
where Self: Sized {
|
|
self.atom_cls().into_inst()
|
|
}
|
|
|
|
/// Wrap the atom in a new expression instance to be placed in a tree
|
|
#[must_use]
|
|
fn atom_expr(self, location: CodeLocation) -> nort::Expr
|
|
where Self: Sized {
|
|
self.atom_clsi().into_expr(location)
|
|
}
|
|
|
|
/// Wrap the atom in a clause to be placed in a
|
|
/// [crate::parse::parsed::SourceLine].
|
|
#[must_use]
|
|
fn ast_cls(self) -> parsed::Clause
|
|
where Self: Sized + Clone {
|
|
parsed::Clause::Atom(AtomGenerator::cloner(self))
|
|
}
|
|
|
|
/// Wrap the atom in an expression to be placed in a
|
|
/// [crate::parse::parsed::SourceLine].
|
|
#[must_use]
|
|
fn ast_exp<N: NameLike>(self, range: SourceRange) -> parsed::Expr
|
|
where Self: Sized + Clone {
|
|
self.ast_cls().into_expr(range)
|
|
}
|
|
|
|
/// Wrap this atomic value in a lexeme. This means that the atom will
|
|
/// participate in macro reproject, so it must implement [Atomic::parser_eq].
|
|
fn lexeme(self) -> Lexeme
|
|
where Self: Sized + Clone {
|
|
Lexeme::Atom(AtomGenerator::cloner(self))
|
|
}
|
|
}
|
|
|
|
/// A struct for generating any number of [Atom]s. Since atoms aren't Clone,
|
|
/// this represents the ability to create any number of instances of an atom
|
|
#[derive(Clone)]
|
|
pub struct AtomGenerator(Arc<dyn Fn() -> Atom + Send + Sync>);
|
|
impl AtomGenerator {
|
|
/// Use a factory function to create any number of atoms
|
|
pub fn new(f: impl Fn() -> Atom + Send + Sync + 'static) -> Self { Self(Arc::new(f)) }
|
|
/// Clone a representative atom when called
|
|
pub fn cloner(atom: impl Atomic + Clone) -> Self {
|
|
let lock = Mutex::new(atom);
|
|
Self::new(move || Atom::new(lock.lock().unwrap().clone()))
|
|
}
|
|
/// Generate an atom
|
|
pub fn run(&self) -> Atom { self.0() }
|
|
}
|
|
impl fmt::Debug for AtomGenerator {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self.run()) }
|
|
}
|
|
impl PartialEq for AtomGenerator {
|
|
fn eq(&self, other: &Self) -> bool { self.run().0.parser_eq(&*other.run().0) }
|
|
}
|
|
|
|
/// Represents a black box unit of data with its own normalization steps.
|
|
/// Typically Rust functions integrated with [super::fn_bridge::xfn] will
|
|
/// produce and consume [Atom]s to represent both raw data, pending
|
|
/// computational tasks, and curried partial calls awaiting their next argument.
|
|
pub struct Atom(pub Box<dyn Atomic>);
|
|
impl Atom {
|
|
/// Wrap an [Atomic] in a type-erased box
|
|
#[must_use]
|
|
pub fn new<T: 'static + Atomic>(data: T) -> Self { Self(Box::new(data) as Box<dyn Atomic>) }
|
|
/// Get the contained data
|
|
#[must_use]
|
|
pub fn data(&self) -> &dyn Atomic { self.0.as_ref() as &dyn Atomic }
|
|
/// Test the type of the contained data without downcasting
|
|
#[must_use]
|
|
pub fn is<T: Atomic>(&self) -> bool { self.data().as_any_ref().is::<T>() }
|
|
/// Downcast contained data, panic if it isn't the specified type
|
|
#[must_use]
|
|
pub fn downcast<T: Atomic>(self) -> T {
|
|
*self.0.as_any().downcast().expect("Type mismatch on Atom::cast")
|
|
}
|
|
/// Normalize the contained data
|
|
pub fn run(self, run: RunData<'_, '_>) -> AtomicResult { self.0.run(run) }
|
|
/// Request a delegate from the encapsulated data
|
|
pub fn request<T: 'static>(&self) -> Option<T> { request(self.0.as_ref()) }
|
|
/// Downcast the atom to a concrete atomic type, or return the original atom
|
|
/// if it is not the specified type
|
|
pub fn try_downcast<T: Atomic>(self) -> Result<T, Self> {
|
|
match self.0.as_any_ref().is::<T>() {
|
|
true => Ok(*self.0.as_any().downcast().expect("checked just above")),
|
|
false => Err(self),
|
|
}
|
|
}
|
|
/// Downcast an atom by reference
|
|
pub fn downcast_ref<T: Atomic>(&self) -> Option<&T> { self.0.as_any_ref().downcast_ref() }
|
|
/// Combine the function with an argument to produce a new clause
|
|
pub fn apply(self, call: CallData) -> RTResult<nort::Clause> { self.0.apply(call) }
|
|
/// Combine the function with an argument to produce a new clause
|
|
pub fn apply_mut(&mut self, call: CallData) -> RTResult<nort::Clause> { self.0.apply_mut(call) }
|
|
}
|
|
|
|
impl fmt::Debug for Atom {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self.data()) }
|
|
}
|
|
|
|
impl Responder for Never {
|
|
fn respond(&self, _request: Request) { match *self {} }
|
|
}
|
|
impl Atomic for Never {
|
|
fn as_any(self: Box<Self>) -> Box<dyn Any> { match *self {} }
|
|
fn as_any_ref(&self) -> &dyn Any { match *self {} }
|
|
fn type_name(&self) -> &'static str { match *self {} }
|
|
fn redirect(&mut self) -> Option<&mut nort::Expr> { match *self {} }
|
|
fn run(self: Box<Self>, _: RunData) -> AtomicResult { match *self {} }
|
|
fn apply_mut(&mut self, _: CallData) -> RTResult<nort::Clause> { match *self {} }
|
|
}
|