diff --git a/Cargo.lock b/Cargo.lock index 89b962336..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" @@ -28,6 +38,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.19" @@ -515,12 +536,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if 1.0.0", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core", ] +[[package]] +name = "datadriven" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c496e3277b660041bd6a2c0618593e99c3ba450b30d5f8d89035f78c87b4106" +dependencies = [ + "anyhow", + "futures", +] + [[package]] name = "datatest-stable" version = "0.1.3" @@ -610,9 +641,10 @@ version = "0.0.1" dependencies = [ "ditto-cst", "indexmap", - "non-empty-vec", + "nonempty", "petgraph", "serde", + "smol_str", ] [[package]] @@ -729,6 +761,7 @@ dependencies = [ "regex", "serde", "similar-asserts", + "smol_str", "thiserror", ] @@ -803,6 +836,19 @@ dependencies = [ "walkdir", ] +[[package]] +name = "ditto-pattern-checker" +version = "0.0.1" +dependencies = [ + "datadriven", + "ditto-ast", + "ditto-cst", + "halfbrown", + "nonempty", + "smol_str", + "thiserror", +] + [[package]] name = "ditto-tree-sitter" version = "0.0.1" @@ -812,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" @@ -846,7 +913,7 @@ checksum = "3c0ed784986bc7ca53042d4f1ec1fb7e31fd7f914c415a7c69a0bc06a8907a52" dependencies = [ "env_logger 0.9.1", "fxhash", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "instant", "log", @@ -1018,6 +1085,7 @@ checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1040,6 +1108,17 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.26" @@ -1189,13 +1268,31 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "halfbrown" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2a3c70a9c00cc1ee87b54e89f9505f73bb17d63f1b25c9a462ba8ef885444f" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", ] [[package]] @@ -1204,7 +1301,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -1356,7 +1453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] @@ -1703,6 +1800,12 @@ name = "non-empty-vec" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceeba69aa8d4c53cdceeac8f17eb2656bb88b468bbe6c0889d34edfdea26ec8b" + +[[package]] +name = "nonempty" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeaf4ad7403de93e699c191202f017118df734d3850b01e13a3a8b2e6953d3c9" dependencies = [ "serde", ] @@ -2111,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", @@ -2123,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", ] @@ -2136,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" @@ -2565,6 +2680,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + [[package]] name = "snapbox" version = "0.4.0" @@ -2666,8 +2790,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32bf088d1d7df2b2b6711b06da3471bc86677383c57b27251e18c56df8deac14" dependencies = [ - "ahash", - "hashbrown", + "ahash 0.7.6", + "hashbrown 0.12.3", ] [[package]] @@ -2841,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" @@ -3156,7 +3289,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", "regex", ] diff --git a/Cargo.toml b/Cargo.toml index 2f6f9c891..834c95f72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,6 @@ members = [ "crates/ditto-lsp", "crates/ditto-tree-sitter", "crates/ditto-highlight", + "crates/ditto-pattern-checker", + "crates/ditto-type-checker", ] diff --git a/crates/ditto-ast/Cargo.toml b/crates/ditto-ast/Cargo.toml index 3e1f82e0c..061af223d 100644 --- a/crates/ditto-ast/Cargo.toml +++ b/crates/ditto-ast/Cargo.toml @@ -11,6 +11,7 @@ doctest = false ditto-cst = { path = "../ditto-cst" } serde = { version = "1.0", features = ["derive"] } petgraph = "0.6" -non-empty-vec = { version = "0.2", features = ["serde"] } +nonempty = { version = "0.8", features = ["serialize"] } indexmap = { version = "1.9", features = ["serde"] } +smol_str = "0.1" #unindent = "xx" <-- might come in useful for smart multi-line strings (like Nix) diff --git a/crates/ditto-ast/src/expression.rs b/crates/ditto-ast/src/expression.rs index 7873e5aff..51502a035 100644 --- a/crates/ditto-ast/src/expression.rs +++ b/crates/ditto-ast/src/expression.rs @@ -1,9 +1,8 @@ -use crate::{ - FullyQualifiedName, FullyQualifiedProperName, Name, ProperName, Span, Type, UnusedName, -}; +use crate::{FullyQualifiedName, FullyQualifiedProperName, Name, Pattern, ProperName, Span, Type}; use indexmap::IndexMap; -use non_empty_vec::NonEmpty; +use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; /// The real business value. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -43,7 +42,7 @@ pub enum Expression { function: Box, /// Arguments to pass to the function expression. - arguments: Vec, + arguments: Vec, }, /// A conditional expression. /// @@ -81,7 +80,7 @@ pub enum Expression { expression: Box, /// Patterns to be matched against and their corresponding expressions. - arms: NonEmpty<(Pattern, Self)>, + arms: Box>, }, /// A value constructor local to the current module, e.g. `Just` and `Ok`. LocalConstructor { @@ -107,6 +106,9 @@ pub enum Expression { }, /// A value local to the current module, e.g. `foo`. LocalVariable { + /// Where this variable was introduced. + introduction: Span, + /// The source span for this expression. span: Span, @@ -118,6 +120,9 @@ pub enum Expression { }, /// A foreign value. ForeignVariable { + /// Where this variable was introduced. + introduction: Span, + /// The source span for this expression. span: Span, /// The type of this variable. @@ -127,6 +132,9 @@ pub enum Expression { }, /// A value that has been imported ImportedVariable { + /// Where this variable was introduced. + introduction: Span, + /// The source span for this expression. span: Span, @@ -166,7 +174,7 @@ pub enum Expression { /// The source span for this expression. span: Span, /// `"string"` - value: String, + value: SmolStr, /// The type of this string literal. /// /// Generally this will be `PrimType::String`, but it might have been aliased. @@ -184,7 +192,7 @@ pub enum Expression { /// For example, if the integer appears in ditto source as "005" we want to preserve that in the /// generated code. /// 2. Storing as a string avoids overflow issues. - value: String, + value: SmolStr, /// The type of this integer literal. /// Generally this will be `PrimType::Int`, but it might have been aliased. value_type: Type, @@ -201,7 +209,7 @@ pub enum Expression { /// For example, if the float appears in ditto source as "5.00" we want to preserve that in the /// generated code. /// 2. Storing as a string avoids float overflow and precision issues. - value: String, + value: SmolStr, /// The type of this float literal. /// Generally this will be `PrimType::Float`, but it might have been aliased. value_type: Type, @@ -226,7 +234,7 @@ pub enum Expression { /// The expression being updated. target: Box, /// The record updates. - fields: IndexMap, + fields: RecordFields, }, /// An array literal. Array { @@ -251,7 +259,7 @@ pub enum Expression { record_type: Type, /// Record fields. - fields: IndexMap, + fields: RecordFields, }, /// `true` True { @@ -279,245 +287,42 @@ pub enum Expression { }, } +/// The type of record fields, for convenience. +pub type RecordFields = IndexMap; + impl Expression { /// Return the [Type] of this [Expression]. - pub fn get_type(&self) -> Type { + pub fn get_type(&self) -> &Type { // It'd be nice if we could call this `typeof` but that's a keyword in rust, sad face match self { - Self::Call { call_type, .. } => call_type.clone(), - Self::Function { function_type, .. } => function_type.clone(), - Self::If { output_type, .. } => output_type.clone(), - Self::Match { match_type, .. } => match_type.clone(), - Self::Effect { effect_type, .. } => effect_type.clone(), + Self::Call { call_type, .. } => call_type, + Self::Function { function_type, .. } => function_type, + Self::If { output_type, .. } => output_type, + Self::Match { match_type, .. } => match_type, + Self::Effect { effect_type, .. } => effect_type, Self::LocalConstructor { constructor_type, .. - } => constructor_type.clone(), + } => constructor_type, Self::ImportedConstructor { constructor_type, .. - } => constructor_type.clone(), - Self::LocalVariable { variable_type, .. } => variable_type.clone(), - Self::ForeignVariable { variable_type, .. } => variable_type.clone(), - Self::ImportedVariable { variable_type, .. } => variable_type.clone(), - Self::RecordAccess { field_type, .. } => field_type.clone(), - Self::RecordUpdate { record_type, .. } => record_type.clone(), - Self::Array { value_type, .. } => value_type.clone(), - Self::Record { record_type, .. } => record_type.clone(), + } => constructor_type, + Self::LocalVariable { variable_type, .. } => variable_type, + Self::ForeignVariable { variable_type, .. } => variable_type, + Self::ImportedVariable { variable_type, .. } => variable_type, + Self::RecordAccess { field_type, .. } => field_type, + Self::RecordUpdate { record_type, .. } => record_type, + Self::Array { value_type, .. } => value_type, + Self::Record { record_type, .. } => record_type, Self::Let { expression, .. } => expression.get_type(), - Self::String { value_type, .. } => value_type.clone(), - Self::Int { value_type, .. } => value_type.clone(), - Self::Float { value_type, .. } => value_type.clone(), - Self::True { value_type, .. } => value_type.clone(), - Self::False { value_type, .. } => value_type.clone(), - Self::Unit { value_type, .. } => value_type.clone(), - } - } - /// Set the type of this expression. - /// Useful when checking against source type annotations that use aliases. - pub fn set_type(self, t: Type) -> Self { - match self { - Self::Call { - span, - call_type: _, - function, - arguments, - } => Self::Call { - span, - call_type: t, - function, - arguments, - }, - Self::Function { - span, - function_type: _, - binders, - body, - } => Self::Function { - span, - function_type: t, - binders, - body, - }, - Self::If { - span, - output_type: _, - condition, - true_clause, - false_clause, - } => Self::If { - span, - output_type: t, - condition, - true_clause, - false_clause, - }, - Self::Match { - span, - match_type: _, - expression, - arms, - } => Self::Match { - span, - match_type: t, - expression, - arms, - }, - Self::Effect { - span, - effect_type: _, - return_type, - effect, - } => Self::Effect { - span, - effect_type: t, - return_type, - effect, - }, - Self::Record { - span, - record_type: _, - fields, - } => Self::Record { - span, - record_type: t, - fields, - }, - Self::LocalConstructor { - span, - constructor_type: _, - constructor, - } => Self::LocalConstructor { - span, - constructor_type: t, - constructor, - }, - Self::ImportedConstructor { - span, - constructor_type: _, - constructor, - } => Self::ImportedConstructor { - span, - constructor_type: t, - constructor, - }, - Self::LocalVariable { - span, - variable_type: _, - variable, - } => Self::LocalVariable { - span, - variable_type: t, - variable, - }, - Self::ForeignVariable { - span, - variable_type: _, - variable, - } => Self::ForeignVariable { - span, - variable_type: t, - variable, - }, - Self::ImportedVariable { - span, - variable_type: _, - variable, - } => Self::ImportedVariable { - span, - variable_type: t, - variable, - }, - Self::Let { - span, - declaration, - box expression, - } => Self::Let { - span, - declaration, - expression: Box::new(expression.set_type(t)), - }, - Self::RecordAccess { - span, - field_type: _, - target, - label, - } => Self::RecordAccess { - span, - field_type: t, - target, - label, - }, - Self::RecordUpdate { - span, - record_type: _, - target, - fields, - } => Self::RecordUpdate { - span, - record_type: t, - target, - fields, - }, - Self::Array { - span, - element_type, - elements, - value_type: _, - } => Self::Array { - span, - element_type, - elements, - value_type: t, - }, - Self::String { - span, - value, - value_type: _, - } => Self::String { - span, - value, - value_type: t, - }, - Self::Int { - span, - value, - value_type: _, - } => Self::Int { - span, - value, - value_type: t, - }, - Self::Float { - span, - value, - value_type: _, - } => Self::Float { - span, - value, - value_type: t, - }, - Self::True { - span, - value_type: _, - } => Self::True { - span, - value_type: t, - }, - Self::False { - span, - value_type: _, - } => Self::False { - span, - value_type: t, - }, - Self::Unit { - span, - value_type: _, - } => Self::Unit { - span, - value_type: t, - }, + Self::String { value_type, .. } => value_type, + Self::Int { value_type, .. } => value_type, + Self::Float { value_type, .. } => value_type, + Self::True { value_type, .. } => value_type, + Self::False { value_type, .. } => value_type, + Self::Unit { value_type, .. } => value_type, } } + /// Get the source span. pub fn get_span(&self) -> Span { match self { @@ -546,70 +351,6 @@ impl Expression { } } -/// An "argument" is passed to a function call. -/// -/// ```ditto -/// some_function(argument) -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum Argument { - /// A standard expression argument. - /// Could be a variable, could be another function call. - Expression(Expression), -} - -impl Argument { - /// Return the [Type] of this [Argument]. - pub fn get_type(&self) -> Type { - match self { - Self::Expression(expression) => expression.get_type(), - } - } - /// Return the source [Span] for this [Argument]. - pub fn get_span(&self) -> Span { - match self { - Self::Expression(expression) => expression.get_span(), - } - } -} - -/// A pattern to be matched. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum Pattern { - /// A local constructor pattern. - LocalConstructor { - /// The source span for this pattern. - span: Span, - /// `Just` - constructor: ProperName, - /// Pattern arguments to the constructor. - arguments: Vec, - }, - /// An imported constructor pattern. - ImportedConstructor { - /// The source span for this pattern. - span: Span, - /// `Maybe.Just` - constructor: FullyQualifiedProperName, - /// Pattern arguments to the constructor. - arguments: Vec, - }, - /// A variable binding pattern. - Variable { - /// The source span for this pattern. - span: Span, - /// Name to bind. - name: Name, - }, - /// An unused pattern. - Unused { - /// The source span for this pattern. - span: Span, - /// The unused name. - unused_name: UnusedName, - }, -} - /// A chain of Effect statements. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Effect { diff --git a/crates/ditto-ast/src/kind.rs b/crates/ditto-ast/src/kind.rs index ed1f9f0de..090fc588f 100644 --- a/crates/ditto-ast/src/kind.rs +++ b/crates/ditto-ast/src/kind.rs @@ -1,5 +1,5 @@ use crate::Var; -use non_empty_vec::NonEmpty; +use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; /// The kind of types. @@ -21,43 +21,8 @@ pub enum Kind { /// Also note that the "return kind" can only be `Kind::Type` at the moment. Function { /// The kinds of the arguments this type expects. - parameters: NonEmpty, + parameters: Box>, }, /// A series of labelled types. Used for records. Row, } - -impl Kind { - /// Render the kind as a compact, single-line string. - /// Useful for testing and debugging, but not much else... - pub fn debug_render(&self) -> String { - self.debug_render_with(|var| format!("${}", var)) - } - - /// Render the kind as a compact, single-line string. - /// Useful for testing and debugging, but not much else... - /// - /// The caller must decide how to render kind variables via `render_var`. - pub fn debug_render_with(&self, render_var: F) -> String - where - F: Fn(Var) -> String + Copy, - { - match self { - Self::Variable(var) => render_var(*var), - Self::Type => String::from("Type"), - Self::Function { parameters } => { - let mut output = String::from("("); - let len = parameters.len(); - parameters.iter().enumerate().for_each(|(i, param)| { - output.push_str(¶m.debug_render()); - if i + 1 != len.into() { - output.push_str(", "); - } - }); - output.push_str(") -> Type"); - output - } - Self::Row => String::from("Row"), - } - } -} diff --git a/crates/ditto-ast/src/lib.rs b/crates/ditto-ast/src/lib.rs index cc7b6498b..ab7713b39 100644 --- a/crates/ditto-ast/src/lib.rs +++ b/crates/ditto-ast/src/lib.rs @@ -7,7 +7,9 @@ pub mod graph; mod kind; mod module; mod name; +mod pattern; mod r#type; +pub mod utils; mod var; pub use ditto_cst::Span; @@ -15,5 +17,6 @@ pub use expression::*; pub use kind::*; pub use module::*; pub use name::*; +pub use pattern::*; pub use r#type::*; pub use var::Var; diff --git a/crates/ditto-ast/src/name.rs b/crates/ditto-ast/src/name.rs index 8a92b5403..2165a2f99 100644 --- a/crates/ditto-ast/src/name.rs +++ b/crates/ditto-ast/src/name.rs @@ -1,11 +1,12 @@ use ditto_cst as cst; -use non_empty_vec::NonEmpty; +use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; use std::fmt; /// A "name" begins with a lower case letter. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct Name(pub String); +pub struct Name(pub SmolStr); impl fmt::Display for Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -21,7 +22,7 @@ impl From for Name { /// An "unused name" begins with a single underscore. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct UnusedName(pub String); +pub struct UnusedName(pub SmolStr); impl fmt::Display for UnusedName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -37,7 +38,7 @@ impl From for UnusedName { /// A "proper name" begins with an upper case letter. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct ProperName(pub String); +pub struct ProperName(pub SmolStr); impl fmt::Display for ProperName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -53,7 +54,7 @@ impl From for ProperName { /// A package name consists of lower case letters, numbers and hyphens. It must start with a letter. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct PackageName(pub String); +pub struct PackageName(pub SmolStr); impl fmt::Display for PackageName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -92,7 +93,7 @@ impl From for ModuleName { .map(|(proper_name, _dot)| proper_name.into()) .collect::>(); proper_names.push(module_name.last.into()); - unsafe { Self(NonEmpty::new_unchecked(proper_names)) } + Self(NonEmpty::from_vec(proper_names).unwrap()) } } @@ -104,7 +105,7 @@ impl std::cmp::PartialEq for ModuleName { impl std::hash::Hash for ModuleName { fn hash(&self, state: &mut H) { - self.0.as_slice().hash(state); + self.0.hash(state); } } @@ -113,7 +114,7 @@ impl fmt::Display for ModuleName { let len = self.0.len(); for (i, proper_name) in self.0.iter().enumerate() { proper_name.fmt(f)?; - if i + 1 != len.into() { + if i + 1 != len { write!(f, ".")?; } } @@ -129,7 +130,7 @@ impl From for ModuleName { .map(|(proper_name, _dot)| proper_name.into()) .collect::>(); proper_names.push(qualified.value.into()); - unsafe { ModuleName(NonEmpty::new_unchecked(proper_names)) } + Self(NonEmpty::from_vec(proper_names).unwrap()) } } @@ -213,43 +214,3 @@ pub type FullyQualifiedName = FullyQualified; /// A [FullyQualified] [ProperName], i.e. a canonical constructor or type name. pub type FullyQualifiedProperName = FullyQualified; - -/// Macro for constructing [Name]s. -/// -/// This isn't checked for syntax correctness, so use with care. -#[macro_export] -macro_rules! name { - ($string_like:expr) => { - $crate::Name(String::from($string_like)) - }; -} - -/// Macro for constructing [ProperName]s. -/// -/// This isn't checked for syntax correctness, so use with care. -#[macro_export] -macro_rules! proper_name { - ($string_like:expr) => { - $crate::ProperName(String::from($string_like)) - }; -} - -/// Macro for constructing [PackageName]s. -/// -/// This isn't checked for syntax correctness, so use with care. -#[macro_export] -macro_rules! package_name { - ($string_like:expr) => { - $crate::PackageName(String::from($string_like)) - }; -} - -/// Macro for constructing [ModuleName]s. -/// -/// This isn't checked for syntax correctness, so use with care. -#[macro_export] -macro_rules! module_name { - ($($proper_name:expr),+) => {{ - $crate::ModuleName(non_empty_vec::ne_vec![$($crate::proper_name!($proper_name)),+]) - }}; -} diff --git a/crates/ditto-ast/src/pattern.rs b/crates/ditto-ast/src/pattern.rs new file mode 100644 index 000000000..1c0add007 --- /dev/null +++ b/crates/ditto-ast/src/pattern.rs @@ -0,0 +1,51 @@ +use crate::{FullyQualifiedProperName, Name, ProperName, Span, UnusedName}; +use serde::{Deserialize, Serialize}; + +/// A pattern to be matched. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum Pattern { + /// A local constructor pattern. + LocalConstructor { + /// The source span for this pattern. + span: Span, + /// `Just` + constructor: ProperName, + /// Pattern arguments to the constructor. + arguments: Vec, + }, + /// An imported constructor pattern. + ImportedConstructor { + /// The source span for this pattern. + span: Span, + /// `Maybe.Just` + constructor: FullyQualifiedProperName, + /// Pattern arguments to the constructor. + arguments: Vec, + }, + /// A variable binding pattern. + Variable { + /// The source span for this pattern. + span: Span, + /// Name to bind. + name: Name, + }, + /// An unused pattern. + Unused { + /// The source span for this pattern. + span: Span, + /// The unused name. + unused_name: UnusedName, + }, +} + +impl Pattern { + /// Get the source span. + pub fn get_span(&self) -> Span { + match self { + Self::LocalConstructor { span, .. } => *span, + Self::ImportedConstructor { span, .. } => *span, + Self::Variable { span, .. } => *span, + Self::Unused { span, .. } => *span, + } + } +} diff --git a/crates/ditto-ast/src/type.rs b/crates/ditto-ast/src/type.rs index 756aaed33..5f15b68e5 100644 --- a/crates/ditto-ast/src/type.rs +++ b/crates/ditto-ast/src/type.rs @@ -1,6 +1,6 @@ use crate::{FullyQualifiedProperName, Kind, Name, ProperName, QualifiedProperName, Var}; use indexmap::IndexMap; -use non_empty_vec::NonEmpty; +use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; use std::fmt; @@ -21,7 +21,7 @@ pub enum Type { /// Type being called. function: Box, /// The non-empty arguments list. - arguments: NonEmpty, + arguments: Box>, }, /// The type of functions. /// @@ -69,6 +69,9 @@ pub enum Type { var: Var, /// Optional name for this type if one was present in the source. source_name: Option, + /// Is this type user specified? Name is borrowed from Haskell/GHC. + /// + is_rigid: bool, }, /// A _closed_ record type. /// @@ -93,6 +96,9 @@ pub enum Type { var: Var, // NOTE this should be `Kind::Row`. /// Optional name for the type `var`. source_name: Option, + /// Is the variable user specified? + /// + is_rigid: bool, /// The labelled types. row: Row, }, @@ -137,16 +143,17 @@ impl fmt::Display for PrimType { impl PrimType { /// Return this type as a [ProperName] pub fn as_proper_name(&self) -> ProperName { - ProperName(self.to_string()) + ProperName(self.to_string().into()) } + /// Return the kind of the given primitive. pub fn get_kind(&self) -> Kind { match self { Self::Effect => Kind::Function { - parameters: NonEmpty::new(Kind::Type), + parameters: Box::new(NonEmpty::new(Kind::Type)), }, Self::Array => Kind::Function { - parameters: NonEmpty::new(Kind::Type), + parameters: Box::new(NonEmpty::new(Kind::Type)), }, Self::Int => Kind::Type, Self::Float => Kind::Type, @@ -159,8 +166,8 @@ impl PrimType { impl Type { /// Return the kind of this `Type`. - /// REVIEW this is wrong I think! pub fn get_kind(&self) -> Kind { + // REVIEW this is wrong I think!? match self { Self::Variable { variable_kind, .. } => variable_kind.clone(), Self::Constructor { @@ -175,329 +182,4 @@ impl Type { Self::Function { .. } => Kind::Type, } } - - /// Remove any aliasing, returning the canonical [Type]. - pub fn unalias(&self) -> &Self { - match self { - Self::Call { - function: box Self::ConstructorAlias { aliased_type, .. }, - .. - } - | Self::ConstructorAlias { aliased_type, .. } => aliased_type.unalias(), - _ => self, - } - } - - /// Removes any type variable names. - pub fn anonymize(&self) -> Self { - match self { - Self::Variable { - variable_kind, - var, - source_name: _, - } => Self::Variable { - variable_kind: variable_kind.clone(), - var: *var, - source_name: None, - }, - Self::RecordOpen { - kind, - var, - source_name: _, - row, - } => Self::RecordOpen { - kind: kind.clone(), - var: *var, - source_name: None, - row: row - .iter() - .map(|(label, t)| (label.clone(), t.anonymize())) - .collect(), - }, - Self::RecordClosed { kind, row } => Self::RecordClosed { - kind: kind.clone(), - row: row - .iter() - .map(|(label, t)| (label.clone(), t.anonymize())) - .collect(), - }, - Self::Call { - function, - arguments, - } => Self::Call { - function: Box::new(function.anonymize()), - arguments: unsafe { - NonEmpty::new_unchecked(arguments.iter().map(|arg| arg.anonymize()).collect()) - }, - }, - Self::Function { - parameters, - return_type, - } => Self::Function { - parameters: parameters.iter().map(|param| param.anonymize()).collect(), - return_type: Box::new(return_type.anonymize()), - }, - Self::ConstructorAlias { - constructor_kind, - canonical_value, - source_value, - alias_variables, - aliased_type, - } => Self::ConstructorAlias { - constructor_kind: constructor_kind.clone(), - canonical_value: canonical_value.clone(), - source_value: source_value.clone(), - alias_variables: alias_variables.to_vec(), - aliased_type: Box::new(aliased_type.anonymize()), - }, - - Self::PrimConstructor { .. } | Self::Constructor { .. } => self.clone(), - } - } - - /// Render the type as a compact, single-line string. - /// Useful for testing and debugging, but not much else... - pub fn debug_render(&self) -> String { - self.debug_render_with(|var, source_name| { - if let Some(name) = source_name { - name.0 - } else { - format!("${var}", var = var) - } - }) - } - - /// Render the type as a compact, single-line string, with type variables formatted as `name$var`. - /// Useful for testing and debugging, but not much else... - pub fn debug_render_verbose(&self) -> String { - self.debug_render_with(|var, source_name| { - if let Some(name) = source_name { - format!("{}${}", name, var) - } else { - format!("${}", var) - } - }) - } - - /// Render the type as a compact, single-line string. - /// Useful for testing and debugging, but not much else... - /// - /// The caller must decide how to render unnamed type variables via `render_var`. - pub fn debug_render_with(&self, render_var: F) -> String - where - F: Fn(Var, Option) -> String + Copy, - { - let mut output = String::new(); - self.debug_render_rec(render_var, &mut output); - output - } - - fn debug_render_rec(&self, render_var: F, output: &mut String) - where - F: Fn(Var, Option) -> String + Copy, - { - match self { - Self::Variable { - var, source_name, .. - } => { - output.push_str(&render_var(*var, source_name.clone())); - } - - Self::Constructor { - constructor_kind: _, - canonical_value, - source_value, - } => { - if let Some(source_value) = source_value { - output.push_str(&source_value.to_string()); - } else { - output.push_str(&canonical_value.to_string()); - } - } - Self::ConstructorAlias { - canonical_value, - source_value, - .. - } => { - if let Some(source_value) = source_value { - output.push_str(&source_value.to_string()); - } else { - output.push_str(&canonical_value.to_string()); - } - } - Self::PrimConstructor(prim) => { - output.push_str(&prim.to_string()); - } - Self::Call { - function, - arguments, - } => { - function.debug_render_rec(render_var, output); - output.push('('); - let arguments_len = arguments.len(); - arguments.iter().enumerate().for_each(|(i, arg)| { - arg.debug_render_rec(render_var, output); - if i + 1 != arguments_len.into() { - output.push_str(", "); - } - }); - output.push(')'); - } - - Self::Function { - parameters, - return_type, - } => { - output.push('('); - let parameters_len = parameters.len(); - parameters.iter().enumerate().for_each(|(i, param)| { - param.debug_render_rec(render_var, output); - if i != parameters_len - 1 { - output.push_str(", "); - } - }); - output.push_str(") -> "); - return_type.debug_render_rec(render_var, output); - } - Self::RecordOpen { - kind, - var, - source_name, - row, - } => { - if cfg!(debug_assertions) && *kind == Kind::Row { - output.push('#'); - } - output.push_str("{ "); - output.push_str(&render_var(*var, source_name.clone())); - output.push_str(" | "); - let row_len = row.len(); - row.iter().enumerate().for_each(|(i, (label, t))| { - output.push_str(&label.0); - output.push_str(": "); - t.debug_render_rec(render_var, output); - if i != row_len - 1 { - output.push_str(", "); - } - }); - output.push_str(" }"); - } - Self::RecordClosed { kind, row } => { - if cfg!(debug_assertions) && *kind == Kind::Row { - output.push('#'); - } - if row.is_empty() { - output.push_str("{}"); - return; - } - output.push_str("{ "); - let row_len = row.len(); - row.iter().enumerate().for_each(|(i, (label, t))| { - output.push_str(&label.0); - output.push_str(": "); - t.debug_render_rec(render_var, output); - if i != row_len - 1 { - output.push_str(", "); - } - }); - output.push_str(" }"); - } - }; - } -} - -#[cfg(test)] -mod tests { - use crate::{ - module_name, name, package_name, proper_name, FullyQualifiedProperName, Kind, PrimType, - Qualified, Type, - }; - use non_empty_vec::ne_vec; - - #[test] - fn it_renders_correctly() { - let test_type = Type::Function { - parameters: vec![], - return_type: Box::new(Type::Function { - parameters: vec![ - Type::PrimConstructor(PrimType::String), - Type::PrimConstructor(PrimType::Bool), - Type::Constructor { - constructor_kind: Kind::Type, - canonical_value: FullyQualifiedProperName { - module_name: (Some(package_name!("dunno")), module_name!("Foo", "Bar")), - value: proper_name!("Baz"), - }, - source_value: Some(Qualified { - module_name: Some(proper_name!("Bar")), - value: proper_name!("Baz"), - }), - }, - ], - return_type: Box::new(Type::Function { - parameters: vec![Type::Function { - parameters: vec![Type::Variable { - variable_kind: Kind::Type, - var: 0, - source_name: Some(name!("a")), - }], - return_type: Box::new(Type::Variable { - variable_kind: Kind::Type, - var: 1, - source_name: Some(name!("b")), - }), - }], - return_type: Box::new(Type::Call { - function: Box::new(Type::Constructor { - constructor_kind: Kind::Function { - parameters: ne_vec![Kind::Type], - }, - canonical_value: FullyQualifiedProperName { - module_name: (Some(package_name!("maybe")), module_name!("Maybe")), - value: proper_name!("Maybe"), - }, - source_value: Some(Qualified { - module_name: None, - value: proper_name!("Maybe"), - }), - }), - arguments: ne_vec![Type::Call { - function: Box::new(Type::Constructor { - constructor_kind: Kind::Function { - parameters: ne_vec![Kind::Type, Kind::Type], - }, - canonical_value: FullyQualifiedProperName { - module_name: ( - Some(package_name!("result")), - module_name!("Result"), - ), - value: proper_name!("Result"), - }, - source_value: Some(Qualified { - module_name: None, - value: proper_name!("Result"), - }), - }), - arguments: ne_vec![ - Type::Variable { - variable_kind: Kind::Type, - var: 2, - source_name: None, - }, - Type::Variable { - variable_kind: Kind::Type, - var: 34, - source_name: None, - } - ] - }], - }), - }), - }), - }; - assert_eq!( - test_type.debug_render(), - "() -> (String, Bool, Bar.Baz) -> ((a) -> b) -> Maybe(Result($2, $34))", - ); - } } diff --git a/crates/ditto-ast/src/utils.rs b/crates/ditto-ast/src/utils.rs new file mode 100644 index 000000000..75d1b8b93 --- /dev/null +++ b/crates/ditto-ast/src/utils.rs @@ -0,0 +1,426 @@ +#![allow(missing_docs)] + +use crate::{FullyQualifiedProperName, Kind, Name, PrimType, Row, Type, Var}; + +/// Macro for constructing [Name]s. +/// +/// This isn't checked for syntax correctness, so use with care. +#[macro_export] +macro_rules! name { + ($string_like:expr) => { + $crate::Name(smol_str::SmolStr::from($string_like)) + }; +} + +/// Macro for constructing [ProperName]s. +/// +/// This isn't checked for syntax correctness, so use with care. +#[macro_export] +macro_rules! proper_name { + ($string_like:expr) => { + $crate::ProperName(smol_str::SmolStr::from($string_like)) + }; +} + +/// Macro for constructing [PackageName]s. +/// +/// This isn't checked for syntax correctness, so use with care. +#[macro_export] +macro_rules! package_name { + ($string_like:expr) => { + $crate::PackageName(smol_str::SmolStr::from($string_like)) + }; +} + +/// Macro for constructing [ModuleName]s. +/// +/// This isn't checked for syntax correctness, so use with care. +#[macro_export] +macro_rules! module_name { + ($($proper_name:expr),+) => {{ + $crate::ModuleName(nonempty::nonempty![$($crate::proper_name!($proper_name)),+]) + }}; +} + +impl Kind { + pub fn debug_render(&self) -> String { + let mut s = String::new(); + self.debug_render_to(&mut s).unwrap(); + s + } + + fn debug_render_to(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result { + match self { + Self::Variable(var) => { + write!(w, "${var}") + } + Self::Type => { + write!(w, "Type") + } + Self::Row => { + write!(w, "Row") + } + Self::Function { parameters } => { + let len = parameters.len(); + for (i, param) in parameters.iter().enumerate() { + param.debug_render_to(w)?; + if i + 1 != len { + write!(w, ", ")?; + } + } + write!(w, ") -> Type") + } + } + } +} + +impl Type { + pub fn debug_render(&self) -> String { + let mut s = String::new(); + self.debug_render_to(&mut s).unwrap(); + s + } + + fn debug_render_to(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result { + match self { + Self::Variable { + var, + source_name, + is_rigid, + .. + } => { + if let Some(source_name) = source_name { + write!(w, "{source_name}")?; + } + write!(w, "${var}")?; + if *is_rigid { + write!(w, "!")?; + } + Ok(()) + } + + Self::Constructor { + canonical_value, + source_value, + constructor_kind: _, + } => { + if let Some(source_value) = source_value { + write!(w, "{source_value}") + } else { + write!(w, "{canonical_value}") + } + } + Self::ConstructorAlias { + canonical_value, + source_value: _, + alias_variables: _, + aliased_type, + constructor_kind: _, + } => { + write!(w, "{canonical_value} / ")?; + aliased_type.debug_render_to(w) + } + Self::PrimConstructor(prim) => { + write!(w, "{prim}") + } + + Self::Call { + function: + box Self::ConstructorAlias { + canonical_value, + source_value: _, + alias_variables: _, + aliased_type, + constructor_kind: _, + }, + arguments, + } => { + write!(w, "{canonical_value}(")?; + let arguments_len = arguments.len(); + for (i, arg) in arguments.iter().enumerate() { + arg.debug_render_to(w)?; + if i + 1 != arguments_len { + write!(w, ", ")?; + } + } + write!(w, ") / ")?; + aliased_type.debug_render_to(w) + } + Self::Call { + function, + arguments, + } => { + function.debug_render_to(w)?; + write!(w, "(")?; + let arguments_len = arguments.len(); + for (i, arg) in arguments.iter().enumerate() { + arg.debug_render_to(w)?; + if i + 1 != arguments_len { + write!(w, ", ")?; + } + } + write!(w, ")") + } + + Self::Function { + parameters, + return_type, + } => { + write!(w, "(")?; + let parameters_len = parameters.len(); + for (i, param) in parameters.iter().enumerate() { + param.debug_render_to(w)?; + if i != parameters_len - 1 { + write!(w, ", ")?; + } + } + write!(w, ") -> ")?; + return_type.debug_render_to(w) + } + Self::RecordOpen { + is_rigid, + kind, + var, + source_name, + row, + } => { + if *kind == Kind::Row { + write!(w, "#")?; + } + write!(w, "{{ ")?; + if let Some(source_name) = source_name { + write!(w, "{source_name}")?; + } + write!(w, "${var}")?; + if *is_rigid { + write!(w, "!")?; + } + write!(w, " | ")?; + let row_len = row.len(); + for (i, (label, t)) in row.iter().enumerate() { + write!(w, "{}: ", label.0)?; + t.debug_render_to(w)?; + if i != row_len - 1 { + write!(w, ", ")?; + } + } + write!(w, " }}") + } + Self::RecordClosed { kind, row } => { + if *kind == Kind::Row { + write!(w, "#")?; + } + if row.is_empty() { + return write!(w, "{{}}"); + } + write!(w, "{{ ")?; + let row_len = row.len(); + for (i, (label, t)) in row.iter().enumerate() { + write!(w, "{}: ", label.0)?; + t.debug_render_to(w)?; + if i != row_len - 1 { + write!(w, ", ")?; + } + } + write!(w, " }}") + } + } + } + + pub fn from_cst_unchecked(cst: ditto_cst::Type, module_name: &crate::ModuleName) -> Self { + Self::from_cst_unchecked_with( + &mut 0, + &mut std::default::Default::default(), + cst, + module_name, + ) + } + + pub fn from_cst_unchecked_with( + supply: &mut usize, + type_vars: &mut std::collections::HashMap, + cst: ditto_cst::Type, + module_name: &crate::ModuleName, + ) -> Self { + match cst { + ditto_cst::Type::Parens(parens) => { + Self::from_cst_unchecked_with(supply, type_vars, *parens.value, module_name) + } + ditto_cst::Type::Call { + function: ditto_cst::TypeCallFunction::Constructor(constructor), + arguments, + } => Self::Call { + function: Box::new(Self::from_cst_unchecked_with( + supply, + type_vars, + ditto_cst::Type::Constructor(constructor), + module_name, + )), + arguments: Box::new(nonempty::NonEmpty { + head: Self::from_cst_unchecked_with( + supply, + type_vars, + *arguments.value.head, + module_name, + ), + tail: arguments + .value + .tail + .into_iter() + .map(|t| { + Self::from_cst_unchecked_with(supply, type_vars, *t.1, module_name) + }) + .collect(), + }), + }, + ditto_cst::Type::Call { + function: ditto_cst::TypeCallFunction::Variable(name), + arguments, + } => Self::Call { + function: Box::new(Self::from_cst_unchecked_with( + supply, + type_vars, + ditto_cst::Type::Variable(name), + module_name, + )), + + arguments: Box::new(nonempty::NonEmpty { + head: Self::from_cst_unchecked_with( + supply, + type_vars, + *arguments.value.head, + module_name, + ), + tail: arguments + .value + .tail + .into_iter() + .map(|t| { + Self::from_cst_unchecked_with(supply, type_vars, *t.1, module_name) + }) + .collect(), + }), + }, + ditto_cst::Type::Function { + parameters: params, + box return_type, + .. + } => { + let mut parameters = Vec::new(); + if let Some(params) = params.value { + for box param in params.into_iter() { + parameters.push(Self::from_cst_unchecked_with( + supply, + type_vars, + param, + module_name, + )); + } + } + Self::Function { + parameters, + return_type: Box::new(Self::from_cst_unchecked_with( + supply, + type_vars, + return_type, + module_name, + )), + } + } + ditto_cst::Type::Constructor(constructor) => match constructor.value.0.value.as_str() { + "Effect" => Self::PrimConstructor(PrimType::Effect), + "Array" => Self::PrimConstructor(PrimType::Array), + "Int" => Self::PrimConstructor(PrimType::Int), + "Float" => Self::PrimConstructor(PrimType::Float), + "String" => Self::PrimConstructor(PrimType::String), + "Bool" => Self::PrimConstructor(PrimType::Bool), + "Unit" => Self::PrimConstructor(PrimType::Unit), + _ => Self::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: ( + None, + constructor + .module_name + .clone() + .map(|(pn, _dot)| { + crate::ModuleName(nonempty::NonEmpty::new(pn.into())) + }) + .unwrap_or_else(|| module_name.clone()), + ), + value: constructor.value.clone().into(), + }, + source_value: Some(constructor.into()), + }, + }, + ditto_cst::Type::Variable(name) => { + let name = name.into(); + let var = type_vars.get(&name).copied().unwrap_or_else(|| { + let var = *supply; + type_vars.insert(name.clone(), var); + *supply += 1; + var + }); + Self::Variable { + source_name: Some(name), + is_rigid: true, + var, + variable_kind: Kind::Type, + } + } + ditto_cst::Type::RecordClosed(braces) => Self::RecordClosed { + kind: Kind::Type, + row: { + let mut row = Row::new(); + if let Some(comma_sep) = braces.value { + for ditto_cst::RecordTypeField { label, value, .. } in comma_sep.into_iter() + { + row.insert( + label.into(), + Self::from_cst_unchecked_with( + supply, + type_vars, + *value, + module_name, + ), + ); + } + } + row + }, + }, + ditto_cst::Type::RecordOpen(braces) => { + let (name, _pipe, comma_sep) = braces.value; + let name = name.into(); + let var = type_vars.get(&name).copied().unwrap_or_else(|| { + let var = *supply; + type_vars.insert(name.clone(), var); + *supply += 1; + var + }); + Self::RecordOpen { + kind: Kind::Type, + var, + source_name: Some(name), + is_rigid: true, + row: { + let mut row = Row::new(); + for ditto_cst::RecordTypeField { label, value, .. } in comma_sep.into_iter() + { + row.insert( + label.into(), + Self::from_cst_unchecked_with( + supply, + type_vars, + *value, + module_name, + ), + ); + } + row + }, + } + } + } + } +} diff --git a/crates/ditto-ast/src/var.rs b/crates/ditto-ast/src/var.rs index 4fa5c871d..dfd5dd533 100644 --- a/crates/ditto-ast/src/var.rs +++ b/crates/ditto-ast/src/var.rs @@ -1,3 +1,3 @@ /// We pepper a lot of variables around the syntax tree as part of type checking. /// This is the type of said variables. -pub type Var = usize; +pub type Var = usize; // REVIEW: this could probably be a smaller uint? diff --git a/crates/ditto-cst/Cargo.toml b/crates/ditto-cst/Cargo.toml index 66e7dfec8..3094e67ae 100644 --- a/crates/ditto-cst/Cargo.toml +++ b/crates/ditto-cst/Cargo.toml @@ -15,6 +15,7 @@ thiserror = "1.0" lalrpop-util = "0.19.8" regex = "1" logos = "0.12" +smol_str = "0.1" #simsearch = "xx" <-- for suggestions #unindent = "xx" <-- might come in useful for smart multi-line strings (like Nix) #codespan = "xx" <-- might be a good replacement for our `Span` type diff --git a/crates/ditto-cst/src/ditto.lalrpop b/crates/ditto-cst/src/ditto.lalrpop index cccc0820f..c05655c44 100644 --- a/crates/ditto-cst/src/ditto.lalrpop +++ b/crates/ditto-cst/src/ditto.lalrpop @@ -1,6 +1,6 @@ -// vi: syntax=rust use crate as cst; use crate::lexer::{Error, Token, Comments}; +use smol_str::SmolStr; // NOTE: An LR(1) parser only uses a single token of lookahead (that's what the 1 in LR(1) means). // https://github.com/lalrpop/lalrpop/issues/552 @@ -203,7 +203,7 @@ MatchArm: cst::MatchArm = { > => cst::MatchArm { pipe, pattern, right_arrow, expression } } -Pattern: cst::Pattern = { +pub Pattern: cst::Pattern = { >> => cst::Pattern::Constructor { constructor, arguments }, => cst::Pattern::NullaryConstructor { constructor }, => cst::Pattern::Variable { name }, @@ -375,13 +375,13 @@ extern { "end" => Token::EndKeyword(), "alias" => Token::AliasKeyword(), "|>" => Token::RightPizzaOperator(), - "name" => Token::Name(<(Comments, String)>), - "ProperName" => Token::ProperName(<(Comments, String)>), - "_unused_name" => Token::UnusedName(<(Comments, String)>), - "package-name" => Token::PackageName(<(Comments, String)>), - "string" => Token::String(<(Comments, String)>), - "integer" => Token::Int(<(Comments, String)>), - "float" => Token::Float(<(Comments, String)>), + "name" => Token::Name(<(Comments, SmolStr)>), + "ProperName" => Token::ProperName(<(Comments, SmolStr)>), + "_unused_name" => Token::UnusedName(<(Comments, SmolStr)>), + "package-name" => Token::PackageName(<(Comments, SmolStr)>), + "string" => Token::String(<(Comments, SmolStr)>), + "integer" => Token::Int(<(Comments, SmolStr)>), + "float" => Token::Float(<(Comments, SmolStr)>), } } diff --git a/crates/ditto-cst/src/lexer.rs b/crates/ditto-cst/src/lexer.rs index 75ec449eb..37094e12c 100644 --- a/crates/ditto-cst/src/lexer.rs +++ b/crates/ditto-cst/src/lexer.rs @@ -1,5 +1,6 @@ use crate::{Comment, Span}; use logos::{Logos, SpannedIter}; +use smol_str::SmolStr; pub struct Lexer<'input> { pub comments: Vec, @@ -116,7 +117,7 @@ impl<'input> Iterator for Lexer<'input> { RawToken::String(string) => Token::String(( self.collect_comments(), // Remove the surrounding quotes - string[1..string.len() - 1].to_owned(), + string[1..string.len() - 1].into(), )), RawToken::Number(string) => { if string.contains('.') { @@ -169,13 +170,13 @@ pub enum Token { EndKeyword(Comments), AliasKeyword(Comments), RightPizzaOperator(Comments), - Name((Comments, String)), - ProperName((Comments, String)), - UnusedName((Comments, String)), - PackageName((Comments, String)), - String((Comments, String)), - Int((Comments, String)), - Float((Comments, String)), + Name((Comments, SmolStr)), + ProperName((Comments, SmolStr)), + UnusedName((Comments, SmolStr)), + PackageName((Comments, SmolStr)), + String((Comments, SmolStr)), + Int((Comments, SmolStr)), + Float((Comments, SmolStr)), } #[derive(Debug, Clone)] @@ -263,26 +264,26 @@ enum RawToken { RightPizzaOperator, #[regex(r"[a-z]\w*", priority = 2, callback = |lex| lex.slice().parse())] - Name(String), // ^^ Needs to be higher priority than PackageName + Name(SmolStr), // ^^ Needs to be higher priority than PackageName #[regex(r"[A-Z]\w*", callback = |lex| lex.slice().parse())] - ProperName(String), + ProperName(SmolStr), #[regex(r"_(?:[a-z]\w*)?", callback = |lex| lex.slice().parse())] - UnusedName(String), + UnusedName(SmolStr), #[regex(r"[a-z][a-z0-9-]*", callback = |lex| lex.slice().parse())] - PackageName(String), + PackageName(SmolStr), #[regex(r"--[^\n]*", callback = |lex| lex.slice().parse())] - Comment(String), + Comment(SmolStr), #[regex(r"\d[\d_]*(?:\.\d[\d_]*)?", callback = |lex| lex.slice().parse())] - Number(String), + Number(SmolStr), // Regex credit: https://stackoverflow.com/a/10786066 #[regex(r#""([^"\\]*(\\.[^"\\]*)*)""#, callback = |lex| lex.slice().parse())] - String(String), + String(SmolStr), #[regex(r"\r?\n")] Newline, diff --git a/crates/ditto-cst/src/parser/mod.rs b/crates/ditto-cst/src/parser/mod.rs index 837b41dd6..a7e62bca9 100644 --- a/crates/ditto-cst/src/parser/mod.rs +++ b/crates/ditto-cst/src/parser/mod.rs @@ -2,8 +2,8 @@ mod tests; use crate::{ - lexer, Expression, ForeignValueDeclaration, Header, ImportLine, Module, ModuleName, Span, Type, - TypeAliasDeclaration, TypeDeclaration, ValueDeclaration, + lexer, Expression, ForeignValueDeclaration, Header, ImportLine, Module, ModuleName, Pattern, + Span, Type, TypeAliasDeclaration, TypeDeclaration, ValueDeclaration, }; use miette::{Diagnostic, NamedSource, SourceSpan}; use thiserror::Error; @@ -279,3 +279,13 @@ impl Expression { Ok(expression) } } + +impl Pattern { + /// Parse a single [Pattern]. + pub fn parse(input: &str) -> Result { + let lexer = lexer::Lexer::new(input); + let parser = ditto::PatternParser::new(); + let pattern = parser.parse(lexer)?; + Ok(pattern) + } +} diff --git a/crates/ditto-cst/src/token.rs b/crates/ditto-cst/src/token.rs index 372404c4c..15980a35d 100644 --- a/crates/ditto-cst/src/token.rs +++ b/crates/ditto-cst/src/token.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; /// A source span. /// TODO: use stdlib Range here as it's basically the same thing and surely brings some optimizations with it @@ -26,6 +27,13 @@ impl Span { } } +#[allow(clippy::from_over_into)] +impl Into for Span { + fn into(self) -> miette::SourceSpan { + miette::SourceSpan::from((self.start_offset, self.end_offset - self.start_offset)) + } +} + /// A syntactic element. /// /// Each token consists of its source location, surrounding comments, and @@ -74,10 +82,10 @@ impl Token { /// A string token prefixed with `"--"`. #[derive(Debug, Clone, PartialEq)] -pub struct Comment(pub String); +pub struct Comment(pub SmolStr); /// A [String] syntax node. -pub type StringToken = Token; +pub type StringToken = Token; /// An empty syntax node. /// diff --git a/crates/ditto-pattern-checker/Cargo.toml b/crates/ditto-pattern-checker/Cargo.toml new file mode 100644 index 000000000..3c96901fa --- /dev/null +++ b/crates/ditto-pattern-checker/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ditto-pattern-checker" +version = "0.0.1" +edition = "2021" +license = "BSD-3-Clause" + +[lib] +doctest = false + +[dependencies] +ditto-ast = { path = "../ditto-ast" } +nonempty = "0.8" +smol_str = "0.1" +halfbrown = "0.1" +thiserror = "1.0" + +[dev-dependencies] +ditto-cst = { path = "../ditto-cst" } +datadriven = "0.6" diff --git a/crates/ditto-pattern-checker/benches/bench.rs b/crates/ditto-pattern-checker/benches/bench.rs new file mode 100644 index 000000000..0632a44a6 --- /dev/null +++ b/crates/ditto-pattern-checker/benches/bench.rs @@ -0,0 +1,186 @@ +#![feature(test)] +#![feature(box_patterns)] + +extern crate test; + +use test::Bencher; + +#[bench] +fn bench_complex_pattern(b: &mut Bencher) { + let env_constructors = mk_test_env_constructors(); + let pattern_type = || { + let cst_type = ditto_cst::Type::parse( + " + Thruple( + Maybe(Thruple(ABC, ABC, ABC)), + Maybe(Thruple(ABC, ABC, ABC)), + Maybe(Thruple(ABC, ABC, ABC)), + )", + ) + .unwrap(); + ditto_ast::Type::from_cst_unchecked(cst_type, &ditto_ast::module_name!("Bench")) + }; + let patterns = || { + vec![convert_pattern( + ditto_cst::Pattern::parse( + " + Thruple( + Just(Thruple(A, A, A)), + Just(Thruple(A, A, A)), + Just(Thruple(A, A, A)), + )", + ) + .unwrap(), + )] + }; + b.iter(|| ditto_pattern_checker::is_exhaustive(&env_constructors, pattern_type(), patterns())) +} + +fn mk_test_env_constructors() -> ditto_pattern_checker::EnvConstructors { + use ditto_ast::{ + module_name, name, proper_name, unqualified, FullyQualifiedProperName, Kind, Type, + }; + use ditto_pattern_checker::{EnvConstructor::ModuleConstructor, EnvConstructors}; + let mut env_constructors = EnvConstructors::new(); + let abc_type = || Type::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: (None, module_name!("Test")), + value: proper_name!("ABC"), + }, + source_value: None, // not needed here + }; + env_constructors.insert( + unqualified(proper_name!("A")), + ModuleConstructor { + constructor: proper_name!("A"), + constructor_type: abc_type(), + }, + ); + env_constructors.insert( + unqualified(proper_name!("B")), + ModuleConstructor { + constructor: proper_name!("B"), + constructor_type: abc_type(), + }, + ); + env_constructors.insert( + unqualified(proper_name!("C")), + ModuleConstructor { + constructor: proper_name!("C"), + constructor_type: 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: None, // not needed here + }), + arguments: Box::new(nonempty::NonEmpty { + head: a.clone(), + tail: vec![], + }), + }; + + env_constructors.insert( + unqualified(proper_name!("Just")), + ModuleConstructor { + constructor: proper_name!("Just"), + constructor_type: Type::Function { + parameters: vec![a.clone()], + return_type: Box::new(maybe_type()), + }, + }, + ); + env_constructors.insert( + unqualified(proper_name!("Nothing")), + ModuleConstructor { + constructor: proper_name!("Nothing"), + constructor_type: 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: None, // not needed here + }), + arguments: Box::new(nonempty::NonEmpty { + head: b.clone(), + tail: vec![c.clone(), d.clone()], + }), + }; + env_constructors.insert( + unqualified(proper_name!("Thruple")), + ModuleConstructor { + constructor: proper_name!("Thruple"), + constructor_type: Type::Function { + parameters: vec![b.clone(), c.clone(), d.clone()], + return_type: Box::new(thruple_type()), + }, + }, + ); + + env_constructors +} + +fn convert_pattern(cst_pattern: ditto_cst::Pattern) -> ditto_ast::Pattern { + let span = cst_pattern.get_span(); + match cst_pattern { + ditto_cst::Pattern::NullaryConstructor { constructor } => { + // NOTE: assuming it's a local constructor for now + ditto_ast::Pattern::LocalConstructor { + span, + constructor: constructor.value.into(), + arguments: vec![], + } + } + ditto_cst::Pattern::Constructor { + constructor, + arguments, + } => { + // again: assuming it's a local constructor for now + ditto_ast::Pattern::LocalConstructor { + span, + constructor: constructor.value.into(), + arguments: arguments + .value + .into_iter() + .map(|box pat| convert_pattern(pat)) + .collect(), + } + } + ditto_cst::Pattern::Variable { name } => ditto_ast::Pattern::Variable { + span, + name: ditto_ast::Name::from(name), + }, + ditto_cst::Pattern::Unused { unused_name } => ditto_ast::Pattern::Unused { + span, + unused_name: ditto_ast::UnusedName::from(unused_name), + }, + } +} diff --git a/crates/ditto-pattern-checker/src/constructor.rs b/crates/ditto-pattern-checker/src/constructor.rs new file mode 100644 index 000000000..7d643d04f --- /dev/null +++ b/crates/ditto-pattern-checker/src/constructor.rs @@ -0,0 +1,124 @@ +use crate::env_constructors::EnvConstructors; +use ditto_ast::{FullyQualifiedProperName, QualifiedProperName, Type}; +use nonempty::NonEmpty; + +#[derive(Debug, Clone)] +pub struct Constructor { + pub name: QualifiedProperName, + pub arguments: Vec, +} + +pub type Constructors = Vec; + +pub fn constructors_for_type( + pattern_type: &Type, + env_constructors: &EnvConstructors, +) -> Constructors { + let pattern_type = unalias_type(pattern_type); + + // Suppose `pattern_type` is `Result(Int, SomeError)` + // + // First we try and get the canonical type name + its arguments. + // i.e. `(std:Result.Result, [Int, SomeError])` + if let Some((want_canonical_value, specific_type_arguments)) = + get_type_constructor(pattern_type) + { + // Sweet, it this type has a canonical name and a (possibly empty) list of arguments, + // let's go looking for the constructors... + env_constructors + .iter() + .filter_map(|(constructor_name, env_constructor)| { + // We're looking for the constructor whose terminal type name matches + // The one we're looking for (std:Result.Result). + // + // e.g. we want to find `Ok`, + // which has the (generic) type `(a) -> Result(a, e)` + // and the "terminal" type `Result(a, e)` + let terminal_type = env_constructor.get_terminal_type(); + let (got_canonical_value, generic_type_arguments) = + get_type_constructor(terminal_type)?; + if got_canonical_value != want_canonical_value { + // Nope, keep looking + return None; + } + let constructor_type = env_constructor.get_type(); + let constructor_arguments = get_type_function_arguments(constructor_type); + if let Some(constructor_arguments) = constructor_arguments { + // Now we need to substitute the constructors arguments to the more + // narrow/specific types of the original `pattern_type` + let arguments = constructor_arguments + .iter() + .map(|t: &Type| -> Type { + for (i, generic_argument) in generic_type_arguments.iter().enumerate() { + if t == *generic_argument { + return specific_type_arguments[i].clone(); + } + } + t.clone() + }) + .collect(); + let constructor = Constructor { + name: constructor_name.clone(), + arguments, + }; + Some(constructor) + } else { + // Constructor takes no arguments (e.g. `Nothing`) + let constructor = Constructor { + name: constructor_name.clone(), + arguments: Vec::new(), + }; + Some(constructor) + } + }) + .collect() + } else { + Constructors::new() + } +} + +/// Given `Result(a, e)` will return `(std:Result.Result, [a, e])` +/// Given `Maybe(a)` will return `(std:Maybe.Maybe, [a])` +/// Given `Ordering` will return `(std:Ordering.Ordering, [])` +fn get_type_constructor(t: &Type) -> Option<(&FullyQualifiedProperName, Vec<&Type>)> { + match t { + // Result(a, e) + Type::Call { + function: + box Type::Constructor { + // std:Result.Result + canonical_value, + .. + }, + // (a, e) + arguments: box NonEmpty { head, tail }, + } => { + let mut arguments = Vec::with_capacity(tail.len() + 1); + arguments.push(head); + arguments.extend(tail); + Some((canonical_value, arguments)) + } + Type::Constructor { + canonical_value, .. + } => Some((canonical_value, vec![])), + _ => None, + } +} + +fn get_type_function_arguments(t: &Type) -> Option<&Vec> { + match t { + Type::Function { parameters, .. } => Some(parameters), + _ => None, + } +} + +pub fn unalias_type(t: &Type) -> &Type { + match t { + Type::Call { + function: box Type::ConstructorAlias { aliased_type, .. }, + .. + } + | Type::ConstructorAlias { aliased_type, .. } => unalias_type(aliased_type), + _ => t, + } +} diff --git a/crates/ditto-pattern-checker/src/env_constructors.rs b/crates/ditto-pattern-checker/src/env_constructors.rs new file mode 100644 index 000000000..525b4ea32 --- /dev/null +++ b/crates/ditto-pattern-checker/src/env_constructors.rs @@ -0,0 +1,42 @@ +use ditto_ast::{FullyQualifiedProperName, ProperName, QualifiedProperName, Type}; +use halfbrown::HashMap; + +pub type EnvConstructors = HashMap; + +#[derive(Clone)] +pub enum EnvConstructor { + ModuleConstructor { + constructor: ProperName, + constructor_type: Type, + }, + ImportedConstructor { + constructor: FullyQualifiedProperName, + constructor_type: Type, + }, +} + +impl EnvConstructor { + pub fn get_type(&self) -> &Type { + match self { + Self::ModuleConstructor { + constructor_type, .. + } => constructor_type, + Self::ImportedConstructor { + constructor_type, .. + } => constructor_type, + } + } + + pub fn get_terminal_type(&self) -> &Type { + match self.get_type() { + Type::Function { + box return_type, .. + } => { + // Type constructors aren't curried! + return_type + } + // This should either be a type constructor or a call of a type constructor! + other => other, + } + } +} diff --git a/crates/ditto-pattern-checker/src/error.rs b/crates/ditto-pattern-checker/src/error.rs new file mode 100644 index 000000000..b9350c8c7 --- /dev/null +++ b/crates/ditto-pattern-checker/src/error.rs @@ -0,0 +1,18 @@ +use crate::patterns::{ClausePatterns, IdealPattern}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("redundant clauses")] + RedundantClauses(ClausePatterns), + + #[error("patterns not covered")] + NotCovered(NotCovered), + + #[error("malformed pattern")] + MalformedPattern { + wanted_nargs: usize, + got_nargs: usize, + }, +} + +pub type NotCovered = Vec; diff --git a/crates/ditto-pattern-checker/src/lib.rs b/crates/ditto-pattern-checker/src/lib.rs new file mode 100644 index 000000000..c4ffb55a7 --- /dev/null +++ b/crates/ditto-pattern-checker/src/lib.rs @@ -0,0 +1,314 @@ +#![feature(box_patterns)] + +// REFERENCE: +// https://adamschoenemann.dk/posts/2018-05-29-pattern-matching.html +// +// Although also worth peeking at: +// https://github.com/elm/compiler/blob/master/compiler/src/Nitpick/PatternMatches.hs +// http://moscova.inria.fr/~maranget/papers/warn/warn.pdf + +mod constructor; +mod env_constructors; +mod error; +mod patterns; +mod substitution; +mod supply; +#[cfg(test)] +mod tests; + +pub use env_constructors::{EnvConstructor, EnvConstructors}; +pub use error::{Error, NotCovered}; + +use constructor::{constructors_for_type, Constructor, Constructors}; +use ditto_ast::{self as ast, Type, Var}; +use halfbrown::HashMap; +use patterns::{ClausePattern, ClausePatterns, IdealPattern}; +use substitution::Substitution; +use supply::Supply; + +pub fn is_exhaustive( + env_constructors: &EnvConstructors, + pattern_type: &Type, + patterns: Vec, +) -> Result { + let mut supply = Supply(0); + let mut env_coverage = EnvCoverage::new(); + let var = supply.fresh(); + let constructors = constructors_for_type(pattern_type, env_constructors); + env_coverage.insert(var, constructors); + let ideal = IdealPattern::Variable { var }; + let clause_patterns = patterns.into_iter().map(ClausePattern::from).collect(); + check_coverage( + &mut supply, + env_constructors, + &env_coverage, + ideal, + clause_patterns, + ) +} + +type EnvCoverage = HashMap; + +enum Clauses { + Cons(Clause, Box), + Nil, +} + +impl Clauses { + fn collect_unused(self, unused: &mut Vec) { + match self { + Self::Nil => {} + Self::Cons(clause, box rest) => { + if clause.usages < 1 { + unused.push(clause.pattern); + } + rest.collect_unused(unused) + } + } + } +} + +#[derive(Debug)] +struct Clause { + usages: usize, + pattern: ClausePattern, +} + +impl Clause { + fn new(pattern: ClausePattern) -> Self { + Self { usages: 0, pattern } + } + + fn use_clause(self) -> Self { + Self { + usages: self.usages + 1, + pattern: self.pattern, + } + } +} + +type Result = std::result::Result; + +fn check_coverage( + supply: &mut Supply, + env_constructors: &EnvConstructors, + env_coverage: &EnvCoverage, + ideal: IdealPattern, + clause_patterns: Vec, +) -> Result { + let clauses: Clauses = clause_patterns + .into_iter() + .rfold(Clauses::Nil, |tail, head| { + Clauses::Cons(Clause::new(head), Box::new(tail)) + }); + + match ideal.check_coverage(supply, env_constructors, env_coverage, clauses) { + Ok((clauses, not_covered)) => { + if !not_covered.is_empty() { + return Err(Error::NotCovered(not_covered)); + } + let mut unused_patterns: ClausePatterns = vec![]; + clauses.collect_unused(&mut unused_patterns); + if !unused_patterns.is_empty() { + return Err(Error::RedundantClauses(unused_patterns)); + } + Ok(()) + } + Err(error) => Err(error), + } +} + +impl IdealPattern { + fn check_coverage( + self, + supply: &mut Supply, + env_constructors: &EnvConstructors, + env_coverage: &EnvCoverage, + clauses: Clauses, + ) -> Result<(Clauses, NotCovered)> { + match clauses { + Clauses::Nil => { + // eprintln!("=> no remaining clauses, can't cover {}", self); + let not_covered = vec![self]; + Ok((Clauses::Nil, not_covered)) + } + Clauses::Cons(clause, box rest) => { + // eprintln!("=> checking {} against {}", clause.pattern, self); + match clause.pattern.to_substitution(supply, &self)? { + None => { + // eprintln!("clause has no substitution, checking the remaining..."); + let (rest, not_covered) = + self.check_coverage(supply, env_constructors, env_coverage, rest)?; + let clauses = Clauses::Cons(clause, Box::new(rest)); + Ok((clauses, not_covered)) + } + Some(substitution) => match substitution.get_first_non_injective_var() { + None => { + // eprintln!("substitution is injective, stopping here"); + let not_covered = NotCovered::new(); + let clauses = Clauses::Cons(clause.use_clause(), Box::new(rest)); + Ok((clauses, not_covered)) + } + Some(ref var) => { + // eprintln!( + // "substitution is not injective, checkng constructors for ${var}" + // ); + env_coverage + .get(var) + .expect("there to be constructors") + .iter() + .try_fold( + (Clauses::Cons(clause, Box::new(rest)), NotCovered::new()), + |(clauses, mut not_covered), constructor| { + let (new_ideal, new_env_coverage) = constructor_to_pattern( + supply, + env_constructors, + constructor, + ); + let mut env_coverage = env_coverage.clone(); + for (k, v) in new_env_coverage { + env_coverage.insert(k, v); + } + let substitution = Substitution::new(*var, new_ideal); + let new_ideal = substitution.apply(self.clone()); + let (clauses, more_not_covered) = new_ideal + .check_coverage( + supply, + env_constructors, + &env_coverage, + clauses, + )?; + not_covered.extend(more_not_covered); + Ok((clauses, not_covered)) + }, + ) + } + }, + } + } + } + } + + fn from_clause_pattern(supply: &mut Supply, clause_pattern: &ClausePattern) -> Self { + match clause_pattern { + ClausePattern::Constructor { + constructor, + arguments, + .. + } => Self::Constructor { + constructor: constructor.clone(), + arguments: arguments + .iter() + .map(|pat| IdealPattern::from_clause_pattern(supply, pat)) + .collect(), + }, + ClausePattern::Variable { .. } => Self::Variable { + var: supply.fresh(), + }, + } + } +} + +impl ClausePattern { + fn to_substitution( + &self, + supply: &mut Supply, + ideal: &IdealPattern, + ) -> Result> { + let subst = match (ideal, self) { + (IdealPattern::Variable { var }, clause_pattern) => { + let subst = Substitution::new( + *var, + IdealPattern::from_clause_pattern(supply, clause_pattern), + ); + Ok(Some(subst)) + } + ( + IdealPattern::Constructor { + constructor: ideal_constructor, + arguments: ideal_arguments, + }, + ClausePattern::Constructor { + constructor: clause_constructor, + arguments: clause_arguments, + .. + }, + ) => { + if ideal_constructor != clause_constructor { + Ok(None) + } else if ideal_arguments.len() != clause_arguments.len() { + Err(Error::MalformedPattern { + wanted_nargs: ideal_arguments.len(), + got_nargs: clause_arguments.len(), + }) + } else if ideal_arguments.is_empty() { + Ok(Some(Substitution::empty())) + } else { + // NOTE: - we want to return an empty substitution if `self` matches `ideal` + // - we want to return `None` to reject this match + // - we want to return a _non-empty_ substitution to recurse + // + // Start by assuming we have a match + let mut subst = Substitution::empty(); + let mut found_bad_match = false; + for (ideal_argument, clause_argument) in + ideal_arguments.iter().zip(clause_arguments) + { + match clause_argument.to_substitution(supply, ideal_argument)? { + None => { + found_bad_match = true; + } + Some(new_subst) => subst.extend(new_subst), + } + } + + // NOTE: this logic is not part of adam schoenemann's blog post: + // https://adamschoenemann.dk/posts/2018-05-29-pattern-matching.html + // But I found it to be very necessary to get the behaviour I want here. + // _Maybe_ it was something he missed, or maybe it's the result of me + // porting it wrong, either way... + if found_bad_match && subst.is_injective() { + Ok(None) + } else { + Ok(Some(subst)) + } + } + } + (IdealPattern::Constructor { .. }, ClausePattern::Variable { .. }) => { + // Ok(None) ??? + Ok(Some(Substitution::empty())) + } + }?; + // eprintln!( + // "to_substitution {} {} {}", + // self, + // ideal, + // subst + // .as_ref() + // .map_or("None".to_string(), |subst| subst.to_string()) + // ); + Ok(subst) + } +} + +fn constructor_to_pattern( + supply: &mut Supply, + env_constructors: &EnvConstructors, + constructor: &Constructor, +) -> (IdealPattern, EnvCoverage) { + let mut env_coverage = EnvCoverage::new(); + let mut pattern_arguments = Vec::new(); + for arg in constructor.arguments.iter() { + let var = supply.fresh(); + let constructors = constructors_for_type(arg, env_constructors); + env_coverage.insert(var, constructors); + pattern_arguments.push(IdealPattern::Variable { var }); + } + ( + IdealPattern::Constructor { + constructor: constructor.name.value.clone(), + arguments: pattern_arguments, + }, + env_coverage, + ) +} diff --git a/crates/ditto-pattern-checker/src/patterns.rs b/crates/ditto-pattern-checker/src/patterns.rs new file mode 100644 index 000000000..a35760d17 --- /dev/null +++ b/crates/ditto-pattern-checker/src/patterns.rs @@ -0,0 +1,193 @@ +use ditto_ast::{self as ast, Pattern, ProperName, Span, Var}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum IdealPattern { + Constructor { + constructor: ProperName, + arguments: Vec, + }, + Variable { + var: T, + }, +} + +impl IdealPattern { + pub fn void(&self) -> IdealPattern<()> { + match self { + Self::Constructor { + constructor, + arguments, + } => IdealPattern::Constructor { + constructor: constructor.clone(), + arguments: arguments.iter().map(|arg| arg.void()).collect(), + }, + Self::Variable { .. } => IdealPattern::Variable { var: () }, + } + } +} + +pub type ClausePatterns = Vec; + +#[derive(Debug)] +pub enum ClausePattern { + Constructor { + span: Span, + constructor: ProperName, + arguments: Vec, + }, + Variable { + span: Span, + var: ClausePatternVar, + }, +} + +#[derive(Debug)] +pub enum ClausePatternVar { + Name(ast::Name), + UnusedName(ast::UnusedName), +} + +impl ClausePattern { + pub fn get_span(&self) -> Span { + match self { + Self::Constructor { span, .. } => *span, + Self::Variable { span, .. } => *span, + } + } +} + +impl std::convert::From for ClausePattern { + fn from(pattern: Pattern) -> Self { + match pattern { + Pattern::LocalConstructor { + span, + constructor, + arguments, + } => Self::Constructor { + span, + constructor, + arguments: arguments.into_iter().map(Self::from).collect(), + }, + Pattern::ImportedConstructor { + span, + constructor, + arguments, + } => Self::Constructor { + span, + constructor: constructor.value, + arguments: arguments.into_iter().map(Self::from).collect(), + }, + Pattern::Variable { name, span } => Self::Variable { + span, + var: ClausePatternVar::Name(name), + }, + Pattern::Unused { unused_name, span } => Self::Variable { + span, + var: ClausePatternVar::UnusedName(unused_name), + }, + } + } +} + +impl std::fmt::Display for ClausePattern { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Constructor { + constructor, + arguments, + .. + } if arguments.is_empty() => write!(f, "{}", constructor), + + Self::Constructor { + constructor, + arguments, + .. + } => { + write!(f, "{}(", constructor)?; + let arguments_len = arguments.len(); + for (i, arg) in arguments.iter().enumerate() { + write!(f, "{}", arg)?; + if i + 1 != arguments_len { + write!(f, ", ")?; + } + } + write!(f, ")") + } + Self::Variable { + var: ClausePatternVar::Name(name), + .. + } => { + write!(f, "{}", name) + } + + Self::Variable { + var: ClausePatternVar::UnusedName(unused), + .. + } => { + write!(f, "{}", unused) + } + } + } +} + +impl std::fmt::Display for IdealPattern<()> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Constructor { + constructor, + arguments, + .. + } if arguments.is_empty() => write!(f, "{}", constructor), + + Self::Constructor { + constructor, + arguments, + .. + } => { + write!(f, "{}(", constructor)?; + let arguments_len = arguments.len(); + for (i, arg) in arguments.iter().enumerate() { + write!(f, "{}", arg)?; + if i + 1 != arguments_len { + write!(f, ", ")?; + } + } + write!(f, ")") + } + Self::Variable { .. } => { + write!(f, "_") + } + } + } +} + +impl std::fmt::Display for IdealPattern { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Constructor { + constructor, + arguments, + .. + } if arguments.is_empty() => write!(f, "{}", constructor), + + Self::Constructor { + constructor, + arguments, + .. + } => { + write!(f, "{}(", constructor)?; + let arguments_len = arguments.len(); + for (i, arg) in arguments.iter().enumerate() { + write!(f, "{}", arg)?; + if i + 1 != arguments_len { + write!(f, ", ")?; + } + } + write!(f, ")") + } + Self::Variable { var } => { + write!(f, "${}", var) + } + } + } +} diff --git a/crates/ditto-pattern-checker/src/state.rs b/crates/ditto-pattern-checker/src/state.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/ditto-pattern-checker/src/state.rs @@ -0,0 +1 @@ + diff --git a/crates/ditto-pattern-checker/src/substitution.rs b/crates/ditto-pattern-checker/src/substitution.rs new file mode 100644 index 000000000..4f6b37956 --- /dev/null +++ b/crates/ditto-pattern-checker/src/substitution.rs @@ -0,0 +1,68 @@ +use crate::patterns::IdealPattern; +use ditto_ast::Var; +use halfbrown::HashMap; + +#[derive(Debug)] +pub struct Substitution(HashMap); + +impl Substitution { + pub fn empty() -> Self { + Self(HashMap::new()) + } + + pub fn new(var: Var, ideal: IdealPattern) -> Self { + let mut hm = HashMap::new(); + hm.insert(var, ideal); + Self(hm) + } + + pub fn extend(&mut self, other: Self) { + for (k, v) in other.0 { + self.0.insert(k, v); + } + } + + pub fn get_first_non_injective_var(&self) -> Option { + for (var, pattern) in self.0.iter() { + if matches!(pattern, IdealPattern::Constructor { .. }) { + return Some(*var); + } + } + None + } + + pub fn is_injective(&self) -> bool { + self.get_first_non_injective_var().is_none() + } + + pub fn apply(&self, ideal: IdealPattern) -> IdealPattern { + match ideal { + IdealPattern::Variable { var } => self.0.get(&var).map_or_else( + || IdealPattern::Variable { var }, + |pattern| self.apply(pattern.clone()), + // ^^ I'm assuming we need to recursively substitute here? + ), + IdealPattern::Constructor { + constructor, + arguments, + } => IdealPattern::Constructor { + constructor, + arguments: arguments.into_iter().map(|arg| self.apply(arg)).collect(), + }, + } + } +} + +impl std::fmt::Display for Substitution { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "({})", + self.0 + .iter() + .map(|(k, v)| format!("{k} -> {v}")) + .collect::>() + .join(", ") + ) + } +} diff --git a/crates/ditto-pattern-checker/src/supply.rs b/crates/ditto-pattern-checker/src/supply.rs new file mode 100644 index 000000000..58911eaed --- /dev/null +++ b/crates/ditto-pattern-checker/src/supply.rs @@ -0,0 +1,11 @@ +use ditto_ast::Var; + +pub struct Supply(pub Var); + +impl Supply { + pub fn fresh(&mut self) -> Var { + let var = self.0; + self.0 += 1; + var + } +} diff --git a/crates/ditto-pattern-checker/src/tests.rs b/crates/ditto-pattern-checker/src/tests.rs new file mode 100644 index 000000000..34b300faf --- /dev/null +++ b/crates/ditto-pattern-checker/src/tests.rs @@ -0,0 +1,213 @@ +#[test] +fn testdata() { + use std::fmt::Write; + datadriven::walk("tests/testdata", |f| { + f.run(|test_case| -> String { + let mut lines = test_case.input.lines(); + let pattern_type = lines + .next() + .and_then(|arg| { + ditto_cst::Type::parse(arg.strip_prefix("pattern_type=").unwrap()).ok() + }) + .map(|cst_type| { + ditto_ast::Type::from_cst_unchecked(cst_type, &ditto_ast::module_name!("Test")) + }) + .unwrap(); + let patterns = lines + .map(|line| convert_pattern(ditto_cst::Pattern::parse(line).unwrap())) + .collect::>(); + + let env_constructors = mk_test_env_constructors(); + if let Err(err) = crate::is_exhaustive(&env_constructors, &pattern_type, patterns) { + match err { + crate::Error::RedundantClauses(redundant) => { + let mut s = String::new(); + write!(s, "Error: redundant clauses\n").unwrap(); + let mut rendered = redundant + .into_iter() + .map(|clause_pattern| clause_pattern.to_string()) + .collect::>(); + rendered.sort(); + for pretty in rendered { + write!(s, " {}\n", pretty).unwrap(); + } + s + } + crate::Error::NotCovered(not_covered) => { + let mut s = String::new(); + write!(s, "Error: clauses not covered\n").unwrap(); + let mut rendered = not_covered + .into_iter() + .map(|ideal_pattern| ideal_pattern.void().to_string()) + .collect::>(); + rendered.sort(); + for pretty in rendered { + write!(s, " {}\n", pretty).unwrap(); + } + s + } + crate::Error::MalformedPattern { + wanted_nargs, + got_nargs, + } => { + format!( + "Error: malformed pattern, expected {} arguments but got {}\n", + wanted_nargs, got_nargs + ) + } + } + } else { + "it's exhaustive boss\n".to_string() + } + }); + }); +} + +fn mk_test_env_constructors() -> crate::EnvConstructors { + use crate::env_constructors::{EnvConstructor::ModuleConstructor, EnvConstructors}; + use ditto_ast::{ + module_name, name, proper_name, unqualified, FullyQualifiedProperName, Kind, Type, + }; + let mut env_constructors = EnvConstructors::new(); + let abc_type = || Type::Constructor { + constructor_kind: Kind::Type, + canonical_value: FullyQualifiedProperName { + module_name: (None, module_name!("Test")), + value: proper_name!("ABC"), + }, + source_value: None, // not needed here + }; + env_constructors.insert( + unqualified(proper_name!("A")), + ModuleConstructor { + constructor: proper_name!("A"), + constructor_type: abc_type(), + }, + ); + env_constructors.insert( + unqualified(proper_name!("B")), + ModuleConstructor { + constructor: proper_name!("B"), + constructor_type: abc_type(), + }, + ); + env_constructors.insert( + unqualified(proper_name!("C")), + ModuleConstructor { + constructor: proper_name!("C"), + constructor_type: 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: None, // not needed here + }), + arguments: Box::new(nonempty::NonEmpty { + head: a.clone(), + tail: vec![], + }), + }; + + env_constructors.insert( + unqualified(proper_name!("Just")), + ModuleConstructor { + constructor: proper_name!("Just"), + constructor_type: Type::Function { + parameters: vec![a.clone()], + return_type: Box::new(maybe_type()), + }, + }, + ); + env_constructors.insert( + unqualified(proper_name!("Nothing")), + ModuleConstructor { + constructor: proper_name!("Nothing"), + constructor_type: 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: None, // not needed here + }), + arguments: Box::new(nonempty::NonEmpty { + head: b.clone(), + tail: vec![c.clone(), d.clone()], + }), + }; + env_constructors.insert( + unqualified(proper_name!("Thruple")), + ModuleConstructor { + constructor: proper_name!("Thruple"), + constructor_type: Type::Function { + parameters: vec![b.clone(), c.clone(), d.clone()], + return_type: Box::new(thruple_type()), + }, + }, + ); + + env_constructors +} + +fn convert_pattern(cst_pattern: ditto_cst::Pattern) -> ditto_ast::Pattern { + let span = cst_pattern.get_span(); + match cst_pattern { + ditto_cst::Pattern::NullaryConstructor { constructor } => { + // NOTE: assuming it's a local constructor for now + ditto_ast::Pattern::LocalConstructor { + span, + constructor: constructor.value.into(), + arguments: vec![], + } + } + ditto_cst::Pattern::Constructor { + constructor, + arguments, + } => { + // again: assuming it's a local constructor for now + ditto_ast::Pattern::LocalConstructor { + span, + constructor: constructor.value.into(), + arguments: arguments + .value + .into_iter() + .map(|box pat| convert_pattern(pat)) + .collect(), + } + } + ditto_cst::Pattern::Variable { name } => ditto_ast::Pattern::Variable { + span, + name: ditto_ast::Name::from(name), + }, + ditto_cst::Pattern::Unused { unused_name } => ditto_ast::Pattern::Unused { + span, + unused_name: ditto_ast::UnusedName::from(unused_name), + }, + } +} diff --git a/crates/ditto-pattern-checker/tests/testdata/.gitignore b/crates/ditto-pattern-checker/tests/testdata/.gitignore new file mode 100644 index 000000000..e55532032 --- /dev/null +++ b/crates/ditto-pattern-checker/tests/testdata/.gitignore @@ -0,0 +1,4 @@ +# Useful for debugging an individual test case +# Add the test case to focus then point datadriven at +# only that file. +focus \ No newline at end of file diff --git a/crates/ditto-pattern-checker/tests/testdata/constructors b/crates/ditto-pattern-checker/tests/testdata/constructors new file mode 100644 index 000000000..e1281bf9f --- /dev/null +++ b/crates/ditto-pattern-checker/tests/testdata/constructors @@ -0,0 +1,325 @@ +is-exhaustive +pattern_type=ABC +A +---- +Error: clauses not covered + B + C + + +is-exhaustive +pattern_type=ABC +B +C +---- +Error: clauses not covered + A + + +is-exhaustive +pattern_type=ABC +A +B +C +C +---- +Error: redundant clauses + C + + +is-exhaustive +pattern_type=Maybe(a) +Just(_) +---- +Error: clauses not covered + Nothing + + +is-exhaustive +pattern_type=Maybe(Maybe(Maybe(ABC))) +Just(Just(Just(A))) +---- +Error: clauses not covered + Just(Just(Just(B))) + Just(Just(Just(C))) + Just(Just(Nothing)) + Just(Nothing) + Nothing + + +is-exhaustive +pattern_type=Maybe(a) +Nothing +---- +Error: clauses not covered + Just(_) + + +is-exhaustive +pattern_type=Maybe(a) +Just(_) +Nothing +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=Maybe(a) +Just(_) +Nothing +Nothing +---- +Error: redundant clauses + Nothing + + +is-exhaustive +pattern_type=Maybe(ABC) +Just(A) +---- +Error: clauses not covered + Just(B) + Just(C) + Nothing + + +is-exhaustive +pattern_type=Maybe(ABC) +Just(A) +Just(A) +---- +Error: clauses not covered + Just(B) + Just(C) + Nothing + + +is-exhaustive +pattern_type=Thruple(ABC, ABC, ABC) +Thruple(A, B) +---- +Error: malformed pattern, expected 3 arguments but got 2 + + +is-exhaustive +pattern_type=Thruple(ABC, ABC, ABC) +Thruple(A, A, A) +---- +Error: clauses not covered + Thruple(A, A, B) + Thruple(A, A, C) + Thruple(A, B, A) + Thruple(A, B, B) + Thruple(A, B, C) + Thruple(A, C, A) + Thruple(A, C, B) + Thruple(A, C, C) + Thruple(B, A, A) + Thruple(B, A, B) + Thruple(B, A, C) + Thruple(B, B, A) + Thruple(B, B, B) + Thruple(B, B, C) + Thruple(B, C, A) + Thruple(B, C, B) + Thruple(B, C, C) + Thruple(C, A, A) + Thruple(C, A, B) + Thruple(C, A, C) + Thruple(C, B, A) + Thruple(C, B, B) + Thruple(C, B, C) + Thruple(C, C, A) + Thruple(C, C, B) + Thruple(C, C, C) + + +is-exhaustive +pattern_type=Thruple(Maybe(ABC), ABC, ABC) +Thruple(Just(A), A, A) +---- +Error: clauses not covered + Thruple(Just(A), A, B) + Thruple(Just(A), A, C) + Thruple(Just(A), B, A) + Thruple(Just(A), B, B) + Thruple(Just(A), B, C) + Thruple(Just(A), C, A) + Thruple(Just(A), C, B) + Thruple(Just(A), C, C) + Thruple(Just(B), A, A) + Thruple(Just(B), A, B) + Thruple(Just(B), A, C) + Thruple(Just(B), B, A) + Thruple(Just(B), B, B) + Thruple(Just(B), B, C) + Thruple(Just(B), C, A) + Thruple(Just(B), C, B) + Thruple(Just(B), C, C) + Thruple(Just(C), A, A) + Thruple(Just(C), A, B) + Thruple(Just(C), A, C) + Thruple(Just(C), B, A) + Thruple(Just(C), B, B) + Thruple(Just(C), B, C) + Thruple(Just(C), C, A) + Thruple(Just(C), C, B) + Thruple(Just(C), C, C) + Thruple(Nothing, A, A) + Thruple(Nothing, A, B) + Thruple(Nothing, A, C) + Thruple(Nothing, B, A) + Thruple(Nothing, B, B) + Thruple(Nothing, B, C) + Thruple(Nothing, C, A) + Thruple(Nothing, C, B) + Thruple(Nothing, C, C) + + +is-exhaustive +pattern_type=Thruple(Maybe(ABC), Int, Int) +Thruple(Just(A), _, _) +---- +Error: clauses not covered + Thruple(Just(B), _, _) + Thruple(Just(C), _, _) + Thruple(Nothing, _, _) + + +is-exhaustive +pattern_type=Thruple(Maybe(Thruple(ABC, ABC, ABC)), Maybe(Thruple(ABC, ABC, ABC)), Maybe(Thruple(ABC, ABC, ABC))) +Thruple(_, _, _) +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=Thruple(Maybe(Thruple(ABC, ABC, ABC)), Maybe(Thruple(ABC, ABC, ABC)), Maybe(Thruple(ABC, ABC, ABC))) +Thruple(Nothing, Nothing, Nothing) +---- +Error: clauses not covered + Thruple(Just(_), Just(_), Just(_)) + Thruple(Just(_), Just(_), Nothing) + Thruple(Just(_), Nothing, Just(_)) + Thruple(Just(_), Nothing, Nothing) + Thruple(Nothing, Just(_), Just(_)) + Thruple(Nothing, Just(_), Nothing) + Thruple(Nothing, Nothing, Just(_)) + + +is-exhaustive +pattern_type=Thruple(Maybe(Thruple(ABC, ABC, ABC)), Maybe(Thruple(ABC, ABC, ABC)), Maybe(Thruple(ABC, ABC, ABC))) +Thruple(Just(Thruple(A, A, A)), Nothing, Nothing) +---- +Error: clauses not covered + Thruple(Just(Thruple(A, A, A)), Just(_), Just(_)) + Thruple(Just(Thruple(A, A, A)), Just(_), Nothing) + Thruple(Just(Thruple(A, A, A)), Nothing, Just(_)) + Thruple(Just(Thruple(A, A, B)), Just(_), Just(_)) + Thruple(Just(Thruple(A, A, B)), Just(_), Nothing) + Thruple(Just(Thruple(A, A, B)), Nothing, Just(_)) + Thruple(Just(Thruple(A, A, B)), Nothing, Nothing) + Thruple(Just(Thruple(A, A, C)), Just(_), Just(_)) + Thruple(Just(Thruple(A, A, C)), Just(_), Nothing) + Thruple(Just(Thruple(A, A, C)), Nothing, Just(_)) + Thruple(Just(Thruple(A, A, C)), Nothing, Nothing) + Thruple(Just(Thruple(A, B, A)), Just(_), Just(_)) + Thruple(Just(Thruple(A, B, A)), Just(_), Nothing) + Thruple(Just(Thruple(A, B, A)), Nothing, Just(_)) + Thruple(Just(Thruple(A, B, A)), Nothing, Nothing) + Thruple(Just(Thruple(A, B, B)), Just(_), Just(_)) + Thruple(Just(Thruple(A, B, B)), Just(_), Nothing) + Thruple(Just(Thruple(A, B, B)), Nothing, Just(_)) + Thruple(Just(Thruple(A, B, B)), Nothing, Nothing) + Thruple(Just(Thruple(A, B, C)), Just(_), Just(_)) + Thruple(Just(Thruple(A, B, C)), Just(_), Nothing) + Thruple(Just(Thruple(A, B, C)), Nothing, Just(_)) + Thruple(Just(Thruple(A, B, C)), Nothing, Nothing) + Thruple(Just(Thruple(A, C, A)), Just(_), Just(_)) + Thruple(Just(Thruple(A, C, A)), Just(_), Nothing) + Thruple(Just(Thruple(A, C, A)), Nothing, Just(_)) + Thruple(Just(Thruple(A, C, A)), Nothing, Nothing) + Thruple(Just(Thruple(A, C, B)), Just(_), Just(_)) + Thruple(Just(Thruple(A, C, B)), Just(_), Nothing) + Thruple(Just(Thruple(A, C, B)), Nothing, Just(_)) + Thruple(Just(Thruple(A, C, B)), Nothing, Nothing) + Thruple(Just(Thruple(A, C, C)), Just(_), Just(_)) + Thruple(Just(Thruple(A, C, C)), Just(_), Nothing) + Thruple(Just(Thruple(A, C, C)), Nothing, Just(_)) + Thruple(Just(Thruple(A, C, C)), Nothing, Nothing) + Thruple(Just(Thruple(B, A, A)), Just(_), Just(_)) + Thruple(Just(Thruple(B, A, A)), Just(_), Nothing) + Thruple(Just(Thruple(B, A, A)), Nothing, Just(_)) + Thruple(Just(Thruple(B, A, A)), Nothing, Nothing) + Thruple(Just(Thruple(B, A, B)), Just(_), Just(_)) + Thruple(Just(Thruple(B, A, B)), Just(_), Nothing) + Thruple(Just(Thruple(B, A, B)), Nothing, Just(_)) + Thruple(Just(Thruple(B, A, B)), Nothing, Nothing) + Thruple(Just(Thruple(B, A, C)), Just(_), Just(_)) + Thruple(Just(Thruple(B, A, C)), Just(_), Nothing) + Thruple(Just(Thruple(B, A, C)), Nothing, Just(_)) + Thruple(Just(Thruple(B, A, C)), Nothing, Nothing) + Thruple(Just(Thruple(B, B, A)), Just(_), Just(_)) + Thruple(Just(Thruple(B, B, A)), Just(_), Nothing) + Thruple(Just(Thruple(B, B, A)), Nothing, Just(_)) + Thruple(Just(Thruple(B, B, A)), Nothing, Nothing) + Thruple(Just(Thruple(B, B, B)), Just(_), Just(_)) + Thruple(Just(Thruple(B, B, B)), Just(_), Nothing) + Thruple(Just(Thruple(B, B, B)), Nothing, Just(_)) + Thruple(Just(Thruple(B, B, B)), Nothing, Nothing) + Thruple(Just(Thruple(B, B, C)), Just(_), Just(_)) + Thruple(Just(Thruple(B, B, C)), Just(_), Nothing) + Thruple(Just(Thruple(B, B, C)), Nothing, Just(_)) + Thruple(Just(Thruple(B, B, C)), Nothing, Nothing) + Thruple(Just(Thruple(B, C, A)), Just(_), Just(_)) + Thruple(Just(Thruple(B, C, A)), Just(_), Nothing) + Thruple(Just(Thruple(B, C, A)), Nothing, Just(_)) + Thruple(Just(Thruple(B, C, A)), Nothing, Nothing) + Thruple(Just(Thruple(B, C, B)), Just(_), Just(_)) + Thruple(Just(Thruple(B, C, B)), Just(_), Nothing) + Thruple(Just(Thruple(B, C, B)), Nothing, Just(_)) + Thruple(Just(Thruple(B, C, B)), Nothing, Nothing) + Thruple(Just(Thruple(B, C, C)), Just(_), Just(_)) + Thruple(Just(Thruple(B, C, C)), Just(_), Nothing) + Thruple(Just(Thruple(B, C, C)), Nothing, Just(_)) + Thruple(Just(Thruple(B, C, C)), Nothing, Nothing) + Thruple(Just(Thruple(C, A, A)), Just(_), Just(_)) + Thruple(Just(Thruple(C, A, A)), Just(_), Nothing) + Thruple(Just(Thruple(C, A, A)), Nothing, Just(_)) + Thruple(Just(Thruple(C, A, A)), Nothing, Nothing) + Thruple(Just(Thruple(C, A, B)), Just(_), Just(_)) + Thruple(Just(Thruple(C, A, B)), Just(_), Nothing) + Thruple(Just(Thruple(C, A, B)), Nothing, Just(_)) + Thruple(Just(Thruple(C, A, B)), Nothing, Nothing) + Thruple(Just(Thruple(C, A, C)), Just(_), Just(_)) + Thruple(Just(Thruple(C, A, C)), Just(_), Nothing) + Thruple(Just(Thruple(C, A, C)), Nothing, Just(_)) + Thruple(Just(Thruple(C, A, C)), Nothing, Nothing) + Thruple(Just(Thruple(C, B, A)), Just(_), Just(_)) + Thruple(Just(Thruple(C, B, A)), Just(_), Nothing) + Thruple(Just(Thruple(C, B, A)), Nothing, Just(_)) + Thruple(Just(Thruple(C, B, A)), Nothing, Nothing) + Thruple(Just(Thruple(C, B, B)), Just(_), Just(_)) + Thruple(Just(Thruple(C, B, B)), Just(_), Nothing) + Thruple(Just(Thruple(C, B, B)), Nothing, Just(_)) + Thruple(Just(Thruple(C, B, B)), Nothing, Nothing) + Thruple(Just(Thruple(C, B, C)), Just(_), Just(_)) + Thruple(Just(Thruple(C, B, C)), Just(_), Nothing) + Thruple(Just(Thruple(C, B, C)), Nothing, Just(_)) + Thruple(Just(Thruple(C, B, C)), Nothing, Nothing) + Thruple(Just(Thruple(C, C, A)), Just(_), Just(_)) + Thruple(Just(Thruple(C, C, A)), Just(_), Nothing) + Thruple(Just(Thruple(C, C, A)), Nothing, Just(_)) + Thruple(Just(Thruple(C, C, A)), Nothing, Nothing) + Thruple(Just(Thruple(C, C, B)), Just(_), Just(_)) + Thruple(Just(Thruple(C, C, B)), Just(_), Nothing) + Thruple(Just(Thruple(C, C, B)), Nothing, Just(_)) + Thruple(Just(Thruple(C, C, B)), Nothing, Nothing) + Thruple(Just(Thruple(C, C, C)), Just(_), Just(_)) + Thruple(Just(Thruple(C, C, C)), Just(_), Nothing) + Thruple(Just(Thruple(C, C, C)), Nothing, Just(_)) + Thruple(Just(Thruple(C, C, C)), Nothing, Nothing) + Thruple(Nothing, Just(_), Just(_)) + Thruple(Nothing, Just(_), Nothing) + Thruple(Nothing, Nothing, Just(_)) + Thruple(Nothing, Nothing, Nothing) diff --git a/crates/ditto-pattern-checker/tests/testdata/variables b/crates/ditto-pattern-checker/tests/testdata/variables new file mode 100644 index 000000000..4cb035fc8 --- /dev/null +++ b/crates/ditto-pattern-checker/tests/testdata/variables @@ -0,0 +1,60 @@ +is-exhaustive +pattern_type=Int +five +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=Int +five +again +---- +Error: redundant clauses + again + + +is-exhaustive +pattern_type=Int +five +again +and_again +---- +Error: redundant clauses + again + and_again + + +is-exhaustive +pattern_type=Int +_ignore +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=Unit +_ignore +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=Float +n +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=Bool +cond +---- +it's exhaustive boss + + +is-exhaustive +pattern_type=String +_stringy +---- +it's exhaustive boss 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 + ╰──── + +---- +----