Compare commits

...

7 Commits

Author SHA1 Message Date
Alona EM 93266a980e Refractor into modules 2021-12-27 22:19:29 +00:00
Alona EM bc0d17e58f Many more tests 2021-12-27 20:43:12 +00:00
Alona EM 1129e1e2d9 New prims `abs` and `<` 2021-12-27 20:43:01 +00:00
Alona EM 4c4928ea9f Refractor tests 2021-12-27 15:53:56 +00:00
Alona EM 46b96f3e82 Drracket compat 2021-12-27 15:07:07 +00:00
Alona EM c1eb1ed61e Basic test harness 2021-12-27 03:35:57 +00:00
Alona EM ebf46fc9e6 Use debug2 from git 2021-12-27 01:19:55 +00:00
29 changed files with 3037 additions and 2174 deletions

5
.gitignore vendored
View File

@ -1 +1,6 @@
/target
# dr racket
*.scm~
*.scm#*
.#*.scm

259
Cargo.lock generated
View File

@ -29,6 +29,20 @@ dependencies = [
"term",
]
[[package]]
name = "assert_cmd"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -73,6 +87,17 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
]
[[package]]
name = "cc"
version = "1.0.72"
@ -105,6 +130,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "console"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"terminal_size",
"winapi",
]
[[package]]
name = "const-random"
version = "0.1.13"
@ -136,6 +174,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "debug2"
version = "0.1.0"
source = "git+https://github.com/adotinthevoid/debug2?rev=8b14afb90fc1d9ce470792ea393a9ab821a9e07f#8b14afb90fc1d9ce470792ea393a9ab821a9e07f"
dependencies = [
"debug2-derive",
]
@ -143,6 +182,7 @@ dependencies = [
[[package]]
name = "debug2-derive"
version = "0.1.0"
source = "git+https://github.com/adotinthevoid/debug2?rev=8b14afb90fc1d9ce470792ea393a9ab821a9e07f#8b14afb90fc1d9ce470792ea393a9ab821a9e07f"
dependencies = [
"proc-macro2",
"quote",
@ -156,6 +196,12 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "dirs-next"
version = "2.0.0"
@ -177,6 +223,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.6.1"
@ -192,6 +244,12 @@ dependencies = [
"log",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "endian-type"
version = "0.1.2"
@ -242,12 +300,27 @@ dependencies = [
"wasi",
]
[[package]]
name = "globset"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
"fnv",
"log",
"regex",
]
[[package]]
name = "handball"
version = "0.1.0"
dependencies = [
"assert_cmd",
"chumsky",
"debug2",
"insta",
"lalrpop",
"lalrpop-util",
"logos",
@ -280,6 +353,23 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "insta"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c4e56d571b4cc829f0ce71506bd865a90369eeab5f3d3657ba96230beb8012"
dependencies = [
"console",
"globset",
"lazy_static",
"serde",
"serde_json",
"serde_yaml",
"similar",
"uuid",
"walkdir",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -298,6 +388,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "lalrpop"
version = "0.19.6"
@ -342,6 +438,12 @@ version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "lock_api"
version = "0.4.5"
@ -483,6 +585,33 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95e5a7689e456ab905c22c2b48225bb921aba7c8dfa58440d68ba13f6222a715"
dependencies = [
"difflib",
"itertools",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
[[package]]
name = "predicates-tree"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
@ -547,6 +676,12 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.25"
@ -593,12 +728,76 @@ dependencies = [
"syn",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
dependencies = [
"indexmap",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
name = "similar"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
[[package]]
name = "siphasher"
version = "0.3.7"
@ -664,6 +863,22 @@ dependencies = [
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "termtree"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a4ec180a2de59b57434704ccfad967f789b12737738798fa08798cd5824c16"
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@ -703,6 +918,32 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
@ -725,6 +966,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -773,3 +1023,12 @@ name = "windows_x86_64_msvc"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View File

@ -7,11 +7,18 @@ edition = "2021"
[dependencies]
chumsky = "0.6.0"
debug2 = { git = "https://github.com/adotinthevoid/debug2", rev="8b14afb90fc1d9ce470792ea393a9ab821a9e07f" }
# fs-err = "2.6.0"
lalrpop-util = { version = "0.19.6", features = ["lexer"] }
logos = "0.12.0"
debug2 = {path="../debug2"}
rustyline = "9.1.1"
rustyline-derive = "0.6.0"
[build-dependencies]
lalrpop = "0.19.6"
[features]
[dev-dependencies]
assert_cmd = "2.0.2"
insta = { version = "1.9.0", features = ["glob"] }

View File

@ -2,5 +2,5 @@ fn main() {
lalrpop::Configuration::new()
.generate_in_source_tree()
.process()
.unwrap();
.expect("handball lalrpop failed");
}

View File

@ -1 +1 @@
nightly-2021-12-16
nightly-2021-12-27

23
src/ast.rs Normal file
View File

@ -0,0 +1,23 @@
use std::rc::Rc;
#[derive(Debug, debug2::Debug, PartialEq)]
crate enum Tree {
Leaf(Literal),
Define(String, Box<Tree>),
If(Box<[Tree; 3]>),
Lambda(Lambda),
Branch(Vec<Tree>),
}
#[derive(Debug, debug2::Debug, PartialEq, Clone)]
crate struct Lambda(crate Rc<[String]>, crate Rc<[Tree]>);
#[derive(Debug, debug2::Debug, PartialEq)]
crate enum Literal {
Sym(String),
Num(f64),
Bool(bool),
}

113
src/eval.rs Normal file
View File

@ -0,0 +1,113 @@
use std::assert_matches::assert_matches;
use std::collections::HashMap;
use crate::value::Value;
use crate::{ast, prims};
pub(crate) fn eval(t: &ast::Tree, env: &mut Env) -> Result<Value, RTError> {
Ok(match t {
ast::Tree::Leaf(l) => match l {
ast::Literal::Sym(s) => match env.lookup(s) {
Some(v) => v,
None => return err(format!("Undefined variable `{}`", s)),
},
ast::Literal::Num(v) => Value::Num(*v),
ast::Literal::Bool(b) => Value::Bool(*b),
},
ast::Tree::Lambda(l) => Value::Lambda(l.clone()),
ast::Tree::Branch(args) => {
let Some(fun) = args.get(0) else { return err("No argument given".to_owned()) };
let fun = eval(fun, env)?;
let fun = fun.as_func()?;
let args = args
.iter()
.skip(1)
.map(|a| eval(a, env))
.collect::<Result<Vec<_>, _>>()?;
// return fun(&args);
match fun {
Callable::Func(f) => f(&args)?,
Callable::Lambda(l) => {
if l.0.len() == args.len() {
let mut env = env.child();
for (x, y) in l.0.iter().zip(args) {
env.define(x.clone(), y)
}
let [main @ .., tail] = &l.1[..] else { unreachable!("Body has 1+ element by parser") };
for i in main {
eval(i, &mut env)?;
}
eval(tail, &mut env)?
} else {
return err(format!("Need {} args, got {}", l.0.len(), args.len()));
}
}
}
}
ast::Tree::Define(name, to) => {
let val = eval(to, env)?;
env.define(name.to_owned(), val);
Value::Trap
}
ast::Tree::If(box [cond, tcase, fcase]) => {
let b = eval(cond, env)?.as_bool()?;
let body = if b { tcase } else { fcase };
eval(body, env)?
}
})
}
#[derive(Debug)]
crate struct RTError(crate String);
pub(crate) fn err<T>(s: String) -> Result<T, RTError> {
Err(RTError(s))
}
pub(crate) enum Callable<'a> {
Func(prims::Func),
Lambda(&'a ast::Lambda),
}
pub(crate) struct Env<'a> {
pub(crate) vars: HashMap<String, Value>,
pub(crate) enclosing: Option<&'a Env<'a>>,
}
impl<'a> Env<'a> {
pub(crate) fn child(&'a self) -> Env<'a> {
Env {
vars: HashMap::new(),
enclosing: Some(self),
}
}
pub(crate) fn lookup(&self, s: &str) -> Option<Value> {
if let Some(v) = self.vars.get(s) {
Some(v.clone())
} else if let Some(parent) = self.enclosing {
parent.lookup(s)
} else {
None
}
}
pub(crate) fn define(&mut self, name: String, val: Value) {
self.vars.insert(name, val);
}
}
pub(crate) fn default_env() -> Env<'static> {
let mut vars = HashMap::new();
for (name, fun) in prims::prims() {
assert_matches!(vars.insert((*name).to_owned(), Value::Func(*fun)), None)
}
Env {
vars,
enclosing: None,
}
}

View File

@ -1,9 +1,18 @@
use crate::{Tree, Literal, Lambda};
use crate::ast::*;
use std::rc::Rc;
grammar<'s>();
pub(crate) Trees = Tree+;
match {
r"\s*" => { }, // The default whitespace skipping is disabled an `ignore pattern` is specified
r";[^\n\r]*[\n\r]*" => { }, // Skip `;` comments
_
}
pub(crate) File = {Lang? <Trees> }
Trees = Tree+;
pub(crate) Tree: Tree = {
"(" <Tree+> ")" => Tree::Branch(<>),
@ -31,4 +40,11 @@ Num: f64 = { r"[0-9]+(\.[0-9]+)?" => <>.parse().unwrap() }
Bool: bool = {
"#t" => true,
"#f" => false,
}
}
Lang: () = { "#lang" LangName }
LangName = {
// TODO: What should these be?
"scheme",
"r7rs",
}

File diff suppressed because it is too large Load Diff

View File

@ -1,145 +1,25 @@
#![feature(let_else)]
#![feature(array_windows)]
#![feature(assert_matches)]
#![feature(box_patterns)]
#![feature(crate_visibility_modifier)]
#![feature(let_else)]
mod grammar;
use std::io;
// use debug2::dbg;
use rustyline::validate::{
MatchingBracketValidator, ValidationContext, ValidationResult, Validator,
};
use rustyline::Editor;
use rustyline_derive::{Completer, Helper, Highlighter, Hinter};
use std::collections::HashMap;
use std::io;
use std::rc::Rc;
#[derive(Debug, debug2::Debug, PartialEq)]
enum Tree {
Leaf(Literal),
Define(String, Box<Tree>),
If(Box<[Tree; 3]>),
Lambda(Lambda),
Branch(Vec<Tree>),
}
#[derive(Debug, debug2::Debug, PartialEq, Clone)]
struct Lambda(Rc<[String]>, Rc<[Tree]>);
#[derive(Debug, debug2::Debug, PartialEq)]
enum Literal {
Sym(String),
Num(f64),
Bool(bool),
}
#[derive(Debug)]
struct RTError(String);
fn err<T>(s: String) -> Result<T, RTError> {
Err(RTError(s))
}
type Func = fn(&[Value]) -> Result<Value, RTError>;
#[derive(Clone)]
enum Value {
Num(f64),
Func(Func),
Lambda(Lambda),
Bool(bool),
NotAValue,
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
use Value::*;
match (self, other) {
(Num(l), Num(r)) => l == r,
(Func(l), Func(r)) => *l as usize == *r as usize,
(Bool(l), Bool(r)) => l == r,
(Lambda(l), Lambda(r)) => Rc::ptr_eq(&l.0, &r.0) && Rc::ptr_eq(&l.1, &r.1),
(Num(_), _) => false,
(Func(_), _) => false,
(Bool(_), _) => false,
(Lambda(_), _) => false,
(NotAValue, _) => panic!("Trap value"),
}
}
}
impl std::fmt::Debug for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Num(n) => n.fmt(f),
Self::Func(_) => f.write_str("#<procedure>"),
Self::Lambda(_) => f.write_str("#<procedure>"),
Self::Bool(b) => f.write_str(if *b { "#t" } else { "#f" }),
Self::NotAValue => Ok(()),
}
}
}
impl Value {
fn as_num(&self) -> Result<f64, RTError> {
if let Self::Num(n) = self {
Ok(*n)
} else {
Err(RTError("Expected a number".to_owned()))
}
}
fn as_func(&self) -> Result<Callable, RTError> {
match self {
Self::Func(f) => Ok(Callable::Func(*f)),
Self::Lambda(l) => Ok(Callable::Lambda(l)),
_ => Err(RTError(format!("Expected a function, got {:?}", self))),
}
}
fn as_bool(&self) -> Result<bool, RTError> {
if let Self::Bool(b) = self {
Ok(*b)
} else {
Err(RTError("Expected a bool".to_owned()))
}
}
}
enum Callable<'a> {
Func(Func),
Lambda(&'a Lambda),
}
struct Env<'a> {
vars: HashMap<String, Value>,
enclosing: Option<&'a Env<'a>>,
}
impl<'a> Env<'a> {
fn child(&'a self) -> Env<'a> {
Env {
vars: HashMap::new(),
enclosing: Some(self),
}
}
fn lookup(&self, s: &str) -> Option<Value> {
if let Some(v) = self.vars.get(s) {
Some(v.clone())
} else if let Some(parent) = self.enclosing {
parent.lookup(s)
} else {
None
}
}
fn define(&mut self, name: String, val: Value) {
self.vars.insert(name, val);
}
}
mod ast;
mod eval;
#[allow(clippy::all)]
mod grammar;
mod prims;
#[cfg(test)]
mod tests;
mod value;
fn main() {
let args = std::env::args().collect::<Vec<_>>();
@ -152,10 +32,10 @@ fn main() {
fn run_file(file: &str) -> io::Result<()> {
let src = std::fs::read_to_string(file)?;
let tree = grammar::TreesParser::new().parse(&src).unwrap();
let mut env = default_env();
let tree = grammar::FileParser::new().parse(&src).unwrap();
let mut env = eval::default_env();
for i in tree {
eval(&i, &mut env).unwrap();
eval::eval(&i, &mut env).unwrap();
}
Ok(())
}
@ -166,207 +46,17 @@ fn repl() {
brackets: MatchingBracketValidator::new(),
}));
let mut env = default_env();
let mut env = eval::default_env();
while let Ok(line) = rl.readline("> ") {
rl.add_history_entry(&line);
let tree = grammar::TreeParser::new().parse(&line).unwrap();
// dbg!(&tree);
println!("< {:?}", eval(&tree, &mut env))
println!("< {:?}", eval::eval(&tree, &mut env))
}
}
fn eval(t: &Tree, env: &mut Env) -> Result<Value, RTError> {
Ok(match t {
Tree::Leaf(l) => match l {
Literal::Sym(s) => match env.lookup(s) {
Some(v) => v.clone(),
None => return err(format!("Undefined variable `{}`", s)),
},
Literal::Num(v) => Value::Num(*v),
Literal::Bool(b) => Value::Bool(*b),
},
Tree::Lambda(l) => Value::Lambda(l.clone()),
Tree::Branch(args) => {
let Some(fun) = args.get(0) else { return err("No argument given".to_owned()) };
let fun = eval(fun, env)?;
let fun = fun.as_func()?;
let args = args
.iter()
.skip(1)
.map(|a| eval(a, env))
.collect::<Result<Vec<_>, _>>()?;
// return fun(&args);
match fun {
Callable::Func(f) => f(&args)?,
Callable::Lambda(l) => {
if l.0.len() == args.len() {
let mut env = env.child();
for (x, y) in l.0.iter().zip(args) {
env.define(x.clone(), y)
}
let [main @ .., tail] = &l.1[..] else { unreachable!("Body has 1+ element by parser") };
for i in main {
eval(i, &mut env)?;
}
eval(tail, &mut env)?
} else {
return err(format!("Need {} args, got {}", l.0.len(), args.len()));
}
}
}
}
Tree::Define(name, to) => {
let val = eval(to, env)?;
env.define(name.to_owned(), val);
Value::NotAValue
}
Tree::If(box [cond, tcase, fcase]) => {
let b = eval(cond, env)?.as_bool()?;
let body = if b { tcase } else { fcase };
eval(body, env)?
}
})
}
fn default_env() -> Env<'static> {
let mut vars = HashMap::new();
for (name, fun) in [
("*", prims::mul as Func),
("+", prims::add as Func),
("-", prims::sub as Func),
("/", prims::div as Func),
// TODO: Spec compiance
("=", prims::equals as Func),
("eq?", prims::equals as Func),
("eqv?", prims::equals as Func),
("equal?", prims::equals as Func),
("display", prims::display as Func),
("newline", prims::newline as Func),
] {
vars.insert(name.to_owned(), Value::Func(fun));
}
Env {
vars,
enclosing: None,
}
}
mod prims {
use crate::{err, RTError, Value};
// TODO: DRY +-/*
pub(crate) fn add(args: &[Value]) -> Result<Value, RTError> {
args.iter()
.map(Value::as_num)
.try_fold(0.0, |a, b| Ok(a + b?))
.map(Value::Num)
}
pub(crate) fn mul(args: &[Value]) -> Result<Value, RTError> {
args.iter()
.map(Value::as_num)
.try_fold(1.0, |a, b| Ok(a * b?))
.map(Value::Num)
}
pub(crate) fn div(args: &[Value]) -> Result<Value, RTError> {
let init = args
.get(0)
.ok_or_else(|| RTError("`div` needs at least one argument".to_owned()))?
.as_num()?;
Ok(Value::Num(if args.len() == 1 {
1.0 / init
} else {
let rest = mul(&args[1..])?.as_num().unwrap();
init / rest
}))
}
pub(crate) fn sub(args: &[Value]) -> Result<Value, RTError> {
let init = args
.get(0)
.ok_or_else(|| RTError("`sub` needs at least one argument".to_owned()))?
.as_num()?;
Ok(Value::Num(if args.len() == 1 {
-init
} else {
let rest = add(&args[1..])?.as_num().unwrap();
init - rest
}))
}
pub(crate) fn equals(args: &[Value]) -> Result<Value, RTError> {
Ok(Value::Bool(args.array_windows().all(|[l, r]| l == r)))
}
pub(crate) fn display(args: &[Value]) -> Result<Value, RTError> {
let [arg] = args else {return err("To many args to `display`".to_owned())};
print!("{:?}", arg);
Ok(Value::NotAValue)
}
pub(crate) fn newline(args: &[Value]) -> Result<Value, RTError> {
if !args.is_empty() {
return err("Newline takes no args".to_owned());
}
println!("");
Ok(Value::NotAValue)
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn simple_math_space() {
// let t = grammar::TreeParser::new()
// .parse("( + 1 2 ( / 2 3 4 5) )")
// .unwrap();
// assert_eq!(
// t,
// Tree::Add(vec![
// Tree::Val(1.0),
// Tree::Val(2.0),
// Tree::Div(vec![
// Tree::Val(2.0),
// Tree::Val(3.0),
// Tree::Val(4.0),
// Tree::Val(5.0),
// ])
// ])
// );
// }
// #[test]
// fn simple_math_dence() {
// let t = grammar::TreeParser::new()
// .parse("(+ 1 2 (/ 2 3 4 5))")
// .unwrap();
// assert_eq!(
// t,
// Tree::Add(vec![
// Tree::Val(1.0),
// Tree::Val(2.0),
// Tree::Div(vec![
// Tree::Val(2.0),
// Tree::Val(3.0),
// Tree::Val(4.0),
// Tree::Val(5.0),
// ])
// ])
// );
// }
// }
#[derive(Completer, Helper, Highlighter, Hinter)]
struct InputValidator {
brackets: MatchingBracketValidator,

124
src/prims.rs Normal file
View File

@ -0,0 +1,124 @@
use crate::eval::{err, RTError};
use crate::value::Value;
crate type Func = fn(&[Value]) -> Result<Value, RTError>;
crate fn prims() -> &'static [(&'static str, Func)] {
&[
("*", mul),
("+", add),
("-", sub),
("/", div),
// TODO: Spec compiance
("=", equals),
("eq?", equals),
("eqv?", equals),
("equal?", equals),
("display", display),
("newline", newline),
("abs", abs),
("<", lt),
(">", gt),
("<=", le),
(">=", ge),
]
}
// TODO: DRY +-/*
fn add(args: &[Value]) -> Result<Value, RTError> {
args.iter()
.map(Value::as_num)
.try_fold(0.0, |a, b| Ok(a + b?))
.map(Value::Num)
}
fn mul(args: &[Value]) -> Result<Value, RTError> {
args.iter()
.map(Value::as_num)
.try_fold(1.0, |a, b| Ok(a * b?))
.map(Value::Num)
}
fn div(args: &[Value]) -> Result<Value, RTError> {
let init = args
.get(0)
.ok_or_else(|| RTError("`div` needs at least one argument".to_owned()))?
.as_num()?;
Ok(Value::Num(if args.len() == 1 {
1.0 / init
} else {
let rest = mul(&args[1..])?.as_num().unwrap();
init / rest
}))
}
fn sub(args: &[Value]) -> Result<Value, RTError> {
let init = args
.get(0)
.ok_or_else(|| RTError("`sub` needs at least one argument".to_owned()))?
.as_num()?;
Ok(Value::Num(if args.len() == 1 {
-init
} else {
let rest = add(&args[1..])?.as_num().unwrap();
init - rest
}))
}
fn equals(args: &[Value]) -> Result<Value, RTError> {
Ok(Value::Bool(args.array_windows().all(|[l, r]| l == r)))
}
fn display(args: &[Value]) -> Result<Value, RTError> {
let [arg] = args else {return err("To many args to `display`".to_owned())};
print!("{:?}", arg);
Ok(Value::Trap)
}
fn newline(args: &[Value]) -> Result<Value, RTError> {
if !args.is_empty() {
return err("Newline takes no args".to_owned());
}
println!();
Ok(Value::Trap)
}
fn abs(args: &[Value]) -> Result<Value, RTError> {
let [v] = args else { return err("abs takes 1 arg".to_owned()) };
let ans = v.as_num()?.abs();
Ok(Value::Num(ans))
}
crate fn compare_core(
args: &[Value],
f: fn(f64, f64) -> bool,
name: &'static str,
) -> Result<Value, RTError> {
for [l, r] in args.array_windows() {
let (Value::Num(l), Value::Num(r)) = (l,r) else
{
return err(format!("`{}` args must be numbers", name));
};
// TODO: Ensure this is correct wrt NaN
if !f(*l, *r) {
return Ok(Value::Bool(false));
};
}
Ok(Value::Bool(true))
}
macro_rules! cmps {
($(($name:ident $op:tt))*) => {
$(
fn $name(args: &[Value]) -> Result<Value, RTError> {
compare_core(args, |l, r| l $op r, stringify!($op))
}
)*
};
}
cmps! { (lt <) (gt >) (le <=) (ge >=) }

View File

@ -0,0 +1,40 @@
---
source: src/tests.rs
assertion_line: 45
expression: run-pass fib.scm
---
10.0
89.0
9.0
55.0
8.0
34.0
7.0
21.0
6.0
13.0
5.0
8.0
4.0
5.0
3.0
3.0
2.0
2.0
1.0
1.0
0.0
1.0

View File

@ -0,0 +1,10 @@
---
source: src/tests.rs
assertion_line: 47
expression: run-pass funcs.scm
---
5.0
11.0
14.0

View File

@ -0,0 +1,21 @@
---
source: src/tests.rs
assertion_line: 47
expression: run-pass lambda-calc.scm
---
1.0
0.0
-1.0
0.0
1.0
1.0
0.0
0.0
0.0
1.0
1.0
1.0
0.0
1.0

View File

@ -0,0 +1,17 @@
---
source: src/tests.rs
assertion_line: 47
expression: run-pass math.scm
---
0.0
1.0
10.0
-1.0
-8.0
7.0
2.0
1.0
0.1
1.0

View File

@ -0,0 +1,7 @@
---
source: src/tests.rs
assertion_line: 45
expression: run-pass multi.scm
---
3.0

View File

@ -0,0 +1,7 @@
---
source: src/tests.rs
assertion_line: 41
expression: run-pass nested-defs.scm
---
1.4142156862745097

View File

@ -0,0 +1,7 @@
---
source: src/tests.rs
assertion_line: 41
expression: run-pass sqrt.scm
---
1.4142156862745097

21
src/test/run-pass/fib.scm Normal file
View File

@ -0,0 +1,21 @@
#lang scheme
(define (displayln x) (display x) (newline))
(define (my-or a b) (if a #t b)) ; Test was written before we had `or`
(define (fib-base? n) (my-or (= n 1) (= n 0)))
(define (fib n)
(if (fib-base? n) 1
(+ (fib (- n 1)) (fib (- n 2)))))
(define (pfib n) (displayln n) (displayln (fib n)) (newline))
(define (pfibs n)
(if (= n (- 1)) 0 ; Hack
((lambda () (pfib n) (pfibs (- n 1)))))) ; Hack
(pfibs 10)

View File

@ -0,0 +1,12 @@
#lang scheme
(define (displayln x) (display x) (newline))
(define add1 +)
(displayln (add1 2 3))
(define add2 (lambda (a b) (+ a b)))
(displayln (add2 5 6))
(define (add3 a b) (+ a b))
(displayln (add3 5 9))

View File

@ -1,3 +1,5 @@
#lang scheme
(define (displayln x) (display x) (newline))
(define (printbool x) (displayln (bool->int x)))
@ -9,8 +11,8 @@
(define (bool->int x)
(if
(equal? x true) 1
(if (equal? x false) 0 (- 1))))
(equal? x true) 1
(if (equal? x false) 0 (- 1))))
(printbool true)
(printbool false)

View File

@ -1,14 +1,15 @@
(define (displayln x) (display x) (newline))
(define (displayln x)
(display x)
(newline))
(displayln (+))
(displayln (+ 1))
(displayln (+ 1 2 3 4))
(displayln (+))
(displayln (- 1))
(displayln (- 1 2 3 4))
(displayln (* 1))
(displayln (* 7))
(displayln (* 1 2))
(displayln (*))

View File

@ -0,0 +1,5 @@
#lang scheme
(define (multi) 1 2 3)
(display (multi))

View File

@ -0,0 +1,20 @@
#lang scheme
; We dont have closures at the time of writing, but each func gets its own env
; http://sarabander.github.io/sicp/html/1_002e1.xhtml#g_t1_002e1_002e7
(define (sqrt x)
(define (sqrt-iter guess x)
(define (good-enough? guess x)
(define (square x) (* x x))
(< (abs (- (square guess) x)) 0.001))
(define (improve guess x)
(define (average x y)
(/ (+ x y) 2))
(average guess (/ x guess)))
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x) x)))
(sqrt-iter 1.0 x))
(display (sqrt 2))

