Skip to content

Commit

Permalink
Merge pull request #294 from Marwes/fixes
Browse files Browse the repository at this point in the history
fix(check): Don't leak inference variables out to type errors
  • Loading branch information
Marwes authored Jun 11, 2017
2 parents 5f3df1a + dfa3f6c commit 70478d1
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 12 deletions.
61 changes: 61 additions & 0 deletions TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,31 @@ let id x = x
in { id }
```

### Array expressions

Arrays can be constructed with array literals.

```f#,rust
// Results in an `Array Int`
[1, 2, 3, 4]
```

Since gluon is statically typed all values must be of the same type. This allows the gluon interpreter to avoid tagging each value individually which makes types such as `Array Byte` be convertible into Rust's `&[u8]` type without any allocations.

```f#
// ERROR:
// Types do not match:
// Expected: Int
// Found: String
[1, ""]
```

Functions to operate on arrays can be found on the `array` module.

```f#
array.len [1, 2, 3]
```

### Variants

While records are great for grouping related data together, there is often a need to have data which can be one of several variants. Unlike records, variants need to be defined before they can be used.
Expand Down Expand Up @@ -187,6 +212,42 @@ let Some y = Some 123
x + y
```

### Tuple expressions

Gluon also have tuple expressions for when you don't have sensible names for your fields.

```f#,rust
(1, "", 3.14) // (Int, String, 3.14)
```

Similarily to records they can be unpacked with `match` and `let`.

```f#,rust
match (1, None) with
| (x, Some y) -> x + y
| (x, None) -> x
let (a, b) = (1.0, 2.14)
a + b
```

Infact, tuples are only syntax sugar over records with fields named after numbers (`_0`, `_1`, ...) which makes the above equivalent to the following code.

```f#,rust
match { _0 = 1, _1 = None } with
| { _0 = x, _1 = Some y } -> x + y
| { _0 = x, _1 = None } -> x
let { _0 = a, _1 = b } = { _0 = 1.0, _1 = 2.14 }
a + b
```

While that example is obviously less readable the tuple syntax, the important thing to note is that tuples equivalency with records allows one to access the fields of a tuple directly without unpacking.

```f#,rust
(0, 3.14)._1 // 3.14
```

### Lambda expressions

While we have seen that functions can be defined in let expressions it is often valuable to define a function without giving it an explicit name.
Expand Down
10 changes: 10 additions & 0 deletions base/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ impl<'a, T> IntoIterator for &'a Errors<T> {
}
}

impl<'a, T> IntoIterator for &'a mut Errors<T> {
type Item = &'a mut T;

type IntoIter = slice::IterMut<'a, T>;

fn into_iter(self) -> slice::IterMut<'a, T> {
self.errors.iter_mut()
}
}

