cl-interpret: Builtin refactor

- Everything is different now
  - Builtins are now built on top of Rust functions, so they can be recursive!
  - TODO: allow macro-defined builtins to call each other?
- The builtins! macro is a lot nicer to work with
  - No redundant return value
  - Maps the result over Into::into, allowing for type inference!
  - Uses explicit pattern syntax instead of weird binding, where possible
  - Does not #[allow(unused)], so you'll get unused variable warnings now!
This commit is contained in:
John 2025-01-31 03:34:45 -06:00
parent 0c2b0002ce
commit d95d35268e
6 changed files with 230 additions and 211 deletions

View File

@ -1,124 +1,200 @@
//! Implementations of built-in functions
#![allow(non_upper_case_globals)]
use super::{
use crate::{
convalue::ConValue,
env::Environment,
error::{Error, IResult},
BuiltIn, Callable,
};
use cl_ast::Sym;
use std::{
io::{stdout, Write},
rc::Rc,
slice,
};
builtins! {
const MISC;
/// A function built into the interpreter.
#[derive(Clone, Copy)]
pub struct Builtin {
/// An identifier to be used during registration
name: &'static str,
/// The signature, displayed when the builtin is printed
desc: &'static str,
/// The function to be run when called
func: &'static dyn Fn(&mut Environment, &[ConValue]) -> IResult<ConValue>,
}
impl Builtin {
/// Constructs a new Builtin
pub const fn new(
name: &'static str,
desc: &'static str,
func: &'static impl Fn(&mut Environment, &[ConValue]) -> IResult<ConValue>,
) -> Builtin {
Builtin { name, desc, func }
}
pub const fn description(&self) -> &'static str {
self.desc
}
}
impl std::fmt::Debug for Builtin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Builtin")
.field("description", &self.desc)
.finish_non_exhaustive()
}
}
impl super::Callable for Builtin {
fn call(&self, interpreter: &mut Environment, args: &[ConValue]) -> IResult<ConValue> {
(self.func)(interpreter, args)
}
fn name(&self) -> cl_ast::Sym {
self.name.into()
}
}
/// Turns a function definition into a [Builtin].
///
/// ```rust
/// # use cl_interpret::{builtin2::builtin, convalue::ConValue};
/// let my_builtin = builtin! {
/// /// Use the `@env` suffix to bind the environment!
/// /// (needed for recursive calls)
/// fn my_builtin(ConValue::Bool(b), rest @ ..) @env {
/// // This is all Rust code!
/// eprintln!("my_builtin({b}, ..)");
/// match rest {
/// [] => Ok(ConValue::Empty),
/// _ => my_builtin(env, rest), // Can be called as a normal function!
/// }
/// }
/// };
/// ```
pub macro builtin(
$(#[$($meta:tt)*])*
fn $name:ident ($($arg:pat),*$(,)?) $(@$env:tt)? $body:block
) {{
$(#[$($meta)*])*
fn $name(_env: &mut Environment, _args: &[ConValue]) -> IResult<ConValue> {
// Set up the builtin! environment
$(let $env = _env;)?
// Allow for single argument `fn foo(args @ ..)` pattern
#[allow(clippy::redundant_at_rest_pattern, irrefutable_let_patterns)]
let [$($arg),*] = _args else {
Err($crate::error::Error::TypeError)?
};
$body.map(Into::into)
}
Builtin {
name: stringify!($name),
desc: stringify![builtin fn $name($($arg),*)],
func: &$name,
}
}}
/// Constructs an array of [Builtin]s from pseudo-function definitions
pub macro builtins($(
$(#[$($meta:tt)*])*
fn $name:ident ($($args:tt)*) $(@$env:tt)? $body:block
)*) {
[$(builtin!($(#[$($meta)*])* fn $name ($($args)*) $(@$env)? $body)),*]
}
/// Creates an [Error::BuiltinDebug] using interpolation of runtime expressions.
/// See [std::format].
pub macro error_format ($($t:tt)*) {
$crate::error::Error::BuiltinDebug(format!($($t)*))
}
pub const Builtins: &[Builtin] = &builtins![
/// Unstable variadic format function
pub fn format<_, args> () -> IResult<ConValue> {
fn fmt(args @ ..) {
use std::fmt::Write;
let mut out = String::new();
for arg in args {
write!(out, "{arg}").ok();
if let Err(e) = args.iter().try_for_each(|arg| write!(out, "{arg}")) {
eprintln!("{e}");
}
Ok(ConValue::String(out.into()))
Ok(out)
}
/// Unstable variadic print function
pub fn print<_, args> () -> IResult<ConValue> {
/// Prints the arguments in-order, with no separators
fn print(args @ ..) {
let mut out = stdout().lock();
for arg in args {
write!(out, "{arg}").ok();
}
Ok(ConValue::Empty)
args.iter().try_for_each(|arg| write!(out, "{arg}") ).ok();
Ok(())
}
/// Unstable variadic println function
pub fn println<_, args> () -> IResult<ConValue> {
/// Prints the arguments in-order, followed by a newline
fn println(args @ ..) {
let mut out = stdout().lock();
for arg in args {
write!(out, "{arg}").ok();
}
args.iter().try_for_each(|arg| write!(out, "{arg}") ).ok();
writeln!(out).ok();
Ok(ConValue::Empty)
Ok(())
}
/// Prints the [Debug](std::fmt::Debug) version of the input values,
/// and passes them back as a tuple.
pub fn dbg<_, args> () -> IResult<ConValue> {
/// Debug-prints the argument, returning a copy
fn dbg(arg) {
println!("{arg:?}");
Ok(arg.clone())
}
/// Debug-prints the argument
fn dbgp(args @ ..) {
let mut out = stdout().lock();
for arg in args {
writeln!(out, "{arg:?}").ok();
}
Ok(args.into())
args.iter().try_for_each(|arg| writeln!(out, "{arg:#?}") ).ok();
Ok(())
}
/// Prints the pretty [Debug](std::fmt::Debug) version of the input values.
pub fn dbgp<_, args> () -> IResult<ConValue> {
let mut out = stdout().lock();
for arg in args {
writeln!(out, "{arg:#?}").ok();
}
Ok(ConValue::Empty)
/// Dumps the environment
fn dump() @env {
println!("{env}");
Ok(())
}
/// Dumps info from the environment
pub fn dump<env, _>() -> IResult<ConValue> {
println!("{}", *env);
Ok(ConValue::Empty)
fn builtins() @env {
for builtin in env.builtins().values().flatten() {
println!("{builtin}");
}
Ok(())
}
/// Gets the length of a container or range
pub fn len<env, _>(list) -> IResult<ConValue> {
Ok(ConValue::Int(match list {
/// Returns the length of the input list as a [ConValue::Int]
fn len(list) @env {
Ok(match list {
ConValue::Empty => 0,
ConValue::String(s) => s.chars().count() as _,
ConValue::Ref(r) => return len.call(env, slice::from_ref(r.as_ref())),
ConValue::Ref(r) => return len(env, slice::from_ref(r.as_ref())),
ConValue::Array(t) => t.len() as _,
ConValue::Tuple(t) => t.len() as _,
ConValue::RangeExc(start, end) => (end - start) as _,
ConValue::RangeInc(start, end) => (end - start + 1) as _,
_ => Err(Error::TypeError)?,
}))
})
}
/// Gets a line of text from stdin
pub fn get_line() -> IResult<ConValue> {
/// Gets a line of input from stdin
fn get_line() {
let mut line = String::new();
let _ = std::io::stdin().read_line(&mut line);
Ok(ConValue::String(line.into()))
Ok(line)
}
/// Lists the potential "upvars" (lifted environment) of a function
pub fn collect_upvars<env, _>(ConValue::Function(f)) -> IResult<ConValue> {
use crate::function::collect_upvars::collect_upvars;
for (name, bind) in collect_upvars(f.decl(), env) {
match bind {
Some(bind) =>println!("{name}: {bind}"),
None => println!("{name}: _"),
}
}
Ok(ConValue::Empty)
/// Returns a shark
fn shark() {
Ok('\u{1f988}')
}
/// Lists the collected environment of a function.
pub fn upvars(ConValue::Function(f)) -> IResult<ConValue> {
let uv = f.upvars();
for (name, bind) in uv.iter() {
match bind {
Some(bind) =>println!("{name}: {bind}"),
None => println!("{name}: _"),
/// Clears the screen
fn clear() {
println!("\x1b[G");
Ok(())
}
}
Ok(ConValue::Empty)
}
}
];
builtins! {
const BINARY;
pub const Math: &[Builtin] = &builtins![
/// Multiplication `a * b`
pub fn mul(lhs, rhs) -> IResult<ConValue> {
fn mul(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a * b),
@ -127,7 +203,7 @@ builtins! {
}
/// Division `a / b`
pub fn div(lhs, rhs) -> IResult<ConValue> {
fn div(lhs, rhs) {
Ok(match (lhs, rhs){
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a / b),
@ -136,7 +212,7 @@ builtins! {
}
/// Remainder `a % b`
pub fn rem(lhs, rhs) -> IResult<ConValue> {
fn rem(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a % b),
@ -145,7 +221,7 @@ builtins! {
}
/// Addition `a + b`
pub fn add(lhs, rhs) -> IResult<ConValue> {
fn add(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a + b),
@ -155,7 +231,7 @@ builtins! {
}
/// Subtraction `a - b`
pub fn sub(lhs, rhs) -> IResult<ConValue> {
fn sub(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a - b),
@ -164,7 +240,7 @@ builtins! {
}
/// Shift Left `a << b`
pub fn shl(lhs, rhs) -> IResult<ConValue> {
fn shl(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a << b),
@ -173,7 +249,7 @@ builtins! {
}
/// Shift Right `a >> b`
pub fn shr(lhs, rhs) -> IResult<ConValue> {
fn shr(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a >> b),
@ -182,7 +258,7 @@ builtins! {
}
/// Bitwise And `a & b`
pub fn and(lhs, rhs) -> IResult<ConValue> {
fn and(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a & b),
@ -192,7 +268,7 @@ builtins! {
}
/// Bitwise Or `a | b`
pub fn or(lhs, rhs) -> IResult<ConValue> {
fn or(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a | b),
@ -202,7 +278,7 @@ builtins! {
}
/// Bitwise Exclusive Or `a ^ b`
pub fn xor(lhs, rhs) -> IResult<ConValue> {
fn xor(lhs, rhs) {
Ok(match (lhs, rhs) {
(ConValue::Empty, ConValue::Empty) => ConValue::Empty,
(ConValue::Int(a), ConValue::Int(b)) => ConValue::Int(a ^ b),
@ -211,58 +287,24 @@ builtins! {
})
}
/// Tests whether `a < b`
pub fn lt(lhs, rhs) -> IResult<ConValue> {
cmp!(lhs, rhs, false, <)
}
/// Tests whether `a <= b`
pub fn lt_eq(lhs, rhs) -> IResult<ConValue> {
cmp!(lhs, rhs, true, <=)
}
/// Tests whether `a == b`
pub fn eq(lhs, rhs) -> IResult<ConValue> {
cmp!(lhs, rhs, true, ==)
}
/// Tests whether `a != b`
pub fn neq(lhs, rhs) -> IResult<ConValue> {
cmp!(lhs, rhs, false, !=)
}
/// Tests whether `a <= b`
pub fn gt_eq(lhs, rhs) -> IResult<ConValue> {
cmp!(lhs, rhs, true, >=)
}
/// Tests whether `a < b`
pub fn gt(lhs, rhs) -> IResult<ConValue> {
cmp!(lhs, rhs, false, >)
}
}
builtins! {
const RANGE;
/// Exclusive Range `a..b`
pub fn range_exc(lhs, rhs) -> IResult<ConValue> {
let (&ConValue::Int(lhs), &ConValue::Int(rhs)) = (lhs, rhs) else {
fn range_exc(from, to) {
let (&ConValue::Int(from), &ConValue::Int(to)) = (from, to) else {
Err(Error::TypeError)?
};
Ok(ConValue::RangeExc(lhs, rhs.saturating_sub(1)))
Ok(ConValue::RangeExc(from, to))
}
/// Inclusive Range `a..=b`
pub fn range_inc(lhs, rhs) -> IResult<ConValue> {
let (&ConValue::Int(lhs), &ConValue::Int(rhs)) = (lhs, rhs) else {
fn range_inc(from, to) {
let (&ConValue::Int(from), &ConValue::Int(to)) = (from, to) else {
Err(Error::TypeError)?
};
Ok(ConValue::RangeInc(lhs, rhs))
Ok(ConValue::RangeInc(from, to))
}
}
builtins! {
const UNARY;
/// Negates the ConValue
pub fn neg(tail) -> IResult<ConValue> {
fn neg(tail) {
Ok(match tail {
ConValue::Empty => ConValue::Empty,
ConValue::Int(v) => ConValue::Int(v.wrapping_neg()),
@ -272,7 +314,7 @@ builtins! {
}
/// Inverts the ConValue
pub fn not(tail) -> IResult<ConValue> {
fn not(tail) {
Ok(match tail {
ConValue::Empty => ConValue::Empty,
ConValue::Int(v) => ConValue::Int(!v),
@ -281,64 +323,23 @@ builtins! {
})
}
/// Compares two values
fn cmp(head, tail) {
Ok(ConValue::Int(match (head, tail) {
(ConValue::Int(a), ConValue::Int(b)) => a.cmp(b) as _,
(ConValue::Bool(a), ConValue::Bool(b)) => a.cmp(b) as _,
(ConValue::Char(a), ConValue::Char(b)) => a.cmp(b) as _,
(ConValue::String(a), ConValue::String(b)) => a.cmp(b) as _,
_ => Err(error_format!("Incomparable values: {head}, {tail}"))?
}))
}
/// Does the opposite of `&`
pub fn deref(tail) -> IResult<ConValue> {
fn deref(tail) {
use std::rc::Rc;
Ok(match tail {
ConValue::Ref(v) => Rc::as_ref(v).clone(),
_ => tail.clone(),
})
}
}
/// Turns an argument slice into an array with the (inferred) correct number of elements
pub fn to_args<const N: usize>(args: &[ConValue]) -> IResult<&[ConValue; N]> {
args.try_into()
.map_err(|_| Error::ArgNumber { want: N, got: args.len() })
}
/// Turns function definitions into ZSTs which implement [Callable] and [BuiltIn]
macro builtins (
$(prefix = $prefix:literal)?
const $defaults:ident $( = [$($additional_builtins:expr),*$(,)?])?;
$(
$(#[$meta:meta])*$vis:vis fn $name:ident$(<$env:tt, $args:tt>)? ( $($($arg:pat),+$(,)?)? ) $(-> $rety:ty)?
$body:block
)*
) {
/// Builtins to load when a new interpreter is created
pub const $defaults: &[&dyn BuiltIn] = &[$(&$name,)* $($additional_builtins)*];
$(
$(#[$meta])* #[allow(non_camel_case_types)] #[derive(Clone, Debug)]
/// ```rust,ignore
#[doc = stringify!(builtin! fn $name($($($arg),*)?) $(-> $rety)? $body)]
/// ```
$vis struct $name;
impl BuiltIn for $name {
fn description(&self) -> &str { concat!("builtin ", stringify!($name), stringify!(($($($arg),*)?) )) }
}
impl Callable for $name {
#[allow(unused, irrefutable_let_patterns)]
fn call(&self, env: &mut Environment, args: &[ConValue]) $(-> $rety)? {
$(let $env = env;
let $args = args;)?
$(let [$($arg),*] = to_args(args)? else {
Err(Error::TypeError)?
};)?
$body
}
fn name(&self) -> Sym { stringify!($name).into() }
}
)*
}
/// Templates comparison functions for [ConValue]
macro cmp ($a:expr, $b:expr, $empty:literal, $op:tt) {
match ($a, $b) {
(ConValue::Empty, ConValue::Empty) => Ok(ConValue::Bool($empty)),
(ConValue::Int(a), ConValue::Int(b)) => Ok(ConValue::Bool(a $op b)),
(ConValue::Bool(a), ConValue::Bool(b)) => Ok(ConValue::Bool(a $op b)),
(ConValue::Char(a), ConValue::Char(b)) => Ok(ConValue::Bool(a $op b)),
(ConValue::String(a), ConValue::String(b)) => Ok(ConValue::Bool(&**a $op &**b)),
_ => Err(Error::TypeError)
}
}
];

View File

@ -4,9 +4,9 @@
use cl_ast::{format::FmtAdapter, ExprKind, Sym};
use super::{
builtin::Builtin,
error::{Error, IResult},
function::Function,
BuiltIn, Callable, Environment,
function::Function, Callable, Environment,
};
use std::{collections::HashMap, ops::*, rc::Rc};
@ -47,7 +47,7 @@ pub enum ConValue {
/// A callable thing
Function(Rc<Function>),
/// A built-in function
BuiltIn(&'static dyn BuiltIn),
Builtin(&'static Builtin),
}
impl ConValue {
@ -113,14 +113,14 @@ impl Callable for ConValue {
fn name(&self) -> Sym {
match self {
ConValue::Function(func) => func.name(),
ConValue::BuiltIn(func) => func.name(),
ConValue::Builtin(func) => func.name(),
_ => "".into(),
}
}
fn call(&self, interpreter: &mut Environment, args: &[ConValue]) -> IResult<ConValue> {
match self {
Self::Function(func) => func.call(interpreter, args),
Self::BuiltIn(func) => func.call(interpreter, args),
Self::Builtin(func) => func.call(interpreter, args),
_ => Err(Error::NotCallable(self.clone())),
}
}
@ -170,7 +170,7 @@ from! {
ExprKind => ConValue::Quote,
Function => ConValue::Function,
Vec<ConValue> => ConValue::Tuple,
&'static dyn BuiltIn => ConValue::BuiltIn,
&'static Builtin => ConValue::Builtin,
}
impl From<()> for ConValue {
fn from(_: ()) -> Self {
@ -328,7 +328,7 @@ impl std::fmt::Display for ConValue {
ConValue::Function(func) => {
write!(f, "{}", func.decl())
}
ConValue::BuiltIn(func) => {
ConValue::Builtin(func) => {
write!(f, "{}", func.description())
}
}

View File

@ -1,11 +1,13 @@
//! Lexical and non-lexical scoping for variables
use crate::builtin::Builtin;
use super::{
builtin::{BINARY, MISC, RANGE, UNARY},
builtin::{Builtins, Math},
convalue::ConValue,
error::{Error, IResult},
function::Function,
BuiltIn, Callable, Interpret,
Callable, Interpret,
};
use cl_ast::{Function as FnDecl, Sym};
use std::{
@ -20,6 +22,7 @@ type StackFrame = HashMap<Sym, Option<ConValue>>;
/// Implements a nested lexical scope
#[derive(Clone, Debug)]
pub struct Environment {
builtin: StackFrame,
global: Vec<(StackFrame, &'static str)>,
frames: Vec<(StackFrame, &'static str)>,
}
@ -49,19 +52,19 @@ impl Display for Environment {
impl Default for Environment {
fn default() -> Self {
Self {
global: vec![
(to_hashmap(RANGE), "range ops"),
(to_hashmap(UNARY), "unary ops"),
(to_hashmap(BINARY), "binary ops"),
(to_hashmap(MISC), "builtins"),
(HashMap::new(), "globals"),
],
builtin: to_hashmap2(Builtins.iter().chain(Math.iter())),
global: vec![(HashMap::new(), "globals")],
frames: vec![],
}
}
}
fn to_hashmap(from: &[&'static dyn BuiltIn]) -> HashMap<Sym, Option<ConValue>> {
from.iter().map(|&v| (v.name(), Some(v.into()))).collect()
// fn to_hashmap(from: &[&'static dyn BuiltIn]) -> HashMap<Sym, Option<ConValue>> {
// from.iter().map(|&v| (v.name(), Some(v.into()))).collect()
// }
fn to_hashmap2(from: impl IntoIterator<Item = &'static Builtin>) -> HashMap<Sym, Option<ConValue>> {
from.into_iter()
.map(|v| (v.name(), Some(v.into())))
.collect()
}
impl Environment {
@ -70,7 +73,19 @@ impl Environment {
}
/// Creates an [Environment] with no [builtins](super::builtin)
pub fn no_builtins() -> Self {
Self { global: vec![(Default::default(), "globals")], frames: vec![] }
Self {
builtin: HashMap::new(),
global: vec![(Default::default(), "globals")],
frames: vec![],
}
}
pub fn builtins(&self) -> &StackFrame {
&self.builtin
}
pub fn add_builtin(&mut self, builtin: &'static Builtin) {
self.builtin.insert(builtin.name(), Some(builtin.into()));
}
pub fn push_frame(&mut self, name: &'static str, frame: StackFrame) {
@ -112,7 +127,7 @@ impl Environment {
return Ok(var);
}
}
Err(Error::NotDefined(id))
self.builtin.get_mut(&id).ok_or(Error::NotDefined(id))
}
/// Resolves a variable immutably.
///
@ -132,7 +147,11 @@ impl Environment {
_ => (),
}
}
Err(Error::NotDefined(id))
self.builtin
.get(&id)
.cloned()
.flatten()
.ok_or(Error::NotDefined(id))
}
pub(crate) fn get_local(&self, id: Sym) -> IResult<ConValue> {

View File

@ -44,6 +44,8 @@ pub enum Error {
PatFailed(Pattern),
/// Fell through a non-exhaustive match
MatchNonexhaustive,
/// Error produced by a Builtin
BuiltinDebug(String),
}
impl std::error::Error for Error {}
@ -89,6 +91,7 @@ impl std::fmt::Display for Error {
Error::MatchNonexhaustive => {
write!(f, "Fell through a non-exhaustive match expression!")
}
Error::BuiltinDebug(s) => write!(f, "DEBUG: {s}"),
}
}
}

View File

@ -345,7 +345,8 @@ mod assignment {
pub(super) fn pat_assign(env: &mut Environment, pat: &Pattern, value: ConValue) -> IResult<()> {
let mut substitution = HashMap::new();
append_sub(&mut substitution, pat, value).map_err(|_| Error::PatFailed(pat.clone()))?;
append_sub(&mut substitution, pat, value)
.map_err(|_| Error::PatFailed(pat.clone().into()))?;
for (path, value) in substitution {
assign_path(env, path, value)?;
}

View File

@ -17,11 +17,6 @@ pub trait Callable: std::fmt::Debug {
fn name(&self) -> Sym;
}
/// [BuiltIn]s are [Callable]s with bespoke definitions
pub trait BuiltIn: std::fmt::Debug + Callable {
fn description(&self) -> &str;
}
pub mod convalue;
pub mod interpret;