From 5a799d05eb38906fc8468c0f1464ad4d55ca62fa Mon Sep 17 00:00:00 2001 From: Jordan Mackie Date: Sun, 26 Feb 2023 15:55:38 +0000 Subject: [PATCH] wip: ditto-type-checker --- Cargo.lock | 55 +- Cargo.toml | 1 + crates/ditto-type-checker/Cargo.toml | 27 + crates/ditto-type-checker/README.md | 8 + crates/ditto-type-checker/src/ast.rs | 239 ++++++++ crates/ditto-type-checker/src/ast/convert.rs | 228 ++++++++ crates/ditto-type-checker/src/check.rs | 530 ++++++++++++++++++ crates/ditto-type-checker/src/constraint.rs | 6 + crates/ditto-type-checker/src/env.rs | 266 +++++++++ crates/ditto-type-checker/src/error.rs | 261 +++++++++ crates/ditto-type-checker/src/infer.rs | 376 +++++++++++++ crates/ditto-type-checker/src/lib.rs | 60 ++ crates/ditto-type-checker/src/outputs.rs | 27 + crates/ditto-type-checker/src/result.rs | 3 + crates/ditto-type-checker/src/scheme.rs | 52 ++ crates/ditto-type-checker/src/state.rs | 6 + crates/ditto-type-checker/src/substitution.rs | 465 +++++++++++++++ crates/ditto-type-checker/src/supply.rs | 38 ++ crates/ditto-type-checker/src/tests.rs | 303 ++++++++++ crates/ditto-type-checker/src/typecheck.rs | 425 ++++++++++++++ crates/ditto-type-checker/src/unify.rs | 446 +++++++++++++++ .../src/utils/make_wobbly.rs | 69 +++ .../ditto-type-checker/src/utils/mk_types.rs | 23 + crates/ditto-type-checker/src/utils/mod.rs | 9 + .../src/utils/type_variables.rs | 53 ++ .../src/utils/unalias_type.rs | 17 + crates/ditto-type-checker/src/warning.rs | 53 ++ .../tests/testdata/.gitignore | 1 + .../ditto-type-checker/tests/testdata/arrays | 82 +++ .../ditto-type-checker/tests/testdata/calls | 134 +++++ .../tests/testdata/conditionals | 90 +++ .../tests/testdata/constructors | 46 ++ .../ditto-type-checker/tests/testdata/errors | 79 +++ .../tests/testdata/functions | 272 +++++++++ .../tests/testdata/literals | 40 ++ .../ditto-type-checker/tests/testdata/records | 357 ++++++++++++ .../tests/testdata/warnings | 65 +++ 37 files changed, 5210 insertions(+), 2 deletions(-) create mode 100644 crates/ditto-type-checker/Cargo.toml create mode 100644 crates/ditto-type-checker/README.md create mode 100644 crates/ditto-type-checker/src/ast.rs create mode 100644 crates/ditto-type-checker/src/ast/convert.rs create mode 100644 crates/ditto-type-checker/src/check.rs create mode 100644 crates/ditto-type-checker/src/constraint.rs create mode 100644 crates/ditto-type-checker/src/env.rs create mode 100644 crates/ditto-type-checker/src/error.rs create mode 100644 crates/ditto-type-checker/src/infer.rs create mode 100644 crates/ditto-type-checker/src/lib.rs create mode 100644 crates/ditto-type-checker/src/outputs.rs create mode 100644 crates/ditto-type-checker/src/result.rs create mode 100644 crates/ditto-type-checker/src/scheme.rs create mode 100644 crates/ditto-type-checker/src/state.rs create mode 100644 crates/ditto-type-checker/src/substitution.rs create mode 100644 crates/ditto-type-checker/src/supply.rs create mode 100644 crates/ditto-type-checker/src/tests.rs create mode 100644 crates/ditto-type-checker/src/typecheck.rs create mode 100644 crates/ditto-type-checker/src/unify.rs create mode 100644 crates/ditto-type-checker/src/utils/make_wobbly.rs create mode 100644 crates/ditto-type-checker/src/utils/mk_types.rs create mode 100644 crates/ditto-type-checker/src/utils/mod.rs create mode 100644 crates/ditto-type-checker/src/utils/type_variables.rs create mode 100644 crates/ditto-type-checker/src/utils/unalias_type.rs create mode 100644 crates/ditto-type-checker/src/warning.rs create mode 100644 crates/ditto-type-checker/tests/testdata/.gitignore create mode 100644 crates/ditto-type-checker/tests/testdata/arrays create mode 100644 crates/ditto-type-checker/tests/testdata/calls create mode 100644 crates/ditto-type-checker/tests/testdata/conditionals create mode 100644 crates/ditto-type-checker/tests/testdata/constructors create mode 100644 crates/ditto-type-checker/tests/testdata/errors create mode 100644 crates/ditto-type-checker/tests/testdata/functions create mode 100644 crates/ditto-type-checker/tests/testdata/literals create mode 100644 crates/ditto-type-checker/tests/testdata/records create mode 100644 crates/ditto-type-checker/tests/testdata/warnings diff --git a/Cargo.lock b/Cargo.lock index bf1022b30..adb70549d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -834,7 +844,6 @@ dependencies = [ "ditto-ast", "ditto-cst", "halfbrown", - "miette", "nonempty", "smol_str", "thiserror", @@ -849,6 +858,27 @@ dependencies = [ "tree-sitter-ditto", ] +[[package]] +name = "ditto-type-checker" +version = "0.0.1" +dependencies = [ + "Inflector", + "datadriven", + "ditto-ast", + "ditto-cst", + "ditto-pattern-checker", + "halfbrown", + "indexmap", + "miette", + "nonempty", + "serde", + "smallvec", + "smol_str", + "thiserror", + "tinyset", + "tracing", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -2184,7 +2214,7 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", "rand_pcg", @@ -2196,6 +2226,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -2209,6 +2241,16 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2923,6 +2965,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinyset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2417e47ddd3809ad40222777ac754ee881b3a6401e38cbeeeb3ee1ca5f30aa0" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index c8b67b048..834c95f72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ members = [ "crates/ditto-tree-sitter", "crates/ditto-highlight", "crates/ditto-pattern-checker", + "crates/ditto-type-checker", ] diff --git a/crates/ditto-type-checker/Cargo.toml b/crates/ditto-type-checker/Cargo.toml new file mode 100644 index 000000000..be98b6a17 --- /dev/null +++ b/crates/ditto-type-checker/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ditto-type-checker" +version = "0.0.1" +edition = "2021" +license = "BSD-3-Clause" + +[lib] +doctest = false + +[dependencies] +ditto-cst = { path = "../ditto-cst" } +ditto-ast = { path = "../ditto-ast" } +ditto-pattern-checker = { path = "../ditto-pattern-checker" } +nonempty = "0.8" +smallvec = "1.0" +smol_str = "0.1" +halfbrown = "0.1" +indexmap = "1.9" +serde = { version = "1.0", features = ["derive"] } +miette = { version = "5.5", features = ["fancy"] } +thiserror = "1.0" +Inflector = "0.11.4" +tracing = "0.1" +tinyset = "0.4" + +[dev-dependencies] +datadriven = "0.6" diff --git a/crates/ditto-type-checker/README.md b/crates/ditto-type-checker/README.md new file mode 100644 index 000000000..732c20932 --- /dev/null +++ b/crates/ditto-type-checker/README.md @@ -0,0 +1,8 @@ +# The ditto type checker + +This crate handles type-checking and linting the ditto syntax. + +The type-checking algorithm is based on [@david-christiansen](https://github.com/david-christiansen)'s [Bidirectional Typing Rules][bidirectional]. + +[bidirectional]: https://www.davidchristiansen.dk/tutorials/bidirectional.pdf +[bidirectional impl]: https://github.com/luc-tielen/typesystem diff --git a/crates/ditto-type-checker/src/ast.rs b/crates/ditto-type-checker/src/ast.rs new file mode 100644 index 000000000..3b4bc56ee --- /dev/null +++ b/crates/ditto-type-checker/src/ast.rs @@ -0,0 +1,239 @@ +#[cfg(test)] +mod convert; + +use ditto_ast::{Name, QualifiedName, QualifiedProperName, Span, Type, UnusedName}; +use nonempty::NonEmpty; +use smallvec::SmallVec; +use smol_str::SmolStr; + +pub type TypeAnnotation = Option; + +pub type FunctionBinder = (Pattern, TypeAnnotation); + +pub type FunctionBinders = SmallVec<[FunctionBinder; 4]>; + +pub type MatchArm = (Pattern, Expression); + +pub type MatchArms = NonEmpty>; + +#[derive(Clone)] +pub struct Label { + pub span: Span, + pub label: Name, +} + +pub type RecordField = (Label, Expression); + +pub type RecordFields = Vec; + +pub type Constructor = QualifiedProperName; + +pub type Variable = QualifiedName; + +pub enum Expression { + Function { + span: Span, + binders: FunctionBinders, + return_type_annotation: TypeAnnotation, + body: Box, + }, + Call { + span: Span, + function: Box, + arguments: Arguments, + }, + If { + span: Span, + condition: Box, + true_clause: Box, + false_clause: Box, + }, + Constructor { + span: Span, + constructor: Constructor, + }, + Match { + span: Span, + expression: Box, + arms: MatchArms, + }, + Effect { + span: Span, + effect: Effect, + }, + Variable { + span: Span, + variable: Variable, + }, + String { + span: Span, + value: SmolStr, + }, + Int { + span: Span, + value: SmolStr, + }, + Float { + span: Span, + value: SmolStr, + }, + Array { + span: Span, + elements: Vec, + }, + Record { + span: Span, + fields: RecordFields, + }, + RecordAccess { + span: Span, + target: Box, + label: Label, + }, + RecordUpdate { + span: Span, + target: Box, + updates: RecordFields, + }, + Let { + span: Span, + declaration: LetValueDeclaration, + expression: Box, + }, + True { + span: Span, + }, + False { + span: Span, + }, + Unit { + span: Span, + }, +} + +pub struct LetValueDeclaration { + pub pattern: Pattern, + pub pattern_span: Span, + pub type_annotation: TypeAnnotation, + pub expression: Box, +} + +pub type Arguments = Vec; + +pub type Patterns = Vec; + +pub enum Pattern { + Constructor { + span: Span, + constructor_span: Span, + constructor: QualifiedProperName, + arguments: Patterns, + }, + Variable { + span: Span, + name: Name, + }, + Unused { + span: Span, + unused_name: UnusedName, + }, +} + +pub enum Effect { + Bind { + name: Name, + name_span: Span, + expression: Box, + rest: Box, + }, + Let { + pattern: Pattern, + pattern_span: Span, + type_annotation: TypeAnnotation, + expression: Box, + rest: Box, + }, + Expression { + expression: Box, + rest: Option>, + }, + Return { + expression: Box, + }, +} + +impl Expression { + pub fn get_span(&self) -> Span { + match self { + Self::Function { span, .. } + | Self::Call { span, .. } + | Self::If { span, .. } + | Self::Constructor { span, .. } + | Self::Match { span, .. } + | Self::Effect { span, .. } + | Self::Variable { span, .. } + | Self::String { span, .. } + | Self::Int { span, .. } + | Self::Float { span, .. } + | Self::Array { span, .. } + | Self::Record { span, .. } + | Self::RecordAccess { span, .. } + | Self::RecordUpdate { span, .. } + | Self::Let { span, .. } + | Self::True { span } + | Self::False { span } + | Self::Unit { span } => *span, + } + } +} + +impl Pattern { + pub fn get_span(&self) -> Span { + match self { + Self::Constructor { span, .. } + | Self::Variable { span, .. } + | Self::Unused { span, .. } => *span, + } + } +} + +impl std::convert::From for Pattern { + fn from(cst_pattern: ditto_cst::Pattern) -> Self { + let span = cst_pattern.get_span(); + match cst_pattern { + ditto_cst::Pattern::NullaryConstructor { constructor } => { + let constructor_span = constructor.get_span(); + Self::Constructor { + span, + constructor_span, + constructor: QualifiedProperName::from(constructor), + arguments: vec![], + } + } + ditto_cst::Pattern::Constructor { + constructor, + arguments, + } => { + let constructor_span = constructor.get_span(); + Self::Constructor { + span, + constructor_span, + constructor: QualifiedProperName::from(constructor), + arguments: arguments + .value + .into_iter() + .map(|box pat| Self::from(pat)) + .collect(), + } + } + ditto_cst::Pattern::Variable { name } => Self::Variable { + span, + name: Name::from(name), + }, + ditto_cst::Pattern::Unused { unused_name } => Self::Unused { + span, + unused_name: UnusedName::from(unused_name), + }, + } + } +} diff --git a/crates/ditto-type-checker/src/ast/convert.rs b/crates/ditto-type-checker/src/ast/convert.rs new file mode 100644 index 000000000..79bb15faa --- /dev/null +++ b/crates/ditto-type-checker/src/ast/convert.rs @@ -0,0 +1,228 @@ +// +// This module exists solely to facilitate testing! +// + +use super::{Arguments, Expression, FunctionBinder, FunctionBinders, Label, Pattern, RecordFields}; +use crate::supply::Supply; +use ditto_ast::{Name, Type, Var}; +use ditto_cst as cst; +use std::collections::HashMap; + +type KnownTypeVars = HashMap; + +impl Expression { + pub fn from_cst( + cst: cst::Expression, + type_vars: &mut KnownTypeVars, + supply: &mut Supply, + ) -> Self { + let span = cst.get_span(); + match cst { + cst::Expression::Parens(parens) => Self::from_cst(*parens.value, type_vars, supply), + cst::Expression::Function { + box parameters, + box return_type_annotation, + box body, + .. + } => { + let binders = if let Some(comma_sep) = parameters.value { + comma_sep + .into_iter() + .map(|(pattern, type_annotation)| -> FunctionBinder { + ( + Pattern::from(pattern), + type_annotation.map(|ann| convert_type(ann.1, type_vars, supply)), + ) + }) + .collect() + } else { + FunctionBinders::new() + }; + let return_type_annotation = + return_type_annotation.map(|ann| convert_type(ann.1, type_vars, supply)); + let body = Box::new(Self::from_cst(body, type_vars, supply)); + Self::Function { + span, + binders, + return_type_annotation, + body, + } + } + cst::Expression::Call { + box function, + arguments, + } => { + let function = Box::new(Self::from_cst(function, type_vars, supply)); + let arguments = if let Some(comma_sep) = arguments.value { + comma_sep + .into_iter() + .map(|box arg| Self::from_cst(arg, type_vars, supply)) + .collect() + } else { + Arguments::new() + }; + Self::Call { + span, + function, + arguments, + } + } + cst::Expression::If { + box condition, + box true_clause, + box false_clause, + .. + } => Self::If { + span, + condition: Box::new(Self::from_cst(condition, type_vars, supply)), + true_clause: Box::new(Self::from_cst(true_clause, type_vars, supply)), + false_clause: Box::new(Self::from_cst(false_clause, type_vars, supply)), + }, + cst::Expression::Match { + match_keyword: _, + expression: _, + with_keyword: _, + head_arm: _, + tail_arms: _, + end_keyword: _, + } => todo!(), + cst::Expression::Effect { + do_keyword: _, + open_brace: _, + effect: _, + close_brace: _, + } => todo!(), + cst::Expression::Constructor(ctor) => Self::Constructor { + span, + constructor: ctor.into(), + }, + cst::Expression::Variable(variable) => Self::Variable { + span, + variable: variable.into(), + }, + cst::Expression::Unit(_) => Self::Unit { span }, + cst::Expression::True(_) => Self::True { span }, + cst::Expression::False(_) => Self::True { span }, + cst::Expression::String(token) => Self::String { + span, + value: token.value.into(), + }, + cst::Expression::Int(token) => Self::Int { + span, + value: token.value.into(), + }, + cst::Expression::Float(token) => Self::Float { + span, + value: token.value.into(), + }, + cst::Expression::Array(brackets) => { + let elements = if let Some(comma_sep) = brackets.value { + comma_sep + .into_iter() + .map(|box element| Self::from_cst(element, type_vars, supply)) + .collect() + } else { + vec![] + }; + Self::Array { span, elements } + } + + cst::Expression::Record(braces) => { + let fields = if let Some(comma_sep) = braces.value { + comma_sep + .into_iter() + .map(|field| { + ( + Label { + span: field.label.get_span(), + label: field.label.into(), + }, + Self::from_cst(*field.value, type_vars, supply), + ) + }) + .collect() + } else { + RecordFields::new() + }; + Self::Record { span, fields } + } + cst::Expression::BinOp { + box lhs, + operator: cst::BinOp::RightPizza(_), + box rhs, + } => { + let mut arguments = vec![Self::from_cst(lhs, type_vars, supply)]; + match Self::from_cst(rhs, type_vars, supply) { + Self::Call { + span: _, + function, + arguments: args, + } => { + arguments.extend(args); + Self::Call { + span, + function, + arguments, + } + } + function => Self::Call { + span, + function: Box::new(function), + arguments, + }, + } + } + cst::Expression::RecordAccess { + box target, + dot: _, + label, + } => Self::RecordAccess { + span, + target: Box::new(Self::from_cst(target, type_vars, supply)), + label: Label { + span: label.get_span(), + label: label.into(), + }, + }, + cst::Expression::RecordUpdate { + open_brace: _, + box target, + pipe: _, + updates, + close_brace: _, + } => Self::RecordUpdate { + span, + target: Box::new(Self::from_cst(target, type_vars, supply)), + updates: updates + .into_iter() + .map(|field| { + ( + Label { + span: field.label.get_span(), + label: field.label.into(), + }, + Self::from_cst(*field.value, type_vars, supply), + ) + }) + .collect(), + }, + cst::Expression::Let { + let_keyword: _, + head_declaration: _, + tail_declarations: _, + in_keyword: _, + expr: _, + } => todo!(), + } + } +} + +// This is dangerous as it trusts that the type being converted is well kinded. +fn convert_type(cst_type: cst::Type, type_vars: &mut KnownTypeVars, supply: &mut Supply) -> Type { + Type::from_cst_unchecked_with( + &mut supply.0, + type_vars, + cst_type, + &ditto_ast::module_name!("Test"), + ) +} diff --git a/crates/ditto-type-checker/src/check.rs b/crates/ditto-type-checker/src/check.rs new file mode 100644 index 000000000..de98c7323 --- /dev/null +++ b/crates/ditto-type-checker/src/check.rs @@ -0,0 +1,530 @@ +use crate::{ + ast::{Expression, Label}, + constraint::Constraint, + env::Env, + error::Error, + outputs::Outputs, + result::Result, + state::State, + warning::Warning, +}; +use ditto_ast as ast; +use nonempty::NonEmpty; + +impl State { + pub fn check( + &mut self, + env: &Env, + expr: Expression, + expected: ast::Type, + ) -> Result { + // TODO: handle recursive un-aliasing and re-aliasing + // if utils::is_aliased(&expected) { + // let unaliased = utils::unalias_type(expected.clone()); + // let (expr, outputs) = self.check(env, expr, unaliased)?; + // // REVIEW: should we substitute `expected` after this? + // return Ok((set_type(expr, expected), outputs)); + // } + + match (expr, expected) { + ( + Expression::Function { + span, + binders, + return_type_annotation, + body, + }, + ast::Type::Function { + parameters, + box return_type, + }, + ) => { + if binders.len() != parameters.len() { + return Err(Error::ArgumentLengthMismatch { + function_span: span, + wanted: parameters.len(), + got: binders.len(), + }); + } + let actual_parameters: Vec = binders + .iter() + .zip(parameters.iter()) + .map(|((pattern, type_ann), parameter)| { + if let Some(t) = type_ann { + self.unify( + pattern.get_span(), + Constraint { + expected: parameter.clone(), + actual: t.clone(), + }, + )?; + Ok(t.clone()) + } else { + Ok(parameter.clone()) + } + }) + .try_collect()?; + let actual_return_type: ast::Type = return_type_annotation + .clone() + .unwrap_or_else(|| return_type.clone()); + let actual = ast::Type::Function { + parameters: actual_parameters.clone(), + return_type: Box::new(actual_return_type.clone()), + }; + let expected = ast::Type::Function { + parameters, + return_type: Box::new(return_type), + }; + self.unify(span, Constraint { expected, actual })?; + let binders = binders + .into_iter() + .zip(actual_parameters.into_iter()) + .map(|((pattern, type_ann), parameter)| { + (pattern, type_ann.or_else(|| Some(parameter))) + }) + .collect(); + let return_type_annotation = + return_type_annotation.or_else(|| Some(actual_return_type)); + self.infer( + env, + Expression::Function { + span, + binders, + return_type_annotation, + body, + }, + ) + } + + ( + Expression::Call { + span, + box function, + arguments, + }, + expected, + ) => self.typecheck_call(env, span, function, arguments, Some(expected)), + + ( + Expression::If { + span, + box condition, + box true_clause, + box false_clause, + }, + expected, + ) => self.typecheck_conditional( + env, + span, + condition, + true_clause, + false_clause, + Some(expected), + ), + + (Expression::Match { .. }, _) => todo!(), + + (Expression::Effect { .. }, _) => todo!(), + + ( + Expression::RecordAccess { + span, + box target, + label: Label { label, .. }, + }, + expected, + ) => self.typecheck_record_access(env, span, target, label, Some(expected)), + + ( + Expression::RecordUpdate { + span, + box target, + updates, + }, + expected, + ) => { + let (target, mut outputs) = self.check(env, target, expected)?; + let (expr, more_outputs) = + self.typecheck_record_updates(env, span, target, updates)?; + outputs.extend(more_outputs); + Ok((expr, outputs)) + } + + (Expression::Record { span, fields }, ast::Type::RecordClosed { row, kind }) => { + let mut checked_fields = ast::RecordFields::with_capacity(fields.len()); + let mut outputs = Outputs::default(); + for (Label { span, label }, expr) in fields { + if checked_fields.contains_key(&label) { + return Err(Error::DuplicateRecordField { span }); + } + if !inflector::cases::snakecase::is_snake_case(&label.0) { + outputs + .warnings + .push(Warning::RecordLabelNotSnakeCase { span }); + } + if let Some(expected) = row.get(&label) { + let (expr, more_outputs) = self.check(env, expr, expected.clone())?; + checked_fields.insert(label, expr); + outputs.extend(more_outputs) + } else { + let expected = ast::Type::RecordClosed { kind, row }; + return Err(Error::UnexpectedRecordField { + span, + label, + record_like_type: expected, + help: None, + }); + } + } + let mut missing: Vec<(ast::Name, ast::Type)> = Vec::new(); + for key in row.keys() { + if !checked_fields.contains_key(key) { + missing.push((key.clone(), row.get(key).unwrap().clone())); + } + } + if !missing.is_empty() { + return Err(Error::MissingRecordFields { + span, + missing, + help: None, + }); + } + let expected = ast::Type::RecordClosed { kind, row }; + let expr = ast::Expression::Record { + span, + record_type: expected, + fields: checked_fields, + }; + Ok((expr, outputs)) + } + ( + Expression::Record { span, fields }, + ast::Type::RecordOpen { + kind, + var, + row, + source_name, + is_rigid, + }, + ) => { + let mut checked_fields = ast::RecordFields::with_capacity(fields.len()); + let mut outputs = Outputs::default(); + let mut actual_row = ast::Row::with_capacity(fields.len()); + for (Label { span, label }, expr) in fields { + if checked_fields.contains_key(&label) { + return Err(Error::DuplicateRecordField { span }); + } + if !inflector::cases::snakecase::is_snake_case(&label.0) { + outputs + .warnings + .push(Warning::RecordLabelNotSnakeCase { span }); + } + if let Some(expected) = row.get(&label) { + let (expr, more_outputs) = self.check(env, expr, expected.clone())?; + actual_row.insert(label.clone(), expr.get_type().clone()); + checked_fields.insert(label, expr); + outputs.extend(more_outputs); + } else { + let (expr, more_outputs) = self.infer(env, expr)?; + actual_row.insert(label.clone(), expr.get_type().clone()); + checked_fields.insert(label, expr); + outputs.extend(more_outputs); + } + } + let mut missing: Vec<(ast::Name, ast::Type)> = Vec::new(); + for key in row.keys() { + if !checked_fields.contains_key(key) { + missing.push((key.clone(), row.get(key).unwrap().clone())); + } + } + if !missing.is_empty() { + return Err(Error::MissingRecordFields { + span, + missing, + help: None, + }); + } + let actual = ast::Type::RecordClosed { + kind: kind.clone(), + row: actual_row, + }; + let expected = ast::Type::RecordOpen { + kind, + var, + row, + source_name, + is_rigid, + }; + // Need to also unify so the type var is bound + self.unify( + span, + Constraint { + expected, + actual: actual.clone(), + }, + )?; + let expr = ast::Expression::Record { + span, + record_type: actual, + fields: checked_fields, + }; + Ok((expr, outputs)) + } + + (Expression::Let { .. }, _) => todo!(), + + (Expression::Array { span, elements }, expected) => { + if let ast::Type::Call { + function: box ast::Type::PrimConstructor(ast::PrimType::Array), + arguments: + box NonEmpty { + head: element_type, + tail, + }, + } = expected + { + debug_assert!(tail.is_empty()); // kind-checker should prevent this! + self.typecheck_array(env, span, elements, Some(element_type)) + } else { + self.typecheck_array(env, span, elements, None) + } + } + (expr @ Expression::Function { .. }, expected) + | (expr @ Expression::Record { .. }, expected) + | ( + expr @ (Expression::Unit { .. } + | Expression::False { .. } + | Expression::True { .. } + | Expression::Float { .. } + | Expression::Int { .. } + | Expression::String { .. } + | Expression::Variable { .. } + | Expression::Constructor { .. }), + expected, + ) => { + let (expression, outputs) = self.infer(env, expr)?; + let span = expression.get_span(); + let constraint = Constraint { + expected: expected.clone(), + actual: expression.get_type().clone(), + }; + self.unify(span, constraint)?; + let expression = set_type(expression, expected); + Ok((expression, outputs)) + } + } + } +} + +fn set_type(expr: ast::Expression, t: ast::Type) -> ast::Expression { + use ast::Expression::*; + match expr { + Call { + span, + call_type: _, + function, + arguments, + } => Call { + span, + call_type: t, + function, + arguments, + }, + Function { + span, + function_type: _, + binders, + body, + } => Function { + span, + function_type: t, + binders, + body, + }, + If { + span, + output_type: _, + condition, + true_clause, + false_clause, + } => If { + span, + output_type: t, + condition, + true_clause, + false_clause, + }, + Match { + span, + match_type: _, + expression, + arms, + } => Match { + span, + match_type: t, + expression, + arms, + }, + Effect { + span, + effect_type: _, + return_type, + effect, + } => Effect { + span, + effect_type: t, + return_type, + effect, + }, + Record { + span, + record_type: _, + fields, + } => Record { + span, + record_type: t, + fields, + }, + LocalConstructor { + span, + constructor_type: _, + constructor, + } => LocalConstructor { + span, + constructor_type: t, + constructor, + }, + ImportedConstructor { + span, + constructor_type: _, + constructor, + } => ImportedConstructor { + span, + constructor_type: t, + constructor, + }, + LocalVariable { + introduction, + span, + variable_type: _, + variable, + } => LocalVariable { + introduction, + span, + variable_type: t, + variable, + }, + ForeignVariable { + introduction, + span, + variable_type: _, + variable, + } => ForeignVariable { + introduction, + span, + variable_type: t, + variable, + }, + ImportedVariable { + introduction, + span, + variable_type: _, + variable, + } => ImportedVariable { + introduction, + span, + variable_type: t, + variable, + }, + Let { + span, + declaration, + box expression, + } => Let { + span, + declaration, + expression: Box::new(set_type(expression, t)), + }, + RecordAccess { + span, + field_type: _, + target, + label, + } => RecordAccess { + span, + field_type: t, + target, + label, + }, + RecordUpdate { + span, + record_type: _, + target, + fields, + } => RecordUpdate { + span, + record_type: t, + target, + fields, + }, + Array { + span, + element_type, + elements, + value_type: _, + } => Array { + span, + element_type, + elements, + value_type: t, + }, + String { + span, + value, + value_type: _, + } => String { + span, + value, + value_type: t, + }, + Int { + span, + value, + value_type: _, + } => Int { + span, + value, + value_type: t, + }, + Float { + span, + value, + value_type: _, + } => Float { + span, + value, + value_type: t, + }, + True { + span, + value_type: _, + } => True { + span, + value_type: t, + }, + False { + span, + value_type: _, + } => False { + span, + value_type: t, + }, + Unit { + span, + value_type: _, + } => Unit { + span, + value_type: t, + }, + } +} diff --git a/crates/ditto-type-checker/src/constraint.rs b/crates/ditto-type-checker/src/constraint.rs new file mode 100644 index 000000000..8cd446a75 --- /dev/null +++ b/crates/ditto-type-checker/src/constraint.rs @@ -0,0 +1,6 @@ +use ditto_ast::Type; + +pub struct Constraint { + pub expected: Type, + pub actual: Type, +} diff --git a/crates/ditto-type-checker/src/env.rs b/crates/ditto-type-checker/src/env.rs new file mode 100644 index 000000000..c6806e7f4 --- /dev/null +++ b/crates/ditto-type-checker/src/env.rs @@ -0,0 +1,266 @@ +use crate::{ + error::Error, + scheme::Scheme, + utils::{self as utils, TypeVars}, +}; +use ditto_ast::{ + unqualified, FullyQualifiedName, FullyQualifiedProperName, Name, ProperName, QualifiedName, + QualifiedProperName, Span, Type, +}; +use ditto_pattern_checker as pattern_checker; +use halfbrown::HashMap; +use std::rc::Rc; + +#[derive(Default, Clone)] +pub struct Env { + pub(crate) values: EnvValues, + pub(crate) constructors: Rc, // NOTE: Using Rc for cheap cloning + pub(crate) pattern_constructors: Rc, +} + +pub type EnvValues = HashMap; + +#[derive(Debug, Clone)] +pub enum EnvValue { + LocalVariable { + span: Span, + scheme: Scheme, + variable: Name, + }, + ModuleValue { + span: Span, + scheme: Scheme, + value: Name, + }, + ForeignValue { + span: Span, + scheme: Scheme, + value: Name, + }, + ImportedValue { + span: Span, + scheme: Scheme, + value: FullyQualifiedName, + }, +} + +pub type EnvConstructors = HashMap; + +#[derive(Debug, Clone)] +pub enum EnvConstructor { + ModuleConstructor { + constructor_scheme: Scheme, + constructor: ProperName, + }, + ImportedConstructor { + constructor_scheme: Scheme, + constructor: FullyQualifiedProperName, + }, +} + +impl Env { + pub(crate) fn insert_local_variable( + &mut self, + span: Span, + variable_name: Name, + variable_type: Type, + ) -> Result<(), Error> { + let key = unqualified(variable_name.clone()); + if let Some(env_value) = self.values.get(&key) { + return Err(Error::ValueShadowed { + introduced: env_value.get_span(), + shadowed: span, + }); + } + self.values.insert( + key, + EnvValue::LocalVariable { + span, + scheme: Scheme { + forall: TypeVars::new(), // Important! + signature: variable_type, + }, + variable: variable_name, + }, + ); + Ok(()) + } + + pub fn insert_module_value( + &mut self, + span: Span, + value_name: Name, + value_type: Type, + ) -> Result<(), Error> { + let key = unqualified(value_name.clone()); + if let Some(env_value) = self.values.get(&key) { + return Err(Error::ValueShadowed { + introduced: env_value.get_span(), + shadowed: span, + }); + } + self.values.insert( + key, + EnvValue::ModuleValue { + span, + scheme: self.generalize(value_type), + value: value_name, + }, + ); + Ok(()) + } + + pub fn insert_imported_value( + &mut self, + span: Span, + key: QualifiedName, + value_name: FullyQualifiedName, + value_type: Type, + ) -> Result<(), Error> { + if let Some(env_value) = self.values.get(&key) { + return Err(Error::ValueShadowed { + introduced: env_value.get_span(), + shadowed: span, + }); + } + self.values.insert( + key, + EnvValue::ImportedValue { + span, + scheme: self.generalize(value_type), + value: value_name, + }, + ); + Ok(()) + } + + pub fn insert_foreign_value( + &mut self, + span: Span, + value_name: Name, + value_type: Type, + ) -> Result<(), Error> { + let key = unqualified(value_name.clone()); + if let Some(env_value) = self.values.get(&key) { + return Err(Error::ValueShadowed { + introduced: env_value.get_span(), + shadowed: span, + }); + } + self.values.insert( + key, + EnvValue::ForeignValue { + span, + scheme: self.generalize(value_type), + value: value_name, + }, + ); + Ok(()) + } + + // NOTE: doesn't check for shadowing! + pub fn insert_module_constructor( + &mut self, + constructor_name: ProperName, + constructor_type: Type, + ) { + let key = unqualified(constructor_name.clone()); + Rc::make_mut(&mut self.pattern_constructors).insert( + key.clone(), + pattern_checker::EnvConstructor::ModuleConstructor { + constructor_type: constructor_type.clone(), + constructor: constructor_name.clone(), + }, + ); + let constructor_scheme = self.generalize(constructor_type); + Rc::make_mut(&mut self.constructors).insert( + key, + EnvConstructor::ModuleConstructor { + constructor_scheme, + constructor: constructor_name, + }, + ); + } + + // NOTE: doesn't check for shadowing! + pub fn insert_imported_constructor( + &mut self, + key: QualifiedProperName, + constructor_name: FullyQualifiedProperName, + constructor_type: Type, + ) { + Rc::make_mut(&mut self.pattern_constructors).insert( + key.clone(), + pattern_checker::EnvConstructor::ImportedConstructor { + constructor_type: constructor_type.clone(), + constructor: constructor_name.clone(), + }, + ); + let constructor_scheme = self.generalize(constructor_type); + Rc::make_mut(&mut self.constructors).insert( + key, + EnvConstructor::ImportedConstructor { + constructor_scheme, + constructor: constructor_name, + }, + ); + } + + /// Abstracts a type over all type variables which are free in the type + /// but not free in the given typing context. + /// + /// I.e. returns the canonical polymorphic type. + pub(crate) fn generalize(&self, ast_type: Type) -> Scheme { + let forall = &utils::type_variables(&ast_type) - &self.free_type_variables(); + + Scheme { + forall, + signature: ast_type, + } + } + + fn free_type_variables(&self) -> TypeVars { + self.constructors + .values() + .flat_map(|env_constructor| env_constructor.get_scheme().free_type_variables()) + .chain( + self.values + .values() + .flat_map(|env_value| env_value.get_scheme().free_type_variables()), + ) + .collect() + } +} + +impl EnvValue { + pub fn get_scheme(&self) -> &Scheme { + match self { + Self::LocalVariable { scheme, .. } + | Self::ModuleValue { scheme, .. } + | Self::ForeignValue { scheme, .. } + | Self::ImportedValue { scheme, .. } => scheme, + } + } + + pub fn get_span(&self) -> Span { + match self { + Self::LocalVariable { span, .. } + | Self::ModuleValue { span, .. } + | Self::ForeignValue { span, .. } + | Self::ImportedValue { span, .. } => *span, + } + } +} + +impl EnvConstructor { + pub fn get_scheme(&self) -> &Scheme { + match self { + Self::ModuleConstructor { + constructor_scheme, .. + } + | Self::ImportedConstructor { + constructor_scheme, .. + } => constructor_scheme, + } + } +} diff --git a/crates/ditto-type-checker/src/error.rs b/crates/ditto-type-checker/src/error.rs new file mode 100644 index 000000000..ff93d8eb9 --- /dev/null +++ b/crates/ditto-type-checker/src/error.rs @@ -0,0 +1,261 @@ +use ditto_ast::{Name, QualifiedName, QualifiedProperName, Span, Type, Var}; +use ditto_pattern_checker as pattern_checker; +use inflector::string::pluralize::to_plural; +use std::collections::HashSet; + +#[derive(Debug, miette::Diagnostic, thiserror::Error)] +pub enum Error { + #[error("types don't unify")] + #[diagnostic(severity(Error))] + TypesNotEqual { + #[label("here")] + span: Span, + expected: Type, + actual: Type, + #[help] + help: Option, + }, + + #[error("types don't unify")] + #[diagnostic(severity(Error))] + UnexpectedRecordField { + #[label("here")] + span: Span, + label: Name, + record_like_type: Type, + #[help] + help: Option, + }, + + #[error("types don't unify")] + #[diagnostic(severity(Error))] + MissingRecordFields { + #[label("this record is missing fields")] + span: Span, + missing: Vec<(Name, Type)>, + #[help] + help: Option, + }, + + #[error("infinite type")] + #[diagnostic(severity(Error), help("try adding type annotations?"))] + InfiniteType { + #[label("here")] + span: Span, + var: Var, + infinite_type: Type, + }, + + #[error("value shadowed")] + #[diagnostic(severity(Error))] + ValueShadowed { + #[label("first bound here")] + introduced: Span, + #[label("shadowed here")] + shadowed: Span, + }, + + #[error("wrong number of arguments")] + #[diagnostic(severity(Error))] + ArgumentLengthMismatch { + #[label("this expects {wanted} {}", pluralize_args(*wanted))] + function_span: Span, + wanted: usize, + got: usize, + }, + + #[error("expression isn't callable")] + #[diagnostic(severity(Error))] + NotAFunction { + #[label("can't call this")] + span: Span, + actual_type: Type, + #[help] + help: Option, + }, + + #[error("unknown variable")] + #[diagnostic(severity(Error))] + UnknownVariable { + #[label("not in scope")] + span: Span, + names_in_scope: HashSet, + #[help] + help: Option, + }, + + #[error("unknown constructor")] + #[diagnostic(severity(Error))] + UnknownConstructor { + #[label("not in scope")] + span: Span, + names_in_scope: HashSet, + #[help] + help: Option, + }, + + #[error("refutable binder")] + #[diagnostic( + severity(Error), + help("missing patterns\n{}", render_not_covered(not_covered)) + )] + RefutableBinder { + not_covered: pattern_checker::NotCovered, + + #[label("not exhaustive")] + span: Span, + }, + + #[error("duplicate record field")] + #[diagnostic(severity(Error))] + DuplicateRecordField { + #[label("here")] + span: Span, + }, +} + +fn pluralize_args(wanted: usize) -> String { + if wanted == 1 { + "arg".to_string() + } else { + to_plural("arg") + } +} + +fn render_not_covered(not_covered: &pattern_checker::NotCovered) -> String { + let mut lines = not_covered + .iter() + .map(|pattern| format!("| {}", pattern.void())) + .collect::>(); + lines.sort(); + + lines.join("\n") +} + +impl Error { + pub fn explain_with_type_printer(self, print_type: impl Fn(&Type) -> String) -> Self { + self.explain_not_a_function(|actual_type| { + format!("expression has type {}", print_type(actual_type)) + }) + .explain_types_not_equal(|expected, actual| { + format!( + "expected {}\ngot {}", + print_type(expected), + print_type(actual) + ) + }) + .explain_unexpected_record_field(|label, record_like_type| { + format!("`{}` not in {}", label, print_type(record_like_type)) + }) + .explain_missing_record_fields(|missing| { + format!( + "need to add\n{}", + missing + .iter() + .map(|(label, t)| format!("{label}: {}", print_type(t))) + .collect::>() + .join("\n") + ) + }) + } + + pub fn explain_types_not_equal(self, explain: impl Fn(&Type, &Type) -> String) -> Self { + match self { + Self::TypesNotEqual { + span, + expected, + actual, + help: _, + } => { + let help = explain(&expected, &actual); + Self::TypesNotEqual { + span, + expected, + actual, + help: Some(help), + } + } + _ => self, + } + } + + pub fn explain_unexpected_record_field(self, explain: impl Fn(&Name, &Type) -> String) -> Self { + match self { + Self::UnexpectedRecordField { + span, + label, + record_like_type, + help: _, + } => { + let help = explain(&label, &record_like_type); + Self::UnexpectedRecordField { + span, + label, + record_like_type, + help: Some(help), + } + } + _ => self, + } + } + + pub fn explain_missing_record_fields( + self, + explain: impl Fn(&[(Name, Type)]) -> String, + ) -> Self { + match self { + Self::MissingRecordFields { + span, + missing, + help: _, + } => { + let help = explain(&missing); + Self::MissingRecordFields { + span, + missing, + help: Some(help), + } + } + _ => self, + } + } + + pub fn explain_not_a_function(self, explain: impl Fn(&Type) -> String) -> Self { + match self { + Self::NotAFunction { + span, + actual_type, + help: _, + } => { + let help = explain(&actual_type); + Self::NotAFunction { + span, + actual_type, + help: Some(help), + } + } + _ => self, + } + } + + pub fn suggest_variable_typo( + self, + explain: impl Fn(&HashSet) -> Option, + ) -> Self { + match self { + Self::UnknownVariable { + span, + names_in_scope, + help: _, + } => { + let help = explain(&names_in_scope); + Self::UnknownVariable { + span, + names_in_scope, + help, + } + } + _ => self, + } + } +} diff --git a/crates/ditto-type-checker/src/infer.rs b/crates/ditto-type-checker/src/infer.rs new file mode 100644 index 000000000..20859be26 --- /dev/null +++ b/crates/ditto-type-checker/src/infer.rs @@ -0,0 +1,376 @@ +use crate::{ + ast::{Expression, Label}, + env::{Env, EnvConstructor, EnvValue}, + error::Error, + outputs::Outputs, + result::Result, + state::State, + utils, + warning::Warning, +}; +use ditto_ast as ast; +use ditto_pattern_checker as pattern_checker; +use std::collections::HashSet; + +impl State { + pub fn infer(&mut self, env: &Env, expr: Expression) -> Result { + match expr { + Expression::Function { + span, + binders, + return_type_annotation, + box body, + } => { + if binders.is_empty() { + let (body, output) = self.typecheck(env, body, return_type_annotation)?; + let return_type = body.get_type().clone(); + let function_type = utils::mk_wobbly_function_type(vec![], return_type); + let expr = ast::Expression::Function { + span, + function_type, + binders: vec![], + body: Box::new(body), + }; + return Ok((expr, output)); + } + + let mut outputs = Outputs::default(); + let mut closure = env.clone(); + let mut parameters = Vec::with_capacity(binders.len()); + let mut ast_binders = Vec::with_capacity(binders.len()); + for (pattern, expected) in binders { + let (pattern, pattern_type, more_outputs) = + self.typecheck_pattern(&mut closure, pattern, expected)?; + + pattern_is_irrefutable(&env.pattern_constructors, &pattern, &pattern_type)?; + + parameters.push(pattern_type.clone()); + ast_binders.push((pattern, pattern_type)); + outputs.extend(more_outputs); + } + let (body, mut more_outputs) = + self.typecheck(&closure, body, return_type_annotation)?; + + // Check that binders were used + for binder_name in closure + .values + .keys() + .collect::>() + .difference(&env.values.keys().collect::>()) + { + if !more_outputs.variable_references.contains(*binder_name) { + if let Some(env_value) = closure.values.get(binder_name) { + let span = env_value.get_span(); + more_outputs.warnings.push(Warning::UnusedBinder { span }); + } + } + // Remove from the bubbling variable references as its shadowed + // (even though we don't currently allow _any_ shadowing, this could change in the future) + more_outputs.variable_references.remove(*binder_name); + } + outputs.extend(more_outputs); + let return_type = body.get_type().clone(); + let function_type = utils::mk_wobbly_function_type(parameters, return_type); + let expr = ast::Expression::Function { + span, + function_type, + binders: ast_binders, + body: Box::new(body), + }; + Ok((expr, outputs)) + } + + Expression::Call { + span, + box function, + arguments, + } => self.typecheck_call(env, span, function, arguments, None), + + Expression::If { + span, + box condition, + box true_clause, + box false_clause, + } => self.typecheck_conditional(env, span, condition, true_clause, false_clause, None), + Expression::Match { + span: _, + expression: _, + arms: _, + } => todo!(), + + Expression::Effect { span: _, effect: _ } => todo!(), + + Expression::RecordAccess { + span, + box target, + label: Label { label, .. }, + } => self.typecheck_record_access(env, span, target, label, None), + + Expression::RecordUpdate { + span, + box target, + updates, + } => { + let (target, mut outputs) = self.infer(env, target)?; + let (expr, more_outputs) = + self.typecheck_record_updates(env, span, target, updates)?; + outputs.extend(more_outputs); + Ok((expr, outputs)) + } + + Expression::Let { + span: _, + declaration: _, + expression: _, + } => todo!(), + + Expression::Constructor { + span, + ref constructor, + } => { + let env_constructor = + env.constructors + .get(constructor) + .ok_or_else(|| Error::UnknownConstructor { + span, + help: None, + names_in_scope: env.constructors.keys().cloned().collect(), + })?; + + // Register the reference! + let mut outputs = Outputs::default(); + if !outputs.constructor_references.contains(constructor) { + outputs.constructor_references.insert(constructor.clone()); + } + match env_constructor { + EnvConstructor::ModuleConstructor { + constructor_scheme, + constructor, + } => { + let constructor_type = + constructor_scheme.clone().instantiate(&mut self.supply); + let constructor = constructor.clone(); + let expr = ast::Expression::LocalConstructor { + span, + constructor_type, + constructor, + }; + Ok((expr, outputs)) + } + EnvConstructor::ImportedConstructor { + constructor_scheme, + constructor, + } => { + let constructor_type = + constructor_scheme.clone().instantiate(&mut self.supply); + let constructor = constructor.clone(); + let expr = ast::Expression::ImportedConstructor { + span, + constructor_type, + constructor, + }; + Ok((expr, outputs)) + } + } + } + Expression::Variable { span, ref variable } => { + let env_value = env + .values + .get(variable) + .ok_or_else(|| Error::UnknownVariable { + span, + help: None, + names_in_scope: env.values.keys().cloned().collect(), + })?; + + // Register the reference! + let mut outputs = Outputs::default(); + if !outputs.variable_references.contains(variable) { + outputs.variable_references.insert(variable.clone()); + } + match env_value { + EnvValue::LocalVariable { + span: introduction, + scheme, + variable, + } => { + let variable_type = scheme.clone().instantiate(&mut self.supply); + let variable = variable.clone(); + let expr = ast::Expression::LocalVariable { + introduction: *introduction, + span, + variable_type, + variable, + }; + Ok((expr, outputs)) + } + EnvValue::ModuleValue { + span: introduction, + scheme, + value: variable, + } => { + let variable_type = scheme.clone().instantiate(&mut self.supply); + let variable = variable.clone(); + let expr = ast::Expression::LocalVariable { + introduction: *introduction, + span, + variable_type, + variable, + }; + Ok((expr, outputs)) + } + EnvValue::ForeignValue { + span: introduction, + scheme, + value: variable, + } => { + let variable_type = scheme.clone().instantiate(&mut self.supply); + let variable = variable.clone(); + let expr = ast::Expression::ForeignVariable { + introduction: *introduction, + span, + variable_type, + variable, + }; + Ok((expr, outputs)) + } + EnvValue::ImportedValue { + span: introduction, + scheme, + value: variable, + } => { + let variable_type = scheme.clone().instantiate(&mut self.supply); + let variable = variable.clone(); + let expr = ast::Expression::ImportedVariable { + introduction: *introduction, + span, + variable_type, + variable, + }; + Ok((expr, outputs)) + } + } + } + Expression::Record { span, fields } => { + if fields.is_empty() { + let record_type = ast::Type::RecordClosed { + kind: ast::Kind::Type, + row: ast::Row::new(), + }; + let expr = ast::Expression::Record { + span, + record_type, + fields: ast::RecordFields::new(), + }; + return Ok((expr, Outputs::default())); + } + let fields_len = fields.len(); + let fields_iter = fields.into_iter(); + let mut row = ast::Row::with_capacity(fields_len); + let mut fields = ast::RecordFields::with_capacity(fields_len); + let mut outputs = Outputs::default(); + for (Label { span, label }, expr) in fields_iter { + if fields.contains_key(&label) { + return Err(Error::DuplicateRecordField { span }); + } + if !inflector::cases::snakecase::is_snake_case(&label.0) { + outputs + .warnings + .push(Warning::RecordLabelNotSnakeCase { span }); + } + let (expr, more_outputs) = self.infer(env, expr)?; + let field_type = expr.get_type().clone(); + row.insert(label.clone(), field_type); + fields.insert(label, expr); + outputs.extend(more_outputs); + } + let record_type = ast::Type::RecordClosed { + kind: ast::Kind::Type, + row, + }; + let expr = ast::Expression::Record { + span, + record_type, + fields, + }; + Ok((expr, outputs)) + } + Expression::Array { span, elements } => self.typecheck_array(env, span, elements, None), + Expression::String { span, value } => { + let value_type = ast::Type::PrimConstructor(ast::PrimType::String); + let expr = ast::Expression::String { + span, + value_type, + value, + }; + Ok((expr, Outputs::default())) + } + Expression::Int { span, value } => { + let value_type = ast::Type::PrimConstructor(ast::PrimType::Int); + let expr = ast::Expression::Int { + span, + value_type, + value, + }; + Ok((expr, Outputs::default())) + } + Expression::Float { span, value } => { + let value_type = ast::Type::PrimConstructor(ast::PrimType::Float); + let expr = ast::Expression::Float { + span, + value_type, + value, + }; + Ok((expr, Outputs::default())) + } + Expression::True { span } => { + let value_type = utils::mk_bool_type(); + let expr = ast::Expression::True { span, value_type }; + Ok((expr, Outputs::default())) + } + Expression::False { span } => { + let value_type = utils::mk_bool_type(); + let expr = ast::Expression::False { span, value_type }; + Ok((expr, Outputs::default())) + } + Expression::Unit { span } => { + let value_type = ast::Type::PrimConstructor(ast::PrimType::Unit); + let expr = ast::Expression::Unit { span, value_type }; + Ok((expr, Outputs::default())) + } + } + } +} + +fn pattern_is_irrefutable( + pattern_constructors: &pattern_checker::EnvConstructors, + pattern: &ast::Pattern, + pattern_type: &ast::Type, +) -> std::result::Result<(), Error> { + // If it's not a variable pattern then check it's infallible + if !matches!( + pattern, + ast::Pattern::Variable { .. } | ast::Pattern::Unused { .. } + ) { + pattern_checker::is_exhaustive(pattern_constructors, pattern_type, vec![pattern.clone()]) + .map_err(|err| match err { + pattern_checker::Error::NotCovered(not_covered) => Error::RefutableBinder { + span: pattern.get_span(), + not_covered, + }, + pattern_checker::Error::MalformedPattern { + wanted_nargs, + got_nargs, + } => Error::ArgumentLengthMismatch { + function_span: pattern.get_span(), + wanted: wanted_nargs, + got: got_nargs, + }, + pattern_checker::Error::RedundantClauses(_) => { + unreachable!("unexpected redundant clauses") + } + }) + } else { + Ok(()) + } +} diff --git a/crates/ditto-type-checker/src/lib.rs b/crates/ditto-type-checker/src/lib.rs new file mode 100644 index 000000000..059c37d5b --- /dev/null +++ b/crates/ditto-type-checker/src/lib.rs @@ -0,0 +1,60 @@ +#![feature(box_patterns)] +#![feature(iterator_try_collect)] +#![doc = include_str!("../README.md")] + +pub mod ast; +mod check; +mod constraint; +mod env; +mod error; +mod infer; +mod outputs; +mod result; +mod scheme; +mod state; +mod substitution; +mod supply; +#[cfg(test)] +mod tests; +mod typecheck; +mod unify; +mod utils; +mod warning; + +pub use env::Env; +pub use error::Error; +pub use outputs::Outputs; +pub use warning::{Warning, Warnings}; + +use ditto_ast::{Name, Type, Var}; + +/// Check the type of an expression given a typing [Env]. +pub fn typecheck_expression( + supply: Var, + env: &Env, + expression: ast::Expression, + expected_type: Option, +) -> Result<(ditto_ast::Expression, Outputs, Var), Error> { + let mut state = state::State { + supply: supply::Supply(supply), + substitution: substitution::Substitution::default(), + }; + let result = state.typecheck(env, expression, expected_type); + result.map(|(expr, mut outputs)| { + outputs.warnings.sort(); + ( + state.substitution.apply_expression(expr), + outputs, + state.supply.into(), + ) + }) +} + +/// Check the type of expressions that are cyclic. +pub fn typecheck_cyclic_expressions( + _supply: Var, + _env: Env, + _cyclic_expressions: Vec<(Name, ast::Expression, Option)>, +) { + todo!(); +} diff --git a/crates/ditto-type-checker/src/outputs.rs b/crates/ditto-type-checker/src/outputs.rs new file mode 100644 index 000000000..cffc8f9cd --- /dev/null +++ b/crates/ditto-type-checker/src/outputs.rs @@ -0,0 +1,27 @@ +use crate::{ + ast::{Constructor, Variable}, + warning::Warnings, +}; +use indexmap::IndexSet; + +#[derive(Default, Debug)] +pub struct Outputs { + pub warnings: Warnings, + pub variable_references: VariableReferences, + pub constructor_references: ConstructorReferences, +} + +impl Outputs { + pub(crate) fn extend(&mut self, other: Self) { + self.warnings.extend(other.warnings); + self.variable_references.extend(other.variable_references); + self.constructor_references + .extend(other.constructor_references); + } +} + +pub type VariableReferences = References; + +pub type ConstructorReferences = References; + +pub type References = IndexSet; // IndexSet because we want to remember insertion order (I think) diff --git a/crates/ditto-type-checker/src/result.rs b/crates/ditto-type-checker/src/result.rs new file mode 100644 index 000000000..2a1201fcb --- /dev/null +++ b/crates/ditto-type-checker/src/result.rs @@ -0,0 +1,3 @@ +use crate::{error::Error, outputs::Outputs}; + +pub type Result = std::result::Result<(T, Outputs), Error>; diff --git a/crates/ditto-type-checker/src/scheme.rs b/crates/ditto-type-checker/src/scheme.rs new file mode 100644 index 000000000..423dec93e --- /dev/null +++ b/crates/ditto-type-checker/src/scheme.rs @@ -0,0 +1,52 @@ +use crate::{ + substitution::Substitution, + supply::Supply, + utils::{self, TypeVars}, +}; +use ditto_ast::Type; + +/// A polymorphic type. +/// +/// Also known as "polytype". +#[derive(Debug, Clone)] +pub struct Scheme { + /// The "quantifier". + pub forall: TypeVars, + /// The enclosed type. + pub signature: Type, +} + +impl Scheme { + /// Converts a polytype type into a monotype type by creating fresh names + /// for each type variable that does not appear in the current typing environment. + pub fn instantiate(self, supply: &mut Supply) -> Type { + let Self { forall, signature } = self; + let substitution = Substitution( + forall + .into_iter() + .map(|var| (var, supply.fresh_type())) + .collect(), + ); + substitution.apply(signature) + } + + /// Returns the variables mentioned in the signature and not bound in the quantifier. + pub fn free_type_variables(&self) -> TypeVars { + &utils::type_variables(&self.signature) - &self.forall + } + + #[cfg(test)] + pub fn debug_render(&self) -> String { + if self.forall.is_empty() { + self.signature.debug_render() + } else { + let vars = self + .forall + .iter() + .map(|var| var.to_string()) + .collect::>() + .join(" "); + format!("forall {}. {}", vars, self.signature.debug_render()) + } + } +} diff --git a/crates/ditto-type-checker/src/state.rs b/crates/ditto-type-checker/src/state.rs new file mode 100644 index 000000000..86eb2fa6d --- /dev/null +++ b/crates/ditto-type-checker/src/state.rs @@ -0,0 +1,6 @@ +use crate::{substitution::Substitution, supply::Supply}; + +pub struct State { + pub supply: Supply, + pub substitution: Substitution, +} diff --git a/crates/ditto-type-checker/src/substitution.rs b/crates/ditto-type-checker/src/substitution.rs new file mode 100644 index 000000000..c6e3e34d8 --- /dev/null +++ b/crates/ditto-type-checker/src/substitution.rs @@ -0,0 +1,465 @@ +use crate::utils; +use ditto_ast::{Effect, Expression, Kind, LetValueDeclaration, Type, Var}; +use halfbrown::HashMap; + +#[derive(Default)] +pub struct Substitution(pub SubstitutionInner); + +pub type SubstitutionInner = HashMap; + +impl Substitution { + pub fn apply(&self, ast_type: Type) -> Type { + apply(&self.0, ast_type, 0) + } + + pub fn apply_expression(&self, expression: Expression) -> Expression { + use Expression::*; + match expression { + Call { + call_type, + span, + box function, + arguments, + } => Call { + call_type: self.apply(call_type), + span, + function: Box::new(self.apply_expression(function)), + arguments: arguments + .into_iter() + .map(|arg| self.apply_expression(arg)) + .collect(), + }, + Function { + span, + function_type, + binders, + box body, + } => Function { + span, + function_type: self.apply(function_type), + binders: binders + .into_iter() + .map(|(pattern, t)| (pattern, self.apply(t))) + .collect(), + body: Box::new(self.apply_expression(body)), + }, + If { + span, + output_type, + box condition, + box true_clause, + box false_clause, + } => If { + span, + output_type: self.apply(output_type), + condition: Box::new(self.apply_expression(condition)), + true_clause: Box::new(self.apply_expression(true_clause)), + false_clause: Box::new(self.apply_expression(false_clause)), + }, + Match { + span, + match_type, + box expression, + box arms, + } => Match { + span, + match_type: self.apply(match_type), + expression: Box::new(self.apply_expression(expression)), + arms: Box::new(arms.map(|(pattern, expr)| (pattern, self.apply_expression(expr)))), + }, + Effect { + span, + effect_type, + return_type, + effect, + } => Effect { + span, + effect_type: self.apply(effect_type), + return_type: self.apply(return_type), + effect: self.apply_effect(effect), + }, + LocalConstructor { + constructor_type, + span, + constructor, + } => LocalConstructor { + constructor_type: self.apply(constructor_type), + span, + constructor, + }, + ImportedConstructor { + constructor_type, + span, + constructor, + } => ImportedConstructor { + constructor_type: self.apply(constructor_type), + span, + constructor, + }, + LocalVariable { + introduction, + variable_type, + span, + variable, + } => LocalVariable { + introduction, + variable_type: self.apply(variable_type), + span, + variable, + }, + ForeignVariable { + introduction, + variable_type, + span, + variable, + } => ForeignVariable { + introduction, + variable_type: self.apply(variable_type), + span, + variable, + }, + ImportedVariable { + introduction, + variable_type, + span, + variable, + } => ImportedVariable { + introduction, + variable_type: self.apply(variable_type), + span, + variable, + }, + Array { + span, + element_type, + elements, + value_type, + } => Array { + span, + element_type: self.apply(element_type), + elements: elements + .into_iter() + .map(|element| self.apply_expression(element)) + .collect(), + value_type: self.apply(value_type), + }, + Record { + span, + record_type, + fields, + } => Record { + span, + record_type: self.apply(record_type), + fields: fields + .into_iter() + .map(|(label, expr)| (label, self.apply_expression(expr))) + .collect(), + }, + RecordAccess { + span, + field_type, + box target, + label, + } => RecordAccess { + span, + field_type: self.apply(field_type), + target: Box::new(self.apply_expression(target)), + label, + }, + RecordUpdate { + span, + record_type, + box target, + fields, + } => RecordUpdate { + span, + record_type: self.apply(record_type), + target: Box::new(self.apply_expression(target)), + fields: fields + .into_iter() + .map(|(label, expr)| (label, self.apply_expression(expr))) + .collect(), + }, + Let { + span, + declaration: + LetValueDeclaration { + pattern: decl_pattern, + expression_type: decl_type, + expression: box decl_expr, + }, + box expression, + } => Let { + span, + declaration: LetValueDeclaration { + pattern: decl_pattern, + expression_type: self.apply(decl_type), + expression: Box::new(self.apply_expression(decl_expr)), + }, + expression: Box::new(self.apply_expression(expression)), + }, + True { span, value_type } => True { + span, + value_type: self.apply(value_type), + }, + False { span, value_type } => False { + span, + value_type: self.apply(value_type), + }, + Unit { span, value_type } => Unit { + span, + value_type: self.apply(value_type), + }, + String { + span, + value, + value_type, + } => String { + span, + value, + value_type: self.apply(value_type), + }, + Int { + span, + value, + value_type, + } => Int { + span, + value, + value_type: self.apply(value_type), + }, + Float { + span, + value, + value_type, + } => Float { + span, + value, + value_type: self.apply(value_type), + }, + } + } + + fn apply_effect(&self, effect: Effect) -> Effect { + if self.0.is_empty() { + return effect; + } + + match effect { + Effect::Return { expression } => Effect::Return { expression }, + Effect::Bind { + name, + box expression, + box rest, + } => Effect::Bind { + name, + expression: Box::new(self.apply_expression(expression)), + rest: Box::new(self.apply_effect(rest)), + }, + Effect::Expression { + box expression, + rest: None, + } => Effect::Expression { + expression: Box::new(self.apply_expression(expression)), + rest: None, + }, + Effect::Expression { + box expression, + rest: Some(box rest), + } => Effect::Expression { + expression: Box::new(self.apply_expression(expression)), + rest: Some(Box::new(self.apply_effect(rest))), + }, + Effect::Let { + pattern, + box expression, + box rest, + } => Effect::Let { + pattern, + expression: Box::new(self.apply_expression(expression)), + rest: Box::new(self.apply_effect(rest)), + }, + } + } +} + +fn apply(subst: &SubstitutionInner, ast_type: Type, depth: usize) -> Type { + // NOTE: substitution proceeds to a fixed point (i.e. recursively), + // which is why we need an occurs check during unification! + if depth > 100 { + // Panicking like this is nicer than a stackoverflow + panic!("Substitution exceeded max depth:\nsubst = {subst:#?}\nast_type = {ast_type:#?}",); + } + match ast_type { + Type::Variable { var, .. } => { + if let Some(t) = subst.get(&var) { + apply(subst, t.clone(), depth + 1) + } else { + ast_type + } + } + Type::RecordOpen { + kind, + var, + row, + source_name, + is_rigid, + } => { + match subst.get(&var).cloned() { + Some(Type::RecordOpen { + kind: _, + var, + source_name, + is_rigid, + row: new_row, + }) => { + let t = Type::RecordOpen { + kind: Kind::Type, + var, + source_name, + is_rigid, + row: row + .into_iter() + .chain(new_row) + .map(|(label, t)| (label, apply(subst, t, depth))) + .collect(), + }; + apply(subst, t, depth + 1) // REVIEW: is this `depth + 1` ? + } + Some(Type::RecordClosed { + kind: _, + row: new_row, + }) => Type::RecordClosed { + kind: Kind::Type, + row: row + .into_iter() + .chain(new_row) + .map(|(label, t)| (label, apply(subst, t, depth))) + .collect(), + }, + // This will happen as a result of instantiation + Some(Type::Variable { + var, + source_name, + is_rigid, + .. + }) => { + let t = Type::RecordOpen { + var, // swap out the var + source_name, + kind, + is_rigid, + row: row + .into_iter() + .map(|(label, t)| (label, apply(subst, t, depth))) + .collect(), + }; + apply(subst, t, depth + 1) + } + Some(wut) => { + unreachable!("unexpected open record substitution: {:?}", wut) + } + None => Type::RecordOpen { + kind, + var, + source_name, + is_rigid, + row: row + .into_iter() + .map(|(label, t)| (label, apply(subst, t, depth))) + .collect(), + }, + } + } + Type::Call { + function: + box Type::ConstructorAlias { + canonical_value, + constructor_kind, + source_value, + alias_variables, + box aliased_type, + }, + box arguments, + } => { + let arguments = arguments.map(|arg| apply(subst, arg, depth)); + let alias_variables = alias_variables + .into_iter() + .map(|var| apply_var(subst, var)) + .collect::>(); + + let mut subst = subst.clone(); + for (var, t) in alias_variables.iter().zip(arguments.iter()) { + // hmmmmmm...feels hacky doing an occurs check like this...? + if !utils::type_variables(t).contains(*var) { + subst.insert(*var, t.clone()); + } + } + let aliased_type = Box::new(apply(&subst, aliased_type, depth)); + let function = Type::ConstructorAlias { + canonical_value, + constructor_kind, + source_value, + alias_variables, + aliased_type, + }; + Type::Call { + function: Box::new(function), + arguments: Box::new(arguments), + } + } + Type::Call { + box function, + box arguments, + } => Type::Call { + function: Box::new(apply(subst, function, depth)), + arguments: Box::new(arguments.map(|argument| apply(subst, argument, depth))), + }, + Type::Function { + parameters, + box return_type, + } => Type::Function { + parameters: parameters + .into_iter() + .map(|t| apply(subst, t, depth)) + .collect(), + return_type: Box::new(apply(subst, return_type, depth)), + }, + Type::RecordClosed { kind, row } => Type::RecordClosed { + kind, + row: row + .into_iter() + .map(|(label, t)| (label, apply(subst, t, depth))) + .collect(), + }, + Type::ConstructorAlias { + constructor_kind, + canonical_value, + source_value, + alias_variables, + box aliased_type, + } => Type::ConstructorAlias { + constructor_kind, + canonical_value, + source_value, + alias_variables: alias_variables + .into_iter() + .map(|var| apply_var(subst, var)) + .collect(), + aliased_type: Box::new(apply(subst, aliased_type, depth)), + }, + Type::Constructor { + constructor_kind: _, + canonical_value: _, + source_value: _, + } + | Type::PrimConstructor(_) => ast_type, + } +} + +fn apply_var(subst: &SubstitutionInner, var: Var) -> Var { + match subst.get(&var) { + Some(Type::Variable { var, .. }) => apply_var(subst, *var), + _ => var, + } +} diff --git a/crates/ditto-type-checker/src/supply.rs b/crates/ditto-type-checker/src/supply.rs new file mode 100644 index 000000000..bd412e0f7 --- /dev/null +++ b/crates/ditto-type-checker/src/supply.rs @@ -0,0 +1,38 @@ +use ditto_ast::{Kind, Row, Type, Var}; + +pub struct Supply(pub Var); + +impl Supply { + pub fn fresh(&mut self) -> Var { + let var = self.0; + self.0 += 1; + var + } + + pub fn fresh_type(&mut self) -> Type { + let var = self.fresh(); + Type::Variable { + variable_kind: Kind::Type, + var, + source_name: None, + is_rigid: false, + } + } + + pub fn fresh_row(&mut self, row: Row) -> Type { + Type::RecordOpen { + kind: Kind::Type, + var: self.fresh(), + source_name: None, + is_rigid: false, + row, + } + } +} + +#[allow(clippy::from_over_into)] +impl std::convert::Into for Supply { + fn into(self) -> Var { + self.0 + } +} diff --git a/crates/ditto-type-checker/src/tests.rs b/crates/ditto-type-checker/src/tests.rs new file mode 100644 index 000000000..bf7c2f82c --- /dev/null +++ b/crates/ditto-type-checker/src/tests.rs @@ -0,0 +1,303 @@ +#[test] +fn testdata() { + use std::fmt::Write; + miette::set_hook(Box::new(|_| { + Box::new( + miette::MietteHandlerOpts::new() + .unicode(true) + .color(false) + .build(), + ) + })) + .unwrap(); + + datadriven::walk("tests/testdata", |f| { + f.run(|test_case| -> String { + let input = test_case.input.to_string(); + let parse_result = ditto_cst::Expression::parse(&input); + match parse_result { + Err(err) => { + format!("{:?}", miette::Report::from(err.into_report("", input))) + } + Ok(cst_expression) => { + let (env, var) = mk_testdata_env(); + let mut supply = crate::supply::Supply(var); + let expression = crate::ast::Expression::from_cst( + cst_expression, + &mut std::collections::HashMap::new(), + &mut supply, + ); + + let typecheck_result = + crate::typecheck_expression(supply.into(), &env, expression, None); + + match typecheck_result { + Err(err) => { + let err = err.explain_with_type_printer(|t| t.debug_render()); + let report = miette::Report::from(err).with_source_code(input); + format!("{report:?}\n") + } + Ok((expression, crate::Outputs { mut warnings, .. }, _var)) => { + let mut out = String::new(); + writeln!(out, "{}", expression.get_type().debug_render()).unwrap(); + if !warnings.0.is_empty() { + warnings.sort(); + let report = miette::Report::from(warnings).with_source_code(input); + writeln!(out, "{report:?}").unwrap(); + } + out + } + } + } + } + }); + }); +} + +fn mk_testdata_env() -> (crate::Env, ditto_ast::Var) { + use ditto_ast::{ + module_name, name, proper_name, unqualified, FullyQualifiedProperName, Kind, Type, + }; + let mut env = crate::Env::default(); + + let abc_type = || Type::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: (None, module_name!("Test")), + value: proper_name!("ABC"), + }, + source_value: Some(unqualified(proper_name!("ABC"))), + }; + env.insert_module_constructor(proper_name!("A"), abc_type()); + env.insert_module_constructor(proper_name!("B"), abc_type()); + env.insert_module_constructor(proper_name!("C"), abc_type()); + + let var = &mut 0; + let mut type_var = |source_name| { + let t = Type::Variable { + variable_kind: Kind::Type, + var: *var, + source_name, + is_rigid: true, + }; + *var += 1; + t + }; + let a = type_var(Some(name!("a"))); + let maybe_type = || Type::Call { + function: Box::new(Type::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: (None, module_name!("Test")), + value: proper_name!("Maybe"), + }, + source_value: Some(unqualified(proper_name!("Maybe"))), + }), + arguments: Box::new(nonempty::NonEmpty { + head: a.clone(), + tail: vec![], + }), + }; + + env.insert_module_constructor( + proper_name!("Just"), + Type::Function { + parameters: vec![a.clone()], + return_type: Box::new(maybe_type()), + }, + ); + env.insert_module_constructor(proper_name!("Nothing"), maybe_type()); + + let b = type_var(Some(name!("b"))); + let c = type_var(Some(name!("c"))); + let d = type_var(Some(name!("d"))); + let thruple_type = || Type::Call { + function: Box::new(Type::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: (None, module_name!("Test")), + value: proper_name!("Thruple"), + }, + source_value: Some(unqualified(proper_name!("Thruple"))), + }), + arguments: Box::new(nonempty::NonEmpty { + head: b.clone(), + tail: vec![c.clone(), d.clone()], + }), + }; + env.insert_module_constructor( + proper_name!("Thruple"), + Type::Function { + parameters: vec![b.clone(), c.clone(), d.clone()], + return_type: Box::new(thruple_type()), + }, + ); + + let e = type_var(Some(name!("e"))); + let wrapper_type = || Type::Call { + function: Box::new(Type::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: (None, module_name!("Test")), + value: proper_name!("Wrapper"), + }, + source_value: Some(unqualified(proper_name!("Wrapper"))), + }), + arguments: Box::new(nonempty::NonEmpty { + head: e.clone(), + tail: vec![], + }), + }; + env.insert_module_constructor( + proper_name!("Wrapper"), + Type::Function { + parameters: vec![e.clone()], + return_type: Box::new(wrapper_type()), + }, + ); + (env, *var) +} + +#[test] +fn generalization() { + use crate::{ + env::{Env, EnvValue}, + scheme::Scheme, + supply::Supply, + utils::TypeVars, + }; + use ditto_ast::{ + name, unqualified, Kind, Span, + Type::{self, *}, + }; + // Zero span, not needed here. + let span = Span { + start_offset: 0, + end_offset: 1, + }; + + let empty_env = Env::default(); + + // Let's start with the identity function type... + let identity_type = mk_identity_type(0); + assert_eq!("(a$0!) -> a$0!", identity_type.debug_render()); + + // In an empty environment, this generalizes to... + let identity_scheme = empty_env.generalize(identity_type); + assert_eq!("forall 0. (a$0!) -> a$0!", identity_scheme.debug_render(),); + + // If we then bound it at the top level, + // lookups would instantiate with a fresh type. + // + // Also known as "let generalization" + let mut supply = Supply(1); + let mut env = empty_env; + let identity_name = || unqualified(name!("identity")); + env.values.insert( + identity_name(), + EnvValue::ModuleValue { + span, + scheme: identity_scheme, + value: identity_name().value, + }, + ); + assert_eq!( + "($1) -> $1", + env.values + .get(&identity_name()) + .unwrap() + .get_scheme() + .clone() + .instantiate(&mut supply) + .debug_render() + ); + assert_eq!( + "($2) -> $2", + env.values + .get(&identity_name()) + .unwrap() + .get_scheme() + .clone() + .instantiate(&mut supply) + .debug_render(), + ); + + // But suppose we were in a function body such as: + // fn (arg0, arg1: a) -> ... + let arg0_name = || unqualified(name!("arg0")); + let arg0_scheme = Scheme { + forall: TypeVars::new(), + signature: Variable { + is_rigid: false, + source_name: None, + variable_kind: Kind::Type, + var: supply.fresh(), + }, + }; + assert_eq!("$3", arg0_scheme.signature.debug_render()); + assert_eq!("$3", arg0_scheme.debug_render()); + env.values.insert( + arg0_name(), + EnvValue::LocalVariable { + span, + scheme: arg0_scheme, + variable: arg0_name().value, + }, + ); + + let arg1_name = || unqualified(name!("arg1")); + let arg1_scheme = Scheme { + forall: TypeVars::new(), + signature: Variable { + is_rigid: true, + source_name: Some(name!("a")), + variable_kind: Kind::Type, + var: supply.fresh(), + }, + }; + assert_eq!("a$4!", arg1_scheme.signature.debug_render()); + assert_eq!("a$4!", arg1_scheme.debug_render()); + env.values.insert( + arg1_name(), + EnvValue::LocalVariable { + span, + scheme: arg1_scheme, + variable: arg1_name().value, + }, + ); + + // When we lookup those variables we then get the same typs back + assert_eq!( + "$3", + env.values + .get(&arg0_name()) + .unwrap() + .get_scheme() + .clone() + .instantiate(&mut supply) + .debug_render(), + ); + assert_eq!( + "a$4!", + env.values + .get(&arg1_name()) + .unwrap() + .get_scheme() + .clone() + .instantiate(&mut supply) + .debug_render(), + ); + + fn mk_identity_type(var: usize) -> Type { + let a = Variable { + is_rigid: true, + source_name: Some(name!("a")), + variable_kind: Kind::Type, + var, + }; + Function { + parameters: vec![a.clone()], + return_type: Box::new(a.clone()), + } + } +} diff --git a/crates/ditto-type-checker/src/typecheck.rs b/crates/ditto-type-checker/src/typecheck.rs new file mode 100644 index 000000000..696bad17b --- /dev/null +++ b/crates/ditto-type-checker/src/typecheck.rs @@ -0,0 +1,425 @@ +use crate::{ + ast::{Arguments, Expression, Label, Pattern, RecordFields}, + constraint::Constraint, + env::{Env, EnvConstructor}, + error::Error, + outputs::Outputs, + result::Result, + scheme::Scheme, + state::State, + utils, + warning::Warning, +}; +use ditto_ast::{self as ast, Row, Span}; + +impl State { + pub fn typecheck( + &mut self, + env: &Env, + expr: Expression, + expected: Option, + ) -> Result { + if let Some(expected) = expected { + self.check(env, expr, expected) + } else { + self.infer(env, expr) + } + } + + pub fn typecheck_call( + &mut self, + env: &Env, + span: Span, + function: Expression, + arguments: Arguments, + expected: Option, + ) -> Result { + let function_span = function.get_span(); + let (function, mut outputs) = self.infer(env, function)?; + + let function_type = function.get_type().clone(); + let function_type = utils::unalias_type(function_type); + let function_type = self.substitution.apply(function_type); + + match function_type { + ast::Type::Function { + parameters, + box return_type, + } => { + let arguments_len = arguments.len(); + let parameters_len = parameters.len(); + if arguments_len != parameters_len { + return Err(Error::ArgumentLengthMismatch { + function_span, + wanted: parameters_len, + got: arguments_len, + }); + } + let mut ast_arguments = Vec::with_capacity(arguments.len()); + for (expr, expected) in arguments.into_iter().zip(parameters.into_iter()) { + let (argument, more_outputs) = self.check(env, expr, expected)?; + ast_arguments.push(argument); + outputs.extend(more_outputs); + } + if let Some(expected) = expected { + let constraint = Constraint { + expected, + actual: return_type.clone(), + }; + self.unify(span, constraint)?; + } + let expr = ast::Expression::Call { + span, + call_type: return_type, + function: Box::new(function), + arguments: ast_arguments, + }; + Ok((expr, outputs)) + } + type_variable @ ast::Type::Variable { .. } => { + let mut ast_arguments = Vec::with_capacity(arguments.len()); + let mut parameters = Vec::with_capacity(arguments.len()); + for expr in arguments { + let (argument, more_outputs) = self.infer(env, expr)?; + parameters.push(argument.get_type().clone()); + ast_arguments.push(argument); + outputs.extend(more_outputs); + } + + let call_type = expected.unwrap_or_else(|| self.supply.fresh_type()); + + let constraint = Constraint { + expected: ast::Type::Function { + parameters, + return_type: Box::new(call_type.clone()), + }, + actual: type_variable, + }; + self.unify(span, constraint)?; + + let expr = ast::Expression::Call { + span, + call_type, + function: Box::new(function), + arguments: ast_arguments, + }; + Ok((expr, outputs)) + } + _ => Err(Error::NotAFunction { + span: function_span, + actual_type: function.get_type().clone(), + help: None, + }), + } + } + + pub fn typecheck_pattern( + &mut self, + env: &mut Env, + pattern: Pattern, + expected: Option, + ) -> std::result::Result<(ast::Pattern, ast::Type, Outputs), Error> { + match pattern { + Pattern::Constructor { + span, + arguments, + constructor, + constructor_span, + } => { + let env_constructor = + env.constructors.get(&constructor).cloned().ok_or_else(|| { + Error::UnknownConstructor { + span, + help: None, + names_in_scope: env.constructors.keys().cloned().collect(), + } + })?; + + #[allow(clippy::type_complexity)] + let (mk_constructor, constructor_scheme): ( + Box) -> ast::Pattern>, + Scheme, + ) = match env_constructor { + EnvConstructor::ModuleConstructor { + constructor, + constructor_scheme, + } => ( + Box::new(|span, checked_arguments| ast::Pattern::LocalConstructor { + span, + constructor, + arguments: checked_arguments, + }), + constructor_scheme, + ), + EnvConstructor::ImportedConstructor { + constructor, + constructor_scheme, + } => ( + Box::new( + |span, checked_arguments| ast::Pattern::ImportedConstructor { + span, + constructor, + arguments: checked_arguments, + }, + ), + constructor_scheme, + ), + }; + + let pattern_type = constructor_scheme.instantiate(&mut self.supply); + + if let ast::Type::Function { + parameters, + return_type: box pattern_type, + } = pattern_type + { + if arguments.len() != parameters.len() { + return Err(Error::ArgumentLengthMismatch { + function_span: constructor_span, + wanted: parameters.len(), + got: arguments.len(), + }); + } + let mut outputs = Outputs::default(); + let mut checked_arguments = Vec::with_capacity(arguments.len()); + for (argument, parameter) in arguments.into_iter().zip(parameters.into_iter()) { + let (argument, _, more_outputs) = + self.typecheck_pattern(env, argument, Some(parameter))?; + checked_arguments.push(argument); + outputs.extend(more_outputs); + } + if let Some(expected) = expected { + self.unify( + span, + Constraint { + expected, + actual: pattern_type.clone(), + }, + )?; + } + Ok(( + mk_constructor(span, checked_arguments), + self.substitution.apply(pattern_type), + outputs, + )) + } else if arguments.is_empty() { + if let Some(expected) = expected { + self.unify( + span, + Constraint { + expected, + actual: pattern_type.clone(), + }, + )?; + } + Ok(( + mk_constructor(span, vec![]), + self.substitution.apply(pattern_type), + Outputs::default(), + )) + } else { + Err(Error::ArgumentLengthMismatch { + function_span: constructor_span, + got: arguments.len(), + wanted: 0, + }) + } + } + Pattern::Variable { span, name } => { + let mut outputs = Outputs::default(); + if !inflector::cases::snakecase::is_snake_case(&name.0) { + outputs + .warnings + .push(Warning::VariableNotSnakeCase { span }); + } + let pattern_type = expected.unwrap_or_else(|| self.supply.fresh_type()); + env.insert_local_variable(span, name.clone(), pattern_type.clone())?; + Ok((ast::Pattern::Variable { span, name }, pattern_type, outputs)) + } + Pattern::Unused { span, unused_name } => { + let pattern_type = expected.unwrap_or_else(|| self.supply.fresh_type()); + Ok(( + ast::Pattern::Unused { span, unused_name }, + pattern_type, + Outputs::default(), + )) + } + } + } + + pub fn typecheck_record_access( + &mut self, + env: &Env, + span: Span, + target: Expression, + label: ast::Name, + expected: Option, + ) -> Result { + let field_type = expected.unwrap_or_else(|| self.supply.fresh_type()); + let expected = self.supply.fresh_row({ + let mut row = Row::new(); + row.insert(label.clone(), field_type.clone()); + row + }); + let (target, outputs) = self.check(env, target, expected)?; + Ok(( + ast::Expression::RecordAccess { + span, + field_type, + target: Box::new(target), + label, + }, + outputs, + )) + } + + pub fn typecheck_array( + &mut self, + env: &Env, + span: Span, + elements: Vec, + expected_element_type: Option, + ) -> Result { + if elements.is_empty() { + let element_type = self.supply.fresh_type(); + let value_type = utils::mk_array_type(element_type.clone()); + let expr = ast::Expression::Array { + span, + value_type, + element_type, + elements: Vec::new(), + }; + return Ok((expr, Outputs::default())); + } + let elements_len = elements.len(); + let mut elements_iter = elements.into_iter(); + let head = elements_iter.next().unwrap(); + let (head, mut outputs) = self.typecheck(env, head, expected_element_type)?; + let element_type = head.get_type().clone(); + let mut elements = Vec::with_capacity(elements_len); + elements.push(head); + for element in elements_iter { + let (element, more_outputs) = self.check(env, element, element_type.clone())?; + elements.push(element); + outputs.extend(more_outputs); + } + let value_type = utils::mk_array_type(element_type.clone()); + let expr = ast::Expression::Array { + span, + value_type, + element_type, + elements, + }; + Ok((expr, outputs)) + } + + pub fn typecheck_conditional( + &mut self, + env: &Env, + span: Span, + condition: Expression, + true_clause: Expression, + false_clause: Expression, + expected: Option, + ) -> Result { + let (condition, mut outputs) = self.check(env, condition, utils::mk_bool_type())?; + let (true_clause, more_outputs) = self.typecheck(env, true_clause, expected)?; + outputs.extend(more_outputs); + let output_type = true_clause.get_type().clone(); + let (false_clause, more_outputs) = self.check(env, false_clause, output_type.clone())?; + outputs.extend(more_outputs); + let expr = ast::Expression::If { + span, + output_type, + condition: Box::new(condition), + true_clause: Box::new(true_clause), + false_clause: Box::new(false_clause), + }; + Ok((expr, outputs)) + } + + pub fn typecheck_record_updates( + &mut self, + env: &Env, + span: Span, + target: ast::Expression, + updates: RecordFields, + ) -> Result { + let mut outputs = Outputs::default(); + let target_type = target.get_type().clone(); + let target_type = utils::unalias_type(target_type); + let target_type = self.substitution.apply(target_type); + match target_type { + ast::Type::RecordClosed { ref row, .. } + | ast::Type::RecordOpen { + is_rigid: true, + ref row, + .. + } => { + let mut fields = ast::RecordFields::with_capacity(updates.len()); + for (Label { span, label }, expr) in updates { + if fields.contains_key(&label) { + return Err(Error::DuplicateRecordField { span }); + } + if let Some(expected) = row.get(&label) { + let (expr, more_outputs) = self.check(env, expr, expected.clone())?; + fields.insert(label, expr); + outputs.extend(more_outputs) + } else { + return Err(Error::UnexpectedRecordField { + span, + label, + record_like_type: target_type, + help: None, + }); + } + } + Ok(( + ast::Expression::RecordUpdate { + span, + record_type: target_type, + target: Box::new(target), + fields, + }, + outputs, + )) + } + _ => { + let mut fields = ast::RecordFields::with_capacity(updates.len()); + let mut row = Row::with_capacity(updates.len()); + for (Label { span, label }, update) in updates { + if fields.contains_key(&label) { + return Err(Error::DuplicateRecordField { span }); + } + let (update, more_outputs) = self.infer(env, update)?; + row.insert(label.clone(), update.get_type().clone()); + fields.insert(label, update); + outputs.extend(more_outputs); + } + let record_type = ast::Type::RecordOpen { + kind: ast::Kind::Type, + var: self.supply.fresh(), + source_name: None, + is_rigid: false, + row, + }; + self.unify( + target.get_span(), + Constraint { + expected: record_type.clone(), + actual: target.get_type().clone(), + }, + )?; + Ok(( + ast::Expression::RecordUpdate { + span, + record_type, + target: Box::new(target), + fields, + }, + outputs, + )) + } + } + } +} diff --git a/crates/ditto-type-checker/src/unify.rs b/crates/ditto-type-checker/src/unify.rs new file mode 100644 index 000000000..dda3c1fa6 --- /dev/null +++ b/crates/ditto-type-checker/src/unify.rs @@ -0,0 +1,446 @@ +use crate::{ + constraint::Constraint, error::Error, state::State, substitution::SubstitutionInner, + supply::Supply, utils, +}; +use ditto_ast::{Kind, Span, Type, Var}; +use std::collections::HashSet; +use tracing::{error, trace}; + +impl State { + pub fn unify(&mut self, span: Span, constraint: Constraint) -> std::result::Result<(), Error> { + let Constraint { actual, expected } = constraint; + let actual = self.substitution.apply(actual); + let expected = self.substitution.apply(expected); + unify( + &mut self.supply, + &mut self.substitution.0, + &expected, + &actual, + ) + .map_err(|err| { + error!("{err:?}"); + match err { + UnificationError::TypesNotEqual => Error::TypesNotEqual { + span, + expected, + actual, + help: None, + }, + UnificationError::InfiniteType { var, infinite_type } => Error::InfiniteType { + span, + infinite_type, + var, + }, + } + })?; + Ok(()) + } +} + +#[derive(Debug)] +enum UnificationError { + TypesNotEqual, + InfiniteType { var: usize, infinite_type: Type }, +} + +type Result = std::result::Result<(), UnificationError>; + +fn unify( + supply: &mut Supply, + subst: &mut SubstitutionInner, + expected: &Type, + actual: &Type, +) -> Result { + trace!( + "{expected} ~ {actual}", + expected = expected.debug_render(), + actual = actual.debug_render() + ); + use Type::*; + match (expected, actual) { + ( + Type::Variable { + source_name: Some(expected), + is_rigid: true, + .. + }, + Type::Variable { + source_name: Some(actual), + .. + }, + ) if expected == actual => Ok(()), + + ( + Type::Variable { + source_name: Some(_), + is_rigid: false, + var, + .. + }, + t, + ) + | ( + Variable { + source_name: None, + var, + .. + }, + t, + ) + | ( + t, + Variable { + source_name: None, + var, + .. + }, + ) => bind(subst, *var, t), + + ( + Constructor { + canonical_value: expected, + .. + }, + Constructor { + canonical_value: actual, + .. + }, + ) if expected == actual => Ok(()), + + (PrimConstructor(expected), PrimConstructor(actual)) if expected == actual => Ok(()), + + ( + Type::Call { + function: box expected_function, + arguments: expected_arguments, + }, + Type::Call { + function: box actual_function, + arguments: actual_arguments, + }, + ) => { + unify(supply, subst, expected_function, actual_function)?; + let expected_arguments_len = expected_arguments.len(); + let actual_arguments_len = actual_arguments.len(); + if expected_arguments_len != actual_arguments_len { + return Err(UnificationError::TypesNotEqual); + } + let arguments = expected_arguments.iter().zip(actual_arguments.iter()); + for (expected_argument, actual_argument) in arguments { + unify(supply, subst, expected_argument, actual_argument)?; + } + Ok(()) + } + ( + Type::Function { + parameters: expected_parameters, + return_type: expected_return_type, + }, + Type::Function { + parameters: actual_parameters, + return_type: actual_return_type, + }, + ) => { + if expected_parameters.len() != actual_parameters.len() { + return Err(UnificationError::TypesNotEqual); + } + for (expected, actual) in expected_parameters.iter().zip(actual_parameters.iter()) { + unify(supply, subst, expected, actual)? + } + unify(supply, subst, expected_return_type, actual_return_type) + } + // + // TODO: unify type aliases + // + ( + Type::RecordClosed { + row: expected_row, .. + }, + Type::RecordClosed { + row: actual_row, .. + }, + ) => { + let expected_row_keys: HashSet<_> = expected_row.keys().collect(); + let actual_row_keys: HashSet<_> = actual_row.keys().collect(); + + if expected_row_keys != actual_row_keys { + return Err(UnificationError::TypesNotEqual); + } + + for (key, expected) in expected_row { + let actual = actual_row.get(key).expect("keys to be equal"); + unify(supply, subst, expected, actual)?; + } + Ok(()) + } + ( + closed_record_type @ Type::RecordClosed { + row: closed_row, .. + }, + Type::RecordOpen { + var, row: open_row, .. + }, + ) => { + let closed_row_keys: HashSet<_> = closed_row.keys().collect(); + let open_row_keys: HashSet<_> = open_row.keys().collect(); + + if !open_row_keys.is_subset(&closed_row_keys) { + return Err(UnificationError::TypesNotEqual); + } + + for (key, actual) in open_row { + let expected = closed_row.get(key).expect("open row keys to be a subset"); + unify(supply, subst, expected, actual)?; + } + bind(subst, *var, closed_record_type) + } + ( + Type::RecordOpen { + var, + row: open_row, + // only unify an open record with a closed record if the + // open record has been inferred + is_rigid: false, + .. + }, + closed_record_type @ Type::RecordClosed { + row: closed_row, .. + }, + ) => { + let closed_row_keys: HashSet<_> = closed_row.keys().collect(); + let open_row_keys: HashSet<_> = open_row.keys().collect(); + + if !open_row_keys.is_subset(&closed_row_keys) { + return Err(UnificationError::TypesNotEqual); + } + + for (key, expected) in open_row { + let actual = closed_row.get(key).expect("open row keys to be a subset"); + unify(supply, subst, expected, actual)?; + } + bind(subst, *var, closed_record_type) + } + // ( + // Type::RecordOpen { + // source_name: Some(expected_source_name), + // is_rigid: true, + // row: expected_row, + // .. + // }, + // Type::RecordOpen { + // source_name: Some(actual_source_name), + // row: actual_row, + // .. + // }, + // ) if expected_source_name == actual_source_name => { + // for (label, expected_type) in expected_row.iter() { + // if let Some(actual_type) = actual_row.remove(label) { + // let constraint = Constraint { + // expected: expected_type.clone(), + // actual: actual_type, + // }; + // unify_else(state, span, constraint, Some(&err))?; + // } + // } + // if !actual_row.is_empty() { + // // If `actual_row` still has entries then these entries + // // aren't in both record types, so fail. + // return Err(err); + // } + // Ok(()) + // } + // ( + // Type::RecordOpen { + // kind: _, + // var: named_var, + // row: named_row, + // source_name: source_name @ Some(_), + // }, + // Type::RecordOpen { + // kind: _, + // var: unnamed_var, + // row: mut unnamed_row, + // source_name: None, + // }, + // ) => { + // for (label, expected_type) in named_row.iter() { + // if let Some(actual_type) = unnamed_row.remove(label) { + // let constraint = Constraint { + // expected: expected_type.clone(), + // actual: actual_type, + // }; + // unify_else(state, span, constraint, Some(&err))?; + // } + // } + // if !unnamed_row.is_empty() { + // return Err(err); + // } + // let var = state.supply.fresh(); + // let bound_type = Type::RecordOpen { + // kind: Kind::Type, + // var, + // row: named_row, + // source_name, + // }; + // bind(state, span, unnamed_var, bound_type.clone())?; + // bind(state, span, named_var, bound_type)?; + // Ok(()) + // } + // ( + // Type::RecordOpen { + // kind: _, + // var: unnamed_var, + // row: mut unnamed_row, + // source_name: None, + // }, + // Type::RecordOpen { + // kind: _, + // var: named_var, + // row: named_row, + // source_name: source_name @ Some(_), + // }, + // ) => { + // for (label, actual_type) in named_row.iter() { + // if let Some(expected_type) = unnamed_row.remove(label) { + // let constraint = Constraint { + // expected: expected_type, + // actual: actual_type.clone(), + // }; + // unify_else(state, span, constraint, Some(&err))?; + // } + // } + // if !unnamed_row.is_empty() { + // return Err(err); + // } + // let var = state.supply.fresh(); + // let bound_type = Type::RecordOpen { + // kind: Kind::Type, + // var, + // row: named_row, + // source_name, + // }; + // bind(state, span, unnamed_var, bound_type.clone())?; + // bind(state, span, named_var, bound_type)?; + // Ok(()) + // } + ( + Type::RecordOpen { + var: expected_var, + row: expected_row, + source_name: None, + .. + }, + actual @ Type::RecordOpen { + row: actual_row, + is_rigid: true, + .. + }, + ) => { + let expected_row_keys: HashSet<_> = expected_row.keys().collect(); + let actual_row_keys: HashSet<_> = actual_row.keys().collect(); + if !expected_row_keys.is_subset(&actual_row_keys) { + return Err(UnificationError::TypesNotEqual); + } + + for key in expected_row_keys { + let expected = expected_row.get(key).expect("to have subset key"); + let actual = actual_row.get(key).expect("to have subset key"); + unify(supply, subst, expected, actual)?; + } + + bind(subst, *expected_var, actual)?; + Ok(()) + } + ( + Type::RecordOpen { + row: expected_row, + is_rigid: true, + source_name: expected_source_name, + .. + }, + Type::RecordOpen { + row: actual_row, + is_rigid: true, + source_name: actual_source_name, + .. + }, + ) if expected_source_name == actual_source_name => { + let expected_row_keys: HashSet<_> = expected_row.keys().collect(); + let actual_row_keys: HashSet<_> = actual_row.keys().collect(); + + if expected_row_keys != actual_row_keys { + return Err(UnificationError::TypesNotEqual); + } + + for (key, expected) in expected_row { + let actual = actual_row.get(key).expect("keys to be equal"); + unify(supply, subst, expected, actual)?; + } + Ok(()) + } + ( + Type::RecordOpen { + var: expected_var, + row: expected_row, + source_name: None, + .. + }, + Type::RecordOpen { + var: actual_var, + row: actual_row, + source_name: None, + .. + }, + ) => { + let expected_row_keys: HashSet<_> = expected_row.keys().collect(); + let actual_row_keys: HashSet<_> = actual_row.keys().collect(); + + for key in expected_row_keys.intersection(&actual_row_keys) { + let expected = expected_row.get(*key).expect("to have a intersection key"); + let actual = actual_row.get(*key).expect("to have a intersection key"); + unify(supply, subst, expected, actual)?; + } + + let mut row = actual_row.clone(); + for key in expected_row_keys.difference(&actual_row_keys).cloned() { + row.insert(key.clone(), expected_row.get(key).unwrap().clone()); + } + let var = supply.fresh(); + let bound_type = Type::RecordOpen { + kind: Kind::Type, + var, + row, + source_name: None, + is_rigid: false, + }; + bind(subst, *expected_var, &bound_type)?; + bind(subst, *actual_var, &bound_type)?; + Ok(()) + } + + _ => Err(UnificationError::TypesNotEqual), + } +} + +fn bind(subst: &mut SubstitutionInner, var: Var, t: &Type) -> Result { + if let Type::Variable { var: var_, .. } = t { + if var == *var_ { + return Ok(()); + } + } + + trace!("binding {var} to {}", t.debug_render()); + occurs_check(var, t)?; + subst.insert(var, t.clone()); + Ok(()) +} + +fn occurs_check(var: Var, t: &Type) -> Result { + if utils::type_variables(t).contains(var) { + return Err(UnificationError::InfiniteType { + var, + infinite_type: t.clone(), + }); + } + Ok(()) +} diff --git a/crates/ditto-type-checker/src/utils/make_wobbly.rs b/crates/ditto-type-checker/src/utils/make_wobbly.rs new file mode 100644 index 000000000..69701bd4b --- /dev/null +++ b/crates/ditto-type-checker/src/utils/make_wobbly.rs @@ -0,0 +1,69 @@ +use ditto_ast::Type; + +pub fn wobbly(t: Type) -> Type { + match t { + Type::Variable { + is_rigid: _, + source_name, + variable_kind, + var, + } => Type::Variable { + is_rigid: false, // important bit + source_name, + variable_kind, + var, + }, + Type::RecordOpen { + is_rigid: _, + source_name, + kind, + var, + row, + } => Type::RecordOpen { + is_rigid: false, // important bit! + source_name, + kind, + var, + row: row + .into_iter() + .map(|(label, t)| (label, wobbly(t))) + .collect(), + }, + Type::RecordClosed { kind, row } => Type::RecordClosed { + kind, + row: row + .into_iter() + .map(|(label, t)| (label, wobbly(t))) + .collect(), + }, + Type::Call { + box function, + box arguments, + } => Type::Call { + function: Box::new(wobbly(function)), + arguments: Box::new(arguments.map(wobbly)), + }, + Type::Function { + parameters, + box return_type, + } => Type::Function { + parameters: parameters.into_iter().map(wobbly).collect(), + return_type: Box::new(wobbly(return_type)), + }, + Type::ConstructorAlias { + constructor_kind, + canonical_value, + source_value, + alias_variables, + box aliased_type, + } => Type::ConstructorAlias { + constructor_kind, + canonical_value, + source_value, + alias_variables, + aliased_type: Box::new(wobbly(aliased_type)), + }, + + Type::PrimConstructor { .. } | Type::Constructor { .. } => t, + } +} diff --git a/crates/ditto-type-checker/src/utils/mk_types.rs b/crates/ditto-type-checker/src/utils/mk_types.rs new file mode 100644 index 000000000..6a8176fab --- /dev/null +++ b/crates/ditto-type-checker/src/utils/mk_types.rs @@ -0,0 +1,23 @@ +use ditto_ast::{PrimType, Type}; + +pub fn mk_bool_type() -> Type { + Type::PrimConstructor(PrimType::Bool) +} + +pub fn mk_array_type(t: Type) -> Type { + Type::Call { + function: Box::new(Type::PrimConstructor(PrimType::Array)), + arguments: Box::new(nonempty::nonempty![t]), + } +} + +pub fn mk_wobbly_function_type(parameters: Vec, return_type: Type) -> Type { + super::wobbly(mk_function_type(parameters, return_type)) +} + +pub fn mk_function_type(parameters: Vec, return_type: Type) -> Type { + Type::Function { + parameters, + return_type: Box::new(return_type), + } +} diff --git a/crates/ditto-type-checker/src/utils/mod.rs b/crates/ditto-type-checker/src/utils/mod.rs new file mode 100644 index 000000000..715648701 --- /dev/null +++ b/crates/ditto-type-checker/src/utils/mod.rs @@ -0,0 +1,9 @@ +mod make_wobbly; +mod mk_types; +mod type_variables; +mod unalias_type; + +pub use make_wobbly::*; +pub use mk_types::*; +pub use type_variables::*; +pub use unalias_type::*; diff --git a/crates/ditto-type-checker/src/utils/type_variables.rs b/crates/ditto-type-checker/src/utils/type_variables.rs new file mode 100644 index 000000000..45570a399 --- /dev/null +++ b/crates/ditto-type-checker/src/utils/type_variables.rs @@ -0,0 +1,53 @@ +use ditto_ast::{Type, Var}; + +// pub type TypeVars = IndexSet; +pub type TypeVars = tinyset::Set64; + +pub fn type_variables(ast_type: &Type) -> TypeVars { + let mut accum = TypeVars::new(); + type_variables_rec(ast_type, &mut accum); + accum +} + +fn type_variables_rec(ast_type: &Type, accum: &mut TypeVars) { + use Type::*; + match ast_type { + Call { + function, + arguments, + } => { + type_variables_rec(function, accum); + arguments.iter().for_each(|arg| { + type_variables_rec(arg, accum); + }); + } + Function { + parameters, + return_type, + } => { + parameters.iter().for_each(|param| { + type_variables_rec(param, accum); + }); + type_variables_rec(return_type, accum); + } + Variable { var, .. } => { + accum.insert(*var); + } + RecordOpen { var, row, .. } => { + accum.insert(*var); + for (_label, t) in row { + type_variables_rec(t, accum); + } + } + RecordClosed { row, .. } => { + for (_label, t) in row { + type_variables_rec(t, accum); + } + } + ConstructorAlias { aliased_type, .. } => { + // REVIEW: is this right? + type_variables_rec(aliased_type, accum) + } + Constructor { .. } | PrimConstructor { .. } => {} + } +} diff --git a/crates/ditto-type-checker/src/utils/unalias_type.rs b/crates/ditto-type-checker/src/utils/unalias_type.rs new file mode 100644 index 000000000..09bcc37a1 --- /dev/null +++ b/crates/ditto-type-checker/src/utils/unalias_type.rs @@ -0,0 +1,17 @@ +use ditto_ast::Type; + +pub fn unalias_type(t: Type) -> Type { + match t { + Type::Call { + function: + box Type::ConstructorAlias { + box aliased_type, .. + }, + .. + } + | Type::ConstructorAlias { + box aliased_type, .. + } => unalias_type(aliased_type), + _ => t, + } +} diff --git a/crates/ditto-type-checker/src/warning.rs b/crates/ditto-type-checker/src/warning.rs new file mode 100644 index 000000000..8badf7d91 --- /dev/null +++ b/crates/ditto-type-checker/src/warning.rs @@ -0,0 +1,53 @@ +use ditto_ast::Span; + +#[derive(Debug, Default, miette::Diagnostic, thiserror::Error)] +#[error("warnings")] +#[diagnostic(severity(Warning))] +pub struct Warnings(#[related] pub Vec); + +impl Warnings { + pub(crate) fn push(&mut self, warning: Warning) { + self.0.push(warning); + } + pub(crate) fn extend(&mut self, warnings: Warnings) { + self.0.extend(warnings.0); + } + pub(crate) fn sort(&mut self) { + self.0 + .sort_by_key(|warning| warning.get_span().start_offset); + } +} + +#[derive(Debug, miette::Diagnostic, thiserror::Error)] +pub enum Warning { + #[error("dodgy label")] + #[diagnostic(severity(Warning))] + RecordLabelNotSnakeCase { + #[label("use snake_case")] + span: Span, + }, + + #[error("dodgy variable")] + #[diagnostic(severity(Warning))] + VariableNotSnakeCase { + #[label("use snake_case")] + span: Span, + }, + + #[error("unused binder")] + #[diagnostic(severity(Warning))] + UnusedBinder { + #[label("this isn't referenced")] + span: Span, + }, +} + +impl Warning { + fn get_span(&self) -> Span { + match self { + Self::RecordLabelNotSnakeCase { span } + | Self::VariableNotSnakeCase { span } + | Self::UnusedBinder { span } => *span, + } + } +} diff --git a/crates/ditto-type-checker/tests/testdata/.gitignore b/crates/ditto-type-checker/tests/testdata/.gitignore new file mode 100644 index 000000000..c92df4ef2 --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/.gitignore @@ -0,0 +1 @@ +focus \ No newline at end of file diff --git a/crates/ditto-type-checker/tests/testdata/arrays b/crates/ditto-type-checker/tests/testdata/arrays new file mode 100644 index 000000000..d104eb344 --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/arrays @@ -0,0 +1,82 @@ +typecheck +[] +---- +Array($5) + + +typecheck +["x"] +---- +Array(String) + + +typecheck +[true, (false)] +---- +Array(Bool) + + +typecheck +[[]] +---- +Array(Array($5)) + + +typecheck +[[], [true]] +---- +Array(Array(Bool)) + + +typecheck +[5, true] +---- +---- + + × types don't unify + ╭──── + 1 │ [5, true] + · ──┬─ + · ╰── here + ╰──── + help: expected Int + got Bool + +---- +---- + + +typecheck +[[5], [true]] +---- +---- + + × types don't unify + ╭──── + 1 │ [[5], [true]] + · ──┬─ + · ╰── here + ╰──── + help: expected Int + got Bool + +---- +---- + + +typecheck +fn (x: Int) -> [[], [true], [x]] +---- +---- + + × types don't unify + ╭──── + 1 │ fn (x: Int) -> [[], [true], [x]] + · ┬ + · ╰── here + ╰──── + help: expected Bool + got Int + +---- +---- diff --git a/crates/ditto-type-checker/tests/testdata/calls b/crates/ditto-type-checker/tests/testdata/calls new file mode 100644 index 000000000..81ea295cb --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/calls @@ -0,0 +1,134 @@ +typecheck +(fn () -> 2)() +---- +Int + +typecheck +(fn (a: a) -> a)(5) +---- +Int + +typecheck +(fn (a): a -> a)(5) +---- +---- + + × types don't unify + ╭──── + 1 │ (fn (a): a -> a)(5) + · ┬ + · ╰── here + ╰──── + help: expected a$5! + got Int + +---- +---- + + +typecheck +(fn (a) -> a)(2.0) +---- +Float + + +typecheck +(fn (_a, b) -> b)(2.0, true) +---- +Bool + + +typecheck +(fn (f) -> [f(1, 2, 3)]) +---- +((Int, Int, Int) -> $6) -> Array($6) + + +typecheck +(fn (x) -> fn (y) -> [x, y, 5.5])(1.1) +---- +(Float) -> Array(Float) + + +typecheck +(fn (f, g, x) -> f(x, g(x))) +---- +(($7, $8) -> $9, ($7) -> $8, $7) -> $9 + + +typecheck +(fn (_f : (a) -> a): Int -> 5)(fn (x: b): b -> x) +---- +Int + + +# Note GHC also won't accept this... +# Try +# f :: (a -> a) -> Int +# f g = g 5 +# +typecheck +(fn (f : (a) -> a): Int -> f(5))(fn (x) -> x) +---- +---- + + × types don't unify + ╭──── + 1 │ (fn (f : (a) -> a): Int -> f(5))(fn (x) -> x) + · ┬ + · ╰── here + ╰──── + help: expected a$5! + got Int + +---- +---- + + +typecheck +(fn () -> 2)(5, 5) +---- +---- + + × wrong number of arguments + ╭──── + 1 │ (fn () -> 2)(5, 5) + · ─────┬──── + · ╰── this expects 0 args + ╰──── + +---- +---- + + +typecheck +(fn (x) -> x)(5, 5) +---- +---- + + × wrong number of arguments + ╭──── + 1 │ (fn (x) -> x)(5, 5) + · ─────┬───── + · ╰── this expects 1 arg + ╰──── + +---- +---- + + +typecheck +[]() +---- +---- + + × expression isn't callable + ╭──── + 1 │ []() + · ─┬ + · ╰── can't call this + ╰──── + help: expression has type Array($5) + +---- +---- diff --git a/crates/ditto-type-checker/tests/testdata/conditionals b/crates/ditto-type-checker/tests/testdata/conditionals new file mode 100644 index 000000000..08ea9c4fc --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/conditionals @@ -0,0 +1,90 @@ +typecheck +if true then "yea" else "nay" +---- +String + + +typecheck +if false then 0 else 1 +---- +Int + + +typecheck +if true then false else true +---- +Bool + + +typecheck +if true then [] else [] +---- +Array($5) + + +typecheck +if true then 1 else "false" +---- +---- + + × types don't unify + ╭──── + 1 │ if true then 1 else "false" + · ───┬─── + · ╰── here + ╰──── + help: expected Int + got String + +---- +---- + + +typecheck +if "true" then "???" else "what" +---- +---- + + × types don't unify + ╭──── + 1 │ if "true" then "???" else "what" + · ───┬── + · ╰── here + ╰──── + help: expected Bool + got String + +---- +---- + + +typecheck +fn () -> if true then if false then [] else [1] else [2] +---- +() -> Array(Int) + + +typecheck +fn (p: Bool): Int -> if p then 5 else 10 +---- +(Bool) -> Int + + +typecheck +fn (p, x) -> if p then fn () -> true else (fn () -> fn () -> false)() +---- +---- +(Bool, $6) -> () -> Bool + + ⚠ warnings + +Warning: + ⚠ unused binder + ╭──── + 1 │ fn (p, x) -> if p then fn () -> true else (fn () -> fn () -> false)() + · ┬ + · ╰── this isn't referenced + ╰──── + +---- +---- diff --git a/crates/ditto-type-checker/tests/testdata/constructors b/crates/ditto-type-checker/tests/testdata/constructors new file mode 100644 index 000000000..3e13ee238 --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/constructors @@ -0,0 +1,46 @@ +typecheck +A +---- +ABC + + +typecheck +Just +---- +($5) -> Maybe($5) + + +typecheck +Nothing +---- +Maybe($5) + + +typecheck +Just(Just(Nothing)) +---- +Maybe(Maybe(Maybe($7))) + + +typecheck +Thruple +---- +($5, $6, $7) -> Thruple($5, $6, $7) + + +typecheck +Thruple(1, 1.0, true) +---- +Thruple(Int, Float, Bool) + + +typecheck +Thruple(Nothing, Nothing, Nothing) +---- +Thruple(Maybe($8), Maybe($9), Maybe($10)) + + +typecheck +Wrapper +---- +($5) -> Wrapper($5) diff --git a/crates/ditto-type-checker/tests/testdata/errors b/crates/ditto-type-checker/tests/testdata/errors new file mode 100644 index 000000000..2218b9c61 --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/errors @@ -0,0 +1,79 @@ +typecheck +fn (f) -> f(f) +---- +---- + + × infinite type + ╭──── + 1 │ fn (f) -> f(f) + · ──┬─ + · ╰── here + ╰──── + help: try adding type annotations? + +---- +---- + +typecheck +wut +---- +---- + + × unknown variable + ╭──── + 1 │ wut + · ─┬─ + · ╰── not in scope + ╰──── + +---- +---- + + +typecheck +Huh +---- +---- + + × unknown constructor + ╭──── + 1 │ Huh + · ─┬─ + · ╰── not in scope + ╰──── + +---- +---- + + +typecheck +fn (x) -> fn (x) -> fn (x) -> fn (x) -> x +---- +---- + + × value shadowed + ╭──── + 1 │ fn (x) -> fn (x) -> fn (x) -> fn (x) -> x + · ┬ ┬ + · │ ╰── shadowed here + · ╰── first bound here + ╰──── + +---- +---- + + +typecheck +fn (Huh) -> 5 +---- +---- + + × unknown constructor + ╭──── + 1 │ fn (Huh) -> 5 + · ─┬─ + · ╰── not in scope + ╰──── + +---- +---- diff --git a/crates/ditto-type-checker/tests/testdata/functions b/crates/ditto-type-checker/tests/testdata/functions new file mode 100644 index 000000000..797d86f1b --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/functions @@ -0,0 +1,272 @@ +typecheck +fn (a: x) -> a +---- +(x$5) -> x$5 + + +typecheck +fn (): a -> 5 +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): a -> 5 + · ┬ + · ╰── here + ╰──── + help: expected a$5! + got Int + +---- +---- + + +typecheck +fn (f): Int -> f(true) +---- +((Bool) -> Int) -> Int + + +typecheck +fn (a, a, a) -> [a, a, a] +---- +---- + + × value shadowed + ╭──── + 1 │ fn (a, a, a) -> [a, a, a] + · ┬ ┬ + · │ ╰── shadowed here + · ╰── first bound here + ╰──── + +---- +---- + + +typecheck +fn (a: some_var, b, c: some_other_var) -> [b, 5] +---- +---- +(some_var$5, Int, some_other_var$6) -> Array(Int) + + ⚠ warnings + +Warning: + ⚠ unused binder + ╭──── + 1 │ fn (a: some_var, b, c: some_other_var) -> [b, 5] + · ┬ + · ╰── this isn't referenced + ╰──── +Warning: + ⚠ unused binder + ╭──── + 1 │ fn (a: some_var, b, c: some_other_var) -> [b, 5] + · ┬ + · ╰── this isn't referenced + ╰──── + +---- +---- + + +typecheck +fn (f) -> [f(true), f(5, true)] +---- +---- + + × wrong number of arguments + ╭──── + 1 │ fn (f) -> [f(true), f(5, true)] + · ┬ + · ╰── this expects 1 arg + ╰──── + +---- +---- + + +typecheck +fn (f) -> [f(true), f(5)] +---- +---- + + × types don't unify + ╭──── + 1 │ fn (f) -> [f(true), f(5)] + · ┬ + · ╰── here + ╰──── + help: expected Bool + got Int + +---- +---- + + +typecheck +fn (f) -> g +---- +---- + + × unknown variable + ╭──── + 1 │ fn (f) -> g + · ┬ + · ╰── not in scope + ╰──── + +---- +---- + + +typecheck +fn (Wrapper(a)) -> a +---- +(Wrapper($5)) -> $5 + + +typecheck +fn (A) -> B +---- +---- + + × refutable binder + ╭──── + 1 │ fn (A) -> B + · ┬ + · ╰── not exhaustive + ╰──── + help: missing patterns + | B + | C + +---- +---- + + +typecheck +fn (Just(Just(A))) -> B +---- +---- + + × refutable binder + ╭──── + 1 │ fn (Just(Just(A))) -> B + · ──────┬────── + · ╰── not exhaustive + ╰──── + help: missing patterns + | Just(Just(B)) + | Just(Just(C)) + | Just(Nothing) + | Nothing + +---- +---- + + +typecheck +fn (Just(A, B, C)) -> B +---- +---- + + × wrong number of arguments + ╭──── + 1 │ fn (Just(A, B, C)) -> B + · ──┬─ + · ╰── this expects 1 arg + ╰──── + +---- +---- + + +typecheck +fn (Nothing(A, B, C)) -> B +---- +---- + + × wrong number of arguments + ╭──── + 1 │ fn (Nothing(A, B, C)) -> B + · ───┬─── + · ╰── this expects 0 args + ╰──── + +---- +---- + + +typecheck +fn (Just(A): ABC) -> B +---- +---- + + × types don't unify + ╭──── + 1 │ fn (Just(A): ABC) -> B + · ───┬─── + · ╰── here + ╰──── + help: expected ABC + got Maybe(ABC) + +---- +---- + + +typecheck +fn (): ((Int, Bool) -> Bool) -> fn (x, y: Int) -> 5 +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): ((Int, Bool) -> Bool) -> fn (x, y: Int) -> 5 + · ┬ + · ╰── here + ╰──── + help: expected Bool + got Int + +---- +---- + + +typecheck +fn (): ((Int, Bool) -> Bool) -> fn (x, y) -> 5 +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): ((Int, Bool) -> Bool) -> fn (x, y) -> 5 + · ┬ + · ╰── here + ╰──── + help: expected Bool + got Int + +---- +---- + + +typecheck +fn (): ((Int, Bool) -> Bool) -> fn (x, y): Int -> 5 +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): ((Int, Bool) -> Bool) -> fn (x, y): Int -> 5 + · ─────────┬───────── + · ╰── here + ╰──── + help: expected (Int, Bool) -> Bool + got (Int, Bool) -> Int + +---- +---- diff --git a/crates/ditto-type-checker/tests/testdata/literals b/crates/ditto-type-checker/tests/testdata/literals new file mode 100644 index 000000000..fc946fce4 --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/literals @@ -0,0 +1,40 @@ +typecheck +true +---- +Bool + + +typecheck +false +---- +Bool + + +typecheck +unit +---- +Unit + + +typecheck +"five" +---- +String + + +typecheck +5 +---- +Int + + +typecheck +5.5 +---- +Float + + +typecheck +(((7.0))) +---- +Float diff --git a/crates/ditto-type-checker/tests/testdata/records b/crates/ditto-type-checker/tests/testdata/records new file mode 100644 index 000000000..26a1ece3a --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/records @@ -0,0 +1,357 @@ +typecheck +{} +---- +{} + + +typecheck +{ foo = true } +---- +{ foo: Bool } + + +typecheck +{ foo = true, foo = true } +---- +---- + + × duplicate record field + ╭──── + 1 │ { foo = true, foo = true } + · ─┬─ + · ╰── here + ╰──── + +---- +---- + + +typecheck +{ foo = {}, bar = [] } +---- +{ foo: {}, bar: Array($5) } + + +typecheck +fn (x) -> x.foo +---- +({ $7 | foo: $6 }) -> $6 + + +typecheck +fn (x: { r | foo: Int }) -> x.foo +---- +({ r$5 | foo: Int }) -> Int + + +typecheck +fn (x) -> x.foo.bar +---- +({ $8 | foo: { $7 | bar: $6 } }) -> $6 + + +typecheck +fn (x) -> [x.foo, x.bar, x.baz] +---- +({ $11 | foo: $6, bar: $6, baz: $6 }) -> Array($6) + + +typecheck +fn (x) -> [x.foo, x.bar, x.baz, 10] +---- +({ $11 | foo: Int, bar: Int, baz: Int }) -> Array(Int) + + +typecheck +fn (x : { r | foo: Int, bar: Int, baz: Int }) -> [x.foo, x.bar, x.baz] +---- +({ r$5 | foo: Int, bar: Int, baz: Int }) -> Array(Int) + + +typecheck +fn (x : { foo: Int, bar: Int, baz: Int }) -> [x.foo, x.bar, x.baz] +---- +({ foo: Int, bar: Int, baz: Int }) -> Array(Int) + + +typecheck +(fn (r) -> r.foo)({ foo = 5 }) +---- +Int + + +typecheck +(fn (r : { r | foo: Bool }) -> r.foo)({ foo = true }) +---- +Bool + + +typecheck +fn (): { r | foo: Bool } -> { foo = true } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): { r | foo: Bool } -> { foo = true } + · ───────┬────── + · ╰── here + ╰──── + help: expected { r$5! | foo: Bool } + got { foo: Bool } + +---- +---- + + +typecheck +(fn (r : { r | foo: Int }) -> r.foo)({ foo = true }) +---- +---- + + × types don't unify + ╭──── + 1 │ (fn (r : { r | foo: Int }) -> r.foo)({ foo = true }) + · ──┬─ + · ╰── here + ╰──── + help: expected Int + got Bool + +---- +---- + + +typecheck +fn (): { foo: Bool } -> {} +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): { foo: Bool } -> {} + · ─┬ + · ╰── this record is missing fields + ╰──── + help: need to add + foo: Bool + +---- +---- + + +typecheck +fn (): {} -> { foo = true } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): {} -> { foo = true } + · ─┬─ + · ╰── here + ╰──── + help: `foo` not in {} + +---- +---- + + +typecheck +fn (): { foo: (Int) -> { bar: Int }} -> { foo = fn (i) -> { bar = true } } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (): { foo: (Int) -> { bar: Int }} -> { foo = fn (i) -> { bar = true } } + · ──┬─ + · ╰── here + ╰──── + help: expected Int + got Bool + +---- +---- + + +typecheck +(fn (r : { r | foo: Bool }) -> r.foo)({ foo = true, bar = unit }) +---- +Bool + + +typecheck +(fn (r) -> r.foo)({}) +---- +---- + + × types don't unify + ╭──── + 1 │ (fn (r) -> r.foo)({}) + · ─┬ + · ╰── this record is missing fields + ╰──── + help: need to add + foo: $6 + +---- +---- + + +typecheck +fn (r) -> { r | foo = 2 } +---- +({ $6 | foo: Int }) -> { $6 | foo: Int } + + +typecheck +fn (r: { foo : Int }) -> { r | foo = 2 } +---- +({ foo: Int }) -> { foo: Int } + + +typecheck +(fn (r) -> { r | foo = 2 })({ foo = 1 }) +---- +{ foo: Int } + + +typecheck +(fn (r) -> { r | foo = 2 })({ foo = 1, bar = 5 }) +---- +{ foo: Int, bar: Int } + + +typecheck +fn (r) -> { r | foo = 2 } +---- +({ $6 | foo: Int }) -> { $6 | foo: Int } + + +typecheck +(fn (r) -> { r | foo = 2 })({ foo = 1, bar = 5 }) +---- +{ foo: Int, bar: Int } + + +typecheck +fn (a, b) -> { a | foo = { b | bar = 5 } } +---- +({ $8 | foo: { $7 | bar: Int } }, { $7 | bar: Int }) -> { $8 | foo: { $7 | bar: Int } } + + +typecheck +fn (x: { r | foo: Int }) -> { x | foo = true } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (x: { r | foo: Int }) -> { x | foo = true } + · ──┬─ + · ╰── here + ╰──── + help: expected Int + got Bool + +---- +---- + + +typecheck +(fn (r) -> { r | foo = 2 })({ foo = 1, bar = 5, bar = unit }) +---- +---- + + × duplicate record field + ╭──── + 1 │ (fn (r) -> { r | foo = 2 })({ foo = 1, bar = 5, bar = unit }) + · ─┬─ + · ╰── here + ╰──── + +---- +---- + + +typecheck +fn (x: { foo: Int }) -> { x | foo = 5, bar = unit } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (x: { foo: Int }) -> { x | foo = 5, bar = unit } + · ─┬─ + · ╰── here + ╰──── + help: `bar` not in { foo: Int } + +---- +---- + + +typecheck +fn (x: { foo: Int }) -> { x.foo | foo = 5, bar = unit } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (x: { foo: Int }) -> { x.foo | foo = 5, bar = unit } + · ──┬── + · ╰── here + ╰──── + help: expected { $7 | foo: Int, bar: Unit } + got Int + +---- +---- + + +typecheck +fn (x: { foo: Int }): Bool -> x.foo +---- +---- + + × types don't unify + ╭──── + 1 │ fn (x: { foo: Int }): Bool -> x.foo + · ┬ + · ╰── here + ╰──── + help: expected { $5 | foo: Bool } + got { foo: Int } + +---- +---- + + +typecheck +fn (x: { foo: Int }): { foo: Int } -> { x | foo = 5 } +---- +({ foo: Int }) -> { foo: Int } + + +typecheck +fn (x: { r | foo: Int }): { r | foo: Int } -> { x | foo = 5 } +---- +({ r$5 | foo: Int }) -> { r$5 | foo: Int } + + +typecheck +fn (r: { x | foo: Unit }) -> { r | foo = unit, bar = 5 } +---- +---- + + × types don't unify + ╭──── + 1 │ fn (r: { x | foo: Unit }) -> { r | foo = unit, bar = 5 } + · ─┬─ + · ╰── here + ╰──── + help: `bar` not in { x$5! | foo: Unit } + +---- +---- diff --git a/crates/ditto-type-checker/tests/testdata/warnings b/crates/ditto-type-checker/tests/testdata/warnings new file mode 100644 index 000000000..4bbbcebb0 --- /dev/null +++ b/crates/ditto-type-checker/tests/testdata/warnings @@ -0,0 +1,65 @@ +typecheck +fn (notSnake) -> notSnake +---- +---- +($5) -> $5 + + ⚠ warnings + +Warning: + ⚠ dodgy variable + ╭──── + 1 │ fn (notSnake) -> notSnake + · ────┬─── + · ╰── use snake_case + ╰──── + +---- +---- + + + +typecheck +fn (not_used) -> 5 +---- +---- +($5) -> Int + + ⚠ warnings + +Warning: + ⚠ unused binder + ╭──── + 1 │ fn (not_used) -> 5 + · ────┬─── + · ╰── this isn't referenced + ╰──── + +---- +---- + +typecheck +fn (plsNo) -> 5 +---- +---- +($5) -> Int + + ⚠ warnings + +Warning: + ⚠ dodgy variable + ╭──── + 1 │ fn (plsNo) -> 5 + · ──┬── + · ╰── use snake_case + ╰──── +Warning: + ⚠ unused binder + ╭──── + 1 │ fn (plsNo) -> 5 + · ──┬── + · ╰── this isn't referenced + ╰──── + +---- +----