Add support for casting.

This commit is contained in:
2023-06-17 15:10:16 -07:00
parent b4ad24030f
commit 041c0a497e
15 changed files with 355 additions and 10 deletions

7
examples/basic/cast1.ngr Normal file
View File

@@ -0,0 +1,7 @@
x8 = 5u8;
x16 = <u16>x8 + 1u16;
print x16;
x32 = <u32>x16 + 1u32;
print x32;
x64 = <u64>x32 + 1u64;
print x64;

7
examples/basic/cast2.ngr Normal file
View File

@@ -0,0 +1,7 @@
x8 = 5i8;
x16 = <i16>x8 - 1i16;
print x16;
x32 = <i32>x16 - 1i32;
print x32;
x64 = <i64>x32 - 1i64;
print x64;

View File

@@ -1,4 +1,4 @@
use crate::backend::runtime::RuntimeFunctionError;
use crate::{backend::runtime::RuntimeFunctionError, eval::PrimitiveType, ir::Type};
use codespan_reporting::diagnostic::Diagnostic;
use cranelift_codegen::{isa::LookupError, settings::SetError, CodegenError};
use cranelift_module::ModuleError;
@@ -39,6 +39,8 @@ pub enum BackendError {
LookupError(#[from] LookupError),
#[error(transparent)]
Write(#[from] cranelift_object::object::write::Error),
#[error("Invalid type cast from {from} to {to}")]
InvalidTypeCast { from: PrimitiveType, to: Type },
}
impl From<BackendError> for Diagnostic<usize> {
@@ -64,6 +66,9 @@ impl From<BackendError> for Diagnostic<usize> {
BackendError::Write(me) => {
Diagnostic::error().with_message(format!("Cranelift object write error: {}", me))
}
BackendError::InvalidTypeCast { from, to } => Diagnostic::error().with_message(
format!("Internal error trying to cast from {} to {}", from, to),
),
}
}
}
@@ -103,6 +108,17 @@ impl PartialEq for BackendError {
BackendError::Write(b) => a == b,
_ => false,
},
BackendError::InvalidTypeCast {
from: from1,
to: to1,
} => match other {
BackendError::InvalidTypeCast {
from: from2,
to: to2,
} => from1 == from2 && to1 == to2,
_ => false,
},
}
}
}

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap;
use crate::ir::{Expression, Primitive, Program, Statement, Value, ValueOrRef};
use crate::eval::PrimitiveType;
use crate::ir::{Expression, Primitive, Program, Statement, Type, Value, ValueOrRef};
use crate::syntax::ConstantType;
use cranelift_codegen::entity::EntityRef;
use cranelift_codegen::ir::{
@@ -307,6 +308,70 @@ impl Expression {
Err(BackendError::VariableLookupFailure(name))
}
Expression::Cast(_, target_type, expr) => {
let (val, val_type) =
expr.into_crane(builder, local_variables, global_variables)?;
match (val_type, &target_type) {
(ConstantType::I8, Type::Primitive(PrimitiveType::I8)) => Ok((val, val_type)),
(ConstantType::I8, Type::Primitive(PrimitiveType::I16)) => {
Ok((builder.ins().sextend(types::I16, val), ConstantType::I16))
}
(ConstantType::I8, Type::Primitive(PrimitiveType::I32)) => {
Ok((builder.ins().sextend(types::I32, val), ConstantType::I32))
}
(ConstantType::I8, Type::Primitive(PrimitiveType::I64)) => {
Ok((builder.ins().sextend(types::I64, val), ConstantType::I64))
}
(ConstantType::I16, Type::Primitive(PrimitiveType::I16)) => Ok((val, val_type)),
(ConstantType::I16, Type::Primitive(PrimitiveType::I32)) => {
Ok((builder.ins().sextend(types::I32, val), ConstantType::I32))
}
(ConstantType::I16, Type::Primitive(PrimitiveType::I64)) => {
Ok((builder.ins().sextend(types::I64, val), ConstantType::I64))
}
(ConstantType::I32, Type::Primitive(PrimitiveType::I32)) => Ok((val, val_type)),
(ConstantType::I32, Type::Primitive(PrimitiveType::I64)) => {
Ok((builder.ins().sextend(types::I64, val), ConstantType::I64))
}
(ConstantType::I64, Type::Primitive(PrimitiveType::I64)) => Ok((val, val_type)),
(ConstantType::U8, Type::Primitive(PrimitiveType::U8)) => Ok((val, val_type)),
(ConstantType::U8, Type::Primitive(PrimitiveType::U16)) => {
Ok((builder.ins().uextend(types::I16, val), ConstantType::U16))
}
(ConstantType::U8, Type::Primitive(PrimitiveType::U32)) => {
Ok((builder.ins().uextend(types::I32, val), ConstantType::U32))
}
(ConstantType::U8, Type::Primitive(PrimitiveType::U64)) => {
Ok((builder.ins().uextend(types::I64, val), ConstantType::U64))
}
(ConstantType::U16, Type::Primitive(PrimitiveType::U16)) => Ok((val, val_type)),
(ConstantType::U16, Type::Primitive(PrimitiveType::U32)) => {
Ok((builder.ins().uextend(types::I32, val), ConstantType::U32))
}
(ConstantType::U16, Type::Primitive(PrimitiveType::U64)) => {
Ok((builder.ins().uextend(types::I64, val), ConstantType::U64))
}
(ConstantType::U32, Type::Primitive(PrimitiveType::U32)) => Ok((val, val_type)),
(ConstantType::U32, Type::Primitive(PrimitiveType::U64)) => {
Ok((builder.ins().uextend(types::I64, val), ConstantType::U64))
}
(ConstantType::U64, Type::Primitive(PrimitiveType::U64)) => Ok((val, val_type)),
_ => Err(BackendError::InvalidTypeCast {
from: val_type.into(),
to: target_type,
}),
}
}
Expression::Primitive(_, prim, mut vals) => {
let mut values = vec![];
let mut first_type = None;

View File

@@ -17,7 +17,7 @@ fn main() {
let args = CommandLineArguments::parse();
let mut compiler = ngr::Compiler::default();
let output_file = args.output.unwrap_or("output.o".to_string());
let output_file = args.output.unwrap_or_else(|| "output.o".to_string());
if let Some(bytes) = compiler.compile(&args.file) {
std::fs::write(&output_file, bytes)

View File

@@ -35,11 +35,13 @@
//!
mod env;
mod primop;
mod primtype;
mod value;
use cranelift_module::ModuleError;
pub use env::{EvalEnvironment, LookupError};
pub use primop::PrimOpError;
pub use primtype::PrimitiveType;
pub use value::Value;
use crate::backend::BackendError;

View File

@@ -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,

121
src/eval/primtype.rs Normal file
View File

@@ -0,0 +1,121 @@
use crate::{
eval::{PrimOpError, Value},
syntax::ConstantType,
};
use std::{fmt::Display, str::FromStr};
#[derive(Clone, Copy, Debug, Eq, 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 {
/// 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,
}),
}
}
}