View File

@ -1,3 +1,5 @@
;; TODO: Enable when ready
(display (real? 2.5+0.0i))
(newline)
(display (real? 2.5+0i))

View File

@ -0,0 +1,25 @@
#lang scheme
; We dont have closures at the time of writing, but each func gets its own env
; http://sarabander.github.io/sicp/html/1_002e1.xhtml#g_t1_002e1_002e7
(define (sqrt-iter guess x)
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x) x)))
(define (improve guess x)
(average guess (/ x guess)))
(define (average x y)
(/ (+ x y) 2))
(define (square x) (* x x))
(define (good-enough? guess x)
(< (abs (- (square guess) x)) 0.001))
(define (sqrt x)
(sqrt-iter 1.0 x))
(display (sqrt 2))

47
src/tests.rs Normal file
View File

@ -0,0 +1,47 @@
use std::path::{Path, PathBuf};
// use fs_err as fs;
/*
# Why are the tests in src
the way insta glob works is it finds the dir the file is in, and then
looks at all children, and then checks if they have a match. This means we
cant use .. in the glob, as parents arnt generated.
A potential solution would be to update insta to use CARGO_MAINIFEST_DIR
as the base, and add file!() to the glob, but I'd need to update insta
upstream for this
*/
#[test]
fn run_pass() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
insta::glob!("test/run-pass/**.scm", |p| {
let p = PathBuf::from(
p.canonicalize()
.unwrap()
.to_str()
.unwrap()
.replace("\\\\?\\", ""), // Work around IDK on windows
);
let cmd = assert_cmd::Command::cargo_bin("handball")
.unwrap()
.arg(&p)
.assert()
.success();
let p = p.strip_prefix(manifest_dir).unwrap();
let p = p.strip_prefix("src/test/run-pass").unwrap();
let p = p.to_str().unwrap();
assert_eq!(cmd.get_output().stderr, Vec::<u8>::new());
insta::assert_snapshot!(
"run-pass",
String::from_utf8(cmd.get_output().stdout.clone()).unwrap(),
&format!("run-pass {}", p)
);
})
}

