diff --git a/.sampo/changesets/borrow-local-scopes.md b/.sampo/changesets/borrow-local-scopes.md new file mode 100644 index 00000000..e83595b3 --- /dev/null +++ b/.sampo/changesets/borrow-local-scopes.md @@ -0,0 +1,5 @@ +--- +cargo/nash-can: minor +--- + +Borrow module data and local binding maps during canonicalization instead of cloning the full environment at each scope. Preserve shadowing, diagnostics, and error recovery. diff --git a/.sampo/changesets/bounded-trait-lookup.md b/.sampo/changesets/bounded-trait-lookup.md new file mode 100644 index 00000000..58ad172d --- /dev/null +++ b/.sampo/changesets/bounded-trait-lookup.md @@ -0,0 +1,6 @@ +--- +cargo/nash-can: patch +cargo/nash-solve: patch +--- + +Use ordered map ranges for trait candidate lookup, evidence construction, entailment, and missing-implementation suggestions. Preserve candidate order without a second index. diff --git a/.sampo/changesets/concise-source-aware-diagnostics.md b/.sampo/changesets/concise-source-aware-diagnostics.md new file mode 100644 index 00000000..f865e3ac --- /dev/null +++ b/.sampo/changesets/concise-source-aware-diagnostics.md @@ -0,0 +1,12 @@ +--- +cargo/nash-parse: minor +cargo/nash-constrain: minor +cargo/nash-solve: patch +cargo/nash-report: minor +cargo/nash-language-server: minor +cargo/nash-cli: minor +--- + +Use concise diagnostics with full expected/actual type comparisons, expectation-origin labels, and stable codes independent of display titles. Retain parser opening positions for closing-delimiter reports. Support arbitrary secondary labels and related reports across source files. + +Extend diagnostic JSON with code, severity, labels, suggestions, and related reports. JSON messages now contain styled prose without embedded source drawings; consumers should render the structured labels. LSP diagnostic codes now use stable identifiers instead of titles and include secondary and related source locations. diff --git a/.sampo/changesets/direct-ast-inference.md b/.sampo/changesets/direct-ast-inference.md new file mode 100644 index 00000000..134f2807 --- /dev/null +++ b/.sampo/changesets/direct-ast-inference.md @@ -0,0 +1,8 @@ +--- +cargo/nash-constrain: minor +cargo/nash-solve: minor +cargo/nash-driver: patch +cargo/nash-report: patch +--- + +Infer directly from the canonical AST into the existing union-find and predicate engine. Remove the allocated constraint tree and intermediate inference Type, preserving schemes, evidence, rank ownership, recursive-group sequencing, and complete diagnostics. Pass canonical modules directly to the solver. diff --git a/.sampo/changesets/parser-accumulators.md b/.sampo/changesets/parser-accumulators.md new file mode 100644 index 00000000..13ddfa4e --- /dev/null +++ b/.sampo/changesets/parser-accumulators.md @@ -0,0 +1,5 @@ +--- +cargo/nash-parse: patch +--- + +Accumulate function arguments and binary operators without cloning partial chains. Keep parser arena allocation linear in operator-chain length. diff --git a/.sampo/changesets/parser-nesting.md b/.sampo/changesets/parser-nesting.md new file mode 100644 index 00000000..672a284c --- /dev/null +++ b/.sampo/changesets/parser-nesting.md @@ -0,0 +1,6 @@ +--- +cargo/nash-parse: patch +cargo/nash-report: patch +--- + +Report excessive expression, pattern, and type nesting before stack exhaustion. Parse flat sequences and nested comments with loops. diff --git a/.sampo/changesets/parser-text-input.md b/.sampo/changesets/parser-text-input.md new file mode 100644 index 00000000..7fbdd36b --- /dev/null +++ b/.sampo/changesets/parser-text-input.md @@ -0,0 +1,6 @@ +--- +cargo/nash-parse: minor +cargo/nash-driver: patch +--- + +Require UTF-8 text at the parser boundary instead of arbitrary bytes. Remove unchecked string conversions and pass source text directly from the driver. diff --git a/.sampo/changesets/remove-interface-cache.md b/.sampo/changesets/remove-interface-cache.md new file mode 100644 index 00000000..d69d9a05 --- /dev/null +++ b/.sampo/changesets/remove-interface-cache.md @@ -0,0 +1,5 @@ +--- +cargo/nash-driver: minor +--- + +Remove unused disk interface-cache APIs, serialization, and cache metadata. Preserve in-memory exports, kind contracts, and fingerprints returned by compilation. diff --git a/.sampo/changesets/source-coordinates.md b/.sampo/changesets/source-coordinates.md new file mode 100644 index 00000000..07d57908 --- /dev/null +++ b/.sampo/changesets/source-coordinates.md @@ -0,0 +1,10 @@ +--- +cargo/nash-region: minor +cargo/nash-source: minor +cargo/nash-parse: minor +cargo/nash-can: patch +cargo/nash-report: minor +cargo/nash-language-server: patch +--- + +Use source-sized coordinates and diagnostic widths throughout parsing and reporting. Check LSP coordinate conversion instead of truncating. Reject oversized Unicode escapes without integer overflow, and make arbitrary lookahead offsets safe. diff --git a/CLAUDE.md b/CLAUDE.md index a58ccdb8..4a845393 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ We use **bumpalo** for arena allocation: ```rust let bump = Bump::new(); let src: &str = bump.alloc_str(&file_contents); -let mut parser = Parser::new(&bump, src.as_bytes()); +let mut parser = Parser::new(&bump, src); ``` ### AST Type Guidelines @@ -31,7 +31,7 @@ let mut parser = Parser::new(&bump, src.as_bytes()); **Inline small `Copy` types** - don't put them behind `&'a`: - Small enums (e.g., `VarType`, `Associativity`) - just store the value - Newtypes around primitives (e.g., `Precedence(u16)`) - just store the value -- `Region` (8 bytes of integers) - same size as a pointer, no benefit to indirection +- `Region` uses native-size source coordinates (32 bytes on 64-bit hosts); keep it inline in AST nodes. **Use `&'a T` for**: - Large types diff --git a/Cargo.lock b/Cargo.lock index 24171819..e724ba38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,15 +240,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bitcoin-consensus-encoding" version = "1.2.0" @@ -1962,7 +1953,6 @@ name = "nash-driver" version = "0.5.0" dependencies = [ "async-trait", - "bincode", "bumpalo", "glob", "indoc", @@ -1978,7 +1968,6 @@ dependencies = [ "nash-report", "nash-solve", "nash-source", - "serde", "thiserror 2.0.20", "tokio", "url", diff --git a/Cargo.toml b/Cargo.toml index d5eadb56..eaa5b778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ license = "Apache-2.0" [workspace.dependencies] async-trait = "0.1" -bincode = "1" bumpalo = { version = "3.19.1", features = ["collections"] } clap = { version = "4.5.60", features = ["derive"] } color-print = "0.3.7" diff --git a/SPEC.md b/SPEC.md index c2743163..ddb7baad 100644 --- a/SPEC.md +++ b/SPEC.md @@ -8,7 +8,7 @@ component specs in [`docs/`](docs/); chunked implementation plans in ## Pipeline ``` -parse -> canonicalize -> kinds -> constrain/solve (+ traits) -> nitpick +parse -> canonicalize -> kinds -> direct inference (+ traits) -> nitpick -> macro expansion (loop) -> Core IR -> optimize -> UPLC ``` @@ -24,10 +24,10 @@ produce UPLC programs; all dependencies inline into each program. | `nash-parse` | parser + Elm error hierarchy | extend ([plans/01](plans/01-syntax.md)) | | `nash-ast` | canonical AST | extend | | `nash-can` | canonicalization, interfaces | extend | -| `nash-constrain` | constraint generation, kinds | extend | -| `nash-solve` | solver, traits, defaulting | extend | +| `nash-constrain` | union-find types, canonical instantiation, type errors | done | +| `nash-solve` | direct AST inference, traits, defaulting | extend | | `nash-nitpick` | exhaustiveness and redundancy | done ([plans/05](plans/05-nitpick.md)) | -| `nash-report` | diagnostics (Elm prose -> miette) | done ([plans/06](plans/06-diagnostics.md)) | +| `nash-report` | concise diagnostics (terminal, JSON, LSP) | done ([plans/06](plans/06-diagnostics.md)) | | `nash-ir` | Core IR + passes | new ([plans/07](plans/07-codegen.md), [08](plans/08-optimizer.md)) | | `nash-codegen` | Can -> Core -> UPLC | new ([plans/07](plans/07-codegen.md)) | | `nash-test` | test runner, fuzzing, shrinking | new ([plans/10](plans/10-testing.md)) | @@ -46,7 +46,8 @@ Done: - [x] Parser (Elm `Parse/*` port, full syntax error hierarchy) - [x] Canonicalization (Elm `Canonicalize/*` port, SCC, interfaces) -- [x] Type inference (Elm `Type/*` port: constraints, rank-based solver, records, aliases) +- [x] Type inference (direct AST inference, rank-based solver, records, aliases) +- [x] Frontend hardening and direct-inference parity ([verification](docs/frontend-hardening-verification.md)) - [x] Project config, driver, dependency-ordered builds, interface cache - [x] `nash check` - [x] UPLC runtime (`nash-plutus`): conformance suite passes @@ -60,7 +61,7 @@ Planned, in execution order (each links to its plan): - [x] 03 Traits: qualified types, resolution, superclasses, defaults, multi-param, orphan rules, literal traits + defaulting, evidence — [plans/03-traits.md](plans/03-traits.md) (default imports deferred to Plan 12) - [x] 04 Representation: remove row polymorphism and Elm supertypes, builtin type inventory, record encoding — [plans/04-representation.md](plans/04-representation.md) - [x] 05 Exhaustiveness (`Nitpick/PatternMatches` port) — [plans/05-nitpick.md](plans/05-nitpick.md) -- [x] 06 Diagnostics (`nash-report`, Elm `Reporting/*` port onto miette) — [plans/06-diagnostics.md](plans/06-diagnostics.md) +- [x] 06 Diagnostics (`nash-report`, concise source labels, stable codes, JSON/LSP) — [plans/06-diagnostics.md](plans/06-diagnostics.md); [concise diagnostics refactor](plans/diagnostics-refactor.md) complete - [ ] 07 Codegen: Core IR, monomorphization, decision trees, recursion, Data casts, UPLC lowering — [plans/07-codegen.md](plans/07-codegen.md) - [ ] 08 Optimizer: inlining, builtin force caching, DCE, case-of-known-ctor/constant folding — [plans/08-optimizer.md](plans/08-optimizer.md) - [ ] 09 Validators + `nash build` — [plans/09-validators-build.md](plans/09-validators-build.md) @@ -80,7 +81,7 @@ Later: LSP features, web playground, package registry (pubgrub), TypeScript code | `AST/Source.hs` | `crates/nash-source/src/lib.rs` | | `AST/Canonical.hs` | `crates/nash-ast/src/lib.rs` | | `Canonicalize/*` | `crates/nash-can/src/*` | -| `Type/Type.hs`, `Type/Constrain/*` | `crates/nash-constrain/src/*` | +| `Type/Type.hs`, `Type/Constrain/*` | `crates/nash-constrain/src/*`, `crates/nash-solve/src/solve/*` | | `Type/{Solve,Unify,Occurs}.hs` | `crates/nash-solve/src/*` | | `Nitpick/PatternMatches.hs` | `crates/nash-nitpick` | | `Reporting/{Doc,Report,Render,Suggest}.hs`, `Reporting/Error/*` | `crates/nash-report` | diff --git a/crates/nash-can/src/entailment.rs b/crates/nash-can/src/entailment.rs index 5063c8e5..3d759d0e 100644 --- a/crates/nash-can/src/entailment.rs +++ b/crates/nash-can/src/entailment.rs @@ -350,9 +350,7 @@ impl<'a> Resolver<'_, 'a> { let mut selected = None; for (key, info) in self .tables - .impls - .iter() - .filter(|(key, _)| Some(key.trait_) == wanted.trait_) + .impls_for(wanted.trait_.ok_or(Failure::Missing)?) { if let nash_ast::head::Match::Yes(arguments) = nash_ast::head::matches( &mut nash_ast::head::Canonical, diff --git a/crates/nash-can/src/environment.rs b/crates/nash-can/src/environment.rs index 5249410f..8df40fb7 100644 --- a/crates/nash-can/src/environment.rs +++ b/crates/nash-can/src/environment.rs @@ -104,7 +104,18 @@ pub fn visible_fields<'a>( fields } -impl Tables<'_> { +impl<'a> Tables<'a> { + /// ImplKey orders the trait before its head slice. The empty slice is the + /// first possible head key, so lookup visits only this trait's candidates. + pub fn impls_for( + &self, + trait_: nash_ast::QualifiedName<'a>, + ) -> impl Iterator, &&'a ImplInfo<'a>)> { + self.impls + .range(nash_ast::ImplKey { trait_, heads: &[] }..) + .take_while(move |(key, _)| key.trait_ == trait_) + } + pub fn has_structural_eq(&self) -> bool { self.traits.contains_key(&nash_ast::primitives::eq_trait()) } @@ -123,7 +134,6 @@ pub enum Var<'a> { annotation: &'a nash_ast::Annotation<'a>, local_region: Option, }, - Local(Region), TopLevel(Region), /// Imported from another module, like Elm's `Foreign home annotation`. /// The annotation comes from the defining module's (post-solve) @@ -259,11 +269,8 @@ pub struct Binop<'a> { pub precedence: Precedence, } -/// The canonicalization environment. -/// -/// Built from imports (foreign) then augmented with local definitions. -/// Consumed by type, pattern, and expression canonicalization. -#[derive(Clone)] +/// Module information for canonicalization, built from imports and top-level +/// definitions. Local expression bindings live in Scope and never mutate it. pub struct Env<'a> { pub kinds: crate::kinds::KindEnv<'a>, pub traits: Exposed<'a, &'a TraitInfo<'a>>, @@ -279,6 +286,86 @@ pub struct Env<'a> { pub q_ctors: Qualified<'a, Ctor<'a>>, } +/// Local bindings borrow module information and their parent scope. Scope exit, +/// including an early error return, cannot change a sibling or ancestor. +pub struct Scope<'scope, 'a> { + pub module: &'scope Env<'a>, + parent: Option<&'scope Scope<'scope, 'a>>, + bindings: &'scope BTreeMap<&'a str, Region>, +} + +impl<'scope, 'a> Scope<'scope, 'a> { + pub fn new( + module: &'scope Env<'a>, + parent: Option<&'scope Scope<'scope, 'a>>, + bindings: &'scope BTreeMap<&'a str, Region>, + ) -> Result>> { + let mut errors = Vec::new(); + for (&name, ®ion) in bindings { + let original = parent.and_then(|scope| scope.local(name)).or_else(|| { + match module.vars.get(name) { + Some(Var::TopLevel(original)) + | Some(Var::Method { + local_region: Some(original), + .. + }) => Some(*original), + _ => None, + } + }); + if let Some(original) = original { + errors.push(Error::Shadowing { + name, + original, + new: region, + }); + } + } + if errors.is_empty() { + Ok(Self { + module, + parent, + bindings, + }) + } else { + Err(errors) + } + } + + pub fn add_locals<'child>( + &'child self, + bindings: &'child BTreeMap<&'a str, Region>, + ) -> Result, Vec>> { + Scope::new(self.module, Some(self), bindings) + } + + pub fn local(&self, name: &str) -> Option { + let mut scope = Some(self); + while let Some(current) = scope { + if let Some(region) = current.bindings.get(name) { + return Some(*region); + } + scope = current.parent; + } + None + } + + pub fn possible_var_names(&self, bump: &'a Bump) -> crate::error::PossibleNames<'a> { + let mut names: std::collections::BTreeSet<_> = self.module.vars.keys().copied().collect(); + let mut scope = Some(self); + while let Some(current) = scope { + names.extend(current.bindings.keys().copied()); + scope = current.parent; + } + let locals = bump.alloc_slice_fill_iter(names); + let qualified = + bump.alloc_slice_fill_iter(self.module.q_vars.iter().map(|(prefix, inner)| { + let names = bump.alloc_slice_fill_iter(inner.keys().copied()); + (*prefix, names as &[&str]) + })); + crate::error::PossibleNames { locals, qualified } + } +} + impl<'a> Env<'a> { /// Compiler-generated calls use trait identity, independent of value names /// and import aliases in the source module. @@ -408,43 +495,6 @@ impl<'a> Env<'a> { } } - /// Extend env with local bindings (clone-on-scope-extension). - /// Shadows foreign imports silently. - /// Errors on re-shadowing a local/top-level. - pub fn add_locals( - &self, - bindings: &std::collections::BTreeMap<&'a str, Region>, - ) -> Result, Vec>> { - let mut new_env = self.clone(); - let mut errors = Vec::new(); - - for (&name, ®ion) in bindings { - match new_env.vars.get(name) { - Some(Var::Local(original)) - | Some(Var::TopLevel(original)) - | Some(Var::Method { - local_region: Some(original), - .. - }) => { - errors.push(Error::Shadowing { - name, - original: *original, - new: region, - }); - } - _ => { - new_env.vars.insert(name, Var::Local(region)); - } - } - } - - if errors.is_empty() { - Ok(new_env) - } else { - Err(errors) - } - } - /// Look up a binop by symbol. Mirrors Elm's `Env.findBinop`. pub fn find_binop( &self, @@ -472,15 +522,6 @@ impl<'a> Env<'a> { bump.alloc_slice_fill_iter(self.binops.keys().copied()) } - pub fn possible_var_names(&self, bump: &'a Bump) -> crate::error::PossibleNames<'a> { - let locals = bump.alloc_slice_fill_iter(self.vars.keys().copied()); - let qualified = bump.alloc_slice_fill_iter(self.q_vars.iter().map(|(prefix, inner)| { - let names = bump.alloc_slice_fill_iter(inner.keys().copied()); - (*prefix, names as &[&str]) - })); - crate::error::PossibleNames { locals, qualified } - } - pub fn possible_type_names(&self, bump: &'a Bump) -> crate::error::PossibleNames<'a> { let locals = bump.alloc_slice_fill_iter(self.types.keys().copied()); let qualified = bump.alloc_slice_fill_iter(self.q_types.iter().map(|(prefix, inner)| { @@ -565,3 +606,81 @@ pub fn merge_qualified<'a, T: Clone>( let inner = table.entry(prefix).or_default(); merge_exposed(inner, name, home, value); } + +#[cfg(test)] +mod tests { + use super::*; + use nash_ast::{Head, ImplKey, PackageName, QualifiedName}; + + #[test] + fn trait_candidates_match_full_map_filter_in_order() { + let bump = Bump::new(); + let mut tables = Tables::default(); + let mut traits = Vec::new(); + for package in [ + None, + Some(PackageName { + author: "a", + project: "b", + }), + ] { + for module in ["A", "AA", "B"] { + for name in ["A", "AA", "B"] { + let trait_ = QualifiedName { + home: ModuleName { + package, + name: module, + }, + name, + }; + traits.push(trait_); + for heads in [ + vec![], + vec![Head::Var(0)], + vec![Head::Var(1)], + vec![Head::Var(0), Head::Var(1)], + ] { + let key = ImplKey { + trait_, + heads: bump.alloc_slice_copy(&heads), + }; + let info = bump.alloc(ImplInfo { + variables: &[], + home: trait_.home, + region: Region::zero(), + trait_, + context: &[], + heads: &[], + methods: &[], + }); + tables.impls.insert(key, info); + } + } + } + } + for module in ["", "AB", "Z"] { + traits.push(QualifiedName { + home: ModuleName { + package: None, + name: module, + }, + name: "Missing", + }); + } + for trait_ in traits { + let expected: Vec<_> = tables + .impls + .iter() + .filter(|(key, _)| key.trait_ == trait_) + .collect(); + let actual: Vec<_> = tables.impls_for(trait_).collect(); + assert_eq!( + actual.iter().map(|(key, _)| *key).collect::>(), + expected.iter().map(|(key, _)| *key).collect::>() + ); + for ((_, actual), (_, expected)) in actual.into_iter().zip(expected) { + assert!(std::ptr::eq(*actual, *expected)); + } + } + } +} diff --git a/crates/nash-can/src/expression.rs b/crates/nash-can/src/expression.rs index 790a9298..5559839e 100644 --- a/crates/nash-can/src/expression.rs +++ b/crates/nash-can/src/expression.rs @@ -14,7 +14,7 @@ use nash_source::{ }; use crate::Error; -use crate::environment::{self, Ctor as EnvCtor, Env, Info, Var}; +use crate::environment::{self, Ctor as EnvCtor, Env, Info, Scope, Var}; use crate::error::DuplicatePatternContext; use crate::pattern::{self, Bindings}; use crate::scc; @@ -88,7 +88,7 @@ pub fn verify_bindings<'a>( pub fn canonicalize_expr<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, expr: &'a Located>, free_locals: &mut FreeLocals<'a>, warnings: &mut Vec>, @@ -149,7 +149,7 @@ pub fn canonicalize_expr<'a>( kind: VarType::CapVar, name, } => { - let ctor = env.find_ctor(bump, region, name)?; + let ctor = env.module.find_ctor(bump, region, name)?; to_var_ctor(bump, env, name, &ctor)? } @@ -164,7 +164,7 @@ pub fn canonicalize_expr<'a>( module, name, } => { - let ctor = env.find_ctor_qual(bump, region, module, name)?; + let ctor = env.module.find_ctor_qual(bump, region, module, name)?; to_var_ctor(bump, env, name, &ctor)? } @@ -173,7 +173,7 @@ pub fn canonicalize_expr<'a>( } SourceExpr::Op(symbol) => { - let binop = env.find_binop(bump, region, symbol)?; + let binop = env.module.find_binop(bump, region, symbol)?; CanExpr::VarOperator { symbol, operator_home: binop.home, @@ -185,6 +185,7 @@ pub fn canonicalize_expr<'a>( SourceExpr::Negate(inner) => { let trait_ = nash_ast::primitives::num_trait(); let annotation = env + .module .method_annotation(trait_, "negate") .ok_or_else(|| vec![Error::NegateWithoutNum { region }])?; let function = bump.alloc(Located::at( @@ -226,9 +227,10 @@ pub fn canonicalize_expr<'a>( grouped: false, } = &argument.value { - env.ctors + env.module + .ctors .values() - .chain(env.q_ctors.values().flat_map(|ctors| ctors.values())) + .chain(env.module.q_ctors.values().flat_map(|ctors| ctors.values())) .find_map(|info| { let Info::Specific( _, @@ -352,7 +354,7 @@ enum SectionSide<'a> { fn canonicalize_section<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, side: SectionSide<'a>, operator: &'a str, region: Region, @@ -361,7 +363,7 @@ fn canonicalize_section<'a>( ) -> Result<&'a Located>, Vec>> { let mut generated = "$section"; let mut suffix = 0; - while env.vars.contains_key(generated) { + while env.local(generated).is_some() || env.module.vars.contains_key(generated) { suffix += 1; generated = bump.alloc_str(&format!("$section{suffix}")); } @@ -400,7 +402,7 @@ fn canonicalize_section<'a>( #[allow(clippy::too_many_arguments)] fn canonicalize_do<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, stmts: &'a [&'a Located>], last: &'a Located>, region: Region, @@ -430,11 +432,14 @@ fn canonicalize_do<'a>( ), }; let trait_ = nash_ast::primitives::monad_trait(); - let annotation = env.method_annotation(trait_, "bind").ok_or_else(|| { - vec![Error::DoWithoutMonad { - region: statement.region, - }] - })?; + let annotation = env + .module + .method_annotation(trait_, "bind") + .ok_or_else(|| { + vec![Error::DoWithoutMonad { + region: statement.region, + }] + })?; // The RHS cannot see its own pattern. The lambda canonicalizer adds the // pattern only for the remaining statements and accounts for delayed uses. let value = canonicalize_expr(bump, env, expression, free_locals, warnings)?; @@ -486,12 +491,16 @@ fn irrefutable(pattern: &SourcePattern<'_>) -> bool { fn find_var<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, region: Region, name: &'a str, free_locals: &mut FreeLocals<'a>, ) -> Result, Vec>> { - match env.vars.get(name) { + if env.local(name).is_some() { + log_var(free_locals, name); + return Ok(CanExpr::VarLocal(name)); + } + match env.module.vars.get(name) { Some(Var::Method { trait_, annotation, .. }) => Ok(CanExpr::VarMethod { @@ -499,14 +508,10 @@ fn find_var<'a>( method: name, annotation, }), - Some(Var::Local(_)) => { - log_var(free_locals, name); - Ok(CanExpr::VarLocal(name)) - } Some(Var::TopLevel(_)) => { log_var(free_locals, name); Ok(CanExpr::VarTopLevel(QualifiedName { - home: env.home, + home: env.module.home, name, })) } @@ -521,8 +526,8 @@ fn find_var<'a>( first_module: *first, other_modules: bump.alloc_slice_fill_iter(others.iter().copied()), }]), - None if env.ctors.contains_key(name) => { - let ctor = env.find_ctor(bump, region, name)?; + None if env.module.ctors.contains_key(name) => { + let ctor = env.module.find_ctor(bump, region, name)?; to_var_ctor(bump, env, name, &ctor) } None => Err(vec![Error::NotFoundVar { @@ -536,24 +541,27 @@ fn find_var<'a>( fn find_var_qual<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, region: Region, prefix: &'a str, name: &'a str, ) -> Result, Vec>> { if !env + .module .q_vars .get(prefix) .is_some_and(|values| values.contains_key(name)) && env + .module .q_ctors .get(prefix) .is_some_and(|ctors| ctors.contains_key(name)) { - let ctor = env.find_ctor_qual(bump, region, prefix, name)?; + let ctor = env.module.find_ctor_qual(bump, region, prefix, name)?; return to_var_ctor(bump, env, name, &ctor); } let info = env + .module .q_vars .get(prefix) .and_then(|m| m.get(name)) @@ -589,7 +597,7 @@ fn find_var_qual<'a>( fn to_var_ctor<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, name: &'a str, ctor: &EnvCtor<'a>, ) -> Result, Vec>> { @@ -637,7 +645,8 @@ fn to_var_ctor<'a>( typ, }); - let annotation = crate::kinds::check_annotation(bump, &env.kinds, name, annotation)?; + let annotation = + crate::kinds::check_annotation(bump, &env.module.kinds, name, annotation)?; CanExpr::VarConstructor { options: *options, reference: ConstructorName { @@ -694,7 +703,8 @@ fn to_var_ctor<'a>( free_vars, typ, }); - let annotation = crate::kinds::check_annotation(bump, &env.kinds, name, annotation)?; + let annotation = + crate::kinds::check_annotation(bump, &env.module.kinds, name, annotation)?; CanExpr::VarConstructor { options: CtorOpts::Normal, @@ -712,7 +722,7 @@ fn to_var_ctor<'a>( fn canonicalize_exprs<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, exprs: &[&'a Located>], free_locals: &mut FreeLocals<'a>, warnings: &mut Vec>, @@ -733,7 +743,7 @@ fn canonicalize_exprs<'a>( fn canonicalize_lambda<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, parameters: &'a [&'a Located>], body: &'a Located>, region: Region, @@ -742,8 +752,12 @@ fn canonicalize_lambda<'a>( ) -> Result<&'a Located>, Vec>> { // One duplicate-detection scope across ALL parameters, so `\x x -> x` // is rejected like in Elm. - let (can_params, all_bindings) = - pattern::verify_all(bump, env, DuplicatePatternContext::LambdaArgs, parameters)?; + let (can_params, all_bindings) = pattern::verify_all( + bump, + env.module, + DuplicatePatternContext::LambdaArgs, + parameters, + )?; let inner_env = env.add_locals(&all_bindings)?; let mut body_free_locals = FreeLocals::new(); @@ -768,7 +782,7 @@ fn canonicalize_lambda<'a>( fn canonicalize_case_branches<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, arms: &[&'a CaseArm<'a>], free_locals: &mut FreeLocals<'a>, warnings: &mut Vec>, @@ -789,13 +803,17 @@ fn canonicalize_case_branches<'a>( fn canonicalize_case_branch<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, arm: &'a CaseArm<'a>, free_locals: &mut FreeLocals<'a>, warnings: &mut Vec>, ) -> Result, Vec>> { - let (can_pattern, bindings) = - pattern::verify(bump, env, DuplicatePatternContext::CaseBranch, arm.pattern)?; + let (can_pattern, bindings) = pattern::verify( + bump, + env.module, + DuplicatePatternContext::CaseBranch, + arm.pattern, + )?; let inner_env = env.add_locals(&bindings)?; let mut body_free_locals = FreeLocals::new(); let can_body = canonicalize_expr(bump, &inner_env, arm.body, &mut body_free_locals, warnings)?; @@ -814,7 +832,7 @@ fn canonicalize_case_branch<'a>( fn canonicalize_if<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, branches: &[&'a SourceIfBranch<'a>], final_else: &'a Located>, free_locals: &mut FreeLocals<'a>, @@ -886,7 +904,7 @@ fn check_field_assigns<'a>( fn canonicalize_record<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, region: Region, fields: &[&'a FieldAssign<'a>], free_locals: &mut FreeLocals<'a>, @@ -895,9 +913,10 @@ fn canonicalize_record<'a>( let field_dict = check_field_assigns(fields)?; let mut candidates = BTreeMap::new(); for candidate in env + .module .ctors .values() - .chain(env.q_ctors.values().flat_map(|m| m.values())) + .chain(env.module.q_ctors.values().flat_map(|m| m.values())) { if let Info::Specific( _, @@ -978,7 +997,7 @@ fn canonicalize_record<'a>( fn canonicalize_update<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, record: &'a Located<&'a str>, fields: &[&'a FieldAssign<'a>], free_locals: &mut FreeLocals<'a>, @@ -1027,7 +1046,7 @@ struct ResolvedOp<'a> { fn canonicalize_binops<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, operands: &[&'a BinOpOperand<'a>], last: &'a Located>, overall_region: Region, @@ -1042,7 +1061,10 @@ fn canonicalize_binops<'a>( Ok(e) => can_exprs.push(e), Err(errs) => errors.extend(errs), } - match env.find_binop(bump, operand.op.region, operand.op.value) { + match env + .module + .find_binop(bump, operand.op.region, operand.op.value) + { Ok(binop) => ops.push(ResolvedOp { symbol: binop.symbol, home: binop.home, @@ -1143,7 +1165,7 @@ enum LetBinding<'a> { fn canonicalize_let<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, defs: &[&'a Located>], body: &'a Located>, region: Region, @@ -1373,7 +1395,7 @@ type LetDefResult<'a> = Result< fn canonicalize_let_def<'a>( bump: &'a Bump, - env: &Env<'a>, + env: &Scope<'_, 'a>, def: &SourceDef<'a>, let_bindings: &Bindings<'a>, warnings: &mut Vec>, @@ -1388,43 +1410,52 @@ fn canonicalize_let_def<'a>( // Mirrors Elm's `addDefNodes`: for typed defs the annotation is // resolved and matched against the arguments BEFORE the body is // canonicalized; either way one duplicate scope spans all args. - let (can_def_builder, arg_bindings): (DefBuilder<'a>, Bindings<'a>) = if let Some(ann) = - annotation - { - let annotation_val = types::to_annotation(bump, env, ann)?; - let annotation_val = - crate::kinds::check_annotation(bump, &env.kinds, name.value, annotation_val)?; - let mut bound: Vec<(&'a str, Region)> = Vec::new(); - let (typed_args, result_type) = - gather_typed_args(bump, env, name.value, args, annotation_val.typ, &mut bound)?; - let arg_bindings = pattern::detect_duplicates( - DuplicatePatternContext::FuncArgs(name.value), - bound, - )?; - ( - DefBuilder::Typed { - context: annotation_val.context, - annotation: annotation_val.typ, - free_vars: annotation_val.free_vars, - args: bump.alloc_slice_fill_iter(typed_args), - typ: result_type, - }, - arg_bindings, - ) - } else { - let (can_args, arg_bindings) = pattern::verify_all( - bump, - env, - DuplicatePatternContext::FuncArgs(name.value), - args, - )?; - ( - DefBuilder::Untyped { - args: bump.alloc_slice_fill_iter(can_args), - }, - arg_bindings, - ) - }; + let (can_def_builder, arg_bindings): (DefBuilder<'a>, Bindings<'a>) = + if let Some(ann) = annotation { + let annotation_val = types::to_annotation(bump, env.module, ann)?; + let annotation_val = crate::kinds::check_annotation( + bump, + &env.module.kinds, + name.value, + annotation_val, + )?; + let mut bound: Vec<(&'a str, Region)> = Vec::new(); + let (typed_args, result_type) = gather_typed_args( + bump, + env.module, + name.value, + args, + annotation_val.typ, + &mut bound, + )?; + let arg_bindings = pattern::detect_duplicates( + DuplicatePatternContext::FuncArgs(name.value), + bound, + )?; + ( + DefBuilder::Typed { + context: annotation_val.context, + annotation: annotation_val.typ, + free_vars: annotation_val.free_vars, + args: bump.alloc_slice_fill_iter(typed_args), + typ: result_type, + }, + arg_bindings, + ) + } else { + let (can_args, arg_bindings) = pattern::verify_all( + bump, + env.module, + DuplicatePatternContext::FuncArgs(name.value), + args, + )?; + ( + DefBuilder::Untyped { + args: bump.alloc_slice_fill_iter(can_args), + }, + arg_bindings, + ) + }; let body_env = env.add_locals(&arg_bindings)?; let mut body_free_locals = FreeLocals::new(); @@ -1476,7 +1507,7 @@ fn canonicalize_let_def<'a>( } SourceDef::Destruct { pattern, body } => { let (can_pattern, _) = - pattern::verify(bump, env, DuplicatePatternContext::Destruct, pattern)?; + pattern::verify(bump, env.module, DuplicatePatternContext::Destruct, pattern)?; let mut body_free_locals = FreeLocals::new(); let can_body = canonicalize_expr(bump, env, body, &mut body_free_locals, warnings)?; let deps: Vec<&'a str> = body_free_locals diff --git a/crates/nash-can/src/kinds.rs b/crates/nash-can/src/kinds.rs index d8034e30..eb3797b8 100644 --- a/crates/nash-can/src/kinds.rs +++ b/crates/nash-can/src/kinds.rs @@ -928,7 +928,7 @@ struct ContextInput<'a> { enum ContextFailure<'a> { Representation(RepresentationFailure<'a>), IrregularRecursion { - reference: GroupReference<'a>, + reference: &'a GroupReference<'a>, parameter: &'a str, }, } @@ -990,7 +990,7 @@ fn close_contexts<'a>( && !matches!(typ.value, Type::Var(_)) { return Err(ContextFailure::IrregularRecursion { - reference, + reference: bump.alloc(reference), parameter, }); } diff --git a/crates/nash-can/src/module.rs b/crates/nash-can/src/module.rs index 66c0fe8a..09cbf440 100644 --- a/crates/nash-can/src/module.rs +++ b/crates/nash-can/src/module.rs @@ -36,18 +36,6 @@ pub struct CanResult<'a> { pub warnings: Vec>, } -fn canonicalize_header<'a>( - context: Context<'a, '_>, - module: &SourceModule<'a>, -) -> Result, Error<'a>> { - let name = module.name.ok_or(Error::MissingModuleHeader)?; - - Ok(ModuleName { - package: context.package, - name: name.value, - }) -} - pub fn canonicalize<'a>( bump: &'a Bump, context: Context<'a, '_>, @@ -68,7 +56,13 @@ pub fn canonicalize<'a>( region, }]); } - let home = canonicalize_header(context, module).map_err(|e| vec![e])?; + let name = module + .name + .ok_or_else(|| vec![Error::MissingModuleHeader])?; + let home = ModuleName { + package: context.package, + name: name.value, + }; let mut env = environment::foreign::create_initial_env(bump, home, context.interfaces, module.imports)?; @@ -365,7 +359,7 @@ fn to_node_one<'a>( ) }; - let body_env = env.add_locals(&arg_bindings)?; + let body_env = crate::environment::Scope::new(env, None, &arg_bindings)?; let mut free_locals = expression::FreeLocals::new(); let can_body = expression::canonicalize_expr(bump, &body_env, src.body, &mut free_locals, warnings)?; @@ -1495,7 +1489,7 @@ mod tests { context: Context<'a, '_>, ) -> Result, Vec>> { let src = bump.alloc_str(input); - let mut parser = nash_parse::Parser::new(bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(bump, src); let module = parser.module().expect("expected successful parse"); canonicalize(bump, context, &module).map(|r| r.module) } @@ -2297,7 +2291,7 @@ mod tests { let bump = Bump::new(); let module = nash_parse::Parser::new( &bump, - b"module Main exposing (..)\nimport Builtin exposing (type bool(..))\nignore flag =\n case flag of\n False -> ()\n True -> ()\n", + "module Main exposing (..)\nimport Builtin exposing (type bool(..))\nignore flag =\n case flag of\n False -> ()\n True -> ()\n", ).module().unwrap(); let interfaces = BTreeMap::from([("Builtin", crate::kinds::builtin_interface(&bump))]); let result = canonicalize( @@ -2613,7 +2607,7 @@ mod tests { context: Context<'a, '_>, ) -> Result<(CanModule<'a>, Vec>), Vec>> { let src = bump.alloc_str(input); - let mut parser = nash_parse::Parser::new(bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(bump, src); let module = parser.module().expect("expected successful parse"); canonicalize(bump, context, &module).map(|r| (r.module, r.warnings)) } @@ -2884,6 +2878,48 @@ mod tests { ); } + #[test] + fn scope_recovery_between_case_branches() { + assert_module_error_snapshot!( + r#" + module Main exposing (..) + + f outer = + case outer of + first -> missing + second -> first + "# + ); + } + + #[test] + fn scope_recovery_after_rejected_shadowing() { + assert_module_error_snapshot!( + r#" + module Main exposing (..) + + f outer = + case outer of + outer -> outer + next -> unknown + "# + ); + } + + #[test] + fn scope_siblings_reuse_binding_names() { + assert_module_snapshot!( + r#" + module Main exposing (..) + + f outer = + case outer of + (first, value) -> value + (second, value) -> value + "# + ); + } + #[test] fn shadowing_local() { assert_module_error_snapshot!( diff --git a/crates/nash-can/src/pattern.rs b/crates/nash-can/src/pattern.rs index ffca4c35..b8ca39f8 100644 --- a/crates/nash-can/src/pattern.rs +++ b/crates/nash-can/src/pattern.rs @@ -439,7 +439,7 @@ mod tests { fn env_with_bool<'a>(bump: &'a Bump) -> Env<'a> { let module = nash_parse::Parser::new( bump, - b"module Main exposing (..)\nimport Builtin exposing (type bool(..))\n", + "module Main exposing (..)\nimport Builtin exposing (type bool(..))\n", ) .module() .unwrap(); @@ -456,7 +456,7 @@ mod tests { fn parse_pattern<'a>(bump: &'a Bump, input: &str) -> &'a Located> { let src = bump.alloc_str(input); - let mut parser = nash_parse::Parser::new(bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(bump, src); let (pat, _end) = parser.pattern_expr().expect("expected successful parse"); pat } diff --git a/crates/nash-can/src/snapshots/nash_can__module__tests__scope_recovery_after_rejected_shadowing.snap b/crates/nash-can/src/snapshots/nash_can__module__tests__scope_recovery_after_rejected_shadowing.snap new file mode 100644 index 00000000..85402dad --- /dev/null +++ b/crates/nash-can/src/snapshots/nash_can__module__tests__scope_recovery_after_rejected_shadowing.snap @@ -0,0 +1,51 @@ +--- +source: crates/nash-can/src/module.rs +description: "Code:\n\nmodule Main exposing (..)\n\nf outer =\n case outer of\n outer -> outer\n next -> unknown\n" +--- +[ + Shadowing { + name: "outer", + original: Region { + start: Position { + line: 3, + column: 3, + }, + end: Position { + line: 3, + column: 8, + }, + }, + new: Region { + start: Position { + line: 5, + column: 9, + }, + end: Position { + line: 5, + column: 14, + }, + }, + }, + NotFoundVar { + region: Region { + start: Position { + line: 6, + column: 17, + }, + end: Position { + line: 6, + column: 24, + }, + }, + prefix: None, + name: "unknown", + suggestions: PossibleNames { + locals: [ + "f", + "next", + "outer", + ], + qualified: [], + }, + }, +] diff --git a/crates/nash-can/src/snapshots/nash_can__module__tests__scope_recovery_between_case_branches.snap b/crates/nash-can/src/snapshots/nash_can__module__tests__scope_recovery_between_case_branches.snap new file mode 100644 index 00000000..7da82df3 --- /dev/null +++ b/crates/nash-can/src/snapshots/nash_can__module__tests__scope_recovery_between_case_branches.snap @@ -0,0 +1,50 @@ +--- +source: crates/nash-can/src/module.rs +description: "Code:\n\nmodule Main exposing (..)\n\nf outer =\n case outer of\n first -> missing\n second -> first\n" +--- +[ + NotFoundVar { + region: Region { + start: Position { + line: 5, + column: 18, + }, + end: Position { + line: 5, + column: 25, + }, + }, + prefix: None, + name: "missing", + suggestions: PossibleNames { + locals: [ + "f", + "first", + "outer", + ], + qualified: [], + }, + }, + NotFoundVar { + region: Region { + start: Position { + line: 6, + column: 19, + }, + end: Position { + line: 6, + column: 24, + }, + }, + prefix: None, + name: "first", + suggestions: PossibleNames { + locals: [ + "f", + "outer", + "second", + ], + qualified: [], + }, + }, +] diff --git a/crates/nash-can/src/snapshots/nash_can__module__tests__scope_siblings_reuse_binding_names.snap b/crates/nash-can/src/snapshots/nash_can__module__tests__scope_siblings_reuse_binding_names.snap new file mode 100644 index 00000000..6d8e62c6 --- /dev/null +++ b/crates/nash-can/src/snapshots/nash_can__module__tests__scope_siblings_reuse_binding_names.snap @@ -0,0 +1,230 @@ +--- +source: crates/nash-can/src/module.rs +description: "Code:\n\nmodule Main exposing (..)\n\nf outer =\n case outer of\n (first, value) -> value\n (second, value) -> value\n" +--- +Module { + traits: [], + impls: [], + kind: Normal, + name: ModuleName { + package: None, + name: "Main", + }, + exports: Everything( + Region { + start: Position { + line: 1, + column: 22, + }, + end: Position { + line: 1, + column: 26, + }, + }, + ), + docs: NoDocs( + Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 7, + column: 1, + }, + }, + ), + decls: Declare { + definition: Def { + name: Located { + region: Region { + start: Position { + line: 3, + column: 1, + }, + end: Position { + line: 3, + column: 2, + }, + }, + value: "f", + }, + args: [ + Located { + region: Region { + start: Position { + line: 3, + column: 3, + }, + end: Position { + line: 3, + column: 8, + }, + }, + value: Var( + "outer", + ), + }, + ], + body: Located { + region: Region { + start: Position { + line: 4, + column: 5, + }, + end: Position { + line: 7, + column: 1, + }, + }, + value: Case { + scrutinee: Located { + region: Region { + start: Position { + line: 4, + column: 10, + }, + end: Position { + line: 4, + column: 15, + }, + }, + value: VarLocal( + "outer", + ), + }, + branches: [ + CaseBranch { + pattern: Located { + region: Region { + start: Position { + line: 5, + column: 9, + }, + end: Position { + line: 5, + column: 23, + }, + }, + value: Tuple { + first: Located { + region: Region { + start: Position { + line: 5, + column: 10, + }, + end: Position { + line: 5, + column: 15, + }, + }, + value: Var( + "first", + ), + }, + second: Located { + region: Region { + start: Position { + line: 5, + column: 17, + }, + end: Position { + line: 5, + column: 22, + }, + }, + value: Var( + "value", + ), + }, + rest: [], + }, + }, + body: Located { + region: Region { + start: Position { + line: 5, + column: 27, + }, + end: Position { + line: 5, + column: 32, + }, + }, + value: VarLocal( + "value", + ), + }, + }, + CaseBranch { + pattern: Located { + region: Region { + start: Position { + line: 6, + column: 9, + }, + end: Position { + line: 6, + column: 24, + }, + }, + value: Tuple { + first: Located { + region: Region { + start: Position { + line: 6, + column: 10, + }, + end: Position { + line: 6, + column: 16, + }, + }, + value: Var( + "second", + ), + }, + second: Located { + region: Region { + start: Position { + line: 6, + column: 18, + }, + end: Position { + line: 6, + column: 23, + }, + }, + value: Var( + "value", + ), + }, + rest: [], + }, + }, + body: Located { + region: Region { + start: Position { + line: 6, + column: 28, + }, + end: Position { + line: 6, + column: 33, + }, + }, + value: VarLocal( + "value", + ), + }, + }, + ], + }, + }, + }, + next: Empty, + }, + unions: [], + aliases: [], + binops: [], +} diff --git a/crates/nash-can/src/types.rs b/crates/nash-can/src/types.rs index 31218a0d..43a010d1 100644 --- a/crates/nash-can/src/types.rs +++ b/crates/nash-can/src/types.rs @@ -856,7 +856,7 @@ mod tests { fn parse_type<'a>(bump: &'a Bump, input: &str) -> &'a Located> { let src = bump.alloc_str(input); - let mut parser = nash_parse::Parser::new(bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(bump, src); let (typ, _end) = parser.type_expr().expect("expected successful parse"); typ } @@ -1028,7 +1028,7 @@ mod context_tests { let source = bump.alloc_str(&format!( "module Main exposing (..)\n\nf : {annotation}\nf x = x\n" )); - let mut parser = nash_parse::Parser::new(bump, source.as_bytes()); + let mut parser = nash_parse::Parser::new(bump, source); parser.module().unwrap().values[0].value.annotation.unwrap() } diff --git a/crates/nash-can/tests/core_casts.rs b/crates/nash-can/tests/core_casts.rs index f6a63ff9..79ccc57f 100644 --- a/crates/nash-can/tests/core_casts.rs +++ b/crates/nash-can/tests/core_casts.rs @@ -36,9 +36,7 @@ fn casts_require_exact_core_package_for_every_import_route() { let source = bump.alloc_str(&format!( "module Main exposing (..)\n{import}\nlift : int -> Int\nlift = {reference}\n" )); - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let interfaces = std::collections::BTreeMap::from([( "Builtin", nash_can::kinds::builtin_interface(&bump), diff --git a/crates/nash-can/tests/do_notation.rs b/crates/nash-can/tests/do_notation.rs index 3ceab145..9615ca81 100644 --- a/crates/nash-can/tests/do_notation.rs +++ b/crates/nash-can/tests/do_notation.rs @@ -2,7 +2,7 @@ use bumpalo::Bump; use indoc::indoc; fn monad(bump: &Bump, core: bool) -> nash_can::Interface<'_> { - let module = nash_parse::Parser::new(bump, b"module Monad exposing (Monad)\ntrait Monad 'm where\n bind : 'm 'a -> ('a -> 'm 'b) -> 'm 'b\n").module().unwrap(); + let module = nash_parse::Parser::new(bump, "module Monad exposing (Monad)\ntrait Monad 'm where\n bind : 'm 'a -> ('a -> 'm 'b) -> 'm 'b\n").module().unwrap(); let canonical = nash_can::canonicalize( bump, nash_can::Context { @@ -34,9 +34,7 @@ fn do_scopes_statements_and_uses_the_core_method() { x "# ); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let canonical = nash_can::canonicalize( &bump, nash_can::Context { @@ -62,9 +60,7 @@ fn do_rejects_missing_core_and_refutable_patterns() { ] { let interfaces = std::collections::BTreeMap::from([("Monad", monad(&bump, core))]); let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Monad\ntype option 'a = Some 'a\nrun m = do\n {pattern} <- {rhs}\n m\n")); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let errors = nash_can::canonicalize( &bump, nash_can::Context { diff --git a/crates/nash-can/tests/impls.rs b/crates/nash-can/tests/impls.rs index 896d577b..f98a3cab 100644 --- a/crates/nash-can/tests/impls.rs +++ b/crates/nash-can/tests/impls.rs @@ -82,9 +82,7 @@ fn impl_cannot_own_an_imported_trait_and_imported_heads() { lower x = x " ); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, nash_can::Context { @@ -141,9 +139,7 @@ fn explicit_lift_impls_cannot_overlap_the_big_reflexive_rule() { ("type alias Alias 'a = 'a", "(Alias 'a) (Alias 'b)"), ] { let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Lift exposing (Lift)\n{declaration}\nimpl Lift {heads} where\n lift x = x\n lower x = x\n")); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); results.push( nash_can::canonicalize( &bump, @@ -173,7 +169,7 @@ fn explicit_lift_impls_cannot_overlap_the_big_reflexive_rule() { } fn core_lift<'a>(bump: &'a Bump) -> nash_can::Interface<'a> { - let module = nash_parse::Parser::new(bump, b"module Lift exposing (Lift)\ntrait Lift 'small 'big where\n lift : 'small -> 'big\n lower : 'big -> 'small\n").module().unwrap(); + let module = nash_parse::Parser::new(bump, "module Lift exposing (Lift)\ntrait Lift 'small 'big where\n lift : 'small -> 'big\n lower : 'big -> 'small\n").module().unwrap(); let result = nash_can::canonicalize( bump, nash_can::Context { @@ -197,9 +193,7 @@ fn reflexive_lift_proves_big_without_narrowing_rigid_variables() { ("container", "(List 'a)"), ] { let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Lift exposing (Lift)\ntype {container} 'a = Wrap 'a\ntrait Tag 'a where\n tag : 'a -> 'a\ntrait Tag 'a => Top 'a where\n top : 'a -> 'a\nimpl Lift {lifted} {lifted} => Tag ({container} 'a) where\n tag x = x\nimpl Top ({container} 'a) where\n top x = x\n")); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); results.push( nash_can::canonicalize( &bump, @@ -225,9 +219,7 @@ fn reflexive_lift_accepts_big_but_not_const() { let mut results = Vec::new(); for head in ["Color", "()"] { let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Lift exposing (Lift)\ntype Color = Red\ntrait Lift 'a 'a => RoundTrip 'a where\n roundTrip : 'a -> 'a\nimpl RoundTrip {head} where\n roundTrip x = x\n")); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); results.push( nash_can::canonicalize( &bump, @@ -262,9 +254,7 @@ fn reflexive_lift_requires_the_exact_core_trait_identity() { ), ] { let source = bump.alloc_str(&format!("module {module_name} exposing (..)\ntrait Lift 'small 'big where\n lift : 'small -> 'big\n lower : 'big -> 'small\n")); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, nash_can::Context { @@ -386,9 +376,7 @@ fn superclass_impl_is_available_from_an_interface() { compare x = x " ); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, nash_can::Context { @@ -540,9 +528,7 @@ fn global_overlap_between_core_modules() { ("Second", "(list int, list int)"), ] { let source = bump.alloc_str(&format!("module {name} exposing (..)\nimport Keep exposing (Keep)\nimpl Keep {head} where\n keep x = x\n")); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, nash_can::Context { @@ -559,7 +545,7 @@ fn global_overlap_between_core_modules() { } let mut all_interfaces = interfaces.clone(); all_interfaces.extend(compiled); - let module = nash_parse::Parser::new(&bump, b"module Main exposing (..)\n") + let module = nash_parse::Parser::new(&bump, "module Main exposing (..)\n") .module() .unwrap(); let result = nash_can::canonicalize( @@ -597,9 +583,7 @@ fn global_impl_metadata_is_available_without_imports() { }; let interfaces = std::collections::BTreeMap::from([("Instances", interface)]); let source = "module Main exposing (..)\n"; - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, nash_can::Context { @@ -641,9 +625,7 @@ fn unit_and_tuple_impls_belong_to_core() { keep x = x " ); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let ordinary = nash_can::canonicalize( &bump, nash_can::Context { @@ -856,9 +838,7 @@ fn canonicalize<'a>( source: &str, ) -> Result, Vec>> { let source = bump.alloc_str(source); - let module = nash_parse::Parser::new(bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(bump, source).module().unwrap(); nash_can::canonicalize(bump, nash_can::Context::default(), &module) } diff --git a/crates/nash-can/tests/kind_predicates.rs b/crates/nash-can/tests/kind_predicates.rs index 67a52407..5d814510 100644 --- a/crates/nash-can/tests/kind_predicates.rs +++ b/crates/nash-can/tests/kind_predicates.rs @@ -8,7 +8,7 @@ fn check<'a>(bump: &'a Bump, body: &str) -> Result, Vec< let source = bump.alloc_str(&format!( "module Main exposing (..)\n\nimport Builtin exposing (..)\n\n{body}\n" )); - let module = nash_parse::Parser::new(bump, source.as_bytes()) + let module = nash_parse::Parser::new(bump, source) .module() .expect("fixture parses"); let interfaces = BTreeMap::from([("Builtin", nash_can::kinds::builtin_interface(bump))]); @@ -221,9 +221,7 @@ fn imported<'a>(bump: &'a Bump, body: &str) -> Result, V let builtin = nash_can::kinds::builtin_interface(bump); let interfaces = BTreeMap::from([("Builtin", builtin)]); let source = bump.alloc_str("module Types exposing (..)\nimport Builtin exposing (..)\ntype Box 'a = Box 'a\ntype wrap 'f 'a = Wrap ('f 'a)\ntype option 'a = None | Some 'a\ntype alias count = int\n"); - let parsed = nash_parse::Parser::new(bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(bump, source).module().unwrap(); let checked = nash_can::canonicalize( bump, Context { @@ -236,9 +234,7 @@ fn imported<'a>(bump: &'a Bump, body: &str) -> Result, V let interface = nash_can::from_module(bump, &checked.module, &BTreeMap::new()); let interfaces = BTreeMap::from([("Builtin", builtin), ("Types", interface)]); let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Builtin exposing (..)\nimport Types exposing (..)\n{body}\n")); - let parsed = nash_parse::Parser::new(bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(bump, source).module().unwrap(); nash_can::canonicalize( bump, Context { diff --git a/crates/nash-can/tests/kinds.rs b/crates/nash-can/tests/kinds.rs index 2a113cdc..48c22b0c 100644 --- a/crates/nash-can/tests/kinds.rs +++ b/crates/nash-can/tests/kinds.rs @@ -7,7 +7,7 @@ macro_rules! assert_kinds_snapshot { ($source:expr) => {{ let bump = Bump::new(); let source = bump.alloc_str(&format!("module Main exposing (..)\n\nimport Builtin exposing (..)\n\n{}\n", $source)); - let module = nash_parse::Parser::new(&bump, source.as_bytes()).module().expect("source parses"); + let module = nash_parse::Parser::new(&bump, source).module().expect("source parses"); let interfaces = BTreeMap::from([("Builtin", nash_can::kinds::builtin_interface(&bump))]); let result = canonicalize(&bump, Context { package: None, interfaces: Some(&interfaces) }, &module).expect("kind checking succeeds"); let unions: Vec<_> = result.module.unions.iter().map(|u| (u.value.name.value, u.value.kind, u.value.context)).collect(); @@ -22,7 +22,7 @@ macro_rules! assert_kind_error_snapshot { ($source:expr, $expected:pat) => {{ let bump = Bump::new(); let source = bump.alloc_str(&format!("module Main exposing (..)\n\nimport Builtin exposing (..)\n\n{}\n", $source)); - let module = nash_parse::Parser::new(&bump, source.as_bytes()).module().expect("source parses"); + let module = nash_parse::Parser::new(&bump, source).module().expect("source parses"); let interfaces = BTreeMap::from([("Builtin", nash_can::kinds::builtin_interface(&bump))]); let errors = canonicalize(&bump, Context { package: None, interfaces: Some(&interfaces) }, &module).expect_err("declaration or annotation checking fails"); assert!(errors.iter().all(|error| matches!(error, $expected)), "wrong diagnostic: {errors:?}"); @@ -262,9 +262,7 @@ fn named_constructor_arity_remains_a_canonicalization_error() { let source = bump.alloc_str( "module Main exposing (..)\n\nimport Builtin exposing (..)\n\ntype alias x = int Int\n", ); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let interfaces = BTreeMap::from([("Builtin", nash_can::kinds::builtin_interface(&bump))]); let errors = canonicalize( &bump, @@ -322,9 +320,7 @@ fn alias_substitution_preserves_application_head_and_argument() { fn applied_head_is_a_free_variable() { let bump = Bump::new(); let source = bump.alloc_str("module Main exposing (..)\n\ntype wrap 'a = Wrap ('f 'a)\n"); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let errors = canonicalize(&bump, Context::default(), &module).unwrap_err(); assert!(matches!( errors.as_slice(), @@ -338,9 +334,7 @@ fn annotation_storable_parameter() { assert_kinds_snapshot!("f : 'a -> list 'a -> list 'a\nf x xs = xs"); let bump = Bump::new(); let source = "module Main exposing (..)\nimport Builtin exposing (..)\nf : 'a -> list 'a -> list 'a\nf x xs = xs\n"; - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let interfaces = BTreeMap::from([("Builtin", nash_can::kinds::builtin_interface(&bump))]); let result = canonicalize( &bump, @@ -413,7 +407,7 @@ fn imported_interfaces_retain_higher_kinded_types() { let interface = { let source_arena = &destination; let source = source_arena.alloc_str("module Shapes exposing (type wrap(..), type applied)\n\ntype wrap 'f 'a = Wrap ('f 'a)\ntype alias applied 'f 'a = 'f 'a\n"); - let module = nash_parse::Parser::new(source_arena, source.as_bytes()) + let module = nash_parse::Parser::new(source_arena, source) .module() .unwrap(); let canonical = canonicalize(source_arena, Context::default(), &module).unwrap(); @@ -434,7 +428,7 @@ fn imported_interfaces_retain_higher_kinded_types() { assert!(matches!(interface.aliases[0].typ.value, Type::App { .. })); let interfaces = BTreeMap::from([("Shapes", interface)]); let source = destination.alloc_str("module Main exposing (..)\n\nimport Shapes exposing (type wrap)\n\ntype holder 'f 'a = Holder (wrap 'f 'a)\n"); - let module = nash_parse::Parser::new(&destination, source.as_bytes()) + let module = nash_parse::Parser::new(&destination, source) .module() .unwrap(); let canonical = canonicalize( @@ -569,9 +563,7 @@ fn annotation_checks_alias_contract_before_argument_splitting() { }; let interfaces = BTreeMap::from([("Restricted", interface)]); let source = bump.alloc_str("module Main exposing (..)\n\nimport Restricted exposing (type restricted)\n\nf : restricted ()\nf x = x\n"); - let module = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let module = nash_parse::Parser::new(&bump, source).module().unwrap(); let errors = canonicalize( &bump, Context { @@ -782,7 +774,7 @@ macro_rules! self_application_cases { .split("```").nth(2 * $number - 1).expect("supplied case").trim(); let bump = Bump::new(); let source = bump.alloc_str(source); - let module = nash_parse::Parser::new(&bump, source.as_bytes()).module().expect("source parses"); + let module = nash_parse::Parser::new(&bump, source).module().expect("source parses"); let result = canonicalize(&bump, Context { package: None, interfaces: None }, &module); let errors = result.expect_err("self application fails the H98 occurs check"); assert!(errors.iter().all(|error| matches!(error, Error::KindInfinite { .. })), "declaration-time occurs check: {errors:?}"); diff --git a/crates/nash-can/tests/structural_eq.rs b/crates/nash-can/tests/structural_eq.rs index 73720fe5..9404d70c 100644 --- a/crates/nash-can/tests/structural_eq.rs +++ b/crates/nash-can/tests/structural_eq.rs @@ -19,9 +19,7 @@ fn structural_eq_rejects_big_overrides_only_for_exact_core_trait() { nash_can::kinds::builtin_interface(&bump), )]); let source = "module Eq exposing (Eq)\ntrait Eq 'a where\n eq : 'a -> 'a -> bool\n"; - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let canonical = nash_can::canonicalize( &bump, Context { @@ -37,9 +35,7 @@ fn structural_eq_rejects_big_overrides_only_for_exact_core_trait() { nash_can::from_module(&bump, &canonical.module, &Default::default()), ); let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Eq exposing (Eq)\nimport Builtin\ntype Token = Token Int\ntype alias Box = {{ item : Int }}\ntype alias Alias 'a = 'a\ntype alias Applied 'f 'a = 'f 'a\nimpl Eq {head} where\n eq _ _ = Builtin.True\n")); - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, Context { diff --git a/crates/nash-can/tests/traits.rs b/crates/nash-can/tests/traits.rs index 4f5e3a4a..ef7e4d60 100644 --- a/crates/nash-can/tests/traits.rs +++ b/crates/nash-can/tests/traits.rs @@ -3,11 +3,7 @@ use indoc::indoc; fn parse<'a>(bump: &'a Bump, source: &str) -> &'a nash_source::Module<'a> { let source = bump.alloc_str(source); - bump.alloc( - nash_parse::Parser::new(bump, source.as_bytes()) - .module() - .unwrap(), - ) + bump.alloc(nash_parse::Parser::new(bump, source).module().unwrap()) } #[test] diff --git a/crates/nash-can/tests/twins.rs b/crates/nash-can/tests/twins.rs index 798c5472..0ca80f47 100644 --- a/crates/nash-can/tests/twins.rs +++ b/crates/nash-can/tests/twins.rs @@ -11,17 +11,13 @@ fn twin_imports_preserve_privacy_and_explicit_exposure() { ] { let bump = Bump::new(); let source = bump.alloc_str(&format!("module Status exposing ({exports})\ntype status = Ready | Waiting\ntype Status = Ready | Waiting\n")); - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let canonical = nash_can::canonicalize(&bump, Context::default(), &parsed).unwrap(); let interface = nash_can::from_module(&bump, &canonical.module, &Default::default()); let interfaces = std::collections::BTreeMap::from([("Status", interface)]); for (constructor, expected) in [("Ready", bare), ("S.Ready", qualified)] { let source = bump.alloc_str(&format!("module Main exposing (..)\nimport Status as S exposing ({imports})\nvalue = {constructor}\n")); - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let result = nash_can::canonicalize( &bump, Context { @@ -63,9 +59,7 @@ fn twin_exception_rejects_unrelated_and_malformed_duplicates() { ] { let bump = Bump::new(); let source = bump.alloc_str(&format!("module Status exposing (..)\n{declarations}")); - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let errors = nash_can::canonicalize(&bump, Context::default(), &parsed).unwrap_err(); assert!( errors diff --git a/crates/nash-cli/tests/diagnostics.rs b/crates/nash-cli/tests/diagnostics.rs index 6bce20c5..8bdca5c9 100644 --- a/crates/nash-cli/tests/diagnostics.rs +++ b/crates/nash-cli/tests/diagnostics.rs @@ -56,7 +56,7 @@ fn terminal_and_json_type_mismatch() { assert!(human.stdout.is_empty()); let text = normalized(&human.stderr, &root); assert!(!text.contains('\u{1b}')); - assert!(text.contains("TYPE MISMATCH")); + assert!(text.contains("nash::type::mismatch")); insta::assert_snapshot!("type_mismatch_terminal", text); let json = check(&root, &["--report=json"]); assert_eq!(json.status.code(), Some(1)); @@ -118,7 +118,7 @@ fn independent_errors_are_stable_and_dependents_are_blocked() { assert_eq!(modules[0]["problems"].as_array().unwrap().len(), 2); let human = check(&project.0, &[]); let text = String::from_utf8(human.stderr).unwrap(); - assert_eq!(text.matches("NAMING ERROR").count(), 3); + assert_eq!(text.matches("nash::names::not_found_var").count(), 3); assert!(text.contains("Skipped")); assert!(!text.contains("IMPORT PROBLEM")); } @@ -142,12 +142,12 @@ fn documented_examples_run_through_the_real_core_package() { let human = check(&root, &["--no-warnings"]); assert_eq!(human.status.code(), Some(1)); let text = normalized(&human.stderr, &root); - assert!(text.contains("This `map` call produces:"), "{text}"); + assert!(text.contains("found `list Int`"), "{text}"); let docs = include_str!("../../../docs/diagnostics.md"); let names = [ - "TYPE MISMATCH", - "MISSING IMPL", - "MISSING PATTERNS", + "nash::type::mismatch", + "nash::type::missing_impl", + "nash::pattern::incomplete", "Compilation failed:", ]; for pair in names.windows(2) { @@ -229,7 +229,7 @@ async fn mixed_errors_match_across_terminal_json_and_lsp() { let text = String::from_utf8(human.stderr).unwrap(); let mut previous = 0; for problem in problems { - let title = problem["title"].as_str().unwrap(); + let title = problem["code"].as_str().unwrap(); assert_eq!(text.matches(&format!("{title}\n")).count(), 1); let position = text.find(&format!("{title}\n")).unwrap(); assert!(position >= previous); @@ -269,7 +269,7 @@ async fn mixed_errors_match_across_terminal_json_and_lsp() { "{rendered}\nExpected {location}" ); let lsp = nash_language_server::diagnostics::to_lsp(report, &source, &uri); - assert_eq!(serde_json::to_value(&lsp).unwrap()["code"], json["title"]); + assert_eq!(serde_json::to_value(&lsp).unwrap()["code"], json["code"]); assert_eq!( u64::from(lsp.range.start.line) + 1, json["region"]["start"]["line"] @@ -315,3 +315,87 @@ fn poisoned_tuple_child_keeps_independent_type_mismatch() { ) ); } + +#[test] +fn type_expectation_origins_reach_json_and_terminal() { + let project = Project::new(&[ + ( + "Annotation", + "module Annotation exposing (..)\nvalue :\n ()\nvalue = ((), ())\n", + ), + ( + "Elements", + "module Elements exposing (..)\nvalue = [(), ((), ())]\n", + ), + ( + "Branches", + "module Branches exposing (..)\nvalue flag = if flag then () else ((), ())\n", + ), + ( + "Cases", + "module Cases exposing (..)\nvalue x =\n case x of\n () -> ()\n _ -> ((), ())\n", + ), + ]); + let output = check(&project.0, &["--report=json"]); + assert_eq!(output.status.code(), Some(1)); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let modules = json["errors"].as_array().unwrap(); + assert_eq!(modules.len(), 4, "{json}"); + for (name, line, column, text) in [ + ("Annotation", 3, 5, "declared type"), + ("Elements", 2, 10, "previous list element"), + ("Branches", 2, 27, "previous branch"), + ("Cases", 4, 15, "previous branch"), + ] { + let module = modules + .iter() + .find(|module| module["name"] == name) + .unwrap(); + let problems = module["problems"].as_array().unwrap(); + assert_eq!(problems.len(), 1, "{module}"); + let labels = problems[0]["labels"].as_array().unwrap(); + let origin = labels + .iter() + .find(|label| label["text"] == text) + .expect("origin label"); + assert_eq!(origin["primary"], false); + assert_eq!( + origin["region"]["start"], + serde_json::json!({"line": line, "column": column}) + ); + } + let output = check(&project.0, &[]); + let text = String::from_utf8(output.stderr).unwrap(); + for label in ["declared type", "previous list element", "previous branch"] { + assert!(text.contains(label), "{text}"); + } +} + +#[test] +fn imported_function_alias_labels_the_local_annotation() { + let project = Project::new(&[ + ( + "Types", + "module Types exposing (type callback)\n\n\n\ntype alias callback = () -> ()\n", + ), + ( + "Main", + "module Main exposing (..)\nimport Types exposing (type callback)\nf : callback\nf (x, y) = ()\n", + ), + ]); + let output = check(&project.0, &["--report=json", "--no-warnings"]); + assert_eq!(output.status.code(), Some(1)); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let problem = &json["errors"][0]["problems"][0]; + assert_eq!(problem["code"], "nash::type::pattern_mismatch"); + let origin = problem["labels"] + .as_array() + .unwrap() + .iter() + .find(|label| label["text"] == "declared argument type") + .unwrap(); + assert_eq!( + origin["region"]["start"], + serde_json::json!({"line": 3, "column": 5}) + ); +} diff --git a/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap index 0fd54f2c..a23e5ece 100644 --- a/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap +++ b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap @@ -9,36 +9,53 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "path": "/app/src/Ledger.nash", "problems": [ { - "message": [ - "Something is off with the body of the `settle` definition:\n\n11| map balanceOf accounts\n ", + "code": "nash::type::mismatch", + "labels": [ { - "bold": false, - "color": "RED", - "string": "^^^^^^^^^^^^^^^^^^^^^^", - "underline": false + "primary": true, + "region": { + "end": { + "column": 27, + "line": 11 + }, + "start": { + "column": 5, + "line": 11 + } + }, + "text": "body of `settle`" }, - "\nThis `map` call produces:\n\n list ", { - "bold": false, - "color": "yellow", - "string": "Int", - "underline": false - }, - "\n\nBut the type annotation on `settle` says it should be:\n\n list ", + "primary": false, + "region": { + "end": { + "column": 34, + "line": 9 + }, + "start": { + "column": 10, + "line": 9 + } + }, + "text": "declared type" + } + ], + "message": [ + "Type mismatch: expected `list ", { "bold": false, "color": "yellow", "string": "int", "underline": false }, - "\n\n", + "`, found `list ", { "bold": false, - "color": null, - "string": "Hint", - "underline": true + "color": "yellow", + "string": "Int", + "underline": false }, - ": `Int` is the Big (Data) type and `int` is the little type. They never\nconvert implicitly. Where an appropriate `Lift` impl is available, use `lower`\nto go from `Int` to `int`, or `lift` to go the other way." + "`.\n\nUse `lower` to convert `Int` to `int` where a `Lift` impl is available." ], "region": { "end": { @@ -50,6 +67,9 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "line": 11 } }, + "related": [], + "severity": "error", + "suggestions": [], "title": "TYPE MISMATCH" } ] @@ -59,22 +79,25 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "path": "/app/src/Steps.nash", "problems": [ { - "message": [ - "I cannot find an `Eq` impl for `step`:\n\n8| isDone s = s == Done\n ", + "code": "nash::type::missing_impl", + "labels": [ { - "bold": false, - "color": "RED", - "string": "^^^^^^^^^", - "underline": false - }, - "\nThe (==) operator needs its arguments to implement `Eq`, and here they are:\n\n step\n\nBut there is no `impl Eq step` in this module or in any import.\n\n`Eq` is implemented for these heads:\n\n bool\n bytes\n int\n (list 'a0)\n\n", - { - "bold": false, - "color": null, - "string": "Hint", - "underline": true - }, - ": This local datatype is a candidate for `@derive(Eq)`, but automatic\nderiving is not available yet. Write the impl by hand:\n\n impl Eq step where\n eq a b = ..." + "primary": true, + "region": { + "end": { + "column": 21, + "line": 8 + }, + "start": { + "column": 12, + "line": 8 + } + }, + "text": "required by `==`" + } + ], + "message": [ + "No impl for `Eq step`.\n\nAvailable impl heads:\n\n bool\n bytes\n int\n (list 'a0)\n\n…\n\nImport or define an impl for `Eq step`." ], "region": { "end": { @@ -86,6 +109,9 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "line": 8 } }, + "related": [], + "severity": "error", + "suggestions": [], "title": "MISSING IMPL" } ] @@ -95,36 +121,25 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "path": "/app/src/Tag.nash", "problems": [ { - "message": [ - "This `case` does not have branches for all possibilities:\n\n 7|", - { - "bold": false, - "color": "RED", - "string": ">", - "underline": false - }, - " case d of\n 8|", + "code": "nash::pattern::incomplete", + "labels": [ { - "bold": false, - "color": "RED", - "string": ">", - "underline": false - }, - " Constr n _ -> n\n 9|", - { - "bold": false, - "color": "RED", - "string": ">", - "underline": false - }, - " List _ -> 0\n10|", - { - "bold": false, - "color": "RED", - "string": ">", - "underline": false - }, - "\n\nMissing possibilities include:\n\n ", + "primary": true, + "region": { + "end": { + "column": 1, + "line": 10 + }, + "start": { + "column": 5, + "line": 7 + } + }, + "text": "" + } + ], + "message": [ + "Case expression is not exhaustive.\n\nMissing patterns:\n\n ", { "bold": false, "color": "yellow", @@ -145,14 +160,7 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "string": "B _", "underline": false }, - "\n\nI would have to crash if I saw one of those. Add branches for them!\n\n", - { - "bold": false, - "color": null, - "string": "Hint", - "underline": true - }, - ": If you want to write the code for each branch later, use `todo` as a\nplaceholder. Read for more\nguidance on this workflow." + "\n\nAdd the missing branches; use `todo` for unfinished bodies." ], "region": { "end": { @@ -164,6 +172,9 @@ expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), "line": 7 } }, + "related": [], + "severity": "error", + "suggestions": [], "title": "MISSING PATTERNS" } ] diff --git a/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap index b0c27da8..0ed2a801 100644 --- a/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap +++ b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap @@ -2,72 +2,56 @@ source: crates/nash-cli/tests/diagnostics.rs expression: text --- -TYPE MISMATCH +nash::type::mismatch - × Something is off with the body of the `settle` definition: + × Type mismatch: expected `list int`, found `list Int`. ╭─[/app/src/Ledger.nash:11:5] + 8 │ + 9 │ settle : list Account -> list int + · ────────────┬─────────── + · ╰── declared type 10 │ settle accounts = 11 │ map balanceOf accounts - · ────────────────────── + · ───────────┬────────── + · ╰── body of `settle` ╰──── - help: This `map` call produces: + help: Use `lower` to convert `Int` to `int` where a `Lift` impl is available. - list Int +nash::type::missing_impl - But the type annotation on `settle` says it should be: - - list int - - Hint: `Int` is the Big (Data) type and `int` is the little type. They never - convert implicitly. Where an appropriate `Lift` impl is available, use `lower` - to go from `Int` to `int`, or `lift` to go the other way. - -MISSING IMPL - - × I cannot find an `Eq` impl for `step`: + × No impl for `Eq step`. ╭─[/app/src/Steps.nash:8:12] 7 │ isDone : step -> bool 8 │ isDone s = s == Done - · ───────── + · ────┬──── + · ╰── required by `==` ╰──── - help: The (==) operator needs its arguments to implement `Eq`, and here they are: - - step - - But there is no `impl Eq step` in this module or in any import. - - `Eq` is implemented for these heads: + help: Available impl heads: bool bytes int (list 'a0) - Hint: This local datatype is a candidate for `@derive(Eq)`, but automatic - deriving is not available yet. Write the impl by hand: + … - impl Eq step where - eq a b = ... + Import or define an impl for `Eq step`. -MISSING PATTERNS +nash::pattern::incomplete - × This `case` does not have branches for all possibilities: + × Case expression is not exhaustive. ╭─[/app/src/Tag.nash:7:5] 6 │ tag d = 7 │ ╭─▶ case d of 8 │ │ Constr n _ -> n 9 │ ╰─▶ List _ -> 0 ╰──── - help: Missing possibilities include: + help: Missing patterns: Map _ I _ B _ - I would have to crash if I saw one of those. Add branches for them! - - Hint: If you want to write the code for each branch later, use `todo` as a - placeholder. Read for more - guidance on this workflow. + Add the missing branches; use `todo` for unfinished bodies. Compilation failed: 22 succeeded, 3 failed or blocked. diff --git a/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_json.snap b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_json.snap index 2b420705..b147e4ef 100644 --- a/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_json.snap +++ b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_json.snap @@ -9,29 +9,53 @@ expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes() "path": "/src/Main.nash", "problems": [ { - "message": [ - "Something is off with the body of the `bad` definition:\n\n3| bad = (().field, \\x -> x)\n ", + "code": "nash::type::mismatch", + "labels": [ { - "bold": false, - "color": "RED", - "string": "^^^^^^^^^^^^^^^^^^^", - "underline": false + "primary": true, + "region": { + "end": { + "column": 26, + "line": 3 + }, + "start": { + "column": 7, + "line": 3 + } + }, + "text": "body of `bad`" }, - "\nThe body is a tuple of type:\n\n ( ?, ", + { + "primary": false, + "region": { + "end": { + "column": 15, + "line": 2 + }, + "start": { + "column": 7, + "line": 2 + } + }, + "text": "declared type" + } + ], + "message": [ + "Type mismatch: expected `( ?, ", { "bold": false, "color": "yellow", - "string": "'a -> 'a", + "string": "unit", "underline": false }, - " )\n\nBut the type annotation on `bad` says it should be:\n\n ( ?, ", + " )`, found `( ?, ", { "bold": false, "color": "yellow", - "string": "unit", + "string": "'a -> 'a", "underline": false }, - " )" + " )`.\n\n" ], "region": { "end": { @@ -43,18 +67,31 @@ expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes() "line": 3 } }, + "related": [], + "severity": "error", + "suggestions": [], "title": "TYPE MISMATCH" }, { - "message": [ - "This value is not a record, so I cannot use it when accessing the `field` field\nof this value:\n\n3| bad = (().field, \\x -> x)\n ", + "code": "nash::type::not_a_record", + "labels": [ { - "bold": false, - "color": "RED", - "string": "^^^^^^^^", - "underline": false - }, - "\nIt has type:\n\n unit\n\nBut I need a value with record fields!" + "primary": true, + "region": { + "end": { + "column": 16, + "line": 3 + }, + "start": { + "column": 8, + "line": 3 + } + }, + "text": "field `field` access" + } + ], + "message": [ + "Expected a record, found `unit`.\n\n" ], "region": { "end": { @@ -66,6 +103,9 @@ expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes() "line": 3 } }, + "related": [], + "severity": "error", + "suggestions": [], "title": "TYPE MISMATCH" } ] diff --git a/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap index 1a7f716d..bea4225c 100644 --- a/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap +++ b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap @@ -2,35 +2,27 @@ source: crates/nash-cli/tests/diagnostics.rs expression: "normalized(&human.stderr, &project.0)" --- -TYPE MISMATCH +nash::type::mismatch - × Something is off with the body of the `bad` definition: + × Type mismatch: expected `( ?, unit )`, found `( ?, 'a -> 'a )`. ╭─[/src/Main.nash:3:7] + 1 │ module Main exposing (..) 2 │ bad : ((), ()) + · ────┬─── + · ╰── declared type 3 │ bad = (().field, \x -> x) - · ─────────────────── + · ─────────┬───────── + · ╰── body of `bad` ╰──── - help: The body is a tuple of type: - ( ?, 'a -> 'a ) +nash::type::not_a_record - But the type annotation on `bad` says it should be: - - ( ?, unit ) - -TYPE MISMATCH - - × This value is not a record, so I cannot use it when accessing the `field` field - │ of this value: + × Expected a record, found `unit`. ╭─[/src/Main.nash:3:8] 2 │ bad : ((), ()) 3 │ bad = (().field, \x -> x) - · ──────── + · ────┬─── + · ╰── field `field` access ╰──── - help: It has type: - - unit - - But I need a value with record fields! Compilation failed: 0 succeeded, 1 failed or blocked. diff --git a/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_json.snap b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_json.snap index f56fba89..c284f481 100644 --- a/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_json.snap +++ b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_json.snap @@ -9,28 +9,53 @@ expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes() "path": "/src/Main.nash", "problems": [ { - "message": [ - "Something is off with the body of the `identity` definition:\n\n5| identity flag = flag\n ", + "code": "nash::type::mismatch", + "labels": [ { - "bold": false, - "color": "RED", - "string": "^^^^", - "underline": false + "primary": true, + "region": { + "end": { + "column": 21, + "line": 5 + }, + "start": { + "column": 17, + "line": 5 + } + }, + "text": "body of `identity`" }, - "\nThis `flag` value is a:\n\n ", + { + "primary": false, + "region": { + "end": { + "column": 24, + "line": 4 + }, + "start": { + "column": 12, + "line": 4 + } + }, + "text": "declared type" + } + ], + "message": [ + "Type mismatch: expected `", { "bold": false, "color": "yellow", - "string": "bool", + "string": "unit", "underline": false }, - "\n\nBut the type annotation on `identity` says it should be:\n\n ", + "`, found `", { "bold": false, "color": "yellow", - "string": "unit", + "string": "bool", "underline": false - } + }, + "`.\n\n" ], "region": { "end": { @@ -42,6 +67,9 @@ expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes() "line": 5 } }, + "related": [], + "severity": "error", + "suggestions": [], "title": "TYPE MISMATCH" } ] diff --git a/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap index 950a6b52..196983b4 100644 --- a/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap +++ b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap @@ -2,20 +2,17 @@ source: crates/nash-cli/tests/diagnostics.rs expression: text --- -TYPE MISMATCH +nash::type::mismatch - × Something is off with the body of the `identity` definition: + × Type mismatch: expected `unit`, found `bool`. ╭─[/src/Main.nash:5:17] + 3 │ 4 │ identity : bool -> unit + · ──────┬───── + · ╰── declared type 5 │ identity flag = flag - · ──── + · ──┬─ + · ╰── body of `identity` ╰──── - help: This `flag` value is a: - - bool - - But the type annotation on `identity` says it should be: - - unit Compilation failed: 0 succeeded, 1 failed or blocked. diff --git a/crates/nash-constrain/Cargo.toml b/crates/nash-constrain/Cargo.toml index fdf08591..026ea38d 100644 --- a/crates/nash-constrain/Cargo.toml +++ b/crates/nash-constrain/Cargo.toml @@ -3,9 +3,8 @@ name = "nash-constrain" version = "0.4.1" edition.workspace = true description = """ -Responsible for building the set of constraints that are used \ -during type inference of a program, and for gathering context \ -needed for pleasant error messages when a type error occurs. +Shared union-find types, canonical type instantiation, and diagnostics \ +for direct type inference. """ homepage.workspace = true repository.workspace = true diff --git a/crates/nash-constrain/src/error.rs b/crates/nash-constrain/src/error.rs index c97adca2..9dd71a78 100644 --- a/crates/nash-constrain/src/error.rs +++ b/crates/nash-constrain/src/error.rs @@ -163,19 +163,19 @@ pub struct AmbiguousPredicate<'a> { pub enum Expected<'a, T> { NoExpectation(T), FromContext(Region, Context<'a>, T), - FromAnnotation(&'a str, usize, SubContext, T), + FromAnnotation(&'a str, Region, usize, SubContext, T), } /// Indexes are zero-based, mirroring Elm's `Index.ZeroBased`. #[derive(Clone, Copy, Debug)] pub enum Context<'a> { RecordField(&'a str, &'a str), - ListEntry(usize), + ListEntry(usize, Option), OpLeft(&'a str), OpRight(&'a str), IfCondition, - IfBranch(usize), - CaseBranch(usize), + IfBranch(usize, Option), + CaseBranch(usize, Option), CallArity(MaybeName<'a>, usize), CallArg(MaybeName<'a>, usize), RecordAccess { @@ -233,7 +233,7 @@ pub enum PExpected<'a, T> { #[derive(Clone, Copy, Debug)] pub enum PContext<'a> { - TypedArg(&'a str, usize), + TypedArg(&'a str, usize, Region), CaseMatch(usize), CtorArg(&'a str, usize), ListEntry(usize), @@ -265,8 +265,8 @@ impl<'a, T> Expected<'a, T> { Expected::FromContext(region, context, _) => { Expected::FromContext(*region, *context, tipe) } - Expected::FromAnnotation(name, arity, context, _) => { - Expected::FromAnnotation(name, *arity, *context, tipe) + Expected::FromAnnotation(name, region, arity, context, _) => { + Expected::FromAnnotation(name, *region, *arity, *context, tipe) } } } diff --git a/crates/nash-constrain/src/expression.rs b/crates/nash-constrain/src/expression.rs deleted file mode 100644 index 280762dc..00000000 --- a/crates/nash-constrain/src/expression.rs +++ /dev/null @@ -1,1534 +0,0 @@ -//! Port of Elm's `Type.Constrain.Expression`: turn canonical expressions -//! into constraints. -//! -//! Deviations from Elm, all because `nash-ast` has no such expressions: -//! no `Float`/`Chr` literals, no `Shader`, no `VarKernel`/`VarDebug`. - -use bumpalo::Bump; -use nash_ast::{ - CaseBranch, Def as CanDef, Expr as CanExpr, FieldUpdate, FieldValue, IfBranch, NodeId, - TypedPattern, -}; -use nash_region::{Located, Region}; - -use crate::error::{Category, Context, Expected, MaybeName, PContext, PExpected, SubContext}; -use crate::instantiate; -use crate::pattern; -use crate::type_::{self, Constraint, Definition, Type, exists, mk_flex_var, name_to_rigid}; -use crate::union_find::{UnionFind, Variable}; - -/// Elm's `RTV`: rigid type variables introduced by enclosing type -/// annotations, shared with nested annotations. -pub type Rtv<'a> = instantiate::FreeVars<'a>; - -type Exp<'a> = Expected<'a, &'a Type<'a>>; - -pub fn constrain<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - expr: &Located>, - expected: Exp<'a>, -) -> Constraint<'a> { - let region = expr.region; - let node = NodeId::expr(expr); - match &expr.value { - CanExpr::VarLocal(name) => Constraint::Local(region, node, name, expected), - - CanExpr::VarTopLevel(reference) => { - Constraint::Local(region, node, reference.name, expected) - } - - CanExpr::VarForeign { - reference, - annotation, - } => Constraint::Foreign(region, node, reference.name, annotation, expected), - - CanExpr::VarMethod { - method, annotation, .. - } => Constraint::Foreign(region, node, method, annotation, expected), - - CanExpr::VarConstructor { - reference, - annotation, - .. - } => Constraint::Foreign(region, node, reference.name, annotation, expected), - - CanExpr::VarOperator { - symbol, annotation, .. - } => Constraint::Foreign(region, node, symbol, annotation, expected), - - CanExpr::Str(_) => Constraint::Foreign( - region, - node, - "fromString", - type_::literal_annotation(bump, &[type_::literal_trait("FromString")]), - expected, - ), - CanExpr::Bytes(_) => Constraint::Foreign( - region, - node, - "fromBytes", - type_::literal_annotation(bump, &[type_::literal_trait("FromBytes")]), - expected, - ), - CanExpr::Int(_) => Constraint::Foreign( - region, - node, - "fromInt", - type_::literal_annotation(bump, &[type_::literal_trait("FromInt")]), - expected, - ), - - CanExpr::List(elements) => constrain_list(bump, uf, rtv, region, elements, expected), - - CanExpr::Binop { - symbol, - annotation, - left, - right, - .. - } => constrain_binop( - bump, uf, rtv, region, node, symbol, annotation, left, right, expected, - ), - - CanExpr::Lambda { parameters, body } => { - constrain_lambda(bump, uf, rtv, region, parameters, body, expected) - } - - CanExpr::Call { - function, - arguments, - } => constrain_call(bump, uf, rtv, region, function, arguments, expected), - - CanExpr::If { - branches, - final_else, - } => constrain_if(bump, uf, rtv, region, branches, final_else, expected), - - CanExpr::Case { - scrutinee, - branches, - } => constrain_case(bump, uf, rtv, region, scrutinee, branches, expected), - - CanExpr::Let { definition, body } => { - let body_con = constrain(bump, uf, rtv, body, expected); - constrain_def(bump, uf, rtv, definition, body_con) - } - - CanExpr::LetRec { definitions, body } => { - let body_con = constrain(bump, uf, rtv, body, expected); - constrain_recursive_defs(bump, uf, rtv, definitions, body_con) - } - - CanExpr::LetDestruct { - pattern, - value, - body, - } => { - let body_con = constrain(bump, uf, rtv, body, expected); - constrain_destruct(bump, uf, rtv, region, pattern, value, body_con) - } - - CanExpr::Accessor(field) => { - let record_var = mk_flex_var(uf); - let field_var = mk_flex_var(uf); - let record_type: &'a Type<'a> = bump.alloc(Type::VarN(record_var)); - let field_type: &'a Type<'a> = bump.alloc(Type::VarN(field_var)); - exists( - bump, - bump.alloc_slice_copy(&[record_var, field_var]), - c_and( - bump, - vec![ - Constraint::Field { - region, - context: type_::FieldContext::Accessor, - record: record_type, - field, - field_type, - }, - Constraint::Equal( - region, - Category::Accessor(field), - bump.alloc(Type::FunN(record_type, field_type)), - expected, - ), - ], - ), - ) - } - - CanExpr::Access { record, field } => { - let record_var = mk_flex_var(uf); - let field_var = mk_flex_var(uf); - let record_type: &'a Type<'a> = bump.alloc(Type::VarN(record_var)); - let field_type: &'a Type<'a> = bump.alloc(Type::VarN(field_var)); - let record_con = constrain(bump, uf, rtv, record, Expected::NoExpectation(record_type)); - exists( - bump, - bump.alloc_slice_copy(&[record_var, field_var]), - c_and( - bump, - vec![ - record_con, - Constraint::Field { - region, - context: type_::FieldContext::Access { - record_region: record.region, - maybe_name: get_access_name(record), - }, - record: record_type, - field: field.value, - field_type, - }, - Constraint::Equal( - region, - Category::Access(field.value), - field_type, - expected, - ), - ], - ), - ) - } - - CanExpr::Update { - record, - base, - fields, - } => constrain_update(bump, uf, rtv, region, record, base, fields, expected), - - CanExpr::Record { - alias, - annotation, - fields, - } => constrain_record( - bump, uf, rtv, region, node, *alias, annotation, fields, expected, - ), - - CanExpr::Unit => Constraint::Equal( - region, - Category::Unit, - bump.alloc(crate::type_::unit()), - expected, - ), - - CanExpr::Tuple { - first, - second, - rest, - } => constrain_tuple(bump, uf, rtv, region, first, second, rest, expected), - } -} - -// HELPERS - -fn c_and<'a>(bump: &'a Bump, cons: Vec>) -> Constraint<'a> { - Constraint::And(bump.alloc_slice_fill_iter(cons)) -} - -fn singleton_header<'a>( - bump: &'a Bump, - name: &'a str, - region: Region, - tipe: &'a Type<'a>, -) -> &'a [(&'a str, Located<&'a Type<'a>>)] { - bump.alloc_slice_copy(&[(name, Located::at(region, tipe))]) -} - -fn header_slice<'a>( - bump: &'a Bump, - headers: pattern::Header<'a>, -) -> &'a [(&'a str, Located<&'a Type<'a>>)] { - bump.alloc_slice_fill_iter(headers) -} - -fn reversed_and<'a>(bump: &'a Bump, mut rev_cons: Vec>) -> Constraint<'a> { - rev_cons.reverse(); - c_and(bump, rev_cons) -} - -// CONSTRAIN LAMBDA - -fn constrain_lambda<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - args: &[&Located>], - body: &Located>, - expected: Exp<'a>, -) -> Constraint<'a> { - let Args { - vars, - tipe, - result_type, - state, - } = constrain_args(bump, uf, args); - - let body_con = constrain(bump, uf, rtv, body, Expected::NoExpectation(result_type)); - - exists( - bump, - bump.alloc_slice_fill_iter(vars), - c_and( - bump, - vec![ - Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(state.vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(reversed_and(bump, state.rev_cons)), - body_con: bump.alloc(body_con), - }, - Constraint::Equal(region, Category::Lambda, tipe, expected), - ], - ), - ) -} - -// CONSTRAIN CALL - -fn constrain_call<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - func: &Located>, - args: &[&Located>], - expected: Exp<'a>, -) -> Constraint<'a> { - let maybe_name = get_name(func); - let func_region = func.region; - - let func_var = mk_flex_var(uf); - let result_var = mk_flex_var(uf); - let func_type: &'a Type<'a> = bump.alloc(Type::VarN(func_var)); - let result_type: &'a Type<'a> = bump.alloc(Type::VarN(result_var)); - - let func_con = constrain(bump, uf, rtv, func, Expected::NoExpectation(func_type)); - - let mut arg_vars = Vec::with_capacity(args.len()); - let mut arg_types = Vec::with_capacity(args.len()); - let mut arg_cons = Vec::with_capacity(args.len()); - for (index, arg) in args.iter().enumerate() { - let arg_var = mk_flex_var(uf); - let arg_type: &'a Type<'a> = bump.alloc(Type::VarN(arg_var)); - let arg_con = constrain( - bump, - uf, - rtv, - arg, - Expected::FromContext(region, Context::CallArg(maybe_name, index), arg_type), - ); - arg_vars.push(arg_var); - arg_types.push(arg_type); - arg_cons.push(arg_con); - } - - let arity_type = arg_types.iter().rev().fold(result_type, |acc, arg_type| { - bump.alloc(Type::FunN(arg_type, acc)) - }); - let category = Category::CallResult(maybe_name); - - let mut vars = vec![func_var, result_var]; - vars.extend(arg_vars); - - exists( - bump, - bump.alloc_slice_fill_iter(vars), - c_and( - bump, - vec![ - func_con, - Constraint::Equal( - func_region, - category, - func_type, - Expected::FromContext( - region, - Context::CallArity(maybe_name, args.len()), - arity_type, - ), - ), - c_and(bump, arg_cons), - Constraint::Equal(region, category, result_type, expected), - ], - ), - ) -} - -fn get_name<'a>(func: &Located>) -> MaybeName<'a> { - match &func.value { - CanExpr::VarMethod { method, .. } => MaybeName::FuncName(method), - CanExpr::VarLocal(name) => MaybeName::FuncName(name), - CanExpr::VarTopLevel(reference) => MaybeName::FuncName(reference.name), - CanExpr::VarForeign { reference, .. } => MaybeName::FuncName(reference.name), - CanExpr::VarConstructor { reference, .. } => MaybeName::CtorName(reference.name), - CanExpr::VarOperator { symbol, .. } => MaybeName::OpName(symbol), - _ => MaybeName::NoName, - } -} - -fn get_access_name<'a>(record: &Located>) -> Option<&'a str> { - match &record.value { - CanExpr::VarLocal(name) => Some(name), - CanExpr::VarTopLevel(reference) => Some(reference.name), - CanExpr::VarForeign { reference, .. } => Some(reference.name), - _ => None, - } -} - -// CONSTRAIN BINOP - -#[allow(clippy::too_many_arguments)] -fn constrain_binop<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - node: NodeId, - op: &'a str, - annotation: &'a nash_ast::Annotation<'a>, - left_expr: &Located>, - right_expr: &Located>, - expected: Exp<'a>, -) -> Constraint<'a> { - let left_var = mk_flex_var(uf); - let right_var = mk_flex_var(uf); - let answer_var = mk_flex_var(uf); - let left_type: &'a Type<'a> = bump.alloc(Type::VarN(left_var)); - let right_type: &'a Type<'a> = bump.alloc(Type::VarN(right_var)); - let answer_type: &'a Type<'a> = bump.alloc(Type::VarN(answer_var)); - let binop_type: &'a Type<'a> = bump.alloc(Type::FunN( - left_type, - bump.alloc(Type::FunN(right_type, answer_type)), - )); - - let op_con = Constraint::Foreign( - region, - node, - op, - annotation, - Expected::NoExpectation(binop_type), - ); - - let left_con = constrain( - bump, - uf, - rtv, - left_expr, - Expected::FromContext(region, Context::OpLeft(op), left_type), - ); - let right_con = constrain( - bump, - uf, - rtv, - right_expr, - Expected::FromContext(region, Context::OpRight(op), right_type), - ); - - exists( - bump, - bump.alloc_slice_copy(&[left_var, right_var, answer_var]), - c_and( - bump, - vec![ - op_con, - left_con, - right_con, - Constraint::Equal( - region, - Category::CallResult(MaybeName::OpName(op)), - answer_type, - expected, - ), - ], - ), - ) -} - -// CONSTRAIN LISTS - -fn constrain_list<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - entries: &[&Located>], - expected: Exp<'a>, -) -> Constraint<'a> { - let entry_var = mk_flex_var(uf); - let entry_type: &'a Type<'a> = bump.alloc(Type::VarN(entry_var)); - let list_type: &'a Type<'a> = bump.alloc(type_::list(bump, entry_type)); - - let entry_cons = entries - .iter() - .enumerate() - .map(|(index, entry)| { - constrain( - bump, - uf, - rtv, - entry, - Expected::FromContext(region, Context::ListEntry(index), entry_type), - ) - }) - .collect(); - - exists( - bump, - bump.alloc_slice_copy(&[entry_var]), - c_and( - bump, - vec![ - c_and(bump, entry_cons), - Constraint::Equal(region, Category::List, list_type, expected), - ], - ), - ) -} - -// CONSTRAIN IF EXPRESSIONS - -fn constrain_if<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - branches: &[IfBranch<'a>], - final_else: &Located>, - expected: Exp<'a>, -) -> Constraint<'a> { - let bool_type: &'a Type<'a> = bump.alloc(type_::bool()); - let cond_cons: Vec> = branches - .iter() - .map(|branch| { - constrain( - bump, - uf, - rtv, - branch.condition, - Expected::FromContext(region, Context::IfCondition, bool_type), - ) - }) - .collect(); - - let exprs: Vec<&Located>> = branches - .iter() - .map(|branch| branch.then_branch) - .chain(std::iter::once(final_else)) - .collect(); - - match expected { - Expected::FromAnnotation(name, arity, _, tipe) => { - let branch_cons = exprs - .iter() - .enumerate() - .map(|(index, branch_expr)| { - constrain( - bump, - uf, - rtv, - branch_expr, - Expected::FromAnnotation( - name, - arity, - SubContext::TypedIfBranch(index), - tipe, - ), - ) - }) - .collect(); - c_and(bump, vec![c_and(bump, cond_cons), c_and(bump, branch_cons)]) - } - - _ => { - let branch_var = mk_flex_var(uf); - let branch_type: &'a Type<'a> = bump.alloc(Type::VarN(branch_var)); - - let branch_cons = exprs - .iter() - .enumerate() - .map(|(index, branch_expr)| { - constrain( - bump, - uf, - rtv, - branch_expr, - Expected::FromContext(region, Context::IfBranch(index), branch_type), - ) - }) - .collect(); - - exists( - bump, - bump.alloc_slice_copy(&[branch_var]), - c_and( - bump, - vec![ - c_and(bump, cond_cons), - c_and(bump, branch_cons), - Constraint::Equal(region, Category::If, branch_type, expected), - ], - ), - ) - } - } -} - -// CONSTRAIN CASE EXPRESSIONS - -fn constrain_case<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - expr: &Located>, - branches: &[CaseBranch<'a>], - expected: Exp<'a>, -) -> Constraint<'a> { - let ptrn_var = mk_flex_var(uf); - let ptrn_type: &'a Type<'a> = bump.alloc(Type::VarN(ptrn_var)); - let expr_con = constrain(bump, uf, rtv, expr, Expected::NoExpectation(ptrn_type)); - - match expected { - Expected::FromAnnotation(name, arity, _, tipe) => { - let mut cons = vec![expr_con]; - for (index, branch) in branches.iter().enumerate() { - cons.push(constrain_case_branch( - bump, - uf, - rtv, - branch, - PExpected::FromContext(region, PContext::CaseMatch(index), ptrn_type), - Expected::FromAnnotation(name, arity, SubContext::TypedCaseBranch(index), tipe), - )); - } - exists(bump, bump.alloc_slice_copy(&[ptrn_var]), c_and(bump, cons)) - } - - _ => { - let branch_var = mk_flex_var(uf); - let branch_type: &'a Type<'a> = bump.alloc(Type::VarN(branch_var)); - - let mut branch_cons = Vec::with_capacity(branches.len()); - for (index, branch) in branches.iter().enumerate() { - branch_cons.push(constrain_case_branch( - bump, - uf, - rtv, - branch, - PExpected::FromContext(region, PContext::CaseMatch(index), ptrn_type), - Expected::FromContext(region, Context::CaseBranch(index), branch_type), - )); - } - - exists( - bump, - bump.alloc_slice_copy(&[ptrn_var, branch_var]), - c_and( - bump, - vec![ - expr_con, - c_and(bump, branch_cons), - Constraint::Equal(region, Category::Case, branch_type, expected), - ], - ), - ) - } - } -} - -fn constrain_case_branch<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - branch: &CaseBranch<'a>, - p_expect: PExpected<'a, &'a Type<'a>>, - b_expect: Exp<'a>, -) -> Constraint<'a> { - let state = pattern::add(bump, uf, branch.pattern, p_expect, pattern::empty_state()); - - let body_con = constrain(bump, uf, rtv, branch.body, b_expect); - - Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(state.vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(reversed_and(bump, state.rev_cons)), - body_con: bump.alloc(body_con), - } -} - -// CONSTRAIN RECORD - -#[allow(clippy::too_many_arguments)] -fn constrain_record<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - node: NodeId, - alias: nash_ast::QualifiedName<'a>, - annotation: &'a nash_ast::Annotation<'a>, - fields: &[FieldValue<'a>], - expected: Exp<'a>, -) -> Constraint<'a> { - let mut vars = Vec::with_capacity(fields.len() + 1); - let mut cons = Vec::with_capacity(fields.len() + 2); - let mut args = Vec::with_capacity(fields.len()); - for field in fields { - let var = mk_flex_var(uf); - let typ: &'a Type<'a> = bump.alloc(Type::VarN(var)); - vars.push(var); - args.push(typ); - cons.push(constrain( - bump, - uf, - rtv, - field.value, - Expected::FromContext( - region, - Context::RecordField(alias.name, field.field.value), - typ, - ), - )); - } - let result = mk_flex_var(uf); - vars.push(result); - let result_type: &'a Type<'a> = bump.alloc(Type::VarN(result)); - let ctor_type = args.into_iter().rev().fold(result_type, |result, arg| { - &*bump.alloc(Type::FunN(arg, result)) - }); - cons.push(Constraint::Foreign( - region, - node, - alias.name, - annotation, - Expected::NoExpectation(ctor_type), - )); - cons.push(Constraint::Equal( - region, - Category::Record, - result_type, - expected, - )); - exists(bump, bump.alloc_slice_fill_iter(vars), c_and(bump, cons)) -} - -// CONSTRAIN RECORD UPDATE - -#[allow(clippy::too_many_arguments)] -fn constrain_update<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - name: &'a str, - expr: &Located>, - fields: &'a [FieldUpdate<'a>], - expected: Exp<'a>, -) -> Constraint<'a> { - let record_var = mk_flex_var(uf); - let record_type: &'a Type<'a> = bump.alloc(Type::VarN(record_var)); - let mut vars = vec![record_var]; - let mut cons = vec![constrain( - bump, - uf, - rtv, - expr, - Expected::FromContext(region, Context::RecordUpdateKeys(name, fields), record_type), - )]; - if fields.is_empty() { - cons.push(Constraint::Record { - region, - context: type_::FieldContext::Update { record: name }, - record: record_type, - }); - } - for field in fields { - let var = mk_flex_var(uf); - vars.push(var); - let field_type: &'a Type<'a> = bump.alloc(Type::VarN(var)); - cons.push(constrain( - bump, - uf, - rtv, - field.value, - Expected::FromContext( - region, - Context::RecordUpdateValue(field.field.value), - field_type, - ), - )); - cons.push(Constraint::Field { - region: field.field.region, - context: type_::FieldContext::Update { record: name }, - record: record_type, - field: field.field.value, - field_type, - }); - } - cons.push(Constraint::Equal( - region, - Category::Record, - record_type, - expected, - )); - exists(bump, bump.alloc_slice_fill_iter(vars), c_and(bump, cons)) -} - -// CONSTRAIN TUPLE - -#[allow(clippy::too_many_arguments)] -fn constrain_tuple<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - a: &Located>, - b: &Located>, - rest: &[&Located>], - expected: Exp<'a>, -) -> Constraint<'a> { - let a_var = mk_flex_var(uf); - let b_var = mk_flex_var(uf); - let a_type: &'a Type<'a> = bump.alloc(Type::VarN(a_var)); - let b_type: &'a Type<'a> = bump.alloc(Type::VarN(b_var)); - - let a_con = constrain(bump, uf, rtv, a, Expected::NoExpectation(a_type)); - let b_con = constrain(bump, uf, rtv, b, Expected::NoExpectation(b_type)); - - let mut vars = vec![a_var, b_var]; - let mut cons = vec![a_con, b_con]; - let mut types = Vec::with_capacity(rest.len()); - for item in rest { - let var = mk_flex_var(uf); - let tipe: &'a Type<'a> = bump.alloc(Type::VarN(var)); - vars.push(var); - types.push(tipe); - cons.push(constrain( - bump, - uf, - rtv, - item, - Expected::NoExpectation(tipe), - )); - } - let tuple_type = bump.alloc(Type::TupleN(a_type, b_type, bump.alloc_slice_copy(&types))); - cons.push(Constraint::Equal( - region, - Category::Tuple, - tuple_type, - expected, - )); - exists(bump, bump.alloc_slice_copy(&vars), c_and(bump, cons)) -} - -// CONSTRAIN DESTRUCTURES - -fn constrain_destruct<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - region: Region, - pattern_ast: &Located>, - expr: &Located>, - body_con: Constraint<'a>, -) -> Constraint<'a> { - let pattern_var = mk_flex_var(uf); - let pattern_type: &'a Type<'a> = bump.alloc(Type::VarN(pattern_var)); - - let mut state = pattern::add( - bump, - uf, - pattern_ast, - PExpected::NoExpectation(pattern_type), - pattern::empty_state(), - ); - - let expr_con = constrain( - bump, - uf, - rtv, - expr, - Expected::FromContext(region, Context::Destructure, pattern_type), - ); - - let mut flex_vars = vec![pattern_var]; - flex_vars.append(&mut state.vars); - - // Elm: `CAnd (reverse (exprCon:revCons))` — exprCon runs last. - let mut cons = state.rev_cons; - cons.reverse(); - cons.push(expr_con); - - let binder = type_::Binder::Pattern { - node: NodeId::pattern(pattern_ast), - name: bump.alloc(Located::at(region, "")), - }; - Constraint::Let { - declarations: &[], - given: &[], - binder: Some(binder), - definitions: bump.alloc_slice_copy(&[Definition { - site: binder, - typ: pattern_type, - context: None, - }]), - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(flex_vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(c_and(bump, cons)), - body_con: bump.alloc(body_con), - } -} - -// CONSTRAIN DEF - -pub fn constrain_def<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - def: &CanDef<'a>, - body_con: Constraint<'a>, -) -> Constraint<'a> { - constrain_definition(bump, uf, rtv, def, body_con, true) -} - -/// Check a method body in the module environment without introducing its -/// name as a top-level value. Its annotation is already specialized for -/// the enclosing trait or impl by canonicalization. -pub fn constrain_method<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - def: &CanDef<'a>, -) -> Constraint<'a> { - assert!(matches!(def, CanDef::TypedDef { .. })); - constrain_definition(bump, uf, &Rtv::new(), def, Constraint::True, false) -} - -fn constrain_definition<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - def: &CanDef<'a>, - body_con: Constraint<'a>, - bind_name: bool, -) -> Constraint<'a> { - match def { - CanDef::Def { name, args, body } => { - let Args { - vars, - tipe, - result_type, - state, - } = constrain_args(bump, uf, args); - - let expr_con = constrain(bump, uf, rtv, body, Expected::NoExpectation(result_type)); - - Constraint::Let { - declarations: &[], - given: &[], - binder: Some(type_::Binder::Named(name)), - definitions: bump.alloc_slice_copy(&[Definition { - site: type_::Binder::Named(name), - typ: tipe, - context: None, - }]), - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(vars), - header: if bind_name { - singleton_header(bump, name.value, name.region, tipe) - } else { - &[] - }, - header_con: bump.alloc(Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(state.vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(reversed_and(bump, state.rev_cons)), - body_con: bump.alloc(expr_con), - }), - body_con: bump.alloc(body_con), - } - } - - CanDef::TypedDef { - name, - free_vars, - context, - args, - body, - typ: src_result_type, - .. - } => { - let (new_rigids, new_rtv) = make_rigids(bump, uf, rtv, free_vars); - - let TypedArgs { - tipe, - result_type, - state, - } = constrain_typed_args(bump, uf, &new_rtv, name.value, args, src_result_type); - - let expected = Expected::FromAnnotation( - name.value, - args.len(), - SubContext::TypedBody, - result_type, - ); - let expr_con = constrain(bump, uf, &new_rtv, body, expected); - let given = instantiate::from_src_context(bump, &new_rtv, context); - - Constraint::Let { - declarations: &[], - given, - binder: Some(type_::Binder::Named(name)), - definitions: bump.alloc_slice_copy(&[Definition { - site: type_::Binder::Named(name), - typ: tipe, - context: Some(given), - }]), - rigid_vars: bump.alloc_slice_fill_iter(new_rigids.iter().map(|(_, var)| *var)), - flex_vars: &[], - header: if bind_name { - singleton_header(bump, name.value, name.region, tipe) - } else { - &[] - }, - header_con: bump.alloc(Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(state.vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(reversed_and(bump, state.rev_cons)), - body_con: bump.alloc(expr_con), - }), - body_con: bump.alloc(body_con), - } - } - } -} - -/// Elm: `newNames = Map.difference freeVars rtv` then `nameToRigid` per -/// name in `Map` (name-sorted) order. -fn make_rigids<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - free_vars: &[&'a str], -) -> (Vec<(&'a str, Variable)>, Rtv<'a>) { - let mut new_names: Vec<&'a str> = free_vars - .iter() - .filter(|name| !rtv.contains_key(*name)) - .copied() - .collect(); - new_names.sort_unstable(); - - let new_rigids: Vec<(&'a str, Variable)> = new_names - .into_iter() - .map(|name| (name, name_to_rigid(uf, name))) - .collect(); - - let mut new_rtv = rtv.clone(); - for (name, var) in &new_rigids { - new_rtv.insert(name, bump.alloc(Type::VarN(*var))); - } - - (new_rigids, new_rtv) -} - -// CONSTRAIN RECURSIVE DEFS - -struct Info<'a> { - definitions: Vec>, - vars: Vec, - cons: Vec>, - headers: pattern::Header<'a>, -} - -impl<'a> Info<'a> { - fn empty() -> Info<'a> { - Info { - definitions: Vec::new(), - vars: Vec::new(), - cons: Vec::new(), - headers: pattern::Header::new(), - } - } -} - -pub fn constrain_recursive_defs<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - defs: &[&CanDef<'a>], - body_con: Constraint<'a>, -) -> Constraint<'a> { - let mut rigid_info = Info::empty(); - let mut flex_info = Info::empty(); - - for def in defs { - match def { - CanDef::Def { name, args, body } => { - let Args { - vars: new_flex_vars, - tipe, - result_type, - state, - } = constrain_args(bump, uf, args); - - let expr_con = constrain(bump, uf, rtv, body, Expected::NoExpectation(result_type)); - - let def_con = Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(state.vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(reversed_and(bump, state.rev_cons)), - body_con: bump.alloc(expr_con), - }; - - // All recursive headers share one rank until every body has - // been checked. Introducing an earlier header's variables in a - // later definition's pattern scope can generalize them early. - flex_info.vars.extend(new_flex_vars); - flex_info.cons.push(def_con); - flex_info.definitions.push(Definition { - site: type_::Binder::Named(name), - typ: tipe, - context: None, - }); - flex_info - .headers - .insert(name.value, Located::at(name.region, tipe)); - } - - CanDef::TypedDef { - name, - free_vars, - context, - args, - body, - typ: src_result_type, - .. - } => { - let (new_rigids, new_rtv) = make_rigids(bump, uf, rtv, free_vars); - - let TypedArgs { - tipe, - result_type, - state, - } = constrain_typed_args(bump, uf, &new_rtv, name.value, args, src_result_type); - - let expr_con = constrain( - bump, - uf, - &new_rtv, - body, - Expected::FromAnnotation( - name.value, - args.len(), - SubContext::TypedBody, - result_type, - ), - ); - - let def_con = Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(state.vars), - header: header_slice(bump, state.headers), - header_con: bump.alloc(reversed_and(bump, state.rev_cons)), - body_con: bump.alloc(expr_con), - }; - - let given = instantiate::from_src_context(bump, &new_rtv, context); - - // Elm prepends each def's rigids: latest def first, names - // sorted within a def. - let mut vars: Vec = new_rigids.iter().map(|(_, var)| *var).collect(); - vars.append(&mut rigid_info.vars); - rigid_info.vars = vars; - rigid_info.definitions.push(Definition { - site: type_::Binder::Named(name), - typ: tipe, - context: Some(given), - }); - rigid_info.cons.push(Constraint::Let { - declarations: &[], - given, - binder: Some(type_::Binder::Named(name)), - definitions: bump.alloc_slice_copy(&[Definition { - site: type_::Binder::Named(name), - typ: tipe, - context: Some(given), - }]), - rigid_vars: bump.alloc_slice_fill_iter(new_rigids.iter().map(|(_, var)| *var)), - flex_vars: &[], - header: &[], - header_con: bump.alloc(def_con), - body_con: bump.alloc(Constraint::True), - }); - rigid_info - .headers - .insert(name.value, Located::at(name.region, tipe)); - } - } - } - - // Elm builds the cons lists by prepending, so they end up latest-first. - rigid_info.cons.reverse(); - flex_info.cons.reverse(); - - let flex_headers = header_slice(bump, flex_info.headers); - let flex_definitions = bump.alloc_slice_fill_iter(flex_info.definitions); - Constraint::Let { - declarations: bump.alloc_slice_fill_iter(rigid_info.definitions), - given: &[], - binder: None, - definitions: &[], - rigid_vars: bump.alloc_slice_fill_iter(rigid_info.vars), - flex_vars: &[], - header: header_slice(bump, rigid_info.headers), - header_con: bump.alloc(Constraint::True), - body_con: bump.alloc(Constraint::Let { - declarations: &[], - given: &[], - binder: flex_definitions.first().map(|def| def.site), - definitions: flex_definitions, - rigid_vars: &[], - flex_vars: bump.alloc_slice_fill_iter(flex_info.vars), - header: flex_headers, - header_con: bump.alloc(Constraint::Let { - declarations: flex_definitions, - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars: &[], - header: flex_headers, - header_con: bump.alloc(Constraint::True), - body_con: bump.alloc(c_and(bump, flex_info.cons)), - }), - body_con: bump.alloc(c_and(bump, vec![c_and(bump, rigid_info.cons), body_con])), - }), - } -} - -// CONSTRAIN ARGS - -struct Args<'a> { - vars: Vec, - tipe: &'a Type<'a>, - result_type: &'a Type<'a>, - state: pattern::State<'a>, -} - -fn constrain_args<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - args: &[&Located>], -) -> Args<'a> { - args_help(bump, uf, args, pattern::empty_state()) -} - -fn args_help<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - args: &[&Located>], - state: pattern::State<'a>, -) -> Args<'a> { - let mut arg_vars = Vec::with_capacity(args.len()); - let mut arg_types: Vec<&'a Type<'a>> = Vec::with_capacity(args.len()); - - let mut state = state; - for arg_pattern in args { - let arg_var = mk_flex_var(uf); - let arg_type: &'a Type<'a> = bump.alloc(Type::VarN(arg_var)); - state = pattern::add( - bump, - uf, - arg_pattern, - PExpected::NoExpectation(arg_type), - state, - ); - arg_vars.push(arg_var); - arg_types.push(arg_type); - } - - let result_var = mk_flex_var(uf); - let result_type: &'a Type<'a> = bump.alloc(Type::VarN(result_var)); - - let tipe = arg_types.iter().rev().fold(result_type, |acc, arg_type| { - bump.alloc(Type::FunN(arg_type, acc)) - }); - - let mut vars = arg_vars; - vars.push(result_var); - - Args { - vars, - tipe, - result_type, - state, - } -} - -// CONSTRAIN TYPED ARGS - -struct TypedArgs<'a> { - tipe: &'a Type<'a>, - result_type: &'a Type<'a>, - state: pattern::State<'a>, -} - -fn constrain_typed_args<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - rtv: &Rtv<'a>, - name: &'a str, - args: &[TypedPattern<'a>], - src_result_type: &Located>, -) -> TypedArgs<'a> { - let mut state = pattern::empty_state(); - let mut arg_types: Vec<&'a Type<'a>> = Vec::with_capacity(args.len()); - - for (index, arg) in args.iter().enumerate() { - let arg_type = instantiate::from_src_type(bump, rtv, arg.typ); - let expected = PExpected::FromContext( - arg.pattern.region, - PContext::TypedArg(name, index), - arg_type, - ); - state = pattern::add(bump, uf, arg.pattern, expected, state); - arg_types.push(arg_type); - } - - let result_type = instantiate::from_src_type(bump, rtv, src_result_type); - - let tipe = arg_types.iter().rev().fold(result_type, |acc, arg_type| { - bump.alloc(Type::FunN(arg_type, acc)) - }); - - TypedArgs { - tipe, - result_type, - state, - } -} - -#[cfg(test)] -mod node_tests { - use super::*; - use nash_ast::{Annotation, ModuleName, QualifiedName}; - - #[test] - fn predicate_instantiation_preserves_order_and_captured_variables() { - let bump = Bump::new(); - let mut uf = UnionFind::new(); - let captured = name_to_rigid(&mut uf, "captured"); - let rtv = Rtv::from([("captured", &*bump.alloc(Type::VarN(captured)))]); - let names = ["z", "captured", "a"]; - let (rigids, scope) = make_rigids(&bump, &mut uf, &rtv, &names); - assert_eq!( - rigids.iter().map(|(name, _)| *name).collect::>(), - ["a", "z"] - ); - let variables = names.map(|name| &*bump.alloc(Located::at_zero(nash_ast::Type::Var(name)))); - let context = [nash_ast::Pred::Apply { - head: variables[0], - args: bump.alloc_slice_copy(&variables[1..]), - }]; - let extract = |scope: &Rtv<'_>| { - instantiate::from_src_context(&bump, scope, &context)[0] - .types() - .map(|typ| { - let Type::VarN(variable) = typ else { - panic!("predicate variable") - }; - *variable - }) - .collect::>() - }; - let signature = extract(&scope); - assert_eq!(signature, [rigids[1].1, captured, rigids[0].1]); - let (_, other_scope) = make_rigids(&bump, &mut uf, &rtv, &names); - let other = extract(&other_scope); - assert_eq!(other[1], captured); - assert_ne!(other[0], signature[0]); - assert_ne!(other[2], signature[2]); - } - - #[test] - fn literals_and_patterns_keep_original_nodes_and_ordered_predicates() { - let bump = Bump::new(); - for (expr, pattern, trait_name) in [ - (CanExpr::Int(7), nash_ast::Pattern::Int(7), "FromInt"), - ( - CanExpr::Bytes(&[0, 255]), - nash_ast::Pattern::Bytes(&[0, 255]), - "FromBytes", - ), - ( - CanExpr::Str("nash"), - nash_ast::Pattern::Str("nash"), - "FromString", - ), - ] { - let expr = bump.alloc(Located::at_zero(expr)); - let pattern = bump.alloc(Located::at_zero(pattern)); - let mut uf = UnionFind::new(); - let expected = bump.alloc(Type::VarN(mk_flex_var(&mut uf))); - let constraint = constrain( - &bump, - &mut uf, - &Rtv::new(), - expr, - Expected::NoExpectation(expected), - ); - let Constraint::Foreign(_, node, _, annotation, _) = constraint else { - panic!("literal scheme") - }; - assert_eq!(node, NodeId::expr(expr)); - assert_eq!(annotation.context.len(), 1); - assert_eq!(annotation.context[0].trait_ref().unwrap().name, trait_name); - assert_eq!( - annotation.context[0].trait_ref().unwrap().home.package, - Some(nash_ast::primitives::CORE) - ); - let state = pattern::add( - &bump, - &mut uf, - pattern, - PExpected::NoExpectation(expected), - pattern::empty_state(), - ); - let annotations: Vec<_> = state - .rev_cons - .iter() - .filter_map(|c| match c { - Constraint::Foreign(_, node, _, annotation, _) => { - assert_eq!(*node, NodeId::pattern(pattern)); - Some(annotation) - } - _ => None, - }) - .collect(); - assert_eq!(annotations.len(), 1, "one evidence instance per pattern"); - assert_eq!( - annotations[0] - .context - .iter() - .map(|p| p.trait_ref().unwrap().name) - .collect::>(), - [trait_name, "Eq"] - ); - assert!(std::ptr::eq( - annotations[0].context[0].args()[0], - annotations[0].context[1].args()[0] - )); - } - } - - fn uses(constraint: &Constraint<'_>, nodes: &mut Vec) { - match constraint { - Constraint::Local(_, node, ..) | Constraint::Foreign(_, node, ..) => nodes.push(*node), - Constraint::And(constraints) => { - for constraint in *constraints { - uses(constraint, nodes); - } - } - Constraint::Let { - header_con, - body_con, - .. - } => { - uses(header_con, nodes); - uses(body_con, nodes); - } - _ => {} - } - } - - #[test] - fn operator_and_method_uses_keep_distinct_node_identity_at_the_same_region() { - let bump = Bump::new(); - let region = Region::zero(); - let reference = QualifiedName { - home: ModuleName { - package: None, - name: "Main", - }, - name: "Combine", - }; - let annotation = bump.alloc(Annotation { - context: &[], - free_vars: &[], - typ: bump.alloc(Located::at(region, nash_ast::Type::unit())), - }); - let local = bump.alloc(Located::at(region, CanExpr::VarLocal("x"))); - let method = bump.alloc(Located::at( - region, - CanExpr::VarMethod { - trait_: reference, - method: "combine", - annotation, - }, - )); - let operator = bump.alloc(Located::at( - region, - CanExpr::Binop { - symbol: "+", - operator_home: reference.home, - reference, - annotation, - left: local, - right: method, - }, - )); - let mut uf = UnionFind::new(); - let constraint = constrain( - &bump, - &mut uf, - &Rtv::new(), - operator, - Expected::NoExpectation(bump.alloc(crate::type_::unit())), - ); - let mut nodes = Vec::new(); - uses(&constraint, &mut nodes); - assert_eq!(nodes.len(), 3); - for expression in [operator as &Located>, local, method] { - assert_eq!( - nodes - .iter() - .filter(|node| **node == NodeId::expr(expression)) - .count(), - 1 - ); - } - } -} diff --git a/crates/nash-constrain/src/instantiate.rs b/crates/nash-constrain/src/instantiate.rs index 18540a23..dc2e8929 100644 --- a/crates/nash-constrain/src/instantiate.rs +++ b/crates/nash-constrain/src/instantiate.rs @@ -1,137 +1,12 @@ -//! Port of Elm's `Type.Instantiate`: turn a canonical type into an -//! inference `Type`, substituting free type variables. +//! Instantiate canonical types directly into union-find variables. use std::collections::BTreeMap; +#[cfg(test)] use bumpalo::Bump; -use nash_ast::{AliasType as CanAliasType, Type as CanType}; +use nash_ast::Type as CanType; use nash_region::Located; -use crate::type_::Type; - -pub type FreeVars<'a> = BTreeMap<&'a str, &'a Type<'a>>; - -/// Instantiate the complete predicate context with the same lexical map as its type. -pub fn from_src_context<'a>( - bump: &'a Bump, - rtv: &FreeVars<'a>, - context: &[nash_ast::Pred<'a>], -) -> &'a [crate::type_::Pred<'a>] { - bump.alloc_slice_fill_iter(context.iter().map(|pred| { - let args = - bump.alloc_slice_fill_iter(pred.args().iter().map(|arg| from_src_type(bump, rtv, arg))); - match *pred { - nash_ast::Pred::Trait { trait_, .. } => crate::type_::Pred::Trait { - trait_, - args, - hidden: false, - }, - nash_ast::Pred::Implied { trait_, .. } => crate::type_::Pred::Trait { - trait_, - args, - hidden: true, - }, - nash_ast::Pred::Apply { head, .. } => crate::type_::Pred::Apply { - head: from_src_type(bump, rtv, head), - args, - }, - } - })) -} - -pub fn from_src_type<'a>( - bump: &'a Bump, - free_vars: &FreeVars<'a>, - src_type: &Located>, -) -> &'a Type<'a> { - match &src_type.value { - CanType::App { head, args } => bump.alloc(Type::AppVarN( - from_src_type(bump, free_vars, head), - bump.alloc_slice_fill_iter(args.iter().map(|arg| from_src_type(bump, free_vars, arg))), - )), - CanType::Lambda { from, to } => bump.alloc(Type::FunN( - from_src_type(bump, free_vars, from), - from_src_type(bump, free_vars, to), - )), - - CanType::Var(name) => free_vars - .get(name) - .expect("canonical types only mention their free variables"), - - CanType::Named { reference, args } => bump.alloc(Type::AppN { - home: reference.home, - name: reference.name, - args: bump - .alloc_slice_fill_iter(args.iter().map(|arg| from_src_type(bump, free_vars, arg))), - }), - - CanType::Alias { - reference, - arguments, - remaining, - target, - } => { - let targs = bump.alloc_slice_fill_iter( - arguments - .iter() - .map(|arg| (arg.name, from_src_type(bump, free_vars, arg.typ))), - ); - let body = match target { - CanAliasType::Open(body) | CanAliasType::Filled { body, .. } => *body, - }; - if !remaining.is_empty() { - assert!( - matches!(target, CanAliasType::Open(_)), - "partial alias body must be closed" - ); - return bump.alloc(Type::PartialAliasN { - home: reference.home, - name: reference.name, - args: targs, - remaining, - body, - }); - } - let real = match target { - CanAliasType::Filled { typ: real_type, .. } => { - from_src_type(bump, free_vars, real_type) - } - CanAliasType::Open(real_type) => { - let arg_vars: FreeVars<'a> = targs.iter().copied().collect(); - from_src_type(bump, &arg_vars, real_type) - } - }; - bump.alloc(Type::AliasN { - home: reference.home, - name: reference.name, - args: targs, - real, - body, - }) - } - - CanType::Tuple { - first, - second, - rest, - } => bump.alloc(Type::TupleN( - from_src_type(bump, free_vars, first), - from_src_type(bump, free_vars, second), - bump.alloc_slice_fill_iter( - rest.iter().map(|item| from_src_type(bump, free_vars, item)), - ), - )), - - CanType::Record { fields } => bump.alloc(Type::RecordN { - fields: bump.alloc_slice_fill_iter( - fields - .iter() - .map(|field| (field.field, from_src_type(bump, free_vars, field.typ))), - ), - }), - } -} - use crate::{Content, FlatType, UnionFind, Variable}; /// A nominal alias application inspected without allocating inference variables. diff --git a/crates/nash-constrain/src/lib.rs b/crates/nash-constrain/src/lib.rs index dd248e4d..619a27ed 100644 --- a/crates/nash-constrain/src/lib.rs +++ b/crates/nash-constrain/src/lib.rs @@ -1,25 +1,15 @@ -//! Constraint generation for nash type inference: a port of Elm's -//! `Type.Constrain.*` plus the shared vocabulary from `Type.Type`, -//! `Type.UnionFind`, `Type.Error`, and `Reporting.Error.Type`. -//! -//! Where Elm creates unification variables in ambient `IO`, nash threads an -//! explicit [`UnionFind`] store: `constrain` fills it with fresh variables -//! and `nash-solve` mutates it while solving the returned [`Constraint`]. +//! Shared union-find types, canonical instantiation, and inference diagnostics. pub mod error; pub mod error_type; pub mod instantiate; -pub mod pattern; pub mod type_; -mod expression; -mod module; mod union_find; pub use crate::error::{ Category, Context, Error, Expected, MaybeName, PCategory, PContext, PExpected, SubContext, }; pub use crate::error_type::ErrorType; -pub use crate::module::constrain; -pub use crate::type_::{Constraint, Content, Descriptor, FlatType, Mark, Type}; +pub use crate::type_::{Content, Descriptor, FlatType, Mark}; pub use crate::union_find::{UnionFind, Variable}; diff --git a/crates/nash-constrain/src/module.rs b/crates/nash-constrain/src/module.rs deleted file mode 100644 index 77f42615..00000000 --- a/crates/nash-constrain/src/module.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Port of Elm's `Type.Constrain.Module`. -//! -//! Nash has no ports or effect managers, so this is just the declaration -//! walk terminated by `CSaveTheEnvironment`. - -use bumpalo::Bump; -use nash_ast::{Decls, Module as CanModule}; - -use crate::expression; -use crate::type_::Constraint; -use crate::union_find::UnionFind; - -pub fn constrain<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - module: &CanModule<'a>, -) -> Constraint<'a> { - let definitions = module - .traits - .iter() - .flat_map(|trait_| { - trait_ - .value - .methods - .iter() - .filter_map(|method| method.default) - }) - .chain( - module - .impls - .iter() - .flat_map(|impl_| impl_.value.methods.iter().copied()), - ); - let mut methods: Vec<_> = definitions - .map(|definition| expression::constrain_method(bump, uf, definition)) - .collect(); - methods.push(Constraint::SaveTheEnvironment); - constrain_decls( - bump, - uf, - module.decls, - Constraint::And(bump.alloc_slice_fill_iter(methods)), - ) -} - -fn constrain_decls<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - decls: &Decls<'a>, - final_constraint: Constraint<'a>, -) -> Constraint<'a> { - match decls { - Decls::Declare { definition, next } => { - let next_con = constrain_decls(bump, uf, next, final_constraint); - expression::constrain_def(bump, uf, &expression::Rtv::new(), definition, next_con) - } - - Decls::DeclareRec { - definition, - following, - next, - } => { - let next_con = constrain_decls(bump, uf, next, final_constraint); - let mut defs = Vec::with_capacity(1 + following.len()); - defs.push(*definition); - defs.extend(following.iter().copied()); - expression::constrain_recursive_defs(bump, uf, &expression::Rtv::new(), &defs, next_con) - } - - Decls::Empty => final_constraint, - } -} diff --git a/crates/nash-constrain/src/pattern.rs b/crates/nash-constrain/src/pattern.rs deleted file mode 100644 index 5682f73e..00000000 --- a/crates/nash-constrain/src/pattern.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Port of Elm's `Type.Constrain.Pattern`: turn a canonical pattern into -//! binding headers plus the constraints its structure implies. - -use std::collections::BTreeMap; - -use bumpalo::Bump; -use nash_ast::{Pattern as CanPattern, PatternCtor}; -use nash_region::{Located, Region}; - -use crate::error::{Expected, PCategory, PContext, PExpected}; -use crate::instantiate; -use crate::type_::{self, Constraint, Type, mk_flex_var, name_to_flex}; -use crate::union_find::{UnionFind, Variable}; - -/// Elm's `Pattern.State`. Constraints are stored in reverse order so that -/// adding one is O(1); callers reverse when building the final `CLet`. -pub struct State<'a> { - pub headers: Header<'a>, - pub vars: Vec, - pub rev_cons: Vec>, -} - -pub type Header<'a> = BTreeMap<&'a str, Located<&'a Type<'a>>>; - -pub fn empty_state<'a>() -> State<'a> { - State { - headers: BTreeMap::new(), - vars: Vec::new(), - rev_cons: Vec::new(), - } -} - -pub fn add<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - pattern: &Located>, - expectation: PExpected<'a, &'a Type<'a>>, - state: State<'a>, -) -> State<'a> { - let region = pattern.region; - match &pattern.value { - CanPattern::Anything => state, - - CanPattern::Var(name) => add_to_headers(region, name, expectation, state), - - CanPattern::Alias { - pattern: real_pattern, - name, - } => { - let state = add_to_headers(region, name, expectation, state); - add(bump, uf, real_pattern, expectation, state) - } - - CanPattern::Unit => { - let mut state = state; - let unit_con = Constraint::Pattern( - region, - PCategory::Unit, - bump.alloc(crate::type_::unit()), - expectation, - ); - state.rev_cons.push(unit_con); - state - } - - CanPattern::Tuple { - first, - second, - rest, - } => add_tuple(bump, uf, region, first, second, rest, expectation, state), - - CanPattern::Constructor(ctor) => add_ctor(bump, uf, region, ctor, expectation, state), - - CanPattern::List(patterns) => { - let entry_var = mk_flex_var(uf); - let entry_type: &'a Type<'a> = bump.alloc(Type::VarN(entry_var)); - let list_type: &'a Type<'a> = bump.alloc(type_::list(bump, entry_type)); - - let mut state = - patterns - .iter() - .enumerate() - .fold(state, |state, (index, entry_pattern)| { - let expectation = - PExpected::FromContext(region, PContext::ListEntry(index), entry_type); - add(bump, uf, entry_pattern, expectation, state) - }); - - let list_con = Constraint::Pattern(region, PCategory::List, list_type, expectation); - state.vars.push(entry_var); - state.rev_cons.push(list_con); - state - } - - CanPattern::Cons { head, tail } => { - let entry_var = mk_flex_var(uf); - let entry_type: &'a Type<'a> = bump.alloc(Type::VarN(entry_var)); - let list_type: &'a Type<'a> = bump.alloc(type_::list(bump, entry_type)); - - let head_expectation = PExpected::NoExpectation(entry_type); - let tail_expectation = PExpected::FromContext(region, PContext::Tail, list_type); - - let state = add(bump, uf, tail, tail_expectation, state); - let mut state = add(bump, uf, head, head_expectation, state); - - let list_con = Constraint::Pattern(region, PCategory::List, list_type, expectation); - state.vars.push(entry_var); - state.rev_cons.push(list_con); - state - } - - CanPattern::Record(fields) => { - let mut state = state; - let record_var = mk_flex_var(uf); - let record_type: &'a Type<'a> = bump.alloc(Type::VarN(record_var)); - state.vars.push(record_var); - state.rev_cons.push(Constraint::Pattern( - region, - PCategory::Record, - record_type, - expectation, - )); - if fields.is_empty() { - state.rev_cons.push(Constraint::Record { - region, - context: type_::FieldContext::Pattern, - record: record_type, - }); - } - for field in *fields { - let var = mk_flex_var(uf); - let field_type: &'a Type<'a> = bump.alloc(Type::VarN(var)); - state.vars.push(var); - state - .headers - .entry(field) - .or_insert_with(|| Located::at(region, field_type)); - state.rev_cons.push(Constraint::Field { - region, - context: type_::FieldContext::Pattern, - record: record_type, - field, - field_type, - }); - } - state - } - - CanPattern::Int(_) | CanPattern::Str(_) | CanPattern::Bytes(_) => { - let (trait_name, category) = match pattern.value { - CanPattern::Int(_) => ("FromInt", PCategory::Int), - CanPattern::Bytes(_) => ("FromBytes", PCategory::Bytes), - _ => ("FromString", PCategory::Str), - }; - let mut state = state; - let var = mk_flex_var(uf); - let typ: &'a Type<'a> = bump.alloc(Type::VarN(var)); - state.vars.push(var); - state - .rev_cons - .push(Constraint::Pattern(region, category, typ, expectation)); - state.rev_cons.push(Constraint::Foreign( - region, - nash_ast::NodeId::pattern(pattern), - "literal", - type_::literal_annotation( - bump, - &[type_::literal_trait(trait_name), type_::eq_trait()], - ), - Expected::NoExpectation(typ), - )); - state - } - - CanPattern::Bool { .. } => { - let mut state = state; - let bool_con = Constraint::Pattern( - region, - PCategory::Bool, - bump.alloc(type_::bool()), - expectation, - ); - state.rev_cons.push(bool_con); - state - } - } -} - -// STATE HELPERS - -fn add_to_headers<'a>( - region: Region, - name: &'a str, - expectation: PExpected<'a, &'a Type<'a>>, - mut state: State<'a>, -) -> State<'a> { - let tipe = get_type(expectation); - state.headers.insert(name, Located::at(region, tipe)); - state -} - -fn get_type<'a>(expectation: PExpected<'a, &'a Type<'a>>) -> &'a Type<'a> { - match expectation { - PExpected::NoExpectation(tipe) => tipe, - PExpected::FromContext(_, _, tipe) => tipe, - } -} - -// CONSTRAIN TUPLE - -#[allow(clippy::too_many_arguments)] -fn add_tuple<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - region: Region, - a: &Located>, - b: &Located>, - rest: &[&Located>], - expectation: PExpected<'a, &'a Type<'a>>, - state: State<'a>, -) -> State<'a> { - let a_var = mk_flex_var(uf); - let b_var = mk_flex_var(uf); - let a_type: &'a Type<'a> = bump.alloc(Type::VarN(a_var)); - let b_type: &'a Type<'a> = bump.alloc(Type::VarN(b_var)); - - let state = simple_add(bump, uf, a, a_type, state); - let mut state = simple_add(bump, uf, b, b_type, state); - state.vars.extend([a_var, b_var]); - let mut types = Vec::with_capacity(rest.len()); - for item in rest { - let var = mk_flex_var(uf); - let tipe: &'a Type<'a> = bump.alloc(Type::VarN(var)); - state = simple_add(bump, uf, item, tipe, state); - state.vars.push(var); - types.push(tipe); - } - state.rev_cons.push(Constraint::Pattern( - region, - PCategory::Tuple, - bump.alloc(Type::TupleN(a_type, b_type, bump.alloc_slice_copy(&types))), - expectation, - )); - state -} - -fn simple_add<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - pattern: &Located>, - pattern_type: &'a Type<'a>, - state: State<'a>, -) -> State<'a> { - add( - bump, - uf, - pattern, - PExpected::NoExpectation(pattern_type), - state, - ) -} - -// CONSTRAIN CONSTRUCTORS - -fn add_ctor<'a>( - bump: &'a Bump, - uf: &mut UnionFind<'a>, - region: Region, - ctor: &PatternCtor<'a>, - expectation: PExpected<'a, &'a Type<'a>>, - state: State<'a>, -) -> State<'a> { - let home = ctor.reference.home; - let type_name = ctor.reference.union; - let ctor_name = ctor.reference.name; - - let var_pairs: Vec<(&'a str, Variable)> = ctor - .union - .parameters - .iter() - .map(|var| (*var, name_to_flex(uf, var))) - .collect(); - let type_pairs: Vec<(&'a str, &'a Type<'a>)> = var_pairs - .iter() - .map(|(name, var)| (*name, &*bump.alloc(Type::VarN(*var)))) - .collect(); - let free_var_dict: instantiate::FreeVars<'a> = type_pairs.iter().copied().collect(); - - let mut state = ctor.arguments.iter().fold(state, |state, arg| { - let tipe = instantiate::from_src_type(bump, &free_var_dict, arg.typ); - let arg_expectation = PExpected::FromContext( - region, - PContext::CtorArg(ctor_name, arg.index as usize), - tipe, - ); - add(bump, uf, arg.pattern, arg_expectation, state) - }); - - let ctor_type: &'a Type<'a> = bump.alloc(Type::AppN { - home, - name: type_name, - args: bump.alloc_slice_fill_iter(type_pairs.iter().map(|(_, typ)| *typ)), - }); - let ctor_con = Constraint::Pattern(region, PCategory::Ctor(ctor_name), ctor_type, expectation); - - state.vars.extend(var_pairs.iter().map(|(_, var)| *var)); - state.rev_cons.push(ctor_con); - state -} diff --git a/crates/nash-constrain/src/type_.rs b/crates/nash-constrain/src/type_.rs index 353d4958..ec983f5b 100644 --- a/crates/nash-constrain/src/type_.rs +++ b/crates/nash-constrain/src/type_.rs @@ -1,5 +1,4 @@ -//! Port of the data half of Elm's `Type.Type`: constraints, the inference -//! `Type` language, and unification variable descriptors. +//! Union-find type descriptors, scheme identities, and literal annotations. //! //! `toAnnotation` and `toErrorType` live in `nash-solve` (they are only //! called by the solver and need `nash-can`'s canonical-type utilities). @@ -9,35 +8,10 @@ use std::collections::BTreeMap; use nash_ast::{Annotation, ModuleName, NodeId, QualifiedName}; use nash_region::{Located, Region}; -use crate::error::{Category, Expected, PCategory, PExpected}; use crate::union_find::{UnionFind, Variable}; // CONSTRAINTS -/// An annotation predicate instantiated over the definition's rigid variables. -#[derive(Clone, Copy, Debug)] -pub enum Pred<'a> { - Trait { - trait_: QualifiedName<'a>, - args: &'a [&'a Type<'a>], - hidden: bool, - }, - Apply { - head: &'a Type<'a>, - args: &'a [&'a Type<'a>], - }, -} - -impl<'a> Pred<'a> { - pub fn types(self) -> impl Iterator> { - let (head, args) = match self { - Self::Trait { args, .. } => (None, args), - Self::Apply { head, args } => (Some(head), args), - }; - head.into_iter().chain(args.iter().copied()) - } -} - /// Scheme identity is an original definition name or a destructuring pattern. #[derive(Clone, Copy, Debug)] pub enum Binder<'a> { @@ -63,92 +37,6 @@ impl<'a> Binder<'a> { } } -/// Preserve the original scheme identity and full type independently of lexical scope. -#[derive(Clone, Copy, Debug)] -pub struct Definition<'a> { - pub site: Binder<'a>, - pub typ: &'a Type<'a>, - /// `Some`, including an empty slice, distinguishes a declared scheme. - pub context: Option<&'a [Pred<'a>]>, -} - -/// Elm's `Type.Constraint`. Allocated in a bump arena, so collections are -/// slices, not owned containers. -#[derive(Debug)] -pub enum Constraint<'a> { - Record { - region: Region, - context: FieldContext<'a>, - record: &'a Type<'a>, - }, - Field { - region: Region, - context: FieldContext<'a>, - record: &'a Type<'a>, - field: &'a str, - field_type: &'a Type<'a>, - }, - True, - SaveTheEnvironment, - Equal( - Region, - Category<'a>, - &'a Type<'a>, - Expected<'a, &'a Type<'a>>, - ), - Local(Region, NodeId, &'a str, Expected<'a, &'a Type<'a>>), - Foreign( - Region, - NodeId, - &'a str, - &'a Annotation<'a>, - Expected<'a, &'a Type<'a>>, - ), - Pattern( - Region, - PCategory<'a>, - &'a Type<'a>, - PExpected<'a, &'a Type<'a>>, - ), - And(&'a [Constraint<'a>]), - Let { - /// Recursive binding identities published before checking group bodies. - /// Annotated declarations also supply their final contexts immediately. - declarations: &'a [Definition<'a>], - /// Assumed while checking the definition body, over its rigid variables. - given: &'a [Pred<'a>], - /// Evidence owner; the first untyped member for a recursive group. - binder: Option>, - /// All definitions generalized here, even when no lexical name is bound. - definitions: &'a [Definition<'a>], - rigid_vars: &'a [Variable], - flex_vars: &'a [Variable], - /// Name-sorted, mirroring Elm's `Map.Map Name (A.Located Type)`. - header: &'a [(&'a str, Located<&'a Type<'a>>)], - header_con: &'a Constraint<'a>, - body_con: &'a Constraint<'a>, - }, -} - -/// Elm's `exists`: a `CLet` binding only flex variables. -pub fn exists<'a>( - bump: &'a bumpalo::Bump, - flex_vars: &'a [Variable], - constraint: Constraint<'a>, -) -> Constraint<'a> { - Constraint::Let { - declarations: &[], - given: &[], - binder: None, - definitions: &[], - rigid_vars: &[], - flex_vars, - header: &[], - header_con: bump.alloc(constraint), - body_con: bump.alloc(Constraint::True), - } -} - // TYPE PRIMITIVES #[derive(Clone, Copy, Debug)] @@ -175,38 +63,6 @@ pub enum FlatType<'a> { Tuple1(Variable, Variable, Vec), } -/// Elm's `Type.Type`: the language the constraint generator writes types in. -#[derive(Clone, Copy, Debug)] -pub enum Type<'a> { - PartialAliasN { - home: ModuleName<'a>, - name: &'a str, - args: &'a [(&'a str, &'a Type<'a>)], - remaining: &'a [&'a str], - body: &'a Located>, - }, - AppVarN(&'a Type<'a>, &'a [&'a Type<'a>]), - AliasN { - home: ModuleName<'a>, - name: &'a str, - args: &'a [(&'a str, &'a Type<'a>)], - real: &'a Type<'a>, - body: &'a Located>, - }, - VarN(Variable), - AppN { - home: ModuleName<'a>, - name: &'a str, - args: &'a [&'a Type<'a>], - }, - FunN(&'a Type<'a>, &'a Type<'a>), - /// Name-sorted, mirroring Elm's `Map.Map Name Type`. - RecordN { - fields: &'a [(&'a str, &'a Type<'a>)], - }, - TupleN(&'a Type<'a>, &'a Type<'a>, &'a [&'a Type<'a>]), -} - /// Flatten application spines whose heads inference has already determined. /// This does not bind unknown heads or expand aliases. pub fn normalize_application<'a>(uf: &mut UnionFind<'a>, term: FlatType<'a>) -> FlatType<'a> { @@ -339,7 +195,7 @@ pub fn literal_annotation<'a>( } /// Only the compiler-known literal traits select a little default type. -pub fn literal_default(trait_: nash_ast::QualifiedName<'_>) -> Option> { +pub fn literal_default(trait_: nash_ast::QualifiedName<'_>) -> Option> { if trait_.home.package != Some(nash_ast::primitives::CORE) || trait_.home.name != "Literal" { return None; } @@ -349,35 +205,11 @@ pub fn literal_default(trait_: nash_ast::QualifiedName<'_>) -> Option "bytes", _ => return None, }; - Some(Type::AppN { - home: nash_ast::primitives::builtin_home(), + Some(FlatType::App1( + nash_ast::primitives::builtin_home(), name, - args: &[], - }) -} - -pub fn list<'a>(bump: &'a bumpalo::Bump, element: &'a Type<'a>) -> Type<'a> { - Type::AppN { - home: nash_ast::primitives::builtin_home(), - name: "list", - args: bump.alloc_slice_copy(&[element]), - } -} - -pub const fn unit<'a>() -> Type<'a> { - Type::AppN { - home: nash_ast::primitives::builtin_home(), - name: "unit", - args: &[], - } -} - -pub const fn bool<'a>() -> Type<'a> { - Type::AppN { - home: nash_ast::primitives::builtin_home(), - name: "bool", - args: &[], - } + Vec::new(), + )) } // MAKE FLEX VARIABLES diff --git a/crates/nash-constrain/tests/predicate_instantiation.rs b/crates/nash-constrain/tests/predicate_instantiation.rs deleted file mode 100644 index 6b74326f..00000000 --- a/crates/nash-constrain/tests/predicate_instantiation.rs +++ /dev/null @@ -1,51 +0,0 @@ -use bumpalo::Bump; -use nash_ast::{Pred, Type as Canonical, primitives::ReprTrait}; -use nash_constrain::{Type, UnionFind, instantiate, type_}; -use nash_region::Located; -use std::collections::BTreeMap; - -#[test] -fn apply_head_and_arguments_share_the_signature_substitution() { - let bump = Bump::new(); - let mut uf = UnionFind::new(); - let f = type_::mk_flex_var(&mut uf); - let a = type_::mk_flex_var(&mut uf); - let f_type = &*bump.alloc(Type::VarN(f)); - let a_type = &*bump.alloc(Type::VarN(a)); - let scope = BTreeMap::from([("f", f_type), ("a", a_type)]); - let head = &*bump.alloc(Located::at_zero(Canonical::Var("f"))); - let arg = &*bump.alloc(Located::at_zero(Canonical::Var("a"))); - let args = bump.alloc_slice_copy(&[arg]); - let context = [ - Pred::Apply { head, args }, - Pred::Implied { - trait_: ReprTrait::Big.qualified(), - args, - }, - ]; - let lowered = instantiate::from_src_context(&bump, &scope, &context); - let type_::Pred::Apply { - head: actual_head, - args: actual_args, - } = lowered[0] - else { - panic!("Apply preserved") - }; - assert!(std::ptr::eq(actual_head, f_type)); - assert!(std::ptr::eq(actual_args[0], a_type)); - let type_::Pred::Trait { hidden, args, .. } = lowered[1] else { - panic!("representation predicate preserved") - }; - assert!(hidden); - assert!(std::ptr::eq(args[0], a_type)); - let signature = Located::at_zero(Canonical::App { - head, - args: bump.alloc_slice_copy(&[arg]), - }); - let Type::AppVarN(type_head, type_args) = instantiate::from_src_type(&bump, &scope, &signature) - else { - panic!("type application preserved") - }; - assert!(std::ptr::eq(*type_head, actual_head)); - assert!(std::ptr::eq(type_args[0], actual_args[0])); -} diff --git a/crates/nash-driver/Cargo.toml b/crates/nash-driver/Cargo.toml index 3980aa07..722aab31 100644 --- a/crates/nash-driver/Cargo.toml +++ b/crates/nash-driver/Cargo.toml @@ -9,11 +9,9 @@ license.workspace = true [dependencies] async-trait.workspace = true -bincode.workspace = true bumpalo.workspace = true glob.workspace = true miette.workspace = true -serde.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["sync", "fs"] } url.workspace = true diff --git a/crates/nash-driver/src/compile.rs b/crates/nash-driver/src/compile.rs index fe3a73f7..9cd91538 100644 --- a/crates/nash-driver/src/compile.rs +++ b/crates/nash-driver/src/compile.rs @@ -1,7 +1,7 @@ //! Module compilation orchestration. //! //! Each module runs Elm's full pipeline: parse -> canonicalize -> -//! constrain -> solve -> nitpick -> `Interface::from_module` with the solver's +//! direct inference -> nitpick -> `Interface::from_module` with the solver's //! annotations. Modules compile in dependency order, and each solved //! module and solved evidence remain in the build scope. Canonical nodes //! live in a shared arena, so interfaces borrow them without moving the @@ -299,7 +299,7 @@ fn compile_module<'s>( let bump = store; let src: &str = bump.alloc_str(source); - let mut parser = nash_parse::Parser::new(bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(bump, src); let module = match parser.module() { Ok(module) => module, Err(error) => { @@ -345,9 +345,8 @@ fn compile_module<'s>( )] }; let mut uf = nash_constrain::UnionFind::new(); - let constraint = nash_constrain::constrain(bump, &mut uf, &can_result.module); - let (annotations, types) = match nash_solve::run(bump, &mut uf, &constraint, &can_result.tables) - { + let module = &can_result.module; + let (annotations, types) = match nash_solve::run(bump, &mut uf, module, &can_result.tables) { Ok(solved) => solved, Err(errors) => { return failed( @@ -425,7 +424,7 @@ fn extract_imports(source: &str, current: &Url, known_modules: &[Url]) -> Vec 'a\nimpl Keep () where\n keep x = x\nimpl Keep () where\n keep x = x\n", - "OVERLAPPING IMPL", + "nash::names::overlapping_impls", ), ] { let result = compile_sources(&[ @@ -829,7 +828,7 @@ mod kind_tests { panic!("consumer must reject hidden label") }; let message = report_text(reports); - assert!(message.contains("not a record"), "{message}"); + assert!(message.contains("nash::type::not_a_record"), "{message}"); } #[tokio::test] @@ -846,7 +845,7 @@ mod kind_tests { }; let message = report_text(reports); assert!( - message.contains("does not support record updates"), + message.contains("nash::type::update_not_record"), "{message}" ); } @@ -865,7 +864,7 @@ mod kind_tests { panic!("private constructor labels must remain hidden") }; let message = report_text(reports); - assert!(message.contains("not a record"), "{message}"); + assert!(message.contains("nash::type::not_a_record"), "{message}"); } #[tokio::test] @@ -945,7 +944,7 @@ mod kind_tests { panic!("producer must report a kind error"); }; let message = report_text(reports); - assert!(message.contains("INFINITE KIND"), "{message}"); + assert!(message.contains("nash::names::kind_infinite"), "{message}"); assert!(message.contains("infinite kind"), "{message}"); } @@ -964,7 +963,7 @@ mod kind_tests { panic!("producer must fail") }; let message = report_text(reports); - assert!(message.contains("INFINITE KIND"), "{message}"); + assert!(message.contains("nash::names::kind_infinite"), "{message}"); } } @@ -1000,7 +999,8 @@ mod kind_tests { }; let message = report_text(reports); assert!( - message.contains("REPRESENTATION MISMATCH") && message.contains("Storable"), + message.contains("nash::names::representation_mismatch") + && message.contains("Storable"), "{message}" ); } @@ -1030,7 +1030,8 @@ mod kind_tests { }; let message = report_text(reports); assert!( - message.contains("REPRESENTATION MISMATCH") && message.contains("Storable"), + message.contains("nash::names::representation_mismatch") + && message.contains("Storable"), "{message}" ); assert!( diff --git a/crates/nash-driver/src/compile/nitpick_source_tests.rs b/crates/nash-driver/src/compile/nitpick_source_tests.rs index caec96d2..704ff36a 100644 --- a/crates/nash-driver/src/compile/nitpick_source_tests.rs +++ b/crates/nash-driver/src/compile/nitpick_source_tests.rs @@ -8,7 +8,7 @@ fn solve_source<'a>( package: Option>, ) -> (&'a nash_ast::Module<'a>, nash_can::Annotations<'a>) { let source = bump.alloc_str(source); - let parsed = nash_parse::Parser::new(bump, source.as_bytes()) + let parsed = nash_parse::Parser::new(bump, source) .module() .expect("source must parse"); let can = nash_can::canonicalize( @@ -21,8 +21,8 @@ fn solve_source<'a>( ) .expect("source must canonicalize"); let mut uf = nash_constrain::UnionFind::new(); - let constraint = nash_constrain::constrain(bump, &mut uf, &can.module); - let (annotations, _) = nash_solve::run(bump, &mut uf, &constraint, &can.tables) + let module = &can.module; + let (annotations, _) = nash_solve::run(bump, &mut uf, module, &can.tables) .expect("source must type check before nitpick"); (bump.alloc(can.module), annotations) } @@ -1412,9 +1412,7 @@ fn string_escapes_round_trip() { nash_nitpick::Pattern::Literal(nash_nitpick::Literal::Str(original)), ); let source = bump.alloc_str(&format!("module Main exposing (..)\nf {rendered} = ()\n")); - let parsed = nash_parse::Parser::new(&bump, source.as_bytes()) - .module() - .unwrap(); + let parsed = nash_parse::Parser::new(&bump, source).module().unwrap(); let can = nash_can::canonicalize(&bump, nash_can::Context::default(), &parsed).unwrap(); let nash_ast::Decls::Declare { definition: nash_ast::Def::Def { args, .. }, diff --git a/crates/nash-driver/src/compile/nitpick_tests.rs b/crates/nash-driver/src/compile/nitpick_tests.rs index 3dab5f45..9a7b0e53 100644 --- a/crates/nash-driver/src/compile/nitpick_tests.rs +++ b/crates/nash-driver/src/compile/nitpick_tests.rs @@ -37,7 +37,7 @@ fn incomplete_case_fails_module() { ); let message = rejected(source); assert!( - message.contains("MISSING PATTERNS") && message.contains("False"), + message.contains("nash::pattern::incomplete") && message.contains("False"), "{message}" ); insta::assert_snapshot!(message); @@ -57,7 +57,7 @@ fn redundant_case_fails_module() { ); let message = rejected(source); assert!( - message.contains("REDUNDANT PATTERN") && message.contains("2nd pattern"), + message.contains("nash::pattern::redundant") && message.contains("2nd pattern"), "{message}" ); insta::assert_snapshot!(message); @@ -66,7 +66,10 @@ fn redundant_case_fails_module() { #[test] fn unsafe_argument_fails_module() { let message = rejected("module Main exposing (..)\nf (x :: _) = x\n"); - assert!(message.contains("function arguments"), "{message}"); + assert!( + message.contains("Argument pattern is not exhaustive"), + "{message}" + ); insta::assert_snapshot!(message); } @@ -83,7 +86,7 @@ fn unsafe_destructure_fails_module() { "# )); assert!( - message.contains("only if there is ONE possibility"), + message.contains("Binding pattern is not exhaustive"), "{message}" ); insta::assert_snapshot!(message); @@ -103,7 +106,7 @@ fn trait_default_without_top_level_definitions_fails_module() { "# )); assert!( - message.contains("MISSING PATTERNS") && message.contains("False"), + message.contains("nash::pattern::incomplete") && message.contains("False"), "{message}" ); insta::assert_snapshot!(message); @@ -122,7 +125,8 @@ fn impl_method_fails_module() { "# )); assert!( - message.contains("UNSAFE PATTERN") && message.contains("function arguments"), + message.contains("nash::pattern::incomplete") + && message.contains("Argument pattern is not exhaustive"), "{message}" ); insta::assert_snapshot!(message); @@ -138,8 +142,8 @@ fn type_errors_precede_nitpick() { f True = True "# )); - assert!(message.contains("TYPE MISMATCH"), "{message}"); - assert!(!message.contains("MISSING PATTERNS"), "{message}"); + assert!(message.contains("nash::type::mismatch"), "{message}"); + assert!(!message.contains("nash::pattern::incomplete"), "{message}"); } #[test] @@ -173,7 +177,7 @@ fn rejected_module_publishes_no_interface_to_dependents() { panic!("base must fail") }; let message = report_text(reports); - assert!(message.contains("UNSAFE PATTERN"), "{message}"); + assert!(message.contains("nash::pattern::incomplete"), "{message}"); assert!( matches!(&result.modules[&url("Main")], ModuleResult::Blocked { dependencies } if dependencies == &[url("Base")]) ); diff --git a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__impl_method_fails_module.snap b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__impl_method_fails_module.snap index 03d17767..75d21a4e 100644 --- a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__impl_method_fails_module.snap +++ b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__impl_method_fails_module.snap @@ -2,18 +2,16 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -UNSAFE PATTERN +nash::pattern::incomplete - × This pattern does not cover all possibilities: + × Argument pattern is not exhaustive. ╭─[/Main.nash:6:12] 5 │ impl Choose bool where 6 │ choose True = () · ──── ╰──── - help: Other possibilities include: + help: Missing patterns: False - I would have to crash if I saw one of those! So rather than pattern matching in - function arguments, put a `case` in the function body to account for all - possibilities. + Use a case expression in the function body to handle the missing patterns. diff --git a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__incomplete_case_fails_module.snap b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__incomplete_case_fails_module.snap index 9c5ef837..3ea0b3d9 100644 --- a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__incomplete_case_fails_module.snap +++ b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__incomplete_case_fails_module.snap @@ -2,20 +2,16 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -MISSING PATTERNS +nash::pattern::incomplete - × This `case` does not have branches for all possibilities: + × Case expression is not exhaustive. ╭─[/Main.nash:4:5] 3 │ f x = 4 │ ╭─▶ case x of 5 │ ╰─▶ True -> () ╰──── - help: Missing possibilities include: + help: Missing patterns: False - I would have to crash if I saw one of those. Add branches for them! - - Hint: If you want to write the code for each branch later, use `todo` as a - placeholder. Read for more - guidance on this workflow. + Add the missing branches; use `todo` for unfinished bodies. diff --git a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__redundant_case_fails_module.snap b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__redundant_case_fails_module.snap index a1c6a34b..5f0ac4ad 100644 --- a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__redundant_case_fails_module.snap +++ b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__redundant_case_fails_module.snap @@ -2,9 +2,9 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -REDUNDANT PATTERN +nash::pattern::redundant - × The 2nd pattern is redundant: + × The 2nd pattern is unreachable. ╭─[/Main.nash:6:9] 3 │ f x = 4 │ case x of @@ -12,5 +12,4 @@ REDUNDANT PATTERN 6 │ True -> () · ───── ╰──── - help: Any value with this shape will be handled by a previous pattern, so it should be - removed. + help: Remove it; earlier patterns cover every matching value. diff --git a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__trait_default_without_top_level_definitions_fails_module.snap b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__trait_default_without_top_level_definitions_fails_module.snap index 1ae07468..85eb2f19 100644 --- a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__trait_default_without_top_level_definitions_fails_module.snap +++ b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__trait_default_without_top_level_definitions_fails_module.snap @@ -2,20 +2,16 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -MISSING PATTERNS +nash::pattern::incomplete - × This `case` does not have branches for all possibilities: + × Case expression is not exhaustive. ╭─[/Main.nash:6:9] 5 │ choose _ flag = 6 │ ╭─▶ case flag of 7 │ ╰─▶ True -> () ╰──── - help: Missing possibilities include: + help: Missing patterns: False - I would have to crash if I saw one of those. Add branches for them! - - Hint: If you want to write the code for each branch later, use `todo` as a - placeholder. Read for more - guidance on this workflow. + Add the missing branches; use `todo` for unfinished bodies. diff --git a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_argument_fails_module.snap b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_argument_fails_module.snap index 7fe6e331..3103e280 100644 --- a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_argument_fails_module.snap +++ b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_argument_fails_module.snap @@ -2,18 +2,16 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -UNSAFE PATTERN +nash::pattern::incomplete - × This pattern does not cover all possibilities: + × Argument pattern is not exhaustive. ╭─[/Main.nash:2:4] 1 │ module Main exposing (..) 2 │ f (x :: _) = x · ────── ╰──── - help: Other possibilities include: + help: Missing patterns: [] - I would have to crash if I saw one of those! So rather than pattern matching in - function arguments, put a `case` in the function body to account for all - possibilities. + Use a case expression in the function body to handle the missing patterns. diff --git a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_destructure_fails_module.snap b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_destructure_fails_module.snap index 65b9a3ed..b1889d98 100644 --- a/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_destructure_fails_module.snap +++ b/crates/nash-driver/src/compile/snapshots/nash_driver__compile__nitpick_tests__unsafe_destructure_fails_module.snap @@ -2,37 +2,28 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -UNSAFE PATTERN +nash::pattern::incomplete - × This pattern does not cover all possible values: + × Binding pattern is not exhaustive. ╭─[/Main.nash:4:10] 3 │ let 4 │ (x :: rest) = xs · ───────── 5 │ in ╰──── - help: Other possibilities include: + help: Missing patterns: [] - I would have to crash if I saw one of those! You can use `let` to deconstruct - values only if there is ONE possibility. Switch to a `case` expression to - account for all possibilities. + Use a case expression to handle the missing patterns. - Hint: Are you calling a function that definitely returns values with a very - specific shape? Try making the return type of that function more specific! +nash::warning::unused_definition -unused definition - - ⚠ You are not using `rest` anywhere. + ⚠ Unused definition `rest`. ╭─[/Main.nash:4:15] 3 │ let 4 │ (x :: rest) = xs · ──── 5 │ in ╰──── - help: Is there a typo? Maybe you intended to use `rest` somewhere but typed another - name instead? - - If you are sure there is no typo, remove the definition. This way future readers - will not have to wonder why it is there! + help: Remove the definition if it is not needed. diff --git a/crates/nash-driver/src/error.rs b/crates/nash-driver/src/error.rs index 42ff1fdc..8dfd8670 100644 --- a/crates/nash-driver/src/error.rs +++ b/crates/nash-driver/src/error.rs @@ -49,9 +49,6 @@ pub enum DriverError { #[error("module not found: {module}")] ModuleNotFound { module: String }, - #[error("failed to serialize interface: {0}")] - SerializeError(#[from] bincode::Error), - #[error("invalid module path: {path}")] InvalidModulePath { path: PathBuf }, } diff --git a/crates/nash-driver/src/interface.rs b/crates/nash-driver/src/interface.rs index d657888c..7354c4a2 100644 --- a/crates/nash-driver/src/interface.rs +++ b/crates/nash-driver/src/interface.rs @@ -1,22 +1,13 @@ -//! Interface file serialization for incremental compilation. -//! -//! Interfaces capture the public API of a module, allowing downstream -//! modules to be skipped during recompilation if their dependencies' -//! interfaces haven't changed. +//! In-memory summaries of public exports and module contracts. -use serde::{Deserialize, Serialize}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; - -use crate::error::DriverError; /// Module interface for incremental compilation. /// /// Contains the public exports of a module and a fingerprint /// for change detection. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Interface { /// Module name (e.g., "Json.Decode"). pub module_name: String, @@ -29,7 +20,7 @@ pub struct Interface { } /// An exported item from a module. -#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Export { /// A value export (function or constant). Value { @@ -102,33 +93,6 @@ impl Interface { result } - /// Load an interface from a file. - pub fn load(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|source| DriverError::ReadError { - path: path.to_path_buf(), - source, - })?; - - bincode::deserialize(&bytes).map_err(DriverError::SerializeError) - } - - /// Save the interface to a file. - pub fn save(&self, path: &Path) -> Result<(), DriverError> { - // Create parent directories if needed - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|source| DriverError::WriteError { - path: parent.to_path_buf(), - source, - })?; - } - - let bytes = bincode::serialize(self)?; - std::fs::write(path, bytes).map_err(|source| DriverError::WriteError { - path: path.to_path_buf(), - source, - }) - } - /// Check if this interface differs from another. pub fn differs_from(&self, other: &Interface) -> bool { self.fingerprint != other.fingerprint @@ -142,99 +106,23 @@ fn compute_fingerprint(exports: &[Export]) -> u64 { hasher.finish() } -/// Metadata about a compiled module for caching decisions. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModuleMeta { - /// Source file modification time. - pub source_time: SystemTime, - - /// Build ID when this module was last compiled. - pub last_compile: u64, - - /// Hash of the generated interface. - pub interface_hash: u64, -} - -impl ModuleMeta { - /// Create metadata for a newly compiled module. - pub fn new(source_time: SystemTime, build_id: u64, interface_hash: u64) -> Self { - ModuleMeta { - source_time, - last_compile: build_id, - interface_hash, - } +fn export_name(export: &Export) -> &str { + match export { + Export::Value { name } | Export::Type { name, .. } => name, } } -/// Cache directory manager for interface files. -pub struct InterfaceCache { - /// Root directory for cached interfaces (e.g., `.nash/interfaces/`). - cache_dir: PathBuf, - - /// Current build ID (incremented each build). - build_id: u64, -} - -impl InterfaceCache { - /// Create a new interface cache in the given directory. - pub fn new(project_root: &Path) -> Self { - let cache_dir = project_root.join(".nash").join("interfaces"); - InterfaceCache { - cache_dir, - build_id: 0, - } - } - - /// Start a new build, incrementing the build ID. - pub fn start_build(&mut self) -> u64 { - self.build_id += 1; - self.build_id - } - - /// Get the cache path for a module. - pub fn cache_path(&self, module_name: &str) -> PathBuf { - // Convert module name to path: "Json.Decode" -> "Json/Decode.nashi" - let relative = module_name.replace('.', "/"); - self.cache_dir.join(format!("{}.nashi", relative)) - } - - /// Load a cached interface for a module. - pub fn load(&self, module_name: &str) -> Option { - let path = self.cache_path(module_name); - Interface::load(&path).ok() - } - - /// Save an interface to the cache. - pub fn save(&self, interface: &Interface) -> Result<(), DriverError> { - let path = self.cache_path(&interface.module_name); - interface.save(&path) - } - - /// Check if a module needs to be rebuilt. - /// - /// A module needs rebuilding if: - /// - Source file changed (mtime is newer) - /// - Any dependency's interface changed since last compile - pub fn needs_rebuild( - &self, - meta: &ModuleMeta, - current_source_time: SystemTime, - dep_metas: &[&ModuleMeta], - ) -> bool { - // Source file changed? - if current_source_time > meta.source_time { - return true; - } - - // Any dependency interface changed after our last compile? - for dep in dep_metas { - if dep.last_compile > meta.last_compile { - return true; +fn render_kind(kind: &nash_ast::Kind<'_>) -> String { + fn render(kind: &nash_ast::Kind<'_>, argument: bool) -> String { + match kind { + nash_ast::Kind::Type => "Type".into(), + nash_ast::Kind::Arrow(from, to) => { + let text = format!("{} -> {}", render(from, true), render(to, false)); + if argument { format!("({text})") } else { text } } } - - false } + render(kind, false) } #[cfg(test)] @@ -270,62 +158,4 @@ mod tests { assert!(!iface1.differs_from(&iface2)); } - - #[test] - fn test_cache_path() { - let cache = InterfaceCache::new(Path::new("/project")); - - assert_eq!( - cache.cache_path("Main"), - PathBuf::from("/project/.nash/interfaces/Main.nashi") - ); - - assert_eq!( - cache.cache_path("Json.Decode"), - PathBuf::from("/project/.nash/interfaces/Json/Decode.nashi") - ); - } -} - -#[cfg(test)] -mod kind_tests { - use super::*; - - #[test] - fn kind_interfaces_round_trip() { - let root = std::env::temp_dir().join(format!("nash-kind-interface-{}", std::process::id())); - let cache = InterfaceCache::new(&root); - let original = Interface::new( - "Kinds".into(), - vec![Export::Type { - name: "list".into(), - constructors_exposed: false, - kind: "Type -> Type".into(), - }], - ); - cache.save(&original).unwrap(); - let loaded = cache.load("Kinds").expect("saved interface loads"); - assert_eq!(loaded.fingerprint, original.fingerprint); - assert_eq!(loaded.exports, original.exports); - std::fs::remove_dir_all(root).unwrap(); - } -} - -fn export_name(export: &Export) -> &str { - match export { - Export::Value { name } | Export::Type { name, .. } => name, - } -} - -fn render_kind(kind: &nash_ast::Kind<'_>) -> String { - fn render(kind: &nash_ast::Kind<'_>, argument: bool) -> String { - match kind { - nash_ast::Kind::Type => "Type".into(), - nash_ast::Kind::Arrow(from, to) => { - let text = format!("{} -> {}", render(from, true), render(to, false)); - if argument { format!("({text})") } else { text } - } - } - } - render(kind, false) } diff --git a/crates/nash-driver/src/lib.rs b/crates/nash-driver/src/lib.rs index 739a5047..4b2517ae 100644 --- a/crates/nash-driver/src/lib.rs +++ b/crates/nash-driver/src/lib.rs @@ -54,6 +54,6 @@ pub use compile::{BuildResult, ModuleResult, build, build_graph}; pub use database::Database; pub use error::DriverError; pub use graph::DepGraph; -pub use interface::{Export, Interface, InterfaceCache, ModuleMeta}; +pub use interface::{Export, Interface}; pub use project::{ModuleOrigins, Project, ProjectMember}; pub use source::{FileSource, FileSystemSource, InMemorySource, OverlaySource}; diff --git a/crates/nash-driver/src/snapshots/nash_driver__compile__trait_tests__driver_reports_orphan_and_overlap_at_the_impl_module.snap b/crates/nash-driver/src/snapshots/nash_driver__compile__trait_tests__driver_reports_orphan_and_overlap_at_the_impl_module.snap index e7dd108e..0269be85 100644 --- a/crates/nash-driver/src/snapshots/nash_driver__compile__trait_tests__driver_reports_orphan_and_overlap_at_the_impl_module.snap +++ b/crates/nash-driver/src/snapshots/nash_driver__compile__trait_tests__driver_reports_orphan_and_overlap_at_the_impl_module.snap @@ -2,7 +2,7 @@ source: crates/nash-driver/src/compile.rs expression: "diagnostics.join(\"\\n\")" --- -orphan: ORPHAN IMPL +orphan: nash::names::orphan_impl × This module cannot define an impl of `Methods.Keep` for Types.Token: ╭─[/Bad.nash:4:1] @@ -13,7 +13,7 @@ orphan: ORPHAN IMPL help: An impl must be defined in the module that defines its trait or one of its head types. Move this impl to one of those modules. -overlap: OVERLAPPING IMPL +overlap: nash::names::overlapping_impls × These `Bad.Keep` impls can both match the same trait arguments. The overlapping │ head is Builtin.unit: @@ -26,6 +26,5 @@ overlap: OVERLAPPING IMPL 7 │ ├─▶ keep x = x · ╰──── overlapping impl in `Bad` ╰──── - help: I cannot choose which impl to use. Remove one of them, or change their heads so - they cannot match the same trait arguments. Adding different context constraints - does not disambiguate overlapping heads. + help: Remove one impl or make their heads disjoint; context constraints do not + disambiguate heads. diff --git a/crates/nash-language-server/src/diagnostics.rs b/crates/nash-language-server/src/diagnostics.rs index 1dca6e1a..57ba383f 100644 --- a/crates/nash-language-server/src/diagnostics.rs +++ b/crates/nash-language-server/src/diagnostics.rs @@ -1,83 +1,144 @@ //! Convert compiler reports to LSP without changing their primary spans. use nash_region::{Position as NashPosition, Region}; -use nash_report::{Report, Severity, Snippet, Source}; +use nash_report::{Report, Severity, Source}; use tower_lsp_server::ls_types::{ Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString, Position, Range, Uri, }; pub fn to_lsp(report: &Report, source: &Source<'_>, uri: &Uri) -> Diagnostic { - let related = match &report.snippet { - Snippet::Pair { first, .. } => Some((first.region, first.text.clone())), - Snippet::Region { - highlight: Some(highlight), - .. - } if *highlight != report.region => Some((*highlight, "Related source".into())), - _ => None, - }; - Diagnostic { - range: to_range(report.region, source), + checked_to_lsp(report, source, uri).unwrap_or_else(|| Diagnostic { + severity: Some(DiagnosticSeverity::ERROR), + source: Some("nash".into()), + message: "Cannot represent this diagnostic's source position in LSP: line or UTF-16 column exceeds the protocol's 32-bit limit.".into(), + ..Diagnostic::default() + }) +} + +fn checked_to_lsp(report: &Report, source: &Source<'_>, uri: &Uri) -> Option { + let mut related = Vec::new(); + for label in &report.labels { + related.push(DiagnosticRelatedInformation { + location: Location { + uri: uri.clone(), + range: to_range(label.region, source)?, + }, + message: label.text.clone(), + }); + } + related_reports(&report.related, uri, &mut related)?; + let related_information = (!related.is_empty()).then_some(related); + Some(Diagnostic { + range: to_range(report.region, source)?, severity: Some(match report.severity { Severity::Error => DiagnosticSeverity::ERROR, Severity::Warning => DiagnosticSeverity::WARNING, }), - code: Some(NumberOrString::String(report.title.clone())), + code: Some(NumberOrString::String(report.code.to_string())), source: Some("nash".into()), - message: format!( - "{}\n\n{}", - report.before.render(80, false), - report.after.render(80, false) - ), - related_information: related.map(|(region, message)| { - vec![DiagnosticRelatedInformation { + message: report.message(), + related_information, + data: (!report.suggestions.is_empty()).then(|| serde_json::json!(report.suggestions)), + ..Diagnostic::default() + }) +} + +fn related_reports( + modules: &[nash_report::ModuleReports], + base_uri: &Uri, + output: &mut Vec, +) -> Option<()> { + for module in modules { + let path = std::path::Path::new(&module.path); + let path = if path.is_absolute() { + path.to_owned() + } else { + let base = url::Url::parse(base_uri.as_str()) + .ok()? + .to_file_path() + .ok()?; + base.parent()?.join(path) + }; + let uri: Uri = url::Url::from_file_path(path).ok()?.as_str().parse().ok()?; + let source = Source::new(&module.source); + for report in &module.reports { + output.push(DiagnosticRelatedInformation { location: Location { uri: uri.clone(), - range: to_range(region, source), + range: to_range(report.region, &source)?, }, - message, - }] - }), - data: (!report.suggestions.is_empty()).then(|| serde_json::json!(report.suggestions)), - ..Diagnostic::default() + message: report.message(), + }); + for label in &report.labels { + output.push(DiagnosticRelatedInformation { + location: Location { + uri: uri.clone(), + range: to_range(label.region, &source)?, + }, + message: label.text.clone(), + }); + } + related_reports(&report.related, &uri, output)?; + } } + Some(()) } -pub fn to_range(region: Region, source: &Source<'_>) -> Range { - Range::new( - to_position(region.start, source), - to_position(region.end, source), - ) +pub fn to_range(region: Region, source: &Source<'_>) -> Option { + Some(Range::new( + to_position(region.start, source)?, + to_position(region.end, source)?, + )) } -fn to_position(position: NashPosition, source: &Source<'_>) -> Position { +fn to_position(position: NashPosition, source: &Source<'_>) -> Option { let offset = source.offset(position); let prefix = &source.text()[..offset]; - let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32; + let line = prefix.bytes().filter(|&b| b == b'\n').count(); let character = prefix .rsplit('\n') .next() .unwrap_or("") .encode_utf16() - .count() as u32; - Position::new(line, character) + .count(); + protocol_position(line, character) +} + +fn protocol_position(line: usize, character: usize) -> Option { + Some(Position::new( + line.try_into().ok()?, + character.try_into().ok()?, + )) } #[cfg(test)] mod tests { use super::*; use nash_report::{Doc, Label}; - fn region(sr: u16, sc: u16, er: u16, ec: u16) -> Region { + fn region(sr: usize, sc: usize, er: usize, ec: usize) -> Region { Region::new(NashPosition::new(sr, sc), NashPosition::new(er, ec)) } #[test] + fn protocol_positions_do_not_truncate() { + let max = u32::MAX as usize; + assert_eq!( + protocol_position(max, max), + Some(Position::new(u32::MAX, u32::MAX)) + ); + if let Some(too_large) = max.checked_add(1) { + assert_eq!(protocol_position(too_large, 0), None); + assert_eq!(protocol_position(0, too_large), None); + } + } + #[test] fn ranges_count_utf16_surrogate_pairs() { let source = Source::new("a😀éz\nlast"); assert_eq!( - to_range(region(1, 6, 1, 8), &source), + to_range(region(1, 6, 1, 8), &source).unwrap(), Range::new(Position::new(0, 3), Position::new(0, 4)) ); assert_eq!( - to_range(region(2, 5, 2, 5), &source), + to_range(region(2, 5, 2, 5), &source).unwrap(), Range::new(Position::new(1, 4), Position::new(1, 4)) ); } @@ -99,12 +160,15 @@ mod tests { ); let source = Source::new("x\nx"); let diagnostic = to_lsp(&report, &source, &uri); - assert_eq!(diagnostic.range, to_range(report.region, &source)); + assert_eq!(diagnostic.range, to_range(report.region, &source).unwrap()); assert_eq!( diagnostic.related_information.unwrap()[0].location.range, - to_range(region(1, 1, 1, 2), &source) + to_range(region(1, 1, 1, 2), &source).unwrap() + ); + assert_eq!( + diagnostic.message, + "Duplicate names:\n\nsecond name\n\nRename one." ); - assert_eq!(diagnostic.message, "Duplicate names:\n\nRename one."); } #[test] fn highlighted_region_and_suggestions_survive() { @@ -122,8 +186,75 @@ mod tests { let diagnostic = to_lsp(&report, &source, &uri); assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING)); assert_eq!(diagnostic.data, Some(serde_json::json!(["known"]))); - assert!(diagnostic.related_information.is_some()); - report.snippet = Snippet::None; + assert_eq!( + diagnostic.range, + to_range(region(1, 5, 1, 12), &source).unwrap() + ); + report = report.without_source(); assert!(to_lsp(&report, &source, &uri).related_information.is_none()); } } + +#[cfg(test)] +mod structured_tests { + use super::*; + use nash_report::{Doc, Label, ModuleReports}; + fn region(column: usize) -> Region { + Region::new( + NashPosition::new(1, column), + NashPosition::new(1, column + 1), + ) + } + #[test] + fn labels_related_files_and_codes_survive_conversion() { + let mut report = Report::snippet( + "OLD TITLE", + region(5), + None, + Doc::text("Type mismatch."), + Doc::Empty, + ) + .with_code("nash::type::mismatch") + .with_label(Label { + region: region(1), + text: "first requirement".into(), + }) + .with_label(Label { + region: region(3), + text: "second requirement".into(), + }); + report.title = "A new display title".into(); + report.primary_label = Some("argument 2 of `f`".into()); + let mut origin = Report::snippet( + "ORIGIN", + region(5), + None, + Doc::text("Declared here."), + Doc::Empty, + ); + origin.primary_label = Some("annotation for `f`".into()); + let report = report.with_related(ModuleReports { + name: "Other".into(), + path: "Other #.nash".into(), + source: "a b c".into(), + reports: vec![origin], + }); + let uri: Uri = "file:///project/Main.nash".parse().unwrap(); + let diagnostic = to_lsp(&report, &Source::new("a b c"), &uri); + assert_eq!( + diagnostic.code, + Some(NumberOrString::String("nash::type::mismatch".into())) + ); + assert!( + diagnostic.message.contains("argument 2 of `f`"), + "{diagnostic:?}" + ); + let related = diagnostic.related_information.unwrap(); + assert_eq!(related.len(), 3); + assert_eq!( + related[2].location.uri.as_str(), + "file:///project/Other%20%23.nash" + ); + assert!(related[2].message.contains("annotation for `f`")); + } +} diff --git a/crates/nash-language-server/src/workspace.rs b/crates/nash-language-server/src/workspace.rs index 65955e98..6d5db528 100644 --- a/crates/nash-language-server/src/workspace.rs +++ b/crates/nash-language-server/src/workspace.rs @@ -329,7 +329,7 @@ mod tests { for (diagnostic, problem) in lsp.iter().zip(problems) { assert_eq!( serde_json::to_value(&diagnostic.code).unwrap(), - problem["title"] + problem["code"] ); assert_eq!( diagnostic.range.start.line + 1, diff --git a/crates/nash-parse/src/bytes.rs b/crates/nash-parse/src/bytes.rs index 4235cc6b..0d0aef85 100644 --- a/crates/nash-parse/src/bytes.rs +++ b/crates/nash-parse/src/bytes.rs @@ -19,7 +19,11 @@ impl<'a> Parser<'a> { loop { match self.peek() { None | Some(b'\n') => { - return Err(to_error(error::Bytes::Endless, self.row(), self.col())); + return Err(to_error( + error::Bytes::Endless(nash_region::Position::new(row, col)), + self.row(), + self.col(), + )); } Some(b'"') => break, Some(b) if b.is_ascii_hexdigit() => self.advance(), diff --git a/crates/nash-parse/src/declaration/attribute.rs b/crates/nash-parse/src/declaration/attribute.rs index bcb87475..8ec1de32 100644 --- a/crates/nash-parse/src/declaration/attribute.rs +++ b/crates/nash-parse/src/declaration/attribute.rs @@ -1,5 +1,5 @@ use bumpalo::collections::Vec as BumpVec; -use nash_region::Located; +use nash_region::{Located, Position}; use nash_source::{Attribute, Expr}; use crate::Parser; @@ -36,17 +36,18 @@ impl<'a> Parser<'a> { let name = self.add_end(name_start, name); let args = self.one_of_with_fallback( vec![Box::new(|parser: &mut Parser<'a>| { - parser.word1(b'(', error::Attribute::End)?; + let opening = parser.get_position(); + parser.word1(b'(', |r, c| error::Attribute::End(opening, r, c))?; parser .chomp_and_check_indent(error::Attribute::Space, error::Attribute::IndentArg)?; parser.one_of( - error::Attribute::End, + |r, c| error::Attribute::End(opening, r, c), vec![ Box::new(|parser: &mut Parser<'a>| { - parser.word1(b')', error::Attribute::End)?; + parser.word1(b')', |r, c| error::Attribute::End(opening, r, c))?; Ok(&[][..]) }), - Box::new(|parser| parser.attribute_args()), + Box::new(|parser| parser.attribute_args(opening)), ], ) })], @@ -57,7 +58,10 @@ impl<'a> Parser<'a> { Ok(self.alloc(Attribute { name, args })) } - fn attribute_args(&mut self) -> Result<&'a [&'a Located>], error::Attribute<'a>> { + fn attribute_args( + &mut self, + opening: Position, + ) -> Result<&'a [&'a Located>], error::Attribute<'a>> { let mut args = BumpVec::new_in(self.bump); loop { let (arg, end) = self.specialize( @@ -65,12 +69,14 @@ impl<'a> Parser<'a> { |parser| parser.expression(), )?; args.push(arg); - self.check_indent(end.line, end.column, error::Attribute::IndentEnd)?; + self.check_indent(end.line, end.column, |r, c| { + error::Attribute::IndentEnd(opening, r, c) + })?; let done = self.one_of( - error::Attribute::End, + |r, c| error::Attribute::End(opening, r, c), vec![ Box::new(|parser: &mut Parser<'a>| { - parser.word1(b',', error::Attribute::End)?; + parser.word1(b',', |r, c| error::Attribute::End(opening, r, c))?; parser.chomp_and_check_indent( error::Attribute::Space, error::Attribute::IndentArg, @@ -78,7 +84,7 @@ impl<'a> Parser<'a> { Ok(false) }), Box::new(|parser| { - parser.word1(b')', error::Attribute::End)?; + parser.word1(b')', |r, c| error::Attribute::End(opening, r, c))?; Ok(true) }), ], diff --git a/crates/nash-parse/src/declaration/infix.rs b/crates/nash-parse/src/declaration/infix.rs index 21141cb2..ba44454d 100644 --- a/crates/nash-parse/src/declaration/infix.rs +++ b/crates/nash-parse/src/declaration/infix.rs @@ -103,7 +103,7 @@ impl<'a> Parser<'a> { /// Parse a precedence digit (0-9). /// /// Mirrors Elm's `Number.precedence`. - fn precedence(&mut self, to_error: impl FnOnce(u16, u16) -> E) -> Result { + fn precedence(&mut self, to_error: impl FnOnce(usize, usize) -> E) -> Result { match self.peek() { Some(b) if b.is_ascii_digit() => { let value = (b - b'0') as u16; @@ -129,7 +129,7 @@ mod tests { let bump = bumpalo::Bump::new(); let src = concat!($src, "\n"); let src_in_arena = bump.alloc_str(src); - let mut parser = Parser::new(&bump, src_in_arena.as_bytes()); + let mut parser = Parser::new(&bump, src_in_arena); match parser.infix_decl() { Ok(infix) => { insta::with_settings!({ diff --git a/crates/nash-parse/src/declaration/mod.rs b/crates/nash-parse/src/declaration/mod.rs index 795e8431..8536d398 100644 --- a/crates/nash-parse/src/declaration/mod.rs +++ b/crates/nash-parse/src/declaration/mod.rs @@ -150,7 +150,7 @@ macro_rules! assert_decl_snapshot { let bump = bumpalo::Bump::new(); let src = indoc::indoc!($src); let src_in_arena = bump.alloc_str(src); - let mut parser = crate::Parser::new(&bump, src_in_arena.as_bytes()); + let mut parser = crate::Parser::new(&bump, src_in_arena); match parser.declaration() { Ok((decl, _end)) => { parser.chomp(|_, _, _| ()).expect("expected trailing space"); @@ -173,7 +173,7 @@ macro_rules! assert_decl_error_snapshot { let bump = bumpalo::Bump::new(); let src = indoc::indoc!($src); let src_in_arena = bump.alloc_str(src); - let mut parser = crate::Parser::new(&bump, src_in_arena.as_bytes()); + let mut parser = crate::Parser::new(&bump, src_in_arena); let error = parser.declaration().expect_err("expected declaration parse error"); insta::with_settings!({ description => src, diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__error_unclosed.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__error_unclosed.snap index 314e68a9..26df3baa 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__error_unclosed.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__error_unclosed.snap @@ -4,6 +4,10 @@ description: "@derive(Eq\ntype T = A" --- Attribute( IndentEnd( + Position { + line: 1, + column: 8, + }, 1, 11, ), diff --git a/crates/nash-parse/src/declaration/union.rs b/crates/nash-parse/src/declaration/union.rs index af287e3f..1d65a02f 100644 --- a/crates/nash-parse/src/declaration/union.rs +++ b/crates/nash-parse/src/declaration/union.rs @@ -111,10 +111,11 @@ impl<'a> Parser<'a> { let (arguments, end) = if self.peek() == Some(b'{') { self.check_indent(name_end.line, name_end.column, CustomType::IndentField)?; + let opening = self.get_position(); self.advance(); self.chomp_and_check_indent(CustomType::Space, CustomType::IndentField)?; - let first = self.ctor_field()?; - let fields = self.ctor_fields_end(first)?; + let first = self.ctor_field(opening)?; + let fields = self.ctor_fields_end(first, opening)?; let end = self.get_position(); self.chomp(CustomType::Space)?; (CtorArgs::Labeled(fields), end) @@ -130,7 +131,7 @@ impl<'a> Parser<'a> { Ok((ctor, end)) } - fn ctor_field(&mut self) -> Result, CustomType<'a>> { + fn ctor_field(&mut self, opening: Position) -> Result, CustomType<'a>> { let name_start = self.get_position(); let name = self.lower_name(CustomType::Field)?; let name = self.add_end(name_start, name); @@ -141,29 +142,32 @@ impl<'a> Parser<'a> { |bump, e, row, col| CustomType::FieldType(bump.alloc(e), row, col), |p| p.type_expr(), )?; - self.check_indent(end.line, end.column, CustomType::FieldEnd)?; + self.check_indent(end.line, end.column, |r, c| { + CustomType::FieldEnd(opening, r, c) + })?; Ok((name, typ)) } fn ctor_fields_end( &mut self, first: CtorField<'a>, + opening: Position, ) -> Result<&'a [CtorField<'a>], CustomType<'a>> { let mut fields = BumpVec::new_in(self.bump); fields.push(first); loop { self.chomp(CustomType::Space)?; let done = self.one_of( - CustomType::FieldEnd, + |r, c| CustomType::FieldEnd(opening, r, c), vec![ Box::new(|p: &mut Parser<'a>| { - p.word1(b',', CustomType::FieldEnd)?; + p.word1(b',', |r, c| CustomType::FieldEnd(opening, r, c))?; p.chomp_and_check_indent(CustomType::Space, CustomType::IndentField)?; - fields.push(p.ctor_field()?); + fields.push(p.ctor_field(opening)?); Ok(false) }), Box::new(|p: &mut Parser<'a>| { - p.word1(b'}', CustomType::FieldEnd)?; + p.word1(b'}', |r, c| CustomType::FieldEnd(opening, r, c))?; Ok(true) }), ], @@ -213,23 +217,26 @@ impl<'a> Parser<'a> { fn chomp_variants( &mut self, mut variants: Vec<&'a Ctor<'a>>, - end: Position, + mut end: Position, ) -> Result<(Vec<&'a Ctor<'a>>, Position), CustomType<'a>> { - let variants_for_fallback = variants.clone(); - - self.one_of_with_fallback( - vec![Box::new(|p: &mut Parser<'a>| { - p.check_indent(end.line, end.column, CustomType::IndentBar)?; - p.word1(b'|', CustomType::Bar)?; - p.chomp_and_check_indent(CustomType::Space, CustomType::IndentAfterBar)?; - - let (variant, new_end) = p.variant()?; - variants.push(variant); - - p.chomp_variants(variants, new_end) - })], - (variants_for_fallback, end), - ) + loop { + let next = self.one_of_with_fallback( + vec![Box::new(|p: &mut Parser<'a>| { + p.check_indent(end.line, end.column, CustomType::IndentBar)?; + p.word1(b'|', CustomType::Bar)?; + p.chomp_and_check_indent(CustomType::Space, CustomType::IndentAfterBar)?; + p.variant().map(Some) + })], + None, + )?; + match next { + Some((variant, new_end)) => { + variants.push(variant); + end = new_end; + } + None => return Ok((variants, end)), + } + } } } diff --git a/crates/nash-parse/src/error.rs b/crates/nash-parse/src/error.rs index de053c9a..676198b3 100644 --- a/crates/nash-parse/src/error.rs +++ b/crates/nash-parse/src/error.rs @@ -7,6 +7,7 @@ //! not AST types. They are allocated in the arena like everything else. use crate::{Col, Row}; +use nash_region::Position; // ============================================================================= // Top-level Error @@ -59,7 +60,7 @@ pub enum Tests<'a> { Test(&'a Test<'a>, Row, Col), Start(Row, Col), IndentStart(Row, Col), - Alignment(u16, Row, Col), + Alignment(usize, Row, Col), } #[derive(Debug)] @@ -72,7 +73,7 @@ pub enum Test<'a> { WithinKind(Row, Col), WithinNumber(Number, Row, Col), WithinDuplicate(Row, Col), - WithinEnd(Row, Col), + WithinEnd(Position, Row, Col), Equals(Row, Col), Do(Row, Col), Body(&'a Do<'a>, Row, Col), @@ -86,7 +87,7 @@ pub enum Test<'a> { IndentBody(Row, Col), IndentBinder(Row, Col), IndentIn(Row, Col), - BinderAlignment(u16, Row, Col), + BinderAlignment(usize, Row, Col), } #[derive(Debug)] @@ -96,7 +97,8 @@ pub enum Exposing { Value(Row, Col), Operator(Row, Col), OperatorReserved(BadOperator, Row, Col), - OperatorRightParen(Row, Col), + OperatorRightParen(Position, Row, Col), + TypePrivacyEnd(Position, Row, Col), TypePrivacy(Row, Col), TypeName(Row, Col), End(Row, Col), @@ -131,7 +133,7 @@ pub enum Impl<'a> { IndentHead(Row, Col), IndentWhere(Row, Col), IndentMethod(Row, Col), - Alignment(u16, Row, Col), + Alignment(usize, Row, Col), } #[derive(Debug)] @@ -152,18 +154,18 @@ pub enum Trait<'a> { IndentMethod(Row, Col), IndentColon(Row, Col), IndentType(Row, Col), - Alignment(u16, Row, Col), + Alignment(usize, Row, Col), } #[derive(Debug)] pub enum Attribute<'a> { Name(Row, Col), Arg(&'a Expr<'a>, Row, Col), - End(Row, Col), + End(Position, Row, Col), Space(Space, Row, Col), FreshLine(Row, Col), IndentArg(Row, Col), - IndentEnd(Row, Col), + IndentEnd(Position, Row, Col), } #[derive(Debug)] @@ -216,7 +218,7 @@ pub enum CustomType<'a> { Field(Row, Col), FieldColon(Row, Col), FieldType(&'a Type<'a>, Row, Col), - FieldEnd(Row, Col), + FieldEnd(Position, Row, Col), IndentField(Row, Col), IndentFieldType(Row, Col), } @@ -273,7 +275,7 @@ pub enum Do<'a> { IndentStmt(Row, Col), IndentArrow(Row, Col), IndentExpr(Row, Col), - Alignment(u16, Row, Col), + Alignment(usize, Row, Col), } #[derive(Debug)] @@ -348,7 +350,7 @@ pub enum Case<'a> { IndentPattern(Row, Col), IndentArrow(Row, Col), IndentBranch(Row, Col), - PatternAlignment(u16, Row, Col), + PatternAlignment(usize, Row, Col), } #[derive(Debug)] @@ -371,7 +373,7 @@ pub enum If<'a> { pub enum Let<'a> { Space(Space, Row, Col), In(Row, Col), - DefAlignment(u16, Row, Col), + DefAlignment(usize, Row, Col), DefName(Row, Col), Def(&'a str, &'a Def<'a>, Row, Col), Destruct(&'a Destruct<'a>, Row, Col), @@ -393,7 +395,7 @@ pub enum Def<'a> { IndentEquals(Row, Col), IndentType(Row, Col), IndentBody(Row, Col), - Alignment(u16, Row, Col), + Alignment(usize, Row, Col), } #[derive(Debug)] @@ -428,9 +430,9 @@ pub enum Pattern<'a> { #[derive(Debug)] pub enum Bytes { - Endless, + Endless(Position), OddLength, - BadHexDigit(u16), + BadHexDigit(usize), } #[derive(Debug)] @@ -537,18 +539,18 @@ pub enum TTuple<'a> { #[derive(Debug)] pub enum StringError { - EndlessSingle, - EndlessMulti, + EndlessSingle(Position), + EndlessMulti(Position), Escape(Escape), } #[derive(Debug)] pub enum Escape { Unknown, - BadUnicodeFormat(u16), - BadUnicodeCode(u16), + BadUnicodeFormat(usize), + BadUnicodeCode(usize), BadUnicodeLength { - code: u16, + code: usize, expected: i32, actual: i32, }, @@ -568,8 +570,9 @@ pub enum Number { #[derive(Debug)] pub enum Space { + TooDeep, HasTab, - EndlessMultiComment, + EndlessMultiComment(Position), } #[derive(Debug)] diff --git a/crates/nash-parse/src/exposing.rs b/crates/nash-parse/src/exposing.rs index 15ac795d..9d9aea5c 100644 --- a/crates/nash-parse/src/exposing.rs +++ b/crates/nash-parse/src/exposing.rs @@ -115,7 +115,9 @@ impl<'a> Parser<'a> { let op = p.operator(error::Exposing::Operator, |bad_op, row, col| { error::Exposing::OperatorReserved(bad_op, row, col) })?; - p.word1(b')', error::Exposing::OperatorRightParen)?; + p.word1(b')', |r, c| { + error::Exposing::OperatorRightParen(start, r, c) + })?; let end = p.get_position(); Ok(p.alloc(Exposed::Operator { region: Region::new(start, end), @@ -156,13 +158,14 @@ impl<'a> Parser<'a> { fn privacy(&mut self) -> Result { self.one_of_with_fallback( vec![Box::new(|p: &mut Parser<'a>| { + let opening = p.get_position(); p.word1(b'(', error::Exposing::TypePrivacy)?; p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::TypePrivacy)?; let start = p.get_position(); p.word2(b'.', b'.', error::Exposing::TypePrivacy)?; let end = p.get_position(); p.chomp_and_check_indent(error::Exposing::Space, error::Exposing::TypePrivacy)?; - p.word1(b')', error::Exposing::TypePrivacy)?; + p.word1(b')', |r, c| error::Exposing::TypePrivacyEnd(opening, r, c))?; Ok(Privacy::Public(Region::new(start, end))) })], Privacy::Private, @@ -187,7 +190,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.exposing(); match result { Ok(ref exposing) => { @@ -212,7 +215,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let error = parser.exposing().expect_err("expected exposing parse error"); insta::with_settings!({ description => format!("Code:\n\n{}", input), diff --git a/crates/nash-parse/src/expression/accessor.rs b/crates/nash-parse/src/expression/accessor.rs index 7fff668c..d719d367 100644 --- a/crates/nash-parse/src/expression/accessor.rs +++ b/crates/nash-parse/src/expression/accessor.rs @@ -47,26 +47,22 @@ impl<'a> Parser<'a> { start: Position, expr: &'a Located>, ) -> Result<&'a Located>, error::Expr<'a>> { - self.one_of_with_fallback( - vec![Box::new(|p: &mut Parser<'a>| { - p.word1(b'.', error::Expr::Dot)?; - let pos = p.get_position(); - let field = p.lower_name(error::Expr::Access)?; - let end = p.get_position(); - - let located_field = p.alloc(Located::at(Region::new(pos, end), field)); - let access_expr = p.alloc(Located::at( - Region::new(start, end), - Expr::Access { - record: expr, - field: located_field, - }, - )); - - p.accessible(start, access_expr) - })], - expr, - ) + let mut expr = expr; + while self.peek() == Some(b'.') { + self.advance(); + let pos = self.get_position(); + let field = self.lower_name(error::Expr::Access)?; + let end = self.get_position(); + let field = self.alloc(Located::at(Region::new(pos, end), field)); + expr = self.alloc(Located::at( + Region::new(start, end), + Expr::Access { + record: expr, + field, + }, + )); + } + Ok(expr) } } diff --git a/crates/nash-parse/src/expression/case.rs b/crates/nash-parse/src/expression/case.rs index 599d434d..359df834 100644 --- a/crates/nash-parse/src/expression/case.rs +++ b/crates/nash-parse/src/expression/case.rs @@ -125,25 +125,24 @@ impl<'a> Parser<'a> { fn chomp_case_end( &mut self, mut arms: Vec<&'a CaseArm<'a>>, - end: Position, + mut end: Position, ) -> Result<(Vec<&'a CaseArm<'a>>, Position), Case<'a>> { - // Clone for the fallback - let arms_for_fallback = arms.clone(); - - self.one_of_with_fallback( - vec![Box::new(|p: &mut Parser<'a>| { - // Check alignment for next pattern - p.check_aligned(Case::PatternAlignment)?; - - // Parse the next branch - let (arm, new_end) = p.chomp_case_branch()?; - arms.push(arm); - - // Continue parsing more branches - p.chomp_case_end(arms, new_end) - })], - (arms_for_fallback, end), - ) + loop { + let next = self.one_of_with_fallback( + vec![Box::new(|p: &mut Parser<'a>| { + p.check_aligned(Case::PatternAlignment)?; + p.chomp_case_branch().map(Some) + })], + None, + )?; + match next { + Some((arm, new_end)) => { + arms.push(arm); + end = new_end; + } + None => return Ok((arms, end)), + } + } } } diff --git a/crates/nash-parse/src/expression/do_.rs b/crates/nash-parse/src/expression/do_.rs index f559f940..5e979184 100644 --- a/crates/nash-parse/src/expression/do_.rs +++ b/crates/nash-parse/src/expression/do_.rs @@ -31,7 +31,7 @@ impl<'a> Parser<'a> { } /// Parse aligned statements ending in an expression. - pub(crate) fn do_body(&mut self, parent_indent: u16) -> Result, Do<'a>> { + pub(crate) fn do_body(&mut self, parent_indent: usize) -> Result, Do<'a>> { let (first, mut end) = self.do_stmt()?; let mut statements = vec![first]; @@ -173,7 +173,7 @@ mod tests { let bump = bumpalo::Bump::new(); let indented = crate::test_support::indent_fragment(indoc::indoc!($code)); let source = bump.alloc_str(&indented); - let mut parser = crate::Parser::new(&bump, source.as_bytes()); + let mut parser = crate::Parser::new(&bump, source); parser .chomp(|_, _, _| "space error") .expect("expected leading indent"); diff --git a/crates/nash-parse/src/expression/if_.rs b/crates/nash-parse/src/expression/if_.rs index c7fc475a..8d32b91a 100644 --- a/crates/nash-parse/src/expression/if_.rs +++ b/crates/nash-parse/src/expression/if_.rs @@ -5,7 +5,6 @@ //! Parses: `if cond then branch else branch` //! Also handles: `if c1 then b1 else if c2 then b2 else b3` -use bumpalo::collections::Vec as BumpVec; use nash_region::{Located, Position, Region}; use nash_source::{Expr, IfBranch}; @@ -60,68 +59,54 @@ impl<'a> Parser<'a> { start: Position, mut branches: Vec<&'a IfBranch<'a>>, ) -> Result<(&'a Located>, Position), If<'a>> { - // Parse condition - self.chomp_and_check_indent(If::Space, If::IndentCondition)?; - let (condition, cond_end) = self.if_condition()?; - - // Parse `then` - self.check_indent(cond_end.line, cond_end.column, If::IndentThen)?; - self.keyword_then(If::Then)?; - - // Parse then branch - self.chomp_and_check_indent(If::Space, If::IndentThenBranch)?; - let (then_branch, then_end) = self.if_then_branch()?; - - // Parse `else` - self.check_indent(then_end.line, then_end.column, If::IndentElse)?; - self.keyword_else(If::Else)?; - - // Create the new branch - let branch = self.bump.alloc(IfBranch { - condition, - then_branch, - }); - branches.push(branch); - - // Parse else branch: either `else if ...` or final else expression - self.chomp_and_check_indent(If::Space, If::IndentElseBranch)?; - - // Clone for second closure - let branches_for_else = branches.clone(); - - self.one_of( - If::ElseBranchStart, - vec![ - // `else if ...` - continue the chain - Box::new(|p: &mut Parser<'a>| { - p.keyword_if(If::ElseBranchStart)?; - p.chomp_if_end(start, branches) - }), - // Final else expression - Box::new(|p: &mut Parser<'a>| { - let (else_branch, else_end) = p.if_else_branch()?; - - // Convert branches to bump slice - // Note: Elm reverses because it uses `:` (prepend), we use push (append) - // so our branches are already in correct order - let mut branch_vec: BumpVec<'a, &'a IfBranch<'a>> = BumpVec::new_in(p.bump); - for b in branches_for_else { - branch_vec.push(b); - } - let branches_slice = branch_vec.into_bump_slice(); - - let if_expr = Expr::If { - branches: branches_slice, - final_else: else_branch, - }; - - Ok(( - p.alloc(Located::at(Region::new(start, else_end), if_expr)), - else_end, - )) - }), - ], - ) + loop { + // Parse condition + self.chomp_and_check_indent(If::Space, If::IndentCondition)?; + let (condition, cond_end) = self.if_condition()?; + + // Parse `then` + self.check_indent(cond_end.line, cond_end.column, If::IndentThen)?; + self.keyword_then(If::Then)?; + + // Parse then branch + self.chomp_and_check_indent(If::Space, If::IndentThenBranch)?; + let (then_branch, then_end) = self.if_then_branch()?; + + // Parse `else` + self.check_indent(then_end.line, then_end.column, If::IndentElse)?; + self.keyword_else(If::Else)?; + + // Create the new branch + let branch = self.bump.alloc(IfBranch { + condition, + then_branch, + }); + branches.push(branch); + + // Parse else branch: either `else if ...` or final else expression + self.chomp_and_check_indent(If::Space, If::IndentElseBranch)?; + + let final_else = self.one_of( + If::ElseBranchStart, + vec![ + Box::new(|p: &mut Parser<'a>| { + p.keyword_if(If::ElseBranchStart)?; + Ok(None) + }), + Box::new(|p: &mut Parser<'a>| p.if_else_branch().map(Some)), + ], + )?; + if let Some((else_branch, else_end)) = final_else { + let if_expr = Expr::If { + branches: self.bump.alloc_slice_copy(&branches), + final_else: else_branch, + }; + return Ok(( + self.alloc(Located::at(Region::new(start, else_end), if_expr)), + else_end, + )); + } + } } /// Parse condition expression in an if. diff --git a/crates/nash-parse/src/expression/lambda.rs b/crates/nash-parse/src/expression/lambda.rs index baac75a9..c82887bd 100644 --- a/crates/nash-parse/src/expression/lambda.rs +++ b/crates/nash-parse/src/expression/lambda.rs @@ -91,30 +91,29 @@ impl<'a> Parser<'a> { &mut self, mut args: Vec<&'a Located>>, ) -> Result>>, Func<'a>> { - // Clone for second closure - cheap since it's just a Vec of references - let args_for_arrow = args.clone(); - - // Use one_of to match Elm's error behavior: fallback error is FuncArrow - self.one_of( - Func::Arrow, - vec![ - // Try to parse another pattern arg (Elm tries this first) - Box::new(|p: &mut Parser<'a>| { - let arg = p.specialize( - |bump, e, r, c| Func::Arg(bump.alloc(e), r, c), - |p| p.pattern_term(), - )?; - args.push(arg); - p.chomp_and_check_indent(Func::Space, Func::IndentArrow)?; - p.chomp_lambda_args(args) - }), - // Or parse the arrow to finish - Box::new(|p: &mut Parser<'a>| { - p.word2(b'-', b'>', Func::Arrow)?; - Ok(args_for_arrow) - }), - ], - ) + loop { + let next = self.one_of( + Func::Arrow, + vec![ + Box::new(|p: &mut Parser<'a>| { + let arg = p.specialize( + |bump, e, r, c| Func::Arg(bump.alloc(e), r, c), + |p| p.pattern_term(), + )?; + p.chomp_and_check_indent(Func::Space, Func::IndentArrow)?; + Ok(Some(arg)) + }), + Box::new(|p: &mut Parser<'a>| { + p.word2(b'-', b'>', Func::Arrow)?; + Ok(None) + }), + ], + )?; + match next { + Some(arg) => args.push(arg), + None => return Ok(args), + } + } } } diff --git a/crates/nash-parse/src/expression/let_.rs b/crates/nash-parse/src/expression/let_.rs index 8e0f9801..5cadcdd1 100644 --- a/crates/nash-parse/src/expression/let_.rs +++ b/crates/nash-parse/src/expression/let_.rs @@ -87,24 +87,24 @@ impl<'a> Parser<'a> { pub(crate) fn chomp_let_defs( &mut self, mut defs: Vec<&'a Located>>, - end: Position, + mut end: Position, ) -> Result<(Vec<&'a Located>>, Position), Let<'a>> { - let defs_for_fallback = defs.clone(); - - self.one_of_with_fallback( - vec![Box::new(|p: &mut Parser<'a>| { - // Check alignment for next definition - p.check_aligned(Let::DefAlignment)?; - - // Parse the next definition - let (def, new_end) = p.chomp_let_def()?; - defs.push(def); - - // Continue parsing more definitions - p.chomp_let_defs(defs, new_end) - })], - (defs_for_fallback, end), - ) + loop { + let next = self.one_of_with_fallback( + vec![Box::new(|p: &mut Parser<'a>| { + p.check_aligned(Let::DefAlignment)?; + p.chomp_let_def().map(Some) + })], + None, + )?; + match next { + Some((def, new_end)) => { + defs.push(def); + end = new_end; + } + None => return Ok((defs, end)), + } + } } /// Parse a single let definition (value or destructure). diff --git a/crates/nash-parse/src/expression/macro_.rs b/crates/nash-parse/src/expression/macro_.rs index a2da74ad..eafa213f 100644 --- a/crates/nash-parse/src/expression/macro_.rs +++ b/crates/nash-parse/src/expression/macro_.rs @@ -29,7 +29,7 @@ impl<'a> Parser<'a> { let name_start = Position::new( variable.region.end.line, - variable.region.end.column - u16::try_from(name.len()).expect("identifier too long"), + variable.region.end.column - name.len(), ); let name = self.alloc(Located::at( Region::new(name_start, variable.region.end), diff --git a/crates/nash-parse/src/expression/mod.rs b/crates/nash-parse/src/expression/mod.rs index 33e6e25f..0fe20c95 100644 --- a/crates/nash-parse/src/expression/mod.rs +++ b/crates/nash-parse/src/expression/mod.rs @@ -45,6 +45,10 @@ impl<'a> Parser<'a> { /// Currently implements: lambda, possiblyNegativeTerm + function application. /// TODO: let, if, case, operators pub fn expression(&mut self) -> Result<(&'a Located>, Position), error::Expr<'a>> { + self.with_depth(error::Expr::Space, Self::expression_inner) + } + + fn expression_inner(&mut self) -> Result<(&'a Located>, Position), error::Expr<'a>> { let start = self.get_position(); self.one_of( @@ -106,8 +110,6 @@ impl<'a> Parser<'a> { let mut current_end = end; loop { - let state_for_fallback = (ops.clone(), current_expr, current_args.clone(), current_end); - let result = if self.is_trailing_section_operator() { ExprEndState::Done } else { @@ -121,10 +123,7 @@ impl<'a> Parser<'a> { let new_end = p.get_position(); p.chomp(error::Expr::Space)?; - let mut new_args = current_args.clone(); - new_args.push(arg); - - Ok(ExprEndState::MoreArgs(new_args, new_end)) + Ok(ExprEndState::MoreArgs(arg, new_end)) }), // operator Box::new(|p: &mut Parser<'a>| { @@ -162,10 +161,7 @@ impl<'a> Parser<'a> { p.alloc(Located::at(neg_region, Expr::Negate(negated_expr))); p.chomp(error::Expr::Space)?; - let mut new_args = current_args.clone(); - new_args.push(neg); - - Ok(ExprEndState::MoreArgs(new_args, neg_end)) + Ok(ExprEndState::MoreArgs(neg, neg_end)) } else { // Regular binary operator p.one_of( @@ -213,20 +209,20 @@ impl<'a> Parser<'a> { }; match result { - ExprEndState::MoreArgs(new_args, new_end) => { - current_args = new_args; + ExprEndState::MoreArgs(arg, new_end) => { + current_args.push(arg); current_end = new_end; } ExprEndState::MoreOps(op, new_expr, new_end) => { // Push (toCall current_expr current_args, op) onto ops - let call_expr = to_call(self, start, current_expr, current_args.clone()); + let call_expr = + to_call(self, start, current_expr, std::mem::take(&mut current_args)); let operand = self.alloc(BinOpOperand { expr: call_expr, op, }); ops.push(operand); current_expr = new_expr; - current_args = Vec::new(); current_end = new_end; } ExprEndState::Final(op, final_expr, final_end) => { @@ -247,20 +243,20 @@ impl<'a> Parser<'a> { return Ok((result, final_end)); } ExprEndState::Done => { - // Finalize - use saved state - let (saved_ops, saved_expr, saved_args, saved_end) = state_for_fallback; - let final_call = to_call(self, start, saved_expr, saved_args); + // No accumulator changes occur until a parse attempt succeeds. + let final_call = to_call(self, start, current_expr, current_args); - if saved_ops.is_empty() { - return Ok((final_call, saved_end)); + if ops.is_empty() { + return Ok((final_call, current_end)); } else { - let ops_slice = saved_ops.into_bump_slice(); + let ops_slice = ops.into_bump_slice(); let binops = Expr::BinOps { operands: ops_slice, last: final_call, }; - let result = self.alloc(Located::at(Region::new(start, saved_end), binops)); - return Ok((result, saved_end)); + let result = + self.alloc(Located::at(Region::new(start, current_end), binops)); + return Ok((result, current_end)); } } } @@ -322,6 +318,10 @@ impl<'a> Parser<'a> { /// ] /// ``` pub fn term(&mut self) -> Result<&'a Located>, error::Expr<'a>> { + self.with_depth(error::Expr::Space, Self::term_inner) + } + + fn term_inner(&mut self) -> Result<&'a Located>, error::Expr<'a>> { let start = self.get_position(); self.one_of( @@ -383,8 +383,8 @@ fn to_call<'a>( /// State for expression end parsing (function application and binary operators). enum ExprEndState<'a> { - /// More function arguments accumulated - MoreArgs(Vec<&'a Located>>, Position), + /// One successfully parsed function argument + MoreArgs(&'a Located>, Position), /// Binary operator found, continue parsing chain MoreOps(&'a Located<&'a str>, &'a Located>, Position), /// Final expression found (let, case, if, lambda) after operator @@ -399,7 +399,7 @@ macro_rules! assert_expr_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let result = parser.term().expect("expected successful parse"); parser.chomp(|_, _, _| ()).expect("expected trailing space"); assert!(parser.is_eof(), "expression parser left trailing input"); @@ -419,7 +419,7 @@ macro_rules! assert_expr_error_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let result = parser.term().expect_err("expected parse error"); insta::with_settings!({ @@ -437,7 +437,7 @@ macro_rules! assert_expression_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let (result, _end) = parser.expression().expect("expected successful parse"); parser.chomp(|_, _, _| ()).expect("expected trailing space"); assert!(parser.is_eof(), "expression parser left trailing input"); @@ -457,7 +457,7 @@ macro_rules! assert_expression_error_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let result = parser.expression().expect_err("expected parse error"); insta::with_settings!({ @@ -479,7 +479,7 @@ macro_rules! assert_indented_expr_snapshot { let fragment = indoc::indoc!($code); let indented = $crate::test_support::indent_fragment(fragment); let src = bump.alloc_str(&indented); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); parser .chomp(|_, _, _| "space error") .expect("expected leading indent"); @@ -504,7 +504,7 @@ macro_rules! assert_indented_expression_snapshot { let fragment = indoc::indoc!($code); let indented = $crate::test_support::indent_fragment(fragment); let src = bump.alloc_str(&indented); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); parser .chomp(|_, _, _| "space error") .expect("expected leading indent"); @@ -536,6 +536,29 @@ pub(crate) use assert_indented_expression_snapshot; #[cfg(test)] mod tests { + #[test] + fn operator_chain_arena_growth_is_linear() { + let mut previous = None; + for count in [1000, 2000, 4000] { + let source = vec!["x"; count].join(" + "); + let bump = bumpalo::Bump::new(); + let mut parser = crate::Parser::new(&bump, &source); + let (expression, _) = parser.expression().expect("operator chain"); + assert!(parser.is_eof()); + let nash_source::Expr::BinOps { operands, .. } = expression.value else { + panic!("expected binary operators"); + }; + assert_eq!(operands.len(), count - 1); + let allocated = bump.allocated_bytes(); + eprintln!("{count} operands: {allocated} arena bytes"); + if let Some(previous) = previous { + // Allow arena chunk rounding while rejecting quadratic growth. + assert!(allocated <= previous * 3, "superlinear arena growth"); + } + previous = Some(allocated); + } + } + #[test] fn call_with_bytes_argument() { assert_expression_snapshot!("f #\"01\" x"); diff --git a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__bytes__tests__error_endless.snap b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__bytes__tests__error_endless.snap index 82906151..0778a1a8 100644 --- a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__bytes__tests__error_endless.snap +++ b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__bytes__tests__error_endless.snap @@ -3,7 +3,12 @@ source: crates/nash-parse/src/expression/bytes.rs description: "Code:\n\n#\"ab" --- Bytes( - Endless, + Endless( + Position { + line: 1, + column: 1, + }, + ), 1, 5, ) diff --git a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_endless.snap b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_endless.snap index 63be420f..99478725 100644 --- a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_endless.snap +++ b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_endless.snap @@ -3,7 +3,12 @@ source: crates/nash-parse/src/expression/string.rs description: "Code:\n\n\"hello" --- String( - EndlessSingle, + EndlessSingle( + Position { + line: 1, + column: 1, + }, + ), 1, - 2, + 7, ) diff --git a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_overflowing_unicode_escape.snap b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_overflowing_unicode_escape.snap new file mode 100644 index 00000000..3fc64608 --- /dev/null +++ b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__error_overflowing_unicode_escape.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-parse/src/expression/string.rs +description: "Code:\n\n\"\\u{FFFFFFFFF}\"" +--- +String( + Escape( + BadUnicodeCode( + 12, + ), + ), + 1, + 3, +) diff --git a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__raw_unicode.snap b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__raw_unicode.snap new file mode 100644 index 00000000..c06c26ca --- /dev/null +++ b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__raw_unicode.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-parse/src/expression/string.rs +description: "Code:\n\n\"é漢😀\"" +--- +Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 12, + }, + }, + value: Str( + "é漢😀", + ), +} diff --git a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__unicode_multiline.snap b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__unicode_multiline.snap new file mode 100644 index 00000000..3d645a56 --- /dev/null +++ b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__unicode_multiline.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-parse/src/expression/string.rs +description: "Code:\n\n\"\"\"é\r\n漢😀\"\"\"" +--- +Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 2, + column: 11, + }, + }, + value: Str( + "é\n漢😀", + ), +} diff --git a/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__unicode_with_escape.snap b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__unicode_with_escape.snap new file mode 100644 index 00000000..f2e48ac5 --- /dev/null +++ b/crates/nash-parse/src/expression/snapshots/nash_parse__expression__string__tests__unicode_with_escape.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-parse/src/expression/string.rs +description: "Code:\n\n\"é\\n漢\\u{1F600}\"" +--- +Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 19, + }, + }, + value: Str( + "é\n漢😀", + ), +} diff --git a/crates/nash-parse/src/expression/string.rs b/crates/nash-parse/src/expression/string.rs index e315f6db..d718f7b9 100644 --- a/crates/nash-parse/src/expression/string.rs +++ b/crates/nash-parse/src/expression/string.rs @@ -53,6 +53,26 @@ mod tests { assert_expr_snapshot!(r#""\u{1F600}""#); } + #[test] + fn raw_unicode() { + assert_expr_snapshot!(r#""é漢😀""#); + } + + #[test] + fn unicode_with_escape() { + assert_expr_snapshot!(r#""é\n漢\u{1F600}""#); + } + + #[test] + fn unicode_multiline() { + assert_expr_snapshot!("\"\"\"é\r\n漢😀\"\"\""); + } + + #[test] + fn error_overflowing_unicode_escape() { + assert_expr_error_snapshot!(r#""\u{FFFFFFFFF}""#); + } + #[test] fn error_endless() { assert_expr_error_snapshot!(r#""hello"#); diff --git a/crates/nash-parse/src/expression/variable.rs b/crates/nash-parse/src/expression/variable.rs index a38d3ad6..c5239733 100644 --- a/crates/nash-parse/src/expression/variable.rs +++ b/crates/nash-parse/src/expression/variable.rs @@ -29,7 +29,7 @@ impl<'a> Parser<'a> { /// Parses `[a-z][a-zA-Z0-9_]*`, checks it's not a reserved word. pub(crate) fn lower_name( &mut self, - to_error: impl FnOnce(u16, u16) -> E, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result<&'a str, E> { let (row, col) = self.position(); let start_pos = self.pos; @@ -56,7 +56,7 @@ impl<'a> Parser<'a> { /// Parse a quoted type variable and return its name without the quote. pub(crate) fn type_var_name( &mut self, - to_error: impl FnOnce(u16, u16) -> E, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result<&'a str, E> { let (row, col) = self.position(); if self.peek() != Some(b'\'') @@ -74,7 +74,7 @@ impl<'a> Parser<'a> { /// Parse an uppercase or lowercase type declaration name. pub(crate) fn type_decl_name( &mut self, - to_error: impl FnOnce(u16, u16) -> E, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result<&'a str, E> { match self.peek() { Some(b) if b.is_ascii_uppercase() => self.upper_name(to_error), @@ -92,7 +92,7 @@ impl<'a> Parser<'a> { /// Parses `[A-Z][a-zA-Z0-9_]*`. No reserved word check for uppercase. pub(crate) fn upper_name( &mut self, - to_error: impl FnOnce(u16, u16) -> E, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result<&'a str, E> { let (row, col) = self.position(); let start_pos = self.pos; @@ -140,7 +140,10 @@ impl<'a> Parser<'a> { /// - `Module.foo` -> VarQual(LowVar, "Module", "foo") /// - `Module.Foo` -> VarQual(CapVar, "Module", "Foo") /// - `A.B.C.foo` -> VarQual(LowVar, "A.B.C", "foo") - fn foreign_alpha(&mut self, to_error: impl FnOnce(u16, u16) -> E) -> Result, E> { + fn foreign_alpha( + &mut self, + to_error: impl FnOnce(usize, usize) -> E, + ) -> Result, E> { let (row, col) = self.position(); let start_pos = self.pos; @@ -210,7 +213,7 @@ impl<'a> Parser<'a> { /// Get a str slice from start_pos to current position. pub(crate) fn slice_from(&self, start_pos: usize) -> &'a str { let bytes = &self.src[start_pos..self.pos]; - unsafe { std::str::from_utf8_unchecked(bytes) } + std::str::from_utf8(bytes).expect("source slice must end at UTF-8 boundaries") } /// Check if current position is a dot followed by uppercase. @@ -229,9 +232,9 @@ impl<'a> Parser<'a> { fn parse_qualified_lower( &mut self, start_pos: usize, - row: u16, - col: u16, - to_error: impl FnOnce(u16, u16) -> E, + row: usize, + col: usize, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result, E> { let module_end = self.pos; self.advance(); // consume dot @@ -239,8 +242,10 @@ impl<'a> Parser<'a> { self.advance(); // consume first lowercase char self.chomp_inner_chars(); - let module = unsafe { std::str::from_utf8_unchecked(&self.src[start_pos..module_end]) }; - let name = unsafe { std::str::from_utf8_unchecked(&self.src[name_start..self.pos]) }; + let module = std::str::from_utf8(&self.src[start_pos..module_end]) + .expect("source slice must end at UTF-8 boundaries"); + let name = std::str::from_utf8(&self.src[name_start..self.pos]) + .expect("source slice must end at UTF-8 boundaries"); if keyword::is_reserved(name) { return Err(to_error(row, col)); @@ -257,9 +262,9 @@ impl<'a> Parser<'a> { fn chomp_qualified_upper( &mut self, start_pos: usize, - row: u16, - col: u16, - to_error: impl FnOnce(u16, u16) -> E, + row: usize, + col: usize, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result, E> { loop { if self.is_dot_upper() { diff --git a/crates/nash-parse/src/import.rs b/crates/nash-parse/src/import.rs index ffc01856..f70c94e7 100644 --- a/crates/nash-parse/src/import.rs +++ b/crates/nash-parse/src/import.rs @@ -141,7 +141,7 @@ impl<'a> Parser<'a> { /// ``` pub(crate) fn module_name( &mut self, - to_error: impl FnOnce(u16, u16) -> E, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result<&'a str, E> { let (row, col) = self.position(); let start_pos = self.pos; @@ -187,7 +187,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.import(); match result { Ok(ref import) => { diff --git a/crates/nash-parse/src/lib.rs b/crates/nash-parse/src/lib.rs index 08199622..453c983e 100644 --- a/crates/nash-parse/src/lib.rs +++ b/crates/nash-parse/src/lib.rs @@ -19,14 +19,14 @@ pub(crate) mod test_support; mod tests_block; mod type_; -pub type Row = u16; -pub type Col = u16; +pub type Row = usize; +pub type Col = usize; /// Saved parser state for backtracking. #[derive(Clone, Copy)] struct ParserState { pos: usize, - indent: u16, + indent: usize, row: Row, col: Col, } @@ -36,7 +36,7 @@ struct ParserState { /// Combines the arena allocator with parsing state for a unified API. /// All parsed AST nodes are allocated in the provided bump arena. /// -/// The source bytes should already be allocated in the arena (via `bump.alloc_str`), +/// The source text should already be allocated in the arena (via `bump.alloc_str`), /// so all string slices in the resulting AST share the `'a` lifetime. pub struct Parser<'a> { /// Arena allocator for AST nodes @@ -46,28 +46,66 @@ pub struct Parser<'a> { /// Current byte position pos: usize, /// Current indentation level (for layout-sensitive parsing) - indent: u16, + indent: usize, /// Current row (1-indexed) row: Row, /// Current column (1-indexed) col: Col, + depth: usize, + depth_error: Option<(Row, Col)>, } impl<'a> Parser<'a> { - /// Create a new parser for the given source bytes. + /// Create a new parser for valid UTF-8 source text. /// /// The source should already be allocated in the arena. - pub fn new(bump: &'a Bump, src: &'a [u8]) -> Self { + /// + /// Arbitrary byte buffers are not a parser input: + /// ```compile_fail + /// let bump = bumpalo::Bump::new(); + /// nash_parse::Parser::new(&bump, &[b'"', 0xff, b'"']); + /// ``` + pub fn new(bump: &'a Bump, src: &'a str) -> Self { Parser { bump, - src, + src: src.as_bytes(), pos: 0, // Elm starts at 0; 1 is behaviorally identical because // `checkIndent`'s `col > 1` guard dominates at top level. indent: 1, row: 1, col: 1, + depth: 0, + depth_error: None, + } + } + + /// Count recursive expression, pattern, and type entries together. This state + /// is deliberately outside ParserState: backtracking cannot undo exhaustion. + fn with_depth( + &mut self, + to_error: impl FnOnce(error::Space, Row, Col) -> E, + parse: impl FnOnce(&mut Self) -> Result, + ) -> Result { + const MAX_DEPTH: usize = 64; + if self.depth == MAX_DEPTH || self.depth_error.is_some() { + let position = self.position(); + let (row, col) = *self.depth_error.get_or_insert(position); + return Err(to_error(error::Space::TooDeep, row, col)); + } + self.depth += 1; + let result = parse(self); + self.depth -= 1; + // A speculative parser may swallow an error. Never turn exhaustion into + // a successful prefix parse, even when it restored the input position. + let result = match (result, self.depth_error) { + (Ok(_), Some((row, col))) => Err(to_error(error::Space::TooDeep, row, col)), + (result, _) => result, + }; + if self.depth == 0 { + self.depth_error = None; } + result } // ------------------------------------------------------------------------- @@ -108,13 +146,13 @@ impl<'a> Parser<'a> { /// Current indentation level. #[inline] - pub fn indent(&self) -> u16 { + pub fn indent(&self) -> usize { self.indent } /// Set the indentation level. #[inline] - pub fn set_indent(&mut self, indent: u16) { + pub fn set_indent(&mut self, indent: usize) { self.indent = indent; } @@ -153,7 +191,7 @@ impl<'a> Parser<'a> { /// ``` pub fn with_backset_indent( &mut self, - backset: u16, + backset: usize, parser: impl FnOnce(&mut Self) -> Result, ) -> Result { let old_indent = self.indent; @@ -229,7 +267,7 @@ impl<'a> Parser<'a> { Ok(value) => return Ok(value), Err(e) => { // Did we consume any input? - if self.pos != before.pos { + if self.pos != before.pos || self.depth_error.is_some() { // Committed - propagate error return Err(e); } @@ -265,7 +303,7 @@ impl<'a> Parser<'a> { Ok(value) => return Ok(value), Err(e) => { // Did we consume any input? - if self.pos != before.pos { + if self.pos != before.pos || self.depth_error.is_some() { // Committed - propagate error return Err(e); } @@ -411,7 +449,7 @@ impl<'a> Parser<'a> { /// Peek at a byte at the given offset from current position. #[inline] pub fn peek_at(&self, offset: usize) -> Option { - self.src.get(self.pos + offset).copied() + self.src.get(self.pos.checked_add(offset)?).copied() } /// Get the remaining bytes from current position. @@ -471,13 +509,166 @@ impl<'a> Parser<'a> { #[cfg(test)] mod tests { + #[test] + fn nesting_is_bounded_on_a_small_stack() { + std::thread::Builder::new() + .stack_size(2 * 1024 * 1024) + .spawn(|| { + for depth in [8, 512] { + for (open, leaf, close, kind) in [ + ("(", "1", ")", 0), + ("(-", "1", ")", 0), + ("[", "1", "]", 0), + ("\\x -> ", "1", "", 0), + ("Just (", "x", ")", 1), + ("(", "x", ")", 2), + ("Int -> ", "Int", "", 3), + ("(", "Int", ")", 4), + ] { + let bump = Bump::new(); + let source = + format!("{}{}{}", open.repeat(depth), leaf, close.repeat(depth)); + let mut parser = Parser::new(&bump, &source); + let result = match kind { + 0 => parser + .expression() + .map(|_| ()) + .map_err(|e| format!("{e:?}")), + 1 => parser + .pattern_expr() + .map(|_| ()) + .map_err(|e| format!("{e:?}")), + 2 => parser + .pattern_term() + .map(|_| ()) + .map_err(|e| format!("{e:?}")), + 3 => parser.type_expr().map(|_| ()).map_err(|e| format!("{e:?}")), + _ => parser.type_term().map(|_| ()).map_err(|e| format!("{e:?}")), + }; + if depth == 8 { + assert!(result.is_ok(), "{open}: {result:?}"); + assert!(parser.is_eof()); + } else { + assert!(result.unwrap_err().contains("TooDeep"), "{open}"); + } + assert_eq!(parser.depth, 0); + assert_eq!(parser.depth_error, None); + } + } + }) + .unwrap() + .join() + .unwrap(); + } + + #[test] + fn nesting_exhaustion_survives_backtracking_and_resets() { + let bump = Bump::new(); + let mut parser = Parser::new(&bump, "x"); + let result = parser.with_depth(error::Expr::Space, |p| { + let saved = p.save_state(); + p.depth_error = Some((1, 1)); + p.restore_state(saved); + p.one_of_with_fallback( + vec![Box::new(|_| { + Err(error::Expr::Space(error::Space::TooDeep, 1, 1)) + })], + (), + ) + }); + assert!(matches!( + result, + Err(error::Expr::Space(error::Space::TooDeep, 1, 1)) + )); + parser.expression().unwrap(); + assert!(parser.is_eof()); + } + + #[test] + fn flat_sequences_use_bounded_stack() { + std::thread::Builder::new() + .stack_size(2 * 1024 * 1024) + .spawn(|| { + let expressions = [ + format!("x{}", ".field".repeat(2_000)), + format!("\\{}-> x", "x ".repeat(2_000)), + format!("{}0", "if True then 1 else ".repeat(2_000)), + format!("let\n{}in x", " x = 1\n".repeat(2_000)), + format!("case x of\n{}", " _ -> 1\n".repeat(2_000)), + ]; + for source in expressions { + let source = crate::test_support::indent_fragment(&source); + let bump = Bump::new(); + let mut parser = Parser::new(&bump, &source); + parser.chomp(|_, _, _| ()).unwrap(); + parser.expression().unwrap(); + assert!(parser.is_eof()); + } + let bump = Bump::new(); + let source = format!("type Many = A{}", " | A".repeat(2_000)); + let mut parser = Parser::new(&bump, &source); + parser.declaration().unwrap(); + assert!(parser.is_eof()); + let source = format!("{}{}", "{-".repeat(65_536), "-}".repeat(65_536)); + let mut parser = Parser::new(&bump, &source); + parser.chomp(|_, _, _| ()).unwrap(); + assert!(parser.is_eof()); + }) + .unwrap() + .join() + .unwrap(); + } + + #[test] + fn coordinates_cover_large_sources() { + let bump = Bump::new(); + let lines = "\n".repeat(65_536); + let mut parser = Parser::new(&bump, &lines); + parser.advance_by(lines.len()); + assert_eq!(parser.row(), 65_537); + assert_eq!(parser.col(), 1); + + let string = format!("\"{}\"", "x".repeat(65_536)); + let mut parser = Parser::new(&bump, &string); + parser.expression().expect("long string"); + assert!(parser.is_eof()); + assert_eq!(parser.col(), string.len() + 1); + + let indented = format!("{}x", " ".repeat(65_536)); + let mut parser = Parser::new(&bump, &indented); + parser.chomp(|_, _, _| ()).expect("long indentation"); + assert_eq!(parser.col(), 65_537); + parser.expression().expect("indented expression"); + assert!(parser.is_eof()); + + let escaped = format!("\"\\u{{{}}}\"", "F".repeat(65_536)); + let mut parser = Parser::new(&bump, &escaped); + let error::Expr::String( + error::StringError::Escape(error::Escape::BadUnicodeCode(width)), + 1, + 3, + ) = parser.expression().expect_err("oversized Unicode escape") + else { + panic!("expected an invalid Unicode code with its full width"); + }; + assert_eq!(width, 65_539); + } + + #[test] + fn lookahead_offset_cannot_wrap() { + let bump = Bump::new(); + let mut parser = Parser::new(&bump, "xy"); + parser.advance(); + assert_eq!(parser.peek_at(usize::MAX), None); + } + use super::*; #[test] fn test_parser_new() { let bump = Bump::new(); let src = bump.alloc_str("hello"); - let parser = Parser::new(&bump, src.as_bytes()); + let parser = Parser::new(&bump, src); assert_eq!(parser.row(), 1); assert_eq!(parser.col(), 1); @@ -489,7 +680,7 @@ mod tests { fn test_parser_advance() { let bump = Bump::new(); let src = bump.alloc_str("ab\ncd"); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); assert_eq!(parser.position(), (1, 1)); parser.advance(); // 'a' @@ -506,7 +697,7 @@ mod tests { fn test_parser_eof() { let bump = Bump::new(); let src = bump.alloc_str("x"); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); assert!(!parser.is_eof()); parser.advance(); diff --git a/crates/nash-parse/src/module.rs b/crates/nash-parse/src/module.rs index 806793dc..5827eeb3 100644 --- a/crates/nash-parse/src/module.rs +++ b/crates/nash-parse/src/module.rs @@ -324,7 +324,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.module_header(); match result { Ok((kind, name, exposing)) => { @@ -381,7 +381,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.module(); match result { Ok(module) => { @@ -404,7 +404,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let error = parser.module().expect_err("expected module parse error"); insta::with_settings!({ description => format!("Code:\n\n{}", input), diff --git a/crates/nash-parse/src/pattern/mod.rs b/crates/nash-parse/src/pattern/mod.rs index aecccc92..270a5de7 100644 --- a/crates/nash-parse/src/pattern/mod.rs +++ b/crates/nash-parse/src/pattern/mod.rs @@ -33,6 +33,10 @@ impl<'a> Parser<'a> { /// ] /// ``` pub fn pattern_term(&mut self) -> Result<&'a Located>, error::Pattern<'a>> { + self.with_depth(error::Pattern::Space, Self::pattern_term_inner) + } + + fn pattern_term_inner(&mut self) -> Result<&'a Located>, error::Pattern<'a>> { let start = self.get_position(); self.one_of( @@ -61,6 +65,12 @@ impl<'a> Parser<'a> { /// ``` pub fn pattern_expr( &mut self, + ) -> Result<(&'a Located>, Position), error::Pattern<'a>> { + self.with_depth(error::Pattern::Space, Self::pattern_expr_inner) + } + + fn pattern_expr_inner( + &mut self, ) -> Result<(&'a Located>, Position), error::Pattern<'a>> { let start = self.get_position(); let (first_pattern, first_end) = self.pattern_expr_part()?; @@ -324,7 +334,7 @@ macro_rules! assert_pattern_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let (result, _end) = parser.pattern_expr().expect("expected successful parse"); parser.chomp(|_, _, _| ()).expect("expected trailing space"); assert!(parser.is_eof(), "pattern parser left trailing input"); @@ -344,7 +354,7 @@ macro_rules! assert_pattern_error_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let result = parser.pattern_expr().expect_err("expected parse error"); insta::with_settings!({ @@ -365,7 +375,7 @@ macro_rules! assert_indented_pattern_snapshot { let fragment = indoc::indoc!($code); let indented = $crate::test_support::indent_fragment(fragment); let src = bump.alloc_str(&indented); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); parser .chomp(|_, _, _| "space error") .expect("expected leading indent"); diff --git a/crates/nash-parse/src/pattern/term.rs b/crates/nash-parse/src/pattern/term.rs index 3e87172e..4a5c4115 100644 --- a/crates/nash-parse/src/pattern/term.rs +++ b/crates/nash-parse/src/pattern/term.rs @@ -112,8 +112,8 @@ impl<'a> Parser<'a> { &mut self, start: Position, ctor_start: usize, - row: u16, - col: u16, + row: usize, + col: usize, ) -> Result<&'a Located>, error::Pattern<'a>> { // Keep chomping Module.Module... until we hit the final name loop { diff --git a/crates/nash-parse/src/space.rs b/crates/nash-parse/src/space.rs index 59aeeac2..d4050404 100644 --- a/crates/nash-parse/src/space.rs +++ b/crates/nash-parse/src/space.rs @@ -22,7 +22,7 @@ pub enum SpaceStatus { /// Encountered a tab character (not allowed). HasTab, /// Encountered an unclosed multi-line comment. - EndlessMultiComment, + EndlessMultiComment(nash_region::Position), } impl<'a> Parser<'a> { @@ -36,9 +36,11 @@ impl<'a> Parser<'a> { match status { SpaceStatus::Good => Ok(()), SpaceStatus::HasTab => Err(to_error(Space::HasTab, new_row, new_col)), - SpaceStatus::EndlessMultiComment => { - Err(to_error(Space::EndlessMultiComment, new_row, new_col)) - } + SpaceStatus::EndlessMultiComment(opening) => Err(to_error( + Space::EndlessMultiComment(opening), + new_row, + new_col, + )), } } @@ -58,13 +60,20 @@ impl<'a> Parser<'a> { if new_col > self.indent && new_col > 1 { Ok(()) } else { - Err(to_indent_error(row_before, col_before)) + let (row, col) = if matches!(self.peek(), Some(b')' | b']' | b'}')) { + (new_row, new_col) + } else { + (row_before, col_before) + }; + Err(to_indent_error(row, col)) } } SpaceStatus::HasTab => Err(to_space_error(Space::HasTab, new_row, new_col)), - SpaceStatus::EndlessMultiComment => { - Err(to_space_error(Space::EndlessMultiComment, new_row, new_col)) - } + SpaceStatus::EndlessMultiComment(opening) => Err(to_space_error( + Space::EndlessMultiComment(opening), + new_row, + new_col, + )), } } @@ -83,14 +92,21 @@ impl<'a> Parser<'a> { if self.col > self.indent && self.col > 1 { Ok(()) } else { - Err(to_error(end_row, end_col)) + // A closing token is present but underindented. Keep its actual + // position even when intervening whitespace contains comments. + let (row, col) = if matches!(self.peek(), Some(b')' | b']' | b'}')) { + self.position() + } else { + (end_row, end_col) + }; + Err(to_error(row, col)) } } /// Check that current column equals indent level (for alignment). /// /// Mirrors Elm's `Space.checkAligned`. - pub fn check_aligned(&self, to_error: impl FnOnce(u16, Row, Col) -> E) -> Result<(), E> { + pub fn check_aligned(&self, to_error: impl FnOnce(usize, Row, Col) -> E) -> Result<(), E> { if self.col == self.indent { Ok(()) } else { @@ -134,7 +150,8 @@ impl<'a> Parser<'a> { let content_start = self.pos; // Use the existing multi-comment helper with nesting=1 - let status = self.eat_multi_comment_help(1); + let status = self + .eat_multi_comment_help(1, nash_region::Position::new(start_row, start_col - 3)); match status { SpaceStatus::Good => { @@ -152,8 +169,8 @@ impl<'a> Parser<'a> { Ok(comment) } SpaceStatus::HasTab => Err(to_space_error(Space::HasTab, self.row, self.col)), - SpaceStatus::EndlessMultiComment => Err(to_space_error( - Space::EndlessMultiComment, + SpaceStatus::EndlessMultiComment(opening) => Err(to_space_error( + Space::EndlessMultiComment(opening), self.row, self.col, )), @@ -255,15 +272,20 @@ impl<'a> Parser<'a> { /// /// Supports nested comments. fn eat_multi_comment(&mut self) -> SpaceStatus { + let opening = nash_region::Position::new(self.row(), self.col()); // Skip the {- self.advance(); self.advance(); - self.eat_multi_comment_help(1) + self.eat_multi_comment_help(1, opening) } /// Helper for eating multi-line comments with nesting. - fn eat_multi_comment_help(&mut self, open_comments: u16) -> SpaceStatus { + fn eat_multi_comment_help( + &mut self, + mut open_comments: usize, + opening: nash_region::Position, + ) -> SpaceStatus { loop { match self.peek() { // Newline @@ -284,7 +306,7 @@ impl<'a> Parser<'a> { if open_comments == 1 { return SpaceStatus::Good; } else { - return self.eat_multi_comment_help(open_comments - 1); + open_comments -= 1; } } else { self.advance(); @@ -296,7 +318,7 @@ impl<'a> Parser<'a> { if self.peek_at(1) == Some(0x2D) { self.advance(); self.advance(); - return self.eat_multi_comment_help(open_comments + 1); + open_comments += 1; } else { self.advance(); } @@ -309,7 +331,7 @@ impl<'a> Parser<'a> { // EOF without closing None => { - return SpaceStatus::EndlessMultiComment; + return SpaceStatus::EndlessMultiComment(opening); } } } @@ -324,7 +346,7 @@ mod tests { fn parse_and_chomp(input: &str) -> (SpaceStatus, usize, Row, Col) { let bump = Bump::new(); let src = bump.alloc_str(input); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let (status, row, col) = parser.eat_spaces(); (status, parser.pos, row, col) @@ -383,7 +405,10 @@ mod tests { #[test] fn endless_multi_comment() { let (status, _, _, _) = parse_and_chomp("{- never closed"); - assert_eq!(status, SpaceStatus::EndlessMultiComment); + assert_eq!( + status, + SpaceStatus::EndlessMultiComment(nash_region::Position::new(1, 1)) + ); } #[test] @@ -405,7 +430,7 @@ mod tests { fn doc_comment_simple() { let bump = Bump::new(); let src = bump.alloc_str("{-| hello -}"); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.doc_comment(|_, _| "expected", |_, _, _| "space error"); assert!(result.is_ok()); @@ -420,7 +445,7 @@ mod tests { fn doc_comment_multiline() { let bump = Bump::new(); let src = bump.alloc_str("{-| line one\nline two -}"); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.doc_comment(|_, _| "expected", |_, _, _| "space error"); assert!(result.is_ok()); @@ -432,7 +457,7 @@ mod tests { fn doc_comment_not_doc() { let bump = Bump::new(); let src = bump.alloc_str("{- not a doc comment -}"); - let mut parser = Parser::new(&bump, src.as_bytes()); + let mut parser = Parser::new(&bump, src); let result = parser.doc_comment(|_, _| "expected", |_, _, _| "space error"); assert!(result.is_err()); diff --git a/crates/nash-parse/src/string.rs b/crates/nash-parse/src/string.rs index 5369493f..2ec2af1b 100644 --- a/crates/nash-parse/src/string.rs +++ b/crates/nash-parse/src/string.rs @@ -41,7 +41,7 @@ impl<'a> Parser<'a> { if self.peek() == Some(b'"') { self.advance(); // consume third " // Multi-line string - let result = self.chomp_multi_string(); + let result = self.chomp_multi_string(nash_region::Position::new(row, col)); match result { StringResult::Ok(s) => Ok(s), StringResult::Err(e, r, c) => Err(to_error(e, r, c)), @@ -52,7 +52,7 @@ impl<'a> Parser<'a> { } } else { // Single-line string - let result = self.chomp_single_string(); + let result = self.chomp_single_string(nash_region::Position::new(row, col)); match result { StringResult::Ok(s) => Ok(s), StringResult::Err(e, r, c) => Err(to_error(e, r, c)), @@ -61,20 +61,27 @@ impl<'a> Parser<'a> { } /// Parse a single-line string (content after opening `"`). - fn chomp_single_string(&mut self) -> StringResult<'a> { + fn chomp_single_string(&mut self, opening: nash_region::Position) -> StringResult<'a> { let start_pos = self.pos; - let (start_row, start_col) = self.position(); let mut needs_escape = false; loop { match self.peek() { None => { // End of file without closing quote - return StringResult::Err(StringError::EndlessSingle, start_row, start_col); + return StringResult::Err( + StringError::EndlessSingle(opening), + self.row(), + self.col(), + ); } Some(b'\n') => { // Newline in single-line string - return StringResult::Err(StringError::EndlessSingle, self.row(), self.col()); + return StringResult::Err( + StringError::EndlessSingle(opening), + self.row(), + self.col(), + ); } Some(b'"') => { // End of string @@ -87,8 +94,8 @@ impl<'a> Parser<'a> { } else { // Return slice directly let bytes = &self.src[start_pos..end_pos]; - // SAFETY: We've verified this is valid UTF-8 by scanning byte-by-byte - let s = unsafe { std::str::from_utf8_unchecked(bytes) }; + let s = std::str::from_utf8(bytes) + .expect("source slice must end at UTF-8 boundaries"); return StringResult::Ok(s); } } @@ -112,9 +119,9 @@ impl<'a> Parser<'a> { } EscapeResult::EndOfFile => { return StringResult::Err( - StringError::EndlessSingle, - start_row, - start_col, + StringError::EndlessSingle(opening), + self.row(), + self.col(), ); } } @@ -129,15 +136,18 @@ impl<'a> Parser<'a> { } /// Parse a multi-line string (content after opening `"""`). - fn chomp_multi_string(&mut self) -> StringResult<'a> { + fn chomp_multi_string(&mut self, opening: nash_region::Position) -> StringResult<'a> { let start_pos = self.pos; - let (start_row, start_col) = self.position(); let mut needs_escape = false; loop { match self.peek() { None => { - return StringResult::Err(StringError::EndlessMulti, start_row, start_col); + return StringResult::Err( + StringError::EndlessMulti(opening), + self.row(), + self.col(), + ); } Some(b'"') => { // Check for closing """ @@ -149,7 +159,8 @@ impl<'a> Parser<'a> { return self.build_escaped_string(start_pos, end_pos, true); } else { let bytes = &self.src[start_pos..end_pos]; - let s = unsafe { std::str::from_utf8_unchecked(bytes) }; + let s = std::str::from_utf8(bytes) + .expect("source slice must end at UTF-8 boundaries"); return StringResult::Ok(s); } } else { @@ -186,9 +197,9 @@ impl<'a> Parser<'a> { } EscapeResult::EndOfFile => { return StringResult::Err( - StringError::EndlessMulti, - start_row, - start_col, + StringError::EndlessMulti(opening), + self.row(), + self.col(), ); } } @@ -248,8 +259,8 @@ impl<'a> Parser<'a> { while pos < end && self.src[pos] != b'}' { pos += 1; } - let hex_str = - unsafe { std::str::from_utf8_unchecked(&self.src[hex_start..pos]) }; + let hex_str = std::str::from_utf8(&self.src[hex_start..pos]) + .expect("source slice must end at UTF-8 boundaries"); if let Ok(code) = u32::from_str_radix(hex_str, 16) && let Some(c) = char::from_u32(code) { @@ -272,7 +283,8 @@ impl<'a> Parser<'a> { // Regular UTF-8 character let width = utf8_char_width(b); let char_bytes = &self.src[pos..pos + width]; - let s = unsafe { std::str::from_utf8_unchecked(char_bytes) }; + let s = std::str::from_utf8(char_bytes) + .expect("source slice must end at UTF-8 boundaries"); result.push_str(s); pos += width; } @@ -308,7 +320,7 @@ impl<'a> Parser<'a> { loop { match self.peek_at(offset) { None => { - return EscapeResult::Problem(Escape::BadUnicodeFormat(offset as u16)); + return EscapeResult::Problem(Escape::BadUnicodeFormat(offset)); } Some(b'}') => { break; @@ -321,25 +333,26 @@ impl<'a> Parser<'a> { } else { (b - b'A' + 10) as u32 }; - code = code * 16 + digit; + // Saturation keeps an oversized escape invalid without overflowing. + code = code.saturating_mul(16).saturating_add(digit); num_digits += 1; offset += 1; } Some(_) => { - return EscapeResult::Problem(Escape::BadUnicodeFormat(offset as u16)); + return EscapeResult::Problem(Escape::BadUnicodeFormat(offset)); } } } // Check code validity if code > 0x10FFFF { - return EscapeResult::Problem(Escape::BadUnicodeCode((offset + 1) as u16)); + return EscapeResult::Problem(Escape::BadUnicodeCode(offset + 1)); } // Check digit count (must be 4-6) if !(4..=6).contains(&num_digits) { return EscapeResult::Problem(Escape::BadUnicodeLength { - code: (offset + 1) as u16, + code: offset + 1, expected: if num_digits < 4 { 4 } else { 6 }, actual: num_digits, }); diff --git a/crates/nash-parse/src/symbol.rs b/crates/nash-parse/src/symbol.rs index 2a5284d5..d2113bbc 100644 --- a/crates/nash-parse/src/symbol.rs +++ b/crates/nash-parse/src/symbol.rs @@ -25,8 +25,8 @@ impl<'a> Parser<'a> { /// - `:` (colon - reserved for type annotations) pub(crate) fn operator( &mut self, - to_expectation: impl FnOnce(u16, u16) -> E, - to_error: impl FnOnce(BadOperator, u16, u16) -> E, + to_expectation: impl FnOnce(usize, usize) -> E, + to_error: impl FnOnce(BadOperator, usize, usize) -> E, ) -> Result<&'a str, E> { let (row, col) = self.position(); let start_pos = self.pos; @@ -63,8 +63,8 @@ impl<'a> Parser<'a> { /// Parse an operator and wrap it in a Located. pub(crate) fn add_location_operator( &mut self, - to_expectation: impl FnOnce(u16, u16) -> E, - to_error: impl FnOnce(BadOperator, u16, u16) -> E, + to_expectation: impl FnOnce(usize, usize) -> E, + to_error: impl FnOnce(BadOperator, usize, usize) -> E, ) -> Result<&'a Located<&'a str>, E> { let start = self.get_position(); let op = self.operator(to_expectation, to_error)?; diff --git a/crates/nash-parse/src/tests_block.rs b/crates/nash-parse/src/tests_block.rs index d2ee52db..4999ce46 100644 --- a/crates/nash-parse/src/tests_block.rs +++ b/crates/nash-parse/src/tests_block.rs @@ -128,20 +128,25 @@ impl<'a> Parser<'a> { vec![Box::new(|parser: &mut Parser<'a>| { parser.keyword_within(TestErr::Equals)?; parser.chomp_and_check_indent(TestErr::Space, TestErr::WithinOpen)?; + let opening = parser.get_position(); parser.word1(b'(', TestErr::WithinOpen)?; parser.chomp_and_check_indent(TestErr::Space, TestErr::WithinKind)?; let first = parser.budget_entry()?; - parser.chomp_and_check_indent(TestErr::Space, TestErr::WithinEnd)?; + parser.chomp_and_check_indent(TestErr::Space, |r, c| { + TestErr::WithinEnd(opening, r, c) + })?; let budget = parser.one_of( - TestErr::WithinEnd, + |r, c| TestErr::WithinEnd(opening, r, c), vec![ Box::new(|parser: &mut Parser<'a>| { - parser.word1(b',', TestErr::WithinEnd)?; + parser.word1(b',', |r, c| TestErr::WithinEnd(opening, r, c))?; parser.chomp_and_check_indent(TestErr::Space, TestErr::WithinKind)?; let (row, col) = parser.position(); let second = parser.budget_entry()?; - parser.chomp_and_check_indent(TestErr::Space, TestErr::WithinEnd)?; - parser.word1(b')', TestErr::WithinEnd)?; + parser.chomp_and_check_indent(TestErr::Space, |r, c| { + TestErr::WithinEnd(opening, r, c) + })?; + parser.word1(b')', |r, c| TestErr::WithinEnd(opening, r, c))?; match (first, second) { (Budget::Cpu(cpu), Budget::Mem(mem)) | (Budget::Mem(mem), Budget::Cpu(cpu)) => { @@ -151,7 +156,7 @@ impl<'a> Parser<'a> { } }), Box::new(|parser: &mut Parser<'a>| { - parser.word1(b')', TestErr::WithinEnd)?; + parser.word1(b')', |r, c| TestErr::WithinEnd(opening, r, c))?; Ok(first) }), ], @@ -253,7 +258,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let source = bump.alloc_str(input); - let mut parser = Parser::new(&bump, source.as_bytes()); + let mut parser = Parser::new(&bump, source); let module = parser.module().expect("expected successful module parse"); insta::with_settings!({ description => format!("Code:\n\n{}", input), @@ -269,7 +274,7 @@ mod tests { let input = indoc!($input); let bump = Bump::new(); let source = bump.alloc_str(input); - let mut parser = Parser::new(&bump, source.as_bytes()); + let mut parser = Parser::new(&bump, source); let error = parser.module().expect_err("expected module parse error"); insta::with_settings!({ description => format!("Code:\n\n{}", input), diff --git a/crates/nash-parse/src/type_.rs b/crates/nash-parse/src/type_.rs index b8dcb521..9543d02e 100644 --- a/crates/nash-parse/src/type_.rs +++ b/crates/nash-parse/src/type_.rs @@ -42,6 +42,10 @@ impl<'a> Parser<'a> { /// oneOfWithFallback [ arrow... ] term1 /// ``` pub fn type_expr(&mut self) -> Result<(&'a Located>, Position), error::Type<'a>> { + self.with_depth(error::Type::Space, Self::type_expr_inner) + } + + fn type_expr_inner(&mut self) -> Result<(&'a Located>, Position), error::Type<'a>> { let start = self.get_position(); // Parse first term - either type application or simple term @@ -243,6 +247,10 @@ impl<'a> Parser<'a> { /// - Tuples: `()`, `(Int, String)` /// - Records: `{}`, `{ name : String }` pub fn type_term(&mut self) -> Result<&'a Located>, error::Type<'a>> { + self.with_depth(error::Type::Space, Self::type_term_inner) + } + + fn type_term_inner(&mut self) -> Result<&'a Located>, error::Type<'a>> { let start = self.get_position(); self.one_of( @@ -584,7 +592,10 @@ impl<'a> Parser<'a> { /// Parse a type name, with an uppercase module path and either type casing. /// /// Mirrors Elm's `Var.foreignUpper`. - fn type_name(&mut self, to_error: impl FnOnce(u16, u16) -> E) -> Result, E> { + fn type_name( + &mut self, + to_error: impl FnOnce(usize, usize) -> E, + ) -> Result, E> { let (row, col) = self.position(); let start_pos = self.pos; @@ -612,7 +623,7 @@ impl<'a> Parser<'a> { fn chomp_qualified_upper_for_type( &mut self, start_pos: usize, - to_error: impl FnOnce(u16, u16) -> E, + to_error: impl FnOnce(usize, usize) -> E, ) -> Result, E> { loop { if self.is_dot_upper() { @@ -651,7 +662,7 @@ macro_rules! assert_type_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let (result, _end) = parser.type_expr().expect("expected successful parse"); parser.chomp(|_, _, _| ()).expect("expected trailing space"); assert!(parser.is_eof(), "type parser left trailing input"); @@ -670,7 +681,7 @@ macro_rules! assert_type_error_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let result = parser.type_expr().expect_err("expected parse error"); insta::with_settings!({ @@ -687,7 +698,7 @@ macro_rules! assert_scheme_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let (result, _end) = parser.type_scheme().expect("expected successful parse"); parser.chomp(|_, _, _| ()).expect("expected trailing space"); assert!(parser.is_eof(), "type scheme parser left trailing input"); @@ -706,7 +717,7 @@ macro_rules! assert_scheme_error_snapshot { ($code:expr) => {{ let bump = bumpalo::Bump::new(); let src = bump.alloc_str(indoc::indoc!($code)); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); let result = parser.type_scheme().expect_err("expected parse error"); insta::with_settings!({ @@ -727,7 +738,7 @@ macro_rules! assert_indented_type_snapshot { let fragment = indoc::indoc!($code); let indented = $crate::test_support::indent_fragment(fragment); let src = bump.alloc_str(&indented); - let mut parser = $crate::Parser::new(&bump, src.as_bytes()); + let mut parser = $crate::Parser::new(&bump, src); parser .chomp(|_, _, _| "space error") .expect("expected leading indent"); diff --git a/crates/nash-region/src/lib.rs b/crates/nash-region/src/lib.rs index fdcc0765..3745a07b 100644 --- a/crates/nash-region/src/lib.rs +++ b/crates/nash-region/src/lib.rs @@ -63,14 +63,17 @@ impl Region { } } +/// One-based source coordinates (columns count UTF-8 bytes). +/// A valid source string is at most isize::MAX bytes long, so its coordinates, +/// including one-past-end positions, fit usize without a separate size limit. #[derive(Clone, Debug, Eq, Copy, PartialEq, Hash)] pub struct Position { - pub line: u16, - pub column: u16, + pub line: usize, + pub column: usize, } impl Position { - pub const fn new(line: u16, column: u16) -> Self { + pub const fn new(line: usize, column: usize) -> Self { Self { line, column } } diff --git a/crates/nash-report/src/canonicalize.rs b/crates/nash-report/src/canonicalize.rs index eae2fefa..8b13bdec 100644 --- a/crates/nash-report/src/canonicalize.rs +++ b/crates/nash-report/src/canonicalize.rs @@ -1,6 +1,6 @@ //! Canonicalization reports, adapted from Elm's Reporting/Error/Canonicalize.hs. //! Nash adds trait, kind, representation, and nominal-record diagnostics. -use crate::{Doc, Label, Report, Snippet, Source, suggest}; +use crate::{Doc, Label, Report, Source, suggest}; use nash_ast::{Kind, ModuleName, QualifiedName}; use nash_can::{ BadArityContext, DuplicatePatternContext, Error, KindContext, PossibleNames, VarKind, @@ -74,7 +74,7 @@ pub fn to_report(source: &Source<'_>, error: &Error<'_>) -> Report { } pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name: &str) -> Report { - match error { + let report = match error { Error::MissingModuleHeader => crate::syntax::to_report( source, &nash_parse::error::Error::ModuleNameUnspecified(expected_name), @@ -319,12 +319,10 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name second, } => Report::pair( "REDUNDANT EXPORT", - label(*first, "once here"), - label(*second, "and again right here"), - Doc::reflow(&format!( - "You are trying to expose `{name}` multiple times! Once here:" - )), - Doc::text("Remove one of them and you should be all set!"), + label(*first, "first export"), + label(*second, "duplicate export"), + Doc::reflow(&format!("Duplicate export `{name}`.")), + Doc::text("Remove the duplicate export."), ), Error::ExportNotFound { region, @@ -332,36 +330,31 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name name, suggestions, } => { - let (article, thing, display) = to_kind_info(*kind, name); + let (_article, thing, display) = to_kind_info(*kind, name); let nearby = nearby(name, suggestions, 4); let mut report = simple( "UNKNOWN EXPORT", *region, - &format!( - "You are trying to expose {article} {thing} named {display} but I cannot find its definition." - ), + &format!("Unknown exported {thing} {display}."), "", ); - report.snippet = Snippet::None; report.after = suggestion_details( &nearby, - "I do not see any super similar names in this file. Is the definition missing?", + "Define the name or remove it from the exposing list.", ); report.with_suggestions(nearby) } Error::ExportOpenAlias { region, name } => simple( "BAD EXPORT", *region, - &format!( - "The (..) syntax is for exposing variants of a custom type. It cannot be used with a type alias like `{name}` though." - ), - "Remove the (..) and you should be fine!", + &format!("Type alias `{name}` has no variants to expose."), + "Remove `(..)`.", ), Error::ImportOpenAlias { region, name } => simple( "BAD IMPORT", *region, - &format!("The `{name}` type alias cannot be followed by (..) like this:"), - "Remove the (..) and it should work.", + &format!("Type alias `{name}` has no variants to import."), + "Remove `(..)`.", ), Error::ImportCtorByName { region, @@ -370,15 +363,13 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name } => simple( "BAD IMPORT", *region, - &format!("You are trying to import the `{name}` variant by name:"), - &format!( - "Try importing {type_name}(..) instead. The dots mean “expose the {type_name} type and all its variants” so it gives you access to {name}." - ), + &format!("Cannot import variant `{name}` directly."), + &format!("Import `{type_name}(..)` to make its variants available."), ), Error::ImportNotFound { region, module } => simple( "UNKNOWN IMPORT", *region, - &format!("I could not find a `{module}` module to import!"), + &format!("Unknown module `{module}`."), "", ), Error::ImportExposingNotFound { @@ -395,10 +386,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name &format!("The `{}` module does not expose `{name}`:", module.name), "", ); - report.after = suggestion_details( - &nearby, - "I cannot find any super similar exposed names. Maybe it is private?", - ); + report.after = suggestion_details(&nearby, "Check that the module exposes this name."); report.with_suggestions(nearby) } Error::BinopFunctionNotFound { @@ -408,16 +396,14 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name } => simple( "INFIX PROBLEM", *region, - &format!( - "The ({op}) operator says it is implemented by `{function}`, but I cannot find a `{function}` definition in this file." - ), + &format!("Operator `({op})` refers to undefined function `{function}`."), "Define it, or point the `infix` declaration at an existing top-level value.", ), Error::BinopConflict { region, op1, op2 } => simple( "INFIX PROBLEM", *region, &format!("You cannot mix ({op1}) and ({op2}) without parentheses."), - "I do not know how to group these expressions. Add parentheses for me!", + "Add parentheses to specify the grouping.", ), Error::NotFoundBinop { region, @@ -427,10 +413,8 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name Error::PatternHasRecordCtor { region, name } => simple( "BAD PATTERN", *region, - &format!( - "You can construct records by using `{name}` as a function, but it is not available in pattern matching like this:" - ), - "I recommend matching the record as a variable and unpacking it later.", + &format!("Record constructor `{name}` cannot be used in a pattern."), + "Bind the record to a variable and access its fields.", ), Error::Shadowing { name, @@ -439,19 +423,9 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name } => Report::pair( "SHADOWING", label(*original, "first defined here"), - label(*new, "defined AGAIN here"), - Doc::reflow(&format!("The name `{name}` is first defined here:")), - Doc::stack([ - Doc::reflow( - "Think of a more helpful name for one of them and you should be all set!", - ), - Doc::link( - "Note", - "Linters advise against shadowing, so Nash makes “best practices” the default. Read", - "shadowing", - "for more details on this choice.", - ), - ]), + label(*new, "shadows this name"), + Doc::reflow(&format!("Name `{name}` is already defined.")), + Doc::text("Rename one of these bindings."), ), Error::RecursiveDecl { name, others } => { recursive_value(name.region, name.value, others, false) @@ -468,12 +442,12 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "BAD TYPE ANNOTATION", *region, &format!( - "The type annotation for `{name}` says it can accept {}, but the definition says it has {}:", + "Annotation for `{name}` expects {}; definition has {}.", args(*index), args(index + leftovers) ), &format!( - "Is the type annotation missing something? Should some argument{} be deleted? Maybe some parentheses are missing?", + "Match the annotation to the definition's argument{}.", if *leftovers == 1 { "" } else { "s" } ), ), @@ -506,9 +480,9 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name } => simple( "KIND MISMATCH", *region, - &format!("I found a kind mismatch in {}:", kind_context(context)), + &format!("Kind mismatch in {}.", kind_context(context)), &format!( - "This position needs kind `{}`, but the type has kind `{}`. Type arguments must have matching kinds.", + "Expected kind `{}`, found `{}`.", kind(expected), kind(actual) ), @@ -520,7 +494,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "This application in {} would require an infinite kind:", kind_context(context) ), - "A type constructor cannot be applied to itself. Check which type is being applied and the kinds of its arguments.", + "Check the type application and the kinds of its arguments.", ), Error::RepresentationMismatch { region, @@ -546,7 +520,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "CONTRADICTORY REPRESENTATION", *region, &format!("The representation requirements on '{variable} are incompatible:"), - "No type can satisfy all of these requirements. Change the constraints or the positions where this type variable is used.", + "Change the incompatible constraints or uses of this variable.", ), Error::IrregularRecursion { region, @@ -559,7 +533,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "This recursive use of `{}` constructs the parameter '{parameter}:", qualified(*constructor) ), - "This parameter controls the constraints needed to form the type. Pass a type variable here so context inference can terminate.", + "Pass a type variable for this parameter so context inference can terminate.", ), Error::ImplOfBuiltinTrait { region, trait_ } => simple( "BUILTIN TRAIT", @@ -578,9 +552,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "MISSING METHOD", *region, &format!("This `{trait_}` impl does not define `{name}`:"), - &format!( - "The `{trait_}` trait requires this method and does not provide a default. Add a `{name}` definition to this impl." - ), + &format!("Add a `{name}` definition to this impl."), ), Error::UnknownMethod { region, @@ -590,7 +562,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "UNKNOWN METHOD", *region, &format!("The `{trait_}` trait has no `{name}` method:"), - "Check the method name against the trait declaration. Remove this definition or rename it to the method you intended to implement.", + "Remove or rename this method to match the trait declaration.", ), Error::BadInstanceHead { region, reason } => { use nash_can::BadHead; @@ -640,7 +612,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name key.heads.iter().map(head).collect::>().join(" ") )), Doc::reflow( - "I cannot choose which impl to use. Remove one of them, or change their heads so they cannot match the same trait arguments. Adding different context constraints does not disambiguate overlapping heads.", + "Remove one impl or make their heads disjoint; context constraints do not disambiguate heads.", ), ), Error::MissingSuperclass { @@ -734,9 +706,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name first.map_or("trait", |n| n.value), &names.iter().skip(1).map(|n| n.value).collect::>(), ), - Doc::reflow( - "Remove a superclass dependency to break the cycle. A trait cannot require itself through its superclasses.", - ), + Doc::reflow("Remove a superclass dependency to break the cycle."), ]); report } @@ -762,7 +732,7 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "UNKNOWN RECORD", *region, &format!( - "I cannot find a visible record alias with exactly these fields: {}.", + "No visible record alias has exactly these fields: {}.", fields.join(", ") ), "Declare or import an alias for this record.", @@ -814,25 +784,25 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name "IMPL PATTERN LIMIT", *region, "This impl pattern is too large or deeply nested:", - "Simplify the impl head so the compiler can compare and resolve its patterns within the supported limit.", + "Simplify the impl head.", ), Error::NegateWithoutNum { region } => simple( "NAMING ERROR", *region, - "I cannot resolve numeric negation here:", - "Negation requires the `Num` trait. Import the module that defines `Num` before using a negative expression.", + "Numeric negation requires `Num`.", + "Import the module that defines `Num`.", ), Error::DoWithoutMonad { region } => simple( "NAMING ERROR", *region, - "I cannot resolve this `do` expression:", - "A `do` expression requires the `Monad` trait. Import the module that defines `Monad`.", + "A `do` expression requires `Monad`.", + "Import the module that defines `Monad`.", ), Error::RefutableBindPattern { region } => simple( "UNSAFE PATTERN", *region, "This `do` binding has a pattern that can fail to match:", - "Use a variable or another irrefutable pattern here. Match individual variants in a `case` expression so every possibility is handled.", + "Bind a variable here, then use `case` to handle every variant.", ), Error::StructuralEqOverride { head } => simple( "STRUCTURAL EQUALITY", @@ -855,10 +825,85 @@ pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name Error::Unsupported { feature, region } => simple( "NOT SUPPORTED", *region, - &format!("I cannot canonicalize {feature} yet:"), + &format!("Unsupported feature: {feature}."), "This syntax is recognized, but its compiler implementation is not available yet.", ), - } + }; + report.with_code(match error { + Error::RecordLiteralNoAlias { .. } => "nash::names::record_literal_no_alias", + Error::RecordLiteralAmbiguous { .. } => "nash::names::record_literal_ambiguous", + Error::RecordTypeOutsideAlias { .. } => "nash::names::record_type_outside_alias", + Error::ImplPatternLimit { .. } => "nash::names::impl_pattern_limit", + Error::NegateWithoutNum { .. } => "nash::names::negate_without_num", + Error::DoWithoutMonad { .. } => "nash::names::do_without_monad", + Error::RefutableBindPattern { .. } => "nash::names::refutable_bind_pattern", + Error::StructuralEqOverride { .. } => "nash::names::structural_eq_override", + Error::ReflexiveLiftOverlap { .. } => "nash::names::reflexive_lift_overlap", + Error::MissingSuperclass { .. } => "nash::names::missing_superclass", + Error::BadInstanceHead { .. } => "nash::names::bad_instance_head", + Error::ImplContextVarNotInHead { .. } => "nash::names::impl_context_var_not_in_head", + Error::MissingMethod { .. } => "nash::names::missing_method", + Error::UnknownMethod { .. } => "nash::names::unknown_method", + Error::OrphanImpl { .. } => "nash::names::orphan_impl", + Error::OverlappingImpls { .. } => "nash::names::overlapping_impls", + Error::ImportOpenTrait { .. } => "nash::names::import_open_trait", + Error::DuplicateTrait { .. } => "nash::names::duplicate_trait", + Error::DuplicateMethod { .. } => "nash::names::duplicate_method", + Error::DuplicateTraitParameter { .. } => "nash::names::duplicate_trait_parameter", + Error::SuperclassBadArg { .. } => "nash::names::superclass_bad_arg", + Error::MethodMissingParameter { .. } => "nash::names::method_missing_parameter", + Error::RecursiveSuperclass { .. } => "nash::names::recursive_superclass", + Error::ExportOpenTrait { .. } => "nash::names::export_open_trait", + Error::NotFoundTrait { .. } => "nash::names::not_found_trait", + Error::AmbiguousTrait { .. } => "nash::names::ambiguous_trait", + Error::TraitArity { .. } => "nash::names::trait_arity", + Error::ContextVarNotInType { .. } => "nash::names::context_var_not_in_type", + Error::KindMismatch { .. } => "nash::names::kind_mismatch", + Error::KindInfinite { .. } => "nash::names::kind_infinite", + Error::RepresentationMismatch { .. } => "nash::names::representation_mismatch", + Error::ContradictoryRepresentation { .. } => "nash::names::contradictory_representation", + Error::ImplOfBuiltinTrait { .. } => "nash::names::impl_of_builtin_trait", + Error::IrregularRecursion { .. } => "nash::names::irregular_recursion", + Error::Unsupported { .. } => "nash::names::unsupported", + Error::MissingModuleHeader => "nash::names::missing_module_header", + Error::NotFoundType { .. } => "nash::names::not_found_type", + Error::ImportNotFound { .. } => "nash::names::import_not_found", + Error::AmbiguousType { .. } => "nash::names::ambiguous_type", + Error::BadArity { .. } => "nash::names::bad_arity", + Error::ExportNotFound { .. } => "nash::names::export_not_found", + Error::ExportOpenAlias { .. } => "nash::names::export_open_alias", + Error::DuplicateDecl { .. } => "nash::names::duplicate_decl", + Error::DuplicateType { .. } => "nash::names::duplicate_type", + Error::DuplicateCtor { .. } => "nash::names::duplicate_ctor", + Error::DuplicateBinop { .. } => "nash::names::duplicate_binop", + Error::BinopFunctionNotFound { .. } => "nash::names::binop_function_not_found", + Error::DuplicateUnionArg { .. } => "nash::names::duplicate_union_arg", + Error::DuplicateAliasArg { .. } => "nash::names::duplicate_alias_arg", + Error::RecursiveAlias { .. } => "nash::names::recursive_alias", + Error::TypeVarsUnboundInUnion { .. } => "nash::names::type_vars_unbound_in_union", + Error::TypeVarsMessedUpInAlias { .. } => "nash::names::type_vars_messed_up_in_alias", + Error::LabeledCtorMissingField { .. } => "nash::names::labeled_ctor_missing_field", + Error::LabeledCtorExtraField { .. } => "nash::names::labeled_ctor_extra_field", + Error::LabeledCtorUnknownField { .. } => "nash::names::labeled_ctor_unknown_field", + Error::DuplicateField { .. } => "nash::names::duplicate_field", + Error::ExportDuplicate { .. } => "nash::names::export_duplicate", + Error::NotFoundCtor { .. } => "nash::names::not_found_ctor", + Error::AmbiguousCtor { .. } => "nash::names::ambiguous_ctor", + Error::PatternHasRecordCtor { .. } => "nash::names::pattern_has_record_ctor", + Error::DuplicatePattern { .. } => "nash::names::duplicate_pattern", + Error::NotFoundVar { .. } => "nash::names::not_found_var", + Error::AmbiguousVar { .. } => "nash::names::ambiguous_var", + Error::NotFoundBinop { .. } => "nash::names::not_found_binop", + Error::AmbiguousBinop { .. } => "nash::names::ambiguous_binop", + Error::BinopConflict { .. } => "nash::names::binop_conflict", + Error::Shadowing { .. } => "nash::names::shadowing", + Error::RecursiveLet { .. } => "nash::names::recursive_let", + Error::RecursiveDecl { .. } => "nash::names::recursive_decl", + Error::AnnotationTooShort { .. } => "nash::names::annotation_too_short", + Error::ImportExposingNotFound { .. } => "nash::names::import_exposing_not_found", + Error::ImportCtorByName { .. } => "nash::names::import_ctor_by_name", + Error::ImportOpenAlias { .. } => "nash::names::import_open_alias", + }) } fn simple(title: &str, region: Region, before: &str, after: &str) -> Report { @@ -873,10 +918,10 @@ fn label(region: Region, text: &str) -> Label { fn name_clash(first: Region, second: Region, message: &str) -> Report { Report::pair( "NAME CLASH", - label(first, "one here"), - label(second, "and another one here"), - Doc::reflow(&format!("{message} One here:")), - Doc::text("How can I know which one you want? Rename one of them!"), + label(first, "first definition"), + label(second, "and another first definition"), + Doc::reflow(message), + Doc::text("Rename one of the definitions."), ) } fn qualified(name: QualifiedName<'_>) -> String { @@ -899,12 +944,12 @@ fn suggestion_details(nearby: &[String], empty: &str) -> Doc { match nearby { [] => Doc::reflow(empty), [one] => Doc::hsep([ - Doc::text("Maybe you want"), + Doc::text("Try"), Doc::text(one).dullyellow(), - Doc::text("instead?"), + Doc::text("instead."), ]), _ => Doc::stack([ - Doc::text("These names seem close though:"), + Doc::text("Similar names:"), Doc::indent( 4, Doc::vcat(nearby.iter().map(|n| Doc::text(n).dullyellow())), @@ -936,49 +981,19 @@ fn not_found( .into_iter() .take(4) .collect(); - let details = match prefix { - None => { - if nearby.is_empty() { - "Is there an `import` or `exposing` missing up top?".into() - } else { - "These names seem close though:".into() - } - } - Some(p) if possible.qualified.iter().any(|(m, _)| *m == p) => format!( - "The `{p}` module does not expose a `{name}` {thing}.{}", - if nearby.is_empty() { - "" - } else { - " These names seem close though:" - } - ), - Some(p) => { - if nearby.is_empty() { - format!("I cannot find a `{p}` module. Is there an `import` for it?") - } else { - format!("I cannot find a `{p}` import. These names seem close though:") - } + let hint = match prefix { + Some(p) if !possible.qualified.iter().any(|(m, _)| *m == p) => { + format!("Import `{p}` or check its alias.") } + Some(p) => format!("Check that `{p}` exposes `{name}`."), + None => "Define or import this name.".into(), }; - let mut docs = vec![Doc::reflow(&details)]; - if !nearby.is_empty() { - docs.push(Doc::indent( - 4, - Doc::vcat(nearby.iter().map(|n| Doc::text(n).dullyellow())), - )); - } - docs.push(Doc::link( - "Hint", - "Read", - "imports", - "to see how `import` declarations work in Nash.", - )); Report::snippet( "NAMING ERROR", region, None, - Doc::reflow(&format!("I cannot find a `{given}` {thing}:")), - Doc::stack(docs), + Doc::text(format!("Unknown {thing} `{given}`.")), + suggestion_details(&nearby, &hint), ) .with_suggestions(nearby) } @@ -993,65 +1008,30 @@ fn ambiguous_name( let mut homes = vec![first]; homes.extend_from_slice(others); homes.sort(); - match prefix { - None => Report::snippet( - "AMBIGUOUS NAME", - region, - None, - Doc::reflow(&format!("This usage of `{name}` is ambiguous:")), - Doc::stack([ - Doc::reflow(&format!( - "This name is exposed by {} of your imports, so I am not sure which one to use:", - homes.len() - )), - Doc::indent( - 4, - Doc::vcat( - homes - .iter() - .map(|h| Doc::text(to_qual_string(h.name, name)).dullyellow()), - ), - ), - Doc::reflow( - "I recommend using qualified names for imported values. I also recommend having at most one `exposing (..)` per file to make name clashes like this less common in the long run.", - ), - Doc::link( - "Note", - "Check out", - "imports", - "for more info on the import syntax.", - ), - ]), - ), - Some(prefix) => Report::snippet( - "AMBIGUOUS NAME", - region, - None, - Doc::reflow(&format!("This usage of `{prefix}.{name}` is ambiguous.")), - Doc::stack([ - Doc::reflow(&format!( - "It could refer to a {thing} from {} of these imports:", - if homes.len() == 2 { "either" } else { "any" } - )), - Doc::indent( - 4, - Doc::vcat(homes.iter().map(|h| { - Doc::text(if prefix == h.name { - format!("import {}", h.name) - } else { - format!("import {} as {prefix}", h.name) - }) - })), - ), - Doc::reflow_link( - "Read", - "imports", - "to learn how to clarify which one you want.", + let given = prefix.map_or_else(|| name.to_string(), |p| to_qual_string(p, name)); + Report::snippet( + "AMBIGUOUS NAME", + region, + None, + Doc::text(format!("Ambiguous {thing} `{given}`.")), + Doc::stack([ + Doc::indent( + 4, + Doc::vcat( + homes + .iter() + .map(|h| Doc::text(to_qual_string(h.name, name))), ), - ]), - ), - } + ), + Doc::text(if prefix.is_some() { + "Give these imports distinct aliases." + } else { + "Use a qualified name." + }), + ]), + ) } + fn args(n: usize) -> String { format!("{n} argument{}", if n == 1 { "" } else { "s" }) } @@ -1070,58 +1050,37 @@ fn arity(region: Region, name: &str, thing: &str, expected: usize, actual: usize args(expected) ), if actual < expected { - "What is missing? Are some parentheses misplaced?" - } else if actual - expected == 1 { - "Which is the extra one? Maybe some parentheses are missing?" + "Supply the missing arguments." } else { - "Which are the extra ones? Maybe some parentheses are missing?" + "Remove the extra arguments or check the grouping." }, ) } fn not_found_binop(region: Region, name: &str, available: &[&str]) -> Report { - let (before,after,suggestions) = match name { - "===" => ("Nash does not have a (===) operator like JavaScript.".into(),"Switch to (==) instead.".into(),vec!["==".into()]), - "!="|"!==" => ("Nash uses a different name for the “not equal” operator:".into(),format!("Switch to (/=) instead. Our (/=) operator is supposed to look like a real “not equal” sign (≠). I hope that history will remember ({name}) as a weird and temporary choice."),vec!["/=".into()]), - "**" => ("I do not recognize the (**) operator:".into(),"Switch to (^) for exponentiation. Or switch to (*) for multiplication.".into(),vec!["^".into(),"*".into()]), - // The stdlib names Int.rem and Int.mod are provisional in Plan 06. - "%" => ("Nash does not use (%) as the remainder operator:".into(),"If you want the behavior of (%) like in JavaScript, use the integer remainder function. If you want modular arithmetic like in math, use the integer modulus function. The difference is how things work when negative numbers are involved.".into(),vec![]), - _ => {let choices=nearby(name,available,2); let mut after="Is there an `import` and `exposing` entry for it?".to_string(); if !choices.is_empty() {after.push_str(&format!(" Maybe you want {} instead?",choices.iter().map(|s|format!("({s})")).collect::>().join(" or ")));} (format!("I do not recognize the ({name}) operator."),after,choices)} + let suggestions = match name { + "===" => vec!["==".into()], + "!=" | "!==" => vec!["/=".into()], + "**" => vec!["^".into(), "*".into()], + "%" => vec![], + _ => nearby(name, available, 2), }; - simple("UNKNOWN OPERATOR", region, &before, &after).with_suggestions(suggestions) + let mut report = simple( + "UNKNOWN OPERATOR", + region, + &format!("Unknown operator `({name})`."), + "", + ); + report.after = suggestion_details( + &suggestions, + if name == "%" { + "Use an integer remainder or modulus function." + } else { + "Import and expose the operator." + }, + ); + report.with_suggestions(suggestions) } fn recursive_value(region: Region, name: &str, others: &[&str], is_let: bool) -> Report { - let before = if others.is_empty() { - format!( - "The `{name}` value is defined directly in terms of itself, causing an infinite loop." - ) - } else if is_let { - "I do not allow cyclic values in `let` expressions.".into() - } else { - format!("The `{name}` definition is causing a very tricky infinite loop.") - }; - let mut docs = if others.is_empty() { - vec![ - Doc::reflow(&format!( - "Are you trying to mutate a variable? Nash does not have mutation, so when I see {name} defined in terms of {name}, I treat it as a recursive definition. Try giving the new value a new name!" - )), - Doc::reflow(&format!( - "Maybe you DO want a recursive value? To define {name} we need to know what {name} is, so let’s expand it. Wait, but now we need to know what {name} is, so let’s expand it... This will keep going infinitely!" - )), - ] - } else { - vec![ - Doc::reflow(&format!( - "The `{name}` value depends on itself through the following chain of definitions:" - )), - Doc::cycle(4, name, others), - ] - }; - docs.push(Doc::link( - "Hint", - "The root problem is often a typo in some variable name, but I recommend reading", - "bad-recursion", - "for more detailed advice, especially if you actually do need a recursive value.", - )); Report::snippet( if is_let { "CYCLIC VALUE" @@ -1130,8 +1089,15 @@ fn recursive_value(region: Region, name: &str, others: &[&str], is_let: bool) -> }, region, None, - Doc::reflow(&before), - Doc::stack(docs), + Doc::text(format!("Value `{name}` depends on itself.")), + Doc::stack([ + if others.is_empty() { + Doc::Empty + } else { + Doc::cycle(4, name, others) + }, + Doc::text("Break the cycle between these value definitions."), + ]), ) } fn alias_recursion_report( @@ -1141,41 +1107,23 @@ fn alias_recursion_report( typ: &nash_region::Located>, others: &[&str], ) -> Report { - let (before, after) = if others.is_empty() { - ( - "This type alias is recursive, forming an infinite type!", - Doc::stack([ - Doc::reflow( - "When I expand a recursive type alias, it just keeps getting bigger and bigger. So dealiasing results in an infinitely large type! Try this instead:", - ), + Report::snippet( + "ALIAS PROBLEM", + region, + None, + Doc::text(format!("Type alias `{name}` expands recursively.")), + Doc::stack(if others.is_empty() { + vec![ + Doc::text("Use a custom type:"), Doc::indent(4, alias_to_union_doc(name, args, typ)), - Doc::link( - "Hint", - "This is kind of a subtle distinction. I suggested the naive fix, but I recommend reading", - "recursive-alias", - "for ideas on how to do better.", - ), - ]), - ) - } else { - ( - "This type alias is part of a mutually recursive set of type aliases.", - Doc::stack([ - Doc::text("It is part of this cycle of type aliases:"), + ] + } else { + vec![ Doc::cycle(4, name, others), - Doc::reflow( - "You need to convert at least one of these type aliases into a `type`.", - ), - Doc::link( - "Note", - "Read", - "recursive-alias", - "to learn why this `type` vs `type alias` distinction matters. It is subtle but important!", - ), - ]), - ) - }; - Report::snippet("ALIAS PROBLEM", region, None, Doc::text(before), after) + Doc::text("Convert at least one alias in this cycle to a custom type."), + ] + }), + ) } fn alias_to_union_doc( name: &str, @@ -1230,12 +1178,8 @@ fn unbound_type_vars( others.is_empty().then_some(first.1), Doc::reflow(&before), Doc::stack([ - Doc::reflow("You probably need to change the declaration to something like this:"), + Doc::text("Declare the type variables:"), declaration(decl, name, args, &names), - Doc::reflow(&format!( - "Why? Well, imagine one `{name}` where `{}` is an Int and another where it is a Bool. When we explicitly list the type variables, the type checker can see that they are actually different types.", - first.0 - )), ]), ) } @@ -1295,13 +1239,10 @@ fn alias_vars( }), Doc::stack([ Doc::reflow(&format!( - "I recommend removing {} from the declaration, like this:", + "Remove {} from the declaration:", unused_names.join(" and ") )), declaration("type alias", name, &kept, &[]), - Doc::reflow( - "Why? Well, if I allowed `type alias Height 'a = Int` I would need to answer some weird questions. Is `Height Bool` the same as `Int`? Is `Height Bool` the same as `Height Int`? My solution is to not need to ask them!", - ), ]), ) } else { @@ -1335,7 +1276,7 @@ fn alias_vars( ) } )), - Doc::reflow("My guess is that a definition like this will work better:"), + Doc::reflow("Match the declaration to the variables used:"), declaration("type alias", name, &kept, &unbound_names), ]), ) @@ -2599,7 +2540,7 @@ mod branches { let input = "module Main exposing (..)\nfirst = missing\nsecond = absent\n"; let bump = bumpalo::Bump::new(); let src = bump.alloc_str(input); - let mut parser = nash_parse::Parser::new(&bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(&bump, src); let module = parser.module().expect("parse"); let errors = nash_can::canonicalize(&bump, nash_can::Context::default(), &module) .expect_err("canonical errors"); @@ -2710,7 +2651,7 @@ mod branches { let input = "module Bad exposing (..)\ntrait Keep 'a where\n keep : 'a -> 'a\nimpl Keep () where\n keep x = x\nimpl Keep () where\n keep x = x\n"; let bump = bumpalo::Bump::new(); let src = bump.alloc_str(input); - let mut parser = nash_parse::Parser::new(&bump, src.as_bytes()); + let mut parser = nash_parse::Parser::new(&bump, src); let module = parser.module().expect("parse"); let errors = nash_can::canonicalize(&bump, nash_can::Context::default(), &module) .expect_err("overlapping impls"); @@ -2721,9 +2662,7 @@ mod branches { let report = to_report(&source, error); assert_eq!(report.title, "OVERLAPPING IMPL"); assert_eq!(report.region, *second); - assert!( - matches!(&report.snippet, Snippet::Pair { first: a, second: b } if a.region == *first && b.region == *second) - ); + assert!(report.labels[0].region == *first && report.region == *second); let rendered = crate::render_plain(&report, &source, "Bad.nash"); assert!(!rendered.contains("Rename")); assert!(rendered.contains("context constraints")); diff --git a/crates/nash-report/src/code.rs b/crates/nash-report/src/code.rs index d3a88c8e..cbe688b1 100644 --- a/crates/nash-report/src/code.rs +++ b/crates/nash-report/src/code.rs @@ -1,8 +1,6 @@ //! Source text access for reports, from Elm's `Reporting/Render/Code.hs`. //! Columns are byte-based, matching `nash_parse::Parser::advance`. -mod snippet; - use miette::SourceSpan; use nash_parse::{Col, Row}; use nash_region::{Position, Region}; @@ -28,7 +26,7 @@ impl<'s> Source<'s> { /// Byte offset of a 1-based position produced by the parser. pub fn offset(&self, position: Position) -> usize { - let row = usize::from(position.line.saturating_sub(1)); + let row = position.line.saturating_sub(1); let Some(&start) = self.line_starts.get(row) else { return self.text.len(); }; @@ -37,7 +35,7 @@ impl<'s> Source<'s> { .get(row + 1) .map_or(self.text.len(), |next| next - 1); let mut offset = start - .saturating_add(usize::from(position.column.saturating_sub(1))) + .saturating_add(position.column.saturating_sub(1)) .min(end); while !self.text.is_char_boundary(offset) { offset -= 1; @@ -58,10 +56,10 @@ impl<'s> Source<'s> { /// Text of a 1-based row, without its newline. pub fn line(&self, row: Row) -> Option<&'s str> { - let start = *self.line_starts.get(usize::from(row.checked_sub(1)?))?; + let start = *self.line_starts.get(row.checked_sub(1)?)?; let end = self .line_starts - .get(usize::from(row)) + .get(row) .map_or(self.text.len(), |next| next - 1); Some(&self.text[start..end.max(start)]) } @@ -70,7 +68,7 @@ impl<'s> Source<'s> { pub fn what_is_next(&self, row: Row, col: Col) -> Next<'s> { let Some(rest) = self .line(row) - .and_then(|line| line.get(usize::from(col.checked_sub(1)?)..)) + .and_then(|line| line.get(col.checked_sub(1)?..)) else { return Next::Other(None); }; @@ -150,7 +148,7 @@ pub fn to_region(row: Row, col: Col) -> Region { } /// Elm's `toWiderRegion`. -pub fn to_wider_region(row: Row, col: Col, extra: u16) -> Region { +pub fn to_wider_region(row: Row, col: Col, extra: usize) -> Region { Region::new( Position::new(row, col), Position::new(row, col.saturating_add(extra)), @@ -159,7 +157,7 @@ pub fn to_wider_region(row: Row, col: Col, extra: u16) -> Region { /// Elm's `toKeywordRegion`. pub fn to_keyword_region(row: Row, col: Col, keyword: &str) -> Region { - to_wider_region(row, col, keyword.len() as u16) + to_wider_region(row, col, keyword.len()) } #[cfg(test)] diff --git a/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__pair_snippet_snapshot.snap b/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__pair_snippet_snapshot.snap deleted file mode 100644 index 280bd08e..00000000 --- a/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__pair_snippet_snapshot.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/nash-report/src/code/snippet.rs -expression: "source.snippet_doc(&snippet).render(80, false)" ---- -1| a = x - ^ - -2| b = y - ^ diff --git a/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__unicode_pair_on_separate_lines.snap b/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__unicode_pair_on_separate_lines.snap deleted file mode 100644 index 2b0b87c3..00000000 --- a/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__unicode_pair_on_separate_lines.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/nash-report/src/code/snippet.rs -expression: "Source::new(\"é x\\n界\\ty\").pair_doc(region(1, 4, 1, 5),\nregion(2, 5, 2, 6)).render(80, false)" ---- -1| é x - ^ - -2| 界 y - ^ diff --git a/crates/nash-report/src/code/snippet.rs b/crates/nash-report/src/code/snippet.rs deleted file mode 100644 index c0ce587b..00000000 --- a/crates/nash-report/src/code/snippet.rs +++ /dev/null @@ -1,372 +0,0 @@ -//! Elm's `Reporting/Render/Code.hs` source drawings for JSON messages. - -use super::Source; -use crate::{Doc, Snippet}; -use nash_region::Region; -use unicode_width::UnicodeWidthChar; - -impl Source<'_> { - /// Render source using Elm's line numbers, red carets, and multiline arrows. - pub fn snippet_doc(&self, snippet: &Snippet) -> Doc { - match snippet { - Snippet::Region { region, highlight } => self.region_doc(*region, *highlight), - Snippet::Pair { first, second } => self.pair_doc(first.region, second.region), - Snippet::None => Doc::Empty, - } - } - - /// Elm's `render`, `makeUnderline`, and `drawLines`. - pub fn region_doc(&self, region: Region, highlight: Option) -> Doc { - let start = region.start.line.max(1); - let end = region.end.line.max(start); - let lines: Vec<_> = (start..=end) - .map_while(|row| self.line(row).map(|line| (row, line))) - .collect(); - let Some(&(last, _)) = lines.last() else { - return Doc::Empty; - }; - let width = last.to_string().len(); - let highlight = highlight.unwrap_or(region); - let underline = highlight.start.line == highlight.end.line && highlight.end.line >= end; - let mut docs: Vec<_> = lines - .into_iter() - .map(|(row, line)| { - let spacer = - if !underline && highlight.start.line <= row && row <= highlight.end.line { - Doc::text(">").red() - } else { - Doc::text(" ") - }; - Doc::cat([ - Doc::text(format!("{row:>width$}|")), - spacer, - Doc::text(display_line(line)), - ]) - }) - .collect(); - docs.push(if underline { - let line = self.line(highlight.start.line).unwrap_or(""); - let (start, end) = visual_range(line, highlight); - Doc::cat([ - Doc::text(" ".repeat(start + width + 2)), - Doc::text("^".repeat(end - start)).red(), - ]) - } else { - Doc::Empty - }); - Doc::vcat(docs) - } - - /// Elm's `renderPair`: one line with two underlines, or two code chunks. - /// The report's primary region is independent of this source ordering. - pub fn pair_doc(&self, first: Region, second: Region) -> Doc { - let (first, second) = - if (first.start.line, first.start.column) <= (second.start.line, second.start.column) { - (first, second) - } else { - (second, first) - }; - if first.start.line == first.end.line - && first.end.line == second.start.line - && second.start.line == second.end.line - { - let row = first.start.line; - let Some(line) = self.line(row) else { - return Doc::Empty; - }; - let width = row.to_string().len(); - let (first_start, first_end) = visual_range(line, first); - let (second_start, second_end) = visual_range(line, second); - Doc::vcat([ - Doc::text(format!("{row}| {}", display_line(line))), - Doc::cat([ - Doc::text(" ".repeat(first_start + width + 2)), - Doc::text("^".repeat(first_end - first_start)).red(), - Doc::text(" ".repeat(second_start.saturating_sub(first_end))), - Doc::text("^".repeat(second_end - second_start)).red(), - ]), - ]) - } else { - Doc::stack([self.region_doc(first, None), self.region_doc(second, None)]) - } - } -} - -// Match miette's default four-cell tab stops, measured from the source text -// rather than the line-number gutter. Regions remain one-based byte columns. -const TAB_WIDTH: usize = 4; - -fn char_width(ch: char, column: usize) -> usize { - if ch == '\t' { - TAB_WIDTH - column % TAB_WIDTH - } else { - ch.width().unwrap_or(0) - } -} - -fn display_line(line: &str) -> String { - let mut text = String::with_capacity(line.len()); - let mut column = 0; - for ch in line.chars() { - let width = char_width(ch, column); - if ch == '\t' { - text.extend(std::iter::repeat_n(' ', width)); - } else { - text.push(ch); - } - column += width; - } - text -} - -fn visual_column(line: &str, byte_column: u16) -> usize { - let mut offset = usize::from(byte_column.saturating_sub(1)).min(line.len()); - // Match Source::offset when an invalid input points inside a UTF-8 scalar. - while !line.is_char_boundary(offset) { - offset -= 1; - } - line[..offset] - .chars() - .fold(0, |column, ch| column + char_width(ch, column)) -} - -fn visual_range(line: &str, region: Region) -> (usize, usize) { - let start = visual_column(line, region.start.column); - let end = visual_column(line, region.end.column).max(start + 1); - (start, end) -} - -#[cfg(test)] -mod tests { - use crate::{Doc, Label, Snippet, Source}; - use nash_region::{Position, Region}; - - fn region(sr: u16, sc: u16, er: u16, ec: u16) -> Region { - Region::new(Position::new(sr, sc), Position::new(er, ec)) - } - - #[test] - fn single_accent_uses_visible_columns() { - assert_eq!( - Source::new("é x") - .region_doc(region(1, 4, 1, 5), None) - .render(80, false), - "1| é x\n ^" - ); - assert_eq!( - Source::new("é x") - .region_doc(region(1, 1, 1, 3), None) - .render(80, false), - "1| é x\n ^" - ); - } - - #[test] - fn single_astral_uses_two_cells() { - assert_eq!( - Source::new("😀 x") - .region_doc(region(1, 6, 1, 7), None) - .render(80, false), - "1| 😀 x\n ^" - ); - assert_eq!( - Source::new("😀 x") - .region_doc(region(1, 1, 1, 5), None) - .render(80, false), - "1| 😀 x\n ^^" - ); - } - - #[test] - fn single_cjk_uses_two_cells() { - assert_eq!( - Source::new("界 x") - .region_doc(region(1, 5, 1, 6), None) - .render(80, false), - "1| 界 x\n ^" - ); - assert_eq!( - Source::new("界 x") - .region_doc(region(1, 1, 1, 4), None) - .render(80, false), - "1| 界 x\n ^^" - ); - } - - #[test] - fn single_tabs_expand_from_source_column() { - assert_eq!( - Source::new("\tx") - .region_doc(region(1, 2, 1, 3), None) - .render(80, false), - "1| x\n ^" - ); - assert_eq!( - Source::new("é\t界 x") - .region_doc(region(1, 8, 1, 9), None) - .render(80, false), - "1| é 界 x\n ^" - ); - } - - #[test] - fn pair_accent_uses_visible_columns() { - assert_eq!( - Source::new("é x") - .pair_doc(region(1, 1, 1, 3), region(1, 4, 1, 5)) - .render(80, false), - "1| é x\n ^ ^" - ); - } - - #[test] - fn pair_astral_uses_two_cells() { - assert_eq!( - Source::new("😀 x") - .pair_doc(region(1, 1, 1, 5), region(1, 6, 1, 7)) - .render(80, false), - "1| 😀 x\n ^^ ^" - ); - } - - #[test] - fn pair_cjk_uses_two_cells() { - assert_eq!( - Source::new("界 x") - .pair_doc(region(1, 1, 1, 4), region(1, 5, 1, 6)) - .render(80, false), - "1| 界 x\n ^^ ^" - ); - } - - #[test] - fn pair_tabs_expand_from_source_column() { - assert_eq!( - Source::new("\tx") - .pair_doc(region(1, 1, 1, 2), region(1, 2, 1, 3)) - .render(80, false), - "1| x\n ^^^^^" - ); - assert_eq!( - Source::new("é\t界 x") - .pair_doc(region(1, 1, 1, 3), region(1, 8, 1, 9)) - .render(80, false), - "1| é 界 x\n ^ ^" - ); - } - - #[test] - fn combining_marks_and_unicode_insertion() { - let source = Source::new("e\u{301} x"); - assert_eq!( - source - .region_doc(region(1, 5, 1, 6), None) - .render(80, false), - "1| e\u{301} x\n ^" - ); - assert_eq!( - source - .region_doc(region(1, 6, 1, 6), None) - .render(80, false), - "1| e\u{301} x\n ^" - ); - } - - #[test] - fn unicode_pair_on_separate_lines() { - insta::assert_snapshot!( - Source::new("é x\n界\ty") - .pair_doc(region(1, 4, 1, 5), region(2, 5, 2, 6)) - .render(80, false) - ); - } - - #[test] - fn single_line_and_insertion_carets() { - let source = Source::new("value = missing"); - assert_eq!( - source - .region_doc(region(1, 9, 1, 16), None) - .render(80, false), - "1| value = missing\n ^^^^^^^" - ); - assert_eq!( - source - .region_doc(region(1, 16, 1, 16), None) - .render(80, false), - "1| value = missing\n ^" - ); - } - - #[test] - fn multiline_highlight_arrows_and_line_number_width() { - let source = Source::new("\n\n\n\n\n\n\n\nfirst\nsecond\nthird"); - assert_eq!( - source - .region_doc(region(9, 1, 11, 6), Some(region(10, 1, 10, 7))) - .render(80, false), - " 9| first\n10|>second\n11| third\n" - ); - assert_eq!( - source - .region_doc(region(9, 1, 11, 6), Some(region(11, 1, 11, 6))) - .render(80, false), - " 9| first\n10| second\n11| third\n ^^^^^" - ); - } - - #[test] - fn same_line_pair_and_separate_chunks() { - let source = Source::new("first second\nthird"); - assert_eq!( - source - .pair_doc(region(1, 1, 1, 6), region(1, 7, 1, 13)) - .render(80, false), - "1| first second\n ^^^^^ ^^^^^^" - ); - assert_eq!( - source - .pair_doc(region(1, 1, 1, 6), region(2, 1, 2, 6)) - .render(80, false), - "1| first second\n ^^^^^\n\n2| third\n ^^^^^" - ); - } - - #[test] - fn pair_accepts_reverse_source_order() { - let source = Source::new("first second"); - assert_eq!( - source - .pair_doc(region(1, 7, 1, 13), region(1, 1, 1, 6)) - .render(80, false), - "1| first second\n ^^^^^ ^^^^^^" - ); - } - - #[test] - fn empty_source_and_absent_snippet() { - assert_eq!( - Source::new("") - .region_doc(region(1, 1, 1, 1), None) - .render(80, false), - "1|\n ^" - ); - assert_eq!(Source::new("").snippet_doc(&Snippet::None), Doc::Empty); - } - - #[test] - fn pair_snippet_snapshot() { - let source = Source::new("a = x\nb = y"); - let snippet = Snippet::Pair { - first: Label { - region: region(1, 5, 1, 6), - text: "first use".into(), - }, - second: Label { - region: region(2, 5, 2, 6), - text: "second use".into(), - }, - }; - insta::assert_snapshot!(source.snippet_doc(&snippet).render(80, false)); - } -} diff --git a/crates/nash-report/src/json.rs b/crates/nash-report/src/json.rs index c959a9f0..663c5b5a 100644 --- a/crates/nash-report/src/json.rs +++ b/crates/nash-report/src/json.rs @@ -1,16 +1,15 @@ -//! Elm's `Reporting/Error.hs` JSON schema and complete styled messages. +//! Structured diagnostics in the Elm compile-error envelope. -use crate::{Doc, ModuleReports, Report, Snippet, Source}; +use crate::{Doc, ModuleReports, Report, Severity}; use nash_region::Region; use serde_json::{Value, json}; /// Elm's `toJson`. Report ordering is established by `ModuleReports::sort`. pub fn module_to_json(module: &ModuleReports) -> Value { - let source = Source::new(&module.source); json!({ "path": module.path, "name": module.name, - "problems": module.reports.iter().map(|report| report_to_json(&source, report)).collect::>(), + "problems": module.reports.iter().map(report_to_json).collect::>(), }) } @@ -24,17 +23,31 @@ pub fn compile_warnings(modules: &[ModuleReports]) -> Value { json!({"type": "compile-warnings", "errors": modules.iter().map(module_to_json).collect::>()}) } -fn report_to_json(source: &Source<'_>, report: &Report) -> Value { - let message = match report.snippet { - Snippet::None => Doc::stack([report.before.clone(), report.after.clone()]), - _ => Doc::vcat([ - report.before.clone(), - Doc::Empty, - source.snippet_doc(&report.snippet), - report.after.clone(), - ]), - }; - json!({"title": report.title, "region": encode_region(report.region), "message": message.encode()}) +pub fn report_to_json(report: &Report) -> Value { + let labels: Vec<_> = report + .primary_label + .iter() + .map(|text| { + json!({ + "region": encode_region(report.region), "text": text, "primary": true, + }) + }) + .chain(report.labels.iter().map(|label| { + json!({ + "region": encode_region(label.region), "text": label.text, "primary": false, + }) + })) + .collect(); + json!({ + "code": report.code, + "title": report.title, + "severity": match report.severity { Severity::Error => "error", Severity::Warning => "warning" }, + "region": encode_region(report.region), + "message": Doc::stack([report.before.clone(), report.after.clone()]).encode(), + "labels": labels, + "suggestions": report.suggestions, + "related": report.related.iter().map(module_to_json).collect::>(), + }) } /// Elm's one-based, half-open source region schema. @@ -48,10 +61,10 @@ pub fn encode_region(region: Region) -> Value { #[cfg(test)] mod tests { use super::*; - use crate::{Doc, Label, Snippet}; + use crate::{Doc, Label}; use nash_region::Position; - fn region(sr: u16, sc: u16, er: u16, ec: u16) -> Region { + fn region(sr: usize, sc: usize, er: usize, ec: usize) -> Region { Region::new(Position::new(sr, sc), Position::new(er, ec)) } @@ -84,7 +97,7 @@ mod tests { encode_region(region(2, 5, 2, 12)) ); assert_eq!(value.as_object().unwrap().len(), 3); - assert_eq!(value["problems"][0].as_object().unwrap().len(), 3); + assert_eq!(value["problems"][0]["suggestions"], json!(["found"])); insta::assert_snapshot!(serde_json::to_string_pretty(&value).unwrap()); } @@ -97,9 +110,9 @@ mod tests { Doc::text("First."), Doc::text("Second."), ); - report.snippet = Snippet::None; + report = report.without_source(); assert_eq!( - report_to_json(&Source::new(""), &report)["message"], + report_to_json(&report)["message"], serde_json::json!(["First.\n\nSecond."]) ); } @@ -119,9 +132,7 @@ mod tests { Doc::text("Both names occur here:"), Doc::text("Choose another name."), ); - insta::assert_snapshot!( - serde_json::to_string_pretty(&report_to_json(&Source::new("x\nx"), &report)).unwrap() - ); + insta::assert_snapshot!(serde_json::to_string_pretty(&report_to_json(&report)).unwrap()); } #[test] @@ -136,3 +147,58 @@ mod tests { ); } } + +#[cfg(test)] +mod structured_tests { + use super::*; + use crate::Label; + #[test] + fn labels_codes_and_related_reports_are_structured() { + let mut report = Report::snippet( + "OLD TITLE", + Region::zero(), + None, + Doc::text("Problem."), + Doc::text("Hint."), + ) + .with_code("nash::type::mismatch") + .with_suggestions(vec!["replacement".into()]); + report.title = "NEW TITLE".into(); + report.primary_label = Some("failing argument".into()); + for text in ["annotation", "earlier argument"] { + report.labels.push(Label { + region: Region::zero(), + text: text.into(), + }); + } + report.related.push(ModuleReports { + name: "Other".into(), + path: "Other.nash".into(), + source: "other".into(), + reports: vec![ + Report::snippet( + "RELATED", + Region::zero(), + None, + Doc::text("Origin."), + Doc::Empty, + ) + .with_code("nash::type::origin"), + ], + }); + let value = report_to_json(&report); + assert_eq!(value["code"], "nash::type::mismatch"); + assert_eq!(value["title"], "NEW TITLE"); + assert_eq!(value["labels"].as_array().unwrap().len(), 3); + assert_eq!(value["labels"][0]["primary"], true); + assert_eq!(value["labels"][1]["text"], "annotation"); + assert_eq!(value["labels"][2]["primary"], false); + assert_eq!(value["related"][0]["path"], "Other.nash"); + assert_eq!( + value["related"][0]["problems"][0]["code"], + "nash::type::origin" + ); + assert_eq!(value["suggestions"], json!(["replacement"])); + assert_eq!(value["message"], json!(["Problem.\n\nHint."])); + } +} diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index 8ec477ec..523c736e 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -1,4 +1,4 @@ -//! Error reports: Elm's `Reporting/*` prose as miette diagnostics. +//! Concise, source-aware diagnostics shared by terminal, JSON, and LSP. //! //! Each phase's error data (`nash_parse::error`, `nash_can::Error`, ...) //! is turned into an owned `Report` that outlives the module arena. A @@ -27,14 +27,19 @@ pub use code::Source; pub use doc::Doc; pub use render::{Rendered, handler, render_plain}; -/// Elm's `Reporting.Report.Report` with the snippet placement split out -/// so miette can draw the code. +/// An owned diagnostic with one primary span, arbitrary secondary labels, and +/// related reports that can refer to other source files. #[derive(Clone, Debug)] pub struct Report { + pub code: &'static str, pub title: String, pub severity: Severity, pub region: Region, - pub snippet: Snippet, + /// Text on the primary region, or None for a report without a source label. + pub primary_label: Option, + pub labels: Vec