From 69369664556cccbd7874acef34b63b7b3a3309b8 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Fri, 22 May 2026 10:10:57 +0300 Subject: [PATCH] Proper quasiquote expansion rules, unquote-splice --- examples/arguments.lysp | 94 ++++++++++++++++++ examples/macros.lysp | 45 +++++++++ src/compile/block.rs | 13 ++- src/compile/codegen/function.rs | 10 +- src/compile/syntax/error.rs | 10 +- src/compile/syntax/mod.rs | 1 + src/error.rs | 45 ++++++--- src/main.rs | 5 +- src/parse.rs | 7 ++ src/prelude.lysp | 8 ++ src/read.rs | 9 +- src/vm/machine.rs | 167 ++++++++++++++++++++++++-------- src/vm/macros.rs | 54 ++++++----- src/vm/prelude/collections.rs | 27 +++++- src/vm/prelude/debug.rs | 3 +- src/vm/prelude/eval.rs | 6 +- src/vm/prelude/mod.rs | 10 +- src/vm/value/function.rs | 23 ++++- src/vm/value/mod.rs | 49 ++++++---- src/vm/value/string.rs | 2 +- tests/integration.rs | 84 +++++++++------- 21 files changed, 516 insertions(+), 156 deletions(-) create mode 100644 examples/arguments.lysp create mode 100644 examples/macros.lysp create mode 100644 src/prelude.lysp diff --git a/examples/arguments.lysp b/examples/arguments.lysp new file mode 100644 index 0000000..06fa69c --- /dev/null +++ b/examples/arguments.lysp @@ -0,0 +1,94 @@ +(defun err? (v) (= 'err (car v))) + +(defun no-arguments () 1234) +(defun only-required (a b c) (+ a b c)) +(defun only-optional (&optional a b c) (list a b c)) +(defun only-rest (&rest r) (length r)) +(defun required-and-optional (a b &optional c d) (list a b c d)) +(defun required-and-rest (a b &rest r) (list a b r)) +(defun optional-and-rest (&optional a b &rest r) (list a b r)) +(defun required-and-optional-and-rest (a b &optional c d &rest r) (list a b c d r)) + +; no-arguments +(assert (= '(ok 1234) (eval '(no-arguments)))) +(assert (err? (eval '(no-arguments 1)))) +; only-required +(assert (= '(ok 6) (eval '(only-required 1 2 3)))) +(assert (err? (eval '(only-required 1 2)))) +; only-optional +(assert + (= + '(ok (nil nil nil)) + (eval '(only-optional)) + ) + ) +(assert + (= + '(ok (1 nil nil)) + (eval '(only-optional 1)) + ) + ) +(assert + (= + '(ok (1 2 nil)) + (eval '(only-optional 1 2)) + ) + ) +(assert + (= + '(ok (1 2 3)) + (eval '(only-optional 1 2 3)) + ) + ) +(assert (err? (eval '(only-optional 1 2 3 4)))) +; only-rest +(assert (= 0 (only-rest))) +(assert (= 1 (only-rest 1))) +(assert (= 2 (only-rest 1 2))) +(assert (= 3 (only-rest 1 2 3))) + +; required-and-optional +(assert (err? (eval '(required-and-optional)))) +(assert (err? (eval '(required-and-optional 1)))) +(assert + (= + '(1 2 nil nil) + (required-and-optional 1 2) + ) + ) +(assert + (= + '(1 2 3 nil) + (required-and-optional 1 2 3) + ) + ) +(assert + (= + '(1 2 3 4) + (required-and-optional 1 2 3 4) + ) + ) +(assert (err? (eval '(required-and-optional 1 2 3 4 5)))) + +; required-and-rest +(assert (err? (eval '(required-and-rest)))) +(assert (err? (eval '(required-and-rest 1)))) +(assert (= '(1 2 nil) (required-and-rest 1 2))) +(assert (= '(1 2 (3)) (required-and-rest 1 2 3))) +(assert (= '(1 2 (3 4 5)) (required-and-rest 1 2 3 4 5))) + +; optional-and-rest +(assert (= '(nil nil nil) (optional-and-rest))) +(assert (= '(1 nil nil) (optional-and-rest 1))) +(assert (= '(1 2 nil) (optional-and-rest 1 2))) +(assert (= '(1 2 (3)) (optional-and-rest 1 2 3))) +(assert (= '(1 2 (3 4 5)) (optional-and-rest 1 2 3 4 5))) + +; required-and-optional-and-rest +(assert (err? (eval '(required-and-optional-and-rest)))) +(assert (err? (eval '(required-and-optional-and-rest 1)))) +(assert (= '(1 2 nil nil nil) (required-and-optional-and-rest 1 2))) +(assert (= '(1 2 3 nil nil) (required-and-optional-and-rest 1 2 3))) +(assert (= '(1 2 3 4 nil) (required-and-optional-and-rest 1 2 3 4))) +(assert (= '(1 2 3 4 (5)) (required-and-optional-and-rest 1 2 3 4 5))) +(assert (= '(1 2 3 4 (5 6 7 8)) (required-and-optional-and-rest 1 2 3 4 5 6 7 8))) diff --git a/examples/macros.lysp b/examples/macros.lysp new file mode 100644 index 0000000..2419730 --- /dev/null +++ b/examples/macros.lysp @@ -0,0 +1,45 @@ +; quoting rules + +(setq glob0 123) +(setq glob1 '(2 3 4)) + +(assert (= (list 1 2 3) '(1 2 3) `(1 2 3))) +(assert (= '(1 glob0 3) `(1 glob0 3))) +(assert (= '(1 123 3) `(1 ,glob0 3))) +(assert (= '(1 glob1 5) `(1 glob1 5))) +(assert (= '(1 (2 3 4) 5) `(1 ,glob1 5))) +(assert (= '(1 2 3 4 5) `(1 ,@glob1 5))) +(assert (= '(2 3 4 5) `(,@glob1 5))) +(assert (= '(1 2 3 4) `(1 ,@glob1))) +(assert (= '(2 3 4) `(,@glob1))) +(assert (= '((2 3 4)) `(,glob1))) + +; Nested +(assert (= '((123 123) (123 123)) `((,glob0 ,glob0) (,glob0 ,glob0)))) +(assert (= '(2 3 4 2 3 4) `(,@glob1 ,@glob1))) +(assert (= '((((2 3 4)))) `(((,glob1))))) + +(defmacro debug (expression) + (print "DEBUG:" expression) + expression + ) + +; those are prelude, but defined in lysp itself: + +(debug + (when #t + (print "a") + (print "b") + ) + ) + +(when 1 + (print "a") + (print "b") + ) +(unless nil + (print "c") + (print "d") + ) + + diff --git a/src/compile/block.rs b/src/compile/block.rs index 14a5bca..4761aef 100644 --- a/src/compile/block.rs +++ b/src/compile/block.rs @@ -382,6 +382,11 @@ impl FunctionBlock { .add_local(optional.clone(), Some(-100)) .expect("couldn't add an argument"); } + if let Some(rest) = signature.rest_argument.as_ref() { + block + .add_local(rest.clone(), Some(-100)) + .expect("couldn't add an argument"); + } block } @@ -489,17 +494,15 @@ impl FunctionBlock { instructions[position] = branch_offset as u8; } - let min_arity = self.signature.min_arity(); - let max_arity = self.signature.max_arity(); - Ok(Rc::new(BytecodeFunction { identifier: self.identifier.clone(), instructions: instructions.into(), docstring: self.docstring.clone(), constants: self.constants.iter().cloned().collect(), upvalues: self.upvalues.iter().copied().collect(), - min_arity, - max_arity, + required_count: self.signature.required_arguments.len(), + optional_count: self.signature.optional_arguments.len(), + has_rest: self.signature.rest_argument.is_some(), })) } diff --git a/src/compile/codegen/function.rs b/src/compile/codegen/function.rs index 7bc9054..4f99093 100644 --- a/src/compile/codegen/function.rs +++ b/src/compile/codegen/function.rs @@ -173,8 +173,9 @@ mod tests { Instruction::Return.into(), ] ); - assert_eq!(lambda_function.min_arity, 1); - assert_eq!(lambda_function.max_arity, 1); + assert_eq!(lambda_function.required_count, 1); + assert_eq!(lambda_function.optional_count, 0); + assert!(!lambda_function.has_rest); assert_eq!( lambda_function.upvalues.as_ref(), &[UpvalueDef { @@ -283,8 +284,9 @@ mod tests { ); // inner function assert!(defun_function.constants.is_empty()); - assert_eq!(defun_function.min_arity, 1); - assert_eq!(defun_function.max_arity, 1); + assert_eq!(defun_function.required_count, 1); + assert_eq!(defun_function.optional_count, 0); + assert!(!defun_function.has_rest); assert_eq!( defun_function.instructions.as_ref(), &[ diff --git a/src/compile/syntax/error.rs b/src/compile/syntax/error.rs index a31692f..26d7a0a 100644 --- a/src/compile/syntax/error.rs +++ b/src/compile/syntax/error.rs @@ -93,13 +93,19 @@ impl ParseErrorKind { } } - pub fn collect_extraneous>(iter: I) -> Self { + pub fn collect_extraneous>(iter: I) -> Self + where + I::IntoIter: ExactSizeIterator + DoubleEndedIterator, + { Self::extraneous(&Value::list_or_nil(iter)) } pub fn try_collect_extraneous>>( iter: I, - ) -> Result { + ) -> Result + where + I::IntoIter: ExactSizeIterator + DoubleEndedIterator, + { Ok(Self::extraneous(&Value::try_list_or_nil(iter)?)) } } diff --git a/src/compile/syntax/mod.rs b/src/compile/syntax/mod.rs index 000362a..c3ef28f 100644 --- a/src/compile/syntax/mod.rs +++ b/src/compile/syntax/mod.rs @@ -69,6 +69,7 @@ impl Expression { Value::String(value) => Rc::new(Self::StringLiteral(value.clone())), Value::Quasi(_value) => todo!("{value}"), Value::Unquote(_value) => todo!("Unquote {_value}"), + Value::UnquoteSplice(_value) => todo!("UnquoteSplice {_value}"), Value::Quote(value) => Rc::new(Self::Quote(value.clone())), Value::Nil => Rc::new(Self::Nil), diff --git a/src/error.rs b/src/error.rs index 0bb3b82..828c071 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::{fmt, io, ops::RangeInclusive, rc::Rc}; +use std::{fmt, io, rc::Rc}; use crate::{ compile::CompileError, @@ -40,7 +40,6 @@ pub enum ReadError { #[derive(Debug, PartialEq)] pub struct ArgumentCountError { pub function: Rc, - pub expected_range: RangeInclusive, pub actual: usize, } @@ -143,11 +142,7 @@ impl PartialEq for ReadError { impl fmt::Display for ArgumentCountError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let min_arity = *self.expected_range.start(); - let max_arity = *self.expected_range.end(); - let one_argument = min_arity == max_arity; - let too_few = self.actual < min_arity; - + let too_few = self.actual < self.function.required_count; write!( f, "too {} arguments for function {}: expected ", @@ -155,15 +150,35 @@ impl fmt::Display for ArgumentCountError { self.function )?; - if one_argument { - write!(f, "{min_arity}")?; - write!(f, " argument")?; - if min_arity != 1 { - write!(f, "s")?; - } + if self.function.has_rest { + let singular = self.function.required_count == 1; + + write!( + f, + "at least {} argument{}", + self.function.required_count, + if singular { "" } else { "s" } + )?; + } else if self.function.optional_count > 0 { + write!( + f, + "{}-{} arguments", + self.function.required_count, + self.function.required_count + self.function.optional_count + )?; } else { - write!(f, "{min_arity}-{max_arity} arguments")?; + let singular = self.function.required_count == 1; + + write!( + f, + "{} argument{}", + self.function.required_count, + if singular { "" } else { "s" } + )?; } - write!(f, ", got {}", self.actual) + + write!(f, ", got {}", self.actual)?; + + Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 336d486..8e8c388 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,7 @@ pub enum Trace { Call, Return, Stack, + Macro, } impl FromStr for Trace { @@ -49,6 +50,7 @@ impl FromStr for Trace { "call" => Ok(Self::Call), "return" => Ok(Self::Return), "stack" => Ok(Self::Stack), + "macro" => Ok(Self::Macro), _ => Err(format!("Unknown trace flag: {s:?}")), } } @@ -178,7 +180,7 @@ fn run_module>( let path = path.as_ref(); let name = format!("{}", path.display()); let reader = BufReader::new(File::open(path)?); - let module_reader = ModuleReader::new(reader); + let module_reader = ModuleReader::new(reader, vm.trace_macros); let function = match module_reader.compile(Some(name.into()), compile_options, env) { Ok(function) => function, Err(error) => return Err(handle_module_error(error)), @@ -203,6 +205,7 @@ fn main() -> ExitCode { vm.trace_calls = args.trace.contains(&Trace::Call); vm.trace_returns = args.trace.contains(&Trace::Return); vm.trace_stack = args.trace.contains(&Trace::Stack); + vm.trace_macros = args.trace.contains(&Trace::Macro); let env = Rc::new(Environment::default()); prelude::load(&env); let mut arguments = vec![]; diff --git a/src/parse.rs b/src/parse.rs index 4995dae..dfb170e 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -305,6 +305,12 @@ fn parse_unquote(input: &str) -> IResult<&str, Value> { .map(Value::Unquote) .parse(input) } +fn parse_unquote_splice(input: &str) -> IResult<&str, Value> { + preceded(tag(",@"), parse_value) + .map(Rc::new) + .map(Value::UnquoteSplice) + .parse(input) +} fn parse_quote(input: &str) -> IResult<&str, Value> { preceded(char('\''), parse_value) @@ -330,6 +336,7 @@ pub fn parse_value(input: &str) -> IResult<&str, Value> { parse_list_or_nil, parse_vector, parse_boolean, + parse_unquote_splice, parse_quote, parse_quasi, parse_unquote, diff --git a/src/prelude.lysp b/src/prelude.lysp new file mode 100644 index 0000000..7ac8488 --- /dev/null +++ b/src/prelude.lysp @@ -0,0 +1,8 @@ +(defmacro when (condition &rest body) + `(if ,condition (progn ,@body)) + ) + +(defmacro unless (condition &rest body) + `(if (not ,condition) (progn ,@body)) + ) + diff --git a/src/read.rs b/src/read.rs index fd337ec..903f680 100644 --- a/src/read.rs +++ b/src/read.rs @@ -16,7 +16,6 @@ use crate::{ env::Environment, instruction::Instruction, machine::Machine, - macros::MacroExpand, value::{BytecodeFunction, IdentifierValue, Value}, }, }; @@ -94,10 +93,12 @@ impl Reader for FileReader { } impl ModuleReader { - pub fn new(reader: R) -> Self { + pub fn new(reader: R, trace_macros: bool) -> Self { + let mut macro_machine = Machine::default(); + macro_machine.trace_macros = trace_macros; Self { reader: FileReader::new(reader), - macro_machine: Machine::default(), + macro_machine, } } @@ -255,6 +256,6 @@ pub fn read( let Some(raw_value) = raw_value else { return Ok(None); }; - let exp_value = raw_value.macro_expand(vm, env, false)?; + let exp_value = vm.macro_expand(env, &raw_value)?; Ok(Some(exp_value)) } diff --git a/src/vm/machine.rs b/src/vm/machine.rs index f3120ea..1e4abec 100644 --- a/src/vm/machine.rs +++ b/src/vm/machine.rs @@ -1,4 +1,7 @@ -use std::rc::Rc; +use std::{ + io::{self, BufReader, Read}, + rc::Rc, +}; use crate::{ compile::{CompileContext, CompileOptions}, @@ -6,6 +9,7 @@ use crate::{ ArgumentCountError, MachineError, MachineErrorAt, MachineErrorLocation, ValueConversionError, }, + read::{self, FileReader}, vm::{ Value, env::Environment, @@ -15,7 +19,7 @@ use crate::{ macros::MacroExpand, prelude, stack::Stack, - value::{ClosureValue, IdentifierValue, NumberValue, UpvalueValue}, + value::{BytecodeFunction, ClosureValue, IdentifierValue, NumberValue, UpvalueValue}, }, }; @@ -36,6 +40,7 @@ pub struct Machine { pub trace_returns: bool, pub trace_stack: bool, pub trace_calls: bool, + pub trace_macros: bool, } impl Default for Machine { @@ -50,6 +55,7 @@ impl Default for Machine { trace_stack: false, trace_returns: false, trace_instructions: false, + trace_macros: false, } } } @@ -186,6 +192,48 @@ impl Machine { Ok(()) } + fn collect_rest_argument(&mut self, count: usize) -> Result { + let mut rest = Value::Nil; + for _ in 0..count { + let value = self.pop()?; + rest = value.cons(rest); + } + Ok(rest) + } + + fn collect_call_arguments( + &mut self, + function: &Rc, + argument_count: usize, + ) -> Result<(), MachineError> { + if !(function.min_arity()..=function.max_arity()).contains(&argument_count) { + return Err(MachineError::ArgumentCount(ArgumentCountError { + function: function.clone(), + actual: argument_count, + })); + } + + // Fill out missing optionals + let mut remaining = argument_count - function.required_count; + if remaining > function.optional_count { + // Collect into &rest X + remaining -= function.optional_count; + } else { + // Pad missing optionals + for _ in remaining..function.optional_count { + self.push(Value::Nil)?; + } + remaining = 0; + } + + if function.has_rest { + let rest = self.collect_rest_argument(remaining)?; + self.push(rest)?; + } + + Ok(()) + } + fn execute_call( &mut self, env: &Rc, @@ -246,22 +294,7 @@ impl Machine { } }; - if !(closure.function.min_arity..=closure.function.max_arity).contains(&argument_count) { - return Err(MachineError::ArgumentCount(ArgumentCountError { - function: closure.function.clone(), - expected_range: closure.function.min_arity..=closure.function.max_arity, - actual: argument_count, - })); - } - if closure.function.max_arity == usize::MAX { - todo!("VM support for &rest argument") - } - for _ in argument_count..closure.function.max_arity { - self.push(Value::Nil)?; - } - // if argument_count != closure.function.arity { - // todo!("TODO error here") - // } + self.collect_call_arguments(&closure.function, argument_count)?; if self.trace_calls { eprintln!("TRACE: Call closure"); @@ -599,26 +632,11 @@ impl Machine { closure: ClosureValue, args: &[Value], ) -> Result { - let max_arity = closure.function.max_arity; - if !(closure.function.min_arity..=closure.function.max_arity).contains(&args.len()) { - return Err(MachineError::ArgumentCount(ArgumentCountError { - function: closure.function.clone(), - expected_range: closure.function.min_arity..=closure.function.max_arity, - actual: args.len(), - }) - .at_unknown()); - } self.push(Value::Closure(closure)) .map_err(MachineErrorAt::at_unknown)?; - if max_arity == usize::MAX { - todo!("VM support for &rest argument") - } - for arg in args.iter() { + for arg in args { self.push(arg.clone()).map_err(MachineErrorAt::at_unknown)?; } - for _ in args.len()..max_arity { - self.push(Value::Nil).map_err(MachineErrorAt::at_unknown)?; - } let unwind_target = self.call_stack.pointer(); self.execute_call(env, args.len()) .map_err(MachineErrorAt::at_unknown)?; @@ -633,6 +651,33 @@ impl Machine { self.pop().map_err(MachineErrorAt::at_unknown) } + pub fn macro_expand( + &mut self, + env: &Rc, + value: &Value, + ) -> Result { + match value.macro_expand(self, env, false) { + Ok(result) => { + if self.trace_macros && *value != result { + eprintln!("TRACE: Macro expansion:"); + eprintln!("TRACE: {value}"); + eprintln!("TRACE: VVVVV"); + eprintln!("TRACE: {result}"); + } + Ok(result) + } + Err(error) => { + if self.trace_macros { + eprintln!("TRACE: Macro expansion:"); + eprintln!("TRACE: {value}"); + eprintln!("TRACE: VVVVV"); + eprintln!("TRACE: {error}"); + } + Err(error) + } + } + } + pub fn evaluate_value( &mut self, compile_options: CompileOptions, @@ -640,8 +685,8 @@ impl Machine { env: &Rc, value: Value, ) -> Result { - let value = value.macro_expand(self, env, false)?; - let function = CompileContext::compile_value(compile_options, chunk_name, &value) + let value_expanded = self.macro_expand(env, &value)?; + let function = CompileContext::compile_value(compile_options, chunk_name, &value_expanded) .map_err(MachineError::Compile) .map_err(MachineErrorAt::at_unknown)?; @@ -653,6 +698,41 @@ impl Machine { let value = self.evaluate_closure(env, closure, 0)?; Ok(value) } + + pub fn evaluate_str( + &mut self, + compile_options: CompileOptions, + chunk_name: Option, + env: &Rc, + text: &str, + ) -> Result { + struct SliceReader<'a>(&'a [u8]); + + impl Read for SliceReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let count = self.0.len().min(buf.len()); + buf[..count].copy_from_slice(&self.0[..count]); + self.0 = &self.0[count..]; + Ok(count) + } + } + + let reader = BufReader::new(SliceReader(text.as_bytes())); + let mut reader = FileReader::new(reader); + + let mut last_value = Value::Nil; + loop { + let value = match read::read(&mut reader, self, env)? { + Some(value) => value, + None => break, + }; + + last_value = + self.evaluate_value(compile_options.clone(), chunk_name.clone(), env, value)?; + } + + Ok(last_value) + } } #[cfg(test)] @@ -684,8 +764,9 @@ mod tests { instructions: instructions.into(), constants: constants.into(), upvalues: [].into(), - min_arity: 0, - max_arity: 0, + required_count: 0, + optional_count: 0, + has_rest: false, }), }; let mut machine = Machine::default(); @@ -758,8 +839,9 @@ mod tests { .into(), constants: [].into(), upvalues: [].into(), - min_arity: 1, - max_arity: 1, + required_count: 1, + optional_count: 0, + has_rest: false, }); let (m, r) = eval0( &env, @@ -799,8 +881,9 @@ mod tests { .into(), constants: [].into(), upvalues: [].into(), - min_arity: 2, - max_arity: 2, + required_count: 2, + optional_count: 0, + has_rest: false, }); let (m, r) = eval0( &env, diff --git a/src/vm/macros.rs b/src/vm/macros.rs index 959c353..2457747 100644 --- a/src/vm/macros.rs +++ b/src/vm/macros.rs @@ -5,7 +5,7 @@ use crate::{ vm::{ env::{Environment, Macro}, machine::Machine, - value::{ClosureValue, ConsCell, Keyword, Value}, + value::{ClosureValue, ConsCell, Value}, }, }; @@ -40,6 +40,7 @@ impl MacroExpand for Value { | Self::NativeFunction(_) | Self::NativeValue(_) | Self::Vector(_) + | Self::UnquoteSplice(_) | Self::Unquote(_) => Ok(self.clone()), // | Self::NativeFunction(_) => Ok(self.clone()), Self::Cons(cons) => { @@ -86,28 +87,37 @@ impl MacroExpand for Value { fn expand_quasiquote(value: &Value) -> Value { match value { Value::Nil => Value::Nil, + // Toplevel-only Value::Unquote(inner) => inner.as_ref().clone(), - Value::Cons(cons) => { - // x . y -> (cons ) - let ConsCell(car, cdr) = cons.as_ref(); - let exp_car = expand_quasiquote(car); - let exp_cdr = expand_quasiquote(cdr); - Value::Identifier("cons".into()).cons(exp_car.cons(exp_cdr.cons(Value::Nil))) + Value::UnquoteSplice(inner) => inner.as_ref().clone(), + Value::Cons(_) => { + let mut elements = vec![]; + let mut current = value; + + elements.push(Value::Identifier("append".into())); + + while !current.is_nil() { + let Value::Cons(cons) = current else { todo!() }; + let ConsCell(car, cdr) = cons.as_ref(); + + match car { + Value::UnquoteSplice(splice) => { + elements.push(splice.as_ref().clone()); + } + _ => { + let exp_car = expand_quasiquote(car); + elements.push(Value::list_or_nil([ + Value::Identifier("list".into()), + exp_car, + ])); + } + } + + current = cdr; + } + + Value::list_or_nil(elements) } - Value::Quote(value) => { - // (cons 'quote inner) - let cons = Value::Identifier("cons".into()); - let quote_kw = Value::Quote(Rc::new(Value::Keyword(Keyword::Quote))); - let quote_nil = Value::Quote(Rc::new(Value::Nil)); - - let exp_inner = expand_quasiquote(value); - - let cons_inner = cons - .clone() - .cons(exp_inner.cons(quote_nil.clone().cons(Value::Nil))); - - cons.cons(quote_kw.cons(cons_inner.cons(Value::Nil))) - } - _ => Value::Quote(Rc::new(value.clone())), + _ => value.clone().quote(), } } diff --git a/src/vm/prelude/collections.rs b/src/vm/prelude/collections.rs index 58b4498..04d743a 100644 --- a/src/vm/prelude/collections.rs +++ b/src/vm/prelude/collections.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::{ - error::MachineError, + error::{MachineError, ValueConversionError}, vm::{ Value, env::Environment, @@ -43,6 +43,31 @@ pub fn load(env: &Rc) { // }); // lists + env.defun_native( + "append", + "Concatenates the lists into one list", + |_, _, args| match args { + [] => Ok(Value::Nil), + [xs] => Ok(xs.clone()), + [head @ .., tail] => { + let mut elements = vec![]; + for arg in head { + let iter = arg.proper_iter(ValueConversionError { + expected: "proper list".into(), + got: arg.clone(), + }); + for element in iter { + elements.push(element?.clone()); + } + } + let mut output = tail.clone(); + for element in elements.into_iter().rev() { + output = element.cons(output); + } + Ok(output) + } + }, + ); env.defun_native( "car", "Returns the CAR value of a cons-cell", diff --git a/src/vm/prelude/debug.rs b/src/vm/prelude/debug.rs index 105bb90..250e4ae 100644 --- a/src/vm/prelude/debug.rs +++ b/src/vm/prelude/debug.rs @@ -20,7 +20,8 @@ pub fn load(env: &Rc) { Value::Boolean(_) => "a boolean value".into(), Value::Quasi(_) => "a quasi-quoted value".into(), Value::Quote(_) => "a quoted value".into(), - Value::Unquote(_) => "an un-quoted value".into(), + Value::Unquote(_) => "an unquoted value".into(), + Value::UnquoteSplice(_) => "an unquote-spliced value".into(), Value::Identifier(identifier) => format!("an identifier {:?}", identifier.as_ref()), Value::Vector(_) => "a vector".into(), Value::String(_) => "a string".into(), diff --git a/src/vm/prelude/eval.rs b/src/vm/prelude/eval.rs index 880ffef..81d2ecc 100644 --- a/src/vm/prelude/eval.rs +++ b/src/vm/prelude/eval.rs @@ -84,7 +84,11 @@ pub fn load(env: &Rc) { if i != 0 { print!(" "); } - print!("{arg}"); + if let Value::String(string) = arg { + print!("{}", &**string); + } else { + print!("{arg}"); + } } println!(); Ok(Value::Nil) diff --git a/src/vm/prelude/mod.rs b/src/vm/prelude/mod.rs index 1180279..740eb9b 100644 --- a/src/vm/prelude/mod.rs +++ b/src/vm/prelude/mod.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::vm::env::Environment; +use crate::vm::{env::Environment, machine::Machine}; mod collections; mod convert; @@ -12,7 +12,11 @@ mod math; pub(crate) use math::*; +const PRELUDE_SOURCE: &str = include_str!("../../prelude.lysp"); + pub fn load(env: &Rc) { + let mut vm = Machine::default(); + math::load(env); eval::load(env); functional::load(env); @@ -20,4 +24,8 @@ pub fn load(env: &Rc) { convert::load(env); debug::load(env); io::load(env); + + // Load the lysp part of the prelude + vm.evaluate_str(Default::default(), None, env, PRELUDE_SOURCE) + .expect("Couldn't evaluate prelude lysp part"); } diff --git a/src/vm/value/function.rs b/src/vm/value/function.rs index 1aeef6b..3261029 100644 --- a/src/vm/value/function.rs +++ b/src/vm/value/function.rs @@ -16,8 +16,10 @@ pub struct BytecodeFunction { pub instructions: Box<[u8]>, pub constants: Box<[Value]>, pub upvalues: Box<[UpvalueDef]>, - pub min_arity: usize, - pub max_arity: usize, + + pub required_count: usize, + pub optional_count: usize, + pub has_rest: bool, } enum TraceArgument { @@ -49,6 +51,23 @@ impl BytecodeFunction { } } + pub fn min_arity(&self) -> usize { + self.required_count + } + + pub fn rest_argument_start(&self) -> Option { + self.has_rest + .then_some(self.required_count + self.optional_count) + } + + pub fn max_arity(&self) -> usize { + if self.has_rest { + usize::MAX + } else { + self.required_count + self.optional_count + } + } + fn trace_immediate_integer_at(&self, address: usize) -> Option { let Some(b0) = self.instructions.get(address).copied() else { eprint!(" "); diff --git a/src/vm/value/mod.rs b/src/vm/value/mod.rs index 820174f..961803e 100644 --- a/src/vm/value/mod.rs +++ b/src/vm/value/mod.rs @@ -49,6 +49,7 @@ pub enum Value { Quasi(Rc), Quote(Rc), Unquote(Rc), + UnquoteSplice(Rc), // Semantic Closure(ClosureValue), Function(Rc), @@ -98,6 +99,9 @@ impl Value { Self::Unquote(value) => { Value::list_or_nil([Self::Identifier("unquote".into()), value.type_id()]) } + Self::UnquoteSplice(value) => { + Value::list_or_nil([Self::Identifier("unquote-splice".into()), value.type_id()]) + } Self::Vector(_) => Self::Identifier("vector".into()), Self::Keyword(_) => Self::Identifier("keyword".into()), Self::String(_) => Self::Identifier("string".into()), @@ -121,6 +125,7 @@ impl Value { | Self::Quasi(_) | Self::Quote(_) | Self::Unquote(_) + | Self::UnquoteSplice(_) | Self::Closure(_) | Self::NativeFunction(_) | Self::NativeValue(_) @@ -132,30 +137,28 @@ impl Value { Self::Cons(Rc::new(ConsCell(self, cdr))) } - pub fn try_list_or_nil>>( - items: I, - ) -> Result { - Self::try_list_or_nil_inner(&mut items.into_iter()) - } - - pub fn list_or_nil>(items: I) -> Self { - Self::list_or_nil_inner(&mut items.into_iter()) - } - - fn try_list_or_nil_inner>>( - items: &mut I, - ) -> Result { - match items.next() { - Some(value) => Ok(value?.cons(Self::try_list_or_nil_inner(items)?)), - None => Ok(Self::Nil), + pub fn try_list_or_nil>>(items: I) -> Result + where + I::IntoIter: ExactSizeIterator + DoubleEndedIterator, + { + let iter = items.into_iter(); + let mut result = Value::Nil; + for element in iter.rev() { + result = element?.cons(result); } + Ok(result) } - fn list_or_nil_inner>(items: &mut I) -> Self { - match items.next() { - Some(value) => value.cons(Self::list_or_nil_inner(items)), - None => Self::Nil, + pub fn list_or_nil>(items: I) -> Self + where + I::IntoIter: ExactSizeIterator + DoubleEndedIterator, + { + let iter = items.into_iter(); + let mut result = Value::Nil; + for element in iter.rev() { + result = element.cons(result); } + result } pub fn proper_iter(&self, error: E) -> ProperListIter<'_, E> { @@ -214,7 +217,7 @@ impl fmt::Display for Value { fmt::Display::fmt(vector, f)?; f.write_char(']') } - Self::Keyword(keyword) => write!(f, ":{keyword}"), + Self::Keyword(keyword) => write!(f, "{keyword}"), Self::Identifier(identifier) => write!(f, "{identifier}"), Self::String(value) => fmt::Display::fmt(value, f), Self::Quote(value) => { @@ -229,6 +232,10 @@ impl fmt::Display for Value { f.write_char(',')?; fmt::Display::fmt(value, f) } + Self::UnquoteSplice(value) => { + f.write_str(",@")?; + fmt::Display::fmt(value, f) + } Self::Number(value) => fmt::Display::fmt(value, f), Self::Boolean(value) => fmt::Display::fmt(value, f), Self::Closure(value) => fmt::Display::fmt(value, f), diff --git a/src/vm/value/string.rs b/src/vm/value/string.rs index f702ff1..50761ab 100644 --- a/src/vm/value/string.rs +++ b/src/vm/value/string.rs @@ -25,6 +25,6 @@ impl Deref for StringValue { impl fmt::Display for StringValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(self.0.as_ref(), f) + fmt::Debug::fmt(self.0.as_ref(), f) } } diff --git a/tests/integration.rs b/tests/integration.rs index 2dc5e86..eeffa46 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,57 +1,56 @@ -use std::{ - io::{self, BufReader, Read}, - rc::Rc, -}; +use std::{fs, path::Path, rc::Rc}; use lysp::{ error::MachineErrorAt, - read::{FileReader, read}, vm::{env::Environment, machine::Machine, prelude, value::Value}, }; -struct SliceReader<'a>(&'a [u8]); - -impl Read for SliceReader<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let count = self.0.len().min(buf.len()); - buf[..count].copy_from_slice(&self.0[..count]); - self.0 = &self.0[count..]; - Ok(count) - } -} - #[track_caller] fn eval_str_in(code: &str, env: &Rc) -> Result { let mut machine = Machine::default(); - let reader = BufReader::new(SliceReader(code.as_bytes())); - let mut reader = FileReader::new(reader); - - let mut last_value = None; - loop { - let value = match read(&mut reader, &mut machine, env) { - Ok(Some(value)) => value, - Ok(None) => break, - Err(error) => panic!("{error}"), - }; - - last_value = Some(machine.evaluate_value(Default::default(), None, env, value)); - } - - last_value.expect("no expressions evaluated") + machine.evaluate_str(Default::default(), None, env, code) } #[track_caller] fn eval_str(code: &str) -> Value { let env = Rc::new(Environment::default()); prelude::load(&env); - eval_str_in(code, &env).expect("expression evaluation failed") + match eval_str_in(code, &env) { + Ok(value) => value, + Err(error) => { + eprintln!("Couldn't evaluate expression:"); + eprintln!(); + eprintln!(" {code}"); + eprintln!(); + eprintln!(":: {error}"); + panic!("TEST FAILED"); + } + } } #[track_caller] fn eval_str_err(code: &str) -> MachineErrorAt { let env = Rc::new(Environment::default()); prelude::load(&env); - eval_str_in(code, &env).expect_err("expression was expected to fail") + match eval_str_in(code, &env) { + Ok(value) => { + eprintln!("Expected the code to fail to evaluate, but it returned success:"); + eprintln!(); + eprintln!(" {code}"); + eprintln!(); + eprintln!("Returned"); + eprintln!(); + eprintln!(":: {value}"); + panic!("TEST FAILED"); + } + Err(error) => error, + } +} + +#[track_caller] +fn eval_file>(path: P) -> Value { + let code = fs::read_to_string(path).expect("file read failed"); + eval_str(&code) } #[test] @@ -186,3 +185,22 @@ fn test_macro() { ]) ); } + +#[test] +fn test_examples_work() { + const EXCLUDE: &[&str] = &["repl.lysp", "echo.lysp", "io.lysp"]; + + // None of them should crash at least + for file in fs::read_dir("examples").unwrap() { + let entry = file.unwrap(); + let filename = entry.file_name(); + let filename = filename.to_str().unwrap(); + + if !filename.ends_with(".lysp") || EXCLUDE.contains(&filename) { + continue; + } + + eprintln!("Eval {}", entry.path().display()); + eval_file(entry.path()); + } +}