From 04b70cb8b068ebc5002dfa07a95f466de7069d36 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 01/12] fix(parse): preserve nested diagnostic context Signed-off-by: microproofs --- crates/nash-parse/src/lib.rs | 4 +- crates/nash-parse/src/module.rs | 37 +++++++++++++++---- ...odule_preserves_annotation_name_error.snap | 18 +++++++++ ...ts__module_preserves_expression_error.snap | 25 +++++++++++++ ...__module_preserves_import_alias_error.snap | 8 ++++ ...tests__module_preserves_pattern_error.snap | 29 +++++++++++++++ ...ts__module_preserves_type_alias_error.snap | 20 ++++++++++ 7 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_annotation_name_error.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_expression_error.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_import_alias_error.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_pattern_error.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_type_alias_error.snap diff --git a/crates/nash-parse/src/lib.rs b/crates/nash-parse/src/lib.rs index d67d00a6..08199622 100644 --- a/crates/nash-parse/src/lib.rs +++ b/crates/nash-parse/src/lib.rs @@ -7,13 +7,13 @@ pub mod error; mod exposing; mod expression; mod import; -mod keyword; +pub mod keyword; mod module; mod number; mod pattern; mod space; mod string; -mod symbol; +pub mod symbol; #[cfg(test)] pub(crate) mod test_support; mod tests_block; diff --git a/crates/nash-parse/src/module.rs b/crates/nash-parse/src/module.rs index dc109a2f..806793dc 100644 --- a/crates/nash-parse/src/module.rs +++ b/crates/nash-parse/src/module.rs @@ -100,14 +100,14 @@ impl<'a> Parser<'a> { imports.push(import); // import() already ensures fresh line at the end } - Err(_) => { + Err(error) => { // If we didn't consume input, we're done with imports if self.pos == state.pos { self.restore_state(state); break; } // Otherwise propagate the error - return Err(error::Module::ImportStart(self.row, self.col)); + return Err(error); } } } @@ -156,18 +156,14 @@ impl<'a> Parser<'a> { break; } } - Err(_) => { + Err(error) => { // If we didn't consume input, we're done with declarations if self.pos == state.pos { self.restore_state(state); break; } // Otherwise propagate the error - return Err(error::Module::Declarations( - self.bump.alloc(error::Decl::Start(self.row, self.col)), - self.row, - self.col, - )); + return Err(error); } } } @@ -593,4 +589,29 @@ mod tests { fn validator_module_must_remain_indented() { assert_module_error_snapshot!("validator\nmodule V exposing (..)"); } + + #[test] + fn module_preserves_import_alias_error() { + assert_module_error_snapshot!("import Cardano.Tx as tx"); + } + + #[test] + fn module_preserves_type_alias_error() { + assert_module_error_snapshot!("type alias account"); + } + + #[test] + fn module_preserves_pattern_error() { + assert_module_error_snapshot!("f (x as) = x"); + } + + #[test] + fn module_preserves_expression_error() { + assert_module_error_snapshot!("value = if True then 42"); + } + + #[test] + fn module_preserves_annotation_name_error() { + assert_module_error_snapshot!("f : int\ng = 1"); + } } diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_annotation_name_error.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_annotation_name_error.snap new file mode 100644 index 00000000..04fa359f --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_annotation_name_error.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-parse/src/module.rs +description: "Code:\n\nf : int\ng = 1" +--- +Declarations( + Def( + "f", + NameMatch( + "g", + 2, + 2, + ), + 1, + 2, + ), + 1, + 1, +) diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_expression_error.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_expression_error.snap new file mode 100644 index 00000000..ca6f49c7 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_expression_error.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-parse/src/module.rs +description: "Code:\n\nvalue = if True then 42" +--- +Declarations( + Def( + "value", + Body( + If( + Else( + 1, + 24, + ), + 1, + 9, + ), + 1, + 9, + ), + 1, + 6, + ), + 1, + 1, +) diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_import_alias_error.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_import_alias_error.snap new file mode 100644 index 00000000..ff440da4 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_import_alias_error.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-parse/src/module.rs +description: "Code:\n\nimport Cardano.Tx as tx" +--- +ImportAlias( + 1, + 22, +) diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_pattern_error.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_pattern_error.snap new file mode 100644 index 00000000..a5c1598a --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_pattern_error.snap @@ -0,0 +1,29 @@ +--- +source: crates/nash-parse/src/module.rs +description: "Code:\n\nf (x as) = x" +--- +Declarations( + Def( + "f", + Arg( + Tuple( + Expr( + Alias( + 1, + 8, + ), + 1, + 4, + ), + 1, + 3, + ), + 1, + 3, + ), + 1, + 2, + ), + 1, + 1, +) diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_type_alias_error.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_type_alias_error.snap new file mode 100644 index 00000000..882e6362 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_preserves_type_alias_error.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-parse/src/module.rs +description: "Code:\n\ntype alias account" +--- +Declarations( + Type( + Alias( + Equals( + 1, + 19, + ), + 1, + 6, + ), + 1, + 1, + ), + 1, + 1, +) From faaeb8b62c00625737bef03526bd1be30dba8ea5 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 02/12] fix(types): retain diagnostic context Signed-off-by: microproofs --- crates/nash-can/src/lib.rs | 2 +- crates/nash-constrain/src/expression.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/nash-can/src/lib.rs b/crates/nash-can/src/lib.rs index fe37bcea..26187bbe 100644 --- a/crates/nash-can/src/lib.rs +++ b/crates/nash-can/src/lib.rs @@ -15,7 +15,7 @@ pub mod warning; pub use crate::entailment::Failure as EntailmentFailure; pub use crate::error::{ - BadArityContext, DuplicatePatternContext, Error, KindContext, PossibleNames, VarKind, + BadArityContext, BadHead, DuplicatePatternContext, Error, KindContext, PossibleNames, VarKind, }; pub use crate::interface::{ AliasVisibility, Annotations, Interface, InterfaceAlias, InterfaceBinop, InterfaceMethod, diff --git a/crates/nash-constrain/src/expression.rs b/crates/nash-constrain/src/expression.rs index f1124d7a..280762dc 100644 --- a/crates/nash-constrain/src/expression.rs +++ b/crates/nash-constrain/src/expression.rs @@ -365,6 +365,7 @@ fn constrain_call<'a>( 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), From f363066be67a532941c3f41aa24e026a8f0249da Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 03/12] fix(solve): recover independent errors Signed-off-by: microproofs --- crates/nash-solve/src/kind_check.rs | 119 +++- crates/nash-solve/src/lib.rs | 1 + crates/nash-solve/src/preds.rs | 7 + crates/nash-solve/src/recovery.rs | 181 ++++++ crates/nash-solve/src/solve.rs | 531 +++++++++++++++--- crates/nash-solve/src/unify.rs | 148 +++++ crates/nash-solve/tests/inference.rs | 430 ++++++++++++++ .../tests/representation_predicates.rs | 125 +++++ ...erence__recovery_mixed_errors_forward.snap | 175 ++++++ ...rence__recovery_mixed_errors_reversed.snap | 175 ++++++ 10 files changed, 1777 insertions(+), 115 deletions(-) create mode 100644 crates/nash-solve/src/recovery.rs create mode 100644 crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_forward.snap create mode 100644 crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_reversed.snap diff --git a/crates/nash-solve/src/kind_check.rs b/crates/nash-solve/src/kind_check.rs index 693a1f68..13cdf799 100644 --- a/crates/nash-solve/src/kind_check.rs +++ b/crates/nash-solve/src/kind_check.rs @@ -15,15 +15,23 @@ use crate::preds::{Body, Store}; #[derive(Clone, Copy)] pub(crate) struct Contract<'a> { pub variable: Variable, + /// Scheme or instantiated use that supplied this requirement. + pub owner: Variable, pub kind: &'a nash_ast::Kind<'a>, pub region: Region, } +pub(crate) struct Failure<'a> { + pub error: Error<'a>, + pub variable: Variable, +} + struct Check<'a, 'env> { infer: Infer<'a>, env: &'env KindEnv<'a>, variables: BTreeMap>, contracts: BTreeMap>>, + failed: Option, } impl<'a, 'env> Check<'a, 'env> { @@ -35,6 +43,9 @@ impl<'a, 'env> Check<'a, 'env> { ) -> Self { let mut requirements = BTreeMap::<_, Vec<_>>::new(); for contract in contracts { + if crate::recovery::is_poisoned(uf, [contract.owner]) { + continue; + } requirements .entry(uf.find(contract.variable)) .or_default() @@ -45,6 +56,7 @@ impl<'a, 'env> Check<'a, 'env> { env, variables: BTreeMap::new(), contracts: requirements, + failed: None, } } @@ -52,6 +64,18 @@ impl<'a, 'env> Check<'a, 'env> { &mut self, uf: &mut UnionFind<'a>, variable: Variable, + ) -> Result<&'a K<'a>, Mismatch<'a>> { + let result = self.check_variable(uf, variable); + if result.is_err() { + self.failed.get_or_insert(variable); + } + result + } + + fn check_variable( + &mut self, + uf: &mut UnionFind<'a>, + variable: Variable, ) -> Result<&'a K<'a>, Mismatch<'a>> { let variable = uf.find(variable); if let Some(kind) = self.variables.get(&variable) { @@ -139,7 +163,9 @@ impl<'a, 'env> Check<'a, 'env> { ) -> Result<(), Mismatch<'a>> { for variable in variables { let kind = self.variable(uf, variable)?; - self.infer.unify(&K::Type, kind)?; + self.infer.unify(&K::Type, kind).inspect_err(|_| { + self.failed.get_or_insert(variable); + })?; } Ok(()) } @@ -155,17 +181,24 @@ impl<'a, 'env> Check<'a, 'env> { assert_eq!(kinds.len(), args.len()); for (kind, variable) in kinds.iter().zip(args) { let actual = self.variable(uf, *variable)?; - self.infer.unify(self.infer.from_kind(kind), actual)?; + self.infer + .unify(self.infer.from_kind(kind), actual) + .inspect_err(|_| { + self.failed.get_or_insert(*variable); + })?; } } Body::Apply { head, args } => { + let head_variable = *head; let head = self.variable(uf, *head)?; let args = args .iter() .map(|v| self.variable(uf, *v)) .collect::, _>>()?; // Partial applications may still have an arrow result kind. - self.infer.apply(head, &args)?; + self.infer.apply(head, &args).inspect_err(|_| { + self.failed.get_or_insert(head_variable); + })?; } } Ok(()) @@ -195,27 +228,55 @@ pub(crate) fn check<'a>( roots: &[(Variable, Region)], predicates: &Store<'a>, contracts: &[Contract<'a>], -) -> Option> { - let mut check = Check::new(bump, uf, env, contracts); - for contract in contracts { - if let Err(mismatch) = check.variable(uf, contract.variable) { - return Some(check.error(contract.region, mismatch)); - } - } - for &(variable, region) in roots { - if let Err(mismatch) = check.values(uf, [variable]) { - return Some(check.error(region, mismatch)); - } - } - for (index, predicate) in predicates.iter().enumerate() { - if let Err(mismatch) = check.predicate(uf, &predicate.body) { - let region = predicates - .use_site(nash_constrain::type_::PredId(index as u32)) - .map_or(Region::zero(), |site| site.region); - return Some(check.error(region, mismatch)); - } + dependencies: &crate::recovery::Dependencies, +) -> Vec> { + // Restart after each failure: kind assignments made by a failed check must + // not affect another diagnostic. Poison the failing nested type, then let + // directed containment skip its parents while retaining sibling roots. + let mut errors = Vec::new(); + loop { + let mut check = Check::new(bump, uf, env, contracts); + let result = (|| { + for contract in contracts { + if !crate::recovery::is_poisoned(uf, [contract.owner, contract.variable]) { + check + .variable(uf, contract.variable) + .map_err(|mismatch| (contract.region, mismatch))?; + } + } + for &(variable, region) in roots { + if !crate::recovery::is_poisoned(uf, [variable]) { + check + .values(uf, [variable]) + .map_err(|mismatch| (region, mismatch))?; + } + } + for (index, predicate) in predicates.iter().enumerate() { + if !crate::recovery::is_poisoned(uf, predicate.body.roots()) { + let id = nash_constrain::type_::PredId(index as u32); + let region = predicates + .use_site(id) + .map_or(Region::zero(), |site| site.region); + check + .predicate(uf, &predicate.body) + .map_err(|mismatch| (region, mismatch))?; + } + } + Ok(()) + })(); + let Err((region, mismatch)) = result else { + break; + }; + errors.push(check.error(region, mismatch)); + dependencies.invalidate( + uf, + [check + .failed + .expect("kind failure identifies a type variable")], + ); + while dependencies.propagate(uf) {} } - None + errors } /// Freeze only quantified variables. Captured type variables remain shared. @@ -227,7 +288,7 @@ pub(crate) fn freeze<'a>( predicates: &[Body<'a>], quantified: &[Variable], contracts: &[Contract<'a>], -) -> Result>, Box>> { +) -> Result>, Box>> { let region = root.region; let mut check = Check::new(bump, uf, env, contracts); let result = (|| { @@ -240,11 +301,19 @@ pub(crate) fn freeze<'a>( let kind = check.variable(uf, variable)?; result.push(Contract { variable, + owner: root.value, kind: check.infer.default_and_zonk(kind), region, }); } Ok(result) })(); - result.map_err(|mismatch| Box::new(check.error(region, mismatch))) + result.map_err(|mismatch| { + Box::new(Failure { + variable: check + .failed + .expect("kind failure identifies a type variable"), + error: check.error(region, mismatch), + }) + }) } diff --git a/crates/nash-solve/src/lib.rs b/crates/nash-solve/src/lib.rs index 2bb7800d..0b69cde8 100644 --- a/crates/nash-solve/src/lib.rs +++ b/crates/nash-solve/src/lib.rs @@ -12,6 +12,7 @@ pub mod evidence; mod kind_check; mod occurs; pub mod preds; +mod recovery; mod representation; mod resolve; mod solve; diff --git a/crates/nash-solve/src/preds.rs b/crates/nash-solve/src/preds.rs index 96b2bec8..bdc70380 100644 --- a/crates/nash-solve/src/preds.rs +++ b/crates/nash-solve/src/preds.rs @@ -253,6 +253,13 @@ impl<'a> Store<'a> { depth } + pub(crate) fn root(&self, mut id: PredId) -> PredId { + while let Origin::Sub { parent, .. } = self.get(id).origin { + id = parent; + } + id + } + pub fn push(&mut self, uf: &mut UnionFind<'a>, predicate: Predicate<'a>) -> PredId { let id = PredId(u32::try_from(self.predicates.len()).expect("predicate store exhausted")); for arg in predicate.body.roots() { diff --git a/crates/nash-solve/src/recovery.rs b/crates/nash-solve/src/recovery.rs new file mode 100644 index 00000000..3d88da3d --- /dev/null +++ b/crates/nash-solve/src/recovery.rs @@ -0,0 +1,181 @@ +//! Dependency closure for failed inference computations. +//! +//! Failed comparisons invalidate changed shared variables. Containment carries +//! that failure to parents without invalidating independent siblings. + +use std::collections::BTreeSet; + +use nash_constrain::{Content, FlatType, UnionFind, Variable}; + +#[derive(Default)] +pub(crate) struct Dependencies { + edges: BTreeSet<(Variable, Variable)>, + computations: BTreeSet<(Variable, Variable)>, + field_receivers: BTreeSet<(Variable, Variable)>, +} + +impl Dependencies { + /// Remember containment before normalization or union can remove an edge. + /// Original variable handles remain valid after their representatives move. + pub(crate) fn remember( + &mut self, + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, + ) { + let mut pending: Vec<_> = roots.into_iter().collect(); + let mut seen = BTreeSet::new(); + while let Some(variable) = pending.pop() { + let variable = uf.find(variable); + if !seen.insert(variable) { + continue; + } + for child in children(&uf.get(variable).content) { + let child = uf.find(child); + self.edges.insert((variable, child)); + pending.push(child); + } + } + } + + pub(crate) fn propagate(&self, uf: &mut UnionFind<'_>) -> bool { + let mut changed = false; + for &(parent, child) in self.edges.iter().chain(&self.computations) { + // Retain an aggregate whose current shape already contains the + // error: unification can still check its unaffected children. + // A lost historical edge or a separate computation result needs + // an explicit error marker because its visible tree has none. + if is_poisoned(uf, [child]) && !is_poisoned(uf, [parent]) { + changed |= poison_roots(uf, [parent]); + } + } + for &(field, receiver) in &self.field_receivers { + // A selected field does not depend on other fields in the record. + // Its own type is linked when field resolution unifies the result. + if matches!(uf.get(receiver).content, Content::Error) && !is_poisoned(uf, [field]) { + changed |= poison_roots(uf, [field]); + } + } + changed + } + + pub(crate) fn computation( + &mut self, + output: Variable, + inputs: impl IntoIterator, + ) { + self.computations + .extend(inputs.into_iter().map(|input| (output, input))); + } + + pub(crate) fn field(&mut self, output: Variable, receiver: Variable) { + self.field_receivers.insert((output, receiver)); + } + + pub(crate) fn invalidate( + &self, + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, + ) { + let variables = self.historical(uf, roots); + poison_roots(uf, variables); + } + + /// Nodes removed by normalization still belong to the original computation. + pub(crate) fn detached( + &self, + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, + ) -> BTreeSet { + let roots: Vec<_> = roots.into_iter().collect(); + let current = reachable(uf, roots.iter().copied()); + self.historical(uf, roots) + .difference(¤t) + .copied() + .collect() + } + + fn historical( + &self, + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, + ) -> BTreeSet { + let mut variables = reachable(uf, roots); + loop { + let before = variables.len(); + for &(parent, child) in &self.edges { + if variables.contains(&uf.find(parent)) { + variables.extend(reachable(uf, [child])); + } + } + if variables.len() == before { + break; + } + } + variables + } +} + +fn children(content: &Content<'_>) -> Vec { + match content { + Content::Alias { args, real, .. } => { + args.iter().map(|(_, var)| *var).chain([*real]).collect() + } + Content::PartialAlias { args, .. } => args.iter().map(|(_, var)| *var).collect(), + Content::Structure(FlatType::App1(_, _, args)) => args.clone(), + Content::Structure(FlatType::AppV1(head, args)) => { + [*head].into_iter().chain(args.iter().copied()).collect() + } + Content::Structure(FlatType::Fun1(from, to)) => vec![*from, *to], + Content::Structure(FlatType::Tuple1(first, second, rest)) => [*first, *second] + .into_iter() + .chain(rest.iter().copied()) + .collect(), + Content::Structure(FlatType::Record1(fields)) => fields.values().copied().collect(), + Content::FlexVar(_) | Content::RigidVar(_) | Content::Error => Vec::new(), + } +} + +pub(crate) fn reachable( + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, +) -> BTreeSet { + let mut pending: Vec<_> = roots.into_iter().collect(); + let mut seen = BTreeSet::new(); + while let Some(variable) = pending.pop() { + let variable = uf.find(variable); + if !seen.insert(variable) { + continue; + } + pending.extend(children(&uf.get(variable).content)); + } + seen +} + +pub(crate) fn is_poisoned( + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, +) -> bool { + reachable(uf, roots) + .into_iter() + .any(|variable| matches!(uf.get(variable).content, Content::Error)) +} + +pub(crate) fn poison(uf: &mut UnionFind<'_>, roots: impl IntoIterator) -> bool { + let variables = reachable(uf, roots); + poison_roots(uf, variables) +} + +/// A dependent expression is unavailable, but its siblings remain trustworthy. +pub(crate) fn poison_roots( + uf: &mut UnionFind<'_>, + roots: impl IntoIterator, +) -> bool { + let mut changed = false; + for variable in roots { + if !matches!(uf.get(variable).content, Content::Error) { + uf.modify(variable, |desc| desc.content = Content::Error); + changed = true; + } + } + changed +} diff --git a/crates/nash-solve/src/solve.rs b/crates/nash-solve/src/solve.rs index 0b1ce274..0e98a1e5 100644 --- a/crates/nash-solve/src/solve.rs +++ b/crates/nash-solve/src/solve.rs @@ -39,6 +39,11 @@ pub fn run<'a>( uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -65,6 +70,7 @@ pub fn run<'a>( } else { // Elm accumulates errors by prepending; match its final order. let mut errors = state.errors; + errors.extend(solver.final_errors(uf)); errors.reverse(); Err(errors) } @@ -105,6 +111,7 @@ enum UseSource { struct UseRecord<'a> { site: UseSite<'a>, + variable: Variable, owner: Option, source: UseSource, predicates: Vec, @@ -143,7 +150,12 @@ struct Solver<'a, 'tables> { recursive_uses: Vec, uses: Vec>, owners: Vec, - resolution_work: std::collections::HashMap, + resolution_work: std::collections::HashMap, + has_poison: bool, + dependencies: crate::recovery::Dependencies, + failed_lineages: BTreeSet, + failed_predicates: BTreeSet, + failed_definitions: std::collections::HashSet, value_roots: Vec<(Variable, nash_region::Region)>, kind_contracts: Vec>, kind_errors: Vec>, @@ -163,6 +175,97 @@ struct Given<'a> { } impl<'a> Solver<'a, '_> { + fn unify( + &mut self, + uf: &mut UnionFind<'a>, + actual: Variable, + expected: Variable, + ) -> unify::Answer<'a> { + self.dependencies.remember(uf, [actual, expected]); + self.propagate_poison(uf); + let detached = self.dependencies.detached(uf, [actual, expected]); + let answer = unify::unify(self.bump, uf, actual, expected); + self.dependencies.remember(uf, [actual, expected]); + if matches!(answer, unify::Answer::Err(..)) { + self.has_poison = true; + crate::recovery::poison_roots(uf, detached); + self.propagate_poison(uf); + } + answer + } + + /// Normalization can remove application heads from the visible type tree. + /// A use still owns those copied variables and their kind contracts. Carry + /// poison through its original roots before checking later obligations. + fn propagate_poison(&self, uf: &mut UnionFind<'a>) { + if !self.has_poison { + return; + } + loop { + let mut changed = self.dependencies.propagate(uf); + for use_ in &self.uses { + let mut roots = vec![use_.variable]; + for id in &use_.predicates { + roots.extend(self.predicates.get(*id).body.roots()); + } + match &use_.source { + UseSource::Local { copies, .. } => { + roots.extend(copies.iter().map(|(_, copy)| copy)) + } + UseSource::Foreign { variables } => roots.extend(variables), + } + if crate::recovery::is_poisoned(uf, roots.iter().copied()) + && !crate::recovery::is_poisoned(uf, [use_.variable]) + { + changed |= crate::recovery::poison_roots(uf, [use_.variable]); + } + } + if !changed { + break; + } + } + } + + fn fail_definition(&mut self, uf: &mut UnionFind<'a>, definition: nash_ast::NodeId) { + self.has_poison = true; + self.failed_definitions.insert(definition); + loop { + let before = self.failed_definitions.len(); + for scheme in &self.schemes { + if self.failed_definitions.contains(&scheme.site.node()) + || self.failed_definitions.contains(&scheme.binder) + { + self.failed_definitions.insert(scheme.site.node()); + crate::recovery::poison_roots(uf, [scheme.binding.variable]); + } + } + for use_ in &self.uses { + if let UseSource::Local { definition, copies } = &use_.source + && self.failed_definitions.contains(definition) + { + crate::recovery::poison( + uf, + [use_.variable] + .into_iter() + .chain(copies.iter().map(|(_, copy)| *copy)), + ); + if let Some(owner) = use_.owner { + self.failed_definitions.insert(owner); + } + } + } + if before == self.failed_definitions.len() { + break; + } + } + } + + fn predicate_blocked(&self, uf: &mut UnionFind<'a>, id: type_::PredId) -> bool { + self.failed_predicates.contains(&id) + || self.failed_lineages.contains(&self.predicates.root(id)) + || crate::recovery::is_poisoned(uf, self.predicates.get(id).body.roots()) + } + /// Field resolution may expose another receiver, so retry to a fixed point. fn retry_fields(&mut self, uf: &mut UnionFind<'a>, rank: usize, errors: &mut Vec>) { loop { @@ -179,6 +282,16 @@ impl<'a> Solver<'a, '_> { } } + fn poison_field(&mut self, uf: &mut UnionFind<'a>, field: DeferredField<'a>) { + self.has_poison = true; + let result = match (field.context, field.field) { + (type_::FieldContext::Update { .. }, _) | (_, None) => field.record, + (_, Some((_, variable))) => variable, + }; + crate::recovery::poison_roots(uf, [result]); + self.propagate_poison(uf); + } + fn try_field( &mut self, uf: &mut UnionFind<'a>, @@ -186,6 +299,35 @@ impl<'a> Solver<'a, '_> { field: DeferredField<'a>, errors: &mut Vec>, ) -> bool { + let before = errors.len(); + let resolved = self.resolve_field(uf, rank, field, errors); + if errors.len() > before { + self.poison_field(uf, field); + } + resolved + } + + fn resolve_field( + &mut self, + uf: &mut UnionFind<'a>, + rank: usize, + field: DeferredField<'a>, + errors: &mut Vec>, + ) -> bool { + if [field.record] + .into_iter() + .chain(field.field.map(|(_, var)| var)) + .any(|variable| matches!(uf.get(variable).content, Content::Error)) + { + self.poison_field(uf, field); + return true; + } + self.dependencies.remember( + uf, + [field.record] + .into_iter() + .chain(field.field.map(|(_, var)| var)), + ); let mut receiver = field.record; let mut seen = BTreeSet::new(); while seen.insert(uf.find(receiver)) { @@ -194,7 +336,10 @@ impl<'a> Solver<'a, '_> { self.introduce(uf, rank, &allocated); match uf.get(receiver).content.clone() { Content::FlexVar(_) => return false, - Content::Error => return true, + Content::Error => { + self.poison_field(uf, field); + return true; + } Content::Alias { body, real, .. } => { if !matches!(body.value, CanType::Record { .. }) { receiver = real; @@ -218,7 +363,7 @@ impl<'a> Solver<'a, '_> { }); return true; }; - return match unify::unify(self.bump, uf, actual, field_type) { + return match self.unify(uf, actual, field_type) { unify::Answer::Ok(vars) => { self.introduce(uf, rank, &vars); true @@ -270,7 +415,7 @@ impl<'a> Solver<'a, '_> { }; let substitution = union.parameters.iter().copied().zip(args).collect(); let actual = self.src_type_to_var(uf, rank, &substitution, actual.typ); - match unify::unify(self.bump, uf, actual, field_type) { + match self.unify(uf, actual, field_type) { unify::Answer::Ok(vars) => self.introduce(uf, rank, &vars), unify::Answer::Err(vars, actual, expected) => { self.introduce(uf, rank, &vars); @@ -333,6 +478,7 @@ impl<'a> Solver<'a, '_> { field: field.field.map(|(name, _)| name), record: to_error_type(self.bump, uf, field.record), }); + self.poison_field(uf, field); } else { self.fields.push(field); } @@ -459,28 +605,28 @@ impl<'a> Solver<'a, '_> { growing } - fn finish( - &self, - uf: &mut UnionFind<'a>, - env: &Env<'a>, - ) -> Result<(Annotations<'a>, crate::SolvedTypes<'a>), Vec>> { - use crate::solved::{Instance, Scheme, SolvedTypes}; - use std::collections::HashMap; - if let Some(error) = crate::kind_check::check( + fn final_errors(&mut self, uf: &mut UnionFind<'a>) -> Vec> { + self.propagate_poison(uf); + let mut errors = crate::kind_check::check( self.bump, uf, &self.tables.kinds, &self.value_roots, &self.predicates, &self.kind_contracts, - ) { - return Err(vec![error]); + &self.dependencies, + ); + if !errors.is_empty() { + self.has_poison = true; + self.propagate_poison(uf); } let growing = self.growing_evidence(); - let mut errors = Vec::new(); for use_ in &self.uses { + if crate::recovery::is_poisoned(uf, [use_.variable]) { + continue; + } for root in &use_.predicates { - if growing.contains(root) { + if growing.contains(root) && !self.predicate_blocked(uf, *root) { let pred = self.predicates.get(*root); let Body::Trait { trait_, args, .. } = &pred.body else { continue; @@ -500,7 +646,7 @@ impl<'a> Solver<'a, '_> { let mut pending = use_.predicates.clone(); let mut seen = BTreeSet::new(); while let Some(id) = pending.pop() { - if !seen.insert(id) { + if !seen.insert(id) || self.predicate_blocked(uf, id) { continue; } let pred = self.predicates.get(id); @@ -536,6 +682,17 @@ impl<'a> Solver<'a, '_> { } } } + errors + } + + fn finish( + &mut self, + uf: &mut UnionFind<'a>, + env: &Env<'a>, + ) -> Result<(Annotations<'a>, crate::SolvedTypes<'a>), Vec>> { + use crate::solved::{Instance, Scheme, SolvedTypes}; + use std::collections::HashMap; + let errors = self.final_errors(uf); if !errors.is_empty() { return Err(errors); } @@ -890,6 +1047,9 @@ impl<'a> Solver<'a, '_> { binder: Option>, annotated: bool, ) -> State<'a> { + // Attribute new failures to their owning binding. This count does not + // gate any independent constraint or reset the collected diagnostics. + let errors_before = state.errors.len(); let depth = self.enter_givens(uf, rank, given, binder); let start = self.wanted.len(); let owner_depth = self.owners.len(); @@ -899,6 +1059,11 @@ impl<'a> Solver<'a, '_> { let mut state = self.solve(uf, env, rank, state, constraint); self.retry_fields(uf, rank, &mut state.errors); state = self.resolve_wanted(uf, rank, state, start, binder, annotated); + if state.errors.len() > errors_before + && let Some(binder) = binder + { + self.fail_definition(uf, binder.node()); + } self.givens.truncate(depth); self.owners.truncate(owner_depth); state @@ -982,6 +1147,8 @@ impl<'a> Solver<'a, '_> { head: Variable, args: &[Variable], ) -> Option>> { + self.dependencies + .remember(uf, [head].into_iter().chain(args.iter().copied())); let (name, supplied) = crate::representation::application(uf, head, args)?; let info = self.tables.kinds.constructor(name); let variables: BTreeMap<_, _> = info.parameters().iter().copied().zip(supplied).collect(); @@ -1005,6 +1172,7 @@ impl<'a> Solver<'a, '_> { variable: Variable, ) -> Option { let mut allocated = Vec::new(); + self.dependencies.remember(uf, [variable]); let repr = crate::representation::known(uf, &self.tables.kinds, variable, &mut allocated); self.introduce(uf, rank, &allocated); repr @@ -1043,8 +1211,8 @@ impl<'a> Solver<'a, '_> { ) -> State<'a> { use crate::preds::Solution; use nash_ast::primitives::{Repr, ReprTrait}; - let report_errors = state.errors.is_empty(); - let report_missing = annotated && report_errors; + self.propagate_poison(uf); + let report_missing = annotated; let mut queue: VecDeque<_> = self .wanted .split_off(start) @@ -1052,6 +1220,10 @@ impl<'a> Solver<'a, '_> { .map(|(rank, id)| (rank, id, self.predicates.depth(id))) .collect(); while let Some((wanted_rank, id, resolution_depth)) = queue.pop_front() { + if self.predicate_blocked(uf, id) { + self.predicates.detach(uf, id); + continue; + } if self.predicates.get(id).solution.is_some() { continue; } @@ -1122,7 +1294,8 @@ impl<'a> Solver<'a, '_> { typ: args[0], }, ); - } else if report_errors && let Some(site) = site { + } else if let Some(site) = site { + self.failed_predicates.insert(id); state.errors.push(Error::MissingImpl { region: site.region, name: site.name, @@ -1141,6 +1314,7 @@ impl<'a> Solver<'a, '_> { && let Some(site) = site && matches!(uf.get(args[0]).content, Content::RigidVar(_)) { + self.failed_predicates.insert(id); state.errors.push(Error::MissingConstraint { region: site.region, name: site.name, @@ -1185,6 +1359,7 @@ impl<'a> Solver<'a, '_> { .iter() .map(|arg| to_error_type(self.bump, uf, *arg)) .collect(); + self.failed_predicates.insert(id); state.errors.push(Error::MissingConstraint { region: site.region, name: site.name, @@ -1194,9 +1369,12 @@ impl<'a> Solver<'a, '_> { }); continue; } - if report_errors && let Some(binder) = binder { + if binder.is_some() { let site = site.expect("wanteds originate at uses"); - let work = self.resolution_work.entry(binder.node()).or_default(); + let work = self + .resolution_work + .entry(self.predicates.root(id)) + .or_default(); *work += 1; if *work > 16_384 || resolution_depth >= 128 { state.errors.push(Error::ImplResolutionLimit { @@ -1204,7 +1382,8 @@ impl<'a> Solver<'a, '_> { name: site.name, trait_, }); - break; + self.failed_lineages.insert(self.predicates.root(id)); + continue; } match crate::resolve::select(self.tables, uf, trait_, &args) { crate::resolve::Selection::Deferred => self.wanted.push((wanted_rank, id)), @@ -1213,7 +1392,8 @@ impl<'a> Solver<'a, '_> { region: site.region, name: site.name, trait_, - }) + }); + self.failed_lineages.insert(self.predicates.root(id)); } crate::resolve::Selection::Missing => { let args: Vec<_> = args @@ -1227,6 +1407,7 @@ impl<'a> Solver<'a, '_> { .filter(|key| key.trait_ == trait_) .map(|key| key.heads) .collect(); + self.failed_predicates.insert(id); state.errors.push(Error::MissingImpl { region: site.region, name: site.name, @@ -1285,6 +1466,7 @@ impl<'a> Solver<'a, '_> { roots: &[Variable], region: nash_region::Region, ) { + self.dependencies.remember(uf, roots.iter().copied()); self.value_roots .extend(roots.iter().map(|variable| (*variable, region))); let Some(owner) = self.owners.last().copied() else { @@ -1404,6 +1586,7 @@ impl<'a> Solver<'a, '_> { } => { let record = self.type_to_variable(uf, rank, record); let field_type = self.type_to_variable(uf, rank, field_type); + self.dependencies.field(field_type, record); self.fields.push(DeferredField { region: *region, context: *context, @@ -1424,8 +1607,27 @@ impl<'a> Solver<'a, '_> { Constraint::Equal(region, category, tipe, expectation) => { let actual = self.type_to_variable(uf, rank, tipe); let expected = self.expected_to_variable(uf, rank, expectation); + if let Expected::FromContext( + _, + nash_constrain::error::Context::CallArity(_, arity), + _, + ) = expectation + { + let mut result = expected; + let mut inputs = vec![actual]; + for _ in 0..*arity { + let Content::Structure(FlatType::Fun1(argument, output)) = + uf.get(result).content + else { + break; + }; + inputs.push(argument); + result = output; + } + self.dependencies.computation(result, inputs); + } self.formed_at(uf, rank, &[actual, expected], *region); - match unify::unify(self.bump, uf, actual, expected) { + match self.unify(uf, actual, expected) { unify::Answer::Ok(vars) => { self.introduce(uf, rank, &vars); state @@ -1461,7 +1663,7 @@ impl<'a> Solver<'a, '_> { ); let expected = self.expected_to_variable(uf, rank, expectation); self.formed_at(uf, rank, &[actual, expected], *region); - match unify::unify(self.bump, uf, actual, expected) { + match self.unify(uf, actual, expected) { unify::Answer::Ok(vars) => { self.introduce(uf, rank, &vars); state @@ -1494,7 +1696,7 @@ impl<'a> Solver<'a, '_> { ); let expected = self.expected_to_variable(uf, rank, expectation); self.formed_at(uf, rank, &[actual, expected], *region); - match unify::unify(self.bump, uf, actual, expected) { + match self.unify(uf, actual, expected) { unify::Answer::Ok(vars) => { self.introduce(uf, rank, &vars); state @@ -1518,7 +1720,7 @@ impl<'a> Solver<'a, '_> { let actual = self.type_to_variable(uf, rank, tipe); let expected = self.pattern_expectation_to_variable(uf, rank, expectation); self.formed_at(uf, rank, &[actual, expected], *region); - match unify::unify(self.bump, uf, actual, expected) { + match self.unify(uf, actual, expected) { unify::Answer::Ok(vars) => { self.introduce(uf, rank, &vars); state @@ -1553,6 +1755,7 @@ impl<'a> Solver<'a, '_> { header_con, body_con, } => { + let errors_before = state.errors.len(); let wanted_start = self.wanted.len(); let annotated = definitions.iter().any(|def| def.context.is_some()); if definitions.is_empty() @@ -1563,17 +1766,13 @@ impl<'a> Solver<'a, '_> { let declared = self.declared_contexts(uf, rank, definitions, declarations); let state1 = self .solve_header(uf, env, rank, state, header_con, given, *binder, annotated); - if state1.errors.is_empty() { - self.record_definitions(uf, rank, definitions, &declared, &[], *binder); - } + self.record_definitions(uf, rank, definitions, &declared, &[], *binder); state1 } else if definitions.is_empty() && rigid_vars.is_empty() && flex_vars.is_empty() { let declared = self.declared_contexts(uf, rank, definitions, declarations); let state1 = self .solve_header(uf, env, rank, state, header_con, given, *binder, annotated); - if state1.errors.is_empty() { - self.record_definitions(uf, rank, definitions, &declared, &[], *binder); - } + self.record_definitions(uf, rank, definitions, &declared, &[], *binder); let locals: Vec<(&'a str, Located)> = header .iter() .map(|(name, loc_type)| { @@ -1646,30 +1845,24 @@ impl<'a> Solver<'a, '_> { self.generalize(uf, young_mark, visit_mark, next_rank); self.pools[next_rank] = Vec::new(); - // check that things went well - if state1.errors.is_empty() { - for rigid in rigid_vars.iter() { - if uf.get(*rigid).rank != NO_RANK { - let owner = binder - .map(|name| (name.name().region, name.name().value)) - .or_else(|| { - header.first().map(|(name, typ)| (typ.region, *name)) - }); - state1.errors.push(Error::AnnotationVariableEscapes { - region: owner - .map_or_else(nash_region::Region::zero, |(region, _)| { - region - }), - name: owner.map(|(_, name)| name), - variable: to_error_type(self.bump, uf, *rigid), - }); - } + // An unrelated error must not suppress an escaping annotation. + for rigid in rigid_vars.iter() { + if uf.get(*rigid).rank != NO_RANK + && !crate::recovery::is_poisoned(uf, [*rigid]) + { + let owner = binder + .map(|name| (name.name().region, name.name().value)) + .or_else(|| header.first().map(|(name, typ)| (typ.region, *name))); + state1.errors.push(Error::AnnotationVariableEscapes { + region: owner + .map_or_else(nash_region::Region::zero, |(region, _)| region), + name: owner.map(|(_, name)| name), + variable: to_error_type(self.bump, uf, *rigid), + }); } } - if state1.errors.is_empty() - && let Some(binder) = *binder - { + if let Some(binder) = *binder { let depth = self.enter_givens(uf, rank, given, Some(binder)); loop { let (errors, defaulted) = self.check_ambiguity( @@ -1680,7 +1873,7 @@ impl<'a> Solver<'a, '_> { binder.name(), ); state1.errors.extend(errors); - if !defaulted || !state1.errors.is_empty() { + if !defaulted { break; } state1 = self.resolve_wanted( @@ -1691,9 +1884,6 @@ impl<'a> Solver<'a, '_> { Some(binder), annotated, ); - if !state1.errors.is_empty() { - break; - } } self.givens.truncate(depth); } @@ -1711,9 +1901,12 @@ impl<'a> Solver<'a, '_> { }; let mut new_env = env.clone(); - if state1.errors.is_empty() { - self.record_definitions(uf, rank, definitions, &declared, context, *binder); + if state1.errors.len() > errors_before + && let Some(binder) = *binder + { + self.fail_definition(uf, binder.node()); } + self.record_definitions(uf, rank, definitions, &declared, context, *binder); for (name, loc) in &locals { new_env.entry(name).or_insert(Binding { declared_quantifiers: if declarations @@ -1795,7 +1988,8 @@ impl<'a> Solver<'a, '_> { let variable = located_variable.value; if occurs::occurs(uf, variable) { let error_type = to_error_type(self.bump, uf, variable); - uf.modify(variable, |desc| desc.content = Content::Error); + self.has_poison = true; + crate::recovery::poison(uf, [variable]); add_error( state, Error::InfiniteType { @@ -2064,6 +2258,7 @@ impl<'a> Solver<'a, '_> { ); self.uses.push(UseRecord { site, + variable: typ, owner: self.owners.last().copied(), source: UseSource::Foreign { variables: annotation @@ -2103,6 +2298,10 @@ impl<'a> Solver<'a, '_> { quantified: &[Variable], region: nash_region::Region, ) { + self.dependencies.remember(uf, [root]); + if crate::recovery::is_poisoned(uf, [root]) { + return; + } let bodies = ids .iter() .map(|id| self.predicates.get(*id).body.clone()) @@ -2117,7 +2316,12 @@ impl<'a> Solver<'a, '_> { &self.kind_contracts, ) { Ok(contracts) => self.kind_contracts.extend(contracts), - Err(error) => self.kind_errors.push(*error), + Err(failure) => { + self.kind_errors.push(failure.error); + self.has_poison = true; + self.dependencies.invalidate(uf, [failure.variable]); + self.propagate_poison(uf); + } } } @@ -2149,6 +2353,11 @@ impl<'a> Solver<'a, '_> { .into_iter() .filter(|var| uf.get(*var).rank == NO_RANK) .collect(); + if self.failed_definitions.contains(&definition.site.node()) + || binder.is_some_and(|binder| self.failed_definitions.contains(&binder.node())) + { + crate::recovery::poison_roots(uf, [binding.variable]); + } self.freeze_kind_contracts( uf, binding.variable, @@ -2164,6 +2373,10 @@ impl<'a> Solver<'a, '_> { parent: self.owners.last().copied(), }); } + let failed: Vec<_> = self.failed_definitions.iter().copied().collect(); + for definition in failed { + self.fail_definition(uf, definition); + } let pending = std::mem::take(&mut self.recursive_uses); for use_index in pending { let use_ = &self.uses[use_index]; @@ -2262,6 +2475,12 @@ impl<'a> Solver<'a, '_> { binding: Binding<'a>, site: UseSite<'a>, ) -> Variable { + if binding + .definition + .is_some_and(|definition| self.failed_definitions.contains(&definition)) + { + return self.register(uf, rank, Content::Error); + } let mut roots = vec![binding.variable]; if let Some(scheme) = self .schemes @@ -2303,6 +2522,7 @@ impl<'a> Solver<'a, '_> { } self.uses.push(UseRecord { site, + variable: copies[0], owner: self.owners.last().copied(), source: UseSource::Local { definition, @@ -2329,6 +2549,9 @@ impl<'a> Solver<'a, '_> { let mut representations: Vec<(Variable, Vec)> = Vec::new(); let mut allocated = Vec::new(); for (_, id) in &self.wanted[start..] { + if self.predicate_blocked(uf, *id) { + continue; + } let Body::Trait { trait_, args, .. } = &self.predicates.get(*id).body else { continue; }; @@ -2349,27 +2572,25 @@ impl<'a> Solver<'a, '_> { } } self.introduce(uf, rank, &allocated); - let conflicts: Vec<_> = representations - .into_iter() - .filter_map(|(subject, requirements)| { - let admitted = requirements - .iter() - .fold(nash_ast::primitives::ReprSet::ALL, |set, requirement| { - set.intersect(requirement.admits()) - }); - admitted - .is_empty() - .then(|| Error::ContradictoryRepresentation { - region: binder.region, - name: binder.value, - typ: to_error_type(self.bump, uf, subject), - requirements: self.bump.alloc_slice_fill_iter(requirements), - }) - }) - .collect(); - if !conflicts.is_empty() { - return (conflicts, false); + let mut errors = Vec::new(); + for (subject, requirements) in representations { + let admitted = requirements + .iter() + .fold(nash_ast::primitives::ReprSet::ALL, |set, requirement| { + set.intersect(requirement.admits()) + }); + if admitted.is_empty() { + errors.push(Error::ContradictoryRepresentation { + region: binder.region, + name: binder.value, + typ: to_error_type(self.bump, uf, subject), + requirements: self.bump.alloc_slice_fill_iter(requirements), + }); + self.has_poison = true; + crate::recovery::poison(uf, [subject]); + } } + self.propagate_poison(uf); let roots: Vec<_> = definitions .iter() .map(|def| self.type_to_variable(uf, rank, def.typ)) @@ -2377,6 +2598,9 @@ impl<'a> Solver<'a, '_> { let reachable = Self::type_variables(uf, roots); let mut ambiguous: BTreeMap<_, Vec<_>> = BTreeMap::new(); for (_, id) in &self.wanted[start..] { + if self.predicate_blocked(uf, *id) { + continue; + } let variables = Self::type_variables(uf, self.predicates.get(*id).body.roots().collect()); for var in variables { @@ -2425,10 +2649,7 @@ impl<'a> Solver<'a, '_> { if defaults.len() == 1 && matches!(uf.get(var).content, Content::FlexVar(_)) { let typ = self.bump.alloc(defaults.into_values().next().unwrap()); let target = self.type_to_variable(uf, rank, typ); - if matches!( - unify::unify(self.bump, uf, var, target), - unify::Answer::Ok(_) - ) { + if matches!(self.unify(uf, var, target), unify::Answer::Ok(_)) { defaulted = true; continue; } @@ -2438,9 +2659,8 @@ impl<'a> Solver<'a, '_> { // A default can unlock an impl which supplies a literal constraint // for another hidden variable. Retry before declaring ambiguity. if defaulted { - return (Vec::new(), true); + return (errors, true); } - let mut errors = Vec::new(); let mut rejected = BTreeSet::new(); for (var, ids, distinct) in unresolved { let predicates: Vec<_> = distinct @@ -2466,6 +2686,7 @@ impl<'a> Solver<'a, '_> { rejected.extend(ids); } for id in &rejected { + self.failed_predicates.insert(*id); self.predicates.detach(uf, *id); } self.wanted.retain(|(_, id)| !rejected.contains(id)); @@ -2516,6 +2737,10 @@ impl<'a> Solver<'a, '_> { let pending = self.wanted.split_off(start); let mut retained = Vec::new(); for (_, id) in pending { + if self.predicate_blocked(uf, id) { + self.predicates.detach(uf, id); + continue; + } let variables = Self::type_variables(uf, self.predicates.get(id).body.roots().collect()); let generalized = variables.iter().any(|var| uf.get(*var).rank == NO_RANK); @@ -2660,7 +2885,7 @@ impl<'a> Solver<'a, '_> { quantified: &[Variable], ) -> (Vec, Vec<(Variable, Variable)>) { debug_assert!(self.copied.is_empty()); - let roots = roots + let roots: Vec<_> = roots .iter() .map(|root| self.make_copy_help(uf, rank, *root, quantified)) .collect(); @@ -2671,6 +2896,7 @@ impl<'a> Solver<'a, '_> { if uf.equivalent(*original, contract.variable) { self.kind_contracts.push(crate::kind_check::Contract { variable: *copy, + owner: roots[0], ..*contract }); } @@ -3086,6 +3312,96 @@ mod copy_tests { }}; } + #[test] + fn recovery_work_limit_does_not_drop_the_next_independent_predicate() { + let bump = Bump::new(); + let tables = nash_can::environment::Tables::default(); + let mut solver = Solver { + bump: &bump, + tables: &tables, + pools: vec![Vec::new(); 8], + copied: Vec::new(), + predicates: Store::default(), + wanted: Vec::new(), + givens: Vec::new(), + schemes: Vec::new(), + recursive_uses: Vec::new(), + uses: Vec::new(), + owners: Vec::new(), + resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), + value_roots: Vec::new(), + kind_contracts: Vec::new(), + kind_errors: Vec::new(), + fields: Vec::new(), + }; + let mut uf = UnionFind::new(); + let name = bump.alloc(Located::at_zero("value")); + let binder = type_::Binder::Named(name); + let mut ids = Vec::new(); + for trait_name in ["Expanding", "Missing"] { + let variable = solver.type_to_variable( + &mut uf, + OUTERMOST_RANK, + &Type::AppN { + home: nash_ast::primitives::builtin_home(), + name: "unit", + args: &[], + }, + ); + let id = solver.predicates.push( + &mut uf, + Predicate { + body: Body::Trait { + trait_: nash_ast::QualifiedName { + home: nash_ast::ModuleName { + package: None, + name: "Main", + }, + name: trait_name, + }, + args: vec![variable], + hidden: false, + }, + origin: Origin::Use { + site: UseSite { + node: binder.node(), + region: name.region, + name: trait_name, + }, + index: 0, + }, + solution: None, + }, + ); + solver.wanted.push((OUTERMOST_RANK, id)); + ids.push(id); + } + solver.resolution_work.insert(ids[0], 16_384); + let result = solver.resolve_wanted( + &mut uf, + OUTERMOST_RANK, + State { + env: Env::new(), + mark: NO_MARK.next(), + errors: Vec::new(), + }, + 0, + Some(binder), + false, + ); + assert!( + matches!(result.errors.as_slice(), [Error::ImplResolutionLimit { trait_: first, .. }, Error::MissingImpl { trait_: second, .. }] if first.name == "Expanding" && second.name == "Missing"), + "{:?}", + result.errors + ); + assert!(solver.wanted.is_empty()); + } + #[test] fn superclass_givens_record_transitive_paths_and_substitute_arguments() { let bump = Bump::new(); @@ -3110,6 +3426,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -3179,6 +3500,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -3241,6 +3567,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -3325,6 +3656,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -3459,6 +3795,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -3628,6 +3969,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), @@ -3707,6 +4053,11 @@ mod copy_tests { uses: Vec::new(), owners: Vec::new(), resolution_work: std::collections::HashMap::new(), + has_poison: false, + dependencies: crate::recovery::Dependencies::default(), + failed_lineages: BTreeSet::new(), + failed_predicates: BTreeSet::new(), + failed_definitions: std::collections::HashSet::new(), value_roots: Vec::new(), kind_contracts: Vec::new(), kind_errors: Vec::new(), diff --git a/crates/nash-solve/src/unify.rs b/crates/nash-solve/src/unify.rs index aed07456..dc36f0f8 100644 --- a/crates/nash-solve/src/unify.rs +++ b/crates/nash-solve/src/unify.rs @@ -24,10 +24,26 @@ pub enum Answer<'a> { } pub fn unify<'a>(bump: &'a Bump, uf: &mut UnionFind<'a>, v1: Variable, v2: Variable) -> Answer<'a> { + // Capture before unions discard child edges. Only assignments made by the + // failed comparison are untrustworthy; untouched siblings remain useful. + let dependencies = crate::recovery::reachable(uf, [v1, v2]); + let before: Vec<_> = dependencies + .into_iter() + .map(|variable| (variable, uf.get(variable).content.clone())) + .collect(); let mut vars = Vec::new(); match guarded_unify(uf, &mut vars, v1, v2) { Ok(()) => Answer::Ok(vars), Err(()) => { + let changed: Vec<_> = before + .into_iter() + .filter_map(|(variable, content)| { + let current = uf.get(variable).content.clone(); + let linked_flex = + matches!(content, Content::FlexVar(_)) && uf.find(variable) != variable; + (linked_flex || !same_content(uf, &content, ¤t)).then_some(variable) + }) + .collect(); let t1 = annotation::to_error_type(bump, uf, v1); let t2 = annotation::to_error_type(bump, uf, v2); let preds = merged_predicates(uf, v1, v2); @@ -42,11 +58,106 @@ pub fn unify<'a>(bump: &'a Bump, uf: &mut UnionFind<'a>, v1: Variable, v2: Varia copy: None, }, ); + crate::recovery::poison_roots(uf, changed.into_iter().chain(vars.iter().copied())); Answer::Err(vars, t1, t2) } } } +/// Compare type information, ignoring representative changes for equal types. +fn same_content(uf: &mut UnionFind<'_>, first: &Content<'_>, second: &Content<'_>) -> bool { + let same_vars = |uf: &mut UnionFind<'_>, first: &[Variable], second: &[Variable]| { + first.len() == second.len() + && first + .iter() + .zip(second) + .all(|(a, b)| uf.find(*a) == uf.find(*b)) + }; + match (first, second) { + (Content::FlexVar(a), Content::FlexVar(b)) => a == b, + (Content::RigidVar(a), Content::RigidVar(b)) => a == b, + (Content::Error, Content::Error) => true, + (Content::Structure(a), Content::Structure(b)) => match (a, b) { + (FlatType::App1(ah, an, aa), FlatType::App1(bh, bn, ba)) => { + ah == bh && an == bn && same_vars(uf, aa, ba) + } + (FlatType::AppV1(ah, aa), FlatType::AppV1(bh, ba)) => { + uf.find(*ah) == uf.find(*bh) && same_vars(uf, aa, ba) + } + (FlatType::Fun1(af, at), FlatType::Fun1(bf, bt)) => { + same_vars(uf, &[*af, *at], &[*bf, *bt]) + } + (FlatType::Tuple1(af, as_, ar), FlatType::Tuple1(bf, bs, br)) => { + same_vars(uf, &[*af, *as_], &[*bf, *bs]) && same_vars(uf, ar, br) + } + (FlatType::Record1(a), FlatType::Record1(b)) => { + a.len() == b.len() + && a.iter() + .zip(b) + .all(|((an, av), (bn, bv))| an == bn && uf.find(*av) == uf.find(*bv)) + } + _ => false, + }, + ( + Content::Alias { + home: ah, + name: an, + args: aa, + real: ar, + body: ab, + }, + Content::Alias { + home: bh, + name: bn, + args: ba, + real: br, + body: bb, + }, + ) => { + ah == bh + && an == bn + && std::ptr::eq(*ab, *bb) + && uf.find(*ar) == uf.find(*br) + && same_alias_args(uf, aa, ba) + } + ( + Content::PartialAlias { + home: ah, + name: an, + args: aa, + remaining: ar, + body: ab, + }, + Content::PartialAlias { + home: bh, + name: bn, + args: ba, + remaining: br, + body: bb, + }, + ) => { + ah == bh + && an == bn + && ar == br + && std::ptr::eq(*ab, *bb) + && same_alias_args(uf, aa, ba) + } + _ => false, + } +} + +fn same_alias_args( + uf: &mut UnionFind<'_>, + first: &[(&str, Variable)], + second: &[(&str, Variable)], +) -> bool { + first.len() == second.len() + && first + .iter() + .zip(second) + .all(|((an, av), (bn, bv))| an == bn && uf.find(*av) == uf.find(*bv)) +} + type UResult = Result<(), ()>; // UNIFICATION HELPERS @@ -575,6 +686,43 @@ mod predicate_tests { use super::*; use type_::PredId; + #[test] + fn failed_composite_unification_poison_reaches_shared_children() { + let bump = Bump::new(); + let mut uf = UnionFind::new(); + let shared = type_::mk_flex_var(&mut uf); + let unit = |uf: &mut UnionFind<'_>| { + uf.fresh(type_::make_descriptor(Content::Structure(FlatType::App1( + nash_ast::primitives::builtin_home(), + "unit", + vec![], + )))) + }; + let first = unit(&mut uf); + let second = unit(&mut uf); + let record = uf.fresh(type_::make_descriptor(Content::Structure( + FlatType::Record1(BTreeMap::new()), + ))); + let left = uf.fresh(type_::make_descriptor(Content::Structure( + FlatType::Tuple1(shared, first, vec![]), + ))); + let right = uf.fresh(type_::make_descriptor(Content::Structure( + FlatType::Tuple1(second, record, vec![]), + ))); + assert!(matches!( + unify(&bump, &mut uf, left, right), + Answer::Err(..) + )); + let later = uf.fresh(type_::make_descriptor(Content::Structure( + FlatType::Record1(BTreeMap::new()), + ))); + assert!( + matches!(unify(&bump, &mut uf, shared, later), Answer::Ok(_)), + "partial child assignments must not cause a second mismatch" + ); + assert!(matches!(uf.get(shared).content, Content::Error)); + } + #[test] fn application_head_and_argument_cycles_are_detected_and_rendered() { for head_cycle in [true, false] { diff --git a/crates/nash-solve/tests/inference.rs b/crates/nash-solve/tests/inference.rs index 83072af9..3bf5c37a 100644 --- a/crates/nash-solve/tests/inference.rs +++ b/crates/nash-solve/tests/inference.rs @@ -74,6 +74,436 @@ fn infer<'a>(bump: &'a Bump, input: &str) -> Result, Vec x\n", + "missing = discard ()\n", + "constraint : 'a -> ()\nconstraint x = discard x\n", + "ambiguous = discard (create ())\n", + "kind = idfa (Higher [])\n", + ]; + for reverse in [false, true] { + let bump = Bump::new(); + let mut definitions = definitions.to_vec(); + if reverse { + definitions.reverse(); + } + let source = format!("{header}{}", definitions.concat()); + let errors = + infer(&bump, &source).expect_err("failed solve must not publish solved output"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadExpr(..))) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::MissingImpl { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::MissingConstraint { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::AmbiguousType { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadKind { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!(errors.len(), 5, "{errors:#?}"); + let mut settings = insta::Settings::clone_current(); + settings.set_description(source); + let _guard = settings.bind_to_scope(); + insta::assert_debug_snapshot!( + format!( + "recovery_mixed_errors_{}", + if reverse { "reversed" } else { "forward" } + ), + errors + ); + } +} + +#[test] +fn recovery_reports_an_independent_escaping_annotation_and_blocks_its_uses() { + let bump = Bump::new(); + let errors = infer( + &bump, + indoc!( + r#" + module Main exposing (..) + mismatch : () + mismatch = \x -> x + outer x = + let + inner : 'a + inner = x + in + (inner (), x ()) + "# + ), + ) + .expect_err("the local annotation is invalid independently of mismatch"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadExpr(..))) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::AnnotationVariableEscapes { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!(errors.len(), 2, "{errors:#?}"); +} + +#[test] +fn recovery_blocks_repeated_and_recursive_uses_but_keeps_sibling_errors() { + for broken in [ + "broken = if True then () else (\\x -> x)\n", + "broken x = x x\n", + "broken x = if True then recurse x else (\\y -> y)\nrecurse x = if True then broken x else ()\n", + "trait Missing 'a where\n missing : 'a -> 'a\nbroken = missing ()\n", + ] { + let bump = Bump::new(); + let source = format!( + "module Main exposing (..)\nimport Builtin exposing (..)\n{broken}first : ()\nfirst = broken\nsecond : ()\nsecond = broken\nsibling : ()\nsibling = \\x -> x\n" + ); + let errors = infer(&bump, &source).expect_err("failed dependencies stay blocked"); + assert_eq!(errors.len(), 2, "{source}\n{errors:#?}"); + if broken == "broken x = x x\n" { + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::InfiniteType { .. })) + .count(), + 1, + "{errors:#?}" + ); + } + } +} + +#[test] +fn recovery_shared_partial_unification_does_not_create_trait_or_call_cascades() { + let bump = Bump::new(); + let errors = infer( + &bump, + indoc!( + r#" + module Main exposing (..) + trait Need 'a where + need : 'a -> () + outer x = + let + broken : ((), ()) + broken = (x, \y -> y) + in + (x (), need x, broken) + sibling : () + sibling = \x -> x + "# + ), + ) + .expect_err("shared argument depends on the failed tuple check"); + assert_eq!(errors.len(), 2, "{errors:#?}"); + assert!( + errors + .iter() + .all(|error| matches!(error, Error::BadExpr(..))), + "{errors:#?}" + ); +} + +#[test] +fn recovery_final_recursive_evidence_error_survives_an_independent_mismatch() { + let bump = Bump::new(); + let errors = infer( + &bump, + indoc!( + r#" + module Main exposing (..) + type option 'a = Some 'a + trait Keep 'a where + keep : 'a -> 'a + impl Keep 'a => Keep (option 'a) where + keep xs = xs + nest : Keep 'a => 'a -> () + nest x = nest (Some x) + sibling : () + sibling = \x -> x + "# + ), + ) + .expect_err("both checks must run"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadExpr(..))) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::PolymorphicRecursion { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!(errors.len(), 2, "{errors:#?}"); +} + +#[test] +fn recovery_resolution_limit_keeps_unrelated_obligations() { + let bump = Bump::new(); + let errors = infer( + &bump, + indoc!( + r#" + module Main exposing (..) + trait Keep 'a where + keep : 'a -> 'a + trait Missing 'a where + missing : 'a -> 'a + impl Keep (list (list 'a)) => Keep (list 'a) where + keep xs = xs + value = (keep [()], missing ()) + sibling : () + sibling = \x -> x + "# + ), + ) + .expect_err("limit and independent failures"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::ImplResolutionLimit { .. })) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!(errors.iter().filter(|error| matches!(error, Error::MissingImpl { trait_, .. } if trait_.name == "Missing")).count(), 1, "{errors:#?}"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadExpr(..))) + .count(), + 1, + "{errors:#?}" + ); +} + +#[test] +fn recovery_field_failures_block_dependent_traits_and_keep_independent_traits() { + for body in [ + "bad : ()\nbad = missing (().field)", + "type alias record = { a : () }\nbad : ()\nbad = missing ({ a = () }.field)", + "type thing = Thing { a : () }\nbad : thing -> ()\nbad record = missing record.absent", + "type thing = Thing { a : () }\nbad : thing -> thing\nbad record = missing { record | a = () }", + "bad : () -> ()\nbad record = missing { record | a = () }", + ] { + let bump = Bump::new(); + let source = format!( + "module Main exposing (..)\ntrait Missing 'a where\n missing : 'a -> 'a\n{body}\nsibling = missing ()\n" + ); + let errors = infer(&bump, &source).expect_err("field failure and independent missing impl"); + assert_eq!(errors.len(), 2, "{source}\n{errors:#?}"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::MissingImpl { .. })) + .count(), + 1, + "{source}\n{errors:#?}" + ); + } +} + +#[test] +fn recovery_keeps_independent_tuple_siblings_in_both_orders() { + let header = "module Main exposing (..)\ntrait Missing 'a where\n missing : 'a -> 'a\ntrait Round 'a where\n create : () -> 'a\n discard : 'a -> ()\ntype higher 'f = Higher ('f ())\nidfa : 'f 'a -> 'f 'a\nidfa x = x\n"; + for (first, second, expected) in [ + ("() ()", "missing ()", ["mismatch", "impl"]), + ("() ()", "discard (create ())", ["mismatch", "ambiguity"]), + ("idfa (Higher [])", "missing ()", ["kind", "impl"]), + ("idfa (Higher [])", "idfa (Higher [])", ["kind", "kind"]), + ] { + for reverse in [false, true] { + let bump = Bump::new(); + let (first, second) = if reverse { + (second, first) + } else { + (first, second) + }; + let source = format!("{header}bad = ({first}, {second})\n"); + let errors = infer(&bump, &source) + .expect_err("both tuple expressions are independently invalid"); + let mut actual: Vec<_> = errors + .iter() + .map(|error| match error { + Error::BadExpr(..) => "mismatch", + Error::MissingImpl { .. } => "impl", + Error::AmbiguousType { .. } => "ambiguity", + Error::BadKind { .. } => "kind", + _ => "unexpected", + }) + .collect(); + actual.sort(); + let mut expected = expected; + expected.sort(); + assert_eq!(actual, expected, "{source}\n{errors:#?}"); + } + } +} + +#[test] +fn recovery_annotated_tuple_keeps_independent_obligations() { + for body in ["(\\x -> x, missing ())", "(missing (), \\x -> x)"] { + let bump = Bump::new(); + let source = format!( + "module Main exposing (..)\ntrait Missing 'a where\n missing : 'a -> 'a\nbad : ((), ())\nbad = {body}\n" + ); + let errors = + infer(&bump, &source).expect_err("both tuple expressions are independently invalid"); + assert_eq!(errors.len(), 2, "{source}\n{errors:#?}"); + assert!( + errors + .iter() + .any(|error| matches!(error, Error::BadExpr(..))), + "{errors:#?}" + ); + assert!( + errors + .iter() + .any(|error| matches!(error, Error::MissingImpl { .. })), + "{errors:#?}" + ); + } +} + +#[test] +fn recovery_poisoned_tuple_child_keeps_independent_type_mismatches() { + for (annotation, body, field_errors) in [ + ("((), ())", "(().field, \\x -> x)", 1), + ("((), ())", "(\\x -> x, ().field)", 1), + ("((), ())", "(() (), \\x -> x)", 0), + ("((), ())", "(\\x -> x, () ())", 0), + ("((), ((), ()))", "((), (().field, \\x -> x))", 1), + ("(((), ()), ())", "((\\x -> x, ().field), ())", 1), + ("(((), ()), ())", "((().field, ()), \\x -> x)", 1), + ("((), ((), ()))", "(\\x -> x, ((), ().field))", 1), + ] { + let bump = Bump::new(); + let source = format!("module Main exposing (..)\nbad : {annotation}\nbad = {body}\n"); + let errors = infer(&bump, &source).expect_err("both tuple errors must survive"); + assert_eq!(errors.len(), 2, "{source}\n{errors:#?}"); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::NotARecord { .. })) + .count(), + field_errors, + "{source}\n{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadExpr(..))) + .count(), + 2 - field_errors, + "{source}\n{errors:#?}" + ); + } +} + +#[test] +fn recovery_field_selection_keeps_errors_independent_of_other_fields() { + for body in [ + "{ a = ().field, b = (\\x -> x) }.b", + "{ a = (\\x -> x), b = ().field }.a", + ] { + let bump = Bump::new(); + let source = format!( + "module Main exposing (..)\ntype alias record 'a 'b = {{ a : 'a, b : 'b }}\nbad : ()\nbad = {body}\n" + ); + let errors = + infer(&bump, &source).expect_err("selected field remains independently invalid"); + assert_eq!(errors.len(), 2, "{source}\n{errors:#?}"); + assert!( + errors + .iter() + .any(|error| matches!(error, Error::NotARecord { .. })), + "{source}\n{errors:#?}" + ); + assert!( + errors + .iter() + .any(|error| matches!(error, Error::BadExpr(..) | Error::FieldMismatch { .. })), + "{source}\n{errors:#?}" + ); + } + for (body, independent) in [ + ("missing ({ a = ().field, b = () }.b)", true), + ("missing ({ a = (), b = ().field }.b)", false), + ] { + let bump = Bump::new(); + let source = format!( + "module Main exposing (..)\ntype alias record 'a 'b = {{ a : 'a, b : 'b }}\ntrait Missing 'a where\n missing : 'a -> 'a\nbad : ()\nbad = {body}\n" + ); + let errors = infer(&bump, &source).expect_err("failed record field"); + assert_eq!( + errors.len(), + if independent { 2 } else { 1 }, + "{source}\n{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::MissingImpl { .. })) + .count(), + usize::from(independent), + "{source}\n{errors:#?}" + ); + assert!( + errors + .iter() + .any(|error| matches!(error, Error::NotARecord { .. })), + "{source}\n{errors:#?}" + ); + } +} + #[test] fn solved_output_records_empty_context_calls_and_preserves_capture_names() { let bump = Bump::new(); diff --git a/crates/nash-solve/tests/representation_predicates.rs b/crates/nash-solve/tests/representation_predicates.rs index f222fcc7..7f24260e 100644 --- a/crates/nash-solve/tests/representation_predicates.rs +++ b/crates/nash-solve/tests/representation_predicates.rs @@ -131,6 +131,131 @@ fn a_partial_constructor_cannot_be_a_value_type() { ); } +#[test] +fn recovery_collects_final_kind_errors_after_independent_type_failures() { + use nash_constrain::{ + Constraint, Type, + error::{Category, Expected}, + }; + let bump = Bump::new(); + let mut uf = UnionFind::new(); + let partial = bump.alloc(Type::AppN { + home: nash_ast::primitives::builtin_home(), + name: "list", + args: &[], + }); + let unit = bump.alloc(Type::AppN { + home: nash_ast::primitives::builtin_home(), + name: "unit", + args: &[], + }); + let function = bump.alloc(Type::FunN(unit, unit)); + let at = |line| nash_region::Region { + start: nash_region::Position { line, column: 1 }, + end: nash_region::Position { line, column: 2 }, + }; + let constraints = [ + Constraint::Equal( + at(1), + Category::Lambda, + function, + Expected::NoExpectation(unit), + ), + Constraint::Equal( + at(2), + Category::List, + partial, + Expected::NoExpectation(partial), + ), + Constraint::Equal( + at(3), + Category::List, + partial, + Expected::NoExpectation(partial), + ), + ]; + let errors = nash_solve::run( + &bump, + &mut uf, + &Constraint::And(&constraints), + &nash_can::environment::Tables::default(), + ) + .unwrap_err(); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadExpr(..))) + .count(), + 1, + "{errors:#?}" + ); + assert_eq!( + errors + .iter() + .filter(|error| matches!(error, Error::BadKind { .. })) + .count(), + 2, + "{errors:#?}" + ); +} + +#[test] +fn recovery_retains_shared_heads_removed_by_successful_normalization() { + use nash_constrain::{ + Constraint, Type, + error::{Category, Expected}, + }; + let bump = Bump::new(); + let mut uf = UnionFind::new(); + let head = nash_constrain::type_::mk_flex_var(&mut uf); + let result = nash_constrain::type_::mk_flex_var(&mut uf); + let head_type = bump.alloc(Type::VarN(head)); + let result_type = bump.alloc(Type::VarN(result)); + let unit = bump.alloc(Type::AppN { + home: nash_ast::primitives::builtin_home(), + name: "unit", + args: &[], + }); + let args = bump.alloc_slice_copy(&[&*unit]); + let applied = bump.alloc(Type::AppVarN(head_type, args)); + let list = bump.alloc(Type::AppN { + home: nash_ast::primitives::builtin_home(), + name: "list", + args, + }); + let function = bump.alloc(Type::FunN(unit, unit)); + let pairs = [ + (result_type as &Type<'_>, applied as &Type<'_>), + (result_type, list), + (result_type, function), + (head_type, function), + ]; + let constraints: Vec<_> = pairs + .into_iter() + .map(|(actual, expected)| { + Constraint::Equal( + nash_region::Region::zero(), + Category::List, + actual, + Expected::NoExpectation(expected), + ) + }) + .collect(); + let errors = nash_solve::run( + &bump, + &mut uf, + &Constraint::And(&constraints), + &nash_can::environment::Tables::default(), + ) + .unwrap_err(); + assert_eq!( + errors.len(), + 1, + "normalization must not sever the dependency from the applied type to its shared head: {errors:#?}" + ); + assert!(matches!(errors[0], Error::BadExpr(..))); +} + #[test] fn annotation_kinds_are_fixed_before_instantiation() { let bump = Bump::new(); diff --git a/crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_forward.snap b/crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_forward.snap new file mode 100644 index 00000000..cf32e13a --- /dev/null +++ b/crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_forward.snap @@ -0,0 +1,175 @@ +--- +source: crates/nash-solve/tests/inference.rs +description: "module Main exposing (..)\ntrait Round 'a where\n create : () -> 'a\n discard : 'a -> ()\ntype higher 'f = Higher ('f ())\nidfa : 'f 'a -> 'f 'a\nidfa x = x\nmismatch : ()\nmismatch = \\x -> x\nmissing = discard ()\nconstraint : 'a -> ()\nconstraint x = discard x\nambiguous = discard (create ())\nkind = idfa (Higher [])\n" +expression: errors +--- +[ + BadKind { + region: Region { + start: Position { + line: 14, + column: 1, + }, + end: Position { + line: 14, + column: 5, + }, + }, + name: "type application", + args: [], + reason: Mismatch { + expected: Arrow( + Type, + Type, + ), + actual: Type, + }, + }, + AmbiguousType { + region: Region { + start: Position { + line: 13, + column: 1, + }, + end: Position { + line: 13, + column: 10, + }, + }, + name: "ambiguous", + variable: FlexVar( + "a", + ), + predicates: [ + AmbiguousPredicate { + trait_: QualifiedName { + home: ModuleName { + package: None, + name: "Main", + }, + name: "Round", + }, + args: [ + FlexVar( + "a", + ), + ], + }, + ], + }, + MissingConstraint { + region: Region { + start: Position { + line: 12, + column: 16, + }, + end: Position { + line: 12, + column: 23, + }, + }, + name: "discard", + trait_: QualifiedName { + home: ModuleName { + package: None, + name: "Main", + }, + name: "Round", + }, + args: [ + RigidVar( + "a", + ), + ], + binder: Located { + region: Region { + start: Position { + line: 12, + column: 1, + }, + end: Position { + line: 12, + column: 11, + }, + }, + value: "constraint", + }, + }, + BadExpr( + Region { + start: Position { + line: 9, + column: 12, + }, + end: Position { + line: 9, + column: 19, + }, + }, + Lambda, + Lambda( + FlexVar( + "a", + ), + FlexVar( + "a", + ), + [], + ), + FromAnnotation( + "mismatch", + 0, + TypedBody, + Type { + home: ModuleName { + package: Some( + PackageName { + author: "nash", + project: "core", + }, + ), + name: "Builtin", + }, + name: "unit", + args: [], + }, + ), + ), + MissingImpl { + region: Region { + start: Position { + line: 10, + column: 11, + }, + end: Position { + line: 10, + column: 18, + }, + }, + name: "discard", + trait_: QualifiedName { + home: ModuleName { + package: None, + name: "Main", + }, + name: "Round", + }, + args: [ + Type { + home: ModuleName { + package: Some( + PackageName { + author: "nash", + project: "core", + }, + ), + name: "Builtin", + }, + name: "unit", + args: [], + }, + ], + available: [], + because: [], + }, +] diff --git a/crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_reversed.snap b/crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_reversed.snap new file mode 100644 index 00000000..fda024ff --- /dev/null +++ b/crates/nash-solve/tests/snapshots/inference__recovery_mixed_errors_reversed.snap @@ -0,0 +1,175 @@ +--- +source: crates/nash-solve/tests/inference.rs +description: "module Main exposing (..)\ntrait Round 'a where\n create : () -> 'a\n discard : 'a -> ()\ntype higher 'f = Higher ('f ())\nidfa : 'f 'a -> 'f 'a\nidfa x = x\nkind = idfa (Higher [])\nambiguous = discard (create ())\nconstraint : 'a -> ()\nconstraint x = discard x\nmissing = discard ()\nmismatch : ()\nmismatch = \\x -> x\n" +expression: errors +--- +[ + BadKind { + region: Region { + start: Position { + line: 8, + column: 1, + }, + end: Position { + line: 8, + column: 5, + }, + }, + name: "type application", + args: [], + reason: Mismatch { + expected: Arrow( + Type, + Type, + ), + actual: Type, + }, + }, + AmbiguousType { + region: Region { + start: Position { + line: 9, + column: 1, + }, + end: Position { + line: 9, + column: 10, + }, + }, + name: "ambiguous", + variable: FlexVar( + "a", + ), + predicates: [ + AmbiguousPredicate { + trait_: QualifiedName { + home: ModuleName { + package: None, + name: "Main", + }, + name: "Round", + }, + args: [ + FlexVar( + "a", + ), + ], + }, + ], + }, + MissingConstraint { + region: Region { + start: Position { + line: 11, + column: 16, + }, + end: Position { + line: 11, + column: 23, + }, + }, + name: "discard", + trait_: QualifiedName { + home: ModuleName { + package: None, + name: "Main", + }, + name: "Round", + }, + args: [ + RigidVar( + "a", + ), + ], + binder: Located { + region: Region { + start: Position { + line: 11, + column: 1, + }, + end: Position { + line: 11, + column: 11, + }, + }, + value: "constraint", + }, + }, + BadExpr( + Region { + start: Position { + line: 14, + column: 12, + }, + end: Position { + line: 14, + column: 19, + }, + }, + Lambda, + Lambda( + FlexVar( + "a", + ), + FlexVar( + "a", + ), + [], + ), + FromAnnotation( + "mismatch", + 0, + TypedBody, + Type { + home: ModuleName { + package: Some( + PackageName { + author: "nash", + project: "core", + }, + ), + name: "Builtin", + }, + name: "unit", + args: [], + }, + ), + ), + MissingImpl { + region: Region { + start: Position { + line: 12, + column: 11, + }, + end: Position { + line: 12, + column: 18, + }, + }, + name: "discard", + trait_: QualifiedName { + home: ModuleName { + package: None, + name: "Main", + }, + name: "Round", + }, + args: [ + Type { + home: ModuleName { + package: Some( + PackageName { + author: "nash", + project: "core", + }, + ), + name: "Builtin", + }, + name: "unit", + args: [], + }, + ], + available: [], + because: [], + }, +] From 49704cefc8489dea1f9a63bdcda08b4c990bcaa9 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 04/12] feat(report): add documents and renderers Signed-off-by: microproofs --- Cargo.lock | 21 + Cargo.toml | 1 + crates/nash-report/Cargo.toml | 27 + crates/nash-report/src/code.rs | 203 +++++ ...snippet__tests__pair_snippet_snapshot.snap | 9 + ...tests__unicode_pair_on_separate_lines.snap | 9 + crates/nash-report/src/code/snippet.rs | 372 ++++++++ crates/nash-report/src/doc.rs | 834 ++++++++++++++++++ crates/nash-report/src/json.rs | 138 +++ crates/nash-report/src/lib.rs | 150 ++++ crates/nash-report/src/render.rs | 256 ++++++ ...__doc__tests__chunks_merge_plain_runs.snap | 25 + .../nash_report__doc__tests__cycle_box.snap | 11 + ...doc__tests__hang_aligns_continuations.snap | 6 + ...ts__reflow_inside_indent_keeps_indent.snap | 7 + ...eport__doc__tests__reflow_wraps_at_80.snap | 7 + ..._doc__tests__sep_breaks_when_too_wide.snap | 34 + ...eport__doc__tests__sep_flat_when_fits.snap | 5 + ...ests__stack_separates_with_blank_line.snap | 7 + ...ema_preserves_primary_span_and_styles.snap | 40 + ...ts__paired_regions_are_self_contained.snap | 34 + ...__render__tests__render_eof_insertion.snap | 12 + ...nder__tests__render_no_snippet_report.snap | 8 + ...rt__render__tests__render_pair_report.snap | 14 + ..._render__tests__render_snippet_report.snap | 12 + ..._render__tests__render_warning_header.snap | 12 + ...__render_zero_width_region_gets_caret.snap | 12 + crates/nash-report/src/suggest.rs | 123 +++ 28 files changed, 2389 insertions(+) create mode 100644 crates/nash-report/Cargo.toml create mode 100644 crates/nash-report/src/code.rs create mode 100644 crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__pair_snippet_snapshot.snap create mode 100644 crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__unicode_pair_on_separate_lines.snap create mode 100644 crates/nash-report/src/code/snippet.rs create mode 100644 crates/nash-report/src/doc.rs create mode 100644 crates/nash-report/src/json.rs create mode 100644 crates/nash-report/src/lib.rs create mode 100644 crates/nash-report/src/render.rs create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__chunks_merge_plain_runs.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__cycle_box.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__hang_aligns_continuations.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_inside_indent_keeps_indent.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_wraps_at_80.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__sep_breaks_when_too_wide.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__sep_flat_when_fits.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__doc__tests__stack_separates_with_blank_line.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__json__tests__elm_schema_preserves_primary_span_and_styles.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__json__tests__paired_regions_are_self_contained.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__render__tests__render_eof_insertion.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__render__tests__render_no_snippet_report.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__render__tests__render_pair_report.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__render__tests__render_snippet_report.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__render__tests__render_warning_header.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__render__tests__render_zero_width_region_gets_caret.snap create mode 100644 crates/nash-report/src/suggest.rs diff --git a/Cargo.lock b/Cargo.lock index ec8b8b84..aa9ea5bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2039,6 +2039,27 @@ dependencies = [ name = "nash-region" version = "0.2.0" +[[package]] +name = "nash-report" +version = "0.1.0" +dependencies = [ + "bumpalo", + "indoc", + "insta", + "miette", + "nash-ast", + "nash-can", + "nash-constrain", + "nash-nitpick", + "nash-parse", + "nash-region", + "nash-solve", + "nash-source", + "serde", + "serde_json", + "unicode-width 0.1.14", +] + [[package]] name = "nash-solve" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 8299fcc0..d5eadb56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ octocrab = { version = "0.49.5", features = ["stream"] } pretty_assertions = "1.4.0" pubgrub = { version = "0.3", features = ["serde"] } serde = { version = "1", features = ["derive"] } +serde_json = "1" tar = "0.4" thiserror = "2" tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread", "sync"] } diff --git a/crates/nash-report/Cargo.toml b/crates/nash-report/Cargo.toml new file mode 100644 index 00000000..5683f4eb --- /dev/null +++ b/crates/nash-report/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nash-report" +version = "0.1.0" +edition.workspace = true +description = "Error reports for the Nash compiler: Elm's prose rendered with miette" +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +miette.workspace = true +unicode-width = "0.1.14" +serde.workspace = true +serde_json.workspace = true +nash-ast = { path = "../nash-ast", version = "0.7.0" } +nash-can = { path = "../nash-can", version = "0.6.0" } +nash-constrain = { path = "../nash-constrain", version = "0.4.0" } +nash-nitpick = { path = "../nash-nitpick", version = "0.2.0" } +nash-parse = { path = "../nash-parse", version = "0.5.0" } +nash-region = { path = "../nash-region", version = "0.2.0" } +nash-source = { path = "../nash-source", version = "0.6.0" } + +[dev-dependencies] +bumpalo.workspace = true +indoc.workspace = true +insta.workspace = true +nash-solve = { path = "../nash-solve", version = "0.4.0" } diff --git a/crates/nash-report/src/code.rs b/crates/nash-report/src/code.rs new file mode 100644 index 00000000..d3a88c8e --- /dev/null +++ b/crates/nash-report/src/code.rs @@ -0,0 +1,203 @@ +//! 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}; + +pub struct Source<'s> { + text: &'s str, + /// Byte offset of every line start; the last entry is `text.len()` + /// when the text ends in a newline (Elm's `lines ++ [""]`). + line_starts: Vec, +} + +impl<'s> Source<'s> { + pub fn new(text: &'s str) -> Source<'s> { + let line_starts = std::iter::once(0) + .chain(text.match_indices('\n').map(|(i, _)| i + 1)) + .collect(); + Source { text, line_starts } + } + + pub fn text(&self) -> &'s str { + self.text + } + + /// 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 Some(&start) = self.line_starts.get(row) else { + return self.text.len(); + }; + let end = self + .line_starts + .get(row + 1) + .map_or(self.text.len(), |next| next - 1); + let mut offset = start + .saturating_add(usize::from(position.column.saturating_sub(1))) + .min(end); + while !self.text.is_char_boundary(offset) { + offset -= 1; + } + offset + } + + /// Keep spans on UTF-8 boundaries. Empty spans highlight the next character; + /// at EOF they remain empty so renderers can draw an insertion caret. + pub fn span(&self, region: Region) -> SourceSpan { + let start = self.offset(region.start); + let mut end = self.offset(region.end).max(start); + if end == start && start < self.text.len() { + end += self.text[start..].chars().next().map_or(0, char::len_utf8); + } + (start, end - start).into() + } + + /// 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 end = self + .line_starts + .get(usize::from(row)) + .map_or(self.text.len(), |next| next - 1); + Some(&self.text[start..end.max(start)]) + } + + /// Elm's `whatIsNext`. + 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)?)..)) + else { + return Next::Other(None); + }; + let mut chars = rest.chars(); + let Some(c) = chars.next() else { + return Next::Other(None); + }; + let inner_len = |s: &str| { + s.chars() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .map(char::len_utf8) + .sum::() + }; + if c.is_uppercase() { + Next::Upper(&rest[..c.len_utf8() + inner_len(chars.as_str())]) + } else if c.is_lowercase() { + let name = &rest[..c.len_utf8() + inner_len(chars.as_str())]; + if nash_parse::keyword::is_reserved(name) { + Next::Keyword(name) + } else { + Next::Lower(name) + } + } else if c.is_ascii() && nash_parse::symbol::is_binop_char(c as u8) { + let len = rest + .bytes() + .take_while(|b| nash_parse::symbol::is_binop_char(*b)) + .count(); + Next::Operator(&rest[..len]) + } else { + match c { + ')' => Next::Close("parenthesis", ')'), + ']' => Next::Close("square bracket", ']'), + '}' => Next::Close("curly brace", '}'), + other => Next::Other(Some(other)), + } + } + } + + /// Elm's `nextLineStartsWithKeyword`. + pub fn next_line_starts_with_keyword(&self, keyword: &str, row: Row) -> Option<(Row, Col)> { + let line = self.line(row.checked_add(1)?)?; + let indent = line.bytes().take_while(|b| *b == b' ').count(); + let rest = &line[indent..]; + let follows = rest.strip_prefix(keyword)?; + let boundary = follows + .chars() + .next() + .is_none_or(|c| !(c.is_alphanumeric() || c == '_')); + boundary.then_some((row + 1, 1 + indent as Col)) + } + + /// Elm's `nextLineStartsWithCloseCurly`. + pub fn next_line_starts_with_close_curly(&self, row: Row) -> Option<(Row, Col)> { + let line = self.line(row.checked_add(1)?)?; + let indent = line.bytes().take_while(|b| *b == b' ').count(); + line[indent..] + .starts_with('}') + .then_some((row + 1, 1 + indent as Col)) + } +} + +/// Elm's `Next`. +#[derive(Debug, PartialEq, Eq)] +pub enum Next<'s> { + Keyword(&'s str), + Operator(&'s str), + Close(&'static str, char), + Upper(&'s str), + Lower(&'s str), + Other(Option), +} + +/// Elm's `toRegion row col`. +pub fn to_region(row: Row, col: Col) -> Region { + let pos = Position::new(row, col); + Region::new(pos, pos) +} + +/// Elm's `toWiderRegion`. +pub fn to_wider_region(row: Row, col: Col, extra: u16) -> Region { + Region::new( + Position::new(row, col), + Position::new(row, col.saturating_add(extra)), + ) +} + +/// Elm's `toKeywordRegion`. +pub fn to_keyword_region(row: Row, col: Col, keyword: &str) -> Region { + to_wider_region(row, col, keyword.len() as u16) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn source_positions_and_tokens() { + let source = Source::new("f = x\n in value"); + assert_eq!(source.offset(Position::new(2, 5)), 10); + assert_eq!(source.next_line_starts_with_keyword("in", 1), Some((2, 5))); + assert_eq!(source.what_is_next(2, 5), Next::Keyword("in")); + assert_eq!( + Source::new("+ ) Upper lower").what_is_next(1, 1), + Next::Operator("+") + ); + assert_eq!( + Source::new(")").what_is_next(1, 1), + Next::Close("parenthesis", ')') + ); + assert_eq!( + Source::new("Upper").what_is_next(1, 1), + Next::Upper("Upper") + ); + assert_eq!( + Source::new("lower").what_is_next(1, 1), + Next::Lower("lower") + ); + assert_eq!(source.what_is_next(1, 99), Next::Other(None)); + assert_eq!(source.line(0), None); + } + #[test] + fn spans_stay_inside_unicode_source_and_eof() { + let source = Source::new("é\nlast"); + assert_eq!(source.offset(Position::new(1, 99)), 2); + assert_eq!(source.offset(Position::new(2, 5)), 7); + let span = source.span(Region::new(Position::new(2, 5), Position::new(2, 5))); + assert_eq!((span.offset(), span.len()), (7, 0)); + assert_eq!(source.offset(Position::new(1, 2)), 0); + assert_eq!(Source::new("").span(Region::zero()).len(), 0); + } +} 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 new file mode 100644 index 00000000..280bd08e --- /dev/null +++ b/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__pair_snippet_snapshot.snap @@ -0,0 +1,9 @@ +--- +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 new file mode 100644 index 00000000..2b0b87c3 --- /dev/null +++ b/crates/nash-report/src/code/snapshots/nash_report__code__snippet__tests__unicode_pair_on_separate_lines.snap @@ -0,0 +1,9 @@ +--- +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 new file mode 100644 index 00000000..c0ce587b --- /dev/null +++ b/crates/nash-report/src/code/snippet.rs @@ -0,0 +1,372 @@ +//! 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/doc.rs b/crates/nash-report/src/doc.rs new file mode 100644 index 00000000..224c7732 --- /dev/null +++ b/crates/nash-report/src/doc.rs @@ -0,0 +1,834 @@ +//! Elm's `Reporting/Doc.hs`: a small Wadler-style pretty printer with the +//! handful of combinators the reports use. + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Doc { + Empty, + Text(String), + Styled(Style, Box), + Cat(Vec), + Nest(usize, Box), + Align(Box), + Line, + LineBreak, + HardLine, + Group(Box), + Fill(Vec), +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Style { + pub bold: bool, + pub underline: bool, + pub color: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Color { + pub base: BaseColor, + pub vivid: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BaseColor { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, +} + +impl From<&str> for Doc { + fn from(s: &str) -> Doc { + Doc::text(s) + } +} + +impl Doc { + /// `P.text` / `fromChars` / `fromName`. Panics on '\n': use `vcat`. + pub fn text(s: impl Into) -> Doc { + let s = s.into(); + assert!(!s.contains('\n'), "Doc::text must not contain newlines"); + Doc::Text(s) + } + + pub fn from_int(n: impl std::fmt::Display) -> Doc { + Doc::text(n.to_string()) + } + + /// `a <> b`. + pub fn cat(docs: impl IntoIterator) -> Doc { + Doc::Cat(docs.into_iter().collect()) + } + /// `P.hsep`. + pub fn hsep(docs: impl IntoIterator) -> Doc { + intersperse(docs, Doc::text(" ")) + } + /// `P.hcat`. + pub fn hcat(docs: impl IntoIterator) -> Doc { + Doc::cat(docs) + } + /// `P.vcat`. + pub fn vcat(docs: impl IntoIterator) -> Doc { + intersperse(docs, Doc::HardLine) + } + /// `P.sep` = `group (vsep docs)`. + pub fn sep(docs: impl IntoIterator) -> Doc { + Doc::Group(Box::new(intersperse(docs, Doc::Line))) + } + /// `P.fillSep`. + pub fn fill_sep(docs: impl IntoIterator) -> Doc { + Doc::Fill(docs.into_iter().collect()) + } + /// `P.indent`. + pub fn indent(n: usize, doc: Doc) -> Doc { + Doc::hang(n, Doc::cat([Doc::text(" ".repeat(n)), doc])) + } + /// `P.hang`. + pub fn hang(n: usize, doc: Doc) -> Doc { + Doc::Align(Box::new(Doc::Nest(n, Box::new(doc)))) + } + /// `P.align`. + pub fn align(doc: Doc) -> Doc { + Doc::Align(Box::new(doc)) + } + + /// Elm `stack`: paragraphs separated by blank lines. + pub fn stack(docs: impl IntoIterator) -> Doc { + intersperse(docs, Doc::cat([Doc::HardLine, Doc::HardLine])) + } + + /// Elm `reflow`: words wrapped to the width. + pub fn reflow(paragraph: &str) -> Doc { + Doc::fill_sep(paragraph.split_whitespace().map(Doc::text)) + } + + /// Elm `commaSep`. + pub fn comma_sep(conjunction: &str, style: impl Fn(Doc) -> Doc, names: Vec) -> Vec { + match names.len() { + 0 => Vec::new(), + 1 => names.into_iter().map(style).collect(), + 2 => { + let mut it = names.into_iter(); + vec![ + style(it.next().unwrap()), + Doc::text(conjunction), + style(it.next().unwrap()), + ] + } + n => { + let mut docs: Vec = names + .iter() + .take(n - 1) + .map(|name| Doc::cat([style(name.clone()), Doc::text(",")])) + .collect(); + docs.push(Doc::text(conjunction)); + docs.push(style(names[n - 1].clone())); + docs + } + } + } + + // STYLES (Elm's P.red .. P.dullyellow) + + fn styled(self, f: impl FnOnce(&mut Style)) -> Doc { + let mut style = Style::default(); + f(&mut style); + Doc::Styled(style, Box::new(self)) + } + fn color(self, base: BaseColor, vivid: bool) -> Doc { + self.styled(|s| s.color = Some(Color { base, vivid })) + } + pub fn red(self) -> Doc { + self.color(BaseColor::Red, true) + } + pub fn dullred(self) -> Doc { + self.color(BaseColor::Red, false) + } + pub fn green(self) -> Doc { + self.color(BaseColor::Green, true) + } + pub fn yellow(self) -> Doc { + self.color(BaseColor::Yellow, true) + } + pub fn dullyellow(self) -> Doc { + self.color(BaseColor::Yellow, false) + } + pub fn cyan(self) -> Doc { + self.color(BaseColor::Cyan, true) + } + pub fn dullcyan(self) -> Doc { + self.color(BaseColor::Cyan, false) + } + pub fn blue(self) -> Doc { + self.color(BaseColor::Blue, true) + } + pub fn magenta(self) -> Doc { + self.color(BaseColor::Magenta, true) + } + pub fn black(self) -> Doc { + self.color(BaseColor::Black, true) + } + pub fn bold(self) -> Doc { + self.styled(|s| s.bold = true) + } + pub fn underline(self) -> Doc { + self.styled(|s| s.underline = true) + } + + // NOTES, HINTS, LINKS + + /// `toFancyNote`: `fillSep (underline "Note" <> ":" : chunks)`. + pub fn to_fancy_note(chunks: impl IntoIterator) -> Doc { + Doc::fill_sep( + std::iter::once(Doc::cat([Doc::text("Note").underline(), Doc::text(":")])) + .chain(chunks), + ) + } + pub fn to_simple_note(message: &str) -> Doc { + Doc::to_fancy_note(message.split_whitespace().map(Doc::text)) + } + pub fn to_fancy_hint(chunks: impl IntoIterator) -> Doc { + Doc::fill_sep( + std::iter::once(Doc::cat([Doc::text("Hint").underline(), Doc::text(":")])) + .chain(chunks), + ) + } + pub fn to_simple_hint(message: &str) -> Doc { + Doc::to_fancy_hint(message.split_whitespace().map(Doc::text)) + } + + /// `link word before fileName after`. + pub fn link(word: &str, before: &str, file_name: &str, after: &str) -> Doc { + Doc::fill_sep( + std::iter::once(Doc::cat([Doc::text(word).underline(), Doc::text(":")])) + .chain(before.split_whitespace().map(Doc::text)) + .chain(std::iter::once(Doc::text(make_link(file_name)))) + .chain(after.split_whitespace().map(Doc::text)), + ) + } + pub fn fancy_link(word: &str, before: Vec, file_name: &str, after: Vec) -> Doc { + Doc::fill_sep( + std::iter::once(Doc::cat([Doc::text(word).underline(), Doc::text(":")])) + .chain(before) + .chain(std::iter::once(Doc::text(make_link(file_name)))) + .chain(after), + ) + } + pub fn reflow_link(before: &str, file_name: &str, after: &str) -> Doc { + Doc::fill_sep( + before + .split_whitespace() + .map(Doc::text) + .chain(std::iter::once(Doc::text(make_link(file_name)))) + .chain(after.split_whitespace().map(Doc::text)), + ) + } + + /// Elm `cycle`: the boxed dependency cycle drawing. + pub fn cycle(indent: usize, name: &str, names: &[&str]) -> Doc { + let to_ln = |n: &str| Doc::cat([Doc::text("│ "), Doc::text(n).dullyellow()]); + let mut lines = vec![Doc::text("┌─────┐")]; + for (i, n) in std::iter::once(name) + .chain(names.iter().copied()) + .enumerate() + { + if i > 0 { + lines.push(Doc::text("│ ↓")); + } + lines.push(to_ln(n)); + } + lines.push(Doc::text("└─────┘")); + Doc::indent(indent, Doc::vcat(lines)) + } + + // RENDERING + + /// Elm `toString` / `toAnsi` at the given width. + pub fn render(&self, width: usize, color: bool) -> String { + render(self, width, color) + } + /// Elm `Doc.encode`: styled chunks for JSON. + pub fn chunks(&self, width: usize) -> Vec { + chunks(self, width) + } +} + +pub fn make_link(file_name: &str) -> String { + format!("") +} +pub fn make_naked_link(file_name: &str) -> String { + format!("https://nash-script.dev/hints/{file_name}") +} + +/// Elm `args`. +pub fn args(n: usize) -> String { + format!("{n} argument{}", if n == 1 { "" } else { "s" }) +} +/// Elm `moreArgs`. +pub fn more_args(n: usize) -> String { + format!("{n} more argument{}", if n == 1 { "" } else { "s" }) +} +/// Elm `ordinal` over a zero-based index. +pub fn ordinal(index: usize) -> String { + int_to_ordinal(index + 1) +} +/// Elm `intToOrdinal`. +pub fn int_to_ordinal(number: usize) -> String { + let ending = match (number % 100, number % 10) { + (11..=13, _) => "th", + (_, 1) => "st", + (_, 2) => "nd", + (_, 3) => "rd", + _ => "th", + }; + format!("{number}{ending}") +} + +fn intersperse(docs: impl IntoIterator, sep: Doc) -> Doc { + let mut out = Vec::new(); + for (i, doc) in docs.into_iter().enumerate() { + if i > 0 { + out.push(sep.clone()); + } + out.push(doc); + } + Doc::Cat(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + const PARAGRAPH: &str = "I cannot find this variable in the current module. Check the spelling of its name and make sure that the module which defines it is imported. These suggestions may help you find the value you intended to use."; + + #[test] + fn fill_allows_first_child_group_to_break() { + let doc = Doc::fill_sep([Doc::sep(["long", "long"].map(Doc::text)), Doc::text("x")]); + assert_eq!(doc.render(5, false), "long\nlong\nx"); + assert_eq!(doc.render(6, false), "long\nlong x"); + } + + #[test] + fn fill_allows_later_child_group_to_break() { + let doc = Doc::fill_sep([Doc::text("a"), Doc::sep(["long", "long"].map(Doc::text))]); + assert_eq!(doc.render(5, false), "a\nlong\nlong"); + assert_eq!(doc.render(6, false), "a long\nlong"); + } + + #[test] + fn fill_preserves_nested_fill_choices() { + let doc = Doc::fill_sep([Doc::text("x"), Doc::fill_sep(["ab", "cd"].map(Doc::text))]); + assert_eq!(doc.render(4, false), "x ab\ncd"); + } + + #[test] + fn fill_nested_layout_keeps_styles_and_alignment() { + let doc = Doc::cat([ + Doc::text("x "), + Doc::align(Doc::fill_sep([ + Doc::sep(["long", "long"].map(Doc::text)).green(), + Doc::text("z"), + ])), + ]); + assert_eq!(doc.render(8, false), "x long\n long z"); + assert!( + doc.chunks(8) + .iter() + .any(|c| matches!(c, Chunk::Styled {text,..} if text == "long")) + ); + } + + #[test] + fn group_fits_when_suffix_group_can_break() { + let doc = Doc::cat([ + Doc::sep(["a", "b"].map(Doc::text)), + Doc::sep(["c", "dddd"].map(Doc::text)), + ]); + assert_eq!(doc.render(4, false), "a bc\ndddd"); + } + + #[test] + fn reflow_wraps_at_80() { + insta::assert_snapshot!(Doc::reflow(PARAGRAPH).render(80, false)); + } + #[test] + fn reflow_inside_indent_keeps_indent() { + insta::assert_snapshot!(Doc::indent(4, Doc::reflow(PARAGRAPH)).render(80, false)); + } + #[test] + fn stack_separates_with_blank_line() { + insta::assert_snapshot!( + Doc::stack([Doc::text("First."), Doc::text("Second.")]).render(80, false) + ); + } + #[test] + fn sep_flat_when_fits() { + insta::assert_snapshot!(Doc::sep(["a", "->", "b"].map(Doc::text)).render(80, false)); + } + #[test] + fn sep_breaks_when_too_wide() { + insta::assert_snapshot!(Doc::sep((0..30).map(|_| Doc::text("longword"))).render(80, false)); + } + #[test] + fn hang_aligns_continuations() { + insta::assert_snapshot!( + Doc::cat([ + Doc::text("0123456789"), + Doc::hang(4, Doc::sep(["argument", "result"].map(Doc::text))) + ]) + .render(20, false) + ); + } + #[test] + fn cycle_box() { + insta::assert_snapshot!(Doc::cycle(4, "a", &["b", "c"]).render(80, false)); + } + #[test] + fn fancy_note_underlines_word() { + assert!( + Doc::to_simple_note("A note.") + .render(80, true) + .contains("\x1b[4mNote") + ); + } + #[test] + fn chunks_merge_plain_runs() { + insta::assert_debug_snapshot!( + Doc::cat([ + Doc::text("a"), + Doc::text("b"), + Doc::text("c").dullyellow(), + Doc::text("d").dullyellow(), + Doc::text("e") + ]) + .chunks(80) + ); + } + #[test] + fn int_to_ordinal_table() { + assert_eq!( + [1, 2, 3, 4, 11, 12, 13, 21, 22, 23, 101, 111] + .map(int_to_ordinal) + .join(" "), + "1st 2nd 3rd 4th 11th 12th 13th 21st 22nd 23rd 101st 111th" + ); + } + #[test] + fn comma_sep_three() { + assert_eq!( + Doc::comma_sep("and", |d| d, ["a", "b", "c"].map(Doc::text).to_vec()) + .iter() + .map(|d| d.render(80, false)) + .collect::>(), + ["a,", "b,", "and", "c"] + ); + } + #[test] + fn group_accounts_for_suffix() { + assert_eq!( + Doc::cat([Doc::sep(["ab", "cd"].map(Doc::text)), Doc::text("ef")]).render(6, false), + "ab\ncdef" + ); + } + #[test] + fn line_break_disappears_only_in_flat_mode() { + let d = Doc::Group(Box::new(Doc::cat([ + Doc::text("ab"), + Doc::LineBreak, + Doc::text("cd"), + ]))); + assert_eq!(d.render(4, false), "abcd"); + assert_eq!(d.render(3, false), "ab\ncd"); + } + #[test] + fn hardline_survives_group() { + assert_eq!( + Doc::Group(Box::new(Doc::vcat([Doc::text("a"), Doc::text("b")]))).render(80, false), + "a\nb" + ); + } + #[test] + fn unicode_width_counts_characters() { + assert_eq!(Doc::reflow("éé λλ x").render(5, false), "éé λλ\nx"); + } + #[test] + fn nested_styles_restore_outer() { + let chunks = Doc::cat([Doc::text("a"), Doc::text("b").green(), Doc::text("c")]) + .dullred() + .underline() + .chunks(80); + assert_eq!(chunks.len(), 3); + assert!( + matches!(&chunks[2],Chunk::Styled{style,text} if style.underline && style.color == Some(Color{base:BaseColor::Red,vivid:false}) && text == "c") + ); + } + #[test] + fn trims_spaces_across_styles() { + assert_eq!( + Doc::vcat([ + Doc::cat([Doc::text("a "), Doc::text(" ").red()]), + Doc::text("b ") + ]) + .render(80, false), + "a\nb" + ); + } + #[test] + fn links_use_nash_hint_site() { + assert_eq!( + Doc::reflow_link("Read", "imports", "for help.").render(80, false), + "Read for help." + ); + } + #[test] + fn json_preserves_dull_and_vivid_colors() { + assert_eq!( + Doc::cat([ + Doc::text("plain"), + Doc::text("dull").dullyellow(), + Doc::text("vivid").yellow(), + Doc::text("bold").bold() + ]) + .encode(), + serde_json::json!([ + "plain", + {"bold":false,"underline":false,"color":"yellow","string":"dull"}, + {"bold":false,"underline":false,"color":"YELLOW","string":"vivid"}, + {"bold":true,"underline":false,"color":null,"string":"bold"} + ]) + ); + } + #[test] + fn outer_group_counts_fill_separators() { + let d = Doc::sep([Doc::fill_sep(["ab", "cd"].map(Doc::text)), Doc::text("ef")]); + assert_eq!(d.render(7, false), "ab cd\nef"); + assert_eq!(d.render(8, false), "ab cd ef"); + } + #[test] + fn indent_at_current_column() { + assert_eq!( + Doc::cat([ + Doc::text("prefix "), + Doc::indent(2, Doc::vcat(["a", "b"].map(Doc::text))) + ]) + .render(80, false), + "prefix a\n b" + ); + } + #[test] + fn narrow_width_preserves_long_words() { + assert_eq!(Doc::reflow("long word").render(0, false), "long\nword"); + } + #[test] + fn empty_documents_render_empty() { + for d in [ + Doc::Empty, + Doc::text(""), + Doc::fill_sep([]), + Doc::sep([]), + Doc::stack([]), + ] { + assert_eq!(d.render(0, false), ""); + assert!(d.chunks(80).is_empty()); + } + } + #[test] + fn paragraph_blank_lines_have_no_indentation() { + assert_eq!( + Doc::indent(4, Doc::stack(["a", "b"].map(Doc::text))).render(80, false), + " a\n\n b" + ); + } + #[test] + fn argument_and_ordinal_helpers() { + assert_eq!(args(0), "0 arguments"); + assert_eq!(args(1), "1 argument"); + assert_eq!(more_args(1), "1 more argument"); + assert_eq!(more_args(2), "2 more arguments"); + assert_eq!(ordinal(0), "1st"); + } + #[test] + #[should_panic(expected = "must not contain newlines")] + fn text_rejects_newlines() { + Doc::text("a\nb"); + } +} + +/// A rendered run, suitable for Elm's JSON message array. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Chunk { + Plain(String), + Styled { style: Style, text: String }, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Mode { + Flat, + Break, +} + +#[derive(Clone, Copy)] +enum Work<'a> { + Doc(usize, Mode, Style, &'a Doc), + Fill(usize, Mode, Style, &'a [Doc]), +} + +fn merge(outer: Style, inner: Style) -> Style { + Style { + bold: outer.bold || inner.bold, + underline: outer.underline || inner.underline, + color: inner.color.or(outer.color), + } +} + +/// Probe the entire pending line, including the suffix outside a group. +fn fits(width: usize, mut column: usize, mut work: Vec>) -> bool { + while let Some(item) = work.pop() { + if column > width { + return false; + } + match item { + Work::Fill(indent, mode, style, docs) => { + if let Some((first, rest)) = docs.split_first() { + if mode == Mode::Break { + // A fill separator is `group line`, so the pending + // line can always end here if its flat choice fails. + return true; + } + column = column.saturating_add(1); + work.push(Work::Fill(indent, mode, style, rest)); + work.push(Work::Doc(indent, mode, style, first)); + } + } + Work::Doc(indent, mode, style, doc) => match doc { + Doc::Empty => {} + Doc::Text(s) => column = column.saturating_add(s.chars().count()), + Doc::Styled(st, inner) => { + work.push(Work::Doc(indent, mode, merge(style, *st), inner)) + } + Doc::Nest(n, inner) => { + work.push(Work::Doc(indent.saturating_add(*n), mode, style, inner)) + } + Doc::Align(inner) => work.push(Work::Doc(column, mode, style, inner)), + Doc::Cat(docs) => { + work.extend(docs.iter().rev().map(|d| Work::Doc(indent, mode, style, d))) + } + // Only a flat ancestor flattens this group. An unflattened + // suffix may break: probing its broken branch suffices because + // the flat choice is selected only when that whole line fits. + Doc::Group(inner) => work.push(Work::Doc(indent, mode, style, inner)), + Doc::Line if mode == Mode::Flat => column = column.saturating_add(1), + Doc::LineBreak if mode == Mode::Flat => {} + Doc::Line | Doc::LineBreak | Doc::HardLine => return true, + Doc::Fill(docs) => { + if let Some((first, rest)) = docs.split_first() { + work.push(Work::Fill(indent, mode, style, rest)); + work.push(Work::Doc(indent, mode, style, first)); + } + } + }, + } + } + column <= width +} + +fn push_run(runs: &mut Vec<(Style, String)>, style: Style, text: &str) { + if text.is_empty() { + return; + } + if let Some((last_style, last_text)) = runs.last_mut() + && *last_style == style + { + last_text.push_str(text); + } else { + runs.push((style, text.to_owned())); + } +} + +fn trim_spaces(runs: &mut Vec<(Style, String)>) { + while let Some((_, text)) = runs.last_mut() { + text.truncate(text.trim_end_matches(' ').len()); + if !text.is_empty() { + break; + } + runs.pop(); + } +} + +fn newline(runs: &mut Vec<(Style, String)>, indent: usize) { + trim_spaces(runs); + push_run(runs, Style::default(), "\n"); + push_run(runs, Style::default(), &" ".repeat(indent)); +} + +fn layout(doc: &Doc, width: usize) -> Vec<(Style, String)> { + let mut runs = Vec::new(); + let mut column: usize = 0; + let mut work = vec![Work::Doc(0, Mode::Break, Style::default(), doc)]; + while let Some(item) = work.pop() { + match item { + Work::Fill(indent, mode, style, docs) => { + if let Some((first, rest)) = docs.split_first() { + let mut probe = work.clone(); + probe.push(Work::Fill(indent, mode, style, rest)); + probe.push(Work::Doc(indent, mode, style, first)); + if mode == Mode::Flat || fits(width, column.saturating_add(1), probe) { + push_run(&mut runs, style, " "); + column = column.saturating_add(1); + } else { + newline(&mut runs, indent); + column = indent; + } + work.push(Work::Fill(indent, mode, style, rest)); + work.push(Work::Doc(indent, mode, style, first)); + } + } + Work::Doc(indent, mode, style, doc) => match doc { + Doc::Empty => {} + Doc::Text(s) => { + push_run(&mut runs, style, s); + column = column.saturating_add(s.chars().count()); + } + Doc::Styled(st, inner) => { + work.push(Work::Doc(indent, mode, merge(style, *st), inner)) + } + Doc::Nest(n, inner) => { + work.push(Work::Doc(indent.saturating_add(*n), mode, style, inner)) + } + Doc::Align(inner) => work.push(Work::Doc(column, mode, style, inner)), + Doc::Cat(docs) => { + work.extend(docs.iter().rev().map(|d| Work::Doc(indent, mode, style, d))) + } + Doc::Group(inner) => { + let mut probe = work.clone(); + probe.push(Work::Doc(indent, Mode::Flat, style, inner)); + let chosen = if mode == Mode::Flat || fits(width, column, probe) { + Mode::Flat + } else { + Mode::Break + }; + work.push(Work::Doc(indent, chosen, style, inner)); + } + Doc::Line if mode == Mode::Flat => { + push_run(&mut runs, style, " "); + column = column.saturating_add(1); + } + Doc::LineBreak if mode == Mode::Flat => {} + Doc::Line | Doc::LineBreak | Doc::HardLine => { + newline(&mut runs, indent); + column = indent; + } + Doc::Fill(docs) => { + if let Some((first, rest)) = docs.split_first() { + work.push(Work::Fill(indent, mode, style, rest)); + work.push(Work::Doc(indent, mode, style, first)); + } + } + }, + } + } + trim_spaces(&mut runs); + // Trimming may expose adjacent runs with the same style. + let mut merged = Vec::new(); + for (style, text) in runs { + push_run(&mut merged, style, &text); + } + merged +} + +fn ansi_open(style: Style) -> String { + let mut codes = Vec::new(); + if style.bold { + codes.push(1); + } + if style.underline { + codes.push(4); + } + if let Some(color) = style.color { + let base = match color.base { + BaseColor::Black => 30, + BaseColor::Red => 31, + BaseColor::Green => 32, + BaseColor::Yellow => 33, + BaseColor::Blue => 34, + BaseColor::Magenta => 35, + BaseColor::Cyan => 36, + BaseColor::White => 37, + }; + codes.push(base + if color.vivid { 60 } else { 0 }); + } + format!( + "\x1b[{}m", + codes + .iter() + .map(ToString::to_string) + .collect::>() + .join(";") + ) +} + +fn render(doc: &Doc, width: usize, color: bool) -> String { + let mut out = String::new(); + for (style, text) in layout(doc, width) { + if color && style != Style::default() { + out.push_str(&ansi_open(style)); + out.push_str(&text); + out.push_str("\x1b[0m"); + } else { + out.push_str(&text); + } + } + out +} + +fn chunks(doc: &Doc, width: usize) -> Vec { + layout(doc, width) + .into_iter() + .map(|(style, text)| { + if style == Style::default() { + Chunk::Plain(text) + } else { + Chunk::Styled { style, text } + } + }) + .collect() +} + +impl Doc { + /// Elm's `toLine`: disable optional wrapping, retaining explicit lines. + pub fn to_line(&self) -> String { + self.render(usize::MAX / 2, false) + } + + /// Elm's `encode`, including its case-sensitive color names. + pub fn encode(&self) -> serde_json::Value { + serde_json::Value::Array(self.chunks(80).into_iter().map(|chunk| match chunk { + Chunk::Plain(text) => serde_json::Value::String(text), + Chunk::Styled {style,text} => serde_json::json!({"bold":style.bold,"underline":style.underline,"color":style.color.map(Color::json_name),"string":text}), + }).collect()) + } +} + +impl Color { + pub fn json_name(self) -> String { + let name = match self.base { + BaseColor::Black => "black", + BaseColor::Red => "red", + BaseColor::Green => "green", + BaseColor::Yellow => "yellow", + BaseColor::Blue => "blue", + BaseColor::Magenta => "magenta", + BaseColor::Cyan => "cyan", + BaseColor::White => "white", + }; + if self.vivid { + name.to_ascii_uppercase() + } else { + name.to_owned() + } + } +} diff --git a/crates/nash-report/src/json.rs b/crates/nash-report/src/json.rs new file mode 100644 index 00000000..c959a9f0 --- /dev/null +++ b/crates/nash-report/src/json.rs @@ -0,0 +1,138 @@ +//! Elm's `Reporting/Error.hs` JSON schema and complete styled messages. + +use crate::{Doc, ModuleReports, Report, Snippet, Source}; +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::>(), + }) +} + +/// Compile-error envelope used by Elm's command-line reporting protocol. +pub fn compile_errors(modules: &[ModuleReports]) -> Value { + json!({"type": "compile-errors", "errors": modules.iter().map(module_to_json).collect::>()}) +} + +/// Nash extension using the same module/problem schema for warnings. +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()}) +} + +/// Elm's one-based, half-open source region schema. +pub fn encode_region(region: Region) -> Value { + json!({ + "start": {"line": region.start.line, "column": region.start.column}, + "end": {"line": region.end.line, "column": region.end.column}, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Doc, Label, Snippet}; + use nash_region::Position; + + fn region(sr: u16, sc: u16, er: u16, ec: u16) -> Region { + Region::new(Position::new(sr, sc), Position::new(er, ec)) + } + + #[test] + fn elm_schema_preserves_primary_span_and_styles() { + let report = Report::snippet( + "NAMING ERROR", + region(2, 5, 2, 12), + None, + Doc::text("I cannot find this name:"), + Doc::text("Try "), + ) + .with_region(region(1, 1, 2, 12)) + .with_suggestions(vec!["found".into()]); + let mut report = report; + report.after = Doc::cat([ + Doc::text("Try "), + Doc::text("found").green(), + Doc::text("."), + ]); + let module = ModuleReports { + name: "Main".into(), + path: "src/Main.nash".into(), + source: "main =\n missing".into(), + reports: vec![report], + }; + let value = module_to_json(&module); + assert_eq!( + value["problems"][0]["region"], + 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); + insta::assert_snapshot!(serde_json::to_string_pretty(&value).unwrap()); + } + + #[test] + fn no_snippet_does_not_add_empty_code_lines() { + let mut report = Report::snippet( + "MODULE NAME MISSING", + Region::zero(), + None, + Doc::text("First."), + Doc::text("Second."), + ); + report.snippet = Snippet::None; + assert_eq!( + report_to_json(&Source::new(""), &report)["message"], + serde_json::json!(["First.\n\nSecond."]) + ); + } + + #[test] + fn paired_regions_are_self_contained() { + let report = Report::pair( + "NAME CLASH", + Label { + region: region(1, 1, 1, 2), + text: "first".into(), + }, + Label { + region: region(2, 1, 2, 2), + text: "second".into(), + }, + 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() + ); + } + + #[test] + fn error_and_warning_envelopes() { + assert_eq!( + compile_errors(&[]), + serde_json::json!({"type":"compile-errors","errors":[]}) + ); + assert_eq!( + compile_warnings(&[]), + serde_json::json!({"type":"compile-warnings","errors":[]}) + ); + } +} diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs new file mode 100644 index 00000000..077a3ab6 --- /dev/null +++ b/crates/nash-report/src/lib.rs @@ -0,0 +1,150 @@ +//! Error reports: Elm's `Reporting/*` prose as miette diagnostics. +//! +//! Each phase's error data (`nash_parse::error`, `nash_can::Error`, ...) +//! is turned into an owned `Report` that outlives the module arena. A +//! `Report` renders three ways: `render` (miette, terminal), `json` +//! (Elm's `--report=json` shape), and the LSP conversion in +//! `nash-language-server`. + +pub mod code; +pub mod doc; +pub mod json; +mod render; +pub mod suggest; + +use nash_region::Region; + +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. +#[derive(Clone, Debug)] +pub struct Report { + pub title: String, + pub severity: Severity, + pub region: Region, + pub snippet: Snippet, + pub before: Doc, + pub after: Doc, + pub suggestions: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Severity { + Error, + Warning, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Snippet { + /// `Code.toSnippet source region highlight`. + Region { + region: Region, + highlight: Option, + }, + /// `Code.toPair source r1 r2`. + Pair { first: Label, second: Label }, + /// No code shown. + None, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Label { + pub region: Region, + pub text: String, +} + +impl Report { + /// `Report.Report title region [] (Code.toSnippet source region highlight (before, after))`. + pub fn snippet( + title: &str, + region: Region, + highlight: Option, + before: Doc, + after: Doc, + ) -> Report { + Report { + title: title.to_string(), + severity: Severity::Error, + region, + snippet: Snippet::Region { region, highlight }, + before, + after, + suggestions: Vec::new(), + } + } + + /// `Report.Report title r2 [] (Code.toPair source r1 r2 ...)`. The + /// "one line" / "two chunks" wording choice Elm makes is replaced by + /// two labels; `before` is Elm's `twoStart` and `after` its `twoEnd`. + pub fn pair(title: &str, first: Label, second: Label, before: Doc, after: Doc) -> Report { + let region = second.region; + Report { + title: title.to_string(), + severity: Severity::Error, + region, + snippet: Snippet::Pair { first, second }, + before, + after, + suggestions: Vec::new(), + } + } + + /// Set the surrounding snippet while retaining the primary diagnostic region. + pub fn with_region(mut self, surroundings: Region) -> Report { + if let Snippet::Region { region, highlight } = &mut self.snippet { + *highlight = Some(highlight.unwrap_or(self.region)); + *region = surroundings; + } + self + } + + pub fn with_suggestions(mut self, suggestions: Vec) -> Report { + self.suggestions = suggestions; + self + } + + pub fn warning(mut self) -> Report { + self.severity = Severity::Warning; + self + } +} + +/// Owned reports remain valid after a compiler module's arena is dropped. +#[derive(Clone, Debug)] +pub struct ModuleReports { + pub name: String, + pub path: String, + pub source: String, + pub reports: Vec, +} + +impl ModuleReports { + pub fn json(&self) -> serde_json::Value { + json::module_to_json(self) + } + pub fn render(&self, color: bool) -> Vec { + let source = Source::new(&self.source); + self.reports + .iter() + .map(|report| report.render(&source, &self.path, color)) + .collect() + } + + /// Stable presentation order without removing distinct errors at one span. + pub fn sort(&mut self) { + self.reports.sort_by_cached_key(|report| { + ( + report.region, + report.title.clone(), + report.before.render(80, false), + report.after.render(80, false), + report.severity, + report.snippet.clone(), + report.suggestions.clone(), + ) + }); + } +} diff --git a/crates/nash-report/src/render.rs b/crates/nash-report/src/render.rs new file mode 100644 index 00000000..6da5a588 --- /dev/null +++ b/crates/nash-report/src/render.rs @@ -0,0 +1,256 @@ +//! `Report` -> `miette::Diagnostic`. + +use std::fmt; + +use miette::{ + Diagnostic, GraphicalReportHandler, GraphicalTheme, LabeledSpan, MietteHandler, + MietteHandlerOpts, NamedSource, SourceCode, +}; + +use crate::code::Source; +use crate::{Report, Severity, Snippet}; + +pub const WIDTH: usize = 80; + +/// A `Report` bound to its file, owning everything miette needs. +#[derive(Debug)] +pub struct Rendered { + title: String, + severity: Severity, + message: String, + help: Option, + labels: Vec, + source: RenderSource, +} + +/// Expand miette's source read to the report's requested surrounding region. +/// Labels retain the narrow primary span; the wider source is context only. +#[derive(Debug)] +struct RenderSource { + source: NamedSource, + surroundings: Option, +} + +impl SourceCode for RenderSource { + fn read_span<'a>( + &'a self, + span: &miette::SourceSpan, + before: usize, + after: usize, + ) -> Result + 'a>, miette::MietteError> { + // miette reads a label without context to locate the header. Expanding + // that read would move the displayed location away from the problem. + if before == 0 && after == 0 { + return self.source.read_span(span, before, after); + } + let expanded = self.surroundings.map_or(*span, |context| { + let start = span.offset().min(context.offset()); + let end = (span.offset() + span.len()).max(context.offset() + context.len()); + (start, end - start).into() + }); + self.source.read_span(&expanded, before, after) + } +} + +impl Report { + pub fn render(&self, source: &Source<'_>, path: &str, color: bool) -> Rendered { + // miette needs a display cell at EOF to draw an insertion caret. This + // padding is terminal-only: source positions and JSON/LSP stay unchanged. + let span = |region| { + let raw = source.span(region); + if raw.is_empty() { + (raw.offset(), 1).into() + } else { + raw + } + }; + let labels = match &self.snippet { + Snippet::Region { region, highlight } => vec![LabeledSpan::new_with_span( + None, + span(highlight.unwrap_or(*region)), + )], + Snippet::Pair { first, second } => vec![ + LabeledSpan::new_with_span(Some(first.text.clone()), span(first.region)), + LabeledSpan::new_primary_with_span(Some(second.text.clone()), span(second.region)), + ], + Snippet::None => Vec::new(), + }; + let mut display_source = source.text().to_string(); + if labels + .iter() + .any(|label| label.offset() + label.len() > display_source.len()) + { + display_source.push('\n'); + } + let after = self.after.render(WIDTH, color); + Rendered { + title: self.title.clone(), + severity: self.severity, + message: self.before.render(WIDTH, color), + help: (!after.is_empty()).then_some(after), + labels, + source: RenderSource { + source: NamedSource::new(path, display_source), + surroundings: match self.snippet { + Snippet::Region { region, .. } => Some(span(region)), + _ => None, + }, + }, + } + } +} + +impl fmt::Display for Rendered { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for Rendered {} + +impl Diagnostic for Rendered { + fn code(&self) -> Option> { + Some(Box::new(&self.title)) + } + + fn severity(&self) -> Option { + Some(match self.severity { + Severity::Error => miette::Severity::Error, + Severity::Warning => miette::Severity::Warning, + }) + } + + fn help(&self) -> Option> { + self.help + .as_ref() + .map(|help| Box::new(help) as Box) + } + + fn source_code(&self) -> Option<&dyn SourceCode> { + Some(&self.source) + } + + fn labels(&self) -> Option + '_>> { + Some(Box::new(self.labels.iter().cloned())) + } +} + +/// The handler the CLI installs through `miette::set_hook`: fixed width, +/// no re-wrapping (Doc already wrapped), color decided by the CLI. +pub fn handler(color: bool) -> MietteHandler { + MietteHandlerOpts::new() + .width(WIDTH) + .force_graphical(true) + .wrap_lines(false) + .color(color) + .unicode(true) + .build() +} + +/// Plain-text rendering for snapshot tests: the same options +/// `MietteHandlerOpts::build` applies, on a handler that can write to a +/// `String`. +pub fn render_plain(report: &Report, source: &Source<'_>, path: &str) -> String { + let rendered = report.render(source, path, false); + let mut out = String::new(); + GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()) + .with_width(WIDTH) + .with_wrap_lines(false) + .render_report(&mut out, &rendered) + .expect("render"); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Doc, Label}; + use nash_region::{Position, Region}; + fn region(a: u16, b: u16) -> Region { + Region::new(Position::new(1, a), Position::new(1, b)) + } + fn snippet() -> Report { + Report::snippet( + "TEST", + region(5, 6), + None, + Doc::text("Before:"), + Doc::text("After."), + ) + } + fn plain(report: &Report) -> String { + render_plain(report, &Source::new("f = x + 1"), "Main.nash") + } + #[test] + fn render_snippet_report() { + insta::assert_snapshot!(plain(&snippet())); + } + #[test] + fn render_warning_header() { + insta::assert_snapshot!(plain(&snippet().warning())); + } + #[test] + fn render_pair_report() { + let pair = Report::pair( + "TEST", + Label { + region: region(1, 2), + text: "first".into(), + }, + Label { + region: region(5, 6), + text: "second".into(), + }, + Doc::text("Before:"), + Doc::text("After."), + ); + let output = plain(&pair); + assert!(output.contains("[Main.nash:1:5]")); + insta::assert_snapshot!(output); + } + #[test] + fn render_no_snippet_report() { + let mut none = snippet(); + none.snippet = Snippet::None; + insta::assert_snapshot!(plain(&none)); + } + #[test] + fn render_zero_width_region_gets_caret() { + let zero = Report::snippet( + "TEST", + region(5, 5), + None, + Doc::text("Before:"), + Doc::text("After."), + ); + insta::assert_snapshot!(plain(&zero)); + } + #[test] + fn render_eof_insertion() { + let eof = Report::snippet( + "MISSING EXPRESSION", + region(4, 4), + None, + Doc::text("I need an expression here:"), + Doc::text("Add an expression after the equals sign."), + ); + insta::assert_snapshot!(render_plain(&eof, &Source::new("f ="), "Main.nash")); + } + #[test] + fn surrounding_region_keeps_primary_highlight() { + let report = snippet().with_region(region(1, 10)); + assert_eq!(report.region, region(5, 6)); + assert!(plain(&report).contains("[Main.nash:1:5]")); + assert!( + matches!(report.snippet, Snippet::Region { region: wide, highlight: Some(narrow) } if wide == region(1,10) && narrow == region(5,6)) + ); + let source = Source::new("f =\n case x of\n _ -> ()\n () -> ()"); + let narrow = Region::new(Position::new(4, 9), Position::new(4, 11)); + let wide = Region::new(Position::new(2, 5), Position::new(4, 17)); + let report = Report::snippet("TEST", narrow, None, Doc::text("Before:"), Doc::Empty) + .with_region(wide); + let output = render_plain(&report, &source, "Main.nash"); + assert!(output.contains("[Main.nash:4:9]"), "{output}"); + assert!(output.contains("case x of"), "{output}"); + } +} diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__chunks_merge_plain_runs.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__chunks_merge_plain_runs.snap new file mode 100644 index 00000000..bff7a17f --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__chunks_merge_plain_runs.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::cat([Doc::text(\"a\"), Doc::text(\"b\"), Doc::text(\"c\").dullyellow(),\nDoc::text(\"d\").dullyellow(), Doc::text(\"e\")]).chunks(80)" +--- +[ + Plain( + "ab", + ), + Styled { + style: Style { + bold: false, + underline: false, + color: Some( + Color { + base: Yellow, + vivid: false, + }, + ), + }, + text: "cd", + }, + Plain( + "e", + ), +] diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__cycle_box.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__cycle_box.snap new file mode 100644 index 00000000..3c93a9ef --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__cycle_box.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::cycle(4, \"a\", &[\"b\", \"c\"]).render(80, false)" +--- + ┌─────┐ + │ a + │ ↓ + │ b + │ ↓ + │ c + └─────┘ diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__hang_aligns_continuations.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__hang_aligns_continuations.snap new file mode 100644 index 00000000..3666f756 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__hang_aligns_continuations.snap @@ -0,0 +1,6 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::cat([Doc::text(\"0123456789\"),\nDoc::hang(4,\nDoc::sep([\"argument\", \"result\"].map(Doc::text)))]).render(20, false)" +--- +0123456789argument + result diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_inside_indent_keeps_indent.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_inside_indent_keeps_indent.snap new file mode 100644 index 00000000..87200805 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_inside_indent_keeps_indent.snap @@ -0,0 +1,7 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::indent(4, Doc::reflow(PARAGRAPH)).render(80, false)" +--- + I cannot find this variable in the current module. Check the spelling of its + name and make sure that the module which defines it is imported. These + suggestions may help you find the value you intended to use. diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_wraps_at_80.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_wraps_at_80.snap new file mode 100644 index 00000000..5f203346 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__reflow_wraps_at_80.snap @@ -0,0 +1,7 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::reflow(PARAGRAPH).render(80, false)" +--- +I cannot find this variable in the current module. Check the spelling of its +name and make sure that the module which defines it is imported. These +suggestions may help you find the value you intended to use. diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__sep_breaks_when_too_wide.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__sep_breaks_when_too_wide.snap new file mode 100644 index 00000000..42e34b19 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__sep_breaks_when_too_wide.snap @@ -0,0 +1,34 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::sep((0..30).map(|_| Doc::text(\"longword\"))).render(80, false)" +--- +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword +longword diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__sep_flat_when_fits.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__sep_flat_when_fits.snap new file mode 100644 index 00000000..acfa6bc9 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__sep_flat_when_fits.snap @@ -0,0 +1,5 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::sep([\"a\", \"->\", \"b\"].map(Doc::text)).render(80, false)" +--- +a -> b diff --git a/crates/nash-report/src/snapshots/nash_report__doc__tests__stack_separates_with_blank_line.snap b/crates/nash-report/src/snapshots/nash_report__doc__tests__stack_separates_with_blank_line.snap new file mode 100644 index 00000000..9d002da7 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__doc__tests__stack_separates_with_blank_line.snap @@ -0,0 +1,7 @@ +--- +source: crates/nash-report/src/doc.rs +expression: "Doc::stack([Doc::text(\"First.\"), Doc::text(\"Second.\")]).render(80, false)" +--- +First. + +Second. diff --git a/crates/nash-report/src/snapshots/nash_report__json__tests__elm_schema_preserves_primary_span_and_styles.snap b/crates/nash-report/src/snapshots/nash_report__json__tests__elm_schema_preserves_primary_span_and_styles.snap new file mode 100644 index 00000000..5bba81d7 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__json__tests__elm_schema_preserves_primary_span_and_styles.snap @@ -0,0 +1,40 @@ +--- +source: crates/nash-report/src/json.rs +expression: "serde_json::to_string_pretty(&value).unwrap()" +--- +{ + "name": "Main", + "path": "src/Main.nash", + "problems": [ + { + "message": [ + "I cannot find this name:\n\n1| main =\n2| missing\n ", + { + "bold": false, + "color": "RED", + "string": "^^^^^^^", + "underline": false + }, + "\nTry ", + { + "bold": false, + "color": "GREEN", + "string": "found", + "underline": false + }, + "." + ], + "region": { + "end": { + "column": 12, + "line": 2 + }, + "start": { + "column": 5, + "line": 2 + } + }, + "title": "NAMING ERROR" + } + ] +} diff --git a/crates/nash-report/src/snapshots/nash_report__json__tests__paired_regions_are_self_contained.snap b/crates/nash-report/src/snapshots/nash_report__json__tests__paired_regions_are_self_contained.snap new file mode 100644 index 00000000..969ccfb3 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__json__tests__paired_regions_are_self_contained.snap @@ -0,0 +1,34 @@ +--- +source: crates/nash-report/src/json.rs +expression: "serde_json::to_string_pretty(&report_to_json(&Source::new(\"x\\nx\"),\n&report)).unwrap()" +--- +{ + "message": [ + "Both names occur here:\n\n1| x\n ", + { + "bold": false, + "color": "RED", + "string": "^", + "underline": false + }, + "\n\n2| x\n ", + { + "bold": false, + "color": "RED", + "string": "^", + "underline": false + }, + "\nChoose another name." + ], + "region": { + "end": { + "column": 2, + "line": 2 + }, + "start": { + "column": 1, + "line": 2 + } + }, + "title": "NAME CLASH" +} diff --git a/crates/nash-report/src/snapshots/nash_report__render__tests__render_eof_insertion.snap b/crates/nash-report/src/snapshots/nash_report__render__tests__render_eof_insertion.snap new file mode 100644 index 00000000..b05326be --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__render__tests__render_eof_insertion.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/render.rs +expression: "render_plain(&eof, &Source::new(\"f =\"), \"Main.nash\")" +--- +MISSING EXPRESSION + + × I need an expression here: + ╭─[Main.nash:1:4] + 1 │ f = + · ─ + ╰──── + help: Add an expression after the equals sign. diff --git a/crates/nash-report/src/snapshots/nash_report__render__tests__render_no_snippet_report.snap b/crates/nash-report/src/snapshots/nash_report__render__tests__render_no_snippet_report.snap new file mode 100644 index 00000000..24834536 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__render__tests__render_no_snippet_report.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-report/src/render.rs +expression: plain(&none) +--- +TEST + + × Before: + help: After. diff --git a/crates/nash-report/src/snapshots/nash_report__render__tests__render_pair_report.snap b/crates/nash-report/src/snapshots/nash_report__render__tests__render_pair_report.snap new file mode 100644 index 00000000..166a7f4e --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__render__tests__render_pair_report.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/render.rs +expression: output +--- +TEST + + × Before: + ╭─[Main.nash:1:5] + 1 │ f = x + 1 + · ┬ ┬ + · │ ╰── second + · ╰── first + ╰──── + help: After. diff --git a/crates/nash-report/src/snapshots/nash_report__render__tests__render_snippet_report.snap b/crates/nash-report/src/snapshots/nash_report__render__tests__render_snippet_report.snap new file mode 100644 index 00000000..8cea7cd0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__render__tests__render_snippet_report.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/render.rs +expression: plain(&snippet()) +--- +TEST + + × Before: + ╭─[Main.nash:1:5] + 1 │ f = x + 1 + · ─ + ╰──── + help: After. diff --git a/crates/nash-report/src/snapshots/nash_report__render__tests__render_warning_header.snap b/crates/nash-report/src/snapshots/nash_report__render__tests__render_warning_header.snap new file mode 100644 index 00000000..e4cfe1cb --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__render__tests__render_warning_header.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/render.rs +expression: plain(&snippet().warning()) +--- +TEST + + ⚠ Before: + ╭─[Main.nash:1:5] + 1 │ f = x + 1 + · ─ + ╰──── + help: After. diff --git a/crates/nash-report/src/snapshots/nash_report__render__tests__render_zero_width_region_gets_caret.snap b/crates/nash-report/src/snapshots/nash_report__render__tests__render_zero_width_region_gets_caret.snap new file mode 100644 index 00000000..6acf5379 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__render__tests__render_zero_width_region_gets_caret.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/render.rs +expression: plain(&zero) +--- +TEST + + × Before: + ╭─[Main.nash:1:5] + 1 │ f = x + 1 + · ─ + ╰──── + help: After. diff --git a/crates/nash-report/src/suggest.rs b/crates/nash-report/src/suggest.rs new file mode 100644 index 00000000..38bc1a50 --- /dev/null +++ b/crates/nash-report/src/suggest.rs @@ -0,0 +1,123 @@ +//! Elm's `Reporting/Suggest.hs`: near-miss name suggestions. + +/// Restricted Damerau-Levenshtein (optimal string alignment) distance. +pub fn distance(x: &str, y: &str) -> usize { + let a: Vec = x.chars().collect(); + let b: Vec = y.chars().collect(); + let mut d = vec![vec![0usize; b.len() + 1]; a.len() + 1]; + for (i, row) in d.iter_mut().enumerate() { + row[0] = i; + } + for (j, value) in d[0].iter_mut().enumerate() { + *value = j; + } + for i in 1..=a.len() { + for j in 1..=b.len() { + let cost = usize::from(a[i - 1] != b[j - 1]); + d[i][j] = (d[i - 1][j] + 1) + .min(d[i][j - 1] + 1) + .min(d[i - 1][j - 1] + cost); + if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1] { + d[i][j] = d[i][j].min(d[i - 2][j - 2] + 1); + } + } + } + d[a.len()][b.len()] +} + +/// Elm `sort`: candidates ordered by distance to `target`, case-insensitive. +/// Equal distances retain their input order. +pub fn sort(target: &str, to_string: impl Fn(&T) -> String, mut values: Vec) -> Vec { + let target = to_lower(target); + values.sort_by_cached_key(|value| distance(&target, &to_lower(&to_string(value)))); + values +} + +/// Elm `rank`: stable candidate ordering with each candidate's distance. +pub fn rank(target: &str, to_string: impl Fn(&T) -> String, values: Vec) -> Vec<(usize, T)> { + let target = to_lower(target); + let mut ranked: Vec<_> = values + .into_iter() + .map(|value| (distance(&target, &to_lower(&to_string(&value))), value)) + .collect(); + ranked.sort_by_key(|(distance, _)| *distance); + ranked +} + +/// Haskell's `map Char.toLower` maps each scalar to one scalar, without +/// contextual final sigma or the full lowercase expansion of dotted capital I. +fn to_lower(string: &str) -> String { + string + .chars() + .map(|c| c.to_lowercase().next().unwrap_or(c)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn distance_transposition_is_one() { + assert_eq!(distance("ab", "ba"), 1); + } + + #[test] + fn distance_empty() { + assert_eq!(distance("", ""), 0); + assert_eq!(distance("", "abc"), 3); + assert_eq!(distance("abc", ""), 3); + } + + #[test] + fn distance_is_restricted_and_case_sensitive() { + // Unrestricted Damerau-Levenshtein would give 2 here. + assert_eq!(distance("CA", "ABC"), 3); + assert_eq!(distance("A", "a"), 1); + assert_eq!(distance("kitten", "sitting"), 3); + assert_eq!(distance("same", "same"), 0); + } + + #[test] + fn distance_counts_unicode_scalars() { + assert_eq!(distance("", "é🦀"), 2); + assert_eq!(distance("é🦀", "🦀é"), 1); + } + + #[test] + fn sort_prefers_case_insensitive_match() { + let values = vec!["height", "len", "LENGTH"]; + assert_eq!( + sort("lenght", |s| (*s).into(), values), + ["LENGTH", "height", "len"] + ); + } + + #[test] + fn rank_keeps_stable_order_for_ties() { + let values = vec!["bat", "hat", "CAT", "mat"]; + assert_eq!( + rank("cat", |s| (*s).into(), values.clone()), + [(0, "CAT"), (1, "bat"), (1, "hat"), (1, "mat")] + ); + assert_eq!( + sort("cat", |s| (*s).into(), values), + ["CAT", "bat", "hat", "mat"] + ); + } + + #[test] + fn lowercase_matches_elms_character_mapping() { + assert_eq!(rank("İ", |s| (*s).into(), vec!["i"]), [(0, "i")]); + assert_eq!( + rank("ΟΣ", |s| (*s).into(), vec!["οσ", "ος"]), + [(0, "οσ"), (1, "ος")] + ); + } + + #[test] + fn sort_and_rank_accept_empty_candidates() { + assert!(sort::("target", Clone::clone, vec![]).is_empty()); + assert!(rank::("target", Clone::clone, vec![]).is_empty()); + } +} From f096e72951bd53d7a368d64373d04a6d0524ddc7 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 05/12] feat(report): render type names and differences Signed-off-by: microproofs --- crates/nash-report/src/lib.rs | 6 +- crates/nash-report/src/localizer.rs | 259 ++++++++ crates/nash-report/src/render_type.rs | 338 +++++++++++ crates/nash-report/src/type_diff.rs | 836 ++++++++++++++++++++++++++ 4 files changed, 1438 insertions(+), 1 deletion(-) create mode 100644 crates/nash-report/src/localizer.rs create mode 100644 crates/nash-report/src/render_type.rs create mode 100644 crates/nash-report/src/type_diff.rs diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index 077a3ab6..f6d4a1f5 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -9,8 +9,12 @@ pub mod code; pub mod doc; pub mod json; -mod render; +pub mod localizer; +pub mod render_type; pub mod suggest; +pub mod type_diff; +pub use localizer::Localizer; +mod render; use nash_region::Region; diff --git a/crates/nash-report/src/localizer.rs b/crates/nash-report/src/localizer.rs new file mode 100644 index 00000000..0be470e2 --- /dev/null +++ b/crates/nash-report/src/localizer.rs @@ -0,0 +1,259 @@ +//! Type names as they can be written in the current source module. +use crate::doc::Doc; +use nash_ast::ModuleName; +use nash_source::{Exposed, Exposing, Import, Module}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Debug, Default)] +pub struct Localizer { + imports: BTreeMap, + local_module: Option, + local_package: Option<(String, String)>, + local_unions: BTreeSet, + bare_primitives: BTreeSet, +} +#[derive(Clone, Debug)] +struct ImportInfo { + alias: Option, + exposing: Option>, +} +impl Localizer { + pub fn from_module(module: &Module<'_>, defaults: &[&Import<'_>]) -> Self { + let mut this = Self { + local_module: Some(module.name.map_or("Main", |name| name.value).to_owned()), + bare_primitives: nash_ast::primitives::PRIMITIVES + .iter() + .map(|primitive| primitive.name.to_owned()) + .collect(), + local_unions: module + .unions + .iter() + .map(|union| union.value.name.value.to_owned()) + .collect(), + ..Self::default() + }; + for import in defaults.iter().chain(module.imports.iter()) { + this.add_import(import); + } + for name in module + .unions + .iter() + .map(|union| union.value.name.value) + .chain(module.aliases.iter().map(|alias| alias.value.name.value)) + { + this.bare_primitives.remove(name); + } + this.imports.insert( + module.name.map_or("Main", |n| n.value).into(), + ImportInfo { + alias: None, + exposing: None, + }, + ); + this + } + pub fn from_names<'a>(names: impl IntoIterator) -> Self { + Self { + imports: names + .into_iter() + .map(|n| { + ( + n.into(), + ImportInfo { + alias: None, + exposing: None, + }, + ) + }) + .collect(), + ..Self::default() + } + } + + pub fn with_package(mut self, package: Option>) -> Self { + self.local_package = + package.map(|package| (package.author.to_owned(), package.project.to_owned())); + self + } + + pub fn is_local_union(&self, home: ModuleName<'_>, name: &str) -> bool { + self.local_module.as_deref() == Some(home.name) + && self + .local_package + .as_ref() + .map(|(author, project)| (author.as_str(), project.as_str())) + == home + .package + .map(|package| (package.author, package.project)) + && self.local_unions.contains(name) + } + + fn add_import(&mut self, import: &Import<'_>) { + let exposing: Option> = match import.exposing { + Exposing::Open => None, + Exposing::Explicit(names) => Some( + names + .iter() + .filter_map(|e| match e { + Exposed::Upper { name, .. } | Exposed::LowerType { name, .. } => { + Some(name.value.to_owned()) + } + Exposed::Lower(_) | Exposed::Operator { .. } => None, + }) + .collect(), + ), + }; + if import.import.value != "Builtin" { + match &exposing { + None => self.bare_primitives.clear(), + Some(names) => self.bare_primitives.retain(|name| !names.contains(name)), + } + } + self.imports.insert( + import.import.value.into(), + ImportInfo { + alias: import.alias.map(str::to_owned), + exposing, + }, + ); + } + pub fn to_string(&self, home: ModuleName<'_>, name: &str) -> String { + if home == nash_ast::primitives::builtin_home() + && nash_ast::primitives::PRIMITIVES + .iter() + .any(|primitive| primitive.name == name) + && self.local_module.is_some() + { + return if self.bare_primitives.contains(name) { + name.to_owned() + } else { + format!("Builtin.{name}") + }; + } + match self.imports.get(home.name) { + Some(ImportInfo { exposing: None, .. }) => name.into(), + Some(ImportInfo { + alias, + exposing: Some(names), + }) => { + if names.contains(name) { + name.into() + } else { + format!("{}.{name}", alias.as_deref().unwrap_or(home.name)) + } + } + None => format!("{}.{name}", home.name), + } + } + pub fn to_doc(&self, home: ModuleName<'_>, name: &str) -> Doc { + Doc::text(self.to_string(home, name)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn explicit_types_and_alias() { + use nash_region::Located; + let name = Located::at_zero("Other"); + let little = Located::at_zero("little"); + let exposed = Exposed::LowerType { + name: &little, + privacy: nash_source::Privacy::Private, + }; + let entries = [&exposed]; + let exposing = Exposing::Explicit(&entries); + let import = Import { + import: &name, + alias: Some("O"), + exposing: &exposing, + }; + let mut localizer = Localizer::default(); + localizer.add_import(&import); + let home = ModuleName { + package: None, + name: "Other", + }; + insta::assert_snapshot!(localizer.to_string(home,"little"), @"little"); + insta::assert_snapshot!(localizer.to_string(home,"Thing"), @"O.Thing"); + } + #[test] + fn names() { + let home = ModuleName { + package: None, + name: "Other", + }; + insta::assert_snapshot!(Localizer::from_names(["Other"]).to_string(home,"Thing"), @"Thing"); + insta::assert_snapshot!(Localizer::default().to_string(home,"Thing"), @"Other.Thing"); + } + #[test] + fn source_module_does_not_invent_default_imports() { + let arena = bumpalo::Bump::new(); + let module = nash_parse::Parser::new(&arena, b"module Local exposing (..)\nx = 1\n") + .module() + .unwrap(); + let localizer = Localizer::from_module(&module, &[]); + insta::assert_snapshot!(localizer.to_string(ModuleName{package:None,name:"Local"},"Own"), @"Own"); + insta::assert_snapshot!(localizer.to_string(nash_ast::primitives::builtin_home(),"Int"), @"Int"); + } + #[test] + fn local_unions_are_not_imported_aliases_or_other_packages() { + let bump = bumpalo::Bump::new(); + let module = nash_parse::Parser::new( + &bump, + b"module Local exposing (..)\ntype Token = Token\ntype alias Wrapper = Token\n", + ) + .module() + .unwrap(); + let localizer = Localizer::from_module(&module, &[]); + let home = ModuleName { + package: None, + name: "Local", + }; + assert!(localizer.is_local_union(home, "Token")); + assert!(!localizer.is_local_union(home, "Wrapper")); + assert!(!localizer.is_local_union( + ModuleName { + name: "Other", + ..home + }, + "Token" + )); + assert!(!localizer.is_local_union( + ModuleName { + package: Some(nash_ast::primitives::CORE), + ..home + }, + "Token" + )); + } + + #[test] + fn shadowed_primitive_keeps_qualified_builtin_identity() { + let bump = bumpalo::Bump::new(); + let module = + nash_parse::Parser::new(&bump, b"module Local exposing (..)\ntype Int = Custom\n") + .module() + .unwrap(); + let localizer = Localizer::from_module(&module, &[]); + assert_eq!( + localizer.to_string(nash_ast::primitives::builtin_home(), "Int"), + "Builtin.Int" + ); + assert_eq!( + localizer.to_string(nash_ast::primitives::builtin_home(), "unit"), + "unit" + ); + assert_eq!( + localizer.to_string( + ModuleName { + package: None, + name: "Local" + }, + "Int" + ), + "Int" + ); + } +} diff --git a/crates/nash-report/src/render_type.rs b/crates/nash-report/src/render_type.rs new file mode 100644 index 00000000..18ad64a8 --- /dev/null +++ b/crates/nash-report/src/render_type.rs @@ -0,0 +1,338 @@ +//! Structural source and canonical type rendering, following Elm's Render.Type. +use crate::{doc::Doc, localizer::Localizer}; +use nash_region::Located; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ctx { + None, + Func, + App, +} +pub type Context = Ctx; +fn parens(doc: Doc) -> Doc { + Doc::cat([Doc::text("("), doc, Doc::text(")")]) +} +pub fn variable(name: &str) -> Doc { + Doc::text(format!("'{}", name.trim_start_matches('\''))) +} +pub fn lambda(ctx: Ctx, a: Doc, b: Doc, rest: Vec) -> Doc { + let doc = Doc::align(Doc::sep( + std::iter::once(a).chain( + std::iter::once(b) + .chain(rest) + .map(|d| Doc::hsep([Doc::text("->"), d])), + ), + )); + if ctx == Ctx::None { doc } else { parens(doc) } +} +pub fn apply(ctx: Ctx, name: Doc, args: Vec) -> Doc { + if args.is_empty() { + return name; + } + let doc = Doc::hang(4, Doc::sep(std::iter::once(name).chain(args))); + if ctx == Ctx::App { parens(doc) } else { doc } +} +pub fn tuple(a: Doc, b: Doc, rest: Vec) -> Doc { + let entries = std::iter::once(a) + .chain(std::iter::once(b)) + .chain(rest) + .enumerate() + .map(|(i, d)| Doc::hsep([Doc::text(if i == 0 { "(" } else { "," }), d])); + Doc::align(Doc::sep([Doc::cat(entries), Doc::text(")")])) +} +fn entry((name, typ): (Doc, Doc)) -> Doc { + Doc::hang(4, Doc::sep([Doc::hsep([name, Doc::text(":")]), typ])) +} +fn record_docs(entries: Vec<(Doc, Doc)>, ext: Option, vertical: bool) -> Doc { + if entries.is_empty() && ext.is_none() { + return Doc::text("{}"); + } + let fields: Vec<_> = entries + .into_iter() + .map(entry) + .enumerate() + .map(|(i, d)| { + Doc::hsep([ + Doc::text(if i == 0 { + if ext.is_some() { "|" } else { "{" } + } else { + "," + }), + d, + ]) + }) + .collect(); + let body = if let Some(ext) = ext { + Doc::hang( + 4, + Doc::sep([Doc::hsep([Doc::text("{"), ext]), Doc::cat(fields)]), + ) + } else if vertical { + Doc::vcat(fields) + } else { + Doc::cat(fields) + }; + if vertical { + Doc::vcat([body, Doc::text("}")]) + } else { + Doc::align(Doc::sep([body, Doc::text("}")])) + } +} +pub fn record(entries: Vec<(Doc, Doc)>, ext: Option) -> Doc { + record_docs(entries, ext, false) +} +pub fn vrecord(entries: Vec<(Doc, Doc)>, ext: Option) -> Doc { + record_docs(entries, ext, true) +} +pub fn vrecord_snippet(first: (Doc, Doc), rest: Vec<(Doc, Doc)>) -> Doc { + Doc::vcat( + std::iter::once(Doc::hsep([Doc::text("{"), entry(first)])) + .chain( + rest.into_iter() + .map(|e| Doc::hsep([Doc::text(","), entry(e)])), + ) + .chain([Doc::text(", ..."), Doc::text("}")]), + ) +} +pub fn src_to_doc(ctx: Ctx, typ: &Located>) -> Doc { + use nash_source::Type::*; + match &typ.value { + Repr { typ, repr } => { + let annotation = match repr.value { + nash_source::Repr::Big => "Big", + nash_source::Repr::Const => "Const", + nash_source::Repr::Term => "Term", + nash_source::Repr::Storable => "Storable", + }; + parens(Doc::hsep([ + src_to_doc(Ctx::None, typ), + Doc::text(":"), + Doc::text(annotation), + ])) + } + Lambda { from, to } => { + let mut parts = vec![src_to_doc(Ctx::Func, from)]; + let mut last = *to; + while let Lambda { from, to } = &last.value { + parts.push(src_to_doc(Ctx::Func, from)); + last = to; + } + parts.push(src_to_doc(Ctx::Func, last)); + let a = parts.remove(0); + let b = parts.remove(0); + lambda(ctx, a, b, parts) + } + Var(name) => variable(name), + VarApp { name, args, .. } => apply( + ctx, + variable(name), + args.iter().map(|t| src_to_doc(Ctx::App, t)).collect(), + ), + Type { name, args, .. } => apply( + ctx, + Doc::text(*name), + args.iter().map(|t| src_to_doc(Ctx::App, t)).collect(), + ), + TypeQual { + module, name, args, .. + } => apply( + ctx, + Doc::text(format!("{module}.{name}")), + args.iter().map(|t| src_to_doc(Ctx::App, t)).collect(), + ), + Record(fields) => record( + fields + .iter() + .map(|f| (Doc::text(f.field.value), src_to_doc(Ctx::None, f.typ))) + .collect(), + None, + ), + Unit => Doc::text("()"), + Tuple { + first, + second, + rest, + } => tuple( + src_to_doc(Ctx::None, first), + src_to_doc(Ctx::None, second), + rest.iter().map(|t| src_to_doc(Ctx::None, t)).collect(), + ), + } +} +pub fn can_to_doc(localizer: &Localizer, ctx: Ctx, typ: &nash_ast::Type<'_>) -> Doc { + use nash_ast::Type::*; + match typ { + Lambda { from, to } => { + let mut parts = vec![can_to_doc(localizer, Ctx::Func, &from.value)]; + let mut last = &to.value; + while let Lambda { from, to } = last { + parts.push(can_to_doc(localizer, Ctx::Func, &from.value)); + last = &to.value; + } + parts.push(can_to_doc(localizer, Ctx::Func, last)); + let a = parts.remove(0); + let b = parts.remove(0); + lambda(ctx, a, b, parts) + } + Var(name) => variable(name), + App { head, args } => apply( + ctx, + can_to_doc(localizer, Ctx::App, &head.value), + args.iter() + .map(|t| can_to_doc(localizer, Ctx::App, &t.value)) + .collect(), + ), + Named { reference, args } => apply( + ctx, + localizer.to_doc(reference.home, reference.name), + args.iter() + .map(|t| can_to_doc(localizer, Ctx::App, &t.value)) + .collect(), + ), + Record { fields } => { + let mut fields: Vec<_> = fields.iter().collect(); + fields.sort_by_key(|f| f.index); + record( + fields + .into_iter() + .map(|f| { + ( + Doc::text(f.field), + can_to_doc(localizer, Ctx::None, &f.typ.value), + ) + }) + .collect(), + None, + ) + } + Tuple { + first, + second, + rest, + } => tuple( + can_to_doc(localizer, Ctx::None, &first.value), + can_to_doc(localizer, Ctx::None, &second.value), + rest.iter() + .map(|t| can_to_doc(localizer, Ctx::None, &t.value)) + .collect(), + ), + Alias { + reference, + arguments, + .. + } => apply( + ctx, + localizer.to_doc(reference.home, reference.name), + arguments + .iter() + .map(|a| can_to_doc(localizer, Ctx::App, &a.typ.value)) + .collect(), + ), + } +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn function_parentheses() { + insta::assert_snapshot!(lambda(Ctx::App,variable("a"),variable("b"),vec![]).render(80,false), @"('a -> 'b)"); + } + #[test] + fn narrow_application() { + insta::assert_snapshot!(apply(Ctx::None,Doc::text("Container"),vec![Doc::text("LongArgument")]).render(12,false), @r" +Container + LongArgument +"); + } + #[test] + fn vertical_snippet() { + insta::assert_snapshot!(vrecord_snippet((Doc::text("x"),variable("a")),vec![]).render(80,false), @r" +{ x : 'a +, ... +} +"); + } + #[test] + fn source_type_shapes() { + use nash_source::{Repr, Type}; + let a = Located::at_zero(Type::Var("a")); + let b = Located::at_zero(Type::TypeQual { + region: nash_region::Region::zero(), + module: "A", + name: "Thing", + args: &[], + }); + let func = Located::at_zero(Type::Lambda { from: &a, to: &b }); + let args = [&func]; + let app = Located::at_zero(Type::VarApp { + region: nash_region::Region::zero(), + name: "f", + args: &args, + }); + insta::assert_snapshot!(src_to_doc(Ctx::None,&app).render(80,false), @"'f ('a -> A.Thing)"); + let repr = Located::at_zero(Repr::Big); + let annotated = Located::at_zero(Type::Repr { + typ: &a, + repr: &repr, + }); + insta::assert_snapshot!(src_to_doc(Ctx::None,&annotated).render(80,false), @"('a : Big)"); + let unit = Located::at_zero(Type::Unit); + insta::assert_snapshot!(src_to_doc(Ctx::None,&unit).render(80,false), @"()"); + } + #[test] + fn source_record_and_tuple() { + use nash_source::{FieldType, Type}; + let a = Located::at_zero(Type::Var("a")); + let name = Located::at_zero("field"); + let field = FieldType { + field: &name, + typ: &a, + }; + let fields = [&field]; + let record = Located::at_zero(Type::Record(&fields)); + insta::assert_snapshot!(src_to_doc(Ctx::None,&record).render(80,false), @"{ field : 'a }"); + let rest = [&a, &a]; + let tuple = Located::at_zero(Type::Tuple { + first: &a, + second: &a, + rest: &rest, + }); + insta::assert_snapshot!(src_to_doc(Ctx::None,&tuple).render(80,false), @"( 'a, 'a, 'a, 'a )"); + } + #[test] + fn canonical_record_preserves_declaration_order() { + use nash_ast::{FieldType, Type}; + let a = Located::at_zero(Type::Var("a")); + let fields = [ + FieldType { + index: 1, + field: "first", + typ: &a, + }, + FieldType { + index: 0, + field: "zebra", + typ: &a, + }, + ]; + insta::assert_snapshot!(can_to_doc(&Localizer::default(),Ctx::None,&Type::Record{fields:&fields}).render(80,false), @"{ zebra : 'a, first : 'a }"); + } + #[test] + fn canonical_alias_keeps_public_name() { + use nash_ast::{AliasArgument, AliasType, ModuleName, QualifiedName, Type}; + let a = Located::at_zero(Type::Var("a")); + let args = [AliasArgument { name: "a", typ: &a }]; + let alias = Type::Alias { + reference: QualifiedName { + home: ModuleName { + package: None, + name: "A", + }, + name: "Box", + }, + arguments: &args, + remaining: &[], + target: AliasType::Open(&a), + }; + insta::assert_snapshot!(can_to_doc(&Localizer::from_names(["A"]),Ctx::App,&alias).render(80,false), @"(Box 'a)"); + } +} diff --git a/crates/nash-report/src/type_diff.rs b/crates/nash-report/src/type_diff.rs new file mode 100644 index 00000000..36e0a694 --- /dev/null +++ b/crates/nash-report/src/type_diff.rs @@ -0,0 +1,836 @@ +//! Structural differences and hint evidence for solved error types. +use crate::{ + doc::Doc, + localizer::Localizer, + render_type::{self as rt, Ctx}, +}; +use nash_ast::{ModuleName, primitives}; +use nash_constrain::error_type::{ErrorType, iterated_dealias}; +use std::collections::BTreeMap; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Direction { + Have, + Need, +} +#[derive(Clone, Debug)] +pub enum Problem<'a> { + AnythingToBool, + AnythingFromOption, + ArityMismatch(usize, usize), + BadRigidVar(&'a str, &'a ErrorType<'a>), + FieldTypo(&'a str, Vec<&'a str>), + FieldsMissing(Vec<&'a str>), + BigLittle { + big: &'a str, + little: &'a str, + direction: Direction, + }, +} +#[derive(Clone, Debug)] +pub enum Status<'a> { + Similar, + Different(Vec>), +} +#[derive(Clone, Debug)] +pub struct Diff<'a, T> { + pub left: T, + pub right: T, + pub status: Status<'a>, +} +pub fn merge<'a>(a: Status<'a>, b: Status<'a>) -> Status<'a> { + match (a, b) { + (Status::Similar, s) | (s, Status::Similar) => s, + (Status::Different(mut a), Status::Different(b)) => { + a.extend(b); + Status::Different(a) + } + } +} +pub fn to_doc(l: &Localizer, ctx: Ctx, t: &ErrorType<'_>) -> Doc { + match t { + ErrorType::VarApp(head, args) => rt::apply( + ctx, + to_doc(l, Ctx::App, head), + args.iter().map(|t| to_doc(l, Ctx::App, t)).collect(), + ), + ErrorType::Lambda(a, b, cs) => rt::lambda( + ctx, + to_doc(l, Ctx::Func, a), + to_doc(l, Ctx::Func, b), + cs.iter().map(|t| to_doc(l, Ctx::Func, t)).collect(), + ), + ErrorType::Infinite => Doc::text("∞"), + ErrorType::Error => Doc::text("?"), + ErrorType::FlexVar(n) | ErrorType::RigidVar(n) => rt::variable(n), + ErrorType::Type { home, name, args } => rt::apply( + ctx, + l.to_doc(*home, name), + args.iter().map(|t| to_doc(l, Ctx::App, t)).collect(), + ), + ErrorType::Record { fields } => rt::record(fields_to_docs(l, fields), None), + ErrorType::Tuple(a, b, cs) => rt::tuple( + to_doc(l, Ctx::None, a), + to_doc(l, Ctx::None, b), + cs.iter().map(|t| to_doc(l, Ctx::None, t)).collect(), + ), + ErrorType::Alias { + home, name, args, .. + } => alias_to_doc(l, ctx, *home, name, args), + } +} +pub fn alias_to_doc( + l: &Localizer, + ctx: Ctx, + home: ModuleName<'_>, + name: &str, + args: &[(&str, &ErrorType<'_>)], +) -> Doc { + rt::apply( + ctx, + l.to_doc(home, name), + args.iter().map(|(_, t)| to_doc(l, Ctx::App, t)).collect(), + ) +} +pub fn fields_to_docs(l: &Localizer, fields: &[(&str, &ErrorType<'_>)]) -> Vec<(Doc, Doc)> { + let sorted: BTreeMap<_, _> = fields.iter().copied().collect(); + sorted + .into_iter() + .map(|(n, t)| (Doc::text(n), to_doc(l, Ctx::None, t))) + .collect() +} +pub fn to_comparison<'a>( + l: &Localizer, + a: &'a ErrorType<'a>, + b: &'a ErrorType<'a>, +) -> (Doc, Doc, Vec>) { + let d = to_diff(l, Ctx::None, a, b); + ( + d.left, + d.right, + match d.status { + Status::Similar => vec![], + Status::Different(p) => p, + }, + ) +} +fn different<'a>(left: Doc, right: Doc, problems: Vec>) -> Diff<'a, Doc> { + Diff { + left, + right, + status: Status::Different(problems), + } +} +pub fn is_similar(d: &Diff<'_, T>) -> bool { + matches!(d.status, Status::Similar) +} +fn similar<'a>(l: &Localizer, c: Ctx, a: &ErrorType<'_>, b: &ErrorType<'_>) -> Diff<'a, Doc> { + Diff { + left: to_doc(l, c, a), + right: to_doc(l, c, b), + status: Status::Similar, + } +} +fn sequence<'a>(diffs: impl IntoIterator>) -> Diff<'a, Vec> { + let mut left = vec![]; + let mut right = vec![]; + let mut status = Status::Similar; + for d in diffs { + left.push(d.left); + right.push(d.right); + status = merge(status, d.status); + } + Diff { + left, + right, + status, + } +} +fn apply_diff<'a>( + l: &Localizer, + c: Ctx, + head: Doc, + a: &[&'a ErrorType<'a>], + b: &[&'a ErrorType<'a>], +) -> Diff<'a, Doc> { + let d = sequence(a.iter().zip(b).map(|(a, b)| to_diff(l, Ctx::App, a, b))); + Diff { + left: rt::apply(c, head.clone(), d.left), + right: rt::apply(c, head, d.right), + status: d.status, + } +} +fn builtin(home: ModuleName<'_>, name: &str, want: &str) -> bool { + home == primitives::builtin_home() && name == want +} +pub fn is_bool(h: ModuleName<'_>, n: &str) -> bool { + builtin(h, n, "bool") +} +pub fn is_int(h: ModuleName<'_>, n: &str) -> bool { + builtin(h, n, "int") +} +pub fn is_string(h: ModuleName<'_>, n: &str) -> bool { + builtin(h, n, "string") +} +pub fn is_list(h: ModuleName<'_>, n: &str) -> bool { + builtin(h, n, "list") +} +/// `option` is a library type, not a compiler primitive. +pub fn is_option(h: ModuleName<'_>, n: &str) -> bool { + h.package == Some(primitives::CORE) && h.name == "Option" && n == "option" +} +fn named<'a>(t: &'a ErrorType<'a>) -> Option<(ModuleName<'a>, &'a str, Vec<&'a ErrorType<'a>>)> { + match t { + ErrorType::Type { home, name, args } => Some((*home, name, args.to_vec())), + ErrorType::Alias { + home, name, args, .. + } => Some((*home, name, args.iter().map(|(_, t)| *t).collect())), + _ => None, + } +} +fn name_clash(l: &Localizer, c: Ctx, h: ModuleName<'_>, n: &str, args: &[&ErrorType<'_>]) -> Doc { + let module = if let Some(p) = h.package { + format!("{}/{}.{}", p.author, p.project, h.name) + } else { + h.name.into() + }; + rt::apply( + c, + Doc::cat([ + Doc::text(module).yellow(), + Doc::text(format!(".{n}")).dullyellow(), + ]), + args.iter().map(|t| to_doc(l, Ctx::App, t)).collect(), + ) +} +fn is_map(t: &ErrorType<'_>) -> bool { + matches!(t,ErrorType::Type{home,name:"Map",args} if *home==primitives::builtin_home()&&args.len()==2) +} +fn is_map_little(t: &ErrorType<'_>) -> bool { + matches!(t,ErrorType::Type{home,name:"list",args:[ErrorType::Type{home:pair_home,name:"pair",args}]} if *home==primitives::builtin_home()&&*pair_home==primitives::builtin_home()&&args.len()==2) +} +/// The producer retains transparent alias chains. The alias directly around +/// the record owns its nominal identity and representation (kinds::record_repr). +fn nominal_record<'a>(mut typ: &'a ErrorType<'a>) -> Option<(ModuleName<'a>, &'a str)> { + while let ErrorType::Alias { + home, name, real, .. + } = typ + { + if matches!(real, ErrorType::Record { .. }) { + return Some((*home, *name)); + } + typ = real; + } + None +} +pub fn to_diff<'a>( + l: &Localizer, + c: Ctx, + a: &'a ErrorType<'a>, + b: &'a ErrorType<'a>, +) -> Diff<'a, Doc> { + use ErrorType::*; + match (a, b) { + (Error, Error) | (Infinite, Infinite) => return similar(l, c, a, b), + (RigidVar(x), RigidVar(y)) if x == y => return similar(l, c, a, b), + (FlexVar(_), _) | (_, FlexVar(_)) => return similar(l, c, a, b), + (Lambda(x, y, z), Lambda(u, v, w)) | (Tuple(x, y, z), Tuple(u, v, w)) + if z.len() == w.len() => + { + let func = matches!(a, Lambda(..)); + let cx = if func { Ctx::Func } else { Ctx::None }; + let d = sequence( + std::iter::once((*x, *u)) + .chain([(*y, *v)]) + .chain(z.iter().copied().zip(w.iter().copied())) + .map(|(a, b)| to_diff(l, cx, a, b)), + ); + let render = |mut p: Vec| { + let a = p.remove(0); + let b = p.remove(0); + if func { + rt::lambda(c, a, b, p) + } else { + rt::tuple(a, b, p) + } + }; + return Diff { + left: render(d.left), + right: render(d.right), + status: d.status, + }; + } + (Lambda(_, _, z), Lambda(_, _, w)) => { + return different( + to_doc(l, c, a).dullyellow(), + to_doc(l, c, b).dullyellow(), + vec![Problem::ArityMismatch(1 + z.len(), 1 + w.len())], + ); + } + (Record { fields: x }, Record { fields: y }) => return diff_record(l, x, y), + (VarApp(x, xs), VarApp(y, ys)) if xs.len() == ys.len() => { + let head = to_diff(l, Ctx::App, x, y); + let args = sequence(xs.iter().zip(*ys).map(|(a, b)| to_diff(l, Ctx::App, a, b))); + return Diff { + left: rt::apply(c, head.left, args.left), + right: rt::apply(c, head.right, args.right), + status: merge(head.status, args.status), + }; + } + _ => {} + } + if let (Some((h, n, x)), Some((j, m, y))) = (named(a), named(b)) { + if h == j && n == m && x.len() == y.len() { + return apply_diff(l, c, l.to_doc(h, n), &x, &y); + } + if l.to_string(h, n) == l.to_string(j, m) && (h != j || n != m) { + return different( + name_clash(l, c, h, n, &x), + name_clash(l, c, j, m, &y), + vec![], + ); + } + if h == primitives::builtin_home() + && j == h + && matches!( + (n, m), + ("Int", "int") + | ("int", "Int") + | ("Bytes", "bytes") + | ("bytes", "Bytes") + | ("List", "list") + | ("list", "List") + ) + { + let (big, little, direction) = if n.as_bytes()[0].is_ascii_uppercase() { + (n, m, Direction::Have) + } else { + (m, n, Direction::Need) + }; + let problem = Problem::BigLittle { + big, + little, + direction, + }; + if x.len() == y.len() { + let args = sequence(x.iter().zip(&y).map(|(a, b)| to_diff(l, Ctx::App, a, b))); + return Diff { + left: rt::apply(c, l.to_doc(h, n).dullyellow(), args.left), + right: rt::apply(c, l.to_doc(j, m).dullyellow(), args.right), + status: merge(Status::Different(vec![problem]), args.status), + }; + } + return different( + to_doc(l, c, a).dullyellow(), + to_doc(l, c, b).dullyellow(), + vec![problem], + ); + } + } + if (is_map(a) && is_map_little(b)) || (is_map(b) && is_map_little(a)) { + return different( + to_doc(l, c, a).dullyellow(), + to_doc(l, c, b).dullyellow(), + vec![Problem::BigLittle { + big: "Map", + little: "list (pair 'k 'v)", + direction: if is_map(a) { + Direction::Have + } else { + Direction::Need + }, + }], + ); + } + if let Type { + home, + name, + args: [inner], + } = a + && is_option(*home, name) + && is_similar(&to_diff(l, c, inner, b)) + { + return different( + rt::apply( + c, + l.to_doc(*home, name).dullyellow(), + vec![to_doc(l, Ctx::App, inner)], + ), + to_doc(l, c, b), + vec![Problem::AnythingFromOption], + ); + } + if let Type { + home, + name, + args: [inner], + } = b + && is_list(*home, name) + && is_similar(&to_diff(l, c, a, inner)) + { + return different( + to_doc(l, c, a), + rt::apply( + c, + l.to_doc(*home, name).dullyellow(), + vec![to_doc(l, Ctx::App, inner)], + ), + vec![], + ); + } + if (matches!(a, Alias { .. }) || matches!(b, Alias { .. })) + && let (Record { fields: x }, Record { fields: y }) = + (iterated_dealias(a), iterated_dealias(b)) + { + let mut d = diff_record(l, x, y); + let left_identity = nominal_record(a); + let right_identity = nominal_record(b); + if left_identity != right_identity { + let mut problems = vec![]; + if let (Some((_, left)), Some((_, right))) = (left_identity, right_identity) { + let left_big = left.chars().next().is_some_and(char::is_uppercase); + let right_big = right.chars().next().is_some_and(char::is_uppercase); + let left_fields: std::collections::BTreeSet<_> = + x.iter().map(|(name, _)| *name).collect(); + let right_fields: std::collections::BTreeSet<_> = + y.iter().map(|(name, _)| *name).collect(); + if left_big != right_big && left_fields == right_fields { + let (big, little, direction) = if left_big { + (left, right, Direction::Have) + } else { + (right, left, Direction::Need) + }; + problems.push(Problem::BigLittle { + big, + little, + direction, + }); + } + } + d.status = merge(Status::Different(problems), d.status); + } + if matches!(a, Alias { .. }) { + d.left = to_doc(l, c, a).dullyellow() + } + if matches!(b, Alias { .. }) { + d.right = to_doc(l, c, b).dullyellow() + } + return d; + } + let problems = match (a, b) { + (RigidVar(n), t) | (t, RigidVar(n)) => vec![Problem::BadRigidVar(n, t)], + (_, Type { home, name, args }) if args.is_empty() && is_bool(*home, name) => { + vec![Problem::AnythingToBool] + } + _ => vec![], + }; + different( + to_doc(l, c, a).dullyellow(), + to_doc(l, c, b).dullyellow(), + problems, + ) +} +fn diff_record<'a>( + l: &Localizer, + a: &[(&'a str, &'a ErrorType<'a>)], + b: &[(&'a str, &'a ErrorType<'a>)], +) -> Diff<'a, Doc> { + let a: BTreeMap<_, _> = a.iter().copied().collect(); + let b: BTreeMap<_, _> = b.iter().copied().collect(); + let mut left = BTreeMap::new(); + let mut right = BTreeMap::new(); + let mut status = Status::Similar; + for (n, t) in &a { + if let Some(u) = b.get(n) { + let d = to_diff(l, Ctx::None, t, u); + left.insert(*n, (Doc::text(*n), d.left)); + right.insert(*n, (Doc::text(*n), d.right)); + status = merge(status, d.status); + } else { + left.insert(*n, (Doc::text(*n).dullyellow(), to_doc(l, Ctx::None, t))); + } + } + for (n, t) in &b { + if !a.contains_key(n) { + right.insert(*n, (Doc::text(*n).dullyellow(), to_doc(l, Ctx::None, t))); + } + } + if let Some(n) = a.keys().find(|n| !b.contains_key(*n)) { + status = merge( + status, + Status::Different(vec![Problem::FieldTypo(n, b.keys().copied().collect())]), + ) + } else { + let missing: Vec<_> = b.keys().filter(|n| !a.contains_key(*n)).copied().collect(); + if !missing.is_empty() { + status = merge( + status, + Status::Different(vec![Problem::FieldsMissing(missing)]), + ) + } + } + Diff { + left: rt::record(left.into_values().collect(), None), + right: rt::record(right.into_values().collect(), None), + status, + } +} +#[cfg(test)] +mod tests { + use super::*; + fn typ(n: &str) -> ErrorType<'_> { + ErrorType::Type { + home: primitives::builtin_home(), + name: n, + args: &[], + } + } + #[test] + fn nested_big_little() { + let a = typ("Int"); + let b = typ("int"); + let aa = [&a]; + let bb = [&b]; + let x = ErrorType::Type { + home: primitives::builtin_home(), + name: "list", + args: &aa, + }; + let y = ErrorType::Type { + home: primitives::builtin_home(), + name: "list", + args: &bb, + }; + let (a, b, p) = to_comparison(&Localizer::from_names(["Builtin"]), &x, &y); + insta::assert_snapshot!(a.render(80,true), @"list \u{1b}[33mInt\u{1b}[0m"); + insta::assert_snapshot!(b.render(80,false), @"list int"); + assert!(matches!( + p.as_slice(), + [Problem::BigLittle { + direction: Direction::Have, + .. + }] + )); + } + #[test] + fn missing_field() { + let t = typ("int"); + let a = ErrorType::Record { fields: &[] }; + let fs = [("x", &t)]; + let b = ErrorType::Record { fields: &fs }; + let (_, b, p) = to_comparison(&Localizer::from_names(["Builtin"]), &a, &b); + insta::assert_snapshot!(b.render(80,false), @"{ x : int }"); + assert!(matches!(p.as_slice(),[Problem::FieldsMissing(f)] if f==&["x"])); + } + #[test] + fn flexible_is_similar() { + let a = ErrorType::FlexVar("a"); + let b = typ("int"); + assert!(is_similar(&to_diff( + &Localizer::default(), + Ctx::None, + &a, + &b + ))); + } + #[test] + fn application_arity_not_truncated() { + let t = typ("int"); + let xs = [&t]; + let a = ErrorType::Type { + home: primitives::builtin_home(), + name: "list", + args: &xs, + }; + let b = typ("list"); + let d = to_diff(&Localizer::from_names(["Builtin"]), Ctx::None, &a, &b); + assert!(!is_similar(&d)); + insta::assert_snapshot!(d.left.render(80,false), @"list int"); + } + #[test] + fn rigid_variable_hint() { + let a = ErrorType::RigidVar("a"); + let b = typ("int"); + let (doc, _, p) = to_comparison(&Localizer::default(), &a, &b); + insta::assert_snapshot!(doc.render(80,false), @"'a"); + assert!(matches!(p.as_slice(), [Problem::BadRigidVar("a", _)])); + } + #[test] + fn bool_requires_exact_builtin_identity() { + let a = typ("int"); + let b = typ("bool"); + assert!(matches!( + to_comparison(&Localizer::default(), &a, &b).2.as_slice(), + [Problem::AnythingToBool] + )); + let b = ErrorType::Type { + home: ModuleName { + package: None, + name: "User", + }, + name: "bool", + args: &[], + }; + assert!(to_comparison(&Localizer::default(), &a, &b).2.is_empty()); + } + #[test] + fn function_argument_counts() { + let t = typ("int"); + let rest = [&t]; + let a = ErrorType::Lambda(&t, &t, &[]); + let b = ErrorType::Lambda(&t, &t, &rest); + assert!(matches!( + to_comparison(&Localizer::default(), &a, &b).2.as_slice(), + [Problem::ArityMismatch(1, 2)] + )); + } + #[test] + fn tuple_arity_is_preserved() { + let t = typ("int"); + let rest = [&t, &t]; + let a = ErrorType::Tuple(&t, &t, &rest); + let b = ErrorType::Tuple(&t, &t, &[]); + let d = to_diff(&Localizer::from_names(["Builtin"]), Ctx::None, &a, &b); + insta::assert_snapshot!(d.left.render(80,false), @"( int, int, int, int )"); + assert!(!is_similar(&d)); + } + #[test] + fn field_typo_and_overlapping_type_diff() { + let a = typ("Int"); + let b = typ("int"); + let xs = [("z", &a), ("naem", &a)]; + let ys = [("name", &b), ("z", &b)]; + let x = ErrorType::Record { fields: &xs }; + let y = ErrorType::Record { fields: &ys }; + let (a, b, p) = to_comparison(&Localizer::from_names(["Builtin"]), &x, &y); + insta::assert_snapshot!(a.render(80,false), @"{ naem : Int, z : Int }"); + insta::assert_snapshot!(b.render(80,false), @"{ name : int, z : int }"); + assert!(matches!( + p.as_slice(), + [Problem::BigLittle { .. }, Problem::FieldTypo("naem", _)] + )); + } + #[test] + fn name_clash_qualifies_modules() { + let a = ErrorType::Type { + home: ModuleName { + package: None, + name: "A", + }, + name: "Thing", + args: &[], + }; + let b = ErrorType::Type { + home: ModuleName { + package: None, + name: "B", + }, + name: "Thing", + args: &[], + }; + let (a, b, _) = to_comparison(&Localizer::from_names(["A", "B"]), &a, &b); + insta::assert_snapshot!(a.render(80,false), @"A.Thing"); + insta::assert_snapshot!(b.render(80,false), @"B.Thing"); + } + #[test] + fn alias_record_keeps_alias_and_field_hint() { + let t = typ("int"); + let fs = [("x", &t)]; + let a = ErrorType::Record { fields: &[] }; + let b = ErrorType::Record { fields: &fs }; + let alias = ErrorType::Alias { + home: primitives::builtin_home(), + name: "Empty", + args: &[], + real: &a, + }; + let (a, _, p) = to_comparison(&Localizer::from_names(["Builtin"]), &alias, &b); + insta::assert_snapshot!(a.render(80,false), @"Empty"); + assert!(matches!(p.as_slice(), [Problem::FieldsMissing(_)])); + } + #[test] + fn higher_kinded_application_diff() { + let f = ErrorType::RigidVar("f"); + let a = typ("Int"); + let b = typ("int"); + let xs = [&a]; + let ys = [&b]; + let x = ErrorType::VarApp(&f, &xs); + let y = ErrorType::VarApp(&f, &ys); + let (a, _, p) = to_comparison(&Localizer::from_names(["Builtin"]), &x, &y); + insta::assert_snapshot!(a.render(80,false), @"'f Int"); + assert!(matches!(p.as_slice(), [Problem::BigLittle { .. }])); + } + #[test] + fn sentinels_render_explicitly() { + let l = Localizer::default(); + insta::assert_snapshot!(to_doc(&l,Ctx::None,&ErrorType::Infinite).render(80,false), @"∞"); + insta::assert_snapshot!(to_doc(&l,Ctx::None,&ErrorType::Error).render(80,false), @"?"); + } + #[test] + fn map_representation_hint_requires_list_of_pairs() { + let t = typ("Data"); + let args = [&t, &t]; + let map = ErrorType::Type { + home: primitives::builtin_home(), + name: "Map", + args: &args, + }; + let pair = ErrorType::Type { + home: primitives::builtin_home(), + name: "pair", + args: &args, + }; + let list_args = [&pair]; + let list = ErrorType::Type { + home: primitives::builtin_home(), + name: "list", + args: &list_args, + }; + assert!(matches!( + to_comparison(&Localizer::default(), &map, &list) + .2 + .as_slice(), + [Problem::BigLittle { + big: "Map", + direction: Direction::Have, + .. + }] + )); + assert!( + to_comparison(&Localizer::default(), &map, &pair) + .2 + .is_empty() + ); + } + #[test] + fn option_hint_preserves_inner_type() { + let t = typ("int"); + let args = [&t]; + let option = ErrorType::Type { + home: ModuleName { + package: Some(primitives::CORE), + name: "Option", + }, + name: "option", + args: &args, + }; + let (a, b, p) = to_comparison(&Localizer::from_names(["Builtin", "Option"]), &option, &t); + insta::assert_snapshot!(a.render(80,false), @"option int"); + insta::assert_snapshot!(b.render(80,false), @"int"); + assert!(matches!(p.as_slice(), [Problem::AnythingFromOption])); + } + #[test] + fn different_alias_names_do_not_unfold_nonrecords() { + let t = typ("int"); + let alias = ErrorType::Alias { + home: primitives::builtin_home(), + name: "Count", + args: &[], + real: &t, + }; + let d = to_diff(&Localizer::from_names(["Builtin"]), Ctx::None, &alias, &t); + insta::assert_snapshot!(d.left.render(80,false), @"Count"); + assert!(!is_similar(&d)); + } + #[test] + fn big_little_container_highlights_only_changed_head() { + let t = typ("Data"); + let args = [&t]; + let a = ErrorType::Type { + home: primitives::builtin_home(), + name: "List", + args: &args, + }; + let b = ErrorType::Type { + home: primitives::builtin_home(), + name: "list", + args: &args, + }; + let (a, _, _) = to_comparison(&Localizer::from_names(["Builtin"]), &a, &b); + insta::assert_snapshot!(a.render(80,true), @"\u{1b}[33mList\u{1b}[0m Data"); + } + #[test] + fn distinct_nominal_records_are_not_similar() { + let real = ErrorType::Record { fields: &[] }; + let a = ErrorType::Alias { + home: primitives::builtin_home(), + name: "One", + args: &[], + real: &real, + }; + let b = ErrorType::Alias { + home: primitives::builtin_home(), + name: "Two", + args: &[], + real: &real, + }; + assert!(!is_similar(&to_diff( + &Localizer::default(), + Ctx::None, + &a, + &b + ))); + } + #[test] + fn record_representation_uses_defining_alias_not_transparent_name() { + let real = ErrorType::Record { fields: &[] }; + let big = ErrorType::Alias { + home: primitives::builtin_home(), + name: "BigRecord", + args: &[], + real: &real, + }; + let little = ErrorType::Alias { + home: primitives::builtin_home(), + name: "littleRecord", + args: &[], + real: &real, + }; + let outer = ErrorType::Alias { + home: primitives::builtin_home(), + name: "MisleadingUppercase", + args: &[], + real: &little, + }; + let (a, b, p) = to_comparison(&Localizer::from_names(["Builtin"]), &big, &outer); + insta::assert_snapshot!(a.render(80,false), @"BigRecord"); + insta::assert_snapshot!(b.render(80,false), @"MisleadingUppercase"); + assert!(matches!( + p.as_slice(), + [Problem::BigLittle { + big: "BigRecord", + little: "littleRecord", + direction: Direction::Have + }] + )); + assert!(is_similar(&to_diff( + &Localizer::default(), + Ctx::None, + &outer, + &little + ))); + } + #[test] + fn solver_error_producer_retains_record_identity() { + use nash_constrain::type_::make_descriptor; + use nash_constrain::{Content, FlatType, UnionFind}; + let arena = bumpalo::Bump::new(); + let mut uf = UnionFind::new(); + let body = nash_region::Located::at_zero(nash_ast::Type::Record { fields: &[] }); + let real = uf.fresh(make_descriptor(Content::Structure(FlatType::Record1( + BTreeMap::new(), + )))); + let little = uf.fresh(make_descriptor(Content::Alias { + home: primitives::builtin_home(), + name: "littleRecord", + args: vec![], + real, + body: &body, + })); + let produced = nash_solve::to_error_type(&arena, &mut uf, little); + assert_eq!( + nominal_record(produced), + Some((primitives::builtin_home(), "littleRecord")) + ); + } +} From f3536944dedfc32bfa7083cb0ab8fed77ce9b4d8 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 06/12] feat(report): explain syntax errors Signed-off-by: microproofs --- crates/nash-report/src/lib.rs | 1 + crates/nash-report/src/syntax/decl.rs | 644 +++++++ crates/nash-report/src/syntax/expr.rs | 1687 ++++++++++++++++ crates/nash-report/src/syntax/mod.rs | 88 + crates/nash-report/src/syntax/module.rs | 583 ++++++ crates/nash-report/src/syntax/pattern.rs | 307 +++ ...yntax__expr__tests__assert_indentbody.snap | 10 + ...ax__expr__tests__assert_indentmessage.snap | 10 + ...t__syntax__expr__tests__bytes_bad_hex.snap | 10 + ...t__syntax__expr__tests__bytes_endless.snap | 10 + ...eport__syntax__expr__tests__bytes_odd.snap | 11 + ...yntax__expr__tests__case_branch_arrow.snap | 11 + ...pr__tests__case_colon_instead_of_cons.snap | 20 + ...__tests__case_equals_instead_of_arrow.snap | 20 + ...yntax__expr__tests__case_indent_arrow.snap | 20 + ...ntax__expr__tests__case_indent_branch.snap | 21 + ...syntax__expr__tests__case_indent_expr.snap | 20 + ...tax__expr__tests__case_indent_pattern.snap | 20 + ...ntax__expr__tests__case_missing_arrow.snap | 20 + ..._syntax__expr__tests__case_missing_of.snap | 20 + ...__expr__tests__case_pattern_alignment.snap | 20 + ...x__expr__tests__case_reserved_pattern.snap | 21 + ...ntax__expr__tests__case_subject_arrow.snap | 10 + ...syntax__expr__tests__case_wrong_arrow.snap | 20 + ...tax__expr__tests__comptime_indentbody.snap | 10 + ...__expr__tests__comptime_indentmessage.snap | 10 + ...t__syntax__expr__tests__def_alignment.snap | 18 + ...port__syntax__expr__tests__def_equals.snap | 17 + ...yntax__expr__tests__def_indent_equals.snap | 18 + ..._syntax__expr__tests__def_indent_type.snap | 18 + ...yntax__expr__tests__def_missing_colon.snap | 18 + ...yntax__expr__tests__def_name_mismatch.snap | 11 + ..._syntax__expr__tests__def_name_repeat.snap | 19 + ...syntax__expr__tests__def_reserved_arg.snap | 10 + ...ax__expr__tests__destruct_indent_body.snap | 10 + ...__expr__tests__destruct_indent_equals.snap | 11 + ...expr__tests__destruct_type_annotation.snap | 12 + ...rt__syntax__expr__tests__do_alignment.snap | 10 + ...report__syntax__expr__tests__do_arrow.snap | 10 + ..._syntax__expr__tests__do_indent_arrow.snap | 11 + ...__syntax__expr__tests__do_indent_expr.snap | 10 + ...__syntax__expr__tests__do_indent_stmt.snap | 10 + ...ntax__expr__tests__do_requires_result.snap | 11 + ...ntax__expr__tests__escape_bad_unicode.snap | 12 + ...ax__expr__tests__escape_short_unicode.snap | 12 + ...__syntax__expr__tests__escape_unknown.snap | 11 + ...yntax__expr__tests__expr_access_upper.snap | 11 + ...x__expr__tests__expr_dot_without_name.snap | 10 + ...__syntax__expr__tests__expr_start_bad.snap | 13 + ..._syntax__expr__tests__fail_indentbody.snap | 10 + ...ntax__expr__tests__fail_indentmessage.snap | 10 + ..._syntax__expr__tests__func_indent_arg.snap | 11 + ...yntax__expr__tests__func_indent_arrow.snap | 10 + ...syntax__expr__tests__func_indent_body.snap | 11 + ...ntax__expr__tests__func_missing_arrow.snap | 11 + ...yntax__expr__tests__func_reserved_arg.snap | 11 + ...ax__expr__tests__if_else_branch_start.snap | 11 + ...tax__expr__tests__if_indent_condition.snap | 11 + ...__syntax__expr__tests__if_indent_else.snap | 11 + ...x__expr__tests__if_indent_else_branch.snap | 11 + ...__syntax__expr__tests__if_indent_then.snap | 10 + ...x__expr__tests__if_indent_then_branch.snap | 11 + ...tax__expr__tests__if_misindented_else.snap | 11 + ..._syntax__expr__tests__if_missing_then.snap | 10 + ...ort__syntax__expr__tests__integer_dot.snap | 10 + ...yntax__expr__tests__let_def_alignment.snap | 13 + ...tax__expr__tests__let_def_indent_body.snap | 17 + ...rt__syntax__expr__tests__let_def_name.snap | 19 + ...r__tests__let_destruct_missing_equals.snap | 11 + ..._syntax__expr__tests__let_indent_body.snap | 20 + ...__syntax__expr__tests__let_indent_def.snap | 19 + ...__syntax__expr__tests__let_missing_in.snap | 13 + ...yntax__expr__tests__let_reserved_name.snap | 11 + ..._syntax__expr__tests__list_indent_end.snap | 17 + ...syntax__expr__tests__list_indent_expr.snap | 17 + ...syntax__expr__tests__list_indent_open.snap | 17 + ...syntax__expr__tests__list_missing_end.snap | 16 + ...eport__syntax__expr__tests__list_open.snap | 16 + ...tax__expr__tests__list_trailing_comma.snap | 16 + ...syntax__expr__tests__macro_indent_arg.snap | 10 + ...syntax__expr__tests__macro_indent_end.snap | 10 + ...yntax__expr__tests__macro_missing_arg.snap | 13 + ...tax__expr__tests__macro_missing_close.snap | 10 + ...port__syntax__expr__tests__macro_open.snap | 10 + ...rt__syntax__expr__tests__missing_else.snap | 11 + ..._syntax__expr__tests__missing_operand.snap | 11 + ...__syntax__expr__tests__number_bad_end.snap | 11 + ...syntax__expr__tests__number_hex_digit.snap | 11 + ...__expr__tests__number_no_leading_zero.snap | 11 + ..._syntax__expr__tests__operand_boolean.snap | 11 + ..._expr__tests__operand_custom_operator.snap | 10 + ...rt__syntax__expr__tests__operand_pipe.snap | 10 + ...ax__expr__tests__operand_reverse_pipe.snap | 10 + ...rt__syntax__expr__tests__operator_dot.snap | 11 + ...ntax__expr__tests__operator_fat_arrow.snap | 11 + ...yntax__expr__tests__operator_has_type.snap | 11 + ...x__expr__tests__operator_indent_right.snap | 14 + ...tax__expr__tests__operator_left_arrow.snap | 10 + ...t__syntax__expr__tests__operator_pipe.snap | 11 + ..._expr__tests__operator_reserved_arrow.snap | 11 + ...ax__expr__tests__parsed_bytes_bad_hex.snap | 12 + ..._expr__tests__parsed_case_wrong_arrow.snap | 23 + ...__expr__tests__parsed_do_last_binding.snap | 14 + ...__expr__tests__parsed_if_missing_else.snap | 13 + ...pr__tests__parsed_lambda_missing_body.snap | 15 + ...x__expr__tests__parsed_let_missing_in.snap | 15 + ...pr__tests__parsed_list_trailing_comma.snap | 18 + ...ntax__expr__tests__parsed_macro_close.snap | 12 + ...__expr__tests__parsed_record_reserved.snap | 13 + ...ax__expr__tests__parsed_unicode_short.snap | 14 + ...expr__tests__record_close_indentation.snap | 16 + ...__expr__tests__record_close_next_line.snap | 16 + ...tax__expr__tests__record_double_comma.snap | 17 + ...t__syntax__expr__tests__record_equals.snap | 16 + ...syntax__expr__tests__record_field_bad.snap | 17 + ...yntax__expr__tests__record_indent_end.snap | 17 + ...ax__expr__tests__record_indent_equals.snap | 17 + ...ntax__expr__tests__record_indent_expr.snap | 17 + ...tax__expr__tests__record_indent_field.snap | 17 + ...ntax__expr__tests__record_indent_open.snap | 17 + ...ntax__expr__tests__record_missing_end.snap | 16 + ...x__expr__tests__record_reserved_field.snap | 11 + ...x__expr__tests__record_trailing_comma.snap | 17 + ...expr__tests__record_unexpected_equals.snap | 13 + ...ax__expr__tests__string_endless_multi.snap | 21 + ...x__expr__tests__string_endless_single.snap | 23 + ..._syntax__expr__tests__todo_indentbody.snap | 10 + ...ntax__expr__tests__todo_indentmessage.snap | 10 + ...syntax__expr__tests__trace_indentbody.snap | 10 + ...tax__expr__tests__trace_indentmessage.snap | 10 + ...ntax__expr__tests__trace_missing_body.snap | 13 + ...syntax__expr__tests__tuple_indent_end.snap | 11 + ...ntax__expr__tests__tuple_indent_expr1.snap | 11 + ...tax__expr__tests__tuple_indent_expr_n.snap | 11 + ...yntax__expr__tests__tuple_missing_end.snap | 11 + ...ax__expr__tests__tuple_operator_close.snap | 11 + ...__syntax__expr__tests__unicode_escape.snap | 11 + ...expr__tests__weird_end_in_def_context.snap | 15 + ...ntax__tests__alias_reserved_parameter.snap | 22 + ...x__tests__custom_type_missing_variant.snap | 21 + ...tests__custom_type_reserved_parameter.snap | 21 + ...__syntax__tests__decl_def_indent_body.snap | 24 + ...yntax__tests__decl_def_missing_equals.snap | 19 + ...t__syntax__tests__decl_def_name_match.snap | 16 + ...eport__syntax__tests__decl_start_case.snap | 14 + ..._report__syntax__tests__decl_start_if.snap | 13 + ...ort__syntax__tests__decl_start_import.snap | 14 + ...__syntax__tests__decl_start_uppercase.snap | 25 + ...ntax__tests__definition_missing_colon.snap | 19 + ...__tests__definition_reserved_argument.snap | 15 + ...tests__definition_unexpected_operator.snap | 19 + ...syntax__tests__exposing_bare_operator.snap | 14 + ...syntax__tests__exposing_missing_paren.snap | 21 + ...syntax__tests__exposing_reserved_word.snap | 13 + ...rt__syntax__tests__exposing_value_bad.snap | 20 + ..._syntax__tests__fresh_line_after_decl.snap | 17 + ...rt__syntax__tests__fresh_line_keyword.snap | 12 + ...port__syntax__tests__import_bad_alias.snap | 17 + ...s__import_exposing_list_missing_paren.snap | 21 + ...t__syntax__tests__import_missing_name.snap | 19 + ..._syntax__tests__module_name_lowercase.snap | 19 + ...__syntax__tests__module_name_mismatch.snap | 19 + ...t__syntax__tests__module_name_missing.snap | 16 + ...report__syntax__tests__module_problem.snap | 19 + ...ax__tests__pattern_alias_missing_name.snap | 16 + ...ntax__tests__pattern_list_missing_end.snap | 12 + ...yntax__tests__pattern_negative_number.snap | 13 + ...ax__tests__pattern_record_missing_end.snap | 15 + ...ax__tests__pattern_reserved_list_open.snap | 12 + ..._tests__pattern_reserved_record_field.snap | 12 + ...ax__tests__pattern_reserved_tuple_end.snap | 12 + ...x__tests__pattern_reserved_tuple_open.snap | 12 + ...__syntax__tests__pattern_start_in_arg.snap | 12 + ..._syntax__tests__pattern_start_in_case.snap | 12 + ...__syntax__tests__pattern_start_in_let.snap | 12 + ..._syntax__tests__pattern_stray_bracket.snap | 12 + ...tax__tests__pattern_tuple_missing_end.snap | 14 + ...__tests__pattern_underscore_only_name.snap | 13 + ...ts__pattern_underscore_uppercase_name.snap | 13 + ...ntax__tests__pattern_wildcard_not_var.snap | 13 + ..._syntax__tests__space_endless_comment.snap | 16 + ..._report__syntax__tests__space_has_tab.snap | 12 + ...t__syntax__tests__type_alias_bad_body.snap | 12 + ...tax__tests__type_alias_missing_equals.snap | 21 + ...__syntax__tests__type_indent_in_alias.snap | 15 + ...ax__tests__type_indent_in_custom_type.snap | 15 + ...ntax__tests__type_record_double_comma.snap | 24 + ...tax__tests__type_record_missing_colon.snap | 25 + ...ax__tests__type_record_reserved_field.snap | 13 + ...tax__tests__type_record_reserved_open.snap | 13 + ...ax__tests__type_record_trailing_comma.snap | 24 + ...ests__type_record_underindented_close.snap | 24 + ...rt__syntax__tests__type_reserved_word.snap | 13 + ...__tests__type_start_bad_in_annotation.snap | 13 + ...t__syntax__tests__type_start_in_alias.snap | 12 + ...tax__tests__type_start_in_custom_type.snap | 12 + ...syntax__tests__type_tuple_missing_end.snap | 16 + ...ntax__tests__type_tuple_reserved_open.snap | 13 + ...rt__syntax__tests__weird_end_backtick.snap | 25 + ..._syntax__tests__weird_end_close_paren.snap | 13 + ...eport__syntax__tests__weird_end_comma.snap | 17 + ...eport__syntax__tests__weird_end_empty.snap | 12 + ...t__syntax__tests__weird_end_lowercase.snap | 13 + ...rt__syntax__tests__weird_end_operator.snap | 25 + ...yntax__tests__weird_end_reserved_word.snap | 14 + ...t__syntax__tests__weird_end_semicolon.snap | 16 + ...t__syntax__tests__weird_end_uppercase.snap | 13 + ...ntax__variants__variant_attribute_arg.snap | 15 + ...ntax__variants__variant_attribute_end.snap | 13 + ...ariants__variant_attribute_fresh_line.snap | 12 + ...ariants__variant_attribute_indent_arg.snap | 13 + ...ariants__variant_attribute_indent_end.snap | 13 + ...tax__variants__variant_attribute_name.snap | 12 + ...ax__variants__variant_attribute_space.snap | 12 + ...ax__variants__variant_custom_type_bar.snap | 20 + ..._variants__variant_custom_type_equals.snap | 20 + ...__variants__variant_custom_type_field.snap | 21 + ...ants__variant_custom_type_field_colon.snap | 20 + ...riants__variant_custom_type_field_end.snap | 20 + ...iants__variant_custom_type_field_type.snap | 12 + ..._variant_custom_type_indent_after_bar.snap | 21 + ...riant_custom_type_indent_after_equals.snap | 21 + ...iants__variant_custom_type_indent_bar.snap | 20 + ...ts__variant_custom_type_indent_equals.snap | 20 + ...nts__variant_custom_type_indent_field.snap | 21 + ...variant_custom_type_indent_field_type.snap | 21 + ...x__variants__variant_custom_type_name.snap | 21 + ...__variants__variant_custom_type_param.snap | 13 + ...__variants__variant_custom_type_space.snap | 12 + ...variants__variant_custom_type_variant.snap | 21 + ...ants__variant_custom_type_variant_arg.snap | 12 + ...tax__variants__variant_decl_attribute.snap | 12 + ...t__syntax__variants__variant_decl_def.snap | 23 + ...yntax__variants__variant_decl_def_arg.snap | 13 + ...ntax__variants__variant_decl_def_body.snap | 15 + ...ax__variants__variant_decl_def_equals.snap | 19 + ...ariants__variant_decl_def_indent_body.snap | 23 + ...iants__variant_decl_def_indent_equals.snap | 23 + ...ariants__variant_decl_def_indent_type.snap | 23 + ...variants__variant_decl_def_name_match.snap | 15 + ...ariants__variant_decl_def_name_repeat.snap | 25 + ...tax__variants__variant_decl_def_space.snap | 12 + ...ntax__variants__variant_decl_def_type.snap | 12 + ...ant_decl_fresh_line_after_doc_comment.snap | 13 + ...__syntax__variants__variant_decl_impl.snap | 13 + ..._syntax__variants__variant_decl_space.snap | 12 + ..._syntax__variants__variant_decl_start.snap | 24 + ..._syntax__variants__variant_decl_trait.snap | 12 + ...__syntax__variants__variant_decl_type.snap | 21 + ...ax__variants__variant_decl_type_alias.snap | 22 + ...riants__variant_decl_type_indent_name.snap | 21 + ...tax__variants__variant_decl_type_name.snap | 21 + ...ax__variants__variant_decl_type_space.snap | 12 + ...ax__variants__variant_decl_type_union.snap | 21 + ...yntax__variants__variant_exposing_end.snap | 12 + ...variants__variant_exposing_indent_end.snap | 15 + ...riants__variant_exposing_indent_value.snap | 12 + ...__variants__variant_exposing_operator.snap | 13 + ...s__variant_exposing_operator_reserved.snap | 12 + ...variant_exposing_operator_right_paren.snap | 13 + ...tax__variants__variant_exposing_space.snap | 12 + ...tax__variants__variant_exposing_start.snap | 21 + ..._variants__variant_exposing_type_name.snap | 13 + ...riants__variant_exposing_type_privacy.snap | 19 + ...tax__variants__variant_exposing_value.snap | 14 + ...tax__variants__variant_impl_alignment.snap | 12 + ...ntax__variants__variant_impl_bad_head.snap | 13 + ...__syntax__variants__variant_impl_head.snap | 12 + ...x__variants__variant_impl_indent_head.snap | 13 + ..._variants__variant_impl_indent_method.snap | 13 + ...__variants__variant_impl_indent_where.snap | 12 + ...syntax__variants__variant_impl_method.snap | 19 + ...x__variants__variant_impl_method_name.snap | 13 + ..._syntax__variants__variant_impl_space.snap | 12 + ..._syntax__variants__variant_impl_where.snap | 12 + ...tax__variants__variant_module_bad_end.snap | 13 + ...variants__variant_module_declarations.snap | 24 + ...ax__variants__variant_module_exposing.snap | 21 + ...__variants__variant_module_fresh_line.snap | 13 + ...variants__variant_module_import_alias.snap | 17 + ...x__variants__variant_module_import_as.snap | 20 + ...__variants__variant_module_import_end.snap | 20 + ...iants__variant_module_import_exposing.snap | 20 + ...__variant_module_import_exposing_list.snap | 21 + ...s__variant_module_import_indent_alias.snap | 20 + ...nt_module_import_indent_exposing_list.snap | 18 + ...ts__variant_module_import_indent_name.snap | 20 + ..._variants__variant_module_import_name.snap | 19 + ...variants__variant_module_import_start.snap | 20 + ...yntax__variants__variant_module_infix.snap | 13 + ...syntax__variants__variant_module_name.snap | 19 + ...tax__variants__variant_module_problem.snap | 19 + ...yntax__variants__variant_module_space.snap | 12 + ...yntax__variants__variant_module_tests.snap | 13 + ...x__variants__variant_module_validator.snap | 13 + ..._syntax__variants__variant_p_list_end.snap | 12 + ...syntax__variants__variant_p_list_expr.snap | 13 + ...__variants__variant_p_list_indent_end.snap | 15 + ..._variants__variant_p_list_indent_expr.snap | 15 + ..._variants__variant_p_list_indent_open.snap | 15 + ...syntax__variants__variant_p_list_open.snap | 12 + ...yntax__variants__variant_p_list_space.snap | 12 + ...yntax__variants__variant_p_record_end.snap | 15 + ...tax__variants__variant_p_record_field.snap | 15 + ...variants__variant_p_record_indent_end.snap | 15 + ...riants__variant_p_record_indent_field.snap | 15 + ...ariants__variant_p_record_indent_open.snap | 15 + ...ntax__variants__variant_p_record_open.snap | 15 + ...tax__variants__variant_p_record_space.snap | 12 + ...syntax__variants__variant_p_tuple_end.snap | 14 + ...yntax__variants__variant_p_tuple_expr.snap | 13 + ..._variants__variant_p_tuple_indent_end.snap | 15 + ...ariants__variant_p_tuple_indent_expr1.snap | 13 + ...riants__variant_p_tuple_indent_expr_n.snap | 16 + ...yntax__variants__variant_p_tuple_open.snap | 13 + ...ntax__variants__variant_p_tuple_space.snap | 12 + ...ntax__variants__variant_pattern_alias.snap | 16 + ...ntax__variants__variant_pattern_bytes.snap | 12 + ...ariants__variant_pattern_indent_alias.snap | 16 + ...ariants__variant_pattern_indent_start.snap | 16 + ...yntax__variants__variant_pattern_list.snap | 12 + ...tax__variants__variant_pattern_number.snap | 13 + ...tax__variants__variant_pattern_record.snap | 15 + ...ntax__variants__variant_pattern_space.snap | 12 + ...ntax__variants__variant_pattern_start.snap | 13 + ...tax__variants__variant_pattern_string.snap | 25 + ...ntax__variants__variant_pattern_tuple.snap | 13 + ...nts__variant_pattern_wildcard_not_var.snap | 13 + ..._syntax__variants__variant_repr_arrow.snap | 13 + ...__syntax__variants__variant_repr_name.snap | 12 + ..._syntax__variants__variant_repr_space.snap | 12 + ..._syntax__variants__variant_repr_start.snap | 12 + ...tax__variants__variant_t_record_colon.snap | 24 + ...yntax__variants__variant_t_record_end.snap | 17 + ...tax__variants__variant_t_record_field.snap | 24 + ...riants__variant_t_record_indent_colon.snap | 24 + ...variants__variant_t_record_indent_end.snap | 24 + ...riants__variant_t_record_indent_field.snap | 26 + ...ariants__variant_t_record_indent_open.snap | 24 + ...ariants__variant_t_record_indent_type.snap | 24 + ...ntax__variants__variant_t_record_open.snap | 13 + ...tax__variants__variant_t_record_space.snap | 12 + ...ntax__variants__variant_t_record_type.snap | 12 + ...syntax__variants__variant_t_tuple_end.snap | 16 + ..._variants__variant_t_tuple_indent_end.snap | 15 + ...variants__variant_t_tuple_indent_repr.snap | 13 + ...ariants__variant_t_tuple_indent_type1.snap | 16 + ...riants__variant_t_tuple_indent_type_n.snap | 17 + ...yntax__variants__variant_t_tuple_open.snap | 13 + ...yntax__variants__variant_t_tuple_repr.snap | 12 + ...ntax__variants__variant_t_tuple_space.snap | 12 + ...yntax__variants__variant_t_tuple_type.snap | 12 + ...riants__variant_test_binder_alignment.snap | 12 + ...__syntax__variants__variant_test_body.snap | 13 + ...rt__syntax__variants__variant_test_do.snap | 13 + ...syntax__variants__variant_test_equals.snap | 12 + ...syntax__variants__variant_test_fuzzer.snap | 15 + ...rt__syntax__variants__variant_test_in.snap | 12 + ..._variants__variant_test_indent_binder.snap | 13 + ...x__variants__variant_test_indent_body.snap | 13 + ..._variants__variant_test_indent_equals.snap | 12 + ...tax__variants__variant_test_indent_in.snap | 12 + ...x__variants__variant_test_indent_name.snap | 12 + ...t__syntax__variants__variant_test_let.snap | 13 + ...__syntax__variants__variant_test_name.snap | 25 + ...ax__variants__variant_test_name_start.snap | 12 + ...iants__variant_test_once_on_unit_test.snap | 13 + ...yntax__variants__variant_test_pattern.snap | 13 + ..._syntax__variants__variant_test_space.snap | 12 + ...t__syntax__variants__variant_test_via.snap | 12 + ...riants__variant_test_within_duplicate.snap | 12 + ...ax__variants__variant_test_within_end.snap | 12 + ...x__variants__variant_test_within_kind.snap | 12 + ..._variants__variant_test_within_number.snap | 13 + ...x__variants__variant_test_within_open.snap | 12 + ...ax__variants__variant_tests_alignment.snap | 12 + ...yntax__variants__variant_tests_import.snap | 19 + ..._variants__variant_tests_indent_start.snap | 13 + ...syntax__variants__variant_tests_space.snap | 12 + ...syntax__variants__variant_tests_start.snap | 13 + ..._syntax__variants__variant_tests_test.snap | 12 + ...ax__variants__variant_trait_alignment.snap | 12 + ...syntax__variants__variant_trait_colon.snap | 12 + ...ntax__variants__variant_trait_default.snap | 19 + ..._variants__variant_trait_indent_colon.snap | 12 + ...variants__variant_trait_indent_method.snap | 12 + ...__variants__variant_trait_indent_name.snap | 12 + ..._variants__variant_trait_indent_param.snap | 13 + ...__variants__variant_trait_indent_type.snap | 13 + ..._variants__variant_trait_indent_where.snap | 13 + ...__variants__variant_trait_method_name.snap | 12 + ..._syntax__variants__variant_trait_name.snap | 12 + ...syntax__variants__variant_trait_param.snap | 13 + ...syntax__variants__variant_trait_space.snap | 12 + ...syntax__variants__variant_trait_super.snap | 12 + ...ax__variants__variant_trait_super_arg.snap | 13 + ..._syntax__variants__variant_trait_type.snap | 12 + ...syntax__variants__variant_trait_where.snap | 13 + ...ax__variants__variant_type_alias_body.snap | 12 + ...__variants__variant_type_alias_equals.snap | 21 + ...iants__variant_type_alias_indent_body.snap | 22 + ...nts__variant_type_alias_indent_equals.snap | 21 + ...ax__variants__variant_type_alias_name.snap | 22 + ...x__variants__variant_type_alias_param.snap | 13 + ...x__variants__variant_type_alias_space.snap | 12 + ...yntax__variants__variant_type_context.snap | 13 + ...ts__variant_type_indent_after_context.snap | 12 + ...__variants__variant_type_indent_start.snap | 15 + ...x__variants__variant_type_param_colon.snap | 13 + ...tax__variants__variant_type_param_end.snap | 12 + ...ants__variant_type_param_indent_colon.snap | 13 + ...riants__variant_type_param_indent_end.snap | 12 + ...iants__variant_type_param_indent_repr.snap | 12 + ...ax__variants__variant_type_param_repr.snap | 12 + ...x__variants__variant_type_param_space.snap | 12 + ...x__variants__variant_type_param_start.snap | 13 + ...syntax__variants__variant_type_record.snap | 13 + ..._syntax__variants__variant_type_space.snap | 12 + ..._syntax__variants__variant_type_start.snap | 12 + ..._syntax__variants__variant_type_tuple.snap | 13 + ...tax__variants__variant_type_var_start.snap | 13 + crates/nash-report/src/syntax/tests.rs | 400 ++++ crates/nash-report/src/syntax/type_.rs | 460 +++++ crates/nash-report/src/syntax/variants.rs | 1716 +++++++++++++++++ 424 files changed, 12024 insertions(+) create mode 100644 crates/nash-report/src/syntax/decl.rs create mode 100644 crates/nash-report/src/syntax/expr.rs create mode 100644 crates/nash-report/src/syntax/mod.rs create mode 100644 crates/nash-report/src/syntax/module.rs create mode 100644 crates/nash-report/src/syntax/pattern.rs create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentbody.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentmessage.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_bad_hex.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_endless.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_odd.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_branch_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_colon_instead_of_cons.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_equals_instead_of_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_branch.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_pattern.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_of.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_pattern_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_reserved_pattern.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_subject_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_wrong_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentbody.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentmessage.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_missing_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_mismatch.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_repeat.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_reserved_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_type_annotation.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_stmt.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_requires_result.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_bad_unicode.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_short_unicode.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_unknown.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_access_upper.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_dot_without_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_start_bad.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentbody.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentmessage.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_missing_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_reserved_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_else_branch_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_condition.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else_branch.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then_branch.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_misindented_else.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_missing_then.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__integer_dot.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_destruct_missing_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_def.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_missing_in.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_reserved_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_trailing_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_close.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_else.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_operand.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_bad_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_hex_digit.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_no_leading_zero.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_boolean.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_custom_operator.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_pipe.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_reverse_pipe.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_dot.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_fat_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_has_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_indent_right.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_left_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_pipe.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_reserved_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_bytes_bad_hex.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_case_wrong_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_do_last_binding.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_if_missing_else.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_lambda_missing_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_let_missing_in.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_list_trailing_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_macro_close.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_record_reserved.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_unicode_short.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_indentation.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_next_line.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_double_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_field_bad.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_reserved_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_trailing_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_unexpected_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_multi.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_single.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentbody.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentmessage.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentbody.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentmessage.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_missing_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr1.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr_n.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_operator_close.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__unicode_escape.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__weird_end_in_def_context.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__alias_reserved_parameter.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_missing_variant.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_reserved_parameter.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_missing_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_name_match.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_case.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_if.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_import.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_uppercase.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_missing_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_reserved_argument.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_unexpected_operator.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_bare_operator.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_missing_paren.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_reserved_word.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_value_bad.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_after_decl.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_keyword.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_bad_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_exposing_list_missing_paren.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_missing_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_lowercase.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_mismatch.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_missing.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_problem.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_alias_missing_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_list_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_negative_number.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_record_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_list_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_record_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_case.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_let.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_stray_bracket.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_tuple_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_only_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_uppercase_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_wildcard_not_var.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_endless_comment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_has_tab.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_bad_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_missing_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_custom_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_double_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_missing_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_trailing_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_underindented_close.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_reserved_word.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_bad_in_annotation.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_custom_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_missing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_reserved_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_backtick.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_close_paren.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_comma.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_empty.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_lowercase.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_operator.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_reserved_word.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_semicolon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_uppercase.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_fresh_line.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_bar.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_bar.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_bar.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_param.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_attribute.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_match.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_repeat.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_fresh_line_after_doc_comment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_impl.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_trait.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_indent_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_union.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_value.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_reserved.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_right_paren.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_privacy.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_value.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_bad_head.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_head.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_head.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_method.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_where.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_where.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_bad_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_declarations.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_exposing.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_fresh_line.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_as.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing_list.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_exposing_list.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_infix.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_problem.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_tests.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_validator.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_expr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr1.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr_n.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_bytes.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_alias.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_list.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_number.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_record.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_string.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_tuple.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_wildcard_not_var.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_arrow.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_field.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_repr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type1.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type_n.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_repr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_binder_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_do.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_fuzzer.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_in.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_binder.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_in.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_let.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_once_on_unit_test.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_pattern.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_via.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_duplicate.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_kind.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_number.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_open.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_import.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_indent_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_test.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_alignment.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_default.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_method.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_param.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_where.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_method_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_param.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super_arg.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_type.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_where.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_body.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_equals.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_name.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_param.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_context.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_after_context.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_colon.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_end.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_repr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_repr.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_record.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_space.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_start.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_tuple.snap create mode 100644 crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_var_start.snap create mode 100644 crates/nash-report/src/syntax/tests.rs create mode 100644 crates/nash-report/src/syntax/type_.rs create mode 100644 crates/nash-report/src/syntax/variants.rs diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index f6d4a1f5..3ecc6e17 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -12,6 +12,7 @@ pub mod json; pub mod localizer; pub mod render_type; pub mod suggest; +pub mod syntax; pub mod type_diff; pub use localizer::Localizer; mod render; diff --git a/crates/nash-report/src/syntax/decl.rs b/crates/nash-report/src/syntax/decl.rs new file mode 100644 index 00000000..828efb2b --- /dev/null +++ b/crates/nash-report/src/syntax/decl.rs @@ -0,0 +1,644 @@ +use super::{Doc, Report, Source, expr, pattern, problem, to_space_report, type_, wide}; +use crate::code::{Next, to_keyword_region}; +use nash_parse::error::{Attribute, CustomType, Decl, DeclDef, DeclType, Impl, Trait, TypeAlias}; +use nash_parse::{Col, Row}; +use pattern::PContext; +use type_::TContext; + +pub(crate) fn to_declarations_report(source: &Source<'_>, error: &Decl<'_>) -> Report { + match *error { + Decl::Start(r, c) => to_decl_start_report(source, r, c), + Decl::Space(ref e, r, c) => to_space_report(source, e, r, c), + Decl::Type(e, r, c) => to_decl_type_report(source, e, r, c), + Decl::Def(name, e, r, c) => to_decl_def_report(source, name, e, r, c), + Decl::FreshLineAfterDocComment(r, c) => problem( + "EXPECTING DECLARATION", + r, + c, + "I just saw a doc comment, but then I got stuck here:", + "I was expecting to see the corresponding declaration next, starting on a fresh line with no indentation.", + ), + Decl::Attribute(e, r, c) => to_attribute_report(source, e, r, c), + Decl::Trait(e, r, c) => to_trait_report(source, e, r, c), + Decl::Impl(e, r, c) => to_impl_report(source, e, r, c), + } +} + +pub(crate) fn to_decl_start_report(source: &Source<'_>, r: Row, c: Col) -> Report { + match source.what_is_next(r, c) { + Next::Close(term, ch) => problem( + &format!("STRAY {}", term.to_uppercase()), + r, + c, + &format!("I was not expecting to see a {term} here:"), + &format!("This {ch} does not match up with an earlier open {term}. Try deleting it?"), + ), + Next::Keyword(k) => { + let after = match k { + "import" => { + "It is reserved for declaring imports at the top of your module. If you want another import, try moving it up top with the other imports. If you want to define a value or function, try changing the name to something else!" + } + "case" => { + "It is reserved for writing `case` expressions. Try using a different name? If you are trying to write a `case` expression, it needs to be part of a definition." + } + "if" => { + "It is reserved for writing `if` expressions. Try using a different name? If you are trying to write an `if` expression, it needs to be part of a definition." + } + _ => "It is a reserved word. Try changing the name to something else?", + }; + Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow(&format!( + "I was not expecting to run into the `{k}` keyword here:" + )), + Doc::reflow(after), + ) + } + Next::Upper(name) => { + let lower = name + .chars() + .next() + .map(|ch| ch.to_lowercase().to_string() + &name[ch.len_utf8()..]) + .unwrap_or_default(); + let mut report = problem( + "UNEXPECTED CAPITAL LETTER", + r, + c, + "Declarations always start with a lower-case letter, so I am getting stuck here:", + &format!("Try a name like {lower} instead?"), + ); + report.after = Doc::stack([ + report.after, + decl_def_note(), + Doc::reflow( + "Notice that they always start with a lower-case letter. Capitalization matters!", + ), + ]); + report + } + Next::Operator(op) => with_note( + problem( + "UNEXPECTED SYMBOL", + r, + c, + &format!("I am getting stuck because this line starts with the {op} symbol:"), + "When a line has no spaces at the beginning, I expect it to be a declaration. If this is not supposed to be a declaration, try adding some spaces before it?", + ), + decl_def_note(), + ), + Next::Other(Some(ch)) + if [ + '(', '{', '[', '+', '-', '*', '/', '^', '&', '|', '"', '\'', '!', '@', '#', '$', + '%', + ] + .contains(&ch) => + { + with_note( + problem( + "UNEXPECTED SYMBOL", + r, + c, + &format!("I am getting stuck because this line starts with the {ch} symbol:"), + "When a line has no spaces at the beginning, I expect it to be a declaration. If this is not supposed to be a declaration, try adding some spaces before it?", + ), + decl_def_note(), + ) + } + Next::Lower(_) | Next::Other(_) => with_note( + problem( + "WEIRD DECLARATION", + r, + c, + "I am trying to parse a declaration, but I am getting stuck here:", + "When a line has no spaces at the beginning, I expect it to be a declaration. Try to make your declaration look like the example? Or if this is not supposed to be a declaration, try adding some spaces before it?", + ), + decl_def_note(), + ), + } +} +fn with_note(mut report: Report, note: Doc) -> Report { + report.after = Doc::stack([report.after, note]); + report +} +pub(super) fn to_decl_type_report( + source: &Source<'_>, + error: &DeclType<'_>, + sr: Row, + sc: Col, +) -> Report { + match *error { + DeclType::Space(ref e, r, c) => to_space_report(source, e, r, c), + DeclType::Alias(e, r, c) => to_type_alias_report(source, e, r, c), + DeclType::Union(e, r, c) => to_custom_type_report(source, e, r, c), + DeclType::Name(r, c) | DeclType::IndentName(r, c) => wide( + with_note( + problem( + "EXPECTING TYPE NAME", + r, + c, + "I think I am parsing a type declaration, but I got stuck here:", + "I was expecting a name like status or option next. Nash uses lower-case names for little types and capitalized names for Big types.", + ), + custom_type_note(), + ), + sr, + sc, + ), + } +} +pub(super) fn to_type_alias_report( + source: &Source<'_>, + error: &TypeAlias<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + TypeAlias::Space(ref e, r, c) => return to_space_report(source, e, r, c), + TypeAlias::Body(e, r, c) => { + return type_::to_type_report(source, TContext::TypeAlias, e, r, c); + } + TypeAlias::Param(e, r, c) => return type_::to_type_param_report(source, e, r, c), + TypeAlias::Name(r, c) => problem( + "EXPECTING TYPE ALIAS NAME", + r, + c, + "I am partway through parsing a type alias, but I got stuck here:", + "I was expecting a name like account or point next. Nash uses lower-case names for little aliases and capitalized names for Big aliases.", + ), + TypeAlias::Equals(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow( + "I ran into a reserved word unexpectedly while parsing this type alias:", + ), + Doc::reflow(&format!( + "It looks like you are trying to use `{k}` as a type variable, but it is a reserved word. Try using a different name?" + )), + ), + _ => problem( + "PROBLEM IN TYPE ALIAS", + r, + c, + "I am partway through parsing a type alias, but I got stuck here:", + "I was expecting to see a type variable or an equals sign next.", + ), + }, + TypeAlias::IndentEquals(r, c) => problem( + "UNFINISHED TYPE ALIAS", + r, + c, + "I am partway through parsing a type alias, but I got stuck here:", + "I was expecting to see a type variable or an equals sign next.", + ), + TypeAlias::IndentBody(r, c) => problem( + "UNFINISHED TYPE ALIAS", + r, + c, + "I am partway through parsing a type alias, but I got stuck here:", + "I was expecting to see a type next. Something as simple as int or string would work!", + ), + }; + wide(with_note(report, type_alias_note()), sr, sc) +} +fn type_alias_note() -> Doc { + Doc::stack([ + Doc::to_simple_note("Here is an example of a valid `type alias` for reference:"), + Doc::indent( + 4, + Doc::text("type alias Account = { owner : Bytes, balance : Int }"), + ), + Doc::reflow( + "This would let us use `Account` as a shorthand for that record type. Using this shorthand makes type annotations much easier to read, and makes changing code easier if you decide later that there is more to an account than owner and balance!", + ), + ]) +} +fn custom_type_note() -> Doc { + Doc::stack([ + Doc::to_simple_note("Here is an example of a valid `type` declaration for reference:"), + Doc::indent(4, Doc::text("type option 'a = None | Some 'a")), + Doc::reflow( + "This defines a new `option` type with two variants. The Some variant has some associated data, allowing us to store a value when one is available. None represents the absence of a value.", + ), + ]) +} +pub(super) fn to_custom_type_report( + source: &Source<'_>, + error: &CustomType<'_>, + sr: Row, + sc: Col, +) -> Report { + let before = "I am partway through parsing a custom type, but I got stuck here:"; + let report = match *error { + CustomType::Space(ref e, r, c) => return to_space_report(source, e, r, c), + CustomType::Param(e, r, c) => return type_::to_type_param_report(source, e, r, c), + CustomType::VariantArg(e, r, c) | CustomType::FieldType(e, r, c) => { + return type_::to_type_report(source, TContext::CustomType, e, r, c); + } + CustomType::Name(r, c) => problem( + "EXPECTING TYPE NAME", + r, + c, + "I think I am parsing a type declaration, but I got stuck here:", + "I was expecting a name like status or option next. Nash uses lower-case names for little types and capitalized names for Big types.", + ), + CustomType::Equals(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow( + "I ran into a reserved word unexpectedly while parsing this custom type:", + ), + Doc::reflow(&format!( + "It looks like you are trying to use `{k}` as a type variable, but it is a reserved word. Try using a different name?" + )), + ), + _ => problem( + "PROBLEM IN CUSTOM TYPE", + r, + c, + before, + "I was expecting to see a type variable or an equals sign next.", + ), + }, + CustomType::Bar(r, c) => problem( + "PROBLEM IN CUSTOM TYPE", + r, + c, + before, + "I was expecting to see a vertical bar like | next.", + ), + CustomType::Variant(r, c) => problem( + "PROBLEM IN CUSTOM TYPE", + r, + c, + before, + "I was expecting to see a variant name next. Something like Success or Sandwich. Any name that starts with a capital letter really!", + ), + CustomType::IndentEquals(r, c) => problem( + "UNFINISHED CUSTOM TYPE", + r, + c, + before, + "I was expecting to see a type variable or an equals sign next.", + ), + CustomType::IndentBar(r, c) => problem( + "UNFINISHED CUSTOM TYPE", + r, + c, + before, + "I was expecting to see a vertical bar like | next.", + ), + CustomType::IndentAfterBar(r, c) => problem( + "UNFINISHED CUSTOM TYPE", + r, + c, + before, + "I just saw a vertical bar, so I was expecting to see another variant defined next.", + ), + CustomType::IndentAfterEquals(r, c) => problem( + "UNFINISHED CUSTOM TYPE", + r, + c, + before, + "I just saw an equals sign, so I was expecting to see the first variant defined next.", + ), + CustomType::Field(r, c) | CustomType::IndentField(r, c) => problem( + "UNFINISHED CONSTRUCTOR FIELD", + r, + c, + before, + "I was expecting a field name next. A named constructor field looks like `owner : Bytes`.", + ), + CustomType::FieldColon(r, c) => problem( + "MISSING FIELD COLON", + r, + c, + before, + "I have the field name, so I was expecting a colon followed by its type.", + ), + CustomType::FieldEnd(r, c) => problem( + "UNFINISHED CONSTRUCTOR FIELDS", + r, + c, + before, + "Separate constructor fields with commas, and close the field list with }.", + ), + CustomType::IndentFieldType(r, c) => problem( + "UNFINISHED FIELD TYPE", + r, + c, + before, + "I just saw a colon, so I was expecting the field type next. Indent it farther than the constructor declaration.", + ), + }; + wide(with_note(report, custom_type_note()), sr, sc) +} +pub(super) fn to_decl_def_report( + source: &Source<'_>, + name: &str, + error: &DeclDef<'_>, + sr: Row, + sc: Col, +) -> Report { + let before = format!("I got stuck while parsing the `{name}` definition:"); + let report = match *error { + DeclDef::Space(ref e, r, c) => return to_space_report(source, e, r, c), + DeclDef::Type(e, r, c) => { + return type_::to_type_report(source, TContext::Annotation(name), e, r, c); + } + DeclDef::Arg(e, r, c) => return pattern::to_pattern_report(source, PContext::Arg, e, r, c), + DeclDef::Body(e, r, c) => { + return expr::to_expr_report(source, expr::Context::InDef(name, sr, sc), e, r, c); + } + DeclDef::Equals(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow(&format!( + "The name `{k}` is reserved in Nash, so it cannot be used as an argument here:" + )), + Doc::stack([ + Doc::reflow("Try renaming it to something else."), + Doc::to_simple_note(&format!( + "The `{k}` keyword has a special meaning in Nash, so it can only be used in certain situations." + )), + ]), + ), + Next::Operator("->") => problem( + "MISSING COLON?", + r, + c, + "I was not expecting to see an arrow here:", + "This usually means a : is missing a bit earlier in a type annotation. It could be something else though, so here is a valid definition for reference:", + ), + Next::Operator(_) => problem( + "UNEXPECTED SYMBOL", + r, + c, + "I was not expecting to see this symbol here:", + "I am not sure what is going wrong exactly, so here is a valid definition (with an optional type annotation) for reference:", + ), + _ => problem( + "PROBLEM IN DEFINITION", + r, + c, + &before, + "I am not sure what is going wrong exactly, so here is a valid definition (with an optional type annotation) for reference:", + ), + }, + DeclDef::NameRepeat(r, c) => problem( + "EXPECTING DEFINITION", + r, + c, + &format!( + "I just saw the type annotation for `{name}` so I was expecting to see its definition here:" + ), + "Type annotations always appear directly above the relevant definition, without anything else in between. (Not even doc comments!)", + ), + DeclDef::NameMatch(actual, r, c) => { + let mut report = problem( + "NAME MISMATCH", + r, + c, + &format!( + "I just saw a type annotation for `{name}`, but it is followed by a definition for `{actual}`:" + ), + "These names do not match! Is there a typo?", + ); + report.after = Doc::stack([ + report.after, + Doc::indent( + 4, + Doc::cat([ + Doc::text(actual).dullyellow(), + Doc::text(" -> "), + Doc::text(name).green(), + ]), + ), + ]); + return wide(report.with_suggestions(vec![name.to_string()]), sr, sc); + } + DeclDef::IndentType(r, c) => problem( + "UNFINISHED DEFINITION", + r, + c, + &format!("I got stuck while parsing the `{name}` type annotation:"), + "I just saw a colon, so I am expecting to see a type next.", + ), + DeclDef::IndentEquals(r, c) => problem( + "UNFINISHED DEFINITION", + r, + c, + &before, + "I was expecting to see an argument or an equals sign next.", + ), + DeclDef::IndentBody(r, c) => problem( + "UNFINISHED DEFINITION", + r, + c, + &before, + "I was expecting to see an expression next. What is it equal to?", + ), + }; + let report = match *error { + DeclDef::Equals(r, c) => match source.what_is_next(r, c) { + Next::Keyword(_) => report, + _ => with_note( + report, + Doc::stack([ + decl_def_example(), + Doc::reflow(&format!( + "Try to use that format with your `{name}` definition!" + )), + ]), + ), + }, + _ => with_note(report, decl_def_note()), + }; + wide(report, sr, sc) +} +fn decl_def_example() -> Doc { + Doc::indent( + 4, + Doc::vcat([ + Doc::text("greet : string -> string"), + Doc::text("greet name ="), + Doc::text(" \"Hello \" ++ name ++ \"!\""), + ]), + ) +} +fn decl_def_note() -> Doc { + Doc::stack([ + Doc::reflow("Here is a valid definition (with a type annotation) for reference:"), + Doc::indent( + 4, + Doc::vcat([ + Doc::text("greet : string -> string"), + Doc::text("greet name ="), + Doc::text(" \"Hello \" ++ name ++ \"!\""), + ]), + ), + Doc::reflow( + "The top line (called a \"type annotation\") is optional. You can leave it off if you want. As you get more comfortable with Nash and as your project grows, it becomes more and more valuable to add them though! They work great as compiler-verified documentation, and they often improve error messages!", + ), + ]) +} + +pub(super) fn to_attribute_report( + source: &Source<'_>, + error: &Attribute<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + Attribute::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Attribute::Arg(e, r, c) => { + return expr::to_expr_report(source, expr::Context::InDestruct(sr, sc), e, r, c); + } + Attribute::Name(r, c) => problem( + "MISSING ATTRIBUTE NAME", + r, + c, + "I just saw @, but I got stuck here:", + "Write the attribute name immediately after @, such as `@derive(Eq)`.", + ), + Attribute::End(r, c) | Attribute::IndentEnd(r, c) => problem( + "UNFINISHED ATTRIBUTE", + r, + c, + "I was parsing an attribute, but I got stuck here:", + "I was expecting a comma between arguments or a closing parenthesis after the final argument.", + ), + Attribute::FreshLine(r, c) => problem( + "ATTRIBUTE NEEDS FRESH LINE", + r, + c, + "I finished this attribute, but I got stuck here:", + "Put the declaration or next attribute on a fresh line with the same indentation.", + ), + Attribute::IndentArg(r, c) => problem( + "MISSING ATTRIBUTE ARGUMENT", + r, + c, + "I was parsing an attribute argument, but I got stuck here:", + "Add the argument expression and indent it farther than the start of the attribute.", + ), + }; + wide(report, sr, sc) +} +pub(super) fn to_trait_report(source: &Source<'_>, error: &Trait<'_>, sr: Row, sc: Col) -> Report { + let before = "I was parsing a trait declaration, but I got stuck here:"; + let report = match *error { + Trait::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Trait::Param(e, r, c) => return type_::to_type_param_report(source, e, r, c), + Trait::Super(e, r, c) => { + return type_::to_type_report(source, TContext::Superclass, e, r, c); + } + Trait::Type(e, r, c) => { + return type_::to_type_report(source, TContext::TraitMethod, e, r, c); + } + Trait::Default(name, e, r, c) => return expr::to_let_def_report(source, name, e, r, c), + Trait::Name(r, c) | Trait::IndentName(r, c) => problem( + "MISSING TRAIT NAME", + r, + c, + before, + "I was expecting a capitalized trait name, such as Eq or Show.", + ), + Trait::SuperArg(r, c) => problem( + "BAD SUPERCLASS ARGUMENT", + r, + c, + before, + "A superclass argument must be one of the trait's quoted type parameters, such as 'a.", + ), + Trait::Where(r, c) | Trait::IndentWhere(r, c) => problem( + "MISSING TRAIT WHERE", + r, + c, + before, + "Add `where` after the trait parameters and superclass constraints, before the method declarations.", + ), + Trait::MethodName(r, c) | Trait::IndentMethod(r, c) => problem( + "MISSING TRAIT METHOD", + r, + c, + before, + "I was expecting an indented method signature, such as `show : 'a -> string`.", + ), + Trait::Colon(r, c) | Trait::IndentColon(r, c) => problem( + "MISSING METHOD COLON", + r, + c, + before, + "Add a colon between the method name and its type annotation.", + ), + Trait::IndentParam(r, c) => problem( + "MISSING TRAIT PARAMETER", + r, + c, + before, + "Add a quoted type parameter, such as 'a, and keep it indented farther than the trait declaration.", + ), + Trait::IndentType(r, c) => problem( + "MISSING METHOD TYPE", + r, + c, + before, + "I just saw a colon, so I was expecting a method type next. Indent the type farther than the method name.", + ), + Trait::Alignment(indent, r, c) => problem( + "TRAIT METHOD ALIGNMENT", + r, + c, + before, + &format!("All methods in this trait must start in column {indent}."), + ), + }; + wide(report, sr, sc) +} +pub(super) fn to_impl_report(source: &Source<'_>, error: &Impl<'_>, sr: Row, sc: Col) -> Report { + let before = "I was parsing an impl declaration, but I got stuck here:"; + let report = match *error { + Impl::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Impl::Head(e, r, c) => { + return type_::to_type_report(source, TContext::ImplHead, e, r, c); + } + Impl::Method(name, e, r, c) => return expr::to_let_def_report(source, name, e, r, c), + Impl::BadHead(r, c) | Impl::IndentHead(r, c) => problem( + "BAD IMPL HEAD", + r, + c, + before, + "I was expecting a trait name followed by its type arguments. For example: `impl Show int where`.", + ), + Impl::Where(r, c) | Impl::IndentWhere(r, c) => problem( + "MISSING IMPL WHERE", + r, + c, + before, + "Add `where` after the impl head, then indent the method definitions below it.", + ), + Impl::MethodName(r, c) | Impl::IndentMethod(r, c) => problem( + "MISSING IMPL METHOD", + r, + c, + before, + "I was expecting a method definition. Write its name and arguments followed by = and the body.", + ), + Impl::Alignment(indent, r, c) => problem( + "IMPL METHOD ALIGNMENT", + r, + c, + before, + &format!("All methods in this impl must start in column {indent}."), + ), + }; + wide(report, sr, sc) +} diff --git a/crates/nash-report/src/syntax/expr.rs b/crates/nash-report/src/syntax/expr.rs new file mode 100644 index 00000000..d7cdb830 --- /dev/null +++ b/crates/nash-report/src/syntax/expr.rs @@ -0,0 +1,1687 @@ +//! Expression syntax reports, adapted from Elm's Reporting/Error/Syntax.hs. + +use super::{pattern, problem, to_space_report, type_, wide}; +use crate::code::Next; +use crate::{Doc, Report, Source}; +use nash_parse::{Col, Row, error::*}; + +#[derive(Clone, Copy)] +#[allow(clippy::enum_variant_names)] // Preserve Elm's context vocabulary. +pub(crate) enum Context<'c> { + InNode(Node, Row, Col, &'c Context<'c>), + InDef(&'c str, Row, Col), + InDestruct(Row, Col), +} +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Node { + Record, + Parens, + List, + Func, + Cond, + Then, + Else, + Case, + Branch, + Do, + Macro, + Keyword(&'static str), +} +fn get_def_name(context: Context<'_>) -> Option<&str> { + match context { + Context::InDef(name, _, _) => Some(name), + Context::InDestruct(_, _) => None, + Context::InNode(_, _, _, outer) => get_def_name(*outer), + } +} +fn is_within(node: Node, context: Context<'_>) -> bool { + matches!(context, Context::InNode(actual, _, _, _) if node == actual) +} +fn context_start(context: Context<'_>) -> (Row, Col, String) { + match context { + Context::InDef(name, r, c) => (r, c, format!("the `{name}` definition")), + Context::InDestruct(r, c) => (r, c, "a definition".into()), + Context::InNode(node, r, c, _) => ( + r, + c, + match node { + Node::Record => "a record", + Node::Parens => "some parentheses", + Node::List => "a list", + Node::Func => "an anonymous function", + Node::Cond | Node::Then | Node::Else => "an `if` expression", + Node::Case | Node::Branch => "a `case` expression", + Node::Do => "a `do` block", + Node::Macro => "a macro invocation", + Node::Keyword(keyword) => { + return ( + r, + c, + format!( + "{} `{keyword}` expression", + if keyword == "assert" { "an" } else { "a" } + ), + ); + } + } + .into(), + ), + } +} +fn unfinished(title: &str, thing: &str, r: Row, c: Col, sr: Row, sc: Col, hint: &str) -> Report { + wide( + problem( + title, + r, + c, + &format!("I was partway through parsing {thing}, but I got stuck here:"), + hint, + ), + sr, + sc, + ) +} +fn width(mut report: Report, amount: u16) -> Report { + report.region.end.column = report.region.start.column.saturating_add(amount); + report.snippet = crate::Snippet::Region { + region: report.region, + highlight: None, + }; + report +} +fn example(mut report: Report, lines: &[&str], note: &str) -> Report { + report.after = Doc::stack([ + report.after, + Doc::indent(4, Doc::vcat(lines.iter().map(|s| Doc::text(*s)))).dullyellow(), + Doc::reflow(note), + ]); + report +} +pub(crate) fn to_expr_report( + source: &Source<'_>, + context: Context<'_>, + expr: &Expr<'_>, + sr: Row, + sc: Col, +) -> Report { + match *expr { + Expr::Let(e, r, c) => to_let_report(source, context, e, r, c), + Expr::Case(e, r, c) => to_case_report(source, context, e, r, c), + Expr::If(e, r, c) => to_if_report(source, context, e, r, c), + Expr::List(e, r, c) => to_list_report(source, context, e, r, c), + Expr::Record(e, r, c) => to_record_report(source, context, e, r, c), + Expr::Tuple(e, r, c) => to_tuple_report(source, context, e, r, c), + Expr::Func(e, r, c) => to_func_report(source, context, e, r, c), + Expr::Do(e, r, c) => to_do_report(source, context, e, r, c), + Expr::Macro(e, r, c) => to_macro_report(source, context, e, r, c), + Expr::Assert(e, r, c) => to_keyword_report(source, context, "assert", e, r, c), + Expr::Fail(e, r, c) => to_keyword_report(source, context, "fail", e, r, c), + Expr::Todo(e, r, c) => to_keyword_report(source, context, "todo", e, r, c), + Expr::Trace(e, r, c) => to_keyword_report(source, context, "trace", e, r, c), + Expr::Comptime(e, r, c) => to_keyword_report(source, context, "comptime", e, r, c), + Expr::Dot(r, c) => problem( + "EXPECTING RECORD ACCESSOR", + r, + c, + "I was expecting to see a record accessor here:", + "Something like .name or .price that accesses a value from a record.", + ), + Expr::Access(r, c) => problem( + "EXPECTING RECORD ACCESSOR", + r, + c, + "I am trying to parse a record accessor here:", + "Something like .name or .price that accesses a value from a record. Record field names must start with a lower case letter!", + ), + Expr::OperatorRight(op, r, c) => { + let hint = match op { "+"|"-"|"*"|"/"|"^"=>format!("I was expecting to see an expression next. Something like 42 or 1000 that makes sense with a {op} sign."),"&&"|"||"=>"I was expecting to see an expression next. Something like True or False that makes sense with boolean logic.".into(),"|>"=>"I was expecting to see a function next.".into(),"<|"=>"I was expecting to see an argument next.".into(),_=>"I was expecting to see an expression next.".into() }; + wide( + problem( + "MISSING EXPRESSION", + r, + c, + &format!( + "I just saw a {op} {}, so I am getting stuck here:", + if matches!(op, "+" | "-" | "*" | "/" | "^") { + "sign" + } else { + "operator" + } + ), + &hint, + ), + sr, + sc, + ) + } + Expr::IndentOperatorRight(op, r, c) => wide( + problem( + "MISSING EXPRESSION", + r, + c, + &format!("I was expecting to see an expression after this {op} operator:"), + &format!( + "You can just put anything for now, like 42 or \"hello\". Once there is something there, I can probably give a more specific hint! I may be getting confused by your indentation? The easiest way to make sure this is not an indentation problem is to put the expression on the right of the {op} operator on the same line." + ), + ), + sr, + sc, + ), + Expr::OperatorReserved(ref op, r, c) => operator_in_context(source, context, op, r, c), + Expr::Start(r, c) => { + let (r0, c0, thing) = context_start(context); + unfinished( + "MISSING EXPRESSION", + &thing, + r, + c, + r0, + c0, + "I was expecting to see an expression like 42 or \"hello\". Once there is something there, I can probably give a more specific hint! This can also happen if I run into reserved words like `let` or `as` unexpectedly, or operators in unexpected spots.", + ) + } + Expr::String(ref e, r, c) => to_string_report(source, e, r, c), + Expr::Bytes(ref e, r, c) => to_bytes_report(source, e, r, c), + Expr::Number(ref e, r, c) => to_number_report(source, e, r, c), + Expr::Space(ref e, r, c) => to_space_report(source, e, r, c), + } +} +pub(crate) fn to_string_report(source: &Source<'_>, e: &StringError, r: Row, c: Col) -> Report { + let report = match e { + StringError::EndlessSingle => problem( + "ENDLESS STRING", + r, + c, + "I got to the end of the line without seeing the closing double quote:", + "Strings look like \"this\" with double quotes on each end. Is the closing double quote missing in your code? For a string that spans multiple lines, use triple double quotes on each end.", + ), + StringError::EndlessMulti => width( + problem( + "ENDLESS STRING", + r, + c, + "I cannot find the end of this multi-line string:", + "Add a \"\"\" somewhere after this to end the string.", + ), + 3, + ), + StringError::Escape(e) => return to_escape_report(source, e, r, c), + }; + example( + report, + &[ + "\"\"\"", + "# Multi-line Strings", + "", + "- start with triple double quotes", + "- write whatever you want", + "- no need to escape newlines or double quotes", + "- end with triple double quotes", + "\"\"\"", + ], + "Here is a valid multi-line string for reference.", + ) +} +fn to_escape_report(_source: &Source<'_>, e: &Escape, r: Row, c: Col) -> Report { + match *e { + Escape::Unknown => width( + problem( + "UNKNOWN ESCAPE", + r, + c, + "Backslashes always start escaped characters, but I do not recognize this one:", + r#"Valid escape characters include \n, \r, \t, \", \', \\, and \u{003D}. Do you want one of those instead? Maybe you need \\ to escape a backslash?"#, + ), + 2, + ), + Escape::BadUnicodeFormat(w) => width( + problem( + "BAD UNICODE ESCAPE", + r, + c, + "I ran into an invalid Unicode escape:", + r"Valid Unicode escapes include \u{0041}, \u{03BB}, and \u{1F60A}. Notice that the code point is always surrounded by curly braces. Maybe you are missing the opening or closing curly brace?", + ), + w, + ), + Escape::BadUnicodeCode(w) => width( + problem( + "BAD UNICODE ESCAPE", + r, + c, + "This is not a valid code point:", + "The valid Unicode scalar values are between 0 and 10FFFF inclusive, excluding the surrogate range D800 through DFFF.", + ), + w, + ), + Escape::BadUnicodeLength { + code, + expected, + actual, + } => width( + problem( + "BAD UNICODE ESCAPE", + r, + c, + "This code point has the wrong number of digits:", + &format!( + "I expected {expected} digits, but found {actual}. Unicode escapes need between four and six hexadecimal digits. Add leading zeros if there are too few, or trim leading zeros if there are too many." + ), + ), + code, + ), + } +} +pub(crate) fn to_number_report(_source: &Source<'_>, e: &Number, r: Row, c: Col) -> Report { + match e { + Number::End => problem( + "WEIRD NUMBER", + r, + c, + "I thought I was reading a number, but I ran into some weird stuff here:", + "I recognize integers like 42 and 0x002B. Is there a way to write it like one of those? Nash has no floating point numbers.", + ), + Number::Dot(n) => problem( + "WEIRD NUMBER", + r, + c, + "Numbers cannot end with a dot like this:", + &format!("Switching to {n} will work though! Nash has no floating point numbers."), + ), + Number::HexDigit => problem( + "WEIRD HEXADECIMAL", + r, + c, + "I thought I was reading a hexadecimal number until I got here:", + "Valid hexadecimal digits include 0123456789abcdefABCDEF, so I can only recognize things like 0x2B, 0x002B, or 0x00ffb3.", + ), + Number::NoLeadingZero => problem( + "LEADING ZEROS", + r, + c, + "I do not accept numbers with leading zeros:", + "Just delete the leading zeros and it should work! Some languages use a leading zero to specify octal numbers. Nash avoids this ambiguity.", + ), + } +} +pub(crate) fn to_bytes_report(_source: &Source<'_>, e: &Bytes, r: Row, c: Col) -> Report { + match e { + Bytes::Endless => problem( + "ENDLESS BYTE STRING", + r, + c, + "I cannot find the end of this byte string:", + "Add a closing double quote to end the byte string.", + ), + Bytes::OddLength => problem( + "INCOMPLETE BYTE", + r, + c, + "This byte string has an odd number of hexadecimal digits:", + "Each byte needs two hexadecimal digits. Add the missing digit or remove the extra one.", + ), + Bytes::BadHexDigit(bad_col) => wide( + problem( + "BAD BYTE STRING", + r, + *bad_col, + "I ran into an invalid hexadecimal digit in this byte string:", + "Use pairs of digits from 0123456789abcdefABCDEF, one pair for each byte.", + ), + r, + c, + ), + } +} +pub(crate) fn to_operator_report(_source: &Source<'_>, e: &BadOperator, r: Row, c: Col) -> Report { + let (title, before, after, w) = match e { + BadOperator::Dot => ( + "UNEXPECTED SYMBOL", + "I was not expecting this dot:", + "Dots are for record access, so they cannot float around on their own. Maybe there is some extra whitespace?", + 1, + ), + BadOperator::Pipe => ( + "UNEXPECTED SYMBOL", + "I was not expecting this vertical bar:", + "Vertical bars appear in custom type declarations and record updates. Maybe you want || instead?", + 1, + ), + BadOperator::Arrow => ( + "UNEXPECTED ARROW", + "I was not expecting this arrow:", + "Arrows belong in `case` branches, anonymous functions, and function types. Maybe an earlier expression is unfinished?", + 2, + ), + BadOperator::Equals => ( + "UNEXPECTED EQUALS", + "I was not expecting this equals sign:", + "An equals sign defines a value. To compare two values, use == instead.", + 1, + ), + BadOperator::HasType => ( + "UNEXPECTED COLON", + "I was not expecting this colon:", + "Colons appear in type annotations. A type annotation must appear directly above its definition.", + 1, + ), + BadOperator::FatArrow => ( + "UNEXPECTED ARROW", + "I was not expecting this fat arrow:", + "Use -> for a `case` branch or an anonymous function. The => arrow belongs in trait constraints.", + 2, + ), + BadOperator::LeftArrow => ( + "UNEXPECTED ARROW", + "I was not expecting this left arrow:", + "The <- arrow binds the result of an action inside a `do` block.", + 2, + ), + }; + width(problem(title, r, c, before, after), w) +} +fn operator_in_context( + source: &Source<'_>, + context: Context<'_>, + e: &BadOperator, + r: Row, + c: Col, +) -> Report { + let mut report = to_operator_report(source, e, r, c); + if matches!(e, BadOperator::Arrow) + && (is_within(Node::Case, context) || is_within(Node::Branch, context)) + { + report.before = Doc::reflow( + "I am parsing a `case` expression right now, but this arrow is confusing me:", + ); + report.after = Doc::reflow(if is_within(Node::Case, context) { + "Maybe the `of` keyword is missing on a previous line?" + } else { + "Maybe this branch is not indented enough? Each pattern must line up with the other patterns." + }); + } else if matches!(e, BadOperator::Equals) && is_within(Node::Record, context) { + report.after = Doc::stack([ + Doc::reflow("Maybe you want == instead? To check if two values are equal?"), + Doc::to_simple_note( + "Records look like { x = 3, y = 4 } with the equals sign right after the field name. So maybe you forgot a comma?", + ), + ]); + } else if matches!(e, BadOperator::Equals) + && let Some(name) = get_def_name(context) + { + report.after = Doc::reflow(&format!( + "Maybe you want == instead? To check if two values are equal? I may be getting confused by your indentation. I think I am still parsing the `{name}` definition. Is this supposed to be part of a definition after that? If so, the problem may be a bit before the equals sign. I need all definitions to be indented exactly the same amount, so the problem may be that this new definition has too many spaces in front of it." + )); + } + report +} +fn to_if_report(source: &Source<'_>, ctx: Context<'_>, e: &If<'_>, sr: Row, sc: Col) -> Report { + let (r, c, hint) = match *e { + If::Space(ref e, r, c) => return to_space_report(source, e, r, c), + If::Condition(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Cond, sr, sc, &ctx), e, r, c); + } + If::ThenBranch(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Then, sr, sc, &ctx), e, r, c); + } + If::ElseBranch(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Else, sr, sc, &ctx), e, r, c); + } + If::Then(r, c) => (r, c, "I was expecting to see the `then` keyword next."), + If::Else(r, c) => ( + r, + c, + "I was expecting to see the `else` keyword next. All `if` expressions need an `else` branch.", + ), + If::ElseBranchStart(r, c) => ( + r, + c, + "I was expecting to see an expression next. Maybe the `else` branch is not filled in yet?", + ), + If::IndentCondition(r, c) => ( + r, + c, + "I was expecting to see a condition next. If it is already present, it may not be indented enough for me to recognize it.", + ), + If::IndentThen(r, c) => ( + r, + c, + "I was expecting to see the `then` keyword next. It may need more indentation.", + ), + If::IndentThenBranch(r, c) => ( + r, + c, + "I was expecting to see an expression next. If the `then` branch is already present, it may not be indented enough for me to recognize it.", + ), + If::IndentElseBranch(r, c) => ( + r, + c, + "I was expecting to see an expression next. If the `else` branch is already present, it may not be indented enough for me to recognize it.", + ), + If::IndentElse(r, c) => { + if let Some((row, col)) = source.next_line_starts_with_keyword("else", r) { + return wide( + width( + problem( + "WEIRD ELSE BRANCH", + row, + col, + "I was partway through an `if` expression when I got stuck here:", + "I think this `else` keyword needs to be indented more. Try adding some spaces before it!", + ), + 4, + ), + sr, + sc, + ); + } + ( + r, + c, + "I was expecting to see an `else` branch after this. All `if` expressions need both branches. Check the indentation if the branch is already present.", + ) + } + }; + wide( + problem( + "UNFINISHED IF", + r, + c, + "I was expecting to see more of this `if` expression, but I got stuck here:", + hint, + ), + sr, + sc, + ) +} +fn case_note(report: Report) -> Report { + example( + report, + &[ + "case maybeWidth of", + " Some width ->", + " width + 200", + "", + " None ->", + " 400", + ], + "Notice the indentation. Each pattern is aligned, and each branch is indented a bit more than the corresponding pattern. That is important!", + ) +} +fn to_case_report(source: &Source<'_>, ctx: Context<'_>, e: &Case<'_>, sr: Row, sc: Col) -> Report { + let (r,c,hint)=match *e { + Case::Space(ref e,r,c)=>return to_space_report(source,e,r,c), + Case::Pattern(e,r,c)=>return pattern::to_pattern_report(source,pattern::PContext::Case,e,r,c), + Case::Expr(e,r,c)=>return to_expr_report(source,Context::InNode(Node::Case,sr,sc,&ctx),e,r,c), + Case::Branch(e,r,c)=>return to_expr_report(source,Context::InNode(Node::Branch,sr,sc,&ctx),e,r,c), + Case::Of(r,c)|Case::IndentOf(r,c)=>(r,c,"I was expecting to see the `of` keyword next.".to_owned()), + Case::Arrow(r,c)=> { + let (title,hint)=match source.what_is_next(r,c) { + Next::Keyword(k)=>("RESERVED WORD",format!("It looks like you are trying to use `{k}` in one of your patterns, but it is a reserved word. Try using a different name?")), + Next::Operator(":")=>("UNEXPECTED OPERATOR","I am seeing : but maybe you want :: instead?".into()), + Next::Operator("=")=>("UNEXPECTED OPERATOR","I am seeing = but maybe you want -> instead?".into()), + _=>("MISSING ARROW","I was expecting to see an arrow next.".into()), + }; + return case_note(unfinished(title,"a `case` expression",r,c,sr,sc,&hint)); + } + Case::IndentExpr(r,c)=>(r,c,"I was expecting to see an expression next.".into()), + Case::IndentPattern(r,c)=>(r,c,"I was expecting to see a pattern next.".into()), + Case::IndentArrow(r,c)=>(r,c,"I was expecting to see an arrow next. It may need more indentation.".into()), + Case::IndentBranch(r,c)=>(r,c,"I was expecting to see an expression next. What should I do when I run into this particular pattern?".into()), + Case::PatternAlignment(indent,r,c)=>(r,c,format!("I suspect this is a pattern that is not indented far enough? ({indent} spaces)")), + }; + case_note(unfinished( + "UNFINISHED CASE", + "a `case` expression", + r, + c, + sr, + sc, + &hint, + )) +} +fn record_note(report: Report) -> Report { + example( + report, + &["{ name = \"Nash\"", " , age = 1", " }"], + "Notice that each line starts with some indentation. Usually two or four spaces.", + ) +} +fn to_record_report( + source: &Source<'_>, + ctx: Context<'_>, + e: &Record<'_>, + sr: Row, + sc: Col, +) -> Report { + let (r, c, title, hint) = match *e { + Record::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Record::Expr(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Record, sr, sc, &ctx), e, r, c); + } + Record::Open(r, c) | Record::Field(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => { + return wide( + width( + problem( + "RESERVED WORD", + r, + c, + "I am partway through parsing a record, but I got stuck on this field name:", + &format!( + "It looks like you are trying to use `{k}` as a field name, but that is a reserved word. Try using a different name!" + ), + ), + k.len() as u16, + ), + sr, + sc, + ); + } + Next::Other(Some(',')) => ( + r, + c, + "EXTRA COMMA", + "I am seeing two commas in a row. This is the second one! Just delete one of the commas and you should be all set!", + ), + Next::Close(_, '}') => ( + r, + c, + "EXTRA COMMA", + "Trailing commas are not allowed in records. Try deleting the comma that appears before this closing curly brace.", + ), + _ => ( + r, + c, + "PROBLEM IN RECORD", + "I was expecting to see a record field next. Record field names must start with a lower case letter.", + ), + }, + Record::End(r, c) => ( + r, + c, + "PROBLEM IN RECORD", + "I was expecting to see a comma or a closing curly brace next.", + ), + Record::Equals(r, c) => ( + r, + c, + "PROBLEM IN RECORD", + "I just saw a record field, so I was expecting to see an equals sign next.", + ), + Record::IndentOpen(r, c) => ( + r, + c, + "UNFINISHED RECORD", + "I just saw the opening curly brace of a record. I was expecting a field name or a closing curly brace next. Try adding more indentation.", + ), + Record::IndentEnd(r, c) => { + if let Some((row, col)) = source.next_line_starts_with_close_curly(r) { + return record_note(unfinished( + "NEED MORE INDENTATION", + "a record", + row, + col, + sr, + sc, + "I need this curly brace to be indented more. Try adding some spaces before it!", + )); + } + if matches!(source.what_is_next(r, c), Next::Close(_, '}')) { + ( + r, + c, + "NEED MORE INDENTATION", + "I need this curly brace to be indented more. Try adding some spaces before it!", + ) + } else { + ( + r, + c, + "UNFINISHED RECORD", + "I was expecting a comma or a closing curly brace next. Try adding more indentation.", + ) + } + } + Record::IndentField(r, c) => ( + r, + c, + "UNFINISHED RECORD", + "Trailing commas are not allowed in records, so the fix may be to delete that last comma? Or maybe you were in the middle of defining an additional field?", + ), + Record::IndentEquals(r, c) => ( + r, + c, + "UNFINISHED RECORD", + "I just saw a record field, so I was expecting to see an equals sign next. Try adding more indentation.", + ), + Record::IndentExpr(r, c) => ( + r, + c, + "UNFINISHED RECORD", + "I was expecting to run into an expression next. If it is already present, it may need more indentation.", + ), + }; + record_note(unfinished(title, "a record", r, c, sr, sc, hint)) +} +fn to_tuple_report( + source: &Source<'_>, + ctx: Context<'_>, + e: &Tuple<'_>, + sr: Row, + sc: Col, +) -> Report { + let (r, c, title, hint) = match *e { + Tuple::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Tuple::Expr(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Parens, sr, sc, &ctx), e, r, c); + } + Tuple::OperatorReserved(ref e, r, c) => return to_operator_report(source, e, r, c), + Tuple::End(r, c) => ( + r, + c, + "UNFINISHED PARENTHESES", + "I was expecting to see a closing parenthesis next. Try adding a ) to see if that helps?", + ), + Tuple::OperatorClose(r, c) => ( + r, + c, + "UNFINISHED OPERATOR FUNCTION", + "I was expecting a closing parenthesis here. Try adding a ) to see if that helps! Operators in parentheses, like (+), can be used as functions.", + ), + Tuple::IndentExpr1(r, c) => ( + r, + c, + "UNFINISHED PARENTHESES", + "I just saw an open parenthesis, so I was expecting to see an expression next. It may need more indentation.", + ), + Tuple::IndentExprN(r, c) => ( + r, + c, + "UNFINISHED TUPLE", + "I just saw a comma, so I was expecting to see an expression next. It may need more indentation.", + ), + Tuple::IndentEnd(r, c) => ( + r, + c, + "UNFINISHED PARENTHESES", + "I was expecting to see a closing parenthesis next. Try adding a ) or adding more indentation to the existing one.", + ), + }; + unfinished(title, "some parentheses", r, c, sr, sc, hint) +} +fn to_list_report(source: &Source<'_>, ctx: Context<'_>, e: &List<'_>, sr: Row, sc: Col) -> Report { + let (r, c, hint) = match *e { + List::Space(ref e, r, c) => return to_space_report(source, e, r, c), + List::Expr(e, r, c) => { + if let Expr::Start(row, col) = *e { + ( + row, + col, + "Trailing commas are not allowed in lists, so the fix may be to delete the comma?", + ) + } else { + return to_expr_report(source, Context::InNode(Node::List, sr, sc, &ctx), e, r, c); + } + } + List::Open(r, c) => ( + r, + c, + "I was expecting an expression or a closing square bracket next.", + ), + List::End(r, c) => ( + r, + c, + "I was expecting a comma or a closing square bracket next.", + ), + List::IndentOpen(r, c) => ( + r, + c, + "I cannot find the end of this list. Try adding a ] or indenting the list entries more.", + ), + List::IndentEnd(r, c) => ( + r, + c, + "I cannot find the end of this list. Try adding a ] or indenting the closing bracket more.", + ), + List::IndentExpr(r, c) => ( + r, + c, + "I was expecting to see another list entry after this comma. Trailing commas are not allowed in lists, so the fix may be to delete the comma?", + ), + }; + example( + unfinished("UNFINISHED LIST", "a list", r, c, sr, sc, hint), + &["[ 1", " , 2", " ]"], + "Notice that each line starts with some indentation. Usually two or four spaces.", + ) +} +fn to_func_report(source: &Source<'_>, ctx: Context<'_>, e: &Func<'_>, sr: Row, sc: Col) -> Report { + let (r, c, title, hint) = match *e { + Func::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Func::Arg(e, r, c) => { + return pattern::to_pattern_report(source, pattern::PContext::Arg, e, r, c); + } + Func::Body(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Func, sr, sc, &ctx), e, r, c); + } + Func::Arrow(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => { + return wide( + width( + problem( + "RESERVED WORD", + r, + c, + "I was parsing an anonymous function, but I got stuck here:", + &format!( + "It looks like you are trying to use `{k}` as an argument, but it is a reserved word in this language. Try using a different argument name!" + ), + ), + k.len() as u16, + ), + sr, + sc, + ); + } + _ => ( + r, + c, + "UNFINISHED ANONYMOUS FUNCTION", + "I was expecting to see an arrow next. The syntax for anonymous functions is \\name -> name.", + ), + }, + Func::IndentArg(r, c) => ( + r, + c, + "MISSING ARGUMENT", + "I just saw the beginning of an anonymous function, so I was expecting to see an argument next. It may need more indentation.", + ), + Func::IndentArrow(r, c) => ( + r, + c, + "UNFINISHED ANONYMOUS FUNCTION", + "I was expecting to see an arrow next. It may need more indentation.", + ), + Func::IndentBody(r, c) => ( + r, + c, + "UNFINISHED ANONYMOUS FUNCTION", + "I was expecting to see an expression after the arrow. It may need more indentation.", + ), + }; + unfinished(title, "an anonymous function", r, c, sr, sc, hint) +} +fn to_let_report(source: &Source<'_>, ctx: Context<'_>, e: &Let<'_>, sr: Row, sc: Col) -> Report { + let (r,c,hint)=match *e { + Let::Space(ref e,r,c)=>return to_space_report(source,e,r,c), + Let::Def(name,e,r,c)=>return to_let_def_report(source,name,e,r,c), + Let::Destruct(e,r,c)=>return to_let_destruct_report(source,e,r,c), + Let::Body(e,r,c)=>return to_expr_report(source,ctx,e,r,c), + Let::In(r,c)|Let::DefAlignment(_,r,c)=>return unfinished("LET PROBLEM", "a `let` expression", r,c,sr,sc,"Based on the indentation, I was expecting to see the `in` keyword next. Is there a typo? This can also happen if you are trying to define another value within the `let` but it is not indented enough. Make sure each definition has exactly the same amount of spaces before it. They should line up exactly!"), + Let::IndentIn(r,c)=>(r,c,"I was expecting to see the `in` keyword next. Or maybe more of that expression?".into()), + Let::DefName(r,c)=>match source.what_is_next(r,c) { + Next::Keyword(k)=>return wide(width(problem("RESERVED WORD",r,c,"I was partway through parsing a `let` expression, but I got stuck here:",&format!("It looks like you are trying to use `{k}` as a variable name, but it is a reserved word! Try using a different name instead.")),k.len() as u16),sr,sc), + _=>(r,c,"I was expecting the name of a definition next.".to_owned()), + }, + Let::IndentDef(r,c)=>(r,c,"I was expecting a value to be defined here. It may need more indentation.".into()), + Let::IndentBody(r,c)=>(r,c,"I was expecting an expression next. Tell me what should happen with the value you just defined!".into()), + }; + example( + unfinished("UNFINISHED LET", "a `let` expression", r, c, sr, sc, &hint), + &[ + "let", + " fullName =", + " first ++ \" \" ++ last", + "in", + "fullName", + ], + "The definition is indented more than the `let` keyword, and its value is indented a bit more than that. That is important!", + ) +} +pub(crate) fn to_let_def_report( + source: &Source<'_>, + name: &str, + e: &Def<'_>, + sr: Row, + sc: Col, +) -> Report { + let (r,c,title,hint)=match *e { + Def::Space(ref e,r,c)=>return to_space_report(source,e,r,c), + Def::Type(e,r,c)=>return type_::to_type_report(source,type_::TContext::Annotation(name),e,r,c), + Def::Arg(e,r,c)=>return pattern::to_pattern_report(source,pattern::PContext::Arg,e,r,c), + Def::Body(e,r,c)=>return to_expr_report(source,Context::InDef(name,sr,sc),e,r,c), + Def::NameRepeat(r,c)=>(r,c,"EXPECTING DEFINITION",format!("I just saw the type annotation for `{name}` so I was expecting to see its definition here. Type annotations always appear directly above the relevant definition, without anything else in between.")), + Def::NameMatch(actual,r,c)=>return wide(width(problem("NAME MISMATCH",r,c,&format!("I just saw a type annotation for `{name}`, but it is followed by a definition for `{actual}`:"),"These names do not match! Is there a typo?"),actual.len() as u16),sr,sc).with_suggestions(vec![name.to_owned()]), + Def::Equals(r,c)=>match source.what_is_next(r,c) { + Next::Keyword(k)=>return wide(width(problem("RESERVED WORD",r,c,&format!("The name `{k}` is reserved, so it cannot be used as an argument:"),"Try renaming it to something else."),k.len() as u16),sr,sc), + Next::Operator("->")=>(r,c,"MISSING COLON?","I was not expecting to see an arrow here. Maybe this is a type annotation missing its colon?".into()), + _=>(r,c,"PROBLEM IN DEFINITION","I was expecting to see an argument or an equals sign next.".into()), + }, + Def::IndentEquals(r,c)=>(r,c,"UNFINISHED DEFINITION","I was expecting to see an argument or an equals sign next. It may need more indentation.".into()), + Def::IndentType(r,c)=>(r,c,"UNFINISHED DEFINITION","I just saw a colon, so I am expecting to see a type next. It may need more indentation.".into()), + Def::IndentBody(r,c)=>(r,c,"UNFINISHED DEFINITION","I was expecting to see an expression next. What is it equal to?".into()), + Def::Alignment(indent,r,c)=>(r,c,"PROBLEM IN DEFINITION",format!("I just saw a type annotation indented {indent} spaces, so I was expecting to see the corresponding definition next with the exact same amount of indentation.")), + }; + example( + wide( + problem( + title, + r, + c, + &format!("I got stuck while parsing the `{name}` definition:"), + &hint, + ), + sr, + sc, + ), + &[ + "greet : string -> string", + "greet name =", + " \"Hello \" ++ name", + ], + "The top line is an optional type annotation. It works as compiler-verified documentation and often improves error messages!", + ) +} +fn to_let_destruct_report(source: &Source<'_>, e: &Destruct<'_>, sr: Row, sc: Col) -> Report { + let (r, c, hint) = match *e { + Destruct::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Destruct::Pattern(e, r, c) => { + return pattern::to_pattern_report(source, pattern::PContext::Let, e, r, c); + } + Destruct::Body(e, r, c) => { + return to_expr_report(source, Context::InDestruct(sr, sc), e, r, c); + } + Destruct::Equals(r, c) => ( + r, + c, + if matches!(source.what_is_next(r, c), Next::Operator(":")) { + "I was expecting to see an equals sign next, followed by an expression telling me what to compute. Destructuring definitions cannot have type annotations. Put the annotation on a named value instead." + } else { + "I was expecting to see an equals sign next, followed by an expression telling me what to compute." + }, + ), + Destruct::IndentEquals(r, c) => ( + r, + c, + "I was expecting to see an equals sign next, followed by an expression telling me what to compute. It may need more indentation.", + ), + Destruct::IndentBody(r, c) => ( + r, + c, + "I was expecting to see an expression next. What is it equal to?", + ), + }; + unfinished( + "UNFINISHED DEFINITION", + "this definition", + r, + c, + sr, + sc, + hint, + ) +} +fn to_keyword_report( + source: &Source<'_>, + ctx: Context<'_>, + keyword: &'static str, + e: &Keyword<'_>, + sr: Row, + sc: Col, +) -> Report { + let (r, c, hint) = match *e { + Keyword::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Keyword::Body(e, r, c) | Keyword::Message(e, r, c) => { + return to_expr_report( + source, + Context::InNode(Node::Keyword(keyword), sr, sc, &ctx), + e, + r, + c, + ); + } + Keyword::IndentBody(r, c) => ( + r, + c, + "I was expecting to see an expression next. It may need more indentation.", + ), + Keyword::IndentMessage(r, c) => ( + r, + c, + "I was expecting to see a message expression next. It may need more indentation.", + ), + }; + unfinished( + "UNFINISHED EXPRESSION", + &format!( + "{} `{keyword}` expression", + if keyword == "assert" { "an" } else { "a" } + ), + r, + c, + sr, + sc, + hint, + ) +} +pub(crate) fn to_do_report( + source: &Source<'_>, + ctx: Context<'_>, + e: &Do<'_>, + sr: Row, + sc: Col, +) -> Report { + let (r,c,hint)=match *e { + Do::Space(ref e,r,c)=>return to_space_report(source,e,r,c), + Do::Let(e,r,c)=>return to_let_report(source,ctx,e,r,c), + Do::Pattern(e,r,c)=>return pattern::to_pattern_report(source,pattern::PContext::Let,e,r,c), + Do::Expr(e,r,c)=>return to_expr_report(source,Context::InNode(Node::Do,sr,sc,&ctx),e,r,c), + Do::Arrow(r,c)=>(r,c,"I was expecting to see <- after this binding pattern.".into()), + Do::LastNotExpr(r,c)=>(r,c,"A `do` block must end with an expression. Add the final expression after this binding.".into()), + Do::IndentStmt(r,c)=>(r,c,"I was expecting an indented statement after `do`.".into()), + Do::IndentArrow(r,c)=>(r,c,"I was expecting to see <- after this binding pattern. It may need more indentation.".into()), + Do::IndentExpr(r,c)=>(r,c,"I was expecting an expression after <-. It may need more indentation.".into()), + Do::Alignment(indent,r,c)=>(r,c,format!("Statements in this `do` block must line up with {indent} spaces of indentation.")), + }; + unfinished("UNFINISHED DO", "a `do` block", r, c, sr, sc, &hint) +} +fn to_macro_report( + source: &Source<'_>, + ctx: Context<'_>, + e: &Macro<'_>, + sr: Row, + sc: Col, +) -> Report { + let (r, c, hint) = match *e { + Macro::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Macro::Arg(e, r, c) => { + return to_expr_report(source, Context::InNode(Node::Macro, sr, sc, &ctx), e, r, c); + } + Macro::Open(r, c) => ( + r, + c, + "I was expecting an opening parenthesis after the macro's ! marker.", + ), + Macro::End(r, c) => ( + r, + c, + "I was expecting a comma or a closing parenthesis after this macro argument.", + ), + Macro::IndentArg(r, c) => ( + r, + c, + "I was expecting a macro argument next. It may need more indentation.", + ), + Macro::IndentEnd(r, c) => ( + r, + c, + "I was expecting a closing parenthesis. It may need more indentation.", + ), + }; + unfinished("UNFINISHED MACRO", "a macro invocation", r, c, sr, sc, hint) +} + +#[cfg(test)] +mod tests { + use super::*; + fn snapshot(error: Expr<'_>, text: &str) -> String { + let report = to_expr_report( + &Source::new(text), + Context::InDef("value", 1, 1), + &error, + 1, + 9, + ); + format!( + "{}\n{:?}\n\n{}\n\n{}", + report.title, + report.region, + report.before.render(80, false), + report.after.render(80, false) + ) + } + macro_rules! report_test { + ($name:ident, $error:expr, $text:expr) => { + #[test] + fn $name() { + insta::assert_snapshot!(snapshot($error, $text)); + } + }; + } + report_test!(expr_start_bad, Expr::Start(1, 9), "value = "); + report_test!(expr_dot_without_name, Expr::Dot(1, 9), "value = "); + report_test!(expr_access_upper, Expr::Access(1, 9), "value = "); + report_test!( + operator_reserved_arrow, + Expr::OperatorReserved(BadOperator::Arrow, 1, 9), + "value = " + ); + report_test!( + string_endless_single, + Expr::String(StringError::EndlessSingle, 1, 9), + "value = " + ); + report_test!( + string_endless_multi, + Expr::String(StringError::EndlessMulti, 1, 9), + "value = " + ); + report_test!( + escape_unknown, + Expr::String(StringError::Escape(Escape::Unknown), 1, 9), + "value = " + ); + report_test!( + escape_bad_unicode, + Expr::String(StringError::Escape(Escape::BadUnicodeFormat(3)), 1, 9), + "value = " + ); + report_test!( + escape_short_unicode, + Expr::String( + StringError::Escape(Escape::BadUnicodeLength { + code: 5, + expected: 4, + actual: 1 + }), + 1, + 9 + ), + "value = " + ); + report_test!( + number_hex_digit, + Expr::Number(Number::HexDigit, 1, 9), + "value = " + ); + report_test!( + number_no_leading_zero, + Expr::Number(Number::NoLeadingZero, 1, 9), + "value = " + ); + report_test!(number_bad_end, Expr::Number(Number::End, 1, 9), "value = "); + report_test!(let_missing_in, Expr::Let(&Let::In(1, 9), 1, 9), "value = "); + report_test!( + let_def_alignment, + Expr::Let(&Let::DefAlignment(4, 1, 9), 1, 9), + "value = " + ); + report_test!( + let_def_indent_body, + Expr::Let(&Let::Def("x", &Def::IndentBody(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + let_destruct_missing_equals, + Expr::Let(&Let::Destruct(&Destruct::Equals(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + case_missing_of, + Expr::Case(&Case::Of(1, 9), 1, 9), + "value = " + ); + report_test!( + case_missing_arrow, + Expr::Case(&Case::Arrow(1, 9), 1, 9), + "value = " + ); + report_test!( + case_pattern_alignment, + Expr::Case(&Case::PatternAlignment(4, 1, 9), 1, 9), + "value = " + ); + report_test!( + case_indent_branch, + Expr::Case(&Case::IndentBranch(1, 9), 1, 9), + "value = " + ); + report_test!(if_missing_then, Expr::If(&If::Then(1, 9), 1, 9), "value = "); + report_test!( + if_else_branch_start, + Expr::If(&If::ElseBranchStart(1, 9), 1, 9), + "value = " + ); + report_test!( + record_missing_end, + Expr::Record(&Record::End(1, 9), 1, 9), + "value = " + ); + report_test!( + record_field_bad, + Expr::Record(&Record::Field(1, 9), 1, 9), + "value = " + ); + report_test!( + record_indent_end, + Expr::Record(&Record::IndentEnd(1, 9), 1, 9), + "value = " + ); + report_test!( + tuple_missing_end, + Expr::Tuple(&Tuple::End(1, 9), 1, 9), + "value = " + ); + report_test!( + tuple_operator_close, + Expr::Tuple(&Tuple::OperatorClose(1, 9), 1, 9), + "value = " + ); + report_test!( + list_missing_end, + Expr::List(&List::End(1, 9), 1, 9), + "value = " + ); + report_test!( + list_indent_expr, + Expr::List(&List::IndentExpr(1, 9), 1, 9), + "value = " + ); + report_test!( + func_missing_arrow, + Expr::Func(&Func::Arrow(1, 9), 1, 9), + "value = " + ); + report_test!( + func_indent_body, + Expr::Func(&Func::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + weird_end_in_def_context, + Expr::OperatorReserved(BadOperator::Equals, 1, 9), + "value = " + ); + report_test!( + case_colon_instead_of_cons, + Expr::Case(&Case::Arrow(1, 9), 1, 9), + "value = :" + ); + report_test!( + case_equals_instead_of_arrow, + Expr::Case(&Case::Arrow(1, 9), 1, 9), + "value = =" + ); + #[test] + fn missing_operand() { + insta::assert_snapshot!(snapshot(Expr::OperatorRight("+", 1, 13), "value = 1 + ")); + } + #[test] + fn missing_else() { + insta::assert_snapshot!(snapshot( + Expr::If(&If::Else(1, 24), 1, 9), + "value = if True then 42" + )); + } + #[test] + fn case_wrong_arrow() { + insta::assert_snapshot!(snapshot( + Expr::Case(&Case::Arrow(1, 30), 1, 9), + "value = case x of Some width =" + )); + } + #[test] + fn record_reserved_field() { + insta::assert_snapshot!(snapshot( + Expr::Record(&Record::Open(1, 11), 1, 9), + "value = { if = 1 }" + )); + } + #[test] + fn list_trailing_comma() { + insta::assert_snapshot!(snapshot( + Expr::List(&List::Expr(&Expr::Start(1, 13), 1, 13), 1, 9), + "value = [1, ]" + )); + } + #[test] + fn do_requires_result() { + insta::assert_snapshot!(snapshot( + Expr::Do(&Do::LastNotExpr(2, 5), 1, 9), + "value = do\n let x = 1" + )); + } + #[test] + fn macro_missing_close() { + insta::assert_snapshot!(snapshot( + Expr::Macro(&Macro::End(1, 15), 1, 9), + "value = foo!(1" + )); + } + #[test] + fn integer_dot() { + insta::assert_snapshot!(snapshot( + Expr::Number(Number::Dot(42), 1, 11), + "value = 42." + )); + } + #[test] + fn unicode_escape() { + insta::assert_snapshot!(snapshot( + Expr::String(StringError::Escape(Escape::BadUnicodeCode(8)), 1, 10), + "value = \"\\u{D800}\"" + )); + } + #[test] + fn bytes_odd() { + insta::assert_snapshot!(snapshot( + Expr::Bytes(Bytes::OddLength, 1, 12), + "value = #\"a\"" + )); + } + report_test!( + if_indent_condition, + Expr::If(&If::IndentCondition(1, 9), 1, 9), + "value = " + ); + report_test!( + if_indent_then, + Expr::If(&If::IndentThen(1, 9), 1, 9), + "value = " + ); + report_test!( + if_indent_then_branch, + Expr::If(&If::IndentThenBranch(1, 9), 1, 9), + "value = " + ); + report_test!( + if_indent_else_branch, + Expr::If(&If::IndentElseBranch(1, 9), 1, 9), + "value = " + ); + report_test!( + if_indent_else, + Expr::If(&If::IndentElse(1, 9), 1, 9), + "value = " + ); + report_test!( + case_indent_expr, + Expr::Case(&Case::IndentExpr(1, 9), 1, 9), + "value = " + ); + report_test!( + case_indent_pattern, + Expr::Case(&Case::IndentPattern(1, 9), 1, 9), + "value = " + ); + report_test!( + case_indent_arrow, + Expr::Case(&Case::IndentArrow(1, 9), 1, 9), + "value = " + ); + report_test!( + record_indent_open, + Expr::Record(&Record::IndentOpen(1, 9), 1, 9), + "value = " + ); + report_test!( + record_indent_field, + Expr::Record(&Record::IndentField(1, 9), 1, 9), + "value = " + ); + report_test!( + record_equals, + Expr::Record(&Record::Equals(1, 9), 1, 9), + "value = " + ); + report_test!( + record_indent_equals, + Expr::Record(&Record::IndentEquals(1, 9), 1, 9), + "value = " + ); + report_test!( + record_indent_expr, + Expr::Record(&Record::IndentExpr(1, 9), 1, 9), + "value = " + ); + report_test!( + tuple_indent_expr1, + Expr::Tuple(&Tuple::IndentExpr1(1, 9), 1, 9), + "value = " + ); + report_test!( + tuple_indent_expr_n, + Expr::Tuple(&Tuple::IndentExprN(1, 9), 1, 9), + "value = " + ); + report_test!( + tuple_indent_end, + Expr::Tuple(&Tuple::IndentEnd(1, 9), 1, 9), + "value = " + ); + report_test!(list_open, Expr::List(&List::Open(1, 9), 1, 9), "value = "); + report_test!( + list_indent_open, + Expr::List(&List::IndentOpen(1, 9), 1, 9), + "value = " + ); + report_test!( + list_indent_end, + Expr::List(&List::IndentEnd(1, 9), 1, 9), + "value = " + ); + report_test!( + func_indent_arg, + Expr::Func(&Func::IndentArg(1, 9), 1, 9), + "value = " + ); + report_test!( + func_indent_arrow, + Expr::Func(&Func::IndentArrow(1, 9), 1, 9), + "value = " + ); + report_test!( + let_def_name, + Expr::Let(&Let::DefName(1, 9), 1, 9), + "value = " + ); + report_test!( + let_indent_def, + Expr::Let(&Let::IndentDef(1, 9), 1, 9), + "value = " + ); + report_test!( + let_indent_body, + Expr::Let(&Let::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + macro_open, + Expr::Macro(&Macro::Open(1, 9), 1, 9), + "value = " + ); + report_test!( + macro_indent_arg, + Expr::Macro(&Macro::IndentArg(1, 9), 1, 9), + "value = " + ); + report_test!( + macro_indent_end, + Expr::Macro(&Macro::IndentEnd(1, 9), 1, 9), + "value = " + ); + report_test!(do_arrow, Expr::Do(&Do::Arrow(1, 9), 1, 9), "value = "); + report_test!( + do_indent_stmt, + Expr::Do(&Do::IndentStmt(1, 9), 1, 9), + "value = " + ); + report_test!( + do_indent_arrow, + Expr::Do(&Do::IndentArrow(1, 9), 1, 9), + "value = " + ); + report_test!( + do_indent_expr, + Expr::Do(&Do::IndentExpr(1, 9), 1, 9), + "value = " + ); + report_test!( + def_name_repeat, + Expr::Let(&Let::Def("x", &Def::NameRepeat(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + def_equals, + Expr::Let(&Let::Def("x", &Def::Equals(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + def_indent_equals, + Expr::Let(&Let::Def("x", &Def::IndentEquals(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + def_indent_type, + Expr::Let(&Let::Def("x", &Def::IndentType(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + operator_dot, + Expr::OperatorReserved(BadOperator::Dot, 1, 9), + "value = " + ); + report_test!( + operator_pipe, + Expr::OperatorReserved(BadOperator::Pipe, 1, 9), + "value = " + ); + report_test!( + operator_has_type, + Expr::OperatorReserved(BadOperator::HasType, 1, 9), + "value = " + ); + report_test!( + operator_fat_arrow, + Expr::OperatorReserved(BadOperator::FatArrow, 1, 9), + "value = " + ); + report_test!( + operator_left_arrow, + Expr::OperatorReserved(BadOperator::LeftArrow, 1, 9), + "value = " + ); + report_test!( + assert_indentbody, + Expr::Assert(&Keyword::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + assert_indentmessage, + Expr::Assert(&Keyword::IndentMessage(1, 9), 1, 9), + "value = " + ); + report_test!( + fail_indentbody, + Expr::Fail(&Keyword::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + fail_indentmessage, + Expr::Fail(&Keyword::IndentMessage(1, 9), 1, 9), + "value = " + ); + report_test!( + todo_indentbody, + Expr::Todo(&Keyword::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + todo_indentmessage, + Expr::Todo(&Keyword::IndentMessage(1, 9), 1, 9), + "value = " + ); + report_test!( + trace_indentbody, + Expr::Trace(&Keyword::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + trace_indentmessage, + Expr::Trace(&Keyword::IndentMessage(1, 9), 1, 9), + "value = " + ); + report_test!( + comptime_indentbody, + Expr::Comptime(&Keyword::IndentBody(1, 9), 1, 9), + "value = " + ); + report_test!( + comptime_indentmessage, + Expr::Comptime(&Keyword::IndentMessage(1, 9), 1, 9), + "value = " + ); + report_test!( + record_double_comma, + Expr::Record(&Record::Field(1, 9), 1, 9), + "value = ," + ); + report_test!( + record_trailing_comma, + Expr::Record(&Record::Field(1, 9), 1, 9), + "value = }" + ); + report_test!( + record_close_indentation, + Expr::Record(&Record::IndentEnd(1, 9), 1, 9), + "value = }" + ); + report_test!( + let_reserved_name, + Expr::Let(&Let::DefName(1, 9), 1, 9), + "value = if" + ); + report_test!( + func_reserved_arg, + Expr::Func(&Func::Arrow(1, 9), 1, 9), + "value = if" + ); + report_test!( + case_reserved_pattern, + Expr::Case(&Case::Arrow(1, 9), 1, 9), + "value = if" + ); + report_test!( + def_reserved_arg, + Expr::Let(&Let::Def("x", &Def::Equals(1, 9), 1, 9), 1, 9), + "value = if" + ); + report_test!( + def_missing_colon, + Expr::Let(&Let::Def("x", &Def::Equals(1, 9), 1, 9), 1, 9), + "value = ->" + ); + report_test!( + def_name_mismatch, + Expr::Let(&Let::Def("x", &Def::NameMatch("y", 1, 9), 1, 9), 1, 9), + "value = y" + ); + report_test!( + bytes_bad_hex, + Expr::Bytes(Bytes::BadHexDigit(12), 1, 12), + "value = #\"ag\"" + ); + report_test!( + bytes_endless, + Expr::Bytes(Bytes::Endless, 1, 12), + "value = #\"aa" + ); + report_test!( + do_alignment, + Expr::Do(&Do::Alignment(4, 1, 9), 1, 9), + "value = " + ); + report_test!( + def_alignment, + Expr::Let(&Let::Def("x", &Def::Alignment(4, 1, 9), 1, 9), 1, 9), + "value = " + ); + macro_rules! parsed_test { + ($name:ident, $input:expr) => { + #[test] + fn $name() { + let input = $input; + let bump = bumpalo::Bump::new(); + let error = nash_parse::Parser::new(&bump, input.as_bytes()) + .module() + .expect_err("expected syntax error"); + let source = Source::new(input); + let report = super::super::to_report(&source, &Error::ParseError(&error)); + insta::assert_snapshot!(crate::render_plain(&report, &source, "Main.nash")); + } + }; + } + parsed_test!(parsed_if_missing_else, "value = if True then 42"); + parsed_test!( + parsed_case_wrong_arrow, + "value = case x of\n Some width = width" + ); + parsed_test!(parsed_record_reserved, "value = { if = 1 }"); + parsed_test!(parsed_list_trailing_comma, "value = [1, ]"); + parsed_test!(parsed_do_last_binding, "value = do\n x <- action"); + parsed_test!(parsed_macro_close, "value = foo!(1"); + parsed_test!(parsed_bytes_bad_hex, "value = #\"ag\""); + parsed_test!(parsed_unicode_short, "value = \"\\u{1}\""); + parsed_test!(parsed_let_missing_in, "value = let x = 1"); + parsed_test!(parsed_lambda_missing_body, "value = \\x ->"); + report_test!(operand_boolean, Expr::OperatorRight("&&", 1, 9), "value = "); + report_test!(operand_pipe, Expr::OperatorRight("|>", 1, 9), "value = "); + report_test!( + operand_reverse_pipe, + Expr::OperatorRight("<|", 1, 9), + "value = " + ); + report_test!( + operand_custom_operator, + Expr::OperatorRight("++", 1, 9), + "value = " + ); + report_test!( + destruct_type_annotation, + Expr::Let(&Let::Destruct(&Destruct::Equals(1, 9), 1, 9), 1, 9), + "value = :" + ); + report_test!( + destruct_indent_equals, + Expr::Let(&Let::Destruct(&Destruct::IndentEquals(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + destruct_indent_body, + Expr::Let(&Let::Destruct(&Destruct::IndentBody(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + case_subject_arrow, + Expr::Case( + &Case::Expr(&Expr::OperatorReserved(BadOperator::Arrow, 1, 9), 1, 9), + 1, + 9 + ), + "value = ->" + ); + report_test!( + case_branch_arrow, + Expr::Case( + &Case::Branch(&Expr::OperatorReserved(BadOperator::Arrow, 1, 9), 1, 9), + 1, + 9 + ), + "value = ->" + ); + report_test!( + if_misindented_else, + Expr::If(&If::IndentElse(1, 25), 1, 9), + "value = if True then 1\nelse 2" + ); + report_test!( + macro_missing_arg, + Expr::Macro(&Macro::Arg(&Expr::Start(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + trace_missing_body, + Expr::Trace(&Keyword::Body(&Expr::Start(1, 9), 1, 9), 1, 9), + "value = " + ); + report_test!( + operator_indent_right, + Expr::IndentOperatorRight("+", 1, 9), + "value = " + ); + report_test!( + record_close_next_line, + Expr::Record(&Record::IndentEnd(1, 19), 1, 9), + "value = { name = 1\n}" + ); + report_test!( + record_unexpected_equals, + Expr::Record( + &Record::Expr(&Expr::OperatorReserved(BadOperator::Equals, 1, 9), 1, 9), + 1, + 9 + ), + "value = =" + ); +} diff --git a/crates/nash-report/src/syntax/mod.rs b/crates/nash-report/src/syntax/mod.rs new file mode 100644 index 00000000..40f7c7ea --- /dev/null +++ b/crates/nash-report/src/syntax/mod.rs @@ -0,0 +1,88 @@ +//! Syntax reports ported from Elm's `Reporting/Error/Syntax.hs`. +//! Nash-only grammar (traits, representations, tests and do blocks) has its own reports. +mod decl; +mod expr; +mod module; +mod pattern; +#[cfg(test)] +mod tests; +mod type_; + +use crate::code::{Source, to_region, to_wider_region}; +use crate::{Doc, Report, Snippet}; +use nash_parse::error::{Error, Space}; +use nash_parse::{Col, Row}; +use nash_region::{Position, Region}; + +pub fn to_report(source: &Source<'_>, error: &Error<'_>) -> Report { + match error { + Error::ModuleNameUnspecified(name) => Report { + title: "MODULE NAME MISSING".into(), severity: crate::Severity::Error, + region: to_region(1, 1), snippet: Snippet::None, + before: Doc::stack([ + Doc::reflow("I need the module name to be declared at the top of this file, like this:"), + Doc::indent(4, Doc::hsep([Doc::text("module").cyan(), Doc::text(*name), Doc::text("exposing").cyan(), Doc::text("(..)")])), + Doc::reflow("Try adding that as the first line of your file!"), + ]), + after: Doc::to_simple_note("It is best to replace (..) with an explicit list of types and functions you want to expose. When you know a value is only used within this module, you can refactor without worrying about uses elsewhere. Limiting exposed values can also speed up compilation because I can skip a bunch of work if I see that the exposed API has not changed."), + suggestions: Vec::new(), + }, + Error::ModuleNameMismatch { expected, actual, row, col } => Report::snippet( + "MODULE NAME MISMATCH", to_wider_region(*row, *col, actual.len() as u16), None, + Doc::text("It looks like this module name is out of sync:"), + Doc::stack([ + Doc::reflow(&format!("I need it to match the file path, so I was expecting to see `{expected}` here. Make the following change, and you should be all set!")), + Doc::indent(4, Doc::cat([Doc::text(*actual).dullyellow(), Doc::text(" -> "), Doc::text(*expected).green()])), + Doc::to_simple_note("I require that module names correspond to file paths. This makes it much easier to explore unfamiliar codebases! So if you want to keep the current module name, try renaming the file instead."), + ]), + ).with_suggestions(vec![expected.to_string()]), + Error::ParseError(error) => module::to_parse_error_report(source, error), + } +} + +pub(crate) fn problem(title: &str, row: Row, col: Col, before: &str, after: &str) -> Report { + Report::snippet( + title, + to_region(row, col), + None, + Doc::reflow(before), + Doc::reflow(after), + ) +} + +pub(crate) fn wide(mut report: Report, row: Row, col: Col) -> Report { + let highlight = report.region; + let start = Position::new(row, col).min(highlight.start); + report.snippet = Snippet::Region { + region: Region::new(start, highlight.end), + highlight: Some(highlight), + }; + report +} + +pub(crate) fn to_space_report(_source: &Source<'_>, space: &Space, row: Row, col: Col) -> Report { + match space { + Space::HasTab => problem( + "NO TABS", + row, + col, + "I ran into a tab, but tabs are not allowed in Nash files.", + "Replace the tab with spaces.", + ), + Space::EndlessMultiComment => Report::snippet( + "ENDLESS COMMENT", + to_wider_region(row, col, 2), + None, + Doc::reflow("I cannot find the end of this multi-line comment:"), + Doc::stack([ + Doc::reflow("Add a -} somewhere after this to end the comment."), + Doc::to_simple_hint( + "Multi-line comments can be nested in Nash, so {- {- -} -} is a comment that happens to contain another comment. Like parentheses and curly braces, the start and end markers must always be balanced. Maybe that is the problem?", + ), + ]), + ), + } +} + +#[cfg(test)] +mod variants; diff --git a/crates/nash-report/src/syntax/module.rs b/crates/nash-report/src/syntax/module.rs new file mode 100644 index 00000000..274c18e6 --- /dev/null +++ b/crates/nash-report/src/syntax/module.rs @@ -0,0 +1,583 @@ +use super::{Doc, Report, Source, decl, expr, pattern, problem, to_space_report, wide}; +use crate::code::{Next, to_keyword_region}; +use nash_parse::error::{BadOperator, Exposing, Module, Test, Tests}; +use nash_parse::{Col, Row}; + +pub(crate) fn to_parse_error_report(source: &Source<'_>, error: &Module<'_>) -> Report { + match *error { + Module::Space(ref e, r, c) => to_space_report(source, e, r, c), + Module::BadEnd(r, 1) => decl::to_decl_start_report(source, r, 1), + Module::BadEnd(r, c) => to_weird_end_report(source, r, c), + Module::Problem(r, c) => { + let mut report = problem( + "UNFINISHED MODULE DECLARATION", + r, + c, + "I am parsing a `module` declaration, but I got stuck here:", + "Here are some examples of valid `module` declarations:", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "module Main exposing (..)", + "module Dict exposing (Dict, empty, get)", + ]), + Doc::reflow( + "I generally recommend using an explicit exposing list. I can skip compiling a bunch of files when the public interface of a module stays the same, so exposing fewer values can help improve compile times!", + ), + ]); + report + } + Module::Name(r, c) => { + let mut report = problem( + "EXPECTING MODULE NAME", + r, + c, + "I was parsing a `module` declaration until I got stuck here:", + "I was expecting to see the module name next, like in these examples:", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "module Dict exposing (..)", + "module Option exposing (..)", + "module Cardano.Tx exposing (..)", + "module Data.Decoder exposing (..)", + ]), + Doc::reflow( + "Notice that the module names all start with capital letters. That is required!", + ), + ]); + report + } + Module::Validator(r, c) => problem( + "UNFINISHED VALIDATOR", + r, + c, + "I was parsing a validator declaration, but I got stuck here:", + "A validator module starts with `validator module`, followed by its module name and exposing list. For example: `validator module Vesting exposing (main)`.", + ), + Module::Exposing(e, r, c) | Module::ImportExposingList(e, r, c) => { + to_exposing_report(source, e, r, c) + } + Module::FreshLine(r, c) => match source.what_is_next(r, c) { + Next::Keyword(keyword) => problem( + "TOO MUCH INDENTATION", + r, + c, + &format!("This `{keyword}` should not have any spaces before it:"), + &format!("Delete the spaces before `{keyword}` until there are none left!"), + ), + _ => problem( + "SYNTAX PROBLEM", + r, + c, + "I got stuck here:", + "A top-level declaration must start on a fresh line with no spaces before it. Move this declaration to its own line.", + ), + }, + Module::ImportName(r, c) => { + let mut report = problem( + "EXPECTING IMPORT NAME", + r, + c, + "I was parsing an `import` until I got stuck here:", + "I was expecting to see a module name next, like in these examples:", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "import Dict", + "import Option", + "import Cardano.Tx as Tx", + "import Data.Decoder exposing (..)", + ]), + Doc::reflow( + "Notice that the module names all start with capital letters. That is required!", + ), + ]); + report + } + Module::ImportAlias(r, c) => { + let mut report = problem( + "EXPECTING IMPORT ALIAS", + r, + c, + "I was parsing an `import` until I got stuck here:", + "I was expecting to see an alias next, like in these examples:", + ); + report.after = Doc::stack([ + report.after, + examples(&["import Cardano.Tx as Tx", "import Data.Decoder as D"]), + Doc::reflow( + "Notice that the alias always starts with a capital letter. That is required!", + ), + ]); + report + } + Module::ImportIndentExposingList(r, c) => { + let mut report = problem( + "UNFINISHED IMPORT", + r, + c, + "I was parsing an `import` until I got stuck here:", + "I was expecting to see the list of exposed values next.", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "import Data.Decoder exposing (..)", + "import Data.Decoder exposing (decode)", + ]), + Doc::reflow( + "I generally recommend the second style. It is more explicit, making it much easier to figure out where values are coming from in large projects!", + ), + ]); + report + } + Module::ImportStart(r, c) + | Module::ImportAs(r, c) + | Module::ImportExposing(r, c) + | Module::ImportEnd(r, c) + | Module::ImportIndentName(r, c) + | Module::ImportIndentAlias(r, c) => to_import_report(r, c), + Module::Infix(r, c) => problem( + "BAD INFIX", + r, + c, + "Something went wrong in this infix operator declaration:", + "An infix declaration gives associativity, precedence, an operator in parentheses, and its implementation name. For example: `infix left 6 (+) = add`.", + ), + Module::Declarations(e, _, _) => decl::to_declarations_report(source, e), + Module::Tests(e, r, c) => to_tests_report(source, e, r, c), + } +} + +fn examples(lines: &[&str]) -> Doc { + Doc::indent(4, Doc::vcat(lines.iter().map(|s| Doc::text(*s)))) +} +fn to_import_report(r: Row, c: Col) -> Report { + let mut report = problem( + "UNFINISHED IMPORT", + r, + c, + "I am partway through parsing an import, but I got stuck here:", + "Here are some examples of valid `import` declarations:", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "import Cardano.Tx", + "import Cardano.Tx as Tx", + "import Cardano.Tx as Tx exposing (..)", + "import Data.Decoder exposing (decode)", + ]), + Doc::reflow( + "You are probably trying to import a different module, but try to make it look like one of these examples!", + ), + ]); + report +} + +pub(crate) fn to_weird_end_report(source: &Source<'_>, r: Row, c: Col) -> Report { + match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow("I got stuck on this reserved word:"), + Doc::reflow(&format!( + "The name `{k}` is reserved, so try using a different name?" + )), + ), + Next::Operator(op) => Report::snippet( + "UNEXPECTED SYMBOL", + to_keyword_region(r, c, op), + None, + Doc::reflow("I ran into an unexpected symbol:"), + Doc::reflow(&format!( + "I was not expecting to see a {op} here. Try deleting it? Maybe I can give a better hint from there?" + )), + ), + Next::Close(term, ch) => problem( + &format!("UNEXPECTED {}", term.to_uppercase()), + r, + c, + &format!("I ran into an unexpected {term}:"), + &format!("This {ch} does not match up with an earlier open {term}. Try deleting it?"), + ), + Next::Lower(name) | Next::Upper(name) => Report::snippet( + "UNEXPECTED NAME", + to_keyword_region(r, c, name), + None, + Doc::reflow("I got stuck on this name:"), + Doc::reflow( + "It is confusing me a lot! Normally I can give fairly specific hints, but something is really tripping me up this time.", + ), + ), + Next::Other(Some(';')) => { + let mut report = problem( + "UNEXPECTED SEMICOLON", + r, + c, + "I got stuck on this semicolon:", + "Try removing it?", + ); + report.after = Doc::stack([ + report.after, + Doc::to_simple_note( + "Some languages require semicolons at the end of each statement. Nash uses indentation to separate declarations and statements in do blocks, so there is no need to use semicolons to separate them.", + ), + ]); + report + } + Next::Other(Some(',')) => { + let mut report = problem( + "UNEXPECTED COMMA", + r, + c, + "I got stuck on this comma:", + "I do not think I am parsing a list or tuple right now. Try deleting the comma?", + ); + report.after = Doc::stack([ + report.after, + Doc::to_simple_note( + "If this is supposed to be part of a list, the problem may be a bit earlier. Perhaps the opening [ is missing? Or perhaps some value in the list has an extra closing ] that is making me think the list ended earlier? The same kinds of things could be going wrong if this is supposed to be a tuple.", + ), + ]); + report + } + Next::Other(Some('`')) => { + let mut report = problem( + "UNEXPECTED CHARACTER", + r, + c, + "I got stuck on this character:", + "It is not used for anything in Nash syntax. It is used for multi-line strings in some languages though, so if you want a string that spans multiple lines, you can use Nash's multi-line string syntax like this:", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "\"\"\"", + "# Multi-line Strings", + "", + "- start with triple double quotes", + "- write whatever you want", + "- no need to escape newlines or double quotes", + "- end with triple double quotes", + "\"\"\"", + ]) + .dullyellow(), + Doc::reflow( + "Otherwise I do not know what is going on! Try removing the character?", + ), + ]); + report + } + Next::Other(Some(ch)) => problem( + "UNEXPECTED CHARACTER", + r, + c, + "I got stuck on this character:", + &format!("It is not a character I expect here (`{ch}`). Try deleting it?"), + ), + Next::Other(None) => problem( + "UNFINISHED FILE", + r, + c, + "I got to the end of the file, but I was expecting more.", + "Maybe a declaration or an expression is incomplete?", + ), + } +} + +pub(super) fn to_exposing_report( + source: &Source<'_>, + error: &Exposing, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + Exposing::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Exposing::Start(r, c) => { + let mut report = problem( + "PROBLEM IN EXPOSING", + r, + c, + "I want to parse exposed values, but I am getting stuck here:", + "Exposed values are always surrounded by parentheses. So try adding a ( here?", + ); + report.after = Doc::stack([ + report.after, + Doc::to_simple_note("Here are some valid examples of `exposing` for reference:"), + examples(&[ + "import Data.Decoder exposing (..)", + "import Data.Decoder exposing (decode)", + ]), + Doc::reflow( + "If you are getting tripped up, you can just expose everything for now. It should get easier to make an explicit exposing list as you see more examples in the wild.", + ), + ]); + report + } + Exposing::Value(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow("I got stuck on this reserved word:"), + Doc::reflow(&format!( + "It looks like you are trying to expose `{k}` but that is a reserved word. Is there a typo?" + )), + ), + Next::Operator(op) => Report::snippet( + "UNEXPECTED SYMBOL", + to_keyword_region(r, c, op), + None, + Doc::reflow("I got stuck on this symbol:"), + Doc::stack([ + Doc::reflow( + "If you are trying to expose an operator, add parentheses around it like this:", + ), + Doc::indent( + 4, + Doc::cat([ + Doc::text(op).dullyellow(), + Doc::text(" -> "), + Doc::text(format!("({op})")).green(), + ]), + ), + ]), + ), + _ => { + let mut report = problem( + "PROBLEM IN EXPOSING", + r, + c, + "I got stuck while parsing these exposed values:", + "I do not have an exact recommendation, so here are some valid examples of `exposing` for reference:", + ); + report.after = Doc::stack([ + report.after, + examples(&[ + "import Data.Decoder exposing (..)", + "import Basics exposing (type int, type bool(..), (+), not)", + ]), + Doc::reflow( + "These examples show how to expose types, variants, operators, and functions. Everything should be some permutation of these examples, just with different names.", + ), + ]); + report + } + }, + Exposing::Operator(r, c) => problem( + "PROBLEM IN EXPOSING", + r, + c, + "I just saw an open parenthesis, so I was expecting an operator next:", + "It is possible to expose operators, so I was expecting to see something like (+) or (|=) or (||) after I saw that open parenthesis.", + ), + Exposing::OperatorReserved(ref op, r, c) => problem( + "RESERVED SYMBOL", + r, + c, + "I cannot expose this as an operator:", + match op { + BadOperator::Pipe => "Maybe you want (||) instead?", + BadOperator::Equals => "Maybe you want (==) instead?", + BadOperator::HasType => "Maybe you want (::) instead?", + BadOperator::Dot + | BadOperator::Arrow + | BadOperator::FatArrow + | BadOperator::LeftArrow => { + "Try getting rid of this entry? Maybe I can give you a better hint after that?" + } + }, + ), + Exposing::OperatorRightParen(r, c) => problem( + "PROBLEM IN EXPOSING", + r, + c, + "It looks like you are exposing an operator, but I got stuck here:", + "I was expecting to see the closing parenthesis immediately after the operator. Try adding a ) right here?", + ), + Exposing::TypePrivacy(r, c) => { + let mut report = problem( + "PROBLEM EXPOSING CUSTOM TYPE VARIANTS", + r, + c, + "It looks like you are trying to expose the variants of a custom type:", + "You need to write something like Status(..) or Entity(..) though. It is all or nothing, otherwise `case` expressions could miss a variant and crash!", + ); + report.after = Doc::stack([ + report.after, + Doc::to_simple_note( + "It is often best to keep the variants hidden! If someone pattern matches on the variants, it is a MAJOR change if any new variants are added. Suddenly their `case` expressions do not cover all variants! So if you do not need people to pattern match, keep the variants hidden and expose functions to construct values of this type. This way you can add new variants as a MINOR change!", + ), + ]); + report + } + Exposing::TypeName(r, c) => problem( + "EXPECTING TYPE NAME", + r, + c, + "I was parsing an exposed type, but I got stuck here:", + "Write the name of the type after `type`. Use `type name(..)` to expose its constructors as well.", + ), + Exposing::End(r, c) => problem( + "UNFINISHED EXPOSING", + r, + c, + "I was partway through parsing exposed values, but I got stuck here:", + "Maybe there is a comma missing before this?", + ), + Exposing::IndentEnd(r, c) => { + let mut report = problem( + "UNFINISHED EXPOSING", + r, + c, + "I was partway through parsing exposed values, but I got stuck here:", + "I was expecting a closing parenthesis. Try adding a ) right here?", + ); + report.after = Doc::stack([ + report.after, + Doc::to_simple_note( + "I can get confused when there is not enough indentation, so if you already have a closing parenthesis, it probably just needs some spaces in front of it.", + ), + ]); + report + } + Exposing::IndentValue(r, c) => problem( + "UNFINISHED EXPOSING", + r, + c, + "I was partway through parsing exposed values, but I got stuck here:", + "I was expecting another value to expose.", + ), + }; + wide(report, sr, sc) +} + +pub(super) fn to_tests_report(source: &Source<'_>, error: &Tests<'_>, sr: Row, sc: Col) -> Report { + let report = match *error { + Tests::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Tests::Import(e, _, _) => return to_parse_error_report(source, e), + Tests::Test(e, r, c) => return to_test_report(source, e, r, c), + Tests::Start(r, c) | Tests::IndentStart(r, c) => problem( + "UNFINISHED TESTS", + r, + c, + "I started parsing a tests section, but I got stuck here:", + "Add an indented `test` or `prop` declaration. Test imports must come before the declarations.", + ), + Tests::Alignment(indent, r, c) => problem( + "TEST ALIGNMENT", + r, + c, + "This test declaration does not line up with the others:", + &format!("Indent every declaration in this tests section to column {indent}."), + ), + }; + wide(report, sr, sc) +} +pub(super) fn to_test_report(source: &Source<'_>, error: &Test<'_>, sr: Row, sc: Col) -> Report { + let report = match *error { + Test::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Test::Name(ref e, r, c) => return expr::to_string_report(source, e, r, c), + Test::WithinNumber(ref e, r, c) => return expr::to_number_report(source, e, r, c), + Test::Body(e, r, c) => { + return expr::to_do_report(source, expr::Context::InDestruct(sr, sc), e, r, c); + } + Test::Pattern(e, r, c) => { + return pattern::to_pattern_report(source, pattern::PContext::Let, e, r, c); + } + Test::Fuzzer(e, r, c) => { + return expr::to_expr_report(source, expr::Context::InDestruct(sr, sc), e, r, c); + } + Test::NameStart(r, c) | Test::IndentName(r, c) => problem( + "MISSING TEST NAME", + r, + c, + "I was parsing a test declaration, but I got stuck here:", + "Give this test a name in double quotes, such as `test \"adds two numbers\"`.", + ), + Test::OnceOnUnitTest(r, c) => problem( + "UNEXPECTED ONCE", + r, + c, + "I found `once` on a unit test:", + "Unit tests already run once. Remove `once`, or use a property declaration when you need generated inputs.", + ), + Test::WithinOpen(r, c) => problem( + "UNFINISHED TEST BUDGET", + r, + c, + "I saw `within`, but I got stuck here:", + "Put the budget inside parentheses, with a budget kind and an integer limit.", + ), + Test::WithinKind(r, c) => problem( + "UNKNOWN TEST BUDGET", + r, + c, + "I was expecting a test budget kind here:", + "Use `cpu` or `mem` followed by an integer limit.", + ), + Test::WithinDuplicate(r, c) => problem( + "DUPLICATE TEST BUDGET", + r, + c, + "This budget kind has already been specified:", + "Keep one limit for each budget kind in the `within` clause.", + ), + Test::WithinEnd(r, c) => problem( + "UNFINISHED TEST BUDGET", + r, + c, + "I was parsing the `within` clause, but I got stuck here:", + "Separate budget limits with a comma, and close the clause with ).", + ), + Test::Equals(r, c) | Test::IndentEquals(r, c) => problem( + "MISSING TEST EQUALS", + r, + c, + "I have the test name, but I got stuck here:", + "Add an = before the test body.", + ), + Test::Do(r, c) | Test::IndentBody(r, c) => problem( + "MISSING TEST BODY", + r, + c, + "I was expecting the test body here:", + "Start the test body with `do`, then indent its statements on the following lines.", + ), + Test::Let(r, c) | Test::IndentBinder(r, c) => problem( + "MISSING PROPERTY BINDER", + r, + c, + "I was parsing generated inputs for a property, but I got stuck here:", + "Start the generated inputs with `let`, then write each pattern followed by `via` and its fuzzer.", + ), + Test::Via(r, c) => problem( + "MISSING FUZZER", + r, + c, + "I have the property input pattern, but I got stuck here:", + "Add `via` followed by the fuzzer expression that generates this input.", + ), + Test::In(r, c) | Test::IndentIn(r, c) => problem( + "MISSING PROPERTY IN", + r, + c, + "I was parsing a property, but I got stuck here:", + "Add `in` after the generated inputs and before the property body.", + ), + Test::BinderAlignment(indent, r, c) => problem( + "PROPERTY BINDER ALIGNMENT", + r, + c, + "This generated input does not line up with the others:", + &format!("Indent each generated input to column {indent}."), + ), + }; + wide(report, sr, sc) +} diff --git a/crates/nash-report/src/syntax/pattern.rs b/crates/nash-report/src/syntax/pattern.rs new file mode 100644 index 00000000..227f2ef6 --- /dev/null +++ b/crates/nash-report/src/syntax/pattern.rs @@ -0,0 +1,307 @@ +use super::{Doc, Report, Source, expr, problem, to_space_report, wide}; +use crate::code::{Next, to_keyword_region, to_wider_region}; +use nash_parse::error::{PList, PRecord, PTuple, Pattern}; +use nash_parse::{Col, Row}; +#[derive(Clone, Copy)] +pub(crate) enum PContext { + Case, + Arg, + Let, +} +pub(crate) fn to_pattern_report( + source: &Source<'_>, + context: PContext, + error: &Pattern<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + Pattern::Record(e, r, c) => return to_p_record_report(source, e, r, c), + Pattern::Tuple(e, r, c) => return to_p_tuple_report(source, context, e, r, c), + Pattern::List(e, r, c) => return to_p_list_report(source, context, e, r, c), + Pattern::String(ref e, r, c) => return expr::to_string_report(source, e, r, c), + Pattern::Bytes(ref e, r, c) => return expr::to_bytes_report(source, e, r, c), + Pattern::Number(ref e, r, c) => return expr::to_number_report(source, e, r, c), + Pattern::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Pattern::Start(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => reserved( + r, + c, + k, + &format!( + "It looks like you are trying to use `{k}` {}:", + match context { + PContext::Arg => "as an argument", + PContext::Case | PContext::Let => "in this pattern", + } + ), + ), + Next::Operator("-") => problem( + "UNEXPECTED SYMBOL", + r, + c, + "I ran into a minus sign unexpectedly in this pattern:", + "It is not possible to pattern match on negative numbers at this time. Try using an `if` expression for that sort of thing for now.", + ), + _ => problem( + "PROBLEM IN PATTERN", + r, + c, + "I wanted to parse a pattern next, but I got stuck here:", + "I am not sure why I am getting stuck exactly. I just know that I want a pattern next. Something as simple as maybeHeight or result would work!", + ), + }, + Pattern::Alias(r, c) | Pattern::IndentAlias(r, c) => problem( + "UNFINISHED PATTERN", + r, + c, + "I was expecting to see a variable name after the `as` keyword:", + "The `as` keyword lets you write patterns like ((x,y) as point) so you can refer to individual parts of the tuple with x and y or you refer to the whole thing with point. So I was expecting to see a variable name after the `as` keyword here. Sometimes people just want to use `as` as a variable name though. Try using a different name in that case!", + ), + Pattern::WildcardNotVar(name, width, r, c) => { + let stripped = name.trim_start_matches('_'); + let example = stripped + .chars() + .next() + .map(|ch| ch.to_lowercase().to_string() + &stripped[ch.len_utf8()..]) + .unwrap_or_else(|| "x or age".into()); + Report::snippet( + "UNEXPECTED NAME", + to_wider_region(r, c, u16::try_from(width).unwrap_or(1)), + None, + Doc::reflow("Variable names cannot start with underscores like this:"), + Doc::reflow(&format!( + "You can either have an underscore like _ to ignore the value, or you can have a name like {example} to use the matched value." + )), + ) + } + Pattern::IndentStart(r, c) => indent_note( + problem( + "UNFINISHED PATTERN", + r, + c, + "I wanted to parse a pattern next, but I got stuck here:", + "I am not sure why I am getting stuck exactly. I just know that I want a pattern next. Something as simple as maybeHeight or result would work!", + ), + "I can get confused by indentation. If you think there is a pattern next, maybe it needs to be indented a bit more?", + ), + }; + wide(report, sr, sc) +} +fn reserved(r: Row, c: Col, k: &str, before: &str) -> Report { + Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow(before), + Doc::reflow("This is a reserved word! Try using some other name?"), + ) +} +fn indent_note(mut report: Report, note: &str) -> Report { + report.after = Doc::stack([report.after, Doc::to_simple_note(note)]); + report +} +pub(super) fn to_p_record_report(source: &Source<'_>, error: &PRecord, sr: Row, sc: Col) -> Report { + let report = match *error { + PRecord::Space(ref e, r, c) => return to_space_report(source, e, r, c), + PRecord::Open(r, c) | PRecord::IndentOpen(r, c) | PRecord::IndentField(r, c) => { + to_unfinish_record_pattern_report(r, c, "I was expecting to see a field name next.") + } + PRecord::End(r, c) | PRecord::IndentEnd(r, c) => to_unfinish_record_pattern_report( + r, + c, + "I was expecting to see a closing curly brace next. Try adding a } here?", + ), + PRecord::Field(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow(&format!( + "I was not expecting to see `{k}` as a record field name:" + )), + Doc::reflow( + "This is a reserved word, not available for variable names. Try another name!", + ), + ), + _ => { + to_unfinish_record_pattern_report(r, c, "I was expecting to see a field name next.") + } + }, + }; + wide(report, sr, sc) +} +fn to_unfinish_record_pattern_report(r: Row, c: Col, message: &str) -> Report { + let mut report = problem( + "UNFINISHED RECORD PATTERN", + r, + c, + "I was partway through parsing a record pattern, but I got stuck here:", + message, + ); + report.after = Doc::stack([ + report.after, + Doc::to_simple_hint( + "A record pattern looks like {x,y} or {name,age} where you list the field names you want to access.", + ), + ]); + report +} +pub(super) fn to_p_tuple_report( + source: &Source<'_>, + context: PContext, + error: &PTuple<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + PTuple::Space(ref e, r, c) => return to_space_report(source, e, r, c), + PTuple::Expr(e, r, c) => return to_pattern_report(source, context, e, r, c), + PTuple::Open(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => reserved( + r, + c, + k, + &format!("It looks like you are trying to use `{k}` as a variable name:"), + ), + _ => problem( + "UNFINISHED PARENTHESES", + r, + c, + "I just saw an open parenthesis, but I got stuck here:", + "I was expecting to see a pattern next. Maybe it will end up being something like (x,y) or (name, _)?", + ), + }, + PTuple::End(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow("I ran into a reserved word in this pattern:"), + Doc::reflow(&format!( + "The `{k}` keyword is reserved. Try using a different name instead!" + )), + ), + Next::Operator(op) => Report::snippet( + "UNEXPECTED SYMBOL", + to_keyword_region(r, c, op), + None, + Doc::reflow(&format!( + "I ran into the {op} symbol unexpectedly in this pattern:" + )), + Doc::reflow( + "Only the :: symbol works in patterns. It is useful if you are pattern matching on lists, trying to get the first element off the front. Did you want that instead?", + ), + ), + Next::Close(term, ch) => problem( + &format!("STRAY {}", term.to_uppercase()), + r, + c, + &format!("I ran into an unexpected {term} in this pattern:"), + &format!( + "This {ch} does not match up with an earlier open {term}. Try deleting it?" + ), + ), + _ => problem( + "UNFINISHED PARENTHESES", + r, + c, + "I was partway through parsing a pattern, but I got stuck here:", + "I was expecting a closing parenthesis next, so try adding a ) to see if that helps?", + ), + }, + PTuple::IndentEnd(r, c) => indent_note( + problem( + "UNFINISHED PARENTHESES", + r, + c, + "I was expecting a closing parenthesis next:", + "Try adding a ) to see if that helps?", + ), + "I can get confused by indentation in cases like this, so maybe you have a closing parenthesis but it is not indented enough?", + ), + PTuple::IndentExpr1(r, c) => problem( + "UNFINISHED PARENTHESES", + r, + c, + "I just saw an open parenthesis, but then I got stuck here:", + "I was expecting to see a pattern next. Maybe it will end up being something like (x,y) or (name, _)?", + ), + PTuple::IndentExprN(r, c) => indent_note( + problem( + "UNFINISHED TUPLE PATTERN", + r, + c, + "I am partway through parsing a tuple pattern, but I got stuck here:", + "I was expecting to see a pattern next. I am expecting the final result to be something like (x,y) or (name, _).", + ), + "I can get confused by indentation in cases like this, so the problem may be that the next part is not indented enough?", + ), + }; + wide(report, sr, sc) +} +pub(super) fn to_p_list_report( + source: &Source<'_>, + context: PContext, + error: &PList<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + PList::Space(ref e, r, c) => return to_space_report(source, e, r, c), + PList::Expr(e, r, c) => return to_pattern_report(source, context, e, r, c), + PList::Open(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => reserved( + r, + c, + k, + &format!("It looks like you are trying to use `{k}` to name an element of a list:"), + ), + _ => problem( + "UNFINISHED LIST PATTERN", + r, + c, + "I just saw an open square bracket, but then I got stuck here:", + "Try adding a ] to see if that helps?", + ), + }, + PList::End(r, c) => problem( + "UNFINISHED LIST PATTERN", + r, + c, + "I was expecting a closing square bracket to end this list pattern:", + "Try adding a ] to see if that helps?", + ), + PList::IndentOpen(r, c) => indent_note( + problem( + "UNFINISHED LIST PATTERN", + r, + c, + "I just saw an open square bracket, but then I got stuck here:", + "Try adding a ] to see if that helps?", + ), + "I can get confused by indentation in cases like this, so maybe there is something next, but it is not indented enough?", + ), + PList::IndentEnd(r, c) => indent_note( + problem( + "UNFINISHED LIST PATTERN", + r, + c, + "I was expecting a closing square bracket to end this list pattern:", + "Try adding a ] to see if that helps?", + ), + "I can get confused by indentation in cases like this, so maybe you have a closing square bracket but it is not indented enough?", + ), + PList::IndentExpr(r, c) => indent_note( + problem( + "UNFINISHED LIST PATTERN", + r, + c, + "I am partway through parsing a list pattern, but I got stuck here:", + "I was expecting to see another pattern next. Maybe a variable name.", + ), + "I can get confused by indentation in cases like this, so maybe there is more to this pattern but it is not indented enough?", + ), + }; + wide(report, sr, sc) +} diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentbody.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentbody.snap new file mode 100644 index 00000000..4a00a066 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentbody.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Assert(&Keyword::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing an `assert` expression, but I got stuck here: + +I was expecting to see an expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentmessage.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentmessage.snap new file mode 100644 index 00000000..351cc283 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__assert_indentmessage.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Assert(&Keyword::IndentMessage(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing an `assert` expression, but I got stuck here: + +I was expecting to see a message expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_bad_hex.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_bad_hex.snap new file mode 100644 index 00000000..3a80ec8c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_bad_hex.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Bytes(Bytes::BadHexDigit(12), 1, 12), \"value = #\\\"ag\\\"\")" +--- +BAD BYTE STRING +Region { start: Position { line: 1, column: 12 }, end: Position { line: 1, column: 12 } } + +I ran into an invalid hexadecimal digit in this byte string: + +Use pairs of digits from 0123456789abcdefABCDEF, one pair for each byte. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_endless.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_endless.snap new file mode 100644 index 00000000..fea55b81 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_endless.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Bytes(Bytes::Endless, 1, 12), \"value = #\\\"aa\")" +--- +ENDLESS BYTE STRING +Region { start: Position { line: 1, column: 12 }, end: Position { line: 1, column: 12 } } + +I cannot find the end of this byte string: + +Add a closing double quote to end the byte string. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_odd.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_odd.snap new file mode 100644 index 00000000..76a3b547 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__bytes_odd.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Bytes(Bytes::OddLength, 1, 12), \"value = #\\\"a\\\"\")" +--- +INCOMPLETE BYTE +Region { start: Position { line: 1, column: 12 }, end: Position { line: 1, column: 12 } } + +This byte string has an odd number of hexadecimal digits: + +Each byte needs two hexadecimal digits. Add the missing digit or remove the +extra one. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_branch_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_branch_arrow.snap new file mode 100644 index 00000000..c8b40b7d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_branch_arrow.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Branch(&Expr::OperatorReserved(BadOperator::Arrow,\n1, 9), 1, 9), 1, 9), \"value = ->\")" +--- +UNEXPECTED ARROW +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I am parsing a `case` expression right now, but this arrow is confusing me: + +Maybe this branch is not indented enough? Each pattern must line up with the +other patterns. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_colon_instead_of_cons.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_colon_instead_of_cons.snap new file mode 100644 index 00000000..e374d9b5 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_colon_instead_of_cons.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Arrow(1, 9), 1, 9), \"value = :\")" +--- +UNEXPECTED OPERATOR +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I am seeing : but maybe you want :: instead? + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_equals_instead_of_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_equals_instead_of_arrow.snap new file mode 100644 index 00000000..afe48eb1 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_equals_instead_of_arrow.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Arrow(1, 9), 1, 9), \"value = =\")" +--- +UNEXPECTED OPERATOR +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I am seeing = but maybe you want -> instead? + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_arrow.snap new file mode 100644 index 00000000..6a8501de --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_arrow.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::IndentArrow(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED CASE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I was expecting to see an arrow next. It may need more indentation. + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_branch.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_branch.snap new file mode 100644 index 00000000..13a5d34a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_branch.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::IndentBranch(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED CASE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I was expecting to see an expression next. What should I do when I run into this +particular pattern? + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_expr.snap new file mode 100644 index 00000000..02810d08 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_expr.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::IndentExpr(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED CASE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I was expecting to see an expression next. + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_pattern.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_pattern.snap new file mode 100644 index 00000000..db519f0b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_indent_pattern.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::IndentPattern(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED CASE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I was expecting to see a pattern next. + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_arrow.snap new file mode 100644 index 00000000..37755bb3 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_arrow.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Arrow(1, 9), 1, 9), \"value = \")" +--- +MISSING ARROW +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I was expecting to see an arrow next. + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_of.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_of.snap new file mode 100644 index 00000000..c67bb8fa --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_missing_of.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Of(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED CASE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I was expecting to see the `of` keyword next. + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_pattern_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_pattern_alignment.snap new file mode 100644 index 00000000..a75f7e4f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_pattern_alignment.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::PatternAlignment(4, 1, 9), 1, 9), \"value = \")" +--- +UNFINISHED CASE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I suspect this is a pattern that is not indented far enough? (4 spaces) + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_reserved_pattern.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_reserved_pattern.snap new file mode 100644 index 00000000..0f9359a0 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_reserved_pattern.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Arrow(1, 9), 1, 9), \"value = if\")" +--- +RESERVED WORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +It looks like you are trying to use `if` in one of your patterns, but it is a +reserved word. Try using a different name? + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_subject_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_subject_arrow.snap new file mode 100644 index 00000000..bf757f07 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_subject_arrow.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Expr(&Expr::OperatorReserved(BadOperator::Arrow, 1,\n9), 1, 9), 1, 9), \"value = ->\")" +--- +UNEXPECTED ARROW +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I am parsing a `case` expression right now, but this arrow is confusing me: + +Maybe the `of` keyword is missing on a previous line? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_wrong_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_wrong_arrow.snap new file mode 100644 index 00000000..5d7d83a4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__case_wrong_arrow.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Case(&Case::Arrow(1, 30), 1, 9),\n\"value = case x of Some width =\")" +--- +UNEXPECTED OPERATOR +Region { start: Position { line: 1, column: 30 }, end: Position { line: 1, column: 30 } } + +I was partway through parsing a `case` expression, but I got stuck here: + +I am seeing = but maybe you want -> instead? + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + +Notice the indentation. Each pattern is aligned, and each branch is indented a +bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentbody.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentbody.snap new file mode 100644 index 00000000..e86cadec --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentbody.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Comptime(&Keyword::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `comptime` expression, but I got stuck here: + +I was expecting to see an expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentmessage.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentmessage.snap new file mode 100644 index 00000000..039169cb --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__comptime_indentmessage.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Comptime(&Keyword::IndentMessage(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `comptime` expression, but I got stuck here: + +I was expecting to see a message expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_alignment.snap new file mode 100644 index 00000000..1fe4078c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_alignment.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::Alignment(4, 1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +PROBLEM IN DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I just saw a type annotation indented 4 spaces, so I was expecting to see the +corresponding definition next with the exact same amount of indentation. + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_equals.snap new file mode 100644 index 00000000..d15229fd --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_equals.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::Equals(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +PROBLEM IN DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I was expecting to see an argument or an equals sign next. + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_equals.snap new file mode 100644 index 00000000..22fa4f4a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_equals.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::IndentEquals(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I was expecting to see an argument or an equals sign next. It may need more +indentation. + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_type.snap new file mode 100644 index 00000000..de842f04 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_indent_type.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::IndentType(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I just saw a colon, so I am expecting to see a type next. It may need more +indentation. + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_missing_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_missing_colon.snap new file mode 100644 index 00000000..8221198d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_missing_colon.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::Equals(1, 9), 1, 9), 1, 9),\n\"value = ->\")" +--- +MISSING COLON? +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I was not expecting to see an arrow here. Maybe this is a type annotation +missing its colon? + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_mismatch.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_mismatch.snap new file mode 100644 index 00000000..f5cb4d55 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_mismatch.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::NameMatch(\"y\", 1, 9), 1, 9), 1, 9),\n\"value = y\")" +--- +NAME MISMATCH +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 10 } } + +I just saw a type annotation for `x`, but it is followed by a definition for +`y`: + +These names do not match! Is there a typo? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_repeat.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_repeat.snap new file mode 100644 index 00000000..2dbc6e23 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_name_repeat.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::NameRepeat(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +EXPECTING DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I just saw the type annotation for `x` so I was expecting to see its definition +here. Type annotations always appear directly above the relevant definition, +without anything else in between. + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_reserved_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_reserved_arg.snap new file mode 100644 index 00000000..e857f754 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__def_reserved_arg.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::Equals(1, 9), 1, 9), 1, 9),\n\"value = if\")" +--- +RESERVED WORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +The name `if` is reserved, so it cannot be used as an argument: + +Try renaming it to something else. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_body.snap new file mode 100644 index 00000000..f744a5c0 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_body.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Destruct(&Destruct::IndentBody(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing this definition, but I got stuck here: + +I was expecting to see an expression next. What is it equal to? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_equals.snap new file mode 100644 index 00000000..5c43f55a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_indent_equals.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Destruct(&Destruct::IndentEquals(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing this definition, but I got stuck here: + +I was expecting to see an equals sign next, followed by an expression telling me +what to compute. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_type_annotation.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_type_annotation.snap new file mode 100644 index 00000000..ea30b086 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__destruct_type_annotation.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Destruct(&Destruct::Equals(1, 9), 1, 9), 1, 9),\n\"value = :\")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing this definition, but I got stuck here: + +I was expecting to see an equals sign next, followed by an expression telling me +what to compute. Destructuring definitions cannot have type annotations. Put the +annotation on a named value instead. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_alignment.snap new file mode 100644 index 00000000..c85944d7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_alignment.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Do(&Do::Alignment(4, 1, 9), 1, 9), \"value = \")" +--- +UNFINISHED DO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `do` block, but I got stuck here: + +Statements in this `do` block must line up with 4 spaces of indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_arrow.snap new file mode 100644 index 00000000..6915653b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_arrow.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Do(&Do::Arrow(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED DO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `do` block, but I got stuck here: + +I was expecting to see <- after this binding pattern. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_arrow.snap new file mode 100644 index 00000000..25e30074 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_arrow.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Do(&Do::IndentArrow(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED DO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `do` block, but I got stuck here: + +I was expecting to see <- after this binding pattern. It may need more +indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_expr.snap new file mode 100644 index 00000000..27044820 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_expr.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Do(&Do::IndentExpr(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED DO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `do` block, but I got stuck here: + +I was expecting an expression after <-. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_stmt.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_stmt.snap new file mode 100644 index 00000000..ab35893a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_indent_stmt.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Do(&Do::IndentStmt(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED DO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `do` block, but I got stuck here: + +I was expecting an indented statement after `do`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_requires_result.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_requires_result.snap new file mode 100644 index 00000000..821b4a5a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__do_requires_result.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Do(&Do::LastNotExpr(2, 5), 1, 9), \"value = do\\n let x = 1\")" +--- +UNFINISHED DO +Region { start: Position { line: 2, column: 5 }, end: Position { line: 2, column: 5 } } + +I was partway through parsing a `do` block, but I got stuck here: + +A `do` block must end with an expression. Add the final expression after this +binding. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_bad_unicode.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_bad_unicode.snap new file mode 100644 index 00000000..7114fc2a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_bad_unicode.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::String(StringError::Escape(Escape::BadUnicodeFormat(3)), 1, 9),\n\"value = \")" +--- +BAD UNICODE ESCAPE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 12 } } + +I ran into an invalid Unicode escape: + +Valid Unicode escapes include \u{0041}, \u{03BB}, and \u{1F60A}. Notice that the +code point is always surrounded by curly braces. Maybe you are missing the +opening or closing curly brace? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_short_unicode.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_short_unicode.snap new file mode 100644 index 00000000..f5d637ac --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_short_unicode.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::String(StringError::Escape(Escape::BadUnicodeLength\n{ code: 5, expected: 4, actual: 1 }), 1, 9), \"value = \")" +--- +BAD UNICODE ESCAPE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 14 } } + +This code point has the wrong number of digits: + +I expected 4 digits, but found 1. Unicode escapes need between four and six +hexadecimal digits. Add leading zeros if there are too few, or trim leading +zeros if there are too many. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_unknown.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_unknown.snap new file mode 100644 index 00000000..e9df26fe --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__escape_unknown.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::String(StringError::Escape(Escape::Unknown), 1, 9), \"value = \")" +--- +UNKNOWN ESCAPE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +Backslashes always start escaped characters, but I do not recognize this one: + +Valid escape characters include \n, \r, \t, \", \', \\, and \u{003D}. Do you +want one of those instead? Maybe you need \\ to escape a backslash? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_access_upper.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_access_upper.snap new file mode 100644 index 00000000..2f1273ee --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_access_upper.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Access(1, 9), \"value = \")" +--- +EXPECTING RECORD ACCESSOR +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I am trying to parse a record accessor here: + +Something like .name or .price that accesses a value from a record. Record field +names must start with a lower case letter! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_dot_without_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_dot_without_name.snap new file mode 100644 index 00000000..8b85c67f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_dot_without_name.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Dot(1, 9), \"value = \")" +--- +EXPECTING RECORD ACCESSOR +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see a record accessor here: + +Something like .name or .price that accesses a value from a record. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_start_bad.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_start_bad.snap new file mode 100644 index 00000000..09db11da --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__expr_start_bad.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Start(1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing the `value` definition, but I got stuck here: + +I was expecting to see an expression like 42 or "hello". Once there is something +there, I can probably give a more specific hint! This can also happen if I run +into reserved words like `let` or `as` unexpectedly, or operators in unexpected +spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentbody.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentbody.snap new file mode 100644 index 00000000..60a1306c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentbody.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Fail(&Keyword::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `fail` expression, but I got stuck here: + +I was expecting to see an expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentmessage.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentmessage.snap new file mode 100644 index 00000000..b432859a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__fail_indentmessage.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Fail(&Keyword::IndentMessage(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `fail` expression, but I got stuck here: + +I was expecting to see a message expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arg.snap new file mode 100644 index 00000000..52a1e970 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arg.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Func(&Func::IndentArg(1, 9), 1, 9), \"value = \")" +--- +MISSING ARGUMENT +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing an anonymous function, but I got stuck here: + +I just saw the beginning of an anonymous function, so I was expecting to see an +argument next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arrow.snap new file mode 100644 index 00000000..34507226 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_arrow.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Func(&Func::IndentArrow(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED ANONYMOUS FUNCTION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing an anonymous function, but I got stuck here: + +I was expecting to see an arrow next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_body.snap new file mode 100644 index 00000000..0a311eb7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_indent_body.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Func(&Func::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED ANONYMOUS FUNCTION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing an anonymous function, but I got stuck here: + +I was expecting to see an expression after the arrow. It may need more +indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_missing_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_missing_arrow.snap new file mode 100644 index 00000000..779138b0 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_missing_arrow.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Func(&Func::Arrow(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED ANONYMOUS FUNCTION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing an anonymous function, but I got stuck here: + +I was expecting to see an arrow next. The syntax for anonymous functions is +\name -> name. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_reserved_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_reserved_arg.snap new file mode 100644 index 00000000..83ab179d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__func_reserved_arg.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Func(&Func::Arrow(1, 9), 1, 9), \"value = if\")" +--- +RESERVED WORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I was parsing an anonymous function, but I got stuck here: + +It looks like you are trying to use `if` as an argument, but it is a reserved +word in this language. Try using a different argument name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_else_branch_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_else_branch_start.snap new file mode 100644 index 00000000..48650d49 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_else_branch_start.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::ElseBranchStart(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see an expression next. Maybe the `else` branch is not filled +in yet? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_condition.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_condition.snap new file mode 100644 index 00000000..40345ce4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_condition.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::IndentCondition(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see a condition next. If it is already present, it may not be +indented enough for me to recognize it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else.snap new file mode 100644 index 00000000..06778cfe --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::IndentElse(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see an `else` branch after this. All `if` expressions need +both branches. Check the indentation if the branch is already present. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else_branch.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else_branch.snap new file mode 100644 index 00000000..7fad322f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_else_branch.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::IndentElseBranch(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see an expression next. If the `else` branch is already +present, it may not be indented enough for me to recognize it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then.snap new file mode 100644 index 00000000..f204de87 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::IndentThen(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see the `then` keyword next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then_branch.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then_branch.snap new file mode 100644 index 00000000..3bfe2717 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_indent_then_branch.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::IndentThenBranch(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see an expression next. If the `then` branch is already +present, it may not be indented enough for me to recognize it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_misindented_else.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_misindented_else.snap new file mode 100644 index 00000000..52375d8e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_misindented_else.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::IndentElse(1, 25), 1, 9),\n\"value = if True then 1\\nelse 2\")" +--- +WEIRD ELSE BRANCH +Region { start: Position { line: 2, column: 1 }, end: Position { line: 2, column: 5 } } + +I was partway through an `if` expression when I got stuck here: + +I think this `else` keyword needs to be indented more. Try adding some spaces +before it! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_missing_then.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_missing_then.snap new file mode 100644 index 00000000..f0fe32b6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__if_missing_then.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::Then(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see the `then` keyword next. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__integer_dot.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__integer_dot.snap new file mode 100644 index 00000000..68b2d6ae --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__integer_dot.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Number(Number::Dot(42), 1, 11), \"value = 42.\")" +--- +WEIRD NUMBER +Region { start: Position { line: 1, column: 11 }, end: Position { line: 1, column: 11 } } + +Numbers cannot end with a dot like this: + +Switching to 42 will work though! Nash has no floating point numbers. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_alignment.snap new file mode 100644 index 00000000..aec769b0 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_alignment.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::DefAlignment(4, 1, 9), 1, 9), \"value = \")" +--- +LET PROBLEM +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `let` expression, but I got stuck here: + +Based on the indentation, I was expecting to see the `in` keyword next. Is there +a typo? This can also happen if you are trying to define another value within +the `let` but it is not indented enough. Make sure each definition has exactly +the same amount of spaces before it. They should line up exactly! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_indent_body.snap new file mode 100644 index 00000000..7989ffe4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_indent_body.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Def(\"x\", &Def::IndentBody(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got stuck while parsing the `x` definition: + +I was expecting to see an expression next. What is it equal to? + + greet : string -> string + greet name = + "Hello " ++ name + +The top line is an optional type annotation. It works as compiler-verified +documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_name.snap new file mode 100644 index 00000000..44b2f9d8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_def_name.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::DefName(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LET +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `let` expression, but I got stuck here: + +I was expecting the name of a definition next. + + let + fullName = + first ++ " " ++ last + in + fullName + +The definition is indented more than the `let` keyword, and its value is +indented a bit more than that. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_destruct_missing_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_destruct_missing_equals.snap new file mode 100644 index 00000000..e856fd3b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_destruct_missing_equals.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::Destruct(&Destruct::Equals(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +UNFINISHED DEFINITION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing this definition, but I got stuck here: + +I was expecting to see an equals sign next, followed by an expression telling me +what to compute. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_body.snap new file mode 100644 index 00000000..d0417870 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_body.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LET +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `let` expression, but I got stuck here: + +I was expecting an expression next. Tell me what should happen with the value +you just defined! + + let + fullName = + first ++ " " ++ last + in + fullName + +The definition is indented more than the `let` keyword, and its value is +indented a bit more than that. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_def.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_def.snap new file mode 100644 index 00000000..ac693f6e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_indent_def.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::IndentDef(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LET +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `let` expression, but I got stuck here: + +I was expecting a value to be defined here. It may need more indentation. + + let + fullName = + first ++ " " ++ last + in + fullName + +The definition is indented more than the `let` keyword, and its value is +indented a bit more than that. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_missing_in.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_missing_in.snap new file mode 100644 index 00000000..9a58e63b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_missing_in.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::In(1, 9), 1, 9), \"value = \")" +--- +LET PROBLEM +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `let` expression, but I got stuck here: + +Based on the indentation, I was expecting to see the `in` keyword next. Is there +a typo? This can also happen if you are trying to define another value within +the `let` but it is not indented enough. Make sure each definition has exactly +the same amount of spaces before it. They should line up exactly! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_reserved_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_reserved_name.snap new file mode 100644 index 00000000..ad6b118b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__let_reserved_name.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Let(&Let::DefName(1, 9), 1, 9), \"value = if\")" +--- +RESERVED WORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I was partway through parsing a `let` expression, but I got stuck here: + +It looks like you are trying to use `if` as a variable name, but it is a +reserved word! Try using a different name instead. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_end.snap new file mode 100644 index 00000000..e00c3f13 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_end.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::List(&List::IndentEnd(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LIST +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a list, but I got stuck here: + +I cannot find the end of this list. Try adding a ] or indenting the closing +bracket more. + + [ 1 + , 2 + ] + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_expr.snap new file mode 100644 index 00000000..f2a5e706 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_expr.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::List(&List::IndentExpr(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LIST +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a list, but I got stuck here: + +I was expecting to see another list entry after this comma. Trailing commas are +not allowed in lists, so the fix may be to delete the comma? + + [ 1 + , 2 + ] + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_open.snap new file mode 100644 index 00000000..c35570e8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_indent_open.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::List(&List::IndentOpen(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LIST +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a list, but I got stuck here: + +I cannot find the end of this list. Try adding a ] or indenting the list entries +more. + + [ 1 + , 2 + ] + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_missing_end.snap new file mode 100644 index 00000000..dce53976 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_missing_end.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::List(&List::End(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LIST +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a list, but I got stuck here: + +I was expecting a comma or a closing square bracket next. + + [ 1 + , 2 + ] + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_open.snap new file mode 100644 index 00000000..503c5657 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_open.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::List(&List::Open(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED LIST +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a list, but I got stuck here: + +I was expecting an expression or a closing square bracket next. + + [ 1 + , 2 + ] + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_trailing_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_trailing_comma.snap new file mode 100644 index 00000000..564740da --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__list_trailing_comma.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::List(&List::Expr(&Expr::Start(1, 13), 1, 13), 1, 9),\n\"value = [1, ]\")" +--- +UNFINISHED LIST +Region { start: Position { line: 1, column: 13 }, end: Position { line: 1, column: 13 } } + +I was partway through parsing a list, but I got stuck here: + +Trailing commas are not allowed in lists, so the fix may be to delete the comma? + + [ 1 + , 2 + ] + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_arg.snap new file mode 100644 index 00000000..3cc05a51 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_arg.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Macro(&Macro::IndentArg(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED MACRO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a macro invocation, but I got stuck here: + +I was expecting a macro argument next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_end.snap new file mode 100644 index 00000000..88106634 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_indent_end.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Macro(&Macro::IndentEnd(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED MACRO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a macro invocation, but I got stuck here: + +I was expecting a closing parenthesis. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_arg.snap new file mode 100644 index 00000000..19d74699 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_arg.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Macro(&Macro::Arg(&Expr::Start(1, 9), 1, 9), 1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a macro invocation, but I got stuck here: + +I was expecting to see an expression like 42 or "hello". Once there is something +there, I can probably give a more specific hint! This can also happen if I run +into reserved words like `let` or `as` unexpectedly, or operators in unexpected +spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_close.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_close.snap new file mode 100644 index 00000000..18d2191c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_missing_close.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Macro(&Macro::End(1, 15), 1, 9), \"value = foo!(1\")" +--- +UNFINISHED MACRO +Region { start: Position { line: 1, column: 15 }, end: Position { line: 1, column: 15 } } + +I was partway through parsing a macro invocation, but I got stuck here: + +I was expecting a comma or a closing parenthesis after this macro argument. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_open.snap new file mode 100644 index 00000000..d93abf95 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__macro_open.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Macro(&Macro::Open(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED MACRO +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a macro invocation, but I got stuck here: + +I was expecting an opening parenthesis after the macro's ! marker. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_else.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_else.snap new file mode 100644 index 00000000..49711388 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_else.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::If(&If::Else(1, 24), 1, 9), \"value = if True then 42\")" +--- +UNFINISHED IF +Region { start: Position { line: 1, column: 24 }, end: Position { line: 1, column: 24 } } + +I was expecting to see more of this `if` expression, but I got stuck here: + +I was expecting to see the `else` keyword next. All `if` expressions need an +`else` branch. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_operand.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_operand.snap new file mode 100644 index 00000000..3500b815 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__missing_operand.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorRight(\"+\", 1, 13), \"value = 1 + \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 13 }, end: Position { line: 1, column: 13 } } + +I just saw a + sign, so I am getting stuck here: + +I was expecting to see an expression next. Something like 42 or 1000 that makes +sense with a + sign. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_bad_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_bad_end.snap new file mode 100644 index 00000000..3fd249d8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_bad_end.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Number(Number::End, 1, 9), \"value = \")" +--- +WEIRD NUMBER +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I thought I was reading a number, but I ran into some weird stuff here: + +I recognize integers like 42 and 0x002B. Is there a way to write it like one of +those? Nash has no floating point numbers. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_hex_digit.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_hex_digit.snap new file mode 100644 index 00000000..343c3d48 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_hex_digit.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Number(Number::HexDigit, 1, 9), \"value = \")" +--- +WEIRD HEXADECIMAL +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I thought I was reading a hexadecimal number until I got here: + +Valid hexadecimal digits include 0123456789abcdefABCDEF, so I can only recognize +things like 0x2B, 0x002B, or 0x00ffb3. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_no_leading_zero.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_no_leading_zero.snap new file mode 100644 index 00000000..c60987ea --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__number_no_leading_zero.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Number(Number::NoLeadingZero, 1, 9), \"value = \")" +--- +LEADING ZEROS +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I do not accept numbers with leading zeros: + +Just delete the leading zeros and it should work! Some languages use a leading +zero to specify octal numbers. Nash avoids this ambiguity. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_boolean.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_boolean.snap new file mode 100644 index 00000000..ab263e46 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_boolean.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorRight(\"&&\", 1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I just saw a && operator, so I am getting stuck here: + +I was expecting to see an expression next. Something like True or False that +makes sense with boolean logic. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_custom_operator.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_custom_operator.snap new file mode 100644 index 00000000..c91257b3 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_custom_operator.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorRight(\"++\", 1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I just saw a ++ operator, so I am getting stuck here: + +I was expecting to see an expression next. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_pipe.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_pipe.snap new file mode 100644 index 00000000..143f97da --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_pipe.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorRight(\"|>\", 1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I just saw a |> operator, so I am getting stuck here: + +I was expecting to see a function next. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_reverse_pipe.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_reverse_pipe.snap new file mode 100644 index 00000000..995a994b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operand_reverse_pipe.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorRight(\"<|\", 1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I just saw a <| operator, so I am getting stuck here: + +I was expecting to see an argument next. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_dot.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_dot.snap new file mode 100644 index 00000000..93189238 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_dot.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::Dot, 1, 9), \"value = \")" +--- +UNEXPECTED SYMBOL +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 10 } } + +I was not expecting this dot: + +Dots are for record access, so they cannot float around on their own. Maybe +there is some extra whitespace? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_fat_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_fat_arrow.snap new file mode 100644 index 00000000..b36c48b4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_fat_arrow.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::FatArrow, 1, 9), \"value = \")" +--- +UNEXPECTED ARROW +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I was not expecting this fat arrow: + +Use -> for a `case` branch or an anonymous function. The => arrow belongs in +trait constraints. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_has_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_has_type.snap new file mode 100644 index 00000000..15be0f52 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_has_type.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::HasType, 1, 9), \"value = \")" +--- +UNEXPECTED COLON +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 10 } } + +I was not expecting this colon: + +Colons appear in type annotations. A type annotation must appear directly above +its definition. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_indent_right.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_indent_right.snap new file mode 100644 index 00000000..0bb03114 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_indent_right.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::IndentOperatorRight(\"+\", 1, 9), \"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was expecting to see an expression after this + operator: + +You can just put anything for now, like 42 or "hello". Once there is something +there, I can probably give a more specific hint! I may be getting confused by +your indentation? The easiest way to make sure this is not an indentation +problem is to put the expression on the right of the + operator on the same +line. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_left_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_left_arrow.snap new file mode 100644 index 00000000..d06eff59 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_left_arrow.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::LeftArrow, 1, 9), \"value = \")" +--- +UNEXPECTED ARROW +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I was not expecting this left arrow: + +The <- arrow binds the result of an action inside a `do` block. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_pipe.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_pipe.snap new file mode 100644 index 00000000..c40155d0 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_pipe.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::Pipe, 1, 9), \"value = \")" +--- +UNEXPECTED SYMBOL +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 10 } } + +I was not expecting this vertical bar: + +Vertical bars appear in custom type declarations and record updates. Maybe you +want || instead? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_reserved_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_reserved_arrow.snap new file mode 100644 index 00000000..d014e988 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__operator_reserved_arrow.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::Arrow, 1, 9), \"value = \")" +--- +UNEXPECTED ARROW +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 11 } } + +I was not expecting this arrow: + +Arrows belong in `case` branches, anonymous functions, and function types. Maybe +an earlier expression is unfinished? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_bytes_bad_hex.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_bytes_bad_hex.snap new file mode 100644 index 00000000..c88f4735 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_bytes_bad_hex.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +BAD BYTE STRING + + × I ran into an invalid hexadecimal digit in this byte string: + ╭─[Main.nash:1:12] + 1 │ value = #"ag" + · ─ + ╰──── + help: Use pairs of digits from 0123456789abcdefABCDEF, one pair for each byte. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_case_wrong_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_case_wrong_arrow.snap new file mode 100644 index 00000000..1ff6897b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_case_wrong_arrow.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +UNEXPECTED OPERATOR + + × I was partway through parsing a `case` expression, but I got stuck here: + ╭─[Main.nash:2:16] + 1 │ value = case x of + 2 │ Some width = width + · ─ + ╰──── + help: I am seeing = but maybe you want -> instead? + + case maybeWidth of + Some width -> + width + 200 + + None -> + 400 + + Notice the indentation. Each pattern is aligned, and each branch is indented a + bit more than the corresponding pattern. That is important! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_do_last_binding.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_do_last_binding.snap new file mode 100644 index 00000000..2a83a054 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_do_last_binding.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +UNFINISHED DO + + × I was partway through parsing a `do` block, but I got stuck here: + ╭─[Main.nash:2:5] + 1 │ value = do + 2 │ x <- action + · ─ + ╰──── + help: A `do` block must end with an expression. Add the final expression after this + binding. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_if_missing_else.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_if_missing_else.snap new file mode 100644 index 00000000..a3cbe350 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_if_missing_else.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +UNFINISHED IF + + × I was expecting to see more of this `if` expression, but I got stuck here: + ╭─[Main.nash:1:24] + 1 │ value = if True then 42 + · ─ + ╰──── + help: I was expecting to see the `else` keyword next. All `if` expressions need an + `else` branch. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_lambda_missing_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_lambda_missing_body.snap new file mode 100644 index 00000000..2e95ee23 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_lambda_missing_body.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +MISSING EXPRESSION + + × I was partway through parsing an anonymous function, but I got stuck here: + ╭─[Main.nash:1:14] + 1 │ value = \x -> + · ─ + ╰──── + help: I was expecting to see an expression like 42 or "hello". Once there is something + there, I can probably give a more specific hint! This can also happen if I run + into reserved words like `let` or `as` unexpectedly, or operators in unexpected + spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_let_missing_in.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_let_missing_in.snap new file mode 100644 index 00000000..22cf09f1 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_let_missing_in.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +LET PROBLEM + + × I was partway through parsing a `let` expression, but I got stuck here: + ╭─[Main.nash:1:18] + 1 │ value = let x = 1 + · ─ + ╰──── + help: Based on the indentation, I was expecting to see the `in` keyword next. Is there + a typo? This can also happen if you are trying to define another value within + the `let` but it is not indented enough. Make sure each definition has exactly + the same amount of spaces before it. They should line up exactly! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_list_trailing_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_list_trailing_comma.snap new file mode 100644 index 00000000..abf6d0ff --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_list_trailing_comma.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +UNFINISHED LIST + + × I was partway through parsing a list, but I got stuck here: + ╭─[Main.nash:1:13] + 1 │ value = [1, ] + · ─ + ╰──── + help: Trailing commas are not allowed in lists, so the fix may be to delete the comma? + + [ 1 + , 2 + ] + + Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_macro_close.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_macro_close.snap new file mode 100644 index 00000000..b0b5ac05 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_macro_close.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +UNFINISHED MACRO + + × I was partway through parsing a macro invocation, but I got stuck here: + ╭─[Main.nash:1:15] + 1 │ value = foo!(1 + · ─ + ╰──── + help: I was expecting a comma or a closing parenthesis after this macro argument. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_record_reserved.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_record_reserved.snap new file mode 100644 index 00000000..7160a020 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_record_reserved.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +RESERVED WORD + + × I am partway through parsing a record, but I got stuck on this field name: + ╭─[Main.nash:1:11] + 1 │ value = { if = 1 } + · ── + ╰──── + help: It looks like you are trying to use `if` as a field name, but that is a reserved + word. Try using a different name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_unicode_short.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_unicode_short.snap new file mode 100644 index 00000000..87c64650 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__parsed_unicode_short.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "crate :: render_plain(& report, & source, \"Main.nash\")" +--- +BAD UNICODE ESCAPE + + × This code point has the wrong number of digits: + ╭─[Main.nash:1:11] + 1 │ value = "\u{1}" + · ──── + ╰──── + help: I expected 4 digits, but found 1. Unicode escapes need between four and six + hexadecimal digits. Add leading zeros if there are too few, or trim leading + zeros if there are too many. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_indentation.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_indentation.snap new file mode 100644 index 00000000..2e681006 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_indentation.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentEnd(1, 9), 1, 9), \"value = }\")" +--- +NEED MORE INDENTATION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I need this curly brace to be indented more. Try adding some spaces before it! + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_next_line.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_next_line.snap new file mode 100644 index 00000000..b9a889aa --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_close_next_line.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentEnd(1, 19), 1, 9),\n\"value = { name = 1\\n}\")" +--- +NEED MORE INDENTATION +Region { start: Position { line: 2, column: 1 }, end: Position { line: 2, column: 1 } } + +I was partway through parsing a record, but I got stuck here: + +I need this curly brace to be indented more. Try adding some spaces before it! + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_double_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_double_comma.snap new file mode 100644 index 00000000..0c653310 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_double_comma.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::Field(1, 9), 1, 9), \"value = ,\")" +--- +EXTRA COMMA +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I am seeing two commas in a row. This is the second one! Just delete one of the +commas and you should be all set! + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_equals.snap new file mode 100644 index 00000000..d4d6e252 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_equals.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::Equals(1, 9), 1, 9), \"value = \")" +--- +PROBLEM IN RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I just saw a record field, so I was expecting to see an equals sign next. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_field_bad.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_field_bad.snap new file mode 100644 index 00000000..f656c85b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_field_bad.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::Field(1, 9), 1, 9), \"value = \")" +--- +PROBLEM IN RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I was expecting to see a record field next. Record field names must start with a +lower case letter. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_end.snap new file mode 100644 index 00000000..879cb095 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_end.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentEnd(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I was expecting a comma or a closing curly brace next. Try adding more +indentation. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_equals.snap new file mode 100644 index 00000000..12649abf --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_equals.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentEquals(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I just saw a record field, so I was expecting to see an equals sign next. Try +adding more indentation. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_expr.snap new file mode 100644 index 00000000..c6c72447 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_expr.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentExpr(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I was expecting to run into an expression next. If it is already present, it may +need more indentation. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_field.snap new file mode 100644 index 00000000..ba9ced67 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_field.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentField(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +Trailing commas are not allowed in records, so the fix may be to delete that +last comma? Or maybe you were in the middle of defining an additional field? + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_open.snap new file mode 100644 index 00000000..37926bd7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_indent_open.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::IndentOpen(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I just saw the opening curly brace of a record. I was expecting a field name or +a closing curly brace next. Try adding more indentation. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_missing_end.snap new file mode 100644 index 00000000..e5f66292 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_missing_end.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::End(1, 9), 1, 9), \"value = \")" +--- +PROBLEM IN RECORD +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +I was expecting to see a comma or a closing curly brace next. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_reserved_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_reserved_field.snap new file mode 100644 index 00000000..f69dcf80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_reserved_field.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::Open(1, 11), 1, 9), \"value = { if = 1 }\")" +--- +RESERVED WORD +Region { start: Position { line: 1, column: 11 }, end: Position { line: 1, column: 13 } } + +I am partway through parsing a record, but I got stuck on this field name: + +It looks like you are trying to use `if` as a field name, but that is a reserved +word. Try using a different name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_trailing_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_trailing_comma.snap new file mode 100644 index 00000000..20957082 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_trailing_comma.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::Field(1, 9), 1, 9), \"value = }\")" +--- +EXTRA COMMA +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a record, but I got stuck here: + +Trailing commas are not allowed in records. Try deleting the comma that appears +before this closing curly brace. + + { name = "Nash" + , age = 1 + } + +Notice that each line starts with some indentation. Usually two or four spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_unexpected_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_unexpected_equals.snap new file mode 100644 index 00000000..142b2696 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__record_unexpected_equals.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Record(&Record::Expr(&Expr::OperatorReserved(BadOperator::Equals,\n1, 9), 1, 9), 1, 9), \"value = =\")" +--- +UNEXPECTED EQUALS +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 10 } } + +I was not expecting this equals sign: + +Maybe you want == instead? To check if two values are equal? + +Note: Records look like { x = 3, y = 4 } with the equals sign right after the +field name. So maybe you forgot a comma? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_multi.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_multi.snap new file mode 100644 index 00000000..b17b18dc --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_multi.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::String(StringError::EndlessMulti, 1, 9), \"value = \")" +--- +ENDLESS STRING +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 12 } } + +I cannot find the end of this multi-line string: + +Add a """ somewhere after this to end the string. + + """ + # Multi-line Strings + + - start with triple double quotes + - write whatever you want + - no need to escape newlines or double quotes + - end with triple double quotes + """ + +Here is a valid multi-line string for reference. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_single.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_single.snap new file mode 100644 index 00000000..80b2e456 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__string_endless_single.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::String(StringError::EndlessSingle, 1, 9), \"value = \")" +--- +ENDLESS STRING +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I got to the end of the line without seeing the closing double quote: + +Strings look like "this" with double quotes on each end. Is the closing double +quote missing in your code? For a string that spans multiple lines, use triple +double quotes on each end. + + """ + # Multi-line Strings + + - start with triple double quotes + - write whatever you want + - no need to escape newlines or double quotes + - end with triple double quotes + """ + +Here is a valid multi-line string for reference. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentbody.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentbody.snap new file mode 100644 index 00000000..7166ad01 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentbody.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Todo(&Keyword::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `todo` expression, but I got stuck here: + +I was expecting to see an expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentmessage.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentmessage.snap new file mode 100644 index 00000000..f25e520c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__todo_indentmessage.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Todo(&Keyword::IndentMessage(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `todo` expression, but I got stuck here: + +I was expecting to see a message expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentbody.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentbody.snap new file mode 100644 index 00000000..bb7fcd0f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentbody.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Trace(&Keyword::IndentBody(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `trace` expression, but I got stuck here: + +I was expecting to see an expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentmessage.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentmessage.snap new file mode 100644 index 00000000..184cfa03 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_indentmessage.snap @@ -0,0 +1,10 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Trace(&Keyword::IndentMessage(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `trace` expression, but I got stuck here: + +I was expecting to see a message expression next. It may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_missing_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_missing_body.snap new file mode 100644 index 00000000..ce576bfb --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__trace_missing_body.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Trace(&Keyword::Body(&Expr::Start(1, 9), 1, 9), 1, 9),\n\"value = \")" +--- +MISSING EXPRESSION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing a `trace` expression, but I got stuck here: + +I was expecting to see an expression like 42 or "hello". Once there is something +there, I can probably give a more specific hint! This can also happen if I run +into reserved words like `let` or `as` unexpectedly, or operators in unexpected +spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_end.snap new file mode 100644 index 00000000..d26666ea --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_end.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Tuple(&Tuple::IndentEnd(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED PARENTHESES +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing some parentheses, but I got stuck here: + +I was expecting to see a closing parenthesis next. Try adding a ) or adding more +indentation to the existing one. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr1.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr1.snap new file mode 100644 index 00000000..9f99ee3c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr1.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Tuple(&Tuple::IndentExpr1(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED PARENTHESES +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing some parentheses, but I got stuck here: + +I just saw an open parenthesis, so I was expecting to see an expression next. It +may need more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr_n.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr_n.snap new file mode 100644 index 00000000..f4642150 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_indent_expr_n.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Tuple(&Tuple::IndentExprN(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED TUPLE +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing some parentheses, but I got stuck here: + +I just saw a comma, so I was expecting to see an expression next. It may need +more indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_missing_end.snap new file mode 100644 index 00000000..ff1962db --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_missing_end.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Tuple(&Tuple::End(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED PARENTHESES +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing some parentheses, but I got stuck here: + +I was expecting to see a closing parenthesis next. Try adding a ) to see if that +helps? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_operator_close.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_operator_close.snap new file mode 100644 index 00000000..d9436886 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__tuple_operator_close.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::Tuple(&Tuple::OperatorClose(1, 9), 1, 9), \"value = \")" +--- +UNFINISHED OPERATOR FUNCTION +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 9 } } + +I was partway through parsing some parentheses, but I got stuck here: + +I was expecting a closing parenthesis here. Try adding a ) to see if that helps! +Operators in parentheses, like (+), can be used as functions. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__unicode_escape.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__unicode_escape.snap new file mode 100644 index 00000000..c5619ee0 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__unicode_escape.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::String(StringError::Escape(Escape::BadUnicodeCode(8)), 1, 10),\n\"value = \\\"\\\\u{D800}\\\"\")" +--- +BAD UNICODE ESCAPE +Region { start: Position { line: 1, column: 10 }, end: Position { line: 1, column: 18 } } + +This is not a valid code point: + +The valid Unicode scalar values are between 0 and 10FFFF inclusive, excluding +the surrogate range D800 through DFFF. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__weird_end_in_def_context.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__weird_end_in_def_context.snap new file mode 100644 index 00000000..572847be --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__expr__tests__weird_end_in_def_context.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/expr.rs +expression: "snapshot(Expr::OperatorReserved(BadOperator::Equals, 1, 9), \"value = \")" +--- +UNEXPECTED EQUALS +Region { start: Position { line: 1, column: 9 }, end: Position { line: 1, column: 10 } } + +I was not expecting this equals sign: + +Maybe you want == instead? To check if two values are equal? I may be getting +confused by your indentation. I think I am still parsing the `value` definition. +Is this supposed to be part of a definition after that? If so, the problem may +be a bit before the equals sign. I need all definitions to be indented exactly +the same amount, so the problem may be that this new definition has too many +spaces in front of it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__alias_reserved_parameter.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__alias_reserved_parameter.snap new file mode 100644 index 00000000..223d58d8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__alias_reserved_parameter.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I ran into a reserved word unexpectedly while parsing this type alias: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to use `if` as a type variable, but it is a + reserved word. Try using a different name? + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_missing_variant.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_missing_variant.snap new file mode 100644 index 00000000..f1ba2277 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_missing_variant.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\ntype option 'a =" +--- +PROBLEM IN CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:17] + 1 │ type option 'a = + · ─ + ╰──── + help: I was expecting to see a variant name next. Something like Success or Sandwich. + Any name that starts with a capital letter really! + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_reserved_parameter.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_reserved_parameter.snap new file mode 100644 index 00000000..25411c97 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__custom_type_reserved_parameter.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I ran into a reserved word unexpectedly while parsing this custom type: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to use `if` as a type variable, but it is a + reserved word. Try using a different name? + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_indent_body.snap new file mode 100644 index 00000000..16f4789a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_indent_body.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf =\n1" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:4] + 1 │ f = + · ─ + 2 │ 1 + ╰──── + help: I was expecting to see an expression next. What is it equal to? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_missing_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_missing_equals.snap new file mode 100644 index 00000000..39ef3cb8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_missing_equals.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf x" +--- +PROBLEM IN DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:4] + 1 │ f x + · ─ + ╰──── + help: I am not sure what is going wrong exactly, so here is a valid definition (with + an optional type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + Try to use that format with your `f` definition! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_name_match.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_name_match.snap new file mode 100644 index 00000000..d7cf3f76 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_def_name_match.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf : int\ng = 1" +--- +NAME MISMATCH + + × I just saw a type annotation for `f`, but it is followed by a definition for + │ `g`: + ╭─[src/Main.nash:2:2] + 1 │ f : int + 2 │ g = 1 + · ─ + ╰──── + help: These names do not match! Is there a typo? + + g -> f diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_case.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_case.snap new file mode 100644 index 00000000..c80c7528 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_case.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: case +--- +RESERVED WORD + + × I was not expecting to run into the `case` keyword here: + ╭─[src/Main.nash:1:1] + 1 │ case + · ──── + ╰──── + help: It is reserved for writing `case` expressions. Try using a different name? If + you are trying to write a `case` expression, it needs to be part of a + definition. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_if.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_if.snap new file mode 100644 index 00000000..f836480f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_if.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I was not expecting to run into the `if` keyword here: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It is reserved for writing `if` expressions. Try using a different name? If you + are trying to write an `if` expression, it needs to be part of a definition. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_import.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_import.snap new file mode 100644 index 00000000..7b929814 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_import.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: import +--- +RESERVED WORD + + × I was not expecting to run into the `import` keyword here: + ╭─[src/Main.nash:1:1] + 1 │ import + · ────── + ╰──── + help: It is reserved for declaring imports at the top of your module. If you want + another import, try moving it up top with the other imports. If you want to + define a value or function, try changing the name to something else! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_uppercase.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_uppercase.snap new file mode 100644 index 00000000..d4a1df66 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__decl_start_uppercase.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: Thing +--- +UNEXPECTED CAPITAL LETTER + + × Declarations always start with a lower-case letter, so I am getting stuck here: + ╭─[src/Main.nash:1:1] + 1 │ Thing + · ─ + ╰──── + help: Try a name like thing instead? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! + + Notice that they always start with a lower-case letter. Capitalization matters! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_missing_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_missing_colon.snap new file mode 100644 index 00000000..dd008b52 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_missing_colon.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "->" +--- +MISSING COLON? + + × I was not expecting to see an arrow here: + ╭─[src/Main.nash:1:1] + 1 │ -> + · ─ + ╰──── + help: This usually means a : is missing a bit earlier in a type annotation. It could + be something else though, so here is a valid definition for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + Try to use that format with your `f` definition! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_reserved_argument.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_reserved_argument.snap new file mode 100644 index 00000000..09c5221a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_reserved_argument.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × The name `if` is reserved in Nash, so it cannot be used as an argument here: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: Try renaming it to something else. + + Note: The `if` keyword has a special meaning in Nash, so it can only be used in + certain situations. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_unexpected_operator.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_unexpected_operator.snap new file mode 100644 index 00000000..2ea46b96 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__definition_unexpected_operator.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: + +--- +UNEXPECTED SYMBOL + + × I was not expecting to see this symbol here: + ╭─[src/Main.nash:1:1] + 1 │ + + · ─ + ╰──── + help: I am not sure what is going wrong exactly, so here is a valid definition (with + an optional type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + Try to use that format with your `f` definition! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_bare_operator.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_bare_operator.snap new file mode 100644 index 00000000..890328b6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_bare_operator.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: + +--- +UNEXPECTED SYMBOL + + × I got stuck on this symbol: + ╭─[src/Main.nash:1:1] + 1 │ + + · ─ + ╰──── + help: If you are trying to expose an operator, add parentheses around it like this: + + + -> (+) diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_missing_paren.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_missing_paren.snap new file mode 100644 index 00000000..84f412eb --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_missing_paren.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule Main exposing .." +--- +PROBLEM IN EXPOSING + + × I want to parse exposed values, but I am getting stuck here: + ╭─[src/Main.nash:1:22] + 1 │ module Main exposing .. + · ─ + ╰──── + help: Exposed values are always surrounded by parentheses. So try adding a ( here? + + Note: Here are some valid examples of `exposing` for reference: + + import Data.Decoder exposing (..) + import Data.Decoder exposing (decode) + + If you are getting tripped up, you can just expose everything for now. It should + get easier to make an explicit exposing list as you see more examples in the + wild. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_reserved_word.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_reserved_word.snap new file mode 100644 index 00000000..3e70e5dc --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_reserved_word.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I got stuck on this reserved word: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to expose `if` but that is a reserved word. Is + there a typo? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_value_bad.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_value_bad.snap new file mode 100644 index 00000000..1881476a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__exposing_value_bad.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule Main exposing (1)" +--- +PROBLEM IN EXPOSING + + × I got stuck while parsing these exposed values: + ╭─[src/Main.nash:1:23] + 1 │ module Main exposing (1) + · ─ + ╰──── + help: I do not have an exact recommendation, so here are some valid examples of + `exposing` for reference: + + import Data.Decoder exposing (..) + import Basics exposing (type int, type bool(..), (+), not) + + These examples show how to expose types, variants, operators, and functions. + Everything should be some permutation of these examples, just with different + names. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_after_decl.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_after_decl.snap new file mode 100644 index 00000000..cac67761 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_after_decl.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nx = 1 y = 2" +--- +UNEXPECTED EQUALS + + × I was not expecting this equals sign: + ╭─[src/Main.nash:1:9] + 1 │ x = 1 y = 2 + · ─ + ╰──── + help: Maybe you want == instead? To check if two values are equal? I may be getting + confused by your indentation. I think I am still parsing the `x` definition. Is + this supposed to be part of a definition after that? If so, the problem may be a + bit before the equals sign. I need all definitions to be indented exactly the + same amount, so the problem may be that this new definition has too many spaces + in front of it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_keyword.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_keyword.snap new file mode 100644 index 00000000..5fa84541 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__fresh_line_keyword.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: module +--- +TOO MUCH INDENTATION + + × This `module` should not have any spaces before it: + ╭─[src/Main.nash:1:1] + 1 │ module + · ─ + ╰──── + help: Delete the spaces before `module` until there are none left! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_bad_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_bad_alias.snap new file mode 100644 index 00000000..29054f40 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_bad_alias.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nimport Cardano.Tx as tx" +--- +EXPECTING IMPORT ALIAS + + × I was parsing an `import` until I got stuck here: + ╭─[src/Main.nash:1:22] + 1 │ import Cardano.Tx as tx + · ─ + ╰──── + help: I was expecting to see an alias next, like in these examples: + + import Cardano.Tx as Tx + import Data.Decoder as D + + Notice that the alias always starts with a capital letter. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_exposing_list_missing_paren.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_exposing_list_missing_paren.snap new file mode 100644 index 00000000..67a4bcb7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_exposing_list_missing_paren.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nimport Cardano.Tx exposing x" +--- +PROBLEM IN EXPOSING + + × I want to parse exposed values, but I am getting stuck here: + ╭─[src/Main.nash:1:28] + 1 │ import Cardano.Tx exposing x + · ─ + ╰──── + help: Exposed values are always surrounded by parentheses. So try adding a ( here? + + Note: Here are some valid examples of `exposing` for reference: + + import Data.Decoder exposing (..) + import Data.Decoder exposing (decode) + + If you are getting tripped up, you can just expose everything for now. It should + get easier to make an explicit exposing list as you see more examples in the + wild. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_missing_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_missing_name.snap new file mode 100644 index 00000000..4f81ae79 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__import_missing_name.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nimport" +--- +EXPECTING IMPORT NAME + + × I was parsing an `import` until I got stuck here: + ╭─[src/Main.nash:1:7] + 1 │ import + · ─ + ╰──── + help: I was expecting to see a module name next, like in these examples: + + import Dict + import Option + import Cardano.Tx as Tx + import Data.Decoder exposing (..) + + Notice that the module names all start with capital letters. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_lowercase.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_lowercase.snap new file mode 100644 index 00000000..5e3f38f7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_lowercase.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule main exposing (..)" +--- +EXPECTING MODULE NAME + + × I was parsing a `module` declaration until I got stuck here: + ╭─[src/Main.nash:1:8] + 1 │ module main exposing (..) + · ─ + ╰──── + help: I was expecting to see the module name next, like in these examples: + + module Dict exposing (..) + module Option exposing (..) + module Cardano.Tx exposing (..) + module Data.Decoder exposing (..) + + Notice that the module names all start with capital letters. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_mismatch.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_mismatch.snap new file mode 100644 index 00000000..7de78d89 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_mismatch.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +expression: "render_plain(&to_report(&Source::new(input), &error), &Source::new(input),\n\"src/Main.nash\")" +--- +MODULE NAME MISMATCH + + × It looks like this module name is out of sync: + ╭─[src/Main.nash:1:8] + 1 │ module Other exposing (..) + · ───── + ╰──── + help: I need it to match the file path, so I was expecting to see `Main` here. Make + the following change, and you should be all set! + + Other -> Main + + Note: I require that module names correspond to file paths. This makes it much + easier to explore unfamiliar codebases! So if you want to keep the current + module name, try renaming the file instead. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_missing.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_missing.snap new file mode 100644 index 00000000..e211c60b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_name_missing.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +expression: "render_plain(&to_report(&Source::new(\"\"),\n&Error::ModuleNameUnspecified(\"Main\")), &Source::new(\"\"), \"src/Main.nash\")" +--- +MODULE NAME MISSING + + × I need the module name to be declared at the top of this file, like this: + │ + │ module Main exposing (..) + │ + │ Try adding that as the first line of your file! + help: Note: It is best to replace (..) with an explicit list of types and functions + you want to expose. When you know a value is only used within this module, you + can refactor without worrying about uses elsewhere. Limiting exposed values can + also speed up compilation because I can skip a bunch of work if I see that the + exposed API has not changed. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_problem.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_problem.snap new file mode 100644 index 00000000..5d7b7876 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__module_problem.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule" +--- +EXPECTING MODULE NAME + + × I was parsing a `module` declaration until I got stuck here: + ╭─[src/Main.nash:1:7] + 1 │ module + · ─ + ╰──── + help: I was expecting to see the module name next, like in these examples: + + module Dict exposing (..) + module Option exposing (..) + module Cardano.Tx exposing (..) + module Data.Decoder exposing (..) + + Notice that the module names all start with capital letters. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_alias_missing_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_alias_missing_name.snap new file mode 100644 index 00000000..e907d3df --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_alias_missing_name.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf (x as) = x" +--- +UNFINISHED PATTERN + + × I was expecting to see a variable name after the `as` keyword: + ╭─[src/Main.nash:1:8] + 1 │ f (x as) = x + · ─ + ╰──── + help: The `as` keyword lets you write patterns like ((x,y) as point) so you can refer + to individual parts of the tuple with x and y or you refer to the whole thing + with point. So I was expecting to see a variable name after the `as` keyword + here. Sometimes people just want to use `as` as a variable name though. Try + using a different name in that case! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_list_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_list_missing_end.snap new file mode 100644 index 00000000..ca66366c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_list_missing_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf [x = 1" +--- +UNFINISHED LIST PATTERN + + × I was expecting a closing square bracket to end this list pattern: + ╭─[src/Main.nash:1:6] + 1 │ f [x = 1 + · ─ + ╰──── + help: Try adding a ] to see if that helps? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_negative_number.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_negative_number.snap new file mode 100644 index 00000000..7f01f5ac --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_negative_number.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "-1" +--- +UNEXPECTED SYMBOL + + × I ran into a minus sign unexpectedly in this pattern: + ╭─[src/Main.nash:1:1] + 1 │ -1 + · ─ + ╰──── + help: It is not possible to pattern match on negative numbers at this time. Try using + an `if` expression for that sort of thing for now. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_record_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_record_missing_end.snap new file mode 100644 index 00000000..cba9ffb3 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_record_missing_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf {x = 1" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:6] + 1 │ f {x = 1 + · ─ + ╰──── + help: I was expecting to see a closing curly brace next. Try adding a } here? + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_list_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_list_open.snap new file mode 100644 index 00000000..f28a6e1b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_list_open.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × It looks like you are trying to use `if` to name an element of a list: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: This is a reserved word! Try using some other name? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_record_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_record_field.snap new file mode 100644 index 00000000..8d765289 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_record_field.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I was not expecting to see `if` as a record field name: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: This is a reserved word, not available for variable names. Try another name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_end.snap new file mode 100644 index 00000000..0f7cba75 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I ran into a reserved word in this pattern: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: The `if` keyword is reserved. Try using a different name instead! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_open.snap new file mode 100644 index 00000000..719a15fd --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_reserved_tuple_open.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × It looks like you are trying to use `if` as a variable name: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: This is a reserved word! Try using some other name? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_arg.snap new file mode 100644 index 00000000..d0d1eb55 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_arg.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × It looks like you are trying to use `if` as an argument: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: This is a reserved word! Try using some other name? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_case.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_case.snap new file mode 100644 index 00000000..002a8c78 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_case.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × It looks like you are trying to use `if` in this pattern: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: This is a reserved word! Try using some other name? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_let.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_let.snap new file mode 100644 index 00000000..002a8c78 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_start_in_let.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × It looks like you are trying to use `if` in this pattern: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: This is a reserved word! Try using some other name? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_stray_bracket.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_stray_bracket.snap new file mode 100644 index 00000000..2de474b8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_stray_bracket.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "]" +--- +STRAY SQUARE BRACKET + + × I ran into an unexpected square bracket in this pattern: + ╭─[src/Main.nash:1:1] + 1 │ ] + · ─ + ╰──── + help: This ] does not match up with an earlier open square bracket. Try deleting it? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_tuple_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_tuple_missing_end.snap new file mode 100644 index 00000000..dedb5898 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_tuple_missing_end.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf (x = 1" +--- +UNEXPECTED SYMBOL + + × I ran into the = symbol unexpectedly in this pattern: + ╭─[src/Main.nash:1:6] + 1 │ f (x = 1 + · ─ + ╰──── + help: Only the :: symbol works in patterns. It is useful if you are pattern matching + on lists, trying to get the first element off the front. Did you want that + instead? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_only_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_only_name.snap new file mode 100644 index 00000000..59551b37 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_only_name.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: ___ +--- +UNEXPECTED NAME + + × Variable names cannot start with underscores like this: + ╭─[src/Main.nash:1:1] + 1 │ ___ + · ─── + ╰──── + help: You can either have an underscore like _ to ignore the value, or you can have a + name like x or age to use the matched value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_uppercase_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_uppercase_name.snap new file mode 100644 index 00000000..e0d233b2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_underscore_uppercase_name.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: _Thing +--- +UNEXPECTED NAME + + × Variable names cannot start with underscores like this: + ╭─[src/Main.nash:1:1] + 1 │ _Thing + · ────── + ╰──── + help: You can either have an underscore like _ to ignore the value, or you can have a + name like thing to use the matched value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_wildcard_not_var.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_wildcard_not_var.snap new file mode 100644 index 00000000..9168586a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__pattern_wildcard_not_var.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf _foo = 1" +--- +UNEXPECTED NAME + + × Variable names cannot start with underscores like this: + ╭─[src/Main.nash:1:3] + 1 │ f _foo = 1 + · ──── + ╰──── + help: You can either have an underscore like _ to ignore the value, or you can have a + name like foo to use the matched value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_endless_comment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_endless_comment.snap new file mode 100644 index 00000000..e73ee245 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_endless_comment.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\n{- unfinished" +--- +ENDLESS COMMENT + + × I cannot find the end of this multi-line comment: + ╭─[src/Main.nash:1:14] + 1 │ {- unfinished + · ─ + ╰──── + help: Add a -} somewhere after this to end the comment. + + Hint: Multi-line comments can be nested in Nash, so {- {- -} -} is a comment + that happens to contain another comment. Like parentheses and curly braces, the + start and end markers must always be balanced. Maybe that is the problem? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_has_tab.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_has_tab.snap new file mode 100644 index 00000000..096b27cd --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__space_has_tab.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nx =\t1" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:4] + 1 │ x = 1 + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_bad_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_bad_body.snap new file mode 100644 index 00000000..fc444f7a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_bad_body.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\ntype alias account = 42" +--- +PROBLEM IN TYPE ALIAS + + × I was partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:22] + 1 │ type alias account = 42 + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_missing_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_missing_equals.snap new file mode 100644 index 00000000..76686d34 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_alias_missing_equals.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\ntype alias account" +--- +PROBLEM IN TYPE ALIAS + + × I am partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:19] + 1 │ type alias account + · ─ + ╰──── + help: I was expecting to see a type variable or an equals sign next. + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_alias.snap new file mode 100644 index 00000000..dd4bf183 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_alias.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "42" +--- +UNFINISHED TYPE ALIAS + + × I was partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:1] + 1 │ 42 + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? + + Note: I can get confused by indentation. If you think there is already a type + next, maybe it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_custom_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_custom_type.snap new file mode 100644 index 00000000..b50fe427 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_indent_in_custom_type.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "42" +--- +UNFINISHED CUSTOM TYPE + + × I was partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:1] + 1 │ 42 + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? + + Note: I can get confused by indentation. If you think there is already a type + next, maybe it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_double_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_double_comma.snap new file mode 100644 index 00000000..8dab3693 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_double_comma.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "," +--- +EXTRA COMMA + + × I am partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:1] + 1 │ , + · ─ + ╰──── + help: I am seeing two commas in a row. This is the second one! Just delete one of the + commas and you should be all set! + + Note: If you are trying to define a record type across multiple lines, I + recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_missing_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_missing_colon.snap new file mode 100644 index 00000000..43d7bdee --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_missing_colon.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf : { x int }\nf = 1" +--- +UNFINISHED RECORD TYPE + + × I am partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:9] + 1 │ f : { x int } + · ─ + 2 │ f = 1 + ╰──── + help: I just saw a field name, so I was expecting to see a colon next. So try putting + a : sign here? + + Note: If you are trying to define a record type across multiple lines, I + recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_field.snap new file mode 100644 index 00000000..abbd33bd --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_field.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I am partway through parsing a record type, but I got stuck on this field name: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to use `if` as a field name, but that is a reserved + word. Try using a different name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_open.snap new file mode 100644 index 00000000..71806fc6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_reserved_open.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I just started parsing a record type, but I got stuck on this field name: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to use `if` as a field name, but that is a reserved + word. Try using a different name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_trailing_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_trailing_comma.snap new file mode 100644 index 00000000..b33d3327 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_trailing_comma.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "}" +--- +EXTRA COMMA + + × I am partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:1] + 1 │ } + · ─ + ╰──── + help: Trailing commas are not allowed in record types. Try deleting the comma that + appears before this closing curly brace. + + Note: If you are trying to define a record type across multiple lines, I + recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_underindented_close.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_underindented_close.snap new file mode 100644 index 00000000..963217d9 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_record_underindented_close.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "f : { x : int\n}" +--- +NEED MORE INDENTATION + + × I was partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:2:1] + 1 │ f : { x : int + 2 │ } + · ─ + ╰──── + help: I need this curly brace to be indented more. Try adding some spaces before it! + + Note: If you are trying to define a record type across multiple lines, I + recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_reserved_word.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_reserved_word.snap new file mode 100644 index 00000000..33cd1706 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_reserved_word.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I was expecting to see a type next, but I got stuck on this reserved word: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to use `if` as a type variable, but it is a + reserved word. Try using a different name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_bad_in_annotation.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_bad_in_annotation.snap new file mode 100644 index 00000000..66a91647 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_bad_in_annotation.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf : 42\nf = 1" +--- +PROBLEM IN TYPE ANNOTATION + + × I was partway through parsing the `f` type annotation, but I got stuck here: + ╭─[src/Main.nash:1:5] + 1 │ f : 42 + · ─ + 2 │ f = 1 + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_alias.snap new file mode 100644 index 00000000..b3dce418 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_alias.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "42" +--- +PROBLEM IN TYPE ALIAS + + × I was partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:1] + 1 │ 42 + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_custom_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_custom_type.snap new file mode 100644 index 00000000..4b511ae5 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_start_in_custom_type.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "42" +--- +PROBLEM IN CUSTOM TYPE + + × I was partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:1] + 1 │ 42 + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_missing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_missing_end.snap new file mode 100644 index 00000000..c7fa9c87 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_missing_end.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nf : (int, int\nf = 1" +--- +UNFINISHED PARENTHESES + + × I was expecting to see a closing parenthesis next: + ╭─[src/Main.nash:1:14] + 1 │ f : (int, int + · ─ + 2 │ f = 1 + ╰──── + help: Try adding a ) to see if that helps! + + Note: I can get confused by indentation in cases like this, so maybe you have a + closing parenthesis but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_reserved_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_reserved_open.snap new file mode 100644 index 00000000..24dff026 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__type_tuple_reserved_open.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: if +--- +RESERVED WORD + + × I ran into a reserved word unexpectedly: + ╭─[src/Main.nash:1:1] + 1 │ if + · ── + ╰──── + help: It looks like you are trying to use `if` as a variable name, but it is a + reserved word. Try using a different name! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_backtick.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_backtick.snap new file mode 100644 index 00000000..4c471a17 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_backtick.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "`" +--- +UNEXPECTED CHARACTER + + × I got stuck on this character: + ╭─[src/Main.nash:1:1] + 1 │ ` + · ─ + ╰──── + help: It is not used for anything in Nash syntax. It is used for multi-line strings in + some languages though, so if you want a string that spans multiple lines, you + can use Nash's multi-line string syntax like this: + + """ + # Multi-line Strings + + - start with triple double quotes + - write whatever you want + - no need to escape newlines or double quotes + - end with triple double quotes + """ + + Otherwise I do not know what is going on! Try removing the character? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_close_paren.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_close_paren.snap new file mode 100644 index 00000000..46573110 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_close_paren.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule Main exposing (..)\n\n)" +--- +STRAY PARENTHESIS + + × I was not expecting to see a parenthesis here: + ╭─[src/Main.nash:3:1] + 2 │ + 3 │ ) + · ─ + ╰──── + help: This ) does not match up with an earlier open parenthesis. Try deleting it? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_comma.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_comma.snap new file mode 100644 index 00000000..dbb95c9c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_comma.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "," +--- +UNEXPECTED COMMA + + × I got stuck on this comma: + ╭─[src/Main.nash:1:1] + 1 │ , + · ─ + ╰──── + help: I do not think I am parsing a list or tuple right now. Try deleting the comma? + + Note: If this is supposed to be part of a list, the problem may be a bit + earlier. Perhaps the opening [ is missing? Or perhaps some value in the list has + an extra closing ] that is making me think the list ended earlier? The same + kinds of things could be going wrong if this is supposed to be a tuple. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_empty.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_empty.snap new file mode 100644 index 00000000..5e6e29fb --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_empty.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "" +--- +UNFINISHED FILE + + × I got to the end of the file, but I was expecting more. + ╭─[src/Main.nash:1:1] + 1 │ + · ─ + ╰──── + help: Maybe a declaration or an expression is incomplete? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_lowercase.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_lowercase.snap new file mode 100644 index 00000000..bf826ef1 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_lowercase.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: thing +--- +UNEXPECTED NAME + + × I got stuck on this name: + ╭─[src/Main.nash:1:1] + 1 │ thing + · ───── + ╰──── + help: It is confusing me a lot! Normally I can give fairly specific hints, but + something is really tripping me up this time. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_operator.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_operator.snap new file mode 100644 index 00000000..0ab373ba --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_operator.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule Main exposing (..)\n\n+" +--- +UNEXPECTED SYMBOL + + × I am getting stuck because this line starts with the + symbol: + ╭─[src/Main.nash:3:1] + 2 │ + 3 │ + + · ─ + ╰──── + help: When a line has no spaces at the beginning, I expect it to be a declaration. If + this is not supposed to be a declaration, try adding some spaces before it? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_reserved_word.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_reserved_word.snap new file mode 100644 index 00000000..8abbd629 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_reserved_word.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: "Code:\n\nmodule Main exposing (..)\n\nif" +--- +RESERVED WORD + + × I was not expecting to run into the `if` keyword here: + ╭─[src/Main.nash:3:1] + 2 │ + 3 │ if + · ── + ╰──── + help: It is reserved for writing `if` expressions. Try using a different name? If you + are trying to write an `if` expression, it needs to be part of a definition. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_semicolon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_semicolon.snap new file mode 100644 index 00000000..f5473648 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_semicolon.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: ; +--- +UNEXPECTED SEMICOLON + + × I got stuck on this semicolon: + ╭─[src/Main.nash:1:1] + 1 │ ; + · ─ + ╰──── + help: Try removing it? + + Note: Some languages require semicolons at the end of each statement. Nash uses + indentation to separate declarations and statements in do blocks, so there is no + need to use semicolons to separate them. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_uppercase.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_uppercase.snap new file mode 100644 index 00000000..1f5c350b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__tests__weird_end_uppercase.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/tests.rs +description: Thing +--- +UNEXPECTED NAME + + × I got stuck on this name: + ╭─[src/Main.nash:1:1] + 1 │ Thing + · ───── + ╰──── + help: It is confusing me a lot! Normally I can give fairly specific hints, but + something is really tripping me up this time. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_arg.snap new file mode 100644 index 00000000..1ab20290 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_arg.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING EXPRESSION + + × I was partway through parsing a definition, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression like 42 or "hello". Once there is something + there, I can probably give a more specific hint! This can also happen if I run + into reserved words like `let` or `as` unexpectedly, or operators in unexpected + spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_end.snap new file mode 100644 index 00000000..184f5587 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_end.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED ATTRIBUTE + + × I was parsing an attribute, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a comma between arguments or a closing parenthesis after the + final argument. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_fresh_line.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_fresh_line.snap new file mode 100644 index 00000000..8f0a6364 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_fresh_line.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +ATTRIBUTE NEEDS FRESH LINE + + × I finished this attribute, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Put the declaration or next attribute on a fresh line with the same indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_arg.snap new file mode 100644 index 00000000..e758a262 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_arg.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING ATTRIBUTE ARGUMENT + + × I was parsing an attribute argument, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add the argument expression and indent it farther than the start of the + attribute. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_end.snap new file mode 100644 index 00000000..184f5587 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_indent_end.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED ATTRIBUTE + + × I was parsing an attribute, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a comma between arguments or a closing parenthesis after the + final argument. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_name.snap new file mode 100644 index 00000000..d93f9202 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_name.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING ATTRIBUTE NAME + + × I just saw @, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write the attribute name immediately after @, such as `@derive(Eq)`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_attribute_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_bar.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_bar.snap new file mode 100644 index 00000000..82786924 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_bar.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a vertical bar like | next. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_equals.snap new file mode 100644 index 00000000..2cb6e19f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_equals.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type variable or an equals sign next. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field.snap new file mode 100644 index 00000000..3a5b8d54 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CONSTRUCTOR FIELD + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a field name next. A named constructor field looks like `owner : + Bytes`. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_colon.snap new file mode 100644 index 00000000..dbb15c0b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_colon.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING FIELD COLON + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I have the field name, so I was expecting a colon followed by its type. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_end.snap new file mode 100644 index 00000000..f437e575 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_end.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CONSTRUCTOR FIELDS + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Separate constructor fields with commas, and close the field list with }. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_type.snap new file mode 100644 index 00000000..1f736470 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_field_type.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN CUSTOM TYPE + + × I was partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_bar.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_bar.snap new file mode 100644 index 00000000..c88c3da1 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_bar.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I just saw a vertical bar, so I was expecting to see another variant defined + next. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_equals.snap new file mode 100644 index 00000000..36149788 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_after_equals.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I just saw an equals sign, so I was expecting to see the first variant defined + next. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_bar.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_bar.snap new file mode 100644 index 00000000..43e8dd42 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_bar.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a vertical bar like | next. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_equals.snap new file mode 100644 index 00000000..dcfbc299 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_equals.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type variable or an equals sign next. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field.snap new file mode 100644 index 00000000..3a5b8d54 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CONSTRUCTOR FIELD + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a field name next. A named constructor field looks like `owner : + Bytes`. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field_type.snap new file mode 100644 index 00000000..d09b82c2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_indent_field_type.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED FIELD TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I just saw a colon, so I was expecting the field type next. Indent it farther + than the constructor declaration. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_name.snap new file mode 100644 index 00000000..15496f80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_name.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE NAME + + × I think I am parsing a type declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like status or option next. Nash uses lower-case names + for little types and capitalized names for Big types. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_param.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_param.snap new file mode 100644 index 00000000..25adcd7e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_param.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TYPE PARAMETER + + × I was parsing a type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Type parameters start with a quote, such as 'a. A representation annotation + looks like ('a : Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant.snap new file mode 100644 index 00000000..4b0296c1 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN CUSTOM TYPE + + × I am partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a variant name next. Something like Success or Sandwich. + Any name that starts with a capital letter really! + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant_arg.snap new file mode 100644 index 00000000..1f736470 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_custom_type_variant_arg.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN CUSTOM TYPE + + × I was partway through parsing a custom type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_attribute.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_attribute.snap new file mode 100644 index 00000000..d93f9202 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_attribute.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING ATTRIBUTE NAME + + × I just saw @, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write the attribute name immediately after @, such as `@derive(Eq)`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def.snap new file mode 100644 index 00000000..c4c2d19c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression next. What is it equal to? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_arg.snap new file mode 100644 index 00000000..0465ff91 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_arg.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN PATTERN + + × I wanted to parse a pattern next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure why I am getting stuck exactly. I just know that I want a pattern + next. Something as simple as maybeHeight or result would work! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_body.snap new file mode 100644 index 00000000..7b8f4e74 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_body.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING EXPRESSION + + × I was partway through parsing the `f` definition, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression like 42 or "hello". Once there is something + there, I can probably give a more specific hint! This can also happen if I run + into reserved words like `let` or `as` unexpectedly, or operators in unexpected + spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_equals.snap new file mode 100644 index 00000000..03cfb4a9 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_equals.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED SYMBOL + + × I was not expecting to see this symbol here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure what is going wrong exactly, so here is a valid definition (with + an optional type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + Try to use that format with your `f` definition! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_body.snap new file mode 100644 index 00000000..c4c2d19c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_body.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression next. What is it equal to? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_equals.snap new file mode 100644 index 00000000..fd5f0840 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_equals.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an argument or an equals sign next. + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_type.snap new file mode 100644 index 00000000..0d554c0b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_indent_type.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` type annotation: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I just saw a colon, so I am expecting to see a type next. + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_match.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_match.snap new file mode 100644 index 00000000..54e8c6aa --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_match.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NAME MISMATCH + + × I just saw a type annotation for `f`, but it is followed by a definition for + │ `g`: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: These names do not match! Is there a typo? + + g -> f diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_repeat.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_repeat.snap new file mode 100644 index 00000000..be966308 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_name_repeat.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING DEFINITION + + × I just saw the type annotation for `f` so I was expecting to see its definition + │ here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Type annotations always appear directly above the relevant definition, without + anything else in between. (Not even doc comments!) + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_type.snap new file mode 100644 index 00000000..22b8a2e2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_def_type.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TYPE ANNOTATION + + × I was partway through parsing the `f` type annotation, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_fresh_line_after_doc_comment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_fresh_line_after_doc_comment.snap new file mode 100644 index 00000000..acb9ba9e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_fresh_line_after_doc_comment.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING DECLARATION + + × I just saw a doc comment, but then I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see the corresponding declaration next, starting on a fresh + line with no indentation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_impl.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_impl.snap new file mode 100644 index 00000000..5af12ba6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_impl.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +BAD IMPL HEAD + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a trait name followed by its type arguments. For example: `impl + Show int where`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_start.snap new file mode 100644 index 00000000..8265f174 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_start.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED SYMBOL + + × I am getting stuck because this line starts with the = symbol: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: When a line has no spaces at the beginning, I expect it to be a declaration. If + this is not supposed to be a declaration, try adding some spaces before it? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_trait.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_trait.snap new file mode 100644 index 00000000..cb30c8c4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_trait.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT NAME + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a capitalized trait name, such as Eq or Show. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type.snap new file mode 100644 index 00000000..15496f80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE NAME + + × I think I am parsing a type declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like status or option next. Nash uses lower-case names + for little types and capitalized names for Big types. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_alias.snap new file mode 100644 index 00000000..b3e93bbe --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_alias.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE ALIAS NAME + + × I am partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like account or point next. Nash uses lower-case names + for little aliases and capitalized names for Big aliases. + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_indent_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_indent_name.snap new file mode 100644 index 00000000..15496f80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_indent_name.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE NAME + + × I think I am parsing a type declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like status or option next. Nash uses lower-case names + for little types and capitalized names for Big types. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_name.snap new file mode 100644 index 00000000..15496f80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_name.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE NAME + + × I think I am parsing a type declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like status or option next. Nash uses lower-case names + for little types and capitalized names for Big types. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_union.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_union.snap new file mode 100644 index 00000000..15496f80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_decl_type_union.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE NAME + + × I think I am parsing a type declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like status or option next. Nash uses lower-case names + for little types and capitalized names for Big types. + + Note: Here is an example of a valid `type` declaration for reference: + + type option 'a = None | Some 'a + + This defines a new `option` type with two variants. The Some variant has some + associated data, allowing us to store a value when one is available. None + represents the absence of a value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_end.snap new file mode 100644 index 00000000..66f233a4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED EXPOSING + + × I was partway through parsing exposed values, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Maybe there is a comma missing before this? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_end.snap new file mode 100644 index 00000000..048d14b6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED EXPOSING + + × I was partway through parsing exposed values, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a closing parenthesis. Try adding a ) right here? + + Note: I can get confused when there is not enough indentation, so if you already + have a closing parenthesis, it probably just needs some spaces in front of it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_value.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_value.snap new file mode 100644 index 00000000..8b5bea61 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_indent_value.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED EXPOSING + + × I was partway through parsing exposed values, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting another value to expose. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator.snap new file mode 100644 index 00000000..7e5e781c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN EXPOSING + + × I just saw an open parenthesis, so I was expecting an operator next: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: It is possible to expose operators, so I was expecting to see something like (+) + or (|=) or (||) after I saw that open parenthesis. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_reserved.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_reserved.snap new file mode 100644 index 00000000..ed6e5048 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_reserved.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +RESERVED SYMBOL + + × I cannot expose this as an operator: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Maybe you want (==) instead? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_right_paren.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_right_paren.snap new file mode 100644 index 00000000..18e43d79 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_operator_right_paren.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN EXPOSING + + × It looks like you are exposing an operator, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see the closing parenthesis immediately after the operator. + Try adding a ) right here? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_start.snap new file mode 100644 index 00000000..b93c9be8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_start.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN EXPOSING + + × I want to parse exposed values, but I am getting stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Exposed values are always surrounded by parentheses. So try adding a ( here? + + Note: Here are some valid examples of `exposing` for reference: + + import Data.Decoder exposing (..) + import Data.Decoder exposing (decode) + + If you are getting tripped up, you can just expose everything for now. It should + get easier to make an explicit exposing list as you see more examples in the + wild. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_name.snap new file mode 100644 index 00000000..c5cccd4e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_name.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE NAME + + × I was parsing an exposed type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write the name of the type after `type`. Use `type name(..)` to expose its + constructors as well. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_privacy.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_privacy.snap new file mode 100644 index 00000000..2b3a9392 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_type_privacy.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM EXPOSING CUSTOM TYPE VARIANTS + + × It looks like you are trying to expose the variants of a custom type: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: You need to write something like Status(..) or Entity(..) though. It is all or + nothing, otherwise `case` expressions could miss a variant and crash! + + Note: It is often best to keep the variants hidden! If someone pattern matches + on the variants, it is a MAJOR change if any new variants are added. Suddenly + their `case` expressions do not cover all variants! So if you do not need people + to pattern match, keep the variants hidden and expose functions to construct + values of this type. This way you can add new variants as a MINOR change! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_value.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_value.snap new file mode 100644 index 00000000..860c9e24 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_exposing_value.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED SYMBOL + + × I got stuck on this symbol: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: If you are trying to expose an operator, add parentheses around it like this: + + = -> (=) diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_alignment.snap new file mode 100644 index 00000000..5c840bdc --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_alignment.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +IMPL METHOD ALIGNMENT + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: All methods in this impl must start in column 3. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_bad_head.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_bad_head.snap new file mode 100644 index 00000000..5af12ba6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_bad_head.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +BAD IMPL HEAD + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a trait name followed by its type arguments. For example: `impl + Show int where`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_head.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_head.snap new file mode 100644 index 00000000..4c8f8ff7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_head.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN IMPL HEAD + + × I was partway through parsing an impl head, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a trait name and its type arguments next. For example, Show int. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_head.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_head.snap new file mode 100644 index 00000000..5af12ba6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_head.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +BAD IMPL HEAD + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a trait name followed by its type arguments. For example: `impl + Show int where`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_method.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_method.snap new file mode 100644 index 00000000..02eabe0f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_method.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING IMPL METHOD + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a method definition. Write its name and arguments followed by = + and the body. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_where.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_where.snap new file mode 100644 index 00000000..84f4318a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_indent_where.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING IMPL WHERE + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `where` after the impl head, then indent the method definitions below it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method.snap new file mode 100644 index 00000000..35725444 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression next. What is it equal to? + + greet : string -> string + greet name = + "Hello " ++ name + + The top line is an optional type annotation. It works as compiler-verified + documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method_name.snap new file mode 100644 index 00000000..02eabe0f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_method_name.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING IMPL METHOD + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a method definition. Write its name and arguments followed by = + and the body. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_where.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_where.snap new file mode 100644 index 00000000..84f4318a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_impl_where.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING IMPL WHERE + + × I was parsing an impl declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `where` after the impl head, then indent the method definitions below it. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_bad_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_bad_end.snap new file mode 100644 index 00000000..0df43f38 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_bad_end.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED SYMBOL + + × I ran into an unexpected symbol: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was not expecting to see a = here. Try deleting it? Maybe I can give a better + hint from there? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_declarations.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_declarations.snap new file mode 100644 index 00000000..8265f174 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_declarations.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED SYMBOL + + × I am getting stuck because this line starts with the = symbol: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: When a line has no spaces at the beginning, I expect it to be a declaration. If + this is not supposed to be a declaration, try adding some spaces before it? + + Here is a valid definition (with a type annotation) for reference: + + greet : string -> string + greet name = + "Hello " ++ name ++ "!" + + The top line (called a "type annotation") is optional. You can leave it off if + you want. As you get more comfortable with Nash and as your project grows, it + becomes more and more valuable to add them though! They work great as + compiler-verified documentation, and they often improve error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_exposing.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_exposing.snap new file mode 100644 index 00000000..b93c9be8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_exposing.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN EXPOSING + + × I want to parse exposed values, but I am getting stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Exposed values are always surrounded by parentheses. So try adding a ( here? + + Note: Here are some valid examples of `exposing` for reference: + + import Data.Decoder exposing (..) + import Data.Decoder exposing (decode) + + If you are getting tripped up, you can just expose everything for now. It should + get easier to make an explicit exposing list as you see more examples in the + wild. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_fresh_line.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_fresh_line.snap new file mode 100644 index 00000000..67412d43 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_fresh_line.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +SYNTAX PROBLEM + + × I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: A top-level declaration must start on a fresh line with no spaces before it. + Move this declaration to its own line. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_alias.snap new file mode 100644 index 00000000..783743a8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_alias.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING IMPORT ALIAS + + × I was parsing an `import` until I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an alias next, like in these examples: + + import Cardano.Tx as Tx + import Data.Decoder as D + + Notice that the alias always starts with a capital letter. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_as.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_as.snap new file mode 100644 index 00000000..c87c8fa8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_as.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I am partway through parsing an import, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `import` declarations: + + import Cardano.Tx + import Cardano.Tx as Tx + import Cardano.Tx as Tx exposing (..) + import Data.Decoder exposing (decode) + + You are probably trying to import a different module, but try to make it look + like one of these examples! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_end.snap new file mode 100644 index 00000000..c87c8fa8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_end.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I am partway through parsing an import, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `import` declarations: + + import Cardano.Tx + import Cardano.Tx as Tx + import Cardano.Tx as Tx exposing (..) + import Data.Decoder exposing (decode) + + You are probably trying to import a different module, but try to make it look + like one of these examples! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing.snap new file mode 100644 index 00000000..c87c8fa8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I am partway through parsing an import, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `import` declarations: + + import Cardano.Tx + import Cardano.Tx as Tx + import Cardano.Tx as Tx exposing (..) + import Data.Decoder exposing (decode) + + You are probably trying to import a different module, but try to make it look + like one of these examples! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing_list.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing_list.snap new file mode 100644 index 00000000..b93c9be8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_exposing_list.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN EXPOSING + + × I want to parse exposed values, but I am getting stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Exposed values are always surrounded by parentheses. So try adding a ( here? + + Note: Here are some valid examples of `exposing` for reference: + + import Data.Decoder exposing (..) + import Data.Decoder exposing (decode) + + If you are getting tripped up, you can just expose everything for now. It should + get easier to make an explicit exposing list as you see more examples in the + wild. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_alias.snap new file mode 100644 index 00000000..c87c8fa8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_alias.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I am partway through parsing an import, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `import` declarations: + + import Cardano.Tx + import Cardano.Tx as Tx + import Cardano.Tx as Tx exposing (..) + import Data.Decoder exposing (decode) + + You are probably trying to import a different module, but try to make it look + like one of these examples! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_exposing_list.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_exposing_list.snap new file mode 100644 index 00000000..bec0fb1a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_exposing_list.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I was parsing an `import` until I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see the list of exposed values next. + + import Data.Decoder exposing (..) + import Data.Decoder exposing (decode) + + I generally recommend the second style. It is more explicit, making it much + easier to figure out where values are coming from in large projects! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_name.snap new file mode 100644 index 00000000..c87c8fa8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_indent_name.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I am partway through parsing an import, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `import` declarations: + + import Cardano.Tx + import Cardano.Tx as Tx + import Cardano.Tx as Tx exposing (..) + import Data.Decoder exposing (decode) + + You are probably trying to import a different module, but try to make it look + like one of these examples! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_name.snap new file mode 100644 index 00000000..d214f980 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_name.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING IMPORT NAME + + × I was parsing an `import` until I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a module name next, like in these examples: + + import Dict + import Option + import Cardano.Tx as Tx + import Data.Decoder exposing (..) + + Notice that the module names all start with capital letters. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_start.snap new file mode 100644 index 00000000..c87c8fa8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_import_start.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED IMPORT + + × I am partway through parsing an import, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `import` declarations: + + import Cardano.Tx + import Cardano.Tx as Tx + import Cardano.Tx as Tx exposing (..) + import Data.Decoder exposing (decode) + + You are probably trying to import a different module, but try to make it look + like one of these examples! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_infix.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_infix.snap new file mode 100644 index 00000000..f7755e2a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_infix.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +BAD INFIX + + × Something went wrong in this infix operator declaration: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: An infix declaration gives associativity, precedence, an operator in + parentheses, and its implementation name. For example: `infix left 6 (+) = add`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_name.snap new file mode 100644 index 00000000..61baf709 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_name.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING MODULE NAME + + × I was parsing a `module` declaration until I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see the module name next, like in these examples: + + module Dict exposing (..) + module Option exposing (..) + module Cardano.Tx exposing (..) + module Data.Decoder exposing (..) + + Notice that the module names all start with capital letters. That is required! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_problem.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_problem.snap new file mode 100644 index 00000000..cd116f27 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_problem.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED MODULE DECLARATION + + × I am parsing a `module` declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `module` declarations: + + module Main exposing (..) + module Dict exposing (Dict, empty, get) + + I generally recommend using an explicit exposing list. I can skip compiling a + bunch of files when the public interface of a module stays the same, so exposing + fewer values can help improve compile times! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_tests.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_tests.snap new file mode 100644 index 00000000..afe25a02 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_tests.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TESTS + + × I started parsing a tests section, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add an indented `test` or `prop` declaration. Test imports must come before the + declarations. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_validator.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_validator.snap new file mode 100644 index 00000000..e35337f2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_module_validator.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED VALIDATOR + + × I was parsing a validator declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: A validator module starts with `validator module`, followed by its module name + and exposing list. For example: `validator module Vesting exposing (main)`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_end.snap new file mode 100644 index 00000000..c39dd2d2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED LIST PATTERN + + × I was expecting a closing square bracket to end this list pattern: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ] to see if that helps? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_expr.snap new file mode 100644 index 00000000..0465ff91 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_expr.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN PATTERN + + × I wanted to parse a pattern next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure why I am getting stuck exactly. I just know that I want a pattern + next. Something as simple as maybeHeight or result would work! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_end.snap new file mode 100644 index 00000000..9c385666 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED LIST PATTERN + + × I was expecting a closing square bracket to end this list pattern: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ] to see if that helps? + + Note: I can get confused by indentation in cases like this, so maybe you have a + closing square bracket but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_expr.snap new file mode 100644 index 00000000..56ecfbdc --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_expr.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED LIST PATTERN + + × I am partway through parsing a list pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see another pattern next. Maybe a variable name. + + Note: I can get confused by indentation in cases like this, so maybe there is + more to this pattern but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_open.snap new file mode 100644 index 00000000..0923da27 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_indent_open.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED LIST PATTERN + + × I just saw an open square bracket, but then I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ] to see if that helps? + + Note: I can get confused by indentation in cases like this, so maybe there is + something next, but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_open.snap new file mode 100644 index 00000000..c02c3256 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_open.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED LIST PATTERN + + × I just saw an open square bracket, but then I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ] to see if that helps? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_list_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_end.snap new file mode 100644 index 00000000..e48aae21 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a closing curly brace next. Try adding a } here? + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_field.snap new file mode 100644 index 00000000..a3554908 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_field.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a field name next. + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_end.snap new file mode 100644 index 00000000..e48aae21 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a closing curly brace next. Try adding a } here? + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_field.snap new file mode 100644 index 00000000..a3554908 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_field.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a field name next. + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_open.snap new file mode 100644 index 00000000..a3554908 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_indent_open.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a field name next. + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_open.snap new file mode 100644 index 00000000..a3554908 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_open.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a field name next. + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_record_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_end.snap new file mode 100644 index 00000000..6dbbdd2e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_end.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED SYMBOL + + × I ran into the = symbol unexpectedly in this pattern: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Only the :: symbol works in patterns. It is useful if you are pattern matching + on lists, trying to get the first element off the front. Did you want that + instead? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_expr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_expr.snap new file mode 100644 index 00000000..0465ff91 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_expr.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN PATTERN + + × I wanted to parse a pattern next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure why I am getting stuck exactly. I just know that I want a pattern + next. Something as simple as maybeHeight or result would work! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_end.snap new file mode 100644 index 00000000..4d46258c --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I was expecting a closing parenthesis next: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ) to see if that helps? + + Note: I can get confused by indentation in cases like this, so maybe you have a + closing parenthesis but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr1.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr1.snap new file mode 100644 index 00000000..841559b2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr1.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I just saw an open parenthesis, but then I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a pattern next. Maybe it will end up being something like + (x,y) or (name, _)? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr_n.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr_n.snap new file mode 100644 index 00000000..26aab95e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_indent_expr_n.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TUPLE PATTERN + + × I am partway through parsing a tuple pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a pattern next. I am expecting the final result to be + something like (x,y) or (name, _). + + Note: I can get confused by indentation in cases like this, so the problem may + be that the next part is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_open.snap new file mode 100644 index 00000000..0916bc02 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_open.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I just saw an open parenthesis, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a pattern next. Maybe it will end up being something like + (x,y) or (name, _)? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_p_tuple_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_alias.snap new file mode 100644 index 00000000..c21f835b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_alias.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PATTERN + + × I was expecting to see a variable name after the `as` keyword: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: The `as` keyword lets you write patterns like ((x,y) as point) so you can refer + to individual parts of the tuple with x and y or you refer to the whole thing + with point. So I was expecting to see a variable name after the `as` keyword + here. Sometimes people just want to use `as` as a variable name though. Try + using a different name in that case! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_bytes.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_bytes.snap new file mode 100644 index 00000000..407906fe --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_bytes.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +ENDLESS BYTE STRING + + × I cannot find the end of this byte string: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a closing double quote to end the byte string. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_alias.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_alias.snap new file mode 100644 index 00000000..c21f835b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_alias.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PATTERN + + × I was expecting to see a variable name after the `as` keyword: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: The `as` keyword lets you write patterns like ((x,y) as point) so you can refer + to individual parts of the tuple with x and y or you refer to the whole thing + with point. So I was expecting to see a variable name after the `as` keyword + here. Sometimes people just want to use `as` as a variable name though. Try + using a different name in that case! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_start.snap new file mode 100644 index 00000000..3531c777 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_indent_start.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PATTERN + + × I wanted to parse a pattern next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure why I am getting stuck exactly. I just know that I want a pattern + next. Something as simple as maybeHeight or result would work! + + Note: I can get confused by indentation. If you think there is a pattern next, + maybe it needs to be indented a bit more? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_list.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_list.snap new file mode 100644 index 00000000..c02c3256 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_list.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED LIST PATTERN + + × I just saw an open square bracket, but then I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ] to see if that helps? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_number.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_number.snap new file mode 100644 index 00000000..dacfd88d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_number.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +WEIRD NUMBER + + × I thought I was reading a number, but I ran into some weird stuff here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I recognize integers like 42 and 0x002B. Is there a way to write it like one of + those? Nash has no floating point numbers. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_record.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_record.snap new file mode 100644 index 00000000..a3554908 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_record.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD PATTERN + + × I was partway through parsing a record pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a field name next. + + Hint: A record pattern looks like {x,y} or {name,age} where you list the field + names you want to access. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_start.snap new file mode 100644 index 00000000..0465ff91 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_start.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN PATTERN + + × I wanted to parse a pattern next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure why I am getting stuck exactly. I just know that I want a pattern + next. Something as simple as maybeHeight or result would work! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_string.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_string.snap new file mode 100644 index 00000000..3961eb0e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_string.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +ENDLESS STRING + + × I got to the end of the line without seeing the closing double quote: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Strings look like "this" with double quotes on each end. Is the closing double + quote missing in your code? For a string that spans multiple lines, use triple + double quotes on each end. + + """ + # Multi-line Strings + + - start with triple double quotes + - write whatever you want + - no need to escape newlines or double quotes + - end with triple double quotes + """ + + Here is a valid multi-line string for reference. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_tuple.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_tuple.snap new file mode 100644 index 00000000..0916bc02 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_tuple.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I just saw an open parenthesis, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a pattern next. Maybe it will end up being something like + (x,y) or (name, _)? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_wildcard_not_var.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_wildcard_not_var.snap new file mode 100644 index 00000000..7ec27fb9 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_pattern_wildcard_not_var.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED NAME + + × Variable names cannot start with underscores like this: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ──── + ╰──── + help: You can either have an underscore like _ to ignore the value, or you can have a + name like foo to use the matched value. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_arrow.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_arrow.snap new file mode 100644 index 00000000..a3262fad --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_arrow.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +REPRESENTATION ARROW + + × I found an arrow inside a representation bound: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Representation bounds do not have arrows. Use a bound such as Storable; the + compiler infers constructor kinds from type use. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_name.snap new file mode 100644 index 00000000..4e10e178 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_name.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNKNOWN REPRESENTATION + + × I do not recognize `Unknown` as a representation bound: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Use a supported representation bound: Big, Const, Term, or Storable. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_start.snap new file mode 100644 index 00000000..b41f05e7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_repr_start.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION + + × I was expecting a representation bound here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write a representation name such as Storable after the colon. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_colon.snap new file mode 100644 index 00000000..511ee03d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_colon.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I am partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I just saw a field name, so I was expecting to see a colon next. So try putting + a : sign here? + + Note: If you are trying to define a record type across multiple lines, I + recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_end.snap new file mode 100644 index 00000000..2f10ac88 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_end.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I am partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a closing curly brace before this, so try adding a } and + see if that helps? + + Note: When I get stuck like this, it usually means that there is a missing + parenthesis or bracket somewhere earlier. It could also be a stray keyword or + operator. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_field.snap new file mode 100644 index 00000000..82be3e83 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_field.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN RECORD TYPE + + × I am partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see another record field defined next, so I am looking for a + name like userName or plantHeight. + + Note: If you are trying to define a record type across multiple lines, I + recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_colon.snap new file mode 100644 index 00000000..45eb6aa7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_colon.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I am partway through parsing a record type. I just saw a record field, so I was + │ expecting to see a colon next: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try putting a : followed by a type? + + Note: I may be confused by indentation. For example, if you are trying to define + a record type across multiple lines, I recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_end.snap new file mode 100644 index 00000000..cd23bfb5 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_end.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I was partway through parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a closing curly brace next. Try putting a } next and see + if that helps? + + Note: I may be confused by indentation. For example, if you are trying to define + a record type across multiple lines, I recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_field.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_field.snap new file mode 100644 index 00000000..29f228f6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_field.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I am partway through parsing a record type, but I got stuck after that last + │ comma: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Trailing commas are not allowed in record types, so the fix may be to delete + that last comma? Or maybe you were in the middle of defining an additional + field? + + Note: I may be confused by indentation. For example, if you are trying to define + a record type across multiple lines, I recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_open.snap new file mode 100644 index 00000000..d2173e93 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_open.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I just saw the opening curly brace of a record type, but then I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am expecting a record like { name : string, age : int } here. Try defining + some fields of your own? + + Note: I may be confused by indentation. For example, if you are trying to define + a record type across multiple lines, I recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_type.snap new file mode 100644 index 00000000..042813b8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_indent_type.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I am partway through parsing a record type, and I was expecting to run into a + │ type next: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try putting something like int or string for now? + + Note: I may be confused by indentation. For example, if you are trying to define + a record type across multiple lines, I recommend using this format: + + { name : string + , age : int + , value : 'a + } + + Notice that each line starts with some indentation. Usually two or four spaces. + This is the stylistic convention in the Nash ecosystem. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_open.snap new file mode 100644 index 00000000..bfbd1c51 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_open.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I just started parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Record types look like { name : string, age : int }, so I was expecting to see a + field name next. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_type.snap new file mode 100644 index 00000000..22b8a2e2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_record_type.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TYPE ANNOTATION + + × I was partway through parsing the `f` type annotation, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_end.snap new file mode 100644 index 00000000..cab7d4a7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_end.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I was expecting to see a closing parenthesis next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ) to see if that helps? + + Note: I can get stuck when I run into keywords, operators, parentheses, or + brackets unexpectedly. So there may be some earlier syntax trouble (like extra + parentheses or missing brackets) that is confusing me. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_end.snap new file mode 100644 index 00000000..0e09e8e4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_end.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I was expecting to see a closing parenthesis next: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Try adding a ) to see if that helps! + + Note: I can get confused by indentation in cases like this, so maybe you have a + closing parenthesis but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_repr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_repr.snap new file mode 100644 index 00000000..3e636681 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_repr.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION + + × I was parsing a representation annotation, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a representation bound such as Storable after the colon, and keep it + indented inside the parentheses. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type1.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type1.snap new file mode 100644 index 00000000..de750833 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type1.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I just saw an open parenthesis, so I was expecting to see a type next. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Something like (option int) or (list 'a). Anything where you are putting + parentheses around normal types. + + Note: I can get confused by indentation in cases like this, so maybe you have a + type but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type_n.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type_n.snap new file mode 100644 index 00000000..6f46fbed --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_indent_type_n.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TUPLE TYPE + + × I think I am in the middle of parsing a tuple type. I just saw a comma, so I was + │ expecting to see a type next. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: A tuple type looks like (int,int) or (string,'a), so I think there is a type + missing here? + + Note: I can get confused by indentation in cases like this, so maybe you have a + type but it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_open.snap new file mode 100644 index 00000000..c135e56d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_open.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I just saw an open parenthesis, so I was expecting to see a type next. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Something like (option int) or (list 'a). Anything where you are putting + parentheses around normal types. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_repr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_repr.snap new file mode 100644 index 00000000..b41f05e7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_repr.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION + + × I was expecting a representation bound here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write a representation name such as Storable after the colon. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_type.snap new file mode 100644 index 00000000..22b8a2e2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_t_tuple_type.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TYPE ANNOTATION + + × I was partway through parsing the `f` type annotation, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_binder_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_binder_alignment.snap new file mode 100644 index 00000000..5b419c78 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_binder_alignment.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROPERTY BINDER ALIGNMENT + + × This generated input does not line up with the others: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Indent each generated input to column 3. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_body.snap new file mode 100644 index 00000000..0b9ce747 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_body.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DO + + × I was partway through parsing a `do` block, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: A `do` block must end with an expression. Add the final expression after this + binding. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_do.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_do.snap new file mode 100644 index 00000000..7c2eb666 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_do.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST BODY + + × I was expecting the test body here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Start the test body with `do`, then indent its statements on the following + lines. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_equals.snap new file mode 100644 index 00000000..37165bbf --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_equals.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST EQUALS + + × I have the test name, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add an = before the test body. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_fuzzer.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_fuzzer.snap new file mode 100644 index 00000000..1ab20290 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_fuzzer.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING EXPRESSION + + × I was partway through parsing a definition, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression like 42 or "hello". Once there is something + there, I can probably give a more specific hint! This can also happen if I run + into reserved words like `let` or `as` unexpectedly, or operators in unexpected + spots. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_in.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_in.snap new file mode 100644 index 00000000..030db3e7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_in.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING PROPERTY IN + + × I was parsing a property, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `in` after the generated inputs and before the property body. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_binder.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_binder.snap new file mode 100644 index 00000000..72b8e548 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_binder.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING PROPERTY BINDER + + × I was parsing generated inputs for a property, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Start the generated inputs with `let`, then write each pattern followed by `via` + and its fuzzer. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_body.snap new file mode 100644 index 00000000..7c2eb666 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_body.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST BODY + + × I was expecting the test body here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Start the test body with `do`, then indent its statements on the following + lines. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_equals.snap new file mode 100644 index 00000000..37165bbf --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_equals.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST EQUALS + + × I have the test name, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add an = before the test body. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_in.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_in.snap new file mode 100644 index 00000000..030db3e7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_in.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING PROPERTY IN + + × I was parsing a property, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `in` after the generated inputs and before the property body. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_name.snap new file mode 100644 index 00000000..083702c4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_indent_name.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST NAME + + × I was parsing a test declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Give this test a name in double quotes, such as `test "adds two numbers"`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_let.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_let.snap new file mode 100644 index 00000000..72b8e548 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_let.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING PROPERTY BINDER + + × I was parsing generated inputs for a property, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Start the generated inputs with `let`, then write each pattern followed by `via` + and its fuzzer. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name.snap new file mode 100644 index 00000000..3961eb0e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +ENDLESS STRING + + × I got to the end of the line without seeing the closing double quote: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Strings look like "this" with double quotes on each end. Is the closing double + quote missing in your code? For a string that spans multiple lines, use triple + double quotes on each end. + + """ + # Multi-line Strings + + - start with triple double quotes + - write whatever you want + - no need to escape newlines or double quotes + - end with triple double quotes + """ + + Here is a valid multi-line string for reference. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name_start.snap new file mode 100644 index 00000000..083702c4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_name_start.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST NAME + + × I was parsing a test declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Give this test a name in double quotes, such as `test "adds two numbers"`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_once_on_unit_test.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_once_on_unit_test.snap new file mode 100644 index 00000000..9cacec15 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_once_on_unit_test.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNEXPECTED ONCE + + × I found `once` on a unit test: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Unit tests already run once. Remove `once`, or use a property declaration when + you need generated inputs. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_pattern.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_pattern.snap new file mode 100644 index 00000000..0465ff91 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_pattern.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN PATTERN + + × I wanted to parse a pattern next, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I am not sure why I am getting stuck exactly. I just know that I want a pattern + next. Something as simple as maybeHeight or result would work! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_via.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_via.snap new file mode 100644 index 00000000..b97a7439 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_via.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING FUZZER + + × I have the property input pattern, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `via` followed by the fuzzer expression that generates this input. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_duplicate.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_duplicate.snap new file mode 100644 index 00000000..075fc490 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_duplicate.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +DUPLICATE TEST BUDGET + + × This budget kind has already been specified: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Keep one limit for each budget kind in the `within` clause. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_end.snap new file mode 100644 index 00000000..81101791 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TEST BUDGET + + × I was parsing the `within` clause, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Separate budget limits with a comma, and close the clause with ). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_kind.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_kind.snap new file mode 100644 index 00000000..0869ee13 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_kind.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNKNOWN TEST BUDGET + + × I was expecting a test budget kind here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Use `cpu` or `mem` followed by an integer limit. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_number.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_number.snap new file mode 100644 index 00000000..dacfd88d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_number.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +WEIRD NUMBER + + × I thought I was reading a number, but I ran into some weird stuff here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I recognize integers like 42 and 0x002B. Is there a way to write it like one of + those? Nash has no floating point numbers. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_open.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_open.snap new file mode 100644 index 00000000..9030e6af --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_test_within_open.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TEST BUDGET + + × I saw `within`, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Put the budget inside parentheses, with a budget kind and an integer limit. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_alignment.snap new file mode 100644 index 00000000..0747a15e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_alignment.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +TEST ALIGNMENT + + × This test declaration does not line up with the others: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Indent every declaration in this tests section to column 3. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_import.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_import.snap new file mode 100644 index 00000000..cd116f27 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_import.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED MODULE DECLARATION + + × I am parsing a `module` declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Here are some examples of valid `module` declarations: + + module Main exposing (..) + module Dict exposing (Dict, empty, get) + + I generally recommend using an explicit exposing list. I can skip compiling a + bunch of files when the public interface of a module stays the same, so exposing + fewer values can help improve compile times! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_indent_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_indent_start.snap new file mode 100644 index 00000000..afe25a02 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_indent_start.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TESTS + + × I started parsing a tests section, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add an indented `test` or `prop` declaration. Test imports must come before the + declarations. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_start.snap new file mode 100644 index 00000000..afe25a02 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_start.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TESTS + + × I started parsing a tests section, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add an indented `test` or `prop` declaration. Test imports must come before the + declarations. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_test.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_test.snap new file mode 100644 index 00000000..083702c4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_tests_test.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TEST NAME + + × I was parsing a test declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Give this test a name in double quotes, such as `test "adds two numbers"`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_alignment.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_alignment.snap new file mode 100644 index 00000000..efa1ab80 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_alignment.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +TRAIT METHOD ALIGNMENT + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: All methods in this trait must start in column 3. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_colon.snap new file mode 100644 index 00000000..e7662cb8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_colon.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING METHOD COLON + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a colon between the method name and its type annotation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_default.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_default.snap new file mode 100644 index 00000000..35725444 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_default.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED DEFINITION + + × I got stuck while parsing the `f` definition: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see an expression next. What is it equal to? + + greet : string -> string + greet name = + "Hello " ++ name + + The top line is an optional type annotation. It works as compiler-verified + documentation and often improves error messages! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_colon.snap new file mode 100644 index 00000000..e7662cb8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_colon.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING METHOD COLON + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a colon between the method name and its type annotation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_method.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_method.snap new file mode 100644 index 00000000..9268a7b4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_method.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT METHOD + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting an indented method signature, such as `show : 'a -> string`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_name.snap new file mode 100644 index 00000000..cb30c8c4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_name.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT NAME + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a capitalized trait name, such as Eq or Show. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_param.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_param.snap new file mode 100644 index 00000000..ac5847bd --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_param.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT PARAMETER + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a quoted type parameter, such as 'a, and keep it indented farther than the + trait declaration. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_type.snap new file mode 100644 index 00000000..68c1da61 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_type.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING METHOD TYPE + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I just saw a colon, so I was expecting a method type next. Indent the type + farther than the method name. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_where.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_where.snap new file mode 100644 index 00000000..f9b960c2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_indent_where.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT WHERE + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `where` after the trait parameters and superclass constraints, before the + method declarations. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_method_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_method_name.snap new file mode 100644 index 00000000..9268a7b4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_method_name.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT METHOD + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting an indented method signature, such as `show : 'a -> string`. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_name.snap new file mode 100644 index 00000000..cb30c8c4 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_name.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT NAME + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a capitalized trait name, such as Eq or Show. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_param.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_param.snap new file mode 100644 index 00000000..25adcd7e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_param.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TYPE PARAMETER + + × I was parsing a type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Type parameters start with a quote, such as 'a. A representation annotation + looks like ('a : Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super.snap new file mode 100644 index 00000000..1400e408 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN SUPERCLASS CONSTRAINT + + × I was partway through parsing a superclass constraint, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a trait name and its type arguments next. For example, Eq 'a. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super_arg.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super_arg.snap new file mode 100644 index 00000000..2c5f9c72 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_super_arg.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +BAD SUPERCLASS ARGUMENT + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: A superclass argument must be one of the trait's quoted type parameters, such as + 'a. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_type.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_type.snap new file mode 100644 index 00000000..096e3bd1 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_type.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TRAIT METHOD TYPE + + × I was partway through parsing a trait method type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_where.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_where.snap new file mode 100644 index 00000000..f9b960c2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_trait_where.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TRAIT WHERE + + × I was parsing a trait declaration, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add `where` after the trait parameters and superclass constraints, before the + method declarations. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_body.snap new file mode 100644 index 00000000..890b1af8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_body.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TYPE ALIAS + + × I was partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_equals.snap new file mode 100644 index 00000000..6fe36768 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_equals.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TYPE ALIAS + + × I am partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type variable or an equals sign next. + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_body.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_body.snap new file mode 100644 index 00000000..d42a1bd8 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_body.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TYPE ALIAS + + × I am partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Something as simple as int or string would + work! + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_equals.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_equals.snap new file mode 100644 index 00000000..8eb7d2ab --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_indent_equals.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TYPE ALIAS + + × I am partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type variable or an equals sign next. + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_name.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_name.snap new file mode 100644 index 00000000..b3e93bbe --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_name.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +EXPECTING TYPE ALIAS NAME + + × I am partway through parsing a type alias, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting a name like account or point next. Nash uses lower-case names + for little aliases and capitalized names for Big aliases. + + Note: Here is an example of a valid `type alias` for reference: + + type alias Account = { owner : Bytes, balance : Int } + + This would let us use `Account` as a shorthand for that record type. Using this + shorthand makes type annotations much easier to read, and makes changing code + easier if you decide later that there is more to an account than owner and + balance! diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_param.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_param.snap new file mode 100644 index 00000000..25adcd7e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_param.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TYPE PARAMETER + + × I was parsing a type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Type parameters start with a quote, such as 'a. A representation annotation + looks like ('a : Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_alias_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_context.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_context.snap new file mode 100644 index 00000000..475a419e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_context.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +BAD TYPE CONSTRAINT + + × I was parsing a type constraint, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write a trait followed by its type arguments before =>. For example: `Eq 'a => + 'a -> 'a -> bool`. Separate multiple constraints with commas inside parentheses. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_after_context.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_after_context.snap new file mode 100644 index 00000000..8677202b --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_after_context.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED CONSTRAINED TYPE + + × I just saw => after the type constraints, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add the type after => and indent it farther than the start of the annotation. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_start.snap new file mode 100644 index 00000000..a64b9a9a --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_indent_start.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TYPE ANNOTATION + + × I was partway through parsing a type annotation, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? + + Note: I can get confused by indentation. If you think there is already a type + next, maybe it is not indented enough? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_colon.snap new file mode 100644 index 00000000..2b2765b6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_colon.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION COLON + + × I have the type parameter name, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Put a colon between the type parameter and its representation bound, as in ('a : + Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_end.snap new file mode 100644 index 00000000..caed48d7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TYPE PARAMETER + + × I was parsing an annotated type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a closing parenthesis after the representation bound, as in ('a : Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_colon.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_colon.snap new file mode 100644 index 00000000..2b2765b6 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_colon.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION COLON + + × I have the type parameter name, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Put a colon between the type parameter and its representation bound, as in ('a : + Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_end.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_end.snap new file mode 100644 index 00000000..caed48d7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_end.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED TYPE PARAMETER + + × I was parsing an annotated type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a closing parenthesis after the representation bound, as in ('a : Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_repr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_repr.snap new file mode 100644 index 00000000..3b8a9b2f --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_indent_repr.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION + + × I just saw a colon after the type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Add a representation bound such as Storable, indented inside the parentheses. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_repr.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_repr.snap new file mode 100644 index 00000000..b41f05e7 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_repr.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING REPRESENTATION + + × I was expecting a representation bound here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Write a representation name such as Storable after the colon. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_start.snap new file mode 100644 index 00000000..25adcd7e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_param_start.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TYPE PARAMETER + + × I was parsing a type parameter, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Type parameters start with a quote, such as 'a. A representation annotation + looks like ('a : Storable). diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_record.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_record.snap new file mode 100644 index 00000000..bfbd1c51 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_record.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED RECORD TYPE + + × I just started parsing a record type, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Record types look like { name : string, age : int }, so I was expecting to see a + field name next. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_space.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_space.snap new file mode 100644 index 00000000..971a2c3e --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_space.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +NO TABS + + × I ran into a tab, but tabs are not allowed in Nash files. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Replace the tab with spaces. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_start.snap new file mode 100644 index 00000000..22b8a2e2 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_start.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +PROBLEM IN TYPE ANNOTATION + + × I was partway through parsing the `f` type annotation, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: I was expecting to see a type next. Try putting int or string for now? diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_tuple.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_tuple.snap new file mode 100644 index 00000000..c135e56d --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_tuple.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +UNFINISHED PARENTHESES + + × I just saw an open parenthesis, so I was expecting to see a type next. + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Something like (option int) or (list 'a). Anything where you are putting + parentheses around normal types. diff --git a/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_var_start.snap b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_var_start.snap new file mode 100644 index 00000000..f690bf31 --- /dev/null +++ b/crates/nash-report/src/syntax/snapshots/nash_report__syntax__variants__variant_type_var_start.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/syntax/variants.rs +expression: "render_plain(&report, &source, \"src/Main.nash\")" +--- +MISSING TYPE VARIABLE + + × I just saw a quote marking a type variable, but I got stuck here: + ╭─[src/Main.nash:1:3] + 1 │ f = value + · ─ + ╰──── + help: Type variables have a quote followed by a lower-case name, such as 'a or + 'result. diff --git a/crates/nash-report/src/syntax/tests.rs b/crates/nash-report/src/syntax/tests.rs new file mode 100644 index 00000000..b02d3101 --- /dev/null +++ b/crates/nash-report/src/syntax/tests.rs @@ -0,0 +1,400 @@ +use super::*; +use crate::render_plain; + +fn parse_error_report(input: &str) -> String { + let bump = bumpalo::Bump::new(); + let error = nash_parse::Parser::new(&bump, input.as_bytes()) + .module() + .expect_err("expected parse error"); + render_plain( + &to_report(&Source::new(input), &Error::ParseError(&error)), + &Source::new(input), + "src/Main.nash", + ) +} + +macro_rules! syntax_snapshot { + ($name:ident, $input:expr) => { + #[test] + fn $name() { + let input = $input; + insta::with_settings!({ description => format!("Code:\n\n{input}"), omit_expression => true }, { + insta::assert_snapshot!(parse_error_report(input)); + }); + } + }; +} + +syntax_snapshot!(module_problem, "module"); +syntax_snapshot!(module_name_lowercase, "module main exposing (..)"); +syntax_snapshot!(exposing_missing_paren, "module Main exposing .."); +syntax_snapshot!(exposing_value_bad, "module Main exposing (1)"); +syntax_snapshot!(import_missing_name, "import"); +syntax_snapshot!(import_bad_alias, "import Cardano.Tx as tx"); +syntax_snapshot!( + import_exposing_list_missing_paren, + "import Cardano.Tx exposing x" +); +syntax_snapshot!(space_has_tab, "x =\t1"); +syntax_snapshot!(space_endless_comment, "{- unfinished"); +syntax_snapshot!(weird_end_reserved_word, "module Main exposing (..)\n\nif"); +syntax_snapshot!(weird_end_close_paren, "module Main exposing (..)\n\n)"); +syntax_snapshot!(weird_end_operator, "module Main exposing (..)\n\n+"); +syntax_snapshot!(fresh_line_after_decl, "x = 1 y = 2"); +syntax_snapshot!(type_alias_missing_equals, "type alias account"); +syntax_snapshot!(type_alias_bad_body, "type alias account = 42"); +syntax_snapshot!(custom_type_missing_variant, "type option 'a ="); +syntax_snapshot!(decl_def_missing_equals, "f x"); +syntax_snapshot!(decl_def_name_match, "f : int\ng = 1"); +syntax_snapshot!(decl_def_indent_body, "f =\n1"); +syntax_snapshot!(pattern_alias_missing_name, "f (x as) = x"); +syntax_snapshot!(pattern_wildcard_not_var, "f _foo = 1"); +syntax_snapshot!(pattern_record_missing_end, "f {x = 1"); +syntax_snapshot!(pattern_tuple_missing_end, "f (x = 1"); +syntax_snapshot!(pattern_list_missing_end, "f [x = 1"); +syntax_snapshot!(type_start_bad_in_annotation, "f : 42\nf = 1"); +syntax_snapshot!(type_record_missing_colon, "f : { x int }\nf = 1"); +syntax_snapshot!(type_tuple_missing_end, "f : (int, int\nf = 1"); + +#[test] +fn module_name_missing() { + insta::assert_snapshot!(render_plain( + &to_report(&Source::new(""), &Error::ModuleNameUnspecified("Main")), + &Source::new(""), + "src/Main.nash" + )); +} + +#[test] +fn module_name_mismatch() { + let input = "module Other exposing (..)"; + let error = Error::ModuleNameMismatch { + expected: "Main", + actual: "Other", + row: 1, + col: 8, + }; + insta::assert_snapshot!(render_plain( + &to_report(&Source::new(input), &error), + &Source::new(input), + "src/Main.nash" + )); +} + +macro_rules! report_branch { + ($name:ident, $input:expr, $source:ident, $report:expr) => { + #[test] + fn $name() { + let $source = Source::new($input); + let report = $report; + insta::with_settings!({ description => $input, omit_expression => true }, { + insta::assert_snapshot!(render_plain(&report, &$source, "src/Main.nash")); + }); + } + }; +} +use nash_parse::error::{ + CustomType, DeclDef, Exposing, Module, PList, PRecord, PTuple, Pattern, TRecord, TTuple, Type, + TypeAlias, +}; +report_branch!( + weird_end_semicolon, + ";", + s, + module::to_weird_end_report(&s, 1, 1) +); +report_branch!( + weird_end_comma, + ",", + s, + module::to_weird_end_report(&s, 1, 1) +); +report_branch!( + weird_end_backtick, + "`", + s, + module::to_weird_end_report(&s, 1, 1) +); +report_branch!( + weird_end_uppercase, + "Thing", + s, + module::to_weird_end_report(&s, 1, 1) +); +report_branch!( + weird_end_lowercase, + "thing", + s, + module::to_weird_end_report(&s, 1, 1) +); +report_branch!( + weird_end_empty, + "", + s, + module::to_weird_end_report(&s, 1, 1) +); +report_branch!( + decl_start_uppercase, + "Thing", + s, + decl::to_decl_start_report(&s, 1, 1) +); +report_branch!( + decl_start_import, + "import", + s, + decl::to_decl_start_report(&s, 1, 1) +); +report_branch!( + decl_start_case, + "case", + s, + decl::to_decl_start_report(&s, 1, 1) +); +report_branch!(decl_start_if, "if", s, decl::to_decl_start_report(&s, 1, 1)); +report_branch!( + fresh_line_keyword, + "module", + s, + module::to_parse_error_report(&s, &Module::FreshLine(1, 1)) +); +report_branch!( + exposing_reserved_word, + "if", + s, + module::to_exposing_report(&s, &Exposing::Value(1, 1), 1, 1) +); +report_branch!( + exposing_bare_operator, + "+", + s, + module::to_exposing_report(&s, &Exposing::Value(1, 1), 1, 1) +); +report_branch!( + alias_reserved_parameter, + "if", + s, + decl::to_type_alias_report(&s, &TypeAlias::Equals(1, 1), 1, 1) +); +report_branch!( + custom_type_reserved_parameter, + "if", + s, + decl::to_custom_type_report(&s, &CustomType::Equals(1, 1), 1, 1) +); +report_branch!( + definition_reserved_argument, + "if", + s, + decl::to_decl_def_report(&s, "f", &DeclDef::Equals(1, 1), 1, 1) +); +report_branch!( + definition_missing_colon, + "->", + s, + decl::to_decl_def_report(&s, "f", &DeclDef::Equals(1, 1), 1, 1) +); +report_branch!( + definition_unexpected_operator, + "+", + s, + decl::to_decl_def_report(&s, "f", &DeclDef::Equals(1, 1), 1, 1) +); +report_branch!( + pattern_start_in_case, + "if", + s, + pattern::to_pattern_report(&s, pattern::PContext::Case, &Pattern::Start(1, 1), 1, 1) +); +report_branch!( + pattern_start_in_arg, + "if", + s, + pattern::to_pattern_report(&s, pattern::PContext::Arg, &Pattern::Start(1, 1), 1, 1) +); +report_branch!( + pattern_start_in_let, + "if", + s, + pattern::to_pattern_report(&s, pattern::PContext::Let, &Pattern::Start(1, 1), 1, 1) +); +report_branch!( + pattern_negative_number, + "-1", + s, + pattern::to_pattern_report(&s, pattern::PContext::Arg, &Pattern::Start(1, 1), 1, 1) +); +report_branch!( + pattern_reserved_record_field, + "if", + s, + pattern::to_p_record_report(&s, &PRecord::Field(1, 1), 1, 1) +); +report_branch!( + pattern_reserved_tuple_open, + "if", + s, + pattern::to_p_tuple_report(&s, pattern::PContext::Arg, &PTuple::Open(1, 1), 1, 1) +); +report_branch!( + pattern_reserved_tuple_end, + "if", + s, + pattern::to_p_tuple_report(&s, pattern::PContext::Arg, &PTuple::End(1, 1), 1, 1) +); +report_branch!( + pattern_stray_bracket, + "]", + s, + pattern::to_p_tuple_report(&s, pattern::PContext::Arg, &PTuple::End(1, 1), 1, 1) +); +report_branch!( + pattern_reserved_list_open, + "if", + s, + pattern::to_p_list_report(&s, pattern::PContext::Arg, &PList::Open(1, 1), 1, 1) +); +report_branch!( + pattern_underscore_only_name, + "___", + s, + pattern::to_pattern_report( + &s, + pattern::PContext::Arg, + &Pattern::WildcardNotVar("___", 3, 1, 1), + 1, + 1 + ) +); +report_branch!( + pattern_underscore_uppercase_name, + "_Thing", + s, + pattern::to_pattern_report( + &s, + pattern::PContext::Arg, + &Pattern::WildcardNotVar("_Thing", 6, 1, 1), + 1, + 1 + ) +); +report_branch!( + type_reserved_word, + "if", + s, + type_::to_type_report( + &s, + type_::TContext::Annotation("f"), + &Type::Start(1, 1), + 1, + 1 + ) +); +report_branch!( + type_start_in_custom_type, + "42", + s, + type_::to_type_report(&s, type_::TContext::CustomType, &Type::Start(1, 1), 1, 1) +); +report_branch!( + type_start_in_alias, + "42", + s, + type_::to_type_report(&s, type_::TContext::TypeAlias, &Type::Start(1, 1), 1, 1) +); +report_branch!( + type_indent_in_custom_type, + "42", + s, + type_::to_type_report( + &s, + type_::TContext::CustomType, + &Type::IndentStart(1, 1), + 1, + 1 + ) +); +report_branch!( + type_indent_in_alias, + "42", + s, + type_::to_type_report( + &s, + type_::TContext::TypeAlias, + &Type::IndentStart(1, 1), + 1, + 1 + ) +); +report_branch!( + type_record_reserved_open, + "if", + s, + type_::to_t_record_report( + &s, + type_::TContext::Annotation("f"), + &TRecord::Open(1, 1), + 1, + 1 + ) +); +report_branch!( + type_record_reserved_field, + "if", + s, + type_::to_t_record_report( + &s, + type_::TContext::Annotation("f"), + &TRecord::Field(1, 1), + 1, + 1 + ) +); +report_branch!( + type_record_double_comma, + ",", + s, + type_::to_t_record_report( + &s, + type_::TContext::Annotation("f"), + &TRecord::Field(1, 1), + 1, + 1 + ) +); +report_branch!( + type_record_trailing_comma, + "}", + s, + type_::to_t_record_report( + &s, + type_::TContext::Annotation("f"), + &TRecord::Field(1, 1), + 1, + 1 + ) +); +report_branch!( + type_record_underindented_close, + "f : { x : int\n}", + s, + type_::to_t_record_report( + &s, + type_::TContext::Annotation("f"), + &TRecord::IndentEnd(1, 14), + 1, + 5 + ) +); +report_branch!( + type_tuple_reserved_open, + "if", + s, + type_::to_t_tuple_report( + &s, + type_::TContext::Annotation("f"), + &TTuple::Open(1, 1), + 1, + 1 + ) +); diff --git a/crates/nash-report/src/syntax/type_.rs b/crates/nash-report/src/syntax/type_.rs new file mode 100644 index 00000000..6da09b30 --- /dev/null +++ b/crates/nash-report/src/syntax/type_.rs @@ -0,0 +1,460 @@ +use super::{Doc, Report, Source, problem, to_space_report, wide}; +use crate::code::{Next, to_keyword_region}; +use nash_parse::error::{Repr, TRecord, TTuple, Type, TypeParam}; +use nash_parse::{Col, Row}; +#[derive(Clone, Copy)] +pub(crate) enum TContext<'a> { + Annotation(&'a str), + CustomType, + TypeAlias, + Superclass, + TraitMethod, + ImplHead, +} +pub(crate) fn to_type_report( + source: &Source<'_>, + context: TContext<'_>, + error: &Type<'_>, + sr: Row, + sc: Col, +) -> Report { + let thing = match context { + TContext::Annotation(_) => "type annotation", + TContext::CustomType => "custom type", + TContext::TypeAlias => "type alias", + TContext::Superclass => "superclass constraint", + TContext::TraitMethod => "trait method type", + TContext::ImplHead => "impl head", + }; + let expected = match context { + TContext::Superclass => { + "I was expecting a trait name and its type arguments next. For example, Eq 'a." + } + TContext::ImplHead => { + "I was expecting a trait name and its type arguments next. For example, Show int." + } + TContext::Annotation(_) + | TContext::CustomType + | TContext::TypeAlias + | TContext::TraitMethod => { + "I was expecting to see a type next. Try putting int or string for now?" + } + }; + let report = match *error { + Type::Record(e, r, c) => return to_t_record_report(source, context, e, r, c), + Type::Tuple(e, r, c) => return to_t_tuple_report(source, context, e, r, c), + Type::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Type::Start(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow( + "I was expecting to see a type next, but I got stuck on this reserved word:", + ), + Doc::reflow(&format!( + "It looks like you are trying to use `{k}` as a type variable, but it is a reserved word. Try using a different name!" + )), + ), + _ => { + let something = match context { + TContext::Annotation(name) => format!("the `{name}` type annotation"), + TContext::CustomType => "a custom type".into(), + TContext::TypeAlias => "a type alias".into(), + TContext::Superclass => "a superclass constraint".into(), + TContext::TraitMethod => "a trait method type".into(), + TContext::ImplHead => "an impl head".into(), + }; + problem( + &format!("PROBLEM IN {}", thing.to_uppercase()), + r, + c, + &format!("I was partway through parsing {something}, but I got stuck here:"), + expected, + ) + } + }, + Type::IndentStart(r, c) => with_note( + problem( + &format!("UNFINISHED {}", thing.to_uppercase()), + r, + c, + &format!( + "I was partway through parsing {} {thing}, but I got stuck here:", + if matches!(context, TContext::ImplHead) { + "an" + } else { + "a" + } + ), + expected, + ), + Doc::to_simple_note( + "I can get confused by indentation. If you think there is already a type next, maybe it is not indented enough?", + ), + ), + Type::VarStart(r, c) => problem( + "MISSING TYPE VARIABLE", + r, + c, + "I just saw a quote marking a type variable, but I got stuck here:", + "Type variables have a quote followed by a lower-case name, such as 'a or 'result.", + ), + Type::Context(r, c) => problem( + "BAD TYPE CONSTRAINT", + r, + c, + "I was parsing a type constraint, but I got stuck here:", + "Write a trait followed by its type arguments before =>. For example: `Eq 'a => 'a -> 'a -> bool`. Separate multiple constraints with commas inside parentheses.", + ), + Type::IndentAfterContext(r, c) => problem( + "UNFINISHED CONSTRAINED TYPE", + r, + c, + "I just saw => after the type constraints, but I got stuck here:", + "Add the type after => and indent it farther than the start of the annotation.", + ), + }; + wide(report, sr, sc) +} +fn with_note(mut report: Report, note: Doc) -> Report { + report.after = Doc::stack([report.after, note]); + report +} +fn field_keyword(r: Row, c: Col, k: &str, before: &str) -> Report { + Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow(before), + Doc::reflow(&format!( + "It looks like you are trying to use `{k}` as a field name, but that is a reserved word. Try using a different name!" + )), + ) +} +pub(super) fn to_t_record_report( + source: &Source<'_>, + context: TContext<'_>, + error: &TRecord<'_>, + sr: Row, + sc: Col, +) -> Report { + let before = "I am partway through parsing a record type, but I got stuck here:"; + let report = match *error { + TRecord::Space(ref e, r, c) => return to_space_report(source, e, r, c), + TRecord::Type(e, r, c) => return to_type_report(source, context, e, r, c), + TRecord::Open(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => field_keyword( + r, + c, + k, + "I just started parsing a record type, but I got stuck on this field name:", + ), + _ => problem( + "UNFINISHED RECORD TYPE", + r, + c, + "I just started parsing a record type, but I got stuck here:", + "Record types look like { name : string, age : int }, so I was expecting to see a field name next.", + ), + }, + TRecord::End(r, c) => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + before, + "I was expecting to see a closing curly brace before this, so try adding a } and see if that helps?", + ), + Doc::to_simple_note( + "When I get stuck like this, it usually means that there is a missing parenthesis or bracket somewhere earlier. It could also be a stray keyword or operator.", + ), + ), + TRecord::Field(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => field_keyword( + r, + c, + k, + "I am partway through parsing a record type, but I got stuck on this field name:", + ), + Next::Other(Some(',')) => with_note( + problem( + "EXTRA COMMA", + r, + c, + before, + "I am seeing two commas in a row. This is the second one! Just delete one of the commas and you should be all set!", + ), + note_for_record_type_error(false), + ), + Next::Close(_, '}') => with_note( + problem( + "EXTRA COMMA", + r, + c, + before, + "Trailing commas are not allowed in record types. Try deleting the comma that appears before this closing curly brace.", + ), + note_for_record_type_error(false), + ), + _ => with_note( + problem( + "PROBLEM IN RECORD TYPE", + r, + c, + before, + "I was expecting to see another record field defined next, so I am looking for a name like userName or plantHeight.", + ), + note_for_record_type_error(false), + ), + }, + TRecord::Colon(r, c) => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + before, + "I just saw a field name, so I was expecting to see a colon next. So try putting a : sign here?", + ), + note_for_record_type_error(false), + ), + TRecord::IndentOpen(r, c) => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + "I just saw the opening curly brace of a record type, but then I got stuck here:", + "I am expecting a record like { name : string, age : int } here. Try defining some fields of your own?", + ), + note_for_record_type_error(true), + ), + TRecord::IndentEnd(r, c) => match source.next_line_starts_with_close_curly(r) { + Some((r, c)) => with_note( + problem( + "NEED MORE INDENTATION", + r, + c, + "I was partway through parsing a record type, but I got stuck here:", + "I need this curly brace to be indented more. Try adding some spaces before it!", + ), + note_for_record_type_error(false), + ), + None => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + "I was partway through parsing a record type, but I got stuck here:", + "I was expecting to see a closing curly brace next. Try putting a } next and see if that helps?", + ), + note_for_record_type_error(true), + ), + }, + TRecord::IndentField(r, c) => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + "I am partway through parsing a record type, but I got stuck after that last comma:", + "Trailing commas are not allowed in record types, so the fix may be to delete that last comma? Or maybe you were in the middle of defining an additional field?", + ), + note_for_record_type_error(true), + ), + TRecord::IndentColon(r, c) => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + "I am partway through parsing a record type. I just saw a record field, so I was expecting to see a colon next:", + "Try putting a : followed by a type?", + ), + note_for_record_type_error(true), + ), + TRecord::IndentType(r, c) => with_note( + problem( + "UNFINISHED RECORD TYPE", + r, + c, + "I am partway through parsing a record type, and I was expecting to run into a type next:", + "Try putting something like int or string for now?", + ), + note_for_record_type_error(true), + ), + }; + wide(report, sr, sc) +} +fn note_for_record_type_error(indent: bool) -> Doc { + Doc::stack([ + Doc::to_simple_note(if indent { + "I may be confused by indentation. For example, if you are trying to define a record type across multiple lines, I recommend using this format:" + } else { + "If you are trying to define a record type across multiple lines, I recommend using this format:" + }), + Doc::indent( + 4, + Doc::vcat([ + Doc::text("{ name : string"), + Doc::text(", age : int"), + Doc::text(", value : 'a"), + Doc::text("}"), + ]), + ), + Doc::reflow( + "Notice that each line starts with some indentation. Usually two or four spaces. This is the stylistic convention in the Nash ecosystem.", + ), + ]) +} +pub(super) fn to_t_tuple_report( + source: &Source<'_>, + context: TContext<'_>, + error: &TTuple<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + TTuple::Space(ref e, r, c) => return to_space_report(source, e, r, c), + TTuple::Type(e, r, c) => return to_type_report(source, context, e, r, c), + TTuple::Repr(e, r, c) => return to_repr_report(source, e, r, c), + TTuple::IndentRepr(r, c) => problem( + "MISSING REPRESENTATION", + r, + c, + "I was parsing a representation annotation, but I got stuck here:", + "Add a representation bound such as Storable after the colon, and keep it indented inside the parentheses.", + ), + TTuple::Open(r, c) => match source.what_is_next(r, c) { + Next::Keyword(k) => Report::snippet( + "RESERVED WORD", + to_keyword_region(r, c, k), + None, + Doc::reflow("I ran into a reserved word unexpectedly:"), + Doc::reflow(&format!( + "It looks like you are trying to use `{k}` as a variable name, but it is a reserved word. Try using a different name!" + )), + ), + _ => problem( + "UNFINISHED PARENTHESES", + r, + c, + "I just saw an open parenthesis, so I was expecting to see a type next.", + "Something like (option int) or (list 'a). Anything where you are putting parentheses around normal types.", + ), + }, + TTuple::End(r, c) => with_note( + problem( + "UNFINISHED PARENTHESES", + r, + c, + "I was expecting to see a closing parenthesis next, but I got stuck here:", + "Try adding a ) to see if that helps?", + ), + Doc::to_simple_note( + "I can get stuck when I run into keywords, operators, parentheses, or brackets unexpectedly. So there may be some earlier syntax trouble (like extra parentheses or missing brackets) that is confusing me.", + ), + ), + TTuple::IndentType1(r, c) => with_note( + problem( + "UNFINISHED PARENTHESES", + r, + c, + "I just saw an open parenthesis, so I was expecting to see a type next.", + "Something like (option int) or (list 'a). Anything where you are putting parentheses around normal types.", + ), + Doc::to_simple_note( + "I can get confused by indentation in cases like this, so maybe you have a type but it is not indented enough?", + ), + ), + TTuple::IndentTypeN(r, c) => with_note( + problem( + "UNFINISHED TUPLE TYPE", + r, + c, + "I think I am in the middle of parsing a tuple type. I just saw a comma, so I was expecting to see a type next.", + "A tuple type looks like (int,int) or (string,'a), so I think there is a type missing here?", + ), + Doc::to_simple_note( + "I can get confused by indentation in cases like this, so maybe you have a type but it is not indented enough?", + ), + ), + TTuple::IndentEnd(r, c) => with_note( + problem( + "UNFINISHED PARENTHESES", + r, + c, + "I was expecting to see a closing parenthesis next:", + "Try adding a ) to see if that helps!", + ), + Doc::to_simple_note( + "I can get confused by indentation in cases like this, so maybe you have a closing parenthesis but it is not indented enough?", + ), + ), + }; + wide(report, sr, sc) +} +pub(crate) fn to_type_param_report( + source: &Source<'_>, + error: &TypeParam<'_>, + sr: Row, + sc: Col, +) -> Report { + let report = match *error { + TypeParam::Space(ref e, r, c) => return to_space_report(source, e, r, c), + TypeParam::Repr(e, r, c) => return to_repr_report(source, e, r, c), + TypeParam::Start(r, c) => problem( + "MISSING TYPE PARAMETER", + r, + c, + "I was parsing a type parameter, but I got stuck here:", + "Type parameters start with a quote, such as 'a. A representation annotation looks like ('a : Storable).", + ), + TypeParam::Colon(r, c) | TypeParam::IndentColon(r, c) => problem( + "MISSING REPRESENTATION COLON", + r, + c, + "I have the type parameter name, but I got stuck here:", + "Put a colon between the type parameter and its representation bound, as in ('a : Storable).", + ), + TypeParam::End(r, c) | TypeParam::IndentEnd(r, c) => problem( + "UNFINISHED TYPE PARAMETER", + r, + c, + "I was parsing an annotated type parameter, but I got stuck here:", + "Add a closing parenthesis after the representation bound, as in ('a : Storable).", + ), + TypeParam::IndentRepr(r, c) => problem( + "MISSING REPRESENTATION", + r, + c, + "I just saw a colon after the type parameter, but I got stuck here:", + "Add a representation bound such as Storable, indented inside the parentheses.", + ), + }; + wide(report, sr, sc) +} +pub(super) fn to_repr_report(source: &Source<'_>, error: &Repr<'_>, sr: Row, sc: Col) -> Report { + let report = match *error { + Repr::Space(ref e, r, c) => return to_space_report(source, e, r, c), + Repr::Arrow(r, c) => problem( + "REPRESENTATION ARROW", + r, + c, + "I found an arrow inside a representation bound:", + "Representation bounds do not have arrows. Use a bound such as Storable; the compiler infers constructor kinds from type use.", + ), + Repr::Start(r, c) => problem( + "MISSING REPRESENTATION", + r, + c, + "I was expecting a representation bound here:", + "Write a representation name such as Storable after the colon.", + ), + Repr::Name(name, r, c) => problem( + "UNKNOWN REPRESENTATION", + r, + c, + &format!("I do not recognize `{name}` as a representation bound:"), + "Use a supported representation bound: Big, Const, Term, or Storable.", + ), + }; + wide(report, sr, sc) +} diff --git a/crates/nash-report/src/syntax/variants.rs b/crates/nash-report/src/syntax/variants.rs new file mode 100644 index 00000000..8ee42ed9 --- /dev/null +++ b/crates/nash-report/src/syntax/variants.rs @@ -0,0 +1,1716 @@ +//! One fixture for every module, declaration, pattern and type error variant. +use super::*; +use crate::render_plain; +use nash_parse::error::*; + +#[test] +fn variant_module_space() { + let source = Source::new("f = value"); + let error = Module::Space(Space::HasTab, 1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_bad_end() { + let source = Source::new("f = value"); + let error = Module::BadEnd(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_problem() { + let source = Source::new("f = value"); + let error = Module::Problem(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_validator() { + let source = Source::new("f = value"); + let error = Module::Validator(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_name() { + let source = Source::new("f = value"); + let error = Module::Name(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_exposing() { + let source = Source::new("f = value"); + let error = Module::Exposing(&Exposing::Start(1, 3), 1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_fresh_line() { + let source = Source::new("f = value"); + let error = Module::FreshLine(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_start() { + let source = Source::new("f = value"); + let error = Module::ImportStart(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_name() { + let source = Source::new("f = value"); + let error = Module::ImportName(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_as() { + let source = Source::new("f = value"); + let error = Module::ImportAs(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_alias() { + let source = Source::new("f = value"); + let error = Module::ImportAlias(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_exposing() { + let source = Source::new("f = value"); + let error = Module::ImportExposing(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_exposing_list() { + let source = Source::new("f = value"); + let error = Module::ImportExposingList(&Exposing::Start(1, 3), 1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_end() { + let source = Source::new("f = value"); + let error = Module::ImportEnd(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_indent_name() { + let source = Source::new("f = value"); + let error = Module::ImportIndentName(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_indent_alias() { + let source = Source::new("f = value"); + let error = Module::ImportIndentAlias(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_import_indent_exposing_list() { + let source = Source::new("f = value"); + let error = Module::ImportIndentExposingList(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_infix() { + let source = Source::new("f = value"); + let error = Module::Infix(1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_declarations() { + let source = Source::new("f = value"); + let error = Module::Declarations(&Decl::Start(1, 3), 1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_module_tests() { + let source = Source::new("f = value"); + let error = Module::Tests(&Tests::Start(1, 3), 1, 3); + let report = module::to_parse_error_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_space() { + let source = Source::new("f = value"); + let error = Exposing::Space(Space::HasTab, 1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_start() { + let source = Source::new("f = value"); + let error = Exposing::Start(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_value() { + let source = Source::new("f = value"); + let error = Exposing::Value(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_operator() { + let source = Source::new("f = value"); + let error = Exposing::Operator(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_operator_reserved() { + let source = Source::new("f = value"); + let error = Exposing::OperatorReserved(BadOperator::Equals, 1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_operator_right_paren() { + let source = Source::new("f = value"); + let error = Exposing::OperatorRightParen(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_type_privacy() { + let source = Source::new("f = value"); + let error = Exposing::TypePrivacy(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_type_name() { + let source = Source::new("f = value"); + let error = Exposing::TypeName(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_end() { + let source = Source::new("f = value"); + let error = Exposing::End(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_indent_end() { + let source = Source::new("f = value"); + let error = Exposing::IndentEnd(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_exposing_indent_value() { + let source = Source::new("f = value"); + let error = Exposing::IndentValue(1, 3); + let report = module::to_exposing_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_start() { + let source = Source::new("f = value"); + let error = Decl::Start(1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_space() { + let source = Source::new("f = value"); + let error = Decl::Space(Space::HasTab, 1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_type() { + let source = Source::new("f = value"); + let error = Decl::Type(&DeclType::Name(1, 3), 1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def() { + let source = Source::new("f = value"); + let error = Decl::Def("f", &DeclDef::IndentBody(1, 3), 1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_fresh_line_after_doc_comment() { + let source = Source::new("f = value"); + let error = Decl::FreshLineAfterDocComment(1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_attribute() { + let source = Source::new("f = value"); + let error = Decl::Attribute(&Attribute::Name(1, 3), 1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_trait() { + let source = Source::new("f = value"); + let error = Decl::Trait(&Trait::Name(1, 3), 1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_impl() { + let source = Source::new("f = value"); + let error = Decl::Impl(&Impl::BadHead(1, 3), 1, 3); + let report = decl::to_declarations_report(&source, &error); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_space() { + let source = Source::new("f = value"); + let error = DeclDef::Space(Space::HasTab, 1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_equals() { + let source = Source::new("f = value"); + let error = DeclDef::Equals(1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_type() { + let source = Source::new("f = value"); + let error = DeclDef::Type(&Type::Start(1, 3), 1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_arg() { + let source = Source::new("f = value"); + let error = DeclDef::Arg(&Pattern::Start(1, 3), 1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_body() { + let source = Source::new("f = value"); + let error = DeclDef::Body(&Expr::Start(1, 3), 1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_name_repeat() { + let source = Source::new("f = value"); + let error = DeclDef::NameRepeat(1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_name_match() { + let source = Source::new("f = value"); + let error = DeclDef::NameMatch("g", 1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_indent_type() { + let source = Source::new("f = value"); + let error = DeclDef::IndentType(1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_indent_equals() { + let source = Source::new("f = value"); + let error = DeclDef::IndentEquals(1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_def_indent_body() { + let source = Source::new("f = value"); + let error = DeclDef::IndentBody(1, 3); + let report = decl::to_decl_def_report(&source, "f", &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_type_space() { + let source = Source::new("f = value"); + let error = DeclType::Space(Space::HasTab, 1, 3); + let report = decl::to_decl_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_type_name() { + let source = Source::new("f = value"); + let error = DeclType::Name(1, 3); + let report = decl::to_decl_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_type_alias() { + let source = Source::new("f = value"); + let error = DeclType::Alias(&TypeAlias::Name(1, 3), 1, 3); + let report = decl::to_decl_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_type_union() { + let source = Source::new("f = value"); + let error = DeclType::Union(&CustomType::Name(1, 3), 1, 3); + let report = decl::to_decl_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_decl_type_indent_name() { + let source = Source::new("f = value"); + let error = DeclType::IndentName(1, 3); + let report = decl::to_decl_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_space() { + let source = Source::new("f = value"); + let error = TypeAlias::Space(Space::HasTab, 1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_name() { + let source = Source::new("f = value"); + let error = TypeAlias::Name(1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_param() { + let source = Source::new("f = value"); + let error = TypeAlias::Param(&TypeParam::Start(1, 3), 1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_equals() { + let source = Source::new("f = value"); + let error = TypeAlias::Equals(1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_body() { + let source = Source::new("f = value"); + let error = TypeAlias::Body(&Type::Start(1, 3), 1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_indent_equals() { + let source = Source::new("f = value"); + let error = TypeAlias::IndentEquals(1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_alias_indent_body() { + let source = Source::new("f = value"); + let error = TypeAlias::IndentBody(1, 3); + let report = decl::to_type_alias_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_space() { + let source = Source::new("f = value"); + let error = CustomType::Space(Space::HasTab, 1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_name() { + let source = Source::new("f = value"); + let error = CustomType::Name(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_param() { + let source = Source::new("f = value"); + let error = CustomType::Param(&TypeParam::Start(1, 3), 1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_equals() { + let source = Source::new("f = value"); + let error = CustomType::Equals(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_bar() { + let source = Source::new("f = value"); + let error = CustomType::Bar(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_variant() { + let source = Source::new("f = value"); + let error = CustomType::Variant(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_variant_arg() { + let source = Source::new("f = value"); + let error = CustomType::VariantArg(&Type::Start(1, 3), 1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_indent_equals() { + let source = Source::new("f = value"); + let error = CustomType::IndentEquals(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_indent_bar() { + let source = Source::new("f = value"); + let error = CustomType::IndentBar(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_indent_after_bar() { + let source = Source::new("f = value"); + let error = CustomType::IndentAfterBar(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_indent_after_equals() { + let source = Source::new("f = value"); + let error = CustomType::IndentAfterEquals(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_field() { + let source = Source::new("f = value"); + let error = CustomType::Field(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_field_colon() { + let source = Source::new("f = value"); + let error = CustomType::FieldColon(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_field_type() { + let source = Source::new("f = value"); + let error = CustomType::FieldType(&Type::Start(1, 3), 1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_field_end() { + let source = Source::new("f = value"); + let error = CustomType::FieldEnd(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_indent_field() { + let source = Source::new("f = value"); + let error = CustomType::IndentField(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_custom_type_indent_field_type() { + let source = Source::new("f = value"); + let error = CustomType::IndentFieldType(1, 3); + let report = decl::to_custom_type_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_name() { + let source = Source::new("f = value"); + let error = Attribute::Name(1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_arg() { + let source = Source::new("f = value"); + let error = Attribute::Arg(&Expr::Start(1, 3), 1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_end() { + let source = Source::new("f = value"); + let error = Attribute::End(1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_space() { + let source = Source::new("f = value"); + let error = Attribute::Space(Space::HasTab, 1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_fresh_line() { + let source = Source::new("f = value"); + let error = Attribute::FreshLine(1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_indent_arg() { + let source = Source::new("f = value"); + let error = Attribute::IndentArg(1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_attribute_indent_end() { + let source = Source::new("f = value"); + let error = Attribute::IndentEnd(1, 3); + let report = decl::to_attribute_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_space() { + let source = Source::new("f = value"); + let error = Trait::Space(Space::HasTab, 1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_name() { + let source = Source::new("f = value"); + let error = Trait::Name(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_param() { + let source = Source::new("f = value"); + let error = Trait::Param(&TypeParam::Start(1, 3), 1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_super() { + let source = Source::new("f = value"); + let error = Trait::Super(&Type::Start(1, 3), 1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_super_arg() { + let source = Source::new("f = value"); + let error = Trait::SuperArg(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_where() { + let source = Source::new("f = value"); + let error = Trait::Where(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_method_name() { + let source = Source::new("f = value"); + let error = Trait::MethodName(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_colon() { + let source = Source::new("f = value"); + let error = Trait::Colon(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_type() { + let source = Source::new("f = value"); + let error = Trait::Type(&Type::Start(1, 3), 1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_default() { + let source = Source::new("f = value"); + let error = Trait::Default("f", &Def::IndentBody(1, 3), 1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_indent_name() { + let source = Source::new("f = value"); + let error = Trait::IndentName(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_indent_param() { + let source = Source::new("f = value"); + let error = Trait::IndentParam(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_indent_where() { + let source = Source::new("f = value"); + let error = Trait::IndentWhere(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_indent_method() { + let source = Source::new("f = value"); + let error = Trait::IndentMethod(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_indent_colon() { + let source = Source::new("f = value"); + let error = Trait::IndentColon(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_indent_type() { + let source = Source::new("f = value"); + let error = Trait::IndentType(1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_trait_alignment() { + let source = Source::new("f = value"); + let error = Trait::Alignment(3, 1, 3); + let report = decl::to_trait_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_space() { + let source = Source::new("f = value"); + let error = Impl::Space(Space::HasTab, 1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_head() { + let source = Source::new("f = value"); + let error = Impl::Head(&Type::Start(1, 3), 1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_bad_head() { + let source = Source::new("f = value"); + let error = Impl::BadHead(1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_where() { + let source = Source::new("f = value"); + let error = Impl::Where(1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_method() { + let source = Source::new("f = value"); + let error = Impl::Method("f", &Def::IndentBody(1, 3), 1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_method_name() { + let source = Source::new("f = value"); + let error = Impl::MethodName(1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_indent_head() { + let source = Source::new("f = value"); + let error = Impl::IndentHead(1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_indent_where() { + let source = Source::new("f = value"); + let error = Impl::IndentWhere(1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_indent_method() { + let source = Source::new("f = value"); + let error = Impl::IndentMethod(1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_impl_alignment() { + let source = Source::new("f = value"); + let error = Impl::Alignment(3, 1, 3); + let report = decl::to_impl_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_tests_space() { + let source = Source::new("f = value"); + let error = Tests::Space(Space::HasTab, 1, 3); + let report = module::to_tests_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_tests_import() { + let source = Source::new("f = value"); + let error = Tests::Import(&Module::Problem(1, 3), 1, 3); + let report = module::to_tests_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_tests_test() { + let source = Source::new("f = value"); + let error = Tests::Test(&Test::NameStart(1, 3), 1, 3); + let report = module::to_tests_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_tests_start() { + let source = Source::new("f = value"); + let error = Tests::Start(1, 3); + let report = module::to_tests_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_tests_indent_start() { + let source = Source::new("f = value"); + let error = Tests::IndentStart(1, 3); + let report = module::to_tests_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_tests_alignment() { + let source = Source::new("f = value"); + let error = Tests::Alignment(3, 1, 3); + let report = module::to_tests_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_space() { + let source = Source::new("f = value"); + let error = Test::Space(Space::HasTab, 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_name() { + let source = Source::new("f = value"); + let error = Test::Name(StringError::EndlessSingle, 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_name_start() { + let source = Source::new("f = value"); + let error = Test::NameStart(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_once_on_unit_test() { + let source = Source::new("f = value"); + let error = Test::OnceOnUnitTest(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_within_open() { + let source = Source::new("f = value"); + let error = Test::WithinOpen(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_within_kind() { + let source = Source::new("f = value"); + let error = Test::WithinKind(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_within_number() { + let source = Source::new("f = value"); + let error = Test::WithinNumber(Number::End, 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_within_duplicate() { + let source = Source::new("f = value"); + let error = Test::WithinDuplicate(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_within_end() { + let source = Source::new("f = value"); + let error = Test::WithinEnd(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_equals() { + let source = Source::new("f = value"); + let error = Test::Equals(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_do() { + let source = Source::new("f = value"); + let error = Test::Do(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_body() { + let source = Source::new("f = value"); + let error = Test::Body(&Do::LastNotExpr(1, 3), 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_let() { + let source = Source::new("f = value"); + let error = Test::Let(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_pattern() { + let source = Source::new("f = value"); + let error = Test::Pattern(&Pattern::Start(1, 3), 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_via() { + let source = Source::new("f = value"); + let error = Test::Via(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_fuzzer() { + let source = Source::new("f = value"); + let error = Test::Fuzzer(&Expr::Start(1, 3), 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_in() { + let source = Source::new("f = value"); + let error = Test::In(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_indent_name() { + let source = Source::new("f = value"); + let error = Test::IndentName(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_indent_equals() { + let source = Source::new("f = value"); + let error = Test::IndentEquals(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_indent_body() { + let source = Source::new("f = value"); + let error = Test::IndentBody(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_indent_binder() { + let source = Source::new("f = value"); + let error = Test::IndentBinder(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_indent_in() { + let source = Source::new("f = value"); + let error = Test::IndentIn(1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_test_binder_alignment() { + let source = Source::new("f = value"); + let error = Test::BinderAlignment(3, 1, 3); + let report = module::to_test_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_record() { + let source = Source::new("f = value"); + let error = Pattern::Record(&PRecord::Open(1, 3), 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_tuple() { + let source = Source::new("f = value"); + let error = Pattern::Tuple(&PTuple::Open(1, 3), 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_list() { + let source = Source::new("f = value"); + let error = Pattern::List(&PList::Open(1, 3), 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_start() { + let source = Source::new("f = value"); + let error = Pattern::Start(1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_string() { + let source = Source::new("f = value"); + let error = Pattern::String(StringError::EndlessSingle, 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_bytes() { + let source = Source::new("f = value"); + let error = Pattern::Bytes(Bytes::Endless, 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_number() { + let source = Source::new("f = value"); + let error = Pattern::Number(Number::End, 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_alias() { + let source = Source::new("f = value"); + let error = Pattern::Alias(1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_wildcard_not_var() { + let source = Source::new("f = value"); + let error = Pattern::WildcardNotVar("_foo", 4, 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_space() { + let source = Source::new("f = value"); + let error = Pattern::Space(Space::HasTab, 1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_indent_start() { + let source = Source::new("f = value"); + let error = Pattern::IndentStart(1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_pattern_indent_alias() { + let source = Source::new("f = value"); + let error = Pattern::IndentAlias(1, 3); + let report = pattern::to_pattern_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_open() { + let source = Source::new("f = value"); + let error = PRecord::Open(1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_end() { + let source = Source::new("f = value"); + let error = PRecord::End(1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_field() { + let source = Source::new("f = value"); + let error = PRecord::Field(1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_space() { + let source = Source::new("f = value"); + let error = PRecord::Space(Space::HasTab, 1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_indent_open() { + let source = Source::new("f = value"); + let error = PRecord::IndentOpen(1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_indent_end() { + let source = Source::new("f = value"); + let error = PRecord::IndentEnd(1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_record_indent_field() { + let source = Source::new("f = value"); + let error = PRecord::IndentField(1, 3); + let report = pattern::to_p_record_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_open() { + let source = Source::new("f = value"); + let error = PTuple::Open(1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_end() { + let source = Source::new("f = value"); + let error = PTuple::End(1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_expr() { + let source = Source::new("f = value"); + let error = PTuple::Expr(&Pattern::Start(1, 3), 1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_space() { + let source = Source::new("f = value"); + let error = PTuple::Space(Space::HasTab, 1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_indent_end() { + let source = Source::new("f = value"); + let error = PTuple::IndentEnd(1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_indent_expr1() { + let source = Source::new("f = value"); + let error = PTuple::IndentExpr1(1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_tuple_indent_expr_n() { + let source = Source::new("f = value"); + let error = PTuple::IndentExprN(1, 3); + let report = pattern::to_p_tuple_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_open() { + let source = Source::new("f = value"); + let error = PList::Open(1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_end() { + let source = Source::new("f = value"); + let error = PList::End(1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_expr() { + let source = Source::new("f = value"); + let error = PList::Expr(&Pattern::Start(1, 3), 1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_space() { + let source = Source::new("f = value"); + let error = PList::Space(Space::HasTab, 1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_indent_open() { + let source = Source::new("f = value"); + let error = PList::IndentOpen(1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_indent_end() { + let source = Source::new("f = value"); + let error = PList::IndentEnd(1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_p_list_indent_expr() { + let source = Source::new("f = value"); + let error = PList::IndentExpr(1, 3); + let report = pattern::to_p_list_report(&source, pattern::PContext::Arg, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_record() { + let source = Source::new("f = value"); + let error = Type::Record(&TRecord::Open(1, 3), 1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_tuple() { + let source = Source::new("f = value"); + let error = Type::Tuple(&TTuple::Open(1, 3), 1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_start() { + let source = Source::new("f = value"); + let error = Type::Start(1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_var_start() { + let source = Source::new("f = value"); + let error = Type::VarStart(1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_context() { + let source = Source::new("f = value"); + let error = Type::Context(1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_indent_after_context() { + let source = Source::new("f = value"); + let error = Type::IndentAfterContext(1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_space() { + let source = Source::new("f = value"); + let error = Type::Space(Space::HasTab, 1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_indent_start() { + let source = Source::new("f = value"); + let error = Type::IndentStart(1, 3); + let report = type_::to_type_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_open() { + let source = Source::new("f = value"); + let error = TRecord::Open(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_end() { + let source = Source::new("f = value"); + let error = TRecord::End(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_field() { + let source = Source::new("f = value"); + let error = TRecord::Field(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_colon() { + let source = Source::new("f = value"); + let error = TRecord::Colon(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_type() { + let source = Source::new("f = value"); + let error = TRecord::Type(&Type::Start(1, 3), 1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_space() { + let source = Source::new("f = value"); + let error = TRecord::Space(Space::HasTab, 1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_indent_open() { + let source = Source::new("f = value"); + let error = TRecord::IndentOpen(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_indent_field() { + let source = Source::new("f = value"); + let error = TRecord::IndentField(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_indent_colon() { + let source = Source::new("f = value"); + let error = TRecord::IndentColon(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_indent_type() { + let source = Source::new("f = value"); + let error = TRecord::IndentType(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_record_indent_end() { + let source = Source::new("f = value"); + let error = TRecord::IndentEnd(1, 3); + let report = type_::to_t_record_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_repr() { + let source = Source::new("f = value"); + let error = TTuple::Repr(&Repr::Start(1, 3), 1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_indent_repr() { + let source = Source::new("f = value"); + let error = TTuple::IndentRepr(1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_open() { + let source = Source::new("f = value"); + let error = TTuple::Open(1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_end() { + let source = Source::new("f = value"); + let error = TTuple::End(1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_type() { + let source = Source::new("f = value"); + let error = TTuple::Type(&Type::Start(1, 3), 1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_space() { + let source = Source::new("f = value"); + let error = TTuple::Space(Space::HasTab, 1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_indent_type1() { + let source = Source::new("f = value"); + let error = TTuple::IndentType1(1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_indent_type_n() { + let source = Source::new("f = value"); + let error = TTuple::IndentTypeN(1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_t_tuple_indent_end() { + let source = Source::new("f = value"); + let error = TTuple::IndentEnd(1, 3); + let report = type_::to_t_tuple_report(&source, type_::TContext::Annotation("f"), &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_start() { + let source = Source::new("f = value"); + let error = TypeParam::Start(1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_colon() { + let source = Source::new("f = value"); + let error = TypeParam::Colon(1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_repr() { + let source = Source::new("f = value"); + let error = TypeParam::Repr(&Repr::Start(1, 3), 1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_end() { + let source = Source::new("f = value"); + let error = TypeParam::End(1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_space() { + let source = Source::new("f = value"); + let error = TypeParam::Space(Space::HasTab, 1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_indent_colon() { + let source = Source::new("f = value"); + let error = TypeParam::IndentColon(1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_indent_repr() { + let source = Source::new("f = value"); + let error = TypeParam::IndentRepr(1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_type_param_indent_end() { + let source = Source::new("f = value"); + let error = TypeParam::IndentEnd(1, 3); + let report = type_::to_type_param_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_repr_arrow() { + let source = Source::new("f = value"); + let error = Repr::Arrow(1, 3); + let report = type_::to_repr_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_repr_start() { + let source = Source::new("f = value"); + let error = Repr::Start(1, 3); + let report = type_::to_repr_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_repr_name() { + let source = Source::new("f = value"); + let error = Repr::Name("Unknown", 1, 3); + let report = type_::to_repr_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} + +#[test] +fn variant_repr_space() { + let source = Source::new("f = value"); + let error = Repr::Space(Space::HasTab, 1, 3); + let report = type_::to_repr_report(&source, &error, 1, 1); + insta::assert_snapshot!(render_plain(&report, &source, "src/Main.nash")); +} From 3d40e553baf08e7b839c6e21f9fdf84c1ba249af Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 07/12] feat(report): explain canonicalization errors Signed-off-by: microproofs --- crates/nash-report/src/canonicalize.rs | 2732 +++++++++++++++++ crates/nash-report/src/lib.rs | 1 + ...nicalize__branches__bad_head_function.snap | 12 + ...nonicalize__branches__bad_head_record.snap | 12 + ...anches__bad_head_variable_application.snap | 12 + ...icalize__branches__duplicate_destruct.snap | 14 + ...icalize__branches__duplicate_function.snap | 14 + ...onicalize__branches__duplicate_lambda.snap | 14 + ...canonicalize__branches__duplicate_let.snap | 14 + ...branches__export_multiple_suggestions.snap | 12 + ...lize__branches__export_no_suggestions.snap | 8 + ...icalize__branches__formation_contexts.snap | 13 + ...e__branches__module_name_is_preserved.snap | 16 + ...__branches__operator_javascript_equal.snap | 12 + ...anches__operator_javascript_not_equal.snap | 14 + ..._operator_javascript_strict_not_equal.snap | 14 + ...onicalize__branches__operator_missing.snap | 12 + ...onicalize__branches__operator_percent.snap | 14 + ...anonicalize__branches__operator_power.snap | 12 + ...calize__branches__qualified_ambiguity.snap | 18 + ...e__branches__qualified_missing_import.snap | 15 + ...lize__branches__qualified_not_exposed.snap | 17 + ...lize__branches__recursive_alias_cycle.snap | 25 + ...alize__branches__recursive_decl_cycle.snap | 22 + ...icalize__branches__recursive_let_self.snap | 23 + ...es__source_pipeline_overlapping_impls.snap | 20 + ...ce_pipeline_reports_all_missing_names.snap | 36 + ...onicalize__branches__superclass_cycle.snap | 14 + ...onicalize__branches__superclass_limit.snap | 14 + ..._canonicalize__branches__too_few_args.snap | 12 + ...alize__branches__too_many_args_plural.snap | 12 + ...ize__branches__unbound_alias_variable.snap | 18 + ...ze__branches__unbound_union_variables.snap | 18 + ...lize__branches__unused_alias_variable.snap | 18 + ...ize__branches__unused_alias_variables.snap | 18 + ...ze__coverage__variant_ambiguous_binop.snap | 23 + ...ize__coverage__variant_ambiguous_ctor.snap | 23 + ...ze__coverage__variant_ambiguous_trait.snap | 23 + ...ize__coverage__variant_ambiguous_type.snap | 23 + ...lize__coverage__variant_ambiguous_var.snap | 23 + ...overage__variant_annotation_too_short.snap | 15 + ...nicalize__coverage__variant_bad_arity.snap | 13 + ...__coverage__variant_bad_instance_head.snap | 13 + ...ize__coverage__variant_binop_conflict.snap | 13 + ...age__variant_binop_function_not_found.snap | 14 + ...rage__variant_context_var_not_in_type.snap | 14 + ..._variant_contradictory_representation.snap | 14 + ...e__coverage__variant_do_without_monad.snap | 14 + ...coverage__variant_duplicate_alias_arg.snap | 16 + ...ze__coverage__variant_duplicate_binop.snap | 16 + ...ize__coverage__variant_duplicate_ctor.snap | 16 + ...ize__coverage__variant_duplicate_decl.snap | 16 + ...ze__coverage__variant_duplicate_field.snap | 16 + ...e__coverage__variant_duplicate_method.snap | 16 + ...__coverage__variant_duplicate_pattern.snap | 16 + ...ze__coverage__variant_duplicate_trait.snap | 16 + ...ge__variant_duplicate_trait_parameter.snap | 16 + ...ize__coverage__variant_duplicate_type.snap | 16 + ...coverage__variant_duplicate_union_arg.snap | 16 + ...e__coverage__variant_export_duplicate.snap | 16 + ...e__coverage__variant_export_not_found.snap | 8 + ...__coverage__variant_export_open_alias.snap | 14 + ...__coverage__variant_export_open_trait.snap | 14 + ..._variant_impl_context_var_not_in_head.snap | 15 + ...verage__variant_impl_of_builtin_trait.snap | 14 + ..._coverage__variant_impl_pattern_limit.snap | 14 + ...coverage__variant_import_ctor_by_name.snap | 14 + ...ge__variant_import_exposing_not_found.snap | 13 + ...e__coverage__variant_import_not_found.snap | 12 + ...__coverage__variant_import_open_alias.snap | 13 + ...__coverage__variant_import_open_trait.snap | 14 + ...coverage__variant_irregular_recursion.snap | 14 + ...lize__coverage__variant_kind_infinite.snap | 14 + ...lize__coverage__variant_kind_mismatch.snap | 14 + ...age__variant_labeled_ctor_extra_field.snap | 14 + ...e__variant_labeled_ctor_missing_field.snap | 13 + ...e__variant_labeled_ctor_unknown_field.snap | 13 + ...age__variant_method_missing_parameter.snap | 14 + ...ize__coverage__variant_missing_method.snap | 14 + ...verage__variant_missing_module_header.snap | 16 + ..._coverage__variant_missing_superclass.snap | 15 + ..._coverage__variant_negate_without_num.snap | 14 + ...ze__coverage__variant_not_found_binop.snap | 14 + ...ize__coverage__variant_not_found_ctor.snap | 18 + ...ze__coverage__variant_not_found_trait.snap | 16 + ...ize__coverage__variant_not_found_type.snap | 18 + ...lize__coverage__variant_not_found_var.snap | 18 + ...calize__coverage__variant_orphan_impl.snap | 14 + ...__coverage__variant_overlapping_impls.snap | 19 + ...rage__variant_pattern_has_record_ctor.snap | 14 + ...age__variant_record_literal_ambiguous.snap | 14 + ...rage__variant_record_literal_no_alias.snap | 13 + ...ge__variant_record_type_outside_alias.snap | 14 + ...ze__coverage__variant_recursive_alias.snap | 22 + ...ize__coverage__variant_recursive_decl.snap | 24 + ...lize__coverage__variant_recursive_let.snap | 23 + ...overage__variant_recursive_superclass.snap | 20 + ...erage__variant_reflexive_lift_overlap.snap | 14 + ...erage__variant_refutable_bind_pattern.snap | 14 + ...rage__variant_representation_mismatch.snap | 14 + ...nicalize__coverage__variant_shadowing.snap | 20 + ...erage__variant_structural_eq_override.snap | 14 + ..._coverage__variant_superclass_bad_arg.snap | 14 + ...calize__coverage__variant_trait_arity.snap | 13 + ..._variant_type_vars_messed_up_in_alias.snap | 18 + ...e__variant_type_vars_unbound_in_union.snap | 19 + ...ize__coverage__variant_unknown_method.snap | 14 + ...calize__coverage__variant_unsupported.snap | 13 + ...t__canonicalize__tests__kind_mismatch.snap | 13 + ...icalize__tests__missing_module_header.snap | 16 + ...t__canonicalize__tests__not_found_var.snap | 15 + ..._tests__not_found_var_with_suggestion.snap | 18 + 112 files changed, 4459 insertions(+) create mode 100644 crates/nash-report/src/canonicalize.rs create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_function.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_record.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_variable_application.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_destruct.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_function.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_lambda.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_let.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_multiple_suggestions.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_no_suggestions.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__formation_contexts.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__module_name_is_preserved.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_equal.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_not_equal.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_strict_not_equal.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_missing.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_percent.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_power.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_ambiguity.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_missing_import.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_not_exposed.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_alias_cycle.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_decl_cycle.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_let_self.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_overlapping_impls.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_reports_all_missing_names.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_cycle.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_limit.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_few_args.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_many_args_plural.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_alias_variable.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_union_variables.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variable.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variables.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_binop.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_ctor.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_trait.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_type.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_var.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_annotation_too_short.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_arity.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_instance_head.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_conflict.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_function_not_found.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_context_var_not_in_type.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_contradictory_representation.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_do_without_monad.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_alias_arg.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_binop.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_ctor.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_decl.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_field.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_method.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_pattern.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait_parameter.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_type.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_union_arg.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_duplicate.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_not_found.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_alias.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_trait.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_context_var_not_in_head.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_of_builtin_trait.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_pattern_limit.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_ctor_by_name.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_exposing_not_found.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_not_found.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_alias.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_trait.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_irregular_recursion.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_infinite.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_mismatch.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_extra_field.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_missing_field.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_unknown_field.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_method_missing_parameter.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_method.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_module_header.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_superclass.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_negate_without_num.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_binop.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_ctor.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_trait.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_type.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_var.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_orphan_impl.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_overlapping_impls.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_pattern_has_record_ctor.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_ambiguous.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_no_alias.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_type_outside_alias.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_alias.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_decl.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_let.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_superclass.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_reflexive_lift_overlap.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_refutable_bind_pattern.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_representation_mismatch.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_shadowing.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_structural_eq_override.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_superclass_bad_arg.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_trait_arity.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_messed_up_in_alias.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_unbound_in_union.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unknown_method.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unsupported.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__tests__kind_mismatch.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__tests__missing_module_header.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var_with_suggestion.snap diff --git a/crates/nash-report/src/canonicalize.rs b/crates/nash-report/src/canonicalize.rs new file mode 100644 index 00000000..eae2fefa --- /dev/null +++ b/crates/nash-report/src/canonicalize.rs @@ -0,0 +1,2732 @@ +//! 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 nash_ast::{Kind, ModuleName, QualifiedName}; +use nash_can::{ + BadArityContext, DuplicatePatternContext, Error, KindContext, PossibleNames, VarKind, +}; +use nash_region::Region; + +#[cfg(test)] +mod tests { + use super::*; + use nash_region::Position; + fn region() -> Region { + Region::new(Position::new(1, 1), Position::new(1, 5)) + } + fn snapshot(name: &str, error: Error<'_>) { + let source = Source::new("name = other\n"); + insta::assert_snapshot!( + name, + crate::render_plain(&to_report(&source, &error), &source, "Main.nash") + ); + } + #[test] + fn not_found_var() { + snapshot( + "not_found_var", + Error::NotFoundVar { + region: region(), + prefix: None, + name: "name", + suggestions: PossibleNames { + locals: &[], + qualified: &[], + }, + }, + ); + } + #[test] + fn not_found_var_with_suggestion() { + snapshot( + "not_found_var_with_suggestion", + Error::NotFoundVar { + region: region(), + prefix: None, + name: "naem", + suggestions: PossibleNames { + locals: &["name", "other"], + qualified: &[], + }, + }, + ); + } + #[test] + fn missing_module_header() { + snapshot("missing_module_header", Error::MissingModuleHeader); + } + #[test] + fn kind_mismatch() { + snapshot( + "kind_mismatch", + Error::KindMismatch { + region: region(), + context: &KindContext::TypeAnnotation, + expected: &Kind::Type, + actual: &Kind::Arrow(&Kind::Type, &Kind::Type), + }, + ); + } +} + +pub fn to_report(source: &Source<'_>, error: &Error<'_>) -> Report { + to_report_with_name(source, error, "Main") +} + +pub fn to_report_with_name(source: &Source<'_>, error: &Error<'_>, expected_name: &str) -> Report { + match error { + Error::MissingModuleHeader => crate::syntax::to_report( + source, + &nash_parse::error::Error::ModuleNameUnspecified(expected_name), + ), + Error::NotFoundVar { + region, + prefix, + name, + suggestions, + } => not_found(*region, *prefix, name, "variable", *suggestions), + Error::NotFoundType { + region, + prefix, + name, + suggestions, + } => not_found(*region, *prefix, name, "type", *suggestions), + Error::NotFoundCtor { + region, + prefix, + name, + suggestions, + } => not_found(*region, *prefix, name, "variant", *suggestions), + Error::NotFoundTrait { + region, + prefix, + name, + } => not_found( + *region, + *prefix, + name, + "trait", + PossibleNames { + locals: &[], + qualified: &[], + }, + ), + Error::AmbiguousVar { + region, + prefix, + name, + first_module, + other_modules, + } => ambiguous_name( + *region, + *prefix, + name, + *first_module, + other_modules, + "variable", + ), + Error::AmbiguousType { + region, + prefix, + name, + first_module, + other_modules, + } => ambiguous_name(*region, *prefix, name, *first_module, other_modules, "type"), + Error::AmbiguousCtor { + region, + prefix, + name, + first_module, + other_modules, + } => ambiguous_name( + *region, + *prefix, + name, + *first_module, + other_modules, + "variant", + ), + Error::AmbiguousTrait { + region, + prefix, + name, + first_module, + other_modules, + } => ambiguous_name( + *region, + *prefix, + name, + *first_module, + other_modules, + "trait", + ), + Error::AmbiguousBinop { + region, + name, + first_module, + other_modules, + } => ambiguous_name( + *region, + None, + name, + *first_module, + other_modules, + "operator", + ), + Error::BadArity { + region, + context, + name, + expected, + actual, + } => arity( + *region, + name, + match context { + BadArityContext::TypeArity => "type", + BadArityContext::PatternArity => "variant", + }, + *expected, + *actual, + ), + Error::TraitArity { + region, + name, + expected, + actual, + } => arity(*region, name, "trait", *expected, *actual), + Error::DuplicateDecl { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This file has multiple `{name}` declarations."), + ), + Error::DuplicateType { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This file defines multiple `{name}` types."), + ), + Error::DuplicateCtor { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This file defines multiple `{name}` type constructors."), + ), + Error::DuplicateBinop { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This file defines multiple ({name}) operators."), + ), + Error::DuplicateField { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This record has multiple `{name}` fields."), + ), + Error::DuplicateTrait { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This file defines multiple `{name}` traits."), + ), + Error::DuplicateMethod { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This trait has multiple `{name}` methods."), + ), + Error::DuplicateTraitParameter { + name, + first, + second, + } => name_clash( + *first, + *second, + &format!("This trait has multiple `{name}` type parameters."), + ), + Error::DuplicateAliasArg { + type_name, + arg_name, + first, + second, + } => name_clash( + *first, + *second, + &format!("The `{type_name}` type alias has multiple `{arg_name}` type variables."), + ), + Error::DuplicateUnionArg { + type_name, + arg_name, + first, + second, + } => name_clash( + *first, + *second, + &format!("The `{type_name}` type has multiple `{arg_name}` type variables."), + ), + Error::DuplicatePattern { + context, + name, + first, + second, + } => name_clash( + *first, + *second, + &match context { + DuplicatePatternContext::LambdaArgs => { + format!("This anonymous function has multiple `{name}` arguments.") + } + DuplicatePatternContext::FuncArgs(function) => { + format!("The `{function}` function has multiple `{name}` arguments.") + } + DuplicatePatternContext::CaseBranch => { + format!("This `case` pattern has multiple `{name}` variables.") + } + DuplicatePatternContext::LetBinding => { + format!("This `let` expression defines `{name}` more than once!") + } + DuplicatePatternContext::Destruct => { + format!("This pattern contains multiple `{name}` variables.") + } + }, + ), + Error::ExportDuplicate { + name, + first, + 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!"), + ), + Error::ExportNotFound { + region, + kind, + name, + suggestions, + } => { + 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." + ), + "", + ); + report.snippet = Snippet::None; + report.after = suggestion_details( + &nearby, + "I do not see any super similar names in this file. Is the definition missing?", + ); + 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!", + ), + 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.", + ), + Error::ImportCtorByName { + region, + name, + type_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}." + ), + ), + Error::ImportNotFound { region, module } => simple( + "UNKNOWN IMPORT", + *region, + &format!("I could not find a `{module}` module to import!"), + "", + ), + Error::ImportExposingNotFound { + region, + module, + name, + available, + } => { + // Rank against the missing value, correcting Elm's module-name ranking typo. + let nearby = nearby(name, available, 4); + let mut report = simple( + "BAD IMPORT", + *region, + &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.with_suggestions(nearby) + } + Error::BinopFunctionNotFound { + region, + op, + function, + } => simple( + "INFIX PROBLEM", + *region, + &format!( + "The ({op}) operator says it is implemented by `{function}`, but I cannot find a `{function}` definition in this file." + ), + "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!", + ), + Error::NotFoundBinop { + region, + name, + available, + } => not_found_binop(*region, name, available), + 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.", + ), + Error::Shadowing { + name, + original, + new, + } => 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.", + ), + ]), + ), + Error::RecursiveDecl { name, others } => { + recursive_value(name.region, name.value, others, false) + } + Error::RecursiveLet { name, others } => { + recursive_value(name.region, name.value, others, true) + } + Error::AnnotationTooShort { + region, + name, + index, + leftovers, + } => simple( + "BAD TYPE ANNOTATION", + *region, + &format!( + "The type annotation for `{name}` says it can accept {}, but the definition says it has {}:", + args(*index), + args(index + leftovers) + ), + &format!( + "Is the type annotation missing something? Should some argument{} be deleted? Maybe some parentheses are missing?", + if *leftovers == 1 { "" } else { "s" } + ), + ), + Error::RecursiveAlias { + region, + name, + args, + typ, + others, + } => alias_recursion_report(*region, name, args, typ, others), + Error::TypeVarsUnboundInUnion { + region, + name, + args, + unbound, + more_unbound, + } => unbound_type_vars(*region, "type", name, args, *unbound, more_unbound), + Error::TypeVarsMessedUpInAlias { + region, + name, + args, + unused, + unbound, + } => alias_vars(*region, name, args, unused, unbound), + Error::KindMismatch { + region, + context, + expected, + actual, + } => simple( + "KIND MISMATCH", + *region, + &format!("I found a kind mismatch in {}:", kind_context(context)), + &format!( + "This position needs kind `{}`, but the type has kind `{}`. Type arguments must have matching kinds.", + kind(expected), + kind(actual) + ), + ), + Error::KindInfinite { region, context } => simple( + "INFINITE KIND", + *region, + &format!( + "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.", + ), + Error::RepresentationMismatch { + region, + context, + required, + actual, + } => simple( + "REPRESENTATION MISMATCH", + *region, + &format!( + "This position requires `{}` in {}:", + required.name(), + kind_context(context) + ), + &format!( + "The type has `{}` representation, but `{}` admits {}. Change the type or the representation requirement.", + repr(*actual), + required.name(), + admitted(*required) + ), + ), + Error::ContradictoryRepresentation { region, variable } => simple( + "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.", + ), + Error::IrregularRecursion { + region, + constructor, + parameter, + } => simple( + "IRREGULAR RECURSION", + *region, + &format!( + "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.", + ), + Error::ImplOfBuiltinTrait { region, trait_ } => simple( + "BUILTIN TRAIT", + *region, + &format!( + "The `{}` trait is owned by the compiler:", + qualified(*trait_) + ), + "Its representation rules cannot be replaced by an impl. Remove this impl and use a type with an admitted representation.", + ), + Error::MissingMethod { + region, + trait_, + name, + } => simple( + "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." + ), + ), + Error::UnknownMethod { + region, + trait_, + name, + } => simple( + "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.", + ), + Error::BadInstanceHead { region, reason } => { + use nash_can::BadHead; + let reason = match reason { + BadHead::BareVariable => "a bare type variable", + BadHead::Function => "a function type", + BadHead::Record => "an anonymous record", + BadHead::VariableApplication => "an application of a type variable", + }; + simple( + "BAD IMPL HEAD", + *region, + &format!("This impl head is {reason}:"), + "Use a named type constructor or a tuple as the outermost type of an impl head.", + ) + } + Error::OrphanImpl { + region, + trait_, + heads, + } => simple( + "ORPHAN IMPL", + *region, + &format!( + "This module cannot define an impl of `{}` for {}:", + qualified(*trait_), + heads.iter().map(head_con).collect::>().join(", ") + ), + "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.", + ), + Error::OverlappingImpls { + key, + first, + second, + first_home, + second_home, + } => Report::pair( + "OVERLAPPING IMPL", + label(*first, &format!("first impl in `{}`", first_home.name)), + label( + *second, + &format!("overlapping impl in `{}`", second_home.name), + ), + Doc::reflow(&format!( + "These `{}` impls can both match the same trait arguments. The overlapping head is {}:", + qualified(key.trait_), + 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.", + ), + ), + Error::MissingSuperclass { + region, + trait_, + heads, + superclass, + index, + reason, + } => { + let reason = match reason { + nash_can::EntailmentFailure::Missing => { + "I cannot find an impl or context constraint that provides it." + } + nash_can::EntailmentFailure::Cycle => { + "Resolving it leads back to the same requirement, forming a cycle." + } + nash_can::EntailmentFailure::Limit => { + "Resolving it exceeded the search limit because the requirements keep expanding." + } + }; + simple( + "MISSING SUPERCLASS", + *region, + &format!( + "The `{}` impl for {} does not establish superclass {}:", + qualified(*trait_), + heads + .iter() + .map(|h| head(&h.value)) + .collect::>() + .join(" "), + usize::from(*index) + 1 + ), + &format!( + "It must also satisfy `{}`. {reason} Add the required impl or include the necessary constraint in this impl's context.", + predicate(superclass) + ), + ) + } + Error::ImplContextVarNotInHead { region, name } => simple( + "TRAIT PROBLEM", + *region, + &format!( + "The impl context mentions '{name}, but this variable does not occur in the impl head:" + ), + "Each variable in an impl context must occur in its head. Check for a misspelled type variable.", + ), + Error::ContextVarNotInType { region, name } => simple( + "TRAIT PROBLEM", + *region, + &format!( + "The context mentions '{name}, but this variable does not occur in the annotated type:" + ), + "Use the variable in the type, or remove the constraint that mentions it.", + ), + Error::MethodMissingParameter { + region, + method, + parameter, + } => simple( + "TRAIT PROBLEM", + *region, + &format!("The `{method}` method does not mention trait parameter '{parameter}:"), + "Every trait parameter must occur in the method's type so a call can determine which impl to use.", + ), + Error::SuperclassBadArg { region, trait_ } => simple( + "TRAIT PROBLEM", + *region, + &format!("The `{trait_}` superclass has an invalid argument:"), + "Superclass arguments must be parameters declared by this trait. Replace this argument with the intended trait parameter.", + ), + Error::RecursiveSuperclass { names } => { + let first = names.first(); + let region = first.map_or( + Region::new( + nash_region::Position::new(1, 1), + nash_region::Position::new(1, 1), + ), + |n| n.region, + ); + let mut report = simple( + "TRAIT PROBLEM", + region, + "These superclass declarations form a cycle:", + "", + ); + report.after = Doc::stack([ + Doc::cycle( + 4, + 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.", + ), + ]); + report + } + Error::ExportOpenTrait { region, name } | Error::ImportOpenTrait { region, name } => { + simple( + if matches!(error, Error::ExportOpenTrait { .. }) { + "BAD EXPORT" + } else { + "BAD IMPORT" + }, + *region, + &format!("The `{name}` trait cannot be followed by (..) like this:"), + "The (..) syntax exposes variants of a custom type. Remove the dots and name the trait methods explicitly when you need them.", + ) + } + Error::RecordTypeOutsideAlias { region } => simple( + "RECORD TYPE", + *region, + "This record type needs a name:", + "A record type is only allowed as the direct body of a type alias. Give this record a named alias.", + ), + Error::RecordLiteralNoAlias { region, fields } => simple( + "UNKNOWN RECORD", + *region, + &format!( + "I cannot find a visible record alias with exactly these fields: {}.", + fields.join(", ") + ), + "Declare or import an alias for this record.", + ), + Error::RecordLiteralAmbiguous { region, candidates } => simple( + "AMBIGUOUS RECORD", + *region, + "Several visible record aliases have exactly these fields:", + &format!( + "The candidates are {}. Use the intended alias constructor to make the record type clear.", + candidates + .iter() + .map(|q| qualified(*q)) + .collect::>() + .join(", ") + ), + ), + Error::LabeledCtorMissingField { + region, + ctor, + field, + } => simple( + "MISSING FIELD", + *region, + &format!("The `{ctor}` constructor needs a `{field}` field:"), + "Add the missing field to this constructor application.", + ), + Error::LabeledCtorExtraField { + region, + ctor, + field, + } => simple( + "UNKNOWN FIELD", + *region, + &format!("The `{ctor}` constructor has no `{field}` field:"), + "Remove this extra field or check its spelling against the constructor declaration.", + ), + Error::LabeledCtorUnknownField { + region, + ctor, + field, + } => simple( + "UNKNOWN FIELD", + *region, + &format!("The `{ctor}` constructor has no `{field}` field:"), + "Check the field name against the constructor declaration.", + ), + Error::ImplPatternLimit { region } => simple( + "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.", + ), + 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.", + ), + 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`.", + ), + 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.", + ), + Error::StructuralEqOverride { head } => simple( + "STRUCTURAL EQUALITY", + head.region, + "This impl would replace structural equality:", + "Equality for this type is supplied by the compiler. Remove the explicit `Eq` impl.", + ), + Error::ReflexiveLiftOverlap { heads } => simple( + "OVERLAPPING IMPL", + heads.first().map_or( + Region::new( + nash_region::Position::new(1, 1), + nash_region::Position::new(1, 1), + ), + |h| h.region, + ), + "This impl overlaps the reflexive `Lift` rule:", + "Every Big type can lift to itself. Remove this impl or choose heads that do not overlap that built-in rule.", + ), + Error::Unsupported { feature, region } => simple( + "NOT SUPPORTED", + *region, + &format!("I cannot canonicalize {feature} yet:"), + "This syntax is recognized, but its compiler implementation is not available yet.", + ), + } +} + +fn simple(title: &str, region: Region, before: &str, after: &str) -> Report { + Report::snippet(title, region, None, Doc::reflow(before), Doc::reflow(after)) +} +fn label(region: Region, text: &str) -> Label { + Label { + region, + text: text.into(), + } +} +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!"), + ) +} +fn qualified(name: QualifiedName<'_>) -> String { + to_qual_string(name.home.name, name.name) +} +fn to_qual_string(prefix: &str, name: &str) -> String { + format!("{prefix}.{name}") +} +fn nearby(name: &str, possible: &[&str], limit: usize) -> Vec { + suggest::sort( + name, + Clone::clone, + possible.iter().map(|s| s.to_string()).collect(), + ) + .into_iter() + .take(limit) + .collect() +} +fn suggestion_details(nearby: &[String], empty: &str) -> Doc { + match nearby { + [] => Doc::reflow(empty), + [one] => Doc::hsep([ + Doc::text("Maybe you want"), + Doc::text(one).dullyellow(), + Doc::text("instead?"), + ]), + _ => Doc::stack([ + Doc::text("These names seem close though:"), + Doc::indent( + 4, + Doc::vcat(nearby.iter().map(|n| Doc::text(n).dullyellow())), + ), + ]), + } +} +fn to_kind_info(kind: VarKind, name: &str) -> (&'static str, &'static str, String) { + match kind { + VarKind::BadOp => ("an", "operator", format!("({name})")), + VarKind::BadVar => ("a", "value", format!("`{name}`")), + VarKind::BadPattern => ("a", "pattern", format!("`{name}`")), + VarKind::BadType => ("a", "type", format!("`{name}`")), + } +} +fn not_found( + region: Region, + prefix: Option<&str>, + name: &str, + thing: &str, + possible: PossibleNames<'_>, +) -> Report { + let given = prefix.map_or_else(|| name.into(), |p| to_qual_string(p, name)); + let mut names: Vec = possible.locals.iter().map(|s| s.to_string()).collect(); + for (module, values) in possible.qualified { + names.extend(values.iter().map(|n| to_qual_string(module, n))); + } + let nearby: Vec<_> = suggest::sort(&given, Clone::clone, names) + .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 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), + ) + .with_suggestions(nearby) +} +fn ambiguous_name( + region: Region, + prefix: Option<&str>, + name: &str, + first: ModuleName<'_>, + others: &[ModuleName<'_>], + thing: &str, +) -> Report { + 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.", + ), + ]), + ), + } +} +fn args(n: usize) -> String { + format!("{n} argument{}", if n == 1 { "" } else { "s" }) +} +fn arity(region: Region, name: &str, thing: &str, expected: usize, actual: usize) -> Report { + simple( + if actual < expected { + "TOO FEW ARGS" + } else if thing == "type" { + "TOO MANY TYPE ARGS" + } else { + "TOO MANY ARGS" + }, + region, + &format!( + "The `{name}` {thing} needs {}, but I see {actual} instead:", + 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?" + } else { + "Which are the extra ones? Maybe some parentheses are missing?" + }, + ) +} +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)} + }; + simple("UNKNOWN OPERATOR", region, &before, &after).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" + } else { + "CYCLIC DEFINITION" + }, + region, + None, + Doc::reflow(&before), + Doc::stack(docs), + ) +} +fn alias_recursion_report( + region: Region, + name: &str, + args: &[&str], + 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:", + ), + 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:"), + 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) +} +fn alias_to_union_doc( + name: &str, + args: &[&str], + typ: &nash_region::Located>, +) -> Doc { + Doc::vcat([ + Doc::hsep( + [Doc::text("type"), Doc::text(name)] + .into_iter() + .chain(args.iter().map(|a| Doc::text(format!("'{a}")))) + .chain([Doc::text("=")]), + ) + .dullyellow(), + Doc::indent(4, Doc::text(name)).green(), + Doc::indent( + 8, + crate::render_type::src_to_doc(crate::render_type::Ctx::App, typ), + ) + .dullyellow(), + ]) +} +fn unbound_type_vars( + region: Region, + decl: &str, + name: &str, + args: &[&str], + first: (&str, Region), + others: &[(&str, Region)], +) -> Report { + let names: Vec<_> = std::iter::once(first.0) + .chain(others.iter().map(|(n, _)| *n)) + .collect(); + let before = if others.is_empty() { + format!( + "The `{name}` {decl} uses an unbound type variable `{}` in its definition:", + first.0 + ) + } else { + format!( + "Type variables {} are unbound in the `{name}` {decl} definition:", + names.join(" and ") + ) + }; + Report::snippet( + if others.is_empty() { + "UNBOUND TYPE VARIABLE" + } else { + "UNBOUND TYPE VARIABLES" + }, + region, + 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:"), + 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 + )), + ]), + ) +} +fn declaration(decl: &str, name: &str, args: &[&str], added: &[&str]) -> Doc { + Doc::indent( + 4, + Doc::hsep( + [Doc::text(decl), Doc::text(name)] + .into_iter() + .chain(args.iter().map(|a| Doc::text(format!("'{a}")))) + .chain(added.iter().map(|a| Doc::text(format!("'{a}")).green())) + .chain([Doc::text("= ...")]), + ), + ) +} +fn alias_vars( + region: Region, + name: &str, + args: &[&str], + unused: &[(&str, Region)], + unbound: &[(&str, Region)], +) -> Report { + if unused.is_empty() + && let Some((first, rest)) = unbound.split_first() + { + return unbound_type_vars(region, "type alias", name, args, *first, rest); + } + let kept: Vec<_> = args + .iter() + .copied() + .filter(|a| !unused.iter().any(|(u, _)| u == a)) + .collect(); + let unused_names = unused.iter().map(|(n, _)| *n).collect::>(); + if unbound.is_empty() { + Report::snippet( + if unused.len() == 1 { + "UNUSED TYPE VARIABLE" + } else { + "UNUSED TYPE VARIABLES" + }, + region, + if unused.len() == 1 { + Some(unused[0].1) + } else { + None + }, + Doc::reflow(&if unused.len() == 1 { + format!( + "Type alias `{name}` does not use the `{}` type variable.", + unused[0].0 + ) + } else { + format!( + "Type variables {} are unused in the `{name}` definition.", + unused_names.join(" and ") + ) + }), + Doc::stack([ + Doc::reflow(&format!( + "I recommend removing {} from the declaration, like this:", + 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 { + let unbound_names = unbound.iter().map(|(n, _)| *n).collect::>(); + Report::snippet( + "TYPE VARIABLE PROBLEMS", + region, + None, + Doc::reflow(&format!( + "Type alias `{name}` has some type variable problems." + )), + Doc::stack([ + Doc::reflow(&format!( + "{} {}", + if let [one] = unbound_names.as_slice() { + format!( + "Type variable `{one}` appears in the definition, but I do not see it declared." + ) + } else { + format!( + "Type variables {} are used in the definition, but I do not see them declared.", + unbound_names.join(" and ") + ) + }, + if let [one] = unused_names.as_slice() { + format!("Likewise, type variable `{one}` is declared, but not used.") + } else { + format!( + "Likewise, type variables {} are declared, but not used.", + unused_names.join(" and ") + ) + } + )), + Doc::reflow("My guess is that a definition like this will work better:"), + declaration("type alias", name, &kept, &unbound_names), + ]), + ) + } +} +fn kind(value: &Kind<'_>) -> String { + match value { + Kind::Type => "Type".into(), + Kind::Arrow(a, b) => format!( + "{} -> {}", + if matches!(a, Kind::Arrow(..)) { + format!("({})", kind(a)) + } else { + kind(a) + }, + kind(b) + ), + } +} +fn kind_context(value: &KindContext<'_>) -> String { + match value { + KindContext::TypeAnnotation => "the type annotation".into(), + KindContext::Annotation { name } => format!("the annotation for `{name}`"), + KindContext::BigField { union, ctor, index } => format!( + "field {} of Big constructor `{ctor}` in `{union}`", + usize::from(*index) + 1 + ), + KindContext::LittleField { union, ctor, index } => format!( + "field {} of little constructor `{ctor}` in `{union}`", + usize::from(*index) + 1 + ), + KindContext::RecordField { alias, field, big } => format!( + "field `{field}` of {} record alias `{alias}`", + if *big { "Big" } else { "little" } + ), + KindContext::AliasCasing { alias, big } => format!( + "the {}case name of alias `{alias}`", + if *big { "upper" } else { "lower" } + ), + KindContext::ImplHead { trait_, index } => format!( + "head {} of impl `{}`", + usize::from(*index) + 1, + qualified(*trait_) + ), + } +} +fn repr(value: nash_ast::primitives::Repr) -> &'static str { + use nash_ast::primitives::Repr; + match value { + Repr::Big => "Big", + Repr::Const => "Const", + Repr::Term => "Term", + } +} +fn admitted(value: nash_ast::primitives::ReprTrait) -> &'static str { + use nash_ast::primitives::ReprTrait; + match value { + ReprTrait::Big => "only Big types", + ReprTrait::Const => "only Const types", + ReprTrait::Term => "only Term types", + ReprTrait::Storable => "Big or Const types", + ReprTrait::Little => "Const or Term types", + } +} +fn head_con(value: &nash_ast::HeadCon<'_>) -> String { + match value { + nash_ast::HeadCon::Named(n) => qualified(*n), + nash_ast::HeadCon::Tuple(n) => format!("a {n}-item tuple"), + nash_ast::HeadCon::Fun => "a function".into(), + } +} +fn head(value: &nash_ast::Head<'_>) -> String { + use nash_ast::Head; + match value { + Head::Var(n) => format!("'a{n}"), + Head::Named { reference, args } => { + if args.is_empty() { + qualified(*reference) + } else { + format!( + "({} {})", + qualified(*reference), + args.iter().map(head).collect::>().join(" ") + ) + } + } + Head::Tuple(items) => format!( + "({})", + items.iter().map(head).collect::>().join(", ") + ), + Head::Function(a, b) => format!("({} -> {})", head(a), head(b)), + } +} + +fn predicate(value: &nash_ast::Pred<'_>) -> String { + let localizer = crate::localizer::Localizer::default(); + let render = |t: &nash_region::Located>| { + crate::render_type::can_to_doc(&localizer, crate::render_type::Ctx::App, &t.value) + .render(80, false) + }; + let (name, args) = match value { + nash_ast::Pred::Trait { trait_, args } | nash_ast::Pred::Implied { trait_, args } => { + (qualified(*trait_), *args) + } + nash_ast::Pred::Apply { head, args } => (format!("formation of {}", render(head)), *args), + }; + std::iter::once(name) + .chain(args.iter().map(|t| render(t))) + .collect::>() + .join(" ") +} + +#[cfg(test)] +mod coverage { + use super::*; + use nash_region::{Located, Position}; + fn r() -> Region { + Region::new(Position::new(1, 1), Position::new(1, 5)) + } + fn r2() -> Region { + Region::new(Position::new(2, 1), Position::new(2, 6)) + } + fn home() -> ModuleName<'static> { + ModuleName { + package: None, + name: "First", + } + } + fn other() -> ModuleName<'static> { + ModuleName { + package: None, + name: "Second", + } + } + fn q() -> QualifiedName<'static> { + QualifiedName { + home: home(), + name: "Equal", + } + } + fn check(name: &str, error: Error<'_>) { + let source = Source::new("name = other\nother = name\n"); + let report = to_report(&source, &error); + insta::assert_snapshot!(name, crate::render_plain(&report, &source, "Main.nash")); + } + #[test] + fn record_literal_no_alias() { + check( + "variant_record_literal_no_alias", + Error::RecordLiteralNoAlias { + region: r(), + fields: &["x", "y"], + }, + ); + } + #[test] + fn record_literal_ambiguous() { + check( + "variant_record_literal_ambiguous", + Error::RecordLiteralAmbiguous { + region: r(), + candidates: &[ + q(), + QualifiedName { + home: other(), + name: "Other", + }, + ], + }, + ); + } + #[test] + fn record_type_outside_alias() { + check( + "variant_record_type_outside_alias", + Error::RecordTypeOutsideAlias { region: r() }, + ); + } + #[test] + fn impl_pattern_limit() { + check( + "variant_impl_pattern_limit", + Error::ImplPatternLimit { region: r() }, + ); + } + #[test] + fn negate_without_num() { + check( + "variant_negate_without_num", + Error::NegateWithoutNum { region: r() }, + ); + } + #[test] + fn do_without_monad() { + check( + "variant_do_without_monad", + Error::DoWithoutMonad { region: r() }, + ); + } + #[test] + fn refutable_bind_pattern() { + check( + "variant_refutable_bind_pattern", + Error::RefutableBindPattern { region: r() }, + ); + } + #[test] + fn structural_eq_override() { + check( + "variant_structural_eq_override", + Error::StructuralEqOverride { + head: &Located::at(r(), nash_ast::Type::Var("a")), + }, + ); + } + #[test] + fn reflexive_lift_overlap() { + check( + "variant_reflexive_lift_overlap", + Error::ReflexiveLiftOverlap { + heads: &[&Located::at(r(), nash_ast::Type::Var("a"))], + }, + ); + } + #[test] + fn missing_superclass() { + check( + "variant_missing_superclass", + Error::MissingSuperclass { + region: r(), + trait_: q(), + heads: &[Located::at(r(), nash_ast::Head::Var(0))], + superclass: &nash_ast::Pred::Trait { + trait_: q(), + args: &[], + }, + index: 0, + reason: nash_can::EntailmentFailure::Missing, + }, + ); + } + #[test] + fn bad_instance_head() { + check( + "variant_bad_instance_head", + Error::BadInstanceHead { + region: r(), + reason: nash_can::BadHead::BareVariable, + }, + ); + } + #[test] + fn impl_context_var_not_in_head() { + check( + "variant_impl_context_var_not_in_head", + Error::ImplContextVarNotInHead { + region: r(), + name: "name", + }, + ); + } + #[test] + fn missing_method() { + check( + "variant_missing_method", + Error::MissingMethod { + region: r(), + trait_: "Equal", + name: "name", + }, + ); + } + #[test] + fn unknown_method() { + check( + "variant_unknown_method", + Error::UnknownMethod { + region: r(), + trait_: "Equal", + name: "name", + }, + ); + } + #[test] + fn orphan_impl() { + check( + "variant_orphan_impl", + Error::OrphanImpl { + region: r(), + trait_: q(), + heads: &[nash_ast::HeadCon::Named(q())], + }, + ); + } + #[test] + fn overlapping_impls() { + check( + "variant_overlapping_impls", + Error::OverlappingImpls { + key: &nash_ast::ImplKey { + trait_: q(), + heads: &[nash_ast::Head::Var(0)], + }, + first: r(), + second: r2(), + first_home: home(), + second_home: other(), + }, + ); + } + #[test] + fn import_open_trait() { + check( + "variant_import_open_trait", + Error::ImportOpenTrait { + region: r(), + name: "name", + }, + ); + } + #[test] + fn duplicate_trait() { + check( + "variant_duplicate_trait", + Error::DuplicateTrait { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn duplicate_method() { + check( + "variant_duplicate_method", + Error::DuplicateMethod { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn duplicate_trait_parameter() { + check( + "variant_duplicate_trait_parameter", + Error::DuplicateTraitParameter { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn superclass_bad_arg() { + check( + "variant_superclass_bad_arg", + Error::SuperclassBadArg { + region: r(), + trait_: "Equal", + }, + ); + } + #[test] + fn method_missing_parameter() { + check( + "variant_method_missing_parameter", + Error::MethodMissingParameter { + region: r(), + method: "a", + parameter: "a", + }, + ); + } + #[test] + fn recursive_superclass() { + check( + "variant_recursive_superclass", + Error::RecursiveSuperclass { + names: &[&Located::at(r(), "First"), &Located::at(r2(), "Second")], + }, + ); + } + #[test] + fn export_open_trait() { + check( + "variant_export_open_trait", + Error::ExportOpenTrait { + region: r(), + name: "name", + }, + ); + } + #[test] + fn not_found_trait() { + check( + "variant_not_found_trait", + Error::NotFoundTrait { + region: r(), + prefix: None, + name: "name", + }, + ); + } + #[test] + fn ambiguous_trait() { + check( + "variant_ambiguous_trait", + Error::AmbiguousTrait { + region: r(), + prefix: None, + name: "name", + first_module: home(), + other_modules: &[other()], + }, + ); + } + #[test] + fn trait_arity() { + check( + "variant_trait_arity", + Error::TraitArity { + region: r(), + name: "name", + expected: 1, + actual: 2, + }, + ); + } + #[test] + fn context_var_not_in_type() { + check( + "variant_context_var_not_in_type", + Error::ContextVarNotInType { + region: r(), + name: "name", + }, + ); + } + #[test] + fn kind_mismatch() { + check( + "variant_kind_mismatch", + Error::KindMismatch { + region: r(), + context: &KindContext::TypeAnnotation, + expected: &Kind::Type, + actual: &Kind::Arrow(&Kind::Type, &Kind::Type), + }, + ); + } + #[test] + fn kind_infinite() { + check( + "variant_kind_infinite", + Error::KindInfinite { + region: r(), + context: &KindContext::Annotation { name: "name" }, + }, + ); + } + #[test] + fn representation_mismatch() { + check( + "variant_representation_mismatch", + Error::RepresentationMismatch { + region: r(), + context: &KindContext::TypeAnnotation, + required: nash_ast::primitives::ReprTrait::Storable, + actual: nash_ast::primitives::Repr::Term, + }, + ); + } + #[test] + fn contradictory_representation() { + check( + "variant_contradictory_representation", + Error::ContradictoryRepresentation { + region: r(), + variable: "a", + }, + ); + } + #[test] + fn impl_of_builtin_trait() { + check( + "variant_impl_of_builtin_trait", + Error::ImplOfBuiltinTrait { + region: r(), + trait_: q(), + }, + ); + } + #[test] + fn irregular_recursion() { + check( + "variant_irregular_recursion", + Error::IrregularRecursion { + region: r(), + constructor: q(), + parameter: "a", + }, + ); + } + #[test] + fn unsupported() { + check( + "variant_unsupported", + Error::Unsupported { + feature: "a", + region: r(), + }, + ); + } + #[test] + fn missing_module_header() { + check("variant_missing_module_header", Error::MissingModuleHeader); + } + #[test] + fn not_found_type() { + check( + "variant_not_found_type", + Error::NotFoundType { + region: r(), + prefix: None, + name: "name", + suggestions: PossibleNames { + locals: &["name"], + qualified: &[], + }, + }, + ); + } + #[test] + fn import_not_found() { + check( + "variant_import_not_found", + Error::ImportNotFound { + region: r(), + module: "Missing", + }, + ); + } + #[test] + fn ambiguous_type() { + check( + "variant_ambiguous_type", + Error::AmbiguousType { + region: r(), + prefix: None, + name: "name", + first_module: home(), + other_modules: &[other()], + }, + ); + } + #[test] + fn bad_arity() { + check( + "variant_bad_arity", + Error::BadArity { + region: r(), + context: BadArityContext::TypeArity, + name: "Box", + expected: 1, + actual: 2, + }, + ); + } + #[test] + fn export_not_found() { + check( + "variant_export_not_found", + Error::ExportNotFound { + region: r(), + kind: VarKind::BadVar, + name: "naem", + suggestions: &["name"], + }, + ); + } + #[test] + fn export_open_alias() { + check( + "variant_export_open_alias", + Error::ExportOpenAlias { + region: r(), + name: "name", + }, + ); + } + #[test] + fn duplicate_decl() { + check( + "variant_duplicate_decl", + Error::DuplicateDecl { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn duplicate_type() { + check( + "variant_duplicate_type", + Error::DuplicateType { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn duplicate_ctor() { + check( + "variant_duplicate_ctor", + Error::DuplicateCtor { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn duplicate_binop() { + check( + "variant_duplicate_binop", + Error::DuplicateBinop { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn binop_function_not_found() { + check( + "variant_binop_function_not_found", + Error::BinopFunctionNotFound { + region: r(), + op: "a", + function: "a", + }, + ); + } + #[test] + fn duplicate_union_arg() { + check( + "variant_duplicate_union_arg", + Error::DuplicateUnionArg { + type_name: "a", + arg_name: "a", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn duplicate_alias_arg() { + check( + "variant_duplicate_alias_arg", + Error::DuplicateAliasArg { + type_name: "a", + arg_name: "a", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn recursive_alias() { + check( + "variant_recursive_alias", + Error::RecursiveAlias { + region: r(), + name: "Loop", + args: &["a"], + typ: &Located::at(r(), nash_source::Type::Var("a")), + others: &[], + }, + ); + } + #[test] + fn type_vars_unbound_in_union() { + check( + "variant_type_vars_unbound_in_union", + Error::TypeVarsUnboundInUnion { + region: r(), + name: "Box", + args: &[], + unbound: ("a", r()), + more_unbound: &[], + }, + ); + } + #[test] + fn type_vars_messed_up_in_alias() { + check( + "variant_type_vars_messed_up_in_alias", + Error::TypeVarsMessedUpInAlias { + region: r(), + name: "Box", + args: &["a"], + unused: &[("a", r())], + unbound: &[("b", r2())], + }, + ); + } + #[test] + fn labeled_ctor_missing_field() { + check( + "variant_labeled_ctor_missing_field", + Error::LabeledCtorMissingField { + region: r(), + ctor: "a", + field: "a", + }, + ); + } + #[test] + fn labeled_ctor_extra_field() { + check( + "variant_labeled_ctor_extra_field", + Error::LabeledCtorExtraField { + region: r(), + ctor: "a", + field: "a", + }, + ); + } + #[test] + fn labeled_ctor_unknown_field() { + check( + "variant_labeled_ctor_unknown_field", + Error::LabeledCtorUnknownField { + region: r(), + ctor: "a", + field: "a", + }, + ); + } + #[test] + fn duplicate_field() { + check( + "variant_duplicate_field", + Error::DuplicateField { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn export_duplicate() { + check( + "variant_export_duplicate", + Error::ExportDuplicate { + name: "name", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn not_found_ctor() { + check( + "variant_not_found_ctor", + Error::NotFoundCtor { + region: r(), + prefix: None, + name: "name", + suggestions: PossibleNames { + locals: &["name"], + qualified: &[], + }, + }, + ); + } + #[test] + fn ambiguous_ctor() { + check( + "variant_ambiguous_ctor", + Error::AmbiguousCtor { + region: r(), + prefix: None, + name: "name", + first_module: home(), + other_modules: &[other()], + }, + ); + } + #[test] + fn pattern_has_record_ctor() { + check( + "variant_pattern_has_record_ctor", + Error::PatternHasRecordCtor { + region: r(), + name: "name", + }, + ); + } + #[test] + fn duplicate_pattern() { + check( + "variant_duplicate_pattern", + Error::DuplicatePattern { + context: DuplicatePatternContext::CaseBranch, + name: "x", + first: r(), + second: r2(), + }, + ); + } + #[test] + fn not_found_var() { + check( + "variant_not_found_var", + Error::NotFoundVar { + region: r(), + prefix: None, + name: "name", + suggestions: PossibleNames { + locals: &["name"], + qualified: &[], + }, + }, + ); + } + #[test] + fn ambiguous_var() { + check( + "variant_ambiguous_var", + Error::AmbiguousVar { + region: r(), + prefix: None, + name: "name", + first_module: home(), + other_modules: &[other()], + }, + ); + } + #[test] + fn not_found_binop() { + check( + "variant_not_found_binop", + Error::NotFoundBinop { + region: r(), + name: "name", + available: &["other"], + }, + ); + } + #[test] + fn ambiguous_binop() { + check( + "variant_ambiguous_binop", + Error::AmbiguousBinop { + region: r(), + name: "name", + first_module: home(), + other_modules: &[other()], + }, + ); + } + #[test] + fn binop_conflict() { + check( + "variant_binop_conflict", + Error::BinopConflict { + region: r(), + op1: "a", + op2: "a", + }, + ); + } + #[test] + fn shadowing() { + check( + "variant_shadowing", + Error::Shadowing { + name: "name", + original: r(), + new: r2(), + }, + ); + } + #[test] + fn recursive_let() { + check( + "variant_recursive_let", + Error::RecursiveLet { + name: &Located::at(r(), "name"), + others: &["other"], + }, + ); + } + #[test] + fn recursive_decl() { + check( + "variant_recursive_decl", + Error::RecursiveDecl { + name: &Located::at(r(), "name"), + others: &[], + }, + ); + } + #[test] + fn annotation_too_short() { + check( + "variant_annotation_too_short", + Error::AnnotationTooShort { + region: r(), + name: "name", + index: 1, + leftovers: 2, + }, + ); + } + #[test] + fn import_exposing_not_found() { + check( + "variant_import_exposing_not_found", + Error::ImportExposingNotFound { + region: r(), + module: home(), + name: "name", + available: &["other"], + }, + ); + } + #[test] + fn import_ctor_by_name() { + check( + "variant_import_ctor_by_name", + Error::ImportCtorByName { + region: r(), + name: "name", + type_name: "a", + }, + ); + } + #[test] + fn import_open_alias() { + check( + "variant_import_open_alias", + Error::ImportOpenAlias { + region: r(), + name: "name", + }, + ); + } +} + +#[cfg(test)] +mod branches { + use super::*; + use nash_region::{Located, Position}; + fn r() -> Region { + Region::new(Position::new(1, 1), Position::new(1, 5)) + } + fn snapshot(name: &str, error: Error<'_>) { + let source = Source::new("name = other\n"); + insta::assert_snapshot!( + name, + crate::render_plain(&to_report(&source, &error), &source, "Main.nash") + ); + } + #[test] + fn qualified_missing_import() { + snapshot( + "qualified_missing_import", + Error::NotFoundVar { + region: r(), + prefix: Some("Missing"), + name: "value", + suggestions: PossibleNames { + locals: &[], + qualified: &[], + }, + }, + ); + } + #[test] + fn qualified_not_exposed() { + snapshot( + "qualified_not_exposed", + Error::NotFoundType { + region: r(), + prefix: Some("Known"), + name: "Box", + suggestions: PossibleNames { + locals: &[], + qualified: &[("Known", &["Bag"])], + }, + }, + ); + } + #[test] + fn qualified_ambiguity() { + snapshot( + "qualified_ambiguity", + Error::AmbiguousType { + region: r(), + prefix: Some("A"), + name: "Box", + first_module: ModuleName { + package: None, + name: "First", + }, + other_modules: &[ModuleName { + package: None, + name: "Second", + }], + }, + ); + } + #[test] + fn too_few_args() { + snapshot( + "too_few_args", + Error::BadArity { + region: r(), + context: BadArityContext::PatternArity, + name: "Pair", + expected: 2, + actual: 1, + }, + ); + } + #[test] + fn too_many_args_plural() { + snapshot( + "too_many_args_plural", + Error::BadArity { + region: r(), + context: BadArityContext::PatternArity, + name: "One", + expected: 1, + actual: 3, + }, + ); + } + #[test] + fn recursive_alias_cycle() { + snapshot( + "recursive_alias_cycle", + Error::RecursiveAlias { + region: r(), + name: "First", + args: &[], + typ: &Located::at(r(), nash_source::Type::Var("a")), + others: &["Second", "Third"], + }, + ); + } + #[test] + fn recursive_decl_cycle() { + snapshot( + "recursive_decl_cycle", + Error::RecursiveDecl { + name: &Located::at(r(), "name"), + others: &["other"], + }, + ); + } + #[test] + fn recursive_let_self() { + snapshot( + "recursive_let_self", + Error::RecursiveLet { + name: &Located::at(r(), "name"), + others: &[], + }, + ); + } + #[test] + fn unused_alias_variable() { + snapshot( + "unused_alias_variable", + Error::TypeVarsMessedUpInAlias { + region: r(), + name: "Box", + args: &["a"], + unused: &[("a", r())], + unbound: &[], + }, + ); + } + #[test] + fn unused_alias_variables() { + snapshot( + "unused_alias_variables", + Error::TypeVarsMessedUpInAlias { + region: r(), + name: "Box", + args: &["a", "b"], + unused: &[("a", r()), ("b", r())], + unbound: &[], + }, + ); + } + #[test] + fn unbound_alias_variable() { + snapshot( + "unbound_alias_variable", + Error::TypeVarsMessedUpInAlias { + region: r(), + name: "Box", + args: &[], + unused: &[], + unbound: &[("a", r())], + }, + ); + } + #[test] + fn unbound_union_variables() { + snapshot( + "unbound_union_variables", + Error::TypeVarsUnboundInUnion { + region: r(), + name: "Box", + args: &[], + unbound: ("a", r()), + more_unbound: &[("b", r())], + }, + ); + } + #[test] + fn operator_javascript_equal() { + snapshot( + "operator_javascript_equal", + Error::NotFoundBinop { + region: r(), + name: "===", + available: &[], + }, + ); + } + #[test] + fn operator_javascript_not_equal() { + snapshot( + "operator_javascript_not_equal", + Error::NotFoundBinop { + region: r(), + name: "!=", + available: &[], + }, + ); + } + #[test] + fn operator_javascript_strict_not_equal() { + snapshot( + "operator_javascript_strict_not_equal", + Error::NotFoundBinop { + region: r(), + name: "!==", + available: &[], + }, + ); + } + #[test] + fn operator_power() { + snapshot( + "operator_power", + Error::NotFoundBinop { + region: r(), + name: "**", + available: &[], + }, + ); + } + #[test] + fn operator_percent() { + snapshot( + "operator_percent", + Error::NotFoundBinop { + region: r(), + name: "%", + available: &[], + }, + ); + } + #[test] + fn operator_missing() { + snapshot( + "operator_missing", + Error::NotFoundBinop { + region: r(), + name: "<+>", + available: &[], + }, + ); + } + #[test] + fn module_name_is_preserved() { + let source = Source::new(""); + let report = to_report_with_name(&source, &Error::MissingModuleHeader, "App.Main"); + insta::assert_snapshot!(crate::render_plain(&report, &source, "App/Main.nash")); + } + #[test] + fn closed_kind_precedence() { + let arrow = Kind::Arrow(&Kind::Type, &Kind::Type); + assert_eq!( + kind(&Kind::Arrow(&arrow, &arrow)), + "(Type -> Type) -> Type -> Type" + ); + } + #[test] + fn formation_contexts() { + let trait_ = QualifiedName { + home: ModuleName { + package: None, + name: "Core", + }, + name: "Functor", + }; + let contexts = [ + KindContext::TypeAnnotation, + KindContext::BigField { + union: "Tree", + ctor: "Node", + index: 1, + }, + KindContext::LittleField { + union: "tree", + ctor: "Node", + index: 0, + }, + KindContext::RecordField { + alias: "State", + field: "x", + big: true, + }, + KindContext::RecordField { + alias: "state", + field: "x", + big: false, + }, + KindContext::AliasCasing { + alias: "Box", + big: true, + }, + KindContext::AliasCasing { + alias: "box", + big: false, + }, + KindContext::Annotation { name: "map" }, + KindContext::ImplHead { trait_, index: 1 }, + ]; + insta::assert_snapshot!( + contexts + .iter() + .map(kind_context) + .collect::>() + .join("\n") + ); + } + #[test] + fn source_pipeline_reports_all_missing_names() { + 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 module = parser.module().expect("parse"); + let errors = nash_can::canonicalize(&bump, nash_can::Context::default(), &module) + .expect_err("canonical errors"); + assert_eq!(errors.len(), 2); + let source = Source::new(input); + insta::assert_snapshot!( + errors + .iter() + .map(|e| crate::render_plain(&to_report(&source, e), &source, "Main.nash")) + .collect::>() + .join("\n") + ); + } + #[test] + fn bad_impl_head_reasons() { + for (name, reason) in [ + ("bad_head_function", nash_can::BadHead::Function), + ("bad_head_record", nash_can::BadHead::Record), + ( + "bad_head_variable_application", + nash_can::BadHead::VariableApplication, + ), + ] { + snapshot( + name, + Error::BadInstanceHead { + region: r(), + reason, + }, + ); + } + } + #[test] + fn superclass_failure_reasons() { + let trait_ = QualifiedName { + home: ModuleName { + package: None, + name: "Core", + }, + name: "Equal", + }; + let arg = Located::at(r(), nash_ast::Type::Var("a")); + for (name, reason) in [ + ("superclass_cycle", nash_can::EntailmentFailure::Cycle), + ("superclass_limit", nash_can::EntailmentFailure::Limit), + ] { + snapshot( + name, + Error::MissingSuperclass { + region: r(), + trait_, + heads: &[Located::at(r(), nash_ast::Head::Var(0))], + superclass: &nash_ast::Pred::Trait { + trait_, + args: &[&arg], + }, + index: 0, + reason, + }, + ); + } + } + #[test] + fn duplicate_pattern_contexts() { + for (name, context) in [ + ("duplicate_lambda", DuplicatePatternContext::LambdaArgs), + ( + "duplicate_function", + DuplicatePatternContext::FuncArgs("map"), + ), + ("duplicate_let", DuplicatePatternContext::LetBinding), + ("duplicate_destruct", DuplicatePatternContext::Destruct), + ] { + snapshot( + name, + Error::DuplicatePattern { + context, + name: "x", + first: r(), + second: r(), + }, + ); + } + } + #[test] + fn export_suggestion_counts() { + snapshot( + "export_no_suggestions", + Error::ExportNotFound { + region: r(), + kind: VarKind::BadType, + name: "Box", + suggestions: &[], + }, + ); + snapshot( + "export_multiple_suggestions", + Error::ExportNotFound { + region: r(), + kind: VarKind::BadOp, + name: "<+>", + suggestions: &["+", "++"], + }, + ); + } + #[test] + fn source_pipeline_overlapping_impls() { + 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 module = parser.module().expect("parse"); + let errors = nash_can::canonicalize(&bump, nash_can::Context::default(), &module) + .expect_err("overlapping impls"); + let [error @ Error::OverlappingImpls { first, second, .. }] = errors.as_slice() else { + panic!("expected one overlap error"); + }; + let source = Source::new(input); + 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) + ); + let rendered = crate::render_plain(&report, &source, "Bad.nash"); + assert!(!rendered.contains("Rename")); + assert!(rendered.contains("context constraints")); + insta::assert_snapshot!(rendered); + } +} diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index 3ecc6e17..793b4408 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -6,6 +6,7 @@ //! (Elm's `--report=json` shape), and the LSP conversion in //! `nash-language-server`. +pub mod canonicalize; pub mod code; pub mod doc; pub mod json; diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_function.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_function.snap new file mode 100644 index 00000000..142cd943 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_function.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +BAD IMPL HEAD + + × This impl head is a function type: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Use a named type constructor or a tuple as the outermost type of an impl head. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_record.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_record.snap new file mode 100644 index 00000000..837a1265 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_record.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +BAD IMPL HEAD + + × This impl head is an anonymous record: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Use a named type constructor or a tuple as the outermost type of an impl head. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_variable_application.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_variable_application.snap new file mode 100644 index 00000000..ee34d0cb --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__bad_head_variable_application.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +BAD IMPL HEAD + + × This impl head is an application of a type variable: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Use a named type constructor or a tuple as the outermost type of an impl head. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_destruct.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_destruct.snap new file mode 100644 index 00000000..9f6fa992 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_destruct.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAME CLASH + + × This pattern contains multiple `x` variables. One here: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──┬─┬ + · │ ╰── and another one here + · ╰── one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_function.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_function.snap new file mode 100644 index 00000000..0f6b9164 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_function.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAME CLASH + + × The `map` function has multiple `x` arguments. One here: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──┬─┬ + · │ ╰── and another one here + · ╰── one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_lambda.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_lambda.snap new file mode 100644 index 00000000..c99ab43b --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_lambda.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAME CLASH + + × This anonymous function has multiple `x` arguments. One here: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──┬─┬ + · │ ╰── and another one here + · ╰── one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_let.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_let.snap new file mode 100644 index 00000000..96f372f5 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__duplicate_let.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAME CLASH + + × This `let` expression defines `x` more than once! One here: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──┬─┬ + · │ ╰── and another one here + · ╰── one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_multiple_suggestions.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_multiple_suggestions.snap new file mode 100644 index 00000000..a6b8304d --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_multiple_suggestions.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN EXPORT + + × You are trying to expose an operator named (<+>) but I cannot find its + │ definition. + help: These names seem close though: + + + + ++ diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_no_suggestions.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_no_suggestions.snap new file mode 100644 index 00000000..2282d89a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__export_no_suggestions.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN EXPORT + + × You are trying to expose a type named `Box` but I cannot find its definition. + help: I do not see any super similar names in this file. Is the definition missing? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__formation_contexts.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__formation_contexts.snap new file mode 100644 index 00000000..1ffa606f --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__formation_contexts.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "contexts.iter().map(kind_context).collect::>().join(\"\\n\")" +--- +the type annotation +field 2 of Big constructor `Node` in `Tree` +field 1 of little constructor `Node` in `tree` +field `x` of Big record alias `State` +field `x` of little record alias `state` +the uppercase name of alias `Box` +the lowercase name of alias `box` +the annotation for `map` +head 2 of impl `Core.Functor` diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__module_name_is_preserved.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__module_name_is_preserved.snap new file mode 100644 index 00000000..0e9882f9 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__module_name_is_preserved.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"App/Main.nash\")" +--- +MODULE NAME MISSING + + × I need the module name to be declared at the top of this file, like this: + │ + │ module App.Main exposing (..) + │ + │ Try adding that as the first line of your file! + help: Note: It is best to replace (..) with an explicit list of types and functions + you want to expose. When you know a value is only used within this module, you + can refactor without worrying about uses elsewhere. Limiting exposed values can + also speed up compilation because I can skip a bunch of work if I see that the + exposed API has not changed. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_equal.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_equal.snap new file mode 100644 index 00000000..70512122 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_equal.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × Nash does not have a (===) operator like JavaScript. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Switch to (==) instead. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_not_equal.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_not_equal.snap new file mode 100644 index 00000000..54a04a32 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_not_equal.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × Nash uses a different name for the “not equal” operator: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Switch to (/=) instead. Our (/=) operator is supposed to look like a real “not + equal” sign (≠). I hope that history will remember (!=) as a weird and temporary + choice. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_strict_not_equal.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_strict_not_equal.snap new file mode 100644 index 00000000..6e42d544 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_javascript_strict_not_equal.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × Nash uses a different name for the “not equal” operator: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Switch to (/=) instead. Our (/=) operator is supposed to look like a real “not + equal” sign (≠). I hope that history will remember (!==) as a weird and + temporary choice. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_missing.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_missing.snap new file mode 100644 index 00000000..4c02ec99 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_missing.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × I do not recognize the (<+>) operator. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Is there an `import` and `exposing` entry for it? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_percent.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_percent.snap new file mode 100644 index 00000000..2992bc73 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_percent.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × Nash does not use (%) as the remainder operator: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_power.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_power.snap new file mode 100644 index 00000000..2ca495f5 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__operator_power.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × I do not recognize the (**) operator: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Switch to (^) for exponentiation. Or switch to (*) for multiplication. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_ambiguity.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_ambiguity.snap new file mode 100644 index 00000000..1040a498 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_ambiguity.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +AMBIGUOUS NAME + + × This usage of `A.Box` is ambiguous. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: It could refer to a type from either of these imports: + + import First as A + import Second as A + + Read to learn how to clarify which one + you want. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_missing_import.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_missing_import.snap new file mode 100644 index 00000000..5d0d0798 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_missing_import.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `Missing.value` variable: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: I cannot find a `Missing` module. Is there an `import` for it? + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_not_exposed.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_not_exposed.snap new file mode 100644 index 00000000..c239fe0a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__qualified_not_exposed.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `Known.Box` type: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: The `Known` module does not expose a `Box` type. These names seem close though: + + Known.Bag + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_alias_cycle.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_alias_cycle.snap new file mode 100644 index 00000000..217e3a5b --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_alias_cycle.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +ALIAS PROBLEM + + × This type alias is part of a mutually recursive set of type aliases. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: It is part of this cycle of type aliases: + + ┌─────┐ + │ First + │ ↓ + │ Second + │ ↓ + │ Third + └─────┘ + + You need to convert at least one of these type aliases into a `type`. + + Note: Read to learn why this + `type` vs `type alias` distinction matters. It is subtle but important! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_decl_cycle.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_decl_cycle.snap new file mode 100644 index 00000000..43456d51 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_decl_cycle.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +CYCLIC DEFINITION + + × The `name` definition is causing a very tricky infinite loop. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: The `name` value depends on itself through the following chain of definitions: + + ┌─────┐ + │ name + │ ↓ + │ other + └─────┘ + + Hint: The root problem is often a typo in some variable name, but I recommend + reading for more detailed advice, + especially if you actually do need a recursive value. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_let_self.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_let_self.snap new file mode 100644 index 00000000..248fb1c0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__recursive_let_self.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +CYCLIC VALUE + + × The `name` value is defined directly in terms of itself, causing an infinite + │ loop. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: 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! + + 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! + + Hint: The root problem is often a typo in some variable name, but I recommend + reading for more detailed advice, + especially if you actually do need a recursive value. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_overlapping_impls.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_overlapping_impls.snap new file mode 100644 index 00000000..81213bc0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_overlapping_impls.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: rendered +--- +OVERLAPPING IMPL + + × These `Bad.Keep` impls can both match the same trait arguments. The overlapping + │ head is Builtin.unit: + ╭─[Bad.nash:6:1] + 3 │ keep : 'a -> 'a + 4 │ ╭─▶ impl Keep () where + 5 │ ├─▶ keep x = x + · ╰──── first impl in `Bad` + 6 │ ╭─▶ impl Keep () where + 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_reports_all_missing_names.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_reports_all_missing_names.snap new file mode 100644 index 00000000..b3bee2e5 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__source_pipeline_reports_all_missing_names.snap @@ -0,0 +1,36 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "errors.iter().map(|e|\ncrate::render_plain(&to_report(&source, e), &source,\n\"Main.nash\")).collect::>().join(\"\\n\")" +--- +NAMING ERROR + + × I cannot find a `missing` variable: + ╭─[Main.nash:2:9] + 1 │ module Main exposing (..) + 2 │ first = missing + · ─────── + 3 │ second = absent + ╰──── + help: These names seem close though: + + first + second + + Hint: Read to see how `import` + declarations work in Nash. + +NAMING ERROR + + × I cannot find a `absent` variable: + ╭─[Main.nash:3:10] + 2 │ first = missing + 3 │ second = absent + · ────── + ╰──── + help: These names seem close though: + + first + second + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_cycle.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_cycle.snap new file mode 100644 index 00000000..70afc08e --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_cycle.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +MISSING SUPERCLASS + + × The `Core.Equal` impl for 'a0 does not establish superclass 1: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: It must also satisfy `Core.Equal 'a`. Resolving it leads back to the same + requirement, forming a cycle. Add the required impl or include the necessary + constraint in this impl's context. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_limit.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_limit.snap new file mode 100644 index 00000000..c74cb9f4 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__superclass_limit.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +MISSING SUPERCLASS + + × The `Core.Equal` impl for 'a0 does not establish superclass 1: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: It must also satisfy `Core.Equal 'a`. Resolving it exceeded the search limit + because the requirements keep expanding. Add the required impl or include the + necessary constraint in this impl's context. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_few_args.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_few_args.snap new file mode 100644 index 00000000..472351a8 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_few_args.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +TOO FEW ARGS + + × The `Pair` variant needs 2 arguments, but I see 1 instead: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: What is missing? Are some parentheses misplaced? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_many_args_plural.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_many_args_plural.snap new file mode 100644 index 00000000..af02c8b4 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__too_many_args_plural.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +TOO MANY ARGS + + × The `One` variant needs 1 argument, but I see 3 instead: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Which are the extra ones? Maybe some parentheses are missing? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_alias_variable.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_alias_variable.snap new file mode 100644 index 00000000..d66a4e0e --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_alias_variable.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNBOUND TYPE VARIABLE + + × The `Box` type alias uses an unbound type variable `a` in its definition: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: You probably need to change the declaration to something like this: + + type alias Box 'a = ... + + Why? Well, imagine one `Box` where `a` 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_union_variables.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_union_variables.snap new file mode 100644 index 00000000..c4f45fad --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unbound_union_variables.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNBOUND TYPE VARIABLES + + × Type variables a and b are unbound in the `Box` type definition: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: You probably need to change the declaration to something like this: + + type Box 'a 'b = ... + + Why? Well, imagine one `Box` where `a` 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variable.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variable.snap new file mode 100644 index 00000000..0e286d9d --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variable.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNUSED TYPE VARIABLE + + × Type alias `Box` does not use the `a` type variable. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: I recommend removing a from the declaration, like this: + + type alias Box = ... + + 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! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variables.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variables.snap new file mode 100644 index 00000000..3ddac73a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__branches__unused_alias_variables.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +UNUSED TYPE VARIABLES + + × Type variables a and b are unused in the `Box` definition. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: I recommend removing a and b from the declaration, like this: + + type alias Box = ... + + 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! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_binop.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_binop.snap new file mode 100644 index 00000000..7d1da77a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_binop.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +AMBIGUOUS NAME + + × This usage of `name` is ambiguous: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This name is exposed by 2 of your imports, so I am not sure which one to use: + + First.name + Second.name + + 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. + + Note: Check out for more info on the + import syntax. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_ctor.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_ctor.snap new file mode 100644 index 00000000..7d1da77a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_ctor.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +AMBIGUOUS NAME + + × This usage of `name` is ambiguous: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This name is exposed by 2 of your imports, so I am not sure which one to use: + + First.name + Second.name + + 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. + + Note: Check out for more info on the + import syntax. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_trait.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_trait.snap new file mode 100644 index 00000000..7d1da77a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_trait.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +AMBIGUOUS NAME + + × This usage of `name` is ambiguous: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This name is exposed by 2 of your imports, so I am not sure which one to use: + + First.name + Second.name + + 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. + + Note: Check out for more info on the + import syntax. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_type.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_type.snap new file mode 100644 index 00000000..7d1da77a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_type.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +AMBIGUOUS NAME + + × This usage of `name` is ambiguous: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This name is exposed by 2 of your imports, so I am not sure which one to use: + + First.name + Second.name + + 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. + + Note: Check out for more info on the + import syntax. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_var.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_var.snap new file mode 100644 index 00000000..7d1da77a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_ambiguous_var.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +AMBIGUOUS NAME + + × This usage of `name` is ambiguous: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This name is exposed by 2 of your imports, so I am not sure which one to use: + + First.name + Second.name + + 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. + + Note: Check out for more info on the + import syntax. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_annotation_too_short.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_annotation_too_short.snap new file mode 100644 index 00000000..25cc609f --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_annotation_too_short.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD TYPE ANNOTATION + + × The type annotation for `name` says it can accept 1 argument, but the definition + │ says it has 3 arguments: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Is the type annotation missing something? Should some arguments be deleted? + Maybe some parentheses are missing? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_arity.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_arity.snap new file mode 100644 index 00000000..9c81230b --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_arity.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TOO MANY TYPE ARGS + + × The `Box` type needs 1 argument, but I see 2 instead: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Which is the extra one? Maybe some parentheses are missing? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_instance_head.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_instance_head.snap new file mode 100644 index 00000000..987e0440 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_bad_instance_head.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD IMPL HEAD + + × This impl head is a bare type variable: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Use a named type constructor or a tuple as the outermost type of an impl head. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_conflict.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_conflict.snap new file mode 100644 index 00000000..85c6cb35 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_conflict.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +INFIX PROBLEM + + × You cannot mix (a) and (a) without parentheses. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: I do not know how to group these expressions. Add parentheses for me! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_function_not_found.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_function_not_found.snap new file mode 100644 index 00000000..3a40d76b --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_binop_function_not_found.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +INFIX PROBLEM + + × The (a) operator says it is implemented by `a`, but I cannot find a `a` + │ definition in this file. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Define it, or point the `infix` declaration at an existing top-level value. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_context_var_not_in_type.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_context_var_not_in_type.snap new file mode 100644 index 00000000..fca9acde --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_context_var_not_in_type.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TRAIT PROBLEM + + × The context mentions 'name, but this variable does not occur in the annotated + │ type: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Use the variable in the type, or remove the constraint that mentions it. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_contradictory_representation.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_contradictory_representation.snap new file mode 100644 index 00000000..28bc3e63 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_contradictory_representation.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +CONTRADICTORY REPRESENTATION + + × The representation requirements on 'a are incompatible: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: No type can satisfy all of these requirements. Change the constraints or the + positions where this type variable is used. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_do_without_monad.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_do_without_monad.snap new file mode 100644 index 00000000..c03ac387 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_do_without_monad.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot resolve this `do` expression: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: A `do` expression requires the `Monad` trait. Import the module that defines + `Monad`. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_alias_arg.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_alias_arg.snap new file mode 100644 index 00000000..6fc72d69 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_alias_arg.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × The `a` type alias has multiple `a` type variables. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_binop.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_binop.snap new file mode 100644 index 00000000..6d7b29f6 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_binop.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This file defines multiple (name) operators. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_ctor.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_ctor.snap new file mode 100644 index 00000000..cc6cc967 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_ctor.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This file defines multiple `name` type constructors. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_decl.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_decl.snap new file mode 100644 index 00000000..e8d822b3 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_decl.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This file has multiple `name` declarations. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_field.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_field.snap new file mode 100644 index 00000000..ebe7b9d4 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_field.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This record has multiple `name` fields. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_method.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_method.snap new file mode 100644 index 00000000..8b20bc51 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_method.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This trait has multiple `name` methods. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_pattern.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_pattern.snap new file mode 100644 index 00000000..75844e3d --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_pattern.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This `case` pattern has multiple `x` variables. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait.snap new file mode 100644 index 00000000..f355d86a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This file defines multiple `name` traits. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait_parameter.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait_parameter.snap new file mode 100644 index 00000000..4d567535 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_trait_parameter.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This trait has multiple `name` type parameters. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_type.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_type.snap new file mode 100644 index 00000000..d991dbf3 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_type.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × This file defines multiple `name` types. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_union_arg.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_union_arg.snap new file mode 100644 index 00000000..45c93607 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_duplicate_union_arg.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAME CLASH + + × The `a` type has multiple `a` type variables. One here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── one here + 2 │ other = name + · ──┬── + · ╰── and another one here + ╰──── + help: How can I know which one you want? Rename one of them! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_duplicate.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_duplicate.snap new file mode 100644 index 00000000..1da47034 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_duplicate.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +REDUNDANT EXPORT + + × You are trying to expose `name` multiple times! Once here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── once here + 2 │ other = name + · ──┬── + · ╰── and again right here + ╰──── + help: Remove one of them and you should be all set! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_not_found.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_not_found.snap new file mode 100644 index 00000000..e2aa587f --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_not_found.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN EXPORT + + × You are trying to expose a value named `naem` but I cannot find its definition. + help: Maybe you want name instead? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_alias.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_alias.snap new file mode 100644 index 00000000..5d55e358 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_alias.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD EXPORT + + × The (..) syntax is for exposing variants of a custom type. It cannot be used + │ with a type alias like `name` though. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Remove the (..) and you should be fine! diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_trait.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_trait.snap new file mode 100644 index 00000000..6efe98f8 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_export_open_trait.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD EXPORT + + × The `name` trait cannot be followed by (..) like this: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: The (..) syntax exposes variants of a custom type. Remove the dots and name the + trait methods explicitly when you need them. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_context_var_not_in_head.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_context_var_not_in_head.snap new file mode 100644 index 00000000..1a670c69 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_context_var_not_in_head.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TRAIT PROBLEM + + × The impl context mentions 'name, but this variable does not occur in the impl + │ head: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Each variable in an impl context must occur in its head. Check for a misspelled + type variable. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_of_builtin_trait.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_of_builtin_trait.snap new file mode 100644 index 00000000..d024b6c0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_of_builtin_trait.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BUILTIN TRAIT + + × The `First.Equal` trait is owned by the compiler: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Its representation rules cannot be replaced by an impl. Remove this impl and use + a type with an admitted representation. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_pattern_limit.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_pattern_limit.snap new file mode 100644 index 00000000..bcda31f1 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_impl_pattern_limit.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +IMPL PATTERN LIMIT + + × This impl pattern is too large or deeply nested: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Simplify the impl head so the compiler can compare and resolve its patterns + within the supported limit. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_ctor_by_name.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_ctor_by_name.snap new file mode 100644 index 00000000..78c6ac99 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_ctor_by_name.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD IMPORT + + × You are trying to import the `name` variant by name: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Try importing a(..) instead. The dots mean “expose the a type and all its + variants” so it gives you access to name. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_exposing_not_found.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_exposing_not_found.snap new file mode 100644 index 00000000..c7314335 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_exposing_not_found.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD IMPORT + + × The `First` module does not expose `name`: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Maybe you want other instead? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_not_found.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_not_found.snap new file mode 100644 index 00000000..3502bedf --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_not_found.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN IMPORT + + × I could not find a `Missing` module to import! + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_alias.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_alias.snap new file mode 100644 index 00000000..902faa77 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_alias.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD IMPORT + + × The `name` type alias cannot be followed by (..) like this: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Remove the (..) and it should work. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_trait.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_trait.snap new file mode 100644 index 00000000..d3f48ffe --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_import_open_trait.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD IMPORT + + × The `name` trait cannot be followed by (..) like this: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: The (..) syntax exposes variants of a custom type. Remove the dots and name the + trait methods explicitly when you need them. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_irregular_recursion.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_irregular_recursion.snap new file mode 100644 index 00000000..d7fa8b0e --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_irregular_recursion.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +IRREGULAR RECURSION + + × This recursive use of `First.Equal` constructs the parameter 'a: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This parameter controls the constraints needed to form the type. Pass a type + variable here so context inference can terminate. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_infinite.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_infinite.snap new file mode 100644 index 00000000..3d612721 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_infinite.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +INFINITE KIND + + × This application in the annotation for `name` would require an infinite kind: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: A type constructor cannot be applied to itself. Check which type is being + applied and the kinds of its arguments. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_mismatch.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_mismatch.snap new file mode 100644 index 00000000..83cd0f85 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_kind_mismatch.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +KIND MISMATCH + + × I found a kind mismatch in the type annotation: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This position needs kind `Type`, but the type has kind `Type -> Type`. Type + arguments must have matching kinds. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_extra_field.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_extra_field.snap new file mode 100644 index 00000000..8d59e8d5 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_extra_field.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN FIELD + + × The `a` constructor has no `a` field: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Remove this extra field or check its spelling against the constructor + declaration. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_missing_field.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_missing_field.snap new file mode 100644 index 00000000..d45e41ec --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_missing_field.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +MISSING FIELD + + × The `a` constructor needs a `a` field: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Add the missing field to this constructor application. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_unknown_field.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_unknown_field.snap new file mode 100644 index 00000000..3fde0d0c --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_labeled_ctor_unknown_field.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN FIELD + + × The `a` constructor has no `a` field: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Check the field name against the constructor declaration. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_method_missing_parameter.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_method_missing_parameter.snap new file mode 100644 index 00000000..b3914092 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_method_missing_parameter.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TRAIT PROBLEM + + × The `a` method does not mention trait parameter 'a: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Every trait parameter must occur in the method's type so a call can determine + which impl to use. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_method.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_method.snap new file mode 100644 index 00000000..ab134bb0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_method.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +MISSING METHOD + + × This `Equal` impl does not define `name`: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: The `Equal` trait requires this method and does not provide a default. Add a + `name` definition to this impl. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_module_header.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_module_header.snap new file mode 100644 index 00000000..c492d088 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_module_header.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +MODULE NAME MISSING + + × I need the module name to be declared at the top of this file, like this: + │ + │ module Main exposing (..) + │ + │ Try adding that as the first line of your file! + help: Note: It is best to replace (..) with an explicit list of types and functions + you want to expose. When you know a value is only used within this module, you + can refactor without worrying about uses elsewhere. Limiting exposed values can + also speed up compilation because I can skip a bunch of work if I see that the + exposed API has not changed. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_superclass.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_superclass.snap new file mode 100644 index 00000000..d0268084 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_missing_superclass.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +MISSING SUPERCLASS + + × The `First.Equal` impl for 'a0 does not establish superclass 1: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: It must also satisfy `First.Equal`. I cannot find an impl or context constraint + that provides it. Add the required impl or include the necessary constraint in + this impl's context. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_negate_without_num.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_negate_without_num.snap new file mode 100644 index 00000000..a7410411 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_negate_without_num.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot resolve numeric negation here: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Negation requires the `Num` trait. Import the module that defines `Num` before + using a negative expression. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_binop.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_binop.snap new file mode 100644 index 00000000..d71e1ba8 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_binop.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN OPERATOR + + × I do not recognize the (name) operator. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Is there an `import` and `exposing` entry for it? Maybe you want (other) + instead? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_ctor.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_ctor.snap new file mode 100644 index 00000000..9ec87ffb --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_ctor.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `name` variant: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: These names seem close though: + + name + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_trait.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_trait.snap new file mode 100644 index 00000000..a798dae8 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_trait.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `name` trait: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Is there an `import` or `exposing` missing up top? + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_type.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_type.snap new file mode 100644 index 00000000..86e6bc19 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_type.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `name` type: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: These names seem close though: + + name + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_var.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_var.snap new file mode 100644 index 00000000..444aebef --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_not_found_var.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `name` variable: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: These names seem close though: + + name + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_orphan_impl.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_orphan_impl.snap new file mode 100644 index 00000000..1e2f7284 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_orphan_impl.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +ORPHAN IMPL + + × This module cannot define an impl of `First.Equal` for First.Equal: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_overlapping_impls.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_overlapping_impls.snap new file mode 100644 index 00000000..c4ff5963 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_overlapping_impls.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +OVERLAPPING IMPL + + × These `First.Equal` impls can both match the same trait arguments. The + │ overlapping head is 'a0: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── first impl in `First` + 2 │ other = name + · ──┬── + · ╰── overlapping impl in `Second` + ╰──── + 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_pattern_has_record_ctor.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_pattern_has_record_ctor.snap new file mode 100644 index 00000000..826b78ff --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_pattern_has_record_ctor.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +BAD PATTERN + + × You can construct records by using `name` as a function, but it is not available + │ in pattern matching like this: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: I recommend matching the record as a variable and unpacking it later. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_ambiguous.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_ambiguous.snap new file mode 100644 index 00000000..143ce499 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_ambiguous.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +AMBIGUOUS RECORD + + × Several visible record aliases have exactly these fields: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: The candidates are First.Equal, Second.Other. Use the intended alias constructor + to make the record type clear. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_no_alias.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_no_alias.snap new file mode 100644 index 00000000..91b6b784 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_literal_no_alias.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN RECORD + + × I cannot find a visible record alias with exactly these fields: x, y. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Declare or import an alias for this record. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_type_outside_alias.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_type_outside_alias.snap new file mode 100644 index 00000000..fb469b54 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_record_type_outside_alias.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +RECORD TYPE + + × This record type needs a name: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: A record type is only allowed as the direct body of a type alias. Give this + record a named alias. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_alias.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_alias.snap new file mode 100644 index 00000000..e37030a9 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_alias.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +ALIAS PROBLEM + + × This type alias is recursive, forming an infinite type! + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: 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: + + type Loop 'a = + Loop + 'a + + Hint: This is kind of a subtle distinction. I suggested the naive fix, but I + recommend reading for ideas on + how to do better. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_decl.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_decl.snap new file mode 100644 index 00000000..1f000b87 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_decl.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +CYCLIC DEFINITION + + × The `name` value is defined directly in terms of itself, causing an infinite + │ loop. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: 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! + + 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! + + Hint: The root problem is often a typo in some variable name, but I recommend + reading for more detailed advice, + especially if you actually do need a recursive value. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_let.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_let.snap new file mode 100644 index 00000000..a65dd265 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_let.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +CYCLIC VALUE + + × I do not allow cyclic values in `let` expressions. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: The `name` value depends on itself through the following chain of definitions: + + ┌─────┐ + │ name + │ ↓ + │ other + └─────┘ + + Hint: The root problem is often a typo in some variable name, but I recommend + reading for more detailed advice, + especially if you actually do need a recursive value. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_superclass.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_superclass.snap new file mode 100644 index 00000000..4a3e9d8a --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_recursive_superclass.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TRAIT PROBLEM + + × These superclass declarations form a cycle: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: ┌─────┐ + │ First + │ ↓ + │ Second + └─────┘ + + Remove a superclass dependency to break the cycle. A trait cannot require itself + through its superclasses. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_reflexive_lift_overlap.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_reflexive_lift_overlap.snap new file mode 100644 index 00000000..98377c1f --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_reflexive_lift_overlap.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +OVERLAPPING IMPL + + × This impl overlaps the reflexive `Lift` rule: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Every Big type can lift to itself. Remove this impl or choose heads that do not + overlap that built-in rule. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_refutable_bind_pattern.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_refutable_bind_pattern.snap new file mode 100644 index 00000000..1b2981be --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_refutable_bind_pattern.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNSAFE PATTERN + + × This `do` binding has a pattern that can fail to match: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Use a variable or another irrefutable pattern here. Match individual variants in + a `case` expression so every possibility is handled. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_representation_mismatch.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_representation_mismatch.snap new file mode 100644 index 00000000..5d6d4418 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_representation_mismatch.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +REPRESENTATION MISMATCH + + × This position requires `Storable` in the type annotation: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: The type has `Term` representation, but `Storable` admits Big or Const types. + Change the type or the representation requirement. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_shadowing.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_shadowing.snap new file mode 100644 index 00000000..ed9797cd --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_shadowing.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +SHADOWING + + × The name `name` is first defined here: + ╭─[Main.nash:2:1] + 1 │ name = other + · ──┬─ + · ╰── first defined here + 2 │ other = name + · ──┬── + · ╰── defined AGAIN here + ╰──── + help: Think of a more helpful name for one of them and you should be all set! + + Note: Linters advise against shadowing, so Nash makes “best practices” the + default. Read for more details on this + choice. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_structural_eq_override.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_structural_eq_override.snap new file mode 100644 index 00000000..6fdad0fa --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_structural_eq_override.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +STRUCTURAL EQUALITY + + × This impl would replace structural equality: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Equality for this type is supplied by the compiler. Remove the explicit `Eq` + impl. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_superclass_bad_arg.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_superclass_bad_arg.snap new file mode 100644 index 00000000..72f8adbf --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_superclass_bad_arg.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TRAIT PROBLEM + + × The `Equal` superclass has an invalid argument: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Superclass arguments must be parameters declared by this trait. Replace this + argument with the intended trait parameter. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_trait_arity.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_trait_arity.snap new file mode 100644 index 00000000..ae899d77 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_trait_arity.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TOO MANY ARGS + + × The `name` trait needs 1 argument, but I see 2 instead: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Which is the extra one? Maybe some parentheses are missing? diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_messed_up_in_alias.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_messed_up_in_alias.snap new file mode 100644 index 00000000..4c6038c6 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_messed_up_in_alias.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +TYPE VARIABLE PROBLEMS + + × Type alias `Box` has some type variable problems. + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Type variable `b` appears in the definition, but I do not see it declared. + Likewise, type variable `a` is declared, but not used. + + My guess is that a definition like this will work better: + + type alias Box 'b = ... diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_unbound_in_union.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_unbound_in_union.snap new file mode 100644 index 00000000..5501f9ce --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_type_vars_unbound_in_union.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNBOUND TYPE VARIABLE + + × The `Box` type uses an unbound type variable `a` in its definition: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: You probably need to change the declaration to something like this: + + type Box 'a = ... + + Why? Well, imagine one `Box` where `a` 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. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unknown_method.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unknown_method.snap new file mode 100644 index 00000000..85d13b62 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unknown_method.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +UNKNOWN METHOD + + × The `Equal` trait has no `name` method: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: Check the method name against the trait declaration. Remove this definition or + rename it to the method you intended to implement. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unsupported.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unsupported.snap new file mode 100644 index 00000000..0dda6ae0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__coverage__variant_unsupported.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&report, &source, \"Main.nash\")" +--- +NOT SUPPORTED + + × I cannot canonicalize a yet: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + 2 │ other = name + ╰──── + help: This syntax is recognized, but its compiler implementation is not available yet. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__kind_mismatch.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__kind_mismatch.snap new file mode 100644 index 00000000..41e637c0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__kind_mismatch.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +KIND MISMATCH + + × I found a kind mismatch in the type annotation: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: This position needs kind `Type`, but the type has kind `Type -> Type`. Type + arguments must have matching kinds. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__missing_module_header.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__missing_module_header.snap new file mode 100644 index 00000000..dfe133d3 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__missing_module_header.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +MODULE NAME MISSING + + × I need the module name to be declared at the top of this file, like this: + │ + │ module Main exposing (..) + │ + │ Try adding that as the first line of your file! + help: Note: It is best to replace (..) with an explicit list of types and functions + you want to expose. When you know a value is only used within this module, you + can refactor without worrying about uses elsewhere. Limiting exposed values can + also speed up compilation because I can skip a bunch of work if I see that the + exposed API has not changed. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var.snap new file mode 100644 index 00000000..4a0dcdfb --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `name` variable: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: Is there an `import` or `exposing` missing up top? + + Hint: Read to see how `import` + declarations work in Nash. diff --git a/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var_with_suggestion.snap b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var_with_suggestion.snap new file mode 100644 index 00000000..4f7fdf92 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__canonicalize__tests__not_found_var_with_suggestion.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/canonicalize.rs +expression: "crate::render_plain(&to_report(&source, &error), &source, \"Main.nash\")" +--- +NAMING ERROR + + × I cannot find a `naem` variable: + ╭─[Main.nash:1:1] + 1 │ name = other + · ──── + ╰──── + help: These names seem close though: + + name + other + + Hint: Read to see how `import` + declarations work in Nash. From f86c2784b8387cf1a5db42f9e2a4da993235b051 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 08/12] feat(report): explain type and trait errors Signed-off-by: microproofs --- crates/nash-report/src/lib.rs | 1 + crates/nash-report/src/type_.rs | 594 ++++++++++++ crates/nash-report/src/type_/operators.rs | 438 +++++++++ crates/nash-report/src/type_/records.rs | 383 ++++++++ ...type___tests__ambiguous_record_access.snap | 17 + ..._report__type___tests__ambiguous_type.snap | 21 + ...___tests__annotation_variable_escapes.snap | 18 + ...report__type___tests__append_int_left.snap | 14 + ...t__type___tests__append_int_to_string.snap | 12 + ...ash_report__type___tests__append_left.snap | 12 + ...sh_report__type___tests__boolean_left.snap | 9 + ...h_report__type___tests__boolean_right.snap | 9 + ...sh_report__type___tests__compare_left.snap | 11 + ...__tests__contradictory_representation.snap | 20 + ...ash_report__type___tests__custom_left.snap | 13 + ...sh_report__type___tests__custom_right.snap | 17 + ...t__type___tests__destructure_mismatch.snap | 18 + ...h_report__type___tests__division_left.snap | 9 + ..._report__type___tests__division_right.snap | 9 + ..._report__type___tests__every_category.snap | 20 + ..._type___tests__every_pattern_category.snap | 13 + ...ts__example_one_big_little_annotation.snap | 23 + ...tests__expression_without_expectation.snap | 18 + ...__type___tests__field_mismatch_update.snap | 21 + ...eport__type___tests__hint_arity_fewer.snap | 5 + ...report__type___tests__hint_arity_more.snap | 5 + ...t__type___tests__hint_big_little_need.snap | 8 + ...port__type___tests__hint_double_rigid.snap | 9 + ...report__type___tests__hint_field_typo.snap | 8 + ...rt__type___tests__hint_missing_fields.snap | 5 + ...ash_report__type___tests__hint_option.snap | 6 + ...__type___tests__impl_resolution_limit.snap | 18 + ...h_report__type___tests__infinite_kind.snap | 17 + ...h_report__type___tests__infinite_type.snap | 18 + ...h_report__type___tests__kind_mismatch.snap | 26 + ...nash_report__type___tests__minus_left.snap | 11 + ...ash_report__type___tests__minus_right.snap | 11 + ...ype___tests__mismatch_annotation_body.snap | 18 + ...type___tests__mismatch_call_arg_first.snap | 18 + ...ts__mismatch_call_arg_second_has_hint.snap | 22 + ..._type___tests__mismatch_case_branches.snap | 22 + ...t__type___tests__mismatch_if_branches.snap | 22 + ...tests__mismatch_if_condition_not_bool.snap | 19 + ...__type___tests__mismatch_list_entries.snap | 22 + ...ort__type___tests__missing_constraint.snap | 19 + ...rt__type___tests__missing_field_alias.snap | 17 + ...sh_report__type___tests__missing_impl.snap | 25 + ...ocal_union_deriving_not_yet_available.snap | 15 + ..._storable_constraint_for_list_element.snap | 24 + ...h_report__type___tests__multiply_left.snap | 11 + ..._report__type___tests__multiply_right.snap | 11 + ...t__type___tests__not_a_record_pattern.snap | 17 + ...__type___tests__op_append_string_list.snap | 13 + ...rt__type___tests__op_compare_mismatch.snap | 21 + ...ype___tests__op_cons_element_mismatch.snap | 20 + ..._type___tests__op_cons_right_not_list.snap | 16 + ...t__type___tests__op_equality_mismatch.snap | 21 + ...pe___tests__op_pipe_argument_mismatch.snap | 18 + ...e___tests__op_pipe_right_not_function.snap | 14 + ...rt__type___tests__op_plus_left_string.snap | 16 + ...___tests__pattern_case_first_mismatch.snap | 20 + ...___tests__pattern_case_later_mismatch.snap | 21 + ...pe___tests__pattern_ctor_arg_mismatch.snap | 18 + ...ort__type___tests__pattern_list_entry.snap | 22 + ...port__type___tests__pattern_list_tail.snap | 18 + ...e___tests__pattern_typed_arg_mismatch.snap | 18 + ...___tests__pattern_without_expectation.snap | 18 + ...ort__type___tests__pipe_left_argument.snap | 13 + ..._type___tests__pipe_left_not_function.snap | 11 + ...nash_report__type___tests__plus_right.snap | 11 + ...__type___tests__polymorphic_recursion.snap | 20 + ...nash_report__type___tests__power_left.snap | 11 + ...ash_report__type___tests__power_right.snap | 11 + ...sts__record_access_missing_field_typo.snap | 18 + ...___tests__record_access_on_non_record.snap | 16 + ...__type___tests__record_field_mismatch.snap | 18 + ...pe___tests__record_update_change_type.snap | 21 + ...___tests__record_update_unknown_field.snap | 17 + ...ort__type___tests__rigid_var_mismatch.snap | 25 + ...ests__source_pipeline_annotation_body.snap | 19 + ..._tests__source_pipeline_call_argument.snap | 19 + ..._source_pipeline_call_second_argument.snap | 23 + ..._tests__source_pipeline_case_branches.snap | 25 + ...___tests__source_pipeline_if_branches.snap | 23 + ...__tests__source_pipeline_if_condition.snap | 20 + ..._tests__source_pipeline_infinite_type.snap | 19 + ...__tests__source_pipeline_list_entries.snap | 23 + ...s__source_pipeline_missing_constraint.snap | 20 + ...__tests__source_pipeline_missing_impl.snap | 22 + ...sts__source_pipeline_pattern_ctor_arg.snap | 19 + ...ts__source_pipeline_pattern_typed_arg.snap | 19 + ..._tests__source_pipeline_record_access.snap | 18 + ...s__source_pipeline_record_update_type.snap | 22 + ...pe___tests__too_many_args_on_function.snap | 12 + ..._type___tests__too_many_args_on_value.snap | 12 + ...nash_report__type___tests__typed_case.snap | 18 + .../nash_report__type___tests__typed_if.snap | 18 + ..._type___tests__unresolved_application.snap | 18 + ...__type___tests__unresolved_constraint.snap | 17 + ...port__type___tests__update_not_record.snap | 17 + crates/nash-report/src/type_/tests.rs | 885 ++++++++++++++++++ crates/nash-report/src/type_/traits.rs | 464 +++++++++ 102 files changed, 4367 insertions(+) create mode 100644 crates/nash-report/src/type_.rs create mode 100644 crates/nash-report/src/type_/operators.rs create mode 100644 crates/nash-report/src/type_/records.rs create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_record_access.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_type.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__annotation_variable_escapes.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_to_string.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__compare_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__contradictory_representation.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__destructure_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_category.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_pattern_category.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__example_one_big_little_annotation.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__expression_without_expectation.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__field_mismatch_update.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_fewer.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_more.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_big_little_need.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_double_rigid.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_field_typo.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_missing_fields.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_option.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__impl_resolution_limit.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_kind.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_type.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__kind_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_annotation_body.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_first.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_second_has_hint.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_case_branches.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_branches.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_condition_not_bool.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_list_entries.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_constraint.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_field_alias.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl_local_union_deriving_not_yet_available.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_storable_constraint_for_list_element.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__not_a_record_pattern.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_append_string_list.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_compare_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_element_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_right_not_list.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_equality_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_argument_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_right_not_function.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_plus_left_string.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_first_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_later_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_ctor_arg_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_entry.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_tail.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_typed_arg_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_without_expectation.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_argument.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_not_function.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__plus_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__polymorphic_recursion.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_left.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_right.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_missing_field_typo.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_on_non_record.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_field_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_change_type.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_unknown_field.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__rigid_var_mismatch.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_annotation_body.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_argument.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_second_argument.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_case_branches.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_branches.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_condition.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_infinite_type.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_list_entries.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_constraint.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_impl.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_ctor_arg.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_typed_arg.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_access.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_update_type.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_function.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_value.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_case.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_if.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_application.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_constraint.snap create mode 100644 crates/nash-report/src/type_/snapshots/nash_report__type___tests__update_not_record.snap create mode 100644 crates/nash-report/src/type_/tests.rs create mode 100644 crates/nash-report/src/type_/traits.rs diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index 793b4408..dc0c183d 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -14,6 +14,7 @@ pub mod localizer; pub mod render_type; pub mod suggest; pub mod syntax; +pub mod type_; pub mod type_diff; pub use localizer::Localizer; mod render; diff --git a/crates/nash-report/src/type_.rs b/crates/nash-report/src/type_.rs new file mode 100644 index 00000000..7eead51f --- /dev/null +++ b/crates/nash-report/src/type_.rs @@ -0,0 +1,594 @@ +//! Type error prose from Elm's `Reporting/Error/Type.hs`, adapted to Nash. + +mod operators; +mod records; +#[cfg(test)] +mod tests; +mod traits; + +use nash_constrain::error::{ + Category, Context, Error, Expected, MaybeName, PCategory, PContext, PExpected, SubContext, +}; +use nash_constrain::error_type::{ErrorType, iterated_dealias}; +use nash_region::Region; + +use crate::doc::{args, ordinal}; +use crate::localizer::Localizer; +use crate::render_type::Ctx; +use crate::type_diff::{self, Direction, Problem}; +use crate::{Doc, Report}; + +/// Elm's `toReport`, including Nash's trait, representation and record errors. +pub fn to_report(localizer: &Localizer, error: &Error<'_>) -> Report { + match error { + Error::BadExpr(region, category, actual, expected) => { + to_expr_report(localizer, *region, *category, actual, expected) + } + Error::BadPattern(region, category, actual, expected) => { + to_pattern_report(localizer, *region, *category, actual, expected) + } + Error::InfiniteType { + region, + name, + overall_type, + } => to_infinite_report(localizer, *region, name, overall_type), + Error::FieldMismatch { + region, + context, + field, + actual, + expected, + } => records::field_mismatch(localizer, *region, *context, field, actual, expected), + Error::MissingField { + region, + context, + field, + record, + available, + } => records::missing_field(localizer, *region, *context, field, record, available), + Error::NotARecord { + region, + context, + field, + record, + } => records::not_a_record(localizer, *region, *context, *field, record), + Error::UpdateNotRecord { region, record } => { + records::update_not_record(localizer, *region, record) + } + Error::AmbiguousRecordAccess { + region, + context, + field, + record, + } => records::ambiguous_access(localizer, *region, *context, *field, record), + Error::BadKind { + region, + name, + args, + reason, + } => traits::bad_kind(localizer, *region, name, args, reason), + Error::AmbiguousType { + region, + name, + variable, + predicates, + } => traits::ambiguous_type(localizer, *region, name, variable, predicates), + Error::ContradictoryRepresentation { + region, + name, + typ, + requirements, + } => traits::contradictory_representation(localizer, *region, name, typ, requirements), + Error::PolymorphicRecursion { + region, + name, + trait_, + args, + } => traits::polymorphic_recursion(localizer, *region, name, *trait_, args), + Error::UnresolvedConstraint { + region, + name, + trait_, + args, + } => traits::unresolved_constraint(localizer, *region, name, *trait_, args), + Error::UnresolvedApplication { + region, + name, + head, + args, + } => traits::unresolved_application(localizer, *region, name, head, args), + Error::MissingImpl { + region, + name, + trait_, + args, + available, + because, + } => traits::missing_impl(localizer, *region, name, *trait_, args, available, because), + Error::ImplResolutionLimit { + region, + name, + trait_, + } => traits::resolution_limit(localizer, *region, name, *trait_), + Error::MissingConstraint { + region, + name, + trait_, + args, + binder, + } => traits::missing_constraint(localizer, *region, name, *trait_, args, binder), + Error::AnnotationVariableEscapes { + region, + name, + variable, + } => traits::annotation_variable_escapes(localizer, *region, *name, variable), + } +} + +fn to_pattern_report( + localizer: &Localizer, + region: Region, + category: PCategory<'_>, + actual: &ErrorType<'_>, + expected: &PExpected<'_, &ErrorType<'_>>, +) -> Report { + let (surroundings, before, seeing, instead, details, expected) = match expected { + PExpected::NoExpectation(expected) => ( + region, + "This pattern is being used in an unexpected way:".into(), + "It is".into(), + "But it needs to match:".into(), + vec![], + *expected, + ), + PExpected::FromContext(surroundings, context, expected) => { + let (before, seeing, instead, details) = match context { + PContext::TypedArg(name, index) => ( + format!("The {} argument to `{name}` is weird.", ordinal(*index)), + "The argument is a pattern that matches".into(), + format!( + "But the type annotation on `{name}` says the {} argument should be:", + ordinal(*index) + ), + vec![], + ), + PContext::CaseMatch(0) => ( + "The 1st pattern in this `case` is causing a mismatch:".into(), + "The first pattern is trying to match".into(), + "But the expression between `case` and `of` is:".into(), + vec![Doc::reflow( + "These can never match! Is the pattern the problem? Or is it the expression?", + )], + ), + PContext::CaseMatch(index) => ( + format!( + "The {} pattern in this `case` does not match the previous ones.", + ordinal(*index) + ), + format!("The {} pattern is trying to match", ordinal(*index)), + "But all the previous patterns match:".into(), + vec![Doc::link( + "Note", + "A `case` expression can only handle one type of value, so you may want to use", + "custom-types", + "to handle “mixing” types.", + )], + ), + PContext::CtorArg(name, index) => ( + format!("The {} argument to `{name}` is weird.", ordinal(*index)), + "It is trying to match".into(), + format!("But `{name}` needs its {} argument to be:", ordinal(*index)), + vec![], + ), + PContext::ListEntry(index) => ( + format!( + "The {} pattern in this list does not match all the previous ones:", + ordinal(*index) + ), + format!("The {} pattern is trying to match", ordinal(*index)), + "But all the previous patterns in the list are:".into(), + vec![list_hint()], + ), + PContext::Tail => ( + "The pattern after (::) is causing issues.".into(), + "The pattern after (::) is trying to match".into(), + "But it needs to match lists like this:".into(), + vec![], + ), + }; + (*surroundings, before, seeing, instead, details, *expected) + } + }; + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(&before), + pattern_type_comparison( + localizer, + actual, + expected, + &add_pattern_category(&seeing, category), + &instead, + details, + ), + ) + .with_region(surroundings) +} + +fn pattern_type_comparison( + localizer: &Localizer, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, + seeing: &str, + instead: &str, + details: Vec, +) -> Doc { + let (actual, expected, problems) = type_diff::to_comparison(localizer, actual, expected); + Doc::stack( + [ + Doc::reflow(seeing), + Doc::indent(4, actual), + Doc::reflow(instead), + Doc::indent(4, expected), + ] + .into_iter() + .chain(problems_to_hint(&problems)) + .chain(details), + ) +} + +fn add_pattern_category(seeing: &str, category: PCategory<'_>) -> String { + format!( + "{seeing}{}", + match category { + PCategory::Record => " record values of type:".into(), + PCategory::Unit => " unit values:".into(), + PCategory::Tuple => " tuples of type:".into(), + PCategory::List => " lists of type:".into(), + PCategory::Ctor(name) => format!(" `{name}` values of type:"), + PCategory::Int => " integers:".into(), + PCategory::Bytes => " bytes:".into(), + PCategory::Str => " strings:".into(), + PCategory::Bool => " booleans:".into(), + } + ) +} + +fn type_comparison( + localizer: &Localizer, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, + seeing: &str, + instead: &str, + details: Vec, +) -> Doc { + let (actual, expected, problems) = type_diff::to_comparison(localizer, actual, expected); + Doc::stack( + [ + Doc::reflow(seeing), + Doc::indent(4, actual), + Doc::reflow(instead), + Doc::indent(4, expected), + ] + .into_iter() + .chain(details) + .chain(problems_to_hint(&problems)), + ) +} + +fn lone_type( + localizer: &Localizer, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, + seeing: Doc, + details: Vec, +) -> Doc { + let (actual, _, problems) = type_diff::to_comparison(localizer, actual, expected); + Doc::stack( + [seeing, Doc::indent(4, actual)] + .into_iter() + .chain(details) + .chain(problems_to_hint(&problems)), + ) +} + +fn add_category(seeing: &str, category: Category<'_>) -> String { + match category { + Category::Local(name) | Category::Foreign(name) => format!("This `{name}` value is a:"), + Category::Access(field) => format!("The value at .{field} is a:"), + Category::Accessor(field) => format!("This .{field} field access function has type:"), + Category::If => "This `if` expression produces:".into(), + Category::Case => "This `case` expression produces:".into(), + Category::List => format!("{seeing} a list of type:"), + Category::String => format!("{seeing} a string of type:"), + Category::Lambda => format!("{seeing} an anonymous function of type:"), + Category::Record => format!("{seeing} a record of type:"), + Category::Tuple => format!("{seeing} a tuple of type:"), + Category::Unit => format!("{seeing} a unit value:"), + Category::CallResult(MaybeName::FuncName(name) | MaybeName::CtorName(name)) => { + format!("This `{name}` call produces:") + } + Category::CallResult(MaybeName::NoName | MaybeName::OpName(_)) => format!("{seeing}:"), + } +} + +fn list_hint() -> Doc { + Doc::link( + "Hint", + "Everything in a list must be the same type of value. This way, we never run into unexpected values partway through a List.map, List.foldl, etc. Read", + "custom-types", + "to learn how to “mix” types.", + ) +} + +fn to_expr_report( + localizer: &Localizer, + region: Region, + category: Category<'_>, + actual: &ErrorType<'_>, + expected: &Expected<'_, &ErrorType<'_>>, +) -> Report { + match expected { + Expected::NoExpectation(expected) => Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow("This expression is being used in an unexpected way:"), + type_comparison( + localizer, + actual, + expected, + &add_category("It is", category), + "But you are trying to use it as:", + vec![], + ), + ), + Expected::FromAnnotation(name, _, context, expected) => { + let (thing, seeing) = match context { + SubContext::TypedIfBranch(index) => ( + format!("{} branch of this `if` expression:", ordinal(*index)), + format!("The {} branch is", ordinal(*index)), + ), + SubContext::TypedCaseBranch(index) => ( + format!("{} branch of this `case` expression:", ordinal(*index)), + format!("The {} branch is", ordinal(*index)), + ), + SubContext::TypedBody => ( + format!("body of the `{name}` definition:"), + "The body is".into(), + ), + }; + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(&format!("Something is off with the {thing}")), + type_comparison( + localizer, + actual, + expected, + &add_category(&seeing, category), + &format!("But the type annotation on `{name}` says it should be:"), + vec![], + ), + ) + } + Expected::FromContext(surroundings, context, expected) => { + let mismatch = |problem: &str, seeing: &str, instead: &str, details: Vec| { + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(problem), + type_comparison( + localizer, + actual, + expected, + &add_category(seeing, category), + instead, + details, + ), + ) + .with_region(*surroundings) + }; + match context { + Context::ListEntry(index) => mismatch(&format!("The {} element of this list does not match all the previous elements:", ordinal(*index)), &format!("The {} element is", ordinal(*index)), "But all the previous elements in the list are:", vec![list_hint()]), + Context::IfCondition => Report::snippet("TYPE MISMATCH", region, None, Doc::reflow("This `if` condition does not evaluate to a boolean value, `True` or `False`."), lone_type(localizer, actual, expected, Doc::reflow(&add_category("It is", category)), vec![Doc::reflow("But I need this `if` condition to be a `bool` value.")])).with_region(*surroundings), + Context::IfBranch(index) | Context::CaseBranch(index) => { + let article = if matches!(context, Context::IfBranch(_)) { "an" } else { "a" }; + let keyword = if matches!(context, Context::IfBranch(_)) { "if" } else { "case" }; + mismatch(&format!("The {} branch of this `{keyword}` does not match all the previous branches:", ordinal(*index)), &format!("The {} branch is", ordinal(*index)), "But all the previous branches result in:", vec![Doc::link("Hint", &format!("All branches in {article} `{keyword}` must produce the same type of values. This way, no matter which branch we take, the result is always a consistent shape. Read"), "custom-types", "to learn how to “mix” types.")]) + } + Context::CallArg(name, index) => { + let function = match name { MaybeName::NoName => "this function".into(), MaybeName::FuncName(name) | MaybeName::CtorName(name) => format!("`{name}`"), MaybeName::OpName(op) => format!("({op})") }; + mismatch(&format!("The {} argument to {function} is not what I expect:", ordinal(*index)), "This argument is", &format!("But {function} needs the {} argument to be:", ordinal(*index)), if *index == 0 { vec![] } else { vec![Doc::to_simple_hint("I always figure out the argument types from left to right. If an argument is acceptable, I assume it is “correct” and move on. So the problem may actually be in one of the previous arguments!")] }) + } + Context::CallArity(name, given) => { + let count = count_args(actual); + let thing = match (name, count) { + (MaybeName::NoName, 0) => "This value".into(), + (MaybeName::NoName, _) => "This function".into(), + (MaybeName::FuncName(name) | MaybeName::CtorName(name), 0) => format!("The `{name}` value"), + (MaybeName::FuncName(name), _) => format!("The `{name}` function"), + (MaybeName::CtorName(name), _) => format!("The `{name}` constructor"), + (MaybeName::OpName(op), _) => format!("The ({op}) operator"), + }; + let problem = if count == 0 { format!("{thing} is not a function, but it was given {}.", args(*given)) } else { format!("{thing} expects {}, but it got {given} instead.", args(count)) }; + Report::snippet("TOO MANY ARGS", region, None, Doc::reflow(&problem), Doc::reflow("Are there any missing commas? Or missing parentheses?")).with_region(*surroundings) + } + Context::OpLeft(op) => { + let (before, after) = operators::op_left_to_docs(localizer, category, op, actual, expected); + Report::snippet("TYPE MISMATCH", region, None, before, after).with_region(*surroundings) + } + Context::OpRight(op) => { + let docs = operators::op_right_to_docs(localizer, category, op, actual, expected); + let (before, after, highlight) = match docs { + operators::RightDocs::EmphBoth(before, after) => (before, after, None), + operators::RightDocs::EmphRight(before, after) => (before, after, Some(region)), + }; + let mut report = Report::snippet("TYPE MISMATCH", *surroundings, highlight, before, after); + report.region = region; + report + } + Context::RecordAccess { record_region, maybe_name, field_region, field } => records::access(localizer, region, *surroundings, *record_region, *maybe_name, *field_region, field, actual, expected), + Context::RecordUpdateKeys(name, fields) => records::update(localizer, region, *surroundings, name, fields, actual, expected), + Context::RecordUpdateValue(field) => mismatch(&format!("I cannot update the `{field}` field like this:"), &format!("You are trying to update `{field}` to be"), "But it should be:", vec![Doc::to_simple_note("The record update syntax does not allow you to change the type of fields. You can achieve that with record constructors or the record literal syntax.")]), + Context::RecordField(name, field) => mismatch(&format!("The `{field}` field of `{name}` is not what I expect:"), "This field is", "But the record declaration says it should be:", vec![]), + Context::Destructure => { + let mut report = mismatch("This definition is causing issues:", "You are defining", "But then trying to destructure it as:", vec![]); + if let crate::Snippet::Region { highlight, .. } = &mut report.snippet { *highlight = None; } + report + } + } + } + } +} + +fn count_args(tipe: &ErrorType<'_>) -> usize { + match iterated_dealias(tipe) { + ErrorType::Lambda(_, _, rest) => 1 + rest.len(), + _ => 0, + } +} + +fn to_infinite_report( + localizer: &Localizer, + region: Region, + name: &str, + overall_type: &ErrorType<'_>, +) -> Report { + Report::snippet( + "INFINITE TYPE", + region, + None, + Doc::reflow(&format!( + "I am inferring a weird self-referential type for {name}:" + )), + Doc::stack([ + Doc::reflow( + "Here is my best effort at writing down the type. You will see ∞ for parts of the type that repeat something already printed out infinitely.", + ), + Doc::indent( + 4, + type_diff::to_doc(localizer, Ctx::None, overall_type).dullyellow(), + ), + Doc::reflow_link( + "Staring at this type is usually not so helpful, so I recommend reading the hints at", + "infinite-type", + "to get unstuck!", + ), + ]), + ) +} + +fn problems_to_hint(problems: &[Problem<'_>]) -> Vec { + problems.first().map_or_else(Vec::new, problem_to_hint) +} + +fn problem_to_hint(problem: &Problem<'_>) -> Vec { + match problem { + Problem::AnythingToBool => vec![Doc::to_simple_hint( + "Nash does not have “truthiness” such that ints and strings and lists are automatically converted to booleans. Do that conversion explicitly!", + )], + Problem::AnythingFromOption => vec![Doc::to_fancy_hint( + [Doc::text("Use"), Doc::text("Option.withDefault").green()] + .into_iter() + .chain("to handle possible errors. Longer term, it is usually better to write out the full `case` though!".split_whitespace().map(Doc::text)), + )], + Problem::ArityMismatch(actual, expected) => { + vec![Doc::to_simple_hint(&if actual < expected { + format!( + "It looks like it takes too few arguments. I was expecting {} more.", + expected - actual + ) + } else { + format!( + "It looks like it takes too many arguments. I see {} extra.", + actual - expected + ) + })] + } + Problem::BadRigidVar(name, tipe) => match tipe { + ErrorType::Lambda(..) => bad_rigid_var(name, "a function"), + ErrorType::Infinite | ErrorType::Error | ErrorType::FlexVar(_) => vec![], + ErrorType::RigidVar(other) => bad_double_rigid(name, other), + ErrorType::Type { name: other, .. } | ErrorType::Alias { name: other, .. } => { + bad_rigid_var(name, &format!("a value of type `{other}`")) + } + ErrorType::Record { .. } => bad_rigid_var(name, "a record"), + ErrorType::Tuple(..) => bad_rigid_var(name, "a tuple"), + ErrorType::VarApp(..) => bad_rigid_var(name, "an applied type constructor"), + }, + Problem::FieldsMissing(fields) => { + if fields.is_empty() { + return vec![]; + } + let names = Doc::comma_sep( + "and", + |doc| doc, + fields.iter().map(|name| Doc::text(*name).green()).collect(), + ); + vec![Doc::to_fancy_hint( + [Doc::text(if fields.len() == 1 { + "Looks like the" + } else { + "Looks like fields" + })] + .into_iter() + .chain(names) + .chain([Doc::text(if fields.len() == 1 { + "field is missing." + } else { + "are missing." + })]), + )] + } + Problem::FieldTypo(typo, possibilities) => { + let ranked = crate::suggest::sort(typo, |s| (*s).to_string(), possibilities.clone()); + match ranked.first() { + None => vec![], + Some(nearest) => vec![ + Doc::to_fancy_hint([ + Doc::text("Seems like a record field typo. Maybe"), + Doc::text(*typo).dullyellow(), + Doc::text("should be"), + Doc::cat([Doc::text(*nearest).green(), Doc::text("?")]), + ]), + Doc::to_simple_hint( + "Can more type annotations be added? Type annotations always help me give more specific messages, and I think they could help a lot in this case!", + ), + ], + } + } + Problem::BigLittle { + big, + little, + direction, + } => vec![Doc::to_simple_hint(&format!( + "`{big}` is the Big (Data) type and `{little}` is the little type. They never convert implicitly. Where an appropriate `Lift` impl is available, use `lower` to go from `{big}` to `{little}`, or `lift` to go the other way.{}", + match direction { + Direction::Have => "", + Direction::Need => + " If this value comes from a validator argument, decode it with `fromData` first.", + } + ))], + } +} + +fn bad_rigid_var(name: &str, thing: &str) -> Vec { + vec![ + Doc::to_simple_hint(&format!( + "Your type annotation uses type variable `'{name}` which means ANY type of value can flow through, but your code is saying it specifically wants {thing}. Maybe change your type annotation to be more specific? Maybe change the code to be more general?" + )), + Doc::reflow_link("Read", "type-annotations", "for more advice!"), + ] +} +fn bad_double_rigid(x: &str, y: &str) -> Vec { + vec![ + Doc::to_simple_hint(&format!( + "Your type annotation uses `'{x}` and `'{y}` as separate type variables. Your code seems to be saying they are the same though. Maybe they should be the same in your type annotation? Maybe your code uses them in a weird way?" + )), + Doc::reflow_link("Read", "type-annotations", "for more advice!"), + ] +} diff --git a/crates/nash-report/src/type_/operators.rs b/crates/nash-report/src/type_/operators.rs new file mode 100644 index 00000000..8c6ff9b4 --- /dev/null +++ b/crates/nash-report/src/type_/operators.rs @@ -0,0 +1,438 @@ +use super::*; + +type Docs = (Doc, Doc); +pub(super) enum RightDocs { + EmphBoth(Doc, Doc), + EmphRight(Doc, Doc), +} +fn right((before, after): Docs) -> RightDocs { + RightDocs::EmphRight(before, after) +} + +pub(super) fn op_left_to_docs( + l: &Localizer, + category: Category<'_>, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Docs { + match op { + "+" => bad_math(l, category, "Addition", "left", op, actual, expected), + "-" => bad_math(l, category, "Subtraction", "left", op, actual, expected), + "*" => bad_math(l, category, "Multiplication", "left", op, actual, expected), + "^" => bad_math(l, category, "Exponentiation", "left", op, actual, expected), + "/" => bad_div(l, "left", actual, expected), + "&&" | "||" => bad_bool(l, op, "left", actual, expected), + "<" | ">" | "<=" | ">=" => bad_comp_left(l, category, op, actual, expected), + "++" => bad_append_left(l, category, actual, expected), + "<|" => ( + Doc::reflow( + "The left side of (<|) needs to be a function so I can pipe arguments to it!", + ), + lone_type( + l, + actual, + expected, + Doc::reflow(&add_category("I am seeing", category)), + vec![Doc::reflow( + "This needs to be some kind of function though!", + )], + ), + ), + _ => ( + Doc::reflow(&format!("The left argument of ({op}) is causing problems:")), + type_comparison( + l, + actual, + expected, + &add_category("The left argument is", category), + &format!("But ({op}) needs the left argument to be:"), + vec![], + ), + ), + } +} + +pub(super) fn op_right_to_docs( + l: &Localizer, + category: Category<'_>, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> RightDocs { + match op { + "+" => right(bad_math( + l, category, "Addition", "right", op, actual, expected, + )), + "-" => right(bad_math( + l, + category, + "Subtraction", + "right", + op, + actual, + expected, + )), + "*" => right(bad_math( + l, + category, + "Multiplication", + "right", + op, + actual, + expected, + )), + "^" => right(bad_math( + l, + category, + "Exponentiation", + "right", + op, + actual, + expected, + )), + "/" => right(bad_div(l, "right", actual, expected)), + "&&" | "||" => right(bad_bool(l, op, "right", actual, expected)), + "<" | ">" | "<=" | ">=" => bad_comp_right(l, op, actual, expected), + "==" | "/=" => bad_equality(l, op, actual, expected), + "::" => bad_cons_right(l, category, actual, expected), + "++" => bad_append_right(l, category, actual, expected), + "<|" => right(( + Doc::reflow("I cannot send this through the (<|) pipe:"), + type_comparison( + l, + actual, + expected, + "The argument is:", + "But (<|) is piping it to a function that expects:", + vec![], + ), + )), + "|>" => match (iterated_dealias(actual), iterated_dealias(expected)) { + (ErrorType::Lambda(expected_arg, _, _), ErrorType::Lambda(arg, _, _)) => right(( + Doc::reflow("This function cannot handle the argument sent through the (|>) pipe:"), + type_comparison( + l, + arg, + expected_arg, + "The argument is:", + "But (|>) is piping it to a function that expects:", + vec![], + ), + )), + _ => right(( + Doc::reflow( + "The right side of (|>) needs to be a function so I can pipe arguments to it!", + ), + lone_type( + l, + actual, + expected, + Doc::reflow(&add_category( + "But instead of a function, I am seeing", + category, + )), + vec![], + ), + )), + }, + _ => bad_op_right_fallback(l, category, op, actual, expected), + } +} +fn bad_op_right_fallback( + l: &Localizer, + category: Category<'_>, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> RightDocs { + right(( + Doc::reflow(&format!( + "The right argument of ({op}) is causing problems." + )), + type_comparison( + l, + actual, + expected, + &add_category("The right argument is", category), + &format!("But ({op}) needs the right argument to be:"), + vec![Doc::to_simple_hint(&format!( + "With operators like ({op}) I always check the left side first. If it seems fine, I assume it is correct and check the right side. So the problem may be in how the left and right arguments interact!" + ))], + ), + )) +} +fn is_int(t: &ErrorType<'_>) -> bool { + matches!(iterated_dealias(t), ErrorType::Type { home, name: "int", args: [] } if *home == nash_ast::primitives::builtin_home()) +} +fn is_string(t: &ErrorType<'_>) -> bool { + matches!(iterated_dealias(t), ErrorType::Type { home, name: "string", args: [] } if *home == nash_ast::primitives::builtin_home()) +} +fn is_list(t: &ErrorType<'_>) -> bool { + list_element(t).is_some() +} +fn list_element<'a>(t: &'a ErrorType<'a>) -> Option<&'a ErrorType<'a>> { + match iterated_dealias(t) { + ErrorType::Type { + home, + name: "list", + args: [element], + } if *home == nash_ast::primitives::builtin_home() => Some(element), + _ => None, + } +} +fn bad_cons_right( + l: &Localizer, + category: Category<'_>, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> RightDocs { + match (list_element(actual), list_element(expected)) { + (Some(actual_element), Some(expected_element)) => RightDocs::EmphBoth( + Doc::reflow("I am having trouble with this (::) operator:"), + type_comparison( + l, + expected_element, + actual_element, + "The left side of (::) is:", + "But you are trying to put that into a list filled with:", + vec![if is_list(expected_element) { + Doc::to_simple_hint( + "Are you trying to append two lists? The (++) operator appends lists, whereas the (::) operator is only for adding ONE element to a list.", + ) + } else { + Doc::reflow("Lists need ALL elements to be the same type though.") + }], + ), + ), + (Some(_), None) => bad_op_right_fallback(l, category, "::", actual, expected), + (None, _) => right(( + Doc::reflow("The (::) operator can only add elements onto lists."), + lone_type( + l, + actual, + expected, + Doc::reflow(&add_category("The right side is", category)), + vec![Doc::reflow("But (::) needs a `list` on the right.")], + ), + )), + } +} +#[derive(Clone, Copy)] +enum AppendType { + Number, + String, + List, + Other, +} +fn to_append_type(t: &ErrorType<'_>) -> AppendType { + if is_int(t) { + AppendType::Number + } else if is_string(t) { + AppendType::String + } else if is_list(t) { + AppendType::List + } else { + AppendType::Other + } +} +fn bad_append_left( + l: &Localizer, + category: Category<'_>, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Docs { + match to_append_type(actual) { + AppendType::Number => ( + Doc::reflow( + "The (++) operator can append list and string values, but not int values like this:", + ), + Doc::to_fancy_hint( + [Doc::text("Try"), Doc::text("using"), Doc::text("Int.toString").green()] + .into_iter() + .chain("to turn it into a string? Or put it in [] to make it a list? Or switch to the (::) operator?".split_whitespace().map(Doc::text)), + ), + ), + AppendType::String | AppendType::List | AppendType::Other => ( + Doc::reflow("The (++) operator cannot append this type of value:"), + lone_type( + l, + actual, + expected, + Doc::reflow(&add_category("I am seeing", category)), + vec![Doc::reflow( + "But the (++) operator is only for appending list and string values. Maybe put this value in [] to make it a list?", + )], + ), + ), + } +} +fn bad_append_right( + l: &Localizer, + category: Category<'_>, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> RightDocs { + match (to_append_type(expected), to_append_type(actual)) { + (AppendType::String, AppendType::Number) => right(( + Doc::reflow("I thought I was appending string values here, not int values like this:"), + Doc::to_fancy_hint([ + Doc::text("Try using"), + Doc::text("Int.toString").green(), + Doc::text("to turn it into a string?"), + ]), + )), + (AppendType::List, AppendType::Number) => right(( + Doc::reflow("I thought I was appending list values here, not int values like this:"), + Doc::reflow("Try putting it in [] to make it a list?"), + )), + (AppendType::String, AppendType::List) => RightDocs::EmphBoth( + Doc::reflow("The (++) operator needs the same type of value on both sides:"), + Doc::reflow( + "I see a string on the left and a list on the right. Which should it be? Does the string need [] around it to become a list?", + ), + ), + (AppendType::List, AppendType::String) => RightDocs::EmphBoth( + Doc::reflow("The (++) operator needs the same type of value on both sides:"), + Doc::reflow( + "I see a list on the left and a string on the right. Which should it be? Does the string need [] around it to become a list?", + ), + ), + _ => RightDocs::EmphBoth( + Doc::reflow("The (++) operator cannot append these two values:"), + type_comparison( + l, + expected, + actual, + "I already figured out that the left side of (++) is:", + &add_category("But this clashes with the right side, which is", category), + vec![], + ), + ), + } +} +fn bad_math( + l: &Localizer, + category: Category<'_>, + operation: &str, + direction: &str, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Docs { + ( + Doc::reflow(&format!("{operation} does not work with this value:")), + lone_type( + l, + actual, + expected, + Doc::reflow(&add_category( + &format!("The {direction} side of ({op}) is"), + category, + )), + vec![Doc::reflow(&format!( + "But ({op}) only works with values whose type implements `Num`." + ))], + ), + ) +} +fn bad_div( + l: &Localizer, + direction: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Docs { + ( + Doc::reflow("The (/) operator is integer division; both sides must be `int`."), + lone_type( + l, + actual, + expected, + Doc::reflow(&format!("But the {direction} side is:")), + vec![], + ), + ) +} +fn bad_bool( + l: &Localizer, + op: &str, + direction: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Docs { + ( + Doc::reflow("I am struggling with this boolean operation:"), + lone_type( + l, + actual, + expected, + Doc::reflow(&format!( + "Both sides of ({op}) must be `bool` values, but the {direction} side is:" + )), + vec![], + ), + ) +} +fn bad_comp_left( + l: &Localizer, + category: Category<'_>, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Docs { + ( + Doc::reflow("I cannot do a comparison with this value:"), + lone_type( + l, + actual, + expected, + Doc::reflow(&add_category( + &format!("The left side of ({op}) is"), + category, + )), + vec![Doc::reflow(&format!( + "But ({op}) only works with values whose type implements `Ord`." + ))], + ), + ) +} +fn bad_comp_right( + l: &Localizer, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> RightDocs { + RightDocs::EmphBoth( + Doc::reflow(&format!("I need both sides of ({op}) to be the same type:")), + type_comparison( + l, + expected, + actual, + &format!("The left side of ({op}) is:"), + "But the right side is:", + vec![Doc::reflow(&format!( + "I cannot compare different types though! Which side of ({op}) is the problem? The type must implement `Ord`." + ))], + ), + ) +} +fn bad_equality( + l: &Localizer, + op: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> RightDocs { + RightDocs::EmphBoth( + Doc::reflow(&format!("I need both sides of ({op}) to be the same type:")), + type_comparison( + l, + expected, + actual, + &format!("The left side of ({op}) is:"), + "But the right side is:", + vec![Doc::reflow( + "Different types can never be equal though! Which side is messed up? The type must implement `Eq`.", + )], + ), + ) +} diff --git a/crates/nash-report/src/type_/records.rs b/crates/nash-report/src/type_/records.rs new file mode 100644 index 00000000..75b78e4b --- /dev/null +++ b/crates/nash-report/src/type_/records.rs @@ -0,0 +1,383 @@ +use super::*; +use nash_constrain::type_::FieldContext; + +fn highlighted( + primary: Region, + surroundings: Region, + highlight: Region, + before: Doc, + after: Doc, +) -> Report { + let mut report = Report::snippet( + "TYPE MISMATCH", + surroundings, + Some(highlight), + before, + after, + ); + report.region = primary; + report +} + +// Keep the regions explicit: the diagnostic points to the expression, while +// Elm highlights the missing field or the non-record value inside its context. +#[allow(clippy::too_many_arguments)] +pub(super) fn access( + l: &Localizer, + primary: Region, + surroundings: Region, + record_region: Region, + name: Option<&str>, + field_region: Region, + field: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Report { + match iterated_dealias(actual) { + ErrorType::Record { fields } => { + let (after, suggestions) = nearby(l, name, field, fields); + highlighted( + primary, + surroundings, + field_region, + Doc::reflow(&format!( + "This {}record does not have a `{field}` field:", + name.map_or_else(String::new, |name| format!("`{name}` ")) + )), + after, + ) + .with_suggestions(suggestions) + } + _ => highlighted( + primary, + surroundings, + record_region, + Doc::reflow("This is not a record, so it has no fields to access!"), + lone_type( + l, + actual, + expected, + Doc::reflow("It is:"), + vec![Doc::reflow(&format!( + "But I need a record with a `{field}` field!" + ))], + ), + ), + } +} + +pub(super) fn update( + l: &Localizer, + primary: Region, + surroundings: Region, + name: &str, + updates: &[nash_ast::FieldUpdate<'_>], + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Report { + match iterated_dealias(actual) { + ErrorType::Record { fields } => { + let missing = updates + .iter() + .filter(|update| !fields.iter().any(|(name, _)| *name == update.field.value)) + .min_by_key(|update| update.field.value); + match missing { + Some(update) => { + let field = update.field.value; + let (after, suggestions) = nearby(l, Some(name), field, fields); + highlighted( + primary, + surroundings, + update.field.region, + Doc::reflow(&format!( + "The `{name}` record does not have a `{field}` field:" + )), + after, + ) + .with_suggestions(suggestions) + } + None => { + let mut report = Report::snippet( + "TYPE MISMATCH", + surroundings, + None, + Doc::reflow("Something is off with this record update:"), + type_comparison( + l, + actual, + expected, + &format!("The `{name}` record is:"), + "But this update needs it to be compatible with:", + vec![Doc::to_simple_hint( + "Add a type annotation to the record and check the types of the fields being updated.", + )], + ), + ); + report.region = primary; + report + } + } + } + _ => highlighted( + primary, + surroundings, + primary, + Doc::reflow("This is not a record, so it has no fields to update!"), + lone_type( + l, + actual, + expected, + Doc::reflow("It is:"), + vec![Doc::reflow("But I need a record!")], + ), + ), + } +} + +fn nearby( + l: &Localizer, + name: Option<&str>, + field: &str, + fields: &[(&str, &ErrorType<'_>)], +) -> (Doc, Vec) { + let sorted = crate::suggest::sort(field, |(name, _)| (*name).into(), fields.to_vec()); + match sorted.split_first() { + None => ( + Doc::reflow(&format!( + "In fact, {} is a record with NO fields!", + name.map_or_else(|| "it".into(), |name| format!("`{name}`")) + )), + vec![], + ), + Some((first, rest)) => ( + Doc::stack([ + Doc::reflow(&format!( + "This is usually a typo. Here are the {}fields that are most similar:", + name.map_or_else(String::new, |name| format!("`{name}` ")) + )), + to_nearby_record(l, *first, rest), + Doc::fill_sep([ + Doc::text("So maybe"), + Doc::text(field).dullyellow(), + Doc::text("should be"), + Doc::cat([Doc::text(first.0).green(), Doc::text("?")]), + ]), + ]), + sorted + .iter() + .take(4) + .map(|(field, _)| (*field).into()) + .collect(), + ), + } +} +fn to_nearby_record( + l: &Localizer, + first: (&str, &ErrorType<'_>), + rest: &[(&str, &ErrorType<'_>)], +) -> Doc { + Doc::indent( + 4, + if rest.len() <= 3 { + crate::render_type::vrecord( + std::iter::once(first) + .chain(rest.iter().copied()) + .map(|field| field_to_docs(l, field)) + .collect(), + None, + ) + } else { + crate::render_type::vrecord_snippet( + field_to_docs(l, first), + rest.iter() + .take(3) + .map(|field| field_to_docs(l, *field)) + .collect(), + ) + }, + ) +} +fn field_to_docs(l: &Localizer, (field, tipe): (&str, &ErrorType<'_>)) -> (Doc, Doc) { + (Doc::text(field), type_diff::to_doc(l, Ctx::None, tipe)) +} + +fn field_context(context: FieldContext<'_>, field: Option<&str>) -> String { + let field = field.map_or_else( + || "these fields".into(), + |field| format!("the `{field}` field"), + ); + match context { + FieldContext::Access { + maybe_name: Some(name), + .. + } => format!("accessing {field} of `{name}`"), + FieldContext::Access { + maybe_name: None, .. + } => format!("accessing {field} of this value"), + FieldContext::Accessor => format!("using the field access function for {field}"), + FieldContext::Update { record } => format!("updating {field} of `{record}`"), + FieldContext::Pattern => format!("matching {field} in this record pattern"), + } +} + +pub(super) fn field_mismatch( + l: &Localizer, + region: Region, + context: FieldContext<'_>, + field: &str, + actual: &ErrorType<'_>, + expected: &ErrorType<'_>, +) -> Report { + if matches!(context, FieldContext::Update { .. }) { + return Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(&format!("I cannot update the `{field}` field like this:")), + type_comparison( + l, + expected, + actual, + &format!("You are trying to update `{field}` to be:"), + "But it should be:", + vec![Doc::to_simple_note( + "The record update syntax does not allow you to change the type of fields. You can achieve that with record constructors or the record literal syntax.", + )], + ), + ); + } + let before = format!( + "Something is off when {}:", + field_context(context, Some(field)) + ); + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(&before), + type_comparison( + l, + actual, + expected, + &format!("The `{field}` field is:"), + "But it needs to be:", + vec![], + ), + ) +} + +pub(super) fn missing_field( + l: &Localizer, + region: Region, + context: FieldContext<'_>, + field: &str, + record: &ErrorType<'_>, + available: &[&str], +) -> Report { + let name = match context { + FieldContext::Access { maybe_name, .. } => maybe_name, + FieldContext::Update { record } => Some(record), + FieldContext::Accessor | FieldContext::Pattern => None, + }; + let (after, suggestions) = match iterated_dealias(record) { + ErrorType::Record { fields } => nearby(l, name, field, fields), + _ => { + let sorted = + crate::suggest::sort(field, |name| (*name).to_string(), available.to_vec()); + let mut docs = vec![ + Doc::reflow("This value has type:"), + Doc::indent(4, type_diff::to_doc(l, Ctx::None, record)), + ]; + if sorted.is_empty() { + docs.push(Doc::reflow("It has no available record fields.")); + } else { + docs.push(Doc::reflow(&format!( + "The available fields most similar to `{field}` are: {}.", + sorted + .iter() + .take(4) + .map(|name| format!("`{name}`")) + .collect::>() + .join(", ") + ))); + } + ( + Doc::stack(docs), + sorted.into_iter().take(4).map(String::from).collect(), + ) + } + }; + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(&format!( + "I cannot find `{field}` when {}:", + field_context(context, Some(field)) + )), + after, + ) + .with_suggestions(suggestions) +} + +pub(super) fn not_a_record( + l: &Localizer, + region: Region, + context: FieldContext<'_>, + field: Option<&str>, + record: &ErrorType<'_>, +) -> Report { + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow(&format!( + "This value is not a record, so I cannot use it when {}:", + field_context(context, field) + )), + Doc::stack([ + Doc::reflow("It has type:"), + Doc::indent(4, type_diff::to_doc(l, Ctx::None, record)), + Doc::reflow("But I need a value with record fields!"), + ]), + ) +} +pub(super) fn update_not_record(l: &Localizer, region: Region, record: &ErrorType<'_>) -> Report { + Report::snippet( + "TYPE MISMATCH", + region, + None, + Doc::reflow("This value does not support record updates:"), + Doc::stack([ + Doc::reflow("It has type:"), + Doc::indent(4, type_diff::to_doc(l, Ctx::None, record)), + Doc::reflow( + "I need a record alias for this update. Rebuild this value with its constructor instead.", + ), + ]), + ) +} +pub(super) fn ambiguous_access( + l: &Localizer, + region: Region, + context: FieldContext<'_>, + field: Option<&str>, + record: &ErrorType<'_>, +) -> Report { + Report::snippet( + "AMBIGUOUS RECORD ACCESS", + region, + None, + Doc::reflow(&format!( + "I cannot determine the record type when {}:", + field_context(context, field) + )), + Doc::stack([ + Doc::reflow("The type is still:"), + Doc::indent(4, type_diff::to_doc(l, Ctx::None, record)), + Doc::to_simple_hint( + "Add a type annotation to tell me which record type provides these fields.", + ), + ]), + ) +} diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_record_access.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_record_access.snap new file mode 100644 index 00000000..20f9c73b --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_record_access.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::AmbiguousRecordAccess\n{\n region: region(), context: nash_constrain::type_::FieldContext::Accessor,\n field: Some(\"name\"), record: &ErrorType::FlexVar(\"a\")\n})" +--- +AMBIGUOUS RECORD ACCESS + + × I cannot determine the record type when using the field access function for the + │ `name` field: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The type is still: + + 'a + + Hint: Add a type annotation to tell me which record type provides these fields. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_type.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_type.snap new file mode 100644 index 00000000..70b86fee --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__ambiguous_type.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::AmbiguousType\n{\n region: region(), name: \"value\", variable: &ErrorType::FlexVar(\"a\"),\n predicates:\n &[nash_constrain::error::AmbiguousPredicate\n {\n trait_: nash_ast::primitives::num_trait(), args:\n &[&ErrorType::FlexVar(\"a\")]\n }]\n})" +--- +AMBIGUOUS TYPE + + × I cannot determine the type needed by `value`: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This type variable is still unresolved: + + 'a + + It must satisfy these constraints: + + Num 'a + + Hint: Add a type annotation that fixes this type. Each constraint needs enough + information to select an impl. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__annotation_variable_escapes.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__annotation_variable_escapes.snap new file mode 100644 index 00000000..138b7a7b --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__annotation_variable_escapes.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::AnnotationVariableEscapes\n{ region: region(), name: Some(\"f\"), variable: &ErrorType::RigidVar(\"a\") })" +--- +ANNOTATION VARIABLE ESCAPES + + × This annotation for `f` quantifies a type variable fixed by an enclosing scope: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: 'a + + The variable cannot stand for every type here because the surrounding definition + has already fixed it. + + Hint: Use the enclosing type variable consistently, or change the annotation so + that it does not promise a fresh independent type. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_left.snap new file mode 100644 index 00000000..3f0f7572 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_left.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: show(&error) +--- +TYPE MISMATCH + + × The (++) operator can append list and string values, but not int values like + │ this: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: Hint: Try using Int.toString to turn it into a string? Or put it in [] to make + it a list? Or switch to the (::) operator? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_to_string.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_to_string.snap new file mode 100644 index 00000000..c2101084 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_int_to_string.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: show(&error) +--- +TYPE MISMATCH + + × I thought I was appending string values here, not int values like this: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: Hint: Try using Int.toString to turn it into a string? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_left.snap new file mode 100644 index 00000000..45c8a046 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__append_left.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +The (++) operator cannot append this type of value: + +I am seeing a string of type: + + string + +But the (++) operator is only for appending list and string values. Maybe put +this value in [] to make it a list? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_left.snap new file mode 100644 index 00000000..2b2376c5 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_left.snap @@ -0,0 +1,9 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +I am struggling with this boolean operation: + +Both sides of (&&) must be `bool` values, but the left side is: + + string diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_right.snap new file mode 100644 index 00000000..52714437 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__boolean_right.snap @@ -0,0 +1,9 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +I am struggling with this boolean operation: + +Both sides of (||) must be `bool` values, but the right side is: + + string diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__compare_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__compare_left.snap new file mode 100644 index 00000000..a8fa532f --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__compare_left.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +I cannot do a comparison with this value: + +The left side of (<) is a string of type: + + string + +But (<) only works with values whose type implements `Ord`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__contradictory_representation.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__contradictory_representation.snap new file mode 100644 index 00000000..738bc9b5 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__contradictory_representation.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::ContradictoryRepresentation\n{\n region: region(), name: \"f\", typ: &ErrorType::RigidVar(\"a\"), requirements:\n &[nash_ast::primitives::ReprTrait::Big,\n nash_ast::primitives::ReprTrait::Little]\n})" +--- +CONTRADICTORY REPRESENTATION + + × `f` requires incompatible representations: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This type is required to satisfy all of the following representation + constraints: + + 'a + + Big, Little + + No type can satisfy all of them. Check where this value is used as Big Data and + where a little builtin representation is required. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_left.snap new file mode 100644 index 00000000..278e82b5 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_left.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +The left argument of () is causing problems: + +The left argument is a string of type: + + string + +But () needs the left argument to be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_right.snap new file mode 100644 index 00000000..28c1d5c6 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__custom_right.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +The right argument of () is causing problems. + +The right argument is a string of type: + + string + +But () needs the right argument to be: + + int + +Hint: With operators like () I always check the left side first. If it seems +fine, I assume it is correct and check the right side. So the problem may be in +how the left and right arguments interact! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__destructure_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__destructure_mismatch.snap new file mode 100644 index 00000000..a749f594 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__destructure_mismatch.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::Destructure, & int())))" +--- +TYPE MISMATCH + + × This definition is causing issues: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: You are defining a string of type: + + string + + But then trying to destructure it as: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_left.snap new file mode 100644 index 00000000..d24ba778 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_left.snap @@ -0,0 +1,9 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +The (/) operator is integer division; both sides must be `int`. + +But the left side is: + + string diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_right.snap new file mode 100644 index 00000000..d67c2809 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__division_right.snap @@ -0,0 +1,9 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +The (/) operator is integer division; both sides must be `int`. + +But the right side is: + + string diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_category.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_category.snap new file mode 100644 index 00000000..3d671254 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_category.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "categories.into_iter().map(|category|\nadd_category(\"It is\", category)).collect::>().join(\"\\n\")" +--- +It is a list of type: +It is a string of type: +This `if` expression produces: +This `case` expression produces: +This `f` call produces: +This `Box` call produces: +It is: +It is: +It is an anonymous function of type: +This .field field access function has type: +The value at .field is a: +It is a record of type: +It is a tuple of type: +It is a unit value: +This `local` value is a: +This `foreign` value is a: diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_pattern_category.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_pattern_category.snap new file mode 100644 index 00000000..382552eb --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__every_pattern_category.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "categories.into_iter().map(|category|\nadd_pattern_category(\"It matches\", category)).collect::>().join(\"\\n\")" +--- +It matches record values of type: +It matches unit values: +It matches tuples of type: +It matches lists of type: +It matches `Box` values of type: +It matches integers: +It matches bytes: +It matches strings: +It matches booleans: diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__example_one_big_little_annotation.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__example_one_big_little_annotation.snap new file mode 100644 index 00000000..8313f6c3 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__example_one_big_little_annotation.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "crate::render_plain(&report, &crate::Source::new(source), \"src/Ledger.nash\")" +--- +TYPE MISMATCH + + × Something is off with the body of the `settle` definition: + ╭─[src/Ledger.nash:10:5] + 9 │ settle accounts = + 10 │ List.map balanceOf accounts + · ─────────────────────────── + ╰──── + help: This `List.map` call produces: + + list Int + + 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. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__expression_without_expectation.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__expression_without_expectation.snap new file mode 100644 index 00000000..40fccd95 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__expression_without_expectation.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::String, &string(),\nExpected::NoExpectation(&int())))" +--- +TYPE MISMATCH + + × This expression is being used in an unexpected way: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It is a string of type: + + string + + But you are trying to use it as: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__field_mismatch_update.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__field_mismatch_update.snap new file mode 100644 index 00000000..8a477aec --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__field_mismatch_update.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::FieldMismatch\n{\n region: region(), context: nash_constrain::type_::FieldContext::Update\n { record: \"person\" }, field: \"age\", actual: &string(), expected: &int()\n})" +--- +TYPE MISMATCH + + × I cannot update the `age` field like this: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: You are trying to update `age` to be: + + int + + But it should be: + + string + + Note: The record update syntax does not allow you to change the type of fields. + You can achieve that with record constructors or the record literal syntax. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_fewer.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_fewer.snap new file mode 100644 index 00000000..15e61a5b --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_fewer.snap @@ -0,0 +1,5 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +Hint: It looks like it takes too few arguments. I was expecting 2 more. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_more.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_more.snap new file mode 100644 index 00000000..e6c3c8e4 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_arity_more.snap @@ -0,0 +1,5 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +Hint: It looks like it takes too many arguments. I see 2 extra. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_big_little_need.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_big_little_need.snap new file mode 100644 index 00000000..72dd919e --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_big_little_need.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +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. If this value comes +from a validator argument, decode it with `fromData` first. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_double_rigid.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_double_rigid.snap new file mode 100644 index 00000000..6d5351b1 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_double_rigid.snap @@ -0,0 +1,9 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +Hint: Your type annotation uses `'a` and `'b` as separate type variables. Your +code seems to be saying they are the same though. Maybe they should be the same +in your type annotation? Maybe your code uses them in a weird way? + +Read for more advice! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_field_typo.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_field_typo.snap new file mode 100644 index 00000000..3cea9330 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_field_typo.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +Hint: Seems like a record field typo. Maybe naem should be name? + +Hint: Can more type annotations be added? Type annotations always help me give +more specific messages, and I think they could help a lot in this case! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_missing_fields.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_missing_fields.snap new file mode 100644 index 00000000..2ddb9e7a --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_missing_fields.snap @@ -0,0 +1,5 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +Hint: Looks like fields name and age are missing. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_option.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_option.snap new file mode 100644 index 00000000..9de4ecb1 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__hint_option.snap @@ -0,0 +1,6 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack(problem_to_hint(&problem)).render(80, false)" +--- +Hint: Use Option.withDefault to handle possible errors. Longer term, it is +usually better to write out the full `case` though! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__impl_resolution_limit.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__impl_resolution_limit.snap new file mode 100644 index 00000000..a99745aa --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__impl_resolution_limit.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::ImplResolutionLimit\n{ region: region(), name: \"f\", trait_: nash_ast::primitives::eq_trait() })" +--- +IMPL RESOLUTION LIMIT + + × I reached the impl resolution limit while checking `f`: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The search for `Eq` evidence exceeded the compiler's work limit. + + This requirement has not been checked completely. The other diagnostics from + this compilation still apply. + + Hint: Check for a cycle or a growing chain of impl constraints, and simplify the + requirement before trying again. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_kind.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_kind.snap new file mode 100644 index 00000000..4f9fbb8f --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_kind.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::BadKind\n{\n region: region(), name: \"f\", args: &[&ErrorType::FlexVar(\"a\")], reason:\n nash_constrain::error::KindProblem::Infinite\n})" +--- +INFINITE KIND + + × The use of `f` would require an infinite kind: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The type arguments at this use are: + + 'a + + A type constructor cannot be applied to itself in this way: its kind would have + to contain itself forever. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_type.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_type.snap new file mode 100644 index 00000000..bcd9574a --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__infinite_type.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::InfiniteType { region: region(), name: \"f\", overall_type: &t })" +--- +INFINITE TYPE + + × I am inferring a weird self-referential type for f: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: Here is my best effort at writing down the type. You will see ∞ for parts of the + type that repeat something already printed out infinitely. + + ∞ -> 'a + + Staring at this type is usually not so helpful, so I recommend reading the hints + at to get unstuck! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__kind_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__kind_mismatch.snap new file mode 100644 index 00000000..7b6bfce8 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__kind_mismatch.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::BadKind\n{\n region: region(), name: \"f\", args: &[&int()], reason:\n nash_constrain::error::KindProblem::Mismatch\n {\n expected:\n &nash_ast::Kind::Arrow(&nash_ast::Kind::Type, &nash_ast::Kind::Type),\n actual: &nash_ast::Kind::Type\n }\n})" +--- +KIND MISMATCH + + × The type arguments to `f` have incompatible kinds: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The type arguments at this use are: + + int + + I need kind: + + Type -> Type + + But this use has kind: + + Type + + Hint: A type constructor needs all of its required type arguments before it can + be used as a value type. An annotation's quantified kinds cannot be specialized + by its body. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_left.snap new file mode 100644 index 00000000..e522edd3 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_left.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Subtraction does not work with this value: + +The left side of (-) is a string of type: + + string + +But (-) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_right.snap new file mode 100644 index 00000000..e65b95ab --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__minus_right.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Subtraction does not work with this value: + +The right side of (-) is a string of type: + + string + +But (-) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_annotation_body.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_annotation_body.snap new file mode 100644 index 00000000..7379cecc --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_annotation_body.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::String, &string(),\nExpected::FromAnnotation(\"value\", 0, SubContext::TypedBody, &int())))" +--- +TYPE MISMATCH + + × Something is off with the body of the `value` definition: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The body is a string of type: + + string + + But the type annotation on `value` says it should be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_first.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_first.snap new file mode 100644 index 00000000..deb1f63f --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_first.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::CallArg(MaybeName::FuncName(\"f\"), 0), &\nint())))" +--- +TYPE MISMATCH + + × The 1st argument to `f` is not what I expect: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This argument is a string of type: + + string + + But `f` needs the 1st argument to be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_second_has_hint.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_second_has_hint.snap new file mode 100644 index 00000000..9349ed4b --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_call_arg_second_has_hint.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::CallArg(MaybeName::FuncName(\"f\"), 1), &\nint())))" +--- +TYPE MISMATCH + + × The 2nd argument to `f` is not what I expect: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This argument is a string of type: + + string + + But `f` needs the 2nd argument to be: + + int + + Hint: I always figure out the argument types from left to right. If an argument + is acceptable, I assume it is “correct” and move on. So the problem may actually + be in one of the previous arguments! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_case_branches.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_case_branches.snap new file mode 100644 index 00000000..c9e512b3 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_case_branches.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::CaseBranch(1), & int())))" +--- +TYPE MISMATCH + + × The 2nd branch of this `case` does not match all the previous branches: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd branch is a string of type: + + string + + But all the previous branches result in: + + int + + Hint: All branches in a `case` must produce the same type of values. This way, + no matter which branch we take, the result is always a consistent shape. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_branches.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_branches.snap new file mode 100644 index 00000000..5a9260c4 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_branches.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::IfBranch(1), & int())))" +--- +TYPE MISMATCH + + × The 2nd branch of this `if` does not match all the previous branches: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd branch is a string of type: + + string + + But all the previous branches result in: + + int + + Hint: All branches in an `if` must produce the same type of values. This way, no + matter which branch we take, the result is always a consistent shape. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_condition_not_bool.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_condition_not_bool.snap new file mode 100644 index 00000000..6edf57b8 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_if_condition_not_bool.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::String, &string(),\nExpected::FromContext(region(), Context::IfCondition, &boolean)))" +--- +TYPE MISMATCH + + × This `if` condition does not evaluate to a boolean value, `True` or `False`. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It is a string of type: + + string + + But I need this `if` condition to be a `bool` value. + + Hint: Nash does not have “truthiness” such that ints and strings and lists are + automatically converted to booleans. Do that conversion explicitly! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_list_entries.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_list_entries.snap new file mode 100644 index 00000000..644d656e --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__mismatch_list_entries.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::ListEntry(1), & int())))" +--- +TYPE MISMATCH + + × The 2nd element of this list does not match all the previous elements: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd element is a string of type: + + string + + But all the previous elements in the list are: + + int + + Hint: Everything in a list must be the same type of value. This way, we never + run into unexpected values partway through a List.map, List.foldl, etc. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_constraint.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_constraint.snap new file mode 100644 index 00000000..3cf11588 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_constraint.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::MissingConstraint\n{\n region: region(), name: \"==\", trait_: nash_ast::primitives::eq_trait(),\n args: &[&ErrorType::RigidVar(\"a\")], binder:\n &nash_region::Located::at(region(), \"f\")\n})" +--- +MISSING CONSTRAINT + + × `==` needs a constraint that the annotation for `f` does not promise: + ╭─[Main.nash:1:1] + 1 │ value + · ──┬──┬ + · │ ╰── needs `Eq 'a` + · ╰── annotation for `f` + ╰──── + help: Eq 'a + + The type variables in `f` must work for every type allowed by its annotation. I + cannot assume this constraint without it being declared. + + Hint: Add `Eq 'a` to the context of the `f` type annotation. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_field_alias.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_field_alias.snap new file mode 100644 index 00000000..069bc6ee --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_field_alias.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::MissingField\n{\n region: region(), context: FieldContext::Access\n { record_region: region(), maybe_name: Some(\"person\") }, field: \"aeg\",\n record: &actual, available: &[\"age\"]\n})" +--- +TYPE MISMATCH + + × I cannot find `aeg` when accessing the `aeg` field of `person`: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This is usually a typo. Here are the `person` fields that are most similar: + + { age : int + } + + So maybe aeg should be age? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl.snap new file mode 100644 index 00000000..677813af --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::MissingImpl\n{\n region: region(), name: \"==\", trait_: nash_ast::primitives::eq_trait(),\n args:\n &[&ErrorType::Type\n {\n home: nash_ast::ModuleName { package: None, name: \"Main\" }, name:\n \"step\", args: &[]\n }], available:\n &[&[nash_ast::Head::Named\n {\n reference: nash_ast::QualifiedName\n { home: nash_ast::primitives::builtin_home(), name: \"int\" }, args: &[]\n }]], because: &[]\n})" +--- +MISSING IMPL + + × I cannot find an `Eq` impl for `step`: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + 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: + + int + + Hint: Write an `impl Eq step` that provides the trait's methods: + + impl Eq step where + eq a b = ... diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl_local_union_deriving_not_yet_available.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl_local_union_deriving_not_yet_available.snap new file mode 100644 index 00000000..88a10e2e --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_impl_local_union_deriving_not_yet_available.snap @@ -0,0 +1,15 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: text +--- +The (==) operator needs its arguments to implement `Eq.Eq`, and here they are: + + step + +But there is no `impl Eq.Eq step` in this module or in any import. + +Hint: This local datatype is a candidate for `@derive(Eq.Eq)`, but automatic +deriving is not available yet. Write the impl by hand: + + impl Eq.Eq step where + eq a b = ... diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_storable_constraint_for_list_element.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_storable_constraint_for_list_element.snap new file mode 100644 index 00000000..e572e175 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__missing_storable_constraint_for_list_element.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::MissingImpl\n{\n region: region(), name: \"values\", trait_:\n nash_ast::primitives::ReprTrait::Storable.qualified(), args:\n &[&ErrorType::Lambda(&int(), &int(), &[])], available: &[], because:\n &[nash_constrain::error::Requirement::Formation(&ErrorType::Type\n {\n home: nash_ast::primitives::builtin_home(), name: \"list\", args:\n &[&ErrorType::Lambda(&int(), &int(), &[])]\n })]\n})" +--- +MISSING IMPL + + × `values` requires a representation that this type does not provide: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: (int -> int) + + `Storable` accepts Big or Const representations. This argument does not meet + that requirement. + + Hint: Representation constraints are compiler-owned. Adding an impl cannot + change a type's representation; change the datatype or convert the value + explicitly. + + This requirement came from the following chain, from the original use to the + failing requirement: + + forming list (int -> int) diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_left.snap new file mode 100644 index 00000000..ac9ad0d7 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_left.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Multiplication does not work with this value: + +The left side of (*) is a string of type: + + string + +But (*) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_right.snap new file mode 100644 index 00000000..f3855985 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__multiply_right.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Multiplication does not work with this value: + +The right side of (*) is a string of type: + + string + +But (*) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__not_a_record_pattern.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__not_a_record_pattern.snap new file mode 100644 index 00000000..58029fac --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__not_a_record_pattern.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::NotARecord\n{\n region: region(), context: nash_constrain::type_::FieldContext::Pattern,\n field: Some(\"name\"), record: &int()\n})" +--- +TYPE MISMATCH + + × This value is not a record, so I cannot use it when matching the `name` field in + │ this record pattern: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It has type: + + int + + But I need a value with record fields! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_append_string_list.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_append_string_list.snap new file mode 100644 index 00000000..94a3b603 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_append_string_list.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::List, &list,\nExpected::FromContext(region(), Context::OpRight(\"++\"), &string())))" +--- +TYPE MISMATCH + + × The (++) operator needs the same type of value on both sides: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: I see a string on the left and a list on the right. Which should it be? Does the + string need [] around it to become a list? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_compare_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_compare_mismatch.snap new file mode 100644 index 00000000..932a5fce --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_compare_mismatch.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::OpRight(\"<\"), & int())))" +--- +TYPE MISMATCH + + × I need both sides of (<) to be the same type: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The left side of (<) is: + + int + + But the right side is: + + string + + I cannot compare different types though! Which side of (<) is the problem? The + type must implement `Ord`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_element_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_element_mismatch.snap new file mode 100644 index 00000000..7df62120 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_element_mismatch.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::List, &actual,\nExpected::FromContext(region(), Context::OpRight(\"::\"), &expected)))" +--- +TYPE MISMATCH + + × I am having trouble with this (::) operator: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The left side of (::) is: + + int + + But you are trying to put that into a list filled with: + + string + + Lists need ALL elements to be the same type though. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_right_not_list.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_right_not_list.snap new file mode 100644 index 00000000..5561ac71 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_cons_right_not_list.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::OpRight(\"::\"), & int())))" +--- +TYPE MISMATCH + + × The (::) operator can only add elements onto lists. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The right side is a string of type: + + string + + But (::) needs a `list` on the right. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_equality_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_equality_mismatch.snap new file mode 100644 index 00000000..2328fc9c --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_equality_mismatch.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::OpRight(\"==\"), & int())))" +--- +TYPE MISMATCH + + × I need both sides of (==) to be the same type: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The left side of (==) is: + + int + + But the right side is: + + string + + Different types can never be equal though! Which side is messed up? The type + must implement `Eq`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_argument_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_argument_mismatch.snap new file mode 100644 index 00000000..444d2cd7 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_argument_mismatch.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::Lambda, &actual,\nExpected::FromContext(region(), Context::OpRight(\"|>\"), &expected)))" +--- +TYPE MISMATCH + + × This function cannot handle the argument sent through the (|>) pipe: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The argument is: + + int + + But (|>) is piping it to a function that expects: + + string diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_right_not_function.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_right_not_function.snap new file mode 100644 index 00000000..fd4608f4 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_pipe_right_not_function.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::OpRight(\"|>\"), & int())))" +--- +TYPE MISMATCH + + × The right side of (|>) needs to be a function so I can pipe arguments to it! + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: But instead of a function, I am seeing a string of type: + + string diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_plus_left_string.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_plus_left_string.snap new file mode 100644 index 00000000..874e93ba --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__op_plus_left_string.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::OpLeft(\"+\"), & int())))" +--- +TYPE MISMATCH + + × Addition does not work with this value: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The left side of (+) is a string of type: + + string + + But (+) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_first_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_first_mismatch.snap new file mode 100644 index 00000000..3ce81899 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_first_mismatch.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadPattern(region(), PCategory :: Str, & string(), PExpected ::\nFromContext(region(), PContext::CaseMatch(0), & int())))" +--- +TYPE MISMATCH + + × The 1st pattern in this `case` is causing a mismatch: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The first pattern is trying to match strings: + + string + + But the expression between `case` and `of` is: + + int + + These can never match! Is the pattern the problem? Or is it the expression? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_later_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_later_mismatch.snap new file mode 100644 index 00000000..4d8673f6 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_case_later_mismatch.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadPattern(region(), PCategory :: Str, & string(), PExpected ::\nFromContext(region(), PContext::CaseMatch(1), & int())))" +--- +TYPE MISMATCH + + × The 2nd pattern in this `case` does not match the previous ones. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd pattern is trying to match strings: + + string + + But all the previous patterns match: + + int + + Note: A `case` expression can only handle one type of value, so you may want to + use to handle “mixing” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_ctor_arg_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_ctor_arg_mismatch.snap new file mode 100644 index 00000000..44e1fd97 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_ctor_arg_mismatch.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadPattern(region(), PCategory :: Str, & string(), PExpected ::\nFromContext(region(), PContext::CtorArg(\"Some\", 0), & int())))" +--- +TYPE MISMATCH + + × The 1st argument to `Some` is weird. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It is trying to match strings: + + string + + But `Some` needs its 1st argument to be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_entry.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_entry.snap new file mode 100644 index 00000000..63d4234f --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_entry.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadPattern(region(), PCategory :: Str, & string(), PExpected ::\nFromContext(region(), PContext::ListEntry(1), & int())))" +--- +TYPE MISMATCH + + × The 2nd pattern in this list does not match all the previous ones: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd pattern is trying to match strings: + + string + + But all the previous patterns in the list are: + + int + + Hint: Everything in a list must be the same type of value. This way, we never + run into unexpected values partway through a List.map, List.foldl, etc. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_tail.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_tail.snap new file mode 100644 index 00000000..04d8f87b --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_list_tail.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadPattern(region(), PCategory::Str, &string(),\nPExpected::FromContext(region(), PContext::Tail, &list)))" +--- +TYPE MISMATCH + + × The pattern after (::) is causing issues. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The pattern after (::) is trying to match strings: + + string + + But it needs to match lists like this: + + list int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_typed_arg_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_typed_arg_mismatch.snap new file mode 100644 index 00000000..45301334 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_typed_arg_mismatch.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadPattern(region(), PCategory :: Str, & string(), PExpected ::\nFromContext(region(), PContext::TypedArg(\"f\", 0), & int())))" +--- +TYPE MISMATCH + + × The 1st argument to `f` is weird. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The argument is a pattern that matches strings: + + string + + But the type annotation on `f` says the 1st argument should be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_without_expectation.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_without_expectation.snap new file mode 100644 index 00000000..efc1e8e2 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pattern_without_expectation.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadPattern(region(), PCategory::Str, &string(),\nPExpected::NoExpectation(&int())))" +--- +TYPE MISMATCH + + × This pattern is being used in an unexpected way: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It is strings: + + string + + But it needs to match: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_argument.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_argument.snap new file mode 100644 index 00000000..4b4e1574 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_argument.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +I cannot send this through the (<|) pipe: + +The argument is: + + string + +But (<|) is piping it to a function that expects: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_not_function.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_not_function.snap new file mode 100644 index 00000000..7a79216c --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__pipe_left_not_function.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +The left side of (<|) needs to be a function so I can pipe arguments to it! + +I am seeing a string of type: + + string + +This needs to be some kind of function though! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__plus_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__plus_right.snap new file mode 100644 index 00000000..ef169358 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__plus_right.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Addition does not work with this value: + +The right side of (+) is a string of type: + + string + +But (+) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__polymorphic_recursion.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__polymorphic_recursion.snap new file mode 100644 index 00000000..93aa95c0 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__polymorphic_recursion.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::PolymorphicRecursion\n{\n region: region(), name: \"f\", trait_: nash_ast::primitives::eq_trait(),\n args: &[&ErrorType::FlexVar(\"a\")]\n})" +--- +POLYMORPHIC RECURSION + + × The recursive use of `f` keeps changing its trait arguments: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The growing requirement is: + + Eq 'a + + Each trip around this recursive call adds another impl wrapper. I cannot + construct a finite set of evidence arguments for it. + + Hint: Keep the trait arguments the same across recursive calls, or split the + work into functions with explicit type annotations. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_left.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_left.snap new file mode 100644 index 00000000..a760dc1e --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_left.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Exponentiation does not work with this value: + +The left side of (^) is a string of type: + + string + +But (^) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_right.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_right.snap new file mode 100644 index 00000000..3dc60d92 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__power_right.snap @@ -0,0 +1,11 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "Doc::stack([report.before, report.after]).render(80, false)" +--- +Exponentiation does not work with this value: + +The right side of (^) is a string of type: + + string + +But (^) only works with values whose type implements `Num`. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_missing_field_typo.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_missing_field_typo.snap new file mode 100644 index 00000000..dd9a9823 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_missing_field_typo.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: show(&error) +--- +TYPE MISMATCH + + × This `person` record does not have a `naem` field: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This is usually a typo. Here are the `person` fields that are most similar: + + { name : string + , age : int + } + + So maybe naem should be name? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_on_non_record.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_on_non_record.snap new file mode 100644 index 00000000..88ec553d --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_access_on_non_record.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(),\nContext::RecordAccess\n{\n record_region: region(), maybe_name: Some(\"value\"), field_region:\n region(), field: \"name\"\n}, & int())))" +--- +TYPE MISMATCH + + × This is not a record, so it has no fields to access! + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It is: + + string + + But I need a record with a `name` field! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_field_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_field_mismatch.snap new file mode 100644 index 00000000..02ce94b1 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_field_mismatch.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::RecordField(\"value\", \"name\"), & int())))" +--- +TYPE MISMATCH + + × The `name` field of `value` is not what I expect: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This field is a string of type: + + string + + But the record declaration says it should be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_change_type.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_change_type.snap new file mode 100644 index 00000000..5300e676 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_change_type.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::RecordUpdateValue(\"name\"), & int())))" +--- +TYPE MISMATCH + + × I cannot update the `name` field like this: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: You are trying to update `name` to be a string of type: + + string + + But it should be: + + int + + Note: The record update syntax does not allow you to change the type of fields. + You can achieve that with record constructors or the record literal syntax. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_unknown_field.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_unknown_field.snap new file mode 100644 index 00000000..2110f953 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__record_update_unknown_field.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::Record, &record,\nExpected::FromContext(region(), Context::RecordUpdateKeys(\"person\", &updates),\n&int())))" +--- +TYPE MISMATCH + + × The `person` record does not have a `naem` field: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: This is usually a typo. Here are the `person` fields that are most similar: + + { name : string + } + + So maybe naem should be name? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__rigid_var_mismatch.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__rigid_var_mismatch.snap new file mode 100644 index 00000000..e5a64504 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__rigid_var_mismatch.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::CallResult(MaybeName::NoName),\n&int(),\nExpected::FromAnnotation(\"f\", 0, SubContext::TypedBody,\n&ErrorType::RigidVar(\"a\"))))" +--- +TYPE MISMATCH + + × Something is off with the body of the `f` definition: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The body is: + + int + + But the type annotation on `f` says it should be: + + 'a + + Hint: Your type annotation uses type variable `'a` which means ANY type of value + can flow through, but your code is saying it specifically wants a value of type + `int`. Maybe change your type annotation to be more specific? Maybe change the + code to be more general? + + Read for more advice! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_annotation_body.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_annotation_body.snap new file mode 100644 index 00000000..4039d600 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_annotation_body.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf : A\\nf = B\\n\")" +--- +TYPE MISMATCH + + × Something is off with the body of the `f` definition: + ╭─[Main.nash:5:5] + 4 │ f : A + 5 │ f = B + · ─ + ╰──── + help: This `B` value is a: + + B + + But the type annotation on `f` says it should be: + + A diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_argument.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_argument.snap new file mode 100644 index 00000000..1118a925 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_argument.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf : A -> A\\nf x = x\\ng = f B\\n\")" +--- +TYPE MISMATCH + + × The 1st argument to `f` is not what I expect: + ╭─[Main.nash:6:7] + 5 │ f x = x + 6 │ g = f B + · ─ + ╰──── + help: This `B` value is a: + + B + + But `f` needs the 1st argument to be: + + A diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_second_argument.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_second_argument.snap new file mode 100644 index 00000000..962395cc --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_call_second_argument.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf : A -> A -> A\\nf x y = x\\ng = f A B\\n\")" +--- +TYPE MISMATCH + + × The 2nd argument to `f` is not what I expect: + ╭─[Main.nash:6:9] + 5 │ f x y = x + 6 │ g = f A B + · ─ + ╰──── + help: This `B` value is a: + + B + + But `f` needs the 2nd argument to be: + + A + + Hint: I always figure out the argument types from left to right. If an argument + is acceptable, I assume it is “correct” and move on. So the problem may actually + be in one of the previous arguments! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_case_branches.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_case_branches.snap new file mode 100644 index 00000000..ca9054b3 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_case_branches.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf a =\\n case a of\\n A -> A\\n _ -> B\\n\")" +--- +TYPE MISMATCH + + × The 2nd branch of this `case` does not match all the previous branches: + ╭─[Main.nash:7:14] + 4 │ f a = + 5 │ case a of + 6 │ A -> A + 7 │ _ -> B + · ─ + ╰──── + help: This `B` value is a: + + B + + But all the previous branches result in: + + A + + Hint: All branches in a `case` must produce the same type of values. This way, + no matter which branch we take, the result is always a consistent shape. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_branches.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_branches.snap new file mode 100644 index 00000000..0abacb38 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_branches.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf condition = if condition then A else B\\n\")" +--- +TYPE MISMATCH + + × The 2nd branch of this `if` does not match all the previous branches: + ╭─[Main.nash:4:40] + 3 │ type B = B + 4 │ f condition = if condition then A else B + · ─ + ╰──── + help: This `B` value is a: + + B + + But all the previous branches result in: + + A + + Hint: All branches in an `if` must produce the same type of values. This way, no + matter which branch we take, the result is always a consistent shape. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_condition.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_condition.snap new file mode 100644 index 00000000..a268c2ee --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_if_condition.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\nf = if A then A else A\\n\")" +--- +TYPE MISMATCH + + × This `if` condition does not evaluate to a boolean value, `True` or `False`. + ╭─[Main.nash:3:8] + 2 │ type A = A + 3 │ f = if A then A else A + · ─ + ╰──── + help: This `A` value is a: + + A + + But I need this `if` condition to be a `bool` value. + + Hint: Nash does not have “truthiness” such that ints and strings and lists are + automatically converted to booleans. Do that conversion explicitly! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_infinite_type.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_infinite_type.snap new file mode 100644 index 00000000..9ba9a025 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_infinite_type.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\nf x = x x\\n\")" +--- +INFINITE TYPE + + × I am inferring a weird self-referential type for x: + ╭─[Main.nash:2:3] + 1 │ module Main exposing (..) + 2 │ f x = x x + · ─ + ╰──── + help: Here is my best effort at writing down the type. You will see ∞ for parts of the + type that repeat something already printed out infinitely. + + ∞ -> 'a + + Staring at this type is usually not so helpful, so I recommend reading the hints + at to get unstuck! diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_list_entries.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_list_entries.snap new file mode 100644 index 00000000..8f47f393 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_list_entries.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf = [A, B]\\n\")" +--- +TYPE MISMATCH + + × The 2nd element of this list does not match all the previous elements: + ╭─[Main.nash:4:9] + 3 │ type B = B + 4 │ f = [A, B] + · ─ + ╰──── + help: This `B` value is a: + + B + + But all the previous elements in the list are: + + A + + Hint: Everything in a list must be the same type of value. This way, we never + run into unexpected values partway through a List.map, List.foldl, etc. Read + to learn how to “mix” types. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_constraint.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_constraint.snap new file mode 100644 index 00000000..24899f24 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_constraint.snap @@ -0,0 +1,20 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntrait Eq 'a where\\n eq : 'a -> 'a -> Builtin.bool\\nf : 'a -> Builtin.bool\\nf a = eq a a\\n\")" +--- +MISSING CONSTRAINT + + × `eq` needs a constraint that the annotation for `f` does not promise: + ╭─[Main.nash:5:7] + 4 │ f : 'a -> Builtin.bool + 5 │ f a = eq a a + · ┬ ─┬ + · │ ╰── needs `Eq 'a` + · ╰── annotation for `f` + ╰──── + help: Eq 'a + + The type variables in `f` must work for every type allowed by its annotation. I + cannot assume this constraint without it being declared. + + Hint: Add `Eq 'a` to the context of the `f` type annotation. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_impl.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_impl.snap new file mode 100644 index 00000000..2c418139 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_missing_impl.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntrait Eq 'a where\\n eq : 'a -> 'a -> Builtin.bool\\nf : A -> Builtin.bool\\nf a = eq a a\\n\")" +--- +MISSING IMPL + + × I cannot find an `Eq` impl for `A`: + ╭─[Main.nash:6:7] + 5 │ f : A -> Builtin.bool + 6 │ f a = eq a a + · ── + ╰──── + help: `eq` needs its arguments to implement `Eq`, and here they are: + + A + + But there is no `impl Eq A` in this module or in any import. + + Hint: Write an `impl Eq A` that provides the trait's methods: + + impl Eq A where + ... diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_ctor_arg.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_ctor_arg.snap new file mode 100644 index 00000000..1a35700b --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_ctor_arg.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\ntype Box = Box A\\nf (Box B) = A\\n\")" +--- +TYPE MISMATCH + + × The 1st argument to `Box` is weird. + ╭─[Main.nash:5:8] + 4 │ type Box = Box A + 5 │ f (Box B) = A + · ─ + ╰──── + help: It is trying to match `B` values of type: + + B + + But `Box` needs its 1st argument to be: + + A diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_typed_arg.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_typed_arg.snap new file mode 100644 index 00000000..e1b1bdb5 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_pattern_typed_arg.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\nf : B -> B\\nf A = B\\n\")" +--- +TYPE MISMATCH + + × The 1st argument to `f` is weird. + ╭─[Main.nash:5:3] + 4 │ f : B -> B + 5 │ f A = B + · ─ + ╰──── + help: The argument is a pattern that matches `A` values of type: + + A + + But the type annotation on `f` says the 1st argument should be: + + B diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_access.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_access.snap new file mode 100644 index 00000000..13527e32 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_access.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype alias Person = { age : A }\\nf : Person -> A\\nf p = p.aeg\\n\")" +--- +TYPE MISMATCH + + × I cannot find `aeg` when accessing the `aeg` field of `p`: + ╭─[Main.nash:5:7] + 4 │ f : Person -> A + 5 │ f p = p.aeg + · ───── + ╰──── + help: This is usually a typo. Here are the `p` fields that are most similar: + + { age : A + } + + So maybe aeg should be age? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_update_type.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_update_type.snap new file mode 100644 index 00000000..a46504a4 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__source_pipeline_record_update_type.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "type_error_reports(\"module Main exposing (..)\\ntype A = A\\ntype B = B\\ntype alias Person = { age : A }\\nf : Person -> Person\\nf p = { p | age = B }\\n\")" +--- +TYPE MISMATCH + + × I cannot update the `age` field like this: + ╭─[Main.nash:6:13] + 5 │ f : Person -> Person + 6 │ f p = { p | age = B } + · ─── + ╰──── + help: You are trying to update `age` to be: + + B + + But it should be: + + A + + Note: The record update syntax does not allow you to change the type of fields. + You can achieve that with record constructors or the record literal syntax. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_function.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_function.snap new file mode 100644 index 00000000..9e1f8fcc --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_function.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::Lambda, &function,\nExpected::FromContext(region(),\nContext::CallArity(MaybeName::FuncName(\"f\"), 3), &i)))" +--- +TOO MANY ARGS + + × The `f` function expects 1 argument, but it got 3 instead. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: Are there any missing commas? Or missing parentheses? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_value.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_value.snap new file mode 100644 index 00000000..83edff13 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__too_many_args_on_value.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error ::\nBadExpr(region(), Category :: String, & string(), Expected ::\nFromContext(region(), Context::CallArity(MaybeName::FuncName(\"value\"), 2), &\nint())))" +--- +TOO MANY ARGS + + × The `value` value is not a function, but it was given 2 arguments. + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: Are there any missing commas? Or missing parentheses? diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_case.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_case.snap new file mode 100644 index 00000000..21b47872 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_case.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::String, &string(),\nExpected::FromAnnotation(\"f\", 0, context, &int())))" +--- +TYPE MISMATCH + + × Something is off with the 2nd branch of this `case` expression: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd branch is a string of type: + + string + + But the type annotation on `f` says it should be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_if.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_if.snap new file mode 100644 index 00000000..80166670 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__typed_if.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&Error::BadExpr(region(), Category::String, &string(),\nExpected::FromAnnotation(\"f\", 0, context, &int())))" +--- +TYPE MISMATCH + + × Something is off with the 2nd branch of this `if` expression: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The 2nd branch is a string of type: + + string + + But the type annotation on `f` says it should be: + + int diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_application.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_application.snap new file mode 100644 index 00000000..c866a75e --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_application.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::UnresolvedApplication\n{\n region: region(), name: \"f\", head: &ErrorType::FlexVar(\"f\"), args:\n &[&int()]\n})" +--- +UNRESOLVED TYPE APPLICATION + + × I could not establish the datatype context required by `f`: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: The unresolved type application is: + + 'f int + + Hint: Add a type annotation that determines the type constructor and its + arguments. Its datatype constraints must be satisfied before this value can be + used. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_constraint.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_constraint.snap new file mode 100644 index 00000000..936a45e7 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__unresolved_constraint.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(&\nError::UnresolvedConstraint\n{\n region: region(), name: \"f\", trait_: nash_ast::primitives::eq_trait(),\n args: &[&ErrorType::FlexVar(\"a\")]\n})" +--- +UNRESOLVED CONSTRAINT + + × I could not establish the constraint required by `f`: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: Eq 'a + + Type inference finished without a proof for this requirement. + + Hint: Add a type annotation to resolve the remaining type variables, then check + that the required impl or annotation constraint is available. diff --git a/crates/nash-report/src/type_/snapshots/nash_report__type___tests__update_not_record.snap b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__update_not_record.snap new file mode 100644 index 00000000..40059ca0 --- /dev/null +++ b/crates/nash-report/src/type_/snapshots/nash_report__type___tests__update_not_record.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/type_/tests.rs +expression: "show(& Error::UpdateNotRecord { region: region(), record: &int() })" +--- +TYPE MISMATCH + + × This value does not support record updates: + ╭─[Main.nash:1:1] + 1 │ value + · ───── + ╰──── + help: It has type: + + int + + I need a record alias for this update. Rebuild this value with its constructor + instead. diff --git a/crates/nash-report/src/type_/tests.rs b/crates/nash-report/src/type_/tests.rs new file mode 100644 index 00000000..d7997e3a --- /dev/null +++ b/crates/nash-report/src/type_/tests.rs @@ -0,0 +1,885 @@ +use super::*; + +use nash_region::Position; + +fn region() -> Region { + Region::new(Position::new(1, 1), Position::new(1, 6)) +} +fn int() -> ErrorType<'static> { + ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "int", + args: &[], + } +} +fn string() -> ErrorType<'static> { + ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "string", + args: &[], + } +} +fn show(error: &Error<'_>) -> String { + crate::render_plain( + &to_report( + &Localizer::from_names(["Builtin", "Main", "Eq", "Num"]), + error, + ), + &crate::Source::new("value\n"), + "Main.nash", + ) +} + +#[test] +fn mismatch_annotation_body() { + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::String, + &string(), + Expected::FromAnnotation("value", 0, SubContext::TypedBody, &int()) + ))); +} + +macro_rules! context_snapshot { + ($test:ident, $context:expr) => { + #[test] + fn $test() { + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::String, + &string(), + Expected::FromContext(region(), $context, &int()) + ))); + } + }; +} +context_snapshot!(mismatch_if_branches, Context::IfBranch(1)); +context_snapshot!(mismatch_case_branches, Context::CaseBranch(1)); +context_snapshot!(mismatch_list_entries, Context::ListEntry(1)); +#[test] +fn mismatch_if_condition_not_bool() { + let boolean = ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "bool", + args: &[], + }; + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::String, + &string(), + Expected::FromContext(region(), Context::IfCondition, &boolean) + ))); +} +context_snapshot!( + mismatch_call_arg_first, + Context::CallArg(MaybeName::FuncName("f"), 0) +); +context_snapshot!( + mismatch_call_arg_second_has_hint, + Context::CallArg(MaybeName::FuncName("f"), 1) +); +context_snapshot!( + too_many_args_on_value, + Context::CallArity(MaybeName::FuncName("value"), 2) +); +context_snapshot!( + record_access_on_non_record, + Context::RecordAccess { + record_region: region(), + maybe_name: Some("value"), + field_region: region(), + field: "name" + } +); +context_snapshot!( + record_update_change_type, + Context::RecordUpdateValue("name") +); +context_snapshot!(op_plus_left_string, Context::OpLeft("+")); +context_snapshot!(op_cons_right_not_list, Context::OpRight("::")); +context_snapshot!(op_compare_mismatch, Context::OpRight("<")); +context_snapshot!(op_equality_mismatch, Context::OpRight("==")); +context_snapshot!(op_pipe_right_not_function, Context::OpRight("|>")); +context_snapshot!(destructure_mismatch, Context::Destructure); +context_snapshot!(record_field_mismatch, Context::RecordField("value", "name")); + +macro_rules! pattern_snapshot { + ($test:ident, $context:expr) => { + #[test] + fn $test() { + insta::assert_snapshot!(show(&Error::BadPattern( + region(), + PCategory::Str, + &string(), + PExpected::FromContext(region(), $context, &int()) + ))); + } + }; +} +pattern_snapshot!(pattern_case_first_mismatch, PContext::CaseMatch(0)); +pattern_snapshot!(pattern_case_later_mismatch, PContext::CaseMatch(1)); +pattern_snapshot!(pattern_ctor_arg_mismatch, PContext::CtorArg("Some", 0)); +pattern_snapshot!(pattern_typed_arg_mismatch, PContext::TypedArg("f", 0)); +pattern_snapshot!(pattern_list_entry, PContext::ListEntry(1)); +#[test] +fn pattern_list_tail() { + let list = ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "list", + args: &[&int()], + }; + insta::assert_snapshot!(show(&Error::BadPattern( + region(), + PCategory::Str, + &string(), + PExpected::FromContext(region(), PContext::Tail, &list) + ))); +} + +#[test] +fn too_many_args_on_function() { + let i = int(); + let function = ErrorType::Lambda(&i, &i, &[]); + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::Lambda, + &function, + Expected::FromContext( + region(), + Context::CallArity(MaybeName::FuncName("f"), 3), + &i + ) + ))); +} +#[test] +fn infinite_type() { + let t = ErrorType::Lambda(&ErrorType::Infinite, &ErrorType::FlexVar("a"), &[]); + insta::assert_snapshot!(show(&Error::InfiniteType { + region: region(), + name: "f", + overall_type: &t + })); +} +#[test] +fn rigid_var_mismatch() { + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::CallResult(MaybeName::NoName), + &int(), + Expected::FromAnnotation("f", 0, SubContext::TypedBody, &ErrorType::RigidVar("a")) + ))); +} +#[test] +fn record_access_missing_field_typo() { + let record = ErrorType::Record { + fields: &[("name", &string()), ("age", &int())], + }; + let error = Error::BadExpr( + region(), + Category::Record, + &record, + Expected::FromContext( + region(), + Context::RecordAccess { + record_region: region(), + maybe_name: Some("person"), + field_region: region(), + field: "naem", + }, + &int(), + ), + ); + let report = to_report(&Localizer::from_names(["Builtin"]), &error); + assert_eq!(report.suggestions, ["name", "age"]); + insta::assert_snapshot!(show(&error)); +} +#[test] +fn record_update_unknown_field() { + let record = ErrorType::Record { + fields: &[("name", &string())], + }; + let field = nash_region::Located::at(region(), "naem"); + let value = nash_region::Located::at(region(), nash_ast::Expr::Unit); + let updates = [nash_ast::FieldUpdate { + field: &field, + value: &value, + }]; + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::Record, + &record, + Expected::FromContext( + region(), + Context::RecordUpdateKeys("person", &updates), + &int() + ) + ))); +} +#[test] +fn missing_field_alias() { + use nash_constrain::type_::FieldContext; + let i = int(); + let actual = ErrorType::Alias { + home: nash_ast::ModuleName { + package: None, + name: "Main", + }, + name: "Person", + args: &[], + real: &ErrorType::Record { + fields: &[("age", &i)], + }, + }; + insta::assert_snapshot!(show(&Error::MissingField { + region: region(), + context: FieldContext::Access { + record_region: region(), + maybe_name: Some("person") + }, + field: "aeg", + record: &actual, + available: &["age"] + })); +} +#[test] +fn op_append_string_list() { + let list = ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "list", + args: &[&int()], + }; + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::List, + &list, + Expected::FromContext(region(), Context::OpRight("++"), &string()) + ))); +} +#[test] +fn op_cons_element_mismatch() { + let actual = ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "list", + args: &[&string()], + }; + let expected = ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "list", + args: &[&int()], + }; + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::List, + &actual, + Expected::FromContext(region(), Context::OpRight("::"), &expected) + ))); +} +#[test] +fn op_pipe_argument_mismatch() { + let actual = ErrorType::Lambda(&string(), &string(), &[]); + let expected = ErrorType::Lambda(&int(), &string(), &[]); + insta::assert_snapshot!(show(&Error::BadExpr( + region(), + Category::Lambda, + &actual, + Expected::FromContext(region(), Context::OpRight("|>"), &expected) + ))); +} + +macro_rules! error_snapshot { + ($name:ident, $error:expr) => { + #[test] + fn $name() { + insta::assert_snapshot!(show(&$error)); + } + }; +} +error_snapshot!( + ambiguous_record_access, + Error::AmbiguousRecordAccess { + region: region(), + context: nash_constrain::type_::FieldContext::Accessor, + field: Some("name"), + record: &ErrorType::FlexVar("a") + } +); +error_snapshot!( + not_a_record_pattern, + Error::NotARecord { + region: region(), + context: nash_constrain::type_::FieldContext::Pattern, + field: Some("name"), + record: &int() + } +); +error_snapshot!( + update_not_record, + Error::UpdateNotRecord { + region: region(), + record: &int() + } +); +error_snapshot!( + field_mismatch_update, + Error::FieldMismatch { + region: region(), + context: nash_constrain::type_::FieldContext::Update { record: "person" }, + field: "age", + actual: &string(), + expected: &int() + } +); +error_snapshot!( + kind_mismatch, + Error::BadKind { + region: region(), + name: "f", + args: &[&int()], + reason: nash_constrain::error::KindProblem::Mismatch { + expected: &nash_ast::Kind::Arrow(&nash_ast::Kind::Type, &nash_ast::Kind::Type), + actual: &nash_ast::Kind::Type + } + } +); +error_snapshot!( + infinite_kind, + Error::BadKind { + region: region(), + name: "f", + args: &[&ErrorType::FlexVar("a")], + reason: nash_constrain::error::KindProblem::Infinite + } +); +error_snapshot!( + ambiguous_type, + Error::AmbiguousType { + region: region(), + name: "value", + variable: &ErrorType::FlexVar("a"), + predicates: &[nash_constrain::error::AmbiguousPredicate { + trait_: nash_ast::primitives::num_trait(), + args: &[&ErrorType::FlexVar("a")] + }] + } +); +error_snapshot!( + contradictory_representation, + Error::ContradictoryRepresentation { + region: region(), + name: "f", + typ: &ErrorType::RigidVar("a"), + requirements: &[ + nash_ast::primitives::ReprTrait::Big, + nash_ast::primitives::ReprTrait::Little + ] + } +); +error_snapshot!( + polymorphic_recursion, + Error::PolymorphicRecursion { + region: region(), + name: "f", + trait_: nash_ast::primitives::eq_trait(), + args: &[&ErrorType::FlexVar("a")] + } +); +error_snapshot!( + unresolved_constraint, + Error::UnresolvedConstraint { + region: region(), + name: "f", + trait_: nash_ast::primitives::eq_trait(), + args: &[&ErrorType::FlexVar("a")] + } +); +error_snapshot!( + unresolved_application, + Error::UnresolvedApplication { + region: region(), + name: "f", + head: &ErrorType::FlexVar("f"), + args: &[&int()] + } +); +error_snapshot!( + impl_resolution_limit, + Error::ImplResolutionLimit { + region: region(), + name: "f", + trait_: nash_ast::primitives::eq_trait() + } +); +error_snapshot!( + missing_constraint, + Error::MissingConstraint { + region: region(), + name: "==", + trait_: nash_ast::primitives::eq_trait(), + args: &[&ErrorType::RigidVar("a")], + binder: &nash_region::Located::at(region(), "f") + } +); +error_snapshot!( + annotation_variable_escapes, + Error::AnnotationVariableEscapes { + region: region(), + name: Some("f"), + variable: &ErrorType::RigidVar("a") + } +); +error_snapshot!( + missing_impl, + Error::MissingImpl { + region: region(), + name: "==", + trait_: nash_ast::primitives::eq_trait(), + args: &[&ErrorType::Type { + home: nash_ast::ModuleName { + package: None, + name: "Main" + }, + name: "step", + args: &[] + }], + available: &[&[nash_ast::Head::Named { + reference: nash_ast::QualifiedName { + home: nash_ast::primitives::builtin_home(), + name: "int" + }, + args: &[] + }]], + because: &[] + } +); +error_snapshot!( + missing_storable_constraint_for_list_element, + Error::MissingImpl { + region: region(), + name: "values", + trait_: nash_ast::primitives::ReprTrait::Storable.qualified(), + args: &[&ErrorType::Lambda(&int(), &int(), &[])], + available: &[], + because: &[nash_constrain::error::Requirement::Formation( + &ErrorType::Type { + home: nash_ast::primitives::builtin_home(), + name: "list", + args: &[&ErrorType::Lambda(&int(), &int(), &[])] + } + )] + } +); + +#[test] +fn every_category() { + let categories = [ + Category::List, + Category::String, + Category::If, + Category::Case, + Category::CallResult(MaybeName::FuncName("f")), + Category::CallResult(MaybeName::CtorName("Box")), + Category::CallResult(MaybeName::OpName("+")), + Category::CallResult(MaybeName::NoName), + Category::Lambda, + Category::Accessor("field"), + Category::Access("field"), + Category::Record, + Category::Tuple, + Category::Unit, + Category::Local("local"), + Category::Foreign("foreign"), + ]; + insta::assert_snapshot!( + categories + .into_iter() + .map(|category| add_category("It is", category)) + .collect::>() + .join("\n") + ); +} +#[test] +fn every_pattern_category() { + let categories = [ + PCategory::Record, + PCategory::Unit, + PCategory::Tuple, + PCategory::List, + PCategory::Ctor("Box"), + PCategory::Int, + PCategory::Bytes, + PCategory::Str, + PCategory::Bool, + ]; + insta::assert_snapshot!( + categories + .into_iter() + .map(|category| add_pattern_category("It matches", category)) + .collect::>() + .join("\n") + ); +} +#[test] +fn every_subcontext() { + for (name, context) in [ + ("typed_if", SubContext::TypedIfBranch(1)), + ("typed_case", SubContext::TypedCaseBranch(1)), + ] { + insta::assert_snapshot!( + name, + show(&Error::BadExpr( + region(), + Category::String, + &string(), + Expected::FromAnnotation("f", 0, context, &int()) + )) + ); + } +} +#[test] +fn expression_and_pattern_without_expectation() { + insta::assert_snapshot!( + "expression_without_expectation", + show(&Error::BadExpr( + region(), + Category::String, + &string(), + Expected::NoExpectation(&int()) + )) + ); + insta::assert_snapshot!( + "pattern_without_expectation", + show(&Error::BadPattern( + region(), + PCategory::Str, + &string(), + PExpected::NoExpectation(&int()) + )) + ); +} + +fn type_error_reports(input: &str) -> String { + let bump = bumpalo::Bump::new(); + let source = bump.alloc_str(input); + let module = nash_parse::Parser::new(&bump, source.as_bytes()) + .module() + .expect("parse fixture"); + let localizer = Localizer::from_module(&module, &[]); + let canonical = nash_can::canonicalize(&bump, nash_can::Context::default(), &module) + .expect("canonicalize fixture"); + let mut uf = nash_constrain::UnionFind::new(); + let constraint = nash_constrain::constrain(&bump, &mut uf, &canonical.module); + let errors = nash_solve::run(&bump, &mut uf, &constraint, &canonical.tables) + .expect_err("fixture must type-fail"); + assert!(!errors.is_empty()); + errors + .iter() + .map(|error| { + crate::render_plain( + &to_report(&localizer, error), + &crate::Source::new(source), + "Main.nash", + ) + }) + .collect::>() + .join("\n") +} +#[test] +fn source_pipeline_annotation_body() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf : A\nf = B\n" + )); +} +#[test] +fn source_pipeline_call_argument() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf : A -> A\nf x = x\ng = f B\n" + )); +} +#[test] +fn source_pipeline_if_branches() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf condition = if condition then A else B\n" + )); +} +#[test] +fn source_pipeline_case_branches() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf a =\n case a of\n A -> A\n _ -> B\n" + )); +} +#[test] +fn source_pipeline_list_entries() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf = [A, B]\n" + )); +} + +#[test] +fn source_pipeline_if_condition() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\nf = if A then A else A\n" + )); +} +#[test] +fn source_pipeline_call_second_argument() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf : A -> A -> A\nf x y = x\ng = f A B\n" + )); +} +#[test] +fn source_pipeline_pattern_typed_arg() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\nf : B -> B\nf A = B\n" + )); +} +#[test] +fn source_pipeline_pattern_ctor_arg() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\ntype Box = Box A\nf (Box B) = A\n" + )); +} +#[test] +fn source_pipeline_record_update_type() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype B = B\ntype alias Person = { age : A }\nf : Person -> Person\nf p = { p | age = B }\n" + )); +} +#[test] +fn source_pipeline_record_access() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntype alias Person = { age : A }\nf : Person -> A\nf p = p.aeg\n" + )); +} +#[test] +fn source_pipeline_infinite_type() { + insta::assert_snapshot!(type_error_reports("module Main exposing (..)\nf x = x x\n")); +} +#[test] +fn source_pipeline_missing_impl() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntype A = A\ntrait Eq 'a where\n eq : 'a -> 'a -> Builtin.bool\nf : A -> Builtin.bool\nf a = eq a a\n" + )); +} +#[test] +fn source_pipeline_missing_constraint() { + insta::assert_snapshot!(type_error_reports( + "module Main exposing (..)\ntrait Eq 'a where\n eq : 'a -> 'a -> Builtin.bool\nf : 'a -> Builtin.bool\nf a = eq a a\n" + )); +} + +#[test] +fn example_one_big_little_annotation() { + let source = "module Ledger exposing (settle)\n\ntype alias Account = { owner : Bytes, balance : Int }\n\nbalanceOf : Account -> Int\nbalanceOf account = account.balance\n\nsettle : list Account -> list int\nsettle accounts =\n List.map balanceOf accounts\n"; + let home = nash_ast::primitives::builtin_home(); + let big = ErrorType::Type { + home, + name: "Int", + args: &[], + }; + let little = int(); + let actual = ErrorType::Type { + home, + name: "list", + args: &[&big], + }; + let expected = ErrorType::Type { + home, + name: "list", + args: &[&little], + }; + let error = Error::BadExpr( + Region::new(Position::new(10, 5), Position::new(10, 32)), + Category::CallResult(MaybeName::FuncName("List.map")), + &actual, + Expected::FromAnnotation("settle", 1, SubContext::TypedBody, &expected), + ); + let report = to_report(&Localizer::from_names(["Builtin"]), &error); + insta::assert_snapshot!(crate::render_plain( + &report, + &crate::Source::new(source), + "src/Ledger.nash" + )); +} + +#[test] +fn operator_branches() { + let l = Localizer::from_names(["Builtin"]); + for (name, op, left) in [ + ("minus_left", "-", true), + ("multiply_left", "*", true), + ("power_left", "^", true), + ("division_left", "/", true), + ("boolean_left", "&&", true), + ("compare_left", "<", true), + ("append_left", "++", true), + ("pipe_left_not_function", "<|", true), + ("custom_left", "", true), + ("plus_right", "+", false), + ("minus_right", "-", false), + ("multiply_right", "*", false), + ("power_right", "^", false), + ("division_right", "/", false), + ("boolean_right", "||", false), + ("pipe_left_argument", "<|", false), + ("custom_right", "", false), + ] { + let actual = string(); + let expected = int(); + let context = if left { + Context::OpLeft(op) + } else { + Context::OpRight(op) + }; + let report = to_report( + &l, + &Error::BadExpr( + region(), + Category::String, + &actual, + Expected::FromContext(region(), context, &expected), + ), + ); + insta::assert_snapshot!( + name, + Doc::stack([report.before, report.after]).render(80, false) + ); + } +} + +#[test] +fn problem_hints() { + for (name, problem) in [ + ("hint_arity_fewer", Problem::ArityMismatch(1, 3)), + ("hint_arity_more", Problem::ArityMismatch(3, 1)), + ( + "hint_missing_fields", + Problem::FieldsMissing(vec!["name", "age"]), + ), + ( + "hint_field_typo", + Problem::FieldTypo("naem", vec!["age", "name"]), + ), + ( + "hint_big_little_need", + Problem::BigLittle { + big: "Int", + little: "int", + direction: Direction::Need, + }, + ), + ("hint_option", Problem::AnythingFromOption), + ( + "hint_double_rigid", + Problem::BadRigidVar("a", &ErrorType::RigidVar("b")), + ), + ] { + insta::assert_snapshot!( + name, + Doc::stack(problem_to_hint(&problem)).render(80, false) + ); + } +} + +#[test] +fn missing_impl_local_union_deriving_not_yet_available() { + let source = "module Main exposing (..)\ntype step = Done | Next Builtin.int\n"; + let bump = bumpalo::Bump::new(); + let module = nash_parse::Parser::new(&bump, source.as_bytes()) + .module() + .unwrap(); + let l = Localizer::from_module(&module, &[]); + let typ = ErrorType::Type { + home: nash_ast::ModuleName { + package: None, + name: "Main", + }, + name: "step", + args: &[], + }; + let error = Error::MissingImpl { + region: region(), + name: "==", + trait_: nash_ast::primitives::eq_trait(), + args: &[&typ], + available: &[], + because: &[], + }; + let report = to_report(&l, &error); + let text = report.after.render(80, false); + assert!( + text.split_whitespace() + .collect::>() + .join(" ") + .contains("automatic deriving is not available yet") + ); + assert!(text.contains("eq a b = ...")); + insta::assert_snapshot!(text); +} + +#[test] +fn missing_impl_imported_or_custom_trait_has_no_derive_hint() { + let source = "module Main exposing (..)\ntype step = Done\n"; + let bump = bumpalo::Bump::new(); + let module = nash_parse::Parser::new(&bump, source.as_bytes()) + .module() + .unwrap(); + let l = Localizer::from_module(&module, &[]); + for (home, trait_) in [ + ( + nash_ast::ModuleName { + package: None, + name: "Imported", + }, + nash_ast::primitives::eq_trait(), + ), + ( + nash_ast::ModuleName { + package: None, + name: "Main", + }, + nash_ast::QualifiedName { + home: nash_ast::ModuleName { + package: None, + name: "Main", + }, + name: "Eq", + }, + ), + ] { + let typ = ErrorType::Type { + home, + name: "step", + args: &[], + }; + let error = Error::MissingImpl { + region: region(), + name: "eq", + trait_, + args: &[&typ], + available: &[], + because: &[], + }; + assert!( + !to_report(&l, &error) + .after + .render(80, false) + .contains("@derive") + ); + } +} + +#[test] +fn append_number_hints_wrap() { + for (name, context, expected) in [ + ("append_int_left", Context::OpLeft("++"), string()), + ("append_int_to_string", Context::OpRight("++"), string()), + ] { + let error = Error::BadExpr( + region(), + Category::CallResult(MaybeName::NoName), + &int(), + Expected::FromContext(region(), context, &expected), + ); + insta::assert_snapshot!(name, show(&error)); + } +} diff --git a/crates/nash-report/src/type_/traits.rs b/crates/nash-report/src/type_/traits.rs new file mode 100644 index 00000000..9b0563f3 --- /dev/null +++ b/crates/nash-report/src/type_/traits.rs @@ -0,0 +1,464 @@ +//! Nash-specific inference, trait, kind and representation diagnostics. +use super::*; +use nash_ast::primitives::ReprTrait; +use nash_ast::{Head, Kind, QualifiedName}; +use nash_constrain::error::{AmbiguousPredicate, KindProblem, Requirement}; + +fn type_docs(l: &Localizer, types: &[&ErrorType<'_>]) -> Doc { + Doc::hsep(types.iter().map(|ty| type_diff::to_doc(l, Ctx::App, ty))) +} +fn predicate(l: &Localizer, trait_: QualifiedName<'_>, args: &[&ErrorType<'_>]) -> Doc { + Doc::hsep( + std::iter::once(l.to_doc(trait_.home, trait_.name)) + .chain(args.iter().map(|ty| type_diff::to_doc(l, Ctx::App, ty))), + ) +} +fn report( + title: &str, + region: Region, + before: String, + docs: impl IntoIterator, +) -> Report { + Report::snippet(title, region, None, Doc::reflow(&before), Doc::stack(docs)) +} +fn kind(kind: &Kind<'_>) -> String { + match kind { + Kind::Type => "Type".into(), + Kind::Arrow(arg, result) => format!( + "{} -> {}", + if matches!(arg, Kind::Arrow(..)) { + format!("({})", self::kind(arg)) + } else { + self::kind(arg) + }, + self::kind(result) + ), + } +} + +pub(super) fn bad_kind( + l: &Localizer, + region: Region, + name: &str, + args: &[&ErrorType<'_>], + reason: &KindProblem<'_>, +) -> Report { + let mut docs = vec![ + Doc::reflow("The type arguments at this use are:"), + Doc::indent(4, type_docs(l, args)), + ]; + let (title, before) = match reason { + KindProblem::Infinite => { + docs.push(Doc::reflow("A type constructor cannot be applied to itself in this way: its kind would have to contain itself forever.")); + ( + "INFINITE KIND", + format!("The use of `{name}` would require an infinite kind:"), + ) + } + KindProblem::Mismatch { expected, actual } => { + docs.extend([Doc::reflow("I need kind:"), Doc::indent(4, Doc::text(kind(expected)).dullyellow()), Doc::reflow("But this use has kind:"), Doc::indent(4, Doc::text(kind(actual)).dullyellow()), Doc::to_simple_hint("A type constructor needs all of its required type arguments before it can be used as a value type. An annotation's quantified kinds cannot be specialized by its body.")]); + ( + "KIND MISMATCH", + format!("The type arguments to `{name}` have incompatible kinds:"), + ) + } + }; + report(title, region, before, docs) +} + +pub(super) fn ambiguous_type( + l: &Localizer, + region: Region, + name: &str, + variable: &ErrorType<'_>, + predicates: &[AmbiguousPredicate<'_>], +) -> Report { + report( + "AMBIGUOUS TYPE", + region, + format!("I cannot determine the type needed by `{name}`:"), + [ + Doc::reflow("This type variable is still unresolved:"), + Doc::indent(4, type_diff::to_doc(l, Ctx::None, variable)), + Doc::reflow("It must satisfy these constraints:"), + Doc::indent( + 4, + Doc::vcat(predicates.iter().map(|p| predicate(l, p.trait_, p.args))), + ), + Doc::to_simple_hint( + "Add a type annotation that fixes this type. Each constraint needs enough information to select an impl.", + ), + ], + ) +} + +pub(super) fn contradictory_representation( + l: &Localizer, + region: Region, + name: &str, + typ: &ErrorType<'_>, + requirements: &[ReprTrait], +) -> Report { + report( + "CONTRADICTORY REPRESENTATION", + region, + format!("`{name}` requires incompatible representations:"), + [ + Doc::reflow( + "This type is required to satisfy all of the following representation constraints:", + ), + Doc::indent(4, type_diff::to_doc(l, Ctx::None, typ)), + Doc::indent( + 4, + Doc::text( + requirements + .iter() + .map(|r| r.name()) + .collect::>() + .join(", "), + ), + ), + Doc::reflow( + "No type can satisfy all of them. Check where this value is used as Big Data and where a little builtin representation is required.", + ), + ], + ) +} + +pub(super) fn polymorphic_recursion( + l: &Localizer, + region: Region, + name: &str, + trait_: QualifiedName<'_>, + args: &[&ErrorType<'_>], +) -> Report { + report( + "POLYMORPHIC RECURSION", + region, + format!("The recursive use of `{name}` keeps changing its trait arguments:"), + [ + Doc::reflow("The growing requirement is:"), + Doc::indent(4, predicate(l, trait_, args)), + Doc::reflow( + "Each trip around this recursive call adds another impl wrapper. I cannot construct a finite set of evidence arguments for it.", + ), + Doc::to_simple_hint( + "Keep the trait arguments the same across recursive calls, or split the work into functions with explicit type annotations.", + ), + ], + ) +} + +pub(super) fn unresolved_constraint( + l: &Localizer, + region: Region, + name: &str, + trait_: QualifiedName<'_>, + args: &[&ErrorType<'_>], +) -> Report { + report( + "UNRESOLVED CONSTRAINT", + region, + format!("I could not establish the constraint required by `{name}`:"), + [ + Doc::indent(4, predicate(l, trait_, args)), + Doc::reflow("Type inference finished without a proof for this requirement."), + Doc::to_simple_hint( + "Add a type annotation to resolve the remaining type variables, then check that the required impl or annotation constraint is available.", + ), + ], + ) +} + +pub(super) fn unresolved_application( + l: &Localizer, + region: Region, + name: &str, + head: &ErrorType<'_>, + args: &[&ErrorType<'_>], +) -> Report { + report( + "UNRESOLVED TYPE APPLICATION", + region, + format!("I could not establish the datatype context required by `{name}`:"), + [ + Doc::reflow("The unresolved type application is:"), + Doc::indent( + 4, + Doc::hsep( + std::iter::once(type_diff::to_doc(l, Ctx::App, head)) + .chain(args.iter().map(|ty| type_diff::to_doc(l, Ctx::App, ty))), + ), + ), + Doc::to_simple_hint( + "Add a type annotation that determines the type constructor and its arguments. Its datatype constraints must be satisfied before this value can be used.", + ), + ], + ) +} + +pub(super) fn resolution_limit( + l: &Localizer, + region: Region, + name: &str, + trait_: QualifiedName<'_>, +) -> Report { + report( + "IMPL RESOLUTION LIMIT", + region, + format!("I reached the impl resolution limit while checking `{name}`:"), + [ + Doc::reflow(&format!( + "The search for `{}` evidence exceeded the compiler's work limit.", + l.to_string(trait_.home, trait_.name) + )), + Doc::reflow( + "This requirement has not been checked completely. The other diagnostics from this compilation still apply.", + ), + Doc::to_simple_hint( + "Check for a cycle or a growing chain of impl constraints, and simplify the requirement before trying again.", + ), + ], + ) +} + +pub(super) fn missing_constraint( + l: &Localizer, + region: Region, + name: &str, + trait_: QualifiedName<'_>, + args: &[&ErrorType<'_>], + binder: &nash_region::Located<&str>, +) -> Report { + let wanted = predicate(l, trait_, args); + let representation = ReprTrait::of(trait_).is_some(); + let mut result = report( + "MISSING CONSTRAINT", + region, + format!( + "`{name}` needs a constraint that the annotation for `{}` does not promise:", + binder.value + ), + [ + Doc::indent(4, wanted.clone()), + Doc::reflow(&format!( + "The type variables in `{}` must work for every type allowed by its annotation. I cannot assume this {}constraint without it being declared.", + binder.value, + if representation { + "representation " + } else { + "" + } + )), + Doc::to_simple_hint(&format!( + "Add `{}` to the context of the `{}` type annotation.", + wanted.render(80, false), + binder.value + )), + ], + ); + result.snippet = crate::Snippet::Pair { + first: crate::Label { + region: binder.region, + text: format!("annotation for `{}`", binder.value), + }, + second: crate::Label { + region, + text: format!("needs `{}`", wanted.render(80, false)), + }, + }; + result +} + +pub(super) fn annotation_variable_escapes( + l: &Localizer, + region: Region, + name: Option<&str>, + variable: &ErrorType<'_>, +) -> Report { + report( + "ANNOTATION VARIABLE ESCAPES", + region, + format!( + "This annotation{} quantifies a type variable fixed by an enclosing scope:", + name.map_or_else(String::new, |name| format!(" for `{name}`")) + ), + [ + Doc::indent(4, type_diff::to_doc(l, Ctx::None, variable)), + Doc::reflow( + "The variable cannot stand for every type here because the surrounding definition has already fixed it.", + ), + Doc::to_simple_hint( + "Use the enclosing type variable consistently, or change the annotation so that it does not promise a fresh independent type.", + ), + ], + ) +} + +fn requirement(l: &Localizer, requirement: &Requirement<'_>) -> Doc { + match requirement { + Requirement::Trait { trait_, args } => predicate(l, *trait_, args), + Requirement::Application { head, args } => Doc::hsep( + std::iter::once(type_diff::to_doc(l, Ctx::App, head)) + .chain(args.iter().map(|ty| type_diff::to_doc(l, Ctx::App, ty))), + ), + Requirement::Formation(ty) => { + Doc::hsep([Doc::text("forming"), type_diff::to_doc(l, Ctx::None, ty)]) + } + } +} + +fn head_doc(l: &Localizer, head: &Head<'_>, nested: bool) -> Doc { + let (doc, parens) = match head { + Head::Var(index) => (Doc::text(format!("'a{index}")), false), + Head::Named { reference, args } => ( + Doc::hsep( + std::iter::once(l.to_doc(reference.home, reference.name)) + .chain(args.iter().map(|head| head_doc(l, head, true))), + ), + !args.is_empty(), + ), + Head::Tuple(items) => ( + Doc::cat([ + Doc::text("("), + Doc::hcat(items.iter().enumerate().map(|(index, head)| { + Doc::cat([ + Doc::text(if index == 0 { "" } else { ", " }), + head_doc(l, head, false), + ]) + })), + Doc::text(")"), + ]), + false, + ), + Head::Function(arg, result) => ( + Doc::hsep([ + head_doc(l, arg, true), + Doc::text("->"), + head_doc(l, result, false), + ]), + true, + ), + }; + if nested && parens { + Doc::cat([Doc::text("("), doc, Doc::text(")")]) + } else { + doc + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn missing_impl( + l: &Localizer, + region: Region, + name: &str, + trait_: QualifiedName<'_>, + args: &[&ErrorType<'_>], + available: &[&[Head<'_>]], + because: &[Requirement<'_>], +) -> Report { + let trait_name = l.to_string(trait_.home, trait_.name); + let args_doc = type_docs(l, args); + let head = args_doc.render(80, false); + let mut docs; + let before; + if let Some(representation) = ReprTrait::of(trait_) { + before = format!("`{name}` requires a representation that this type does not provide:"); + let admitted = match representation { + ReprTrait::Big => "Big", + ReprTrait::Const => "Const", + ReprTrait::Term => "Term", + ReprTrait::Storable => "Big or Const", + ReprTrait::Little => "Const or Term", + }; + docs = vec![ + Doc::indent(4, args_doc), + Doc::reflow(&format!( + "`{}` accepts {admitted} representations. This argument does not meet that requirement.", + representation.name() + )), + Doc::to_simple_hint( + "Representation constraints are compiler-owned. Adding an impl cannot change a type's representation; change the datatype or convert the value explicitly.", + ), + ]; + } else { + before = format!("I cannot find an `{trait_name}` impl for `{head}`:"); + let thing = if !name.is_empty() + && name + .chars() + .all(|c| c.is_ascii() && nash_parse::symbol::is_binop_char(c as u8)) + { + format!("The ({name}) operator") + } else { + format!("`{name}`") + }; + docs = vec![ + Doc::reflow(&format!( + "{thing} needs its arguments to implement `{trait_name}`, and here they are:" + )), + Doc::indent(4, args_doc), + Doc::reflow(&format!( + "But there is no `impl {trait_name} {head}` in this module or in any import." + )), + ]; + if !available.is_empty() { + docs.push(Doc::reflow(&format!( + "`{trait_name}` is implemented for these heads:" + ))); + docs.push(Doc::indent( + 4, + Doc::vcat( + available + .iter() + .take(4) + .map(|heads| Doc::hsep(heads.iter().map(|head| head_doc(l, head, true)))), + ), + )); + } + docs.extend(derive_hint(l, trait_, args, &trait_name, &head)); + } + if !because.is_empty() { + docs.push(Doc::reflow("This requirement came from the following chain, from the original use to the failing requirement:")); + docs.push(Doc::indent( + 4, + Doc::vcat(because.iter().map(|reason| requirement(l, reason))), + )); + } + report("MISSING IMPL", region, before, docs) +} + +fn derive_hint( + l: &Localizer, + trait_: QualifiedName<'_>, + args: &[&ErrorType<'_>], + trait_name: &str, + head: &str, +) -> Vec { + let core_trait = + trait_.home.package == Some(nash_ast::primitives::CORE) && trait_.home.name == trait_.name; + let derivable = + core_trait && matches!(trait_.name, "Eq" | "Ord" | "Show" | "ToData" | "FromData"); + let local_union = + matches!(args, [ErrorType::Type { home, name, .. }] if l.is_local_union(*home, name)); + let mut docs = Vec::new(); + if derivable && local_union { + docs.push(Doc::to_simple_hint(&format!("This local datatype is a candidate for `@derive({trait_name})`, but automatic deriving is not available yet. Write the impl by hand:"))); + } else { + docs.push(Doc::to_simple_hint(&format!( + "Write an `impl {trait_name} {head}` that provides the trait's methods:" + ))); + } + let method = if core_trait && trait_.name == "Eq" { + " eq a b = ..." + } else { + " ..." + }; + docs.push(Doc::indent( + 4, + Doc::vcat([ + Doc::text(format!("impl {trait_name} {head} where")), + Doc::text(method), + ]), + )); + docs +} From c533ee3be0527e026e22eb32db4dfc30f721af8f Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 09/12] feat(report): collect phase errors and warnings Signed-off-by: microproofs --- crates/nash-report/src/lib.rs | 29 +++ crates/nash-report/src/pattern.rs | 192 ++++++++++++++++++ ..._literal_witnesses_escape_source_text.snap | 8 + ...pattern__tests__missing_patterns_data.snap | 24 +++ ...__tests__missing_patterns_nested_list.snap | 22 ++ ...rt__pattern__tests__redundant_pattern.snap | 16 ++ ...sh_report__pattern__tests__unsafe_arg.snap | 19 ++ ...port__pattern__tests__unsafe_destruct.snap | 23 +++ ...rt__warning__tests__unused_definition.snap | 18 ++ ...report__warning__tests__unused_import.snap | 14 ++ ...rning__tests__unused_variable_pattern.snap | 17 ++ crates/nash-report/src/warning.rs | 120 +++++++++++ 12 files changed, 502 insertions(+) create mode 100644 crates/nash-report/src/pattern.rs create mode 100644 crates/nash-report/src/snapshots/nash_report__pattern__tests__literal_witnesses_escape_source_text.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_data.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_nested_list.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__pattern__tests__redundant_pattern.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_arg.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_destruct.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__warning__tests__unused_definition.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__warning__tests__unused_import.snap create mode 100644 crates/nash-report/src/snapshots/nash_report__warning__tests__unused_variable_pattern.snap create mode 100644 crates/nash-report/src/warning.rs diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index dc0c183d..8ec477ec 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -11,6 +11,7 @@ pub mod code; pub mod doc; pub mod json; pub mod localizer; +pub mod pattern; pub mod render_type; pub mod suggest; pub mod syntax; @@ -18,6 +19,7 @@ pub mod type_; pub mod type_diff; pub use localizer::Localizer; mod render; +pub mod warning; use nash_region::Region; @@ -155,3 +157,30 @@ impl ModuleReports { }); } } + +/// Phase error data is converted while its arena is still alive. +pub enum ModuleError<'a> { + Syntax(nash_parse::error::Error<'a>), + Names(Vec>), + Types(Localizer, Vec>), + Patterns(Vec>), +} + +pub fn to_reports( + source: &Source<'_>, + expected_module_name: &str, + error: &ModuleError<'_>, +) -> Vec { + match error { + ModuleError::Syntax(error) => vec![syntax::to_report(source, error)], + ModuleError::Names(errors) => errors + .iter() + .map(|error| canonicalize::to_report_with_name(source, error, expected_module_name)) + .collect(), + ModuleError::Types(localizer, errors) => errors + .iter() + .map(|error| type_::to_report(localizer, error)) + .collect(), + ModuleError::Patterns(errors) => errors.iter().map(pattern::to_report).collect(), + } +} diff --git a/crates/nash-report/src/pattern.rs b/crates/nash-report/src/pattern.rs new file mode 100644 index 00000000..5f7acd5b --- /dev/null +++ b/crates/nash-report/src/pattern.rs @@ -0,0 +1,192 @@ +//! `Reporting/Error/Pattern.hs`. +use crate::doc::int_to_ordinal; +use crate::{Doc, Report}; + +use nash_nitpick::render::{RenderContext, pattern_to_string}; +use nash_nitpick::{Context, Error, Pattern}; + +pub fn to_report(error: &Error<'_>) -> Report { + match error { + Error::Redundant { case_region, pattern_region, index } => Report::snippet( + "REDUNDANT PATTERN", + *pattern_region, + Some(*pattern_region), + Doc::reflow(&format!("The {} pattern is redundant:", int_to_ordinal(*index))), + Doc::reflow("Any value with this shape will be handled by a previous pattern, so it should be removed."), + ) + .with_region(*case_region), + + Error::Incomplete { region, context, unhandled } => match context { + Context::BadArg => Report::snippet( + "UNSAFE PATTERN", + *region, + None, + Doc::text("This pattern does not cover all possibilities:"), + Doc::stack([ + Doc::text("Other possibilities include:"), + unhandled_patterns_to_doc_block(unhandled), + Doc::reflow( + "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.", + ), + ]), + ), + Context::BadDestruct => Report::snippet( + "UNSAFE PATTERN", + *region, + None, + Doc::text("This pattern does not cover all possible values:"), + Doc::stack([ + Doc::text("Other possibilities include:"), + unhandled_patterns_to_doc_block(unhandled), + Doc::reflow( + "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.", + ), + Doc::to_simple_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!", + ), + ]), + ), + Context::BadCase => Report::snippet( + "MISSING PATTERNS", + *region, + None, + Doc::text("This `case` does not have branches for all possibilities:"), + Doc::stack([ + Doc::text("Missing possibilities include:"), + unhandled_patterns_to_doc_block(unhandled), + Doc::reflow("I would have to crash if I saw one of those. Add branches for them!"), + Doc::link( + "Hint", + "If you want to write the code for each branch later, use `todo` as a placeholder. Read", + "missing-patterns", + "for more guidance on this workflow.", + ), + ]), + ), + }, + } +} + +fn unhandled_patterns_to_doc_block(unhandled: &[Pattern<'_>]) -> Doc { + Doc::indent( + 4, + Doc::vcat( + unhandled + .iter() + .map(|p| Doc::text(pattern_to_string(RenderContext::Unambiguous, *p))), + ) + .dullyellow(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Snippet, Source, render_plain}; + + fn reports(input: &str) -> Vec { + let bump = bumpalo::Bump::new(); + let text = bump.alloc_str(input); + let module = nash_parse::Parser::new(&bump, text.as_bytes()) + .module() + .expect("parse"); + let interfaces = std::collections::BTreeMap::from([( + "Builtin", + nash_can::kinds::builtin_interface(&bump), + )]); + let can = nash_can::canonicalize( + &bump, + nash_can::Context { + package: None, + interfaces: Some(&interfaces), + }, + &module, + ) + .expect("canonicalize"); + let mut uf = nash_constrain::UnionFind::new(); + let constraint = nash_constrain::constrain(&bump, &mut uf, &can.module); + nash_solve::run(&bump, &mut uf, &constraint, &can.tables) + .expect("solve before checking coverage"); + nash_nitpick::check(&bump, &can.module) + .expect_err("expected pattern errors") + .iter() + .map(to_report) + .collect() + } + fn render(input: &str) -> String { + let reports = reports(input); + assert_eq!(reports.len(), 1); + render_plain(&reports[0], &Source::new(input), "src/Main.nash") + } + #[test] + fn missing_patterns_data() { + insta::assert_snapshot!(render(indoc::indoc! {r#" + module Main exposing (..) + import Builtin exposing (Data(..)) + tag d = + case d of + Constr _ _ -> () + List _ -> () + "#})); + } + #[test] + fn missing_patterns_nested_list() { + insta::assert_snapshot!(render(indoc::indoc! {r#" + module Main exposing (..) + first xs = + case xs of + [] -> 0 + [] :: _ -> 1 + "#})); + } + #[test] + fn unsafe_arg() { + insta::assert_snapshot!(render("module Main exposing (..)\nf [x] = x\n")); + } + #[test] + fn unsafe_destruct() { + insta::assert_snapshot!(render(indoc::indoc! {r#" + module Main exposing (..) + f xs = + let + [x] = xs + in + x + "#})); + } + #[test] + fn redundant_pattern() { + let input = indoc::indoc! {r#" + module Main exposing (..) + f xs = + case xs of + _ -> 0 + [] -> 1 + "#}; + let reports = reports(input); + assert_eq!(reports.len(), 1); + let report = &reports[0]; + assert_eq!(report.region.start.line, 5); + assert!( + matches!(report.snippet, Snippet::Region{region,highlight:Some(h)} if region.start.line == 3 && h == report.region) + ); + insta::assert_snapshot!(render_plain(report, &Source::new(input), "src/Main.nash")); + } + #[test] + fn literal_witnesses_escape_source_text() { + use nash_nitpick::Literal; + let patterns = [ + Pattern::Anything, + Pattern::Literal(Literal::Int(-7)), + Pattern::Literal(Literal::Str("a\n\"b")), + Pattern::Literal(Literal::Bytes(&[0, 255])), + ]; + let doc = unhandled_patterns_to_doc_block(&patterns); + insta::assert_snapshot!(doc.render(80, false)); + assert!(doc.chunks(80).iter().any(|c| matches!(c,crate::doc::Chunk::Styled{style,..} if style.color == Some(crate::doc::Color{base:crate::doc::BaseColor::Yellow,vivid:false})))); + } +} diff --git a/crates/nash-report/src/snapshots/nash_report__pattern__tests__literal_witnesses_escape_source_text.snap b/crates/nash-report/src/snapshots/nash_report__pattern__tests__literal_witnesses_escape_source_text.snap new file mode 100644 index 00000000..30a485d0 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__pattern__tests__literal_witnesses_escape_source_text.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-report/src/pattern.rs +expression: "doc.render(80, false)" +--- + _ + -7 + "a\n\"b" + #"00ff" diff --git a/crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_data.snap b/crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_data.snap new file mode 100644 index 00000000..b3b59b15 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_data.snap @@ -0,0 +1,24 @@ +--- +source: crates/nash-report/src/pattern.rs +expression: "render(indoc::indoc!\n{r#\"\n module Main exposing (..)\n import Builtin exposing (Data(..))\n tag d =\n case d of\n Constr _ _ -> ()\n List _ -> ()\n \"#})" +--- +MISSING PATTERNS + + × This `case` does not have branches for all possibilities: + ╭─[src/Main.nash:4:5] + 3 │ tag d = + 4 │ ╭─▶ case d of + 5 │ │ Constr _ _ -> () + 6 │ ╰─▶ List _ -> () + ╰──── + help: Missing possibilities include: + + 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. diff --git a/crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_nested_list.snap b/crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_nested_list.snap new file mode 100644 index 00000000..69013290 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__pattern__tests__missing_patterns_nested_list.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-report/src/pattern.rs +expression: "render(indoc::indoc!\n{r#\"\n module Main exposing (..)\n first xs =\n case xs of\n [] -> 0\n [] :: _ -> 1\n \"#})" +--- +MISSING PATTERNS + + × This `case` does not have branches for all possibilities: + ╭─[src/Main.nash:3:5] + 2 │ first xs = + 3 │ ╭─▶ case xs of + 4 │ │ [] -> 0 + 5 │ ╰─▶ [] :: _ -> 1 + ╰──── + help: Missing possibilities include: + + (_ :: _) :: _ + + 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. diff --git a/crates/nash-report/src/snapshots/nash_report__pattern__tests__redundant_pattern.snap b/crates/nash-report/src/snapshots/nash_report__pattern__tests__redundant_pattern.snap new file mode 100644 index 00000000..80ae5327 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__pattern__tests__redundant_pattern.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-report/src/pattern.rs +expression: "render_plain(report, &Source::new(input), \"src/Main.nash\")" +--- +REDUNDANT PATTERN + + × The 2nd pattern is redundant: + ╭─[src/Main.nash:5:9] + 2 │ f xs = + 3 │ case xs of + 4 │ _ -> 0 + 5 │ [] -> 1 + · ── + ╰──── + help: Any value with this shape will be handled by a previous pattern, so it should be + removed. diff --git a/crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_arg.snap b/crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_arg.snap new file mode 100644 index 00000000..29012b3c --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_arg.snap @@ -0,0 +1,19 @@ +--- +source: crates/nash-report/src/pattern.rs +expression: "render(\"module Main exposing (..)\\nf [x] = x\\n\")" +--- +UNSAFE PATTERN + + × This pattern does not cover all possibilities: + ╭─[src/Main.nash:2:3] + 1 │ module Main exposing (..) + 2 │ f [x] = x + · ─── + ╰──── + help: Other possibilities include: + + [] + + 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. diff --git a/crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_destruct.snap b/crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_destruct.snap new file mode 100644 index 00000000..edd7e92f --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__pattern__tests__unsafe_destruct.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-report/src/pattern.rs +expression: "render(indoc::indoc!\n{r#\"\n module Main exposing (..)\n f xs =\n let\n [x] = xs\n in\n x\n \"#})" +--- +UNSAFE PATTERN + + × This pattern does not cover all possible values: + ╭─[src/Main.nash:4:9] + 3 │ let + 4 │ [x] = xs + · ─── + 5 │ in + ╰──── + help: Other possibilities include: + + [] + + 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. + + 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! diff --git a/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_definition.snap b/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_definition.snap new file mode 100644 index 00000000..167f4744 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_definition.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-report/src/warning.rs +expression: "render_plain(&only_warning(input), &Source::new(input), \"src/Main.nash\")" +--- +unused definition + + ⚠ You are not using `unused` anywhere. + ╭─[src/Main.nash:4:9] + 3 │ let + 4 │ unused = 1 + · ────── + 5 │ in + ╰──── + help: Is there a typo? Maybe you intended to use `unused` 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! diff --git a/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_import.snap b/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_import.snap new file mode 100644 index 00000000..ffb93557 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_import.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-report/src/warning.rs +expression: "render_plain(&report, &Source::new(input), \"src/Main.nash\")" +--- +unused import + + ⚠ Nothing from the `Tools` module is used in this file. + ╭─[src/Main.nash:2:8] + 1 │ module Main exposing (..) + 2 │ import Tools + · ───── + 3 │ x = 1 + ╰──── + help: I recommend removing unused imports. diff --git a/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_variable_pattern.snap b/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_variable_pattern.snap new file mode 100644 index 00000000..242174f3 --- /dev/null +++ b/crates/nash-report/src/snapshots/nash_report__warning__tests__unused_variable_pattern.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-report/src/warning.rs +expression: "render_plain(&only_warning(input), &Source::new(input), \"src/Main.nash\")" +--- +unused variable + + ⚠ You are not using `unused` anywhere. + ╭─[src/Main.nash:2:3] + 1 │ module Main exposing (..) + 2 │ f unused = 1 + · ────── + ╰──── + help: Is there a typo? Maybe you intended to use `unused` somewhere but typed another + name instead? + + If you are sure there is no typo, replace `unused` with _ so future readers will + not have to wonder why it is there! diff --git a/crates/nash-report/src/warning.rs b/crates/nash-report/src/warning.rs new file mode 100644 index 00000000..495cafd8 --- /dev/null +++ b/crates/nash-report/src/warning.rs @@ -0,0 +1,120 @@ +//! Elm Reporting/Warning.hs for the warnings currently emitted by Nash. +use crate::{Doc, Report}; +use nash_can::{Warning, WarningContext}; +pub fn to_report(warning: &Warning<'_>) -> Report { + match warning { + Warning::UnusedImport { + region, + module_name, + } => Report::snippet( + "unused import", + *region, + None, + Doc::reflow(&format!( + "Nothing from the `{module_name}` module is used in this file." + )), + Doc::text("I recommend removing unused imports."), + ) + .warning(), + Warning::UnusedVariable { + region, + context, + name, + } => { + let (title, advice) = match context { + WarningContext::Def => ("unused definition", "If you are sure there is no typo, remove the definition. This way future readers will not have to wonder why it is there!".to_string()), + WarningContext::Pattern => ("unused variable", format!("If you are sure there is no typo, replace `{name}` with _ so future readers will not have to wonder why it is there!")), + }; + Report::snippet( + title, *region, None, + Doc::reflow(&format!("You are not using `{name}` anywhere.")), + Doc::stack([ + Doc::reflow(&format!("Is there a typo? Maybe you intended to use `{name}` somewhere but typed another name instead?")), + Doc::reflow(&advice), + ]), + ).warning() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Severity, Source, render_plain}; + + fn only_warning(input: &str) -> Report { + let bump = bumpalo::Bump::new(); + let text = bump.alloc_str(input); + let module = nash_parse::Parser::new(&bump, text.as_bytes()) + .module() + .expect("parse"); + let can = nash_can::canonicalize(&bump, nash_can::Context::default(), &module) + .expect("canonicalize"); + assert_eq!(can.warnings.len(), 1); + let report = to_report(&can.warnings[0]); + assert_eq!(report.severity, Severity::Warning); + report + } + #[test] + fn unused_import() { + let input = "module Main exposing (..)\nimport Tools\nx = 1\n"; + let bump = bumpalo::Bump::new(); + let interfaces = std::collections::BTreeMap::from([( + "Tools", + nash_can::Interface { + home: nash_ast::ModuleName { + package: None, + name: "Tools", + }, + impls: &[], + traits: &[], + values: &[], + aliases: &[], + unions: &[], + binops: &[], + }, + )]); + let text = bump.alloc_str(input); + let module = nash_parse::Parser::new(&bump, text.as_bytes()) + .module() + .expect("parse"); + let can = nash_can::canonicalize( + &bump, + nash_can::Context { + package: None, + interfaces: Some(&interfaces), + }, + &module, + ) + .expect("canonicalize"); + assert_eq!(can.warnings.len(), 1); + let report = to_report(&can.warnings[0]); + assert_eq!(report.severity, Severity::Warning); + insta::assert_snapshot!(render_plain(&report, &Source::new(input), "src/Main.nash")); + } + #[test] + fn unused_variable_pattern() { + let input = "module Main exposing (..)\nf unused = 1\n"; + insta::assert_snapshot!(render_plain( + &only_warning(input), + &Source::new(input), + "src/Main.nash" + )); + } + #[test] + fn unused_definition() { + let input = indoc::indoc! {r#" + module Main exposing (..) + f = + let + unused = 1 + in + 2 + "#}; + insta::assert_snapshot!(render_plain( + &only_warning(input), + &Source::new(input), + "src/Main.nash" + )); + } +} From 5afb65ca41d877eb460938611121c27aae377373 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 10/12] feat(cli): render structured build diagnostics Signed-off-by: microproofs --- Cargo.lock | 4 + crates/nash-cli/Cargo.toml | 5 + crates/nash-cli/src/cli.rs | 21 +- crates/nash-cli/src/cmd/check.rs | 179 ++++++++---- crates/nash-cli/src/cmd/mod.rs | 4 +- crates/nash-driver/Cargo.toml | 1 + crates/nash-driver/src/compile.rs | 265 ++++++++++++++---- .../src/compile/collection_tests.rs | 74 +++++ .../nash-driver/src/compile/nitpick_tests.rs | 35 +-- ...tpick_tests__impl_method_fails_module.snap | 16 +- ...k_tests__incomplete_case_fails_module.snap | 18 +- ...ck_tests__redundant_case_fails_module.snap | 13 +- ...ut_top_level_definitions_fails_module.snap | 18 +- ...k_tests__unsafe_argument_fails_module.snap | 16 +- ...ests__unsafe_destructure_fails_module.snap | 35 ++- crates/nash-driver/src/diagnostics.rs | 199 ------------- crates/nash-driver/src/graph.rs | 42 ++- crates/nash-driver/src/lib.rs | 1 - crates/nash-driver/src/project.rs | 2 +- ...orphan_and_overlap_at_the_impl_module.snap | 29 +- 20 files changed, 624 insertions(+), 353 deletions(-) create mode 100644 crates/nash-driver/src/compile/collection_tests.rs delete mode 100644 crates/nash-driver/src/diagnostics.rs diff --git a/Cargo.lock b/Cargo.lock index aa9ea5bf..6a39ec25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,11 +1920,14 @@ dependencies = [ "color-print", "dirs", "futures", + "insta", "miette", "nash-config", "nash-driver", "nash-language-server", + "nash-report", "octocrab", + "serde_json", "tar", "tokio", "tokio-util", @@ -1972,6 +1975,7 @@ dependencies = [ "nash-nitpick", "nash-parse", "nash-region", + "nash-report", "nash-solve", "nash-source", "serde", diff --git a/crates/nash-cli/Cargo.toml b/crates/nash-cli/Cargo.toml index 876e1fc9..6534ce96 100644 --- a/crates/nash-cli/Cargo.toml +++ b/crates/nash-cli/Cargo.toml @@ -21,7 +21,9 @@ color-print.workspace = true dirs.workspace = true futures.workspace = true miette.workspace = true +serde_json.workspace = true nash-config = { path = "../nash-config", version = "0.3.0" } +nash-report = { path = "../nash-report", version = "0.1.0" } nash-driver = { path = "../nash-driver", version = "0.4.0" } nash-language-server = { path = "../nash-language-server", version = "0.2.0" } octocrab.workspace = true @@ -31,3 +33,6 @@ tokio-util = { version = "0.7.18", features = ["compat"] } tower-lsp-server.workspace = true xz2.workspace = true zip.workspace = true + +[dev-dependencies] +insta.workspace = true diff --git a/crates/nash-cli/src/cli.rs b/crates/nash-cli/src/cli.rs index d06ad19b..46c5d2bf 100644 --- a/crates/nash-cli/src/cli.rs +++ b/crates/nash-cli/src/cli.rs @@ -1,4 +1,5 @@ use clap::Parser; +use std::io::IsTerminal; use crate::cmd; @@ -6,10 +7,20 @@ use crate::cmd; #[command(name = "nash", version, about = "The Nash programming language compiler", long_about = Some(crate::BANNER))] #[command(propagate_version = true)] pub struct Cli { + /// Control diagnostic colors. NO_COLOR disables automatic colors. + #[arg(long, global = true, value_enum, default_value = "auto")] + pub color: Color, #[command(subcommand)] pub cmd: cmd::Cmd, } +#[derive(Clone, Copy, clap::ValueEnum)] +pub enum Color { + Auto, + Always, + Never, +} + impl Default for Cli { fn default() -> Self { Self::parse() @@ -18,6 +29,14 @@ impl Default for Cli { impl Cli { pub async fn exec(self) -> miette::Result<()> { - self.cmd.exec().await + let color = match self.color { + Color::Always => true, + Color::Never => false, + Color::Auto => { + std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none() + } + }; + miette::set_hook(Box::new(move |_| Box::new(nash_report::handler(color))))?; + self.cmd.exec(color).await } } diff --git a/crates/nash-cli/src/cmd/check.rs b/crates/nash-cli/src/cmd/check.rs index 9f4496e7..36378fa6 100644 --- a/crates/nash-cli/src/cmd/check.rs +++ b/crates/nash-cli/src/cmd/check.rs @@ -2,85 +2,148 @@ use std::path::PathBuf; use std::sync::Arc; use miette::{IntoDiagnostic, Result}; -use nash_driver::{Database, FileSystemSource, Project, build, build_graph}; +use nash_driver::{Database, FileSystemSource, ModuleResult, Project, build, build_graph}; +use nash_report::Severity; use tokio::sync::Mutex; +#[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum ReportFormat { + Human, + Json, +} + #[derive(clap::Args)] pub struct Args { - /// Path to the project (defaults to current directory) + /// Path to the project (defaults to current directory). #[arg(default_value = ".")] pub path: PathBuf, + /// Diagnostic output format. + #[arg(long, value_enum, default_value = "human")] + pub report: ReportFormat, + /// Hide compiler warnings. + #[arg(long)] + pub no_warnings: bool, } impl Args { - pub async fn exec(self) -> Result<()> { - eprintln!("Loading project from {:?}...", self.path); - let project = Project::load(&self.path).await.into_diagnostic()?; - - eprintln!("Project root: {:?}", project.root); - eprintln!("Members: {}", project.members.len()); - - let db = Arc::new(Mutex::new(Database::new(FileSystemSource::new()))); - - eprintln!("Discovering modules..."); - let modules = project - .discover_modules(&*db.lock().await) - .await - .into_diagnostic()?; - - eprintln!("Found {} modules", modules.len()); - - if modules.is_empty() { - eprintln!("No Nash source files found."); - return Ok(()); + pub async fn exec(self, color: bool) -> Result<()> { + let result = self.check().await; + let result = match result { + Ok(result) => result, + Err(error) if self.report == ReportFormat::Json => { + println!( + "{}", + serde_json::json!({ + "type": "error", "path": self.path, "title": "PROJECT ERROR", "message": [error.to_string()] + }) + ); + std::process::exit(1); + } + Err(error) => return Err(error), + }; + let modules = result.ordered_reports(); + if self.report == ReportFormat::Json { + let select = |severity| { + modules + .iter() + .filter_map(|module| { + let mut selected = (**module).clone(); + selected + .reports + .retain(|report| report.severity == severity); + (!selected.reports.is_empty()).then_some(selected) + }) + .collect::>() + }; + println!( + "{}", + nash_report::json::compile_errors(&select(Severity::Error)) + ); + if !self.no_warnings { + let warnings = select(Severity::Warning); + if !warnings.is_empty() { + eprintln!("{}", nash_report::json::compile_warnings(&warnings)); + } + } + } else { + for module in &modules { + let source = nash_report::Source::new(&module.source); + for report in &module.reports { + if self.no_warnings && report.severity == Severity::Warning { + continue; + } + eprintln!( + "{:?}", + miette::Report::new(report.render(&source, &module.path, color)) + ); + } + } } - - eprintln!("Building dependency graph..."); - let graph = build_graph(db.clone(), &modules.keys().cloned().collect::>()) - .await - .into_diagnostic()?; - - eprintln!("Dependency order: {} modules", graph.order.len()); - - eprintln!("Compiling..."); - let result = build(db, &graph, &modules).await; - - eprintln!(); - if !result.warnings.is_empty() { - for warning in &result.warnings { - eprintln!("Warning: {}", warning); + let mut states: Vec<_> = result.modules.iter().collect(); + states.sort_by_key(|(uri, _)| *uri); + for (uri, state) in states { + match state { + ModuleResult::Blocked { dependencies } if self.report == ReportFormat::Human => { + eprintln!( + "Skipped {} because these dependencies failed: {}", + uri.path(), + dependencies + .iter() + .map(|uri| uri.path()) + .collect::>() + .join(", ") + ); + } + ModuleResult::SourceUnavailable { message } => { + if self.report == ReportFormat::Json { + eprintln!( + "{}", + serde_json::json!({"type":"error", "path":uri.path(), "title":"SOURCE UNAVAILABLE", "message":[message]}) + ); + } else { + eprintln!("Could not read {}: {}", uri.path(), message); + } + } + _ => {} } - eprintln!(); } - if result.is_success() { - eprintln!( - "Success! Compiled {} modules ({} declarations)", - result.total, - result + if self.report == ReportFormat::Human { + let declarations: usize = result .modules .values() - .filter_map(|r| match r { - nash_driver::ModuleResult::Success { decl_count } => Some(decl_count), + .filter_map(|result| match result { + ModuleResult::Success { decl_count } => Some(decl_count), _ => None, }) - .sum::() - ); + .sum(); + eprintln!( + "Success! Compiled {} modules ({} declarations)", + result.total, declarations + ); + } Ok(()) } else { - eprintln!("Compilation failed."); - eprintln!(" {} succeeded", result.success); - eprintln!(" {} failed", result.failed); - - for (uri, module_result) in &result.modules { - if let nash_driver::ModuleResult::Failed { message } = module_result { - eprintln!(); - eprintln!("Error in {}:", uri.path()); - eprintln!(" {}", message); - } + if self.report == ReportFormat::Human { + eprintln!( + "Compilation failed: {} succeeded, {} failed or blocked.", + result.success, result.failed + ); } - std::process::exit(1); } } + + async fn check(&self) -> Result { + let project = Project::load(&self.path).await.into_diagnostic()?; + let db = Arc::new(Mutex::new(Database::new(FileSystemSource::new()))); + let modules = project + .discover_modules(&*db.lock().await) + .await + .into_diagnostic()?; + let graph = build_graph(db.clone(), &modules.keys().cloned().collect::>()) + .await + .into_diagnostic()?; + Ok(build(db, &graph, &modules).await) + } } diff --git a/crates/nash-cli/src/cmd/mod.rs b/crates/nash-cli/src/cmd/mod.rs index 684bba01..41f5b072 100644 --- a/crates/nash-cli/src/cmd/mod.rs +++ b/crates/nash-cli/src/cmd/mod.rs @@ -11,9 +11,9 @@ pub enum Cmd { } impl Cmd { - pub async fn exec(self) -> miette::Result<()> { + pub async fn exec(self, color: bool) -> miette::Result<()> { match self { - Cmd::Check(args) => args.exec().await, + Cmd::Check(args) => args.exec(color).await, Cmd::Lsp(args) => lsp::exec(args).await, } } diff --git a/crates/nash-driver/Cargo.toml b/crates/nash-driver/Cargo.toml index 99ee953c..c6ad7f4a 100644 --- a/crates/nash-driver/Cargo.toml +++ b/crates/nash-driver/Cargo.toml @@ -24,6 +24,7 @@ nash-ast = { path = "../nash-ast", version = "0.7.0" } nash-can = { path = "../nash-can", version = "0.6.0" } nash-constrain = { path = "../nash-constrain", version = "0.4.0" } nash-parse = { path = "../nash-parse", version = "0.5.0" } +nash-report = { path = "../nash-report", version = "0.1.0" } nash-region = { path = "../nash-region", version = "0.2.0" } nash-solve = { path = "../nash-solve", version = "0.4.0" } nash-source = { path = "../nash-source", version = "0.6.0" } diff --git a/crates/nash-driver/src/compile.rs b/crates/nash-driver/src/compile.rs index ff31d30a..fe3a73f7 100644 --- a/crates/nash-driver/src/compile.rs +++ b/crates/nash-driver/src/compile.rs @@ -19,6 +19,8 @@ use crate::database::Database; use crate::error::DriverError; use crate::graph::DepGraph; +#[cfg(test)] +mod collection_tests; #[cfg(test)] mod nitpick_source_tests; #[cfg(test)] @@ -32,11 +34,12 @@ pub enum ModuleResult { /// Number of declarations in the module. decl_count: usize, }, - /// Module failed to compile. - Failed { - /// Parse or other error message. - message: String, - }, + /// Compilation was skipped because these original root dependencies failed. + Blocked { dependencies: Vec }, + /// Structured errors produced while the module arena was alive. + Failed(nash_report::ModuleReports), + /// Source I/O failed before a compiler phase could run. + SourceUnavailable { message: String }, } /// Result of a full build. @@ -58,10 +61,25 @@ pub struct BuildResult { pub failed: usize, /// Warnings collected during canonicalization. - pub warnings: Vec, + pub warnings: Vec, } impl BuildResult { + /// Compiler errors and warnings in a common order for every frontend. + pub fn ordered_reports(&self) -> Vec<&nash_report::ModuleReports> { + let mut reports: Vec<_> = self + .modules + .values() + .filter_map(|result| match result { + ModuleResult::Failed(reports) => Some(reports), + _ => None, + }) + .chain(self.warnings.iter()) + .collect(); + reports.sort_by(|a, b| a.path.cmp(&b.path).then(a.name.cmp(&b.name))); + reports + } + /// Check if the build was completely successful. pub fn is_success(&self) -> bool { self.failed == 0 @@ -82,7 +100,7 @@ pub struct SolvedModule<'a> { struct CompileOutput { uri: Url, result: ModuleResult, - warnings: Vec, + warnings: Vec, } /// Compile all modules through the full pipeline, in dependency order. @@ -106,7 +124,8 @@ pub async fn build( }) .collect(); - tokio::task::spawn_blocking(move || build_sync(sources)) + let edges = graph.edges.clone(); + tokio::task::spawn_blocking(move || build_sync_with_edges(sources, &edges)) .await .expect("compile task panicked") } @@ -116,12 +135,13 @@ pub async fn build( /// /// Type checking is inherently dependency-ordered, so within-build /// compilation is sequential within a build. -fn build_sync( +fn build_sync_with_edges( sources: Vec<( Url, Option, Result, )>, + edges: &HashMap>, ) -> BuildResult { let store = Bump::new(); let mut interfaces = BTreeMap::from([("Builtin", nash_can::kinds::builtin_interface(&store))]); @@ -129,9 +149,30 @@ fn build_sync( let mut solved = BTreeMap::new(); let mut results: HashMap = HashMap::new(); - let mut all_warnings: Vec = Vec::new(); + let mut all_warnings: Vec = Vec::new(); for (uri, package, source) in &sources { + let mut dependencies = std::collections::BTreeSet::new(); + for dependency in edges.get(uri).into_iter().flatten() { + match results.get(dependency) { + Some(ModuleResult::Success { .. }) => {} + Some(ModuleResult::Blocked { + dependencies: roots, + }) => dependencies.extend(roots.iter().cloned()), + _ => { + dependencies.insert(dependency.clone()); + } + } + } + if !dependencies.is_empty() { + results.insert( + uri.clone(), + ModuleResult::Blocked { + dependencies: dependencies.into_iter().collect(), + }, + ); + continue; + } let (output, compiled) = compile_module(uri, package.as_ref(), source, &store, &interfaces); if let Some((interface, module)) = compiled { public_interfaces.insert( @@ -161,6 +202,27 @@ fn build_sync( } } +#[cfg(test)] +fn build_sync( + sources: Vec<( + Url, + Option, + Result, + )>, +) -> BuildResult { + let known: Vec<_> = sources.iter().map(|(uri, _, _)| uri.clone()).collect(); + let edges = sources + .iter() + .map(|(uri, _, source)| { + let dependencies = source + .as_ref() + .map_or_else(|_| vec![], |source| extract_imports(source, uri, &known)); + (uri.clone(), dependencies) + }) + .collect(); + build_sync_with_edges(sources, &edges) +} + /// Fetch source content in dependency order, retaining failed reads in place. async fn fetch_sources( db: &Arc>, @@ -186,31 +248,79 @@ fn compile_module<'s>( store: &'s Bump, interfaces: &BTreeMap<&'s str, Interface<'s>>, ) -> (CompileOutput, Option<(Interface<'s>, SolvedModule<'s>)>) { - let failed = |message: String| { + let source = match source { + Ok(source) => source, + Err(message) => { + return ( + CompileOutput { + uri: uri.clone(), + result: ModuleResult::SourceUnavailable { + message: message.clone(), + }, + warnings: vec![], + }, + None, + ); + } + }; + let path = uri.to_file_path().map_or_else( + |_| uri.path().to_owned(), + |path| path.to_string_lossy().into_owned(), + ); + let expected_name = std::path::Path::new(&path) + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("Main"); + let source_view = nash_report::Source::new(source); + let owned = |name: &str, reports: Vec| { + let mut reports = nash_report::ModuleReports { + name: name.to_owned(), + path: path.clone(), + source: source.clone(), + reports, + }; + reports.sort(); + reports + }; + let failed = |name: &str, + error: nash_report::ModuleError<'_>, + warnings: Vec| { + let mut reports = nash_report::to_reports(&source_view, expected_name, &error); + reports.extend(warnings.into_iter().flat_map(|module| module.reports)); ( CompileOutput { uri: uri.clone(), - result: ModuleResult::Failed { message }, + result: ModuleResult::Failed(owned(name, reports)), warnings: vec![], }, None, ) }; - let source = match source { - Ok(s) => s, - Err(e) => return failed(e.clone()), - }; - let bump = store; let src: &str = bump.alloc_str(source); let mut parser = nash_parse::Parser::new(bump, src.as_bytes()); - let module = match parser.module() { Ok(module) => module, - Err(e) => return failed(format!("{:?}", e)), + Err(error) => { + return failed( + expected_name, + nash_report::ModuleError::Syntax(nash_parse::error::Error::ParseError( + bump.alloc(error), + )), + vec![], + ); + } }; - + let name = module.name.map_or(expected_name, |name| name.value); + // Default imports belong to Plan 12; localize exactly the imports in use. + let localizer = + nash_report::Localizer::from_module(&module, &[]).with_package(package.map(|package| { + nash_ast::PackageName { + author: bump.alloc_str(package.author()), + project: bump.alloc_str(package.project()), + } + })); let context = nash_can::Context { package: package.map(|package| nash_ast::PackageName { author: bump.alloc_str(package.author()), @@ -220,26 +330,36 @@ fn compile_module<'s>( }; let can_result = match nash_can::canonicalize(bump, context, &module) { Ok(can_result) => can_result, - Err(errors) => return failed(crate::diagnostics::canonical(src, &errors)), + Err(errors) => return failed(name, nash_report::ModuleError::Names(errors), vec![]), + }; + let warnings = if can_result.warnings.is_empty() { + vec![] + } else { + vec![owned( + name, + can_result + .warnings + .iter() + .map(nash_report::warning::to_report) + .collect(), + )] }; - let warnings: Vec = can_result - .warnings - .iter() - .map(|w| format!("{:?}", w)) - .collect(); - 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) { Ok(solved) => solved, - Err(errors) => return failed(crate::diagnostics::inference(src, &errors)), + Err(errors) => { + return failed( + name, + nash_report::ModuleError::Types(localizer, errors), + warnings, + ); + } }; - if let Err(errors) = nash_nitpick::check(bump, &can_result.module) { - return failed(format!("{errors:?}")); + return failed(name, nash_report::ModuleError::Patterns(errors), warnings); } - let module = bump.alloc(can_result.module); let interface = nash_can::from_module(bump, module, &annotations); let solved = SolvedModule { @@ -247,17 +367,14 @@ fn compile_module<'s>( annotations, types, }; - - ( - CompileOutput { - uri: uri.clone(), - result: ModuleResult::Success { - decl_count: count_decls(module.decls), - }, - warnings, + let output = CompileOutput { + uri: uri.clone(), + result: ModuleResult::Success { + decl_count: count_decls(module.decls), }, - Some((interface, solved)), - ) + warnings, + }; + (output, Some((interface, solved))) } fn count_decls(decls: &nash_ast::Decls<'_>) -> usize { @@ -284,10 +401,14 @@ pub async fn build_graph( // Parse module to get imports let source = { let mut db = db.lock().await; - db.source(uri).await?.to_string() + db.source(uri).await.map(str::to_owned) }; - let imports = extract_imports(&source, uri, modules); + // Retain unreadable nodes: the build reports their I/O failure and + // blocks dependents while continuing independent modules. + let imports = source + .as_ref() + .map_or_else(|_| vec![], |source| extract_imports(source, uri, modules)); graph.add_module(uri.clone(), imports); } @@ -338,6 +459,17 @@ fn resolve_import(name: &str, _current: &Url, known_modules: &[Url]) -> Option String { + let source = nash_report::Source::new(&reports.source); + reports + .reports + .iter() + .map(|report| nash_report::render_plain(report, &source, &reports.path)) + .collect::>() + .join("\n") +} + #[cfg(test)] mod tests { use super::*; @@ -520,7 +652,7 @@ main = Utils.helper "not a function argument" assert_eq!(result.failed, 1); assert!(matches!( result.modules[&url("Main.nash")], - ModuleResult::Failed { .. } + ModuleResult::Failed(_) )); } @@ -618,12 +750,12 @@ mod trait_tests { ( "orphan", "module Bad exposing (..)\nimport Methods exposing (Keep)\nimport Types exposing (Token)\nimpl Keep Token where\n keep x = x\n", - "OrphanImpl", + "ORPHAN IMPL", ), ( "overlap", "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", - "OverlappingImpls", + "OVERLAPPING IMPL", ), ] { let result = compile_sources(&[ @@ -640,11 +772,12 @@ mod trait_tests { .await; assert_eq!(result.success, 2, "{result:?}"); assert_eq!(result.failed, 1, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Bad.nash").unwrap()] else { panic!("impl module must fail") }; + let message = report_text(reports); assert!(message.contains(expected), "{message}"); diagnostics.push(format!("{case}: {message}")); } @@ -690,12 +823,13 @@ mod kind_tests { ) .await; assert_eq!(result.success, 1, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Main.nash").unwrap()] else { panic!("consumer must reject hidden label") }; - assert!(message.contains("NotARecord"), "{message}"); + let message = report_text(reports); + assert!(message.contains("not a record"), "{message}"); } #[tokio::test] @@ -705,12 +839,16 @@ mod kind_tests { "module Main exposing (..)\nimport Types\nchange : Types.box -> Types.box\nchange x = { x | value = () }\n", ).await; assert_eq!(result.success, 1, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Main.nash").unwrap()] else { panic!("consumer must reject union update") }; - assert!(message.contains("UpdateNotRecord"), "{message}"); + let message = report_text(reports); + assert!( + message.contains("does not support record updates"), + "{message}" + ); } #[tokio::test] @@ -721,12 +859,13 @@ mod kind_tests { ) .await; assert_eq!(result.success, 1, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Main.nash").unwrap()] else { panic!("private constructor labels must remain hidden") }; - assert!(message.contains("NotARecord"), "{message}"); + let message = report_text(reports); + assert!(message.contains("not a record"), "{message}"); } #[tokio::test] @@ -800,12 +939,13 @@ mod kind_tests { .await; assert_eq!(result.success, 0, "{result:?}"); assert_eq!(result.failed, 2, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Types.nash").unwrap()] else { panic!("producer must report a kind error"); }; - assert!(message.contains("KindInfinite"), "{message}"); + let message = report_text(reports); + assert!(message.contains("INFINITE KIND"), "{message}"); assert!(message.contains("infinite kind"), "{message}"); } @@ -818,12 +958,13 @@ mod kind_tests { ).await; assert_eq!(result.success, 0, "{result:?}"); assert_eq!(result.failed, 2, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Types.nash").unwrap()] else { panic!("producer must fail") }; - assert!(message.contains("KindInfinite"), "{message}"); + let message = report_text(reports); + assert!(message.contains("INFINITE KIND"), "{message}"); } } @@ -852,13 +993,14 @@ mod kind_tests { assert_eq!(result.success, if succeeds { 2 } else { 1 }, "{result:?}"); assert_eq!(result.failed, usize::from(!succeeds), "{result:?}"); if !succeeds { - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Main.nash").unwrap()] else { panic!("consumer must reject the supplied Term argument") }; + let message = report_text(reports); assert!( - message.contains("RepresentationMismatch") && message.contains("Storable"), + message.contains("REPRESENTATION MISMATCH") && message.contains("Storable"), "{message}" ); } @@ -881,13 +1023,14 @@ mod kind_tests { "module Main exposing (..)\n\nimport Builtin exposing (..)\nimport Types exposing (type item)\n\ntype alias items = list item\n", ).await; assert_eq!(result.success, 1, "{result:?}"); - let ModuleResult::Failed { message } = + let ModuleResult::Failed(reports) = &result.modules[&Url::parse("file:///Main.nash").unwrap()] else { panic!("invalid consumer compiled"); }; + let message = report_text(reports); assert!( - message.contains("RepresentationMismatch") && message.contains("Storable"), + message.contains("REPRESENTATION MISMATCH") && message.contains("Storable"), "{message}" ); assert!( diff --git a/crates/nash-driver/src/compile/collection_tests.rs b/crates/nash-driver/src/compile/collection_tests.rs new file mode 100644 index 00000000..3b546a1f --- /dev/null +++ b/crates/nash-driver/src/compile/collection_tests.rs @@ -0,0 +1,74 @@ +use super::*; +use crate::InMemorySource; + +fn url(name: &str) -> Url { + Url::parse(&format!("file:///project/{name}.nash")).unwrap() +} + +#[tokio::test] +async fn independent_failures_block_only_their_dependents() { + let files = [ + ("Broken", "module Broken exposing (..)\nf = missing\n"), + ("Other", "module Other exposing (..)\ng = absent\n"), + ( + "Middle", + "module Middle exposing (..)\nimport Broken\nx = Broken.f\n", + ), + ( + "Main", + "module Main exposing (..)\nimport Middle\nx = Middle.x\n", + ), + ("Good", "module Good exposing (..)\nx = ()\n"), + ]; + let memory = InMemorySource::new(); + for (name, source) in files { + memory.insert(url(name), source.into()); + } + let db = Arc::new(Mutex::new(Database::new(memory))); + let uris: Vec<_> = files.iter().map(|(name, _)| url(name)).collect(); + let graph = build_graph(db.clone(), &uris).await.unwrap(); + let origins = uris.iter().cloned().map(|uri| (uri, None)).collect(); + let result = build(db, &graph, &origins).await; + assert_eq!(result.success, 1); + assert_eq!(result.failed, 4); + assert_eq!(result.interfaces.len(), 1); + assert!(result.interfaces.contains_key(&url("Good"))); + for name in ["Middle", "Main"] { + assert!( + matches!(&result.modules[&url(name)], ModuleResult::Blocked { dependencies } if dependencies == &[url("Broken")]) + ); + } +} + +#[tokio::test] +async fn unreadable_source_does_not_hide_independent_errors() { + let memory = InMemorySource::new(); + memory.insert( + url("Main"), + "module Main exposing (..)\nx = unknown\n".into(), + ); + memory.insert( + url("Dependent"), + "module Dependent exposing (..)\nimport Missing\nx = Missing.x\n".into(), + ); + let uris = [url("Missing"), url("Main"), url("Dependent")]; + let db = Arc::new(Mutex::new(Database::new(memory))); + let graph = build_graph(db.clone(), &uris).await.unwrap(); + let result = build( + db, + &graph, + &uris.iter().cloned().map(|uri| (uri, None)).collect(), + ) + .await; + assert!(matches!( + result.modules[&url("Missing")], + ModuleResult::SourceUnavailable { .. } + )); + assert!( + matches!(&result.modules[&url("Main")], ModuleResult::Failed(reports) if reports.reports.iter().any(|report| report.title == "NAMING ERROR")) + ); + assert!( + matches!(&result.modules[&url("Dependent")], ModuleResult::Blocked { dependencies } if dependencies == &[url("Missing")]) + ); + assert!(result.interfaces.is_empty()); +} diff --git a/crates/nash-driver/src/compile/nitpick_tests.rs b/crates/nash-driver/src/compile/nitpick_tests.rs index b6044c30..3dab5f45 100644 --- a/crates/nash-driver/src/compile/nitpick_tests.rs +++ b/crates/nash-driver/src/compile/nitpick_tests.rs @@ -18,10 +18,10 @@ fn rejected(source: &str) -> String { compiled.is_none(), "rejected module must not publish an interface or solved module" ); - let ModuleResult::Failed { message } = output.result else { + let ModuleResult::Failed(reports) = output.result else { panic!("expected a failed module") }; - message + report_text(&reports) } #[test] @@ -37,7 +37,7 @@ fn incomplete_case_fails_module() { ); let message = rejected(source); assert!( - message.contains("Incomplete") && message.contains("False"), + message.contains("MISSING PATTERNS") && 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") && message.contains("index: 2"), + message.contains("REDUNDANT PATTERN") && message.contains("2nd pattern"), "{message}" ); insta::assert_snapshot!(message); @@ -66,7 +66,7 @@ fn redundant_case_fails_module() { #[test] fn unsafe_argument_fails_module() { let message = rejected("module Main exposing (..)\nf (x :: _) = x\n"); - assert!(message.contains("BadArg"), "{message}"); + assert!(message.contains("function arguments"), "{message}"); insta::assert_snapshot!(message); } @@ -82,7 +82,10 @@ fn unsafe_destructure_fails_module() { x "# )); - assert!(message.contains("BadDestruct"), "{message}"); + assert!( + message.contains("only if there is ONE possibility"), + "{message}" + ); insta::assert_snapshot!(message); } @@ -100,7 +103,7 @@ fn trait_default_without_top_level_definitions_fails_module() { "# )); assert!( - message.contains("Incomplete") && message.contains("False"), + message.contains("MISSING PATTERNS") && message.contains("False"), "{message}" ); insta::assert_snapshot!(message); @@ -119,7 +122,7 @@ fn impl_method_fails_module() { "# )); assert!( - message.contains("Incomplete") && message.contains("BadArg"), + message.contains("UNSAFE PATTERN") && message.contains("function arguments"), "{message}" ); insta::assert_snapshot!(message); @@ -135,8 +138,8 @@ fn type_errors_precede_nitpick() { f True = True "# )); - assert!(message.contains("BadExpr"), "{message}"); - assert!(!message.contains("Incomplete"), "{message}"); + assert!(message.contains("TYPE MISMATCH"), "{message}"); + assert!(!message.contains("MISSING PATTERNS"), "{message}"); } #[test] @@ -166,14 +169,14 @@ fn rejected_module_publishes_no_interface_to_dependents() { assert_eq!(result.success, 1, "{result:?}"); assert_eq!(result.interfaces.len(), 1); assert!(result.interfaces.contains_key(&url("Good"))); - let ModuleResult::Failed { message } = &result.modules[&url("Base")] else { + let ModuleResult::Failed(reports) = &result.modules[&url("Base")] else { panic!("base must fail") }; - assert!(message.contains("Incomplete"), "{message}"); - let ModuleResult::Failed { message } = &result.modules[&url("Main")] else { - panic!("dependent must fail") - }; - assert!(message.contains("ImportNotFound"), "{message}"); + let message = report_text(reports); + assert!(message.contains("UNSAFE PATTERN"), "{message}"); + assert!( + matches!(&result.modules[&url("Main")], ModuleResult::Blocked { dependencies } if dependencies == &[url("Base")]) + ); } #[test] 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 fdfa5a40..03d17767 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,4 +2,18 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -[Incomplete { region: Region { start: Position { line: 6, column: 12 }, end: Position { line: 6, column: 16 } }, context: BadArg, unhandled: [False] }] +UNSAFE PATTERN + + × This pattern does not cover all possibilities: + ╭─[/Main.nash:6:12] + 5 │ impl Choose bool where + 6 │ choose True = () + · ──── + ╰──── + help: Other possibilities include: + + 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. 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 7f268443..9c5ef837 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,4 +2,20 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -[Incomplete { region: Region { start: Position { line: 4, column: 5 }, end: Position { line: 6, column: 1 } }, context: BadCase, unhandled: [False] }] +MISSING PATTERNS + + × This `case` does not have branches for all possibilities: + ╭─[/Main.nash:4:5] + 3 │ f x = + 4 │ ╭─▶ case x of + 5 │ ╰─▶ True -> () + ╰──── + help: Missing possibilities include: + + 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. 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 34413a07..a1c6a34b 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,4 +2,15 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -[Redundant { case_region: Region { start: Position { line: 4, column: 5 }, end: Position { line: 7, column: 1 } }, pattern_region: Region { start: Position { line: 6, column: 9 }, end: Position { line: 6, column: 14 } }, index: 2 }] +REDUNDANT PATTERN + + × The 2nd pattern is redundant: + ╭─[/Main.nash:6:9] + 3 │ f x = + 4 │ case x of + 5 │ _ -> () + 6 │ True -> () + · ───── + ╰──── + help: Any value with this shape will be handled by a previous pattern, so it should be + removed. 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 cd9a8c9c..1ae07468 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,4 +2,20 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -[Incomplete { region: Region { start: Position { line: 6, column: 9 }, end: Position { line: 8, column: 1 } }, context: BadCase, unhandled: [False] }] +MISSING PATTERNS + + × This `case` does not have branches for all possibilities: + ╭─[/Main.nash:6:9] + 5 │ choose _ flag = + 6 │ ╭─▶ case flag of + 7 │ ╰─▶ True -> () + ╰──── + help: Missing possibilities include: + + 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. 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 d97645d7..7fe6e331 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,4 +2,18 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -[Incomplete { region: Region { start: Position { line: 2, column: 4 }, end: Position { line: 2, column: 10 } }, context: BadArg, unhandled: [[]] }] +UNSAFE PATTERN + + × This pattern does not cover all possibilities: + ╭─[/Main.nash:2:4] + 1 │ module Main exposing (..) + 2 │ f (x :: _) = x + · ────── + ╰──── + help: Other possibilities include: + + [] + + 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. 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 26e97d4d..65b9a3ed 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,4 +2,37 @@ source: crates/nash-driver/src/compile/nitpick_tests.rs expression: message --- -[Incomplete { region: Region { start: Position { line: 4, column: 10 }, end: Position { line: 4, column: 19 } }, context: BadDestruct, unhandled: [[]] }] +UNSAFE PATTERN + + × This pattern does not cover all possible values: + ╭─[/Main.nash:4:10] + 3 │ let + 4 │ (x :: rest) = xs + · ───────── + 5 │ in + ╰──── + help: Other possibilities include: + + [] + + 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. + + 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! + +unused definition + + ⚠ You are not using `rest` anywhere. + ╭─[/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! diff --git a/crates/nash-driver/src/diagnostics.rs b/crates/nash-driver/src/diagnostics.rs deleted file mode 100644 index 76a1d0bd..00000000 --- a/crates/nash-driver/src/diagnostics.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Plain diagnostic text for kind and representation checking. The full -//! reporting layer can add source spans and styling without changing errors. -use nash_ast::{Kind, primitives::ReprTrait}; -use nash_region::Region; - -fn at(source: &str, region: Region, tag: &str, message: String) -> String { - let line = region.start.line; - let excerpt = source - .lines() - .nth(usize::from(line.saturating_sub(1))) - .unwrap_or(""); - format!( - "{tag} at {line}:{}: {message}\n {excerpt}", - region.start.column - ) -} - -fn kind(value: &Kind<'_>) -> String { - match value { - Kind::Type => "Type".into(), - Kind::Arrow(from, to) => { - let from = match from { - Kind::Arrow(..) => format!("({})", kind(from)), - _ => kind(from), - }; - format!("{from} -> {}", kind(to)) - } - } -} - -fn admitted(trait_: ReprTrait) -> &'static str { - match trait_ { - ReprTrait::Big => "Big", - ReprTrait::Const => "Const", - ReprTrait::Term => "Term", - ReprTrait::Storable => "Big or Const", - ReprTrait::Little => "Const or Term", - } -} - -fn application(head: String, args: impl IntoIterator) -> String { - let args: Vec<_> = args.into_iter().collect(); - if args.is_empty() { - head - } else { - format!("({head} {})", args.join(" ")) - } -} - -fn typ(value: &nash_constrain::error_type::ErrorType<'_>) -> String { - use nash_constrain::error_type::ErrorType; - let qualified = |home: nash_ast::ModuleName<'_>, name: &str| { - if home == nash_ast::primitives::builtin_home() { - name.to_owned() - } else { - format!("{}.{name}", home.name) - } - }; - match value { - ErrorType::FlexVar(name) | ErrorType::RigidVar(name) => format!("'{name}"), - ErrorType::Type { home, name, args } => { - application(qualified(*home, name), args.iter().map(|arg| typ(arg))) - } - ErrorType::Alias { - home, name, args, .. - } => application(qualified(*home, name), args.iter().map(|(_, arg)| typ(arg))), - ErrorType::VarApp(head, args) => application(typ(head), args.iter().map(|arg| typ(arg))), - ErrorType::Tuple(first, second, rest) => format!( - "({})", - [*first, *second] - .into_iter() - .chain(rest.iter().copied()) - .map(typ) - .collect::>() - .join(", ") - ), - ErrorType::Lambda(first, second, rest) => format!( - "({})", - [*first, *second] - .into_iter() - .chain(rest.iter().copied()) - .map(typ) - .collect::>() - .join(" -> ") - ), - ErrorType::Record { fields } => { - let fields = fields - .iter() - .map(|(name, value)| format!("{name} : {}", typ(value))) - .collect::>() - .join(", "); - format!("{{ {fields} }}") - } - ErrorType::Infinite => "".into(), - ErrorType::Error => "".into(), - } -} - -fn requirement(value: &nash_constrain::error::Requirement<'_>) -> String { - use nash_constrain::error::Requirement; - match value { - Requirement::Trait { trait_, args } => { - application(trait_.name.into(), args.iter().map(|arg| typ(arg))) - } - Requirement::Application { head, args } => format!( - "application {}", - application(typ(head), args.iter().map(|arg| typ(arg))) - ), - Requirement::Formation(value) => format!("formation of {}", typ(value)), - } -} - -fn context(value: &nash_can::KindContext<'_>) -> String { - use nash_can::KindContext; - match value { - KindContext::TypeAnnotation => "the type annotation".into(), - KindContext::Annotation { name } => format!("the annotation for {name}"), - KindContext::BigField { union, ctor, index } - | KindContext::LittleField { union, ctor, index } => { - format!("field {} of {ctor} in {union}", usize::from(*index) + 1) - } - KindContext::RecordField { alias, field, .. } => format!("field {field} of {alias}"), - KindContext::AliasCasing { alias, big } => format!( - "the {}case name of alias {alias}", - if *big { "upper" } else { "lower" } - ), - KindContext::ImplHead { trait_, index } => { - format!("head {} of impl {}", usize::from(*index) + 1, trait_.name) - } - } -} - -pub(crate) fn canonical(source: &str, errors: &[nash_can::Error<'_>]) -> String { - use nash_can::Error; - errors.iter().map(|error| { - let (region, tag, message) = match error { - Error::LabeledCtorMissingField { region, ctor, field } => (*region, "LabeledCtorMissingField", format!("constructor {ctor} needs the field {field}.")), - Error::LabeledCtorExtraField { region, ctor, field } => (*region, "LabeledCtorExtraField", format!("constructor {ctor} has no field named {field}.")), - Error::LabeledCtorUnknownField { region, ctor, field } => (*region, "LabeledCtorUnknownField", format!("constructor {ctor} has no field named {field}.")), - Error::RecordTypeOutsideAlias { region } => (*region, "RecordTypeOutsideAlias", - "a record type is only allowed as the direct body of a type alias. Give this record a named alias.".into()), - Error::RecordLiteralNoAlias { region, fields } => (*region, "RecordLiteralNoAlias", - format!("no visible record alias has exactly these fields: {}. Declare or import an alias for this record.", fields.join(", "))), - Error::RecordLiteralAmbiguous { region, candidates } => (*region, "RecordLiteralAmbiguous", - format!("these record aliases have the same field set: {}. Use the intended alias constructor.", candidates.iter().map(|name| format!("{}.{}", name.home.name, name.name)).collect::>().join(", "))), - Error::KindMismatch { region, expected, actual, .. } => (*region, "KindMismatch", - format!("expected kind {}, but found {}. Type arguments must have matching kinds.", kind(expected), kind(actual))), - Error::KindInfinite { region, .. } => (*region, "KindInfinite", - "this application would require an infinite kind. A type constructor cannot be applied to itself.".into()), - Error::RepresentationMismatch { region, context: origin, required, actual } => (*region, "RepresentationMismatch", - format!("this position requires {} ({}), but the type has {actual:?} representation. Required by {}.", required.name(), admitted(*required), context(origin))), - Error::ContradictoryRepresentation { region, variable } => (*region, "ContradictoryRepresentation", - format!("the representation requirements on '{variable} are incompatible; no type can satisfy all of them.")), - Error::ImplOfBuiltinTrait { region, trait_ } => (*region, "ImplOfBuiltinTrait", - format!("{} is compiler-owned. Its representation rules cannot be replaced by an impl.", trait_.name)), - Error::IrregularRecursion { region, constructor, parameter } => (*region, "IrregularRecursion", - format!("recursive use of {} constructs the applied-relevant parameter '{parameter}. Pass a type variable here so context inference can terminate.", constructor.name)), - _ => return format!("{error:?}"), - }; - at(source, region, tag, message) - }).collect::>().join("\n") -} - -pub(crate) fn inference(source: &str, errors: &[nash_constrain::error::Error<'_>]) -> String { - use nash_constrain::error::{Error, KindProblem}; - errors.iter().map(|error| { - let (region, tag, message) = match error { - Error::AmbiguousRecordAccess { region, .. } => (*region, "AmbiguousRecordAccess", - "the record type is not known before this definition is generalized. Add a type annotation.".into()), - Error::NotARecord { region, record, .. } => (*region, "NotARecord", - format!("{} does not provide record fields.", typ(record))), - Error::MissingField { region, field, record, available, .. } => (*region, "MissingField", - format!("{} has no field {field}. Available fields: {}.", typ(record), available.join(", "))), - Error::FieldMismatch { region, field, actual, expected, .. } => (*region, "FieldMismatch", - format!("field {field} has type {}, but this use requires {}.", typ(actual), typ(expected))), - Error::UpdateNotRecord { region, record } => (*region, "UpdateNotRecord", - format!("{} is not a record alias. Rebuild the value with its constructor.", typ(record))), - Error::BadKind { region, name, reason, .. } => (*region, "BadKind", match reason { - KindProblem::Infinite => format!("{name} requires an infinite kind. A type constructor cannot be applied to itself."), - KindProblem::Mismatch { expected, actual } => format!("{name} requires kind {}, but this use has kind {}. An annotation's quantified kinds cannot be specialized by its body.", kind(expected), kind(actual)), - }), - Error::ContradictoryRepresentation { region, name, requirements, .. } => (*region, "ContradictoryRepresentation", - format!("{name} requires incompatible representations {requirements:?}. No type can satisfy all of them.")), - Error::MissingImpl { region, name, trait_, args, because, .. } - if ReprTrait::of(*trait_).is_some() => { - let required = ReprTrait::of(*trait_).unwrap(); - let chain = if because.is_empty() { String::new() } else { format!(" Required through {}.", because.iter().map(requirement).collect::>().join(" -> ")) }; - (*region, "MissingImpl", format!("{name} requires {} ({}), but its argument does not have an allowed representation: {}.{chain}", required.name(), admitted(required), args.iter().map(|arg| typ(arg)).collect::>().join(", "))) - } - Error::MissingConstraint { region, name, trait_, binder, .. } - if ReprTrait::of(*trait_).is_some() => (*region, "MissingConstraint", - format!("{name} requires {}, but the annotation for {} does not promise it. Add the representation constraint to that annotation.", trait_.name, binder.value)), - Error::UnresolvedApplication { region, name, .. } => (*region, "UnresolvedApplication", - format!("the datatype context required by {name} could not be established for this type application.")), - _ => return format!("{error:?}"), - }; - at(source, region, tag, message) - }).collect::>().join("\n") -} diff --git a/crates/nash-driver/src/graph.rs b/crates/nash-driver/src/graph.rs index 85961898..f11670a0 100644 --- a/crates/nash-driver/src/graph.rs +++ b/crates/nash-driver/src/graph.rs @@ -3,7 +3,7 @@ //! Builds a graph of module dependencies by parsing import statements, //! performs topological sorting for compilation order, and detects cycles. -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{BTreeSet, HashMap, HashSet}; use url::Url; use crate::error::DriverError; @@ -62,7 +62,7 @@ impl DepGraph { } // Start with nodes that have no dependencies (out-degree = 0) - let mut queue: VecDeque<&Url> = out_degree + let mut queue: BTreeSet<&Url> = out_degree .iter() .filter(|&(_, deg)| *deg == 0) .map(|(&node, _)| node) @@ -72,7 +72,7 @@ impl DepGraph { let mut depths: HashMap = HashMap::new(); // Process nodes in order - while let Some(node) = queue.pop_front() { + while let Some(node) = queue.pop_first() { // Calculate depth: max depth of imports + 1 let depth = self .edges @@ -97,7 +97,7 @@ impl DepGraph { if let Some(deg) = out_degree.get_mut(importer) { *deg -= 1; if *deg == 0 { - queue.push_back(importer); + queue.insert(importer); } } } @@ -124,7 +124,7 @@ impl DepGraph { let mut stack: HashSet<&Url> = HashSet::new(); let mut path: Vec<&Url> = Vec::new(); - for start in self.edges.keys() { + for start in self.edges.keys().collect::>() { if self.dfs_cycle(start, &mut visited, &mut stack, &mut path) { // Format cycle as: A -> B -> C -> A let cycle_str: Vec = path.iter().map(|u| module_name_from_uri(u)).collect(); @@ -160,7 +160,7 @@ impl DepGraph { path.push(node); if let Some(imports) = self.edges.get(node) { - for import in imports { + for import in imports.iter().collect::>() { if self.dfs_cycle(import, visited, stack, path) { return true; } @@ -188,6 +188,9 @@ impl DepGraph { levels[depth].push(module); } + for level in &mut levels { + level.sort(); + } levels } @@ -236,6 +239,33 @@ mod tests { Url::parse(&format!("file:///{}", path)).unwrap() } + #[test] + fn discovery_order_does_not_change_compile_order() { + let nodes = ["D.nash", "B.nash", "A.nash", "C.nash"]; + let make = |reverse: bool| { + let mut graph = DepGraph::new(); + let mut input = nodes.to_vec(); + if reverse { + input.reverse(); + } + for name in input { + let imports = if name == "D.nash" { + vec![url("B.nash"), url("C.nash")] + } else { + vec![] + }; + graph.add_module(url(name), imports); + } + graph.compute_order().unwrap(); + graph + }; + let a = make(false); + let b = make(true); + assert_eq!(a.order, b.order); + assert_eq!(a.levels(), b.levels()); + assert_eq!(a.order, ["A.nash", "B.nash", "C.nash", "D.nash"].map(url)); + } + #[test] fn test_cycle_detection() { let mut graph = DepGraph::new(); diff --git a/crates/nash-driver/src/lib.rs b/crates/nash-driver/src/lib.rs index bb98cec7..739a5047 100644 --- a/crates/nash-driver/src/lib.rs +++ b/crates/nash-driver/src/lib.rs @@ -43,7 +43,6 @@ pub mod compile; pub mod database; -mod diagnostics; pub mod error; pub mod graph; pub mod interface; diff --git a/crates/nash-driver/src/project.rs b/crates/nash-driver/src/project.rs index e1d9e960..a1c49dff 100644 --- a/crates/nash-driver/src/project.rs +++ b/crates/nash-driver/src/project.rs @@ -301,7 +301,7 @@ mod tests { modules.insert(literal.clone(), Some("example/literals".parse().unwrap())); let result = build(db.clone(), &graph, &modules).await; assert!( - matches!(&result.modules[&main], ModuleResult::Failed { message } if message.contains("AmbiguousType")) + matches!(&result.modules[&main], ModuleResult::Failed(reports) if reports.reports.iter().any(|report| report.title == "AMBIGUOUS TYPE")) ); // An application can name a source directory outside its own root. let overlap = nash_config::parse( 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 4f7ceb2a..e7dd108e 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,5 +2,30 @@ source: crates/nash-driver/src/compile.rs expression: "diagnostics.join(\"\\n\")" --- -orphan: OrphanImpl { region: Region { start: Position { line: 4, column: 1 }, end: Position { line: 5, column: 15 } }, trait_: QualifiedName { home: ModuleName { package: None, name: "Methods" }, name: "Keep" }, heads: [Named(QualifiedName { home: ModuleName { package: None, name: "Types" }, name: "Token" })] } -overlap: OverlappingImpls { key: ImplKey { trait_: QualifiedName { home: ModuleName { package: None, name: "Bad" }, name: "Keep" }, heads: [Named { reference: QualifiedName { home: ModuleName { package: Some(PackageName { author: "nash", project: "core" }), name: "Builtin" }, name: "unit" }, args: [] }] }, first: Region { start: Position { line: 4, column: 1 }, end: Position { line: 5, column: 15 } }, second: Region { start: Position { line: 6, column: 1 }, end: Position { line: 7, column: 15 } }, first_home: ModuleName { package: None, name: "Bad" }, second_home: ModuleName { package: None, name: "Bad" } } +orphan: ORPHAN IMPL + + × This module cannot define an impl of `Methods.Keep` for Types.Token: + ╭─[/Bad.nash:4:1] + 3 │ import Types exposing (Token) + 4 │ ╭─▶ impl Keep Token where + 5 │ ╰─▶ keep x = x + ╰──── + 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 + + × These `Bad.Keep` impls can both match the same trait arguments. The overlapping + │ head is Builtin.unit: + ╭─[/Bad.nash:6:1] + 3 │ keep : 'a -> 'a + 4 │ ╭─▶ impl Keep () where + 5 │ ├─▶ keep x = x + · ╰──── first impl in `Bad` + 6 │ ╭─▶ impl Keep () where + 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. From 1168f0a1e69d4b97f29f299531dc4d0a67a0e390 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 11/12] feat(lsp): publish live compiler diagnostics Signed-off-by: microproofs --- Cargo.lock | 7 + crates/nash-language-server/Cargo.toml | 9 + .../nash-language-server/src/capabilities.rs | 17 +- .../nash-language-server/src/diagnostics.rs | 129 +++++ crates/nash-language-server/src/lib.rs | 2 + crates/nash-language-server/src/server.rs | 80 ++- crates/nash-language-server/src/workspace.rs | 499 ++++++++++++++++++ 7 files changed, 738 insertions(+), 5 deletions(-) create mode 100644 crates/nash-language-server/src/diagnostics.rs create mode 100644 crates/nash-language-server/src/workspace.rs diff --git a/Cargo.lock b/Cargo.lock index 6a39ec25..5230aac6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1988,7 +1988,14 @@ dependencies = [ name = "nash-language-server" version = "0.2.0" dependencies = [ + "nash-driver", + "nash-region", + "nash-report", + "serde_json", + "tempfile", + "tokio", "tower-lsp-server", + "url", ] [[package]] diff --git a/crates/nash-language-server/Cargo.toml b/crates/nash-language-server/Cargo.toml index 43bcf213..726a22e6 100644 --- a/crates/nash-language-server/Cargo.toml +++ b/crates/nash-language-server/Cargo.toml @@ -9,3 +9,12 @@ license.workspace = true [dependencies] tower-lsp-server.workspace = true +nash-driver = { path = "../nash-driver", version = "0.4.0" } +nash-report = { path = "../nash-report", version = "0.1.0" } +nash-region = { path = "../nash-region", version = "0.2.0" } +serde_json.workspace = true +tokio.workspace = true +url.workspace = true + +[dev-dependencies] +tempfile = "3" diff --git a/crates/nash-language-server/src/capabilities.rs b/crates/nash-language-server/src/capabilities.rs index 038ecd0c..15dc1c19 100644 --- a/crates/nash-language-server/src/capabilities.rs +++ b/crates/nash-language-server/src/capabilities.rs @@ -1,5 +1,18 @@ -use tower_lsp_server::ls_types::ServerCapabilities; +use tower_lsp_server::ls_types::{ + PositionEncodingKind, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, +}; pub fn server_capabilities() -> ServerCapabilities { - ServerCapabilities::default() + ServerCapabilities { + position_encoding: Some(PositionEncodingKind::UTF16), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::FULL), + ..Default::default() + }, + )), + ..ServerCapabilities::default() + } } diff --git a/crates/nash-language-server/src/diagnostics.rs b/crates/nash-language-server/src/diagnostics.rs new file mode 100644 index 00000000..1dca6e1a --- /dev/null +++ b/crates/nash-language-server/src/diagnostics.rs @@ -0,0 +1,129 @@ +//! 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 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), + severity: Some(match report.severity { + Severity::Error => DiagnosticSeverity::ERROR, + Severity::Warning => DiagnosticSeverity::WARNING, + }), + code: Some(NumberOrString::String(report.title.clone())), + 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 { + location: Location { + uri: uri.clone(), + range: to_range(region, source), + }, + message, + }] + }), + data: (!report.suggestions.is_empty()).then(|| serde_json::json!(report.suggestions)), + ..Diagnostic::default() + } +} + +pub fn to_range(region: Region, source: &Source<'_>) -> Range { + Range::new( + to_position(region.start, source), + to_position(region.end, source), + ) +} + +fn to_position(position: NashPosition, source: &Source<'_>) -> Position { + let offset = source.offset(position); + let prefix = &source.text()[..offset]; + let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32; + let character = prefix + .rsplit('\n') + .next() + .unwrap_or("") + .encode_utf16() + .count() as u32; + Position::new(line, character) +} + +#[cfg(test)] +mod tests { + use super::*; + use nash_report::{Doc, Label}; + fn region(sr: u16, sc: u16, er: u16, ec: u16) -> Region { + Region::new(NashPosition::new(sr, sc), NashPosition::new(er, ec)) + } + #[test] + fn ranges_count_utf16_surrogate_pairs() { + let source = Source::new("a😀éz\nlast"); + assert_eq!( + to_range(region(1, 6, 1, 8), &source), + Range::new(Position::new(0, 3), Position::new(0, 4)) + ); + assert_eq!( + to_range(region(2, 5, 2, 5), &source), + Range::new(Position::new(1, 4), Position::new(1, 4)) + ); + } + #[test] + fn pair_preserves_primary_and_related_range() { + let uri: Uri = "file:///test/Main.nash".parse().unwrap(); + let report = Report::pair( + "NAME CLASH", + Label { + region: region(1, 1, 1, 2), + text: "first name".into(), + }, + Label { + region: region(2, 1, 2, 2), + text: "second name".into(), + }, + Doc::text("Duplicate names:"), + Doc::text("Rename one."), + ); + 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.related_information.unwrap()[0].location.range, + to_range(region(1, 1, 1, 2), &source) + ); + assert_eq!(diagnostic.message, "Duplicate names:\n\nRename one."); + } + #[test] + fn highlighted_region_and_suggestions_survive() { + let source = Source::new("a = unknown"); + let mut report = Report::snippet( + "NAME", + region(1, 1, 1, 2), + Some(region(1, 5, 1, 12)), + Doc::text("Name:"), + Doc::Empty, + ) + .with_suggestions(vec!["known".into()]) + .warning(); + let uri = "file:///test/Main.nash".parse().unwrap(); + 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!(to_lsp(&report, &source, &uri).related_information.is_none()); + } +} diff --git a/crates/nash-language-server/src/lib.rs b/crates/nash-language-server/src/lib.rs index 353f2139..a5413b96 100644 --- a/crates/nash-language-server/src/lib.rs +++ b/crates/nash-language-server/src/lib.rs @@ -1,4 +1,6 @@ mod capabilities; +pub mod diagnostics; mod server; +mod workspace; pub use server::{SERVER_NAME, Server}; diff --git a/crates/nash-language-server/src/server.rs b/crates/nash-language-server/src/server.rs index a82e3afe..1259517f 100644 --- a/crates/nash-language-server/src/server.rs +++ b/crates/nash-language-server/src/server.rs @@ -1,20 +1,29 @@ +use tokio::sync::Mutex; use tower_lsp_server::jsonrpc::Result; use tower_lsp_server::ls_types::{ - InitializeParams, InitializeResult, InitializedParams, MessageType, ServerInfo, + DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + InitializeParams, InitializeResult, InitializedParams, MessageType, ServerInfo, Uri, }; use tower_lsp_server::{Client, LanguageServer}; +use url::Url; use crate::capabilities::server_capabilities; +use crate::workspace::Workspace; pub const SERVER_NAME: &str = "nash-language-server"; pub struct Server { client: Client, + // Hold through publication: an old build cannot overtake a newer edit. + workspace: Mutex, } impl Server { pub fn new(client: Client) -> Self { - Self { client } + Self { + client, + workspace: Mutex::new(Workspace::default()), + } } pub fn server_info() -> ServerInfo { @@ -23,10 +32,42 @@ impl Server { version: Some(env!("CARGO_PKG_VERSION").to_owned()), } } + + async fn publish(&self, workspace: &mut Workspace, uri: &Uri) { + let (notifications, error) = workspace.rebuild(uri).await; + for notification in notifications { + self.client + .publish_diagnostics( + notification.uri, + notification.diagnostics, + notification.version, + ) + .await; + } + if let Some(error) = error { + self.client.log_message(MessageType::ERROR, error).await; + } + } } impl LanguageServer for Server { - async fn initialize(&self, _: InitializeParams) -> Result { + async fn initialize(&self, params: InitializeParams) -> Result { + let mut workspace = self.workspace.lock().await; + workspace.roots = params + .workspace_folders + .unwrap_or_default() + .into_iter() + .filter_map(|folder| Url::parse(folder.uri.as_str()).ok()?.to_file_path().ok()) + .map(|path| path.canonicalize().unwrap_or(path)) + .collect(); + #[allow(deprecated)] + if workspace.roots.is_empty() + && let Some(uri) = params.root_uri + && let Ok(url) = Url::parse(uri.as_str()) + && let Ok(path) = url.to_file_path() + { + workspace.roots.push(path.canonicalize().unwrap_or(path)); + } Ok(InitializeResult { capabilities: server_capabilities(), server_info: Some(Self::server_info()), @@ -40,6 +81,39 @@ impl LanguageServer for Server { .await; } + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let document = params.text_document; + let mut workspace = self.workspace.lock().await; + if workspace.open(document.uri.clone(), document.text, document.version) { + self.publish(&mut workspace, &document.uri).await; + } + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let Some(change) = params.content_changes.into_iter().last() else { + return; + }; + if change.range.is_some() { + return; + } // FULL synchronization is advertised. + let mut workspace = self.workspace.lock().await; + if workspace.change( + ¶ms.text_document.uri, + change.text, + params.text_document.version, + ) { + self.publish(&mut workspace, ¶ms.text_document.uri) + .await; + } + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + let mut workspace = self.workspace.lock().await; + workspace.close(¶ms.text_document.uri); + self.publish(&mut workspace, ¶ms.text_document.uri) + .await; + } + async fn shutdown(&self) -> Result<()> { Ok(()) } diff --git a/crates/nash-language-server/src/workspace.rs b/crates/nash-language-server/src/workspace.rs new file mode 100644 index 00000000..65955e98 --- /dev/null +++ b/crates/nash-language-server/src/workspace.rs @@ -0,0 +1,499 @@ +//! Serialized editor state. Every build uses an immutable overlay snapshot. +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; +use std::sync::Arc; + +use nash_driver::{ + Database, DriverError, FileSystemSource, InMemorySource, ModuleOrigins, ModuleResult, + OverlaySource, Project, build, build_graph, +}; +use nash_report::Source; +use tokio::sync::Mutex; +use tower_lsp_server::ls_types::{ + Diagnostic, DiagnosticSeverity, NumberOrString, PublishDiagnosticsParams, Uri, +}; +use url::Url; + +use crate::diagnostics::to_lsp; + +struct Buffer { + text: String, + version: i32, +} + +#[derive(Default)] +pub(crate) struct Workspace { + pub roots: Vec, + buffers: BTreeMap, + uris: BTreeMap, + published: BTreeMap>, + closed: BTreeSet, +} + +impl Workspace { + pub fn open(&mut self, uri: Uri, text: String, version: i32) -> bool { + if self + .buffers + .get(&uri) + .is_some_and(|buffer| version <= buffer.version) + { + return false; + } + if let Some(url) = file_url(&uri) { + self.uris.insert(url, uri.clone()); + } + self.closed.remove(&uri); + self.buffers.insert(uri, Buffer { text, version }); + true + } + + pub fn change(&mut self, uri: &Uri, text: String, version: i32) -> bool { + if !self.buffers.contains_key(uri) { + return false; + } + self.open(uri.clone(), text, version) + } + + pub fn close(&mut self, uri: &Uri) { + self.buffers.remove(uri); + self.closed.insert(uri.clone()); + } + + fn client_uri(&self, url: &Url) -> Option { + self.uris + .get(url) + .cloned() + .or_else(|| url.as_str().parse().ok()) + } + + pub async fn rebuild( + &mut self, + trigger: &Uri, + ) -> (Vec, Option) { + let Some(url) = file_url(trigger) else { + return (vec![], Some("Invalid document URI".into())); + }; + let Ok(path) = url.to_file_path() else { + return (vec![], Some("Only file documents can be compiled".into())); + }; + let parent = path.parent().unwrap_or(&path).to_path_buf(); + let root = self + .roots + .iter() + .filter(|root| path.starts_with(root)) + .max_by_key(|root| root.components().count()); + let project = match root { + Some(root) => match Project::load(root).await { + Ok(project) + if project + .source_directories() + .iter() + .any(|dir| path.starts_with(dir)) => + { + Ok(project) + } + Ok(_) | Err(DriverError::ProjectNotFound { .. }) => Project::load(&parent).await, + Err(error) => Err(error), + }, + None => Project::load(&parent).await, + }; + let scope = project.as_ref().map_or_else( + |_| { + self.published + .keys() + .filter(|root| path.starts_with(root)) + .max_by_key(|root| root.components().count()) + .cloned() + .unwrap_or_else(|| parent.clone()) + }, + |project| project.root.clone(), + ); + let source_dirs = project.as_ref().ok().map(Project::source_directories); + let overlay = + InMemorySource::with_files(self.buffers.iter().filter_map(|(uri, buffer)| { + let url = file_url(uri)?; + let path = url.to_file_path().ok()?; + if source_dirs + .as_ref() + .is_some_and(|dirs| !dirs.iter().any(|dir| path.starts_with(dir))) + { + return None; + } + Some((url, buffer.text.clone())) + })); + let db = Arc::new(Mutex::new(Database::new(OverlaySource::new( + overlay, + FileSystemSource::new(), + )))); + let modules = match project { + Ok(project) => project.discover_modules(&*db.lock().await).await, + // Standalone files still get parser, name, and type diagnostics. + Err(DriverError::ProjectNotFound { .. }) => Ok(self + .buffers + .keys() + .filter_map(|uri| { + let uri = file_url(uri)?; + let candidate = uri.to_file_path().ok()?; + (candidate.parent() == Some(parent.as_path())).then_some((uri, None)) + }) + .collect::()), + Err(error) => Err(error), + }; + let result = async { + let modules = modules?; + let graph = + build_graph(db.clone(), &modules.keys().cloned().collect::>()).await?; + Ok::<_, DriverError>(build(db, &graph, &modules).await) + } + .await; + let mut diagnostics: BTreeMap> = BTreeMap::new(); + let error = match result { + Ok(result) => { + for (uri, module) in &result.modules { + if let Some(uri) = self.client_uri(uri) { + let problems = match module { + ModuleResult::SourceUnavailable { message } => vec![Diagnostic { + severity: Some(DiagnosticSeverity::ERROR), + code: Some(NumberOrString::String("SOURCE UNAVAILABLE".into())), + source: Some("nash".into()), + message: message.clone(), + ..Diagnostic::default() + }], + _ => vec![], + }; + diagnostics.insert(uri, problems); + } + } + for module in result.ordered_reports() { + if let Ok(url) = Url::from_file_path(&module.path) + && let Some(uri) = self.client_uri(&url) + { + let source = Source::new(&module.source); + diagnostics.entry(uri.clone()).or_default().extend( + module + .reports + .iter() + .map(|report| to_lsp(report, &source, &uri)), + ); + } + } + None + } + Err(error) => Some(error.to_string()), + }; + diagnostics.retain(|uri, _| !self.closed.contains(uri)); + let current: BTreeSet<_> = diagnostics.keys().cloned().collect(); + let previous = self.published.insert(scope, current).unwrap_or_default(); + for uri in previous { + diagnostics.entry(uri).or_default(); + } + // Always acknowledge this document, including clean buffers and close. + diagnostics.entry(trigger.clone()).or_default(); + let notifications = diagnostics + .into_iter() + .map(|(uri, diagnostics)| { + let version = self.buffers.get(&uri).map(|buffer| buffer.version); + PublishDiagnosticsParams { + uri, + diagnostics, + version, + } + }) + .collect(); + (notifications, error) + } +} + +// Project discovery canonicalizes roots. Keep the client's original URI for +// publications while matching disk and overlay files by canonical path. +fn file_url(uri: &Uri) -> Option { + let url = Url::parse(uri.as_str()).ok()?; + let path = url.to_file_path().ok()?; + let canonical = path + .canonicalize() + .ok() + .or_else(|| Some(path.parent()?.canonicalize().ok()?.join(path.file_name()?))) + .unwrap_or(path); + Url::from_file_path(canonical).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use nash_report::json::module_to_json; + + const CLEAN: &str = "module Main exposing (..)\nvalue = ()\n"; + const BROKEN: &str = "module Main exposing (..)\nvalue = unknown\n"; + + fn fixture() -> (tempfile::TempDir, Uri) { + let dir = tempfile::tempdir().unwrap(); + std::fs::create_dir(dir.path().join("src")).unwrap(); + std::fs::write( + dir.path().join("nash.jsonc"), + r#"{"type":"application","sourceDirectories":["src"]}"#, + ) + .unwrap(); + let path = dir.path().join("src/Main.nash"); + std::fs::write(&path, CLEAN).unwrap(); + let uri = Url::from_file_path(path).unwrap().as_str().parse().unwrap(); + (dir, uri) + } + + fn for_uri<'a>( + notifications: &'a [PublishDiagnosticsParams], + uri: &Uri, + ) -> &'a PublishDiagnosticsParams { + notifications + .iter() + .find(|notification| ¬ification.uri == uri) + .unwrap() + } + + #[tokio::test] + async fn unsaved_versions_override_disk_and_clear_fixed_errors() { + let (_dir, uri) = fixture(); + let mut workspace = Workspace::default(); + assert!(workspace.open(uri.clone(), BROKEN.into(), 1)); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + let notification = for_uri(¬ifications, &uri); + assert_eq!(notification.version, Some(1)); + assert!(!notification.diagnostics.is_empty()); + assert!(workspace.change(&uri, CLEAN.into(), 3)); + assert!(!workspace.change(&uri, BROKEN.into(), 2)); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + let notification = for_uri(¬ifications, &uri); + assert_eq!(notification.version, Some(3)); + assert!( + notification.diagnostics.is_empty(), + "{:?}", + notification.diagnostics + ); + assert_eq!( + std::fs::read_to_string(Url::parse(uri.as_str()).unwrap().to_file_path().unwrap()) + .unwrap(), + CLEAN + ); + } + + #[tokio::test] + async fn close_discards_overlay_and_clears_diagnostics() { + let (_dir, uri) = fixture(); + let mut workspace = Workspace::default(); + workspace.open(uri.clone(), BROKEN.into(), 8); + assert!( + !for_uri(&workspace.rebuild(&uri).await.0, &uri) + .diagnostics + .is_empty() + ); + workspace.close(&uri); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + let notification = for_uri(¬ifications, &uri); + assert_eq!(notification.version, None); + assert!(notification.diagnostics.is_empty()); + assert!(!workspace.change(&uri, BROKEN.into(), 9)); + } + + #[tokio::test] + async fn lsp_and_json_contain_the_same_problem_set_and_spans() { + let (dir, uri) = fixture(); + let mut workspace = Workspace::default(); + workspace.open(uri.clone(), BROKEN.into(), 1); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + let lsp = &for_uri(¬ifications, &uri).diagnostics; + let overlay = InMemorySource::with_files([(file_url(&uri).unwrap(), BROKEN.into())]); + let db = Arc::new(Mutex::new(Database::new(OverlaySource::new( + overlay, + FileSystemSource::new(), + )))); + let project = Project::load(dir.path()).await.unwrap(); + let modules = project.discover_modules(&*db.lock().await).await.unwrap(); + let graph = build_graph(db.clone(), &modules.keys().cloned().collect::>()) + .await + .unwrap(); + let result = build(db, &graph, &modules).await; + let modules = result.ordered_reports(); + let problems: Vec<_> = modules + .iter() + .flat_map(|module| { + module_to_json(module)["problems"] + .as_array() + .unwrap() + .clone() + }) + .collect(); + assert_eq!(lsp.len(), problems.len()); + for (diagnostic, problem) in lsp.iter().zip(problems) { + assert_eq!( + serde_json::to_value(&diagnostic.code).unwrap(), + problem["title"] + ); + assert_eq!( + diagnostic.range.start.line + 1, + problem["region"]["start"]["line"].as_u64().unwrap() as u32 + ); + assert_eq!( + diagnostic.range.start.character + 1, + problem["region"]["start"]["column"].as_u64().unwrap() as u32 + ); + assert_eq!( + diagnostic.range.end.line + 1, + problem["region"]["end"]["line"].as_u64().unwrap() as u32 + ); + assert_eq!( + diagnostic.range.end.character + 1, + problem["region"]["end"]["column"].as_u64().unwrap() as u32 + ); + } + } + + #[tokio::test] + async fn independent_disk_module_errors_are_published_and_cleared() { + let (dir, uri) = fixture(); + let other_path = dir.path().join("src/Other.nash"); + let other: Uri = Url::from_file_path( + other_path + .parent() + .unwrap() + .canonicalize() + .unwrap() + .join("Other.nash"), + ) + .unwrap() + .as_str() + .parse() + .unwrap(); + std::fs::write(&other_path, "module Other exposing (..)\nvalue = missing\n").unwrap(); + let mut workspace = Workspace::default(); + workspace.open(uri.clone(), BROKEN.into(), 1); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + assert!(!for_uri(¬ifications, &uri).diagnostics.is_empty()); + assert!(!for_uri(¬ifications, &other).diagnostics.is_empty()); + std::fs::remove_file(other_path).unwrap(); + let (notifications, _) = workspace.rebuild(&uri).await; + assert!(for_uri(¬ifications, &other).diagnostics.is_empty()); + } + #[tokio::test] + async fn close_rebuilds_dependents_from_disk_without_cascade_errors() { + let (dir, uri) = fixture(); + let other_path = dir.path().join("src/Other.nash"); + std::fs::write(&other_path, "module Other exposing (..)\nvalue = ()\n").unwrap(); + let other: Uri = Url::from_file_path(&other_path) + .unwrap() + .as_str() + .parse() + .unwrap(); + let mut workspace = Workspace::default(); + workspace.open( + uri.clone(), + "module Main exposing (..)\nimport Other\nvalue = Other.value\n".into(), + 1, + ); + workspace.open( + other.clone(), + "module Other exposing (..)\nvalue = missing\n".into(), + 1, + ); + let (notifications, error) = workspace.rebuild(&other).await; + assert!(error.is_none(), "{error:?}"); + assert!(!for_uri(¬ifications, &other).diagnostics.is_empty()); + assert!(for_uri(¬ifications, &uri).diagnostics.is_empty()); + workspace.close(&other); + let (notifications, error) = workspace.rebuild(&other).await; + assert!(error.is_none(), "{error:?}"); + assert!(for_uri(¬ifications, &other).diagnostics.is_empty()); + assert!(for_uri(¬ifications, &uri).diagnostics.is_empty()); + } + + #[tokio::test] + async fn unsaved_new_file_is_discovered() { + let (dir, _) = fixture(); + let uri: Uri = Url::from_file_path(dir.path().join("src/New.nash")) + .unwrap() + .as_str() + .parse() + .unwrap(); + let mut workspace = Workspace::default(); + workspace.open( + uri.clone(), + "module New exposing (..)\nvalue = missing\n".into(), + 1, + ); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + assert!(!for_uri(¬ifications, &uri).diagnostics.is_empty()); + } + #[tokio::test] + async fn nested_projects_use_the_most_specific_owning_root() { + let (dir, _) = fixture(); + let nested = dir.path().join("nested"); + std::fs::create_dir_all(nested.join("src")).unwrap(); + std::fs::write( + nested.join("nash.jsonc"), + r#"{"type":"application","sourceDirectories":["src"]}"#, + ) + .unwrap(); + let path = nested.join("src/Main.nash"); + std::fs::write(&path, CLEAN).unwrap(); + let uri: Uri = Url::from_file_path(path).unwrap().as_str().parse().unwrap(); + // Also cover a nested project not separately registered by the client: + // the parent application does not own its source directory. + for roots in [ + vec![ + dir.path().canonicalize().unwrap(), + nested.canonicalize().unwrap(), + ], + vec![dir.path().canonicalize().unwrap()], + ] { + let mut workspace = Workspace { + roots, + ..Workspace::default() + }; + workspace.open(uri.clone(), BROKEN.into(), 1); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + assert!(!for_uri(¬ifications, &uri).diagnostics.is_empty()); + } + } + + #[tokio::test] + async fn unreadable_source_has_a_diagnostic_that_clears_after_repair() { + let (dir, uri) = fixture(); + let unavailable = dir.path().join("src/Unavailable.nash"); + // Reading a directory is a deterministic I/O failure even as root. + std::fs::create_dir(&unavailable).unwrap(); + let unavailable_uri: Uri = Url::from_file_path(unavailable.canonicalize().unwrap()) + .unwrap() + .as_str() + .parse() + .unwrap(); + let mut workspace = Workspace::default(); + workspace.open(uri.clone(), BROKEN.into(), 1); + let (notifications, _) = workspace.rebuild(&uri).await; + let diagnostic = &for_uri(¬ifications, &unavailable_uri).diagnostics[0]; + assert_eq!( + diagnostic.code, + Some(NumberOrString::String("SOURCE UNAVAILABLE".into())) + ); + assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR)); + assert!(diagnostic.message.contains("read")); + assert!(!for_uri(¬ifications, &uri).diagnostics.is_empty()); + std::fs::remove_dir(&unavailable).unwrap(); + std::fs::write( + &unavailable, + "module Unavailable exposing (..)\nvalue = ()\n", + ) + .unwrap(); + let (notifications, error) = workspace.rebuild(&uri).await; + assert!(error.is_none(), "{error:?}"); + assert!( + for_uri(¬ifications, &unavailable_uri) + .diagnostics + .is_empty() + ); + } +} From b7ff823b144beb39d5cc355052710b7032f0509e Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 10 Sep 2026 00:16:50 -0400 Subject: [PATCH 12/12] test(report): verify diagnostics and finish plan Signed-off-by: microproofs --- .sampo/changesets/report-foundation.md | 6 + .sampo/changesets/report-wiring.md | 10 + SPEC.md | 8 +- crates/nash-cli/tests/diagnostics.rs | 317 ++++++++++++++++++ .../diagnostic-examples/app/nash.jsonc | 1 + .../diagnostic-examples/app/src/Ledger.nash | 11 + .../diagnostic-examples/app/src/Steps.nash | 8 + .../diagnostic-examples/app/src/Tag.nash | 9 + .../fixtures/diagnostic-examples/nash.jsonc | 7 + .../tests/fixtures/type-mismatch/nash.jsonc | 1 + .../fixtures/type-mismatch/src/Main.nash | 5 + ...diagnostics__documented_examples_json.snap | 173 ++++++++++ ...nostics__documented_examples_terminal.snap | 73 ++++ .../diagnostics__poisoned_tuple_json.snap | 75 +++++ .../diagnostics__poisoned_tuple_terminal.snap | 36 ++ .../diagnostics__type_mismatch_json.snap | 51 +++ .../diagnostics__type_mismatch_terminal.snap | 21 ++ docs/diagnostics.md | 112 ++++++- plans/06-diagnostics.md | 98 +++++- 19 files changed, 994 insertions(+), 28 deletions(-) create mode 100644 .sampo/changesets/report-foundation.md create mode 100644 .sampo/changesets/report-wiring.md create mode 100644 crates/nash-cli/tests/diagnostics.rs create mode 100644 crates/nash-cli/tests/fixtures/diagnostic-examples/app/nash.jsonc create mode 100644 crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Ledger.nash create mode 100644 crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Steps.nash create mode 100644 crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Tag.nash create mode 100644 crates/nash-cli/tests/fixtures/diagnostic-examples/nash.jsonc create mode 100644 crates/nash-cli/tests/fixtures/type-mismatch/nash.jsonc create mode 100644 crates/nash-cli/tests/fixtures/type-mismatch/src/Main.nash create mode 100644 crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap create mode 100644 crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap create mode 100644 crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_json.snap create mode 100644 crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap create mode 100644 crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_json.snap create mode 100644 crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap diff --git a/.sampo/changesets/report-foundation.md b/.sampo/changesets/report-foundation.md new file mode 100644 index 00000000..a9f146e9 --- /dev/null +++ b/.sampo/changesets/report-foundation.md @@ -0,0 +1,6 @@ +--- +cargo/nash-report: minor +cargo/nash-parse: patch +--- + +Add compiler report documents, source spans, terminal rendering and name suggestions. Expose parser token classifiers and preserve nested parse errors for diagnostics. diff --git a/.sampo/changesets/report-wiring.md b/.sampo/changesets/report-wiring.md new file mode 100644 index 00000000..28792dd6 --- /dev/null +++ b/.sampo/changesets/report-wiring.md @@ -0,0 +1,10 @@ +--- +cargo/nash-can: patch +cargo/nash-constrain: patch +cargo/nash-solve: patch +cargo/nash-driver: minor +cargo/nash-cli: minor +cargo/nash-language-server: minor +--- + +Collect independent compiler errors with dependency-aware recovery, retain failed module dependencies, and render owned diagnostics in the terminal, JSON, and language server. Preserve trait-method call names in error context. Add JSON and warning controls to `nash check`, and publish diagnostics for unsaved editor buffers with UTF-16 ranges. diff --git a/SPEC.md b/SPEC.md index c53da0d4..c2743163 100644 --- a/SPEC.md +++ b/SPEC.md @@ -27,7 +27,7 @@ produce UPLC programs; all dependencies inline into each program. | `nash-constrain` | constraint generation, kinds | extend | | `nash-solve` | solver, traits, defaulting | extend | | `nash-nitpick` | exhaustiveness and redundancy | done ([plans/05](plans/05-nitpick.md)) | -| `nash-report` | diagnostics (Elm prose -> miette) | new ([plans/06](plans/06-diagnostics.md)) | +| `nash-report` | diagnostics (Elm prose -> miette) | 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)) | @@ -37,7 +37,7 @@ produce UPLC programs; all dependencies inline into each program. | `nash-config` | `nash.jsonc` | done, extend | | `nash-driver` | build graph, caching | done, extend | | `nash-cli` | `nash` binary | `check`, `lsp`; add `build test fmt docs` | -| `nash-language-server` | LSP | stub | +| `nash-language-server` | LSP | live compiler diagnostics with UTF-16 ranges | | `core/` | `nash/core` stdlib package (Nash source) | new ([plans/12](plans/12-stdlib.md)) | ## Progress @@ -60,7 +60,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) -- [ ] 06 Diagnostics (`nash-report`, Elm `Reporting/*` port onto miette) — [plans/06-diagnostics.md](plans/06-diagnostics.md) +- [x] 06 Diagnostics (`nash-report`, Elm `Reporting/*` port onto miette) — [plans/06-diagnostics.md](plans/06-diagnostics.md) - [ ] 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) @@ -83,7 +83,7 @@ Later: LSP features, web playground, package registry (pubgrub), TypeScript code | `Type/Type.hs`, `Type/Constrain/*` | `crates/nash-constrain/src/*` | | `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` (planned) | +| `Reporting/{Doc,Report,Render,Suggest}.hs`, `Reporting/Error/*` | `crates/nash-report` | | `builder/src/Elm/Outline.hs` | `crates/nash-config` | | `builder/src/Build.hs` | `crates/nash-driver` | diff --git a/crates/nash-cli/tests/diagnostics.rs b/crates/nash-cli/tests/diagnostics.rs new file mode 100644 index 00000000..6bce20c5 --- /dev/null +++ b/crates/nash-cli/tests/diagnostics.rs @@ -0,0 +1,317 @@ +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +fn check(path: &Path, args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_nash")) + .env("NASH_PROXY_VERSION", env!("CARGO_PKG_VERSION")) + .env("NO_COLOR", "1") + .args(["check"]) + .arg(path) + .args(args) + .output() + .unwrap() +} + +struct Project(PathBuf); +impl Project { + fn new(files: &[(&str, &str)]) -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + let path = std::env::temp_dir().join(format!( + "nash-cli-diagnostics-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(path.join("src")).unwrap(); + std::fs::write( + path.join("nash.jsonc"), + r#"{"type":"application","sourceDirectories":["src"]}"#, + ) + .unwrap(); + for (name, text) in files { + std::fs::write(path.join("src").join(format!("{name}.nash")), text).unwrap(); + } + Self(path.canonicalize().unwrap()) + } +} +impl Drop for Project { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.0).unwrap(); + } +} +fn normalized(text: &[u8], root: &Path) -> String { + String::from_utf8(text.to_vec()) + .unwrap() + .replace(root.to_str().unwrap(), "") +} + +#[test] +fn terminal_and_json_type_mismatch() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/type-mismatch") + .canonicalize() + .unwrap(); + let human = check(&root, &[]); + assert_eq!(human.status.code(), Some(1)); + assert!(human.stdout.is_empty()); + let text = normalized(&human.stderr, &root); + assert!(!text.contains('\u{1b}')); + assert!(text.contains("TYPE MISMATCH")); + insta::assert_snapshot!("type_mismatch_terminal", text); + let json = check(&root, &["--report=json"]); + assert_eq!(json.status.code(), Some(1)); + assert!( + json.stderr.is_empty(), + "{}", + String::from_utf8_lossy(&json.stderr) + ); + let value: serde_json::Value = serde_json::from_slice(&json.stdout).unwrap(); + assert_eq!(value["errors"][0]["problems"][0]["title"], "TYPE MISMATCH"); + insta::assert_snapshot!( + "type_mismatch_json", + normalized( + serde_json::to_string_pretty(&value).unwrap().as_bytes(), + &root + ) + ); +} + +#[test] +fn warnings_keep_success_exit_and_can_be_hidden() { + let project = Project::new(&[("Main", "module Main exposing (..)\nf unused = ()\n")]); + let output = check(&project.0, &["--report=json"]); + assert!(output.status.success()); + let errors: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(errors["errors"], serde_json::json!([])); + let warnings: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap(); + assert_eq!(warnings["type"], "compile-warnings"); + assert_eq!( + warnings["errors"][0]["problems"][0]["title"], + "unused variable" + ); + let hidden = check(&project.0, &["--report=json", "--no-warnings"]); + assert!(hidden.status.success()); + assert!(hidden.stderr.is_empty()); +} + +#[test] +fn independent_errors_are_stable_and_dependents_are_blocked() { + let project = Project::new(&[ + ("Z", "module Z exposing (..)\nx = missingZ\n"), + ("A", "module A exposing (..)\nx = missingA\ny = missingB\n"), + ("Main", "module Main exposing (..)\nimport Z\nx = Z.x\n"), + ]); + let first = check(&project.0, &["--report=json"]); + assert_eq!(first.status.code(), Some(1)); + for _ in 0..3 { + assert_eq!(check(&project.0, &["--report=json"]).stdout, first.stdout); + } + let json: serde_json::Value = serde_json::from_slice(&first.stdout).unwrap(); + let modules = json["errors"].as_array().unwrap(); + assert_eq!( + modules + .iter() + .map(|module| module["name"].as_str().unwrap()) + .collect::>(), + ["A", "Z"] + ); + 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!(text.contains("Skipped")); + assert!(!text.contains("IMPORT PROBLEM")); +} + +#[test] +fn forced_color_is_consistent_and_json_has_no_ansi() { + let project = Project::new(&[("Main", "module Main exposing (..)\nx = missing\n")]); + let human = check(&project.0, &["--color=always"]); + assert!(String::from_utf8_lossy(&human.stderr).contains('\u{1b}')); + let json = check(&project.0, &["--color=always", "--report=json"]); + assert!(!String::from_utf8_lossy(&json.stdout).contains('\u{1b}')); + serde_json::from_slice::(&json.stdout).unwrap(); +} + +#[test] +fn documented_examples_run_through_the_real_core_package() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/diagnostic-examples") + .canonicalize() + .unwrap(); + 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}"); + let docs = include_str!("../../../docs/diagnostics.md"); + let names = [ + "TYPE MISMATCH", + "MISSING IMPL", + "MISSING PATTERNS", + "Compilation failed:", + ]; + for pair in names.windows(2) { + let start = text.find(&format!("{}\n", pair[0])).unwrap(); + let end = text[start..].find(pair[1]).unwrap() + start; + let block = text[start..end] + .trim_end() + .replace("/app/src/", "src/"); + assert!(docs.contains(&block), "documented output differs:\n{block}"); + } + insta::assert_snapshot!("documented_examples_terminal", text); + let output = check(&root, &["--report=json", "--no-warnings"]); + assert!(output.stderr.is_empty()); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let modules = json["errors"].as_array().unwrap(); + assert_eq!( + modules + .iter() + .map(|module| ( + module["name"].as_str().unwrap(), + module["problems"][0]["title"].as_str().unwrap() + )) + .collect::>(), + [ + ("Ledger", "TYPE MISMATCH"), + ("Steps", "MISSING IMPL"), + ("Tag", "MISSING PATTERNS") + ] + ); + insta::assert_snapshot!( + "documented_examples_json", + normalized( + serde_json::to_string_pretty(&json).unwrap().as_bytes(), + &root + ) + ); +} + +#[tokio::test] +async fn mixed_errors_match_across_terminal_json_and_lsp() { + use nash_driver::{Database, FileSystemSource, Project as DriverProject, build, build_graph}; + use std::sync::Arc; + use tokio::sync::Mutex; + let header = "module Main exposing (..)\ntrait Round 'a where\n create : () -> 'a\n discard : 'a -> ()\ntype higher 'f = Higher ('f ())\nidfa : 'f 'a -> 'f 'a\nidfa x = x\n"; + let definitions = [ + "mismatch : ()\nmismatch = \\x -> x\n", + "missing = discard ()\n", + "constraint : 'a -> ()\nconstraint x = discard x\n", + "ambiguous = discard (create ())\n", + "kind = idfa (Higher [])\n", + ]; + for reverse in [false, true] { + let mut definitions = definitions.to_vec(); + if reverse { + definitions.reverse(); + } + let source = format!("{header}{}", definitions.concat()); + let project = Project::new(&[("Main", &source)]); + 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 problems = json["errors"][0]["problems"].as_array().unwrap(); + let mut titles = problems + .iter() + .map(|p| p["title"].as_str().unwrap()) + .collect::>(); + titles.sort(); + assert_eq!( + titles, + [ + "AMBIGUOUS TYPE", + "KIND MISMATCH", + "MISSING CONSTRAINT", + "MISSING IMPL", + "TYPE MISMATCH" + ] + ); + let human = check(&project.0, &["--no-warnings"]); + let text = String::from_utf8(human.stderr).unwrap(); + let mut previous = 0; + for problem in problems { + let title = problem["title"].as_str().unwrap(); + assert_eq!(text.matches(&format!("{title}\n")).count(), 1); + let position = text.find(&format!("{title}\n")).unwrap(); + assert!(position >= previous); + previous = position; + } + let db = Arc::new(Mutex::new(Database::new(FileSystemSource::new()))); + let loaded = DriverProject::load(&project.0).await.unwrap(); + let modules = loaded.discover_modules(&*db.lock().await).await.unwrap(); + let graph = build_graph(db.clone(), &modules.keys().cloned().collect::>()) + .await + .unwrap(); + let result = build(db, &graph, &modules).await; + assert!(result.interfaces.is_empty()); + let reports = result.ordered_reports(); + let module = reports[0]; + let source = nash_report::Source::new(&module.source); + let uri = format!("file://{}", module.path).parse().unwrap(); + let errors: Vec<_> = module + .reports + .iter() + .filter(|report| report.severity == nash_report::Severity::Error) + .collect(); + assert_eq!(errors.len(), problems.len()); + for (report, json) in errors.into_iter().zip(problems) { + assert_eq!(report.title, json["title"]); + assert_eq!( + nash_report::json::encode_region(report.region), + json["region"] + ); + let location = format!( + "[{}:{}:{}]", + module.path, report.region.start.line, report.region.start.column + ); + let rendered = nash_report::render_plain(report, &source, &module.path); + assert!( + rendered.contains(&location), + "{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!( + u64::from(lsp.range.start.line) + 1, + json["region"]["start"]["line"] + ); + assert_eq!( + u64::from(lsp.range.start.character) + 1, + json["region"]["start"]["column"] + ); + assert_eq!( + u64::from(lsp.range.end.line) + 1, + json["region"]["end"]["line"] + ); + assert_eq!( + u64::from(lsp.range.end.character) + 1, + json["region"]["end"]["column"] + ); + } + } +} + +#[test] +fn poisoned_tuple_child_keeps_independent_type_mismatch() { + let project = Project::new(&[( + "Main", + "module Main exposing (..)\nbad : ((), ())\nbad = (().field, \\x -> x)\n", + )]); + let json = check(&project.0, &["--report=json", "--no-warnings"]); + assert_eq!(json.status.code(), Some(1)); + let value: serde_json::Value = serde_json::from_slice(&json.stdout).unwrap(); + let problems = value["errors"][0]["problems"].as_array().unwrap(); + assert_eq!(problems.len(), 2, "{value:#}"); + let human = check(&project.0, &["--no-warnings"]); + assert_eq!(human.status.code(), Some(1)); + insta::assert_snapshot!( + "poisoned_tuple_terminal", + normalized(&human.stderr, &project.0) + ); + insta::assert_snapshot!( + "poisoned_tuple_json", + normalized( + serde_json::to_string_pretty(&value).unwrap().as_bytes(), + &project.0 + ) + ); +} diff --git a/crates/nash-cli/tests/fixtures/diagnostic-examples/app/nash.jsonc b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/nash.jsonc new file mode 100644 index 00000000..4a532462 --- /dev/null +++ b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/nash.jsonc @@ -0,0 +1 @@ +{"type":"application","sourceDirectories":["src"]} diff --git a/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Ledger.nash b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Ledger.nash new file mode 100644 index 00000000..ac1848ef --- /dev/null +++ b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Ledger.nash @@ -0,0 +1,11 @@ +module Ledger exposing (settle) +import Functor exposing (Functor) + +type alias Account = { owner : Bytes, balance : Int } + +balanceOf : Account -> Int +balanceOf account = account.balance + +settle : list Account -> list int +settle accounts = + map balanceOf accounts diff --git a/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Steps.nash b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Steps.nash new file mode 100644 index 00000000..bc9e2cb1 --- /dev/null +++ b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Steps.nash @@ -0,0 +1,8 @@ +module Steps exposing (isDone) +import Prelude exposing ((==)) +import Eq exposing (Eq) + +type step = Done | Next int + +isDone : step -> bool +isDone s = s == Done diff --git a/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Tag.nash b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Tag.nash new file mode 100644 index 00000000..1b6f2401 --- /dev/null +++ b/crates/nash-cli/tests/fixtures/diagnostic-examples/app/src/Tag.nash @@ -0,0 +1,9 @@ +module Tag exposing (tag) +import Builtin exposing (Data(..)) +import Literal exposing (FromInt) + +tag : Data -> int +tag d = + case d of + Constr n _ -> n + List _ -> 0 diff --git a/crates/nash-cli/tests/fixtures/diagnostic-examples/nash.jsonc b/crates/nash-cli/tests/fixtures/diagnostic-examples/nash.jsonc new file mode 100644 index 00000000..70394539 --- /dev/null +++ b/crates/nash-cli/tests/fixtures/diagnostic-examples/nash.jsonc @@ -0,0 +1,7 @@ +{ + "type": "workspace", + "members": [ + "../../../../../core", + "app" + ] +} diff --git a/crates/nash-cli/tests/fixtures/type-mismatch/nash.jsonc b/crates/nash-cli/tests/fixtures/type-mismatch/nash.jsonc new file mode 100644 index 00000000..4a532462 --- /dev/null +++ b/crates/nash-cli/tests/fixtures/type-mismatch/nash.jsonc @@ -0,0 +1 @@ +{"type":"application","sourceDirectories":["src"]} diff --git a/crates/nash-cli/tests/fixtures/type-mismatch/src/Main.nash b/crates/nash-cli/tests/fixtures/type-mismatch/src/Main.nash new file mode 100644 index 00000000..de8fbc6f --- /dev/null +++ b/crates/nash-cli/tests/fixtures/type-mismatch/src/Main.nash @@ -0,0 +1,5 @@ +module Main exposing (..) +import Builtin exposing (type bool(..)) + +identity : bool -> unit +identity flag = flag diff --git a/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap new file mode 100644 index 00000000..0fd54f2c --- /dev/null +++ b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_json.snap @@ -0,0 +1,173 @@ +--- +source: crates/nash-cli/tests/diagnostics.rs +expression: "normalized(serde_json::to_string_pretty(&json).unwrap().as_bytes(), &root)" +--- +{ + "errors": [ + { + "name": "Ledger", + "path": "/app/src/Ledger.nash", + "problems": [ + { + "message": [ + "Something is off with the body of the `settle` definition:\n\n11| map balanceOf accounts\n ", + { + "bold": false, + "color": "RED", + "string": "^^^^^^^^^^^^^^^^^^^^^^", + "underline": false + }, + "\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 ", + { + "bold": false, + "color": "yellow", + "string": "int", + "underline": false + }, + "\n\n", + { + "bold": false, + "color": null, + "string": "Hint", + "underline": true + }, + ": `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." + ], + "region": { + "end": { + "column": 27, + "line": 11 + }, + "start": { + "column": 5, + "line": 11 + } + }, + "title": "TYPE MISMATCH" + } + ] + }, + { + "name": "Steps", + "path": "/app/src/Steps.nash", + "problems": [ + { + "message": [ + "I cannot find an `Eq` impl for `step`:\n\n8| isDone s = s == Done\n ", + { + "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 = ..." + ], + "region": { + "end": { + "column": 21, + "line": 8 + }, + "start": { + "column": 12, + "line": 8 + } + }, + "title": "MISSING IMPL" + } + ] + }, + { + "name": "Tag", + "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|", + { + "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 ", + { + "bold": false, + "color": "yellow", + "string": "Map _", + "underline": false + }, + "\n ", + { + "bold": false, + "color": "yellow", + "string": "I _", + "underline": false + }, + "\n ", + { + "bold": false, + "color": "yellow", + "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." + ], + "region": { + "end": { + "column": 1, + "line": 10 + }, + "start": { + "column": 5, + "line": 7 + } + }, + "title": "MISSING PATTERNS" + } + ] + } + ], + "type": "compile-errors" +} diff --git a/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap new file mode 100644 index 00000000..b0c27da8 --- /dev/null +++ b/crates/nash-cli/tests/snapshots/diagnostics__documented_examples_terminal.snap @@ -0,0 +1,73 @@ +--- +source: crates/nash-cli/tests/diagnostics.rs +expression: text +--- +TYPE MISMATCH + + × Something is off with the body of the `settle` definition: + ╭─[/app/src/Ledger.nash:11:5] + 10 │ settle accounts = + 11 │ map balanceOf accounts + · ────────────────────── + ╰──── + help: This `map` call produces: + + list Int + + 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`: + ╭─[/app/src/Steps.nash:8:12] + 7 │ isDone : step -> bool + 8 │ isDone s = s == Done + · ───────── + ╰──── + 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: + + 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 = ... + +MISSING PATTERNS + + × This `case` does not have branches for all possibilities: + ╭─[/app/src/Tag.nash:7:5] + 6 │ tag d = + 7 │ ╭─▶ case d of + 8 │ │ Constr n _ -> n + 9 │ ╰─▶ List _ -> 0 + ╰──── + help: Missing possibilities include: + + 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. + +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 new file mode 100644 index 00000000..2b420705 --- /dev/null +++ b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_json.snap @@ -0,0 +1,75 @@ +--- +source: crates/nash-cli/tests/diagnostics.rs +expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes(),\n&project.0)" +--- +{ + "errors": [ + { + "name": "Main", + "path": "/src/Main.nash", + "problems": [ + { + "message": [ + "Something is off with the body of the `bad` definition:\n\n3| bad = (().field, \\x -> x)\n ", + { + "bold": false, + "color": "RED", + "string": "^^^^^^^^^^^^^^^^^^^", + "underline": false + }, + "\nThe body is a tuple of type:\n\n ( ?, ", + { + "bold": false, + "color": "yellow", + "string": "'a -> 'a", + "underline": false + }, + " )\n\nBut the type annotation on `bad` says it should be:\n\n ( ?, ", + { + "bold": false, + "color": "yellow", + "string": "unit", + "underline": false + }, + " )" + ], + "region": { + "end": { + "column": 26, + "line": 3 + }, + "start": { + "column": 7, + "line": 3 + } + }, + "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 ", + { + "bold": false, + "color": "RED", + "string": "^^^^^^^^", + "underline": false + }, + "\nIt has type:\n\n unit\n\nBut I need a value with record fields!" + ], + "region": { + "end": { + "column": 16, + "line": 3 + }, + "start": { + "column": 8, + "line": 3 + } + }, + "title": "TYPE MISMATCH" + } + ] + } + ], + "type": "compile-errors" +} diff --git a/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap new file mode 100644 index 00000000..1a7f716d --- /dev/null +++ b/crates/nash-cli/tests/snapshots/diagnostics__poisoned_tuple_terminal.snap @@ -0,0 +1,36 @@ +--- +source: crates/nash-cli/tests/diagnostics.rs +expression: "normalized(&human.stderr, &project.0)" +--- +TYPE MISMATCH + + × Something is off with the body of the `bad` definition: + ╭─[/src/Main.nash:3:7] + 2 │ bad : ((), ()) + 3 │ bad = (().field, \x -> x) + · ─────────────────── + ╰──── + help: The body is a tuple of type: + + ( ?, 'a -> 'a ) + + 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: + ╭─[/src/Main.nash:3:8] + 2 │ bad : ((), ()) + 3 │ bad = (().field, \x -> x) + · ──────── + ╰──── + 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 new file mode 100644 index 00000000..f56fba89 --- /dev/null +++ b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_json.snap @@ -0,0 +1,51 @@ +--- +source: crates/nash-cli/tests/diagnostics.rs +expression: "normalized(serde_json::to_string_pretty(&value).unwrap().as_bytes(), &root)" +--- +{ + "errors": [ + { + "name": "Main", + "path": "/src/Main.nash", + "problems": [ + { + "message": [ + "Something is off with the body of the `identity` definition:\n\n5| identity flag = flag\n ", + { + "bold": false, + "color": "RED", + "string": "^^^^", + "underline": false + }, + "\nThis `flag` value is a:\n\n ", + { + "bold": false, + "color": "yellow", + "string": "bool", + "underline": false + }, + "\n\nBut the type annotation on `identity` says it should be:\n\n ", + { + "bold": false, + "color": "yellow", + "string": "unit", + "underline": false + } + ], + "region": { + "end": { + "column": 21, + "line": 5 + }, + "start": { + "column": 17, + "line": 5 + } + }, + "title": "TYPE MISMATCH" + } + ] + } + ], + "type": "compile-errors" +} diff --git a/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap new file mode 100644 index 00000000..950a6b52 --- /dev/null +++ b/crates/nash-cli/tests/snapshots/diagnostics__type_mismatch_terminal.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-cli/tests/diagnostics.rs +expression: text +--- +TYPE MISMATCH + + × Something is off with the body of the `identity` definition: + ╭─[/src/Main.nash:5:17] + 4 │ identity : bool -> unit + 5 │ identity flag = flag + · ──── + ╰──── + 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/docs/diagnostics.md b/docs/diagnostics.md index b98ab162..2980b9f1 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -30,6 +30,11 @@ Recovery must preserve sound inference state. Mark failed expressions and constraints so that their dependent uses do not generate misleading follow-on errors; continue at valid definition, constraint, and dependency boundaries. Do not remove suppression guards without replacing their dependency tracking. +Preserve the known shape of a tuple or record when one child fails, so unaffected +children can still be checked against their annotations. Record selection depends +on the receiver's shape and the selected field; failure in an unrelated field +must not suppress a mismatch or trait obligation on the selected field. Changes +to shared inference variables still invalidate every computation that uses them. If parsing or canonicalization cannot produce valid input for the next phase, stop that module at that phase and continue independent modules. A failed module exports neither an interface nor successful solved output. Mark its @@ -149,6 +154,12 @@ exposing, so the dual prelude types (`Int`, `int`, `List`, `list`, `Data`, `Data.Map.Map` renders as `Data.Map.Map` unless the user exposed it. That subsumes Elm's hard-coded `List` special case. +Compiler primitive types from `nash_ast::primitives::PRIMITIVES` are already +available without imports. The localizer includes that current inventory and +keeps shadowed primitives qualified. Prelude default imports remain Plan 12; +the driver currently supplies no future default imports. Local union ownership +and package identity are retained for actionable impl advice. + The localizer is built once per module by the driver from the *source* module (it only needs the import list) and is threaded to every type error report, the same way `Reporting.Error.BadTypes` carries it. @@ -204,10 +215,16 @@ Driver-level errors (`nash_driver::DriverError`: file not found, config problems, import cycles) already derive `miette::Diagnostic` via `thiserror` and keep doing so; they are not `Report`s. +The following examples are checked against the shipping `core/` package by the +CLI integration tests. Output uses `--color=never --no-warnings`; only the +project path is shortened to `src/`. `map` currently comes from `Functor`; a +future `List` convenience module is not assumed. + ### Example 1 — type mismatch with a diff ```elm module Ledger exposing (settle) +import Functor exposing (Functor) type alias Account = { owner : Bytes, balance : Int } @@ -216,9 +233,27 @@ balanceOf account = account.balance settle : list Account -> list int settle accounts = - List.map balanceOf accounts + map balanceOf accounts ``` +TYPE MISMATCH + + × Something is off with the body of the `settle` definition: + ╭─[src/Ledger.nash:11:5] + 10 │ settle accounts = + 11 │ map balanceOf accounts + · ────────────────────── + ╰──── + help: This `map` call produces: + + list Int + 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. ``` Error: TYPE MISMATCH @@ -238,8 +273,8 @@ Error: TYPE MISMATCH list int Hint: `Int` is the Big (Data) integer and `int` is the little - builtin one. They never convert implicitly. Use `lower` to go from - `Int` to `int`, or `lift` to go the other way. + type. They never convert implicitly. Use `lower` or `lift` where + an appropriate `Lift` impl is available. ``` `Int` and `int` in the two type blocks are `dullyellow` on a color @@ -250,13 +285,40 @@ terminal; the rest of each type is plain. The hint is ```elm module Steps exposing (isDone) +import Prelude exposing ((==)) +import Eq exposing (Eq) type step = Done | Next int isDone : step -> bool isDone s = s == Done ``` +MISSING IMPL + + × I cannot find an `Eq` impl for `step`: + ╭─[src/Steps.nash:8:12] + 7 │ isDone : step -> bool + 8 │ isDone s = s == Done + · ───────── + ╰──── + 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: + + 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 = ... ``` Error: MISSING IMPL @@ -272,19 +334,21 @@ Error: MISSING IMPL step But there is no `impl Eq step` in this module or in any import, - and `step` is not marked `@derive(Eq)`. + and no matching impl is available. - Hint: Add `@derive(Eq)` above the `step` declaration, or write - the impl by hand: + Hint: Write the impl by hand. Deriving with `@derive(Eq)` + belongs to the later macro-expansion plan: impl Eq step where - (==) a b = ... + eq a b = ... ``` ### Example 3 — non-exhaustive case ```elm module Tag exposing (tag) +import Builtin exposing (Data(..)) +import Literal exposing (FromInt) tag : Data -> int tag d = @@ -292,7 +356,26 @@ tag d = Constr n _ -> n List _ -> 0 ``` +MISSING PATTERNS + + × This `case` does not have branches for all possibilities: + ╭─[src/Tag.nash:7:5] + 6 │ tag d = + 7 │ ╭─▶ case d of + 8 │ │ Constr n _ -> n + 9 │ ╰─▶ List _ -> 0 + ╰──── + help: Missing possibilities include: + 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. ``` Error: MISSING PATTERNS @@ -365,7 +448,14 @@ purpose only), then `after`. Driver errors serialize as `{"type":"error","path":..,"title":..,"message":[..]}`. Warnings use `"type": "compile-warnings"` with the same problem shape -(Elm has no JSON warnings; this is an addition). +(Elm has no JSON warnings; this is an addition). `--report=json` writes one +compile-error document to stdout, including an empty error array on success. +Warnings are a separate JSON document on stderr; `--no-warnings` suppresses it. +Source I/O failures use driver-error documents on stderr alongside any warning +document. There is no progress prose in JSON mode. Exit status is 1 for errors +or blocked modules and 0 for successful checks with or without warnings. +Human output uses `--color=auto|always|never`; auto respects `NO_COLOR` and the +terminal capability. JSON never contains ANSI sequences. ## LSP consumption @@ -379,9 +469,13 @@ Warnings use `"type": "compile-warnings"` with the same problem shape | `code` | `title` | | `source` | `"nash"` | | `message` | `before` + `"\n\n"` + `after`, rendered plain at width 80 | -| `related_information` | `Snippet::Pair` second label, and the `highlight` of `Snippet::Region` when it differs from `region` | +| `related_information` | `Snippet::Pair` first label (the second is primary), and the non-primary highlight of `Snippet::Region` | | `data` | `suggestions` (for a future quick-fix code action) | +The server uses full-text synchronization and snapshots unsaved buffers over +the filesystem. It rejects stale document versions, rebuilds on close, and +selects the closest project that owns the edited file. + The server runs the same driver pipeline on `didOpen`/`didChange` and publishes one `PublishDiagnostics` per module, including modules that became clean (empty list). diff --git a/plans/06-diagnostics.md b/plans/06-diagnostics.md index d3944c9a..bd9e4731 100644 --- a/plans/06-diagnostics.md +++ b/plans/06-diagnostics.md @@ -13,6 +13,75 @@ rendering. Reporting a vector of errors is insufficient if inference or the driver suppressed independent errors before constructing it. Follow the collection and cascade-suppression contract in `docs/diagnostics.md`. +## Implementation notes + +The code blocks below are porting sketches. The implementation follows the +current error enums and compiler APIs: 369 parser variants, canonical kind and +representation errors, current trait-resolution provenance, and nominal records. +`Localizer` includes compiler-known primitives but no future Prelude imports. +Missing-impl reports advise an explicit impl; local `@derive` advice states that +macro expansion belongs to Plan 11. The shipping `Eq` method is `eq`. + +`Doc` uses Elm's nested `fillSep` semantics. JSON snippets convert byte regions +to display-cell carets (including Unicode and tabs); LSP converts to UTF-16. +Terminal rendering retains wide source context and draws an insertion caret at +EOF without changing the primary source region. Warnings on failed modules are +sorted together with errors. Source I/O failures preserve independent modules; +blocked dependents retain the original failed dependency paths. + +All 14 chunks are complete. Validation covers report rendering and independent +error collection, including the recovery cases listed below. + +## Chunk status + +- [x] 1: Owned reports, byte-safe source spans, miette rendering and source context. +- [x] 2: Document layout, styled chunks, nested fill and Unicode widths. +- [x] 3: Name suggestions and deterministic distance ordering. +- [x] 4: Module, import, exposing, whitespace and end-of-input syntax reports. +- [x] 5: Declaration syntax reports. +- [x] 6: Expression syntax reports and nested parser-error preservation. +- [x] 7: Pattern and type syntax reports. +- [x] 8: Canonicalization reports, including paired declaration locations. +- [x] 9: Source-aware type names, type rendering and focused differences. +- [x] 10: Type errors with expected/actual types and expression context. +- [x] 11: Pattern errors and warnings. +- [x] 12: Owned module reports and Elm-shaped JSON with complete snippets. +- [x] 13: Kind, representation and trait reports for current compiler errors. +- [x] 14: Driver, CLI and LSP wiring, independent-error recovery and final acceptance. + +## Acceptance evidence + +- `nash-solve/tests/inference.rs`: mixed mismatches, missing impls and constraints, + ambiguity and kind errors in both declaration orders; repeated and recursive + uses; shared partial unification; field failures; independent tuple siblings + with and without annotations; nested aggregates; generic record selection + with failures in other fields; explicit resolution limits. +- `nash-solve/tests/representation_predicates.rs`: final kind failures survive + unrelated type errors; shared heads removed during normalization still suppress + dependent cascades. Failed solves return errors without successful solved output. +- `nash-driver/src/compile/collection_tests.rs` and graph tests: independent + modules continue, failed dependencies block their transitive users, unreadable + files preserve independent diagnostics, failed modules export no interfaces, + and shuffled discovery produces stable compilation order. +- `nash-cli/tests/diagnostics.rs`: repeated runs are ordered identically; terminal, + JSON and LSP share the mixed problem set and source ranges; terminal headers + identify primary locations; color, warning controls and exit codes are checked. + All three examples in `docs/diagnostics.md` run against the real core package. +- Language-server tests: UTF-16 positions, paired related locations, unsaved + buffers, version checks, close/repair clearing and nested workspace ownership. +- Final checks: `cargo fmt --all`; strict Clippy with all targets and features; + `cargo test --workspace --no-fail-fast` (2,950 passed, three ignored doctests); + `cargo insta test --workspace --check --unreferenced reject` (no pending or + unreferenced snapshots). Internal path dependency versions match their crates; + the publication graph, including development dependencies, is acyclic. +- Independent review rebuilt the current compiler and passed all 28 additional + recovery probes, including shared variables, repeated/recursive uses, tuple + orders and selected record fields. No concrete review finding remains open. + +No Plan 06 chunk is deferred. Automatic Prelude imports and macro derivation +remain assigned to Plans 12 and 11 respectively. Diagnostics describe the current +compiler and do not imply that these future features are available. + ## Prerequisites - Plan 05 (`nash-nitpick`) for the pattern chunk. @@ -930,7 +999,7 @@ pub fn rank(target: &str, to_string: impl Fn(&T) -> String, values: Vec) - **Tests** — `distance_transposition_is_one` (`"ab"`/`"ba"`), `distance_empty`, `sort_prefers_case_insensitive_match` (`"lenght"` → -`["length", "len", "height"]`), `rank_keeps_stable_order_for_ties`. +`["length", "height", "len"]`), `rank_keeps_stable_order_for_ties`. **Done when** tests pass. (Placed before the canonicalize chunk that needs it; the brief listed it later.) @@ -2519,17 +2588,16 @@ not establish completion. --- -## Open questions - -1. **`Report::with_region`.** Elm's `Code.toSnippet source surroundings - (Just region)` pattern (wide snippet, narrow highlight, report region = - the point) appears in most syntax reports. The API in chunk 1 grows a - `with_region(wide)` builder in chunk 5; consider adding it in chunk 1 - directly. -2. **Prelude imports.** `Localizer::from_module` needs the same default - import list canonicalization prepends (`docs/stdlib.md`, "Default - imports"). The function name `nash_can::imports::defaults()` mirrors - Elm's `Imports.defaults`; rename if plan 04 chunk C1 picks another. -3. **Stdlib function names in hints** (`Int.toString`, - `Option.withDefault`, `Int.rem`/`Int.mod`) are placeholders until - `docs/stdlib.md` fixes them. +## Resolved integration details + +1. **Primary regions and context.** `Report::with_region` widens the source + context while preserving the primary problem region. Terminal + rendering, JSON and LSP all use the same report region. +2. **Prelude imports.** Plan 12 still owns automatic Prelude imports. The + localizer uses actual source imports and the compiler's primitive inventory. + It preserves qualification when imports or local types can shadow primitives. +3. **Nash hints.** Reports use Nash type and declaration syntax. Trait defaults + use `Eq.eq`; overlapping impl contexts do not disambiguate heads. Derivation + remains a Plan 11 feature, and reports state that limitation when mentioning + it. The three documented examples are compiled against the current core + package and checked against the terminal snapshots.