View File

@@ -1,10 +1,14 @@
use crate::syntax::{self, ConstantType, Location};
use crate::{
eval::PrimitiveType,
syntax::{self, ConstantType, Location},
};
use internment::ArcIntern;
use pretty::{DocAllocator, Pretty};
use proptest::{
prelude::Arbitrary,
strategy::{BoxedStrategy, Strategy},
};
use std::fmt;
/// We're going to represent variables as interned strings.
///
@@ -115,6 +119,7 @@ where
pub enum Expression {
Value(Location, Value),
Reference(Location, Variable),
Cast(Location, Type, ValueOrRef),
Primitive(Location, Primitive, Vec<ValueOrRef>),
}
@@ -127,6 +132,11 @@ where
match self {
Expression::Value(_, val) => val.pretty(allocator),
Expression::Reference(_, var) => allocator.text(var.as_ref().to_string()),
Expression::Cast(_, t, e) => allocator
.text("<")
.append(t.pretty(allocator))
.append(allocator.text(">"))
.append(e.pretty(allocator)),
Expression::Primitive(_, op, exprs) if exprs.len() == 1 => {
op.pretty(allocator).append(exprs[0].pretty(allocator))
}
@@ -283,3 +293,28 @@ where
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum Type {
Primitive(PrimitiveType),
}
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Type
where
A: 'a,
D: ?Sized + DocAllocator<'a, A>,
{
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
match self {
Type::Primitive(pt) => allocator.text(format!("{}", pt)),
}
}
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Type::Primitive(pt) => pt.fmt(f),
}
}
}

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
@@ -47,6 +47,19 @@ impl Expression {
Expression::Reference(_, n) => Ok(env.lookup(n.clone())?),
Expression::Cast(_, t, valref) => {
let value = match valref {
ValueOrRef::Ref(_, n) => env.lookup(n.clone())?,
ValueOrRef::Value(loc, val) => {
Expression::Value(loc.clone(), val.clone()).eval(env)?
}
};
match t {
Type::Primitive(pt) => Ok(pt.safe_cast(&value)?),
}
}
Expression::Primitive(_, op, args) => {
let mut arg_values = Vec::with_capacity(args.len());

View File

@@ -1,6 +1,8 @@
use internment::ArcIntern;
use std::str::FromStr;
use std::sync::atomic::AtomicUsize;
use crate::eval::PrimitiveType;
use crate::ir::ast as ir;
use crate::syntax;
@@ -104,7 +106,20 @@ impl syntax::Expression {
(vec![], ValueOrRef::Ref(loc, ArcIntern::new(name)))
}
syntax::Expression::Cast(_, _, _) => unimplemented!(),
syntax::Expression::Cast(loc, t, expr) => {
let (mut prereqs, new_expr) = expr.rebind(base_name);
let new_name = gensym(base_name);
prereqs.push(ir::Statement::Binding(
loc.clone(),
new_name.clone(),
ir::Expression::Cast(
loc.clone(),
ir::Type::Primitive(PrimitiveType::from_str(&t).unwrap()),
new_expr,
),
));
(prereqs, ValueOrRef::Ref(loc, new_name))
}
// Primitive expressions are where we do the real work.
syntax::Expression::Primitive(loc, prim, mut expressions) => {

View File

@@ -124,7 +124,30 @@ impl Arbitrary for Expression {
Union::new([value_strategy, reference_strategy]).boxed()
};
let cast_strategy = if let Some(bigger_type) = target_type {
let mut smaller_types = bigger_type.safe_casts_to();
if smaller_types.is_empty() {
leaf_strategy
} else {
let duplicated_env = defined_variables.clone();
let cast_exp = |t, e| Expression::Cast(Location::manufactured(), t, Box::new(e));
let smaller_strats: Vec<BoxedStrategy<Expression>> = smaller_types
.drain(..)
.map(|t| {
Expression::arbitrary_with((Some(duplicated_env.clone()), Some(t)))
.prop_map(move |e| cast_exp(t.name(), e))
.boxed()
})
.collect();
Union::new(smaller_strats).boxed()
}
} else {
leaf_strategy
};
cast_strategy
.prop_recursive(3, 64, 2, move |inner| {
(select(OPERATORS), proptest::collection::vec(inner, 2)).prop_map(
move |((operator, arg_count), mut exprs)| {

View File

@@ -1,7 +1,8 @@
use internment::ArcIntern;
use crate::eval::{EvalEnvironment, EvalError, Value};
use crate::eval::{EvalEnvironment, EvalError, PrimitiveType, Value};
use crate::syntax::{ConstantType, Expression, Program, Statement};
use std::str::FromStr;
impl Program {
/// Evaluate the program, returning either an error or what it prints out when run.
@@ -59,7 +60,11 @@ impl Expression {
Expression::Reference(_, n) => Ok(env.lookup(ArcIntern::new(n.clone()))?),
Expression::Cast(_, _, _) => unimplemented!(),
Expression::Cast(_, target, expr) => {
let target_type = PrimitiveType::from_str(target)?;
let value = expr.eval(env)?;
Ok(target_type.safe_cast(&value)?)
}
Expression::Primitive(_, op, args) => {
let mut arg_values = Vec::with_capacity(args.len());

View File

@@ -175,6 +175,34 @@ impl ConstantType {
ConstantType::I8 | ConstantType::I16 | ConstantType::I32 | ConstantType::I64
)
}
/// Return the set of types that can be safely casted into this type.
pub fn safe_casts_to(self) -> Vec<ConstantType> {
match self {
ConstantType::I8 => vec![],
ConstantType::I16 => vec![ConstantType::I8],
ConstantType::I32 => vec![ConstantType::I16, ConstantType::I8],
ConstantType::I64 => vec![ConstantType::I32, ConstantType::I16, ConstantType::I8],
ConstantType::U8 => vec![],
ConstantType::U16 => vec![ConstantType::U8],
ConstantType::U32 => vec![ConstantType::U16, ConstantType::U8],
ConstantType::U64 => vec![ConstantType::U32, ConstantType::U16, ConstantType::U8],
}
}
/// Return the name of the given type, as a string
pub fn name(&self) -> String {
match self {
ConstantType::I8 => "i8".to_string(),
ConstantType::I16 => "i16".to_string(),
ConstantType::I32 => "i32".to_string(),
ConstantType::I64 => "i64".to_string(),
ConstantType::U8 => "u8".to_string(),
ConstantType::U16 => "u16".to_string(),
ConstantType::U32 => "u32".to_string(),
ConstantType::U64 => "u64".to_string(),
}
}
}
#[derive(Debug, Error, PartialEq)]

View File

@@ -127,7 +127,7 @@ impl Expression {
vec![Error::UnboundVariable(loc.clone(), var.clone())],
vec![],
),
Expression::Cast(_, _, _) => unimplemented!(),
Expression::Cast(_, _, expr) => expr.validate(variable_map),
Expression::Primitive(_, _, args) => {
let mut errors = vec![];
let mut warnings = vec![];