72
src/value.rs Normal file
View File

@ -0,0 +1,72 @@
use crate::{ast, eval, prims};
use std::rc::Rc;
#[derive(Clone)]
crate enum Value {
Num(f64),
Func(prims::Func),
Lambda(ast::Lambda),
Bool(bool),
/// Result of things that shouldnt have values, like (define x 3)
Trap,
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
use Value::*;
match (self, other) {
(Num(l), Num(r)) => l == r,
(Func(l), Func(r)) => *l as usize == *r as usize,
(Bool(l), Bool(r)) => l == r,
(Lambda(l), Lambda(r)) => Rc::ptr_eq(&l.0, &r.0) && Rc::ptr_eq(&l.1, &r.1),
(Num(_), _) => false,
(Func(_), _) => false,
(Bool(_), _) => false,
(Lambda(_), _) => false,
(Trap, _) => panic!("Trap value"),
}
}
}
impl std::fmt::Debug for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Num(n) => n.fmt(f),
Self::Func(_) => f.write_str("#<procedure>"),
Self::Lambda(_) => f.write_str("#<procedure>"),
Self::Bool(b) => f.write_str(if *b { "#t" } else { "#f" }),
Self::Trap => Ok(()),
}
}
}
impl Value {
crate fn as_num(&self) -> Result<f64, eval::RTError> {
if let Self::Num(n) = self {
Ok(*n)
} else {
Err(eval::RTError("Expected a number".to_owned()))
}
}
crate fn as_func(&self) -> Result<eval::Callable, eval::RTError> {
match self {
Self::Func(f) => Ok(eval::Callable::Func(*f)),
Self::Lambda(l) => Ok(eval::Callable::Lambda(l)),
_ => Err(eval::RTError(format!(
"Expected a function, got {:?}",
self
))),
}
}
crate fn as_bool(&self) -> Result<bool, eval::RTError> {
if let Self::Bool(b) = self {
Ok(*b)
} else {
Err(eval::RTError("Expected a bool".to_owned()))
}
}
}