🤔 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:
@@ -87,9 +87,9 @@ mod tests {
|
||||
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_eq!(tester.lookup(arced("foo")), Ok(1i64.into()));
|
||||
assert_eq!(tester.lookup(arced("bar")), Ok(2i64.into()));
|
||||
assert_eq!(tester.lookup(arced("goo")), Ok(5i64.into()));
|
||||
assert!(tester.lookup(arced("baz")).is_err());
|
||||
}
|
||||
|
||||
@@ -103,14 +103,14 @@ mod tests {
|
||||
|
||||
check_nested(&tester);
|
||||
|
||||
assert_eq!(tester.lookup(arced("foo")), Ok(1.into()));
|
||||
assert_eq!(tester.lookup(arced("foo")), Ok(1i64.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()));
|
||||
assert_eq!(nested_env.lookup(arced("foo")), Ok(1i64.into()));
|
||||
assert_eq!(nested_env.lookup(arced("bar")), Ok(2i64.into()));
|
||||
}
|
||||
|
||||
fn arced(s: &str) -> ArcIntern<String> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::eval::primtype::PrimitiveType;
|
||||
use crate::eval::value::Value;
|
||||
|
||||
/// Errors that can occur running primitive operations in the evaluators.
|
||||
@@ -22,6 +23,13 @@ pub enum PrimOpError {
|
||||
BadArgCount(String, usize),
|
||||
#[error("Unknown primitive operation {0}")]
|
||||
UnknownPrimOp(String),
|
||||
#[error("Unsafe cast from {from} to {to}")]
|
||||
UnsafeCast {
|
||||
from: PrimitiveType,
|
||||
to: PrimitiveType,
|
||||
},
|
||||
#[error("Unknown primitive type {0}")]
|
||||
UnknownPrimType(String),
|
||||
}
|
||||
|
||||
// Implementing primitives in an interpreter like this is *super* tedious,
|
||||
@@ -37,39 +45,95 @@ pub enum PrimOpError {
|
||||
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),
|
||||
"+" => Ok($left.wrapping_add($right).into()),
|
||||
"-" => Ok($left.wrapping_sub($right).into()),
|
||||
"*" => Ok($left.wrapping_mul($right).into()),
|
||||
"/" if $right == 0 => Err(PrimOpError::MathFailure("/")),
|
||||
"/" => Ok($left.wrapping_div($right).into()),
|
||||
_ => Err(PrimOpError::UnknownPrimOp($op.to_string())),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Value {
|
||||
fn unary_op(operation: &str, value: &Value) -> Result<Value, PrimOpError> {
|
||||
match operation {
|
||||
"-" => match value {
|
||||
Value::I8(x) => Ok(Value::I8(x.wrapping_neg())),
|
||||
Value::I16(x) => Ok(Value::I16(x.wrapping_neg())),
|
||||
Value::I32(x) => Ok(Value::I32(x.wrapping_neg())),
|
||||
Value::I64(x) => Ok(Value::I64(x.wrapping_neg())),
|
||||
_ => Err(PrimOpError::BadTypeFor("-", value.clone())),
|
||||
},
|
||||
_ => Err(PrimOpError::BadArgCount(operation.to_owned(), 1)),
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_op(operation: &str, left: &Value, right: &Value) -> Result<Value, PrimOpError> {
|
||||
match left {
|
||||
// for now we only have one type, but in the future this is
|
||||
// going to be very irritating.
|
||||
Value::I8(x) => match right {
|
||||
Value::I8(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::I16(x) => match right {
|
||||
Value::I16(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::I32(x) => match right {
|
||||
Value::I32(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::I64(x) => match right {
|
||||
Value::I64(y) => run_op!(operation, x, *y),
|
||||
// _ => Err(PrimOpError::TypeMismatch(
|
||||
// operation.to_string(),
|
||||
// left.clone(),
|
||||
// right.clone(),
|
||||
// )),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::U8(x) => match right {
|
||||
Value::U8(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::U16(x) => match right {
|
||||
Value::U16(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::U32(x) => match right {
|
||||
Value::U32(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
Value::U64(x) => match right {
|
||||
Value::U64(y) => run_op!(operation, x, *y),
|
||||
_ => Err(PrimOpError::TypeMismatch(
|
||||
operation.to_string(),
|
||||
left.clone(),
|
||||
right.clone(),
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -83,13 +147,10 @@ impl Value {
|
||||
/// its worth being careful to make sure that your inputs won't cause either
|
||||
/// condition.
|
||||
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(),
|
||||
))
|
||||
match values.len() {
|
||||
1 => Value::unary_op(operation, &values[0]),
|
||||
2 => Value::binary_op(operation, &values[0], &values[1]),
|
||||
x => Err(PrimOpError::BadArgCount(operation.to_string(), x)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
173
src/eval/primtype.rs
Normal file
173
src/eval/primtype.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use crate::{
|
||||
eval::{PrimOpError, Value},
|
||||
syntax::ConstantType,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum PrimitiveType {
|
||||
U8,
|
||||
U16,
|
||||
U32,
|
||||
U64,
|
||||
I8,
|
||||
I16,
|
||||
I32,
|
||||
I64,
|
||||
}
|
||||
|
||||
impl Display for PrimitiveType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PrimitiveType::I8 => write!(f, "i8"),
|
||||
PrimitiveType::I16 => write!(f, "i16"),
|
||||
PrimitiveType::I32 => write!(f, "i32"),
|
||||
PrimitiveType::I64 => write!(f, "i64"),
|
||||
PrimitiveType::U8 => write!(f, "u8"),
|
||||
PrimitiveType::U16 => write!(f, "u16"),
|
||||
PrimitiveType::U32 => write!(f, "u32"),
|
||||
PrimitiveType::U64 => write!(f, "u64"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Value> for PrimitiveType {
|
||||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
Value::I8(_) => PrimitiveType::I8,
|
||||
Value::I16(_) => PrimitiveType::I16,
|
||||
Value::I32(_) => PrimitiveType::I32,
|
||||
Value::I64(_) => PrimitiveType::I64,
|
||||
Value::U8(_) => PrimitiveType::U8,
|
||||
Value::U16(_) => PrimitiveType::U16,
|
||||
Value::U32(_) => PrimitiveType::U32,
|
||||
Value::U64(_) => PrimitiveType::U64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConstantType> for PrimitiveType {
|
||||
fn from(value: ConstantType) -> Self {
|
||||
match value {
|
||||
ConstantType::I8 => PrimitiveType::I8,
|
||||
ConstantType::I16 => PrimitiveType::I16,
|
||||
ConstantType::I32 => PrimitiveType::I32,
|
||||
ConstantType::I64 => PrimitiveType::I64,
|
||||
ConstantType::U8 => PrimitiveType::U8,
|
||||
ConstantType::U16 => PrimitiveType::U16,
|
||||
ConstantType::U32 => PrimitiveType::U32,
|
||||
ConstantType::U64 => PrimitiveType::U64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PrimitiveType {
|
||||
type Err = PrimOpError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"i8" => Ok(PrimitiveType::I8),
|
||||
"i16" => Ok(PrimitiveType::I16),
|
||||
"i32" => Ok(PrimitiveType::I32),
|
||||
"i64" => Ok(PrimitiveType::I64),
|
||||
"u8" => Ok(PrimitiveType::U8),
|
||||
"u16" => Ok(PrimitiveType::U16),
|
||||
"u32" => Ok(PrimitiveType::U32),
|
||||
"u64" => Ok(PrimitiveType::U64),
|
||||
_ => Err(PrimOpError::UnknownPrimType(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType {
|
||||
/// Return true if this type can be safely cast into the target type.
|
||||
pub fn can_cast_to(&self, target: &PrimitiveType) -> bool {
|
||||
match self {
|
||||
PrimitiveType::U8 => matches!(
|
||||
target,
|
||||
PrimitiveType::U8
|
||||
| PrimitiveType::U16
|
||||
| PrimitiveType::U32
|
||||
| PrimitiveType::U64
|
||||
| PrimitiveType::I16
|
||||
| PrimitiveType::I32
|
||||
| PrimitiveType::I64
|
||||
),
|
||||
PrimitiveType::U16 => matches!(
|
||||
target,
|
||||
PrimitiveType::U16
|
||||
| PrimitiveType::U32
|
||||
| PrimitiveType::U64
|
||||
| PrimitiveType::I32
|
||||
| PrimitiveType::I64
|
||||
),
|
||||
PrimitiveType::U32 => matches!(
|
||||
target,
|
||||
PrimitiveType::U32 | PrimitiveType::U64 | PrimitiveType::I64
|
||||
),
|
||||
PrimitiveType::U64 => target == &PrimitiveType::U64,
|
||||
PrimitiveType::I8 => matches!(
|
||||
target,
|
||||
PrimitiveType::I8 | PrimitiveType::I16 | PrimitiveType::I32 | PrimitiveType::I64
|
||||
),
|
||||
PrimitiveType::I16 => matches!(
|
||||
target,
|
||||
PrimitiveType::I16 | PrimitiveType::I32 | PrimitiveType::I64
|
||||
),
|
||||
PrimitiveType::I32 => matches!(target, PrimitiveType::I32 | PrimitiveType::I64),
|
||||
PrimitiveType::I64 => target == &PrimitiveType::I64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to cast the given value to this type, returning the new value.
|
||||
///
|
||||
/// Returns an error if the cast is not safe *in* *general*. This means that
|
||||
/// this function will error even if the number will actually fit in the target
|
||||
/// type, but it would not be generally safe to cast a member of the given
|
||||
/// type to the target type. (So, for example, "1i64" is a number that could
|
||||
/// work as a "u64", but since negative numbers wouldn't work, a cast from
|
||||
/// "1i64" to "u64" will fail.)
|
||||
pub fn safe_cast(&self, source: &Value) -> Result<Value, PrimOpError> {
|
||||
match (self, source) {
|
||||
(PrimitiveType::U8, Value::U8(x)) => Ok(Value::U8(*x)),
|
||||
(PrimitiveType::U16, Value::U8(x)) => Ok(Value::U16(*x as u16)),
|
||||
(PrimitiveType::U16, Value::U16(x)) => Ok(Value::U16(*x)),
|
||||
(PrimitiveType::U32, Value::U8(x)) => Ok(Value::U32(*x as u32)),
|
||||
(PrimitiveType::U32, Value::U16(x)) => Ok(Value::U32(*x as u32)),
|
||||
(PrimitiveType::U32, Value::U32(x)) => Ok(Value::U32(*x)),
|
||||
(PrimitiveType::U64, Value::U8(x)) => Ok(Value::U64(*x as u64)),
|
||||
(PrimitiveType::U64, Value::U16(x)) => Ok(Value::U64(*x as u64)),
|
||||
(PrimitiveType::U64, Value::U32(x)) => Ok(Value::U64(*x as u64)),
|
||||
(PrimitiveType::U64, Value::U64(x)) => Ok(Value::U64(*x)),
|
||||
|
||||
(PrimitiveType::I8, Value::I8(x)) => Ok(Value::I8(*x)),
|
||||
(PrimitiveType::I16, Value::I8(x)) => Ok(Value::I16(*x as i16)),
|
||||
(PrimitiveType::I16, Value::I16(x)) => Ok(Value::I16(*x)),
|
||||
(PrimitiveType::I32, Value::I8(x)) => Ok(Value::I32(*x as i32)),
|
||||
(PrimitiveType::I32, Value::I16(x)) => Ok(Value::I32(*x as i32)),
|
||||
(PrimitiveType::I32, Value::I32(x)) => Ok(Value::I32(*x)),
|
||||
(PrimitiveType::I64, Value::I8(x)) => Ok(Value::I64(*x as i64)),
|
||||
(PrimitiveType::I64, Value::I16(x)) => Ok(Value::I64(*x as i64)),
|
||||
(PrimitiveType::I64, Value::I32(x)) => Ok(Value::I64(*x as i64)),
|
||||
(PrimitiveType::I64, Value::I64(x)) => Ok(Value::I64(*x)),
|
||||
|
||||
_ => Err(PrimOpError::UnsafeCast {
|
||||
from: source.into(),
|
||||
to: *self,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_value(&self) -> u64 {
|
||||
match self {
|
||||
PrimitiveType::U8 => u8::MAX as u64,
|
||||
PrimitiveType::U16 => u16::MAX as u64,
|
||||
PrimitiveType::U32 => u32::MAX as u64,
|
||||
PrimitiveType::U64 => u64::MAX,
|
||||
PrimitiveType::I8 => i8::MAX as u64,
|
||||
PrimitiveType::I16 => i16::MAX as u64,
|
||||
PrimitiveType::I32 => i32::MAX as u64,
|
||||
PrimitiveType::I64 => i64::MAX as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,75 @@ use std::fmt::Display;
|
||||
/// by type so that we don't mix them up.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Value {
|
||||
I8(i8),
|
||||
I16(i16),
|
||||
I32(i32),
|
||||
I64(i64),
|
||||
U8(u8),
|
||||
U16(u16),
|
||||
U32(u32),
|
||||
U64(u64),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::I8(x) => write!(f, "{}i8", x),
|
||||
Value::I16(x) => write!(f, "{}i16", x),
|
||||
Value::I32(x) => write!(f, "{}i32", x),
|
||||
Value::I64(x) => write!(f, "{}i64", x),
|
||||
Value::U8(x) => write!(f, "{}u8", x),
|
||||
Value::U16(x) => write!(f, "{}u16", x),
|
||||
Value::U32(x) => write!(f, "{}u32", x),
|
||||
Value::U64(x) => write!(f, "{}u64", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i8> for Value {
|
||||
fn from(value: i8) -> Self {
|
||||
Value::I8(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Value {
|
||||
fn from(value: i16) -> Self {
|
||||
Value::I16(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(value: i32) -> Self {
|
||||
Value::I32(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Self {
|
||||
Value::I64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Value {
|
||||
fn from(value: u8) -> Self {
|
||||
Value::U8(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Value {
|
||||
fn from(value: u16) -> Self {
|
||||
Value::U16(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value {
|
||||
fn from(value: u32) -> Self {
|
||||
Value::U32(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(value: u64) -> Self {
|
||||
Value::U64(value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user