🧪 Add evaluation tests to ensure that passes retain NGR semantics. #2
@@ -26,6 +26,7 @@ pretty = { version = "^0.11.2", features = ["termcolor"] }
|
|||||||
proptest = "^1.0.0"
|
proptest = "^1.0.0"
|
||||||
rustyline = "^11.0.0"
|
rustyline = "^11.0.0"
|
||||||
target-lexicon = "^0.12.5"
|
target-lexicon = "^0.12.5"
|
||||||
|
tempfile = "^3.5.0"
|
||||||
thiserror = "^1.0.30"
|
thiserror = "^1.0.30"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
|
||||||
void print(char *variable_name, uint64_t value) {
|
void print(char *_ignore, char *variable_name, int64_t value) {
|
||||||
printf("%s = %llu\n", variable_name, value);
|
printf("%s = %" PRId64 "i64\n", variable_name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void caller() {
|
void caller() {
|
||||||
print("x", 4);
|
print(NULL, "x", 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern void gogogo();
|
extern void gogogo();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod error;
|
mod error;
|
||||||
|
mod eval;
|
||||||
mod into_crane;
|
mod into_crane;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ use cranelift_codegen::settings::Configurable;
|
|||||||
use cranelift_codegen::{isa, settings};
|
use cranelift_codegen::{isa, settings};
|
||||||
use cranelift_jit::{JITBuilder, JITModule};
|
use cranelift_jit::{JITBuilder, JITModule};
|
||||||
use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module};
|
use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module};
|
||||||
use cranelift_object::{object, ObjectBuilder, ObjectModule};
|
use cranelift_object::{ObjectBuilder, ObjectModule};
|
||||||
use target_lexicon::Triple;
|
use target_lexicon::Triple;
|
||||||
|
|
||||||
const EMPTY_DATUM: [u8; 8] = [0; 8];
|
const EMPTY_DATUM: [u8; 8] = [0; 8];
|
||||||
@@ -21,10 +22,11 @@ pub struct Backend<M: Module> {
|
|||||||
runtime_functions: RuntimeFunctions,
|
runtime_functions: RuntimeFunctions,
|
||||||
defined_strings: HashMap<String, DataId>,
|
defined_strings: HashMap<String, DataId>,
|
||||||
defined_symbols: HashMap<String, DataId>,
|
defined_symbols: HashMap<String, DataId>,
|
||||||
|
output_buffer: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend<JITModule> {
|
impl Backend<JITModule> {
|
||||||
pub fn jit() -> Result<Self, BackendError> {
|
pub fn jit(output_buffer: Option<String>) -> Result<Self, BackendError> {
|
||||||
let platform = Triple::host();
|
let platform = Triple::host();
|
||||||
let isa_builder = isa::lookup(platform.clone())?;
|
let isa_builder = isa::lookup(platform.clone())?;
|
||||||
let mut settings_builder = settings::builder();
|
let mut settings_builder = settings::builder();
|
||||||
@@ -44,6 +46,7 @@ impl Backend<JITModule> {
|
|||||||
runtime_functions,
|
runtime_functions,
|
||||||
defined_strings: HashMap::new(),
|
defined_strings: HashMap::new(),
|
||||||
defined_symbols: HashMap::new(),
|
defined_symbols: HashMap::new(),
|
||||||
|
output_buffer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,23 +72,26 @@ impl Backend<ObjectModule> {
|
|||||||
runtime_functions,
|
runtime_functions,
|
||||||
defined_strings: HashMap::new(),
|
defined_strings: HashMap::new(),
|
||||||
defined_symbols: HashMap::new(),
|
defined_symbols: HashMap::new(),
|
||||||
|
output_buffer: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bytes(self) -> Result<Vec<u8>, object::write::Error> {
|
pub fn bytes(self) -> Result<Vec<u8>, BackendError> {
|
||||||
self.module.finish().emit()
|
self.module.finish().emit().map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Module> Backend<M> {
|
impl<M: Module> Backend<M> {
|
||||||
pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> {
|
pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> {
|
||||||
let name = format!("<string_constant>{}", s);
|
let name = format!("<string_constant>{}", s);
|
||||||
|
let s0 = format!("{}\0", s);
|
||||||
|
|
||||||
let global_id = self
|
let global_id = self
|
||||||
.module
|
.module
|
||||||
.declare_data(&name, Linkage::Local, false, false)?;
|
.declare_data(&name, Linkage::Local, false, false)?;
|
||||||
let mut data_context = DataContext::new();
|
let mut data_context = DataContext::new();
|
||||||
data_context.set_align(8);
|
data_context.set_align(8);
|
||||||
data_context.define(s.to_owned().into_boxed_str().into_boxed_bytes());
|
data_context.define(s0.into_boxed_str().into_boxed_bytes());
|
||||||
self.module.define_data(global_id, &data_context)?;
|
self.module.define_data(global_id, &data_context)?;
|
||||||
self.defined_strings.insert(s.to_owned(), global_id);
|
self.defined_strings.insert(s.to_owned(), global_id);
|
||||||
Ok(global_id)
|
Ok(global_id)
|
||||||
@@ -101,4 +107,20 @@ impl<M: Module> Backend<M> {
|
|||||||
self.defined_symbols.insert(name, id);
|
self.defined_symbols.insert(name, id);
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_buffer_ptr(&mut self) -> *mut String {
|
||||||
|
if let Some(str) = self.output_buffer.as_mut() {
|
||||||
|
str as *mut String
|
||||||
|
} else {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output(self) -> String {
|
||||||
|
if let Some(s) = self.output_buffer {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub enum BackendError {
|
|||||||
SetError(#[from] SetError),
|
SetError(#[from] SetError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
LookupError(#[from] LookupError),
|
LookupError(#[from] LookupError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Write(#[from] cranelift_object::object::write::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BackendError> for Diagnostic<usize> {
|
impl From<BackendError> for Diagnostic<usize> {
|
||||||
@@ -41,6 +43,41 @@ impl From<BackendError> for Diagnostic<usize> {
|
|||||||
BackendError::LookupError(me) => {
|
BackendError::LookupError(me) => {
|
||||||
Diagnostic::error().with_message(format!("Internal error: {}", me))
|
Diagnostic::error().with_message(format!("Internal error: {}", me))
|
||||||
}
|
}
|
||||||
|
BackendError::Write(me) => {
|
||||||
|
Diagnostic::error().with_message(format!("Cranelift object write error: {}", me))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BackendError {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match self {
|
||||||
|
BackendError::BuiltinError(a) => match other {
|
||||||
|
BackendError::BuiltinError(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
BackendError::CodegenError(_) => matches!(other, BackendError::CodegenError(_)),
|
||||||
|
|
||||||
|
BackendError::Cranelift(_) => matches!(other, BackendError::Cranelift(_)),
|
||||||
|
|
||||||
|
BackendError::LookupError(a) => match other {
|
||||||
|
BackendError::LookupError(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
BackendError::SetError(a) => match other {
|
||||||
|
BackendError::SetError(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
BackendError::VariableLookupFailure => other == &BackendError::VariableLookupFailure,
|
||||||
|
|
||||||
|
BackendError::Write(a) => match other {
|
||||||
|
BackendError::Write(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
106
src/backend/eval.rs
Normal file
106
src/backend/eval.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::eval::EvalError;
|
||||||
|
use crate::ir::Program;
|
||||||
|
use cranelift_jit::JITModule;
|
||||||
|
use cranelift_object::ObjectModule;
|
||||||
|
use target_lexicon::Triple;
|
||||||
|
|
||||||
|
impl Backend<JITModule> {
|
||||||
|
pub fn eval(program: Program) -> Result<String, EvalError> {
|
||||||
|
let mut jitter = Backend::jit(Some(String::new()))?;
|
||||||
|
let function_id = jitter.compile_function("test", program)?;
|
||||||
|
jitter.module.finalize_definitions()?;
|
||||||
|
let compiled_bytes = jitter.bytes(function_id);
|
||||||
|
let compiled_function = unsafe { std::mem::transmute::<_, fn() -> ()>(compiled_bytes) };
|
||||||
|
compiled_function();
|
||||||
|
Ok(jitter.output())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend<ObjectModule> {
|
||||||
|
pub fn eval(program: Program) -> Result<String, EvalError> {
|
||||||
|
//use pretty::{Arena, Pretty};
|
||||||
|
//let allocator = Arena::<()>::new();
|
||||||
|
//program.pretty(&allocator).render(80, &mut std::io::stdout())?;
|
||||||
|
|
||||||
|
let mut backend = Self::object_file(Triple::host())?;
|
||||||
|
let my_directory = tempfile::tempdir()?;
|
||||||
|
let object_path = my_directory.path().join("object.o");
|
||||||
|
let executable_path = my_directory.path().join("test_executable");
|
||||||
|
|
||||||
|
backend.compile_function("gogogo", program)?;
|
||||||
|
let bytes = backend.bytes()?;
|
||||||
|
std::fs::write(&object_path, bytes)?;
|
||||||
|
Self::link(&object_path, &executable_path)?;
|
||||||
|
let output = std::process::Command::new(executable_path).output()?;
|
||||||
|
|
||||||
|
if output.stderr.is_empty() {
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(std::string::String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
} else {
|
||||||
|
Err(EvalError::IO(format!(
|
||||||
|
"Exitted with error code {}",
|
||||||
|
output.status
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(EvalError::IO(
|
||||||
|
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link(object_file: &Path, executable_path: &Path) -> Result<(), EvalError> {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
let output = std::process::Command::new("clang")
|
||||||
|
.arg(
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("runtime")
|
||||||
|
.join("rts.c"),
|
||||||
|
)
|
||||||
|
.arg(object_file)
|
||||||
|
.arg("-o")
|
||||||
|
.arg(executable_path)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.stderr.is_empty() {
|
||||||
|
return Err(EvalError::IO(
|
||||||
|
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest::proptest! {
|
||||||
|
#[test]
|
||||||
|
fn file_backend_works(program: Program) {
|
||||||
|
use crate::eval::PrimOpError;
|
||||||
|
|
||||||
|
let basic_result = program.eval();
|
||||||
|
|
||||||
|
#[cfg(target_family="windows")]
|
||||||
|
let basic_result = basic_result.map(|x| x.replace('\n', "\r\n"));
|
||||||
|
|
||||||
|
if !matches!(basic_result, Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))) {
|
||||||
|
let compiled_result = Backend::<ObjectModule>::eval(program);
|
||||||
|
assert_eq!(basic_result, compiled_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jit_backend_works(program: Program) {
|
||||||
|
use crate::eval::PrimOpError;
|
||||||
|
|
||||||
|
let basic_result = program.eval();
|
||||||
|
|
||||||
|
if !matches!(basic_result, Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))) {
|
||||||
|
let compiled_result = Backend::<JITModule>::eval(program);
|
||||||
|
assert_eq!(basic_result, compiled_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,8 @@ impl<M: Module> Backend<M> {
|
|||||||
for stmt in program.statements.drain(..) {
|
for stmt in program.statements.drain(..) {
|
||||||
match stmt {
|
match stmt {
|
||||||
Statement::Print(ann, var) => {
|
Statement::Print(ann, var) => {
|
||||||
|
let buffer_ptr = self.output_buffer_ptr();
|
||||||
|
let buffer_ptr = builder.ins().iconst(types::I64, buffer_ptr as i64);
|
||||||
let local_name_ref = string_table.get(&var).unwrap();
|
let local_name_ref = string_table.get(&var).unwrap();
|
||||||
let name_ptr = builder.ins().symbol_value(types::I64, *local_name_ref);
|
let name_ptr = builder.ins().symbol_value(types::I64, *local_name_ref);
|
||||||
let val = ValueOrRef::Ref(ann, var).into_cranelift(
|
let val = ValueOrRef::Ref(ann, var).into_cranelift(
|
||||||
@@ -67,7 +69,9 @@ impl<M: Module> Backend<M> {
|
|||||||
&variable_table,
|
&variable_table,
|
||||||
&pre_defined_symbols,
|
&pre_defined_symbols,
|
||||||
)?;
|
)?;
|
||||||
builder.ins().call(print_func_ref, &[name_ptr, val]);
|
builder
|
||||||
|
.ins()
|
||||||
|
.call(print_func_ref, &[buffer_ptr, name_ptr, val]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Statement::Binding(_, var_name, value) => {
|
Statement::Binding(_, var_name, value) => {
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
struct BackendObject {
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackendObject {
|
|
||||||
pub fn new() -> Result<Self, ()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ use cranelift_jit::JITBuilder;
|
|||||||
use cranelift_module::{FuncId, Linkage, Module, ModuleResult};
|
use cranelift_module::{FuncId, Linkage, Module, ModuleResult};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
|
use std::fmt::Write;
|
||||||
use target_lexicon::Triple;
|
use target_lexicon::Triple;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -12,16 +13,21 @@ pub struct RuntimeFunctions {
|
|||||||
_referenced_functions: Vec<String>,
|
_referenced_functions: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error, PartialEq)]
|
||||||
pub enum RuntimeFunctionError {
|
pub enum RuntimeFunctionError {
|
||||||
#[error("Could not find runtime function named '{0}'")]
|
#[error("Could not find runtime function named '{0}'")]
|
||||||
CannotFindRuntimeFunction(String),
|
CannotFindRuntimeFunction(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn runtime_print(name: *const i8, value: u64) {
|
extern "C" fn runtime_print(output_buffer: *mut String, name: *const i8, value: i64) {
|
||||||
let cstr = unsafe { CStr::from_ptr(name) };
|
let cstr = unsafe { CStr::from_ptr(name) };
|
||||||
let reconstituted = cstr.to_string_lossy();
|
let reconstituted = cstr.to_string_lossy();
|
||||||
|
|
||||||
|
if let Some(output_buffer) = unsafe { output_buffer.as_mut() } {
|
||||||
|
writeln!(output_buffer, "{} = {}i64", reconstituted, value).unwrap();
|
||||||
|
} else {
|
||||||
println!("{} = {}", reconstituted, value);
|
println!("{} = {}", reconstituted, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeFunctions {
|
impl RuntimeFunctions {
|
||||||
@@ -36,7 +42,7 @@ impl RuntimeFunctions {
|
|||||||
"print",
|
"print",
|
||||||
Linkage::Import,
|
Linkage::Import,
|
||||||
&Signature {
|
&Signature {
|
||||||
params: vec![string_param, int64_param],
|
params: vec![string_param, string_param, int64_param],
|
||||||
returns: vec![],
|
returns: vec![],
|
||||||
call_conv: CallConv::triple_default(platform),
|
call_conv: CallConv::triple_default(platform),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ impl<'a> RunLoop<'a> {
|
|||||||
pub fn new(writer: &'a mut dyn WriteColor, config: Config) -> Result<Self, BackendError> {
|
pub fn new(writer: &'a mut dyn WriteColor, config: Config) -> Result<Self, BackendError> {
|
||||||
Ok(RunLoop {
|
Ok(RunLoop {
|
||||||
file_database: SimpleFiles::new(),
|
file_database: SimpleFiles::new(),
|
||||||
jitter: Backend::jit()?,
|
jitter: Backend::jit(None)?,
|
||||||
variable_binding_sites: HashMap::new(),
|
variable_binding_sites: HashMap::new(),
|
||||||
gensym_index: 1,
|
gensym_index: 1,
|
||||||
writer,
|
writer,
|
||||||
|
|||||||
61
src/eval.rs
Normal file
61
src/eval.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
mod env;
|
||||||
|
mod primop;
|
||||||
|
mod value;
|
||||||
|
|
||||||
|
use cranelift_module::ModuleError;
|
||||||
|
pub use env::{EvalEnvironment, LookupError};
|
||||||
|
pub use primop::PrimOpError;
|
||||||
|
pub use value::Value;
|
||||||
|
|
||||||
|
use crate::backend::BackendError;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum EvalError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Lookup(#[from] LookupError),
|
||||||
|
#[error(transparent)]
|
||||||
|
PrimOp(#[from] PrimOpError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Backend(#[from] BackendError),
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IO(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
Module(#[from] ModuleError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for EvalError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
EvalError::IO(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for EvalError {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match self {
|
||||||
|
EvalError::Lookup(a) => match other {
|
||||||
|
EvalError::Lookup(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
EvalError::PrimOp(a) => match other {
|
||||||
|
EvalError::PrimOp(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
EvalError::Backend(a) => match other {
|
||||||
|
EvalError::Backend(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
EvalError::IO(a) => match other {
|
||||||
|
EvalError::IO(b) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
EvalError::Module(a) => match other {
|
||||||
|
EvalError::Module(b) => a.to_string() == b.to_string(),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/eval/env.rs
Normal file
93
src/eval/env.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use crate::eval::Value;
|
||||||
|
use internment::ArcIntern;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct EvalEnvironment {
|
||||||
|
inner: Arc<EvalEnvInternal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum EvalEnvInternal {
|
||||||
|
Empty,
|
||||||
|
Value(ArcIntern<String>, Value, Arc<EvalEnvInternal>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||||
|
pub enum LookupError {
|
||||||
|
#[error("Could not find variable '{0}' in environment")]
|
||||||
|
CouldNotFind(ArcIntern<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EvalEnvironment {
|
||||||
|
fn default() -> Self {
|
||||||
|
EvalEnvironment::empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EvalEnvironment {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
EvalEnvironment {
|
||||||
|
inner: Arc::new(EvalEnvInternal::Empty),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend(&self, name: ArcIntern<String>, value: Value) -> Self {
|
||||||
|
EvalEnvironment {
|
||||||
|
inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup(&self, n: ArcIntern<String>) -> Result<Value, LookupError> {
|
||||||
|
self.inner.lookup(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EvalEnvInternal {
|
||||||
|
fn lookup(&self, n: ArcIntern<String>) -> Result<Value, LookupError> {
|
||||||
|
match self {
|
||||||
|
EvalEnvInternal::Empty => Err(LookupError::CouldNotFind(n)),
|
||||||
|
EvalEnvInternal::Value(name, value, _) if *name == n => Ok(value.clone()),
|
||||||
|
EvalEnvInternal::Value(_, _, rest) => rest.lookup(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use internment::ArcIntern;
|
||||||
|
|
||||||
|
use super::EvalEnvironment;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_lookups() {
|
||||||
|
let tester = EvalEnvironment::default();
|
||||||
|
let tester = tester.extend(arced("foo"), 1i64.into());
|
||||||
|
let tester = tester.extend(arced("bar"), 2i64.into());
|
||||||
|
let tester = tester.extend(arced("goo"), 5i64.into());
|
||||||
|
|
||||||
|
assert_eq!(tester.lookup(arced("foo")), Ok(1.into()));
|
||||||
|
assert_eq!(tester.lookup(arced("bar")), Ok(2.into()));
|
||||||
|
assert_eq!(tester.lookup(arced("goo")), Ok(5.into()));
|
||||||
|
assert!(tester.lookup(arced("baz")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested() {
|
||||||
|
let tester = EvalEnvironment::default();
|
||||||
|
let tester = tester.extend(arced("foo"), 1i64.into());
|
||||||
|
|
||||||
|
check_nested(&tester);
|
||||||
|
|
||||||
|
assert_eq!(tester.lookup(arced("foo")), Ok(1.into()));
|
||||||
|
assert!(tester.lookup(arced("bar")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_nested(env: &EvalEnvironment) {
|
||||||
|
let nested_env = env.extend(arced("bar"), 2i64.into());
|
||||||
|
assert_eq!(nested_env.lookup(arced("foo")), Ok(1.into()));
|
||||||
|
assert_eq!(nested_env.lookup(arced("bar")), Ok(2.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arced(s: &str) -> ArcIntern<String> {
|
||||||
|
ArcIntern::new(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/eval/primop.rs
Normal file
65
src/eval/primop.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::eval::value::Value;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||||
|
pub enum PrimOpError {
|
||||||
|
#[error("Math error (underflow or overflow) computing {0} operator")]
|
||||||
|
MathFailure(&'static str),
|
||||||
|
#[error("Type mismatch ({1} vs {2}) computing {0} operator")]
|
||||||
|
TypeMismatch(String, Value, Value),
|
||||||
|
#[error("Bad type for operator {0}: {1}")]
|
||||||
|
BadTypeFor(&'static str, Value),
|
||||||
|
#[error("Illegal number of arguments for {0}: {1} arguments found")]
|
||||||
|
BadArgCount(String, usize),
|
||||||
|
#[error("Unknown primitive operation {0}")]
|
||||||
|
UnknownPrimOp(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! run_op {
|
||||||
|
($op: ident, $left: expr, $right: expr) => {
|
||||||
|
match $op {
|
||||||
|
"+" => $left
|
||||||
|
.checked_add($right)
|
||||||
|
.ok_or(PrimOpError::MathFailure("+"))
|
||||||
|
.map(Into::into),
|
||||||
|
"-" => $left
|
||||||
|
.checked_sub($right)
|
||||||
|
.ok_or(PrimOpError::MathFailure("+"))
|
||||||
|
.map(Into::into),
|
||||||
|
"*" => $left
|
||||||
|
.checked_mul($right)
|
||||||
|
.ok_or(PrimOpError::MathFailure("+"))
|
||||||
|
.map(Into::into),
|
||||||
|
"/" => $left
|
||||||
|
.checked_div($right)
|
||||||
|
.ok_or(PrimOpError::MathFailure("+"))
|
||||||
|
.map(Into::into),
|
||||||
|
_ => Err(PrimOpError::UnknownPrimOp($op.to_string())),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
fn binary_op(operation: &str, left: &Value, right: &Value) -> Result<Value, PrimOpError> {
|
||||||
|
match left {
|
||||||
|
Value::I64(x) => match right {
|
||||||
|
Value::I64(y) => run_op!(operation, x, *y),
|
||||||
|
// _ => Err(PrimOpError::TypeMismatch(
|
||||||
|
// operation.to_string(),
|
||||||
|
// left.clone(),
|
||||||
|
// right.clone(),
|
||||||
|
// )),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(operation: &str, values: Vec<Value>) -> Result<Value, PrimOpError> {
|
||||||
|
if values.len() == 2 {
|
||||||
|
Value::binary_op(operation, &values[0], &values[1])
|
||||||
|
} else {
|
||||||
|
Err(PrimOpError::BadArgCount(
|
||||||
|
operation.to_string(),
|
||||||
|
values.len(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/eval/value.rs
Normal file
20
src/eval/value.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Value {
|
||||||
|
I64(i64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Value {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Value::I64(x) => write!(f, "{}i64", x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for Value {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
Value::I64(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod ast;
|
mod ast;
|
||||||
|
mod eval;
|
||||||
mod from_syntax;
|
mod from_syntax;
|
||||||
mod strings;
|
mod strings;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use internment::ArcIntern;
|
use internment::ArcIntern;
|
||||||
use pretty::{DocAllocator, Pretty};
|
use pretty::{DocAllocator, Pretty};
|
||||||
|
use proptest::{
|
||||||
|
prelude::Arbitrary,
|
||||||
|
strategy::{BoxedStrategy, Strategy},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::syntax::Location;
|
use crate::syntax::Location;
|
||||||
|
|
||||||
type Variable = ArcIntern<String>;
|
type Variable = ArcIntern<String>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Program {
|
pub struct Program {
|
||||||
pub statements: Vec<Statement>,
|
pub statements: Vec<Statement>,
|
||||||
}
|
}
|
||||||
@@ -28,6 +33,18 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Arbitrary for Program {
|
||||||
|
type Parameters = ();
|
||||||
|
type Strategy = BoxedStrategy<Self>;
|
||||||
|
|
||||||
|
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
|
||||||
|
crate::syntax::Program::arbitrary_with(args)
|
||||||
|
.prop_map(|x| Program::from(x.simplify()))
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Statement {
|
pub enum Statement {
|
||||||
Binding(Location, Variable, Expression),
|
Binding(Location, Variable, Expression),
|
||||||
Print(Location, Variable),
|
Print(Location, Variable),
|
||||||
@@ -54,6 +71,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Expression {
|
pub enum Expression {
|
||||||
Value(Location, Value),
|
Value(Location, Value),
|
||||||
Reference(Location, Variable),
|
Reference(Location, Variable),
|
||||||
@@ -126,6 +144,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ValueOrRef {
|
pub enum ValueOrRef {
|
||||||
Value(Location, Value),
|
Value(Location, Value),
|
||||||
Ref(Location, ArcIntern<String>),
|
Ref(Location, ArcIntern<String>),
|
||||||
@@ -144,6 +163,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Number(Option<u8>, i64),
|
Number(Option<u8>, i64),
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/ir/eval.rs
Normal file
77
src/ir/eval.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use crate::eval::{EvalEnvironment, EvalError, Value};
|
||||||
|
use crate::ir::{Expression, Program, Statement};
|
||||||
|
|
||||||
|
use super::{Primitive, ValueOrRef};
|
||||||
|
|
||||||
|
impl Program {
|
||||||
|
pub fn eval(&self) -> Result<String, EvalError> {
|
||||||
|
let mut env = EvalEnvironment::empty();
|
||||||
|
let mut stdout = String::new();
|
||||||
|
|
||||||
|
for stmt in self.statements.iter() {
|
||||||
|
match stmt {
|
||||||
|
Statement::Binding(_, name, value) => {
|
||||||
|
let actual_value = value.eval(&env)?;
|
||||||
|
env = env.extend(name.clone(), actual_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Print(_, name) => {
|
||||||
|
let value = env.lookup(name.clone())?;
|
||||||
|
let line = format!("{} = {}\n", name, value);
|
||||||
|
stdout.push_str(&line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expression {
|
||||||
|
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
|
||||||
|
match self {
|
||||||
|
Expression::Value(_, v) => match v {
|
||||||
|
super::Value::Number(_, v) => Ok(Value::I64(*v)),
|
||||||
|
},
|
||||||
|
|
||||||
|
Expression::Reference(_, n) => Ok(env.lookup(n.clone())?),
|
||||||
|
|
||||||
|
Expression::Primitive(_, op, args) => {
|
||||||
|
let mut arg_values = Vec::with_capacity(args.len());
|
||||||
|
|
||||||
|
for arg in args.iter() {
|
||||||
|
match arg {
|
||||||
|
ValueOrRef::Ref(_, n) => arg_values.push(env.lookup(n.clone())?),
|
||||||
|
ValueOrRef::Value(_, super::Value::Number(_, v)) => {
|
||||||
|
arg_values.push(Value::I64(*v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match op {
|
||||||
|
Primitive::Plus => Ok(Value::calculate("+", arg_values)?),
|
||||||
|
Primitive::Minus => Ok(Value::calculate("-", arg_values)?),
|
||||||
|
Primitive::Times => Ok(Value::calculate("*", arg_values)?),
|
||||||
|
Primitive::Divide => Ok(Value::calculate("/", arg_values)?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_plus_three() {
|
||||||
|
let input = crate::syntax::Program::parse(0, "x = 2 + 3; print x;").expect("parse works");
|
||||||
|
let ir = Program::from(input.simplify());
|
||||||
|
let output = ir.eval().expect("runs successfully");
|
||||||
|
assert_eq!("x = 5i64\n", &output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lotsa_math() {
|
||||||
|
let input =
|
||||||
|
crate::syntax::Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works");
|
||||||
|
let ir = Program::from(input.simplify());
|
||||||
|
let output = ir.eval().expect("runs successfully");
|
||||||
|
assert_eq!("x = 7i64\n", &output);
|
||||||
|
}
|
||||||
@@ -71,3 +71,13 @@ impl From<syntax::Value> for ir::Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proptest::proptest! {
|
||||||
|
#[test]
|
||||||
|
fn translation_maintains_semantics(input: syntax::Program) {
|
||||||
|
let syntax_result = input.eval();
|
||||||
|
let ir = ir::Program::from(input.simplify());
|
||||||
|
let ir_result = ir.eval();
|
||||||
|
assert_eq!(syntax_result, ir_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod backend;
|
pub mod backend;
|
||||||
|
pub mod eval;
|
||||||
pub mod ir;
|
pub mod ir;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use logos::Logos;
|
|||||||
|
|
||||||
mod arbitrary;
|
mod arbitrary;
|
||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
mod eval;
|
||||||
mod location;
|
mod location;
|
||||||
mod simplify;
|
mod simplify;
|
||||||
mod tokens;
|
mod tokens;
|
||||||
@@ -269,4 +270,10 @@ proptest::proptest! {
|
|||||||
let (errors, _) = program.validate();
|
let (errors, _) = program.validate();
|
||||||
prop_assert!(errors.is_empty());
|
prop_assert!(errors.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_run_or_overflow(program: Program) {
|
||||||
|
use crate::eval::{EvalError, PrimOpError};
|
||||||
|
assert!(matches!(program.eval(), Ok(_) | Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/syntax/eval.rs
Normal file
64
src/syntax/eval.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use internment::ArcIntern;
|
||||||
|
|
||||||
|
use crate::eval::{EvalEnvironment, EvalError, Value};
|
||||||
|
use crate::syntax::{Expression, Program, Statement};
|
||||||
|
|
||||||
|
impl Program {
|
||||||
|
pub fn eval(&self) -> Result<String, EvalError> {
|
||||||
|
let mut env = EvalEnvironment::empty();
|
||||||
|
let mut stdout = String::new();
|
||||||
|
|
||||||
|
for stmt in self.statements.iter() {
|
||||||
|
match stmt {
|
||||||
|
Statement::Binding(_, name, value) => {
|
||||||
|
let actual_value = value.eval(&env)?;
|
||||||
|
env = env.extend(ArcIntern::new(name.clone()), actual_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Print(_, name) => {
|
||||||
|
let value = env.lookup(ArcIntern::new(name.clone()))?;
|
||||||
|
let line = format!("{} = {}\n", name, value);
|
||||||
|
stdout.push_str(&line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expression {
|
||||||
|
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
|
||||||
|
match self {
|
||||||
|
Expression::Value(_, v) => match v {
|
||||||
|
super::Value::Number(_, v) => Ok(Value::I64(*v)),
|
||||||
|
},
|
||||||
|
|
||||||
|
Expression::Reference(_, n) => Ok(env.lookup(ArcIntern::new(n.clone()))?),
|
||||||
|
|
||||||
|
Expression::Primitive(_, op, args) => {
|
||||||
|
let mut arg_values = Vec::with_capacity(args.len());
|
||||||
|
|
||||||
|
for arg in args.iter() {
|
||||||
|
arg_values.push(arg.eval(env)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::calculate(op, arg_values)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_plus_three() {
|
||||||
|
let input = Program::parse(0, "x = 2 + 3; print x;").expect("parse works");
|
||||||
|
let output = input.eval().expect("runs successfully");
|
||||||
|
assert_eq!("x = 5i64\n", &output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lotsa_math() {
|
||||||
|
let input = Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works");
|
||||||
|
let output = input.eval().expect("runs successfully");
|
||||||
|
assert_eq!("x = 7i64\n", &output);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user