diff --git a/Cargo.lock b/Cargo.lock index 32f059a00d..d631578ca5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,11 @@ name = "difference" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lazy_static" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "maplit" version = "1.0.1" @@ -29,6 +34,7 @@ dependencies = [ name = "roc" version = "0.1.0" dependencies = [ + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -55,6 +61,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" "checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43" "checksum pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a029430f0d744bc3d15dd474d591bed2402b645d024583082b9f63bb936dac6" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" diff --git a/Cargo.toml b/Cargo.toml index 51cd1d3a9a..ac841fd2b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Richard Feldman "] [dependencies] +lazy_static = "1.2.0" [dev-dependencies] pretty_assertions="0.5.1" diff --git a/src/interpret.rs b/src/interpret.rs index 618b6ba6c1..524176e5c0 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -4,13 +4,24 @@ use unify::Literal; pub fn eval<'a>(expr: &'a Expr<'a>) -> &'a Literal<'a> { match expr { Expr::Literal(literal) => literal, - Expr::Assignment(_, subexpr) => eval(subexpr) + Expr::Assignment(_, subexpr) => eval(subexpr), + Expr::If(cond, if_true, if_false) => { + match eval(cond) { + Literal::Symbol("True") => eval(if_true), + Literal::Symbol("False") => eval(if_false), + _ => { + panic!("somehow an if-conditional did not evaluate to True or False!") + } + } + } } } pub fn literal_to_string<'a>(literal: &'a Literal<'a>) -> String { match literal { Literal::String(str) => format!("\"{}\"", str), + Literal::Symbol(str) => str.to_string(), + Literal::Number(str) => str.to_string(), Literal::Record(fields) => { let mut field_strings = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 9f9d88c31b..3abce68762 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(box_patterns)] + pub mod unify; pub mod interpret; pub mod repl; diff --git a/src/repl.rs b/src/repl.rs index 44b45e423f..95004490b1 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,35 +1,42 @@ use interpret::{eval, literal_to_string}; use unify::infer; use unify::Expr; -use unify::Field; use unify::Type; pub fn eval_and_print<'a>(expr: &Expr<'a>) -> String { - let lit = eval(expr); - let typ = infer(&Expr::Literal(lit)); + match infer(&expr) { + Ok(typ) => { + let lit = eval(expr); - format!("{}\n: {}", literal_to_string(lit), type_to_string(typ)) + format!("{}\n: {}", literal_to_string(lit), type_to_string(&typ)) + }, + Err(_) => + "[TYPE MISMATCH!]".to_string() + } } -pub fn type_to_string<'a>(typ: Type<'a>) -> String { +pub fn type_to_string<'a>(typ: &'a Type<'a>) -> String { match typ { Type::String => "String".to_string(), + Type::Int => "Int".to_string(), + Type::Float => "Float".to_string(), + Type::Number => "Int | Float".to_string(), + Type::Symbol(sym) => format!(":{}", sym), Type::Record(fields) => { - let mut field_strings = Vec::new(); - let mut field_pairs = fields.into_iter().collect::, Type<'a>)>>(); - - // Sort record fields alphabetically in the type - field_pairs.sort_unstable_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap()); - - for (field, subtyp) in field_pairs { + let field_strings = fields.into_iter().map(|(field, subtyp)| { let typ_str = type_to_string(subtyp); - field_strings.push(format!("{} : {}", field, typ_str)); - } + format!("{} : {}", field, typ_str) + }); - format!("{{ {} }}", field_strings.join(", ")) + format!("{{ {} }}", field_strings.collect::>().join(", ")) + } + Type::Assignment(_, assigned_typ) => type_to_string(assigned_typ), + Type::Union(set) => { + set.into_iter().collect::>>().into_iter().map(|typ_in_set| { + type_to_string(typ_in_set) + }).collect::>().join(" | |") } - Type::Assignment(_, typ) => type_to_string(*typ), } } diff --git a/src/unify.rs b/src/unify.rs index 48a1da05f1..3d858960c0 100644 --- a/src/unify.rs +++ b/src/unify.rs @@ -1,55 +1,19 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; -use std::cmp::Ordering; +use std::collections::BTreeSet; pub type Ident<'a> = &'a str; pub type Field<'a> = &'a str; -#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Type<'a> { String, + Int, + Float, + Number, Symbol(&'a str), Record(Vec<(Field<'a>, Type<'a>)>), Assignment(Ident<'a>, Box>), - Union(TypeSet<'a>), -} - -#[derive(Debug)] -struct TypeSet<'a>(HashSet>); - -impl<'a> PartialOrd for TypeSet<'a> { - fn partial_cmp(&self, other: &TypeSet<'a>) -> Option { - Some(self.cmp(other)) - } -} - -impl<'a> Ord for TypeSet<'a> { - fn cmp(&self, other: &TypeSet<'a>) -> Ordering { - panic!("TypeSet does not support ordering or equality. Do not use them!"); - } -} - -impl<'a> PartialEq for TypeSet<'a> { - fn eq(&self, other: &TypeSet) -> bool { - match (self, other) { - (TypeSet(my_set), TypeSet(other_set)) => { - // Delegate to HashSet's eq - my_set == other_set - } - } - } -} - -impl<'a> Eq for TypeSet<'a> {} - -impl<'a> Hash for TypeSet<'a> { - fn hash(&self, state: &mut H) { - panic!("at the dissco"); - // self.id.hash(state); - // self.phone.hash(state); - } + Union(BTreeSet>), } // CANONICAL IR - we have already done stuff like giving errors for @@ -68,31 +32,83 @@ pub enum Expr<'a> { #[derive(Debug, PartialEq)] pub enum Literal<'a> { String(&'a str), + Number(&'a str), Symbol(&'a str), Record(Vec<(Field<'a>, &'a Expr<'a>)>) } -pub fn infer<'a>(expr: &Expr<'a>) -> Result, ()> { +pub fn infer<'a>(expr: &Expr<'a>) -> Result, UnificationProblem> { match expr { Expr::Literal(Literal::String(_)) => Ok(Type::String), + Expr::Literal(Literal::Number(_)) => Ok(Type::Number), Expr::Literal(Literal::Symbol(sym)) => Ok(Type::Symbol(sym)), Expr::Literal(Literal::Record(fields)) => { - let mut rec_type: HashMap<&'a str, Type<'a>> = HashMap::new(); + let mut rec_type: Vec<(&'a str, Type<'a>)> = Vec::new(); for (field, subexpr) in fields { let field_type = infer(subexpr)?; - rec_type.insert(&field, field_type); + rec_type.push((&field, field_type)); } Ok(Type::Record(rec_type)) }, - Expr::If(cond, expr_if_true, expr_if_false) => { - panic!("TODO union the types of expr_if_true and expr_if_false"); + Expr::If(box cond, expr_if_true, expr_if_false) => { + let cond_type = infer(&cond)?; + + // if-conditionals must be of type Bool + if !matches_bool_type(&cond_type) { + return Err(UnificationProblem::IfConditionNotBool); + } + + // unify the true and false branches + let true_type = infer(&expr_if_true)?; + let false_type = infer(&expr_if_false)?; + + let mut unified_type = BTreeSet::new(); + + unified_type.insert(true_type); + unified_type.insert(false_type); + + if unified_type.len() == 1 { + // No point in storing a union of 1. + // + // We can't reuse true_type because it's been moved into the set + // but we can pull it back out of the set + Ok(unified_type.into_iter().next().unwrap()) + } else { + Ok(Type::Union(unified_type)) + } }, Expr::Assignment(ident, subexpr) => { Ok(Type::Assignment(ident, Box::new(infer(subexpr)?))) } } } + +const TRUE_SYMBOL_STR: &'static str = "True"; +const FALSE_SYMBOL_STR: &'static str = "False"; + +pub fn matches_bool_type<'a>(candidate: &Type<'a>) -> bool { + match candidate { + Type::Symbol(str) => { + str == &TRUE_SYMBOL_STR || str == &FALSE_SYMBOL_STR + } + Type::Union(types) => { + types.len() <= 2 && types.iter().all(|typ| matches_bool_type(typ)) + } + _ => { + false + } + } +} + +#[derive(Debug)] +pub enum UnificationProblem { + CannotUnifyAssignments, + NotMemberOfUnion, + IfConditionNotBool, + SymbolMismatch +} + diff --git a/tests/test_repl.rs b/tests/test_repl.rs index c177ab15e7..83e178ecaa 100644 --- a/tests/test_repl.rs +++ b/tests/test_repl.rs @@ -19,7 +19,7 @@ mod repl_tests { #[test] fn test_record_literal() { - let expected = "{ string = \"abc\", record = { x = \"one\", y = \"two\" } }\n: { record : { x : String, y : String }, string : String }"; + let expected = "{ string = \"abc\", record = { x = \"one\", y = \"two\" } }\n: { string : String, record : { x : String, y : String } }"; let str0 = &String("abc"); let str1 = &String("one"); diff --git a/tests/test_unify.rs b/tests/test_unify.rs index 48f9f9c3c6..ada3a9cf74 100644 --- a/tests/test_unify.rs +++ b/tests/test_unify.rs @@ -1,4 +1,3 @@ -#[macro_use] extern crate maplit; #[macro_use] extern crate pretty_assertions; extern crate roc; @@ -27,14 +26,14 @@ mod tests { let expr = Literal(literal); - let expected_type = Type::Record(hashmap!{ - "string" => Type::String, - "record" => Type::Record(hashmap!{ - "x" => Type::String, - "y" => Type::String, - }) - }); + let expected_type = Type::Record(vec![ + ("string", Type::String), + ("record", Type::Record(vec![ + ("x", Type::String), + ("y", Type::String) + ])) + ]); - assert_eq!(expected_type, infer(&expr)); + assert_eq!(expected_type, infer(&expr).unwrap()); } }