🤔 Add a type inference engine, along with typed literals. (#4)

The typed literal formatting mirrors that of Rust. If no type can be
inferred for an untagged literal, the type inference engine will warn
the user and then assume that they meant an unsigned 64-bit number.
(This is slightly inconvenient, because there can be cases in which our
Arbitrary instance may generate a unary negation, in which we should
assume that it's a signed 64-bit number; we may want to revisit this
later.)

The type inference engine is a standard two phase one, in which we first
generate a series of type constraints, and then we solve those
constraints. In this particular implementation, we actually use a third
phase to generate a final AST.

Finally, to increase the amount of testing performed, I've removed the
overflow checking in the evaluator. The only thing we now check for is
division by zero. This does make things a trace slower in testing, but
hopefully we get more coverage this way.
This commit was merged in pull request #4.
This commit is contained in:
2023-09-19 20:40:05 -07:00
committed by GitHub
parent 1fbfd0c2d2
commit bd3b9af469
44 changed files with 3258 additions and 702 deletions

View File

@@ -1,7 +1,7 @@
use crate::eval::{EvalEnvironment, EvalError, Value};
use crate::ir::{Expression, Program, Statement};
use super::{Primitive, ValueOrRef};
use super::{Primitive, Type, ValueOrRef};
impl Program {
/// Evaluate the program, returning either an error or a string containing everything
@@ -14,12 +14,12 @@ impl Program {
for stmt in self.statements.iter() {
match stmt {
Statement::Binding(_, name, value) => {
Statement::Binding(_, name, _, value) => {
let actual_value = value.eval(&env)?;
env = env.extend(name.clone(), actual_value);
}
Statement::Print(_, name) => {
Statement::Print(_, _, name) => {
let value = env.lookup(name.clone())?;
let line = format!("{} = {}\n", name, value);
stdout.push_str(&line);
@@ -34,26 +34,21 @@ impl Program {
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::Atomic(x) => x.eval(env),
Expression::Reference(_, n) => Ok(env.lookup(n.clone())?),
Expression::Cast(_, t, valref) => {
let value = valref.eval(env)?;
Expression::Primitive(_, op, args) => {
let mut arg_values = Vec::with_capacity(args.len());
// we implement primitive operations by first evaluating each of the
// arguments to the function, and then gathering up all the values
// produced.
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 t {
Type::Primitive(pt) => Ok(pt.safe_cast(&value)?),
}
}
Expression::Primitive(_, _, op, args) => {
let arg_values = args
.iter()
.map(|x| x.eval(env))
.collect::<Result<Vec<Value>, EvalError>>()?;
// and then finally we call `calculate` to run them. trust me, it's nice
// to not have to deal with all the nonsense hidden under `calculate`.
@@ -68,19 +63,38 @@ impl Expression {
}
}
impl ValueOrRef {
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
match self {
ValueOrRef::Value(_, _, v) => match v {
super::Value::I8(_, v) => Ok(Value::I8(*v)),
super::Value::I16(_, v) => Ok(Value::I16(*v)),
super::Value::I32(_, v) => Ok(Value::I32(*v)),
super::Value::I64(_, v) => Ok(Value::I64(*v)),
super::Value::U8(_, v) => Ok(Value::U8(*v)),
super::Value::U16(_, v) => Ok(Value::U16(*v)),
super::Value::U32(_, v) => Ok(Value::U32(*v)),
super::Value::U64(_, v) => Ok(Value::U64(*v)),
},
ValueOrRef::Ref(_, _, n) => Ok(env.lookup(n.clone())?),
}
}
}
#[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);
let ir = input.type_infer().expect("test should be type-valid");
let output = ir.eval().expect("runs successfully");
assert_eq!("x = 5i64\n", &output);
assert_eq!("x = 5u64\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);
let ir = input.type_infer().expect("test should be type-valid");
let output = ir.eval().expect("runs successfully");
assert_eq!("x = 7i64\n", &output);
assert_eq!("x = 7u64\n", &output);
}