From 9e20dcf59c1eecc2e7e1f110639746a01a0c7e90 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Fri, 7 Jan 2022 20:30:55 -0800 Subject: [PATCH] Add parser, vm, objects Big ol thing. You should check it out sometime Signed-off-by: Alek Ratzloff --- examples/hello.sy | 1 + src/compile.rs | 0 src/main.rs | 23 +++- src/object.rs | 198 +++++++++++++++++++++--------- src/syn/ast.rs | 19 +++ src/syn/error.rs | 7 +- src/syn/lexer.rs | 12 +- src/syn/mod.rs | 2 +- src/syn/parser.rs | 304 +++++++++++++++++++++++++++++++++++++++++++++- src/syn/span.rs | 35 +++++- src/syn/token.rs | 15 +++ src/syn/util.rs | 11 -- src/vm/error.rs | 36 ++++++ src/vm/eval.rs | 49 ++++++++ src/vm/inst.rs | 11 ++ src/vm/machine.rs | 69 +++++++++++ src/vm/mod.rs | 4 + 17 files changed, 712 insertions(+), 84 deletions(-) create mode 100644 examples/hello.sy create mode 100644 src/compile.rs create mode 100644 src/syn/ast.rs delete mode 100644 src/syn/util.rs create mode 100644 src/vm/error.rs create mode 100644 src/vm/eval.rs create mode 100644 src/vm/inst.rs create mode 100644 src/vm/machine.rs create mode 100644 src/vm/mod.rs diff --git a/examples/hello.sy b/examples/hello.sy new file mode 100644 index 0000000..01d3317 --- /dev/null +++ b/examples/hello.sy @@ -0,0 +1 @@ +"Hell world" . \ No newline at end of file diff --git a/src/compile.rs b/src/compile.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs index 08c4b70..45396a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,24 @@ +mod compile; mod object; mod syn; +mod vm; use std::io::Read; use std::path::PathBuf; use structopt::StructOpt; -use syn::lexer::Lexer; +use syn::parser::Parser; +use vm::machine::MachineBuilder; #[derive(Debug, StructOpt)] struct Opt { #[structopt(name = "PATH", parse(from_os_str))] path: Option, + + #[structopt(long)] + max_stack_size: Option, + + #[structopt(long)] + max_arena_objects: Option, } type Result> = std::result::Result; @@ -25,10 +34,16 @@ fn main() -> Result { input }; - let mut lexer = Lexer::new(&text); - while let Some(token) = lexer.next()? { - println!("{:?}", token); + let mut parser = Parser::from(text.as_str()); + let mut exprs = Vec::new(); + while !parser.is_eof() { + exprs.extend(parser.next_expr_list()?); } + let machine = MachineBuilder::default() + .max_stack_size(opt.max_stack_size) + .max_arena_objects(opt.max_arena_objects) + .finish(); + Ok(()) } diff --git a/src/object.rs b/src/object.rs index 6f16979..a7258fb 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,36 +1,58 @@ +// TODO - remove this at some point. +// I haven't gotten around to testing or working with this API yet and I don't +// want it to clog the warnings yet. +#![allow(dead_code)] + +use crate::syn::ast::SpExpr; use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::collections::{BTreeMap, HashMap}; use std::rc::{Rc, Weak}; pub type Str = String; pub type Int = i64; pub type Float = f64; +pub type VTable = HashMap; #[derive(Debug, Clone)] pub enum Value { Array(Vec), Float(Float), Int(Int), - Str(String), - Obj(ObjPtr), + Str(Str), + Quote(Vec), + ObjPtr(ObjPtr), } #[derive(Debug, Clone)] pub struct ObjPtr { - arena: Weak, + arena: Weak>, slot: usize, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Obj { - vtable: HashMap, + vtable: VTable, +} + +impl Obj { + pub fn new(vtable: VTable) -> Self { + Self { vtable } + } + + pub fn vtable(&self) -> &VTable { + &self.vtable + } + + pub fn vtable_mut(&mut self) -> &mut VTable { + &mut self.vtable + } } #[derive(Debug)] pub struct Arena { slots: Vec, slots_dirty: bool, - objects: BTreeMap>, + objects: BTreeMap, max_size: Option, } @@ -76,19 +98,26 @@ impl Arena { } } self.slots = slots; + // Make sure to indicate that slots available are not dirty. + self.slots_dirty = false; } - pub fn obj_new(self: &mut Rc, obj: Obj) -> Option { + /// Creates a shared pointer for an Obj value, using the next available slot. + /// + /// If no slot is available, None is returned. + pub fn alloc_obj(self_rc: &Rc>, obj: Obj) -> Option { use SlotRange::*; + let mut self_mut = self_rc + .try_borrow_mut() + .expect("could not get arena mutably from Rc pointer"); // Compress if necessary - if !self.slots_dirty { - let self_mut = Rc::get_mut(self).expect("could not get arena mutably from Rc pointer"); + if self_mut.slots_dirty { self_mut.compress_slots(); } - if self + if self_mut .max_size - .map(|max_size| self.objects.len() >= max_size) + .map(|max_size| self_mut.objects.len() >= max_size) .unwrap_or(false) { // TODO : return err instead of option @@ -97,7 +126,6 @@ impl Arena { } // Get the next slot - let self_mut = Rc::get_mut(self).expect("could not get arena mutably from Rc pointer"); let slots = &mut self_mut.slots; let slot = match slots.first().copied().unwrap() { Range(start, end) => { @@ -113,68 +141,42 @@ impl Arena { index } }; + let previous = self_mut.objects.insert(slot, obj); + assert!( + previous.is_none(), + "slot {} was allocated but is already in use", + slot + ); Some(ObjPtr { - arena: Rc::downgrade(self), + arena: Rc::downgrade(self_rc), slot, }) } pub fn free_obj(&mut self, obj_ptr: ObjPtr) { // Compress if necessary - if !self.slots_dirty { + if self.slots_dirty { self.compress_slots(); } + let value = self.objects.remove(&obj_ptr.slot); + assert!( + value.is_some(), + "attempted to free object that was not tracked (slot {})", + obj_ptr.slot + ); self.slots .push(SlotRange::Range(obj_ptr.slot, obj_ptr.slot)); self.slots_dirty = true; // not my problem } } -#[test] -fn test_arena_compress_slots() { - use SlotRange::*; - let tests = [ - (vec![Range(0, 4), Range(2, 6), Open(0)], vec![Open(0)]), - (vec![Open(7), Range(0, 4), Range(2, 6)], vec![Open(0)]), - ( - vec![Open(8), Range(0, 4), Range(2, 6)], - vec![Range(0, 6), Open(8)], - ), - (vec![Range(0, 4), Range(2, 6)], vec![Range(0, 6)]), - (vec![Range(0, 4), Range(2, 6)], vec![Range(0, 6)]), - ( - vec![Range(0, 1), Range(2, 2), Range(3, 4)], - vec![Range(0, 4)], - ), - ( - vec![Range(0, 4), Range(3, 4), Range(2, 2)], - vec![Range(0, 4)], - ), - ( - vec![Range(3, 6), Range(0, 4), Range(2, 2)], - vec![Range(0, 6)], - ), - (vec![Range(0, 6), Range(6, 6)], vec![Range(0, 6)]), - (vec![Range(7, 6), Range(6, 6)], vec![Range(6, 6)]), - ]; - - for (slots, expected) in tests { - let mut arena = Arena { - slots, - slots_dirty: true, - objects: Default::default(), - max_size: None, - }; - arena.compress_slots(); - assert_eq!(arena.slots, expected); - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SlotRange { - /// A list of slots that are in a range, inclusive. + /// A list of slots that are in a range, exclusive. + /// + /// If the range start and end are equal, then it is a single opening. Range(usize, usize), - /// Everything from here and onward is open + /// Everything from here and onward is open. Open(usize), } @@ -243,3 +245,85 @@ fn test_slot_range_merge() { ); } } + +#[test] +fn test_arena_compress_slots() { + use SlotRange::*; + let tests = [ + (vec![Range(0, 4), Range(2, 6), Open(0)], vec![Open(0)]), + (vec![Open(7), Range(0, 4), Range(2, 6)], vec![Open(0)]), + ( + vec![Open(8), Range(0, 4), Range(2, 6)], + vec![Range(0, 6), Open(8)], + ), + (vec![Range(0, 4), Range(2, 6)], vec![Range(0, 6)]), + (vec![Range(0, 4), Range(2, 6)], vec![Range(0, 6)]), + ( + vec![Range(0, 1), Range(2, 2), Range(3, 4)], + vec![Range(0, 4)], + ), + ( + vec![Range(0, 4), Range(3, 4), Range(2, 2)], + vec![Range(0, 4)], + ), + ( + vec![Range(3, 6), Range(0, 4), Range(2, 2)], + vec![Range(0, 6)], + ), + (vec![Range(0, 6), Range(6, 6)], vec![Range(0, 6)]), + (vec![Range(7, 6), Range(6, 6)], vec![Range(6, 6)]), + ( + vec![Range(0, 0), Range(1, 1), Range(2, 2)], + vec![Range(0, 2)], + ), + ( + vec![Range(0, 0), Range(1, 1), Range(2, 2), Open(3)], + vec![Open(0)], + ), + ]; + + for (slots, expected) in tests { + let mut arena = Arena { + slots, + slots_dirty: true, + objects: Default::default(), + max_size: None, + }; + arena.compress_slots(); + assert_eq!(arena.slots, expected); + } +} + +#[test] +fn test_arena_obj_lifetime() { + let arena = Rc::new(RefCell::new(Arena::new(None))); + + let p1 = Arena::alloc_obj(&arena, Obj::default()).unwrap(); + assert_eq!(arena.borrow().slots, vec![SlotRange::Open(1)]); + let p2 = Arena::alloc_obj(&arena, Obj::default()).unwrap(); + assert_eq!(arena.borrow().slots, vec![SlotRange::Open(2)]); + let p3 = Arena::alloc_obj(&arena, Obj::default()).unwrap(); + assert_eq!(arena.borrow().slots, vec![SlotRange::Open(3)]); + + { + let mut arena_mut = arena.borrow_mut(); + arena_mut.free_obj(p2); + arena_mut.compress_slots(); + assert_eq!( + arena_mut.slots, + vec![SlotRange::Range(1, 1), SlotRange::Open(3)] + ); + } + + let p4 = Arena::alloc_obj(&arena, Obj::default()).unwrap(); + assert_eq!(arena.borrow().slots, vec![SlotRange::Open(3)]); + + { + let mut arena_mut = arena.borrow_mut(); + arena_mut.free_obj(p1); + arena_mut.free_obj(p3); + arena_mut.free_obj(p4); + arena_mut.compress_slots(); + assert_eq!(arena_mut.slots, vec![SlotRange::Open(0)],); + } +} diff --git a/src/syn/ast.rs b/src/syn/ast.rs new file mode 100644 index 0000000..57bb866 --- /dev/null +++ b/src/syn/ast.rs @@ -0,0 +1,19 @@ +use crate::object::{Float, Int, Str, Value}; +use crate::syn::span::*; + +#[derive(Debug, Clone, PartialEq)] +pub enum Expr { + Atom(SpAtom), + Quote(Vec), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Atom { + Word(Str), + Float(Float), + Int(Int), + Str(Str), +} + +pub type SpAtom = Spanned; +pub type SpExpr = Spanned; diff --git a/src/syn/error.rs b/src/syn/error.rs index 1f2bf08..7d637e9 100644 --- a/src/syn/error.rs +++ b/src/syn/error.rs @@ -1,10 +1,9 @@ use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum SyntaxError { - #[error("unexpected {0}")] - Unexpected(String), - + //#[error("unexpected {0}")] + //Unexpected(String), #[error("expected {expected}, but got {got}")] ExpectedGot { expected: String, got: String }, } diff --git a/src/syn/lexer.rs b/src/syn/lexer.rs index 545f643..b103495 100644 --- a/src/syn/lexer.rs +++ b/src/syn/lexer.rs @@ -1,4 +1,4 @@ -use crate::syn::{error::*, span::*, token::*, util::*}; +use crate::syn::{error::*, span::*, token::*}; use lazy_static::lazy_static; use regex::{Regex, RegexBuilder}; @@ -11,6 +11,7 @@ lazy_static! { | (?P\[) | (?P\]) | (?P:) + | (?P"([^"\\]|\\["'\\ntrb])*") )"# ) .ignore_whitespace(true) @@ -19,6 +20,7 @@ lazy_static! { } /// Lexes things. +#[derive(Debug, Clone)] pub struct Lexer<'t> { text: &'t str, start: Pos, @@ -113,6 +115,10 @@ impl<'t> Lexer<'t> { } } +fn expected_got_char(c: char) -> String { + format!("character {}", c.escape_debug()) +} + #[cfg(test)] mod test { use super::*; @@ -200,7 +206,7 @@ mod test { #[test] fn test_colon() { - let mut lexer = Lexer::new(": :: ::: ::::"); + let mut lexer = Lexer::new(": :: ::: :::: some-name:"); assert_token!(lexer, Token::Colon); assert_token!(lexer, Token::Colon); assert_token!(lexer, Token::Colon); @@ -211,6 +217,8 @@ mod test { assert_token!(lexer, Token::Colon); assert_token!(lexer, Token::Colon); assert_token!(lexer, Token::Colon); + assert_token!(lexer, Token::Word); + assert_token!(lexer, Token::Colon); assert!(lexer.is_eof()); } } diff --git a/src/syn/mod.rs b/src/syn/mod.rs index 0a257c8..63ccd89 100644 --- a/src/syn/mod.rs +++ b/src/syn/mod.rs @@ -1,6 +1,6 @@ +pub mod ast; pub mod error; pub mod lexer; pub mod parser; pub mod span; pub mod token; -pub mod util; diff --git a/src/syn/parser.rs b/src/syn/parser.rs index debed32..a24b8ce 100644 --- a/src/syn/parser.rs +++ b/src/syn/parser.rs @@ -1,5 +1,307 @@ -use crate::syn::lexer::*; +use crate::syn::{ast::*, error::*, lexer::*, token::*}; +#[derive(Debug)] pub struct Parser<'t> { lexer: Lexer<'t>, + token: Result>, +} + +impl<'t> From<&'t str> for Parser<'t> { + fn from(text: &'t str) -> Self { + Parser::new(Lexer::new(text)) + } +} + +impl<'t> Parser<'t> { + pub fn new(mut lexer: Lexer<'t>) -> Self { + let token = lexer.next(); + Self { lexer, token } + } + + pub fn is_eof(&self) -> bool { + self.lexer.is_eof() + } + + fn peek(&self) -> Result> { + self.token.clone() + } + + fn adv(&mut self) -> Result> { + let next = self.lexer.next(); + std::mem::replace(&mut self.token, next) + } + + /// Checks if the next token is part of the list, returning it if so. + fn expect_any_token(&mut self, expected: &[Token]) -> Result { + let token = self.peek()?; + + match (token, expected) { + // Token matches + (Some(token), expected) if expected.contains(token.inner()) => { + self.adv()?; + Ok(token) + } + + // Token does not match, only one token expected + (token, &[expected]) => { + // get the string version of whether this is a token or EOF + let got = token + .map(|t| format!("{} token", t.inner().name())) + .unwrap_or_else(|| "EOF".to_string()); + Err(SyntaxError::ExpectedGot { + expected: format!("{} token", expected.name()), + got, + }) + } + + // Token does not match, any of N tokens expected + (token, expected) => { + // make the comma-separated list of everything except for the last item + let expected_str = expected + .iter() + .take(expected.len() - 1) + .map(Token::name) + .collect::>() + .join(", "); + // get the string version of whether this is a token or EOF + let got = token + .map(|t| format!("{} token", t.inner().name())) + .unwrap_or_else(|| "EOF".to_string()); + Err(SyntaxError::ExpectedGot { + expected: format!( + "{} or {} token", + expected_str, + expected.last().unwrap().name() + ), + got, + }) + } + } + } + + /// Gets all expressions until EOF is reached, or until a quote end is reached. + pub fn next_expr_list(&mut self) -> Result> { + let mut exprs = Vec::new(); + while let Some(peek) = self.peek()? { + match peek.inner() { + Token::RQuote => break, + _ => exprs.push(self.next_expr()?), + } + } + Ok(exprs) + } + + pub fn next_expr(&mut self) -> Result { + // peek ahead and see if we need to handle a quote + match self.peek()? { + Some(peek) if *peek.inner() == Token::LQuote => self.next_quote(), + _ => { + let atom = self.next_atom()?; + let span = atom.span(); + Ok(SpExpr::new(span, Expr::Atom(atom))) + } + } + } + + pub fn next_quote(&mut self) -> Result { + let start = self.expect_any_token(&[Token::LQuote])?; + let exprs = self.next_expr_list()?; + let end = self.expect_any_token(&[Token::RQuote])?; + let span = start.span().union(end.span()); + Ok(SpExpr::new(span, Expr::Quote(exprs))) + } + + pub fn next_atom(&mut self) -> Result { + use Token::*; + let token = self.expect_any_token(&[Word, Float, Int, Str])?; + Ok(self.token_to_atom(token)) + } + + fn token_to_atom(&self, token: SpToken) -> SpAtom { + // NOTE - self is required for this because we get the text + let (span, token) = token.into_split(); + let text = span.text_at(self.lexer.text()); + let atom = match token { + Token::Word => Atom::Word(text.to_string()), + Token::Float => Atom::Float(text.parse().unwrap()), + Token::Int => Atom::Int(text.parse().unwrap()), + Token::Str => Atom::Str(unescape_string(text)), + _ => panic!("invalid token specified for token_to_atom, it should be an atom"), + }; + SpAtom::new(span, atom) + } +} + +fn unescape_string(text: &str) -> String { + let mut string = String::with_capacity(text.len() - 2); + let mut chars = text.chars().skip(1).take(text.len() - 2); + while let Some(c) = chars.next() { + if c == '\\' { + let c = match chars + .next() + .expect("reached end of string literal before escape") + { + '"' => '"', + '\'' => '\'', + '\\' => '\\', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + u => panic!( + "unexpected character escape that made it through the lexer: {:?}", + u + ), + }; + string.push(c); + } else { + string.push(c); + } + } + string +} + +#[cfg(test)] +macro_rules! expect_atom { + ($parser:expr, $expected:expr) => {{ + let expr_result = $parser.next_expr(); + assert!( + expr_result.is_ok(), + "expected {:?} but got {:?} instead", + $expected, + expr_result + ); + let expr = expr_result.unwrap(); + let (span, expr) = expr.into_split(); + assert_eq!(expr, Expr::Atom(SpAtom::new(span, $expected))); + }}; +} + +#[cfg(test)] +macro_rules! expect_expr { + ($parser:expr, $expected:expr) => {{ + let expr_result = $parser.next_expr(); + assert!( + expr_result.is_ok(), + "expected {:?} but got {:?} instead", + $expected, + expr_result + ); + let expr = expr_result.unwrap(); + assert_eq!(expr, $expected); + }}; +} + +/// Makes an SpExpr Quote value using the given SpExpr values +#[cfg(test)] +macro_rules! make_quote { + ($($expr:expr),+ $(,)?) => {{ + SpExpr::new( + Default::default(), + Expr::Quote(make_quote_vec!($($expr),+)) + ) + }}; +} + +/// Makes a vec appropriate for an Expr::Quote +#[cfg(test)] +macro_rules! make_quote_vec { + ($($expr:expr),+ $(,)?) => {{ + vec![$( + SpExpr::new(Default::default(), $expr) + ),+] + }}; +} + +/// Makes an SpAtom from an Atom type. +#[cfg(test)] +macro_rules! make_atom { + ($atom:expr) => {{ + SpAtom::new(Default::default(), $atom) + }}; +} + +#[test] +fn test_parser_atoms() { + let mut parser = Parser::from( + r#" + a ab bcd dcefg foo bar baz + 1 2 3 4 5 + 1.2 3.4 5.6 7.8 9.10 + "this is a string" + "this\nis\na\nstring\nwith\nnewlines" + "this\tis\ta\tstring\twith\ttabs" + "#, + ); + expect_atom!(parser, Atom::Word("a".to_string())); + expect_atom!(parser, Atom::Word("ab".to_string())); + expect_atom!(parser, Atom::Word("bcd".to_string())); + expect_atom!(parser, Atom::Word("dcefg".to_string())); + expect_atom!(parser, Atom::Word("foo".to_string())); + expect_atom!(parser, Atom::Word("bar".to_string())); + expect_atom!(parser, Atom::Word("baz".to_string())); + expect_atom!(parser, Atom::Int(1)); + expect_atom!(parser, Atom::Int(2)); + expect_atom!(parser, Atom::Int(3)); + expect_atom!(parser, Atom::Int(4)); + expect_atom!(parser, Atom::Int(5)); + expect_atom!(parser, Atom::Float(1.2)); + expect_atom!(parser, Atom::Float(3.4)); + expect_atom!(parser, Atom::Float(5.6)); + expect_atom!(parser, Atom::Float(7.8)); + expect_atom!(parser, Atom::Float(9.1)); + expect_atom!(parser, Atom::Str("this is a string".to_string())); + expect_atom!( + parser, + Atom::Str("this\nis\na\nstring\nwith\nnewlines".to_string()) + ); + expect_atom!( + parser, + Atom::Str("this\tis\ta\tstring\twith\ttabs".to_string()) + ); + assert!(parser.is_eof()); +} + +#[test] +fn test_parser_quotes() { + let mut parser = Parser::from( + r#" + [ + a ab bcd dcefg foo bar baz + ] + [1 2 3 4 5 + [1.2 3.4 5.6 7.8 9.10] + ] + "#, + ); + + expect_expr!( + parser, + make_quote![ + Expr::Atom(make_atom!(Atom::Word("a".to_string()))), + Expr::Atom(make_atom!(Atom::Word("ab".to_string()))), + Expr::Atom(make_atom!(Atom::Word("bcd".to_string()))), + Expr::Atom(make_atom!(Atom::Word("dcefg".to_string()))), + Expr::Atom(make_atom!(Atom::Word("foo".to_string()))), + Expr::Atom(make_atom!(Atom::Word("bar".to_string()))), + Expr::Atom(make_atom!(Atom::Word("baz".to_string()))), + ] + ); + + expect_expr!( + parser, + make_quote![ + Expr::Atom(make_atom!(Atom::Int(1))), + Expr::Atom(make_atom!(Atom::Int(2))), + Expr::Atom(make_atom!(Atom::Int(3))), + Expr::Atom(make_atom!(Atom::Int(4))), + Expr::Atom(make_atom!(Atom::Int(5))), + Expr::Quote(make_quote_vec![ + Expr::Atom(make_atom!(Atom::Float(1.2))), + Expr::Atom(make_atom!(Atom::Float(3.4))), + Expr::Atom(make_atom!(Atom::Float(5.6))), + Expr::Atom(make_atom!(Atom::Float(7.8))), + Expr::Atom(make_atom!(Atom::Float(9.10))), + ]), + ] + ); } diff --git a/src/syn/span.rs b/src/syn/span.rs index ed687ac..88f93e7 100644 --- a/src/syn/span.rs +++ b/src/syn/span.rs @@ -1,6 +1,13 @@ -use std::cmp::{Ord, Ordering, PartialOrd}; +// TODO - remove this at some point. +// I'm happy with this API design and I don't think that it should be clogging +// up the warning lists because I'm not using a logical part of the API *at the moment*. +#![allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +use std::cmp::{Ord, Ordering, PartialOrd}; +use std::fmt::{self, Debug}; + +#[cfg_attr(not(test), derive(PartialEq))] +#[derive(Debug, Default, Clone, Copy, Eq)] pub struct Pos { pub source: usize, pub line: usize, @@ -9,6 +16,14 @@ pub struct Pos { pub c: char, } +// when testing, don't actually compare positions +#[cfg(test)] +impl PartialEq for Pos { + fn eq(&self, _other: &Pos) -> bool { + true + } +} + impl Pos { pub fn new(c: char) -> Self { Pos { @@ -56,7 +71,7 @@ impl Ord for Pos { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Span { pub start: Pos, pub end: Pos, @@ -66,9 +81,15 @@ impl Span { pub fn text_at<'t>(&self, text: &'t str) -> &'t str { &text[self.start.byte..self.end.byte] } + + pub fn union(&self, other: Span) -> Self { + let start = self.start.min(other.start); + let end = self.end.max(other.end); + Span { start, end } + } } -#[derive(Debug, Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct Spanned { span: Span, inner: T, @@ -107,3 +128,9 @@ impl Spanned { self.span().text_at(text) } } + +impl Debug for Spanned { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + Debug::fmt(self.inner(), fmt) + } +} diff --git a/src/syn/token.rs b/src/syn/token.rs index 092f702..cbf7e82 100644 --- a/src/syn/token.rs +++ b/src/syn/token.rs @@ -25,5 +25,20 @@ pub enum Token { Colon, } +impl Token { + pub fn name(&self) -> &'static str { + use Token::*; + match self { + Word => "word", + Float => "float", + Int => "int", + Str => "str", + LQuote => "quote begin", + RQuote => "quote end", + Colon => "colon", + } + } +} + /// Spanned token. pub type SpToken = Spanned; diff --git a/src/syn/util.rs b/src/syn/util.rs deleted file mode 100644 index 38ba1ae..0000000 --- a/src/syn/util.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub fn got_char_or_eof(got: Option) -> String { - if let Some(got) = got { - expected_got_char(got) - } else { - "EOF".to_string() - } -} - -pub fn expected_got_char(c: char) -> String { - format!("character {}", c.escape_debug()) -} diff --git a/src/vm/error.rs b/src/vm/error.rs new file mode 100644 index 0000000..bc80024 --- /dev/null +++ b/src/vm/error.rs @@ -0,0 +1,36 @@ +use thiserror::Error; + +#[derive(Error, Debug, Clone)] +pub enum RuntimeError { + #[error("stack overflow")] + StackOverflow, + #[error("stack underflow")] + StackUnderflow, + //#[error("unexpected {0}")] + //Unexpected(String), + //#[error("expected {expected}, but got {got}")] + //ExpectedGot { expected: String, got: String }, +} + +pub type Result = std::result::Result; + +// TODO +// Error building idea: +// pass RuntimeError values upward, but also keep a list of spans/locations +// added by functions adding errors upward. + +// e.g. + +/* + +fn do_thing() -> Result<()> { + ... + let result = do_fallible_thing(); + if let Err(e) = result { + e.add_location(context.file, context.span); + return Err(e); + } + ... +} + +*/ diff --git a/src/vm/eval.rs b/src/vm/eval.rs new file mode 100644 index 0000000..0ab5b01 --- /dev/null +++ b/src/vm/eval.rs @@ -0,0 +1,49 @@ +use crate::object::*; +use crate::syn::ast::*; +use crate::vm::{error::*, inst::Inst, machine::Machine}; + +/// An evaluation context for the VM. +pub struct Eval<'m> { + machine: &'m mut Machine, +} + +impl<'m> Eval<'m> { + pub fn new(machine: &'m mut Machine) -> Self { + Self { machine } + } + + pub fn eval_expr(&mut self, expr: &SpExpr) -> Result<()> { + match expr.inner() { + Expr::Atom(atom) => match atom.inner() { + Atom::Float(f) => self.machine.stack_push(Value::Float(*f))?, + Atom::Int(i) => self.machine.stack_push(Value::Int(*i))?, + Atom::Str(s) => self.machine.stack_push(Value::Str(s.clone()))?, + Atom::Word(_) => todo!("TODO - word lookup"), + }, + Expr::Quote(quote) => { + self.machine.stack_push(Value::Quote(quote.clone()))?; + } + } + Ok(()) + } + + pub fn eval_inst(&mut self, inst: Inst) -> Result<()> { + match inst { + Inst::Push(v) => self.machine.stack_push(v)?, + Inst::Pop => { + self.machine + .stack_pop() + .ok_or(RuntimeError::StackUnderflow)?; + } + Inst::When => todo!(), + Inst::If => todo!(), + Inst::Eval => todo!(), + Inst::Print => todo!(), + } + Ok(()) + } +} + +// IDEA: Eval chain +// chain of eval-able things, including macros. An eval can add something to the +// top of the eval chain and immediately jump to it, such as an include diff --git a/src/vm/inst.rs b/src/vm/inst.rs new file mode 100644 index 0000000..05eedbe --- /dev/null +++ b/src/vm/inst.rs @@ -0,0 +1,11 @@ +use crate::object::Value; + +#[derive(Debug, Clone)] +pub enum Inst { + Push(Value), + Pop, + When, + If, + Eval, + Print, +} diff --git a/src/vm/machine.rs b/src/vm/machine.rs new file mode 100644 index 0000000..44e7846 --- /dev/null +++ b/src/vm/machine.rs @@ -0,0 +1,69 @@ +use crate::object::*; +use crate::vm::error::*; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Default)] +pub struct MachineBuilder { + max_stack_size: Option, + max_arena_objects: Option, +} + +impl MachineBuilder { + pub fn max_stack_size(mut self, max_stack_size: Option) -> Self { + self.max_stack_size = max_stack_size; + self + } + + pub fn max_arena_objects(mut self, max_arena_objects: Option) -> Self { + self.max_arena_objects = max_arena_objects; + self + } + + pub fn finish(self) -> Machine { + Machine::new(self.max_stack_size, Arena::new(self.max_arena_objects)) + } +} + +/// The current state of a VM. +#[derive(Debug)] +pub struct Machine { + stack: Vec, + max_stack_size: Option, + arena: Rc>, +} + +impl Machine { + pub fn new(max_stack_size: Option, arena: Arena) -> Self { + Machine { + stack: Default::default(), + max_stack_size, + arena: Rc::new(RefCell::new(arena)), + } + } + + pub fn max_stack_size(&self) -> Option { + self.max_stack_size + } + + pub fn stack(&self) -> &Vec { + &self.stack + } + pub fn stack_mut(&mut self) -> &mut Vec { + &mut self.stack + } + + pub fn stack_push(&mut self, value: Value) -> Result<()> { + if let Some(max) = self.max_stack_size() { + if self.stack.len() >= max { + return Err(RuntimeError::StackOverflow); + } + } + self.stack_mut().push(value); + Ok(()) + } + + pub fn stack_pop(&mut self) -> Option { + self.stack_mut().pop() + } +} diff --git a/src/vm/mod.rs b/src/vm/mod.rs new file mode 100644 index 0000000..cb9ed28 --- /dev/null +++ b/src/vm/mod.rs @@ -0,0 +1,4 @@ +pub mod error; +pub mod eval; +pub mod inst; +pub mod machine;