🤔 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:
197
src/ir/ast.rs
197
src/ir/ast.rs
@@ -1,10 +1,14 @@
|
||||
use crate::syntax::Location;
|
||||
use crate::{
|
||||
eval::PrimitiveType,
|
||||
syntax::{self, ConstantType, Location},
|
||||
};
|
||||
use internment::ArcIntern;
|
||||
use pretty::{DocAllocator, Pretty};
|
||||
use pretty::{BoxAllocator, DocAllocator, Pretty};
|
||||
use proptest::{
|
||||
prelude::Arbitrary,
|
||||
strategy::{BoxedStrategy, Strategy},
|
||||
};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
/// We're going to represent variables as interned strings.
|
||||
///
|
||||
@@ -52,12 +56,15 @@ where
|
||||
}
|
||||
|
||||
impl Arbitrary for Program {
|
||||
type Parameters = ();
|
||||
type Parameters = crate::syntax::arbitrary::GenerationEnvironment;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
|
||||
crate::syntax::Program::arbitrary_with(args)
|
||||
.prop_map(Program::from)
|
||||
.prop_map(|x| {
|
||||
x.type_infer()
|
||||
.expect("arbitrary_with should generate type-correct programs")
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -74,8 +81,8 @@ impl Arbitrary for Program {
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub enum Statement {
|
||||
Binding(Location, Variable, Expression),
|
||||
Print(Location, Variable),
|
||||
Binding(Location, Variable, Type, Expression),
|
||||
Print(Location, Type, Variable),
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Statement
|
||||
@@ -85,13 +92,13 @@ where
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
Statement::Binding(_, var, expr) => allocator
|
||||
Statement::Binding(_, var, _, expr) => allocator
|
||||
.text(var.as_ref().to_string())
|
||||
.append(allocator.space())
|
||||
.append(allocator.text("="))
|
||||
.append(allocator.space())
|
||||
.append(expr.pretty(allocator)),
|
||||
Statement::Print(_, var) => allocator
|
||||
Statement::Print(_, _, var) => allocator
|
||||
.text("print")
|
||||
.append(allocator.space())
|
||||
.append(allocator.text(var.as_ref().to_string())),
|
||||
@@ -113,9 +120,32 @@ where
|
||||
/// variable reference.
|
||||
#[derive(Debug)]
|
||||
pub enum Expression {
|
||||
Value(Location, Value),
|
||||
Reference(Location, Variable),
|
||||
Primitive(Location, Primitive, Vec<ValueOrRef>),
|
||||
Atomic(ValueOrRef),
|
||||
Cast(Location, Type, ValueOrRef),
|
||||
Primitive(Location, Type, Primitive, Vec<ValueOrRef>),
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
/// Return a reference to the type of the expression, as inferred or recently
|
||||
/// computed.
|
||||
pub fn type_of(&self) -> &Type {
|
||||
match self {
|
||||
Expression::Atomic(ValueOrRef::Ref(_, t, _)) => t,
|
||||
Expression::Atomic(ValueOrRef::Value(_, t, _)) => t,
|
||||
Expression::Cast(_, t, _) => t,
|
||||
Expression::Primitive(_, t, _, _) => t,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the location associated with the expression.
|
||||
pub fn location(&self) -> &Location {
|
||||
match self {
|
||||
Expression::Atomic(ValueOrRef::Ref(l, _, _)) => l,
|
||||
Expression::Atomic(ValueOrRef::Value(l, _, _)) => l,
|
||||
Expression::Cast(l, _, _) => l,
|
||||
Expression::Primitive(l, _, _, _) => l,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Expression
|
||||
@@ -125,12 +155,16 @@ where
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
Expression::Value(_, val) => val.pretty(allocator),
|
||||
Expression::Reference(_, var) => allocator.text(var.as_ref().to_string()),
|
||||
Expression::Primitive(_, op, exprs) if exprs.len() == 1 => {
|
||||
Expression::Atomic(x) => x.pretty(allocator),
|
||||
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))
|
||||
}
|
||||
Expression::Primitive(_, op, exprs) if exprs.len() == 2 => {
|
||||
Expression::Primitive(_, _, op, exprs) if exprs.len() == 2 => {
|
||||
let left = exprs[0].pretty(allocator);
|
||||
let right = exprs[1].pretty(allocator);
|
||||
|
||||
@@ -140,7 +174,7 @@ where
|
||||
.append(right)
|
||||
.parens()
|
||||
}
|
||||
Expression::Primitive(_, op, exprs) => {
|
||||
Expression::Primitive(_, _, op, exprs) => {
|
||||
allocator.text(format!("!!{:?} with {} arguments!!", op, exprs.len()))
|
||||
}
|
||||
}
|
||||
@@ -161,10 +195,10 @@ pub enum Primitive {
|
||||
Divide,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Primitive {
|
||||
type Error = String;
|
||||
impl FromStr for Primitive {
|
||||
type Err = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"+" => Ok(Primitive::Plus),
|
||||
"-" => Ok(Primitive::Minus),
|
||||
@@ -190,15 +224,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Primitive {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
<&Primitive as Pretty<'_, BoxAllocator, ()>>::pretty(self, &BoxAllocator).render_fmt(72, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// An expression that is always either a value or a reference.
|
||||
///
|
||||
/// This is the type used to guarantee that we don't nest expressions
|
||||
/// at this level. Instead, expressions that take arguments take one
|
||||
/// of these, which can only be a constant or a reference.
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ValueOrRef {
|
||||
Value(Location, Value),
|
||||
Ref(Location, ArcIntern<String>),
|
||||
Value(Location, Type, Value),
|
||||
Ref(Location, Type, ArcIntern<String>),
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b ValueOrRef
|
||||
@@ -208,30 +248,50 @@ where
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
ValueOrRef::Value(_, v) => v.pretty(allocator),
|
||||
ValueOrRef::Ref(_, v) => allocator.text(v.as_ref().to_string()),
|
||||
ValueOrRef::Value(_, _, v) => v.pretty(allocator),
|
||||
ValueOrRef::Ref(_, _, v) => allocator.text(v.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValueOrRef> for Expression {
|
||||
fn from(value: ValueOrRef) -> Self {
|
||||
match value {
|
||||
ValueOrRef::Value(loc, val) => Expression::Value(loc, val),
|
||||
ValueOrRef::Ref(loc, var) => Expression::Reference(loc, var),
|
||||
}
|
||||
Expression::Atomic(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A constant in the IR.
|
||||
#[derive(Debug)]
|
||||
///
|
||||
/// The optional argument in numeric types is the base that was used by the
|
||||
/// user to input the number. By retaining it, we can ensure that if we need
|
||||
/// to print the number back out, we can do so in the form that the user
|
||||
/// entered it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
/// A numerical constant.
|
||||
///
|
||||
/// The optional argument is the base that was used by the user to input
|
||||
/// the number. By retaining it, we can ensure that if we need to print the
|
||||
/// number back out, we can do so in the form that the user entered it.
|
||||
Number(Option<u8>, i64),
|
||||
I8(Option<u8>, i8),
|
||||
I16(Option<u8>, i16),
|
||||
I32(Option<u8>, i32),
|
||||
I64(Option<u8>, i64),
|
||||
U8(Option<u8>, u8),
|
||||
U16(Option<u8>, u16),
|
||||
U32(Option<u8>, u32),
|
||||
U64(Option<u8>, u64),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Return the type described by this value
|
||||
pub fn type_of(&self) -> Type {
|
||||
match self {
|
||||
Value::I8(_, _) => Type::Primitive(PrimitiveType::I8),
|
||||
Value::I16(_, _) => Type::Primitive(PrimitiveType::I16),
|
||||
Value::I32(_, _) => Type::Primitive(PrimitiveType::I32),
|
||||
Value::I64(_, _) => Type::Primitive(PrimitiveType::I64),
|
||||
Value::U8(_, _) => Type::Primitive(PrimitiveType::U8),
|
||||
Value::U16(_, _) => Type::Primitive(PrimitiveType::U16),
|
||||
Value::U32(_, _) => Type::Primitive(PrimitiveType::U32),
|
||||
Value::U64(_, _) => Type::Primitive(PrimitiveType::U64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Value
|
||||
@@ -240,19 +300,64 @@ where
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
Value::Number(opt_base, value) => {
|
||||
let value_str = match opt_base {
|
||||
None => format!("{}", value),
|
||||
Some(2) => format!("0b{:b}", value),
|
||||
Some(8) => format!("0o{:o}", value),
|
||||
Some(10) => format!("0d{}", value),
|
||||
Some(16) => format!("0x{:x}", value),
|
||||
Some(_) => format!("!!{:x}!!", value),
|
||||
};
|
||||
let pretty_internal = |opt_base: &Option<u8>, x, t| {
|
||||
syntax::Value::Number(*opt_base, Some(t), x).pretty(allocator)
|
||||
};
|
||||
|
||||
allocator.text(value_str)
|
||||
let pretty_internal_signed = |opt_base, x: i64, t| {
|
||||
let base = pretty_internal(opt_base, x.unsigned_abs(), t);
|
||||
|
||||
allocator.text("-").append(base)
|
||||
};
|
||||
|
||||
match self {
|
||||
Value::I8(opt_base, value) => {
|
||||
pretty_internal_signed(opt_base, *value as i64, ConstantType::I8)
|
||||
}
|
||||
Value::I16(opt_base, value) => {
|
||||
pretty_internal_signed(opt_base, *value as i64, ConstantType::I16)
|
||||
}
|
||||
Value::I32(opt_base, value) => {
|
||||
pretty_internal_signed(opt_base, *value as i64, ConstantType::I32)
|
||||
}
|
||||
Value::I64(opt_base, value) => {
|
||||
pretty_internal_signed(opt_base, *value, ConstantType::I64)
|
||||
}
|
||||
Value::U8(opt_base, value) => {
|
||||
pretty_internal(opt_base, *value as u64, ConstantType::U8)
|
||||
}
|
||||
Value::U16(opt_base, value) => {
|
||||
pretty_internal(opt_base, *value as u64, ConstantType::U16)
|
||||
}
|
||||
Value::U32(opt_base, value) => {
|
||||
pretty_internal(opt_base, *value as u64, ConstantType::U32)
|
||||
}
|
||||
Value::U64(opt_base, value) => pretty_internal(opt_base, *value, ConstantType::U64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user