impl<T: fmt::Display> fmt::Display for Errors<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for error in &self.errors {
Expand Down
59 changes: 58 additions & 1 deletion check/src/typecheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,61 @@ impl<'a> Typecheck<'a> {
self.type_variables.exit_scope();
}

fn generalize_type_errors(&mut self, errors: &mut Error) {
self.type_variables.enter_scope();

for err in errors {
use self::TypeError::*;

match err.value {
UndefinedVariable(_) |
UndefinedType(_) |
DuplicateTypeDefinition(_) |
DuplicateField(_) |
UndefinedRecord { .. } |
EmptyCase |
ErrorExpression |
Rename(_) |
KindError(_) => (),
NotAFunction(ref mut typ) |
UndefinedField(ref mut typ, _) |
PatternError(ref mut typ, _) |
InvalidProjection(ref mut typ) => {
self.generalize_type(0, typ);
}
Unification(ref mut expected, ref mut actual, ref mut errors) => {
self.generalize_type(0, expected);
self.generalize_type(0, actual);
for err in errors {
match *err {
unify::Error::TypeMismatch(ref mut l, ref mut r) => {
self.generalize_type(0, l);
self.generalize_type(0, r);
}
unify::Error::Occurs(ref mut var, ref mut typ) => {
self.generalize_type(0, var);
self.generalize_type(0, typ);
}
unify::Error::Other(ref mut err) => {
if let unify_type::TypeError::MissingFields(ref mut typ, _) = *err {
self.generalize_type(0, typ);
}
}
}
}
}
}
}

self.type_variables.exit_scope();
}

fn generalize_type(&mut self, level: u32, typ: &mut ArcType) {
if let Some(finished) = self.finish_type(level, typ) {
*typ = finished;
}
}

/// Typecheck `expr`. If successful the type of the expression will be returned and all
/// identifiers in `expr` will be filled with the inferred type
pub fn typecheck_expr(&mut self, expr: &mut SpannedExpr<Symbol>) -> Result<ArcType, Error> {
Expand Down Expand Up @@ -437,7 +492,9 @@ impl<'a> Typecheck<'a> {
self.generalize_variables(0, &mut [], tail_expr(expr));

if self.errors.has_errors() {
Err(mem::replace(&mut self.errors, Errors::new()))
let mut errors = mem::replace(&mut self.errors, Errors::new());
self.generalize_type_errors(&mut errors);
Err(errors)
} else {
match ::rename::rename(&mut self.symbols, &self.environment, expr) {
Ok(()) => {
Expand Down
13 changes: 6 additions & 7 deletions check/src/unify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ use base::fnv::FnvMap;
use substitution::{Substitution, Substitutable, Variable};

#[derive(Debug, PartialEq)]
pub enum Error<T: Substitutable, E> {
pub enum Error<T, E> {
TypeMismatch(T, T),
Occurs(T::Variable, T),
Occurs(T, T),
Other(E),
}

impl<T, E> fmt::Display for Error<T, E>
where T: Substitutable + fmt::Display,
T::Variable: fmt::Display,
where T: fmt::Display,
E: fmt::Display
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -135,13 +134,13 @@ impl<'e, S, T> Unifier<S, T> for Unify<'e, T, T::Error>
(_, Some(r)) => {
match subs.union(r, l) {
Ok(()) => Ok(None),
Err(()) => Err(Error::Occurs(r.clone(), l.clone())),
Err(()) => Err(Error::Occurs(T::from_variable(r.clone()), l.clone())),
}
}
(Some(l), _) => {
match subs.union(l, r) {
Ok(()) => Ok(Some(r.clone())),
Err(()) => Err(Error::Occurs(l.clone(), r.clone())),
Err(()) => Err(Error::Occurs(T::from_variable(l.clone()), r.clone())),
}
}
(None, None) => {
Expand Down Expand Up @@ -358,7 +357,7 @@ mod test {
let fun = TType(Box::new(Type::Arrow(string.clone(), var1.clone())));
let result = unify(&subs, &fun, &var1);
assert_eq!(result,
Err(Errors::from(vec![Error::Occurs(*var1.get_var().unwrap(), fun.clone())])));
Err(Errors::from(vec![Error::Occurs(var1, fun.clone())])));
}

#[test]
Expand Down
6 changes: 3 additions & 3 deletions check/src/unify_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,20 +713,20 @@ impl<'a, 'e> Unifier<State<'a>, ArcType> for Merge<'e> {
};
match subs.union(r_var, left) {
Ok(()) => Ok(None),
Err(()) => Err(UnifyError::Occurs(r_var.clone(), left.clone())),
Err(()) => Err(UnifyError::Occurs(ArcType::from_variable(r_var.clone()), left.clone())),
}

}
(_, &Type::Variable(ref r)) => {
match subs.union(r, l) {
Ok(()) => Ok(None),
Err(()) => Err(UnifyError::Occurs(r.clone(), l.clone())),
Err(()) => Err(UnifyError::Occurs(ArcType::from_variable(r.clone()), l.clone())),
}
}
(&Type::Variable(ref l), _) => {
match subs.union(l, r) {
Ok(()) => Ok(Some(r.clone())),
Err(()) => Err(UnifyError::Occurs(l.clone(), r.clone())),
Err(()) => Err(UnifyError::Occurs(ArcType::from_variable(l.clone()), r.clone())),
}
}
_ => {
Expand Down
23 changes: 23 additions & 0 deletions check/tests/fail.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#[macro_use]
extern crate collect_mac;
extern crate env_logger;
#[macro_use]
extern crate pretty_assertions;

extern crate gluon_base as base;
extern crate gluon_parser as parser;
Expand Down Expand Up @@ -416,3 +418,24 @@ let Test = 1
let result = support::typecheck(text);
assert_err!(result, UndefinedVariable(..));
}

#[test]
fn no_inference_variable_in_error() {
let _ = ::env_logger::init();
let text = r#"
() 1
"#;
let result = support::typecheck(text);

assert_eq!(&*format!("{}", result.unwrap_err()).replace("\t", " "),
r#"test:Line: 2, Column: 1: Expected the following types to be equal
Expected: b0 -> b1
Found: {}
1 errors were found during unification:
Types do not match:
Expected: b0 -> b1
Found: {}
() 1
^~~~
"#);
}
2 changes: 1 addition & 1 deletion vm/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ impl GlobalVmState {
.unwrap()
.get(&id)
.cloned()
.unwrap_or_else(|| panic!("Expected type to be inserted before get_type call"))
.unwrap_or_else(|| panic!("Expected type to be inserted before get_type call. Did you forget to call `Thread::register_type`?"))
}

/// Checks if a global exists called `name`
Expand Down

0 comments on commit 70478d1

Please sign in to comment.