From 551b5d968c1066b00b7e9bc334bc4fd064468b8f Mon Sep 17 00:00:00 2001 From: microproofs Date: Fri, 25 Sep 2026 22:25:22 -0400 Subject: [PATCH 01/19] feat(parse): preserve comments and documentation Signed-off-by: microproofs --- .sampo/changesets/preserve-source-comments.md | 6 + SPEC.md | 2 +- crates/nash-can/src/module.rs | 1 + crates/nash-parse/src/declaration/impl_.rs | 2 + crates/nash-parse/src/declaration/mod.rs | 20 +- ...aration__attribute__tests__cost_alias.snap | 3 +- ...ation__attribute__tests__derive_union.snap | 3 +- ..._attribute__tests__doc_then_attribute.snap | 41 +- ...ation__attribute__tests__inline_value.snap | 3 +- ...eclaration__attribute__tests__stacked.snap | 3 +- ...__impl___tests__implementation_simple.snap | 2 + ...___tests__implementation_with_context.snap | 2 + ...entation_with_multiple_head_arguments.snap | 2 + ...mentation_with_tuple_context_and_head.snap | 2 + ...ined_method_does_not_imply_superclass.snap | 2 + ...s__trait_with_higher_kinded_parameter.snap | 2 + ..._with_multiple_parameters_and_methods.snap | 2 + ...sts__trait_with_multiple_superclasses.snap | 2 + ...ts__trait_with_superclass_and_default.snap | 2 + ...tion__type_alias__tests__little_alias.snap | 3 +- ..._type_alias__tests__type_alias_record.snap | 3 +- ..._type_alias__tests__type_alias_simple.snap | 3 +- ...s__tests__type_alias_with_doc_comment.snap | 77 +- ..._alias__tests__type_alias_with_params.snap | 3 +- ...on__union__tests__labeled_constructor.snap | 3 +- ...tests__labeled_constructors_multiline.snap | 3 +- ...claration__union__tests__little_union.snap | 3 +- ...ts__positional_record_argument_parses.snap | 3 +- ...ration__union__tests__union_multiline.snap | 3 +- ...on__union__tests__union_multiple_args.snap | 3 +- ...claration__union__tests__union_simple.snap | 3 +- ..._union__tests__union_with_doc_comment.snap | 87 ++- ...tion__union__tests__union_with_params.snap | 3 +- ...n__value__tests__value_multiline_body.snap | 3 +- ...claration__value__tests__value_simple.snap | 3 +- ...ration__value__tests__value_with_args.snap | 3 +- ...ts__value_with_constrained_annotation.snap | 3 +- ..._value__tests__value_with_doc_comment.snap | 87 ++- ...sts__value_with_doc_comment_multiline.snap | 149 ++-- ...ue__tests__value_with_type_annotation.snap | 3 +- crates/nash-parse/src/declaration/trait_.rs | 2 + .../nash-parse/src/declaration/type_alias.rs | 2 + crates/nash-parse/src/declaration/union.rs | 2 + crates/nash-parse/src/declaration/value.rs | 6 +- crates/nash-parse/src/lib.rs | 5 + crates/nash-parse/src/module.rs | 133 +++- ...ktracking_does_not_duplicate_comments.snap | 288 ++++++++ ...module__tests__comments_are_collected.snap | 161 ++++ ..._tests__comments_in_tests_and_strings.snap | 189 +++++ ...s__comments_preserve_unicode_and_crlf.snap | 119 +++ ...claration_docs_survive_categorization.snap | 695 ++++++++++++++++++ ..._tests__doc_comment_attaches_to_value.snap | 121 +++ ...raint_does_not_imply_trait_superclass.snap | 4 + ...se__module__tests__module_doc_comment.snap | 313 ++++++++ ...ash_parse__module__tests__module_full.snap | 5 + ...se__module__tests__module_header_only.snap | 2 + ...arse__module__tests__module_no_header.snap | 3 + ...rse__module__tests__module_with_alias.snap | 3 + ...ts__module_with_empty_impl_then_value.snap | 4 + ...s__module_with_empty_trait_then_value.snap | 4 + ...e__module__tests__module_with_imports.snap | 2 + ...rse__module__tests__module_with_infix.snap | 3 + ...arse__module__tests__module_with_type.snap | 3 + ...__tests__module_with_union_then_value.snap | 4 + ...rse__module__tests__module_with_value.snap | 3 + ..._module__tests__validator_module_full.snap | 6 + ...ts_block__tests__complete_tests_block.snap | 3 + ...ests__nested_monadic_do_in_test_block.snap | 2 + ...ck__tests__syntax_overview_acceptance.snap | 9 + ...block__tests__tests_with_imports_only.snap | 2 + ...s_block__tests__tests_without_imports.snap | 2 + crates/nash-parse/src/space.rs | 70 +- crates/nash-source/src/lib.rs | 28 +- plans/13-fmt-docs.md | 136 +--- 74 files changed, 2522 insertions(+), 365 deletions(-) create mode 100644 .sampo/changesets/preserve-source-comments.md create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__backtracking_does_not_duplicate_comments.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_are_collected.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_in_tests_and_strings.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_preserve_unicode_and_crlf.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__declaration_docs_survive_categorization.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__doc_comment_attaches_to_value.snap create mode 100644 crates/nash-parse/src/snapshots/nash_parse__module__tests__module_doc_comment.snap diff --git a/.sampo/changesets/preserve-source-comments.md b/.sampo/changesets/preserve-source-comments.md new file mode 100644 index 00000000..ad476dce --- /dev/null +++ b/.sampo/changesets/preserve-source-comments.md @@ -0,0 +1,6 @@ +--- +cargo/nash-source: minor +cargo/nash-parse: minor +--- + +Preserve source comments and documentation in the surface AST for formatting and documentation extraction. Retain comment regions and text through parser backtracking, and attach module and declaration documentation. diff --git a/SPEC.md b/SPEC.md index c348e2b9..8b265532 100644 --- a/SPEC.md +++ b/SPEC.md @@ -68,7 +68,7 @@ Implementation plans (Plan 08 is deferred; Plans 09 and 10 are complete): - [x] 10 Testing: `tests` block, props, generators, shrinking, power-assert, `nash test` — [plans/10-testing.md](plans/10-testing.md) - [ ] 11 Macros + comptime — [plans/11-macros-comptime.md](plans/11-macros-comptime.md) - [ ] 12 Stdlib `nash/base` — [plans/12-stdlib.md](plans/12-stdlib.md) (chunks 1–9 and 11–13 complete; chunk 10 Ast/Derive deferred) -- [ ] 13 `nash fmt`, `nash docs` — [plans/13-fmt-docs.md](plans/13-fmt-docs.md) +- [ ] 13 `nash fmt`, `nash docs` — [plans/13-fmt-docs.md](plans/13-fmt-docs.md) (chunk 1 comment preservation complete; chunks 2–7 pending) - [x] Representation-classed impl heads: `Big`/`Little` blankets and concrete little impls coexist — [plans/repr-classed-impl-heads.md](plans/repr-classed-impl-heads.md) Later: LSP features, web playground, package registry (pubgrub), TypeScript codegen. diff --git a/crates/nash-can/src/module.rs b/crates/nash-can/src/module.rs index aa302919..41c458b6 100644 --- a/crates/nash-can/src/module.rs +++ b/crates/nash-can/src/module.rs @@ -1564,6 +1564,7 @@ pub(crate) fn canonicalize_typed_value<'a>( let value = bump.alloc(Located::at( definition.region, SourceValue { + docs: None, name, arguments: args, body, diff --git a/crates/nash-parse/src/declaration/impl_.rs b/crates/nash-parse/src/declaration/impl_.rs index 66a3e343..8e77fb45 100644 --- a/crates/nash-parse/src/declaration/impl_.rs +++ b/crates/nash-parse/src/declaration/impl_.rs @@ -9,6 +9,7 @@ use crate::error::{self, Def as DefErr, Impl as ImplErr}; impl<'a> Parser<'a> { pub(super) fn impl_decl( &mut self, + docs: Option<&'a nash_source::Comment<'a>>, attributes: &'a [&'a Attribute<'a>], start: Position, ) -> Result<(Decl<'a>, Position), error::Decl<'a>> { @@ -31,6 +32,7 @@ impl<'a> Parser<'a> { parser.chomp(ImplErr::Space)?; let (methods, end) = parser.impl_body(where_end)?; let impl_ = Impl { + docs, context: scheme.constraints, head, methods, diff --git a/crates/nash-parse/src/declaration/mod.rs b/crates/nash-parse/src/declaration/mod.rs index 5e453dbf..89a425e4 100644 --- a/crates/nash-parse/src/declaration/mod.rs +++ b/crates/nash-parse/src/declaration/mod.rs @@ -20,9 +20,9 @@ use crate::error::{self, Decl as DeclErr}; /// A parsed declaration with optional doc comment. #[derive(Debug)] pub enum Decl<'a> { - Value(Option<&'a Comment<'a>>, &'a Located>), - Union(Option<&'a Comment<'a>>, &'a Located>), - Alias(Option<&'a Comment<'a>>, &'a Located>), + Value(&'a Located>), + Union(&'a Located>), + Alias(&'a Located>), Trait(&'a Located>), Impl(&'a Located>), } @@ -53,8 +53,8 @@ impl<'a> Parser<'a> { vec![ // type alias or type (union) Box::new(|p: &mut Parser<'a>| p.type_decl(maybe_docs, attributes, start)), - Box::new(|p: &mut Parser<'a>| p.trait_decl(attributes, start)), - Box::new(|p: &mut Parser<'a>| p.impl_decl(attributes, start)), + Box::new(|p: &mut Parser<'a>| p.trait_decl(maybe_docs, attributes, start)), + Box::new(|p: &mut Parser<'a>| p.impl_decl(maybe_docs, attributes, start)), // value definition Box::new(|p| p.value_decl(maybe_docs, attributes, start)), ], @@ -121,8 +121,9 @@ impl<'a> Parser<'a> { |bump, e, row, col| error::DeclType::Alias(bump.alloc(e), row, col), |p| p.keyword_alias(error::DeclType::Name), |p| { - let (alias, end) = p.type_alias_body(start, attributes)?; - Ok((Decl::Alias(maybe_docs, alias), end)) + let (alias, end) = + p.type_alias_body(start, attributes, maybe_docs)?; + Ok((Decl::Alias(alias), end)) }, ) }), @@ -131,8 +132,9 @@ impl<'a> Parser<'a> { p.specialize( |bump, e, row, col| error::DeclType::Union(bump.alloc(e), row, col), |p| { - let (union, end) = p.union_body(start, attributes)?; - Ok((Decl::Union(maybe_docs, union), end)) + let (union, end) = + p.union_body(start, attributes, maybe_docs)?; + Ok((Decl::Union(union), end)) }, ) }), diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__cost_alias.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__cost_alias.snap index e7ad779a..d95f4819 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__cost_alias.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__cost_alias.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/attribute.rs +assertion_line: 113 description: "@cost(cpu 10, \"n\")\ntype alias a = int" --- Alias( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Alias( }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__derive_union.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__derive_union.snap index 8d791d80..a1dd2778 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__derive_union.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__derive_union.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/attribute.rs +assertion_line: 105 description: "@derive(Eq, Show)\ntype T = A | B" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__doc_then_attribute.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__doc_then_attribute.snap index 8714a14e..6fade329 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__doc_then_attribute.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__doc_then_attribute.snap @@ -1,23 +1,9 @@ --- source: crates/nash-parse/src/declaration/attribute.rs +assertion_line: 117 description: "{-| doc -}\n@derive(Eq)\ntype T = A" --- Union( - Some( - Comment( - Snippet { - data: [ - 32, - 100, - 111, - 99, - 32, - ], - off_row: 1, - off_col: 4, - }, - ), - ), Located { region: Region { start: Position { @@ -30,6 +16,31 @@ Union( }, }, value: Union { + docs: Some( + Comment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 11, + }, + }, + snippet: Snippet { + data: [ + 32, + 100, + 111, + 99, + 32, + ], + off_row: 1, + off_col: 4, + }, + }, + ), name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__inline_value.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__inline_value.snap index a994145b..805b5e60 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__inline_value.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__inline_value.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/attribute.rs +assertion_line: 109 description: "@inline\nf x = x" --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__stacked.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__stacked.snap index be7c22cd..91b947a8 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__stacked.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__attribute__tests__stacked.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/attribute.rs +assertion_line: 121 description: "@first\n@second(1)\nf = 1" --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_simple.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_simple.snap index 319af3c7..1299b6a4 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_simple.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_simple.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/impl_.rs +assertion_line: 97 description: "impl Ord int where\n compare = Builtin.compareInteger\n" --- Impl( @@ -15,6 +16,7 @@ Impl( }, }, value: Impl { + docs: None, context: [], head: Located { region: Region { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_context.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_context.snap index 203190dd..f0d4bc8e 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_context.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_context.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/impl_.rs +assertion_line: 107 description: "impl Eq 'a => Eq (list 'a) where\n eq xs ys = eqList xs ys\n" --- Impl( @@ -15,6 +16,7 @@ Impl( }, }, value: Impl { + docs: None, context: [ Located { region: Region { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_multiple_head_arguments.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_multiple_head_arguments.snap index e7921cad..3a6c3517 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_multiple_head_arguments.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_multiple_head_arguments.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/impl_.rs +assertion_line: 117 description: "impl Lift int Int where\n lift = liftInt\n lower = lowerInt\n" --- Impl( @@ -15,6 +16,7 @@ Impl( }, }, value: Impl { + docs: None, context: [], head: Located { region: Region { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_tuple_context_and_head.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_tuple_context_and_head.snap index 396560bf..d467df31 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_tuple_context_and_head.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__impl___tests__implementation_with_tuple_context_and_head.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/impl_.rs +assertion_line: 128 description: "impl (Eq 'a, Eq 'b) => Eq ('a, 'b) where\n eq (a, b) (c, d) = a == c && b == d\n" --- Impl( @@ -15,6 +16,7 @@ Impl( }, }, value: Impl { + docs: None, context: [ Located { region: Region { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__constrained_method_does_not_imply_superclass.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__constrained_method_does_not_imply_superclass.snap index 5e3edc90..17ba4109 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__constrained_method_does_not_imply_superclass.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__constrained_method_does_not_imply_superclass.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/trait_.rs +assertion_line: 343 description: "trait Show 'a where\n show : Eq 'a => 'a -> string\n" --- Trait( @@ -15,6 +16,7 @@ Trait( }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_higher_kinded_parameter.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_higher_kinded_parameter.snap index 71f7397b..21a88f2a 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_higher_kinded_parameter.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_higher_kinded_parameter.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/trait_.rs +assertion_line: 266 description: "trait Functor 'f where\n map : ('a -> 'b) -> 'f 'a -> 'f 'b\n" --- Trait( @@ -15,6 +16,7 @@ Trait( }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_parameters_and_methods.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_parameters_and_methods.snap index be958c05..9f10bb0c 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_parameters_and_methods.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_parameters_and_methods.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/trait_.rs +assertion_line: 286 description: "trait Lift 'small 'big where\n lift : 'small -> 'big\n lower : 'big -> 'small\n" --- Trait( @@ -15,6 +16,7 @@ Trait( }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_superclasses.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_superclasses.snap index ced2c7c1..1911061e 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_superclasses.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_multiple_superclasses.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/trait_.rs +assertion_line: 276 description: "trait (Ord 'k, ToData 'k) => Key 'k where\n hash : 'k -> Bytes\n" --- Trait( @@ -15,6 +16,7 @@ Trait( }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_superclass_and_default.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_superclass_and_default.snap index 79f9f1e9..bca3d94e 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_superclass_and_default.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__trait___tests__trait_with_superclass_and_default.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/declaration/trait_.rs +assertion_line: 253 description: "trait Eq 'a => Ord 'a where\n compare : 'a -> 'a -> ordering\n\n lt : 'a -> 'a -> bool\n lt a b = compare a b == LT\n" --- Trait( @@ -15,6 +16,7 @@ Trait( }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__little_alias.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__little_alias.snap index 6ec53329..f148fab0 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__little_alias.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__little_alias.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/type_alias.rs +assertion_line: 137 description: "type alias acc = { total : int, seen : list Int }" --- Alias( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Alias( }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_record.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_record.snap index 94459065..f7524c27 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_record.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_record.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/type_alias.rs +assertion_line: 122 description: "type alias Model = { count : Int }" --- Alias( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Alias( }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_simple.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_simple.snap index 4d1aa5e1..b7ed7c16 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_simple.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_simple.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/type_alias.rs +assertion_line: 112 description: type alias Name = String --- Alias( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Alias( }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_doc_comment.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_doc_comment.snap index 3b9216f4..aa51644e 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_doc_comment.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_doc_comment.snap @@ -1,41 +1,9 @@ --- source: crates/nash-parse/src/declaration/type_alias.rs +assertion_line: 127 description: "{-| The application model -}\ntype alias Model = { count : Int }\n" --- Alias( - Some( - Comment( - Snippet { - data: [ - 32, - 84, - 104, - 101, - 32, - 97, - 112, - 112, - 108, - 105, - 99, - 97, - 116, - 105, - 111, - 110, - 32, - 109, - 111, - 100, - 101, - 108, - 32, - ], - off_row: 1, - off_col: 4, - }, - ), - ), Located { region: Region { start: Position { @@ -48,6 +16,49 @@ Alias( }, }, value: Alias { + docs: Some( + Comment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 29, + }, + }, + snippet: Snippet { + data: [ + 32, + 84, + 104, + 101, + 32, + 97, + 112, + 112, + 108, + 105, + 99, + 97, + 116, + 105, + 111, + 110, + 32, + 109, + 111, + 100, + 101, + 108, + 32, + ], + off_row: 1, + off_col: 4, + }, + }, + ), name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_params.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_params.snap index 1e036371..71631eb0 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_params.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__type_alias__tests__type_alias_with_params.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/type_alias.rs +assertion_line: 117 description: "type alias Pair 'a 'b = ('a, 'b)" --- Alias( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Alias( }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructor.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructor.snap index 268ed8f8..93825f92 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructor.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructor.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 310 description: "type Datum = Datum { owner : Bytes, deadline : Int }" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructors_multiline.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructors_multiline.snap index 61bca0d9..14dc7364 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructors_multiline.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__labeled_constructors_multiline.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 315 description: "type Shape\n = Circle { r : int }\n | Rect { w : int, h : int }\n" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__little_union.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__little_union.snap index 48685a3e..2dd0858f 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__little_union.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__little_union.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 305 description: "type step 'a = Done 'a | Next int 'a" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__positional_record_argument_parses.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__positional_record_argument_parses.snap index d679c637..3025f5ae 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__positional_record_argument_parses.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__positional_record_argument_parses.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 346 description: "type D = D int { x : int }" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiline.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiline.snap index 8728de1d..78650640 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiline.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiline.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 278 description: "type Msg\n = Increment\n | Decrement\n | Reset\n" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiple_args.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiple_args.snap index 02787905..38b2d3fe 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiple_args.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_multiple_args.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 273 description: "type Outcome 'e 'a = Ok 'a | Err 'e" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_simple.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_simple.snap index 81f4e1ca..52569c5b 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_simple.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_simple.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 263 description: type Bool = True | False --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_doc_comment.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_doc_comment.snap index d11ca762..a3f7236e 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_doc_comment.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_doc_comment.snap @@ -1,46 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 290 description: "{-| Represents optional values -}\ntype Maybe 'a = Just 'a | Nothing\n" --- Union( - Some( - Comment( - Snippet { - data: [ - 32, - 82, - 101, - 112, - 114, - 101, - 115, - 101, - 110, - 116, - 115, - 32, - 111, - 112, - 116, - 105, - 111, - 110, - 97, - 108, - 32, - 118, - 97, - 108, - 117, - 101, - 115, - 32, - ], - off_row: 1, - off_col: 4, - }, - ), - ), Located { region: Region { start: Position { @@ -53,6 +16,54 @@ Union( }, }, value: Union { + docs: Some( + Comment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 34, + }, + }, + snippet: Snippet { + data: [ + 32, + 82, + 101, + 112, + 114, + 101, + 115, + 101, + 110, + 116, + 115, + 32, + 111, + 112, + 116, + 105, + 111, + 110, + 97, + 108, + 32, + 118, + 97, + 108, + 117, + 101, + 115, + 32, + ], + off_row: 1, + off_col: 4, + }, + }, + ), name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_params.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_params.snap index fc525747..09f5fa7f 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_params.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__union__tests__union_with_params.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/union.rs +assertion_line: 268 description: "type option 'a = Some 'a | None" --- Union( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Union( }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_multiline_body.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_multiline_body.snap index b25e4006..68558556 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_multiline_body.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_multiline_body.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 210 description: "greet name =\n name\n" --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_simple.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_simple.snap index 0a7afa11..67c5d520 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_simple.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_simple.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 180 description: foo = 1 --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_args.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_args.snap index 0fe058c0..63bf06ee 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_args.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_args.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 185 description: add x y = x --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_constrained_annotation.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_constrained_annotation.snap index e1c93dba..3bd52898 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_constrained_annotation.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_constrained_annotation.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 200 description: "max : Ord 'a => 'a -> 'a -> 'a\nmax a b = a\n" --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment.snap index e344d5c2..164ed62c 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment.snap @@ -1,46 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 220 description: "{-| A simple greeting function -}\ngreet name = name\n" --- Value( - Some( - Comment( - Snippet { - data: [ - 32, - 65, - 32, - 115, - 105, - 109, - 112, - 108, - 101, - 32, - 103, - 114, - 101, - 101, - 116, - 105, - 110, - 103, - 32, - 102, - 117, - 110, - 99, - 116, - 105, - 111, - 110, - 32, - ], - off_row: 1, - off_col: 4, - }, - ), - ), Located { region: Region { start: Position { @@ -53,6 +16,54 @@ Value( }, }, value: Value { + docs: Some( + Comment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 34, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 32, + 115, + 105, + 109, + 112, + 108, + 101, + 32, + 103, + 114, + 101, + 101, + 116, + 105, + 110, + 103, + 32, + 102, + 117, + 110, + 99, + 116, + 105, + 111, + 110, + 32, + ], + off_row: 1, + off_col: 4, + }, + }, + ), name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment_multiline.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment_multiline.snap index b4471670..79007349 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment_multiline.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_doc_comment_multiline.snap @@ -1,77 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 230 description: "{-| Adds two numbers together.\n\nThis is a longer description.\n-}\nadd : Int -> Int -> Int\nadd x y = x\n" --- Value( - Some( - Comment( - Snippet { - data: [ - 32, - 65, - 100, - 100, - 115, - 32, - 116, - 119, - 111, - 32, - 110, - 117, - 109, - 98, - 101, - 114, - 115, - 32, - 116, - 111, - 103, - 101, - 116, - 104, - 101, - 114, - 46, - 10, - 10, - 84, - 104, - 105, - 115, - 32, - 105, - 115, - 32, - 97, - 32, - 108, - 111, - 110, - 103, - 101, - 114, - 32, - 100, - 101, - 115, - 99, - 114, - 105, - 112, - 116, - 105, - 111, - 110, - 46, - 10, - ], - off_row: 1, - off_col: 4, - }, - ), - ), Located { region: Region { start: Position { @@ -84,6 +16,85 @@ Value( }, }, value: Value { + docs: Some( + Comment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 4, + column: 3, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 100, + 100, + 115, + 32, + 116, + 119, + 111, + 32, + 110, + 117, + 109, + 98, + 101, + 114, + 115, + 32, + 116, + 111, + 103, + 101, + 116, + 104, + 101, + 114, + 46, + 10, + 10, + 84, + 104, + 105, + 115, + 32, + 105, + 115, + 32, + 97, + 32, + 108, + 111, + 110, + 103, + 101, + 114, + 32, + 100, + 101, + 115, + 99, + 114, + 105, + 112, + 116, + 105, + 111, + 110, + 46, + 10, + ], + off_row: 1, + off_col: 4, + }, + }, + ), name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_type_annotation.snap b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_type_annotation.snap index 960b2d16..5c3556e6 100644 --- a/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_type_annotation.snap +++ b/crates/nash-parse/src/declaration/snapshots/nash_parse__declaration__value__tests__value_with_type_annotation.snap @@ -1,9 +1,9 @@ --- source: crates/nash-parse/src/declaration/value.rs +assertion_line: 190 description: "add : Int -> Int -> Int\nadd x y = x\n" --- Value( - None, Located { region: Region { start: Position { @@ -16,6 +16,7 @@ Value( }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/declaration/trait_.rs b/crates/nash-parse/src/declaration/trait_.rs index 4d7ee426..1f40da00 100644 --- a/crates/nash-parse/src/declaration/trait_.rs +++ b/crates/nash-parse/src/declaration/trait_.rs @@ -15,6 +15,7 @@ type TraitHead<'a> = ( impl<'a> Parser<'a> { pub(super) fn trait_decl( &mut self, + docs: Option<&'a nash_source::Comment<'a>>, attributes: &'a [&'a Attribute<'a>], start: Position, ) -> Result<(Decl<'a>, Position), error::Decl<'a>> { @@ -29,6 +30,7 @@ impl<'a> Parser<'a> { parser.chomp(TraitErr::Space)?; let (methods, end) = parser.trait_body(where_end)?; let trait_ = Trait { + docs, name, params, supers, diff --git a/crates/nash-parse/src/declaration/type_alias.rs b/crates/nash-parse/src/declaration/type_alias.rs index 2f6439cd..e18a8710 100644 --- a/crates/nash-parse/src/declaration/type_alias.rs +++ b/crates/nash-parse/src/declaration/type_alias.rs @@ -19,6 +19,7 @@ impl<'a> Parser<'a> { &mut self, start: Position, attributes: &'a [&'a Attribute<'a>], + docs: Option<&'a nash_source::Comment<'a>>, ) -> Result<(&'a Located>, Position), TypeAlias<'a>> { self.chomp_and_check_indent(TypeAlias::Space, TypeAlias::IndentEquals)?; let (name, args) = self.chomp_alias_name_to_equals()?; @@ -29,6 +30,7 @@ impl<'a> Parser<'a> { )?; let alias = Alias { + docs, name, arguments: args, typ, diff --git a/crates/nash-parse/src/declaration/union.rs b/crates/nash-parse/src/declaration/union.rs index 780e327d..be8dca92 100644 --- a/crates/nash-parse/src/declaration/union.rs +++ b/crates/nash-parse/src/declaration/union.rs @@ -22,6 +22,7 @@ impl<'a> Parser<'a> { &mut self, start: Position, attributes: &'a [&'a Attribute<'a>], + docs: Option<&'a nash_source::Comment<'a>>, ) -> Result<(&'a Located>, Position), CustomType<'a>> { let (name, args) = self.chomp_custom_name_to_equals()?; @@ -33,6 +34,7 @@ impl<'a> Parser<'a> { let ctors = self.alloc_slice_copy(&variants); let union = Union { + docs, name, arguments: args, ctors, diff --git a/crates/nash-parse/src/declaration/value.rs b/crates/nash-parse/src/declaration/value.rs index 3aab2eb8..2ff67f3a 100644 --- a/crates/nash-parse/src/declaration/value.rs +++ b/crates/nash-parse/src/declaration/value.rs @@ -124,6 +124,7 @@ impl<'a> Parser<'a> { let args_slice = args_for_body.into_bump_slice(); let value = Value { + docs: maybe_docs, name, arguments: args_slice, body, @@ -131,10 +132,7 @@ impl<'a> Parser<'a> { attributes, }; let located_value = p.add_end(start, value); - Ok(ValueDeclState::Done( - Decl::Value(maybe_docs, located_value), - end, - )) + Ok(ValueDeclState::Done(Decl::Value(located_value), end)) }), ], )?; diff --git a/crates/nash-parse/src/lib.rs b/crates/nash-parse/src/lib.rs index 453c983e..f19374da 100644 --- a/crates/nash-parse/src/lib.rs +++ b/crates/nash-parse/src/lib.rs @@ -25,6 +25,7 @@ pub type Col = usize; /// Saved parser state for backtracking. #[derive(Clone, Copy)] struct ParserState { + comments_len: usize, pos: usize, indent: usize, row: Row, @@ -39,6 +40,7 @@ struct ParserState { /// The source text should already be allocated in the arena (via `bump.alloc_str`), /// so all string slices in the resulting AST share the `'a` lifetime. pub struct Parser<'a> { + comments: Vec<&'a nash_source::SourceComment<'a>>, /// Arena allocator for AST nodes bump: &'a Bump, /// Source bytes (UTF-8, already in arena) @@ -67,6 +69,7 @@ impl<'a> Parser<'a> { /// ``` pub fn new(bump: &'a Bump, src: &'a str) -> Self { Parser { + comments: Vec::new(), bump, src: src.as_bytes(), pos: 0, @@ -211,6 +214,7 @@ impl<'a> Parser<'a> { #[inline] fn save_state(&self) -> ParserState { ParserState { + comments_len: self.comments.len(), pos: self.pos, indent: self.indent, row: self.row, @@ -221,6 +225,7 @@ impl<'a> Parser<'a> { /// Restore parser state for backtracking. #[inline] fn restore_state(&mut self, state: ParserState) { + self.comments.truncate(state.comments_len); self.pos = state.pos; self.indent = state.indent; self.row = state.row; diff --git a/crates/nash-parse/src/module.rs b/crates/nash-parse/src/module.rs index 4a30bdc5..a804d624 100644 --- a/crates/nash-parse/src/module.rs +++ b/crates/nash-parse/src/module.rs @@ -212,15 +212,23 @@ impl<'a> Parser<'a> { let start_pos = self.get_position(); // Try to parse module header (optional) - let (kind, name, exports) = + let (kind, name, exports, overview) = if self.starts_keyword(b"module") || self.starts_keyword(b"validator") { let (kind, name, exports) = self.module_header()?; self.chomp(error::Module::Space)?; self.check_fresh_line(error::Module::FreshLine)?; - (kind, Some(name), exports) + let overview = if self.remaining().starts_with(b"{-|") { + let doc = self.doc_comment(error::Module::FreshLine, error::Module::Space)?; + self.chomp(error::Module::Space)?; + self.check_fresh_line(error::Module::FreshLine)?; + Some(doc) + } else { + None + }; + (kind, Some(name), exports, overview) } else { let default_exports = self.alloc(Located::at(Region::one(), Exposing::Open)); - (ModuleKind::Normal, None, default_exports) + (ModuleKind::Normal, None, default_exports, None) }; // Parse imports @@ -248,14 +256,36 @@ impl<'a> Parser<'a> { // Categorize declarations into values, unions, aliases let (values, unions, aliases, traits, impls) = self.categorize_decls(decls); - // Build docs (simplified: no module-level docs for now) - let docs = self.alloc(Docs::NoDocs(Region::new(start_pos, self.get_position()))); + let docs = self.alloc(match overview { + Some(overview) => { + let mut comments = Vec::new(); + for (name, doc) in values + .iter() + .map(|v| (v.value.name, v.value.docs)) + .chain(unions.iter().map(|v| (v.value.name, v.value.docs))) + .chain(aliases.iter().map(|v| (v.value.name, v.value.docs))) + .chain(traits.iter().map(|v| (v.value.name, v.value.docs))) + { + if let Some(doc) = doc { + comments.push(self.alloc((name.value, doc))); + } + } + comments.sort_by_key(|(_, doc)| (doc.region.start.line, doc.region.start.column)); + Docs::YesDocs { + overview, + comments: self.alloc_slice_copy(&comments), + } + } + None => Docs::NoDocs(Region::new(start_pos, self.get_position())), + }); + let comments = self.alloc_slice_copy(&self.comments); Ok(Module { kind, name, exports, docs, + comments, imports, values, unions, @@ -295,9 +325,9 @@ impl<'a> Parser<'a> { for decl in decls { match decl { - Decl::Value(_doc, value) => values.push(value), - Decl::Union(_doc, union) => unions.push(union), - Decl::Alias(_doc, alias) => aliases.push(alias), + Decl::Value(value) => values.push(value), + Decl::Union(union) => unions.push(union), + Decl::Alias(alias) => aliases.push(alias), Decl::Trait(trait_) => traits.push(trait_), Decl::Impl(impl_) => impls.push(impl_), } @@ -615,4 +645,91 @@ mod tests { fn module_preserves_annotation_name_error() { assert_module_error_snapshot!("f : int\ng = 1"); } + #[test] + fn comments_are_collected() { + assert_module_snapshot!( + r#" + -- leading + {- outer {- nested -} block -} + x = {- before value -} 1 -- trailing + -- eof + "# + ); + } + + #[test] + fn comments_preserve_unicode_and_crlf() { + assert_module_snapshot!("-- café\r\nx = 1 -- 終"); + } + + #[test] + fn doc_comment_attaches_to_value() { + assert_module_snapshot!("{-| value docs -}\nx = 1"); + } + + #[test] + fn module_doc_comment() { + assert_module_snapshot!( + r#" + module Main exposing (..) + -- before overview + {-| Overview with {- nested -} content. -} + import A -- import note + + {-| Value docs. -} + @inline + x : int + x = 1 + "# + ); + } + + #[test] + fn declaration_docs_survive_categorization() { + assert_module_snapshot!( + r#" + module Documented exposing (..) + {-| Overview. -} + + {-| A union. -} + type Box = Box int + + {-| An alias. -} + type alias Count = int + + {-| A trait. -} + trait Measure 'a where + measure : 'a -> int + + {-| An implementation. -} + impl Measure int where + measure x = x + "# + ); + } + + #[test] + fn backtracking_does_not_duplicate_comments() { + assert_module_snapshot!( + r#" + section = (1 + {- section -}) + action = do + f {- speculative pattern -} x + pure () + "# + ); + } + + #[test] + fn comments_in_tests_and_strings() { + assert_module_snapshot!( + r#" + text = "-- not a comment {- neither -}" + tests + -- test note + test "works" = do + assert {- assertion note -} True + "# + ); + } } diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__backtracking_does_not_duplicate_comments.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__backtracking_does_not_duplicate_comments.snap new file mode 100644 index 00000000..ee888e19 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__backtracking_does_not_duplicate_comments.snap @@ -0,0 +1,288 @@ +--- +source: crates/nash-parse/src/module.rs +assertion_line: 699 +description: "Code:\n\nsection = (1 + {- section -})\naction = do\n f {- speculative pattern -} x\n pure ()\n" +--- +Module { + kind: Normal, + name: None, + exports: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 1, + }, + }, + value: Open, + }, + docs: NoDocs( + Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 5, + column: 1, + }, + }, + ), + comments: [ + SourceComment { + region: Region { + start: Position { + line: 1, + column: 16, + }, + end: Position { + line: 1, + column: 29, + }, + }, + kind: Block, + text: " section ", + }, + SourceComment { + region: Region { + start: Position { + line: 3, + column: 7, + }, + end: Position { + line: 3, + column: 32, + }, + }, + kind: Block, + text: " speculative pattern ", + }, + ], + imports: [], + values: [ + Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 2, + column: 1, + }, + }, + value: Value { + docs: None, + name: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 8, + }, + }, + value: "section", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 1, + column: 11, + }, + end: Position { + line: 1, + column: 30, + }, + }, + value: LeftSection { + left: Located { + region: Region { + start: Position { + line: 1, + column: 12, + }, + end: Position { + line: 1, + column: 13, + }, + }, + value: Int( + 1, + ), + }, + operator: "+", + }, + }, + annotation: None, + attributes: [], + }, + }, + Located { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 5, + column: 1, + }, + }, + value: Value { + docs: None, + name: Located { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 7, + }, + }, + value: "action", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 2, + column: 10, + }, + end: Position { + line: 4, + column: 12, + }, + }, + value: Do { + stmts: [ + Located { + region: Region { + start: Position { + line: 3, + column: 5, + }, + end: Position { + line: 3, + column: 34, + }, + }, + value: Expr( + Located { + region: Region { + start: Position { + line: 3, + column: 5, + }, + end: Position { + line: 3, + column: 34, + }, + }, + value: Call { + function: Located { + region: Region { + start: Position { + line: 3, + column: 5, + }, + end: Position { + line: 3, + column: 6, + }, + }, + value: Var { + kind: LowVar, + name: "f", + }, + }, + arguments: [ + Located { + region: Region { + start: Position { + line: 3, + column: 33, + }, + end: Position { + line: 3, + column: 34, + }, + }, + value: Var { + kind: LowVar, + name: "x", + }, + }, + ], + }, + }, + ), + }, + ], + last: Located { + region: Region { + start: Position { + line: 4, + column: 5, + }, + end: Position { + line: 4, + column: 12, + }, + }, + value: Call { + function: Located { + region: Region { + start: Position { + line: 4, + column: 5, + }, + end: Position { + line: 4, + column: 9, + }, + }, + value: Var { + kind: LowVar, + name: "pure", + }, + }, + arguments: [ + Located { + region: Region { + start: Position { + line: 4, + column: 10, + }, + end: Position { + line: 4, + column: 12, + }, + }, + value: Unit, + }, + ], + }, + }, + }, + }, + annotation: None, + attributes: [], + }, + }, + ], + unions: [], + aliases: [], + traits: [], + impls: [], + tests: None, + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_are_collected.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_are_collected.snap new file mode 100644 index 00000000..93be7885 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_are_collected.snap @@ -0,0 +1,161 @@ +--- +source: crates/nash-parse/src/module.rs +assertion_line: 645 +description: "Code:\n\n-- leading\n{- outer {- nested -} block -}\nx = {- before value -} 1 -- trailing\n-- eof\n" +--- +Module { + kind: Normal, + name: None, + exports: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 1, + }, + }, + value: Open, + }, + docs: NoDocs( + Region { + start: Position { + line: 3, + column: 1, + }, + end: Position { + line: 5, + column: 1, + }, + }, + ), + comments: [ + SourceComment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 11, + }, + }, + kind: Line, + text: " leading", + }, + SourceComment { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 31, + }, + }, + kind: Block, + text: " outer {- nested -} block ", + }, + SourceComment { + region: Region { + start: Position { + line: 3, + column: 5, + }, + end: Position { + line: 3, + column: 23, + }, + }, + kind: Block, + text: " before value ", + }, + SourceComment { + region: Region { + start: Position { + line: 3, + column: 26, + }, + end: Position { + line: 3, + column: 37, + }, + }, + kind: Line, + text: " trailing", + }, + SourceComment { + region: Region { + start: Position { + line: 4, + column: 1, + }, + end: Position { + line: 4, + column: 7, + }, + }, + kind: Line, + text: " eof", + }, + ], + imports: [], + values: [ + Located { + region: Region { + start: Position { + line: 3, + column: 1, + }, + end: Position { + line: 5, + column: 1, + }, + }, + value: Value { + docs: None, + name: Located { + region: Region { + start: Position { + line: 3, + column: 1, + }, + end: Position { + line: 3, + column: 2, + }, + }, + value: "x", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 3, + column: 24, + }, + end: Position { + line: 3, + column: 25, + }, + }, + value: Int( + 1, + ), + }, + annotation: None, + attributes: [], + }, + }, + ], + unions: [], + aliases: [], + traits: [], + impls: [], + tests: None, + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_in_tests_and_strings.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_in_tests_and_strings.snap new file mode 100644 index 00000000..398da686 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_in_tests_and_strings.snap @@ -0,0 +1,189 @@ +--- +source: crates/nash-parse/src/module.rs +assertion_line: 709 +description: "Code:\n\ntext = \"-- not a comment {- neither -}\"\ntests\n -- test note\n test \"works\" = do\n assert {- assertion note -} True\n" +--- +Module { + kind: Normal, + name: None, + exports: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 1, + }, + }, + value: Open, + }, + docs: NoDocs( + Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 6, + column: 1, + }, + }, + ), + comments: [ + SourceComment { + region: Region { + start: Position { + line: 3, + column: 5, + }, + end: Position { + line: 3, + column: 17, + }, + }, + kind: Line, + text: " test note", + }, + SourceComment { + region: Region { + start: Position { + line: 5, + column: 16, + }, + end: Position { + line: 5, + column: 36, + }, + }, + kind: Block, + text: " assertion note ", + }, + ], + imports: [], + values: [ + Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 2, + column: 1, + }, + }, + value: Value { + docs: None, + name: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 5, + }, + }, + value: "text", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 1, + column: 8, + }, + end: Position { + line: 1, + column: 40, + }, + }, + value: Str( + "-- not a comment {- neither -}", + ), + }, + annotation: None, + attributes: [], + }, + }, + ], + unions: [], + aliases: [], + traits: [], + impls: [], + tests: Some( + Tests { + imports: [], + tests: [ + Located { + region: Region { + start: Position { + line: 4, + column: 5, + }, + end: Position { + line: 5, + column: 41, + }, + }, + value: Test { + name: Located { + region: Region { + start: Position { + line: 4, + column: 10, + }, + end: Position { + line: 4, + column: 17, + }, + }, + value: "works", + }, + expect: Pass, + budget: None, + body: Unit( + Block { + stmts: [], + last: Located { + region: Region { + start: Position { + line: 5, + column: 9, + }, + end: Position { + line: 5, + column: 41, + }, + }, + value: Assert( + Located { + region: Region { + start: Position { + line: 5, + column: 37, + }, + end: Position { + line: 5, + column: 41, + }, + }, + value: Var { + kind: CapVar, + name: "True", + }, + }, + ), + }, + }, + ), + }, + }, + ], + }, + ), + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_preserve_unicode_and_crlf.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_preserve_unicode_and_crlf.snap new file mode 100644 index 00000000..6c49db11 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__comments_preserve_unicode_and_crlf.snap @@ -0,0 +1,119 @@ +--- +source: crates/nash-parse/src/module.rs +assertion_line: 655 +description: "Code:\n\n-- café\r\nx = 1 -- 終" +--- +Module { + kind: Normal, + name: None, + exports: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 1, + }, + }, + value: Open, + }, + docs: NoDocs( + Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 13, + }, + }, + ), + comments: [ + SourceComment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 9, + }, + }, + kind: Line, + text: " café", + }, + SourceComment { + region: Region { + start: Position { + line: 2, + column: 7, + }, + end: Position { + line: 2, + column: 13, + }, + }, + kind: Line, + text: " 終", + }, + ], + imports: [], + values: [ + Located { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 13, + }, + }, + value: Value { + docs: None, + name: Located { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 2, + }, + }, + value: "x", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 2, + column: 5, + }, + end: Position { + line: 2, + column: 6, + }, + }, + value: Int( + 1, + ), + }, + annotation: None, + attributes: [], + }, + }, + ], + unions: [], + aliases: [], + traits: [], + impls: [], + tests: None, + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__declaration_docs_survive_categorization.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__declaration_docs_survive_categorization.snap new file mode 100644 index 00000000..eaf56b1a --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__declaration_docs_survive_categorization.snap @@ -0,0 +1,695 @@ +--- +source: crates/nash-parse/src/module.rs +description: "Code:\n\nmodule Documented exposing (..)\n{-| Overview. -}\n\n{-| A union. -}\ntype Box = Box int\n\n{-| An alias. -}\ntype alias Count = int\n\n{-| A trait. -}\ntrait Measure 'a where\n measure : 'a -> int\n\n{-| An implementation. -}\nimpl Measure int where\n measure x = x\n" +--- +Module { + kind: Normal, + name: Some( + Located { + region: Region { + start: Position { + line: 1, + column: 8, + }, + end: Position { + line: 1, + column: 18, + }, + }, + value: "Documented", + }, + ), + exports: Located { + region: Region { + start: Position { + line: 1, + column: 28, + }, + end: Position { + line: 1, + column: 32, + }, + }, + value: Open, + }, + docs: YesDocs { + overview: Comment { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 17, + }, + }, + snippet: Snippet { + data: [ + 32, + 79, + 118, + 101, + 114, + 118, + 105, + 101, + 119, + 46, + 32, + ], + off_row: 2, + off_col: 4, + }, + }, + comments: [ + ( + "Box", + Comment { + region: Region { + start: Position { + line: 4, + column: 1, + }, + end: Position { + line: 4, + column: 16, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 32, + 117, + 110, + 105, + 111, + 110, + 46, + 32, + ], + off_row: 4, + off_col: 4, + }, + }, + ), + ( + "Count", + Comment { + region: Region { + start: Position { + line: 7, + column: 1, + }, + end: Position { + line: 7, + column: 17, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 110, + 32, + 97, + 108, + 105, + 97, + 115, + 46, + 32, + ], + off_row: 7, + off_col: 4, + }, + }, + ), + ( + "Measure", + Comment { + region: Region { + start: Position { + line: 10, + column: 1, + }, + end: Position { + line: 10, + column: 16, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 32, + 116, + 114, + 97, + 105, + 116, + 46, + 32, + ], + off_row: 10, + off_col: 4, + }, + }, + ), + ], + }, + comments: [], + imports: [], + values: [], + unions: [ + Located { + region: Region { + start: Position { + line: 5, + column: 1, + }, + end: Position { + line: 7, + column: 1, + }, + }, + value: Union { + docs: Some( + Comment { + region: Region { + start: Position { + line: 4, + column: 1, + }, + end: Position { + line: 4, + column: 16, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 32, + 117, + 110, + 105, + 111, + 110, + 46, + 32, + ], + off_row: 4, + off_col: 4, + }, + }, + ), + name: Located { + region: Region { + start: Position { + line: 5, + column: 6, + }, + end: Position { + line: 5, + column: 9, + }, + }, + value: "Box", + }, + arguments: [], + ctors: [ + Ctor { + name: Located { + region: Region { + start: Position { + line: 5, + column: 12, + }, + end: Position { + line: 5, + column: 15, + }, + }, + value: "Box", + }, + arguments: Positional( + [ + Located { + region: Region { + start: Position { + line: 5, + column: 16, + }, + end: Position { + line: 5, + column: 19, + }, + }, + value: Type { + region: Region { + start: Position { + line: 5, + column: 16, + }, + end: Position { + line: 5, + column: 19, + }, + }, + name: "int", + args: [], + }, + }, + ], + ), + }, + ], + attributes: [], + }, + }, + ], + aliases: [ + Located { + region: Region { + start: Position { + line: 8, + column: 1, + }, + end: Position { + line: 10, + column: 1, + }, + }, + value: Alias { + docs: Some( + Comment { + region: Region { + start: Position { + line: 7, + column: 1, + }, + end: Position { + line: 7, + column: 17, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 110, + 32, + 97, + 108, + 105, + 97, + 115, + 46, + 32, + ], + off_row: 7, + off_col: 4, + }, + }, + ), + name: Located { + region: Region { + start: Position { + line: 8, + column: 12, + }, + end: Position { + line: 8, + column: 17, + }, + }, + value: "Count", + }, + arguments: [], + typ: Located { + region: Region { + start: Position { + line: 8, + column: 20, + }, + end: Position { + line: 8, + column: 23, + }, + }, + value: Type { + region: Region { + start: Position { + line: 8, + column: 20, + }, + end: Position { + line: 8, + column: 23, + }, + }, + name: "int", + args: [], + }, + }, + attributes: [], + }, + }, + ], + traits: [ + Located { + region: Region { + start: Position { + line: 11, + column: 1, + }, + end: Position { + line: 12, + column: 24, + }, + }, + value: Trait { + docs: Some( + Comment { + region: Region { + start: Position { + line: 10, + column: 1, + }, + end: Position { + line: 10, + column: 16, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 32, + 116, + 114, + 97, + 105, + 116, + 46, + 32, + ], + off_row: 10, + off_col: 4, + }, + }, + ), + name: Located { + region: Region { + start: Position { + line: 11, + column: 7, + }, + end: Position { + line: 11, + column: 14, + }, + }, + value: "Measure", + }, + params: [ + TypeParam { + name: Located { + region: Region { + start: Position { + line: 11, + column: 15, + }, + end: Position { + line: 11, + column: 17, + }, + }, + value: "a", + }, + repr: None, + }, + ], + supers: [], + methods: [ + TraitMethod { + name: Located { + region: Region { + start: Position { + line: 12, + column: 5, + }, + end: Position { + line: 12, + column: 12, + }, + }, + value: "measure", + }, + annotation: Annotation { + constraints: [], + typ: Located { + region: Region { + start: Position { + line: 12, + column: 15, + }, + end: Position { + line: 12, + column: 24, + }, + }, + value: Lambda { + from: Located { + region: Region { + start: Position { + line: 12, + column: 15, + }, + end: Position { + line: 12, + column: 17, + }, + }, + value: Var( + "a", + ), + }, + to: Located { + region: Region { + start: Position { + line: 12, + column: 21, + }, + end: Position { + line: 12, + column: 24, + }, + }, + value: Type { + region: Region { + start: Position { + line: 12, + column: 21, + }, + end: Position { + line: 12, + column: 24, + }, + }, + name: "int", + args: [], + }, + }, + }, + }, + }, + default: None, + }, + ], + attributes: [], + }, + }, + ], + impls: [ + Located { + region: Region { + start: Position { + line: 15, + column: 1, + }, + end: Position { + line: 16, + column: 18, + }, + }, + value: Impl { + docs: Some( + Comment { + region: Region { + start: Position { + line: 14, + column: 1, + }, + end: Position { + line: 14, + column: 26, + }, + }, + snippet: Snippet { + data: [ + 32, + 65, + 110, + 32, + 105, + 109, + 112, + 108, + 101, + 109, + 101, + 110, + 116, + 97, + 116, + 105, + 111, + 110, + 46, + 32, + ], + off_row: 14, + off_col: 4, + }, + }, + ), + context: [], + head: Located { + region: Region { + start: Position { + line: 15, + column: 6, + }, + end: Position { + line: 15, + column: 17, + }, + }, + value: Constraint { + class: Located { + region: Region { + start: Position { + line: 15, + column: 6, + }, + end: Position { + line: 15, + column: 13, + }, + }, + value: "Measure", + }, + module: None, + args: [ + Located { + region: Region { + start: Position { + line: 15, + column: 14, + }, + end: Position { + line: 15, + column: 17, + }, + }, + value: Type { + region: Region { + start: Position { + line: 15, + column: 14, + }, + end: Position { + line: 15, + column: 17, + }, + }, + name: "int", + args: [], + }, + }, + ], + }, + }, + methods: [ + Located { + region: Region { + start: Position { + line: 16, + column: 5, + }, + end: Position { + line: 17, + column: 1, + }, + }, + value: Define { + name: Located { + region: Region { + start: Position { + line: 16, + column: 5, + }, + end: Position { + line: 16, + column: 12, + }, + }, + value: "measure", + }, + args: [ + Located { + region: Region { + start: Position { + line: 16, + column: 13, + }, + end: Position { + line: 16, + column: 14, + }, + }, + value: Var( + "x", + ), + }, + ], + body: Located { + region: Region { + start: Position { + line: 16, + column: 17, + }, + end: Position { + line: 16, + column: 18, + }, + }, + value: Var { + kind: LowVar, + name: "x", + }, + }, + annotation: None, + }, + }, + ], + attributes: [], + }, + }, + ], + tests: None, + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__doc_comment_attaches_to_value.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__doc_comment_attaches_to_value.snap new file mode 100644 index 00000000..78817918 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__doc_comment_attaches_to_value.snap @@ -0,0 +1,121 @@ +--- +source: crates/nash-parse/src/module.rs +assertion_line: 660 +description: "Code:\n\n{-| value docs -}\nx = 1" +--- +Module { + kind: Normal, + name: None, + exports: Located { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 1, + }, + }, + value: Open, + }, + docs: NoDocs( + Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 2, + column: 6, + }, + }, + ), + comments: [], + imports: [], + values: [ + Located { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 6, + }, + }, + value: Value { + docs: Some( + Comment { + region: Region { + start: Position { + line: 1, + column: 1, + }, + end: Position { + line: 1, + column: 18, + }, + }, + snippet: Snippet { + data: [ + 32, + 118, + 97, + 108, + 117, + 101, + 32, + 100, + 111, + 99, + 115, + 32, + ], + off_row: 1, + off_col: 4, + }, + }, + ), + name: Located { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 2, + }, + }, + value: "x", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 2, + column: 5, + }, + end: Position { + line: 2, + column: 6, + }, + }, + value: Int( + 1, + ), + }, + annotation: None, + attributes: [], + }, + }, + ], + unions: [], + aliases: [], + traits: [], + impls: [], + tests: None, + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__later_constraint_does_not_imply_trait_superclass.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__later_constraint_does_not_imply_trait_superclass.snap index 762795c1..d8eb3063 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__later_constraint_does_not_imply_trait_superclass.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__later_constraint_does_not_imply_trait_superclass.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 565 description: "Code:\n\nmodule Main exposing (..)\n\ntrait Show 'a where\n\nid : Eq 'a => 'a -> 'a\nid x = x\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -58,6 +60,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -218,6 +221,7 @@ Module { }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_doc_comment.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_doc_comment.snap new file mode 100644 index 00000000..1e904892 --- /dev/null +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_doc_comment.snap @@ -0,0 +1,313 @@ +--- +source: crates/nash-parse/src/module.rs +assertion_line: 665 +description: "Code:\n\nmodule Main exposing (..)\n-- before overview\n{-| Overview with {- nested -} content. -}\nimport A -- import note\n\n{-| Value docs. -}\n@inline\nx : int\nx = 1\n" +--- +Module { + kind: Normal, + name: Some( + Located { + region: Region { + start: Position { + line: 1, + column: 8, + }, + end: Position { + line: 1, + column: 12, + }, + }, + value: "Main", + }, + ), + exports: Located { + region: Region { + start: Position { + line: 1, + column: 22, + }, + end: Position { + line: 1, + column: 26, + }, + }, + value: Open, + }, + docs: YesDocs { + overview: Comment { + region: Region { + start: Position { + line: 3, + column: 1, + }, + end: Position { + line: 3, + column: 43, + }, + }, + snippet: Snippet { + data: [ + 32, + 79, + 118, + 101, + 114, + 118, + 105, + 101, + 119, + 32, + 119, + 105, + 116, + 104, + 32, + 123, + 45, + 32, + 110, + 101, + 115, + 116, + 101, + 100, + 32, + 45, + 125, + 32, + 99, + 111, + 110, + 116, + 101, + 110, + 116, + 46, + 32, + ], + off_row: 3, + off_col: 4, + }, + }, + comments: [ + ( + "x", + Comment { + region: Region { + start: Position { + line: 6, + column: 1, + }, + end: Position { + line: 6, + column: 19, + }, + }, + snippet: Snippet { + data: [ + 32, + 86, + 97, + 108, + 117, + 101, + 32, + 100, + 111, + 99, + 115, + 46, + 32, + ], + off_row: 6, + off_col: 4, + }, + }, + ), + ], + }, + comments: [ + SourceComment { + region: Region { + start: Position { + line: 2, + column: 1, + }, + end: Position { + line: 2, + column: 19, + }, + }, + kind: Line, + text: " before overview", + }, + SourceComment { + region: Region { + start: Position { + line: 4, + column: 10, + }, + end: Position { + line: 4, + column: 24, + }, + }, + kind: Line, + text: " import note", + }, + ], + imports: [ + Import { + import: Located { + region: Region { + start: Position { + line: 4, + column: 8, + }, + end: Position { + line: 4, + column: 9, + }, + }, + value: "A", + }, + alias: None, + exposing: Explicit( + [], + ), + }, + ], + values: [ + Located { + region: Region { + start: Position { + line: 8, + column: 1, + }, + end: Position { + line: 10, + column: 1, + }, + }, + value: Value { + docs: Some( + Comment { + region: Region { + start: Position { + line: 6, + column: 1, + }, + end: Position { + line: 6, + column: 19, + }, + }, + snippet: Snippet { + data: [ + 32, + 86, + 97, + 108, + 117, + 101, + 32, + 100, + 111, + 99, + 115, + 46, + 32, + ], + off_row: 6, + off_col: 4, + }, + }, + ), + name: Located { + region: Region { + start: Position { + line: 9, + column: 1, + }, + end: Position { + line: 9, + column: 2, + }, + }, + value: "x", + }, + arguments: [], + body: Located { + region: Region { + start: Position { + line: 9, + column: 5, + }, + end: Position { + line: 9, + column: 6, + }, + }, + value: Int( + 1, + ), + }, + annotation: Some( + Annotation { + constraints: [], + typ: Located { + region: Region { + start: Position { + line: 8, + column: 5, + }, + end: Position { + line: 8, + column: 8, + }, + }, + value: Type { + region: Region { + start: Position { + line: 8, + column: 5, + }, + end: Position { + line: 8, + column: 8, + }, + }, + name: "int", + args: [], + }, + }, + }, + ), + attributes: [ + Attribute { + name: Located { + region: Region { + start: Position { + line: 7, + column: 2, + }, + end: Position { + line: 7, + column: 8, + }, + }, + value: "inline", + }, + args: [], + }, + ], + }, + }, + ], + unions: [], + aliases: [], + traits: [], + impls: [], + tests: None, + binops: [], +} diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_full.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_full.snap index ca750a44..24de3d61 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_full.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_full.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 514 description: "Code:\n\nmodule Main exposing (main, Model, Msg(..))\n\nimport Html exposing (div)\nimport Platform.Cmd as Cmd\n\ntype alias Model = { count : Int }\n\ntype Msg\n = Increment\n | Decrement\n\nmain = 0\n" --- Module { @@ -105,6 +106,7 @@ Module { }, }, ), + comments: [], imports: [ Import { import: Located { @@ -176,6 +178,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -223,6 +226,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { @@ -292,6 +296,7 @@ Module { }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_header_only.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_header_only.snap index 19c3d831..a098fe62 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_header_only.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_header_only.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 446 description: "Code:\n\nmodule Main exposing (..)\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [], unions: [], diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_no_header.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_no_header.snap index 949c462e..ff4b9f71 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_no_header.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_no_header.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 535 description: "Code:\n\nx = 1\n" --- Module { @@ -30,6 +31,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -44,6 +46,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_alias.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_alias.snap index e4e7b9a5..fe3a242a 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_alias.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_alias.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 503 description: "Code:\n\nmodule Main exposing (..)\n\ntype alias Point = { x : Int, y : Int }\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [], unions: [], @@ -60,6 +62,7 @@ Module { }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_impl_then_value.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_impl_then_value.snap index 230c8b4a..ccb50831 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_impl_then_value.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_impl_then_value.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 579 description: "Code:\n\nmodule Main exposing (..)\n\nimpl Show unit where\n\nx = 1\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -58,6 +60,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -108,6 +111,7 @@ Module { }, }, value: Impl { + docs: None, context: [], head: Located { region: Region { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_trait_then_value.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_trait_then_value.snap index e0c1e925..e8c91f26 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_trait_then_value.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_empty_trait_then_value.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 553 description: "Code:\n\nmodule Main exposing (..)\n\ntrait Marker 'a where\n\nx = 1\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -58,6 +60,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -107,6 +110,7 @@ Module { }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_imports.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_imports.snap index 421ecccd..d9105cc5 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_imports.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_imports.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 451 description: "Code:\n\nmodule Main exposing (..)\n\nimport List\nimport Maybe exposing (Maybe(..))\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [ Import { import: Located { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_infix.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_infix.snap index 775d7a90..d1e7198d 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_infix.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_infix.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 540 description: "Code:\n\nmodule Main exposing (..)\n\ninfix left 6 (|>) = apR\n\napR f x = f x\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -58,6 +60,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_type.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_type.snap index b3434517..ebd1a2fc 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_type.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_type.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 474 description: "Code:\n\nmodule Main exposing (..)\n\ntype Maybe 'a\n = Just 'a\n | Nothing\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [], unions: [ @@ -59,6 +61,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_union_then_value.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_union_then_value.snap index 2ea28bb9..d0270271 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_union_then_value.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_union_then_value.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 489 description: "Code:\n\nmodule Main exposing (..)\n\ntype Wrap 'a\n = Wrap 'a\n\nf w = w\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -58,6 +60,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -122,6 +125,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_value.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_value.snap index 877d1306..578ff588 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_value.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__module_with_value.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 463 description: "Code:\n\nmodule Main exposing (..)\n\nmain = 42\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -58,6 +60,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__module__tests__validator_module_full.snap b/crates/nash-parse/src/snapshots/nash_parse__module__tests__validator_module_full.snap index 908b8c10..b4eae759 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__module__tests__validator_module_full.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__module__tests__validator_module_full.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/module.rs +assertion_line: 592 description: "Code:\n\nvalidator module Vesting exposing (main)\n\nimport Cardano.Tx exposing (Tx, Output)\n\ntype Datum = Datum { owner : Bytes, deadline : Int }\n\ntype step 'a = Done 'a | Next int 'a\n\ntype alias acc = { total : int, seen : list Int }\n\nmain datum = assert True\n" --- Module { @@ -73,6 +74,7 @@ Module { }, }, ), + comments: [], imports: [ Import { import: Located { @@ -140,6 +142,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -218,6 +221,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { @@ -350,6 +354,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { @@ -494,6 +499,7 @@ Module { }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__complete_tests_block.snap b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__complete_tests_block.snap index 323256e6..0f7e935e 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__complete_tests_block.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__complete_tests_block.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/tests_block.rs +assertion_line: 291 description: "Code:\n\nmodule Main exposing (main)\n\nmain = 1\n\ntests\n import Prop exposing (int, listOf)\n\n test \"lt is strict\" = do\n assert (not (lt 1 1))\n\n test \"fails\" fail = do\n assert (1 / 0 == 0)\n\n test \"budget\" within (cpu 1000, mem 50) = do\n assert True\n\n prop \"antisym\" fail once within (mem 5) =\n let\n a via int\n b via int\n in\n do\n label \"x\"\n assert (compare a b == invert (compare b a))\n\n prop \"sorted\" =\n let xs via listOf int in\n do\n let\n ys = sort xs\n n <- length ys\n assert (n == length xs)\n" --- Module { @@ -62,6 +63,7 @@ Module { }, }, ), + comments: [], imports: [], values: [ Located { @@ -76,6 +78,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { diff --git a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__nested_monadic_do_in_test_block.snap b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__nested_monadic_do_in_test_block.snap index 513c12ee..3d9e6f80 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__nested_monadic_do_in_test_block.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__nested_monadic_do_in_test_block.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/tests_block.rs +assertion_line: 356 description: "Code:\n\nmodule Main exposing (..)\n\ntests\n test \"nested do\" = do\n r <- do\n x <- action\n pure x\n assert r\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [], unions: [], diff --git a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__syntax_overview_acceptance.snap b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__syntax_overview_acceptance.snap index c607c979..af3c606b 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__syntax_overview_acceptance.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__syntax_overview_acceptance.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/tests_block.rs +assertion_line: 514 description: "Code:\n\nvalidator module Vesting exposing (main)\n\nimport Cardano.Tx exposing (Tx, Output)\n\ntype Datum = Datum { owner : Bytes, deadline : Int }\n\ntype step 'a = Done 'a | Next int 'a\n\ntype alias acc = { total : int, seen : list Int }\n\ntrait Eq 'a => Ord 'a where\n compare : 'a -> 'a -> ordering\n\n lt : 'a -> 'a -> bool\n lt a b = compare a b == LT\n\nimpl Ord int where\n compare = Builtin.compareInteger\n\n@derive(Eq, Show, ToData, FromData)\ntype Redeemer = Claim | Cancel\n\nmain : Datum -> Redeemer -> Data -> unit\nmain datum redeemer ctx =\n case redeemer of\n Claim -> assert (lower datum.deadline < currentSlot ctx)\n Cancel -> assert (signedBy ctx datum.owner)\n\ntests\n import Prop exposing (int, listOf)\n\n test \"lt is strict\" = do\n assert (not (lt 1 1))\n\n prop \"compare is antisymmetric\" =\n let\n a via int\n b via int\n in\n do\n label (if a < b then \"lt\" else \"ge\")\n assert (compare a b == invert (compare b a))\n\n prop \"division by zero fails\" fail =\n let x via int in\n do\n assert (x / 0 == 0)\n" --- Module { @@ -73,6 +74,7 @@ Module { }, }, ), + comments: [], imports: [ Import { import: Located { @@ -140,6 +142,7 @@ Module { }, }, value: Value { + docs: None, name: Located { region: Region { start: Position { @@ -729,6 +732,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { @@ -861,6 +865,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { @@ -1003,6 +1008,7 @@ Module { }, }, value: Union { + docs: None, name: Located { region: Region { start: Position { @@ -1154,6 +1160,7 @@ Module { }, }, value: Alias { + docs: None, name: Located { region: Region { start: Position { @@ -1310,6 +1317,7 @@ Module { }, }, value: Trait { + docs: None, name: Located { region: Region { start: Position { @@ -1785,6 +1793,7 @@ Module { }, }, value: Impl { + docs: None, context: [], head: Located { region: Region { diff --git a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_with_imports_only.snap b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_with_imports_only.snap index 0da2c04b..622b02ef 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_with_imports_only.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_with_imports_only.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/tests_block.rs +assertion_line: 344 description: "Code:\n\nmodule Main exposing (..)\n\ntests\n import Prop exposing (int)\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [], unions: [], diff --git a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_without_imports.snap b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_without_imports.snap index a6fc18be..5f1cd515 100644 --- a/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_without_imports.snap +++ b/crates/nash-parse/src/snapshots/nash_parse__tests_block__tests__tests_without_imports.snap @@ -1,5 +1,6 @@ --- source: crates/nash-parse/src/tests_block.rs +assertion_line: 331 description: "Code:\n\nmodule Main exposing (..)\n\ntests\n test \"truth\" = do\n assert True\n" --- Module { @@ -44,6 +45,7 @@ Module { }, }, ), + comments: [], imports: [], values: [], unions: [], diff --git a/crates/nash-parse/src/space.rs b/crates/nash-parse/src/space.rs index d4050404..c037896e 100644 --- a/crates/nash-parse/src/space.rs +++ b/crates/nash-parse/src/space.rs @@ -12,7 +12,7 @@ use crate::error::Space; use crate::{Col, Parser, Row}; -use nash_source::{Comment, Snippet}; +use nash_source::{Comment, CommentKind, Snippet, SourceComment}; /// Result of eating spaces. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -164,7 +164,13 @@ impl<'a> Parser<'a> { off_row: start_row, off_col: start_col, }); - let comment = self.alloc(Comment(snippet)); + let comment = self.alloc(Comment { + region: nash_region::Region::new( + nash_region::Position::new(start_row, start_col - 3), + self.get_position(), + ), + snippet, + }); Ok(comment) } @@ -246,26 +252,33 @@ impl<'a> Parser<'a> { /// Eat a line comment (from -- to end of line). fn eat_line_comment(&mut self) { - // Skip the -- - self.advance(); - self.advance(); - - loop { - match self.peek() { - Some(0x0A) => { - // Newline ends the comment - self.advance(); - return; - } - Some(_) => { - self.advance(); - } - None => { - // EOF ends the comment - return; - } - } + let start = self.get_position(); + self.advance_by(2); + let text_start = self.pos; + while !matches!(self.peek(), None | Some(b'\n')) + && !(self.peek() == Some(b'\r') && self.peek_at(1) == Some(b'\n')) + { + self.advance(); } + self.push_comment(start, CommentKind::Line, text_start, self.pos); + // The surrounding whitespace loop consumes the line ending. + } + + fn push_comment( + &mut self, + start: nash_region::Position, + kind: CommentKind, + text_start: usize, + text_end: usize, + ) { + let text = std::str::from_utf8(&self.src[text_start..text_end]) + .expect("source is valid UTF-8 and comment delimiters are ASCII"); + let comment = self.alloc(SourceComment { + region: nash_region::Region::new(start, self.get_position()), + kind, + text, + }); + self.comments.push(comment); } /// Eat a multi-line comment ({- ... -}). @@ -277,7 +290,12 @@ impl<'a> Parser<'a> { self.advance(); self.advance(); - self.eat_multi_comment_help(1, opening) + let text_start = self.pos; + let status = self.eat_multi_comment_help(1, opening); + if matches!(status, SpaceStatus::Good) { + self.push_comment(opening, CommentKind::Block, text_start, self.pos - 2); + } + status } /// Helper for eating multi-line comments with nesting. @@ -436,9 +454,9 @@ mod tests { assert!(result.is_ok()); let comment = result.unwrap(); // Content is " hello " (between {-| and -}) - assert_eq!(comment.0.data, b" hello "); - assert_eq!(comment.0.off_row, 1); - assert_eq!(comment.0.off_col, 4); // Column after {-| + assert_eq!(comment.snippet.data, b" hello "); + assert_eq!(comment.snippet.off_row, 1); + assert_eq!(comment.snippet.off_col, 4); // Column after {-| } #[test] @@ -450,7 +468,7 @@ mod tests { let result = parser.doc_comment(|_, _| "expected", |_, _, _| "space error"); assert!(result.is_ok()); let comment = result.unwrap(); - assert_eq!(comment.0.data, b" line one\nline two "); + assert_eq!(comment.snippet.data, b" line one\nline two "); } #[test] diff --git a/crates/nash-source/src/lib.rs b/crates/nash-source/src/lib.rs index c911dcb1..40625416 100644 --- a/crates/nash-source/src/lib.rs +++ b/crates/nash-source/src/lib.rs @@ -6,6 +6,8 @@ pub struct Module<'a> { pub name: Option<&'a Located<&'a str>>, pub exports: &'a Located>, pub docs: &'a Docs<'a>, + /// Ordinary comments in source order, including nested block text. + pub comments: &'a [&'a SourceComment<'a>], pub imports: &'a [&'a Import<'a>], pub values: &'a [&'a Located>], pub unions: &'a [&'a Located>], @@ -31,6 +33,7 @@ pub struct Import<'a> { #[derive(Debug)] pub struct Value<'a> { + pub docs: Option<&'a Comment<'a>>, pub name: &'a Located<&'a str>, pub arguments: &'a [&'a Located>], pub body: &'a Located>, @@ -46,6 +49,7 @@ pub struct Attribute<'a> { #[derive(Debug)] pub struct Trait<'a> { + pub docs: Option<&'a Comment<'a>>, pub name: &'a Located<&'a str>, pub params: &'a [&'a TypeParam<'a>], pub supers: &'a [&'a Located>], @@ -62,6 +66,7 @@ pub struct TraitMethod<'a> { #[derive(Debug)] pub struct Impl<'a> { + pub docs: Option<&'a Comment<'a>>, pub context: &'a [&'a Located>], pub head: &'a Located>, pub methods: &'a [&'a Located>], @@ -136,6 +141,7 @@ pub struct Constraint<'a> { // | Nothing #[derive(Debug)] pub struct Union<'a> { + pub docs: Option<&'a Comment<'a>>, pub name: &'a Located<&'a str>, // type vars pub arguments: &'a [&'a TypeParam<'a>], @@ -157,6 +163,7 @@ pub enum CtorArgs<'a> { #[derive(Debug)] pub struct Alias<'a> { + pub docs: Option<&'a Comment<'a>>, pub name: &'a Located<&'a str>, // type vars pub arguments: &'a [&'a TypeParam<'a>], @@ -443,8 +450,27 @@ pub enum Docs<'a> { }, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CommentKind { + Line, + Block, +} + #[derive(Debug)] -pub struct Comment<'a>(pub &'a Snippet<'a>); +pub struct SourceComment<'a> { + /// Includes delimiters, excludes the line ending for line comments. + pub region: Region, + pub kind: CommentKind, + /// Exact text between delimiters, without a line comment's CRLF or LF. + pub text: &'a str, +} + +#[derive(Debug)] +pub struct Comment<'a> { + /// Includes the opening and closing doc-comment delimiters. + pub region: Region, + pub snippet: &'a Snippet<'a>, +} #[derive(Debug)] pub struct Snippet<'a> { diff --git a/plans/13-fmt-docs.md b/plans/13-fmt-docs.md index 4596c1c9..70726327 100644 --- a/plans/13-fmt-docs.md +++ b/plans/13-fmt-docs.md @@ -14,14 +14,10 @@ Crates touched: `nash-source`, `nash-parse`, new `nash-fmt`, new References: -- Comments today: the parser drops line and block comments in - `eat_spaces` (`crates/nash-parse/src/space.rs:167`, - `eat_line_comment` :212, `eat_multi_comment` :236). Doc comments are - captured by `doc_comment` (:107) as `Comment(&Snippet { data, off_row, off_col })` - (`crates/nash-source/src/lib.rs:257`), attached to `Decl::Value(Option<&Comment>, ..)` - (`crates/nash-parse/src/declaration/mod.rs:19`), then discarded by - `categorize_decls` (`crates/nash-parse/src/module.rs:258`). Module - docs are always `Docs::NoDocs` (`module.rs:241`). +- Comments: `nash-parse/src/space.rs` retains ordinary comments in a + source-ordered `Module.comments` side table. Doc comments retain their + region and source snippet, attach to declarations, and populate `Docs::YesDocs` + when an explicit module header is followed by an overview. - Elm: `Parse/Module.hs` `chompModuleDocCommentSpace`, `Elm/Docs.hs` (`Module`, `Union`, `Alias`, `Value`, `Binop`, `fromModule`, the `@docs` overview parser), `Elm/Compiler/Type/Extract.hs` (types for @@ -33,100 +29,36 @@ References: --- -## Chunk 1: comments in the surface AST - -**Files** - -- `crates/nash-source/src/lib.rs` -- `crates/nash-parse/src/space.rs`, `lib.rs`, `module.rs`, `declaration/mod.rs` - -**Change** - -Keep every comment with its region in a side table on `Module`, and keep -doc comments on declarations and on the module. - -**Code** - -```rust -// crates/nash-source/src/lib.rs -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CommentKind { - Line, // -- ... - Block, // {- ... -} -} - -#[derive(Debug)] -pub struct SourceComment<'a> { - pub region: Region, - pub kind: CommentKind, - /// Text between the delimiters, untrimmed. - pub text: &'a str, -} - -pub struct Module<'a> { - // ... - /// All non-doc comments in source order. - pub comments: &'a [&'a SourceComment<'a>], - pub docs: &'a Docs<'a>, // now filled: module overview + per-decl docs -} - -pub struct Value<'a> { /* ... */ pub docs: Option<&'a Comment<'a>> } -pub struct Union<'a> { /* ... */ pub docs: Option<&'a Comment<'a>> } -pub struct Alias<'a> { /* ... */ pub docs: Option<&'a Comment<'a>> } -``` - -`Parser` gains `comments: Vec<&'a SourceComment<'a>>`. `eat_line_comment` -and `eat_multi_comment` record start position before advancing and push -after: - -```rust -fn eat_line_comment(&mut self) { - let start = self.get_position(); - let text_start = self.pos + 2; - self.advance(); self.advance(); - let mut text_end = self.pos; - loop { - match self.peek() { - Some(0x0A) => { self.advance(); break; } - Some(_) => { self.advance(); text_end = self.pos; } - None => break, - } - } - self.push_comment(start, CommentKind::Line, text_start, text_end); -} - -fn push_comment(&mut self, start: Position, kind: CommentKind, text_start: usize, text_end: usize) { - let text = std::str::from_utf8(&self.src[text_start..text_end]).expect("source is UTF-8"); - let comment = self.alloc(SourceComment { region: Region::new(start, self.get_position()), kind, text }); - self.comments.push(comment); -} -``` - -`module()` moves `self.comments` into `Module.comments` at the end and -parses a module doc comment after the header (Elm's -`chompModuleDocCommentSpace`). `categorize_decls` stores the `Decl`'s -doc on the value/union/alias instead of dropping it. - -Backtracking: `save_state`/`restore_state` (`module.rs:208`) must -truncate `comments` to the saved length so a failed alternative does not -leave duplicates. - -**Elm/Aiken reference** - -`Parse/Space.hs` `eatLineComment`, `eatMultiComment` (structure); -`Parse/Module.hs` `chompModuleDocCommentSpace`. Aiken keeps comments the -same way: `crates/aiken-lang/src/parser/token.rs` `Token::Comment` plus -`extra.comments` spans in `crates/aiken-lang/src/parser/extra.rs`. - -**Tests** (`crates/nash-parse/src/space.rs`, `module.rs`) - -- `comments_are_collected`: `-- a\n{- b -}\nx = 1` yields two `SourceComment`s with regions and texts `" a"`, `" b "`. -- `doc_comment_attaches_to_value`: `{-| doc -}\nx = 1` → `values[0].docs.is_some()`. -- `module_doc_comment`: `module M exposing (..)\n{-| overview -}\nx = 1` → `Docs::YesDocs`. -- `backtracking_does_not_duplicate_comments`. - -**Done when** existing parser snapshots are unchanged except for the new -fields. +## Chunk 1: comments in the surface AST — complete + +Implemented in `nash-source`, `nash-parse`, and the canonicalizer's synthetic +value initializer. + +- `Module.comments` retains ordinary line and block comments in source order. + Each `SourceComment` stores its kind, region, and exact inner text. +- Line-comment regions include `--` and exclude LF/CRLF; block-comment regions + include both delimiters. Nested blocks remain part of the outer comment text. +- `Comment` stores the doc-comment region and its existing source `Snippet`. + Values, unions, aliases, traits, and implementations retain attached docs. +- A doc comment after an explicit module header is the module overview. + `Docs::YesDocs` indexes named declaration docs in source order. Headerless + declaration docs stay on their declaration; implementation docs stay on the + implementation because implementations have no unique declaration name. +- Parser save/restore includes the ordinary-comment count. Restoring a failed + alternative truncates the collection, so lookahead does not duplicate comments. +- Internal `Decl` variants refer to the documented surface nodes directly; + docs are no longer temporarily stored on wrappers and discarded during + categorization. + +Snapshot coverage includes leading/inline/trailing comments, nested blocks, +Unicode, CRLF and EOF, doc attachments (including attributes), module overviews, +comments in tests, comment markers in strings, and section/do backtracking. +Existing declaration/module snapshots now include the preserved metadata. +Validation: 462 parser tests pass; full workspace tests, strict all-targets/ +all-features Clippy, and formatting checks pass. + +Chunk 1 does not format source or attach ordinary comments to individual nodes; +those tasks belong to later chunks. --- From fd56cc68eced010fb5e16d4c8811865249fca40d Mon Sep 17 00:00:00 2001 From: microproofs Date: Fri, 25 Sep 2026 23:08:00 -0400 Subject: [PATCH 02/19] feat(fmt): add source formatter and check diffs Signed-off-by: microproofs --- .sampo/changesets/nash-source-formatter.md | 12 + Cargo.lock | 15 + SPEC.md | 6 +- crates/nash-cli/Cargo.toml | 1 + crates/nash-cli/src/cmd/format.rs | 109 +++++ crates/nash-cli/src/cmd/mod.rs | 5 + crates/nash-fmt/Cargo.toml | 19 + crates/nash-fmt/src/declarations.rs | 367 ++++++++++++++ crates/nash-fmt/src/doc.rs | 111 +++++ crates/nash-fmt/src/expr.rs | 378 +++++++++++++++ crates/nash-fmt/src/lib.rs | 24 + crates/nash-fmt/src/printer.rs | 139 ++++++ ...ash_fmt__tests__aligned_do_statements.snap | 26 + .../nash_fmt__tests__comment_only.snap | 6 + .../nash_fmt__tests__comment_whitespace.snap | 7 + .../snapshots/nash_fmt__tests__comments.snap | 21 + .../nash_fmt__tests__comments_in_syntax.snap | 30 ++ .../snapshots/nash_fmt__tests__crlf_diff.snap | 13 + .../nash_fmt__tests__declarations.snap | 31 ++ .../nash_fmt__tests__do_control_flow.snap | 34 ++ .../nash_fmt__tests__do_operators.snap | 12 + ...h_fmt__tests__documented_declarations.snap | 35 ++ .../nash_fmt__tests__expressions.snap | 33 ++ .../nash_fmt__tests__formatting_diff.snap | 21 + .../nash_fmt__tests__keywords_and_macros.snap | 23 + .../nash_fmt__tests__literal_patterns.snap | 29 ++ .../snapshots/nash_fmt__tests__literals.snap | 26 + .../nash_fmt__tests__long_lines.snap | 32 ++ ...ash_fmt__tests__multiline_annotations.snap | 16 + ...ash_fmt__tests__multiline_collections.snap | 28 ++ .../nash_fmt__tests__nested_control_flow.snap | 42 ++ .../snapshots/nash_fmt__tests__operators.snap | 26 + .../nash_fmt__tests__parse_error.snap | 12 + .../nash_fmt__tests__patterns_and_types.snap | 26 + .../nash_fmt__tests__record_updates.snap | 14 + ...fmt__tests__semantics_sensitive_forms.snap | 26 + .../nash_fmt__tests__tests_block.snap | 25 + ...mt__tests__underindented_do_delimiter.snap | 17 + .../snapshots/nash_fmt__tests__validator.snap | 8 + .../snapshots/nash_fmt__tests__values.snap | 8 + crates/nash-fmt/src/tests.rs | 458 ++++++++++++++++++ crates/nash-fmt/src/tests/semantic.rs | 209 ++++++++ crates/nash-fmt/src/types.rs | 125 +++++ crates/nash-parse/src/declaration/union.rs | 2 +- crates/nash-parse/src/expression/do_.rs | 2 +- crates/nash-parse/src/expression/if_.rs | 4 +- crates/nash-parse/src/expression/keyword.rs | 1 + crates/nash-parse/src/expression/let_.rs | 2 +- crates/nash-parse/src/expression/list.rs | 4 +- crates/nash-parse/src/expression/record.rs | 4 +- crates/nash-parse/src/expression/tuple.rs | 8 +- crates/nash-parse/src/space.rs | 24 +- crates/nash-report/Cargo.toml | 1 + crates/nash-report/src/format.rs | 81 ++++ crates/nash-report/src/lib.rs | 1 + docs/cli.md | 12 +- docs/formatter.md | 53 ++ docs/overview.md | 4 +- plans/13-fmt-docs.md | 245 +++------- 59 files changed, 2843 insertions(+), 210 deletions(-) create mode 100644 .sampo/changesets/nash-source-formatter.md create mode 100644 crates/nash-cli/src/cmd/format.rs create mode 100644 crates/nash-fmt/Cargo.toml create mode 100644 crates/nash-fmt/src/declarations.rs create mode 100644 crates/nash-fmt/src/doc.rs create mode 100644 crates/nash-fmt/src/expr.rs create mode 100644 crates/nash-fmt/src/lib.rs create mode 100644 crates/nash-fmt/src/printer.rs create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_only.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_whitespace.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__comments.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__crlf_diff.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__documented_declarations.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__expressions.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__formatting_diff.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__literal_patterns.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__literals.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_collections.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__nested_control_flow.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__operators.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__parse_error.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__record_updates.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__semantics_sensitive_forms.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__tests_block.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__underindented_do_delimiter.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__validator.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__values.snap create mode 100644 crates/nash-fmt/src/tests.rs create mode 100644 crates/nash-fmt/src/tests/semantic.rs create mode 100644 crates/nash-fmt/src/types.rs create mode 100644 crates/nash-report/src/format.rs create mode 100644 docs/formatter.md diff --git a/.sampo/changesets/nash-source-formatter.md b/.sampo/changesets/nash-source-formatter.md new file mode 100644 index 00000000..bf84e366 --- /dev/null +++ b/.sampo/changesets/nash-source-formatter.md @@ -0,0 +1,12 @@ +--- +cargo/nash-fmt: minor +cargo/nash-cli: minor +cargo/nash-report: minor +cargo/nash-parse: patch +--- + +Add an AST-based Nash source formatter with 80-column layout, comment preservation, +and source snapshot tests. Expose `nash format` (`fmt`), in-place and stdin +formatting, and contextual `--check` diffs through the existing report style. +Correct whitespace handling after message keywords and before constructor docs. +Allow aligned explicit continuations inside `do` without merging statements. diff --git a/Cargo.lock b/Cargo.lock index c8266845..d29b56b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1921,6 +1921,7 @@ dependencies = [ "nash-codegen", "nash-config", "nash-driver", + "nash-fmt", "nash-language-server", "nash-plutus", "nash-report", @@ -2015,6 +2016,19 @@ dependencies = [ "url", ] +[[package]] +name = "nash-fmt" +version = "0.1.0" +dependencies = [ + "bumpalo", + "indoc", + "insta", + "nash-parse", + "nash-region", + "nash-report", + "nash-source", +] + [[package]] name = "nash-ir" version = "0.3.3" @@ -2111,6 +2125,7 @@ dependencies = [ "nash-source", "serde", "serde_json", + "similar", "unicode-width 0.1.14", ] diff --git a/SPEC.md b/SPEC.md index 8b265532..bd238a4d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -32,11 +32,11 @@ produce UPLC programs; all dependencies inline into each program. | `nash-codegen` | Can -> Core -> UPLC | new ([plans/07](plans/07-codegen.md)) | | `nash-test` | test runner, generation, shrinking | new ([plans/10](plans/10-testing.md)) | | `nash-macro` | macro expansion, comptime | new ([plans/11](plans/11-macros-comptime.md)) | -| `nash-fmt` / `nash-docs` | formatter, docs | new ([plans/13](plans/13-fmt-docs.md)) | +| `nash-fmt` / `nash-docs` | formatter, docs | formatter implemented; docs pending ([plans/13](plans/13-fmt-docs.md)) | | `nash-plutus` | UPLC terms, flat, CEK, cost models | done | | `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-cli` | `nash` binary | `check`, `build`, `test`, `format` (`fmt`), `lsp`; docs pending | | `nash-language-server` | LSP | live compiler diagnostics with UTF-16 ranges | | `crates/nash-driver/base/` | compiler-bundled `nash/base` foundation (Nash source) | implemented; remaining stdlib work ([plans/12](plans/12-stdlib.md)) | @@ -68,7 +68,7 @@ Implementation plans (Plan 08 is deferred; Plans 09 and 10 are complete): - [x] 10 Testing: `tests` block, props, generators, shrinking, power-assert, `nash test` — [plans/10-testing.md](plans/10-testing.md) - [ ] 11 Macros + comptime — [plans/11-macros-comptime.md](plans/11-macros-comptime.md) - [ ] 12 Stdlib `nash/base` — [plans/12-stdlib.md](plans/12-stdlib.md) (chunks 1–9 and 11–13 complete; chunk 10 Ast/Derive deferred) -- [ ] 13 `nash fmt`, `nash docs` — [plans/13-fmt-docs.md](plans/13-fmt-docs.md) (chunk 1 comment preservation complete; chunks 2–7 pending) +- [ ] 13 `nash format` (`fmt`), `nash docs` — [plans/13-fmt-docs.md](plans/13-fmt-docs.md) (chunks 1–5 formatter complete; chunks 6–7 docs pending) - [x] Representation-classed impl heads: `Big`/`Little` blankets and concrete little impls coexist — [plans/repr-classed-impl-heads.md](plans/repr-classed-impl-heads.md) Later: LSP features, web playground, package registry (pubgrub), TypeScript codegen. diff --git a/crates/nash-cli/Cargo.toml b/crates/nash-cli/Cargo.toml index 03b1e65d..3a08aadf 100644 --- a/crates/nash-cli/Cargo.toml +++ b/crates/nash-cli/Cargo.toml @@ -16,6 +16,7 @@ name = "nash" path = "src/main.rs" [dependencies] +nash-fmt = { path = "../nash-fmt", version = "0.1.0" } nash-codegen = { path = "../nash-codegen", version = "0.4.2" } clap.workspace = true color-print.workspace = true diff --git a/crates/nash-cli/src/cmd/format.rs b/crates/nash-cli/src/cmd/format.rs new file mode 100644 index 00000000..bbac80d2 --- /dev/null +++ b/crates/nash-cli/src/cmd/format.rs @@ -0,0 +1,109 @@ +use miette::{IntoDiagnostic, Result}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +#[derive(clap::Args)] +pub struct Args { + /// Nash files or directories to format recursively. + #[arg(default_value = ".")] + pub paths: Vec, + /// Show formatting differences without changing files. + #[arg(long)] + pub check: bool, + /// Format one module from stdin to stdout. + #[arg(long, conflicts_with_all = ["paths", "check"])] + pub stdin: bool, +} + +impl Args { + pub async fn exec(self, color: bool) -> Result<()> { + if self.stdin { + let mut source = String::new(); + std::io::stdin() + .read_to_string(&mut source) + .into_diagnostic()?; + let formatted = nash_fmt::format(&source).map_err(|report| { + miette::Report::new(report.render( + &nash_report::Source::new(&source), + "", + color, + )) + })?; + std::io::stdout() + .write_all(formatted.as_bytes()) + .into_diagnostic()?; + return Ok(()); + } + let mut files = Vec::new(); + for path in self.paths { + collect(&path.canonicalize().into_diagnostic()?, &mut files)?; + } + files.sort(); + files.dedup(); + let root = std::env::current_dir().into_diagnostic()?; + let links = supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr); + let mut failed = false; + for path in files { + let source = std::fs::read_to_string(&path).into_diagnostic()?; + let name = crate::reporting::source_name(&root, links, &path.to_string_lossy()); + let formatted = match nash_fmt::format(&source) { + Ok(formatted) => formatted, + Err(report) => { + eprintln!( + "{:?}", + miette::Report::new(report.render( + &nash_report::Source::new(&source), + &name, + color + )) + ); + failed = true; + continue; + } + }; + if self.check { + if let Some(report) = nash_report::format::difference(&name, &source, &formatted) { + eprintln!( + "{:?}", + miette::Report::new(report.render( + &nash_report::Source::new(&source), + &name, + color + )) + ); + failed = true; + } + } else if source != formatted { + std::fs::write(&path, formatted).into_diagnostic()?; + } + } + if failed { + std::process::exit(1); + } + Ok(()) + } +} + +fn collect(path: &Path, files: &mut Vec) -> Result<()> { + let metadata = std::fs::symlink_metadata(path).into_diagnostic()?; + if metadata.is_file() { + if path + .extension() + .is_some_and(|extension| extension == "nash") + { + files.push(path.canonicalize().into_diagnostic()?); + } + } else if metadata.is_dir() { + for entry in std::fs::read_dir(path).into_diagnostic()? { + let entry = entry.into_diagnostic()?; + if matches!( + entry.file_name().to_str(), + Some(".git" | ".jj" | "target" | "build" | "node_modules") + ) { + continue; + } + collect(&entry.path(), files)?; + } + } + Ok(()) +} diff --git a/crates/nash-cli/src/cmd/mod.rs b/crates/nash-cli/src/cmd/mod.rs index 6b26e196..17390508 100644 --- a/crates/nash-cli/src/cmd/mod.rs +++ b/crates/nash-cli/src/cmd/mod.rs @@ -1,5 +1,6 @@ pub mod build; pub mod check; +pub mod format; pub mod lsp; pub mod test; @@ -14,6 +15,9 @@ pub enum Cmd { /// Compile and execute module tests and properties #[clap(visible_alias = "t")] Test(test::Args), + /// Format Nash source files + #[clap(visible_alias = "fmt")] + Format(format::Args), /// Start the Nash language server over stdio Lsp(lsp::Args), } @@ -24,6 +28,7 @@ impl Cmd { Cmd::Check(args) => args.exec(color).await, Cmd::Build(args) => args.exec(color).await, Cmd::Test(args) => args.exec(color).await, + Cmd::Format(args) => args.exec(color).await, Cmd::Lsp(args) => lsp::exec(args).await, } } diff --git a/crates/nash-fmt/Cargo.toml b/crates/nash-fmt/Cargo.toml new file mode 100644 index 00000000..7cc884a2 --- /dev/null +++ b/crates/nash-fmt/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nash-fmt" +version = "0.1.0" +edition.workspace = true +description = "Source formatter for Nash" +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +bumpalo.workspace = true +nash-source = { path = "../nash-source", version = "0.9.0" } +nash-region = { path = "../nash-region", version = "0.3.0" } +nash-parse = { path = "../nash-parse", version = "0.8.0" } +nash-report = { path = "../nash-report", version = "0.5.3" } + +[dev-dependencies] +indoc.workspace = true +insta.workspace = true diff --git a/crates/nash-fmt/src/declarations.rs b/crates/nash-fmt/src/declarations.rs new file mode 100644 index 00000000..9b6f9b87 --- /dev/null +++ b/crates/nash-fmt/src/declarations.rs @@ -0,0 +1,367 @@ +use crate::{ + doc::{Doc, cat, join, text}, + printer::Printer, +}; +use nash_region::Position; +use nash_source::*; + +impl Printer<'_> { + pub fn exposing(&mut self, exposing: &Exposing<'_>, broken: bool) -> Doc { + match exposing { + Exposing::Open => text("(..)"), + Exposing::Explicit(items) => { + let docs = items + .iter() + .map(|item| match item { + Exposed::Lower(name) => { + cat([self.before(name.region.start), text(name.value)]) + } + Exposed::Upper { name, privacy } => cat([ + self.before(name.region.start), + text(name.value), + text(if matches!(privacy, Privacy::Public(_)) { + "(..)" + } else { + "" + }), + ]), + Exposed::LowerType { name, privacy } => cat([ + self.before(name.region.start), + text("type "), + text(name.value), + text(if matches!(privacy, Privacy::Public(_)) { + "(..)" + } else { + "" + }), + ]), + Exposed::Operator { region, op } => { + cat([self.before(region.start), text(format!("({op})"))]) + } + }) + .collect(); + self.collection("(", ")", docs, broken).nest() + } + } + } + fn import(&mut self, import: &Import<'_>) -> Doc { + let before = self.before(import.import.region.start); + let mut docs = vec![before, text("import "), text(import.import.value)]; + if let Some(alias) = import.alias { + docs.push(text(format!(" as {alias}"))); + } + if !matches!(import.exposing,Exposing::Explicit(items) if items.is_empty()) { + docs.extend([text(" exposing "), self.exposing(import.exposing, false)]); + } + cat(docs) + } + fn attributes(&mut self, attributes: &[&Attribute<'_>]) -> Doc { + let mut docs = Vec::new(); + for a in attributes { + docs.extend([ + self.before(a.name.region.start), + text(format!("@{}", a.name.value)), + ]); + if !a.args.is_empty() { + let args = a.args.iter().map(|e| self.expr(e, 0)).collect(); + docs.push(self.collection("(", ")", args, false)); + } + docs.push(Doc::Hard); + } + cat(docs) + } + fn union(&mut self, u: &Union<'_>) -> Doc { + let docs = self.documented(u.docs); + let attributes = self.attributes(u.attributes); + let before = self.before(u.name.region.start); + let params = self.params(u.arguments); + let ctors = join( + u.ctors.iter().map(|c| { + let before = self.before(c.name.region.start); + let args = match &c.arguments { + CtorArgs::Positional(args) => args + .iter() + .enumerate() + .map(|(index, t)| { + let doc = self.typ(t, 2); + if index == 0 && matches!(t.value, Type::Record(_)) { + self.parens(doc) + } else { + doc + } + }) + .collect(), + CtorArgs::Labeled(fields) => { + let fields = fields + .iter() + .map(|(name, typ)| { + let before = self.before(name.region.start); + cat([before, text(name.value), text(" : "), self.typ(typ, 0)]) + }) + .collect(); + vec![self.collection("{", "}", fields, false)] + } + }; + cat([before, self.args(text(c.name.value), args, false)]) + }), + cat([Doc::Hard, text("| ")]), + ); + cat([ + docs, + attributes, + before, + text(format!("type {}", u.name.value)), + if u.arguments.is_empty() { + text("") + } else { + cat([text(" "), params]) + }, + cat([Doc::Hard, text("= "), ctors]).nest(), + ]) + } + fn alias(&mut self, a: &Alias<'_>) -> Doc { + let docs = self.documented(a.docs); + let attributes = self.attributes(a.attributes); + let before = self.before(a.name.region.start); + let params = self.params(a.arguments); + let typ = self.typ(a.typ, 0); + cat([ + docs, + attributes, + before, + text(format!("type alias {}", a.name.value)), + if a.arguments.is_empty() { + text("") + } else { + cat([text(" "), params]) + }, + text(" ="), + cat([Doc::Hard, typ]).nest(), + ]) + } + fn trait_(&mut self, t: &Trait<'_>) -> Doc { + let docs = self.documented(t.docs); + let attributes = self.attributes(t.attributes); + let context = self.context(t.supers); + let before = self.before(t.name.region.start); + let params = self.params(t.params); + let methods = join( + t.methods.iter().map(|m| { + let before = self.before(m.name.region.start); + let annotation = self.annotation(m.annotation); + let default = m + .default + .map_or_else(|| text(""), |d| cat([Doc::Hard, self.def(d)])); + cat([before, text(m.name.value), text(" : "), annotation, default]) + }), + cat([Doc::Hard, Doc::Hard]), + ); + cat([ + docs, + attributes, + before, + text("trait "), + context, + text(t.name.value), + text(" "), + params, + text(" where"), + cat([Doc::Hard, methods]).nest(), + ]) + } + fn impl_(&mut self, i: &Impl<'_>) -> Doc { + let docs = self.documented(i.docs); + let attributes = self.attributes(i.attributes); + let context = self.context(i.context); + let head = self.constraint(i.head); + let methods = join( + i.methods.iter().map(|d| self.def(d)), + cat([Doc::Hard, Doc::Hard]), + ); + cat([ + docs, + attributes, + text("impl "), + context, + head, + text(" where"), + cat([Doc::Hard, methods]).nest(), + ]) + } + fn tests(&mut self, tests: &Tests<'_>) -> Doc { + let mut docs = Vec::new(); + for import in tests.imports { + docs.push(self.import(import)); + } + for located in tests.tests { + let before = self.before(located.region.start); + let test = &located.value; + let kind = if matches!(test.body, TestBody::Prop { .. }) { + "prop" + } else { + "test" + }; + let expectation = match test.expect { + Expect::Pass => "", + Expect::Fail => " fail", + Expect::FailOnce => " fail once", + }; + let budget = match test.budget { + None => String::new(), + Some(Budget::Cpu(n)) => format!(" within (cpu {n})"), + Some(Budget::Mem(n)) => format!(" within (mem {n})"), + Some(Budget::Both { cpu, mem }) => format!(" within (cpu {cpu}, mem {mem})"), + }; + let body = match &test.body { + TestBody::Unit(b) => self.block(b.stmts, b.last), + TestBody::Prop { binders, body } => { + let binders = join( + binders.iter().map(|b| { + let pattern = self.pattern(b.value.pattern, 0); + let generator = self.expr(b.value.generator, 0); + cat([pattern, text(" via "), generator]) + }), + Doc::Hard, + ); + cat([ + text("let"), + cat([Doc::Hard, binders]).nest(), + Doc::Hard, + text("in"), + cat([Doc::Hard, self.block(body.stmts, body.last)]).nest(), + ]) + } + }; + docs.push(cat([ + before, + text(format!("{kind} ")), + self.literal(test.name.region), + text(format!("{expectation}{budget} =")), + cat([Doc::Hard, body]).nest(), + ])); + } + cat([ + text("tests"), + cat([Doc::Hard, join(docs, cat([Doc::Hard, Doc::Hard]))]).nest(), + ]) + } + pub fn module(&mut self, module: &Module<'_>) -> Doc { + enum Decl<'a> { + Value(&'a Value<'a>), + Union(&'a Union<'a>), + Alias(&'a Alias<'a>), + Trait(&'a Trait<'a>), + Impl(&'a Impl<'a>), + Infix(&'a Infix<'a>), + } + let mut chunks = Vec::new(); + if let Some(name) = module.name { + let before = self.before(name.region.start); + let exports = + self.exposing(&module.exports.value, self.multiline(module.exports.region)); + chunks.push(cat([ + before, + text(if matches!(module.kind, ModuleKind::Validator(_)) { + "validator module " + } else { + "module " + }), + text(name.value), + text(" exposing "), + exports, + ])); + } + if let Docs::YesDocs { overview, .. } = module.docs { + chunks.push(cat([ + self.before(overview.region.start), + text(self.raw(overview.region)), + ])); + } + if !module.imports.is_empty() { + chunks.push(join( + module.imports.iter().map(|i| self.import(i)), + Doc::Hard, + )); + } + let mut declarations = Vec::new(); + declarations.extend( + module + .values + .iter() + .map(|d| (d.region.start, Decl::Value(&d.value))), + ); + declarations.extend( + module + .unions + .iter() + .map(|d| (d.region.start, Decl::Union(&d.value))), + ); + declarations.extend( + module + .aliases + .iter() + .map(|d| (d.region.start, Decl::Alias(&d.value))), + ); + declarations.extend( + module + .traits + .iter() + .map(|d| (d.region.start, Decl::Trait(&d.value))), + ); + declarations.extend( + module + .impls + .iter() + .map(|d| (d.region.start, Decl::Impl(&d.value))), + ); + declarations.extend( + module + .binops + .iter() + .map(|d| (d.region.start, Decl::Infix(&d.value))), + ); + declarations.sort_by_key(|(p, _)| (p.line, p.column)); + for (position, decl) in declarations { + let before = self.before(position); + let doc = match decl { + Decl::Value(v) => { + let docs = self.documented(v.docs); + let attributes = self.attributes(v.attributes); + cat([ + docs, + attributes, + self.definition(v.name, v.arguments, v.body, v.annotation), + ]) + } + Decl::Union(u) => self.union(u), + Decl::Alias(a) => self.alias(a), + Decl::Trait(t) => self.trait_(t), + Decl::Impl(i) => self.impl_(i), + Decl::Infix(i) => text(format!( + "infix {} {} ({}) = {}", + match i.associativity { + Associativity::Left => "left", + Associativity::Right => "right", + Associativity::None => "non", + }, + i.precedence.0, + i.op, + i.name + )), + }; + chunks.push(cat([before, doc])); + } + if let Some(tests) = module.tests { + chunks.push(self.tests(tests)); + } + let trailing = self.before(Position::new(usize::MAX, usize::MAX)); + if chunks.is_empty() { + return trailing; + } + cat([ + join(chunks, cat([Doc::Hard, Doc::Hard, Doc::Hard])), + Doc::Hard, + trailing, + ]) + } +} diff --git a/crates/nash-fmt/src/doc.rs b/crates/nash-fmt/src/doc.rs new file mode 100644 index 00000000..d935e2d9 --- /dev/null +++ b/crates/nash-fmt/src/doc.rs @@ -0,0 +1,111 @@ +//! Small document algebra: groups flatten when they fit; hard lines never flatten. +#[derive(Clone)] +pub(crate) enum Doc { + Text(String), + Suffix(String), + Line(&'static str), + Hard, + Cat(Vec), + Nest(Box), + Group(Box), +} +use Doc::*; +pub(crate) fn text(value: impl ToString) -> Doc { + Text(value.to_string()) +} +pub(crate) fn cat(docs: impl IntoIterator) -> Doc { + Cat(docs.into_iter().collect()) +} +pub(crate) fn join(docs: impl IntoIterator, separator: Doc) -> Doc { + let mut out = Vec::new(); + for doc in docs { + if !out.is_empty() { + out.push(separator.clone()); + } + out.push(doc); + } + Cat(out) +} +impl Doc { + pub(crate) fn nest(self) -> Self { + Nest(Box::new(self)) + } + pub(crate) fn group(self) -> Self { + Group(Box::new(self)) + } + fn width(&self) -> Option { + match self { + Text(s) if !s.contains('\n') => Some(s.chars().count()), + Text(_) | Suffix(_) | Hard => None, + Line(s) => Some(s.len()), + Nest(d) | Group(d) => d.width(), + Cat(ds) => ds.iter().try_fold(0usize, |n, d| n.checked_add(d.width()?)), + } + } + pub(crate) fn render(&self, width: usize) -> String { + struct Output { + text: String, + column: usize, + pending: Option, + width: usize, + suffix: String, + } + fn write(d: &Doc, indent: usize, flat: bool, out: &mut Output) { + match d { + Text(s) => { + if s.is_empty() { + return; + } + if let Some(n) = out.pending.take() { + out.text.extend(std::iter::repeat_n(' ', n)); + out.column = n; + } + out.text.push_str(s); + out.column = s + .rsplit_once('\n') + .map_or(out.column + s.chars().count(), |(_, last)| { + last.chars().count() + }); + } + Suffix(s) => out.suffix.push_str(s), + Line(s) if flat => write(&text(s), indent, flat, out), + Line(_) | Hard => { + out.text.push_str(&std::mem::take(&mut out.suffix)); + out.text.push('\n'); + out.pending = Some(indent); + out.column = indent; + } + Cat(ds) => { + for d in ds { + write(d, indent, flat, out); + } + } + Nest(d) => write(d, indent + 4, flat, out), + Group(d) => write( + d, + indent, + flat || d + .width() + .is_some_and(|n| n <= out.width.saturating_sub(out.column)), + out, + ), + } + } + let mut out = Output { + text: String::new(), + column: 0, + pending: None, + width, + suffix: String::new(), + }; + write(self, 0, false, &mut out); + out.text.push_str(&out.suffix); + while out.text.ends_with('\n') { + out.text.pop(); + } + if !out.text.is_empty() { + out.text.push('\n'); + } + out.text + } +} diff --git a/crates/nash-fmt/src/expr.rs b/crates/nash-fmt/src/expr.rs new file mode 100644 index 00000000..8229c45c --- /dev/null +++ b/crates/nash-fmt/src/expr.rs @@ -0,0 +1,378 @@ +use crate::{ + doc::{Doc, cat, join, text}, + printer::Printer, +}; +use nash_region::Located; +use nash_source::*; + +impl Printer<'_> { + pub fn pattern(&mut self, located: &Located>, context: u8) -> Doc { + let leading = self.before(located.region.start); + let (doc, precedence) = match &located.value { + Pattern::Anything => (text("_"), 3), + Pattern::Var(name) => (text(name), 3), + Pattern::Unit => (text("()"), 3), + Pattern::Str(_) | Pattern::Bytes(_) | Pattern::Int(_) => { + (self.literal(located.region), 3) + } + Pattern::Alias { pattern, name } => { + let inner = self.pattern(pattern, 1); + (cat([inner, text(" as "), text(name.value)]), 0) + } + Pattern::Cons { head, tail } => { + let head = self.pattern(head, 2); + let tail = self.pattern(tail, 1); + (cat([head, text(" :: "), tail]), 1) + } + Pattern::Ctor { name, args, .. } | Pattern::CtorQual { name, args, .. } => { + let name = if let Pattern::CtorQual { module, .. } = &located.value { + format!("{module}.{name}") + } else { + name.to_string() + }; + let docs = args.iter().map(|a| self.pattern(a, 3)).collect(); + ( + self.args(text(name), docs, false), + if args.is_empty() { 3 } else { 2 }, + ) + } + Pattern::Record(fields) => { + let docs = fields + .iter() + .map(|f| cat([self.before(f.region.start), text(f.value)])) + .collect(); + (self.collection_at("{", "}", docs, located.region), 3) + } + Pattern::List(items) => { + let docs = items.iter().map(|p| self.pattern(p, 0)).collect(); + (self.collection_at("[", "]", docs, located.region), 3) + } + Pattern::Pair { first, second } => { + let first = self.pattern(first, 0); + let second = self.pattern(second, 0); + ( + cat([text("pair("), first, text(", "), second, text(")")]), + 3, + ) + } + Pattern::Tuple { + first, + second, + rest, + } => { + let docs = [*first, *second] + .into_iter() + .chain(rest.iter().copied()) + .map(|p| self.pattern(p, 0)) + .collect(); + (self.collection_at("(", ")", docs, located.region), 3) + } + }; + let trailing = self.trailing(located.region.end); + cat([ + leading, + if precedence < context { + self.parens(doc) + } else { + doc + }, + trailing, + ]) + } + pub fn expr(&mut self, located: &Located>, context: u8) -> Doc { + let leading = self.before(located.region.start); + let (doc, precedence) = match &located.value { + Expr::Str(_) | Expr::Bytes(_) | Expr::Int(_) => (self.literal(located.region), 4), + Expr::Unit => (text("()"), 4), + Expr::Var { name, .. } => (text(name), 4), + Expr::VarQual { module, name, .. } => (text(format!("{module}.{name}")), 4), + Expr::Op(op) => (text(format!("({op})")), 4), + Expr::Accessor(field) => (text(format!(".{field}")), 4), + Expr::Access { record, field } => { + let record = self.expr(record, 4); + (cat([record, text("."), text(field.value)]), 4) + } + Expr::Negate(value) => { + let value = self.expr(value, 3); + (cat([text("-"), value]), 2) + } + Expr::List(items) => { + let docs = items.iter().map(|e| self.expr(e, 0)).collect(); + (self.collection_at("[", "]", docs, located.region), 4) + } + Expr::Tuple { + first, + second, + rest, + } => { + let docs = [*first, *second] + .into_iter() + .chain(rest.iter().copied()) + .map(|e| self.expr(e, 0)) + .collect(); + (self.collection_at("(", ")", docs, located.region), 4) + } + Expr::Record { fields, grouped } => { + let docs = self.fields(fields); + let doc = self.collection_at("{", "}", docs, located.region); + (if *grouped { self.parens(doc) } else { doc }, 4) + } + Expr::Update { record, fields } => { + let docs = self.fields(fields); + ( + self.collection_at( + &format!("{{ {} |", record.value), + "}", + docs, + located.region, + ), + 4, + ) + } + Expr::Call { + function, + arguments, + } => { + let function = self.expr(function, 3); + let args = arguments.iter().map(|e| self.expr(e, 3)).collect(); + (self.args(function, args, self.multiline(located.region)), 2) + } + Expr::MacroCall { name, module, args } => { + let head = module + .map_or_else(|| name.value.to_string(), |m| format!("{m}.{}", name.value)); + let args = args.iter().map(|e| self.expr(e, 0)).collect(); + ( + self.collection( + &format!("{head}!("), + ")", + args, + self.multiline(located.region), + ), + 4, + ) + } + Expr::LeftSection { left, operator } => { + let left = self.expr(left, 0); + (cat([text("("), left, text(format!(" {operator})"))]), 4) + } + Expr::RightSection { operator, right } => { + let right = self.expr(right, 0); + (cat([text(format!("({operator} ")), right, text(")")]), 4) + } + Expr::BinOps { operands, last } => { + let mut docs = Vec::new(); + let broken = self.multiline(located.region); + for operand in *operands { + docs.push(self.expr(operand.expr, 2)); + let line = if broken { Doc::Hard } else { Doc::Line(" ") }; + if matches!(operand.op.value, "|>" | "<|") { + docs.push(line); + docs.push(text(operand.op.value)); + docs.push(text(" ")); + } else { + docs.push(text(" ")); + docs.push(text(operand.op.value)); + docs.push(line); + } + } + docs.push(self.expr(last, 2)); + (cat([docs.remove(0), cat(docs).nest()]).group(), 1) + } + Expr::Lambda { parameters, body } => { + let args = join(parameters.iter().map(|p| self.pattern(p, 3)), text(" ")); + let body = self.expr(body, 0); + ( + cat([ + text("\\"), + args, + text(" ->"), + cat([Doc::Line(" "), body]).nest(), + ]) + .group(), + 0, + ) + } + Expr::If { + branches, + final_else, + } => { + let mut docs = Vec::new(); + for (i, b) in branches.iter().enumerate() { + if i > 0 { + docs.extend([Doc::Hard, text("else ")]); + } + docs.extend([ + text("if "), + self.expr(b.condition, 0), + text(" then"), + cat([Doc::Hard, self.expr(b.then_branch, 0)]).nest(), + ]); + } + docs.extend([ + Doc::Hard, + text("else"), + cat([Doc::Hard, self.expr(final_else, 0)]).nest(), + ]); + (cat(docs), 0) + } + Expr::Let { defs, body } => { + let defs = join( + defs.iter().map(|d| self.def(d)), + cat([Doc::Hard, Doc::Hard]), + ); + let body = self.expr(body, 0); + ( + cat([ + text("let"), + cat([Doc::Hard, defs]).nest(), + Doc::Hard, + text("in"), + cat([Doc::Hard, body]).nest(), + ]), + 0, + ) + } + Expr::Case { scrutinee, arms } => { + let scrutinee = self.expr(scrutinee, 0); + let arms = join( + arms.iter().map(|a| { + let pattern = self.pattern(a.pattern, 0).nest(); + let body = self.expr(a.body, 0); + cat([pattern, text(" ->"), cat([Doc::Hard, body]).nest()]) + }), + cat([Doc::Hard, Doc::Hard]), + ); + ( + cat([ + text("case "), + scrutinee, + text(" of"), + cat([Doc::Hard, arms]).nest(), + ]), + 0, + ) + } + Expr::Do { stmts, last } => (self.block(stmts, last), 0), + Expr::Assert(value) | Expr::Comptime(value) => { + let keyword = if matches!(located.value, Expr::Assert(_)) { + "assert" + } else { + "comptime" + }; + let value = self.expr(value, 3); + (self.args(text(keyword), vec![value], false), 0) + } + Expr::Fail(value) | Expr::Todo(value) => { + let keyword = if matches!(located.value, Expr::Fail(_)) { + "fail" + } else { + "todo" + }; + let args = value.iter().map(|e| self.expr(e, 3)).collect(); + (self.args(text(keyword), args, false), 0) + } + Expr::Trace { message, body } => { + let message = self.expr(message, 3); + let body = self.expr(body, 0); + ( + cat([text("trace "), message, cat([Doc::Hard, body]).nest()]), + 0, + ) + } + }; + let trailing = self.trailing(located.region.end); + cat([ + leading, + if precedence < context { + self.parens(doc) + } else { + doc + }, + trailing, + ]) + } + fn fields(&mut self, fields: &[&FieldAssign<'_>]) -> Vec { + fields + .iter() + .map(|f| { + let before = self.before(f.field.region.start); + let value = self.expr(f.value, 3); + cat([ + before, + text(f.field.value), + text(" ="), + cat([Doc::Line(" "), value]).nest(), + ]) + .group() + }) + .collect() + } + pub fn definition( + &mut self, + name: &Located<&str>, + args: &[&Located>], + body: &Located>, + annotation: Option<&Annotation<'_>>, + ) -> Doc { + let before = self.before(name.region.start); + let annotation = annotation.map_or_else( + || text(""), + |a| cat([text(name.value), text(" : "), self.annotation(a), Doc::Hard]), + ); + let args = args.iter().map(|p| self.pattern(p, 3)).collect(); + let head = self.args(text(name.value), args, false); + let body_doc = self.expr(body, 0); + let line = if body.region.start.line > name.region.start.line { + Doc::Hard + } else { + Doc::Line(" ") + }; + cat([ + before, + annotation, + head, + text(" ="), + cat([line, body_doc]).nest(), + ]) + .group() + } + pub fn def(&mut self, located: &Located>) -> Doc { + match &located.value { + Def::Define { + name, + args, + body, + annotation, + } => self.definition(name, args, body, *annotation), + Def::Destruct { pattern, body } => { + let pattern = self.pattern(pattern, 0).nest(); + let body = self.expr(body, 0); + cat([pattern, text(" ="), cat([Doc::Line(" "), body]).nest()]).group() + } + } + } + pub fn block(&mut self, stmts: &[&Located>], last: &Located>) -> Doc { + let mut docs = Vec::new(); + for stmt in stmts { + let before = self.before(stmt.region.start); + let doc = match &stmt.value { + Stmt::Let(defs) => { + let defs = join( + defs.iter().map(|d| self.def(d)), + cat([Doc::Hard, Doc::Hard]), + ); + cat([text("let"), cat([Doc::Hard, defs]).nest()]) + } + Stmt::Bind { pattern, expr } => { + let pattern = self.pattern(pattern, 0).nest(); + let expr = self.expr(expr, 0); + cat([pattern, text(" <-"), cat([Doc::Line(" "), expr]).nest()]).group() + } + Stmt::Expr(expr) => self.expr(expr, 0), + }; + docs.push(cat([before, doc])); + } + docs.push(self.expr(last, 0)); + cat([text("do"), cat([Doc::Hard, join(docs, Doc::Hard)]).nest()]) + } +} diff --git a/crates/nash-fmt/src/lib.rs b/crates/nash-fmt/src/lib.rs new file mode 100644 index 00000000..efab6c28 --- /dev/null +++ b/crates/nash-fmt/src/lib.rs @@ -0,0 +1,24 @@ +//! Formats surface syntax without loading dependencies or expanding macros. +mod declarations; +mod doc; +mod expr; +mod printer; +#[cfg(test)] +mod tests; +mod types; + +/// Format one complete module at 80 columns with four-space indentation. +/// Parse errors use Nash's normal diagnostic model. +pub fn format(source: &str) -> Result> { + let arena = bumpalo::Bump::new(); + let mut parser = nash_parse::Parser::new(&arena, source); + let module = parser.module().map_err(|error| { + Box::new(nash_report::syntax::to_report( + &nash_report::Source::new(source), + &nash_parse::error::Error::ParseError(&error), + )) + })?; + Ok(printer::Printer::new(source, &module) + .module(&module) + .render(80)) +} diff --git a/crates/nash-fmt/src/printer.rs b/crates/nash-fmt/src/printer.rs new file mode 100644 index 00000000..1ba10a74 --- /dev/null +++ b/crates/nash-fmt/src/printer.rs @@ -0,0 +1,139 @@ +use crate::doc::{Doc, cat, join, text}; +use nash_region::{Position, Region}; +use nash_source::{Comment, CommentKind, Module, SourceComment}; + +pub(crate) struct Printer<'a> { + pub source: &'a str, + lines: Vec, + comments: &'a [&'a SourceComment<'a>], + cursor: usize, +} +impl<'a> Printer<'a> { + pub fn new(source: &'a str, module: &'a Module<'a>) -> Self { + let mut lines = vec![0]; + lines.extend(source.match_indices('\n').map(|(i, _)| i + 1)); + Self { + source, + lines, + comments: module.comments, + cursor: 0, + } + } + pub fn offset(&self, p: Position) -> usize { + self.lines + .get(p.line - 1) + .copied() + .unwrap_or(self.source.len()) + + p.column + - 1 + } + pub fn raw(&self, r: Region) -> &str { + &self.source[self.offset(r.start)..self.offset(r.end)] + } + pub fn multiline(&self, r: Region) -> bool { + r.start.line != r.end.line + } + pub fn before(&mut self, p: Position) -> Doc { + let mut docs = Vec::new(); + while let Some(c) = self.comments.get(self.cursor) { + if (c.region.start.line, c.region.start.column) >= (p.line, p.column) { + break; + } + docs.push(text(match c.kind { + CommentKind::Line => format!("--{}", c.text), + CommentKind::Block => format!("{{-{}-}}", c.text), + })); + docs.push(Doc::Hard); + self.cursor += 1; + } + cat(docs) + } + pub fn trailing(&mut self, end: Position) -> Doc { + let Some(comment) = self.comments.get(self.cursor) else { + return text(""); + }; + if comment.region.start.line != end.line || comment.region.start < end { + return text(""); + } + let gap = &self.source[self.offset(end)..self.offset(comment.region.start)]; + if !gap.chars().all(|c| c.is_whitespace() || c == ',') { + return text(""); + } + let raw = self.raw(comment.region).to_string(); + self.cursor += 1; + Doc::Suffix(format!(" {raw}")) + } + pub fn documented(&mut self, doc: Option<&Comment<'_>>) -> Doc { + doc.map_or_else( + || text(""), + |d| { + cat([ + self.before(d.region.start), + text(self.raw(d.region)), + Doc::Hard, + ]) + }, + ) + } + pub fn literal(&self, r: Region) -> Doc { + text(self.raw(r)) + } + pub fn parens(&self, doc: Doc) -> Doc { + cat([ + text("("), + cat([Doc::Line(""), doc]).nest(), + Doc::Line(""), + text(")"), + ]) + .group() + } + pub fn collection(&self, open: &str, close: &str, docs: Vec, broken: bool) -> Doc { + Self::collection_with_comments(open, close, docs, broken, text("")) + } + fn collection_with_comments( + open: &str, + close: &str, + docs: Vec, + broken: bool, + trailing: Doc, + ) -> Doc { + if docs.is_empty() && matches!(&trailing,Doc::Text(s) if s.is_empty()) { + return text(format!("{open}{close}")); + } + let opening_space = if docs.is_empty() { "" } else { " " }; + let separator = cat([if broken { Doc::Hard } else { Doc::Line("") }, text(", ")]); + cat([ + text(open), + text(opening_space), + join(docs, separator), + if broken { Doc::Hard } else { Doc::Line(" ") }, + trailing, + text(close), + ]) + .group() + } + pub fn collection_at( + &mut self, + open: &str, + close: &str, + docs: Vec, + region: Region, + ) -> Doc { + let trailing = self.before(region.end); + let has_comments = !matches!(&trailing,Doc::Cat(ds) if ds.is_empty()); + Self::collection_with_comments( + open, + close, + docs, + self.multiline(region) || has_comments, + if has_comments { trailing } else { text("") }, + ) + } + pub fn args(&self, head: Doc, args: Vec, broken: bool) -> Doc { + if args.is_empty() { + return head; + } + let line = if broken { Doc::Hard } else { Doc::Line(" ") }; + cat([head, cat([line.clone(), join(args, line)]).nest()]).group() + } +} diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap new file mode 100644 index 00000000..3aee54ef --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "main = do\n if condition then\n first\n else\n second\n let\n x = 1\n in\n pure x\n [ 1\n , 2\n ]\n ( 1\n , 2\n )\n { field = 1\n }\n [\n ]\n {\n }\n (\n )\n next\n" +--- +main = + do + if condition then + first + else + second + let + x = 1 + in + pure x + [ 1 + , 2 + ] + ( 1 + , 2 + ) + { field = 1 + } + [] + {} + () + next diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_only.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_only.snap new file mode 100644 index 00000000..4678d715 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "-- hello\n" +--- + +-- hello diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_whitespace.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_whitespace.snap new file mode 100644 index 00000000..ac85dbfd --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comment_whitespace.snap @@ -0,0 +1,7 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "-- leading \r\nx = 1 -- trailing \r\n" +--- +-- leading +x = + 1 -- trailing diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments.snap new file mode 100644 index 00000000..9c8eb550 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "-- before\nmodule Main exposing (..)\n{-| Main docs -}\nimport Foo\n-- value comment\n{-| Value docs -}\nx = [ 1, -- first\n 2 -- second\n ]\n-- end\n" +--- +-- before +module Main exposing (..) + + +{-| Main docs -} + + +import Foo + + +-- value comment +{-| Value docs -} +x = + [ 1 -- first + , 2 -- second + ] +-- end diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap new file mode 100644 index 00000000..c5f3f07e --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap @@ -0,0 +1,30 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (x, -- export\n y)\nimport Foo exposing (a, -- import\n b)\nx = f -- argument\n 1\ny = { a = -- field\n 2, b = [ -- empty\n ] }\n" +--- +module Main exposing ( x + , -- export + y + ) + + +import Foo exposing ( a + , -- import + b + ) + + +x = + f -- argument + 1 + + +y = + { a = + -- field + 2 + , b = + [ + -- empty + ] + } diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__crlf_diff.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__crlf_diff.snap new file mode 100644 index 00000000..cf5e0dc7 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__crlf_diff.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "x = 1\r\n" +--- +nash::format + + × This file needs formatting. + │ + │ ╭─[Main.nash:1] + │ 1 │- x = 1 ␍ + │ 1 │+ x = 1 + │ ╰──── + help: Run `nash format` to apply formatting. diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap new file mode 100644 index 00000000..f4fb9f01 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap @@ -0,0 +1,31 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (Thing(..), Value, run)\nimport Foo as F exposing (item)\ntype Thing 'a = First 'a | Second\ntype alias Value = { first : int, second : string }\ntrait Eq 'a => Test 'a where\n test : 'a -> bool\n test _ = True\nimpl Test int where\n test n = n == 0\nrun : int -> int\nrun n = n\n" +--- +module Main exposing ( Thing(..), Value, run ) + + +import Foo as F exposing ( item ) + + +type Thing 'a + = First 'a + | Second + + +type alias Value = + { first : int, second : string } + + +trait Eq 'a => Test 'a where + test : 'a -> bool + test _ = True + + +impl Test int where + test n = n == 0 + + +run : int -> int +run n = + n diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap new file mode 100644 index 00000000..c028e449 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap @@ -0,0 +1,34 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "a = do\n x <- read\n let y = x + 1 in pure y\nb = do\n if condition then first else second\nc = do\n trace \"message\" result\nd = do\n [ firstLongIdentifier, secondLongIdentifier, thirdLongIdentifier, fourthLongIdentifier ]\n" +--- +a = + do + x <- read + let + y = x + 1 + in + pure y + + +b = + do + if condition then + first + else + second + + +c = + do + trace "message" + result + + +d = + do + [ firstLongIdentifier + , secondLongIdentifier + , thirdLongIdentifier + , fourthLongIdentifier + ] diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap new file mode 100644 index 00000000..a8006e70 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "main = do\n a +\n b\n source\n |> transform\n |> consume\n next\n" +--- +main = + do + a + + b + source + |> transform + |> consume + next diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__documented_declarations.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__documented_declarations.snap new file mode 100644 index 00000000..a8819eab --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__documented_declarations.snap @@ -0,0 +1,35 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (..)\n{-| Overview\n@docs thing, run\n-}\n{-| A type. -}\ntype thing = Thing\n\n{-| An alias. -}\ntype alias other = int\n\n{-| A trait. -}\ntrait Trait 'a where\n method : 'a -> int\n\n{-| An implementation. -}\nimpl Trait int where\n method x = x\n\n{-| A function. -}\n@inline\nrun x = x\n" +--- +module Main exposing (..) + + +{-| Overview +@docs thing, run +-} + + +{-| A type. -} +type thing + = Thing + + +{-| An alias. -} +type alias other = + int + + +{-| A trait. -} +trait Trait 'a where + method : 'a -> int + + +{-| An implementation. -} +impl Trait int where + method x = x + + +{-| A function. -} +@inline +run x = x diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__expressions.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__expressions.snap new file mode 100644 index 00000000..20108769 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__expressions.snap @@ -0,0 +1,33 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (..)\nf x = if x then [1,2] else [3]\ng x = let a = x in case a of\n Some n -> n\n _ -> 0\nrecord = { a=1,b=2 }\ntuple = (1, \"hello\", 0xff)\napply = List.map (\\x -> x + 1) [1,2]\n" +--- +module Main exposing (..) + + +f x = + if x then + [ 1, 2 ] + else + [ 3 ] + + +g x = + let + a = x + in + case a of + Some n -> + n + + _ -> + 0 + + +record = { a = 1, b = 2 } + + +tuple = ( 1, "hello", 0xff ) + + +apply = List.map (\x -> x + 1) [ 1, 2 ] diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__formatting_diff.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__formatting_diff.snap new file mode 100644 index 00000000..24e708db --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__formatting_diff.snap @@ -0,0 +1,21 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (..)\n\nx= 1 \ny =2" +--- +nash::format + + × This file needs formatting. + │ + │ ╭─[src/Main.nash:1] + │ 1 1 │ module Main exposing (..) + │ 2 2 │ + │ 3 │- x= 1·· + │ 4 │- y =2 + │ │ No newline at end of file + │ 3 │+ + │ 4 │+ x = 1 + │ 5 │+ + │ 6 │+ + │ 7 │+ y = 2 + │ ╰──── + help: Run `nash format` to apply formatting. diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap new file mode 100644 index 00000000..d7607db8 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap @@ -0,0 +1,23 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "@inline\nf x = trace \"read\" (assert (x > 0))\ng = comptime (1 + 2)\nh = Array.fromList!([1,2])\ni = do\n x <- read\n let y = x + 1\n write y\n pure y\n" +--- +@inline +f x = + trace "read" + assert (x > 0) + + +g = comptime (1 + 2) + + +h = Array.fromList!( [ 1, 2 ] ) + + +i = + do + x <- read + let + y = x + 1 + write y + pure y diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__literal_patterns.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__literal_patterns.snap new file mode 100644 index 00000000..f789ad55 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__literal_patterns.snap @@ -0,0 +1,29 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "f x = case x of\n 42 -> True\n _ -> fail\ng x = case x of\n \"hello\" -> 1\n _ -> 2\nh x = case x of\n #\"00\" -> ()\n _ -> ()\n" +--- +f x = + case x of + 42 -> + True + + _ -> + fail + + +g x = + case x of + "hello" -> + 1 + + _ -> + 2 + + +h x = + case x of + #"00" -> + () + + _ -> + () diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__literals.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__literals.snap new file mode 100644 index 00000000..89e2d71a --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__literals.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "a = \"héllo\\n\\\"world\\\"\"\nb = #\"deadbeef\"\nc = \"\"\"first\n second\nthird\"\"\"\nd = -42\ne = ()\nf = []\ng = {}\n" +--- +a = "héllo\n\"world\"" + + +b = #"deadbeef" + + +c = + """first + second +third""" + + +d = -42 + + +e = () + + +f = [] + + +g = {} diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap new file mode 100644 index 00000000..4c139be9 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap @@ -0,0 +1,32 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module VeryLongModuleName exposing (firstFunction, secondFunction, thirdFunction, fourthFunction, fifthFunction)\nfunction firstArgument secondArgument thirdArgument = call firstArgument secondArgument thirdArgument \"a fairly long final argument\"\nvalues = [firstLongIdentifier, secondLongIdentifier, thirdLongIdentifier, fourthLongIdentifier]\nfield = { veryLongFirstFieldName = firstLongIdentifier, veryLongSecondFieldName = secondLongIdentifier }\n" +--- +module VeryLongModuleName exposing ( firstFunction + , secondFunction + , thirdFunction + , fourthFunction + , fifthFunction + ) + + +function firstArgument secondArgument thirdArgument = + call + firstArgument + secondArgument + thirdArgument + "a fairly long final argument" + + +values = + [ firstLongIdentifier + , secondLongIdentifier + , thirdLongIdentifier + , fourthLongIdentifier + ] + + +field = + { veryLongFirstFieldName = firstLongIdentifier + , veryLongSecondFieldName = secondLongIdentifier + } diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap new file mode 100644 index 00000000..0eca4867 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "x : ( int\n , int\n )\nx = (1,2)\ntrait Example 'a where\n method : { first : 'a\n , second : 'a\n } -> int\n" +--- +x : ( int + , int + ) +x = + ( 1, 2 ) + + +trait Example 'a where + method : { first : 'a + , second : 'a + } -> + int diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_collections.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_collections.snap new file mode 100644 index 00000000..d1b4d5cf --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_collections.snap @@ -0,0 +1,28 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "list =\n [ one\n , two\n , three\n ]\nrecord =\n { first = 1\n , second = 2\n }\ntuple =\n ( 1\n , 2\n , 3\n )\npipeline value =\n value\n |> first\n |> second\n" +--- +list = + [ one + , two + , three + ] + + +record = + { first = 1 + , second = 2 + } + + +tuple = + ( 1 + , 2 + , 3 + ) + + +pipeline value = + value + |> first + |> second diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__nested_control_flow.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__nested_control_flow.snap new file mode 100644 index 00000000..bf480c70 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__nested_control_flow.snap @@ -0,0 +1,42 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "f x = if x == 1 then 2 else if x == 2 then 3 else 4\ng x = (if x then f else g) x\nh x = case x of\n Some (a :: rest) -> case a of\n Some n -> n\n None -> 0\n _ -> 0\ndestruct value = let pair(a,b) = value in a + b\n" +--- +f x = + if x == 1 then + 2 + else if x == 2 then + 3 + else + 4 + + +g x = + ( + if x then + f + else + g + ) + x + + +h x = + case x of + Some (a :: rest) -> + case a of + Some n -> + n + + None -> + 0 + + _ -> + 0 + + +destruct value = + let + pair(a, b) = value + in + a + b diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__operators.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__operators.snap new file mode 100644 index 00000000..61d07c20 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__operators.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "infix left 6 (+) = add\na = (x + y) * z\nb = f (-x) (g y) (x |> h)\nc = (f x).field\nd = (+)\ne = (1 +)\nf = (+ 1)\ng = x |> f |> h\n" +--- +infix left 6 (+) = add + + +a = (x + y) * z + + +b = f (-x) (g y) (x |> h) + + +c = (f x).field + + +d = (+) + + +e = (1 +) + + +f = (+ 1) + + +g = x |> f |> h diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__parse_error.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__parse_error.snap new file mode 100644 index 00000000..8d5bf4cb --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__parse_error.snap @@ -0,0 +1,12 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "f = [1," +--- +nash::syntax + + × Expected an expression in a list. + ╭─[Main.nash:1:8] + 1 │ f = [1, + · ─ + ╰──── + help: Add an expression or `todo` for an unfinished body. diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap new file mode 100644 index 00000000..93f5641b --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "type alias Func ('a : Little) = ('a -> int) -> (list 'a, pair int int)\nf pair(a,b) (x :: xs) { field } = (a,b,x,xs,field)\ng value = case value of\n (Some [x, y] as both) -> both\n Other.Value _ -> fail\n _ -> todo \"later\"\nqualified : (Eq 'a, Other.Show 'a) => 'a -> string\nqualified x = Other.show x\n" +--- +type alias Func ('a : Little) = + ('a -> int) -> ( list 'a, pair int int ) + + +f pair(a, b) (x :: xs) { field } = ( a, b, x, xs, field ) + + +g value = + case value of + Some [ x, y ] as both -> + both + + Other.Value _ -> + fail + + _ -> + todo "later" + + +qualified : ( Eq 'a, Other.Show 'a ) => 'a -> string +qualified x = + Other.show x diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__record_updates.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__record_updates.snap new file mode 100644 index 00000000..a2c38d15 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__record_updates.snap @@ -0,0 +1,14 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "read = .field\nchange record = { record | first = 1, second = (f record.first) }\npositional = Thing ({ first = 1 })\nlabeled = Thing { first = 1 }\n" +--- +read = .field + + +change record = { record | first = 1, second = (f record.first) } + + +positional = Thing ({ first = 1 }) + + +labeled = Thing { first = 1 } diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__semantics_sensitive_forms.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__semantics_sensitive_forms.snap new file mode 100644 index 00000000..ae3b6702 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__semantics_sensitive_forms.snap @@ -0,0 +1,26 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (type thing(..), Thing(..))\ntype thing = Small\ntype Thing = Positional ({ field : int }) | Labeled { field : int }\na = (assert x) + y\nb = (comptime f) x\nc = (fail \"reason\") + x\nd = f ({ field = 1 })\n" +--- +module Main exposing ( type thing(..), Thing(..) ) + + +type thing + = Small + + +type Thing + = Positional ({ field : int }) + | Labeled { field : int } + + +a = (assert x) + y + + +b = (comptime f) x + + +c = (fail "reason") + x + + +d = f ({ field = 1 }) diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__tests_block.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__tests_block.snap new file mode 100644 index 00000000..1da67ba7 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__tests_block.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module Main exposing (..)\ntests\n import Prop\n test \"truth\" = do\n assert True\n test \"failure\" fail within (cpu 1000, mem 50) = do\n fail\n prop \"identity\" fail once =\n let\n x via Prop.int\n y via Prop.int\n in\n do\n assert (x == y)\n" +--- +module Main exposing (..) + + +tests + import Prop + + test "truth" = + do + assert True + + test "failure" fail within (cpu 1000, mem 50) = + do + fail + + prop "identity" fail once = + let + x via Prop.int + y via Prop.int + in + do + assert (x == y) diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__underindented_do_delimiter.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__underindented_do_delimiter.snap new file mode 100644 index 00000000..d0153bab --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__underindented_do_delimiter.snap @@ -0,0 +1,17 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "main = do\n [ 1\n ]\n" +--- +nash::syntax::indentation + + × Closing `]` is underindented. + ╭─[Main.nash:3:3] + 1 │ main = do + 2 │ [ 1 + · ┬ + · ╰── opened here + 3 │ ] + · ┬ + · ╰── underindented closing delimiter + ╰──── + help: Indent `]` to continue this construct. diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__validator.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__validator.snap new file mode 100644 index 00000000..92e63ef2 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__validator.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "validator module Main exposing (main)\nmain context = ()\n" +--- +validator module Main exposing ( main ) + + +main context = () diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__values.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__values.snap new file mode 100644 index 00000000..bb30bb9c --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__values.snap @@ -0,0 +1,8 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "x=42\ny a b=a+b\n" +--- +x = 42 + + +y a b = a + b diff --git a/crates/nash-fmt/src/tests.rs b/crates/nash-fmt/src/tests.rs new file mode 100644 index 00000000..822bba75 --- /dev/null +++ b/crates/nash-fmt/src/tests.rs @@ -0,0 +1,458 @@ +mod semantic; +use indoc::indoc; +macro_rules! assert_format_snapshot { + ($source:expr) => {{ + let source=indoc!($source); + let formatted=super::format(source).unwrap_or_else(|e| panic!("input: {e:?}")); + let second=super::format(&formatted).unwrap_or_else(|e| panic!("output:\n{formatted}\n{e:?}")); + assert_eq!(formatted,second,"formatting must be idempotent"); + assert_eq!(semantic::tree(source),semantic::tree(&formatted),"formatting changed the AST"); + insta::with_settings!({description=>source,omit_expression=>true},{insta::assert_snapshot!(formatted);}); + formatted + }}; +} +#[test] +fn values() { + assert_format_snapshot!("x=42\ny a b=a+b\n"); +} +#[test] +fn expressions() { + assert_format_snapshot!( + r#" + module Main exposing (..) + f x = if x then [1,2] else [3] + g x = let a = x in case a of + Some n -> n + _ -> 0 + record = { a=1,b=2 } + tuple = (1, "hello", 0xff) + apply = List.map (\x -> x + 1) [1,2] +"# + ); +} +#[test] +fn declarations() { + assert_format_snapshot!( + r#" + module Main exposing (Thing(..), Value, run) + import Foo as F exposing (item) + type Thing 'a = First 'a | Second + type alias Value = { first : int, second : string } + trait Eq 'a => Test 'a where + test : 'a -> bool + test _ = True + impl Test int where + test n = n == 0 + run : int -> int + run n = n +"# + ); +} +#[test] +fn comments() { + assert_format_snapshot!( + r#" + -- before + module Main exposing (..) + {-| Main docs -} + import Foo + -- value comment + {-| Value docs -} + x = [ 1, -- first + 2 -- second + ] + -- end +"# + ); +} +#[test] +fn patterns_and_types() { + assert_format_snapshot!( + r#" + type alias Func ('a : Little) = ('a -> int) -> (list 'a, pair int int) + f pair(a,b) (x :: xs) { field } = (a,b,x,xs,field) + g value = case value of + (Some [x, y] as both) -> both + Other.Value _ -> fail + _ -> todo "later" + qualified : (Eq 'a, Other.Show 'a) => 'a -> string + qualified x = Other.show x +"# + ); +} +#[test] +fn operators() { + assert_format_snapshot!( + r#" + infix left 6 (+) = add + a = (x + y) * z + b = f (-x) (g y) (x |> h) + c = (f x).field + d = (+) + e = (1 +) + f = (+ 1) + g = x |> f |> h +"# + ); +} +#[test] +fn keywords_and_macros() { + assert_format_snapshot!( + r#" + @inline + f x = trace "read" (assert (x > 0)) + g = comptime (1 + 2) + h = Array.fromList!([1,2]) + i = do + x <- read + let y = x + 1 + write y + pure y +"# + ); +} +#[test] +fn tests_block() { + assert_format_snapshot!( + r#" + module Main exposing (..) + tests + import Prop + test "truth" = do + assert True + test "failure" fail within (cpu 1000, mem 50) = do + fail + prop "identity" fail once = + let + x via Prop.int + y via Prop.int + in + do + assert (x == y) +"# + ); +} +#[test] +fn base_modules() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../nash-driver/base/src"); + fn visit(path: &std::path::Path) { + for entry in std::fs::read_dir(path).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + visit(&path); + } else if path.extension().is_some_and(|e| e == "nash") { + let source = std::fs::read_to_string(&path).unwrap(); + let formatted = super::format(&source) + .unwrap_or_else(|e| panic!("{} input {e:?}", path.display())); + let second = super::format(&formatted) + .unwrap_or_else(|e| panic!("{} output:\n{formatted}\n{e:?}", path.display())); + assert_eq!(formatted, second, "{}", path.display()); + assert_eq!( + semantic::tree(&source), + semantic::tree(&formatted), + "{} semantic change", + path.display() + ); + } + } + } + visit(&root); +} +#[test] +fn formatting_diff() { + let source = "module Main exposing (..)\n\nx= 1 \ny =2"; + let formatted = super::format(source).unwrap(); + let report = nash_report::format::difference("src/Main.nash", source, &formatted).unwrap(); + insta::with_settings!({description=>source,omit_expression=>true},{insta::assert_snapshot!(nash_report::render_plain(&report,&nash_report::Source::new(source),"src/Main.nash"));}); + assert!(nash_report::format::difference("src/Main.nash", &formatted, &formatted).is_none()); +} +#[test] +fn semantics_sensitive_forms() { + assert_format_snapshot!( + r#" + module Main exposing (type thing(..), Thing(..)) + type thing = Small + type Thing = Positional ({ field : int }) | Labeled { field : int } + a = (assert x) + y + b = (comptime f) x + c = (fail "reason") + x + d = f ({ field = 1 }) +"# + ); +} +#[test] +fn literals() { + assert_format_snapshot!( + r##" + a = "héllo\n\"world\"" + b = #"deadbeef" + c = """first + second + third""" + d = -42 + e = () + f = [] + g = {} +"## + ); +} +#[test] +fn record_updates() { + assert_format_snapshot!( + r#" + read = .field + change record = { record | first = 1, second = (f record.first) } + positional = Thing ({ first = 1 }) + labeled = Thing { first = 1 } +"# + ); +} +#[test] +fn literal_patterns() { + assert_format_snapshot!( + r##" + f x = case x of + 42 -> True + _ -> fail + g x = case x of + "hello" -> 1 + _ -> 2 + h x = case x of + #"00" -> () + _ -> () +"## + ); +} +#[test] +fn multiline_collections() { + assert_format_snapshot!( + r#" + list = + [ one + , two + , three + ] + record = + { first = 1 + , second = 2 + } + tuple = + ( 1 + , 2 + , 3 + ) + pipeline value = + value + |> first + |> second +"# + ); +} +#[test] +fn long_lines() { + assert_format_snapshot!( + r#" + module VeryLongModuleName exposing (firstFunction, secondFunction, thirdFunction, fourthFunction, fifthFunction) + function firstArgument secondArgument thirdArgument = call firstArgument secondArgument thirdArgument "a fairly long final argument" + values = [firstLongIdentifier, secondLongIdentifier, thirdLongIdentifier, fourthLongIdentifier] + field = { veryLongFirstFieldName = firstLongIdentifier, veryLongSecondFieldName = secondLongIdentifier } +"# + ); +} +#[test] +fn nested_control_flow() { + assert_format_snapshot!( + r#" + f x = if x == 1 then 2 else if x == 2 then 3 else 4 + g x = (if x then f else g) x + h x = case x of + Some (a :: rest) -> case a of + Some n -> n + None -> 0 + _ -> 0 + destruct value = let pair(a,b) = value in a + b +"# + ); +} +#[test] +fn comments_in_syntax() { + assert_format_snapshot!( + r#" + module Main exposing (x, -- export + y) + import Foo exposing (a, -- import + b) + x = f -- argument + 1 + y = { a = -- field + 2, b = [ -- empty + ] } +"# + ); +} +#[test] +fn documented_declarations() { + assert_format_snapshot!( + r#" + module Main exposing (..) + {-| Overview + @docs thing, run + -} + {-| A type. -} + type thing = Thing + + {-| An alias. -} + type alias other = int + + {-| A trait. -} + trait Trait 'a where + method : 'a -> int + + {-| An implementation. -} + impl Trait int where + method x = x + + {-| A function. -} + @inline + run x = x +"# + ); +} +#[test] +fn validator() { + assert_format_snapshot!( + r#" + validator module Main exposing (main) + main context = () +"# + ); +} +#[test] +fn empty_module() { + assert_eq!(super::format("").unwrap(), ""); +} +#[test] +fn comment_only() { + assert_format_snapshot!("-- hello\n"); +} +#[test] +fn parse_error() { + let source = "f = [1,"; + let report = super::format(source).expect_err("unclosed list"); + insta::with_settings!({description=>source,omit_expression=>true},{insta::assert_snapshot!(nash_report::render_plain(&report,&nash_report::Source::new(source),"Main.nash"));}); +} +#[test] +fn documents() { + use crate::doc::{Doc, cat, text}; + let doc = cat([text("a"), cat([Doc::Line(" "), text("bbb")]).nest()]).group(); + assert_eq!(doc.render(80), "a bbb\n"); + assert_eq!(doc.render(3), "a\n bbb\n"); + let hard = cat([text("a"), Doc::Hard, text("b")]).group(); + assert_eq!(hard.render(80), "a\nb\n"); +} +#[test] +fn do_control_flow() { + assert_format_snapshot!( + r#" + a = do + x <- read + let y = x + 1 in pure y + b = do + if condition then first else second + c = do + trace "message" result + d = do + [ firstLongIdentifier, secondLongIdentifier, thirdLongIdentifier, fourthLongIdentifier ] + "# + ); +} +#[test] +fn comment_whitespace() { + assert_format_snapshot!("-- leading \r\nx = 1 -- trailing \r\n"); +} +#[test] +fn crlf_diff() { + let source = "x = 1\r\n"; + let formatted = super::format(source).unwrap(); + let report = nash_report::format::difference("Main.nash", source, &formatted).unwrap(); + insta::with_settings!({description=>source,omit_expression=>true},{insta::assert_snapshot!(nash_report::render_plain(&report,&nash_report::Source::new(source),"Main.nash"));}); +} +#[test] +fn aligned_do_statements() { + let formatted = assert_format_snapshot!( + r#" + main = do + if condition then + first + else + second + let + x = 1 + in + pure x + [ 1 + , 2 + ] + ( 1 + , 2 + ) + { field = 1 + } + [ + ] + { + } + ( + ) + next + "# + ); + let arena = bumpalo::Bump::new(); + let module = nash_parse::Parser::new(&arena, &formatted) + .module() + .unwrap(); + let nash_source::Expr::Do { stmts, last } = &module.values[0].value.body.value else { + panic!("expected do") + }; + assert_eq!(stmts.len(), 8, "aligned statements must remain separate"); + assert!(matches!( + last.value, + nash_source::Expr::Var { name: "next", .. } + )); +} + +#[test] +fn underindented_do_delimiter() { + let source = indoc!("main = do\n [ 1\n ]\n"); + let report = super::format(source).expect_err("closing below statement indentation"); + insta::with_settings!({description=>source,omit_expression=>true},{insta::assert_snapshot!(nash_report::render_plain(&report,&nash_report::Source::new(source),"Main.nash"));}); +} +#[test] +fn do_operators() { + assert_format_snapshot!( + r#" + main = do + a + + b + source + |> transform + |> consume + next + "# + ); +} +#[test] +fn multiline_annotations() { + assert_format_snapshot!( + r#" + x : ( int + , int + ) + x = (1,2) + trait Example 'a where + method : { first : 'a + , second : 'a + } -> int + "# + ); +} diff --git a/crates/nash-fmt/src/tests/semantic.rs b/crates/nash-fmt/src/tests/semantic.rs new file mode 100644 index 00000000..9b5a2e4f --- /dev/null +++ b/crates/nash-fmt/src/tests/semantic.rs @@ -0,0 +1,209 @@ +//! Structural round-trip oracle, independent of the printer. Only source positions +//! are omitted; strings, docs, comments, and representation annotations remain. +use nash_region::{Located, Region}; +use nash_source::*; +trait Semantic { + fn write(&self, out: &mut String); +} +fn token(out: &mut String, value: impl std::fmt::Debug) { + out.push_str(&format!("{value:?};")); +} +impl Semantic for &T { + fn write(&self, out: &mut String) { + (*self).write(out) + } +} +impl Semantic for [T] { + fn write(&self, out: &mut String) { + token(out, self.len()); + for v in self { + v.write(out); + } + } +} +impl Semantic for Option { + fn write(&self, out: &mut String) { + token(out, self.is_some()); + if let Some(v) = self { + v.write(out) + } + } +} +impl Semantic for Located { + fn write(&self, out: &mut String) { + self.value.write(out) + } +} +impl Semantic for (A, B) { + fn write(&self, out: &mut String) { + self.0.write(out); + self.1.write(out) + } +} +impl Semantic for Region { + fn write(&self, _: &mut String) {} +} +macro_rules! scalar {($($ty:ty),*)=>{$(impl Semantic for $ty {fn write(&self,out:&mut String){token(out,self)}})*};} +scalar!( + str, + bool, + u8, + i128, + Repr, + VarType, + Expect, + Budget, + Associativity, + Precedence, + CommentKind +); +macro_rules! structure { + ($name:ident {$($field:ident),*})=>{impl Semantic for $name<'_>{fn write(&self,out:&mut String){let Self{$($field),*}=self;token(out,stringify!($name));$($field.write(out);)*}}}; +} +macro_rules! variants { + ($name:ident {$($variant:ident $(($payload:ident))? $({$($field:ident),*})?),* $(,)?})=>{ + impl Semantic for $name<'_>{fn write(&self,out:&mut String){match self{$(Self::$variant $(($payload))? $({$($field),*})? =>{token(out,stringify!($variant));$($payload.write(out);)?$($($field.write(out);)*)?}),*}}} + }; +} +structure!(Module { + kind, + name, + exports, + docs, + comments, + imports, + values, + unions, + aliases, + traits, + impls, + tests, + binops +}); +impl Semantic for ModuleKind { + fn write(&self, out: &mut String) { + token(out, matches!(self, Self::Validator(_))); + } +} +structure!(Import { + import, + alias, + exposing +}); +structure!(Value { + docs, + name, + arguments, + body, + annotation, + attributes +}); +structure!(Attribute { name, args }); +structure!(Trait { + docs, + name, + params, + supers, + methods, + attributes +}); +structure!(TraitMethod { + name, + annotation, + default +}); +structure!(Impl { + docs, + context, + head, + methods, + attributes +}); +structure!(Tests { imports, tests }); +structure!(Test { + name, + expect, + budget, + body +}); +structure!(Block { stmts, last }); +variants!(TestBody{Unit(block),Prop{binders,body}}); +structure!(ViaBinder { pattern, generator }); +structure!(Annotation { constraints, typ }); +structure!(Constraint { + class, + module, + args +}); +structure!(Union { + docs, + name, + arguments, + ctors, + attributes +}); +structure!(Ctor { name, arguments }); +variants!(CtorArgs{Positional(args),Labeled(fields)}); +structure!(Alias { + docs, + name, + arguments, + typ, + attributes +}); +structure!(Infix { + op, + associativity, + precedence, + name +}); +variants!(Expr{ + Str(value),Bytes(value),Int(value),Assert(value),Fail(value),Todo(value), + Trace{message,body},Comptime(value),Do{stmts,last},MacroCall{name,module,args}, + LeftSection{left,operator},RightSection{operator,right},Var{kind,name},VarQual{kind,module,name}, + List(items),Op(op),Negate(value),BinOps{operands,last},Lambda{parameters,body},Call{function,arguments}, + If{branches,final_else},Let{defs,body},Case{scrutinee,arms},Accessor(field),Access{record,field}, + Update{record,fields},Record{fields,grouped},Unit,Tuple{first,second,rest} +}); +variants!(Stmt{Let(defs),Bind{pattern,expr},Expr(expr)}); +structure!(IfBranch { + condition, + then_branch +}); +structure!(BinOpOperand { expr, op }); +variants!(Def{Define{name,args,body,annotation},Destruct{pattern,body}}); +structure!(CaseArm { pattern, body }); +structure!(FieldAssign { field, value }); +variants!(Pattern{Anything,Var(name),Record(fields),Alias{pattern,name},Unit,Pair{first,second},Tuple{first,second,rest},Ctor{region,name,args},CtorQual{region,module,name,args},List(items),Cons{head,tail},Str(value),Bytes(value),Int(value)}); +variants!(Type{Repr{typ,repr},Lambda{from,to},Var(name),VarApp{region,name,args},Type{region,name,args},TypeQual{region,module,name,args},Record(fields),Unit,Tuple{first,second,rest}}); +structure!(TypeParam { name, repr }); +structure!(FieldType { field, typ }); +variants!(Docs{NoDocs(region),YesDocs{overview,comments}}); +structure!(SourceComment { region, kind, text }); +structure!(Comment { region, snippet }); +impl Semantic for Snippet<'_> { + fn write(&self, out: &mut String) { + let Self { + data, + off_row: _, + off_col: _, + } = self; + data.write(out) + } +} +variants!(Exposing{Open,Explicit(items)}); +variants!(Exposed{Lower(name),Upper{name,privacy},LowerType{name,privacy},Operator{region,op}}); +impl Semantic for Privacy { + fn write(&self, out: &mut String) { + token(out, matches!(self, Self::Public(_))); + } +} +pub fn tree(source: &str) -> String { + let arena = bumpalo::Bump::new(); + let module = nash_parse::Parser::new(&arena, source) + .module() + .expect("valid module"); + let mut out = String::new(); + module.write(&mut out); + out +} diff --git a/crates/nash-fmt/src/types.rs b/crates/nash-fmt/src/types.rs new file mode 100644 index 00000000..597d1bca --- /dev/null +++ b/crates/nash-fmt/src/types.rs @@ -0,0 +1,125 @@ +use crate::{ + doc::{Doc, cat, join, text}, + printer::Printer, +}; +use nash_region::Located; +use nash_source::*; + +impl Printer<'_> { + pub fn typ(&mut self, located: &Located>, context: u8) -> Doc { + let leading = self.before(located.region.start); + let (doc, precedence) = match &located.value { + Type::Var(name) => (text(format!("'{name}")), 3), + Type::Unit => (text("()"), 3), + Type::Repr { typ, repr } => { + let inner = self.typ(typ, 0); + ( + cat([text("("), inner, text(format!(" : {:?})", repr.value))]), + 3, + ) + } + Type::Lambda { from, to } => { + let from = self.typ(from, 1); + let to = self.typ(to, 0); + ( + cat([from, text(" ->"), cat([Doc::Line(" "), to]).nest()]).group(), + 0, + ) + } + Type::Type { name, args, .. } | Type::VarApp { name, args, .. } => { + let name = if matches!(located.value, Type::VarApp { .. }) { + format!("'{name}") + } else { + name.to_string() + }; + let docs = args.iter().map(|a| self.typ(a, 2)).collect(); + ( + self.args(text(name), docs, false), + if args.is_empty() { 3 } else { 1 }, + ) + } + Type::TypeQual { + module, name, args, .. + } => { + let docs = args.iter().map(|a| self.typ(a, 2)).collect(); + ( + self.args(text(format!("{module}.{name}")), docs, false), + if args.is_empty() { 3 } else { 1 }, + ) + } + Type::Record(fields) => { + let docs = fields + .iter() + .map(|f| { + let before = self.before(f.field.region.start); + let typ = self.typ(f.typ, 0); + cat([before, text(f.field.value), text(" : "), typ]) + }) + .collect(); + (self.collection_at("{", "}", docs, located.region), 3) + } + Type::Tuple { + first, + second, + rest, + } => { + let docs = [*first, *second] + .into_iter() + .chain(rest.iter().copied()) + .map(|t| self.typ(t, 0)) + .collect(); + (self.collection_at("(", ")", docs, located.region), 3) + } + }; + cat([ + leading, + if precedence < context { + self.parens(doc) + } else { + doc + }, + ]) + } + pub fn constraint(&mut self, c: &Located>) -> Doc { + let before = self.before(c.region.start); + let c = &c.value; + let name = c.module.map_or_else( + || c.class.value.to_string(), + |m| format!("{m}.{}", c.class.value), + ); + let args = c.args.iter().map(|a| self.typ(a, 2)).collect(); + cat([before, self.args(text(name), args, false)]) + } + pub fn context(&mut self, constraints: &[&Located>]) -> Doc { + if constraints.is_empty() { + return text(""); + } + let docs: Vec<_> = constraints.iter().map(|c| self.constraint(c)).collect(); + let doc = if docs.len() == 1 { + docs.into_iter().next().unwrap() + } else { + self.collection("(", ")", docs, false) + }; + cat([doc, text(" => ")]) + } + pub fn annotation(&mut self, annotation: &Annotation<'_>) -> Doc { + let context = self.context(annotation.constraints); + cat([context, self.typ(annotation.typ, 0)]).nest() + } + pub fn params(&mut self, params: &[&TypeParam<'_>]) -> Doc { + join( + params.iter().map(|p| { + let before = self.before(p.name.region.start); + let name = format!("'{}", p.name.value); + cat([ + before, + text( + p.repr + .map_or(name.clone(), |r| format!("({name} : {:?})", r.value)), + ), + ]) + }), + text(" "), + ) + } +} diff --git a/crates/nash-parse/src/declaration/union.rs b/crates/nash-parse/src/declaration/union.rs index be8dca92..376998e2 100644 --- a/crates/nash-parse/src/declaration/union.rs +++ b/crates/nash-parse/src/declaration/union.rs @@ -111,7 +111,7 @@ impl<'a> Parser<'a> { self.chomp(CustomType::Space)?; - let (arguments, end) = if self.peek() == Some(b'{') { + let (arguments, end) = if self.peek() == Some(b'{') && self.peek_at(1) != Some(b'-') { self.check_indent(name_end.line, name_end.column, CustomType::IndentField)?; let opening = self.get_position(); self.advance(); diff --git a/crates/nash-parse/src/expression/do_.rs b/crates/nash-parse/src/expression/do_.rs index 8a62444e..5d8b577d 100644 --- a/crates/nash-parse/src/expression/do_.rs +++ b/crates/nash-parse/src/expression/do_.rs @@ -137,7 +137,7 @@ impl<'a> Parser<'a> { )?; let has_in = self - .check_indent(defs_end.line, defs_end.column, |_, _| ()) + .check_explicit_indent(defs_end.line, defs_end.column, |_, _| ()) .is_ok() && self.keyword_in(|_, _| ()).is_ok(); let defs = self.alloc_slice_copy(&defs); diff --git a/crates/nash-parse/src/expression/if_.rs b/crates/nash-parse/src/expression/if_.rs index 8d32b91a..844c85f5 100644 --- a/crates/nash-parse/src/expression/if_.rs +++ b/crates/nash-parse/src/expression/if_.rs @@ -65,7 +65,7 @@ impl<'a> Parser<'a> { let (condition, cond_end) = self.if_condition()?; // Parse `then` - self.check_indent(cond_end.line, cond_end.column, If::IndentThen)?; + self.check_explicit_indent(cond_end.line, cond_end.column, If::IndentThen)?; self.keyword_then(If::Then)?; // Parse then branch @@ -73,7 +73,7 @@ impl<'a> Parser<'a> { let (then_branch, then_end) = self.if_then_branch()?; // Parse `else` - self.check_indent(then_end.line, then_end.column, If::IndentElse)?; + self.check_explicit_indent(then_end.line, then_end.column, If::IndentElse)?; self.keyword_else(If::Else)?; // Create the new branch diff --git a/crates/nash-parse/src/expression/keyword.rs b/crates/nash-parse/src/expression/keyword.rs index 0c305dc5..7a0af88f 100644 --- a/crates/nash-parse/src/expression/keyword.rs +++ b/crates/nash-parse/src/expression/keyword.rs @@ -84,6 +84,7 @@ impl<'a> Parser<'a> { None, )?; let end = message.map_or(keyword_end, |message| message.region.end); + parser.chomp(error::Keyword::Space)?; Ok(( parser.alloc(Located::at(Region::new(start, end), build(message))), end, diff --git a/crates/nash-parse/src/expression/let_.rs b/crates/nash-parse/src/expression/let_.rs index 876a3622..19ea5bb4 100644 --- a/crates/nash-parse/src/expression/let_.rs +++ b/crates/nash-parse/src/expression/let_.rs @@ -46,7 +46,7 @@ impl<'a> Parser<'a> { })?; // Check indent for "in" keyword - p.check_indent(defs_end.line, defs_end.column, Let::IndentIn)?; + p.check_explicit_indent(defs_end.line, defs_end.column, Let::IndentIn)?; // Parse "in" keyword p.keyword_in(Let::In)?; diff --git a/crates/nash-parse/src/expression/list.rs b/crates/nash-parse/src/expression/list.rs index 3a7a36a1..a5d5ef2f 100644 --- a/crates/nash-parse/src/expression/list.rs +++ b/crates/nash-parse/src/expression/list.rs @@ -55,7 +55,7 @@ impl<'a> Parser<'a> { let (first, end) = p.list_expr()?; // Check indent using expression's end position (not current parser position) - p.check_indent(end.line, end.column, List::IndentEnd)?; + p.check_explicit_indent(end.line, end.column, List::IndentEnd)?; // Parse remaining elements let mut elements = BumpVec::new_in(p.bump); @@ -125,7 +125,7 @@ impl<'a> Parser<'a> { elements.push(elem); // Check indent using expression's end position - p.check_indent(end.line, end.column, List::IndentEnd)?; + p.check_explicit_indent(end.line, end.column, List::IndentEnd)?; Ok(false) // Not done, continue loop }), diff --git a/crates/nash-parse/src/expression/record.rs b/crates/nash-parse/src/expression/record.rs index 169933ba..d1ed5a63 100644 --- a/crates/nash-parse/src/expression/record.rs +++ b/crates/nash-parse/src/expression/record.rs @@ -149,7 +149,7 @@ impl<'a> Parser<'a> { // Check indent after expression let (end_row, end_col) = self.position(); - self.check_indent(end_row, end_col, Record::IndentEnd)?; + self.check_explicit_indent(end_row, end_col, Record::IndentEnd)?; // Build first field let first_field = self.alloc(FieldAssign { @@ -196,7 +196,7 @@ impl<'a> Parser<'a> { // Check indent after expression let (end_row, end_col) = self.position(); - self.check_indent(end_row, end_col, Record::IndentEnd)?; + self.check_explicit_indent(end_row, end_col, Record::IndentEnd)?; Ok(self.alloc(FieldAssign { field, value })) } diff --git a/crates/nash-parse/src/expression/tuple.rs b/crates/nash-parse/src/expression/tuple.rs index 3b2e5329..99d81cc1 100644 --- a/crates/nash-parse/src/expression/tuple.rs +++ b/crates/nash-parse/src/expression/tuple.rs @@ -76,13 +76,13 @@ impl<'a> Parser<'a> { |bump, error, row, col| Tuple::Expr(bump.alloc(error), row, col), |p| p.chomp_expr_end(start, negated, vec![], neg_end), )?; - p.check_indent(end.line, end.column, Tuple::IndentEnd)?; + p.check_explicit_indent(end.line, end.column, Tuple::IndentEnd)?; return p.chomp_tuple_end(start, full); } p.chomp_and_check_indent(Tuple::Space, Tuple::IndentExpr1)?; let (right, end) = p.tuple_expr()?; - p.check_indent(end.line, end.column, Tuple::IndentEnd)?; + p.check_explicit_indent(end.line, end.column, Tuple::IndentEnd)?; p.chomp(Tuple::Space)?; p.word1(b')', Tuple::OperatorClose)?; Ok(p.add_end( @@ -99,7 +99,7 @@ impl<'a> Parser<'a> { }), Box::new(|p: &mut Parser<'a>| { let (first, end) = p.tuple_expr()?; - p.check_indent(end.line, end.column, Tuple::IndentEnd)?; + p.check_explicit_indent(end.line, end.column, Tuple::IndentEnd)?; p.chomp_tuple_end(start, first) }), ], @@ -183,7 +183,7 @@ impl<'a> Parser<'a> { rest.push(elem); // Check indent using expression's end position - p.check_indent(end.line, end.column, Tuple::IndentEnd)?; + p.check_explicit_indent(end.line, end.column, Tuple::IndentEnd)?; Ok(false) // Not done, continue loop }), diff --git a/crates/nash-parse/src/space.rs b/crates/nash-parse/src/space.rs index c037896e..9605278a 100644 --- a/crates/nash-parse/src/space.rs +++ b/crates/nash-parse/src/space.rs @@ -44,7 +44,8 @@ impl<'a> Parser<'a> { } } - /// Consume whitespace and check that we're indented past the current indent level. + /// Consume whitespace and require indentation past the current level. + /// A closing delimiter may align with the current statement. /// /// Mirrors Elm's `Space.chompAndCheckIndent`. pub fn chomp_and_check_indent( @@ -57,7 +58,11 @@ impl<'a> Parser<'a> { match status { SpaceStatus::Good => { - if new_col > self.indent && new_col > 1 { + if new_col > 1 + && (new_col > self.indent + || (new_col == self.indent + && matches!(self.peek(), Some(b')' | b']' | b'}')))) + { Ok(()) } else { let (row, col) = if matches!(self.peek(), Some(b')' | b']' | b'}')) { @@ -103,6 +108,21 @@ impl<'a> Parser<'a> { } } + /// Explicit continuation tokens may align with a statement. Ordinary + /// expression continuation stays strict so adjacent statements never merge. + pub(crate) fn check_explicit_indent( + &self, + end_row: Row, + end_col: Col, + to_error: impl FnOnce(Row, Col) -> E, + ) -> Result<(), E> { + if self.col == self.indent && self.col > 1 { + Ok(()) + } else { + self.check_indent(end_row, end_col, to_error) + } + } + /// Check that current column equals indent level (for alignment). /// /// Mirrors Elm's `Space.checkAligned`. diff --git a/crates/nash-report/Cargo.toml b/crates/nash-report/Cargo.toml index 2b0aa352..ad50be40 100644 --- a/crates/nash-report/Cargo.toml +++ b/crates/nash-report/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true license.workspace = true [dependencies] +similar = "2.7.0" miette.workspace = true unicode-width = "0.1.14" serde.workspace = true diff --git a/crates/nash-report/src/format.rs b/crates/nash-report/src/format.rs new file mode 100644 index 00000000..4544b898 --- /dev/null +++ b/crates/nash-report/src/format.rs @@ -0,0 +1,81 @@ +//! Formatting differences use the same report handler as compiler diagnostics. +use crate::{Doc, Report}; +use nash_region::Region; +use similar::{ChangeTag, TextDiff}; + +/// Return a contextual diff only when the source needs formatting. +/// TextDiff computes edits; this module owns their terminal presentation. +pub fn difference(path: &str, original: &str, formatted: &str) -> Option { + if original == formatted { + return None; + } + let diff = TextDiff::from_lines(original, formatted); + let width = original + .lines() + .count() + .max(formatted.lines().count()) + .max(1) + .to_string() + .len(); + let mut lines = vec![Doc::text("This file needs formatting."), Doc::text("")]; + for group in diff.grouped_ops(3) { + let line = group.first().map_or(1, |op| op.old_range().start + 1); + lines.push(Doc::text(format!("{:width$} {:width$} ╭─[{path}:{line}]", "", "")).cyan()); + for op in group { + for change in diff.iter_changes(&op) { + let old = change + .old_index() + .map_or(String::new(), |n| (n + 1).to_string()); + let new = change + .new_index() + .map_or(String::new(), |n| (n + 1).to_string()); + let marker = match change.tag() { + ChangeTag::Equal => ' ', + ChangeTag::Delete => '-', + ChangeTag::Insert => '+', + }; + let value = change.value().trim_end_matches(['\r', '\n']); + // Make whitespace-only edits visible without changing context lines. + let value = if change.tag() != ChangeTag::Equal { + let trimmed = value.trim_end_matches([' ', '\t']); + let trailing = value[trimmed.len()..].replace(' ', "·").replace('\t', "→"); + format!("{trimmed}{trailing}") + } else { + value.to_string() + }; + let ending = if change.tag() != ChangeTag::Equal && change.value().ends_with("\r\n") + { + " ␍" + } else { + "" + }; + let row = Doc::text(format!( + "{old:>width$} {new:>width$} │{marker} {value}{ending}" + )); + lines.push(match change.tag() { + ChangeTag::Equal => row, + ChangeTag::Delete => row.red(), + ChangeTag::Insert => row.green(), + }); + if change.missing_newline() { + lines.push(Doc::text(format!( + "{:width$} {:width$} │ No newline at end of file", + "", "" + ))); + } + } + } + lines.push(Doc::text(format!("{:width$} {:width$} ╰────", "", "")).cyan()); + } + Some( + Report::snippet( + "FORMATTING", + Region::one(), + None, + Doc::vcat(lines), + Doc::text("Run `nash format` to apply formatting."), + ) + .without_source() + .with_code("nash::format"), + ) +} diff --git a/crates/nash-report/src/lib.rs b/crates/nash-report/src/lib.rs index 523c736e..7441a984 100644 --- a/crates/nash-report/src/lib.rs +++ b/crates/nash-report/src/lib.rs @@ -9,6 +9,7 @@ pub mod canonicalize; pub mod code; pub mod doc; +pub mod format; pub mod json; pub mod localizer; pub mod pattern; diff --git a/docs/cli.md b/docs/cli.md index 2cd3e007..d9ef4c1f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ # CLI -The `nash` binary is `crates/nash-cli`. Every command loads the project from +The `nash` binary is `crates/nash-cli`. The check, build, and test commands load the project from `PATH` (default `.`) by walking up to the nearest `nash.jsonc`, exactly like `Project::load` in `crates/nash-driver/src/project.rs` does today. @@ -11,12 +11,12 @@ The `nash` binary is `crates/nash-cli`. Every command loads the project from | `nash check [PATH]` | exists | Parse, canonicalize and type check every module. Includes test blocks but does not execute them. No codegen. | | `nash build [PATH]` | exists | Exclude test blocks, check the frontend, then compile every validator module for its configured target. | | `nash test [PATH]` | exists | Check, compile and run project `test` and `prop` declarations. | -| `nash fmt [PATH...]` | planned | Format files in place, or `--check` to report unformatted files. | +| `nash format [PATH...]` | exists | Format files in place, or `--check` to show contextual formatting diffs. | | `nash docs [PATH]` | planned | Generate HTML documentation for exposed modules into `docs/`. | | `nash lsp` | exists | Language server over stdio. | | `nash init NAME` | planned | Create a project skeleton: `nash.jsonc`, `src/`, one validator module with a `tests` block. | -Aliases: `nash c` for `check`, `nash b` for `build`; `nash t` for `test`. +Aliases: `nash c` for `check`, `nash b` for `build`; `nash t` for `test`; `nash fmt` for `format`. Version proxying stays as it is: `nash` reads the `compiler` field of `nash.jsonc` and re-executes the matching downloaded compiler @@ -59,13 +59,15 @@ See [target compatibility](validators.md#target-compatibility). | `--json` | off | Write one structured result document to stdout. | | `--coverage labels\|tests` | `labels` | Denominator of the label table: total labels, or total iterations. | -`nash fmt`: +`nash format` (alias `fmt`): | Flag | Effect | |---|---| -| `--check` | Do not write. Exit `1` if any file would change. | +| `--check` | Do not write. Show a miette-styled contextual diff and exit `1` if any file would change. Clean checks are silent. | | `--stdin` | Format stdin to stdout. | +See [formatter behavior and testing](formatter.md). + `nash check`: | Flag | Effect | diff --git a/docs/formatter.md b/docs/formatter.md new file mode 100644 index 00000000..a16e1b5e --- /dev/null +++ b/docs/formatter.md @@ -0,0 +1,53 @@ +# Formatter + +`nash format [PATH...]` (`nash fmt`) formats Nash files in place. Directories +are visited recursively; the default is the current directory. The command +skips `.git`, `.jj`, `target`, `build`, and `node_modules`, and does not follow +symlinks discovered during directory traversal. Explicit file symlinks resolve +to their targets. It formats source without loading a project, imports, or Base. + +`--check` leaves files untouched and exits 1 when formatting differs or a file +cannot be parsed. A clean check is silent. Changed files have a contextual +red/green diff rendered through `nash-report` and the existing miette handler. +Diffs show old and new line numbers, visible trailing spaces/tabs, CRLF markers, +and missing final newlines. Global `--color` controls this output. + +`--stdin` formats one module from stdin to stdout. It cannot be combined with +paths or `--check`. Parse errors use normal Nash diagnostics. Files with parse +errors remain unchanged; other selected files can still be formatted. + +## Layout + +The printer targets 80 columns with four-space indentation. Literal text and +comments can exceed the target; formatting never changes their contents. +Strings, byte literals, and numeric literals retain their source spelling. +Output uses LF layout and one final newline for a nonempty module. + +- `if`, `case`, `let`, and `do` use multiline bodies. Explicit continuations + (`then`, `else`, `in`, and collection closers) may align with a `do` + statement. Adjacent statements remain separate expressions. +- Collections and applications retain an existing multiline layout; otherwise + they stay on one line when they fit. Broken collections use leading commas. +- Pipes begin continuation lines; other infix operators end their lines. +- Top-level declarations have two blank lines between them. Local declarations + and trait/implementation methods have one. +- Imports, exposing entries, declarations, and fields retain source order. + The formatter does not sort, deduplicate, inject imports, or expand macros. +- Parentheses preserve expression grouping and the distinction between labeled + constructor arguments and positional record arguments. +- Ordinary comments retain their text and order. End-of-line comments remain + line suffixes where an AST boundary identifies their owner. Other comments + precede the next source node; comments before collection closers stay inside. + Doc comments remain attached to their declarations. + +## Library and tests + +`nash-fmt::format(&str)` returns formatted source or an owned `nash-report` +diagnostic. Its document layer and printers are private to the crate. + +Formatter unit tests use a shared snapshot macro: the snapshot description is +input Nash source and the body is formatted Nash source. The same helper +reparses the output, compares the complete source AST with source coordinates +removed, and checks idempotence. All shipped Base modules also undergo these +round-trip checks. Diff and parse-error snapshots live in `nash-fmt` as well. +There are no CLI subprocess tests for formatting. diff --git a/docs/overview.md b/docs/overview.md index 8777640e..4dba0edb 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -85,7 +85,7 @@ implementation, default imports, Prop and the full validator example. | Stdlib | `nash/base` package in-repo (`crates/nash-driver/base/`), implicit default imports like Elm's `core`. `Builtin` exposes only real UPLC builtin functions. Synthetic `Primitive` owns primitive types, representation traits, constructors, and unchecked `coerce`. Base ships embedded in `nash-driver`; applications need no explicit dependency or download. Defaults affect canonical scope, not source imports. One module per type pair named by the uppercase name (`List`, `Int`, ...); helpers accept Big or little outer inputs and return little outer results, preserving payload types. No Big `String`. | | Exposing little types | `exposing (type option(..), map)` — the `type` prefix marks a lowercase type in exposing/import lists. | | Target | Plutus V3, latest builtins (`case`/`constr`, bitwise, BLS, arrays, ledger `Value`). | -| CLI v1 | `nash check`, `nash build`, `nash test`, `nash fmt`, `nash docs`, `nash lsp`. | +| CLI v1 | `nash check`, `nash build`, `nash test`, `nash format` (alias `fmt`), `nash docs`, `nash lsp`. | | Optimizations | Inline single-use lets / small lambdas; builtin force caching; DCE + unused params; case-of-known-constructor + constant folding (via CEK). | ## Kinds and representation in one page @@ -222,7 +222,7 @@ crates/ nash-codegen Can AST -> Core -> UPLC (new) nash-test test runner, generate driver, shrinker(new) nash-macro expansion loop, Ast reification (new) - nash-fmt formatter (new) + nash-fmt formatter nash-docs doc generator (new) nash-plutus UPLC runtime nash-config / nash-driver / nash-cli / nash-language-server diff --git a/plans/13-fmt-docs.md b/plans/13-fmt-docs.md index 70726327..88d5969e 100644 --- a/plans/13-fmt-docs.md +++ b/plans/13-fmt-docs.md @@ -1,16 +1,15 @@ # Plan 13: `nash-fmt` and `nash-docs` -Goal: `nash fmt` (a layout-preserving pretty printer over the surface AST, -comments included) and `nash docs` (static HTML and Markdown from +Goal: `nash format` (alias `fmt`), a layout-preserving pretty printer over +the surface AST including comments, and `nash docs` (static HTML and Markdown from `{-| -}` doc comments plus solved interfaces). -Prerequisites: plans/01 (syntax: traits, tests block, attributes, `do`, -macros) so the printer covers the whole surface AST; plans/11 chunk 12's -`nash_source::print` is the non-preserving seed this plan replaces; the -driver's interface map for `nash docs`. +Prerequisites: the surface AST and parser from plans/01, including traits, +tests, attributes, `do`, and macro calls. Documentation extraction also needs +the driver's interface map. Crates touched: `nash-source`, `nash-parse`, new `nash-fmt`, new -`nash-docs`, `nash-cli`. +`nash-docs`, `nash-cli`, and `nash-report`. References: @@ -62,202 +61,79 @@ those tasks belong to later chunks. --- -## Chunk 2: `nash-fmt` document layer +## Chunk 2: `nash-fmt` document layer — complete -**Files** - -- `crates/nash-fmt/Cargo.toml`, `src/lib.rs`, `src/doc.rs` - -**Change** - -A small Wadler/Leijen `Doc` with `group`, `nest`, `line`, `softline`, -`hardline`, `text`, rendered at width 80 with 4-space indentation. Nash -is layout-sensitive, so the renderer never joins lines that the layout -rules need separate: `let`, `case`, `if` bodies, and `do` blocks are -always broken (`hardline`), matching elm-format. - -**Code** - -```rust -pub enum Doc<'a> { - Nil, - Text(&'a str), - Line, // space when flat, newline when broken - SoftLine, // nothing when flat, newline when broken - HardLine, // always newline - Nest(u16, &'a Doc<'a>), - Group(&'a Doc<'a>), - Concat(&'a [&'a Doc<'a>]), -} - -pub struct Printer<'a> { bump: &'a Bump } - -impl<'a> Printer<'a> { - pub fn render(&self, doc: &'a Doc<'a>, width: usize) -> String -} -``` - -Rendering is the standard "fits" algorithm with a work stack; no -backtracking beyond one group. - -**Tests** - -`group(text a, line, text b)` flat at width 80, broken at width 3; -nesting under a broken group indents by 4. +Implemented in `crates/nash-fmt/src/doc.rs` with groups, four-space nesting, +soft/hard lines, and deferred line-comment suffixes. Groups stay flat when +they fit the 80-column target. Literal and comment contents are preserved. +The public entry point is `nash_fmt::format(&str)`; printer internals are private. -**Done when** the doc tests pass. +Unit tests cover flat/broken groups and nesting. Shared formatter snapshots +exercise the document layer through real Nash syntax. --- -## Chunk 3: expressions, patterns, types - -**Files** - -- `crates/nash-fmt/src/expr.rs`, `pattern.rs`, `typ.rs`, `layout.rs` - -**Change** - -Print every `Expr`, `Pattern`, and `Type` variant. Layout-preservation -rule: a node whose source region spans more than one line is printed -broken; a single-line node is printed as a `group` (flat if it fits). -That reproduces elm-format's "you chose multiline, we keep it" behaviour -without an extra AST flag: `Located.region` already says. - -```rust -// crates/nash-fmt/src/layout.rs -pub fn is_multiline(region: Region) -> bool { - region.start.line != region.end.line -} -``` - -Rules worth spelling out: - -- Calls: `f a b`; if multiline, arguments each on their own line indented. -- Binary operator chains (`BinOps`): one operand per line when broken, - operator leading (`|> f` style for `|>`, `<|`; operator trailing for - arithmetic), following elm-format. -- `if`/`case`/`let`: always broken, `let` definitions separated by a - blank line if the source had one (checked via comment/region gaps). -- Lists, records, tuples: `[ a, b ]` flat; broken form puts `, ` at line - starts (elm-format style). -- Lambdas: `\x y -> body`; body broken on its own line if multiline. -- Strings: verbatim, including multi-line `"""`. -- Attributes, `MacroCall`, `Quote`, `Splice`, `Comptime`, `Do`: printed - in their surface form; `do` blocks always broken. -- `VarGlobal` never appears in user source; `unreachable!`. - -**Elm/Aiken reference** +## Chunk 3: expressions, patterns, types — complete -elm-format's `ElmFormat/Render/Box.hs` `formatExpression` (rules only). -Aiken `crates/aiken-lang/src/format.rs` `Formatter::expr` (Rust code -with the same structure, useful for the operator-chain and `when` -layouts). +Implemented in `expr.rs` and `types.rs`. Every currently parsed source AST +variant has an explicit printer, including representation qualifiers, +operator sections, pair patterns, labeled records, attributes, macro calls, +and tests. Future quote/splice syntax awaits its own parser support. -**Tests** (`crates/nash-fmt/src/snapshots`) +- Layout-sensitive bodies use hard lines. Existing multiline collections and + applications stay multiline; other groups break when they exceed the target. +- Broken collections use leading commas; pipes lead continuation lines. +- Parentheses preserve operator grouping, nested application shape, and + positional record arguments. Record field values retain the term grouping + required by the grammar. +- Original string, bytes, and number spelling comes from the source region. -`assert_fmt_snapshot!(input)` snapshots the output; `assert_fmt_idempotent!(input)` -asserts `fmt(fmt(x)) == fmt(x)`. Inputs: every expression form in -`crates/nash-parse/src/expression/*` tests, flat and multiline, plus the -operator chains above. - -**Done when** every parser expression snapshot input round-trips -idempotently. +The shared `assert_format_snapshot!` helper stores Nash input in the snapshot +metadata and formatted Nash in the body. Every test reparses the output, +compares the source trees with coordinates removed, and checks idempotence. +All 35 shipped Base modules undergo the same structural and idempotence checks. +The formatter crate currently has 31 unit tests, including report snapshots. --- -## Chunk 4: declarations, module header, comment attachment - -**Files** - -- `crates/nash-fmt/src/module.rs`, `decl.rs`, `comments.rs` +## Chunk 4: declarations, module header, comments — complete -**Change** - -Print the module header (`module`/`validator module`, exposing list one -per line if multiline, sorted never), imports (sorted by module name, -deduplicated exposing lists), infix declarations, declarations in source -order, traits, impls, macros, and the `tests` block. Two blank lines -between top-level declarations, one inside `let`/`where`. - -Comment attachment: comments are a side table with regions. The printer -walks declarations in order and, before printing a node, emits any -comments whose region ends before the node's start and after the -previous node's end ("leading" comments); comments on the same line -after a node's end are "trailing" and printed after it with one space. -Comments inside an expression are attached to the innermost enclosing -`Located` whose region contains them, by the same leading/trailing rule. -A comment the rules cannot place (inside a token gap with no enclosing -node, e.g. between `if` and its condition) is printed as a leading -comment of the node that follows it; nothing is ever dropped, which the -idempotency tests check by counting comments before and after. - -**Code** +Implemented in `declarations.rs` and `printer.rs`. -```rust -pub struct Comments<'a> { - all: &'a [&'a SourceComment<'a>], - next: usize, -} +Headers, imports, exposing entries, infix declarations, traits, implementations, +and tests retain source order. Formatting does not sort or deduplicate imports +or inject Base/Prelude code. Declarations have two blank lines between them; +local definitions and methods have one. -impl<'a> Comments<'a> { - /// Comments strictly before `pos` that have not been emitted yet. - pub fn leading(&mut self, pos: Position) -> &'a [&'a SourceComment<'a>] - /// Comments starting on `line` after `pos`. - pub fn trailing(&mut self, pos: Position) -> Option<&'a SourceComment<'a>> -} -``` - -**Elm/Aiken reference** - -elm-format `ElmFormat/Render/Box.hs` `formatModule`, `formatComment`; -Aiken `format.rs` `Formatter::definitions` and `pop_doc_comments` for -the side-table approach. - -**Tests** +The source-ordered comment cursor emits leading comments before the next node, +retains identifiable trailing comments as line suffixes, and keeps closing +collection comments inside their container. Documentation stays attached to +its declaration. The structural comparison includes exact comment/doc contents. -- Snapshot every declaration form with leading, trailing, and inner - comments. -- `comments_preserved`: for each input, count of `--`/`{-` in output - equals count in input. -- Idempotency over the whole `crates/nash-driver/base/` tree once it exists. - -**Done when** `nash fmt --check core/` reports no changes after one -`nash fmt core/`. +Formatter fixtures also exposed two parser whitespace defects: a doc comment +after a constructor could be mistaken for labeled fields, and a `fail`/`todo` +message did not consume following whitespace. Those parser paths are corrected. Explicit continuation tokens and collection +closers can also align with a `do` statement, while ordinary application +continuation remains strictly indented. --- -## Chunk 5: `nash fmt` command - -**Files** - -- `crates/nash-cli/src/cmd/fmt.rs`, `cmd/mod.rs` - -**Change** - -`nash fmt [paths...]` formats in place; `--check` exits 1 and lists -files that would change; `--stdin` reads one module from stdin and -writes to stdout (for editors). Parse errors are reported through -`nash-report` and the file is left untouched. +## Chunk 5: `nash format` command (alias `fmt`) — complete -```rust -#[derive(clap::Args)] -pub struct Args { - #[arg(default_value = ".")] - pub paths: Vec, - #[arg(long)] - pub check: bool, - #[arg(long)] - pub stdin: bool, -} -``` - -**Tests** +Implemented in `crates/nash-cli/src/cmd/format.rs` and `cmd/mod.rs`. -CLI integration test with a temp dir: `--check` exit codes, in-place -rewrite, `--stdin` round trip. +- `nash format [PATH...]` formats files and recursively visits directories. +- `--check` writes nothing, is silent when clean, and exits 1 for differences. +- `--stdin` formats one module from stdin to stdout. +- Parse failures use Nash diagnostics and leave the affected file unchanged. +- `similar::TextDiff` computes contextual changes; `nash-report::format` owns + gutters, file headings, colors, and integration with the existing miette + handler. Whitespace-only edits and missing final newlines are visible. -**Done when** CI runs `nash fmt --check` on `crates/nash-driver/base/` and the repo's -examples. +Formatter, parse-error, and diff snapshots live only in `nash-fmt` unit tests. +No formatter CLI integration/subprocess tests are added. The formatter does not +rewrite the repository's Base sources as a side effect of this implementation. +See [formatter behavior](../docs/formatter.md) for command and layout details. --- @@ -358,10 +234,9 @@ publishes it for the repo. ## Order ``` -1 comments ─ 2 doc ─ 3 exprs ─ 4 decls+comments ─ 5 nash fmt +1 comments ─ 2 doc ─ 3 exprs ─ 4 decls+comments ─ 5 nash format 1 comments ─ 6 extract ─ 7 render + nash docs ``` Chunk 6 depends only on chunk 1 and can run in parallel with 2–5. -Plans/11 chunk 11 (macro diagnostics) switches its generated-code -excerpts from `nash_source::print` to `nash-fmt` after chunk 4. +Future macro diagnostics can use `nash-fmt` once they emit surface source. From 27d64a27f81c0c911bedd831f4a69ad8004b20de Mon Sep 17 00:00:00 2001 From: microproofs Date: Fri, 25 Sep 2026 23:53:58 -0400 Subject: [PATCH 03/19] fix(fmt): keep short expressions compact Signed-off-by: microproofs --- .sampo/changesets/formatter-compact-layout.md | 5 +++ crates/nash-fmt/src/expr.rs | 35 +++++++-------- crates/nash-fmt/src/printer.rs | 5 +++ ...ash_fmt__tests__aligned_do_statements.snap | 43 +++++++++--------- .../nash_fmt__tests__declarations.snap | 3 +- .../nash_fmt__tests__definition_layout.snap | 27 ++++++++++++ .../nash_fmt__tests__do_control_flow.snap | 44 +++++++++---------- .../nash_fmt__tests__do_operators.snap | 14 +++--- .../nash_fmt__tests__keywords_and_macros.snap | 13 +++--- ...ash_fmt__tests__multiline_annotations.snap | 3 +- .../nash_fmt__tests__patterns_and_types.snap | 3 +- crates/nash-fmt/src/tests.rs | 29 ++++++++++++ docs/formatter.md | 11 ++++- plans/13-fmt-docs.md | 2 +- 14 files changed, 150 insertions(+), 87 deletions(-) create mode 100644 .sampo/changesets/formatter-compact-layout.md create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__definition_layout.snap diff --git a/.sampo/changesets/formatter-compact-layout.md b/.sampo/changesets/formatter-compact-layout.md new file mode 100644 index 00000000..2de5239c --- /dev/null +++ b/.sampo/changesets/formatter-compact-layout.md @@ -0,0 +1,5 @@ +--- +cargo/nash-fmt: patch +--- + +Keep short definitions and arithmetic compact, and place do blocks after assignment and bind operators without extra indentation. diff --git a/crates/nash-fmt/src/expr.rs b/crates/nash-fmt/src/expr.rs index 8229c45c..23c5a7bd 100644 --- a/crates/nash-fmt/src/expr.rs +++ b/crates/nash-fmt/src/expr.rs @@ -161,7 +161,10 @@ impl Printer<'_> { } Expr::BinOps { operands, last } => { let mut docs = Vec::new(); - let broken = self.multiline(located.region); + let broken = self.multiline(located.region) + && operands + .iter() + .any(|operand| matches!(operand.op.value, "|>" | "<|")); for operand in *operands { docs.push(self.expr(operand.expr, 2)); let line = if broken { Doc::Hard } else { Doc::Line(" ") }; @@ -321,20 +324,18 @@ impl Printer<'_> { ); let args = args.iter().map(|p| self.pattern(p, 3)).collect(); let head = self.args(text(name.value), args, false); - let body_doc = self.expr(body, 0); - let line = if body.region.start.line > name.region.start.line { - Doc::Hard + let definition = self.rhs(cat([head, text(" =")]), body); + cat([before, annotation, definition]) + } + fn rhs(&mut self, head: Doc, body: &Located>) -> Doc { + let hanging = + matches!(body.value, Expr::Do { .. }) && !self.has_comment_before(body.region.start); + let body = self.expr(body, 0); + if hanging { + cat([head, text(" "), body]) } else { - Doc::Line(" ") - }; - cat([ - before, - annotation, - head, - text(" ="), - cat([line, body_doc]).nest(), - ]) - .group() + cat([head, cat([Doc::Line(" "), body]).nest()]).group() + } } pub fn def(&mut self, located: &Located>) -> Doc { match &located.value { @@ -346,8 +347,7 @@ impl Printer<'_> { } => self.definition(name, args, body, *annotation), Def::Destruct { pattern, body } => { let pattern = self.pattern(pattern, 0).nest(); - let body = self.expr(body, 0); - cat([pattern, text(" ="), cat([Doc::Line(" "), body]).nest()]).group() + self.rhs(cat([pattern, text(" =")]), body) } } } @@ -365,8 +365,7 @@ impl Printer<'_> { } Stmt::Bind { pattern, expr } => { let pattern = self.pattern(pattern, 0).nest(); - let expr = self.expr(expr, 0); - cat([pattern, text(" <-"), cat([Doc::Line(" "), expr]).nest()]).group() + self.rhs(cat([pattern, text(" <-")]), expr) } Stmt::Expr(expr) => self.expr(expr, 0), }; diff --git a/crates/nash-fmt/src/printer.rs b/crates/nash-fmt/src/printer.rs index 1ba10a74..3a50a5b4 100644 --- a/crates/nash-fmt/src/printer.rs +++ b/crates/nash-fmt/src/printer.rs @@ -33,6 +33,11 @@ impl<'a> Printer<'a> { pub fn multiline(&self, r: Region) -> bool { r.start.line != r.end.line } + pub fn has_comment_before(&self, position: Position) -> bool { + self.comments + .get(self.cursor) + .is_some_and(|comment| comment.region.start < position) + } pub fn before(&mut self, p: Position) -> Doc { let mut docs = Vec::new(); while let Some(c) = self.comments.get(self.cursor) { diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap index 3aee54ef..dde1e627 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__aligned_do_statements.snap @@ -2,25 +2,24 @@ source: crates/nash-fmt/src/tests.rs description: "main = do\n if condition then\n first\n else\n second\n let\n x = 1\n in\n pure x\n [ 1\n , 2\n ]\n ( 1\n , 2\n )\n { field = 1\n }\n [\n ]\n {\n }\n (\n )\n next\n" --- -main = - do - if condition then - first - else - second - let - x = 1 - in - pure x - [ 1 - , 2 - ] - ( 1 - , 2 - ) - { field = 1 - } - [] - {} - () - next +main = do + if condition then + first + else + second + let + x = 1 + in + pure x + [ 1 + , 2 + ] + ( 1 + , 2 + ) + { field = 1 + } + [] + {} + () + next diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap index f4fb9f01..d0aa4472 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__declarations.snap @@ -27,5 +27,4 @@ impl Test int where run : int -> int -run n = - n +run n = n diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__definition_layout.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__definition_layout.snap new file mode 100644 index 00000000..044fe2d4 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__definition_layout.snap @@ -0,0 +1,27 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "run : int -> int\nrun n = n\n\nidentity value =\n value\n\nmain = do\n result <- do\n first\n second\n let\n nested = do\n first\n second\n result\n\ncommented =\n -- Keep this comment above the block.\n do\n first\n second\n" +--- +run : int -> int +run n = n + + +identity value = value + + +main = do + result <- do + first + second + let + nested = do + first + second + result + + +commented = + -- Keep this comment above the block. + do + first + second diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap index c028e449..4ff1b8af 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_control_flow.snap @@ -2,33 +2,29 @@ source: crates/nash-fmt/src/tests.rs description: "a = do\n x <- read\n let y = x + 1 in pure y\nb = do\n if condition then first else second\nc = do\n trace \"message\" result\nd = do\n [ firstLongIdentifier, secondLongIdentifier, thirdLongIdentifier, fourthLongIdentifier ]\n" --- -a = - do - x <- read - let - y = x + 1 - in - pure y +a = do + x <- read + let + y = x + 1 + in + pure y -b = - do - if condition then - first - else - second +b = do + if condition then + first + else + second -c = - do - trace "message" - result +c = do + trace "message" + result -d = - do - [ firstLongIdentifier - , secondLongIdentifier - , thirdLongIdentifier - , fourthLongIdentifier - ] +d = do + [ firstLongIdentifier + , secondLongIdentifier + , thirdLongIdentifier + , fourthLongIdentifier + ] diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap index a8006e70..757148c8 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__do_operators.snap @@ -2,11 +2,9 @@ source: crates/nash-fmt/src/tests.rs description: "main = do\n a +\n b\n source\n |> transform\n |> consume\n next\n" --- -main = - do - a + - b - source - |> transform - |> consume - next +main = do + a + b + source + |> transform + |> consume + next diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap index d7607db8..d8b43a25 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__keywords_and_macros.snap @@ -14,10 +14,9 @@ g = comptime (1 + 2) h = Array.fromList!( [ 1, 2 ] ) -i = - do - x <- read - let - y = x + 1 - write y - pure y +i = do + x <- read + let + y = x + 1 + write y + pure y diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap index 0eca4867..e4d4d93a 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_annotations.snap @@ -5,8 +5,7 @@ description: "x : ( int\n , int\n )\nx = (1,2)\ntrait Example 'a where\n x : ( int , int ) -x = - ( 1, 2 ) +x = ( 1, 2 ) trait Example 'a where diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap index 93f5641b..afbd3bf6 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__patterns_and_types.snap @@ -22,5 +22,4 @@ g value = qualified : ( Eq 'a, Other.Show 'a ) => 'a -> string -qualified x = - Other.show x +qualified x = Other.show x diff --git a/crates/nash-fmt/src/tests.rs b/crates/nash-fmt/src/tests.rs index 822bba75..42ed3473 100644 --- a/crates/nash-fmt/src/tests.rs +++ b/crates/nash-fmt/src/tests.rs @@ -456,3 +456,32 @@ fn multiline_annotations() { "# ); } + +#[test] +fn definition_layout() { + assert_format_snapshot!( + r#" + run : int -> int + run n = n + + identity value = + value + + main = do + result <- do + first + second + let + nested = do + first + second + result + + commented = + -- Keep this comment above the block. + do + first + second + "# + ); +} diff --git a/docs/formatter.md b/docs/formatter.md index a16e1b5e..5056beab 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -23,12 +23,21 @@ comments can exceed the target; formatting never changes their contents. Strings, byte literals, and numeric literals retain their source spelling. Output uses LF layout and one final newline for a nonempty module. +The layout follows elm-format's collection and pipeline conventions and +Fourmolu's compact definitions and hanging `do` blocks. Nash syntax and its +layout-sensitive parser determine where those conventions apply. + +- Short definitions remain on one line, including definitions with annotations. + A `do` block follows `=` or `<-` on the same line; its statements indent once. + A comment before the block keeps the block below that comment. - `if`, `case`, `let`, and `do` use multiline bodies. Explicit continuations (`then`, `else`, `in`, and collection closers) may align with a `do` statement. Adjacent statements remain separate expressions. - Collections and applications retain an existing multiline layout; otherwise they stay on one line when they fit. Broken collections use leading commas. -- Pipes begin continuation lines; other infix operators end their lines. +- Pipes begin continuation lines and retain an existing multiline layout. + Other infix expressions stay on one line when they fit; when they wrap, + their operators end the preceding lines. Parentheses retain precedence. - Top-level declarations have two blank lines between them. Local declarations and trait/implementation methods have one. - Imports, exposing entries, declarations, and fields retain source order. diff --git a/plans/13-fmt-docs.md b/plans/13-fmt-docs.md index 88d5969e..8f924e14 100644 --- a/plans/13-fmt-docs.md +++ b/plans/13-fmt-docs.md @@ -92,7 +92,7 @@ The shared `assert_format_snapshot!` helper stores Nash input in the snapshot metadata and formatted Nash in the body. Every test reparses the output, compares the source trees with coordinates removed, and checks idempotence. All 35 shipped Base modules undergo the same structural and idempotence checks. -The formatter crate currently has 31 unit tests, including report snapshots. +The formatter crate currently has 32 unit tests, including report snapshots. --- From 4c49ead716f6eeeb18850cceb933e2e5dd127dab Mon Sep 17 00:00:00 2001 From: microproofs Date: Sat, 26 Sep 2026 00:00:26 -0400 Subject: [PATCH 04/19] test(driver): remove integer-width benchmark Signed-off-by: microproofs --- ...sting_base__integer_width_bound_costs.snap | 34 ----------------- crates/nash-driver/tests/testing_base.rs | 38 ------------------- 2 files changed, 72 deletions(-) delete mode 100644 crates/nash-driver/tests/snapshots/testing_base__integer_width_bound_costs.snap diff --git a/crates/nash-driver/tests/snapshots/testing_base__integer_width_bound_costs.snap b/crates/nash-driver/tests/snapshots/testing_base__integer_width_bound_costs.snap deleted file mode 100644 index a909c8aa..00000000 --- a/crates/nash-driver/tests/snapshots/testing_base__integer_width_bound_costs.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: crates/nash-driver/tests/testing_base.rs -description: " assert ((Int.pow 2 1 - 1) == 1)\n assert ((Int.pow2 1 - 1) == 1)\n assert ((1 * Builtin.expModInteger 2 1 257 - 1) == 1)\n assert ((1 * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) 1 - 1) == 1)\n assert ((1 * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) 1 - 1) == 1)\n assert ((Int.pow 2 4 - 1) == 15)\n assert ((Int.pow2 4 - 1) == 15)\n assert ((1 * Builtin.expModInteger 2 4 257 - 1) == 15)\n assert ((1 * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) 4 - 1) == 15)\n assert ((1 * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) 4 - 1) == 15)\n assert ((Int.pow 2 8 - 1) == 255)\n assert ((Int.pow2 8 - 1) == 255)\n assert ((1 * Builtin.expModInteger 2 8 257 - 1) == 255)\n assert ((1 * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) 8 - 1) == 255)\n assert ((1 * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) 8 - 1) == 255)\n assert ((Int.pow 2 16 - 1) == 65535)\n assert ((Int.pow2 16 - 1) == 65535)\n assert ((256 * Builtin.expModInteger 2 8 257 - 1) == 65535)\n assert ((256 * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) 8 - 1) == 65535)\n assert ((256 * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) 8 - 1) == 65535)\n assert ((Int.pow 2 64 - 1) == 18446744073709551615)\n assert ((Int.pow2 64 - 1) == 18446744073709551615)\n assert ((72057594037927936 * Builtin.expModInteger 2 8 257 - 1) == 18446744073709551615)\n assert ((72057594037927936 * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) 8 - 1) == 18446744073709551615)\n assert ((72057594037927936 * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) 8 - 1) == 18446744073709551615)\n assert ((Int.pow 2 120 - 1) == 1329227995784915872903807060280344575)\n assert ((Int.pow2 120 - 1) == 1329227995784915872903807060280344575)\n assert ((5192296858534827628530496329220096 * Builtin.expModInteger 2 8 257 - 1) == 1329227995784915872903807060280344575)\n assert ((5192296858534827628530496329220096 * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) 8 - 1) == 1329227995784915872903807060280344575)\n assert ((5192296858534827628530496329220096 * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) 8 - 1) == 1329227995784915872903807060280344575)" ---- -width=1 pow: cpu=3620897 memory=19139 -width=1 pow2: cpu=3232564 memory=17038 -width=1 expMod: cpu=2096588 memory=6106 -width=1 array: cpu=3400404 memory=13241 -width=1 constantArray: cpu=1324604 memory=5437 -width=4 pow: cpu=3620897 memory=19139 -width=4 pow2: cpu=3232564 memory=17038 -width=4 expMod: cpu=2096588 memory=6106 -width=4 array: cpu=3400404 memory=13241 -width=4 constantArray: cpu=1324604 memory=5437 -width=8 pow: cpu=3769230 memory=19740 -width=8 pow2: cpu=3380897 memory=17639 -width=8 expMod: cpu=2096588 memory=6106 -width=8 array: cpu=3400404 memory=13241 -width=8 constantArray: cpu=1324604 memory=5437 -width=16 pow: cpu=7750432 memory=36759 -width=16 pow2: cpu=7362099 memory=34658 -width=16 expMod: cpu=2096588 memory=6106 -width=16 array: cpu=3400404 memory=13241 -width=16 constantArray: cpu=1324604 memory=5437 -width=64 pow: cpu=11187716 memory=51376 -width=64 pow2: cpu=10799383 memory=49275 -width=64 expMod: cpu=2097008 memory=6107 -width=64 array: cpu=3400824 memory=13242 -width=64 constantArray: cpu=1325024 memory=5438 -width=120 pow: cpu=12752272 memory=58283 -width=120 pow2: cpu=12363939 memory=56182 -width=120 expMod: cpu=2098085 memory=6108 -width=120 array: cpu=3401901 memory=13243 -width=120 constantArray: cpu=1326101 memory=5439 diff --git a/crates/nash-driver/tests/testing_base.rs b/crates/nash-driver/tests/testing_base.rs index 95d22032..9bcabe2a 100644 --- a/crates/nash-driver/tests/testing_base.rs +++ b/crates/nash-driver/tests/testing_base.rs @@ -164,44 +164,6 @@ async fn test_protocol_logs_survive_silent_user_traces() { } } -#[tokio::test] -async fn integer_width_bound_costs() { - let mut report = String::new(); - let mut sources = Vec::new(); - for width in [1_u32, 4, 8, 16, 64, 120] { - let local = (width - 1) % 8 + 1; - let scale = 1_i128 << (width - local); - let expected = (1_i128 << width) - 1; - for (name, expression) in [ - ("pow", format!("Int.pow 2 {width} - 1")), - ("pow2", format!("Int.pow2 {width} - 1")), - ( - "expMod", - format!("{scale} * Builtin.expModInteger 2 {local} 257 - 1"), - ), - ( - "array", - format!( - "{scale} * Builtin.indexArray (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) {local} - 1" - ), - ), - ( - "constantArray", - format!( - "{scale} * Builtin.indexArray (comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256])) {local} - 1" - ), - ), - ] { - let source = format!(" assert (({expression}) == {expected})"); - report.push_str(&measure_budget(&format!("width={width} {name}"), &source).await); - sources.push(source); - } - } - insta::with_settings!({description => sources.join("\n"), omit_expression => true}, { - insta::assert_snapshot!(report); - }); -} - async fn measure_budget(name: &str, source: &str) -> String { let output = compile(source).await; let arena = Arena::new(); From 0cfd85a33e3b976845224d603afc1b1ef18aa143 Mon Sep 17 00:00:00 2001 From: microproofs Date: Sat, 26 Sep 2026 00:05:30 -0400 Subject: [PATCH 05/19] fix(cli): format files concurrently with Tokio Signed-off-by: microproofs --- .sampo/changesets/async-format-command.md | 5 + Cargo.lock | 1 + crates/nash-cli/Cargo.toml | 2 +- crates/nash-cli/src/cmd/format.rs | 150 ++++++++++++---------- docs/formatter.md | 4 + 5 files changed, 96 insertions(+), 66 deletions(-) create mode 100644 .sampo/changesets/async-format-command.md diff --git a/.sampo/changesets/async-format-command.md b/.sampo/changesets/async-format-command.md new file mode 100644 index 00000000..325cc87d --- /dev/null +++ b/.sampo/changesets/async-format-command.md @@ -0,0 +1,5 @@ +--- +cargo/nash-cli: patch +--- + +Process format inputs concurrently with asynchronous filesystem and stream I/O, and keep formatting work off Tokio executor threads. diff --git a/Cargo.lock b/Cargo.lock index d29b56b3..70a859cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3496,6 +3496,7 @@ version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", diff --git a/crates/nash-cli/Cargo.toml b/crates/nash-cli/Cargo.toml index 3a08aadf..733c0f0b 100644 --- a/crates/nash-cli/Cargo.toml +++ b/crates/nash-cli/Cargo.toml @@ -32,7 +32,7 @@ nash-driver = { path = "../nash-driver", version = "0.11.0" } nash-language-server = { path = "../nash-language-server", version = "0.4.5" } octocrab.workspace = true tar.workspace = true -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util"] } tokio-util = { version = "0.7.18", features = ["compat"] } tower-lsp-server.workspace = true xz2.workspace = true diff --git a/crates/nash-cli/src/cmd/format.rs b/crates/nash-cli/src/cmd/format.rs index bbac80d2..f7bcc2e1 100644 --- a/crates/nash-cli/src/cmd/format.rs +++ b/crates/nash-cli/src/cmd/format.rs @@ -1,6 +1,8 @@ use miette::{IntoDiagnostic, Result}; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; +use std::collections::HashSet; +use std::path::PathBuf; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::task::{JoinSet, spawn_blocking}; #[derive(clap::Args)] pub struct Args { @@ -19,64 +21,45 @@ impl Args { pub async fn exec(self, color: bool) -> Result<()> { if self.stdin { let mut source = String::new(); - std::io::stdin() + tokio::io::stdin() .read_to_string(&mut source) + .await .into_diagnostic()?; - let formatted = nash_fmt::format(&source).map_err(|report| { - miette::Report::new(report.render( - &nash_report::Source::new(&source), - "", - color, - )) - })?; - std::io::stdout() + let formatted = spawn_blocking(move || format_source(&source, "", color)) + .await + .into_diagnostic()??; + let mut stdout = tokio::io::stdout(); + stdout .write_all(formatted.as_bytes()) + .await .into_diagnostic()?; + stdout.flush().await.into_diagnostic()?; return Ok(()); } - let mut files = Vec::new(); - for path in self.paths { - collect(&path.canonicalize().into_diagnostic()?, &mut files)?; - } - files.sort(); - files.dedup(); + let files = collect(self.paths).await?; let root = std::env::current_dir().into_diagnostic()?; let links = supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr); - let mut failed = false; + let mut tasks = JoinSet::new(); for path in files { - let source = std::fs::read_to_string(&path).into_diagnostic()?; let name = crate::reporting::source_name(&root, links, &path.to_string_lossy()); - let formatted = match nash_fmt::format(&source) { - Ok(formatted) => formatted, - Err(report) => { - eprintln!( - "{:?}", - miette::Report::new(report.render( - &nash_report::Source::new(&source), - &name, - color - )) - ); - failed = true; - continue; - } - }; - if self.check { - if let Some(report) = nash_report::format::difference(&name, &source, &formatted) { - eprintln!( - "{:?}", - miette::Report::new(report.render( - &nash_report::Source::new(&source), - &name, - color - )) - ); - failed = true; + tasks.spawn(format_file(path, name, self.check, color)); + } + let mut failed = false; + let mut stderr = tokio::io::stderr(); + let mut output = Ok(()); + while let Some(result) = tasks.join_next().await { + if let Err(report) = result.into_diagnostic().and_then(|result| result) { + if output.is_ok() { + output = stderr + .write_all(format!("{report:?}\n").as_bytes()) + .await + .into_diagnostic(); } - } else if source != formatted { - std::fs::write(&path, formatted).into_diagnostic()?; + failed = true; } } + output?; + stderr.flush().await.into_diagnostic()?; if failed { std::process::exit(1); } @@ -84,26 +67,63 @@ impl Args { } } -fn collect(path: &Path, files: &mut Vec) -> Result<()> { - let metadata = std::fs::symlink_metadata(path).into_diagnostic()?; - if metadata.is_file() { - if path - .extension() - .is_some_and(|extension| extension == "nash") - { - files.push(path.canonicalize().into_diagnostic()?); - } - } else if metadata.is_dir() { - for entry in std::fs::read_dir(path).into_diagnostic()? { - let entry = entry.into_diagnostic()?; - if matches!( - entry.file_name().to_str(), - Some(".git" | ".jj" | "target" | "build" | "node_modules") - ) { - continue; +fn format_source(source: &str, name: &str, color: bool) -> Result { + nash_fmt::format(source).map_err(|report| { + miette::Report::new(report.render(&nash_report::Source::new(source), name, color)) + }) +} + +async fn format_file(path: PathBuf, name: String, check: bool, color: bool) -> Result<()> { + let source = tokio::fs::read_to_string(&path).await.into_diagnostic()?; + let formatted = spawn_blocking(move || { + let formatted = format_source(&source, &name, color)?; + if check { + if let Some(report) = nash_report::format::difference(&name, &source, &formatted) { + return Err(miette::Report::new(report.render( + &nash_report::Source::new(&source), + &name, + color, + ))); } - collect(&entry.path(), files)?; + Ok(None) + } else { + Ok((source != formatted).then_some(formatted)) } + }) + .await + .into_diagnostic()??; + if let Some(formatted) = formatted { + tokio::fs::write(path, formatted).await.into_diagnostic()?; } Ok(()) } + +async fn collect(paths: Vec) -> Result> { + let mut pending = Vec::new(); + for path in paths { + pending.push(tokio::fs::canonicalize(path).await.into_diagnostic()?); + } + let mut files = HashSet::new(); + while let Some(path) = pending.pop() { + let metadata = tokio::fs::symlink_metadata(&path).await.into_diagnostic()?; + if metadata.is_file() { + if path + .extension() + .is_some_and(|extension| extension == "nash") + { + files.insert(tokio::fs::canonicalize(&path).await.into_diagnostic()?); + } + } else if metadata.is_dir() { + let mut entries = tokio::fs::read_dir(&path).await.into_diagnostic()?; + while let Some(entry) = entries.next_entry().await.into_diagnostic()? { + if !matches!( + entry.file_name().to_str(), + Some(".git" | ".jj" | "target" | "build" | "node_modules") + ) { + pending.push(entry.path()); + } + } + } + } + Ok(files) +} diff --git a/docs/formatter.md b/docs/formatter.md index 5056beab..45316f44 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -5,6 +5,10 @@ are visited recursively; the default is the current directory. The command skips `.git`, `.jj`, `target`, `build`, and `node_modules`, and does not follow symlinks discovered during directory traversal. Explicit file symlinks resolve to their targets. It formats source without loading a project, imports, or Base. +Filesystem and stream I/O is asynchronous. Files are processed concurrently, +with parsing, formatting, and diff construction on blocking worker threads. +Diagnostics appear as files finish; their order is unspecified. Overlapping +input paths are deduplicated before processing. `--check` leaves files untouched and exits 1 when formatting differs or a file cannot be parsed. A clean check is silent. Changed files have a contextual From bc3d00cd223650e60c05ac843bd8110c9df9a38d Mon Sep 17 00:00:00 2001 From: microproofs Date: Sat, 26 Sep 2026 00:18:41 -0400 Subject: [PATCH 06/19] fix(fmt): refine signatures and exposing layout Signed-off-by: microproofs --- .../changesets/formatter-constraint-breaks.md | 5 + crates/nash-driver/base/src/Applicative.nash | 7 +- crates/nash-driver/base/src/Array.nash | 23 +- crates/nash-driver/base/src/Bool.nash | 99 +- crates/nash-driver/base/src/Bytes.nash | 111 +- .../nash-driver/base/src/Cardano/Address.nash | 116 +- crates/nash-driver/base/src/Cardano/Time.nash | 222 +++- crates/nash-driver/base/src/Cardano/Tx.nash | 1129 +++++++++++------ .../nash-driver/base/src/Cardano/Value.nash | 77 +- crates/nash-driver/base/src/Cons.nash | 139 +- crates/nash-driver/base/src/Crypto.nash | 53 +- crates/nash-driver/base/src/Data.nash | 463 +++++-- crates/nash-driver/base/src/Eq.nash | 60 +- crates/nash-driver/base/src/Function.nash | 7 +- crates/nash-driver/base/src/Functor.nash | 13 +- crates/nash-driver/base/src/Int.nash | 278 ++-- crates/nash-driver/base/src/Integral.nash | 13 +- crates/nash-driver/base/src/Lift.nash | 16 +- crates/nash-driver/base/src/List.nash | 395 ++++-- crates/nash-driver/base/src/Literal.nash | 16 +- crates/nash-driver/base/src/Map.nash | 115 +- crates/nash-driver/base/src/Monad.nash | 6 +- crates/nash-driver/base/src/Monoid.nash | 10 +- crates/nash-driver/base/src/Num.nash | 11 +- crates/nash-driver/base/src/Option.nash | 140 +- crates/nash-driver/base/src/Ord.nash | 100 +- crates/nash-driver/base/src/Ordering.nash | 84 +- crates/nash-driver/base/src/Pair.nash | 6 +- crates/nash-driver/base/src/Prelude.nash | 259 +++- crates/nash-driver/base/src/Prop.nash | 424 +++++-- crates/nash-driver/base/src/Rational.nash | 92 +- crates/nash-driver/base/src/Semigroup.nash | 15 +- crates/nash-driver/base/src/Show.nash | 145 ++- crates/nash-driver/base/src/String.nash | 46 +- crates/nash-driver/base/src/Test.nash | 40 +- crates/nash-driver/base/src/Unit.nash | 14 +- crates/nash-fmt/src/declarations.rs | 11 +- .../nash_fmt__tests__comments_in_syntax.snap | 6 +- ...sh_fmt__tests__constrained_signatures.snap | 16 + .../nash_fmt__tests__long_lines.snap | 3 +- .../nash_fmt__tests__multiline_exposing.snap | 16 + crates/nash-fmt/src/tests.rs | 33 + crates/nash-fmt/src/types.rs | 29 +- docs/formatter.md | 4 + plans/13-fmt-docs.md | 2 +- 45 files changed, 3566 insertions(+), 1303 deletions(-) create mode 100644 .sampo/changesets/formatter-constraint-breaks.md create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__constrained_signatures.snap create mode 100644 crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_exposing.snap diff --git a/.sampo/changesets/formatter-constraint-breaks.md b/.sampo/changesets/formatter-constraint-breaks.md new file mode 100644 index 00000000..a4801936 --- /dev/null +++ b/.sampo/changesets/formatter-constraint-breaks.md @@ -0,0 +1,5 @@ +--- +cargo/nash-fmt: patch +--- + +Wrap constrained type signatures before the fat arrow, keeping the function type together when it fits. Start multiline exposing lists on the next line. diff --git a/crates/nash-driver/base/src/Applicative.nash b/crates/nash-driver/base/src/Applicative.nash index 2c4755ed..6c1b39ec 100644 --- a/crates/nash-driver/base/src/Applicative.nash +++ b/crates/nash-driver/base/src/Applicative.nash @@ -1,7 +1,10 @@ -module Applicative exposing (Applicative) +module Applicative exposing ( Applicative ) + + +import Functor exposing ( Functor ) -import Functor exposing (Functor) trait Functor 'f => Applicative 'f where pure : 'a -> 'f 'a + apply : 'f ('a -> 'b) -> 'f 'a -> 'f 'b diff --git a/crates/nash-driver/base/src/Array.nash b/crates/nash-driver/base/src/Array.nash index 540c8860..abdd613a 100644 --- a/crates/nash-driver/base/src/Array.nash +++ b/crates/nash-driver/base/src/Array.nash @@ -1,10 +1,12 @@ -module Array exposing (fromList, length, at, get) +module Array exposing ( fromList, length, at, get ) + import Builtin -import Function exposing ((|>)) -import Lift exposing (Lift) +import Function exposing ( (|>) ) +import Lift exposing ( Lift ) import Literal -import Option exposing (type option(..)) +import Option exposing ( type option(..) ) + fromList : Lift (list 'a) ('f 'a) => 'f 'a -> array 'a fromList values = @@ -12,17 +14,24 @@ fromList values = |> lower |> Builtin.listToArray + length : array 'a -> int length = Builtin.lengthOfArray + at : Lift int 'n => array 'a -> 'n -> 'a at values index = Builtin.indexArray values (lower index) + get : Lift int 'n => array 'a -> 'n -> option 'a get values index = getIndex values (lower index) + getIndex : array 'a -> int -> option 'a getIndex values index = - if Builtin.lessThanInteger index 0 then None - else if Builtin.lessThanInteger index (length values) then Some (at values index) - else None + if Builtin.lessThanInteger index 0 then + None + else if Builtin.lessThanInteger index (length values) then + Some (at values index) + else + None diff --git a/crates/nash-driver/base/src/Bool.nash b/crates/nash-driver/base/src/Bool.nash index 68d634d5..33c0e145 100644 --- a/crates/nash-driver/base/src/Bool.nash +++ b/crates/nash-driver/base/src/Bool.nash @@ -1,45 +1,96 @@ -module Bool exposing (Bool(..), not, and, or, xor) +module Bool exposing ( Bool(..), not, and, or, xor ) -import Literal exposing (FromBool) + +import Literal exposing ( FromBool ) import Primitive -import Lift exposing (Lift) -import Data exposing (Validate, Decode) -import Option exposing (type option(..)) +import Lift exposing ( Lift ) +import Data exposing ( Validate, Decode ) +import Option exposing ( type option(..) ) import Eq -type Bool = False | True + +type Bool + = False + | True + not : Lift bool 'a => 'a -> bool -not value = if lower value then Primitive.False else Primitive.True +not value = + if lower value then + Primitive.False + else + Primitive.True + + +and : ( Lift bool 'a, Lift bool 'b ) => 'a -> 'b -> bool +and a b = + if lower a then + lower b + else + Primitive.False -and : (Lift bool 'a, Lift bool 'b) => 'a -> 'b -> bool -and a b = if lower a then lower b else Primitive.False -or : (Lift bool 'a, Lift bool 'b) => 'a -> 'b -> bool -or a b = if lower a then Primitive.True else lower b +or : ( Lift bool 'a, Lift bool 'b ) => 'a -> 'b -> bool +or a b = + if lower a then + Primitive.True + else + lower b + + +xor : ( Lift bool 'a, Lift bool 'b ) => 'a -> 'b -> bool +xor a b = + if lower a then + not b + else + lower b -xor : (Lift bool 'a, Lift bool 'b) => 'a -> 'b -> bool -xor a b = if lower a then not b else lower b impl Lift bool Bool where - lift value = if value then Bool.True else Bool.False + lift value = + if value then + Bool.True + else + Bool.False + lower value = case value of - Bool.True -> Primitive.True - Bool.False -> Primitive.False + Bool.True -> + Primitive.True + + Bool.False -> + Primitive.False + impl FromBool Bool where - fromBool value = if value then Bool.True else Bool.False + fromBool value = + if value then + Bool.True + else + Bool.False + impl Validate Bool where validate value = case Builtin.unConstrData value of - pair(0, []) -> Bool.False - pair(1, []) -> Bool.True - _ -> fail + pair(0, []) -> + Bool.False + + pair(1, []) -> + Bool.True + + _ -> + fail + impl Decode Bool where - decode value = case value of - Primitive.Constr pair(0, []) -> Some Bool.False - Primitive.Constr pair(1, []) -> Some Bool.True - _ -> None + decode value = + case value of + Primitive.Constr pair(0, []) -> + Some Bool.False + + Primitive.Constr pair(1, []) -> + Some Bool.True + + _ -> + None diff --git a/crates/nash-driver/base/src/Bytes.nash b/crates/nash-driver/base/src/Bytes.nash index 819cf401..4ee5a62b 100644 --- a/crates/nash-driver/base/src/Bytes.nash +++ b/crates/nash-driver/base/src/Bytes.nash @@ -1,57 +1,100 @@ -module Bytes exposing (append, compare, min, max, length, at, slice, take, drop, concat, and, or, xor, complement, readBit, writeBits, shift, rotate, countSetBits, findFirstSetBit, toHex) +module Bytes exposing + ( append + , compare + , min + , max + , length + , at + , slice + , take + , drop + , concat + , and + , or + , xor + , complement + , readBit + , writeBits + , shift + , rotate + , countSetBits + , findFirstSetBit + , toHex + ) + import Literal -import Lift exposing (Lift) +import Lift exposing ( Lift ) import Builtin -import Function exposing ((|>)) +import Function exposing ( (|>) ) import List import Show import Ord -import Ordering exposing (type ordering) +import Ordering exposing ( type ordering ) + -append : (Lift bytes 'a, Lift bytes 'b) => 'a -> 'b -> bytes +append : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> bytes append a b = Builtin.appendByteString (lower a) (lower b) -compare : (Lift bytes 'a, Lift bytes 'b) => 'a -> 'b -> ordering + +compare : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> ordering compare a b = compareBytes (lower a) (lower b) + compareBytes : bytes -> bytes -> ordering compareBytes = Ord.compare -min : (Lift bytes 'a, Lift bytes 'b) => 'a -> 'b -> bytes + +min : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> bytes min a b = minBytes (lower a) (lower b) + minBytes : bytes -> bytes -> bytes minBytes = Ord.min -max : (Lift bytes 'a, Lift bytes 'b) => 'a -> 'b -> bytes + +max : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> bytes max a b = maxBytes (lower a) (lower b) + maxBytes : bytes -> bytes -> bytes maxBytes = Ord.max + length : Lift bytes 'b => 'b -> int length value = value |> lower |> Builtin.lengthOfByteString -at : (Lift bytes 'b, Lift int 'n) => 'b -> 'n -> int + +at : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> int at value index = Builtin.indexByteString (lower value) (lower index) -slice : (Lift int 's, Lift int 'n, Lift bytes 'b) => 's -> 'n -> 'b -> bytes -slice start size value = Builtin.sliceByteString (lower start) (lower size) (lower value) -take : (Lift int 'n, Lift bytes 'b) => 'n -> 'b -> bytes +slice : ( Lift int 's, Lift int 'n, Lift bytes 'b ) => 's -> 'n -> 'b -> bytes +slice start size value = + Builtin.sliceByteString (lower start) (lower size) (lower value) + + +take : ( Lift int 'n, Lift bytes 'b ) => 'n -> 'b -> bytes take count value = Builtin.sliceByteString 0 (lower count) (lower value) -drop : (Lift int 'n, Lift bytes 'b) => 'n -> 'b -> bytes + +drop : ( Lift int 'n, Lift bytes 'b ) => 'n -> 'b -> bytes drop count value = - let bytes = lower value in - Builtin.sliceByteString (lower count) (Builtin.lengthOfByteString bytes) bytes + let + bytes = lower value + in + Builtin.sliceByteString + (lower count) + (Builtin.lengthOfByteString bytes) + bytes + -concat : (Lift (list 'b) ('f 'b), Lift bytes 'b) => ('f 'b) -> bytes -concat values = List.foldr (\x acc -> Builtin.appendByteString (lower x) acc) #"" values +concat : ( Lift (list 'b) ('f 'b), Lift bytes 'b ) => 'f 'b -> bytes +concat values = + List.foldr (\x acc -> Builtin.appendByteString (lower x) acc) #"" values complement : Lift bytes 'b => 'b -> bytes @@ -60,38 +103,52 @@ complement value = |> lower |> Builtin.complementByteString + countSetBits : Lift bytes 'b => 'b -> int countSetBits value = value |> lower |> Builtin.countSetBits + findFirstSetBit : Lift bytes 'b => 'b -> int findFirstSetBit value = value |> lower |> Builtin.findFirstSetBit -and : (Lift bool 'p, Lift bytes 'a, Lift bytes 'b) => 'p -> 'a -> 'b -> bytes -and pad left right = Builtin.andByteString (lower pad) (lower left) (lower right) -or : (Lift bool 'p, Lift bytes 'a, Lift bytes 'b) => 'p -> 'a -> 'b -> bytes +and : ( Lift bool 'p, Lift bytes 'a, Lift bytes 'b ) => 'p -> 'a -> 'b -> bytes +and pad left right = + Builtin.andByteString (lower pad) (lower left) (lower right) + + +or : ( Lift bool 'p, Lift bytes 'a, Lift bytes 'b ) => 'p -> 'a -> 'b -> bytes or pad left right = Builtin.orByteString (lower pad) (lower left) (lower right) -xor : (Lift bool 'p, Lift bytes 'a, Lift bytes 'b) => 'p -> 'a -> 'b -> bytes -xor pad left right = Builtin.xorByteString (lower pad) (lower left) (lower right) -readBit : (Lift bytes 'b, Lift int 'n) => 'b -> 'n -> bool +xor : ( Lift bool 'p, Lift bytes 'a, Lift bytes 'b ) => 'p -> 'a -> 'b -> bytes +xor pad left right = + Builtin.xorByteString (lower pad) (lower left) (lower right) + + +readBit : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> bool readBit value count = Builtin.readBit (lower value) (lower count) -shift : (Lift bytes 'b, Lift int 'n) => 'b -> 'n -> bytes + +shift : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> bytes shift value count = Builtin.shiftByteString (lower value) (lower count) -rotate : (Lift bytes 'b, Lift int 'n) => 'b -> 'n -> bytes + +rotate : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> bytes rotate value count = Builtin.rotateByteString (lower value) (lower count) -writeBits : (Lift bytes 'b, Lift (list 'n) ('f 'n), Lift int 'n, Lift bool 'v) => 'b -> ('f 'n) -> 'v -> bytes -writeBits value indices bit = Builtin.writeBits (lower value) (List.map lower indices) (lower bit) + +writeBits : ( Lift bytes 'b, Lift (list 'n) ('f 'n), Lift int 'n, Lift bool 'v ) + => 'b -> 'f 'n -> 'v -> bytes +writeBits value indices bit = + Builtin.writeBits (lower value) (List.map lower indices) (lower bit) + toHex : Lift bytes 'b => 'b -> string toHex value = diff --git a/crates/nash-driver/base/src/Cardano/Address.nash b/crates/nash-driver/base/src/Cardano/Address.nash index 14399445..6821a349 100644 --- a/crates/nash-driver/base/src/Cardano/Address.nash +++ b/crates/nash-driver/base/src/Cardano/Address.nash @@ -1,67 +1,111 @@ module Cardano.Address exposing (..) -import Primitive exposing (Data(..)) -import Monad exposing (Monad) -import Data exposing (Validate, Decode) + +import Primitive exposing ( Data(..) ) +import Monad exposing ( Monad ) +import Data exposing ( Validate, Decode ) import Eq import Literal -import Option exposing (Option, type option(..)) +import Option exposing ( Option, type option(..) ) + + +type Credential + = PubKeyCredential Bytes + | ScriptCredential Bytes + + +type StakingCredential + = StakingHash Credential + | StakingPtr Int Int Int -type Credential = PubKeyCredential Bytes | ScriptCredential Bytes -type StakingCredential = StakingHash Credential | StakingPtr Int Int Int +type Address + = Address + { paymentCredential : Credential + , stakingCredential : Option StakingCredential + } -type Address = Address { paymentCredential : Credential, stakingCredential : Option StakingCredential } impl Validate Credential where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> PubKeyCredential (validate f0) - pair(1, [f0]) -> ScriptCredential (validate f0) - _ -> fail + pair(0, [ f0 ]) -> + PubKeyCredential (validate f0) + + pair(1, [ f0 ]) -> + ScriptCredential (validate f0) + + _ -> + fail + impl Validate StakingCredential where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> StakingHash (validate f0) - pair(1, [f0, f1, f2]) -> StakingPtr (validate f0) (validate f1) (validate f2) - _ -> fail + pair(0, [ f0 ]) -> + StakingHash (validate f0) + + pair(1, [ f0, f1, f2 ]) -> + StakingPtr (validate f0) (validate f1) (validate f2) + + _ -> + fail + impl Validate Address where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> Address (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + Address (validate f0) (validate f1) + + _ -> + fail + impl Decode Credential where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (PubKeyCredential d0) - Constr pair(1, [f0]) -> do - d0 <- decode f0 - Some (ScriptCredential d0) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (PubKeyCredential d0) + + Constr pair(1, [ f0 ]) -> + do + d0 <- decode f0 + Some (ScriptCredential d0) + + _ -> + None + impl Decode StakingCredential where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (StakingHash d0) - Constr pair(1, [f0, f1, f2]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - Some (StakingPtr d0 d1 d2) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (StakingHash d0) + + Constr pair(1, [ f0, f1, f2 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + Some (StakingPtr d0 d1 d2) + + _ -> + None + impl Decode Address where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Address d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Address d0 d1) + + _ -> + None diff --git a/crates/nash-driver/base/src/Cardano/Time.nash b/crates/nash-driver/base/src/Cardano/Time.nash index 0c4e88c9..7eca16ec 100644 --- a/crates/nash-driver/base/src/Cardano/Time.nash +++ b/crates/nash-driver/base/src/Cardano/Time.nash @@ -1,115 +1,221 @@ -module Cardano.Time exposing (Extended(..), IntervalBound(..), Interval(..), interval, from, to, always, never, member, contains, isEmpty) - -import Monad exposing (Monad) -import Option exposing (type option(..)) +module Cardano.Time exposing + ( Extended(..) + , IntervalBound(..) + , Interval(..) + , interval + , from + , to + , always + , never + , member + , contains + , isEmpty + ) + + +import Monad exposing ( Monad ) +import Option exposing ( type option(..) ) import Prelude exposing (..) -import Primitive exposing (Data(..), type bool(..)) +import Primitive exposing ( Data(..), type bool(..) ) import Num -import Data exposing (Validate, Decode) -import Bool exposing (Bool) -import Lift exposing (Lift) +import Data exposing ( Validate, Decode ) +import Bool exposing ( Bool ) +import Lift exposing ( Lift ) + + +type Extended + = NegInf + | Finite Int + | PosInf + + +type IntervalBound + = IntervalBound { bound : Extended, inclusive : Bool } -type Extended = NegInf | Finite Int | PosInf -type IntervalBound = IntervalBound { bound : Extended, inclusive : Bool } +type Interval + = Interval { lowerBound : IntervalBound, upperBound : IntervalBound } -type Interval = Interval { lowerBound : IntervalBound, upperBound : IntervalBound } impl Validate Extended where validate raw = case Builtin.unConstrData raw of - pair(0, []) -> NegInf - pair(1, [f0]) -> Finite (validate f0) - pair(2, []) -> PosInf - _ -> fail + pair(0, []) -> + NegInf + + pair(1, [ f0 ]) -> + Finite (validate f0) + + pair(2, []) -> + PosInf + + _ -> + fail + impl Validate IntervalBound where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> IntervalBound (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + IntervalBound (validate f0) (validate f1) + + _ -> + fail + impl Validate Interval where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> Interval (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + Interval (validate f0) (validate f1) + + _ -> + fail + + +type endpoint + = Negative + | At int + | Positive -type endpoint = Negative | At int | Positive inclusiveEndpoint : int -> IntervalBound -> endpoint inclusiveEndpoint step (IntervalBound bound closed) = case bound of - NegInf -> Negative - PosInf -> Positive - Finite value -> At (lower value + (if lower closed then 0 else step)) + NegInf -> + Negative + + PosInf -> + Positive + + Finite value -> + At + ( + lower value + + ( + if lower closed then + 0 + else + step + ) + ) + endpointLe : endpoint -> endpoint -> bool endpointLe left right = - case (left, right) of - (Negative, _) -> True - (_, Positive) -> True - (Positive, _) -> False - (_, Negative) -> False - (At a, At b) -> a <= b + case ( left, right ) of + ( Negative, _ ) -> + True + + ( _, Positive ) -> + True + + ( Positive, _ ) -> + False + + ( _, Negative ) -> + False + + ( At a, At b ) -> + a <= b + isEmpty : Interval -> bool isEmpty (Interval lo hi) = - Bool.not (endpointLe (inclusiveEndpoint 1 lo) (inclusiveEndpoint -1 hi)) + Bool.not (endpointLe (inclusiveEndpoint 1 lo) (inclusiveEndpoint (-1) hi)) + contains : Interval -> Interval -> bool contains outer inner = - if isEmpty inner then True - else if isEmpty outer then False + if isEmpty inner then + True + else if isEmpty outer then + False else - endpointLe (inclusiveEndpoint 1 outer.lowerBound) (inclusiveEndpoint 1 inner.lowerBound) - && endpointLe (inclusiveEndpoint -1 inner.upperBound) (inclusiveEndpoint -1 outer.upperBound) + endpointLe + (inclusiveEndpoint 1 outer.lowerBound) + (inclusiveEndpoint 1 inner.lowerBound) && + endpointLe + (inclusiveEndpoint (-1) inner.upperBound) + (inclusiveEndpoint (-1) outer.upperBound) + member : Lift int 'n => 'n -> Interval -> bool member point (Interval lo hi) = - endpointLe (inclusiveEndpoint 1 lo) (At (lower point)) - && endpointLe (At (lower point)) (inclusiveEndpoint -1 hi) + endpointLe (inclusiveEndpoint 1 lo) (At (lower point)) && + endpointLe (At (lower point)) (inclusiveEndpoint (-1) hi) + -interval : (Lift int 'a, Lift int 'b) => 'a -> 'b -> Interval +interval : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> Interval interval start end = - Interval (IntervalBound (Finite (lift (lower start))) Bool.True) (IntervalBound (Finite (lift (lower end))) Bool.True) + Interval + (IntervalBound (Finite (lift (lower start))) Bool.True) + (IntervalBound (Finite (lift (lower end))) Bool.True) + from : Lift int 'a => 'a -> Interval -from start = Interval (IntervalBound (Finite (lift (lower start))) Bool.True) (IntervalBound PosInf Bool.True) +from start = + Interval + (IntervalBound (Finite (lift (lower start))) Bool.True) + (IntervalBound PosInf Bool.True) + to : Lift int 'a => 'a -> Interval -to end = Interval (IntervalBound NegInf Bool.True) (IntervalBound (Finite (lift (lower end))) Bool.True) +to end = + Interval + (IntervalBound NegInf Bool.True) + (IntervalBound (Finite (lift (lower end))) Bool.True) + always : Interval -always = Interval (IntervalBound NegInf Bool.True) (IntervalBound PosInf Bool.True) +always = + Interval (IntervalBound NegInf Bool.True) (IntervalBound PosInf Bool.True) + never : Interval -never = Interval (IntervalBound PosInf Bool.True) (IntervalBound NegInf Bool.True) +never = + Interval (IntervalBound PosInf Bool.True) (IntervalBound NegInf Bool.True) + impl Decode Extended where decode raw = case raw of - Constr pair(0, []) -> Some NegInf - Constr pair(1, [f0]) -> do - d0 <- decode f0 - Some (Finite d0) - Constr pair(2, []) -> Some PosInf - _ -> None + Constr pair(0, []) -> + Some NegInf + + Constr pair(1, [ f0 ]) -> + do + d0 <- decode f0 + Some (Finite d0) + + Constr pair(2, []) -> + Some PosInf + + _ -> + None + impl Decode IntervalBound where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (IntervalBound d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (IntervalBound d0 d1) + + _ -> + None + impl Decode Interval where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Interval d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Interval d0 d1) + + _ -> + None diff --git a/crates/nash-driver/base/src/Cardano/Tx.nash b/crates/nash-driver/base/src/Cardano/Tx.nash index ead64e6f..b585e565 100644 --- a/crates/nash-driver/base/src/Cardano/Tx.nash +++ b/crates/nash-driver/base/src/Cardano/Tx.nash @@ -1,25 +1,29 @@ module Cardano.Tx exposing (..) -import Primitive exposing (Data(..)) -import Monad exposing (Monad) -import Data exposing (Validate, Decode) + +import Primitive exposing ( Data(..) ) +import Monad exposing ( Monad ) +import Data exposing ( Validate, Decode ) import Eq import Literal -import Option exposing (Option, type option(..)) -import Cardano.Address exposing (Address, Credential) -import Cardano.Time exposing (Interval) -import Cardano.Value exposing (Value) +import Option exposing ( Option, type option(..) ) +import Cardano.Address exposing ( Address, Credential ) +import Cardano.Time exposing ( Interval ) +import Cardano.Value exposing ( Value ) + type DRep = DRep Credential | DRepAlwaysAbstain | DRepAlwaysNoConfidence + type Delegatee = DelegStake Bytes | DelegVote DRep | DelegStakeVote Bytes DRep + type Certificate = RegStaking Credential (Option Int) | UnRegStaking Credential (Option Int) @@ -33,74 +37,83 @@ type Certificate | AuthHotCommittee Credential Credential | ResignColdCommittee Credential + type Voter = CommitteeVoter Credential | DRepVoter Credential | StakePoolVoter Bytes + type Vote = VoteNo | VoteYes | Abstain -type GovernanceActionId = GovernanceActionId - { txId : Bytes - , index : Int - } -type Rational = Rational - { numerator : Int - , denominator : Int - } +type GovernanceActionId + = GovernanceActionId { txId : Bytes, index : Int } + + +type Rational + = Rational { numerator : Int, denominator : Int } + -type Committee = Committee - { members : Map Credential Int - , quorum : Rational - } +type Committee + = Committee { members : Map Credential Int, quorum : Rational } -type Constitution = Constitution (Option Bytes) -type ProtocolVersion = ProtocolVersion - { major : Int - , minor : Int - } +type Constitution + = Constitution (Option Bytes) + + +type ProtocolVersion + = ProtocolVersion { major : Int, minor : Int } + type GovernanceAction = ParameterChange (Option GovernanceActionId) Data (Option Bytes) | HardForkInitiation (Option GovernanceActionId) ProtocolVersion | TreasuryWithdrawals (Map Credential Int) (Option Bytes) | NoConfidence (Option GovernanceActionId) - | UpdateCommittee (Option GovernanceActionId) (List Credential) (Map Credential Int) Rational + | UpdateCommittee + (Option GovernanceActionId) + (List Credential) + (Map Credential Int) + Rational | NewConstitution (Option GovernanceActionId) Constitution | InfoAction -type Proposal = Proposal - { deposit : Int - , returnAddress : Credential - , governanceAction : GovernanceAction - } -type OutputReference = OutputReference - { txId : Bytes - , index : Int - } +type Proposal + = Proposal + { deposit : Int + , returnAddress : Credential + , governanceAction : GovernanceAction + } + + +type OutputReference + = OutputReference { txId : Bytes, index : Int } + type Datum = NoDatum | DatumHash Bytes | InlineDatum Data -type Output = Output - { address : Address - , value : Value - , datum : Datum - , script : Option Bytes - } -type Input = Input - { outputReference : OutputReference - , output : Output - } +type Output + = Output + { address : Address + , value : Value + , datum : Datum + , script : Option Bytes + } + + +type Input + = Input { outputReference : OutputReference, output : Output } + type ScriptPurpose = Minting Bytes @@ -110,6 +123,7 @@ type ScriptPurpose | Voting Voter | Proposing Int Proposal + type ScriptInfo = MintingScript Bytes | SpendingScript OutputReference (Option Data) @@ -118,488 +132,865 @@ type ScriptInfo | VotingScript Voter | ProposingScript Int Proposal -type Tx = Tx - { inputs : List Input - , referenceInputs : List Input - , outputs : List Output - , fee : Int - , mint : Value - , certificates : List Certificate - , withdrawals : Map Credential Int - , validityRange : Interval - , extraSignatories : List Bytes - , redeemers : Map ScriptPurpose Data - , datums : Map Bytes Data - , id : Bytes - , votes : Map Voter (Map GovernanceActionId Vote) - , proposals : List Proposal - , currentTreasury : Option Int - , treasuryDonation : Option Int - } - -type ScriptContext = ScriptContext - { tx : Tx, redeemer : Data - , scriptInfo : ScriptInfo - } + +type Tx + = Tx + { inputs : List Input + , referenceInputs : List Input + , outputs : List Output + , fee : Int + , mint : Value + , certificates : List Certificate + , withdrawals : Map Credential Int + , validityRange : Interval + , extraSignatories : List Bytes + , redeemers : Map ScriptPurpose Data + , datums : Map Bytes Data + , id : Bytes + , votes : Map Voter (Map GovernanceActionId Vote) + , proposals : List Proposal + , currentTreasury : Option Int + , treasuryDonation : Option Int + } + + +type ScriptContext + = ScriptContext { tx : Tx, redeemer : Data, scriptInfo : ScriptInfo } + impl Validate DRep where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> DRep (validate f0) - pair(1, []) -> DRepAlwaysAbstain - pair(2, []) -> DRepAlwaysNoConfidence - _ -> fail + pair(0, [ f0 ]) -> + DRep (validate f0) + + pair(1, []) -> + DRepAlwaysAbstain + + pair(2, []) -> + DRepAlwaysNoConfidence + + _ -> + fail + impl Validate Delegatee where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> DelegStake (validate f0) - pair(1, [f0]) -> DelegVote (validate f0) - pair(2, [f0, f1]) -> DelegStakeVote (validate f0) (validate f1) - _ -> fail + pair(0, [ f0 ]) -> + DelegStake (validate f0) + + pair(1, [ f0 ]) -> + DelegVote (validate f0) + + pair(2, [ f0, f1 ]) -> + DelegStakeVote (validate f0) (validate f1) + + _ -> + fail + impl Validate Certificate where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> RegStaking (validate f0) (validate f1) - pair(1, [f0, f1]) -> UnRegStaking (validate f0) (validate f1) - pair(2, [f0, f1]) -> DelegStaking (validate f0) (validate f1) - pair(3, [f0, f1, f2]) -> RegDeleg (validate f0) (validate f1) (validate f2) - pair(4, [f0, f1]) -> RegDRep (validate f0) (validate f1) - pair(5, [f0]) -> UpdateDRep (validate f0) - pair(6, [f0, f1]) -> UnRegDRep (validate f0) (validate f1) - pair(7, [f0, f1]) -> PoolRegister (validate f0) (validate f1) - pair(8, [f0, f1]) -> PoolRetire (validate f0) (validate f1) - pair(9, [f0, f1]) -> AuthHotCommittee (validate f0) (validate f1) - pair(10, [f0]) -> ResignColdCommittee (validate f0) - _ -> fail + pair(0, [ f0, f1 ]) -> + RegStaking (validate f0) (validate f1) + + pair(1, [ f0, f1 ]) -> + UnRegStaking (validate f0) (validate f1) + + pair(2, [ f0, f1 ]) -> + DelegStaking (validate f0) (validate f1) + + pair(3, [ f0, f1, f2 ]) -> + RegDeleg (validate f0) (validate f1) (validate f2) + + pair(4, [ f0, f1 ]) -> + RegDRep (validate f0) (validate f1) + + pair(5, [ f0 ]) -> + UpdateDRep (validate f0) + + pair(6, [ f0, f1 ]) -> + UnRegDRep (validate f0) (validate f1) + + pair(7, [ f0, f1 ]) -> + PoolRegister (validate f0) (validate f1) + + pair(8, [ f0, f1 ]) -> + PoolRetire (validate f0) (validate f1) + + pair(9, [ f0, f1 ]) -> + AuthHotCommittee (validate f0) (validate f1) + + pair(10, [ f0 ]) -> + ResignColdCommittee (validate f0) + + _ -> + fail + impl Validate Voter where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> CommitteeVoter (validate f0) - pair(1, [f0]) -> DRepVoter (validate f0) - pair(2, [f0]) -> StakePoolVoter (validate f0) - _ -> fail + pair(0, [ f0 ]) -> + CommitteeVoter (validate f0) + + pair(1, [ f0 ]) -> + DRepVoter (validate f0) + + pair(2, [ f0 ]) -> + StakePoolVoter (validate f0) + + _ -> + fail + impl Validate Vote where validate raw = case Builtin.unConstrData raw of - pair(0, []) -> VoteNo - pair(1, []) -> VoteYes - pair(2, []) -> Abstain - _ -> fail + pair(0, []) -> + VoteNo + + pair(1, []) -> + VoteYes + + pair(2, []) -> + Abstain + + _ -> + fail + impl Validate GovernanceActionId where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> GovernanceActionId (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + GovernanceActionId (validate f0) (validate f1) + + _ -> + fail + impl Validate Rational where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> Rational (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + Rational (validate f0) (validate f1) + + _ -> + fail + impl Validate Committee where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> Committee (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + Committee (validate f0) (validate f1) + + _ -> + fail + impl Validate Constitution where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> Constitution (validate f0) - _ -> fail + pair(0, [ f0 ]) -> + Constitution (validate f0) + + _ -> + fail + impl Validate ProtocolVersion where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> ProtocolVersion (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + ProtocolVersion (validate f0) (validate f1) + + _ -> + fail + impl Validate GovernanceAction where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1, f2]) -> ParameterChange (validate f0) (validate f1) (validate f2) - pair(1, [f0, f1]) -> HardForkInitiation (validate f0) (validate f1) - pair(2, [f0, f1]) -> TreasuryWithdrawals (validate f0) (validate f1) - pair(3, [f0]) -> NoConfidence (validate f0) - pair(4, [f0, f1, f2, f3]) -> UpdateCommittee (validate f0) (validate f1) (validate f2) (validate f3) - pair(5, [f0, f1]) -> NewConstitution (validate f0) (validate f1) - pair(6, []) -> InfoAction - _ -> fail + pair(0, [ f0, f1, f2 ]) -> + ParameterChange (validate f0) (validate f1) (validate f2) + + pair(1, [ f0, f1 ]) -> + HardForkInitiation (validate f0) (validate f1) + + pair(2, [ f0, f1 ]) -> + TreasuryWithdrawals (validate f0) (validate f1) + + pair(3, [ f0 ]) -> + NoConfidence (validate f0) + + pair(4, [ f0, f1, f2, f3 ]) -> + UpdateCommittee + (validate f0) + (validate f1) + (validate f2) + (validate f3) + + pair(5, [ f0, f1 ]) -> + NewConstitution (validate f0) (validate f1) + + pair(6, []) -> + InfoAction + + _ -> + fail + impl Validate Proposal where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1, f2]) -> Proposal (validate f0) (validate f1) (validate f2) - _ -> fail + pair(0, [ f0, f1, f2 ]) -> + Proposal (validate f0) (validate f1) (validate f2) + + _ -> + fail + impl Validate OutputReference where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> OutputReference (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + OutputReference (validate f0) (validate f1) + + _ -> + fail + impl Validate Datum where validate raw = case Builtin.unConstrData raw of - pair(0, []) -> NoDatum - pair(1, [f0]) -> DatumHash (validate f0) - pair(2, [f0]) -> InlineDatum (validate f0) - _ -> fail + pair(0, []) -> + NoDatum + + pair(1, [ f0 ]) -> + DatumHash (validate f0) + + pair(2, [ f0 ]) -> + InlineDatum (validate f0) + + _ -> + fail + impl Validate Output where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1, f2, f3]) -> Output (validate f0) (validate f1) (validate f2) (validate f3) - _ -> fail + pair(0, [ f0, f1, f2, f3 ]) -> + Output (validate f0) (validate f1) (validate f2) (validate f3) + + _ -> + fail + impl Validate Input where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1]) -> Input (validate f0) (validate f1) - _ -> fail + pair(0, [ f0, f1 ]) -> + Input (validate f0) (validate f1) + + _ -> + fail + impl Validate ScriptPurpose where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> Minting (validate f0) - pair(1, [f0]) -> Spending (validate f0) - pair(2, [f0]) -> Rewarding (validate f0) - pair(3, [f0, f1]) -> Certifying (validate f0) (validate f1) - pair(4, [f0]) -> Voting (validate f0) - pair(5, [f0, f1]) -> Proposing (validate f0) (validate f1) - _ -> fail + pair(0, [ f0 ]) -> + Minting (validate f0) + + pair(1, [ f0 ]) -> + Spending (validate f0) + + pair(2, [ f0 ]) -> + Rewarding (validate f0) + + pair(3, [ f0, f1 ]) -> + Certifying (validate f0) (validate f1) + + pair(4, [ f0 ]) -> + Voting (validate f0) + + pair(5, [ f0, f1 ]) -> + Proposing (validate f0) (validate f1) + + _ -> + fail + impl Validate ScriptInfo where validate raw = case Builtin.unConstrData raw of - pair(0, [f0]) -> MintingScript (validate f0) - pair(1, [f0, f1]) -> SpendingScript (validate f0) (validate f1) - pair(2, [f0]) -> RewardingScript (validate f0) - pair(3, [f0, f1]) -> CertifyingScript (validate f0) (validate f1) - pair(4, [f0]) -> VotingScript (validate f0) - pair(5, [f0, f1]) -> ProposingScript (validate f0) (validate f1) - _ -> fail + pair(0, [ f0 ]) -> + MintingScript (validate f0) + + pair(1, [ f0, f1 ]) -> + SpendingScript (validate f0) (validate f1) + + pair(2, [ f0 ]) -> + RewardingScript (validate f0) + + pair(3, [ f0, f1 ]) -> + CertifyingScript (validate f0) (validate f1) + + pair(4, [ f0 ]) -> + VotingScript (validate f0) + + pair(5, [ f0, f1 ]) -> + ProposingScript (validate f0) (validate f1) + + _ -> + fail + impl Validate Tx where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15]) -> Tx (validate f0) (validate f1) (validate f2) (validate f3) (validate f4) (validate f5) (validate f6) (validate f7) (validate f8) (validate f9) (validate f10) (validate f11) (validate f12) (validate f13) (validate f14) (validate f15) - _ -> fail + pair(0, [ f0 + , f1 + , f2 + , f3 + , f4 + , f5 + , f6 + , f7 + , f8 + , f9 + , f10 + , f11 + , f12 + , f13 + , f14 + , f15 + ]) -> + Tx + (validate f0) + (validate f1) + (validate f2) + (validate f3) + (validate f4) + (validate f5) + (validate f6) + (validate f7) + (validate f8) + (validate f9) + (validate f10) + (validate f11) + (validate f12) + (validate f13) + (validate f14) + (validate f15) + + _ -> + fail + impl Validate ScriptContext where validate raw = case Builtin.unConstrData raw of - pair(0, [f0, f1, f2]) -> ScriptContext (validate f0) (validate f1) (validate f2) - _ -> fail + pair(0, [ f0, f1, f2 ]) -> + ScriptContext (validate f0) (validate f1) (validate f2) + + _ -> + fail + impl Decode DRep where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (DRep d0) - Constr pair(1, []) -> Some DRepAlwaysAbstain - Constr pair(2, []) -> Some DRepAlwaysNoConfidence - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (DRep d0) + + Constr pair(1, []) -> + Some DRepAlwaysAbstain + + Constr pair(2, []) -> + Some DRepAlwaysNoConfidence + + _ -> + None + impl Decode Delegatee where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (DelegStake d0) - Constr pair(1, [f0]) -> do - d0 <- decode f0 - Some (DelegVote d0) - Constr pair(2, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (DelegStakeVote d0 d1) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (DelegStake d0) + + Constr pair(1, [ f0 ]) -> + do + d0 <- decode f0 + Some (DelegVote d0) + + Constr pair(2, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (DelegStakeVote d0 d1) + + _ -> + None + impl Decode Certificate where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (RegStaking d0 d1) - Constr pair(1, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (UnRegStaking d0 d1) - Constr pair(2, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (DelegStaking d0 d1) - Constr pair(3, [f0, f1, f2]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - Some (RegDeleg d0 d1 d2) - Constr pair(4, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (RegDRep d0 d1) - Constr pair(5, [f0]) -> do - d0 <- decode f0 - Some (UpdateDRep d0) - Constr pair(6, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (UnRegDRep d0 d1) - Constr pair(7, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (PoolRegister d0 d1) - Constr pair(8, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (PoolRetire d0 d1) - Constr pair(9, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (AuthHotCommittee d0 d1) - Constr pair(10, [f0]) -> do - d0 <- decode f0 - Some (ResignColdCommittee d0) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (RegStaking d0 d1) + + Constr pair(1, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (UnRegStaking d0 d1) + + Constr pair(2, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (DelegStaking d0 d1) + + Constr pair(3, [ f0, f1, f2 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + Some (RegDeleg d0 d1 d2) + + Constr pair(4, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (RegDRep d0 d1) + + Constr pair(5, [ f0 ]) -> + do + d0 <- decode f0 + Some (UpdateDRep d0) + + Constr pair(6, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (UnRegDRep d0 d1) + + Constr pair(7, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (PoolRegister d0 d1) + + Constr pair(8, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (PoolRetire d0 d1) + + Constr pair(9, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (AuthHotCommittee d0 d1) + + Constr pair(10, [ f0 ]) -> + do + d0 <- decode f0 + Some (ResignColdCommittee d0) + + _ -> + None + impl Decode Voter where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (CommitteeVoter d0) - Constr pair(1, [f0]) -> do - d0 <- decode f0 - Some (DRepVoter d0) - Constr pair(2, [f0]) -> do - d0 <- decode f0 - Some (StakePoolVoter d0) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (CommitteeVoter d0) + + Constr pair(1, [ f0 ]) -> + do + d0 <- decode f0 + Some (DRepVoter d0) + + Constr pair(2, [ f0 ]) -> + do + d0 <- decode f0 + Some (StakePoolVoter d0) + + _ -> + None + impl Decode Vote where decode raw = case raw of - Constr pair(0, []) -> Some VoteNo - Constr pair(1, []) -> Some VoteYes - Constr pair(2, []) -> Some Abstain - _ -> None + Constr pair(0, []) -> + Some VoteNo + + Constr pair(1, []) -> + Some VoteYes + + Constr pair(2, []) -> + Some Abstain + + _ -> + None + impl Decode GovernanceActionId where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (GovernanceActionId d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (GovernanceActionId d0 d1) + + _ -> + None + impl Decode Rational where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Rational d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Rational d0 d1) + + _ -> + None + impl Decode Committee where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Committee d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Committee d0 d1) + + _ -> + None + impl Decode Constitution where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (Constitution d0) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (Constitution d0) + + _ -> + None + impl Decode ProtocolVersion where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (ProtocolVersion d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (ProtocolVersion d0 d1) + + _ -> + None + impl Decode GovernanceAction where decode raw = case raw of - Constr pair(0, [f0, f1, f2]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - Some (ParameterChange d0 d1 d2) - Constr pair(1, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (HardForkInitiation d0 d1) - Constr pair(2, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (TreasuryWithdrawals d0 d1) - Constr pair(3, [f0]) -> do - d0 <- decode f0 - Some (NoConfidence d0) - Constr pair(4, [f0, f1, f2, f3]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - d3 <- decode f3 - Some (UpdateCommittee d0 d1 d2 d3) - Constr pair(5, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (NewConstitution d0 d1) - Constr pair(6, []) -> Some InfoAction - _ -> None + Constr pair(0, [ f0, f1, f2 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + Some (ParameterChange d0 d1 d2) + + Constr pair(1, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (HardForkInitiation d0 d1) + + Constr pair(2, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (TreasuryWithdrawals d0 d1) + + Constr pair(3, [ f0 ]) -> + do + d0 <- decode f0 + Some (NoConfidence d0) + + Constr pair(4, [ f0, f1, f2, f3 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + d3 <- decode f3 + Some (UpdateCommittee d0 d1 d2 d3) + + Constr pair(5, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (NewConstitution d0 d1) + + Constr pair(6, []) -> + Some InfoAction + + _ -> + None + impl Decode Proposal where decode raw = case raw of - Constr pair(0, [f0, f1, f2]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - Some (Proposal d0 d1 d2) - _ -> None + Constr pair(0, [ f0, f1, f2 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + Some (Proposal d0 d1 d2) + + _ -> + None + impl Decode OutputReference where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (OutputReference d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (OutputReference d0 d1) + + _ -> + None + impl Decode Datum where decode raw = case raw of - Constr pair(0, []) -> Some NoDatum - Constr pair(1, [f0]) -> do - d0 <- decode f0 - Some (DatumHash d0) - Constr pair(2, [f0]) -> do - d0 <- decode f0 - Some (InlineDatum d0) - _ -> None + Constr pair(0, []) -> + Some NoDatum + + Constr pair(1, [ f0 ]) -> + do + d0 <- decode f0 + Some (DatumHash d0) + + Constr pair(2, [ f0 ]) -> + do + d0 <- decode f0 + Some (InlineDatum d0) + + _ -> + None + impl Decode Output where decode raw = case raw of - Constr pair(0, [f0, f1, f2, f3]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - d3 <- decode f3 - Some (Output d0 d1 d2 d3) - _ -> None + Constr pair(0, [ f0, f1, f2, f3 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + d3 <- decode f3 + Some (Output d0 d1 d2 d3) + + _ -> + None + impl Decode Input where decode raw = case raw of - Constr pair(0, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Input d0 d1) - _ -> None + Constr pair(0, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Input d0 d1) + + _ -> + None + impl Decode ScriptPurpose where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (Minting d0) - Constr pair(1, [f0]) -> do - d0 <- decode f0 - Some (Spending d0) - Constr pair(2, [f0]) -> do - d0 <- decode f0 - Some (Rewarding d0) - Constr pair(3, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Certifying d0 d1) - Constr pair(4, [f0]) -> do - d0 <- decode f0 - Some (Voting d0) - Constr pair(5, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (Proposing d0 d1) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (Minting d0) + + Constr pair(1, [ f0 ]) -> + do + d0 <- decode f0 + Some (Spending d0) + + Constr pair(2, [ f0 ]) -> + do + d0 <- decode f0 + Some (Rewarding d0) + + Constr pair(3, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Certifying d0 d1) + + Constr pair(4, [ f0 ]) -> + do + d0 <- decode f0 + Some (Voting d0) + + Constr pair(5, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (Proposing d0 d1) + + _ -> + None + impl Decode ScriptInfo where decode raw = case raw of - Constr pair(0, [f0]) -> do - d0 <- decode f0 - Some (MintingScript d0) - Constr pair(1, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (SpendingScript d0 d1) - Constr pair(2, [f0]) -> do - d0 <- decode f0 - Some (RewardingScript d0) - Constr pair(3, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (CertifyingScript d0 d1) - Constr pair(4, [f0]) -> do - d0 <- decode f0 - Some (VotingScript d0) - Constr pair(5, [f0, f1]) -> do - d0 <- decode f0 - d1 <- decode f1 - Some (ProposingScript d0 d1) - _ -> None + Constr pair(0, [ f0 ]) -> + do + d0 <- decode f0 + Some (MintingScript d0) + + Constr pair(1, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (SpendingScript d0 d1) + + Constr pair(2, [ f0 ]) -> + do + d0 <- decode f0 + Some (RewardingScript d0) + + Constr pair(3, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (CertifyingScript d0 d1) + + Constr pair(4, [ f0 ]) -> + do + d0 <- decode f0 + Some (VotingScript d0) + + Constr pair(5, [ f0, f1 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + Some (ProposingScript d0 d1) + + _ -> + None + impl Decode Tx where decode raw = case raw of - Constr pair(0, [f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - d3 <- decode f3 - d4 <- decode f4 - d5 <- decode f5 - d6 <- decode f6 - d7 <- decode f7 - d8 <- decode f8 - d9 <- decode f9 - d10 <- decode f10 - d11 <- decode f11 - d12 <- decode f12 - d13 <- decode f13 - d14 <- decode f14 - d15 <- decode f15 - Some (Tx d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15) - _ -> None + Constr + pair(0, [ f0 + , f1 + , f2 + , f3 + , f4 + , f5 + , f6 + , f7 + , f8 + , f9 + , f10 + , f11 + , f12 + , f13 + , f14 + , f15 + ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + d3 <- decode f3 + d4 <- decode f4 + d5 <- decode f5 + d6 <- decode f6 + d7 <- decode f7 + d8 <- decode f8 + d9 <- decode f9 + d10 <- decode f10 + d11 <- decode f11 + d12 <- decode f12 + d13 <- decode f13 + d14 <- decode f14 + d15 <- decode f15 + Some + ( + Tx + d0 + d1 + d2 + d3 + d4 + d5 + d6 + d7 + d8 + d9 + d10 + d11 + d12 + d13 + d14 + d15 + ) + + _ -> + None + impl Decode ScriptContext where decode raw = case raw of - Constr pair(0, [f0, f1, f2]) -> do - d0 <- decode f0 - d1 <- decode f1 - d2 <- decode f2 - Some (ScriptContext d0 d1 d2) - _ -> None + Constr pair(0, [ f0, f1, f2 ]) -> + do + d0 <- decode f0 + d1 <- decode f1 + d2 <- decode f2 + Some (ScriptContext d0 d1 d2) + + _ -> + None diff --git a/crates/nash-driver/base/src/Cardano/Value.nash b/crates/nash-driver/base/src/Cardano/Value.nash index 15807062..d01f2e08 100644 --- a/crates/nash-driver/base/src/Cardano/Value.nash +++ b/crates/nash-driver/base/src/Cardano/Value.nash @@ -1,64 +1,99 @@ -module Cardano.Value exposing (Value, empty, singleton, insert, lovelace, quantityOf, add, contains, scale) +module Cardano.Value exposing + ( Value + , empty + , singleton + , insert + , lovelace + , quantityOf + , add + , contains + , scale + ) -import Function exposing ((|>)) -import Option exposing (type option(..)) -import Lift exposing (Lift) -import Data exposing (FromData, ToData, Validate, Decode) -type alias Value = Map Bytes (Map Bytes Int) +import Function exposing ( (|>) ) +import Option exposing ( type option(..) ) +import Lift exposing ( Lift ) +import Data exposing ( FromData, ToData, Validate, Decode ) + + +type alias Value = + Map Bytes (Map Bytes Int) + impl Lift value Value where lift value = value |> Builtin.valueData |> fromData + lower value = value |> toData |> Builtin.unValueData + impl Validate Value where validate raw = let checked : Map Bytes (Map Bytes Int) checked = validate raw in - checked - |> toData - |> fromData + checked + |> toData + |> fromData + empty : value empty = Builtin.unValueData (toData emptyMap) + emptyMap : Value emptyMap = Builtin.mapData [] -singleton : (Lift bytes 'p, Lift bytes 't, Lift int 'n) => 'p -> 't -> 'n -> value + +singleton : ( Lift bytes 'p, Lift bytes 't, Lift int 'n ) + => 'p -> 't -> 'n -> value singleton policy token amount = insert policy token amount empty -insert : (Lift bytes 'p, Lift bytes 't, Lift int 'n, Lift value 'v) => 'p -> 't -> 'n -> 'v -> value -insert policy token amount value = Builtin.insertCoin (lower policy) (lower token) (lower amount) (lower value) + +insert : ( Lift bytes 'p, Lift bytes 't, Lift int 'n, Lift value 'v ) + => 'p -> 't -> 'n -> 'v -> value +insert policy token amount value = + Builtin.insertCoin (lower policy) (lower token) (lower amount) (lower value) + lovelace : Lift value 'v => 'v -> int lovelace value = quantityOf #"" #"" value -quantityOf : (Lift bytes 'p, Lift bytes 't, Lift value 'v) => 'p -> 't -> 'v -> int -quantityOf policy token value = Builtin.lookupCoin (lower policy) (lower token) (lower value) -add : (Lift value 'a, Lift value 'b) => 'a -> 'b -> value +quantityOf : ( Lift bytes 'p, Lift bytes 't, Lift value 'v ) + => 'p -> 't -> 'v -> int +quantityOf policy token value = + Builtin.lookupCoin (lower policy) (lower token) (lower value) + + +add : ( Lift value 'a, Lift value 'b ) => 'a -> 'b -> value add left right = Builtin.unionValue (lower left) (lower right) -contains : (Lift value 'a, Lift value 'b) => 'a -> 'b -> bool + +contains : ( Lift value 'a, Lift value 'b ) => 'a -> 'b -> bool contains left right = Builtin.valueContains (lower left) (lower right) -scale : (Lift int 'n, Lift value 'v) => 'n -> 'v -> value + +scale : ( Lift int 'n, Lift value 'v ) => 'n -> 'v -> value scale amount value = Builtin.scaleValue (lower amount) (lower value) + impl Decode Value where decode raw = - let checked : option (Map Bytes (Map Bytes Int)) + let + checked : option (Map Bytes (Map Bytes Int)) checked = decode raw in - case checked of - Some value -> Some (fromData (toData value)) - None -> None + case checked of + Some value -> + Some (fromData (toData value)) + + None -> + None diff --git a/crates/nash-driver/base/src/Cons.nash b/crates/nash-driver/base/src/Cons.nash index e97a40ef..98ff343b 100644 --- a/crates/nash-driver/base/src/Cons.nash +++ b/crates/nash-driver/base/src/Cons.nash @@ -1,96 +1,169 @@ -module Cons exposing (type cons(..), map, indexedMap, map2, foldr, foldl, append, length, singleton, fromList, toList) +module Cons exposing + ( type cons(..) + , map + , indexedMap + , map2 + , foldr + , foldl + , append + , length + , singleton + , fromList + , toList + ) -import Builtin -import Function exposing ((|>)) -import Lift exposing (Lift) -import Primitive exposing (type bool(..)) +import Builtin +import Function exposing ( (|>) ) +import Lift exposing ( Lift ) +import Primitive exposing ( type bool(..) ) import Literal -import Eq exposing (Eq) -import Show exposing (Show) +import Eq exposing ( Eq ) +import Show exposing ( Show ) import Functor -type cons 'a = Nil | Cons 'a (cons 'a) + +type cons 'a + = Nil + | Cons 'a (cons 'a) + singleton : 'a -> cons 'a singleton value = Cons value Nil + map : ('a -> 'b) -> cons 'a -> cons 'b map = Functor.map + impl Functor.Functor cons where map f values = case values of - Nil -> Nil - Cons x xs -> Cons (f x) (Functor.map f xs) + Nil -> + Nil + + Cons x xs -> + Cons (f x) (Functor.map f xs) + indexedMap : (int -> 'a -> 'b) -> cons 'a -> cons 'b indexedMap f values = indexedFrom 0 f values + indexedFrom : int -> (int -> 'a -> 'b) -> cons 'a -> cons 'b indexedFrom index f values = case values of - Nil -> Nil - Cons x xs -> Cons (f index x) (indexedFrom (Builtin.addInteger index 1) f xs) + Nil -> + Nil + + Cons x xs -> + Cons (f index x) (indexedFrom (Builtin.addInteger index 1) f xs) + map2 : ('a -> 'b -> 'c) -> cons 'a -> cons 'b -> cons 'c map2 f left right = - case (left, right) of - (Cons x xs, Cons y ys) -> Cons (f x y) (map2 f xs ys) - _ -> Nil + case ( left, right ) of + ( Cons x xs, Cons y ys ) -> + Cons (f x y) (map2 f xs ys) + + _ -> + Nil + foldr : ('a -> 'b -> 'b) -> 'b -> cons 'a -> 'b foldr f acc values = case values of - Nil -> acc - Cons x xs -> f x (foldr f acc xs) + Nil -> + acc + + Cons x xs -> + f x (foldr f acc xs) + foldl : ('a -> 'b -> 'b) -> 'b -> cons 'a -> 'b foldl f acc values = case values of - Nil -> acc - Cons x xs -> foldl f (f x acc) xs + Nil -> + acc + + Cons x xs -> + foldl f (f x acc) xs + append : cons 'a -> cons 'a -> cons 'a append left right = case left of - Nil -> right - Cons x xs -> Cons x (append xs right) + Nil -> + right + + Cons x xs -> + Cons x (append xs right) + length : cons 'a -> int length values = foldl (\_ count -> Builtin.addInteger count 1) 0 values + fromList : Lift (list 'a) ('f 'a) => 'f 'a -> cons 'a fromList values = values |> lower |> fromLittleList + fromLittleList : list 'a -> cons 'a fromLittleList values = case values of - [] -> Nil - x :: xs -> Cons x (fromLittleList xs) + [] -> + Nil + + x :: xs -> + Cons x (fromLittleList xs) + toList : cons 'a -> list 'a toList values = case values of - Nil -> [] - Cons x xs -> Builtin.mkCons x (toList xs) + Nil -> + [] + + Cons x xs -> + Builtin.mkCons x (toList xs) + impl Eq 'a => Eq (cons 'a) where eq left right = - case (left, right) of - (Nil, Nil) -> True - (Cons x xs, Cons y ys) -> if eq x y then eq xs ys else False - _ -> False + case ( left, right ) of + ( Nil, Nil ) -> + True + + ( Cons x xs, Cons y ys ) -> + if eq x y then + eq xs ys + else + False + + _ -> + False + impl Show 'a => Show (cons 'a) where - show values = Builtin.appendString "Cons [" (Builtin.appendString (showItems values) "]") + show values = + Builtin.appendString + "Cons [" + (Builtin.appendString (showItems values) "]") + showItems : Show 'a => cons 'a -> string showItems values = case values of - Nil -> "" - Cons x Nil -> show x - Cons x xs -> Builtin.appendString (show x) (Builtin.appendString ", " (showItems xs)) + Nil -> + "" + + Cons x Nil -> + show x + + Cons x xs -> + Builtin.appendString + (show x) + (Builtin.appendString ", " (showItems xs)) diff --git a/crates/nash-driver/base/src/Crypto.nash b/crates/nash-driver/base/src/Crypto.nash index e72aa39b..22ec8279 100644 --- a/crates/nash-driver/base/src/Crypto.nash +++ b/crates/nash-driver/base/src/Crypto.nash @@ -1,8 +1,20 @@ -module Crypto exposing (sha2_256, sha3_256, blake2b_224, blake2b_256, keccak_256, ripemd_160, verifyEd25519Signature, verifyEcdsaSecp256k1Signature, verifySchnorrSecp256k1Signature) +module Crypto exposing + ( sha2_256 + , sha3_256 + , blake2b_224 + , blake2b_256 + , keccak_256 + , ripemd_160 + , verifyEd25519Signature + , verifyEcdsaSecp256k1Signature + , verifySchnorrSecp256k1Signature + ) -import Lift exposing (Lift) + +import Lift exposing ( Lift ) import Builtin -import Function exposing ((|>)) +import Function exposing ( (|>) ) + sha2_256 : Lift bytes 'a => 'a -> bytes sha2_256 value = @@ -10,41 +22,64 @@ sha2_256 value = |> lower |> Builtin.sha2_256 + sha3_256 : Lift bytes 'a => 'a -> bytes sha3_256 value = value |> lower |> Builtin.sha3_256 + blake2b_224 : Lift bytes 'a => 'a -> bytes blake2b_224 value = value |> lower |> Builtin.blake2b_224 + blake2b_256 : Lift bytes 'a => 'a -> bytes blake2b_256 value = value |> lower |> Builtin.blake2b_256 + keccak_256 : Lift bytes 'a => 'a -> bytes keccak_256 value = value |> lower |> Builtin.keccak_256 + ripemd_160 : Lift bytes 'a => 'a -> bytes ripemd_160 value = value |> lower |> Builtin.ripemd_160 -verifyEd25519Signature : (Lift bytes 'k, Lift bytes 'm, Lift bytes 's) => 'k -> 'm -> 's -> bool -verifyEd25519Signature key message signature = Builtin.verifyEd25519Signature (lower key) (lower message) (lower signature) -verifyEcdsaSecp256k1Signature : (Lift bytes 'k, Lift bytes 'm, Lift bytes 's) => 'k -> 'm -> 's -> bool -verifyEcdsaSecp256k1Signature key message signature = Builtin.verifyEcdsaSecp256k1Signature (lower key) (lower message) (lower signature) +verifyEd25519Signature : ( Lift bytes 'k, Lift bytes 'm, Lift bytes 's ) + => 'k -> 'm -> 's -> bool +verifyEd25519Signature key message signature = + Builtin.verifyEd25519Signature (lower key) (lower message) (lower signature) + + +verifyEcdsaSecp256k1Signature : ( Lift bytes 'k, Lift bytes 'm, Lift bytes 's ) + => 'k -> 'm -> 's -> bool +verifyEcdsaSecp256k1Signature key message signature = + Builtin.verifyEcdsaSecp256k1Signature + (lower key) + (lower message) + (lower signature) + -verifySchnorrSecp256k1Signature : (Lift bytes 'k, Lift bytes 'm, Lift bytes 's) => 'k -> 'm -> 's -> bool -verifySchnorrSecp256k1Signature key message signature = Builtin.verifySchnorrSecp256k1Signature (lower key) (lower message) (lower signature) +verifySchnorrSecp256k1Signature : ( Lift bytes 'k + , Lift bytes 'm + , Lift bytes 's + ) + => 'k -> 'm -> 's -> bool +verifySchnorrSecp256k1Signature key message signature = + Builtin.verifySchnorrSecp256k1Signature + (lower key) + (lower message) + (lower signature) diff --git a/crates/nash-driver/base/src/Data.nash b/crates/nash-driver/base/src/Data.nash index d1f33eea..a9217de2 100644 --- a/crates/nash-driver/base/src/Data.nash +++ b/crates/nash-driver/base/src/Data.nash @@ -1,58 +1,88 @@ -module Data exposing (ToData, FromData, Validate, Decode, serialise, tag, fields) +module Data exposing + ( ToData + , FromData + , Validate + , Decode + , serialise + , tag + , fields + ) + import Builtin -import Function exposing ((|>)) +import Function exposing ( (|>) ) import Literal -import Eq exposing (Eq) -import Option exposing (Option, type option(..)) -import Monad exposing (Monad) +import Eq exposing ( Eq ) +import Option exposing ( Option, type option(..) ) +import Monad exposing ( Monad ) +import Primitive exposing ( Big, Data(..), type bool(..) ) -import Primitive exposing (Big, Data(..), type bool(..)) trait ToData 'a where toData : 'a -> Data + impl ToData ('a : Big) where toData = Primitive.coerce + trait FromData 'a where fromData : Data -> 'a + impl FromData ('a : Big) where fromData = Primitive.coerce + trait Validate ('a : Big) where validate : Data -> 'a + impl Validate Data where validate value = value + impl Validate Int where validate value = let candidate : Int candidate = Primitive.coerce value + _ = Builtin.unIData candidate in - candidate + candidate + impl Validate Bytes where validate value = let candidate : Bytes candidate = Primitive.coerce value + _ = Builtin.unBData candidate in - candidate + candidate + serialise : Data -> bytes serialise = Builtin.serialiseData + tag : Data -> int -tag value = let pair(index, _) = Builtin.unConstrData value in index +tag value = + let + pair(index, _) = Builtin.unConstrData value + in + index + fields : Data -> list Data -fields value = let pair(_, items) = Builtin.unConstrData value in items +fields value = + let + pair(_, items) = Builtin.unConstrData value + in + items + impl Validate 'a => Validate (List 'a) where validate value = @@ -60,57 +90,79 @@ impl Validate 'a => Validate (List 'a) where candidate : List Data candidate = Primitive.coerce value in - candidate - |> Builtin.unListData - |> mapList validate - |> Builtin.listData + candidate + |> Builtin.unListData + |> mapList validate + |> Builtin.listData + -impl (Validate 'k, Validate 'v) => Validate (Map 'k 'v) where +impl ( Validate 'k, Validate 'v ) => Validate (Map 'k 'v) where validate value = let candidate : Map Data Data candidate = Primitive.coerce value in - candidate - |> Builtin.unMapData - |> mapList pairValidate - |> Builtin.mapData + candidate + |> Builtin.unMapData + |> mapList pairValidate + |> Builtin.mapData + mapList : ('a -> 'b) -> list 'a -> list 'b mapList f values = case values of - [] -> [] - first :: rest -> Builtin.mkCons (f first) (mapList f rest) + [] -> + [] + + first :: rest -> + Builtin.mkCons (f first) (mapList f rest) + + +pairValidate : ( Validate 'k, Validate 'v ) => pair Data Data -> pair 'k 'v +pairValidate pair(key, value) = + Builtin.mkPairData (validate key) (validate value) -pairValidate : (Validate 'k, Validate 'v) => pair Data Data -> pair 'k 'v -pairValidate pair(key, value) = Builtin.mkPairData (validate key) (validate value) impl Validate 'a => Validate (Option 'a) where validate value = case Builtin.unConstrData value of - pair(0, [item]) -> Option.Some (validate item) - pair(1, []) -> Option.None - _ -> fail + pair(0, [ item ]) -> + Option.Some (validate item) + + pair(1, []) -> + Option.None + + _ -> + fail + trait Decode 'a where decode : Data -> option 'a + impl ToData int where toData value = value |> Builtin.iData |> Primitive.coerce + impl FromData int where fromData value = value |> Primitive.coerce |> Builtin.unIData + impl Decode int where - decode value = case value of - I number -> Some number - _ -> None + decode value = + case value of + I number -> + Some number + + _ -> + None + impl ToData bytes where toData value = @@ -118,16 +170,23 @@ impl ToData bytes where |> Builtin.bData |> Primitive.coerce + impl FromData bytes where fromData value = value |> Primitive.coerce |> Builtin.unBData + impl Decode bytes where - decode value = case value of - B raw -> Some raw - _ -> None + decode value = + case value of + B raw -> + Some raw + + _ -> + None + impl ToData string where toData value = @@ -135,52 +194,99 @@ impl ToData string where |> Builtin.encodeUtf8 |> toData + impl FromData string where fromData value = value |> fromData |> Builtin.decodeUtf8 + impl Decode Data where decode value = Some value + impl Decode Int where - decode value = case value of - I _ -> Some (fromData value) - _ -> None + decode value = + case value of + I _ -> + Some (fromData value) + + _ -> + None + impl Decode Bytes where - decode value = case value of - B _ -> Some (fromData value) - _ -> None + decode value = + case value of + B _ -> + Some (fromData value) + + _ -> + None + impl ToData bool where - toData value = Builtin.constrData (if value then 1 else 0) [] + toData value = + Builtin.constrData + ( + if value then + 1 + else + 0 + ) + [] + impl FromData bool where - fromData value = case Builtin.unConstrData value of - pair(0, []) -> False - pair(1, []) -> True - _ -> fail + fromData value = + case Builtin.unConstrData value of + pair(0, []) -> + False + + pair(1, []) -> + True + + _ -> + fail + impl Decode bool where - decode value = case value of - Constr pair(0, []) -> Some False - Constr pair(1, []) -> Some True - _ -> None + decode value = + case value of + Constr pair(0, []) -> + Some False + + Constr pair(1, []) -> + Some True + + _ -> + None + impl ToData unit where toData _ = Builtin.constrData 0 [] + impl FromData unit where - fromData value = case Builtin.unConstrData value of - pair(0, []) -> () - _ -> fail + fromData value = + case Builtin.unConstrData value of + pair(0, []) -> + () + + _ -> + fail + impl Decode unit where - decode value = case value of - Constr pair(0, []) -> Some () - _ -> None + decode value = + case value of + Constr pair(0, []) -> + Some () + + _ -> + None + impl ToData 'a => ToData (list 'a) where toData values = @@ -189,6 +295,7 @@ impl ToData 'a => ToData (list 'a) where |> Builtin.listData |> Primitive.coerce + impl FromData 'a => FromData (list 'a) where fromData value = value @@ -196,97 +303,197 @@ impl FromData 'a => FromData (list 'a) where |> Builtin.unListData |> mapList fromData + impl Decode 'a => Decode (list 'a) where - decode value = case value of - List items -> decodeList items - _ -> None + decode value = + case value of + List items -> + decodeList items + + _ -> + None + decodeList : Decode 'a => list Data -> option (list 'a) -decodeList values = case values of - [] -> Some [] - first :: rest -> do - item <- decode first - items <- decodeList rest - Some (Builtin.mkCons item items) - -impl (Big 'a, Decode 'a) => Decode (List 'a) where - decode value = case value of - List items -> do - decoded <- decodeList items - Some (Builtin.listData decoded) - _ -> None - -impl (Big 'k, Big 'v, Decode 'k, Decode 'v) => Decode (Map 'k 'v) where - decode value = case value of - Map entries -> do - decoded <- decodeEntries entries - Some (Builtin.mapData decoded) - _ -> None - -decodeEntries : (Big 'k, Big 'v, Decode 'k, Decode 'v) => list (pair Data Data) -> option (list (pair 'k 'v)) -decodeEntries entries = case entries of - [] -> Some [] - pair(key, value) :: rest -> do - k <- decode key - v <- decode value - tail <- decodeEntries rest - Some (Builtin.mkCons (Builtin.mkPairData k v) tail) +decodeList values = + case values of + [] -> + Some [] + + first :: rest -> + do + item <- decode first + items <- decodeList rest + Some (Builtin.mkCons item items) + + +impl ( Big 'a, Decode 'a ) => Decode (List 'a) where + decode value = + case value of + List items -> + do + decoded <- decodeList items + Some (Builtin.listData decoded) + + _ -> + None + + +impl ( Big 'k, Big 'v, Decode 'k, Decode 'v ) => Decode (Map 'k 'v) where + decode value = + case value of + Map entries -> + do + decoded <- decodeEntries entries + Some (Builtin.mapData decoded) + + _ -> + None + + +decodeEntries : ( Big 'k, Big 'v, Decode 'k, Decode 'v ) + => list (pair Data Data) -> option (list (pair 'k 'v)) +decodeEntries entries = + case entries of + [] -> + Some [] + + pair(key, value) :: rest -> + do + k <- decode key + v <- decode value + tail <- decodeEntries rest + Some (Builtin.mkCons (Builtin.mkPairData k v) tail) + impl ToData 'a => ToData (option 'a) where - toData value = case value of - Some item -> Builtin.constrData 0 [toData item] - None -> Builtin.constrData 1 [] + toData value = + case value of + Some item -> + Builtin.constrData 0 [ toData item ] + + None -> + Builtin.constrData 1 [] + impl FromData 'a => FromData (option 'a) where - fromData value = case Builtin.unConstrData value of - pair(0, [item]) -> Some (fromData item) - pair(1, []) -> None - _ -> fail + fromData value = + case Builtin.unConstrData value of + pair(0, [ item ]) -> + Some (fromData item) + + pair(1, []) -> + None + + _ -> + fail + impl Decode 'a => Decode (option 'a) where - decode value = case value of - Constr pair(0, [item]) -> do - decoded <- decode item - Some (Some decoded) - Constr pair(1, []) -> Some None - _ -> None - -impl (Big 'a, Decode 'a) => Decode (Option 'a) where - decode value = case value of - Constr pair(0, [item]) -> do - decoded <- decode item - Some (Option.Some decoded) - Constr pair(1, []) -> Some Option.None - _ -> None + decode value = + case value of + Constr pair(0, [ item ]) -> + do + decoded <- decode item + Some (Some decoded) + + Constr pair(1, []) -> + Some None + + _ -> + None + + +impl ( Big 'a, Decode 'a ) => Decode (Option 'a) where + decode value = + case value of + Constr pair(0, [ item ]) -> + do + decoded <- decode item + Some (Option.Some decoded) + + Constr pair(1, []) -> + Some Option.None + + _ -> + None + impl Decode string where - decode value = case value of - B raw -> - if utf8 raw (Builtin.lengthOfByteString raw) 0 then Some (Builtin.decodeUtf8 raw) - else None - _ -> None + decode value = + case value of + B raw -> + if utf8 raw (Builtin.lengthOfByteString raw) 0 then + Some (Builtin.decodeUtf8 raw) + else + None + + _ -> + None + utf8 : bytes -> int -> int -> bool utf8 raw size index = - if Builtin.equalsInteger index size then True + if Builtin.equalsInteger index size then + True else - let first = Builtin.indexByteString raw index in - if Builtin.lessThanInteger first 128 then utf8 raw size (Builtin.addInteger index 1) - else if (if Builtin.lessThanEqualsInteger 194 first then Builtin.lessThanEqualsInteger first 223 else False) then continuation raw size (Builtin.addInteger index 1) 1 128 191 - else if Builtin.equalsInteger first 224 then continuation raw size (Builtin.addInteger index 1) 2 160 191 - else if (if Builtin.lessThanEqualsInteger 225 first then Builtin.lessThanEqualsInteger first 236 else False) then continuation raw size (Builtin.addInteger index 1) 2 128 191 - else if Builtin.equalsInteger first 237 then continuation raw size (Builtin.addInteger index 1) 2 128 159 - else if (if Builtin.lessThanEqualsInteger 238 first then Builtin.lessThanEqualsInteger first 239 else False) then continuation raw size (Builtin.addInteger index 1) 2 128 191 - else if Builtin.equalsInteger first 240 then continuation raw size (Builtin.addInteger index 1) 3 144 191 - else if (if Builtin.lessThanEqualsInteger 241 first then Builtin.lessThanEqualsInteger first 243 else False) then continuation raw size (Builtin.addInteger index 1) 3 128 191 - else if Builtin.equalsInteger first 244 then continuation raw size (Builtin.addInteger index 1) 3 128 143 - else False + let + first = Builtin.indexByteString raw index + in + if Builtin.lessThanInteger first 128 then + utf8 raw size (Builtin.addInteger index 1) + else if if Builtin.lessThanEqualsInteger 194 first then + Builtin.lessThanEqualsInteger first 223 + else + False then + continuation raw size (Builtin.addInteger index 1) 1 128 191 + else if Builtin.equalsInteger first 224 then + continuation raw size (Builtin.addInteger index 1) 2 160 191 + else if if Builtin.lessThanEqualsInteger 225 first then + Builtin.lessThanEqualsInteger first 236 + else + False then + continuation raw size (Builtin.addInteger index 1) 2 128 191 + else if Builtin.equalsInteger first 237 then + continuation raw size (Builtin.addInteger index 1) 2 128 159 + else if if Builtin.lessThanEqualsInteger 238 first then + Builtin.lessThanEqualsInteger first 239 + else + False then + continuation raw size (Builtin.addInteger index 1) 2 128 191 + else if Builtin.equalsInteger first 240 then + continuation raw size (Builtin.addInteger index 1) 3 144 191 + else if if Builtin.lessThanEqualsInteger 241 first then + Builtin.lessThanEqualsInteger first 243 + else + False then + continuation raw size (Builtin.addInteger index 1) 3 128 191 + else if Builtin.equalsInteger first 244 then + continuation raw size (Builtin.addInteger index 1) 3 128 143 + else + False + continuation : bytes -> int -> int -> int -> int -> int -> bool continuation raw size index remaining low high = - if Builtin.equalsInteger remaining 0 then utf8 raw size index - else if Builtin.lessThanEqualsInteger size index then False + if Builtin.equalsInteger remaining 0 then + utf8 raw size index + else if Builtin.lessThanEqualsInteger size index then + False else - let next = Builtin.indexByteString raw index in - if (if Builtin.lessThanInteger next low then True else Builtin.lessThanInteger high next) then False - else continuation raw size (Builtin.addInteger index 1) (Builtin.subtractInteger remaining 1) 128 191 + let + next = Builtin.indexByteString raw index + in + if if Builtin.lessThanInteger next low then + True + else + Builtin.lessThanInteger high next then + False + else + continuation + raw + size + (Builtin.addInteger index 1) + (Builtin.subtractInteger remaining 1) + 128 + 191 diff --git a/crates/nash-driver/base/src/Eq.nash b/crates/nash-driver/base/src/Eq.nash index 094e02a3..431f12a8 100644 --- a/crates/nash-driver/base/src/Eq.nash +++ b/crates/nash-driver/base/src/Eq.nash @@ -1,47 +1,83 @@ -module Eq exposing (Eq) +module Eq exposing ( Eq ) + import Literal import Primitive exposing (..) import Builtin exposing (..) + trait Eq 'a where eq : 'a -> 'a -> bool neq : 'a -> 'a -> bool - neq a b = if eq a b then False else True + neq a b = + if eq a b then + False + else + True + impl Eq int where eq = Builtin.equalsInteger + impl Eq bytes where eq = Builtin.equalsByteString + impl Eq string where eq = Builtin.equalsString + impl Eq bool where - eq a b = if a then b else if b then False else True + eq a b = + if a then + b + else if b then + False + else + True + impl Eq unit where eq _ _ = True + impl Eq 'a => Eq (list 'a) where eq xs ys = case xs of - [] -> case ys of - [] -> True - _ :: _ -> False - x :: restX -> case ys of - [] -> False - y :: restY -> if eq x y then eq restX restY else False - -impl (Eq 'a, Eq 'b) => Eq (pair 'a 'b) where + [] -> + case ys of + [] -> + True + + _ :: _ -> + False + + x :: restX -> + case ys of + [] -> + False + + y :: restY -> + if eq x y then + eq restX restY + else + False + + +impl ( Eq 'a, Eq 'b ) => Eq (pair 'a 'b) where eq a b = let pair(firstA, secondA) = a + pair(firstB, secondB) = b in - if eq firstA firstB then eq secondA secondB else False + if eq firstA firstB then + eq secondA secondB + else + False + impl Eq value where eq a b = Builtin.equalsData (Builtin.valueData a) (Builtin.valueData b) diff --git a/crates/nash-driver/base/src/Function.nash b/crates/nash-driver/base/src/Function.nash index e4dcfb8a..21d0b5c2 100644 --- a/crates/nash-driver/base/src/Function.nash +++ b/crates/nash-driver/base/src/Function.nash @@ -1,10 +1,15 @@ -module Function exposing (applyForward, applyBackward, (|>), (<|)) +module Function exposing ( applyForward, applyBackward, (|>), (<|) ) + infix left 0 (|>) = applyForward + + infix right 0 (<|) = applyBackward + applyForward : 'a -> ('a -> 'b) -> 'b applyForward x f = f x + applyBackward : ('a -> 'b) -> 'a -> 'b applyBackward f x = f x diff --git a/crates/nash-driver/base/src/Functor.nash b/crates/nash-driver/base/src/Functor.nash index 411aa021..0e58c0a9 100644 --- a/crates/nash-driver/base/src/Functor.nash +++ b/crates/nash-driver/base/src/Functor.nash @@ -1,15 +1,22 @@ -module Functor exposing (Functor) +module Functor exposing ( Functor ) + import Builtin + trait Functor 'f where map : ('a -> 'b) -> 'f 'a -> 'f 'b + impl Functor list where map = mapList + mapList : ('a -> 'b) -> list 'a -> list 'b mapList f values = case values of - [] -> [] - x :: xs -> Builtin.mkCons (f x) (mapList f xs) + [] -> + [] + + x :: xs -> + Builtin.mkCons (f x) (mapList f xs) diff --git a/crates/nash-driver/base/src/Int.nash b/crates/nash-driver/base/src/Int.nash index 24e75485..0b9411d7 100644 --- a/crates/nash-driver/base/src/Int.nash +++ b/crates/nash-driver/base/src/Int.nash @@ -1,109 +1,187 @@ -module Int exposing (add, sub, mul, negate, div, mod, quot, rem, compare, min, max, abs, pow, pow2, powMod, gcd, lcm, isqrt, isGcd, isLcm, isSqrt, toBytes, fromBytes, toString) - -import Primitive exposing (type bool(..)) +module Int exposing + ( add + , sub + , mul + , negate + , div + , mod + , quot + , rem + , compare + , min + , max + , abs + , pow + , pow2 + , powMod + , gcd + , lcm + , isqrt + , isGcd + , isLcm + , isSqrt + , toBytes + , fromBytes + , toString + ) + + +import Primitive exposing ( type bool(..) ) import Prelude exposing (..) -import Lift exposing (Lift) +import Lift exposing ( Lift ) import Builtin import Show import Ord -import Ordering exposing (type ordering) +import Ordering exposing ( type ordering ) + -add : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int +add : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int add a b = Builtin.addInteger (lower a) (lower b) -sub : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +sub : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int sub a b = Builtin.subtractInteger (lower a) (lower b) -mul : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +mul : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int mul a b = Builtin.multiplyInteger (lower a) (lower b) -div : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +div : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int div a b = Builtin.divideInteger (lower a) (lower b) -mod : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +mod : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int mod a b = Builtin.modInteger (lower a) (lower b) -quot : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +quot : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int quot a b = Builtin.quotientInteger (lower a) (lower b) -rem : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +rem : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int rem a b = Builtin.remainderInteger (lower a) (lower b) + negate : Lift int 'a => 'a -> int negate value = value |> lower |> Builtin.subtractInteger 0 -compare : (Lift int 'a, Lift int 'b) => 'a -> 'b -> ordering + +compare : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> ordering compare a b = compareInt (lower a) (lower b) + compareInt : int -> int -> ordering compareInt = Ord.compare -min : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +min : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int min a b = minInt (lower a) (lower b) + minInt : int -> int -> int minInt = Ord.min -max : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +max : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int max a b = maxInt (lower a) (lower b) + maxInt : int -> int -> int maxInt = Ord.max + abs : Lift int 'n => 'n -> int abs value = let n : int n = lower value - in if n < 0 then Builtin.subtractInteger 0 n else n + in + if n < 0 then + Builtin.subtractInteger 0 n + else + n + -pow : (Lift int 'n, Lift int 'e) => 'n -> 'e -> int +pow : ( Lift int 'n, Lift int 'e ) => 'n -> 'e -> int pow value exponent = let e : int e = lower exponent in - if e < 0 then fail - else - let n : int - n = lower value - in if n == 2 then powerOfTwo e else power n e + if e < 0 then + fail + else + let + n : int + n = lower value + in + if n == 2 then + powerOfTwo e + else + power n e + power : int -> int -> int power n e = - if e == 0 then 1 + if e == 0 then + 1 else - let half = power n (quot e 2) in - if rem e 2 == 0 then half * half else n * half * half + let + half = power n (quot e 2) + in + if rem e 2 == 0 then + half * half + else + n * half * half + pow2 : Lift int 'e => 'e -> int pow2 exponent = - let e : int + let + e : int e = lower exponent - in if e < 0 then fail else powerOfTwo e + in + if e < 0 then + fail + else + powerOfTwo e + smallPowersOfTwo : array int -smallPowersOfTwo = comptime (Builtin.listToArray [1, 2, 4, 8, 16, 32, 64, 128, 256]) +smallPowersOfTwo = + comptime (Builtin.listToArray [ 1, 2, 4, 8, 16, 32, 64, 128, 256 ]) + powerOfTwo : int -> int powerOfTwo e = - if e <= 8 then Builtin.indexArray smallPowersOfTwo e + if e <= 8 then + Builtin.indexArray smallPowersOfTwo e else - let bands = quot (e - 1) 8 + let + bands = quot (e - 1) 8 + bits = rem (e - 1) 8 + 1 - in power 256 bands * Builtin.indexArray smallPowersOfTwo bits + in + power 256 bands * Builtin.indexArray smallPowersOfTwo bits + -powMod : (Lift int 'n, Lift int 'e, Lift int 'm) => 'n -> 'e -> 'm -> int +powMod : ( Lift int 'n, Lift int 'e, Lift int 'm ) => 'n -> 'e -> 'm -> int powMod n e m = Builtin.expModInteger (lower n) (lower e) (lower m) -toBytes : (Lift bool 'b, Lift int 's, Lift int 'n) => 'b -> 's -> 'n -> bytes -toBytes bigEndian size n = Builtin.integerToByteString (lower bigEndian) (lower size) (lower n) -fromBytes : (Lift bool 'b, Lift bytes 'v) => 'b -> 'v -> int -fromBytes bigEndian value = Builtin.byteStringToInteger (lower bigEndian) (lower value) +toBytes : ( Lift bool 'b, Lift int 's, Lift int 'n ) => 'b -> 's -> 'n -> bytes +toBytes bigEndian size n = + Builtin.integerToByteString (lower bigEndian) (lower size) (lower n) + + +fromBytes : ( Lift bool 'b, Lift bytes 'v ) => 'b -> 'v -> int +fromBytes bigEndian value = + Builtin.byteStringToInteger (lower bigEndian) (lower value) + toString : Lift int 'n => 'n -> string toString value = @@ -111,78 +189,134 @@ toString value = |> lower |> showInt + showInt : int -> string showInt = Show.show -gcd : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +gcd : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int gcd left right = gcdInt (abs left) (abs right) + gcdInt : int -> int -> int -gcdInt a b = if b == 0 then a else gcdInt b (Builtin.modInteger a b) +gcdInt a b = + if b == 0 then + a + else + gcdInt b (Builtin.modInteger a b) -lcm : (Lift int 'a, Lift int 'b) => 'a -> 'b -> int + +lcm : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int lcm left right = - let a : int + let + a : int a = lower left + b : int b = lower right in - if a == 0 || b == 0 then 0 - else abs (Builtin.quotientInteger a (gcd a b) * b) + if a == 0 || b == 0 then + 0 + else + abs (Builtin.quotientInteger a (gcd a b) * b) + isqrt : Lift int 'n => 'n -> int isqrt value = - let n : int + let + n : int n = lower value in - if n < 0 then fail - else if n < 2 then n - else sqrtNewton n (sqrtUpper n 1) + if n < 0 then + fail + else if n < 2 then + n + else + sqrtNewton n (sqrtUpper n 1) + sqrtUpper : int -> int -> int sqrtUpper remaining bound = - if remaining == 0 then bound - else sqrtUpper (Builtin.quotientInteger remaining 4) (bound * 2) + if remaining == 0 then + bound + else + sqrtUpper (Builtin.quotientInteger remaining 4) (bound * 2) + sqrtNewton : int -> int -> int sqrtNewton n current = - let next = Builtin.quotientInteger (current + Builtin.quotientInteger n current) 2 in - if next >= current then current else sqrtNewton n next + let + next = + Builtin.quotientInteger + (current + Builtin.quotientInteger n current) + 2 + in + if next >= current then + current + else + sqrtNewton n next -isGcd : (Lift int 'a, Lift int 'b, Lift int 'g) => 'a -> 'b -> 'g -> bool + +isGcd : ( Lift int 'a, Lift int 'b, Lift int 'g ) => 'a -> 'b -> 'g -> bool isGcd left right expected = - let a = abs left + let + a = abs left + b = abs right + g : int g = lower expected in - if g < 0 then False - else if g == 0 then a == 0 && b == 0 - else if g == 1 then gcdInt a b == 1 - else if Builtin.modInteger a g /= 0 then False - else if Builtin.modInteger b g /= 0 then False - else gcdInt (Builtin.quotientInteger a g) (Builtin.quotientInteger b g) == 1 - -isLcm : (Lift int 'a, Lift int 'b, Lift int 'm) => 'a -> 'b -> 'm -> bool + if g < 0 then + False + else if g == 0 then + a == 0 && b == 0 + else if g == 1 then + gcdInt a b == 1 + else if Builtin.modInteger a g /= 0 then + False + else if Builtin.modInteger b g /= 0 then + False + else + gcdInt (Builtin.quotientInteger a g) (Builtin.quotientInteger b g) == + 1 + + +isLcm : ( Lift int 'a, Lift int 'b, Lift int 'm ) => 'a -> 'b -> 'm -> bool isLcm left right expected = - let a = abs left + let + a = abs left + b = abs right + m : int m = lower expected in - if m < 0 then False - else if a == 0 || b == 0 then m == 0 - else if m == 0 then False - else if Builtin.modInteger m a /= 0 then False - else if Builtin.modInteger m b /= 0 then False - else gcdInt (Builtin.quotientInteger m a) (Builtin.quotientInteger m b) == 1 - -isSqrt : (Lift int 'n, Lift int 'r) => 'n -> 'r -> bool + if m < 0 then + False + else if a == 0 || b == 0 then + m == 0 + else if m == 0 then + False + else if Builtin.modInteger m a /= 0 then + False + else if Builtin.modInteger m b /= 0 then + False + else + gcdInt (Builtin.quotientInteger m a) (Builtin.quotientInteger m b) == + 1 + + +isSqrt : ( Lift int 'n, Lift int 'r ) => 'n -> 'r -> bool isSqrt value expected = - let n : int + let + n : int n = lower value + root : int root = lower expected in - if n < 0 || root < 0 then False - else root * root <= n && n < (root + 1) * (root + 1) + if n < 0 || root < 0 then + False + else + root * root <= n && n < (root + 1) * (root + 1) diff --git a/crates/nash-driver/base/src/Integral.nash b/crates/nash-driver/base/src/Integral.nash index de90babc..846fc0db 100644 --- a/crates/nash-driver/base/src/Integral.nash +++ b/crates/nash-driver/base/src/Integral.nash @@ -1,16 +1,25 @@ -module Integral exposing (Integral) +module Integral exposing ( Integral ) + import Builtin -import Num exposing (Num) +import Num exposing ( Num ) + trait Num 'a => Integral 'a where div : 'a -> 'a -> 'a + mod : 'a -> 'a -> 'a + quot : 'a -> 'a -> 'a + rem : 'a -> 'a -> 'a + impl Integral int where div = Builtin.divideInteger + mod = Builtin.modInteger + quot = Builtin.quotientInteger + rem = Builtin.remainderInteger diff --git a/crates/nash-driver/base/src/Lift.nash b/crates/nash-driver/base/src/Lift.nash index 622b0b7e..5b6ad966 100644 --- a/crates/nash-driver/base/src/Lift.nash +++ b/crates/nash-driver/base/src/Lift.nash @@ -1,25 +1,35 @@ -module Lift exposing (Lift) +module Lift exposing ( Lift ) + import Builtin +import Primitive exposing ( Big ) -import Primitive exposing (Big) trait Lift 'small 'big where lift : 'small -> 'big + lower : 'big -> 'small + impl Lift int Int where lift = Builtin.iData + lower = Builtin.unIData + impl Lift bytes Bytes where lift = Builtin.bData + lower = Builtin.unBData + impl Lift (list ('a : Big)) (List 'a) where lift = Builtin.listData + lower = Builtin.unListData -impl (Big 'k, Big 'v) => Lift (list (pair 'k 'v)) (Map 'k 'v) where + +impl ( Big 'k, Big 'v ) => Lift (list (pair 'k 'v)) (Map 'k 'v) where lift = Builtin.mapData + lower = Builtin.unMapData diff --git a/crates/nash-driver/base/src/List.nash b/crates/nash-driver/base/src/List.nash index 8972e09d..0c2b28a6 100644 --- a/crates/nash-driver/base/src/List.nash +++ b/crates/nash-driver/base/src/List.nash @@ -1,228 +1,423 @@ -module List exposing (singleton, repeat, range, head, tail, isEmpty, length, isLength, reverse, append, concat, map, indexedMap, filter, filterMap, foldl, foldr, any, all, find, member, take, drop, at, map2, sort, sortBy, sum, partition, toArray) +module List exposing + ( singleton + , repeat + , range + , head + , tail + , isEmpty + , length + , isLength + , reverse + , append + , concat + , map + , indexedMap + , filter + , filterMap + , foldl + , foldr + , any + , all + , find + , member + , take + , drop + , at + , map2 + , sort + , sortBy + , sum + , partition + , toArray + ) + import Prelude exposing (..) -import Lift exposing (Lift) +import Lift exposing ( Lift ) import Builtin import Functor -import Option exposing (type option(..)) -import Eq exposing (Eq) -import Ord exposing (Ord) +import Option exposing ( type option(..) ) +import Eq exposing ( Eq ) +import Ord exposing ( Ord ) import Literal -import Primitive exposing (type bool(..)) -import Ordering exposing (type ordering(..)) +import Primitive exposing ( type bool(..) ) +import Ordering exposing ( type ordering(..) ) + singleton : 'a -> list 'a -singleton x = [x] +singleton x = [ x ] + repeat : Lift int 'n => 'n -> 'a -> list 'a repeat count x = repeatList (lower count) x + repeatList : int -> 'a -> list 'a -repeatList n x = if n <= 0 then [] else x :: repeatList (n - 1) x +repeatList n x = + if n <= 0 then + [] + else + x :: repeatList (n - 1) x + -range : (Lift int 's, Lift int 'e) => 's -> 'e -> list int +range : ( Lift int 's, Lift int 'e ) => 's -> 'e -> list int range start end = rangeList (lower start) (lower end) + rangeList : int -> int -> list int -rangeList start end = if start >= end then [] else start :: rangeList (start + 1) end +rangeList start end = + if start >= end then + [] + else + start :: rangeList (start + 1) end + + +head : Lift (list 'a) ('f 'a) => 'f 'a -> option 'a +head values = + case lowerOuter values of + [] -> + None + + x :: _ -> + Some x -head : Lift (list 'a) ('f 'a) => ('f 'a) -> option 'a -head values = case lowerOuter values of - [] -> None - x :: _ -> Some x -tail : Lift (list 'a) ('f 'a) => ('f 'a) -> option (list 'a) -tail values = case lowerOuter values of - [] -> None - _ :: xs -> Some xs +tail : Lift (list 'a) ('f 'a) => 'f 'a -> option (list 'a) +tail values = + case lowerOuter values of + [] -> + None -isEmpty : Lift (list 'a) ('f 'a) => ('f 'a) -> bool + _ :: xs -> + Some xs + + +isEmpty : Lift (list 'a) ('f 'a) => 'f 'a -> bool isEmpty values = values |> lowerOuter |> Builtin.nullList -length : Lift (list 'a) ('f 'a) => ('f 'a) -> int + +length : Lift (list 'a) ('f 'a) => 'f 'a -> int length values = values |> lowerOuter |> foldlList (\_ n -> n + 1) 0 -isLength : (Lift (list 'a) ('f 'a), Lift int 'n) => ('f 'a) -> 'n -> bool + +isLength : ( Lift (list 'a) ('f 'a), Lift int 'n ) => 'f 'a -> 'n -> bool isLength values mustLength = let count : int count = lower mustLength + items = lowerOuter values in - if count <= 0 then - case items of - [] -> count == 0 - _ :: _ -> False - else case Builtin.dropList (count - 1) items of - [_] -> True - _ -> False - -reverse : Lift (list 'a) ('f 'a) => ('f 'a) -> list 'a + if count <= 0 then + case items of + [] -> + count == 0 + + _ :: _ -> + False + else + case Builtin.dropList (count - 1) items of + [ _ ] -> + True + + _ -> + False + + +reverse : Lift (list 'a) ('f 'a) => 'f 'a -> list 'a reverse values = values |> lowerOuter |> foldlList (\x xs -> x :: xs) [] -append : (Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a)) => ('f 'a) -> ('g 'a) -> list 'a + +append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) + => 'f 'a -> 'g 'a -> list 'a append xs ys = appendList (lowerOuter xs) (lowerOuter ys) + appendList : list 'a -> list 'a -> list 'a -appendList xs ys = case xs of - [] -> ys - x :: rest -> x :: appendList rest ys +appendList xs ys = + case xs of + [] -> + ys + + x :: rest -> + x :: appendList rest ys -concat : (Lift (list ('f 'a)) ('g ('f 'a)), Lift (list 'a) ('f 'a)) => 'g ('f 'a) -> list 'a + +concat : ( Lift (list ('f 'a)) ('g ('f 'a)), Lift (list 'a) ('f 'a) ) + => 'g ('f 'a) -> list 'a concat values = values |> lowerOuter |> foldrList (\xs acc -> appendList (lowerOuter xs) acc) [] -map : Lift (list 'a) ('f 'a) => ('a -> 'b) -> ('f 'a) -> list 'b + +map : Lift (list 'a) ('f 'a) => ('a -> 'b) -> 'f 'a -> list 'b map f values = Functor.map f (lowerOuter values) -indexedMap : Lift (list 'a) ('f 'a) => (int -> 'a -> 'b) -> ('f 'a) -> list 'b -indexedMap f values = indexedFrom 0 f (lowerOuter values) -indexedFrom : int -> (int -> 'a -> 'b) -> list 'a -> list 'b -indexedFrom index f xs = case xs of - [] -> [] - x :: rest -> f index x :: indexedFrom (index + 1) f rest +indexedMap : Lift (list 'a) ('f 'a) => (int -> 'a -> 'b) -> 'f 'a -> list 'b +indexedMap f values = indexedFrom 0 f (lowerOuter values) -filter : (Lift (list 'a) ('f 'a), Lift bool 'p) => ('a -> 'p) -> ('f 'a) -> list 'a -filter p values = foldrList (\x acc -> if lower (p x) then x :: acc else acc) [] (lowerOuter values) -filterMap : (Lift (list 'a) ('f 'a), Lift (option 'b) ('g 'b)) => ('a -> 'g 'b) -> ('f 'a) -> list 'b +indexedFrom : int -> (int -> 'a -> 'b) -> list 'a -> list 'b +indexedFrom index f xs = + case xs of + [] -> + [] + + x :: rest -> + f index x :: indexedFrom (index + 1) f rest + + +filter : ( Lift (list 'a) ('f 'a), Lift bool 'p ) + => ('a -> 'p) -> 'f 'a -> list 'a +filter p values = + foldrList + ( + \x acc -> + if lower (p x) then + x :: acc + else + acc + ) + [] + (lowerOuter values) + + +filterMap : ( Lift (list 'a) ('f 'a), Lift (option 'b) ('g 'b) ) + => ('a -> 'g 'b) -> 'f 'a -> list 'b filterMap f values = filterMapList f (lowerOuter values) + filterMapList : Lift (option 'b) ('g 'b) => ('a -> 'g 'b) -> list 'a -> list 'b -filterMapList f xs = case xs of - [] -> [] - x :: rest -> case lower (f x) of - Some y -> y :: filterMapList f rest - None -> filterMapList f rest +filterMapList f xs = + case xs of + [] -> + [] + + x :: rest -> + case lower (f x) of + Some y -> + y :: filterMapList f rest -foldl : Lift (list 'a) ('f 'a) => ('a -> 'b -> 'b) -> 'b -> ('f 'a) -> 'b + None -> + filterMapList f rest + + +foldl : Lift (list 'a) ('f 'a) => ('a -> 'b -> 'b) -> 'b -> 'f 'a -> 'b foldl f acc values = foldlList f acc (lowerOuter values) + foldlList : ('a -> 'b -> 'b) -> 'b -> list 'a -> 'b -foldlList f acc xs = case xs of - [] -> acc - x :: rest -> foldlList f (f x acc) rest +foldlList f acc xs = + case xs of + [] -> + acc + + x :: rest -> + foldlList f (f x acc) rest -foldr : Lift (list 'a) ('f 'a) => ('a -> 'b -> 'b) -> 'b -> ('f 'a) -> 'b + +foldr : Lift (list 'a) ('f 'a) => ('a -> 'b -> 'b) -> 'b -> 'f 'a -> 'b foldr f acc values = foldrList f acc (lowerOuter values) + foldrList : ('a -> 'b -> 'b) -> 'b -> list 'a -> 'b -foldrList f acc xs = case xs of - [] -> acc - x :: rest -> f x (foldrList f acc rest) +foldrList f acc xs = + case xs of + [] -> + acc + + x :: rest -> + f x (foldrList f acc rest) -any : (Lift (list 'a) ('f 'a), Lift bool 'p) => ('a -> 'p) -> ('f 'a) -> bool + +any : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> bool any p values = anyList (\x -> lower (p x)) (lowerOuter values) + anyList : ('a -> bool) -> list 'a -> bool -anyList p xs = case xs of - [] -> False - x :: rest -> if p x then True else anyList p rest +anyList p xs = + case xs of + [] -> + False -all : (Lift (list 'a) ('f 'a), Lift bool 'p) => ('a -> 'p) -> ('f 'a) -> bool + x :: rest -> + if p x then + True + else + anyList p rest + + +all : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> bool all p values = allList (\x -> lower (p x)) (lowerOuter values) + allList : ('a -> bool) -> list 'a -> bool -allList p xs = case xs of - [] -> True - x :: rest -> if p x then allList p rest else False +allList p xs = + case xs of + [] -> + True + + x :: rest -> + if p x then + allList p rest + else + False -find : (Lift (list 'a) ('f 'a), Lift bool 'p) => ('a -> 'p) -> ('f 'a) -> option 'a + +find : ( Lift (list 'a) ('f 'a), Lift bool 'p ) + => ('a -> 'p) -> 'f 'a -> option 'a find p values = findList (\x -> lower (p x)) (lowerOuter values) + findList : ('a -> bool) -> list 'a -> option 'a -findList p xs = case xs of - [] -> None - x :: rest -> if p x then Some x else findList p rest +findList p xs = + case xs of + [] -> + None + + x :: rest -> + if p x then + Some x + else + findList p rest + -member : (Eq 'a, Lift (list 'a) ('f 'a)) => 'a -> ('f 'a) -> bool +member : ( Eq 'a, Lift (list 'a) ('f 'a) ) => 'a -> 'f 'a -> bool member x values = anyList (\y -> x == y) (lowerOuter values) -take : (Lift int 'n, Lift (list 'a) ('f 'a)) => 'n -> ('f 'a) -> list 'a + +take : ( Lift int 'n, Lift (list 'a) ('f 'a) ) => 'n -> 'f 'a -> list 'a take count values = takeList (lower count) (lowerOuter values) + takeList : int -> list 'a -> list 'a takeList n xs = - if n <= 0 then [] else case xs of - [] -> [] - x :: rest -> x :: takeList (n - 1) rest + if n <= 0 then + [] + else + case xs of + [] -> + [] + + x :: rest -> + x :: takeList (n - 1) rest + -drop : (Lift int 'n, Lift (list 'a) ('f 'a)) => 'n -> ('f 'a) -> list 'a +drop : ( Lift int 'n, Lift (list 'a) ('f 'a) ) => 'n -> 'f 'a -> list 'a drop count values = let n : int n = lower count in - if n <= 0 then lowerOuter values else Builtin.dropList n (lowerOuter values) + if n <= 0 then + lowerOuter values + else + Builtin.dropList n (lowerOuter values) -at : (Lift int 'n, Lift (list 'a) ('f 'a)) => 'n -> ('f 'a) -> option 'a + +at : ( Lift int 'n, Lift (list 'a) ('f 'a) ) => 'n -> 'f 'a -> option 'a at index values = let n : int n = lower index in - if n < 0 then None else head (Builtin.dropList n (lowerOuter values)) + if n < 0 then + None + else + head (Builtin.dropList n (lowerOuter values)) + -map2 : (Lift (list 'a) ('f 'a), Lift (list 'b) ('g 'b)) => ('a -> 'b -> 'c) -> ('f 'a) -> ('g 'b) -> list 'c +map2 : ( Lift (list 'a) ('f 'a), Lift (list 'b) ('g 'b) ) + => ('a -> 'b -> 'c) -> 'f 'a -> 'g 'b -> list 'c map2 f xs ys = map2List f (lowerOuter xs) (lowerOuter ys) + map2List : ('a -> 'b -> 'c) -> list 'a -> list 'b -> list 'c -map2List f xs ys = case (xs, ys) of - (x :: rest, y :: more) -> f x y :: map2List f rest more - _ -> [] +map2List f xs ys = + case ( xs, ys ) of + ( x :: rest, y :: more ) -> + f x y :: map2List f rest more + + _ -> + [] -sort : (Ord 'a, Lift (list 'a) ('f 'a)) => ('f 'a) -> list 'a + +sort : ( Ord 'a, Lift (list 'a) ('f 'a) ) => 'f 'a -> list 'a sort values = values |> lowerOuter |> sortList compare -sortBy : (Lift (list 'a) ('f 'a), Lift ordering 'o) => ('a -> 'a -> 'o) -> ('f 'a) -> list 'a + +sortBy : ( Lift (list 'a) ('f 'a), Lift ordering 'o ) + => ('a -> 'a -> 'o) -> 'f 'a -> list 'a sortBy cmp values = sortList (\a b -> lower (cmp a b)) (lowerOuter values) + sortList : ('a -> 'a -> ordering) -> list 'a -> list 'a sortList cmp xs = foldrList (insert cmp) [] xs + insert : ('a -> 'a -> ordering) -> 'a -> list 'a -> list 'a -insert cmp x xs = case xs of - [] -> [x] - y :: rest -> case cmp x y of - GT -> y :: insert cmp x rest - _ -> x :: xs +insert cmp x xs = + case xs of + [] -> + [ x ] -sum : (Lift int 'a, Lift (list 'a) ('f 'a)) => 'f 'a -> int + y :: rest -> + case cmp x y of + GT -> + y :: insert cmp x rest + + _ -> + x :: xs + + +sum : ( Lift int 'a, Lift (list 'a) ('f 'a) ) => 'f 'a -> int sum values = values |> lowerOuter |> foldlList (\x acc -> Builtin.addInteger (lower x) acc) 0 -partition : (Lift (list 'a) ('f 'a), Lift bool 'p) => ('a -> 'p) -> ('f 'a) -> (list 'a, list 'a) + +partition : ( Lift (list 'a) ('f 'a), Lift bool 'p ) + => ('a -> 'p) -> 'f 'a -> ( list 'a, list 'a ) partition p values = partitionList (\x -> lower (p x)) (lowerOuter values) -partitionList : ('a -> bool) -> list 'a -> (list 'a, list 'a) -partitionList p xs = case xs of - [] -> ([], []) - x :: rest -> - let (yes, no) = partitionList p rest in - if p x then (x :: yes, no) else (yes, x :: no) -toArray : Lift (list 'a) ('f 'a) => ('f 'a) -> array 'a +partitionList : ('a -> bool) -> list 'a -> ( list 'a, list 'a ) +partitionList p xs = + case xs of + [] -> + ( [], [] ) + + x :: rest -> + let + ( yes, no ) = partitionList p rest + in + if p x then + ( x :: yes, no ) + else + ( yes, x :: no ) + + +toArray : Lift (list 'a) ('f 'a) => 'f 'a -> array 'a toArray values = values |> lowerOuter |> Builtin.listToArray + lowerOuter : Lift (list 'a) ('f 'a) => 'f 'a -> list 'a lowerOuter = lower diff --git a/crates/nash-driver/base/src/Literal.nash b/crates/nash-driver/base/src/Literal.nash index d63993d9..57661bed 100644 --- a/crates/nash-driver/base/src/Literal.nash +++ b/crates/nash-driver/base/src/Literal.nash @@ -1,43 +1,57 @@ -module Literal exposing (FromInt, FromString, FromBytes, FromBool, FromUnit) +module Literal exposing ( FromInt, FromString, FromBytes, FromBool, FromUnit ) + import Primitive exposing (..) import Builtin exposing (..) + trait FromInt 'a where fromInt : int -> 'a + trait FromString 'a where fromString : string -> 'a + trait FromBytes 'a where fromBytes : bytes -> 'a + impl FromInt int where fromInt value = value + impl FromString string where fromString value = value + impl FromBytes bytes where fromBytes value = value + impl FromInt Int where fromInt = Builtin.iData + impl FromBytes Bytes where fromBytes = Builtin.bData + impl FromString bytes where fromString = Builtin.encodeUtf8 + trait FromBool 'a where fromBool : bool -> 'a + trait FromUnit 'a where fromUnit : unit -> 'a + impl FromBool bool where fromBool value = value + impl FromUnit unit where fromUnit value = value diff --git a/crates/nash-driver/base/src/Map.nash b/crates/nash-driver/base/src/Map.nash index 624eb1ab..066cc51d 100644 --- a/crates/nash-driver/base/src/Map.nash +++ b/crates/nash-driver/base/src/Map.nash @@ -1,56 +1,110 @@ -module Map exposing (empty, singleton, insert, get, remove, keys, values, toList, foldl, union) +module Map exposing + ( empty + , singleton + , insert + , get + , remove + , keys + , values + , toList + , foldl + , union + ) + import Builtin -import Function exposing ((|>)) -import Primitive exposing (type bool(..)) -import Lift exposing (Lift) -import Eq exposing (Eq) +import Function exposing ( (|>) ) +import Primitive exposing ( type bool(..) ) +import Lift exposing ( Lift ) +import Eq exposing ( Eq ) import List -import Option exposing (type option(..)) +import Option exposing ( type option(..) ) import Pair import Functor -union : (Eq 'k, Lift (list (pair 'k 'v)) 'a, Lift (list (pair 'k 'v)) 'b) => 'a -> 'b -> list (pair 'k 'v) + +union : ( Eq 'k, Lift (list (pair 'k 'v)) 'a, Lift (list (pair 'k 'v)) 'b ) + => 'a -> 'b -> list (pair 'k 'v) union left right = unionList (lower left) (lower right) + unionList : Eq 'k => list (pair 'k 'v) -> list (pair 'k 'v) -> list (pair 'k 'v) unionList left right = List.append (withoutRightKeys left right) right -withoutRightKeys : Eq 'k => list (pair 'k 'v) -> list (pair 'k 'v) -> list (pair 'k 'v) + +withoutRightKeys : Eq 'k + => list (pair 'k 'v) -> list (pair 'k 'v) -> list (pair 'k 'v) withoutRightKeys left right = case left of - [] -> [] + [] -> + [] + entry :: rest -> - let pair(key, _) = entry in - if containsKey key right then withoutRightKeys rest right - else Builtin.mkCons entry (withoutRightKeys rest right) + let + pair(key, _) = entry + in + if containsKey key right then + withoutRightKeys rest right + else + Builtin.mkCons entry (withoutRightKeys rest right) + containsKey : Eq 'k => 'k -> list (pair 'k 'v) -> bool containsKey key entries = case entries of - [] -> False + [] -> + False + pair(entryKey, _) :: rest -> - if eq key entryKey then True else containsKey key rest + if eq key entryKey then + True + else + containsKey key rest + empty : list (pair 'k 'v) empty = [] + singleton : ('k : Big) -> ('v : Big) -> list (pair 'k 'v) -singleton key value = [Builtin.mkPairData key value] +singleton key value = [ Builtin.mkPairData key value ] + -insert : (Eq 'k, Lift (list (pair 'k 'v)) 'm) => ('k : Big) -> ('v : Big) -> 'm -> list (pair 'k 'v) -insert key value entries = List.append (remove key entries) (singleton key value) +insert : ( Eq 'k, Lift (list (pair 'k 'v)) 'm ) + => ('k : Big) -> ('v : Big) -> 'm -> list (pair 'k 'v) +insert key value entries = + List.append (remove key entries) (singleton key value) -get : (Eq 'k, Lift (list (pair 'k 'v)) 'm) => 'k -> 'm -> option 'v + +get : ( Eq 'k, Lift (list (pair 'k 'v)) 'm ) => 'k -> 'm -> option 'v get key entries = getList key (lower entries) + getList : Eq 'k => 'k -> list (pair 'k 'v) -> option 'v -getList key entries = case entries of - [] -> None - pair(k, v) :: rest -> if eq key k then Some v else getList key rest +getList key entries = + case entries of + [] -> + None + + pair(k, v) :: rest -> + if eq key k then + Some v + else + getList key rest + + +remove : ( Eq 'k, Lift (list (pair 'k 'v)) 'm ) => 'k -> 'm -> list (pair 'k 'v) +remove key entries = + List.filter + ( + \entry -> + let + pair(k, _) = entry + in + neq key k + ) + (toList entries) -remove : (Eq 'k, Lift (list (pair 'k 'v)) 'm) => 'k -> 'm -> list (pair 'k 'v) -remove key entries = List.filter (\entry -> let pair(k, _) = entry in neq key k) (toList entries) keys : Lift (list (pair 'k 'v)) 'input => 'input -> list 'k keys entries = @@ -58,14 +112,27 @@ keys entries = |> lower |> Functor.map Pair.fst + values : Lift (list (pair 'k 'v)) 'input => 'input -> list 'v values entries = entries |> lower |> Functor.map Pair.snd + toList : Lift (list (pair 'k 'v)) 'm => 'm -> list (pair 'k 'v) toList = lower + foldl : Lift (list (pair 'k 'v)) 'm => ('k -> 'v -> 'b -> 'b) -> 'b -> 'm -> 'b -foldl f initial entries = List.foldl (\entry acc -> let pair(k, v) = entry in f k v acc) initial (toList entries) +foldl f initial entries = + List.foldl + ( + \entry acc -> + let + pair(k, v) = entry + in + f k v acc + ) + initial + (toList entries) diff --git a/crates/nash-driver/base/src/Monad.nash b/crates/nash-driver/base/src/Monad.nash index 7c99472b..ea414a24 100644 --- a/crates/nash-driver/base/src/Monad.nash +++ b/crates/nash-driver/base/src/Monad.nash @@ -1,6 +1,8 @@ -module Monad exposing (Monad) +module Monad exposing ( Monad ) + + +import Applicative exposing ( Applicative ) -import Applicative exposing (Applicative) trait Applicative 'm => Monad 'm where bind : 'm 'a -> ('a -> 'm 'b) -> 'm 'b diff --git a/crates/nash-driver/base/src/Monoid.nash b/crates/nash-driver/base/src/Monoid.nash index b37cbf8d..22f58fad 100644 --- a/crates/nash-driver/base/src/Monoid.nash +++ b/crates/nash-driver/base/src/Monoid.nash @@ -1,19 +1,25 @@ -module Monoid exposing (Monoid) +module Monoid exposing ( Monoid ) + import Literal -import Semigroup exposing (Semigroup) +import Semigroup exposing ( Semigroup ) + trait Semigroup 'a => Monoid 'a where empty : 'a + impl Monoid bytes where empty = #"" + impl Monoid string where empty = "" + impl Monoid (list 'a) where empty = [] + impl Monoid unit where empty = () diff --git a/crates/nash-driver/base/src/Num.nash b/crates/nash-driver/base/src/Num.nash index 461b9e88..f57c34ae 100644 --- a/crates/nash-driver/base/src/Num.nash +++ b/crates/nash-driver/base/src/Num.nash @@ -1,16 +1,25 @@ -module Num exposing (Num) +module Num exposing ( Num ) + import Builtin import Literal + trait Num 'a where add : 'a -> 'a -> 'a + sub : 'a -> 'a -> 'a + mul : 'a -> 'a -> 'a + negate : 'a -> 'a + impl Num int where add = Builtin.addInteger + sub = Builtin.subtractInteger + mul = Builtin.multiplyInteger + negate x = Builtin.subtractInteger 0 x diff --git a/crates/nash-driver/base/src/Option.nash b/crates/nash-driver/base/src/Option.nash index ce0e3c49..030fb21a 100644 --- a/crates/nash-driver/base/src/Option.nash +++ b/crates/nash-driver/base/src/Option.nash @@ -1,86 +1,156 @@ -module Option exposing (Option(..), type option(..), withDefault, map, apply, map2, andThen, isSome, unwrap) +module Option exposing + ( Option(..) + , type option(..) + , withDefault + , map + , apply + , map2 + , andThen + , isSome + , unwrap + ) -import Primitive exposing (type bool(..)) -import Eq exposing (Eq) -import Lift exposing (Lift) + +import Primitive exposing ( type bool(..) ) +import Eq exposing ( Eq ) +import Lift exposing ( Lift ) import Functor -import Applicative exposing (Applicative) -import Monad exposing (Monad) +import Applicative exposing ( Applicative ) +import Monad exposing ( Monad ) + + +type option 'a + = Some 'a + | None + + +type Option 'a + = Some 'a + | None -type option 'a = Some 'a | None -type Option 'a = Some 'a | None impl Functor.Functor option where map f value = case value of - Some item -> Some (f item) - None -> None + Some item -> + Some (f item) + + None -> + None + impl Applicative option where pure = Some + apply functions value = case functions of - Some f -> Functor.map f value - None -> None + Some f -> + Functor.map f value + + None -> + None + impl Monad option where bind value f = case value of - Some item -> f item - None -> None + Some item -> + f item + + None -> + None + withDefault : Lift (option 'a) ('f 'a) => 'a -> 'f 'a -> 'a withDefault fallback value = case lowerOuter value of - Some item -> item - None -> fallback + Some item -> + item + + None -> + fallback + impl Eq 'a => Eq (option 'a) where eq a b = - case (a, b) of - (Some x, Some y) -> eq x y - (None, None) -> True - _ -> False + case ( a, b ) of + ( Some x, Some y ) -> + eq x y + + ( None, None ) -> + True + + _ -> + False + impl Lift (option ('a : Big)) (Option 'a) where lift value = case value of - Some item -> Option.Some item - None -> Option.None + Some item -> + Option.Some item + + None -> + Option.None + lower value = case value of - Option.Some item -> Some item - Option.None -> None + Option.Some item -> + Some item + + Option.None -> + None + map : Lift (option 'a) ('f 'a) => ('a -> 'b) -> 'f 'a -> option 'b map f value = Functor.map f (lowerOuter value) + apply : Lift (option 'a) ('f 'a) => option ('a -> 'b) -> 'f 'a -> option 'b apply functions value = map2 (\f x -> f x) functions value -map2 : (Lift (option 'a) ('f 'a), Lift (option 'b) ('g 'b)) => ('a -> 'b -> 'c) -> 'f 'a -> 'g 'b -> option 'c + +map2 : ( Lift (option 'a) ('f 'a), Lift (option 'b) ('g 'b) ) + => ('a -> 'b -> 'c) -> 'f 'a -> 'g 'b -> option 'c map2 f left right = - case (lowerOuter left, lowerOuter right) of - (Some a, Some b) -> Some (f a b) - _ -> None + case ( lowerOuter left, lowerOuter right ) of + ( Some a, Some b ) -> + Some (f a b) + + _ -> + None -andThen : (Lift (option 'a) ('f 'a), Lift (option 'b) ('g 'b)) => ('a -> 'g 'b) -> 'f 'a -> option 'b + +andThen : ( Lift (option 'a) ('f 'a), Lift (option 'b) ('g 'b) ) + => ('a -> 'g 'b) -> 'f 'a -> option 'b andThen f value = case lowerOuter value of - Some x -> lower (f x) - None -> None + Some x -> + lower (f x) + + None -> + None + isSome : Lift (option 'a) ('f 'a) => 'f 'a -> bool isSome value = case lowerOuter value of - Some _ -> True - None -> False + Some _ -> + True + + None -> + False + unwrap : Lift (option 'a) ('f 'a) => 'f 'a -> 'a unwrap value = case lowerOuter value of - Some x -> x - None -> fail + Some x -> + x + + None -> + fail + lowerOuter : Lift (option 'a) ('f 'a) => 'f 'a -> option 'a lowerOuter = lower diff --git a/crates/nash-driver/base/src/Ord.nash b/crates/nash-driver/base/src/Ord.nash index a1e1b803..c8ad5ce1 100644 --- a/crates/nash-driver/base/src/Ord.nash +++ b/crates/nash-driver/base/src/Ord.nash @@ -1,10 +1,11 @@ -module Ord exposing (Ord) +module Ord exposing ( Ord ) + import Builtin +import Primitive exposing ( type bool(..) ) +import Eq exposing ( Eq ) +import Ordering exposing ( type ordering(..) ) -import Primitive exposing (type bool(..)) -import Eq exposing (Eq) -import Ordering exposing (type ordering(..)) trait Eq 'a => Ord 'a where compare : 'a -> 'a -> ordering @@ -12,14 +13,20 @@ trait Eq 'a => Ord 'a where lt : 'a -> 'a -> bool lt a b = case compare a b of - LT -> True - _ -> False + LT -> + True + + _ -> + False le : 'a -> 'a -> bool le a b = case compare a b of - GT -> False - _ -> True + GT -> + False + + _ -> + True gt : 'a -> 'a -> bool gt a b = lt b a @@ -28,42 +35,81 @@ trait Eq 'a => Ord 'a where ge a b = le b a max : 'a -> 'a -> 'a - max a b = if lt a b then b else a + max a b = + if lt a b then + b + else + a min : 'a -> 'a -> 'a - min a b = if lt a b then a else b + min a b = + if lt a b then + a + else + b + impl Ord int where compare a b = - if Builtin.lessThanInteger a b then LT - else if Builtin.equalsInteger a b then EQ - else GT + if Builtin.lessThanInteger a b then + LT + else if Builtin.equalsInteger a b then + EQ + else + GT + impl Ord bytes where compare a b = - if Builtin.lessThanByteString a b then LT - else if Builtin.equalsByteString a b then EQ - else GT + if Builtin.lessThanByteString a b then + LT + else if Builtin.equalsByteString a b then + EQ + else + GT + impl Ord string where compare a b = compare (Builtin.encodeUtf8 a) (Builtin.encodeUtf8 b) + impl Ord bool where compare a b = - if a then if b then EQ else GT - else if b then LT else EQ + if a then + if b then + EQ + else + GT + else if b then + LT + else + EQ + impl Ord unit where compare _ _ = EQ -impl (Ord 'a, Eq (list 'a)) => Ord (list 'a) where + +impl ( Ord 'a, Eq (list 'a) ) => Ord (list 'a) where compare xs ys = case xs of - [] -> case ys of - [] -> EQ - _ :: _ -> LT - x :: restX -> case ys of - [] -> GT - y :: restY -> case compare x y of - EQ -> compare restX restY - order -> order + [] -> + case ys of + [] -> + EQ + + _ :: _ -> + LT + + x :: restX -> + case ys of + [] -> + GT + + y :: restY -> + case compare x y of + EQ -> + compare restX restY + + order -> + order diff --git a/crates/nash-driver/base/src/Ordering.nash b/crates/nash-driver/base/src/Ordering.nash index f725856f..11b7dd82 100644 --- a/crates/nash-driver/base/src/Ordering.nash +++ b/crates/nash-driver/base/src/Ordering.nash @@ -1,43 +1,81 @@ -module Ordering exposing (Ordering(..), type ordering(..), invert, then_) +module Ordering exposing ( Ordering(..), type ordering(..), invert, then_ ) -import Primitive exposing (type bool(..)) -import Eq exposing (Eq) -import Lift exposing (Lift) -type ordering = LT | EQ | GT +import Primitive exposing ( type bool(..) ) +import Eq exposing ( Eq ) +import Lift exposing ( Lift ) + + +type ordering + = LT + | EQ + | GT + + +type Ordering + = LT + | EQ + | GT -type Ordering = LT | EQ | GT invert : Lift ordering 'a => 'a -> ordering invert order = case lower order of - LT -> GT - EQ -> EQ - GT -> LT + LT -> + GT + + EQ -> + EQ -then_ : (Lift ordering 'a, Lift ordering 'b) => 'a -> 'b -> ordering + GT -> + LT + + +then_ : ( Lift ordering 'a, Lift ordering 'b ) => 'a -> 'b -> ordering then_ first second = case lower first of - EQ -> lower second - order -> order + EQ -> + lower second + + order -> + order + impl Eq ordering where eq a b = - case (a, b) of - (LT, LT) -> True - (EQ, EQ) -> True - (GT, GT) -> True - _ -> False + case ( a, b ) of + ( LT, LT ) -> + True + + ( EQ, EQ ) -> + True + + ( GT, GT ) -> + True + + _ -> + False + impl Lift ordering Ordering where lift value = case value of - LT -> Ordering.LT - EQ -> Ordering.EQ - GT -> Ordering.GT + LT -> + Ordering.LT + + EQ -> + Ordering.EQ + + GT -> + Ordering.GT + lower value = case value of - Ordering.LT -> LT - Ordering.EQ -> EQ - Ordering.GT -> GT + Ordering.LT -> + LT + + Ordering.EQ -> + EQ + Ordering.GT -> + GT diff --git a/crates/nash-driver/base/src/Pair.nash b/crates/nash-driver/base/src/Pair.nash index 8ec745cc..12c3422c 100644 --- a/crates/nash-driver/base/src/Pair.nash +++ b/crates/nash-driver/base/src/Pair.nash @@ -1,12 +1,16 @@ -module Pair exposing (fst, snd, make) +module Pair exposing ( fst, snd, make ) + import Builtin + fst : pair 'a 'b -> 'a fst pair(first, _) = first + snd : pair 'a 'b -> 'b snd pair(_, second) = second + make : Data -> Data -> pair Data Data make = Builtin.mkPairData diff --git a/crates/nash-driver/base/src/Prelude.nash b/crates/nash-driver/base/src/Prelude.nash index 57722023..17f6eb2f 100644 --- a/crates/nash-driver/base/src/Prelude.nash +++ b/crates/nash-driver/base/src/Prelude.nash @@ -1,131 +1,262 @@ module Prelude exposing (..) + import Builtin import Function +import Primitive exposing ( type bool(..) ) +import Bool exposing ( and, or ) +import Eq exposing ( Eq ) +import Ord exposing ( Ord ) +import Ordering exposing ( type ordering(..) ) +import Show exposing ( Show ) +import Num exposing ( Num ) +import Integral exposing ( Integral ) +import Semigroup exposing ( Semigroup ) +import Functor exposing ( Functor ) +import Applicative exposing ( Applicative ) +import Monad exposing ( Monad ) -import Primitive exposing (type bool(..)) -import Bool exposing (and, or) -import Eq exposing (Eq) -import Ord exposing (Ord) -import Ordering exposing (type ordering(..)) -import Show exposing (Show) -import Num exposing (Num) -import Integral exposing (Integral) -import Semigroup exposing (Semigroup) -import Functor exposing (Functor) -import Applicative exposing (Applicative) -import Monad exposing (Monad) infix left 0 (|>) = applyForward + + infix right 0 (<|) = applyBackward + + infix right 9 (<<) = composeLeft + + infix right 9 (>>) = composeRight + + infix right 2 (||) = or + + infix right 3 (&&) = and + + infix non 4 (==) = eq + + infix non 4 (/=) = neq + + infix non 4 (<) = lt + + infix non 4 (>) = gt + + infix non 4 (<=) = le + + infix non 4 (>=) = ge + + infix right 5 (++) = append + + infix right 5 (::) = prepend + + infix left 6 (+) = add + + infix left 6 (-) = sub + + infix left 7 (*) = mul + + infix left 7 (/) = div + + infix left 7 (%) = mod + + infix left 1 (>>=) = bind + + infix left 4 (<$>) = map + + infix left 4 (<*>) = apply + identity : 'a -> 'a identity x = x + always : 'a -> 'b -> 'a always x _ = x + applyForward : 'a -> ('a -> 'b) -> 'b applyForward = Function.applyForward + applyBackward : ('a -> 'b) -> 'a -> 'b applyBackward = Function.applyBackward - - composeLeft : ('b -> 'c) -> ('a -> 'b) -> 'a -> 'c composeLeft g f x = g (f x) + composeRight : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c composeRight f g x = g (f x) + prepend : 'a -> list 'a -> list 'a prepend = Builtin.mkCons -impl (Eq 'a, Eq 'b) => Eq ('a, 'b) where - eq (a, b) (c, d) = if eq a c then eq b d else False -impl (Eq 'a, Eq 'b, Eq 'c) => Eq ('a, 'b, 'c) where - eq (a, b, c) (d, e, f) = - if eq a d then if eq b e then eq c f else False - else False +impl ( Eq 'a, Eq 'b ) => Eq ( 'a, 'b ) where + eq ( a, b ) ( c, d ) = + if eq a c then + eq b d + else + False + + +impl ( Eq 'a, Eq 'b, Eq 'c ) => Eq ( 'a, 'b, 'c ) where + eq ( a, b, c ) ( d, e, f ) = + if eq a d then + if eq b e then + eq c f + else + False + else + False + -impl (Eq 'a, Eq 'b, Eq 'c, Eq 'd) => Eq ('a, 'b, 'c, 'd) where - eq (a, b, c, d) (e, f, g, h) = +impl ( Eq 'a, Eq 'b, Eq 'c, Eq 'd ) => Eq ( 'a, 'b, 'c, 'd ) where + eq ( a, b, c, d ) ( e, f, g, h ) = if eq a e then if eq b f then - if eq c g then eq d h else False - else False - else False + if eq c g then + eq d h + else + False + else + False + else + False -impl (Ord 'a, Ord 'b) => Ord ('a, 'b) where - compare (a, b) (c, d) = + +impl ( Ord 'a, Ord 'b ) => Ord ( 'a, 'b ) where + compare ( a, b ) ( c, d ) = case compare a c of - EQ -> compare b d - order -> order + EQ -> + compare b d + + order -> + order -impl (Ord 'a, Ord 'b, Ord 'c) => Ord ('a, 'b, 'c) where - compare (a, b, c) (d, e, f) = + +impl ( Ord 'a, Ord 'b, Ord 'c ) => Ord ( 'a, 'b, 'c ) where + compare ( a, b, c ) ( d, e, f ) = case compare a d of EQ -> case compare b e of - EQ -> compare c f - order -> order - order -> order + EQ -> + compare c f -impl (Ord 'a, Ord 'b, Ord 'c, Ord 'd) => Ord ('a, 'b, 'c, 'd) where - compare (a, b, c, d) (e, f, g, h) = + order -> + order + + order -> + order + + +impl ( Ord 'a, Ord 'b, Ord 'c, Ord 'd ) => Ord ( 'a, 'b, 'c, 'd ) where + compare ( a, b, c, d ) ( e, f, g, h ) = case compare a e of EQ -> case compare b f of EQ -> case compare c g of - EQ -> compare d h - order -> order - order -> order - order -> order - -impl (Show 'a, Show 'b) => Show ('a, 'b) where - show (a, b) = - Builtin.appendString "(" - (Builtin.appendString (show a) - (Builtin.appendString ", " (Builtin.appendString (show b) ")"))) - -impl (Show 'a, Show 'b, Show 'c) => Show ('a, 'b, 'c) where - show (a, b, c) = - Builtin.appendString "(" - (Builtin.appendString (show a) - (Builtin.appendString ", " - (Builtin.appendString (show b) - (Builtin.appendString ", " (Builtin.appendString (show c) ")"))))) - -impl (Show 'a, Show 'b, Show 'c, Show 'd) => Show ('a, 'b, 'c, 'd) where - show (a, b, c, d) = - Builtin.appendString "(" - (Builtin.appendString (show a) - (Builtin.appendString ", " - (Builtin.appendString (show b) - (Builtin.appendString ", " - (Builtin.appendString (show c) - (Builtin.appendString ", " (Builtin.appendString (show d) ")"))))))) + EQ -> + compare d h + + order -> + order + + order -> + order + + order -> + order + + +impl ( Show 'a, Show 'b ) => Show ( 'a, 'b ) where + show ( a, b ) = + Builtin.appendString + "(" + ( + Builtin.appendString + (show a) + ( + Builtin.appendString + ", " + (Builtin.appendString (show b) ")") + ) + ) + + +impl ( Show 'a, Show 'b, Show 'c ) => Show ( 'a, 'b, 'c ) where + show ( a, b, c ) = + Builtin.appendString + "(" + ( + Builtin.appendString + (show a) + ( + Builtin.appendString + ", " + ( + Builtin.appendString + (show b) + ( + Builtin.appendString + ", " + (Builtin.appendString (show c) ")") + ) + ) + ) + ) + + +impl ( Show 'a, Show 'b, Show 'c, Show 'd ) => Show ( 'a, 'b, 'c, 'd ) where + show ( a, b, c, d ) = + Builtin.appendString + "(" + ( + Builtin.appendString + (show a) + ( + Builtin.appendString + ", " + ( + Builtin.appendString + (show b) + ( + Builtin.appendString + ", " + ( + Builtin.appendString + (show c) + ( + Builtin.appendString + ", " + ( + Builtin.appendString + (show d) + ")" + ) + ) + ) + ) + ) + ) + ) diff --git a/crates/nash-driver/base/src/Prop.nash b/crates/nash-driver/base/src/Prop.nash index e5abef15..9e773e4b 100644 --- a/crates/nash-driver/base/src/Prop.nash +++ b/crates/nash-driver/base/src/Prop.nash @@ -1,4 +1,25 @@ -module Prop exposing (type prng(..), type choiceTree(..), type generator, group, choice, constant, intBetween, int, listOf, listBetween, tuple2, oneOf, bytes, bool, option, bytesBetween, bytesExactly, frequency, intAtLeast) +module Prop exposing + ( type prng(..) + , type choiceTree(..) + , type generator + , group + , choice + , constant + , intBetween + , int + , listOf + , listBetween + , tuple2 + , oneOf + , bytes + , bool + , option + , bytesBetween + , bytesExactly + , frequency + , intAtLeast + ) + import Builtin import Int @@ -6,24 +27,28 @@ import Functor import Applicative import Monad import Prelude exposing (..) -import Primitive exposing (type bool(..)) +import Primitive exposing ( type bool(..) ) import Literal -import Num exposing (Num) -import Lift exposing (Lift) -import Option exposing (type option(..)) -import Cons exposing (type cons(..)) +import Num exposing ( Num ) +import Lift exposing ( Lift ) +import Option exposing ( type option(..) ) +import Cons exposing ( type cons(..) ) + type choiceTree = Choice int | Group (cons choiceTree) + type prng = Seeded bytes (cons choiceTree) | Replayed (cons choiceTree) (cons choiceTree) | Rebuilding (list int) (cons choiceTree) -type alias generator 'a - = prng -> option ('a, prng) + +type alias generator 'a = + prng -> option ( 'a, prng ) + choice : Lift int 'n => 'n -> generator int choice bound = @@ -31,240 +56,411 @@ choice bound = |> lower |> choiceInt + choiceInt : int -> generator int choiceInt bound state = if bound < 0 || bound > 18446744073709551615 then - (fail "Prop.choice bound must be in 0..18446744073709551615") + fail "Prop.choice bound must be in 0..18446744073709551615" else case state of Seeded seed recorded -> let seed2 = Builtin.blake2b_256 seed + value = Builtin.byteStringToInteger True seed2 % (bound + 1) in - Some (value, Seeded seed2 (Cons (Choice value) recorded)) + Some ( value, Seeded seed2 (Cons (Choice value) recorded) ) + Rebuilding remaining recorded -> case remaining of value :: rest -> - if value < 0 || value > bound then None - else Some (value, Rebuilding rest (Cons (Choice value) recorded)) - [] -> None + if value < 0 || value > bound then + None + else + Some + ( value + , Rebuilding rest (Cons (Choice value) recorded) + ) + + [] -> + None + Replayed remaining recorded -> case remaining of Cons (Choice value) rest -> - if value < 0 || value > bound then None - else Some (value, Replayed rest (Cons (Choice value) recorded)) - _ -> None + if value < 0 || value > bound then + None + else + Some + ( value + , Replayed rest (Cons (Choice value) recorded) + ) + + _ -> + None + group : generator 'a -> generator 'a group generator state = case state of Seeded seed recorded -> case generator (Seeded seed Nil) of - Some (value, Seeded next children) -> - Some (value, Seeded next (Cons (Group children) recorded)) - _ -> None + Some ( value, Seeded next children ) -> + Some ( value, Seeded next (Cons (Group children) recorded) ) + + _ -> + None + Rebuilding remaining recorded -> case generator (Rebuilding remaining Nil) of - Some (value, Rebuilding rest children) -> - Some (value, Rebuilding rest (Cons (Group children) recorded)) - _ -> None + Some ( value, Rebuilding rest children ) -> + Some + ( value + , Rebuilding rest (Cons (Group children) recorded) + ) + + _ -> + None + Replayed remaining recorded -> case remaining of Cons (Group children) rest -> case generator (Replayed children Nil) of - Some (value, Replayed _ used) -> - Some (value, Replayed rest (Cons (Group used) recorded)) - _ -> None - _ -> None + Some ( value, Replayed _ used ) -> + Some + ( value + , Replayed rest (Cons (Group used) recorded) + ) + + _ -> + None + + _ -> + None + impl Functor.Functor generator where map f generator state = case generator state of - None -> None - Some (value, next) -> Some (f value, next) + None -> + None + + Some ( value, next ) -> + Some ( f value, next ) + impl Applicative.Applicative generator where pure = constant + apply functions values state = case group functions state of - None -> None - Some (f, next) -> + None -> + None + + Some ( f, next ) -> case group values next of - None -> None - Some (value, final) -> Some (f value, final) + None -> + None + + Some ( value, final ) -> + Some ( f value, final ) + impl Monad.Monad generator where bind generator continuation state = case group generator state of - None -> None - Some (value, next) -> continuation value next + None -> + None + + Some ( value, next ) -> + continuation value next + constant : 'a -> generator 'a -constant a prng = Some (a, prng) +constant a prng = Some ( a, prng ) -intBetween : (Lift int 'a, Lift int 'b) => 'a -> 'b -> generator int + +intBetween : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> generator int intBetween low high = intBetweenInt (lower low) (lower high) + intBetweenInt : int -> int -> generator int intBetweenInt lo hi = - if hi < lo then (fail "Prop.intBetween has reversed bounds") - else if hi == lo then constant lo + if hi < lo then + fail "Prop.intBetween has reversed bounds" + else if hi == lo then + constant lo else - let draw = bounded (hi - lo) in - \state -> - case draw state of - None -> None - Some (n, next) -> Some (lo + n, next) + let + draw = bounded (hi - lo) + in + \state -> + case draw state of + None -> + None + + Some ( n, next ) -> + Some ( lo + n, next ) + -- Bounded offsets; large ranges use whole 64-bit chunks and rejection. bounded : int -> generator int bounded maximum = - if maximum <= 18446744073709551615 then choice maximum - else do - high <- bounded (maximum / 18446744073709551616) - low <- choice 18446744073709551615 - let value = high * 18446744073709551616 + low in - if value <= maximum then Applicative.pure value else bounded maximum + if maximum <= 18446744073709551615 then + choice maximum + else + do + high <- bounded (maximum / 18446744073709551616) + low <- choice 18446744073709551615 + let + value = high * 18446744073709551616 + low + in + if value <= maximum then + Applicative.pure value + else + bounded maximum + -- Favor small bit widths, then sample within that width. natural : generator int natural = naturalFrom 1 + naturalFrom : int -> generator int naturalFrom scale = do wider <- choice 3 case wider of - 3 -> naturalFrom (scale * 256) - _ -> do - bits <- choice 7 - bounded (scale * Int.pow2 (bits + 1) - 1) + 3 -> + naturalFrom (scale * 256) + + _ -> + do + bits <- choice 7 + bounded (scale * Int.pow2 (bits + 1) - 1) + intAtLeast : Lift int 'n => 'n -> generator int intAtLeast minimum = - let lo = lower minimum in - Functor.map (\offset -> lo + offset) natural + let + lo = lower minimum + in + Functor.map (\offset -> lo + offset) natural + -- Favor small values; the wide branch has no fixed integer ceiling. int : generator int int state = case choice 2 state of - None -> None - Some (width, next) -> + None -> + None + + Some ( width, next ) -> case width of - 0 -> choice 255 next - 1 -> intBetween -32768 32767 next - _ -> signed next + 0 -> + choice 255 next + + 1 -> + intBetween (-32768) 32767 next + + _ -> + signed next + signed : generator int signed = do magnitude <- natural case magnitude of - 0 -> Applicative.pure 0 - _ -> do - negative <- bool - Applicative.pure (if negative then -magnitude else magnitude) + 0 -> + Applicative.pure 0 + + _ -> + do + negative <- bool + Applicative.pure + ( + if negative then + -magnitude + else + magnitude + ) + listOf : generator 'a -> generator (list 'a) listOf = listBetween 0 20 -listBetween : (Lift int 'l, Lift int 'h) => 'l -> 'h -> generator 'a -> generator (list 'a) -listBetween low high generator = listBetweenInt (lower low) (lower high) generator + +listBetween : ( Lift int 'l, Lift int 'h ) + => 'l -> 'h -> generator 'a -> generator (list 'a) +listBetween low high generator = + listBetweenInt (lower low) (lower high) generator + listBetweenInt : int -> int -> generator 'a -> generator (list 'a) listBetweenInt lo hi item = - if lo < 0 || hi < lo then (fail "Prop.listBetween has invalid bounds") - else listFrom 0 lo hi item + if lo < 0 || hi < lo then + fail "Prop.listBetween has invalid bounds" + else + listFrom 0 lo hi item + listFrom : int -> int -> int -> generator 'a -> generator (list 'a) listFrom n lo hi item = - if n >= hi then Applicative.pure [] + if n >= hi then + Applicative.pure [] else do step <- listIteration (n < lo) item case step of - None -> Applicative.pure [] + None -> + Applicative.pure [] + Some value -> - Functor.map (Builtin.mkCons value) (listFrom (n + 1) lo hi item) + Functor.map + (Builtin.mkCons value) + (listFrom (n + 1) lo hi item) + listIteration : bool -> generator 'a -> generator (option 'a) listIteration required item state = do - (continue, next) <- (if required then constant 1 else choice 1) state + ( continue, next ) <- + ( + if required then + constant 1 + else + choice 1 + ) + state case continue of - 0 -> Some (None, next) - _ -> (Monad.bind item (\value -> Applicative.pure (Some value))) next + 0 -> + Some ( None, next ) + + _ -> + (Monad.bind item (\value -> Applicative.pure (Some value))) next + -tuple2 : generator 'a -> generator 'b -> generator ('a, 'b) +tuple2 : generator 'a -> generator 'b -> generator ( 'a, 'b ) tuple2 first second = do a <- first b <- second - Applicative.pure (a, b) + Applicative.pure ( a, b ) + -- Generators contain functions, so use the Term-representation Cons list. oneOf : cons (generator 'a) -> generator 'a oneOf generators = case generators of - Nil -> \_ -> None + Nil -> + \_ -> None + Cons _ _ -> - let draw = choice (Cons.length generators - 1) in - \state -> - case draw state of - None -> None - Some (n, next) -> pick n generators next + let + draw = choice (Cons.length generators - 1) + in + \state -> + case draw state of + None -> + None + + Some ( n, next ) -> + pick n generators next + pick : int -> cons (generator 'a) -> generator 'a pick index generators = case generators of - Nil -> \_ -> None - Cons first rest -> if index == 0 then group first else pick (index - 1) rest + Nil -> + \_ -> None + + Cons first rest -> + if index == 0 then + group first + else + pick (index - 1) rest + bytes : generator bytes bytes = bytesBetween 0 32 + bool : generator bool bool = Functor.map (\n -> n == 1) (choice 1) + option : generator 'a -> generator (option 'a) option item = do present <- bool - if present then Functor.map Some (group item) else Applicative.pure None + if present then + Functor.map Some (group item) + else + Applicative.pure None -bytesBetween : (Lift int 'l, Lift int 'h) => 'l -> 'h -> generator bytes + +bytesBetween : ( Lift int 'l, Lift int 'h ) => 'l -> 'h -> generator bytes bytesBetween lo hi = Functor.map packBytes (listBetween lo hi (choice 255)) + bytesExactly : Lift int 'n => 'n -> generator bytes bytesExactly count = - let n = lower count in - bytesBetween n n + let + n = lower count + in + bytesBetween n n -frequency : Lift int 'w => cons ('w, generator 'a) -> generator 'a + +frequency : Lift int 'w => cons ( 'w, generator 'a ) -> generator 'a frequency entries = - let total = totalWeight entries in - if total == 0 then \_ -> None - else - let draw = bounded (total - 1) in - \state -> do - (index, next) <- draw state - weighted index entries next - -totalWeight : Lift int 'w => cons ('w, generator 'a) -> int -totalWeight entries = case entries of - Nil -> 0 - Cons (weight, _) rest -> - let n = lower weight in - if n < 0 then (fail "Prop.frequency requires nonnegative weights") - else n + totalWeight rest - -weighted : Lift int 'w => int -> cons ('w, generator 'a) -> generator 'a -weighted index entries = case entries of - Nil -> \_ -> None - Cons (weight, item) rest -> - let n = lower weight in - if index < n then group item - else weighted (index - n) rest + let + total = totalWeight entries + in + if total == 0 then + \_ -> None + else + let + draw = bounded (total - 1) + in + \state -> + do + ( index, next ) <- draw state + weighted index entries next + + +totalWeight : Lift int 'w => cons ( 'w, generator 'a ) -> int +totalWeight entries = + case entries of + Nil -> + 0 + + Cons ( weight, _ ) rest -> + let + n = lower weight + in + if n < 0 then + fail "Prop.frequency requires nonnegative weights" + else + n + totalWeight rest + + +weighted : Lift int 'w => int -> cons ( 'w, generator 'a ) -> generator 'a +weighted index entries = + case entries of + Nil -> + \_ -> None + + Cons ( weight, item ) rest -> + let + n = lower weight + in + if index < n then + group item + else + weighted (index - n) rest + packBytes : list int -> bytes packBytes values = case values of - [] -> #"" - x :: xs -> Builtin.consByteString x (packBytes xs) + [] -> + #"" + + x :: xs -> + Builtin.consByteString x (packBytes xs) diff --git a/crates/nash-driver/base/src/Rational.nash b/crates/nash-driver/base/src/Rational.nash index 9c3f28cd..954915fb 100644 --- a/crates/nash-driver/base/src/Rational.nash +++ b/crates/nash-driver/base/src/Rational.nash @@ -1,97 +1,153 @@ -module Rational exposing (type rational, new, numerator, denominator, add, sub, mul, div, negate, abs, compare, floor, ceiling, truncate) +module Rational exposing + ( type rational + , new + , numerator + , denominator + , add + , sub + , mul + , div + , negate + , abs + , compare + , floor + , ceiling + , truncate + ) + import Prelude exposing (..) -import Lift exposing (Lift) -import Eq exposing (Eq) -import Ord exposing (Ord) -import Num exposing (Num) -import Literal exposing (FromInt) -import Show exposing (Show) -import Ordering exposing (type ordering) +import Lift exposing ( Lift ) +import Eq exposing ( Eq ) +import Ord exposing ( Ord ) +import Num exposing ( Num ) +import Literal exposing ( FromInt ) +import Show exposing ( Show ) +import Ordering exposing ( type ordering ) import Int import Builtin -type rational = Rational int int -new : (Lift int 'n, Lift int 'd) => 'n -> 'd -> rational +type rational + = Rational int int + + +new : ( Lift int 'n, Lift int 'd ) => 'n -> 'd -> rational new top bottom = - let n : int + let + n : int n = lower top + d : int d = lower bottom in - if d == 0 then fail - else - let divisor = Int.gcd n d - sign = if d < 0 then -1 else 1 - in - Rational (Int.quot n divisor * sign) (Int.quot d divisor * sign) + if d == 0 then + fail + else + let + divisor = Int.gcd n d + + sign = + if d < 0 then + -1 + else + 1 + in + Rational (Int.quot n divisor * sign) (Int.quot d divisor * sign) + numerator : rational -> int numerator (Rational n _) = n + denominator : rational -> int denominator (Rational _ d) = d + add : rational -> rational -> rational add = addRational + addRational : rational -> rational -> rational addRational (Rational a b) (Rational c d) = new (a * d + c * b) (b * d) + sub : rational -> rational -> rational sub = subRational + subRational : rational -> rational -> rational subRational (Rational a b) (Rational c d) = new (a * d - c * b) (b * d) + mul : rational -> rational -> rational mul = mulRational + mulRational : rational -> rational -> rational mulRational (Rational a b) (Rational c d) = new (a * c) (b * d) + div : rational -> rational -> rational div (Rational a b) (Rational c d) = new (a * d) (b * c) + negate : rational -> rational negate = negateRational + negateRational : rational -> rational negateRational (Rational n d) = Rational (Int.negate n) d + abs : rational -> rational abs (Rational n d) = Rational (Int.abs n) d + compare : rational -> rational -> ordering compare = compareRational + compareRational : rational -> rational -> ordering compareRational (Rational a b) (Rational c d) = Int.compare (a * d) (c * b) + floor : rational -> int floor (Rational n d) = Int.div n d + ceiling : rational -> int ceiling (Rational n d) = Int.negate (Int.div (Int.negate n) d) + truncate : rational -> int truncate (Rational n d) = Int.quot n d + impl Eq rational where eq (Rational a b) (Rational c d) = a == c && b == d + impl Ord rational where compare = compareRational + impl Num rational where add = addRational + sub = subRational + mul = mulRational + negate = negateRational + impl FromInt rational where fromInt n = Rational n 1 + impl Show rational where - show (Rational n d) = Builtin.appendString (Int.toString n) (Builtin.appendString "/" (Int.toString d)) + show (Rational n d) = + Builtin.appendString + (Int.toString n) + (Builtin.appendString "/" (Int.toString d)) diff --git a/crates/nash-driver/base/src/Semigroup.nash b/crates/nash-driver/base/src/Semigroup.nash index 05aebeb6..79d90420 100644 --- a/crates/nash-driver/base/src/Semigroup.nash +++ b/crates/nash-driver/base/src/Semigroup.nash @@ -1,21 +1,30 @@ -module Semigroup exposing (Semigroup) +module Semigroup exposing ( Semigroup ) + import Builtin + trait Semigroup 'a where append : 'a -> 'a -> 'a + impl Semigroup bytes where append = Builtin.appendByteString + impl Semigroup string where append = Builtin.appendString + impl Semigroup (list 'a) where append xs ys = case xs of - [] -> ys - head :: tail -> Builtin.mkCons head (append tail ys) + [] -> + ys + + head :: tail -> + Builtin.mkCons head (append tail ys) + impl Semigroup unit where append _ _ = () diff --git a/crates/nash-driver/base/src/Show.nash b/crates/nash-driver/base/src/Show.nash index 91ba8293..7eaa3bfb 100644 --- a/crates/nash-driver/base/src/Show.nash +++ b/crates/nash-driver/base/src/Show.nash @@ -1,14 +1,16 @@ -module Show exposing (Show, hexBytes) +module Show exposing ( Show, hexBytes ) -import Builtin -import Function exposing ((|>)) -import Primitive exposing (Data(..), type bool(..)) +import Builtin +import Function exposing ( (|>) ) +import Primitive exposing ( Data(..), type bool(..) ) import Literal + trait Show 'a where show : 'a -> string + impl Show int where show n = if Builtin.lessThanInteger n 0 then @@ -22,6 +24,7 @@ impl Show int where |> decimal |> Builtin.decodeUtf8 + decimal : int -> bytes decimal n = if Builtin.lessThanInteger n 10 then @@ -29,20 +32,31 @@ decimal n = else Builtin.appendByteString (decimal (Builtin.quotientInteger n 10)) - (Builtin.consByteString (Builtin.addInteger 48 (Builtin.remainderInteger n 10)) #"") + ( + Builtin.consByteString + (Builtin.addInteger 48 (Builtin.remainderInteger n 10)) + #"" + ) + impl Show bytes where show value = - Builtin.appendString "#\"" (Builtin.appendString (Builtin.decodeUtf8 (hex value 0)) "\"") + Builtin.appendString + "#\"" + (Builtin.appendString (Builtin.decodeUtf8 (hex value 0)) "\"") + hexByte : int -> bytes hexByte byte = let digits = #"30313233343536373839616263646566" + high = Builtin.indexByteString digits (Builtin.quotientInteger byte 16) + low = Builtin.indexByteString digits (Builtin.remainderInteger byte 16) in - Builtin.consByteString high (Builtin.consByteString low #"") + Builtin.consByteString high (Builtin.consByteString low #"") + hex : bytes -> int -> bytes hex value index = @@ -50,71 +64,128 @@ hex value index = Builtin.appendByteString (hexByte (Builtin.indexByteString value index)) (hex value (Builtin.addInteger index 1)) - else #"" + else + #"" + impl Show string where show value = Builtin.decodeUtf8 - (Builtin.appendByteString #"22" - (Builtin.appendByteString (escaped (Builtin.encodeUtf8 value) 0) #"22")) + ( + Builtin.appendByteString + #"22" + ( + Builtin.appendByteString + (escaped (Builtin.encodeUtf8 value) 0) + #"22" + ) + ) + escaped : bytes -> int -> bytes escaped value index = if Builtin.lessThanInteger index (Builtin.lengthOfByteString value) then let byte = Builtin.indexByteString value index + rest = escaped value (Builtin.addInteger index 1) + prefix = - if Builtin.equalsInteger byte 34 then #"5c22" - else if Builtin.equalsInteger byte 92 then #"5c5c" - else if Builtin.equalsInteger byte 10 then #"5c6e" - else if Builtin.equalsInteger byte 13 then #"5c72" - else if Builtin.equalsInteger byte 9 then #"5c74" + if Builtin.equalsInteger byte 34 then + #"5c22" + else if Builtin.equalsInteger byte 92 then + #"5c5c" + else if Builtin.equalsInteger byte 10 then + #"5c6e" + else if Builtin.equalsInteger byte 13 then + #"5c72" + else if Builtin.equalsInteger byte 9 then + #"5c74" else if Builtin.lessThanInteger byte 32 then - Builtin.appendByteString #"5c757b3030" (Builtin.appendByteString (hexByte byte) #"7d") - else Builtin.consByteString byte #"" + Builtin.appendByteString + #"5c757b3030" + (Builtin.appendByteString (hexByte byte) #"7d") + else + Builtin.consByteString byte #"" in - Builtin.appendByteString prefix rest - else #"" + Builtin.appendByteString prefix rest + else + #"" + impl Show bool where - show value = if value then "True" else "False" + show value = + if value then + "True" + else + "False" + impl Show unit where show _ = "()" + impl Show 'a => Show (list 'a) where - show values = Builtin.appendString "[" (Builtin.appendString (showItems values) "]") + show values = + Builtin.appendString "[" (Builtin.appendString (showItems values) "]") + showItems : Show 'a => list 'a -> string showItems values = case values of - [] -> "" + [] -> + "" + value :: rest -> let first = show value in - case rest of - [] -> first - _ :: _ -> Builtin.appendString first (Builtin.appendString ", " (showItems rest)) + case rest of + [] -> + first -impl (Show 'a, Show 'b) => Show (pair 'a 'b) where + _ :: _ -> + Builtin.appendString + first + (Builtin.appendString ", " (showItems rest)) + + +impl ( Show 'a, Show 'b ) => Show (pair 'a 'b) where show value = let pair(first, second) = value in - Builtin.appendString "(" - (Builtin.appendString (show first) - (Builtin.appendString ", " (Builtin.appendString (show second) ")"))) + Builtin.appendString + "(" + ( + Builtin.appendString + (show first) + ( + Builtin.appendString + ", " + (Builtin.appendString (show second) ")") + ) + ) + impl Show Data where show value = case value of - Constr payload -> Builtin.appendString "Constr " (show payload) - Map entries -> Builtin.appendString "Map " (show entries) - List values -> Builtin.appendString "List " (show values) - I integer -> Builtin.appendString "I " (show integer) - B bytes -> Builtin.appendString "B " (show bytes) + Constr payload -> + Builtin.appendString "Constr " (show payload) + + Map entries -> + Builtin.appendString "Map " (show entries) + + List values -> + Builtin.appendString "List " (show values) + + I integer -> + Builtin.appendString "I " (show integer) + + B bytes -> + Builtin.appendString "B " (show bytes) + impl Show Int where show value = @@ -122,24 +193,28 @@ impl Show Int where |> Builtin.unIData |> show + impl Show Bytes where show value = value |> Builtin.unBData |> show + impl Show 'a => Show (List 'a) where show value = value |> Builtin.unListData |> show -impl (Show 'k, Show 'v) => Show (Map 'k 'v) where + +impl ( Show 'k, Show 'v ) => Show (Map 'k 'v) where show value = value |> Builtin.unMapData |> show |> Builtin.appendString "Map " + hexBytes : bytes -> string hexBytes value = Builtin.decodeUtf8 (hex value 0) diff --git a/crates/nash-driver/base/src/String.nash b/crates/nash-driver/base/src/String.nash index b9298511..c58406de 100644 --- a/crates/nash-driver/base/src/String.nash +++ b/crates/nash-driver/base/src/String.nash @@ -1,37 +1,59 @@ -module String exposing (toBytes, fromBytes, fromInt, concat, join) +module String exposing ( toBytes, fromBytes, fromInt, concat, join ) + import Literal -import Lift exposing (Lift) +import Lift exposing ( Lift ) import Builtin -import Function exposing ((|>)) +import Function exposing ( (|>) ) import Int + toBytes : string -> bytes toBytes = Builtin.encodeUtf8 + fromBytes : Lift bytes 'b => 'b -> string fromBytes value = value |> lower |> Builtin.decodeUtf8 + fromInt : Lift int 'n => 'n -> string fromInt = Int.toString + concat : list string -> string concat values = concatList values + concatList : list string -> string -concatList xs = case xs of - [] -> "" - x :: rest -> Builtin.appendString x (concatList rest) +concatList xs = + case xs of + [] -> + "" + + x :: rest -> + Builtin.appendString x (concatList rest) + join : string -> list string -> string -join separator values = case values of - [] -> "" - x :: rest -> Builtin.appendString x (joinRest separator rest) +join separator values = + case values of + [] -> + "" + + x :: rest -> + Builtin.appendString x (joinRest separator rest) + joinRest : string -> list string -> string -joinRest separator xs = case xs of - [] -> "" - x :: rest -> Builtin.appendString separator (Builtin.appendString x (joinRest separator rest)) +joinRest separator xs = + case xs of + [] -> + "" + + x :: rest -> + Builtin.appendString + separator + (Builtin.appendString x (joinRest separator rest)) diff --git a/crates/nash-driver/base/src/Test.nash b/crates/nash-driver/base/src/Test.nash index fb28632d..be18affa 100644 --- a/crates/nash-driver/base/src/Test.nash +++ b/crates/nash-driver/base/src/Test.nash @@ -1,31 +1,47 @@ -module Test exposing (label, prepare, both, assertAt, assertCapture) +module Test exposing ( label, prepare, both, assertAt, assertCapture ) + import Builtin -import Prop exposing (type generator, type prng) -import Option exposing (type option(..)) +import Prop exposing ( type generator, type prng ) +import Option exposing ( type option(..) ) + label : string -> unit label s = Builtin.trace (Builtin.appendString "\u{0000}label\u{0000}" s) () -prepare : generator 'a -> ('a -> unit) -> ('a -> list string) -> prng -> option (prng, unit -> unit, unit -> list string) + +prepare : generator 'a -> + ('a -> unit) -> + ('a -> list string) -> + prng -> option ( prng, unit -> unit, unit -> list string ) prepare generator property display state = case generator state of - None -> None - Some (value, next) -> - Some (next, \() -> property value, \() -> display value) + None -> + None + + Some ( value, next ) -> + Some ( next, \() -> property value, \() -> display value ) + -both : generator 'a -> (unit -> generator 'b) -> generator ('a, 'b) +both : generator 'a -> (unit -> generator 'b) -> generator ( 'a, 'b ) both first rest state = case Prop.group first state of - None -> None - Some (a, next) -> + None -> + None + + Some ( a, next ) -> case rest () next of - None -> None - Some (b, final) -> Some ((a, b), final) + None -> + None + + Some ( b, final ) -> + Some ( ( a, b ), final ) + assertAt : string -> (unit -> unit) -> unit assertAt marker rest = (Builtin.trace marker rest) () + assertCapture : string -> string -> (unit -> unit) -> unit assertCapture prefix value rest = assertAt (Builtin.appendString prefix value) rest diff --git a/crates/nash-driver/base/src/Unit.nash b/crates/nash-driver/base/src/Unit.nash index e9831d5f..05f0036c 100644 --- a/crates/nash-driver/base/src/Unit.nash +++ b/crates/nash-driver/base/src/Unit.nash @@ -1,13 +1,19 @@ -module Unit exposing (Unit(..)) +module Unit exposing ( Unit(..) ) -import Literal exposing (FromUnit) -import Lift exposing (Lift) -type Unit = Unit +import Literal exposing ( FromUnit ) +import Lift exposing ( Lift ) + + +type Unit + = Unit + impl Lift unit Unit where lift _ = Unit.Unit + lower _ = () + impl FromUnit Unit where fromUnit _ = Unit diff --git a/crates/nash-fmt/src/declarations.rs b/crates/nash-fmt/src/declarations.rs index 9b6f9b87..3a940f83 100644 --- a/crates/nash-fmt/src/declarations.rs +++ b/crates/nash-fmt/src/declarations.rs @@ -7,7 +7,7 @@ use nash_source::*; impl Printer<'_> { pub fn exposing(&mut self, exposing: &Exposing<'_>, broken: bool) -> Doc { - match exposing { + let doc = match exposing { Exposing::Open => text("(..)"), Exposing::Explicit(items) => { let docs = items @@ -40,9 +40,10 @@ impl Printer<'_> { } }) .collect(); - self.collection("(", ")", docs, broken).nest() + self.collection("(", ")", docs, broken) } - } + }; + cat([Doc::Line(" "), doc]).nest().group() } fn import(&mut self, import: &Import<'_>) -> Doc { let before = self.before(import.import.region.start); @@ -51,7 +52,7 @@ impl Printer<'_> { docs.push(text(format!(" as {alias}"))); } if !matches!(import.exposing,Exposing::Explicit(items) if items.is_empty()) { - docs.extend([text(" exposing "), self.exposing(import.exposing, false)]); + docs.extend([text(" exposing"), self.exposing(import.exposing, false)]); } cat(docs) } @@ -267,7 +268,7 @@ impl Printer<'_> { "module " }), text(name.value), - text(" exposing "), + text(" exposing"), exports, ])); } diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap index c5f3f07e..46c523dd 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__comments_in_syntax.snap @@ -2,13 +2,15 @@ source: crates/nash-fmt/src/tests.rs description: "module Main exposing (x, -- export\n y)\nimport Foo exposing (a, -- import\n b)\nx = f -- argument\n 1\ny = { a = -- field\n 2, b = [ -- empty\n ] }\n" --- -module Main exposing ( x +module Main exposing + ( x , -- export y ) -import Foo exposing ( a +import Foo exposing + ( a , -- import b ) diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__constrained_signatures.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__constrained_signatures.snap new file mode 100644 index 00000000..a9149982 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__constrained_signatures.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) => 'f 'a ->\n 'g 'a -> list 'a\nappend xs ys = appendList (lowerOuter xs) (lowerOuter ys)\n\nrender : Show 'a => 'a -> string\nrender value = show value\n\ntrait Example 'a where\n append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) => 'f 'a -> 'g 'a -> list 'a\n" +--- +append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) + => 'f 'a -> 'g 'a -> list 'a +append xs ys = appendList (lowerOuter xs) (lowerOuter ys) + + +render : Show 'a => 'a -> string +render value = show value + + +trait Example 'a where + append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) + => 'f 'a -> 'g 'a -> list 'a diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap index 4c139be9..44f133b6 100644 --- a/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__long_lines.snap @@ -2,7 +2,8 @@ source: crates/nash-fmt/src/tests.rs description: "module VeryLongModuleName exposing (firstFunction, secondFunction, thirdFunction, fourthFunction, fifthFunction)\nfunction firstArgument secondArgument thirdArgument = call firstArgument secondArgument thirdArgument \"a fairly long final argument\"\nvalues = [firstLongIdentifier, secondLongIdentifier, thirdLongIdentifier, fourthLongIdentifier]\nfield = { veryLongFirstFieldName = firstLongIdentifier, veryLongSecondFieldName = secondLongIdentifier }\n" --- -module VeryLongModuleName exposing ( firstFunction +module VeryLongModuleName exposing + ( firstFunction , secondFunction , thirdFunction , fourthFunction diff --git a/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_exposing.snap b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_exposing.snap new file mode 100644 index 00000000..cdfbf389 --- /dev/null +++ b/crates/nash-fmt/src/snapshots/nash_fmt__tests__multiline_exposing.snap @@ -0,0 +1,16 @@ +--- +source: crates/nash-fmt/src/tests.rs +description: "module List exposing ( singleton\n , repeat\n , range\n )\n\nimport VeryLongModuleName exposing ( firstLongExportedName, secondLongExportedName, thirdLongExportedName )\n\nsingleton x = [x]\n" +--- +module List exposing + ( singleton + , repeat + , range + ) + + +import VeryLongModuleName exposing + ( firstLongExportedName, secondLongExportedName, thirdLongExportedName ) + + +singleton x = [ x ] diff --git a/crates/nash-fmt/src/tests.rs b/crates/nash-fmt/src/tests.rs index 42ed3473..b6beb001 100644 --- a/crates/nash-fmt/src/tests.rs +++ b/crates/nash-fmt/src/tests.rs @@ -485,3 +485,36 @@ fn definition_layout() { "# ); } + +#[test] +fn constrained_signatures() { + assert_format_snapshot!( + r#" + append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) => 'f 'a -> + 'g 'a -> list 'a + append xs ys = appendList (lowerOuter xs) (lowerOuter ys) + + render : Show 'a => 'a -> string + render value = show value + + trait Example 'a where + append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) => 'f 'a -> 'g 'a -> list 'a + "# + ); +} + +#[test] +fn multiline_exposing() { + assert_format_snapshot!( + r#" + module List exposing ( singleton + , repeat + , range + ) + + import VeryLongModuleName exposing ( firstLongExportedName, secondLongExportedName, thirdLongExportedName ) + + singleton x = [x] + "# + ); +} diff --git a/crates/nash-fmt/src/types.rs b/crates/nash-fmt/src/types.rs index 597d1bca..a2a792bd 100644 --- a/crates/nash-fmt/src/types.rs +++ b/crates/nash-fmt/src/types.rs @@ -90,21 +90,32 @@ impl Printer<'_> { let args = c.args.iter().map(|a| self.typ(a, 2)).collect(); cat([before, self.args(text(name), args, false)]) } - pub fn context(&mut self, constraints: &[&Located>]) -> Doc { - if constraints.is_empty() { - return text(""); - } + fn constraints(&mut self, constraints: &[&Located>]) -> Doc { let docs: Vec<_> = constraints.iter().map(|c| self.constraint(c)).collect(); - let doc = if docs.len() == 1 { + if docs.len() == 1 { docs.into_iter().next().unwrap() } else { self.collection("(", ")", docs, false) - }; - cat([doc, text(" => ")]) + } + } + pub fn context(&mut self, constraints: &[&Located>]) -> Doc { + if constraints.is_empty() { + text("") + } else { + cat([self.constraints(constraints), text(" => ")]) + } } pub fn annotation(&mut self, annotation: &Annotation<'_>) -> Doc { - let context = self.context(annotation.constraints); - cat([context, self.typ(annotation.typ, 0)]).nest() + let context = if annotation.constraints.is_empty() { + text("") + } else { + cat([ + self.constraints(annotation.constraints), + Doc::Line(" "), + text("=> "), + ]) + }; + cat([context, self.typ(annotation.typ, 0)]).nest().group() } pub fn params(&mut self, params: &[&TypeParam<'_>]) -> Doc { join( diff --git a/docs/formatter.md b/docs/formatter.md index 45316f44..25e0fad5 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -34,9 +34,13 @@ layout-sensitive parser determine where those conventions apply. - Short definitions remain on one line, including definitions with annotations. A `do` block follows `=` or `<-` on the same line; its statements indent once. A comment before the block keeps the block below that comment. +- Constrained type signatures wrap before `=>`, which leads the continuation + line. The function type stays together when it fits on that line. - `if`, `case`, `let`, and `do` use multiline bodies. Explicit continuations (`then`, `else`, `in`, and collection closers) may align with a `do` statement. Adjacent statements remain separate expressions. +- Multiline exposing lists start on the next line, with the opening parenthesis + aligned with the leading commas and closing parenthesis. - Collections and applications retain an existing multiline layout; otherwise they stay on one line when they fit. Broken collections use leading commas. - Pipes begin continuation lines and retain an existing multiline layout. diff --git a/plans/13-fmt-docs.md b/plans/13-fmt-docs.md index 8f924e14..f4f63680 100644 --- a/plans/13-fmt-docs.md +++ b/plans/13-fmt-docs.md @@ -92,7 +92,7 @@ The shared `assert_format_snapshot!` helper stores Nash input in the snapshot metadata and formatted Nash in the body. Every test reparses the output, compares the source trees with coordinates removed, and checks idempotence. All 35 shipped Base modules undergo the same structural and idempotence checks. -The formatter crate currently has 32 unit tests, including report snapshots. +The formatter crate currently has 34 unit tests, including report snapshots. --- From 4858f5966cf26e123f2d30eeeae0a9815dedcff3 Mon Sep 17 00:00:00 2001 From: microproofs Date: Sat, 26 Sep 2026 01:26:28 -0400 Subject: [PATCH 07/19] feat(docs): extract public API documentation Signed-off-by: microproofs --- .sampo/changesets/docs-extraction.md | 6 + Cargo.lock | 43 + SPEC.md | 2 +- crates/nash-docs/Cargo.toml | 25 + crates/nash-docs/src/extract.rs | 437 ++++++++++ crates/nash-docs/src/lib.rs | 54 ++ ...h_docs__tests__compiler_owned_modules.snap | 751 ++++++++++++++++++ .../nash_docs__tests__constrained_types.snap | 18 + ...__tests__overview_markdown_boundaries.snap | 22 + ...nash_docs__tests__public_declarations.snap | 52 ++ ...__tests__same_trait_method_constraint.snap | 18 + ...ash_docs__tests__warnings_keep_output.snap | 25 + crates/nash-docs/src/tests.rs | 191 +++++ crates/nash-docs/src/types.rs | 72 ++ crates/nash-driver/base/src/Applicative.nash | 1 + crates/nash-driver/base/src/Array.nash | 4 + crates/nash-driver/base/src/Bool.nash | 5 + crates/nash-driver/base/src/Bytes.nash | 21 + .../nash-driver/base/src/Cardano/Address.nash | 3 + crates/nash-driver/base/src/Cardano/Time.nash | 11 + crates/nash-driver/base/src/Cardano/Tx.nash | 20 + .../nash-driver/base/src/Cardano/Value.nash | 9 + crates/nash-driver/base/src/Cons.nash | 11 + crates/nash-driver/base/src/Crypto.nash | 9 + crates/nash-driver/base/src/Data.nash | 7 + crates/nash-driver/base/src/Eq.nash | 1 + crates/nash-driver/base/src/Function.nash | 2 + crates/nash-driver/base/src/Functor.nash | 1 + crates/nash-driver/base/src/Int.nash | 24 + crates/nash-driver/base/src/Integral.nash | 1 + crates/nash-driver/base/src/Lift.nash | 1 + crates/nash-driver/base/src/List.nash | 30 + crates/nash-driver/base/src/Literal.nash | 5 + crates/nash-driver/base/src/Map.nash | 10 + crates/nash-driver/base/src/Monad.nash | 1 + crates/nash-driver/base/src/Monoid.nash | 1 + crates/nash-driver/base/src/Num.nash | 1 + crates/nash-driver/base/src/Option.nash | 9 + crates/nash-driver/base/src/Ord.nash | 1 + crates/nash-driver/base/src/Ordering.nash | 4 + crates/nash-driver/base/src/Pair.nash | 3 + crates/nash-driver/base/src/Prelude.nash | 7 + crates/nash-driver/base/src/Prop.nash | 19 + crates/nash-driver/base/src/Rational.nash | 14 + crates/nash-driver/base/src/Semigroup.nash | 1 + crates/nash-driver/base/src/Show.nash | 2 + crates/nash-driver/base/src/String.nash | 5 + crates/nash-driver/base/src/Test.nash | 5 + crates/nash-driver/base/src/Unit.nash | 1 + plans/13-fmt-docs.md | 77 +- 50 files changed, 1989 insertions(+), 54 deletions(-) create mode 100644 .sampo/changesets/docs-extraction.md create mode 100644 crates/nash-docs/Cargo.toml create mode 100644 crates/nash-docs/src/extract.rs create mode 100644 crates/nash-docs/src/lib.rs create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__compiler_owned_modules.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__constrained_types.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__overview_markdown_boundaries.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__public_declarations.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__same_trait_method_constraint.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__warnings_keep_output.snap create mode 100644 crates/nash-docs/src/tests.rs create mode 100644 crates/nash-docs/src/types.rs diff --git a/.sampo/changesets/docs-extraction.md b/.sampo/changesets/docs-extraction.md new file mode 100644 index 00000000..5542e9e5 --- /dev/null +++ b/.sampo/changesets/docs-extraction.md @@ -0,0 +1,6 @@ +--- +cargo/nash-docs: minor +cargo/nash-driver: patch +--- + +Extract public documentation from source comments and solved interfaces, and document the compiler-bundled Base API. diff --git a/Cargo.lock b/Cargo.lock index 70a859cd..c0b073d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1987,6 +1987,25 @@ dependencies = [ "nash-region", ] +[[package]] +name = "nash-docs" +version = "0.1.0" +dependencies = [ + "bumpalo", + "indoc", + "insta", + "nash-ast", + "nash-can", + "nash-driver", + "nash-parse", + "nash-report", + "nash-source", + "pulldown-cmark", + "serde", + "tokio", + "url", +] + [[package]] name = "nash-driver" version = "0.11.0" @@ -2660,6 +2679,24 @@ dependencies = [ "version-ranges", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.47" @@ -3651,6 +3688,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.26" diff --git a/SPEC.md b/SPEC.md index bd238a4d..8e6b4bed 100644 --- a/SPEC.md +++ b/SPEC.md @@ -32,7 +32,7 @@ produce UPLC programs; all dependencies inline into each program. | `nash-codegen` | Can -> Core -> UPLC | new ([plans/07](plans/07-codegen.md)) | | `nash-test` | test runner, generation, shrinking | new ([plans/10](plans/10-testing.md)) | | `nash-macro` | macro expansion, comptime | new ([plans/11](plans/11-macros-comptime.md)) | -| `nash-fmt` / `nash-docs` | formatter, docs | formatter implemented; docs pending ([plans/13](plans/13-fmt-docs.md)) | +| `nash-fmt` / `nash-docs` | formatter, docs | formatter and docs extraction implemented; rendering pending ([plans/13](plans/13-fmt-docs.md)) | | `nash-plutus` | UPLC terms, flat, CEK, cost models | done | | `nash-config` | `nash.jsonc` | done, extend | | `nash-driver` | build graph, caching | done, extend | diff --git a/crates/nash-docs/Cargo.toml b/crates/nash-docs/Cargo.toml new file mode 100644 index 00000000..61ff2e92 --- /dev/null +++ b/crates/nash-docs/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nash-docs" +version = "0.1.0" +edition.workspace = true +description = "Documentation extraction and rendering for Nash" +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +nash-ast = { path = "../nash-ast", version = "0.11.0" } +nash-can = { path = "../nash-can", version = "0.12.0" } +nash-source = { path = "../nash-source", version = "0.9.0" } +nash-report = { path = "../nash-report", version = "0.5.3" } +serde.workspace = true +pulldown-cmark = { version = "0.13", default-features = false, features = ["html"] } + +[dev-dependencies] +bumpalo.workspace = true +indoc.workspace = true +insta.workspace = true +nash-driver = { path = "../nash-driver" } +nash-parse = { path = "../nash-parse" } +tokio.workspace = true +url.workspace = true diff --git a/crates/nash-docs/src/extract.rs b/crates/nash-docs/src/extract.rs new file mode 100644 index 00000000..0bcc33df --- /dev/null +++ b/crates/nash-docs/src/extract.rs @@ -0,0 +1,437 @@ +use crate::{Block, Declaration, DeclarationKind as K, DocsWarning, Extraction, ModuleDocs, types}; +use nash_can::Interface; +use nash_report::{ + localizer::Localizer, + render_type::{Ctx, src_to_doc}, +}; +use nash_source::{Comment, Docs, Module}; + +fn comment(comment: Option<&Comment<'_>>) -> String { + comment.map_or_else(String::new, |c| { + String::from_utf8_lossy(c.snippet.data).trim().to_owned() + }) +} + +/// Extract public declarations. Warnings never discard documentation. +pub fn extract(source: &Module<'_>, interface: &Interface<'_>) -> Extraction { + let name = interface.home.name.to_owned(); + let local = Localizer::from_module(source, &[]).with_package(interface.home.package); + let mut declarations = Vec::new(); + let mut warnings = Vec::new(); + let mut add = |line, name: &str, kind, signature, doc: String, require_doc| { + if require_doc && doc.is_empty() { + warnings.push(DocsWarning { + module: interface.home.name.into(), + name: name.into(), + message: "Export has no documentation comment.".into(), + }); + } + let type_kind = match kind { + K::Union => interface + .unions + .iter() + .find(|u| u.name == name) + .map(|u| types::kind(u.kind)), + K::Alias => interface + .aliases + .iter() + .find(|a| a.name == name) + .map(|a| types::kind(a.kind)), + _ => None, + }; + declarations.push(( + line, + Declaration { + name: name.into(), + kind, + signature, + type_kind, + doc, + }, + )); + }; + for value in interface.values { + let definition = source + .values + .iter() + .find(|v| v.value.name.value == value.name); + add( + definition.map_or(0, |v| v.region.start.line), + value.name, + K::Value, + format!( + "{} : {}", + value.name, + types::annotation(&local, value.annotation) + ), + comment(definition.and_then(|v| v.value.docs)), + true, + ); + } + for union in interface.unions.iter().filter_map(|u| u.to_public()) { + let definition = source + .unions + .iter() + .find(|u| u.value.name.value == union.name); + let mut signature = format!( + "type {}{}{}", + types::context(&local, union.context), + union.name, + parameters(union.parameters) + ); + if !union.ctors.is_empty() { + signature.push_str("\n = "); + signature.push_str( + &union + .ctors + .iter() + .map(|c| types::constructor(&local, c)) + .collect::>() + .join("\n | "), + ); + } + add( + definition.map_or(0, |v| v.region.start.line), + union.name, + K::Union, + signature, + comment(definition.and_then(|v| v.value.docs)), + true, + ); + } + for alias in interface.aliases.iter().filter_map(|a| a.to_public()) { + let definition = source + .aliases + .iter() + .find(|a| a.value.name.value == alias.name); + add( + definition.map_or(0, |v| v.region.start.line), + alias.name, + K::Alias, + format!( + "type alias {}{}{} = {}", + types::context(&local, alias.context), + alias.name, + parameters(alias.parameters), + types::typ(&local, &alias.typ.value, Ctx::None) + ), + comment(definition.and_then(|v| v.value.docs)), + true, + ); + } + for trait_ in interface.traits.iter().filter(|t| t.exported) { + let definition = source + .traits + .iter() + .find(|t| t.value.name.value == trait_.name); + let mut signature = format!( + "trait {}{}{} where", + types::context(&local, trait_.supers), + trait_.name, + parameters(trait_.parameters) + ); + for method in trait_.methods { + // The enclosing trait supplies its own predicate. + let context = &method.annotation.context[1..]; + signature.push_str(&format!( + "\n {} : {}{}", + method.name, + types::context(&local, context), + types::typ(&local, &method.annotation.typ.value, Ctx::None) + )); + } + add( + definition.map_or(0, |v| v.region.start.line), + trait_.name, + K::Trait, + signature, + comment(definition.and_then(|v| v.value.docs)), + true, + ); + } + for operator in interface.binops { + let definition = source.binops.iter().find(|b| b.value.op == operator.symbol); + let doc = comment( + source + .values + .iter() + .find(|v| { + operator.function.home == interface.home + && v.value.name.value == operator.function.name + }) + .and_then(|v| v.value.docs), + ); + let doc = if doc.is_empty() { + format!( + "Infix form of `{}.{}`.", + operator.function.home.name, operator.function.name + ) + } else { + doc + }; + let associativity = match operator.associativity { + nash_ast::Associativity::Left => "left", + nash_ast::Associativity::Right => "right", + nash_ast::Associativity::None => "non", + }; + add( + definition.map_or(0, |v| v.region.start.line), + &format!("({})", operator.symbol), + K::Operator, + format!( + "({}) : {}\ninfix {associativity} {} ({}) = {}", + operator.symbol, + types::annotation(&local, operator.annotation), + operator.precedence.0, + operator.symbol, + local + .to_doc(operator.function.home, operator.function.name) + .render(100, false) + ), + doc, + false, + ); + } + for implementation in source.impls { + let head = constraint(&implementation.value.head.value); + let context = implementation + .value + .context + .iter() + .map(|c| constraint(&c.value)) + .collect::>(); + let context = match context.as_slice() { + [] => String::new(), + [one] => format!("{one} => "), + _ => format!("({}) => ", context.join(", ")), + }; + let signature = format!("impl {context}{head}"); + add( + implementation.region.start.line, + &signature, + K::Implementation, + signature.clone(), + comment(implementation.value.docs), + false, + ); + } + declarations.sort_by_key(|(line, _)| *line); + let overview = match source.docs { + Docs::YesDocs { overview, .. } => comment(Some(overview)), + Docs::NoDocs(_) => String::new(), + }; + let mut pending: Vec<_> = declarations.into_iter().map(|(_, d)| Some(d)).collect(); + let mut blocks = Vec::new(); + let mut prose = String::new(); + let code_ranges: Vec<_> = pulldown_cmark::Parser::new(&overview) + .into_offset_iter() + .filter_map(|(event, range)| { + matches!( + event, + pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(_)) + ) + .then_some(range) + }) + .collect(); + let mut seen = std::collections::HashSet::new(); + let mut lines = overview + .split_inclusive('\n') + .scan(0, |offset, line| { + let start = *offset; + *offset += line.len(); + Some((start, line.trim_end_matches('\n'))) + }) + .peekable(); + while let Some((offset, line)) = lines.next() { + let trimmed = line.trim_start(); + let directive = trimmed + .strip_prefix("@docs") + .filter(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace)); + if let Some(rest) = directive.filter(|_| { + !code_ranges + .iter() + .any(|range| range.contains(&(offset + line.len() - trimmed.len()))) + }) { + push_prose(&mut blocks, &mut prose); + let mut names = rest.trim().to_owned(); + while names.is_empty() || names.ends_with(',') { + let Some((_, next)) = lines.peek().filter(|(_, next)| doc_names(next.trim())) + else { + break; + }; + names.push_str(next.trim()); + lines.next(); + } + if names.is_empty() { + warnings.push(DocsWarning { + module: name.clone(), + name: "@docs".into(), + message: "@docs requires at least one exported name.".into(), + }); + } + for item in names.split(',').map(str::trim).filter(|n| !n.is_empty()) { + let item = item.to_owned(); + if !seen.insert(item.clone()) { + warnings.push(DocsWarning { + module: name.clone(), + name: item, + message: "Repeated @docs entry.".into(), + }); + } else if let Some(entry) = pending + .iter_mut() + .find(|d| d.as_ref().is_some_and(|d| d.name == item)) + { + blocks.push(Block::Declaration(entry.take().unwrap())); + } else { + warnings.push(DocsWarning { + module: name.clone(), + name: item, + message: "@docs name is not exported by this module.".into(), + }); + } + } + } else { + prose.push_str(line); + prose.push('\n'); + } + } + push_prose(&mut blocks, &mut prose); + blocks.extend(pending.into_iter().flatten().map(Block::Declaration)); + Extraction { + module: ModuleDocs { name, blocks }, + warnings, + } +} + +fn doc_names(line: &str) -> bool { + !line.is_empty() + && line.trim_end_matches(',').split(',').all(|part| { + let name = part.trim(); + if name.starts_with('(') && name.ends_with(')') { + name.len() > 2 + && name[1..name.len() - 1] + .chars() + .all(|c| "+-/*=.$<>:&|^?%!~#".contains(c)) + } else { + name.starts_with(char::is_alphabetic) + && name + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '\'') + } + }) +} + +fn push_prose(blocks: &mut Vec, prose: &mut String) { + if !prose.trim().is_empty() { + blocks.push(Block::Text(prose.trim_matches('\n').to_owned())); + } + prose.clear(); +} +fn parameters(parameters: &[&str]) -> String { + parameters.iter().map(|p| format!(" '{p}")).collect() +} +fn constraint(constraint: &nash_source::Constraint<'_>) -> String { + let mut out = constraint.module.map_or_else( + || constraint.class.value.to_owned(), + |m| format!("{m}.{}", constraint.class.value), + ); + for arg in constraint.args { + out.push(' '); + out.push_str(&src_to_doc(Ctx::App, arg).render(100, false)); + } + out +} + +/// Compiler-owned modules have no source file, so document their actual catalog. +pub fn primitives() -> Vec { + use nash_ast::primitives::{BUILTINS, COERCE, PRIMITIVES, ReprTrait}; + let local = Localizer::from_names(["Primitive", "Builtin"]); + let builtins = BUILTINS + .iter() + .map(|b| { + Block::Declaration(Declaration { + name: b.name.into(), + kind: K::Builtin, + type_kind: None, + signature: format!( + "{} : {}{}", + b.name, + types::context(&local, b.context), + types::typ(&local, &b.typ.value, Ctx::None) + ), + doc: format!("Plutus Core `{}` builtin.", b.variant), + }) + }) + .collect(); + let mut primitive: Vec<_> = PRIMITIVES + .iter() + .map(|p| { + let mut doc = format!( + "{} representation; kind `{}`.", + match p.repr { + nash_ast::primitives::Repr::Big => "Data", + nash_ast::primitives::Repr::Const => "Constant", + nash_ast::primitives::Repr::Term => "Term", + }, + types::kind(p.kind) + ); + for (index, repr) in p.context { + doc.push_str(&format!( + " Parameter {} requires `{}`.", + index + 1, + repr.name() + )); + } + let mut signature = format!( + "type {}{}", + p.name, + (0..p.kind.arity()) + .map(|index| format!(" '{}", char::from(b'a' + index as u8))) + .collect::() + ); + if !p.ctors.is_empty() { + signature.push_str(" = "); + signature.push_str( + &p.ctors + .iter() + .map(|c| types::constructor(&local, c)) + .collect::>() + .join(" | "), + ); + } + Block::Declaration(Declaration { + name: p.name.into(), + kind: K::Primitive, + signature, + type_kind: Some(types::kind(p.kind)), + doc, + }) + }) + .collect(); + for repr in ReprTrait::ALL { + primitive.push(Block::Declaration(Declaration { + name: repr.name().into(), + kind: K::Trait, + type_kind: None, + signature: format!("trait {} 'a", repr.name()), + doc: "Compiler-checked representation constraint; no runtime test is emitted.".into(), + })); + } + primitive.push(Block::Declaration(Declaration { + name: "coerce".into(), + kind: K::Value, + type_kind: None, + signature: format!("coerce : {}", types::annotation(&local, &COERCE)), + doc: "Unchecked coercion. The caller is responsible for the runtime representation.".into(), + })); + vec![ + ModuleDocs { + name: "Builtin".into(), + blocks: builtins, + }, + ModuleDocs { + name: "Primitive".into(), + blocks: primitive, + }, + ] +} diff --git a/crates/nash-docs/src/lib.rs b/crates/nash-docs/src/lib.rs new file mode 100644 index 00000000..bf30b40f --- /dev/null +++ b/crates/nash-docs/src/lib.rs @@ -0,0 +1,54 @@ +//! Owned documentation extracted from source comments and solved interfaces. +mod extract; +mod types; +pub use extract::{extract, primitives}; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct ModuleDocs { + pub name: String, + pub blocks: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub enum Block { + Text(String), + Declaration(Declaration), +} + +#[derive(Debug, Clone, Serialize)] +pub struct Declaration { + pub name: String, + pub kind: DeclarationKind, + pub signature: String, + pub type_kind: Option, + pub doc: String, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum DeclarationKind { + Value, + Union, + Alias, + Operator, + Trait, + Implementation, + Builtin, + Primitive, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DocsWarning { + pub module: String, + pub name: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Extraction { + pub module: ModuleDocs, + pub warnings: Vec, +} + +#[cfg(test)] +mod tests; diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__compiler_owned_modules.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__compiler_owned_modules.snap new file mode 100644 index 00000000..81b3135e --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__compiler_owned_modules.snap @@ -0,0 +1,751 @@ +--- +source: crates/nash-docs/src/tests.rs +--- +- name: Builtin + blocks: + - Declaration: + name: addInteger + kind: Builtin + signature: "addInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `AddInteger` builtin." + - Declaration: + name: subtractInteger + kind: Builtin + signature: "subtractInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `SubtractInteger` builtin." + - Declaration: + name: multiplyInteger + kind: Builtin + signature: "multiplyInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `MultiplyInteger` builtin." + - Declaration: + name: divideInteger + kind: Builtin + signature: "divideInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `DivideInteger` builtin." + - Declaration: + name: quotientInteger + kind: Builtin + signature: "quotientInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `QuotientInteger` builtin." + - Declaration: + name: remainderInteger + kind: Builtin + signature: "remainderInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `RemainderInteger` builtin." + - Declaration: + name: modInteger + kind: Builtin + signature: "modInteger : int -> int -> int" + type_kind: ~ + doc: "Plutus Core `ModInteger` builtin." + - Declaration: + name: equalsInteger + kind: Builtin + signature: "equalsInteger : int -> int -> bool" + type_kind: ~ + doc: "Plutus Core `EqualsInteger` builtin." + - Declaration: + name: lessThanInteger + kind: Builtin + signature: "lessThanInteger : int -> int -> bool" + type_kind: ~ + doc: "Plutus Core `LessThanInteger` builtin." + - Declaration: + name: lessThanEqualsInteger + kind: Builtin + signature: "lessThanEqualsInteger : int -> int -> bool" + type_kind: ~ + doc: "Plutus Core `LessThanEqualsInteger` builtin." + - Declaration: + name: appendByteString + kind: Builtin + signature: "appendByteString : bytes -> bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `AppendByteString` builtin." + - Declaration: + name: consByteString + kind: Builtin + signature: "consByteString : int -> bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `ConsByteString` builtin." + - Declaration: + name: sliceByteString + kind: Builtin + signature: "sliceByteString : int -> int -> bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `SliceByteString` builtin." + - Declaration: + name: lengthOfByteString + kind: Builtin + signature: "lengthOfByteString : bytes -> int" + type_kind: ~ + doc: "Plutus Core `LengthOfByteString` builtin." + - Declaration: + name: indexByteString + kind: Builtin + signature: "indexByteString : bytes -> int -> int" + type_kind: ~ + doc: "Plutus Core `IndexByteString` builtin." + - Declaration: + name: equalsByteString + kind: Builtin + signature: "equalsByteString : bytes -> bytes -> bool" + type_kind: ~ + doc: "Plutus Core `EqualsByteString` builtin." + - Declaration: + name: lessThanByteString + kind: Builtin + signature: "lessThanByteString : bytes -> bytes -> bool" + type_kind: ~ + doc: "Plutus Core `LessThanByteString` builtin." + - Declaration: + name: lessThanEqualsByteString + kind: Builtin + signature: "lessThanEqualsByteString : bytes -> bytes -> bool" + type_kind: ~ + doc: "Plutus Core `LessThanEqualsByteString` builtin." + - Declaration: + name: sha2_256 + kind: Builtin + signature: "sha2_256 : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `Sha2_256` builtin." + - Declaration: + name: sha3_256 + kind: Builtin + signature: "sha3_256 : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `Sha3_256` builtin." + - Declaration: + name: blake2b_256 + kind: Builtin + signature: "blake2b_256 : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `Blake2b_256` builtin." + - Declaration: + name: blake2b_224 + kind: Builtin + signature: "blake2b_224 : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `Blake2b_224` builtin." + - Declaration: + name: keccak_256 + kind: Builtin + signature: "keccak_256 : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `Keccak_256` builtin." + - Declaration: + name: ripemd_160 + kind: Builtin + signature: "ripemd_160 : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `Ripemd_160` builtin." + - Declaration: + name: verifyEd25519Signature + kind: Builtin + signature: "verifyEd25519Signature : bytes -> bytes -> bytes -> bool" + type_kind: ~ + doc: "Plutus Core `VerifyEd25519Signature` builtin." + - Declaration: + name: verifyEcdsaSecp256k1Signature + kind: Builtin + signature: "verifyEcdsaSecp256k1Signature : bytes -> bytes -> bytes -> bool" + type_kind: ~ + doc: "Plutus Core `VerifyEcdsaSecp256k1Signature` builtin." + - Declaration: + name: verifySchnorrSecp256k1Signature + kind: Builtin + signature: "verifySchnorrSecp256k1Signature : bytes -> bytes -> bytes -> bool" + type_kind: ~ + doc: "Plutus Core `VerifySchnorrSecp256k1Signature` builtin." + - Declaration: + name: appendString + kind: Builtin + signature: "appendString : string -> string -> string" + type_kind: ~ + doc: "Plutus Core `AppendString` builtin." + - Declaration: + name: equalsString + kind: Builtin + signature: "equalsString : string -> string -> bool" + type_kind: ~ + doc: "Plutus Core `EqualsString` builtin." + - Declaration: + name: encodeUtf8 + kind: Builtin + signature: "encodeUtf8 : string -> bytes" + type_kind: ~ + doc: "Plutus Core `EncodeUtf8` builtin." + - Declaration: + name: decodeUtf8 + kind: Builtin + signature: "decodeUtf8 : bytes -> string" + type_kind: ~ + doc: "Plutus Core `DecodeUtf8` builtin." + - Declaration: + name: ifThenElse + kind: Builtin + signature: "ifThenElse : bool -> 'a -> 'a -> 'a" + type_kind: ~ + doc: "Plutus Core `IfThenElse` builtin." + - Declaration: + name: chooseUnit + kind: Builtin + signature: "chooseUnit : unit -> 'a -> 'a" + type_kind: ~ + doc: "Plutus Core `ChooseUnit` builtin." + - Declaration: + name: trace + kind: Builtin + signature: "trace : string -> 'a -> 'a" + type_kind: ~ + doc: "Plutus Core `Trace` builtin." + - Declaration: + name: fstPair + kind: Builtin + signature: "fstPair : pair 'a 'b -> 'a" + type_kind: ~ + doc: "Plutus Core `FstPair` builtin." + - Declaration: + name: sndPair + kind: Builtin + signature: "sndPair : pair 'a 'b -> 'b" + type_kind: ~ + doc: "Plutus Core `SndPair` builtin." + - Declaration: + name: chooseList + kind: Builtin + signature: "chooseList : list 'a -> 'b -> 'b -> 'b" + type_kind: ~ + doc: "Plutus Core `ChooseList` builtin." + - Declaration: + name: mkCons + kind: Builtin + signature: "mkCons : 'a -> list 'a -> list 'a" + type_kind: ~ + doc: "Plutus Core `MkCons` builtin." + - Declaration: + name: headList + kind: Builtin + signature: "headList : list 'a -> 'a" + type_kind: ~ + doc: "Plutus Core `HeadList` builtin." + - Declaration: + name: tailList + kind: Builtin + signature: "tailList : list 'a -> list 'a" + type_kind: ~ + doc: "Plutus Core `TailList` builtin." + - Declaration: + name: nullList + kind: Builtin + signature: "nullList : list 'a -> bool" + type_kind: ~ + doc: "Plutus Core `NullList` builtin." + - Declaration: + name: dropList + kind: Builtin + signature: "dropList : int -> list 'a -> list 'a" + type_kind: ~ + doc: "Plutus Core `DropList` builtin." + - Declaration: + name: chooseData + kind: Builtin + signature: "chooseData : Data -> 'a -> 'a -> 'a -> 'a -> 'a -> 'a" + type_kind: ~ + doc: "Plutus Core `ChooseData` builtin." + - Declaration: + name: constrData + kind: Builtin + signature: "constrData : int -> list Data -> Data" + type_kind: ~ + doc: "Plutus Core `ConstrData` builtin." + - Declaration: + name: mapData + kind: Builtin + signature: "mapData : (Big 'a, Big 'b) => list (pair 'a 'b) -> Map 'a 'b" + type_kind: ~ + doc: "Plutus Core `MapData` builtin." + - Declaration: + name: listData + kind: Builtin + signature: "listData : Big 'a => list 'a -> List 'a" + type_kind: ~ + doc: "Plutus Core `ListData` builtin." + - Declaration: + name: iData + kind: Builtin + signature: "iData : int -> Int" + type_kind: ~ + doc: "Plutus Core `IData` builtin." + - Declaration: + name: bData + kind: Builtin + signature: "bData : bytes -> Bytes" + type_kind: ~ + doc: "Plutus Core `BData` builtin." + - Declaration: + name: unConstrData + kind: Builtin + signature: "unConstrData : Data -> pair int (list Data)" + type_kind: ~ + doc: "Plutus Core `UnConstrData` builtin." + - Declaration: + name: unMapData + kind: Builtin + signature: "unMapData : (Big 'a, Big 'b) => Map 'a 'b -> list (pair 'a 'b)" + type_kind: ~ + doc: "Plutus Core `UnMapData` builtin." + - Declaration: + name: unListData + kind: Builtin + signature: "unListData : Big 'a => List 'a -> list 'a" + type_kind: ~ + doc: "Plutus Core `UnListData` builtin." + - Declaration: + name: unIData + kind: Builtin + signature: "unIData : Int -> int" + type_kind: ~ + doc: "Plutus Core `UnIData` builtin." + - Declaration: + name: unBData + kind: Builtin + signature: "unBData : Bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `UnBData` builtin." + - Declaration: + name: equalsData + kind: Builtin + signature: "equalsData : Data -> Data -> bool" + type_kind: ~ + doc: "Plutus Core `EqualsData` builtin." + - Declaration: + name: serialiseData + kind: Builtin + signature: "serialiseData : Data -> bytes" + type_kind: ~ + doc: "Plutus Core `SerialiseData` builtin." + - Declaration: + name: mkPairData + kind: Builtin + signature: "mkPairData : (Big 'a, Big 'b) => 'a -> 'b -> pair 'a 'b" + type_kind: ~ + doc: "Plutus Core `MkPairData` builtin." + - Declaration: + name: mkNilData + kind: Builtin + signature: "mkNilData : unit -> list Data" + type_kind: ~ + doc: "Plutus Core `MkNilData` builtin." + - Declaration: + name: mkNilPairData + kind: Builtin + signature: "mkNilPairData : unit -> list (pair Data Data)" + type_kind: ~ + doc: "Plutus Core `MkNilPairData` builtin." + - Declaration: + name: bls12_381_g1_add + kind: Builtin + signature: "bls12_381_g1_add : bls_g1 -> bls_g1 -> bls_g1" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_Add` builtin." + - Declaration: + name: bls12_381_g1_neg + kind: Builtin + signature: "bls12_381_g1_neg : bls_g1 -> bls_g1" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_Neg` builtin." + - Declaration: + name: bls12_381_g1_scalarMul + kind: Builtin + signature: "bls12_381_g1_scalarMul : int -> bls_g1 -> bls_g1" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_ScalarMul` builtin." + - Declaration: + name: bls12_381_g1_equal + kind: Builtin + signature: "bls12_381_g1_equal : bls_g1 -> bls_g1 -> bool" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_Equal` builtin." + - Declaration: + name: bls12_381_g1_compress + kind: Builtin + signature: "bls12_381_g1_compress : bls_g1 -> bytes" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_Compress` builtin." + - Declaration: + name: bls12_381_g1_uncompress + kind: Builtin + signature: "bls12_381_g1_uncompress : bytes -> bls_g1" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_Uncompress` builtin." + - Declaration: + name: bls12_381_g1_hashToGroup + kind: Builtin + signature: "bls12_381_g1_hashToGroup : bytes -> bytes -> bls_g1" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_HashToGroup` builtin." + - Declaration: + name: bls12_381_g1_multiScalarMul + kind: Builtin + signature: "bls12_381_g1_multiScalarMul : list int -> list bls_g1 -> bls_g1" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G1_MultiScalarMul` builtin." + - Declaration: + name: bls12_381_g2_add + kind: Builtin + signature: "bls12_381_g2_add : bls_g2 -> bls_g2 -> bls_g2" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_Add` builtin." + - Declaration: + name: bls12_381_g2_neg + kind: Builtin + signature: "bls12_381_g2_neg : bls_g2 -> bls_g2" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_Neg` builtin." + - Declaration: + name: bls12_381_g2_scalarMul + kind: Builtin + signature: "bls12_381_g2_scalarMul : int -> bls_g2 -> bls_g2" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_ScalarMul` builtin." + - Declaration: + name: bls12_381_g2_equal + kind: Builtin + signature: "bls12_381_g2_equal : bls_g2 -> bls_g2 -> bool" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_Equal` builtin." + - Declaration: + name: bls12_381_g2_compress + kind: Builtin + signature: "bls12_381_g2_compress : bls_g2 -> bytes" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_Compress` builtin." + - Declaration: + name: bls12_381_g2_uncompress + kind: Builtin + signature: "bls12_381_g2_uncompress : bytes -> bls_g2" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_Uncompress` builtin." + - Declaration: + name: bls12_381_g2_hashToGroup + kind: Builtin + signature: "bls12_381_g2_hashToGroup : bytes -> bytes -> bls_g2" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_HashToGroup` builtin." + - Declaration: + name: bls12_381_g2_multiScalarMul + kind: Builtin + signature: "bls12_381_g2_multiScalarMul : list int -> list bls_g2 -> bls_g2" + type_kind: ~ + doc: "Plutus Core `Bls12_381_G2_MultiScalarMul` builtin." + - Declaration: + name: bls12_381_millerLoop + kind: Builtin + signature: "bls12_381_millerLoop : bls_g1 -> bls_g2 -> bls_mlr" + type_kind: ~ + doc: "Plutus Core `Bls12_381_MillerLoop` builtin." + - Declaration: + name: bls12_381_mulMlResult + kind: Builtin + signature: "bls12_381_mulMlResult : bls_mlr -> bls_mlr -> bls_mlr" + type_kind: ~ + doc: "Plutus Core `Bls12_381_MulMlResult` builtin." + - Declaration: + name: bls12_381_finalVerify + kind: Builtin + signature: "bls12_381_finalVerify : bls_mlr -> bls_mlr -> bool" + type_kind: ~ + doc: "Plutus Core `Bls12_381_FinalVerify` builtin." + - Declaration: + name: integerToByteString + kind: Builtin + signature: "integerToByteString : bool -> int -> int -> bytes" + type_kind: ~ + doc: "Plutus Core `IntegerToByteString` builtin." + - Declaration: + name: byteStringToInteger + kind: Builtin + signature: "byteStringToInteger : bool -> bytes -> int" + type_kind: ~ + doc: "Plutus Core `ByteStringToInteger` builtin." + - Declaration: + name: andByteString + kind: Builtin + signature: "andByteString : bool -> bytes -> bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `AndByteString` builtin." + - Declaration: + name: orByteString + kind: Builtin + signature: "orByteString : bool -> bytes -> bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `OrByteString` builtin." + - Declaration: + name: xorByteString + kind: Builtin + signature: "xorByteString : bool -> bytes -> bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `XorByteString` builtin." + - Declaration: + name: complementByteString + kind: Builtin + signature: "complementByteString : bytes -> bytes" + type_kind: ~ + doc: "Plutus Core `ComplementByteString` builtin." + - Declaration: + name: readBit + kind: Builtin + signature: "readBit : bytes -> int -> bool" + type_kind: ~ + doc: "Plutus Core `ReadBit` builtin." + - Declaration: + name: writeBits + kind: Builtin + signature: "writeBits : bytes -> list int -> bool -> bytes" + type_kind: ~ + doc: "Plutus Core `WriteBits` builtin." + - Declaration: + name: replicateByte + kind: Builtin + signature: "replicateByte : int -> int -> bytes" + type_kind: ~ + doc: "Plutus Core `ReplicateByte` builtin." + - Declaration: + name: shiftByteString + kind: Builtin + signature: "shiftByteString : bytes -> int -> bytes" + type_kind: ~ + doc: "Plutus Core `ShiftByteString` builtin." + - Declaration: + name: rotateByteString + kind: Builtin + signature: "rotateByteString : bytes -> int -> bytes" + type_kind: ~ + doc: "Plutus Core `RotateByteString` builtin." + - Declaration: + name: countSetBits + kind: Builtin + signature: "countSetBits : bytes -> int" + type_kind: ~ + doc: "Plutus Core `CountSetBits` builtin." + - Declaration: + name: findFirstSetBit + kind: Builtin + signature: "findFirstSetBit : bytes -> int" + type_kind: ~ + doc: "Plutus Core `FindFirstSetBit` builtin." + - Declaration: + name: expModInteger + kind: Builtin + signature: "expModInteger : int -> int -> int -> int" + type_kind: ~ + doc: "Plutus Core `ExpModInteger` builtin." + - Declaration: + name: lengthOfArray + kind: Builtin + signature: "lengthOfArray : array 'a -> int" + type_kind: ~ + doc: "Plutus Core `LengthOfArray` builtin." + - Declaration: + name: listToArray + kind: Builtin + signature: "listToArray : list 'a -> array 'a" + type_kind: ~ + doc: "Plutus Core `ListToArray` builtin." + - Declaration: + name: indexArray + kind: Builtin + signature: "indexArray : array 'a -> int -> 'a" + type_kind: ~ + doc: "Plutus Core `IndexArray` builtin." + - Declaration: + name: insertCoin + kind: Builtin + signature: "insertCoin : bytes -> bytes -> int -> value -> value" + type_kind: ~ + doc: "Plutus Core `InsertCoin` builtin." + - Declaration: + name: lookupCoin + kind: Builtin + signature: "lookupCoin : bytes -> bytes -> value -> int" + type_kind: ~ + doc: "Plutus Core `LookupCoin` builtin." + - Declaration: + name: unionValue + kind: Builtin + signature: "unionValue : value -> value -> value" + type_kind: ~ + doc: "Plutus Core `UnionValue` builtin." + - Declaration: + name: valueContains + kind: Builtin + signature: "valueContains : value -> value -> bool" + type_kind: ~ + doc: "Plutus Core `ValueContains` builtin." + - Declaration: + name: valueData + kind: Builtin + signature: "valueData : value -> Data" + type_kind: ~ + doc: "Plutus Core `ValueData` builtin." + - Declaration: + name: unValueData + kind: Builtin + signature: "unValueData : Data -> value" + type_kind: ~ + doc: "Plutus Core `UnValueData` builtin." + - Declaration: + name: scaleValue + kind: Builtin + signature: "scaleValue : int -> value -> value" + type_kind: ~ + doc: "Plutus Core `ScaleValue` builtin." +- name: Primitive + blocks: + - Declaration: + name: Data + kind: Primitive + signature: type Data = Constr (pair int (list Data)) | Map (list (pair Data Data)) | List (list Data) | I int | B bytes + type_kind: Type + doc: "Data representation; kind `Type`." + - Declaration: + name: Int + kind: Primitive + signature: type Int + type_kind: Type + doc: "Data representation; kind `Type`." + - Declaration: + name: Bytes + kind: Primitive + signature: type Bytes + type_kind: Type + doc: "Data representation; kind `Type`." + - Declaration: + name: List + kind: Primitive + signature: "type List 'a" + type_kind: Type -> Type + doc: "Data representation; kind `Type -> Type`. Parameter 1 requires `Big`." + - Declaration: + name: Map + kind: Primitive + signature: "type Map 'a 'b" + type_kind: Type -> Type -> Type + doc: "Data representation; kind `Type -> Type -> Type`. Parameter 1 requires `Big`. Parameter 2 requires `Big`." + - Declaration: + name: int + kind: Primitive + signature: type int + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: bytes + kind: Primitive + signature: type bytes + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: string + kind: Primitive + signature: type string + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: bool + kind: Primitive + signature: type bool = False | True + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: unit + kind: Primitive + signature: type unit + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: bls_g1 + kind: Primitive + signature: type bls_g1 + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: bls_g2 + kind: Primitive + signature: type bls_g2 + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: bls_mlr + kind: Primitive + signature: type bls_mlr + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: value + kind: Primitive + signature: type value + type_kind: Type + doc: "Constant representation; kind `Type`." + - Declaration: + name: list + kind: Primitive + signature: "type list 'a" + type_kind: Type -> Type + doc: "Constant representation; kind `Type -> Type`. Parameter 1 requires `Storable`." + - Declaration: + name: array + kind: Primitive + signature: "type array 'a" + type_kind: Type -> Type + doc: "Constant representation; kind `Type -> Type`. Parameter 1 requires `Storable`." + - Declaration: + name: pair + kind: Primitive + signature: "type pair 'a 'b" + type_kind: Type -> Type -> Type + doc: "Constant representation; kind `Type -> Type -> Type`. Parameter 1 requires `Storable`. Parameter 2 requires `Storable`." + - Declaration: + name: Big + kind: Trait + signature: "trait Big 'a" + type_kind: ~ + doc: Compiler-checked representation constraint; no runtime test is emitted. + - Declaration: + name: Const + kind: Trait + signature: "trait Const 'a" + type_kind: ~ + doc: Compiler-checked representation constraint; no runtime test is emitted. + - Declaration: + name: Term + kind: Trait + signature: "trait Term 'a" + type_kind: ~ + doc: Compiler-checked representation constraint; no runtime test is emitted. + - Declaration: + name: Storable + kind: Trait + signature: "trait Storable 'a" + type_kind: ~ + doc: Compiler-checked representation constraint; no runtime test is emitted. + - Declaration: + name: Little + kind: Trait + signature: "trait Little 'a" + type_kind: ~ + doc: Compiler-checked representation constraint; no runtime test is emitted. + - Declaration: + name: coerce + kind: Value + signature: "coerce : 'a -> 'b" + type_kind: ~ + doc: Unchecked coercion. The caller is responsible for the runtime representation. diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__constrained_types.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__constrained_types.snap new file mode 100644 index 00000000..f3dde970 --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__constrained_types.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Example exposing (render)\n{-| Render a value with its Show instance. -}\nrender : Show 'a => 'a -> string\nrender value = show value\n" +--- +module: + name: Example + blocks: + - Text: Render a value with its Show instance. + - Declaration: + name: render + kind: Value + signature: "render : Show.Show 'a => 'a -> string" + type_kind: ~ + doc: "" +warnings: + - module: Example + name: render + message: Export has no documentation comment. diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__overview_markdown_boundaries.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__overview_markdown_boundaries.snap new file mode 100644 index 00000000..76759fa4 --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__overview_markdown_boundaries.snap @@ -0,0 +1,22 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Example exposing (first, second)\n{-| Order and prose.\n\n@docs\n second,\n## Keep this heading\n\n````nash\n```\n@docs first\n````\n\n @docs alsoCode\n\n@docs first\n-}\n{-| First value. -}\nfirst x = x\n{-| Second value. -}\nsecond x = x\n" +--- +module: + name: Example + blocks: + - Text: Order and prose. + - Declaration: + name: second + kind: Value + signature: "second : 'a -> 'a" + type_kind: ~ + doc: Second value. + - Text: "## Keep this heading\n\n````nash\n```\n@docs first\n````\n\n @docs alsoCode" + - Declaration: + name: first + kind: Value + signature: "first : 'a -> 'a" + type_kind: ~ + doc: First value. +warnings: [] diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__public_declarations.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__public_declarations.snap new file mode 100644 index 00000000..68d51d25 --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__public_declarations.snap @@ -0,0 +1,52 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Example exposing (id, type box(..), type secret, type record, Keep, (%%))\n{-| Example API.\n\n@docs Keep, id\n\nTypes below.\n\n@docs box, record, secret, (%%)\n-}\n\ninfix left 5 (%%) = combine\n\n{-| Keep a value unchanged. -}\ntrait Keep 'a where\n keep : 'a -> 'a\n\n{-| Identity implementation. -}\nimpl Keep int where\n keep x = x\n\n{-| Preserve the argument. -}\nid x = x\n\n{-| One little payload. -}\ntype box 'a = Box 'a\n\n{-| A hidden constructor. -}\ntype secret = Secret int\n\n{-| A labeled record. -}\ntype alias record = { value : int }\n\ncombine x y = x\nprivate x = x\n" +--- +module: + name: Example + blocks: + - Text: Example API. + - Declaration: + name: Keep + kind: Trait + signature: "trait Keep 'a where\n keep : 'a -> 'a" + type_kind: ~ + doc: Keep a value unchanged. + - Declaration: + name: id + kind: Value + signature: "id : 'a -> 'a" + type_kind: ~ + doc: Preserve the argument. + - Text: Types below. + - Declaration: + name: box + kind: Union + signature: "type box 'a\n = Box 'a" + type_kind: Type -> Type + doc: One little payload. + - Declaration: + name: record + kind: Alias + signature: "type alias record = { value : int }" + type_kind: Type + doc: A labeled record. + - Declaration: + name: secret + kind: Union + signature: type secret + type_kind: Type + doc: A hidden constructor. + - Declaration: + name: (%%) + kind: Operator + signature: "(%%) : 'a -> 'b -> 'a\ninfix left 5 (%%) = combine" + type_kind: ~ + doc: "Infix form of `Example.combine`." + - Declaration: + name: impl Keep int + kind: Implementation + signature: impl Keep int + type_kind: ~ + doc: Identity implementation. +warnings: [] diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__same_trait_method_constraint.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__same_trait_method_constraint.snap new file mode 100644 index 00000000..1f7bc087 --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__same_trait_method_constraint.snap @@ -0,0 +1,18 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Example exposing (Keep)\n{-| A method may require the same trait at another type. -}\ntrait Keep 'a where\n other : Keep 'b => 'b -> 'a\n" +--- +module: + name: Example + blocks: + - Text: A method may require the same trait at another type. + - Declaration: + name: Keep + kind: Trait + signature: "trait Keep 'a where\n other : Keep 'b => 'b -> 'a" + type_kind: ~ + doc: "" +warnings: + - module: Example + name: Keep + message: Export has no documentation comment. diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__warnings_keep_output.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__warnings_keep_output.snap new file mode 100644 index 00000000..e0def918 --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__warnings_keep_output.snap @@ -0,0 +1,25 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Example exposing (id)\n{-| Still rendered.\n\n@docs id, missing, id\n\n```nash\n@docs notADirective\n```\n-}\nid x = x\n" +--- +module: + name: Example + blocks: + - Text: Still rendered. + - Declaration: + name: id + kind: Value + signature: "id : 'a -> 'a" + type_kind: ~ + doc: "" + - Text: "```nash\n@docs notADirective\n```" +warnings: + - module: Example + name: id + message: Export has no documentation comment. + - module: Example + name: missing + message: "@docs name is not exported by this module." + - module: Example + name: id + message: Repeated @docs entry. diff --git a/crates/nash-docs/src/tests.rs b/crates/nash-docs/src/tests.rs new file mode 100644 index 00000000..3468b0b2 --- /dev/null +++ b/crates/nash-docs/src/tests.rs @@ -0,0 +1,191 @@ +use super::*; +use nash_driver::{Database, InMemorySource, build_graph_production, build_with, bundled_base}; +use std::sync::Arc; +use tokio::sync::Mutex; +use url::Url; + +async fn documented(source: &str) -> Extraction { + let uri = Url::parse("file:///docs/src/Example.nash").unwrap(); + let memory = InMemorySource::with_files([(uri.clone(), source.into())]); + let mut origins = bundled_base::modules(); + origins.insert(uri.clone(), None); + let db = Arc::new(Mutex::new(Database::new(memory))); + let graph = build_graph_production(db.clone(), &origins.keys().cloned().collect::>()) + .await + .unwrap(); + let (result, docs) = build_with(db, &graph, &origins, move |solved| { + let module = solved.modules.iter().find(|m| m.uri == uri).unwrap(); + let source = nash_parse::Parser::new(solved.store, module.source) + .module() + .unwrap(); + extract( + &source, + &nash_can::from_module(solved.store, module.module, &module.annotations), + ) + }) + .await; + assert!(result.is_success(), "{:?}", result.ordered_reports()); + docs.unwrap() +} + +macro_rules! assert_docs_snapshot { + ($source:expr) => {{ + let source = indoc::indoc!($source); + let output = documented(source).await; + insta::with_settings!({description => source, omit_expression => true}, { insta::assert_yaml_snapshot!(output); }); + }}; +} + +#[tokio::test] +async fn public_declarations() { + assert_docs_snapshot!( + r#" + module Example exposing (id, type box(..), type secret, type record, Keep, (%%)) + {-| Example API. + + @docs Keep, id + + Types below. + + @docs box, record, secret, (%%) + -} + + infix left 5 (%%) = combine + + {-| Keep a value unchanged. -} + trait Keep 'a where + keep : 'a -> 'a + + {-| Identity implementation. -} + impl Keep int where + keep x = x + + {-| Preserve the argument. -} + id x = x + + {-| One little payload. -} + type box 'a = Box 'a + + {-| A hidden constructor. -} + type secret = Secret int + + {-| A labeled record. -} + type alias record = { value : int } + + combine x y = x + private x = x + "# + ); +} + +#[tokio::test] +async fn warnings_keep_output() { + assert_docs_snapshot!( + r#" + module Example exposing (id) + {-| Still rendered. + + @docs id, missing, id + + ```nash + @docs notADirective + ``` + -} + id x = x + "# + ); +} + +#[tokio::test] +async fn constrained_types() { + assert_docs_snapshot!( + r#" + module Example exposing (render) + {-| Render a value with its Show instance. -} + render : Show 'a => 'a -> string + render value = show value + "# + ); +} + +#[test] +fn compiler_owned_modules() { + insta::with_settings!({omit_expression => true}, { insta::assert_yaml_snapshot!(primitives()); }); +} + +#[tokio::test] +async fn base_documentation() { + let origins = bundled_base::modules(); + let db = Arc::new(Mutex::new(Database::new(InMemorySource::new()))); + let graph = build_graph_production(db.clone(), &origins.keys().cloned().collect::>()) + .await + .unwrap(); + let (result, warnings) = build_with(db, &graph, &origins, |solved| { + solved + .modules + .iter() + .flat_map(|module| { + let source = nash_parse::Parser::new(solved.store, module.source) + .module() + .unwrap(); + extract( + &source, + &nash_can::from_module(solved.store, module.module, &module.annotations), + ) + .warnings + }) + .collect::>() + }) + .await; + assert!(result.is_success(), "{:?}", result.ordered_reports()); + assert!( + warnings.as_ref().unwrap().is_empty(), + "{}", + warnings + .unwrap() + .iter() + .map(|w| format!("{}.{}: {}", w.module, w.name, w.message)) + .collect::>() + .join("\n") + ); +} + +#[tokio::test] +async fn same_trait_method_constraint() { + assert_docs_snapshot!( + r#" + module Example exposing (Keep) + {-| A method may require the same trait at another type. -} + trait Keep 'a where + other : Keep 'b => 'b -> 'a + "# + ); +} + +#[tokio::test] +async fn overview_markdown_boundaries() { + assert_docs_snapshot!( + r#" + module Example exposing (first, second) + {-| Order and prose. + + @docs + second, + ## Keep this heading + + ````nash + ``` + @docs first + ```` + + @docs alsoCode + + @docs first + -} + {-| First value. -} + first x = x + {-| Second value. -} + second x = x + "# + ); +} diff --git a/crates/nash-docs/src/types.rs b/crates/nash-docs/src/types.rs new file mode 100644 index 00000000..f626d09b --- /dev/null +++ b/crates/nash-docs/src/types.rs @@ -0,0 +1,72 @@ +use nash_ast::{Annotation, Kind, Pred, Type}; +use nash_report::{ + localizer::Localizer, + render_type::{Ctx, can_to_doc}, +}; + +pub fn typ(local: &Localizer, typ: &Type<'_>, context: Ctx) -> String { + can_to_doc(local, context, typ).render(100, false) +} + +pub fn context(local: &Localizer, predicates: &[Pred<'_>]) -> String { + let items: Vec<_> = predicates + .iter() + .filter_map(|pred| { + let Pred::Trait { trait_, args } = pred else { + return None; + }; + let mut text = local.to_doc(trait_.home, trait_.name).render(100, false); + for arg in *args { + text.push(' '); + text.push_str(&typ(local, &arg.value, Ctx::App)); + } + Some(text) + }) + .collect(); + match items.as_slice() { + [] => String::new(), + [one] => format!("{one} => "), + _ => format!("({}) => ", items.join(", ")), + } +} + +pub fn annotation(local: &Localizer, annotation: &Annotation<'_>) -> String { + format!( + "{}{}", + context(local, annotation.context), + typ(local, &annotation.typ.value, Ctx::None) + ) +} + +pub fn kind(kind: &Kind<'_>) -> String { + match kind { + Kind::Type => "Type".into(), + Kind::Arrow(from, to) => { + let left = self::kind(from); + let left = if matches!(from, Kind::Arrow(..)) { + format!("({left})") + } else { + left + }; + format!("{left} -> {}", self::kind(to)) + } + } +} + +pub fn constructor(local: &Localizer, ctor: &nash_ast::Ctor<'_>) -> String { + if let Some(labels) = ctor.labels { + let fields: Vec<_> = labels + .iter() + .zip(ctor.arguments) + .map(|(name, t)| format!("{name} : {}", typ(local, &t.value, Ctx::None))) + .collect(); + format!("{} {{ {} }}", ctor.name, fields.join(", ")) + } else { + let mut text = ctor.name.to_owned(); + for arg in ctor.arguments { + text.push(' '); + text.push_str(&typ(local, &arg.value, Ctx::App)); + } + text + } +} diff --git a/crates/nash-driver/base/src/Applicative.nash b/crates/nash-driver/base/src/Applicative.nash index 6c1b39ec..5ae28815 100644 --- a/crates/nash-driver/base/src/Applicative.nash +++ b/crates/nash-driver/base/src/Applicative.nash @@ -4,6 +4,7 @@ module Applicative exposing ( Applicative ) import Functor exposing ( Functor ) +{-| Lift a value with pure and apply a wrapped function to a wrapped value. -} trait Functor 'f => Applicative 'f where pure : 'a -> 'f 'a diff --git a/crates/nash-driver/base/src/Array.nash b/crates/nash-driver/base/src/Array.nash index abdd613a..553ffb3f 100644 --- a/crates/nash-driver/base/src/Array.nash +++ b/crates/nash-driver/base/src/Array.nash @@ -8,6 +8,7 @@ import Literal import Option exposing ( type option(..) ) +{-| Convert a Big or little list to an array, preserving element representations. -} fromList : Lift (list 'a) ('f 'a) => 'f 'a -> array 'a fromList values = values @@ -15,14 +16,17 @@ fromList values = |> Builtin.listToArray +{-| Return the number of array elements. -} length : array 'a -> int length = Builtin.lengthOfArray +{-| Read a zero-based index. An invalid index fails. -} at : Lift int 'n => array 'a -> 'n -> 'a at values index = Builtin.indexArray values (lower index) +{-| Read a zero-based index, returning None for an invalid index. -} get : Lift int 'n => array 'a -> 'n -> option 'a get values index = getIndex values (lower index) diff --git a/crates/nash-driver/base/src/Bool.nash b/crates/nash-driver/base/src/Bool.nash index 33c0e145..25787110 100644 --- a/crates/nash-driver/base/src/Bool.nash +++ b/crates/nash-driver/base/src/Bool.nash @@ -9,11 +9,13 @@ import Option exposing ( type option(..) ) import Eq +{-| A Data-represented Boolean. False uses constructor 0 and True uses constructor 1, both without fields. -} type Bool = False | True +{-| Negate a Big or little Boolean, returning bool. -} not : Lift bool 'a => 'a -> bool not value = if lower value then @@ -22,6 +24,7 @@ not value = Primitive.True +{-| Boolean conjunction. Fully applied calls short-circuit; partial and indirect calls are strict. -} and : ( Lift bool 'a, Lift bool 'b ) => 'a -> 'b -> bool and a b = if lower a then @@ -30,6 +33,7 @@ and a b = Primitive.False +{-| Boolean disjunction. Fully applied calls short-circuit; partial and indirect calls are strict. -} or : ( Lift bool 'a, Lift bool 'b ) => 'a -> 'b -> bool or a b = if lower a then @@ -38,6 +42,7 @@ or a b = lower b +{-| Return True when the Boolean operands differ. -} xor : ( Lift bool 'a, Lift bool 'b ) => 'a -> 'b -> bool xor a b = if lower a then diff --git a/crates/nash-driver/base/src/Bytes.nash b/crates/nash-driver/base/src/Bytes.nash index 4ee5a62b..dd6c6315 100644 --- a/crates/nash-driver/base/src/Bytes.nash +++ b/crates/nash-driver/base/src/Bytes.nash @@ -33,10 +33,12 @@ import Ord import Ordering exposing ( type ordering ) +{-| Concatenate two byte strings, returning little bytes. -} append : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> bytes append a b = Builtin.appendByteString (lower a) (lower b) +{-| Compare byte strings lexicographically. -} compare : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> ordering compare a b = compareBytes (lower a) (lower b) @@ -45,6 +47,7 @@ compareBytes : bytes -> bytes -> ordering compareBytes = Ord.compare +{-| Return the lexicographically smaller byte string. -} min : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> bytes min a b = minBytes (lower a) (lower b) @@ -53,6 +56,7 @@ minBytes : bytes -> bytes -> bytes minBytes = Ord.min +{-| Return the lexicographically larger byte string. -} max : ( Lift bytes 'a, Lift bytes 'b ) => 'a -> 'b -> bytes max a b = maxBytes (lower a) (lower b) @@ -61,6 +65,7 @@ maxBytes : bytes -> bytes -> bytes maxBytes = Ord.max +{-| Count bytes. -} length : Lift bytes 'b => 'b -> int length value = value @@ -68,19 +73,23 @@ length value = |> Builtin.lengthOfByteString +{-| Read a byte at a zero-based index. Out-of-range indices fail. -} at : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> int at value index = Builtin.indexByteString (lower value) (lower index) +{-| Take a byte slice using start and count, with the Plutus builtin's clamping behavior. -} slice : ( Lift int 's, Lift int 'n, Lift bytes 'b ) => 's -> 'n -> 'b -> bytes slice start size value = Builtin.sliceByteString (lower start) (lower size) (lower value) +{-| Take up to the supplied number of bytes from the front. -} take : ( Lift int 'n, Lift bytes 'b ) => 'n -> 'b -> bytes take count value = Builtin.sliceByteString 0 (lower count) (lower value) +{-| Drop the supplied number of bytes from the front. -} drop : ( Lift int 'n, Lift bytes 'b ) => 'n -> 'b -> bytes drop count value = let @@ -92,11 +101,13 @@ drop count value = bytes +{-| Concatenate a list of byte strings. -} concat : ( Lift (list 'b) ('f 'b), Lift bytes 'b ) => 'f 'b -> bytes concat values = List.foldr (\x acc -> Builtin.appendByteString (lower x) acc) #"" values +{-| Invert every bit. -} complement : Lift bytes 'b => 'b -> bytes complement value = value @@ -104,6 +115,7 @@ complement value = |> Builtin.complementByteString +{-| Count bits set to one. -} countSetBits : Lift bytes 'b => 'b -> int countSetBits value = value @@ -111,6 +123,7 @@ countSetBits value = |> Builtin.countSetBits +{-| Find the first set bit using Plutus bit indexing, or return -1 when all bits are zero. -} findFirstSetBit : Lift bytes 'b => 'b -> int findFirstSetBit value = value @@ -118,38 +131,46 @@ findFirstSetBit value = |> Builtin.findFirstSetBit +{-| Compute bitwise AND. The first argument selects the builtin padding behavior for unequal lengths. -} and : ( Lift bool 'p, Lift bytes 'a, Lift bytes 'b ) => 'p -> 'a -> 'b -> bytes and pad left right = Builtin.andByteString (lower pad) (lower left) (lower right) +{-| Compute bitwise OR. The first argument selects the builtin padding behavior for unequal lengths. -} or : ( Lift bool 'p, Lift bytes 'a, Lift bytes 'b ) => 'p -> 'a -> 'b -> bytes or pad left right = Builtin.orByteString (lower pad) (lower left) (lower right) +{-| Compute bitwise XOR. The first argument selects the builtin padding behavior for unequal lengths. -} xor : ( Lift bool 'p, Lift bytes 'a, Lift bytes 'b ) => 'p -> 'a -> 'b -> bytes xor pad left right = Builtin.xorByteString (lower pad) (lower left) (lower right) +{-| Read a bit using Plutus bit indexing. Invalid indices fail. -} readBit : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> bool readBit value count = Builtin.readBit (lower value) (lower count) +{-| Shift bits using the signed displacement accepted by the Plutus builtin. -} shift : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> bytes shift value count = Builtin.shiftByteString (lower value) (lower count) +{-| Rotate bits using the signed displacement accepted by the Plutus builtin. -} rotate : ( Lift bytes 'b, Lift int 'n ) => 'b -> 'n -> bytes rotate value count = Builtin.rotateByteString (lower value) (lower count) +{-| Set the listed bit positions to the supplied Boolean value. Invalid indices fail. -} writeBits : ( Lift bytes 'b, Lift (list 'n) ('f 'n), Lift int 'n, Lift bool 'v ) => 'b -> 'f 'n -> 'v -> bytes writeBits value indices bit = Builtin.writeBits (lower value) (List.map lower indices) (lower bit) +{-| Render lowercase hexadecimal without a prefix. -} toHex : Lift bytes 'b => 'b -> string toHex value = value diff --git a/crates/nash-driver/base/src/Cardano/Address.nash b/crates/nash-driver/base/src/Cardano/Address.nash index 6821a349..05e1a09e 100644 --- a/crates/nash-driver/base/src/Cardano/Address.nash +++ b/crates/nash-driver/base/src/Cardano/Address.nash @@ -9,16 +9,19 @@ import Literal import Option exposing ( Option, type option(..) ) +{-| A ledger payment or staking credential, distinguished as a key hash or script hash. -} type Credential = PubKeyCredential Bytes | ScriptCredential Bytes +{-| A staking hash or ledger staking pointer. -} type StakingCredential = StakingHash Credential | StakingPtr Int Int Int +{-| A ledger address containing a payment credential and optional staking credential. -} type Address = Address { paymentCredential : Credential diff --git a/crates/nash-driver/base/src/Cardano/Time.nash b/crates/nash-driver/base/src/Cardano/Time.nash index 7eca16ec..deb5a833 100644 --- a/crates/nash-driver/base/src/Cardano/Time.nash +++ b/crates/nash-driver/base/src/Cardano/Time.nash @@ -23,16 +23,19 @@ import Bool exposing ( Bool ) import Lift exposing ( Lift ) +{-| A finite ledger time or negative/positive infinity. -} type Extended = NegInf | Finite Int | PosInf +{-| A ledger interval endpoint and its inclusive/exclusive flag. -} type IntervalBound = IntervalBound { bound : Extended, inclusive : Bool } +{-| A Plutus ledger interval with lower and upper bounds. -} type Interval = Interval { lowerBound : IntervalBound, upperBound : IntervalBound } @@ -120,11 +123,13 @@ endpointLe left right = a <= b +{-| Test whether an interval contains no times. -} isEmpty : Interval -> bool isEmpty (Interval lo hi) = Bool.not (endpointLe (inclusiveEndpoint 1 lo) (inclusiveEndpoint (-1) hi)) +{-| Test whether the first interval contains the second. -} contains : Interval -> Interval -> bool contains outer inner = if isEmpty inner then @@ -140,12 +145,14 @@ contains outer inner = (inclusiveEndpoint (-1) outer.upperBound) +{-| Test whether a time lies within an interval, respecting endpoint inclusion. -} member : Lift int 'n => 'n -> Interval -> bool member point (Interval lo hi) = endpointLe (inclusiveEndpoint 1 lo) (At (lower point)) && endpointLe (At (lower point)) (inclusiveEndpoint (-1) hi) +{-| Construct an interval from the supplied finite endpoints. -} interval : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> Interval interval start end = Interval @@ -153,6 +160,7 @@ interval start end = (IntervalBound (Finite (lift (lower end))) Bool.True) +{-| Construct an interval with a finite lower bound and no upper limit. -} from : Lift int 'a => 'a -> Interval from start = Interval @@ -160,6 +168,7 @@ from start = (IntervalBound PosInf Bool.True) +{-| Construct an interval with no lower limit and a finite upper bound. -} to : Lift int 'a => 'a -> Interval to end = Interval @@ -167,11 +176,13 @@ to end = (IntervalBound (Finite (lift (lower end))) Bool.True) +{-| The interval containing all times. -} always : Interval always = Interval (IntervalBound NegInf Bool.True) (IntervalBound PosInf Bool.True) +{-| An empty interval. -} never : Interval never = Interval (IntervalBound PosInf Bool.True) (IntervalBound NegInf Bool.True) diff --git a/crates/nash-driver/base/src/Cardano/Tx.nash b/crates/nash-driver/base/src/Cardano/Tx.nash index b585e565..6ace28df 100644 --- a/crates/nash-driver/base/src/Cardano/Tx.nash +++ b/crates/nash-driver/base/src/Cardano/Tx.nash @@ -12,18 +12,21 @@ import Cardano.Time exposing ( Interval ) import Cardano.Value exposing ( Value ) +{-| A governance representative credential, always-abstain representative, or always-no-confidence representative. -} type DRep = DRep Credential | DRepAlwaysAbstain | DRepAlwaysNoConfidence +{-| The ledger delegation target for staking, voting, or both. -} type Delegatee = DelegStake Bytes | DelegVote DRep | DelegStakeVote Bytes DRep +{-| A Plutus V3 transaction certificate using the ledger constructor order. -} type Certificate = RegStaking Credential (Option Int) | UnRegStaking Credential (Option Int) @@ -38,38 +41,46 @@ type Certificate | ResignColdCommittee Credential +{-| A committee, representative, or stake-pool voter in ledger encoding. -} type Voter = CommitteeVoter Credential | DRepVoter Credential | StakePoolVoter Bytes +{-| A ledger governance vote: no, yes, or abstain. -} type Vote = VoteNo | VoteYes | Abstain +{-| A governance proposal identified by transaction ID and proposal index. -} type GovernanceActionId = GovernanceActionId { txId : Bytes, index : Int } +{-| A ledger rational encoded as a constructor containing numerator and denominator. Unlike Rational.rational, it is not normalized. -} type Rational = Rational { numerator : Int, denominator : Int } +{-| Ledger committee membership and quorum data. -} type Committee = Committee { members : Map Credential Int, quorum : Rational } +{-| Ledger constitution data, including its optional script hash. -} type Constitution = Constitution (Option Bytes) +{-| A ledger protocol major/minor version pair. -} type ProtocolVersion = ProtocolVersion { major : Int, minor : Int } +{-| A Plutus V3 governance action in ledger constructor order. -} type GovernanceAction = ParameterChange (Option GovernanceActionId) Data (Option Bytes) | HardForkInitiation (Option GovernanceActionId) ProtocolVersion @@ -84,6 +95,7 @@ type GovernanceAction | InfoAction +{-| A ledger governance proposal with its deposit, return credential, and action. -} type Proposal = Proposal { deposit : Int @@ -92,16 +104,19 @@ type Proposal } +{-| A transaction output identified by transaction ID and output index. -} type OutputReference = OutputReference { txId : Bytes, index : Int } +{-| An absent datum, datum hash, or inline datum in ledger encoding. -} type Datum = NoDatum | DatumHash Bytes | InlineDatum Data +{-| A ledger transaction output with address, value, datum, and optional reference script. -} type Output = Output { address : Address @@ -111,10 +126,12 @@ type Output } +{-| A transaction input containing its output reference and resolved output. -} type Input = Input { outputReference : OutputReference, output : Output } +{-| The ledger purpose used as a redeemer-map key. -} type ScriptPurpose = Minting Bytes | Spending OutputReference @@ -124,6 +141,7 @@ type ScriptPurpose | Proposing Int Proposal +{-| The current script's purpose-specific context; spending includes an optional datum. -} type ScriptInfo = MintingScript Bytes | SpendingScript OutputReference (Option Data) @@ -133,6 +151,7 @@ type ScriptInfo | ProposingScript Int Proposal +{-| Plutus V3 transaction information in ledger field order. -} type Tx = Tx { inputs : List Input @@ -154,6 +173,7 @@ type Tx } +{-| The Plutus V3 script argument: transaction, redeemer Data, and script information. Validation checks wire shape, not ledger validity. -} type ScriptContext = ScriptContext { tx : Tx, redeemer : Data, scriptInfo : ScriptInfo } diff --git a/crates/nash-driver/base/src/Cardano/Value.nash b/crates/nash-driver/base/src/Cardano/Value.nash index d01f2e08..2e3f9a8c 100644 --- a/crates/nash-driver/base/src/Cardano/Value.nash +++ b/crates/nash-driver/base/src/Cardano/Value.nash @@ -17,6 +17,7 @@ import Lift exposing ( Lift ) import Data exposing ( FromData, ToData, Validate, Decode ) +{-| The ledger Data encoding of a multi-asset value: policy IDs map to asset-name/quantity maps. -} type alias Value = Map Bytes (Map Bytes Int) @@ -44,6 +45,7 @@ impl Validate Value where |> fromData +{-| An empty native value. -} empty : value empty = Builtin.unValueData (toData emptyMap) @@ -52,35 +54,42 @@ emptyMap : Value emptyMap = Builtin.mapData [] +{-| Construct a native value for one policy, asset name, and signed quantity. -} singleton : ( Lift bytes 'p, Lift bytes 't, Lift int 'n ) => 'p -> 't -> 'n -> value singleton policy token amount = insert policy token amount empty +{-| Replace a coin quantity; zero removes that coin. Native value constraints and overflow failures apply. -} insert : ( Lift bytes 'p, Lift bytes 't, Lift int 'n, Lift value 'v ) => 'p -> 't -> 'n -> 'v -> value insert policy token amount value = Builtin.insertCoin (lower policy) (lower token) (lower amount) (lower value) +{-| Return the lovelace quantity in a native or ledger value. -} lovelace : Lift value 'v => 'v -> int lovelace value = quantityOf #"" #"" value +{-| Look up the quantity for a policy and asset name. -} quantityOf : ( Lift bytes 'p, Lift bytes 't, Lift value 'v ) => 'p -> 't -> 'v -> int quantityOf policy token value = Builtin.lookupCoin (lower policy) (lower token) (lower value) +{-| Add native values using the Plutus value builtin. Quantity overflow fails. -} add : ( Lift value 'a, Lift value 'b ) => 'a -> 'b -> value add left right = Builtin.unionValue (lower left) (lower right) +{-| Test value containment using the Plutus builtin, which rejects negative quantities. -} contains : ( Lift value 'a, Lift value 'b ) => 'a -> 'b -> bool contains left right = Builtin.valueContains (lower left) (lower right) +{-| Multiply every quantity by a signed integer. Native quantity overflow fails. -} scale : ( Lift int 'n, Lift value 'v ) => 'n -> 'v -> value scale amount value = Builtin.scaleValue (lower amount) (lower value) diff --git a/crates/nash-driver/base/src/Cons.nash b/crates/nash-driver/base/src/Cons.nash index 98ff343b..362aa032 100644 --- a/crates/nash-driver/base/src/Cons.nash +++ b/crates/nash-driver/base/src/Cons.nash @@ -23,15 +23,18 @@ import Show exposing ( Show ) import Functor +{-| A Term list that can contain functions and other non-Storable values. Nil is empty; Cons holds an element and the remaining list. -} type cons 'a = Nil | Cons 'a (cons 'a) +{-| Create a Term list with one element. -} singleton : 'a -> cons 'a singleton value = Cons value Nil +{-| Apply a function to each element of a Term list. -} map : ('a -> 'b) -> cons 'a -> cons 'b map = Functor.map @@ -46,6 +49,7 @@ impl Functor.Functor cons where Cons (f x) (Functor.map f xs) +{-| Map elements with zero-based indices. -} indexedMap : (int -> 'a -> 'b) -> cons 'a -> cons 'b indexedMap f values = indexedFrom 0 f values @@ -60,6 +64,7 @@ indexedFrom index f values = Cons (f index x) (indexedFrom (Builtin.addInteger index 1) f xs) +{-| Combine corresponding elements, stopping at the shorter input. -} map2 : ('a -> 'b -> 'c) -> cons 'a -> cons 'b -> cons 'c map2 f left right = case ( left, right ) of @@ -70,6 +75,7 @@ map2 f left right = Nil +{-| Fold from the right, passing each element before the accumulator. -} foldr : ('a -> 'b -> 'b) -> 'b -> cons 'a -> 'b foldr f acc values = case values of @@ -80,6 +86,7 @@ foldr f acc values = f x (foldr f acc xs) +{-| Fold left to right, passing each element before the accumulator. -} foldl : ('a -> 'b -> 'b) -> 'b -> cons 'a -> 'b foldl f acc values = case values of @@ -90,6 +97,7 @@ foldl f acc values = foldl f (f x acc) xs +{-| Append the second Term list to the first. -} append : cons 'a -> cons 'a -> cons 'a append left right = case left of @@ -100,10 +108,12 @@ append left right = Cons x (append xs right) +{-| Count the elements of a Term list. -} length : cons 'a -> int length values = foldl (\_ count -> Builtin.addInteger count 1) 0 values +{-| Convert a Big or little native list to a Term list, preserving its elements. -} fromList : Lift (list 'a) ('f 'a) => 'f 'a -> cons 'a fromList values = values @@ -121,6 +131,7 @@ fromLittleList values = Cons x (fromLittleList xs) +{-| Convert a Term list to a native list. Its elements must be Storable. -} toList : cons 'a -> list 'a toList values = case values of diff --git a/crates/nash-driver/base/src/Crypto.nash b/crates/nash-driver/base/src/Crypto.nash index 22ec8279..f591934c 100644 --- a/crates/nash-driver/base/src/Crypto.nash +++ b/crates/nash-driver/base/src/Crypto.nash @@ -16,6 +16,7 @@ import Builtin import Function exposing ( (|>) ) +{-| Compute a SHA-256 digest, returning bytes. -} sha2_256 : Lift bytes 'a => 'a -> bytes sha2_256 value = value @@ -23,6 +24,7 @@ sha2_256 value = |> Builtin.sha2_256 +{-| Compute a SHA3-256 digest, returning bytes. This differs from Keccak-256. -} sha3_256 : Lift bytes 'a => 'a -> bytes sha3_256 value = value @@ -30,6 +32,7 @@ sha3_256 value = |> Builtin.sha3_256 +{-| Compute a 224-bit BLAKE2b digest, returning bytes. -} blake2b_224 : Lift bytes 'a => 'a -> bytes blake2b_224 value = value @@ -37,6 +40,7 @@ blake2b_224 value = |> Builtin.blake2b_224 +{-| Compute a 256-bit BLAKE2b digest, returning bytes. -} blake2b_256 : Lift bytes 'a => 'a -> bytes blake2b_256 value = value @@ -44,6 +48,7 @@ blake2b_256 value = |> Builtin.blake2b_256 +{-| Compute a Keccak-256 digest, returning bytes. This differs from SHA3-256. -} keccak_256 : Lift bytes 'a => 'a -> bytes keccak_256 value = value @@ -51,6 +56,7 @@ keccak_256 value = |> Builtin.keccak_256 +{-| Compute a RIPEMD-160 digest, returning bytes. -} ripemd_160 : Lift bytes 'a => 'a -> bytes ripemd_160 value = value @@ -58,12 +64,14 @@ ripemd_160 value = |> Builtin.ripemd_160 +{-| Verify an Ed25519 public key, message, and signature. Malformed required lengths fail; a well-formed invalid signature returns False. -} verifyEd25519Signature : ( Lift bytes 'k, Lift bytes 'm, Lift bytes 's ) => 'k -> 'm -> 's -> bool verifyEd25519Signature key message signature = Builtin.verifyEd25519Signature (lower key) (lower message) (lower signature) +{-| Verify a secp256k1 ECDSA key, precomputed digest, and compact signature. This does not hash the message; malformed inputs may fail. -} verifyEcdsaSecp256k1Signature : ( Lift bytes 'k, Lift bytes 'm, Lift bytes 's ) => 'k -> 'm -> 's -> bool verifyEcdsaSecp256k1Signature key message signature = @@ -73,6 +81,7 @@ verifyEcdsaSecp256k1Signature key message signature = (lower signature) +{-| Verify a secp256k1 Schnorr x-only key, message, and signature. Malformed inputs may fail. -} verifySchnorrSecp256k1Signature : ( Lift bytes 'k , Lift bytes 'm , Lift bytes 's diff --git a/crates/nash-driver/base/src/Data.nash b/crates/nash-driver/base/src/Data.nash index a9217de2..2ddcfb3e 100644 --- a/crates/nash-driver/base/src/Data.nash +++ b/crates/nash-driver/base/src/Data.nash @@ -18,6 +18,7 @@ import Monad exposing ( Monad ) import Primitive exposing ( Big, Data(..), type bool(..) ) +{-| Encode a value as Data. Big types use a blanket representation-preserving implementation; little types provide explicit encodings. -} trait ToData 'a where toData : 'a -> Data @@ -26,6 +27,7 @@ impl ToData ('a : Big) where toData = Primitive.coerce +{-| Convert Data without recursive validation guarantees. Big types use an unchecked blanket cast; little implementations may unwrap and fail. -} trait FromData 'a where fromData : Data -> 'a @@ -34,6 +36,7 @@ impl FromData ('a : Big) where fromData = Primitive.coerce +{-| Validate Data recursively and return a Big value, failing on mismatch. Implementations provide their own checks. -} trait Validate ('a : Big) where validate : Data -> 'a @@ -64,10 +67,12 @@ impl Validate Bytes where candidate +{-| Serialize Data to CBOR bytes. -} serialise : Data -> bytes serialise = Builtin.serialiseData +{-| Extract the tag from constructor Data. Other Data variants fail. -} tag : Data -> int tag value = let @@ -76,6 +81,7 @@ tag value = index +{-| Extract the field list from constructor Data. Other Data variants fail. -} fields : Data -> list Data fields value = let @@ -136,6 +142,7 @@ impl Validate 'a => Validate (Option 'a) where fail +{-| Check Data and return Some of the decoded value or None on mismatch. This trait is independent of Validate; it does not catch VM failures. -} trait Decode 'a where decode : Data -> option 'a diff --git a/crates/nash-driver/base/src/Eq.nash b/crates/nash-driver/base/src/Eq.nash index 431f12a8..71c9928f 100644 --- a/crates/nash-driver/base/src/Eq.nash +++ b/crates/nash-driver/base/src/Eq.nash @@ -6,6 +6,7 @@ import Primitive exposing (..) import Builtin exposing (..) +{-| Compare values for equality and inequality. -} trait Eq 'a where eq : 'a -> 'a -> bool diff --git a/crates/nash-driver/base/src/Function.nash b/crates/nash-driver/base/src/Function.nash index 21d0b5c2..76c12b28 100644 --- a/crates/nash-driver/base/src/Function.nash +++ b/crates/nash-driver/base/src/Function.nash @@ -7,9 +7,11 @@ infix left 0 (|>) = applyForward infix right 0 (<|) = applyBackward +{-| Apply the second argument as a function to the first; used by the forward pipe. -} applyForward : 'a -> ('a -> 'b) -> 'b applyForward x f = f x +{-| Apply the first argument as a function to the second; used by the backward pipe. -} applyBackward : ('a -> 'b) -> 'a -> 'b applyBackward f x = f x diff --git a/crates/nash-driver/base/src/Functor.nash b/crates/nash-driver/base/src/Functor.nash index 0e58c0a9..de90a2ac 100644 --- a/crates/nash-driver/base/src/Functor.nash +++ b/crates/nash-driver/base/src/Functor.nash @@ -4,6 +4,7 @@ module Functor exposing ( Functor ) import Builtin +{-| Map a function over the contents of a little container. -} trait Functor 'f where map : ('a -> 'b) -> 'f 'a -> 'f 'b diff --git a/crates/nash-driver/base/src/Int.nash b/crates/nash-driver/base/src/Int.nash index 0b9411d7..7e55d7d8 100644 --- a/crates/nash-driver/base/src/Int.nash +++ b/crates/nash-driver/base/src/Int.nash @@ -35,34 +35,42 @@ import Ord import Ordering exposing ( type ordering ) +{-| Add two integers, returning a little int. -} add : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int add a b = Builtin.addInteger (lower a) (lower b) +{-| Subtract the second integer from the first. -} sub : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int sub a b = Builtin.subtractInteger (lower a) (lower b) +{-| Multiply two integers. -} mul : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int mul a b = Builtin.multiplyInteger (lower a) (lower b) +{-| Divide with rounding toward negative infinity. A zero divisor fails. -} div : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int div a b = Builtin.divideInteger (lower a) (lower b) +{-| Return the remainder corresponding to floor division. A zero divisor fails. -} mod : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int mod a b = Builtin.modInteger (lower a) (lower b) +{-| Divide with rounding toward zero. A zero divisor fails. -} quot : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int quot a b = Builtin.quotientInteger (lower a) (lower b) +{-| Return the remainder corresponding to division toward zero. A zero divisor fails. -} rem : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int rem a b = Builtin.remainderInteger (lower a) (lower b) +{-| Negate an integer, returning a little int. -} negate : Lift int 'a => 'a -> int negate value = value @@ -70,6 +78,7 @@ negate value = |> Builtin.subtractInteger 0 +{-| Compare two integers, returning LT, EQ, or GT. -} compare : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> ordering compare a b = compareInt (lower a) (lower b) @@ -78,6 +87,7 @@ compareInt : int -> int -> ordering compareInt = Ord.compare +{-| Return the smaller integer as a little int. -} min : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int min a b = minInt (lower a) (lower b) @@ -86,6 +96,7 @@ minInt : int -> int -> int minInt = Ord.min +{-| Return the larger integer as a little int. -} max : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int max a b = maxInt (lower a) (lower b) @@ -94,6 +105,7 @@ maxInt : int -> int -> int maxInt = Ord.max +{-| Return the nonnegative magnitude. -} abs : Lift int 'n => 'n -> int abs value = let @@ -106,6 +118,7 @@ abs value = n +{-| Raise a base to a nonnegative integer exponent. Negative exponents fail; exponent zero returns one. -} pow : ( Lift int 'n, Lift int 'e ) => 'n -> 'e -> int pow value exponent = let @@ -139,6 +152,7 @@ power n e = n * half * half +{-| Compute an exact power of two. Negative exponents fail; exponent zero returns one. -} pow2 : Lift int 'e => 'e -> int pow2 exponent = let @@ -169,20 +183,24 @@ powerOfTwo e = power 256 bands * Builtin.indexArray smallPowersOfTwo bits +{-| Use Plutus modular exponentiation. Invalid moduli and unavailable modular inverses fail. -} powMod : ( Lift int 'n, Lift int 'e, Lift int 'm ) => 'n -> 'e -> 'm -> int powMod n e m = Builtin.expModInteger (lower n) (lower e) (lower m) +{-| Encode a nonnegative integer with an endianness flag and byte size. True selects big-endian; size zero uses the minimum width. Invalid sizes or values that do not fit fail. -} toBytes : ( Lift bool 'b, Lift int 's, Lift int 'n ) => 'b -> 's -> 'n -> bytes toBytes bigEndian size n = Builtin.integerToByteString (lower bigEndian) (lower size) (lower n) +{-| Read an unsigned integer from bytes. True selects big-endian; False selects little-endian. -} fromBytes : ( Lift bool 'b, Lift bytes 'v ) => 'b -> 'v -> int fromBytes bigEndian value = Builtin.byteStringToInteger (lower bigEndian) (lower value) +{-| Render an integer in decimal notation. -} toString : Lift int 'n => 'n -> string toString value = value @@ -194,6 +212,7 @@ showInt : int -> string showInt = Show.show +{-| Compute the nonnegative greatest common divisor. gcd 0 0 is zero. -} gcd : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int gcd left right = gcdInt (abs left) (abs right) @@ -206,6 +225,7 @@ gcdInt a b = gcdInt b (Builtin.modInteger a b) +{-| Compute the nonnegative least common multiple. If either input is zero, the result is zero. -} lcm : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> int lcm left right = let @@ -221,6 +241,7 @@ lcm left right = abs (Builtin.quotientInteger a (gcd a b) * b) +{-| Compute the floor square root. Negative inputs fail. -} isqrt : Lift int 'n => 'n -> int isqrt value = let @@ -257,6 +278,7 @@ sqrtNewton n current = sqrtNewton n next +{-| Test whether the final argument is the greatest common divisor. Negative candidates return False. -} isGcd : ( Lift int 'a, Lift int 'b, Lift int 'g ) => 'a -> 'b -> 'g -> bool isGcd left right expected = let @@ -282,6 +304,7 @@ isGcd left right expected = 1 +{-| Test whether the final argument is the least common multiple. Negative candidates return False. -} isLcm : ( Lift int 'a, Lift int 'b, Lift int 'm ) => 'a -> 'b -> 'm -> bool isLcm left right expected = let @@ -307,6 +330,7 @@ isLcm left right expected = 1 +{-| Test whether the final argument is the floor square root, using adjacent squares. Negative inputs or candidates return False. -} isSqrt : ( Lift int 'n, Lift int 'r ) => 'n -> 'r -> bool isSqrt value expected = let diff --git a/crates/nash-driver/base/src/Integral.nash b/crates/nash-driver/base/src/Integral.nash index 846fc0db..7765e239 100644 --- a/crates/nash-driver/base/src/Integral.nash +++ b/crates/nash-driver/base/src/Integral.nash @@ -5,6 +5,7 @@ import Builtin import Num exposing ( Num ) +{-| Integer division and remainder operations on little integer types. -} trait Num 'a => Integral 'a where div : 'a -> 'a -> 'a diff --git a/crates/nash-driver/base/src/Lift.nash b/crates/nash-driver/base/src/Lift.nash index 5b6ad966..71159943 100644 --- a/crates/nash-driver/base/src/Lift.nash +++ b/crates/nash-driver/base/src/Lift.nash @@ -5,6 +5,7 @@ import Builtin import Primitive exposing ( Big ) +{-| Convert only the outer representation between little and Big forms. Element representations are preserved. -} trait Lift 'small 'big where lift : 'small -> 'big diff --git a/crates/nash-driver/base/src/List.nash b/crates/nash-driver/base/src/List.nash index 0c2b28a6..1a2b552c 100644 --- a/crates/nash-driver/base/src/List.nash +++ b/crates/nash-driver/base/src/List.nash @@ -44,10 +44,12 @@ import Primitive exposing ( type bool(..) ) import Ordering exposing ( type ordering(..) ) +{-| Create a list containing one element. -} singleton : 'a -> list 'a singleton x = [ x ] +{-| Repeat a value count times. A nonpositive count returns an empty list. -} repeat : Lift int 'n => 'n -> 'a -> list 'a repeat count x = repeatList (lower count) x @@ -60,6 +62,7 @@ repeatList n x = x :: repeatList (n - 1) x +{-| Generate integers from the inclusive start to the exclusive end. Reversed or equal bounds return an empty list. -} range : ( Lift int 's, Lift int 'e ) => 's -> 'e -> list int range start end = rangeList (lower start) (lower end) @@ -72,6 +75,7 @@ rangeList start end = start :: rangeList (start + 1) end +{-| Return the first element, or None for an empty list. -} head : Lift (list 'a) ('f 'a) => 'f 'a -> option 'a head values = case lowerOuter values of @@ -82,6 +86,7 @@ head values = Some x +{-| Return the remaining elements, or None for an empty list. -} tail : Lift (list 'a) ('f 'a) => 'f 'a -> option (list 'a) tail values = case lowerOuter values of @@ -92,6 +97,7 @@ tail values = Some xs +{-| Test whether the list has no elements. -} isEmpty : Lift (list 'a) ('f 'a) => 'f 'a -> bool isEmpty values = values @@ -99,6 +105,7 @@ isEmpty values = |> Builtin.nullList +{-| Count the elements. -} length : Lift (list 'a) ('f 'a) => 'f 'a -> int length values = values @@ -106,6 +113,7 @@ length values = |> foldlList (\_ n -> n + 1) 0 +{-| Test for exactly the supplied count without first computing the list's length. Negative counts return False. -} isLength : ( Lift (list 'a) ('f 'a), Lift int 'n ) => 'f 'a -> 'n -> bool isLength values mustLength = let @@ -130,6 +138,7 @@ isLength values mustLength = False +{-| Reverse element order without converting the elements. -} reverse : Lift (list 'a) ('f 'a) => 'f 'a -> list 'a reverse values = values @@ -137,6 +146,7 @@ reverse values = |> foldlList (\x xs -> x :: xs) [] +{-| Append the second list to the first, preserving element representations. -} append : ( Lift (list 'a) ('f 'a), Lift (list 'a) ('g 'a) ) => 'f 'a -> 'g 'a -> list 'a append xs ys = appendList (lowerOuter xs) (lowerOuter ys) @@ -152,6 +162,7 @@ appendList xs ys = x :: appendList rest ys +{-| Flatten one level of lists, preserving element order. -} concat : ( Lift (list ('f 'a)) ('g ('f 'a)), Lift (list 'a) ('f 'a) ) => 'g ('f 'a) -> list 'a concat values = @@ -160,10 +171,12 @@ concat values = |> foldrList (\xs acc -> appendList (lowerOuter xs) acc) [] +{-| Apply a function to each element. -} map : Lift (list 'a) ('f 'a) => ('a -> 'b) -> 'f 'a -> list 'b map f values = Functor.map f (lowerOuter values) +{-| Map elements with their zero-based indices. -} indexedMap : Lift (list 'a) ('f 'a) => (int -> 'a -> 'b) -> 'f 'a -> list 'b indexedMap f values = indexedFrom 0 f (lowerOuter values) @@ -178,6 +191,7 @@ indexedFrom index f xs = f index x :: indexedFrom (index + 1) f rest +{-| Keep elements whose predicate is true. -} filter : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> list 'a filter p values = @@ -193,6 +207,7 @@ filter p values = (lowerOuter values) +{-| Map each element and keep the payloads of Some results. -} filterMap : ( Lift (list 'a) ('f 'a), Lift (option 'b) ('g 'b) ) => ('a -> 'g 'b) -> 'f 'a -> list 'b filterMap f values = filterMapList f (lowerOuter values) @@ -213,6 +228,7 @@ filterMapList f xs = filterMapList f rest +{-| Fold left to right, passing each element before the accumulator. -} foldl : Lift (list 'a) ('f 'a) => ('a -> 'b -> 'b) -> 'b -> 'f 'a -> 'b foldl f acc values = foldlList f acc (lowerOuter values) @@ -227,6 +243,7 @@ foldlList f acc xs = foldlList f (f x acc) rest +{-| Fold from the right, passing each element before the accumulator. -} foldr : Lift (list 'a) ('f 'a) => ('a -> 'b -> 'b) -> 'b -> 'f 'a -> 'b foldr f acc values = foldrList f acc (lowerOuter values) @@ -241,6 +258,7 @@ foldrList f acc xs = f x (foldrList f acc rest) +{-| Return True at the first matching element. An empty list returns False. -} any : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> bool any p values = anyList (\x -> lower (p x)) (lowerOuter values) @@ -258,6 +276,7 @@ anyList p xs = anyList p rest +{-| Return False at the first nonmatching element. An empty list returns True. -} all : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> bool all p values = allList (\x -> lower (p x)) (lowerOuter values) @@ -275,6 +294,7 @@ allList p xs = False +{-| Return the first matching element, or None. -} find : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> option 'a find p values = findList (\x -> lower (p x)) (lowerOuter values) @@ -293,10 +313,12 @@ findList p xs = findList p rest +{-| Test whether an equal element occurs in the list. -} member : ( Eq 'a, Lift (list 'a) ('f 'a) ) => 'a -> 'f 'a -> bool member x values = anyList (\y -> x == y) (lowerOuter values) +{-| Keep at most count elements from the front. Nonpositive counts return an empty list. -} take : ( Lift int 'n, Lift (list 'a) ('f 'a) ) => 'n -> 'f 'a -> list 'a take count values = takeList (lower count) (lowerOuter values) @@ -314,6 +336,7 @@ takeList n xs = x :: takeList (n - 1) rest +{-| Skip count elements. Nonpositive counts leave the list unchanged; counts past the end return an empty list. -} drop : ( Lift int 'n, Lift (list 'a) ('f 'a) ) => 'n -> 'f 'a -> list 'a drop count values = let @@ -326,6 +349,7 @@ drop count values = Builtin.dropList n (lowerOuter values) +{-| Return the element at a zero-based index, or None for an invalid index. -} at : ( Lift int 'n, Lift (list 'a) ('f 'a) ) => 'n -> 'f 'a -> option 'a at index values = let @@ -338,6 +362,7 @@ at index values = head (Builtin.dropList n (lowerOuter values)) +{-| Combine corresponding elements, stopping at the shorter list. -} map2 : ( Lift (list 'a) ('f 'a), Lift (list 'b) ('g 'b) ) => ('a -> 'b -> 'c) -> 'f 'a -> 'g 'b -> list 'c map2 f xs ys = map2List f (lowerOuter xs) (lowerOuter ys) @@ -353,6 +378,7 @@ map2List f xs ys = [] +{-| Sort in ascending order using Ord. Equal elements retain their original order. -} sort : ( Ord 'a, Lift (list 'a) ('f 'a) ) => 'f 'a -> list 'a sort values = values @@ -360,6 +386,7 @@ sort values = |> sortList compare +{-| Sort using a comparator that returns ordering or Ordering. Equal elements retain their original order. -} sortBy : ( Lift (list 'a) ('f 'a), Lift ordering 'o ) => ('a -> 'a -> 'o) -> 'f 'a -> list 'a sortBy cmp values = sortList (\a b -> lower (cmp a b)) (lowerOuter values) @@ -384,6 +411,7 @@ insert cmp x xs = x :: xs +{-| Add integer elements, lowering each to int. An empty list sums to zero. -} sum : ( Lift int 'a, Lift (list 'a) ('f 'a) ) => 'f 'a -> int sum values = values @@ -391,6 +419,7 @@ sum values = |> foldlList (\x acc -> Builtin.addInteger (lower x) acc) 0 +{-| Return matching elements first and nonmatching elements second, preserving order within both lists. -} partition : ( Lift (list 'a) ('f 'a), Lift bool 'p ) => ('a -> 'p) -> 'f 'a -> ( list 'a, list 'a ) partition p values = partitionList (\x -> lower (p x)) (lowerOuter values) @@ -412,6 +441,7 @@ partitionList p xs = ( yes, x :: no ) +{-| Convert the outer list to an array without converting its elements. -} toArray : Lift (list 'a) ('f 'a) => 'f 'a -> array 'a toArray values = values diff --git a/crates/nash-driver/base/src/Literal.nash b/crates/nash-driver/base/src/Literal.nash index 57661bed..bc87005d 100644 --- a/crates/nash-driver/base/src/Literal.nash +++ b/crates/nash-driver/base/src/Literal.nash @@ -5,14 +5,17 @@ import Primitive exposing (..) import Builtin exposing (..) +{-| Interpret an integer literal as the requested type. -} trait FromInt 'a where fromInt : int -> 'a +{-| Interpret a string literal as the requested type. -} trait FromString 'a where fromString : string -> 'a +{-| Interpret a bytes literal as the requested type. -} trait FromBytes 'a where fromBytes : bytes -> 'a @@ -41,10 +44,12 @@ impl FromString bytes where fromString = Builtin.encodeUtf8 +{-| Interpret a Boolean literal as the requested type. -} trait FromBool 'a where fromBool : bool -> 'a +{-| Interpret a unit literal as the requested type. -} trait FromUnit 'a where fromUnit : unit -> 'a diff --git a/crates/nash-driver/base/src/Map.nash b/crates/nash-driver/base/src/Map.nash index 066cc51d..3eb5c877 100644 --- a/crates/nash-driver/base/src/Map.nash +++ b/crates/nash-driver/base/src/Map.nash @@ -23,6 +23,7 @@ import Pair import Functor +{-| Remove left entries whose keys occur on the right, then append the right entries. Other ordering and duplicates are preserved. -} union : ( Eq 'k, Lift (list (pair 'k 'v)) 'a, Lift (list (pair 'k 'v)) 'b ) => 'a -> 'b -> list (pair 'k 'v) union left right = unionList (lower left) (lower right) @@ -62,20 +63,24 @@ containsKey key entries = containsKey key rest +{-| An empty native map list. -} empty : list (pair 'k 'v) empty = [] +{-| Construct one entry from Big key and value components. -} singleton : ('k : Big) -> ('v : Big) -> list (pair 'k 'v) singleton key value = [ Builtin.mkPairData key value ] +{-| Remove all entries with the key, then append the new Big key/value pair. -} insert : ( Eq 'k, Lift (list (pair 'k 'v)) 'm ) => ('k : Big) -> ('v : Big) -> 'm -> list (pair 'k 'v) insert key value entries = List.append (remove key entries) (singleton key value) +{-| Return the value of the first matching key, or None. -} get : ( Eq 'k, Lift (list (pair 'k 'v)) 'm ) => 'k -> 'm -> option 'v get key entries = getList key (lower entries) @@ -93,6 +98,7 @@ getList key entries = getList key rest +{-| Remove all entries with a matching key. -} remove : ( Eq 'k, Lift (list (pair 'k 'v)) 'm ) => 'k -> 'm -> list (pair 'k 'v) remove key entries = List.filter @@ -106,6 +112,7 @@ remove key entries = (toList entries) +{-| Return keys in entry order, preserving their representations. -} keys : Lift (list (pair 'k 'v)) 'input => 'input -> list 'k keys entries = entries @@ -113,6 +120,7 @@ keys entries = |> Functor.map Pair.fst +{-| Return values in entry order, preserving their representations. -} values : Lift (list (pair 'k 'v)) 'input => 'input -> list 'v values entries = entries @@ -120,10 +128,12 @@ values entries = |> Functor.map Pair.snd +{-| Lower the outer map to its native pair list without converting keys or values. -} toList : Lift (list (pair 'k 'v)) 'm => 'm -> list (pair 'k 'v) toList = lower +{-| Fold entries left to right, passing key, value, then accumulator. -} foldl : Lift (list (pair 'k 'v)) 'm => ('k -> 'v -> 'b -> 'b) -> 'b -> 'm -> 'b foldl f initial entries = List.foldl diff --git a/crates/nash-driver/base/src/Monad.nash b/crates/nash-driver/base/src/Monad.nash index ea414a24..0e84ae34 100644 --- a/crates/nash-driver/base/src/Monad.nash +++ b/crates/nash-driver/base/src/Monad.nash @@ -4,5 +4,6 @@ module Monad exposing ( Monad ) import Applicative exposing ( Applicative ) +{-| Sequence dependent computations with bind. Nash do notation uses this trait. -} trait Applicative 'm => Monad 'm where bind : 'm 'a -> ('a -> 'm 'b) -> 'm 'b diff --git a/crates/nash-driver/base/src/Monoid.nash b/crates/nash-driver/base/src/Monoid.nash index 22f58fad..b9058415 100644 --- a/crates/nash-driver/base/src/Monoid.nash +++ b/crates/nash-driver/base/src/Monoid.nash @@ -5,6 +5,7 @@ import Literal import Semigroup exposing ( Semigroup ) +{-| Combine values associatively with an identity element. -} trait Semigroup 'a => Monoid 'a where empty : 'a diff --git a/crates/nash-driver/base/src/Num.nash b/crates/nash-driver/base/src/Num.nash index f57c34ae..a0e552a2 100644 --- a/crates/nash-driver/base/src/Num.nash +++ b/crates/nash-driver/base/src/Num.nash @@ -5,6 +5,7 @@ import Builtin import Literal +{-| Arithmetic operations on little numeric types. -} trait Num 'a where add : 'a -> 'a -> 'a diff --git a/crates/nash-driver/base/src/Option.nash b/crates/nash-driver/base/src/Option.nash index 030fb21a..1baa3eb3 100644 --- a/crates/nash-driver/base/src/Option.nash +++ b/crates/nash-driver/base/src/Option.nash @@ -19,11 +19,13 @@ import Applicative exposing ( Applicative ) import Monad exposing ( Monad ) +{-| A little optional value: None or Some payload. -} type option 'a = Some 'a | None +{-| A Data-represented optional value with a Big payload. -} type Option 'a = Some 'a | None @@ -61,6 +63,7 @@ impl Monad option where None +{-| Return a present payload or the supplied default. -} withDefault : Lift (option 'a) ('f 'a) => 'a -> 'f 'a -> 'a withDefault fallback value = case lowerOuter value of @@ -102,14 +105,17 @@ impl Lift (option ('a : Big)) (Option 'a) where None +{-| Map a present payload, returning a little option. -} map : Lift (option 'a) ('f 'a) => ('a -> 'b) -> 'f 'a -> option 'b map f value = Functor.map f (lowerOuter value) +{-| Apply an optional function to an optional value. -} apply : Lift (option 'a) ('f 'a) => option ('a -> 'b) -> 'f 'a -> option 'b apply functions value = map2 (\f x -> f x) functions value +{-| Combine two present payloads; return None if either input is absent. -} map2 : ( Lift (option 'a) ('f 'a), Lift (option 'b) ('g 'b) ) => ('a -> 'b -> 'c) -> 'f 'a -> 'g 'b -> option 'c map2 f left right = @@ -121,6 +127,7 @@ map2 f left right = None +{-| Pass a present payload to a function returning an option. -} andThen : ( Lift (option 'a) ('f 'a), Lift (option 'b) ('g 'b) ) => ('a -> 'g 'b) -> 'f 'a -> option 'b andThen f value = @@ -132,6 +139,7 @@ andThen f value = None +{-| Test whether a payload is present. -} isSome : Lift (option 'a) ('f 'a) => 'f 'a -> bool isSome value = case lowerOuter value of @@ -142,6 +150,7 @@ isSome value = False +{-| Return the payload or fail for None. -} unwrap : Lift (option 'a) ('f 'a) => 'f 'a -> 'a unwrap value = case lowerOuter value of diff --git a/crates/nash-driver/base/src/Ord.nash b/crates/nash-driver/base/src/Ord.nash index c8ad5ce1..244b03ae 100644 --- a/crates/nash-driver/base/src/Ord.nash +++ b/crates/nash-driver/base/src/Ord.nash @@ -7,6 +7,7 @@ import Eq exposing ( Eq ) import Ordering exposing ( type ordering(..) ) +{-| Compare values with LT, EQ, or GT and derive relational comparisons. -} trait Eq 'a => Ord 'a where compare : 'a -> 'a -> ordering diff --git a/crates/nash-driver/base/src/Ordering.nash b/crates/nash-driver/base/src/Ordering.nash index 11b7dd82..6342805c 100644 --- a/crates/nash-driver/base/src/Ordering.nash +++ b/crates/nash-driver/base/src/Ordering.nash @@ -6,18 +6,21 @@ import Eq exposing ( Eq ) import Lift exposing ( Lift ) +{-| A little comparison result: LT, EQ, or GT. -} type ordering = LT | EQ | GT +{-| A Data-represented comparison result: LT, EQ, or GT. -} type Ordering = LT | EQ | GT +{-| Exchange LT and GT, leaving EQ unchanged; return little ordering. -} invert : Lift ordering 'a => 'a -> ordering invert order = case lower order of @@ -31,6 +34,7 @@ invert order = LT +{-| Use the second ordering when the first is EQ; otherwise keep the first. -} then_ : ( Lift ordering 'a, Lift ordering 'b ) => 'a -> 'b -> ordering then_ first second = case lower first of diff --git a/crates/nash-driver/base/src/Pair.nash b/crates/nash-driver/base/src/Pair.nash index 12c3422c..aa7c1767 100644 --- a/crates/nash-driver/base/src/Pair.nash +++ b/crates/nash-driver/base/src/Pair.nash @@ -4,13 +4,16 @@ module Pair exposing ( fst, snd, make ) import Builtin +{-| Extract the first component of a builtin pair. -} fst : pair 'a 'b -> 'a fst pair(first, _) = first +{-| Extract the second component of a builtin pair. -} snd : pair 'a 'b -> 'b snd pair(_, second) = second +{-| Construct a builtin pair from two Data values. -} make : Data -> Data -> pair Data Data make = Builtin.mkPairData diff --git a/crates/nash-driver/base/src/Prelude.nash b/crates/nash-driver/base/src/Prelude.nash index 17f6eb2f..789cdb74 100644 --- a/crates/nash-driver/base/src/Prelude.nash +++ b/crates/nash-driver/base/src/Prelude.nash @@ -83,30 +83,37 @@ infix left 4 (<$>) = map infix left 4 (<*>) = apply +{-| Return the argument unchanged. -} identity : 'a -> 'a identity x = x +{-| Return the first argument, ignoring the second. -} always : 'a -> 'b -> 'a always x _ = x +{-| Apply the second argument as a function to the first; used by the forward pipe. -} applyForward : 'a -> ('a -> 'b) -> 'b applyForward = Function.applyForward +{-| Apply the first argument as a function to the second; used by the backward pipe. -} applyBackward : ('a -> 'b) -> 'a -> 'b applyBackward = Function.applyBackward +{-| Compose functions so the second runs first. -} composeLeft : ('b -> 'c) -> ('a -> 'b) -> 'a -> 'c composeLeft g f x = g (f x) +{-| Compose functions so the first runs first. -} composeRight : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c composeRight f g x = g (f x) +{-| Prepend an element to a native list. -} prepend : 'a -> list 'a -> list 'a prepend = Builtin.mkCons diff --git a/crates/nash-driver/base/src/Prop.nash b/crates/nash-driver/base/src/Prop.nash index 9e773e4b..7b825733 100644 --- a/crates/nash-driver/base/src/Prop.nash +++ b/crates/nash-driver/base/src/Prop.nash @@ -35,21 +35,25 @@ import Option exposing ( type option(..) ) import Cons exposing ( type cons(..) ) +{-| A nested record of integer draws. Group nodes delimit strict replay scopes. -} type choiceTree = Choice int | Group (cons choiceTree) +{-| Generator state for seeded draws, strict tree replay, or rebuilding nesting from supplied integers. -} type prng = Seeded bytes (cons choiceTree) | Replayed (cons choiceTree) (cons choiceTree) | Rebuilding (list int) (cons choiceTree) +{-| A state function returning an optional value and next PRNG state. None rejects the draw; ordinary Monad do notation sequences generators. -} type alias generator 'a = prng -> option ( 'a, prng ) +{-| Draw an integer from zero through the inclusive bound. Invalid bounds fail; incompatible replay returns None. -} choice : Lift int 'n => 'n -> generator int choice bound = bound @@ -100,6 +104,7 @@ choiceInt bound state = None +{-| Put a generator's draws inside a strict replay boundary so they cannot consume sibling choices. -} group : generator 'a -> generator 'a group generator state = case state of @@ -176,10 +181,12 @@ impl Monad.Monad generator where continuation value next +{-| Return a value without consuming a choice. -} constant : 'a -> generator 'a constant a prng = Some ( a, prng ) +{-| Generate an integer between inclusive bounds. Equal bounds consume no choices; reversed bounds fail. -} intBetween : ( Lift int 'a, Lift int 'b ) => 'a -> 'b -> generator int intBetween low high = intBetweenInt (lower low) (lower high) @@ -239,6 +246,7 @@ naturalFrom scale = do bounded (scale * Int.pow2 (bits + 1) - 1) +{-| Generate an integer at least the supplied lower bound, with a bias toward small offsets. -} intAtLeast : Lift int 'n => 'n -> generator int intAtLeast minimum = let @@ -248,6 +256,7 @@ intAtLeast minimum = -- Favor small values; the wide branch has no fixed integer ceiling. +{-| Generate a small-biased arbitrary-precision signed integer, subject to VM budgets. -} int : generator int int state = case choice 2 state of @@ -285,10 +294,12 @@ signed = do ) +{-| Generate a list of zero to twenty elements. -} listOf : generator 'a -> generator (list 'a) listOf = listBetween 0 20 +{-| Generate a list within inclusive length bounds. Invalid bounds fail; elements retain their representations. -} listBetween : ( Lift int 'l, Lift int 'h ) => 'l -> 'h -> generator 'a -> generator (list 'a) listBetween low high generator = @@ -338,6 +349,7 @@ listIteration required item state = do (Monad.bind item (\value -> Applicative.pure (Some value))) next +{-| Generate both components of a tuple. -} tuple2 : generator 'a -> generator 'b -> generator ( 'a, 'b ) tuple2 first second = do a <- first @@ -346,6 +358,7 @@ tuple2 first second = do -- Generators contain functions, so use the Term-representation Cons list. +{-| Choose a generator from a Term list. An empty list rejects generation. -} oneOf : cons (generator 'a) -> generator 'a oneOf generators = case generators of @@ -378,14 +391,17 @@ pick index generators = pick (index - 1) rest +{-| Generate a byte string with length from zero through 32. -} bytes : generator bytes bytes = bytesBetween 0 32 +{-| Generate a little Boolean from one binary choice. -} bool : generator bool bool = Functor.map (\n -> n == 1) (choice 1) +{-| Generate None or a present value, grouping the presence and payload draws. -} option : generator 'a -> generator (option 'a) option item = do present <- bool @@ -395,10 +411,12 @@ option item = do Applicative.pure None +{-| Generate bytes within inclusive length bounds. Negative or reversed bounds fail. -} bytesBetween : ( Lift int 'l, Lift int 'h ) => 'l -> 'h -> generator bytes bytesBetween lo hi = Functor.map packBytes (listBetween lo hi (choice 255)) +{-| Generate exactly the supplied number of bytes. Negative lengths fail. -} bytesExactly : Lift int 'n => 'n -> generator bytes bytesExactly count = let @@ -407,6 +425,7 @@ bytesExactly count = bytesBetween n n +{-| Choose a generator using relative integer weights. Zero weights are skipped, negative weights fail, and empty or all-zero inputs reject generation. -} frequency : Lift int 'w => cons ( 'w, generator 'a ) -> generator 'a frequency entries = let diff --git a/crates/nash-driver/base/src/Rational.nash b/crates/nash-driver/base/src/Rational.nash index 954915fb..1752ee67 100644 --- a/crates/nash-driver/base/src/Rational.nash +++ b/crates/nash-driver/base/src/Rational.nash @@ -28,10 +28,12 @@ import Int import Builtin +{-| An exact little rational with a hidden constructor, positive denominator, and coprime components. Zero is represented as 0/1. -} type rational = Rational int int +{-| Normalize a numerator and denominator. A zero denominator fails. -} new : ( Lift int 'n, Lift int 'd ) => 'n -> 'd -> rational new top bottom = let @@ -56,14 +58,17 @@ new top bottom = Rational (Int.quot n divisor * sign) (Int.quot d divisor * sign) +{-| Return the normalized numerator. -} numerator : rational -> int numerator (Rational n _) = n +{-| Return the positive normalized denominator. -} denominator : rational -> int denominator (Rational _ d) = d +{-| Add exact rationals and normalize the result. -} add : rational -> rational -> rational add = addRational @@ -72,6 +77,7 @@ addRational : rational -> rational -> rational addRational (Rational a b) (Rational c d) = new (a * d + c * b) (b * d) +{-| Subtract the second rational from the first and normalize. -} sub : rational -> rational -> rational sub = subRational @@ -80,6 +86,7 @@ subRational : rational -> rational -> rational subRational (Rational a b) (Rational c d) = new (a * d - c * b) (b * d) +{-| Multiply exact rationals and normalize. -} mul : rational -> rational -> rational mul = mulRational @@ -88,10 +95,12 @@ mulRational : rational -> rational -> rational mulRational (Rational a b) (Rational c d) = new (a * c) (b * d) +{-| Divide by a nonzero rational and normalize. Division by zero fails. -} div : rational -> rational -> rational div (Rational a b) (Rational c d) = new (a * d) (b * c) +{-| Negate the rational. -} negate : rational -> rational negate = negateRational @@ -100,10 +109,12 @@ negateRational : rational -> rational negateRational (Rational n d) = Rational (Int.negate n) d +{-| Return the nonnegative magnitude. -} abs : rational -> rational abs (Rational n d) = Rational (Int.abs n) d +{-| Compare exact rationals without converting to floating point. -} compare : rational -> rational -> ordering compare = compareRational @@ -112,14 +123,17 @@ compareRational : rational -> rational -> ordering compareRational (Rational a b) (Rational c d) = Int.compare (a * d) (c * b) +{-| Round toward negative infinity. -} floor : rational -> int floor (Rational n d) = Int.div n d +{-| Round toward positive infinity. -} ceiling : rational -> int ceiling (Rational n d) = Int.negate (Int.div (Int.negate n) d) +{-| Round toward zero. -} truncate : rational -> int truncate (Rational n d) = Int.quot n d diff --git a/crates/nash-driver/base/src/Semigroup.nash b/crates/nash-driver/base/src/Semigroup.nash index 79d90420..12f3c03f 100644 --- a/crates/nash-driver/base/src/Semigroup.nash +++ b/crates/nash-driver/base/src/Semigroup.nash @@ -4,6 +4,7 @@ module Semigroup exposing ( Semigroup ) import Builtin +{-| Combine values associatively with append. -} trait Semigroup 'a where append : 'a -> 'a -> 'a diff --git a/crates/nash-driver/base/src/Show.nash b/crates/nash-driver/base/src/Show.nash index 7eaa3bfb..d914c451 100644 --- a/crates/nash-driver/base/src/Show.nash +++ b/crates/nash-driver/base/src/Show.nash @@ -7,6 +7,7 @@ import Primitive exposing ( Data(..), type bool(..) ) import Literal +{-| Render a value as a string. -} trait Show 'a where show : 'a -> string @@ -216,5 +217,6 @@ impl ( Show 'k, Show 'v ) => Show (Map 'k 'v) where |> Builtin.appendString "Map " +{-| Render bytes as lowercase hexadecimal without a prefix. -} hexBytes : bytes -> string hexBytes value = Builtin.decodeUtf8 (hex value 0) diff --git a/crates/nash-driver/base/src/String.nash b/crates/nash-driver/base/src/String.nash index c58406de..e3897243 100644 --- a/crates/nash-driver/base/src/String.nash +++ b/crates/nash-driver/base/src/String.nash @@ -8,10 +8,12 @@ import Function exposing ( (|>) ) import Int +{-| Encode a string as UTF-8 bytes. -} toBytes : string -> bytes toBytes = Builtin.encodeUtf8 +{-| Decode UTF-8 bytes. Invalid UTF-8 fails. -} fromBytes : Lift bytes 'b => 'b -> string fromBytes value = value @@ -19,10 +21,12 @@ fromBytes value = |> Builtin.decodeUtf8 +{-| Render an integer in decimal notation. -} fromInt : Lift int 'n => 'n -> string fromInt = Int.toString +{-| Concatenate strings in list order. -} concat : list string -> string concat values = concatList values @@ -37,6 +41,7 @@ concatList xs = Builtin.appendString x (concatList rest) +{-| Join strings with a separator placed only between entries. -} join : string -> list string -> string join separator values = case values of diff --git a/crates/nash-driver/base/src/Test.nash b/crates/nash-driver/base/src/Test.nash index be18affa..57d8c30e 100644 --- a/crates/nash-driver/base/src/Test.nash +++ b/crates/nash-driver/base/src/Test.nash @@ -6,10 +6,12 @@ import Prop exposing ( type generator, type prng ) import Option exposing ( type option(..) ) +{-| Record a coverage label in the test runner protocol. -} label : string -> unit label s = Builtin.trace (Builtin.appendString "\u{0000}label\u{0000}" s) () +{-| Generate a sample and return its next state, property thunk, and display thunk. Generation finishes before either thunk runs. -} prepare : generator 'a -> ('a -> unit) -> ('a -> list string) -> @@ -23,6 +25,7 @@ prepare generator property display state = Some ( next, \() -> property value, \() -> display value ) +{-| Sequence two generators, delaying construction of the second until the first succeeds. -} both : generator 'a -> (unit -> generator 'b) -> generator ( 'a, 'b ) both first rest state = case Prop.group first state of @@ -38,10 +41,12 @@ both first rest state = Some ( ( a, b ), final ) +{-| Run an assertion thunk with its source-location marker for failure reporting. -} assertAt : string -> (unit -> unit) -> unit assertAt marker rest = (Builtin.trace marker rest) () +{-| Record a displayed assertion value before running the continuation thunk. -} assertCapture : string -> string -> (unit -> unit) -> unit assertCapture prefix value rest = assertAt (Builtin.appendString prefix value) rest diff --git a/crates/nash-driver/base/src/Unit.nash b/crates/nash-driver/base/src/Unit.nash index 05f0036c..1106066c 100644 --- a/crates/nash-driver/base/src/Unit.nash +++ b/crates/nash-driver/base/src/Unit.nash @@ -5,6 +5,7 @@ import Literal exposing ( FromUnit ) import Lift exposing ( Lift ) +{-| A Data-represented unit value, encoded as constructor zero without fields. -} type Unit = Unit diff --git a/plans/13-fmt-docs.md b/plans/13-fmt-docs.md index f4f63680..5adb140b 100644 --- a/plans/13-fmt-docs.md +++ b/plans/13-fmt-docs.md @@ -137,59 +137,30 @@ See [formatter behavior](../docs/formatter.md) for command and layout details. --- -## Chunk 6: `nash-docs` extraction - -**Files** - -- `crates/nash-docs/Cargo.toml`, `src/lib.rs`, `src/extract.rs` - -**Change** - -Port `Elm/Docs.hs`: from a module's surface docs (chunk 1) and its solved -`Interface`, produce a `Docs` value: overview text, `@docs` ordering, and -one entry per exported value, union, alias, binop, trait, impl, macro, -each with its doc comment and rendered type. Undocumented exports and -`@docs` names that do not exist are warnings, not errors (Elm errors; -Nash warns so `nash docs` always produces output). - -```rust -pub struct ModuleDocs { - pub name: String, - pub overview: String, // Markdown - pub blocks: Vec, // in @docs order, then leftovers -} - -pub enum Block { - Text(String), - Value { name: String, typ: String, doc: String }, - Union { name: String, params: Vec, ctors: Vec<(String, Vec)>, kind: String, doc: String }, - Alias { name: String, params: Vec, typ: String, doc: String }, - Binop { symbol: String, function: String, precedence: u16, assoc: String, doc: String }, - Trait { name: String, params: Vec, supers: Vec, methods: Vec<(String, String)>, doc: String }, - Impl { head: String, doc: String }, - Macro { name: String, shape: String, doc: String }, - Builtin { name: String, typ: String }, // for the synthetic Builtin module, from nash_ast::primitives::PRIMITIVES (types, plans/02 chunk 3) and BUILTINS (functions, plans/12 chunk 3) -} - -pub fn extract(module: &SourceModule<'_>, interface: &Interface<'_>) -> Result> -``` - -Type rendering reuses `nash-report`'s type pretty printer (Elm's -`Reporting/Render/Type.hs`, already ported for diagnostics). - -**Elm/Aiken reference** - -`Elm/Docs.hs` `fromModule`, `parseOverview`, `checkNames`; -`Elm/Compiler/Type/Extract.hs` `fromType`. Aiken -`crates/aiken-project/src/docs.rs` `generate_all` (module listing, search -index). - -**Tests** - -Snapshot `ModuleDocs` for a module with `@docs`, one of each block kind, -and a missing name (warning present, docs still produced). - -**Done when** `extract` runs over `crates/nash-driver/base/` without warnings. +## Chunk 6: `nash-docs` extraction — complete + +`nash-docs::extract` combines a parsed source module and its solved +`nash_can::Interface` into owned `ModuleDocs` plus documentation warnings. +It uses the existing report type printer, retains explicit trait constraints, +and includes public values, types, aliases, operators, traits and implementations. +Private declarations and hidden constructors are omitted. Type kinds remain in +the output. Unnamed implementations do not require separate documentation. + +Overview prose and `@docs` groups retain their order. Missing comments, +unknown names and duplicate directives produce warnings without discarding +output. Undirected declarations follow in source order. Source-backed macro +declarations are not implemented by the parser/interface yet, so no synthetic +macro entries are invented. + +The actual compiler catalogs supply separate `Builtin` and `Primitive` modules; +`coerce` belongs to Primitive. Public Base declarations now have source doc +comments. A library test compiles all 35 bundled modules and checks that +extraction produces no documentation warnings. Source-described snapshots cover +all supported declaration kinds, inferred/constrained signatures, ordering, +visibility, warning recovery and synthetic catalogs. No CLI processes are used. + +Base has no separate project manifest. Chunk 7 provides `nash docs --base` +using the compiler-bundled sources, alongside normal project documentation. --- From d87929edf1ab8ef1fa84570b86be0f6f9514033a Mon Sep 17 00:00:00 2001 From: microproofs Date: Sat, 26 Sep 2026 01:38:31 -0400 Subject: [PATCH 08/19] feat(docs): render searchable API documentation Signed-off-by: microproofs --- .github/workflows/docs.yml | 49 +++ .sampo/changesets/docs-rendering.md | 5 + Cargo.lock | 6 + SPEC.md | 6 +- crates/nash-cli/Cargo.toml | 2 + crates/nash-cli/src/cmd/docs.rs | 127 ++++++++ crates/nash-cli/src/cmd/mod.rs | 4 + crates/nash-docs/Cargo.toml | 12 +- crates/nash-docs/src/assets/search.js | 35 +++ crates/nash-docs/src/assets/style.css | 38 +++ crates/nash-docs/src/highlight.rs | 88 ++++++ crates/nash-docs/src/lib.rs | 4 + crates/nash-docs/src/project.rs | 124 ++++++++ crates/nash-docs/src/render.rs | 292 ++++++++++++++++++ ...nash_docs__tests__highlight_fragments.snap | 13 + .../nash_docs__tests__html_output.snap | 68 ++++ .../nash_docs__tests__markdown_output.snap | 52 ++++ .../nash_docs__tests__project_exports.snap | 27 ++ crates/nash-docs/src/tests.rs | 245 +++++++++++++-- docs/cli.md | 8 +- docs/documentation.md | 41 +++ plans/13-fmt-docs.md | 60 ++-- 22 files changed, 1232 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .sampo/changesets/docs-rendering.md create mode 100644 crates/nash-cli/src/cmd/docs.rs create mode 100644 crates/nash-docs/src/assets/search.js create mode 100644 crates/nash-docs/src/assets/style.css create mode 100644 crates/nash-docs/src/highlight.rs create mode 100644 crates/nash-docs/src/project.rs create mode 100644 crates/nash-docs/src/render.rs create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__highlight_fragments.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__html_output.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__markdown_output.snap create mode 100644 crates/nash-docs/src/snapshots/nash_docs__tests__project_exports.snap create mode 100644 docs/documentation.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..479e21f5 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: Documentation + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + run: rustup show + - uses: Swatinem/rust-cache@v2 + - name: Generate Base documentation + run: cargo run -p nash-cli -- docs --base --out target/base-docs + - name: Save documentation preview + uses: actions/upload-artifact@v4 + with: + name: base-documentation + path: target/base-docs + - name: Prepare Pages artifact + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v4 + with: + path: target/base-docs + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + concurrency: + group: pages + cancel-in-progress: false + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy documentation + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.sampo/changesets/docs-rendering.md b/.sampo/changesets/docs-rendering.md new file mode 100644 index 00000000..14507a3e --- /dev/null +++ b/.sampo/changesets/docs-rendering.md @@ -0,0 +1,5 @@ +--- +cargo/nash-docs: minor +cargo/nash-cli: minor +--- +Render searchable HTML and Markdown API documentation with `nash docs`, including compiler-bundled Base documentation. diff --git a/Cargo.lock b/Cargo.lock index c0b073d6..bb5f1392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,10 +1920,12 @@ dependencies = [ "miette", "nash-codegen", "nash-config", + "nash-docs", "nash-driver", "nash-fmt", "nash-language-server", "nash-plutus", + "nash-region", "nash-report", "nash-test", "octocrab", @@ -1996,12 +1998,16 @@ dependencies = [ "insta", "nash-ast", "nash-can", + "nash-config", "nash-driver", "nash-parse", "nash-report", "nash-source", "pulldown-cmark", "serde", + "serde_json", + "tempfile", + "thiserror 2.0.21", "tokio", "url", ] diff --git a/SPEC.md b/SPEC.md index 8e6b4bed..ef5ef61b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -32,11 +32,11 @@ produce UPLC programs; all dependencies inline into each program. | `nash-codegen` | Can -> Core -> UPLC | new ([plans/07](plans/07-codegen.md)) | | `nash-test` | test runner, generation, shrinking | new ([plans/10](plans/10-testing.md)) | | `nash-macro` | macro expansion, comptime | new ([plans/11](plans/11-macros-comptime.md)) | -| `nash-fmt` / `nash-docs` | formatter, docs | formatter and docs extraction implemented; rendering pending ([plans/13](plans/13-fmt-docs.md)) | +| `nash-fmt` / `nash-docs` | formatter, docs | formatter and documentation implemented ([plans/13](plans/13-fmt-docs.md)) | | `nash-plutus` | UPLC terms, flat, CEK, cost models | done | | `nash-config` | `nash.jsonc` | done, extend | | `nash-driver` | build graph, caching | done, extend | -| `nash-cli` | `nash` binary | `check`, `build`, `test`, `format` (`fmt`), `lsp`; docs pending | +| `nash-cli` | `nash` binary | `check`, `build`, `test`, `format` (`fmt`), `docs`, `lsp` | | `nash-language-server` | LSP | live compiler diagnostics with UTF-16 ranges | | `crates/nash-driver/base/` | compiler-bundled `nash/base` foundation (Nash source) | implemented; remaining stdlib work ([plans/12](plans/12-stdlib.md)) | @@ -68,7 +68,7 @@ Implementation plans (Plan 08 is deferred; Plans 09 and 10 are complete): - [x] 10 Testing: `tests` block, props, generators, shrinking, power-assert, `nash test` — [plans/10-testing.md](plans/10-testing.md) - [ ] 11 Macros + comptime — [plans/11-macros-comptime.md](plans/11-macros-comptime.md) - [ ] 12 Stdlib `nash/base` — [plans/12-stdlib.md](plans/12-stdlib.md) (chunks 1–9 and 11–13 complete; chunk 10 Ast/Derive deferred) -- [ ] 13 `nash format` (`fmt`), `nash docs` — [plans/13-fmt-docs.md](plans/13-fmt-docs.md) (chunks 1–5 formatter complete; chunks 6–7 docs pending) +- [x] 13 `nash format` (`fmt`), `nash docs` — [plans/13-fmt-docs.md](plans/13-fmt-docs.md) (chunks 1–7 complete) - [x] Representation-classed impl heads: `Big`/`Little` blankets and concrete little impls coexist — [plans/repr-classed-impl-heads.md](plans/repr-classed-impl-heads.md) Later: LSP features, web playground, package registry (pubgrub), TypeScript codegen. diff --git a/crates/nash-cli/Cargo.toml b/crates/nash-cli/Cargo.toml index 733c0f0b..ced409cc 100644 --- a/crates/nash-cli/Cargo.toml +++ b/crates/nash-cli/Cargo.toml @@ -16,6 +16,8 @@ name = "nash" path = "src/main.rs" [dependencies] +nash-region = { path = "../nash-region", version = "0.3.0" } +nash-docs = { path = "../nash-docs", version = "0.1.0" } nash-fmt = { path = "../nash-fmt", version = "0.1.0" } nash-codegen = { path = "../nash-codegen", version = "0.4.2" } clap.workspace = true diff --git a/crates/nash-cli/src/cmd/docs.rs b/crates/nash-cli/src/cmd/docs.rs new file mode 100644 index 00000000..77d83dc1 --- /dev/null +++ b/crates/nash-cli/src/cmd/docs.rs @@ -0,0 +1,127 @@ +use miette::{IntoDiagnostic, Result}; +use std::path::PathBuf; +use tokio::{ + io::AsyncWriteExt, + task::{JoinSet, spawn_blocking}, +}; + +#[derive(Clone, Copy, clap::ValueEnum)] +pub enum Format { + Html, + Markdown, +} + +#[derive(clap::Args)] +pub struct Args { + /// Project directory; defaults to the current directory. + #[arg(conflicts_with = "base")] + pub path: Option, + /// Document the compiler-bundled Base modules. + #[arg(long)] + pub base: bool, + /// Documentation output format. + #[arg(long, value_enum, default_value = "html")] + pub format: Format, + /// Directory for generated documentation. + #[arg(long, default_value = "docs")] + pub out: PathBuf, +} + +impl Args { + pub async fn exec(self, color: bool) -> Result<()> { + let path = self.path.unwrap_or_else(|| PathBuf::from(".")); + let input = if self.base { + nash_docs::project::Input::Base + } else { + nash_docs::project::Input::Project(&path) + }; + let docs = nash_docs::project::build(input).await.into_diagnostic()?; + let success = docs.compiler.is_success(); + // Render diagnostics off the executor just like documentation pages. + let (files, diagnostics) = spawn_blocking(move || { + let mut diagnostics = String::new(); + for module in docs.compiler.ordered_reports() { + let source = nash_report::Source::new(&module.source); + for report in &module.reports { + diagnostics.push_str(&format!( + "{:?}\n", + miette::Report::new(report.render(&source, &module.path, color)) + )); + } + } + for (uri, state) in &docs.compiler.modules { + if let nash_driver::ModuleResult::SourceUnavailable { message } = state { + diagnostics.push_str(&format!("Could not read {uri}: {message}\n")); + } + } + for warning in &docs.warnings { + let mut report = nash_report::Report::snippet( + "DOCUMENTATION WARNING", + nash_region::Region::zero(), + None, + nash_report::Doc::text(format!( + "{}.{}: {}", + warning.module, warning.name, warning.message + )), + nash_report::Doc::Empty, + ) + .warning(); + report.primary_label = None; + report.context = None; + diagnostics.push_str(&format!( + "{:?}\n", + miette::Report::new(report.render( + &nash_report::Source::new(""), + &warning.module, + color + )) + )); + } + let format = match self.format { + Format::Html => nash_docs::Format::Html, + Format::Markdown => nash_docs::Format::Markdown, + }; + ( + if success { + nash_docs::render(&docs.modules, format) + } else { + Default::default() + }, + diagnostics, + ) + }) + .await + .into_diagnostic()?; + let mut stderr = tokio::io::stderr(); + stderr + .write_all(diagnostics.as_bytes()) + .await + .into_diagnostic()?; + stderr.flush().await.into_diagnostic()?; + if !success { + return Err(miette::miette!( + "Documentation generation stopped because compilation failed" + )); + } + let mut writes = JoinSet::new(); + for (relative, contents) in files { + let path = self.out.join(relative); + writes.spawn(async move { + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + tokio::fs::write(path, contents).await + }); + } + let mut result = Ok(()); + while let Some(write) = writes.join_next().await { + if let Err(error) = write + .into_diagnostic() + .and_then(|write| write.into_diagnostic()) + { + result = Err(error); + } + } + result + } +} diff --git a/crates/nash-cli/src/cmd/mod.rs b/crates/nash-cli/src/cmd/mod.rs index 17390508..8f44be5d 100644 --- a/crates/nash-cli/src/cmd/mod.rs +++ b/crates/nash-cli/src/cmd/mod.rs @@ -1,5 +1,6 @@ pub mod build; pub mod check; +pub mod docs; pub mod format; pub mod lsp; pub mod test; @@ -18,6 +19,8 @@ pub enum Cmd { /// Format Nash source files #[clap(visible_alias = "fmt")] Format(format::Args), + /// Generate public API documentation + Docs(docs::Args), /// Start the Nash language server over stdio Lsp(lsp::Args), } @@ -25,6 +28,7 @@ pub enum Cmd { impl Cmd { pub async fn exec(self, color: bool) -> miette::Result<()> { match self { + Cmd::Docs(args) => args.exec(color).await, Cmd::Check(args) => args.exec(color).await, Cmd::Build(args) => args.exec(color).await, Cmd::Test(args) => args.exec(color).await, diff --git a/crates/nash-docs/Cargo.toml b/crates/nash-docs/Cargo.toml index 61ff2e92..b29dbb6d 100644 --- a/crates/nash-docs/Cargo.toml +++ b/crates/nash-docs/Cargo.toml @@ -8,6 +8,13 @@ repository.workspace = true license.workspace = true [dependencies] +bumpalo.workspace = true +tokio.workspace = true +serde_json.workspace = true +thiserror.workspace = true +nash-driver = { path = "../nash-driver", version = "0.11.0" } +nash-config = { path = "../nash-config", version = "0.5.0" } +nash-parse = { path = "../nash-parse", version = "0.8.0" } nash-ast = { path = "../nash-ast", version = "0.11.0" } nash-can = { path = "../nash-can", version = "0.12.0" } nash-source = { path = "../nash-source", version = "0.9.0" } @@ -16,10 +23,7 @@ serde.workspace = true pulldown-cmark = { version = "0.13", default-features = false, features = ["html"] } [dev-dependencies] -bumpalo.workspace = true +tempfile = "3" indoc.workspace = true insta.workspace = true -nash-driver = { path = "../nash-driver" } -nash-parse = { path = "../nash-parse" } -tokio.workspace = true url.workspace = true diff --git a/crates/nash-docs/src/assets/search.js b/crates/nash-docs/src/assets/search.js new file mode 100644 index 00000000..d9312683 --- /dev/null +++ b/crates/nash-docs/src/assets/search.js @@ -0,0 +1,35 @@ +(() => { + const root = new URL('.', document.currentScript.src); + const input = document.getElementById('search'); + const results = document.getElementById('search-results'); + const status = document.getElementById('search-status'); + let entries; + let failed = false; + const update = () => { + results.replaceChildren(); + const query = input.value.trim().toLowerCase(); + results.hidden = !query; + if (!query) { status.textContent = ''; return; } + if (failed) { status.textContent = 'Search unavailable. Browse the module links below.'; return; } + if (!entries) { status.textContent = 'Loading search…'; return; } + const matches = entries.filter(entry => `${entry.module} ${entry.name} ${entry.signature} ${entry.summary}`.toLowerCase().includes(query)); + status.textContent = `${matches.length} result${matches.length === 1 ? '' : 's'}`; + for (const entry of matches.slice(0, 40)) { + const link = document.createElement('a'); + link.href = new URL(entry.url, root).href; + link.textContent = entry.name; + const module = document.createElement('small'); + module.textContent = entry.module; + link.append(module); + results.append(link); + } + }; + input.addEventListener('input', update); + fetch(new URL('search.json', root)).then(response => { + if (!response.ok) throw new Error('Search unavailable'); + return response.json(); + }).then(data => { entries = data; update(); }).catch(() => { + failed = true; + update(); + }); +})(); diff --git a/crates/nash-docs/src/assets/style.css b/crates/nash-docs/src/assets/style.css new file mode 100644 index 00000000..2af4c5e6 --- /dev/null +++ b/crates/nash-docs/src/assets/style.css @@ -0,0 +1,38 @@ +:root { color-scheme: light dark; --bg: #fafaf8; --panel: #fff; --text: #20252b; --muted: #59636f; --line: #dce1e5; --accent: #176f65; } +* { box-sizing: border-box; } +body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.65 system-ui, sans-serif; } +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } +a:focus-visible, input:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } +aside { position: fixed; inset: 0 auto 0 0; width: 270px; overflow-y: auto; padding: 28px 24px; border-right: 1px solid var(--line); background: var(--panel); } +.brand { display: block; margin-bottom: 28px; font-size: 26px; font-weight: 750; } +.brand span { display: block; color: var(--muted); font-size: 13px; font-weight: 450; letter-spacing: .08em; } +label { display: block; color: var(--muted); font-size: 13px; } +input { width: 100%; padding: 9px 10px; border: 1px solid var(--line); border-radius: 5px; background: var(--bg); color: var(--text); font: inherit; font-size: 14px; } +nav { display: grid; gap: 3px; margin-top: 20px; } +nav a { overflow-wrap: anywhere; padding: 3px 0; font-size: 14px; } +nav small { display: block; color: var(--muted); } +[hidden] { display: none; } +#search-status { font-size: 12px; color: var(--muted); } +main { max-width: 1250px; margin-left: 270px; padding: 40px 52px 100px; } +h1 { font-size: 38px; line-height: 1.2; margin: 0 0 28px; letter-spacing: -.03em; } +h2 { font-size: 21px; line-height: 1.4; overflow-wrap: anywhere; margin: 0; } +section { padding: 28px 0; border-top: 1px solid var(--line); scroll-margin-top: 20px; } +section header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 12px; } +.kind, .type-kind { color: var(--muted); font-size: 12px; } +.kind { text-transform: uppercase; letter-spacing: .06em; } +pre { overflow-x: auto; padding: 18px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; line-height: 1.6; } +code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: .88em; } +p code { padding: 2px 4px; background: var(--panel); } +.keyword { color: #8b3895; font-weight: 600; } +.type { color: #236eb3; } +.string { color: #33772b; } +.number { color: #9c4e12; } +.operator { color: #9b3e3e; } +.comment { color: var(--muted); font-style: italic; } +table { border-collapse: collapse; } +th, td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; } +.module-list { columns: 2; list-style: none; padding: 0; } +.module-list li { padding: 5px 0; } +@media (prefers-color-scheme: dark) { :root { --bg: #171c20; --panel: #1f262c; --text: #e2e8ed; --muted: #a3afb9; --line: #38444e; --accent: #72d4bd; } .keyword { color: #d5a0ed; } .type { color: #8dbefa; } .string { color: #a4d986; } .number { color: #f0bb79; } .operator { color: #eea6a2; } } +@media (max-width: 760px) { aside { position: static; width: 100%; max-height: 340px; border-right: 0; border-bottom: 1px solid var(--line); } main { margin-left: 0; padding: 30px 20px; } h1 { font-size: 30px; } .module-list { columns: 1; } } diff --git a/crates/nash-docs/src/highlight.rs b/crates/nash-docs/src/highlight.rs new file mode 100644 index 00000000..0177f490 --- /dev/null +++ b/crates/nash-docs/src/highlight.rs @@ -0,0 +1,88 @@ +use bumpalo::Bump; + +pub fn escape(text: &str) -> String { + text.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +/// Highlight fragments with Nash's literal parsers and keyword/operator catalog. +/// Unfinished examples are still printable; unrecognized characters stay escaped. +pub fn highlight(source: &str) -> String { + let arena = Bump::new(); + let mut rest = source; + let mut output = String::new(); + while !rest.is_empty() { + let first = rest.chars().next().unwrap(); + let (len, class) = if rest.starts_with("--") { + (rest.find('\n').unwrap_or(rest.len()), "comment") + } else if rest.starts_with("{-") { + let mut depth = 1; + let mut end = 2; + while end < rest.len() && depth != 0 { + if rest[end..].starts_with("{-") { + depth += 1; + end += 2; + } else if rest[end..].starts_with("-}") { + depth -= 1; + end += 2; + } else { + end += rest[end..].chars().next().unwrap().len_utf8(); + } + } + (end, "comment") + } else if rest.starts_with('#') && rest.as_bytes().get(1) == Some(&b'"') + || first == '"' + || first.is_ascii_digit() + { + let mut parser = nash_parse::Parser::new(&arena, rest); + let class = if rest.starts_with("#\"") { + let _ = parser.bytes_literal(|_, _| (), |_, _, _| ()); + "string" + } else if first == '"' { + let _ = parser.string_literal(|_, _| (), |_, _, _| ()); + "string" + } else { + let _ = parser.number_literal(|_, _| (), |_, _, _| ()); + "number" + }; + ( + (rest.len() - parser.remaining().len()).max(first.len_utf8()), + class, + ) + } else if first.is_alphabetic() || first == '_' || first == '\'' { + let len = rest + .char_indices() + .take_while(|(_, c)| c.is_alphanumeric() || *c == '_' || *c == '\'') + .last() + .map_or(first.len_utf8(), |(i, c)| i + c.len_utf8()); + let word = &rest[..len]; + let class = if nash_parse::keyword::is_reserved(word) { + "keyword" + } else if first.is_uppercase() { + "type" + } else { + "" + }; + (len, class) + } else if first.is_ascii() && nash_parse::symbol::is_binop_char(first as u8) { + let len = rest + .bytes() + .take_while(|b| nash_parse::symbol::is_binop_char(*b)) + .count(); + (len, "operator") + } else { + (first.len_utf8(), "") + }; + let token = escape(&rest[..len]); + if class.is_empty() { + output.push_str(&token); + } else { + output.push_str(&format!("{token}")); + } + rest = &rest[len..]; + } + output +} diff --git a/crates/nash-docs/src/lib.rs b/crates/nash-docs/src/lib.rs index bf30b40f..79a2d30b 100644 --- a/crates/nash-docs/src/lib.rs +++ b/crates/nash-docs/src/lib.rs @@ -1,4 +1,8 @@ //! Owned documentation extracted from source comments and solved interfaces. +mod highlight; +pub mod project; +mod render; +pub use render::{Format, render}; mod extract; mod types; pub use extract::{extract, primitives}; diff --git a/crates/nash-docs/src/project.rs b/crates/nash-docs/src/project.rs new file mode 100644 index 00000000..35f7f715 --- /dev/null +++ b/crates/nash-docs/src/project.rs @@ -0,0 +1,124 @@ +//! Run documentation extraction through the same solved frontend as builds. +use crate::{DocsWarning, ModuleDocs, extract, primitives}; +use nash_config::Config; +use nash_driver::{ + Database, DriverError, FileSystemSource, Project, build_graph_production, build_with, + bundled_base, +}; +use std::{ + collections::{BTreeMap, HashSet}, + path::Path, + sync::Arc, +}; +use tokio::sync::Mutex; + +pub struct Documentation { + pub compiler: nash_driver::BuildResult, + pub modules: Vec, + pub warnings: Vec, +} + +pub enum Input<'a> { + Base, + Project(&'a Path), +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Driver(#[from] DriverError), + #[error( + "module {0} appears in multiple packages; generate documentation for each package separately" + )] + DuplicateModule(String), +} + +pub async fn build(input: Input<'_>) -> Result { + let db = Arc::new(Mutex::new(Database::new(FileSystemSource::new()))); + let base = matches!(input, Input::Base); + let mut packages = BTreeMap::new(); + let (origins, roots) = match input { + Input::Base => { + let modules = bundled_base::modules(); + let roots = modules.keys().cloned().collect::>(); + (modules, roots) + } + Input::Project(path) => { + let project = Project::load(path).await?; + for member in &project.members { + if let Config::Package(package) = &member.config { + packages.insert( + package.name.to_string(), + package + .exposed_modules + .flatten() + .into_iter() + .map(str::to_owned) + .collect::>(), + ); + } + } + let database = db.lock().await; + let origins = project.discover_modules_production(&database).await?; + let roots = project + .discover_own_modules(&database) + .await? + .into_keys() + .collect::>(); + (origins, roots) + } + }; + let graph = + build_graph_production(db.clone(), &origins.keys().cloned().collect::>()).await?; + let selected_roots = roots.clone(); + let (compiler, extracted) = build_with(db, &graph, &origins, move |solved| { + solved + .modules + .iter() + .filter(|m| { + selected_roots.contains(&m.uri) + && (base + || m.module.name.package.is_none_or(|p| { + packages + .get(&format!("{}/{}", p.author, p.project)) + .is_some_and(|exports| exports.contains(m.module.name.name)) + })) + }) + .map(|module| { + let parsed = nash_parse::Parser::new(solved.store, module.source) + .module() + .expect("solved source parses"); + let interface = + nash_can::from_module(solved.store, module.module, &module.annotations); + extract(&parsed, &interface) + }) + .collect::>() + }) + .await; + // The driver backend keeps one solved module per name. Detect collisions + // from its URI-keyed interfaces before an overwritten module can disappear. + for (uri, interface) in &compiler.interfaces { + if roots.contains(uri) + && compiler.interfaces.iter().any(|(other_uri, other)| { + uri != other_uri && interface.module_name == other.module_name + }) + { + return Err(Error::DuplicateModule(interface.module_name.clone())); + } + } + let mut modules = Vec::new(); + let mut warnings = Vec::new(); + for extraction in extracted.unwrap_or_default() { + modules.push(extraction.module); + warnings.extend(extraction.warnings); + } + if base && compiler.is_success() { + modules.extend(primitives()); + } + modules.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(Documentation { + compiler, + modules, + warnings, + }) +} diff --git a/crates/nash-docs/src/render.rs b/crates/nash-docs/src/render.rs new file mode 100644 index 00000000..f80e51eb --- /dev/null +++ b/crates/nash-docs/src/render.rs @@ -0,0 +1,292 @@ +use crate::highlight::{escape, highlight}; +use crate::{Block, DeclarationKind, ModuleDocs}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; +use serde::Serialize; +use std::collections::BTreeMap; + +pub const STYLE: &str = include_str!("assets/style.css"); +pub const SEARCH: &str = include_str!("assets/search.js"); + +#[derive(Clone, Copy, Debug)] +pub enum Format { + Html, + Markdown, +} + +#[derive(Serialize)] +struct SearchEntry<'a> { + module: &'a str, + name: &'a str, + kind: &'static str, + signature: &'a str, + summary: &'a str, + url: String, +} + +pub fn anchor(name: &str) -> String { + format!( + "entry-{}", + name.bytes().map(|b| format!("{b:02x}")).collect::() + ) +} +fn path(name: &str, extension: &str) -> String { + format!("{}.{extension}", name.replace('.', "/")) +} +fn kind(kind: DeclarationKind) -> &'static str { + match kind { + DeclarationKind::Value => "value", + DeclarationKind::Union => "type", + DeclarationKind::Alias => "alias", + DeclarationKind::Operator => "operator", + DeclarationKind::Trait => "trait", + DeclarationKind::Implementation => "implementation", + DeclarationKind::Builtin => "builtin", + DeclarationKind::Primitive => "primitive", + } +} + +/// Render a complete site or Markdown collection. I/O belongs to the frontend. +pub fn render(modules: &[ModuleDocs], format: Format) -> BTreeMap { + let mut modules: Vec<_> = modules.iter().collect(); + modules.sort_by(|a, b| a.name.cmp(&b.name)); + let mut files = BTreeMap::new(); + match format { + Format::Markdown => { + let mut index = "# Nash documentation\n\n".to_owned(); + for module in &modules { + let filename = path(&module.name, "md"); + index.push_str(&format!("- [{}]({filename})\n", module.name)); + files.insert(filename, markdown(module)); + } + files.insert("index.md".into(), index); + } + Format::Html => { + let mut index = "

Nash documentation

Public modules and their APIs.

    ".to_owned(); + let mut search = Vec::new(); + for module in &modules { + let filename = path(&module.name, "html"); + index.push_str(&format!( + "
  • {}
  • ", + escape(&filename), + escape(&module.name) + )); + let root = "../".repeat(module.name.matches('.').count()); + let mut body = format!("

    {}

    ", escape(&module.name)); + let overview = module + .blocks + .iter() + .filter_map(|block| match block { + Block::Text(text) => Some(text.as_str()), + _ => None, + }) + .collect::>() + .join("\n\n"); + let overview_parser = Parser::new(&overview); + for block in &module.blocks { + match block { + Block::Text(text) => body + .push_str(&prose(text, Some(overview_parser.reference_definitions()))), + Block::Declaration(entry) => { + let id = anchor(&entry.name); + body.push_str(&format!("
    {}

    {}

    {}
    ", kind(entry.kind), escape(&entry.name), highlight(&entry.signature))); + if let Some(kind) = &entry.type_kind { + body.push_str(&format!( + "

    Kind: {}

    ", + escape(kind) + )); + } + body.push_str(&prose(&entry.doc, None)); + body.push_str("
    \n"); + search.push(SearchEntry { + module: &module.name, + name: &entry.name, + kind: kind(entry.kind), + signature: &entry.signature, + summary: entry.doc.lines().next().unwrap_or(""), + url: format!("{filename}#{id}"), + }); + } + } + } + files.insert(filename, page(&module.name, &root, &modules, &body)); + } + index.push_str("
"); + files.insert( + "index.html".into(), + page("Nash documentation", "", &modules, &index), + ); + files.insert( + "search.json".into(), + serde_json::to_string_pretty(&search).expect("documentation strings serialize"), + ); + files.insert("style.css".into(), STYLE.into()); + files.insert("search.js".into(), SEARCH.into()); + } + } + files +} + +fn page(title: &str, root: &str, modules: &[&ModuleDocs], body: &str) -> String { + let links = modules + .iter() + .map(|module| { + format!( + "{}", + root, + escape(&path(&module.name, "html")), + escape(&module.name) + ) + }) + .collect::>() + .join("\n"); + format!( + r#" + + + + +{title} · Nash + + + + + +
{body}
+ + +"#, + title = escape(title) + ) +} + +fn safe_link(url: &str) -> bool { + !url.chars().any(char::is_control) + && url.split_once(':').is_none_or(|(scheme, _)| { + matches!( + scheme.to_ascii_lowercase().as_str(), + "http" | "https" | "mailto" + ) + }) +} +fn prose(source: &str, references: Option<&pulldown_cmark::RefDefs<'_>>) -> String { + let resolve = |link: pulldown_cmark::BrokenLink<'_>| { + references + .and_then(|refs| refs.get(&link.reference)) + .map(|definition| { + ( + definition.dest.to_string().into(), + definition.title.as_deref().unwrap_or("").to_string().into(), + ) + }) + }; + let mut parser = Parser::new_with_broken_link_callback( + source, + Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH, + Some(resolve), + ) + .peekable(); + let mut events = Vec::new(); + while let Some(event) = parser.next() { + match event { + Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(language))) + if language.split_whitespace().next() == Some("nash") => + { + let mut code = String::new(); + for part in parser.by_ref() { + match part { + Event::End(TagEnd::CodeBlock) => break, + Event::Text(text) => code.push_str(&text), + _ => {} + } + } + events.push(Event::Html( + format!( + "
{}
\n", + highlight(&code) + ) + .into(), + )); + } + Event::Html(html) | Event::InlineHtml(html) => events.push(Event::Text(html)), + Event::Start(Tag::Link { + link_type, + dest_url, + title, + id, + }) => events.push(Event::Start(Tag::Link { + link_type, + dest_url: if safe_link(&dest_url) { + dest_url + } else { + "".into() + }, + title, + id, + })), + Event::Start(Tag::Image { + link_type, + dest_url, + title, + id, + }) => events.push(Event::Start(Tag::Image { + link_type, + dest_url: if safe_link(&dest_url) { + dest_url + } else { + "".into() + }, + title, + id, + })), + other => events.push(other), + } + } + let mut html = String::new(); + pulldown_cmark::html::push_html(&mut html, events.into_iter()); + html +} + +pub fn markdown(module: &ModuleDocs) -> String { + let mut out = format!("# {}\n\n", module.name); + for block in &module.blocks { + match block { + Block::Text(text) => { + out.push_str(text); + out.push_str("\n\n"); + } + Block::Declaration(entry) => { + out.push_str(&format!( + "\n\n## {}\n\n", + anchor(&entry.name), + entry.name + )); + let fence = "`".repeat( + entry + .signature + .split(|c| c != '`') + .map(str::len) + .max() + .unwrap_or(0) + .max(2) + + 1, + ); + out.push_str(&format!("{fence}nash\n{}\n{fence}\n\n", entry.signature)); + if let Some(kind) = &entry.type_kind { + out.push_str(&format!("Kind: `{kind}`\n\n")); + } + if !entry.doc.is_empty() { + out.push_str(&entry.doc); + out.push_str("\n\n"); + } + } + } + } + out +} diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__highlight_fragments.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__highlight_fragments.snap new file mode 100644 index 00000000..4837f37b --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__highlight_fragments.snap @@ -0,0 +1,13 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "-- A comment\n{- Nested {- comment -} λ -}\nrun : int -> int\nrun value = if value < 0 then -value else value\nbytes = #\"00ab\"\ntext = \"\n\n[Unsafe link](javascript:alert%281%29)\n-}\nidentity x = x\n" +--- +--- Example.html + + + + + +Example · Nash + + + + + +

Example

Public API with a link.

+
value

identity

identity : 'a -> 'a

Preserve a value.

+
identity "hello"
+
+<script>alert("raw HTML")</script> +

Unsafe link

+
+
+ + + +--- index.html + + + + + +Nash documentation · Nash + + + + + +

Nash documentation

Public modules and their APIs.

+ + + +--- search.json +[ + { + "module": "Example", + "name": "identity", + "kind": "value", + "signature": "identity : 'a -> 'a", + "summary": "Preserve a value.", + "url": "Example.html#entry-6964656e74697479" + } +] diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__markdown_output.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__markdown_output.snap new file mode 100644 index 00000000..d3747f2f --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__markdown_output.snap @@ -0,0 +1,52 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Example exposing (type box(..), identity, (%%))\n{-| # A small API\n\n@docs box, identity, (%%)\n-}\n\ninfix left 5 (%%) = combine\n\n{-| A little container. -}\ntype box 'a = Box 'a\n\n{-| Preserve a value.\n\n```nash\nidentity 42\n```\n-}\nidentity x = x\n\ncombine x y = x\n" +--- +--- Example.md +# Example + +# A small API + + + +## box + +```nash +type box 'a + = Box 'a +``` + +Kind: `Type -> Type` + +A little container. + + + +## identity + +```nash +identity : 'a -> 'a +``` + +Preserve a value. + +```nash +identity 42 +``` + + + +## (%%) + +```nash +(%%) : 'a -> 'b -> 'a +infix left 5 (%%) = combine +``` + +Infix form of `Example.combine`. + + +--- index.md +# Nash documentation + +- [Example](Example.md) diff --git a/crates/nash-docs/src/snapshots/nash_docs__tests__project_exports.snap b/crates/nash-docs/src/snapshots/nash_docs__tests__project_exports.snap new file mode 100644 index 00000000..1e288ab8 --- /dev/null +++ b/crates/nash-docs/src/snapshots/nash_docs__tests__project_exports.snap @@ -0,0 +1,27 @@ +--- +source: crates/nash-docs/src/tests.rs +description: "module Public.Api exposing (answer)\n{-| The answer. -}\nanswer = 42\n" +--- + + + + + +Public.Api · Nash + + + + + +

Public.Api

The answer.

+
value

answer

answer : Literal.FromInt 'a => 'a
+
+ + diff --git a/crates/nash-docs/src/tests.rs b/crates/nash-docs/src/tests.rs index 3468b0b2..2ee8a4d2 100644 --- a/crates/nash-docs/src/tests.rs +++ b/crates/nash-docs/src/tests.rs @@ -115,39 +115,26 @@ fn compiler_owned_modules() { #[tokio::test] async fn base_documentation() { - let origins = bundled_base::modules(); - let db = Arc::new(Mutex::new(Database::new(InMemorySource::new()))); - let graph = build_graph_production(db.clone(), &origins.keys().cloned().collect::>()) - .await - .unwrap(); - let (result, warnings) = build_with(db, &graph, &origins, |solved| { - solved - .modules - .iter() - .flat_map(|module| { - let source = nash_parse::Parser::new(solved.store, module.source) - .module() - .unwrap(); - extract( - &source, - &nash_can::from_module(solved.store, module.module, &module.annotations), - ) - .warnings - }) - .collect::>() - }) - .await; - assert!(result.is_success(), "{:?}", result.ordered_reports()); + let docs = project::build(project::Input::Base).await.unwrap(); assert!( - warnings.as_ref().unwrap().is_empty(), - "{}", - warnings - .unwrap() - .iter() - .map(|w| format!("{}.{}: {}", w.module, w.name, w.message)) - .collect::>() - .join("\n") + docs.compiler.is_success(), + "{:?}", + docs.compiler.ordered_reports() ); + assert!(docs.warnings.is_empty(), "{:?}", docs.warnings); + assert_eq!(docs.modules.len(), bundled_base::modules().len() + 2); + let files = render(&docs.modules, Format::Html); + let index = &files["index.html"]; + for module in &docs.modules { + let path = format!("{}.html", module.name.replace('.', "/")); + assert!(index.contains(&format!("href=\"{path}\""))); + assert!(files.contains_key(&path)); + } + let search: serde_json::Value = serde_json::from_str(&files["search.json"]).unwrap(); + for entry in search.as_array().unwrap() { + let (path, anchor) = entry["url"].as_str().unwrap().split_once('#').unwrap(); + assert!(files[path].contains(&format!("id=\"{anchor}\""))); + } } #[tokio::test] @@ -189,3 +176,199 @@ async fn overview_markdown_boundaries() { "# ); } + +macro_rules! assert_render_snapshot { + ($format:expr, $source:expr) => {{ + let source = indoc::indoc!($source); + let docs = documented(source).await; + let files = render(&[docs.module], $format); + insta::with_settings!({description => source, omit_expression => true}, { + let output = files.into_iter() + .filter(|(path, _)| !path.ends_with(".css") && !path.ends_with(".js")) + .map(|(path, content)| format!("--- {path}\n{content}")) + .collect::>().join("\n"); + insta::assert_snapshot!(output); + }); + }}; +} + +#[tokio::test] +async fn markdown_output() { + assert_render_snapshot!( + Format::Markdown, + r#" + module Example exposing (type box(..), identity, (%%)) + {-| # A small API + + @docs box, identity, (%%) + -} + + infix left 5 (%%) = combine + + {-| A little container. -} + type box 'a = Box 'a + + {-| Preserve a value. + + ```nash + identity 42 + ``` + -} + identity x = x + + combine x y = x + "# + ); +} + +#[tokio::test] +async fn html_output() { + assert_render_snapshot!( + Format::Html, + r#" + module Example exposing (identity) + {-| **Public API** with [a link][guide]. + + @docs identity + + [guide]: https://example.com + -} + + {-| Preserve a value. + + ```nash + identity "hello" + ``` + + + + [Unsafe link](javascript:alert%281%29) + -} + identity x = x + "# + ); +} + +#[test] +fn highlight_fragments() { + let source = indoc::indoc!( + r##" + -- A comment + {- Nested {- comment -} λ -} + run : int -> int + run value = if value < 0 then -value else value + bytes = #"00ab" + text = "