From 4b311f85cbc76aee2e920963b5687775b7541f95 Mon Sep 17 00:00:00 2001 From: Yorkin Date: Wed, 13 May 2026 11:25:56 +0800 Subject: [PATCH 1/6] refactor: use vector for pkg parser objects --- pkg_parser/ast.mbt | 8 ++-- pkg_parser/imports.mbt | 3 ++ pkg_parser/moon.pkg | 2 + pkg_parser/parser.mbt | 12 +++--- pkg_parser/pkg.generated.mbti | 5 ++- pkg_parser/pkg_parser_test.mbt | 76 +++++++++++++++++++++++++++++++++- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/pkg_parser/ast.mbt b/pkg_parser/ast.mbt index 7bd55b28..b4fcd80a 100644 --- a/pkg_parser/ast.mbt +++ b/pkg_parser/ast.mbt @@ -6,7 +6,7 @@ pub(all) enum Ast { Str(str~ : String, loc~ : Location) Float(float~ : String, loc~ : Location) Arr(content~ : Array[Ast], loc~ : Location) - Obj(map~ : Map[String, Ast], loc~ : Location) + Obj(fields~ : Vector[(String, Ast)], loc~ : Location) } derive(Debug, Eq) ///| @@ -56,9 +56,9 @@ pub fn Ast::as_array(self : Self) -> Array[Ast]? { } ///| -pub fn Ast::as_object(self : Self) -> Map[String, Ast]? { +pub fn Ast::as_object(self : Self) -> Vector[(String, Ast)]? { match self { - Obj(map~, ..) => Some(map) + Obj(fields~, ..) => Some(fields) _ => None } } @@ -72,6 +72,6 @@ pub impl ToJson for Ast with to_json(self) { Str(str~, loc~) => { "type": "Str", "str": str, "loc": loc } Float(float~, loc~) => { "type": "Float", "float": float, "loc": loc } Arr(content~, loc~) => { "type": "Arr", "content": content, "loc": loc } - Obj(map~, loc~) => { "type": "Obj", "map": map, "loc": loc } + Obj(fields~, loc~) => { "type": "Obj", "fields": fields, "loc": loc } } } diff --git a/pkg_parser/imports.mbt b/pkg_parser/imports.mbt index dd0f9fd6..22550cf6 100644 --- a/pkg_parser/imports.mbt +++ b/pkg_parser/imports.mbt @@ -3,3 +3,6 @@ using @basic {type Location, type Position, type Report} ///| using @tokens {type Token, type TokenKind, type Triple, type Triples} + +///| +using @vector {type Vector} diff --git a/pkg_parser/moon.pkg b/pkg_parser/moon.pkg index 40731568..ab13673a 100644 --- a/pkg_parser/moon.pkg +++ b/pkg_parser/moon.pkg @@ -3,9 +3,11 @@ import { "moonbitlang/parser/tokens", "moonbitlang/parser/lexer", "moonbitlang/x/fs", + "moonbitlang/core/immut/vector", } import { "moonbitlang/core/test", "moonbitlang/core/json", + "moonbitlang/core/debug", } for "test" diff --git a/pkg_parser/parser.mbt b/pkg_parser/parser.mbt index 759c0a2d..6b2728a0 100644 --- a/pkg_parser/parser.mbt +++ b/pkg_parser/parser.mbt @@ -302,7 +302,7 @@ fn parse_map(state : State) -> Ast { return null_here(state) } let entries = parse_comma_separated(state, right=TK_RBRACE, parse_map_entry) - Obj(map=Map::from_array(entries), loc=state.loc_start_with(start)) + Obj(fields=Vector(entries), loc=state.loc_start_with(start)) } ///| @@ -346,10 +346,10 @@ fn parse_apply(state : State) -> (String, Ast) { state.report_expected_here("\"(\"") state.skip_until_statement_end() let loc = Location::{ start: args_start, end: args_start } - return (name, Obj(map=Map([]), loc~)) + return (name, Obj(fields=Vector([]), loc~)) } let args = parse_comma_separated(state, right=TK_RPAREN, parse_argument) - (name, Obj(map=Map::from_array(args), loc=state.loc_start_with(args_start))) + (name, Obj(fields=Vector(args), loc=state.loc_start_with(args_start))) } ///| @@ -424,7 +424,7 @@ fn parse_import_item(state : State) -> Ast { None => Str(str=path, loc=path_loc) Some((alias_name, alias_loc)) => Obj( - map=Map::from_array([ + fields=Vector([ ("path", Str(str=path, loc=path_loc)), ("alias", Str(str=alias_name, loc=alias_loc)), ]), @@ -612,7 +612,7 @@ fn parse_statements(state : State) -> Array[(String, Ast)] { pub fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { if tokens.is_empty() { let loc = Location::{ start: dummy_pos, end: dummy_pos } - return (Obj(map=Map([]), loc~), []) + return (Obj(fields=Vector([]), loc~), []) } let start = tokens[0].1 let state = State::{ @@ -625,7 +625,7 @@ pub fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { state.parsed_position = object_start let statements = parse_statements(state) let ast = Obj( - map=Map::from_array(statements), + fields=Vector(statements), loc=state.loc_start_with(object_start), ) (ast, state.diagnostics) diff --git a/pkg_parser/pkg.generated.mbti b/pkg_parser/pkg.generated.mbti index 0e71b779..a4a5f312 100644 --- a/pkg_parser/pkg.generated.mbti +++ b/pkg_parser/pkg.generated.mbti @@ -3,6 +3,7 @@ package "moonbitlang/parser/pkg_parser" import { "moonbitlang/core/debug", + "moonbitlang/core/immut/vector", "moonbitlang/parser/basic", "moonbitlang/parser/tokens", "moonbitlang/x/fs", @@ -25,12 +26,12 @@ pub(all) enum Ast { Str(str~ : String, loc~ : @basic.Location) Float(float~ : String, loc~ : @basic.Location) Arr(content~ : Array[Ast], loc~ : @basic.Location) - Obj(map~ : Map[String, Ast], loc~ : @basic.Location) + Obj(fields~ : @vector.Vector[(String, Ast)], loc~ : @basic.Location) } derive(Eq, @debug.Debug) pub fn Ast::as_array(Self) -> Array[Self]? pub fn Ast::as_bool(Self) -> Bool? pub fn Ast::as_number(Self) -> String? -pub fn Ast::as_object(Self) -> Map[String, Self]? +pub fn Ast::as_object(Self) -> @vector.Vector[(String, Self)]? pub fn Ast::as_string(Self) -> String? pub fn Ast::loc(Self) -> @basic.Location pub impl ToJson for Ast diff --git a/pkg_parser/pkg_parser_test.mbt b/pkg_parser/pkg_parser_test.mbt index 86bb9dd7..034c3856 100644 --- a/pkg_parser/pkg_parser_test.mbt +++ b/pkg_parser/pkg_parser_test.mbt @@ -13,9 +13,10 @@ fn ast_summary(ast : Ast) -> Json { Float(float~, ..) => Json::object(Map::from_array([("$number", Json::string(float))])) Arr(content~, ..) => Json::array(content.map(ast_summary)) - Obj(map~, ..) => { + Obj(fields~, ..) => { let object : Map[String, Json] = Map([]) - for key, value in map { + for field in fields { + let (key, value) = field object[key] = ast_summary(value) } Json::object(object) @@ -224,6 +225,77 @@ test "pkg_parser accepts bigint literal in pkg values" { json_inspect(ast_summary(ast), content={ "big": { "$number": "1N" } }) } +///| +test "pkg_parser preserves duplicate object and statement entries" { + fn duplicate_entry_summary(ast : Ast) -> Array[(String, String?)] raise { + match ast { + Obj(fields=root_fields, ..) => { + let (_, options_value) = root_fields[0] + match options_value { + Obj(fields=options_fields, ..) => { + let (_, dup_value) = options_fields[0] + match dup_value { + Obj(fields=dup_fields, ..) => + dup_fields + .to_array() + .map(field => { + let (key, value) = field + (key, value.as_number()) + }) + _ => fail("expected duplicate entry value to be an object") + } + } + _ => fail("expected options argument list to be an object") + } + } + _ => fail("expected root to be an object") + } + } + + fn root_entry_summary(ast : Ast) -> Array[(String, String?)] raise { + match ast { + Obj(fields=root_fields, ..) => + root_fields + .to_array() + .map(field => { + let (key, value) = field + let value_name = match value { + Obj(fields~, ..) => + match fields[0] { + ("name", Str(str~, ..)) => Some(str) + _ => None + } + _ => None + } + (key, value_name) + }) + _ => fail("expected root to be an object") + } + } + + let source = + #|options(dup: { "a": 1, "a": 2 }) + #|rule(name:"rule1", command:"exe") + #|rule(name:"rule2", command:"exe") + let (ast, reports) = parse_pkg(source) + json_inspect(report_checks(reports, []), content={ + "count": 0, + "contains": [], + }) + debug_inspect( + duplicate_entry_summary(ast), + content=( + #|[("a", Some("1")), ("a", Some("2"))] + ), + ) + debug_inspect( + root_entry_summary(ast), + content=( + #|[("options", None), ("rule", Some("rule1")), ("rule", Some("rule2"))] + ), + ) +} + ///| test "pkg_parser recovers map value after missing colon" { let source = From 3e3fffc9b2c717d808d39678849d9de8db4cae70 Mon Sep 17 00:00:00 2001 From: Yorkin Date: Wed, 13 May 2026 11:44:08 +0800 Subject: [PATCH 2/6] refactor: simplify pkg parser ast --- pkg_parser/ast.mbt | 52 ++++++++++++++++------------------ pkg_parser/parser.mbt | 47 ++++++++++++++---------------- pkg_parser/pkg.generated.mbti | 15 +++++----- pkg_parser/pkg_parser_test.mbt | 23 +++++++-------- 4 files changed, 64 insertions(+), 73 deletions(-) diff --git a/pkg_parser/ast.mbt b/pkg_parser/ast.mbt index b4fcd80a..8432de70 100644 --- a/pkg_parser/ast.mbt +++ b/pkg_parser/ast.mbt @@ -1,32 +1,29 @@ ///| pub(all) enum Ast { - Null(Location) - False(Location) - True(Location) - Str(str~ : String, loc~ : Location) - Float(float~ : String, loc~ : Location) - Arr(content~ : Array[Ast], loc~ : Location) - Obj(fields~ : Vector[(String, Ast)], loc~ : Location) + Null(loc~ : Location) + Bool(Bool, loc~ : Location) + Str(String, loc~ : Location) + Float(String, loc~ : Location) + Arr(Vector[Ast], loc~ : Location) + Obj(Vector[(String, Ast)], loc~ : Location) } derive(Debug, Eq) ///| pub fn Ast::loc(self : Self) -> Location { match self { - Null(loc) => loc - False(loc) => loc - True(loc) => loc - Str(loc~, ..) => loc - Float(loc~, ..) => loc - Arr(loc~, ..) => loc - Obj(loc~, ..) => loc + Null(loc~) => loc + Bool(_, loc~) => loc + Str(_, loc~) => loc + Float(_, loc~) => loc + Arr(_, loc~) => loc + Obj(_, loc~) => loc } } ///| pub fn Ast::as_bool(self : Self) -> Bool? { match self { - True(_) => Some(true) - False(_) => Some(false) + Bool(value, ..) => Some(value) _ => None } } @@ -34,7 +31,7 @@ pub fn Ast::as_bool(self : Self) -> Bool? { ///| pub fn Ast::as_string(self : Self) -> String? { match self { - Str(str~, ..) => Some(str) + Str(str, ..) => Some(str) _ => None } } @@ -42,15 +39,15 @@ pub fn Ast::as_string(self : Self) -> String? { ///| pub fn Ast::as_number(self : Self) -> String? { match self { - Float(float~, ..) => Some(float) + Float(float, ..) => Some(float) _ => None } } ///| -pub fn Ast::as_array(self : Self) -> Array[Ast]? { +pub fn Ast::as_array(self : Self) -> Vector[Ast]? { match self { - Arr(content~, ..) => Some(content) + Arr(content, ..) => Some(content) _ => None } } @@ -58,7 +55,7 @@ pub fn Ast::as_array(self : Self) -> Array[Ast]? { ///| pub fn Ast::as_object(self : Self) -> Vector[(String, Ast)]? { match self { - Obj(fields~, ..) => Some(fields) + Obj(fields, ..) => Some(fields) _ => None } } @@ -66,12 +63,11 @@ pub fn Ast::as_object(self : Self) -> Vector[(String, Ast)]? { ///| pub impl ToJson for Ast with to_json(self) { match self { - Null(loc) => { "type": "Null", "loc": loc } - False(loc) => { "type": "False", "loc": loc } - True(loc) => { "type": "True", "loc": loc } - Str(str~, loc~) => { "type": "Str", "str": str, "loc": loc } - Float(float~, loc~) => { "type": "Float", "float": float, "loc": loc } - Arr(content~, loc~) => { "type": "Arr", "content": content, "loc": loc } - Obj(fields~, loc~) => { "type": "Obj", "fields": fields, "loc": loc } + Null(loc~) => { "type": "Null", "loc": loc } + Bool(value, loc~) => { "type": "Bool", "value": value, "loc": loc } + Str(str, loc~) => { "type": "Str", "str": str, "loc": loc } + Float(float, loc~) => { "type": "Float", "float": float, "loc": loc } + Arr(content, loc~) => { "type": "Arr", "content": content, "loc": loc } + Obj(fields, loc~) => { "type": "Obj", "fields": fields, "loc": loc } } } diff --git a/pkg_parser/parser.mbt b/pkg_parser/parser.mbt index 6b2728a0..d04b418c 100644 --- a/pkg_parser/parser.mbt +++ b/pkg_parser/parser.mbt @@ -177,7 +177,7 @@ fn import_kind_to_key(kind : ImportKind) -> String { ///| fn null_here(state : State) -> Ast { - Null(state.peek_location()) + Null(loc=state.peek_location()) } ///| @@ -262,7 +262,7 @@ fn parse_array(state : State) -> Ast { return null_here(state) } let content = parse_comma_separated(state, right=TK_RBRACKET, parse_expr) - Arr(content~, loc=state.loc_start_with(start)) + Arr(Vector(content), loc=state.loc_start_with(start)) } ///| @@ -302,7 +302,7 @@ fn parse_map(state : State) -> Ast { return null_here(state) } let entries = parse_comma_separated(state, right=TK_RBRACE, parse_map_entry) - Obj(fields=Vector(entries), loc=state.loc_start_with(start)) + Obj(Vector(entries), loc=state.loc_start_with(start)) } ///| @@ -346,10 +346,10 @@ fn parse_apply(state : State) -> (String, Ast) { state.report_expected_here("\"(\"") state.skip_until_statement_end() let loc = Location::{ start: args_start, end: args_start } - return (name, Obj(fields=Vector([]), loc~)) + return (name, Obj(Vector([]), loc~)) } let args = parse_comma_separated(state, right=TK_RPAREN, parse_argument) - (name, Obj(fields=Vector(args), loc=state.loc_start_with(args_start))) + (name, Obj(Vector(args), loc=state.loc_start_with(args_start))) } ///| @@ -373,7 +373,7 @@ fn parse_assign(state : State) -> (String, Ast) { if is_statement_start_after_newline(state) { let loc = state.peek_location() state.report_failed_to_parse(state.peek_token(), "expression", loc) - return (name, Null(loc)) + return (name, Null(loc~)) } (name, parse_expr(state)) } @@ -421,12 +421,12 @@ fn parse_import_item(state : State) -> Ast { _ => None } match package_alias { - None => Str(str=path, loc=path_loc) + None => Str(path, loc=path_loc) Some((alias_name, alias_loc)) => Obj( - fields=Vector([ - ("path", Str(str=path, loc=path_loc)), - ("alias", Str(str=alias_name, loc=alias_loc)), + Vector([ + ("path", Str(path, loc=path_loc)), + ("alias", Str(alias_name, loc=alias_loc)), ]), loc=path_loc, ) @@ -482,7 +482,7 @@ fn parse_import_statement(state : State) -> (String, Ast) { state.skip_until_statement_end() return ( import_kind_to_key(kind), - Arr(content=[], loc=state.loc_start_with(start)), + Arr(Vector([]), loc=state.loc_start_with(start)), ) } let packages = parse_comma_separated( @@ -495,7 +495,7 @@ fn parse_import_statement(state : State) -> (String, Ast) { } ( import_kind_to_key(kind), - Arr(content=packages, loc=state.loc_start_with(start)), + Arr(Vector(packages), loc=state.loc_start_with(start)), ) } @@ -505,17 +505,17 @@ fn parse_expr(state : State) -> Ast { TRUE => { let loc = state.peek_location() state.skip() - True(loc) + Bool(true, loc~) } FALSE => { let loc = state.peek_location() state.skip() - False(loc) + Bool(false, loc~) } STRING(str) => { let loc = state.peek_location() state.skip() - Str(str~, loc~) + Str(str, loc~) } INT(integer) as token => { let loc = state.peek_location() @@ -524,16 +524,16 @@ fn parse_expr(state : State) -> Ast { state.report_failed_to_parse( token, "integer literals without U or L suffixes", loc, ) - Null(loc) + Null(loc~) } else { - Float(float=integer, loc~) + Float(integer, loc~) } } FLOAT(_) | DOUBLE(_) as token => { let loc = state.peek_location() state.skip() state.report_failed_to_parse(token, "expression", loc) - Null(loc) + Null(loc~) } LBRACE => parse_map(state) LBRACKET => parse_array(state) @@ -543,7 +543,7 @@ fn parse_expr(state : State) -> Ast { if other.kind() != TK_EOF && other.kind() != TK_SEMI { state.skip() } - Null(loc) + Null(loc~) } } } @@ -560,7 +560,7 @@ fn parse_statement(state : State) -> (String, Ast) { if other.kind() != TK_EOF { state.skip() } - ("", Null(loc)) + ("", Null(loc~)) } } } @@ -612,7 +612,7 @@ fn parse_statements(state : State) -> Array[(String, Ast)] { pub fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { if tokens.is_empty() { let loc = Location::{ start: dummy_pos, end: dummy_pos } - return (Obj(fields=Vector([]), loc~), []) + return (Obj(Vector([]), loc~), []) } let start = tokens[0].1 let state = State::{ @@ -624,10 +624,7 @@ pub fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { let object_start = state.peek_spos() state.parsed_position = object_start let statements = parse_statements(state) - let ast = Obj( - fields=Vector(statements), - loc=state.loc_start_with(object_start), - ) + let ast = Obj(Vector(statements), loc=state.loc_start_with(object_start)) (ast, state.diagnostics) } diff --git a/pkg_parser/pkg.generated.mbti b/pkg_parser/pkg.generated.mbti index a4a5f312..0ecbc53f 100644 --- a/pkg_parser/pkg.generated.mbti +++ b/pkg_parser/pkg.generated.mbti @@ -20,15 +20,14 @@ pub fn parse_string(String, name? : String) -> (Ast, Array[@basic.Report]) // Types and methods pub(all) enum Ast { - Null(@basic.Location) - False(@basic.Location) - True(@basic.Location) - Str(str~ : String, loc~ : @basic.Location) - Float(float~ : String, loc~ : @basic.Location) - Arr(content~ : Array[Ast], loc~ : @basic.Location) - Obj(fields~ : @vector.Vector[(String, Ast)], loc~ : @basic.Location) + Null(loc~ : @basic.Location) + Bool(Bool, loc~ : @basic.Location) + Str(String, loc~ : @basic.Location) + Float(String, loc~ : @basic.Location) + Arr(@vector.Vector[Ast], loc~ : @basic.Location) + Obj(@vector.Vector[(String, Ast)], loc~ : @basic.Location) } derive(Eq, @debug.Debug) -pub fn Ast::as_array(Self) -> Array[Self]? +pub fn Ast::as_array(Self) -> @vector.Vector[Self]? pub fn Ast::as_bool(Self) -> Bool? pub fn Ast::as_number(Self) -> String? pub fn Ast::as_object(Self) -> @vector.Vector[(String, Self)]? diff --git a/pkg_parser/pkg_parser_test.mbt b/pkg_parser/pkg_parser_test.mbt index 034c3856..3673b63d 100644 --- a/pkg_parser/pkg_parser_test.mbt +++ b/pkg_parser/pkg_parser_test.mbt @@ -7,13 +7,12 @@ fn parse_pkg(source : String) -> (Ast, Array[@basic.Report]) { fn ast_summary(ast : Ast) -> Json { match ast { Null(_) => Json::null() - False(_) => Json::boolean(false) - True(_) => Json::boolean(true) - Str(str~, ..) => Json::string(str) - Float(float~, ..) => + Bool(value, ..) => Json::boolean(value) + Str(str, ..) => Json::string(str) + Float(float, ..) => Json::object(Map::from_array([("$number", Json::string(float))])) - Arr(content~, ..) => Json::array(content.map(ast_summary)) - Obj(fields~, ..) => { + Arr(content, ..) => Json::array(content.to_array().map(ast_summary)) + Obj(fields, ..) => { let object : Map[String, Json] = Map([]) for field in fields { let (key, value) = field @@ -229,13 +228,13 @@ test "pkg_parser accepts bigint literal in pkg values" { test "pkg_parser preserves duplicate object and statement entries" { fn duplicate_entry_summary(ast : Ast) -> Array[(String, String?)] raise { match ast { - Obj(fields=root_fields, ..) => { + Obj(root_fields, ..) => { let (_, options_value) = root_fields[0] match options_value { - Obj(fields=options_fields, ..) => { + Obj(options_fields, ..) => { let (_, dup_value) = options_fields[0] match dup_value { - Obj(fields=dup_fields, ..) => + Obj(dup_fields, ..) => dup_fields .to_array() .map(field => { @@ -254,15 +253,15 @@ test "pkg_parser preserves duplicate object and statement entries" { fn root_entry_summary(ast : Ast) -> Array[(String, String?)] raise { match ast { - Obj(fields=root_fields, ..) => + Obj(root_fields, ..) => root_fields .to_array() .map(field => { let (key, value) = field let value_name = match value { - Obj(fields~, ..) => + Obj(fields, ..) => match fields[0] { - ("name", Str(str~, ..)) => Some(str) + ("name", Str(str, ..)) => Some(str) _ => None } _ => None From 78f7fa415bd6e9dcc43ef4eedc88d60d76169608 Mon Sep 17 00:00:00 2001 From: Yorkin Date: Wed, 13 May 2026 14:32:55 +0800 Subject: [PATCH 3/6] refactor: rename pkg parser to moon config --- {pkg_parser => moon_config}/ast.mbt | 0 {pkg_parser => moon_config}/imports.mbt | 0 {pkg_parser => moon_config}/moon.pkg | 0 .../moon_config_test.mbt | 42 +++++++++---------- {pkg_parser => moon_config}/parser.mbt | 0 .../pkg.generated.mbti | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) rename {pkg_parser => moon_config}/ast.mbt (100%) rename {pkg_parser => moon_config}/imports.mbt (100%) rename {pkg_parser => moon_config}/moon.pkg (100%) rename pkg_parser/pkg_parser_test.mbt => moon_config/moon_config_test.mbt (87%) rename {pkg_parser => moon_config}/parser.mbt (100%) rename {pkg_parser => moon_config}/pkg.generated.mbti (96%) diff --git a/pkg_parser/ast.mbt b/moon_config/ast.mbt similarity index 100% rename from pkg_parser/ast.mbt rename to moon_config/ast.mbt diff --git a/pkg_parser/imports.mbt b/moon_config/imports.mbt similarity index 100% rename from pkg_parser/imports.mbt rename to moon_config/imports.mbt diff --git a/pkg_parser/moon.pkg b/moon_config/moon.pkg similarity index 100% rename from pkg_parser/moon.pkg rename to moon_config/moon.pkg diff --git a/pkg_parser/pkg_parser_test.mbt b/moon_config/moon_config_test.mbt similarity index 87% rename from pkg_parser/pkg_parser_test.mbt rename to moon_config/moon_config_test.mbt index 3673b63d..67df4e63 100644 --- a/pkg_parser/pkg_parser_test.mbt +++ b/moon_config/moon_config_test.mbt @@ -52,7 +52,7 @@ fn report_checks( } ///| -test "pkg_parser parse nested options" { +test "moon_config parse nested options" { let source = #|options( #| nested: { "a": ["x", "y", 123], "b": { "c": false } } @@ -71,7 +71,7 @@ test "pkg_parser parse nested options" { } ///| -test "pkg_parser parse import for wbtest" { +test "moon_config parse import for wbtest" { let source = #|import { "a/pkg", "b/pkg" } for "wbtest" #| @@ -84,7 +84,7 @@ test "pkg_parser parse import for wbtest" { } ///| -test "pkg_parser rejects legacy import kind syntax" { +test "moon_config rejects legacy import kind syntax" { let source = #|import "test" { "path/to/pkg", "p" } #| @@ -97,7 +97,7 @@ test "pkg_parser rejects legacy import kind syntax" { } ///| -test "pkg_parser rejects legacy import alias syntax" { +test "moon_config rejects legacy import alias syntax" { let source = #|import { "a" as @alias, "b" @ok } #| @@ -112,7 +112,7 @@ test "pkg_parser rejects legacy import alias syntax" { } ///| -test "pkg_parser avoids cascading after invalid import kind" { +test "moon_config avoids cascading after invalid import kind" { let source = #|import { "a" } for next = true #| @@ -127,7 +127,7 @@ test "pkg_parser avoids cascading after invalid import kind" { } ///| -test "pkg_parser reports positional argument" { +test "moon_config reports positional argument" { let source = #|apply("positional") #| @@ -140,7 +140,7 @@ test "pkg_parser reports positional argument" { } ///| -test "pkg_parser parse assignments" { +test "moon_config parse assignments" { let source = #|warnings = "+w1-w1" #|supported_targets = "+wasm" @@ -157,7 +157,7 @@ test "pkg_parser parse assignments" { } ///| -test "pkg_parser requires statement separators" { +test "moon_config requires statement separators" { let source = #|a = "x" b = "y" #| @@ -170,7 +170,7 @@ test "pkg_parser requires statement separators" { } ///| -test "pkg_parser keeps next statement after missing assignment value before semicolon" { +test "moon_config keeps next statement after missing assignment value before semicolon" { let source = #|a = ; #|b = 1 @@ -184,7 +184,7 @@ test "pkg_parser keeps next statement after missing assignment value before semi } ///| -test "pkg_parser keeps next statement after missing assignment value on next line" { +test "moon_config keeps next statement after missing assignment value on next line" { let source = #|a = #|b = 1 @@ -198,7 +198,7 @@ test "pkg_parser keeps next statement after missing assignment value on next lin } ///| -test "pkg_parser rejects unsupported numeric literals" { +test "moon_config rejects unsupported numeric literals" { let source = #|unsigned = 1U #|floaty = 1.5 @@ -212,7 +212,7 @@ test "pkg_parser rejects unsupported numeric literals" { } ///| -test "pkg_parser accepts bigint literal in pkg values" { +test "moon_config accepts bigint literal in pkg values" { let source = #|big = 1N #| @@ -225,7 +225,7 @@ test "pkg_parser accepts bigint literal in pkg values" { } ///| -test "pkg_parser preserves duplicate object and statement entries" { +test "moon_config preserves duplicate object and statement entries" { fn duplicate_entry_summary(ast : Ast) -> Array[(String, String?)] raise { match ast { Obj(root_fields, ..) => { @@ -296,7 +296,7 @@ test "pkg_parser preserves duplicate object and statement entries" { } ///| -test "pkg_parser recovers map value after missing colon" { +test "moon_config recovers map value after missing colon" { let source = #|options(nested: { "a" 1 }) #| @@ -311,7 +311,7 @@ test "pkg_parser recovers map value after missing colon" { } ///| -test "pkg_parser keeps next statement after missing array right bracket" { +test "moon_config keeps next statement after missing array right bracket" { let source = #|a = [1 #|b = 2 @@ -328,7 +328,7 @@ test "pkg_parser keeps next statement after missing array right bracket" { } ///| -test "pkg_parser reports missing import right brace at eof" { +test "moon_config reports missing import right brace at eof" { let source = #|import { #| @@ -341,7 +341,7 @@ test "pkg_parser reports missing import right brace at eof" { } ///| -test "pkg_parser reports missing apply right paren at eof" { +test "moon_config reports missing apply right paren at eof" { let source = #|options( #| @@ -354,7 +354,7 @@ test "pkg_parser reports missing apply right paren at eof" { } ///| -test "pkg_parser reports missing array right bracket at eof" { +test "moon_config reports missing array right bracket at eof" { let source = #|a = [ #| @@ -367,7 +367,7 @@ test "pkg_parser reports missing array right bracket at eof" { } ///| -test "pkg_parser keeps next statement after missing apply left paren" { +test "moon_config keeps next statement after missing apply left paren" { let source = #|options nested: true #|next = false @@ -381,7 +381,7 @@ test "pkg_parser keeps next statement after missing apply left paren" { } ///| -test "pkg_parser keeps next statement after missing import left brace" { +test "moon_config keeps next statement after missing import left brace" { let source = #|import [ "a/pkg" ] #|next = false @@ -395,7 +395,7 @@ test "pkg_parser keeps next statement after missing import left brace" { } ///| -test "pkg_parser root loc stays ordered for layout-only source" { +test "moon_config root loc stays ordered for layout-only source" { let source = #| #| diff --git a/pkg_parser/parser.mbt b/moon_config/parser.mbt similarity index 100% rename from pkg_parser/parser.mbt rename to moon_config/parser.mbt diff --git a/pkg_parser/pkg.generated.mbti b/moon_config/pkg.generated.mbti similarity index 96% rename from pkg_parser/pkg.generated.mbti rename to moon_config/pkg.generated.mbti index 0ecbc53f..b23ac671 100644 --- a/pkg_parser/pkg.generated.mbti +++ b/moon_config/pkg.generated.mbti @@ -1,5 +1,5 @@ // Generated using `moon info`, DON'T EDIT IT -package "moonbitlang/parser/pkg_parser" +package "moonbitlang/parser/moon_config" import { "moonbitlang/core/debug", From 9254a54a2490b14638f5b25614add04cac00f9c9 Mon Sep 17 00:00:00 2001 From: Yorkin Date: Thu, 14 May 2026 11:30:31 +0800 Subject: [PATCH 4/6] refactor: add moon config post processing --- moon_config/api.mbt | 26 ++ moon_config/ast.mbt | 52 --- moon_config/legacy_json.mbt | 46 ++ moon_config/legacy_json_wbtest.mbt | 331 ++++++++++++++ moon_config/moon.pkg | 9 +- moon_config/moon_config_test.mbt | 701 ++++++++++++++-------------- moon_config/moon_config_wbtest.mbt | 702 +++++++++++++++++++++++++++++ moon_config/parser.mbt | 11 +- moon_config/pkg.generated.mbti | 13 +- moon_config/post_process.mbt | 205 +++++++++ 10 files changed, 1677 insertions(+), 419 deletions(-) create mode 100644 moon_config/api.mbt create mode 100644 moon_config/legacy_json.mbt create mode 100644 moon_config/legacy_json_wbtest.mbt create mode 100644 moon_config/moon_config_wbtest.mbt create mode 100644 moon_config/post_process.mbt diff --git a/moon_config/api.mbt b/moon_config/api.mbt new file mode 100644 index 00000000..70be7bfb --- /dev/null +++ b/moon_config/api.mbt @@ -0,0 +1,26 @@ +///| +pub fn parse_moon_pkg( + name? : String = "moon.pkg", + source : String, +) -> (Ast, Array[Report]) { + let (ast, diagnostics) = parse_string(source, name~) + (post_process_moon_pkg(ast, diagnostics), diagnostics) +} + +///| +pub fn parse_moon_mod( + name? : String = "moon.mod", + source : String, +) -> (Ast, Array[Report]) { + let (ast, diagnostics) = parse_string(source, name~) + (post_process_moon_mod(ast, diagnostics), diagnostics) +} + +///| +pub fn parse_moon_work( + name? : String = "moon.work", + source : String, +) -> (Ast, Array[Report]) { + let (ast, diagnostics) = parse_string(source, name~) + (post_process_moon_work(ast, diagnostics), diagnostics) +} diff --git a/moon_config/ast.mbt b/moon_config/ast.mbt index 8432de70..733da1ca 100644 --- a/moon_config/ast.mbt +++ b/moon_config/ast.mbt @@ -19,55 +19,3 @@ pub fn Ast::loc(self : Self) -> Location { Obj(_, loc~) => loc } } - -///| -pub fn Ast::as_bool(self : Self) -> Bool? { - match self { - Bool(value, ..) => Some(value) - _ => None - } -} - -///| -pub fn Ast::as_string(self : Self) -> String? { - match self { - Str(str, ..) => Some(str) - _ => None - } -} - -///| -pub fn Ast::as_number(self : Self) -> String? { - match self { - Float(float, ..) => Some(float) - _ => None - } -} - -///| -pub fn Ast::as_array(self : Self) -> Vector[Ast]? { - match self { - Arr(content, ..) => Some(content) - _ => None - } -} - -///| -pub fn Ast::as_object(self : Self) -> Vector[(String, Ast)]? { - match self { - Obj(fields, ..) => Some(fields) - _ => None - } -} - -///| -pub impl ToJson for Ast with to_json(self) { - match self { - Null(loc~) => { "type": "Null", "loc": loc } - Bool(value, loc~) => { "type": "Bool", "value": value, "loc": loc } - Str(str, loc~) => { "type": "Str", "str": str, "loc": loc } - Float(float, loc~) => { "type": "Float", "float": float, "loc": loc } - Arr(content, loc~) => { "type": "Arr", "content": content, "loc": loc } - Obj(fields, loc~) => { "type": "Obj", "fields": fields, "loc": loc } - } -} diff --git a/moon_config/legacy_json.mbt b/moon_config/legacy_json.mbt new file mode 100644 index 00000000..7a66cd35 --- /dev/null +++ b/moon_config/legacy_json.mbt @@ -0,0 +1,46 @@ +///| +pub impl ToJson for Ast with to_json(self) { + fn convert(ast : Ast, fold_duplicates : Bool) -> Json { + match ast { + Null(_) => Json::null() + Bool(value, ..) => Json::boolean(value) + Str(str, ..) => Json::string(str) + Float(float, ..) => Json::number(0.0, repr=float) + Arr(content, ..) => + Json::array(content.to_array().map(item => convert(item, false))) + Obj(fields, ..) => { + if fold_duplicates { + let groups : Map[String, Array[Json]] = {} + for field in fields { + let (key, value) = field + let item = convert(value, false) + match groups.get(key) { + Some(items) => { + items.push(item) + groups[key] = items + } + None => groups[key] = [item] + } + } + let object : Map[String, Json] = {} + groups.each(fn(key, items) { + object[key] = if items.length() == 1 { + items[0] + } else { + Json::array(items) + } + }) + return Json::object(object) + } + let object : Map[String, Json] = Map([]) + for field in fields { + let (key, value) = field + object[key] = convert(value, false) + } + Json::object(object) + } + } + } + + convert(self, true) +} diff --git a/moon_config/legacy_json_wbtest.mbt b/moon_config/legacy_json_wbtest.mbt new file mode 100644 index 00000000..3a3d2190 --- /dev/null +++ b/moon_config/legacy_json_wbtest.mbt @@ -0,0 +1,331 @@ +///| +fn legacy_loc() -> @basic.Location { + let pos = @basic.Position::{ fname: "", lnum: 0, bol: 0, cnum: 0 } + @basic.Location::{ start: pos, end: pos } +} + +///| +fn legacy_ast_null(loc : @basic.Location) -> Ast { + Null(loc~) +} + +///| +fn legacy_bool(value : Bool, loc : @basic.Location) -> Ast { + Bool(value, loc~) +} + +///| +fn legacy_str(value : String, loc : @basic.Location) -> Ast { + Str(value, loc~) +} + +///| +fn legacy_float(value : String, loc : @basic.Location) -> Ast { + Float(value, loc~) +} + +///| +fn legacy_arr(items : Array[Ast], loc : @basic.Location) -> Ast { + Arr(Vector(items), loc~) +} + +///| +fn legacy_obj(fields : Array[(String, Ast)], loc : @basic.Location) -> Ast { + Obj(Vector(fields), loc~) +} + +///| +fn legacy_parse_pkg(source : String) -> Ast { + let (ast, _) = parse_moon_pkg(source) + ast +} + +///| +fn legacy_parse_mod(source : String) -> Ast { + let (ast, _) = parse_moon_mod(source) + ast +} + +///| +fn legacy_parse_work(source : String) -> Ast { + let (ast, _) = parse_moon_work(source) + ast +} + +///| +test "legacy JSON converts Ast values" { + let loc = legacy_loc() + json_inspect( + legacy_obj( + [ + ("null", legacy_ast_null(loc)), + ("bool", legacy_bool(true, loc)), + ("str", legacy_str("text", loc)), + ("float", legacy_float("1.5", loc)), + ( + "array", + legacy_arr([legacy_float("1", loc), legacy_str("x", loc)], loc), + ), + ("object", legacy_obj([("nested", legacy_bool(false, loc))], loc)), + ], + loc, + ).to_json(), + content={ + "null": null, + "bool": true, + "str": "text", + "float": 1.5, + "array": [1, "x"], + "object": { "nested": false }, + }, + ) +} + +///| +test "legacy JSON folds repeated top-level entries only" { + let loc = legacy_loc() + let ast = legacy_obj( + [ + ( + "rule", + legacy_obj( + [ + ("name", legacy_str("rule1", loc)), + ("command", legacy_str("exe", loc)), + ( + "env", + legacy_obj( + [ + ("k", legacy_str("first", loc)), + ("k", legacy_str("second", loc)), + ], + loc, + ), + ), + ], + loc, + ), + ), + ( + "rule", + legacy_obj( + [ + ("name", legacy_str("rule2", loc)), + ("command", legacy_str("exe", loc)), + ], + loc, + ), + ), + ], + loc, + ) + json_inspect(ast.to_json(), content={ + "rule": [ + { "name": "rule1", "command": "exe", "env": { "k": "second" } }, + { "name": "rule2", "command": "exe" }, + ], + }) +} + +///| +test "legacy JSON preserves numeric repr" { + let loc = legacy_loc() + json_inspect( + legacy_obj([("big", legacy_float("1N", loc))], loc).to_json(), + content=Json::object( + Map::from_array([("big", Json::number(0.0, repr="1N"))]), + ), + ) +} + +///| +test "legacy JSON postprocesses moon.pkg output" { + let source = + #|warnings = "+w1" + #|supported_targets = "+wasm" + #|options( + #| "is-main": true, + #|) + #|rule(name: "rule1", command: "exe") + #|rule(name: "rule2", command: "exe") + #|dev_build(rule: "rule1", input: "a", output: "b") + #|dev_build(rule: "rule2", input: "c", output: "d") + #| + json_inspect(legacy_parse_pkg(source).to_json(), content={ + "warn-list": "+w1", + "supported-targets": "+wasm", + "is-main": true, + "rule": [ + { "name": "rule1", "command": "exe" }, + { "name": "rule2", "command": "exe" }, + ], + "dev_build": [ + { "rule": "rule1", "input": "a", "output": "b" }, + { "rule": "rule2", "input": "c", "output": "d" }, + ], + }) +} + +///| +test "legacy JSON handles package imports aliases and targets" { + let source = + #|import { + #| "moonbit-community/fullstack-one-project-doc/shared" @shared, + #| "moonbitlang/core/json" @json, + #| "moonbitlang/async", + #|} + #| + #|import { + #| "moonbitlang/core/test", + #|} for "test" + #| + #|warnings = "-test_unqualified_package" + #|supported_targets = "native" + #| + #|options( + #| "is-main": true, + #| targets: { + #| "main.mbt": [ "native" ], + #| "browser.mbt": [ "js" ], + #| }, + #|) + #| + json_inspect(legacy_parse_pkg(source).to_json(), content={ + "import": [ + { + "path": "moonbit-community/fullstack-one-project-doc/shared", + "alias": "shared", + }, + { "path": "moonbitlang/core/json", "alias": "json" }, + "moonbitlang/async", + ], + "test-import": ["moonbitlang/core/test"], + "warn-list": "-test_unqualified_package", + "supported-targets": "native", + "is-main": true, + "targets": { "main.mbt": ["native"], "browser.mbt": ["js"] }, + }) +} + +///| +test "legacy JSON handles link options" { + let source = + #|options( + #| link: { + #| "wasm-gc": { + #| "use-js-builtin-string": true, + #| "import-memory": { "module": "env", "name": "memory" }, + #| "memory-limits": { "min": 1, "max": 4 }, + #| "shared-memory": true, + #| }, + #| }, + #|) + #| + json_inspect(legacy_parse_pkg(source).to_json(), content={ + "link": { + "wasm-gc": { + "use-js-builtin-string": true, + "import-memory": { "module": "env", "name": "memory" }, + "memory-limits": { "min": 1, "max": 4 }, + "shared-memory": true, + }, + }, + }) +} + +///| +test "legacy JSON postprocesses moon.mod output" { + let source = + #|name = "user/mod" + #|version = "0.1.0" + #|import { "dep/a@1.2.3", "dep/b" } + #|warnings = "+w1" + #|supported_targets = "+wasm" + #|options( + #| license: "MIT", + #| repository: "https://example.com/repo", + #|) + #|rule(name: "rule1", command: "exe") + #|rule(name: "rule2", command: "exe") + #| + json_inspect(legacy_parse_mod(source).to_json(), content={ + "name": "user/mod", + "version": "0.1.0", + "deps": { "dep/a": "1.2.3", "dep/b": "" }, + "warn-list": "+w1", + "supported-targets": "+wasm", + "license": "MIT", + "repository": "https://example.com/repo", + "rule": [ + { "name": "rule1", "command": "exe" }, + { "name": "rule2", "command": "exe" }, + ], + }) +} + +///| +test "legacy JSON handles moon.mod metadata options" { + let source = + #|name = "example/dsl_only" + #|version = "0.1.0" + #|import { "moonbitlang/x@0.4.6", "../dep" } + #|options( + #| "bin-deps": { + #| "tool/cli": { "bin-pkg": [ "cli" ], "path": "../bin" }, + #| }, + #| "compile-flags": [ "-DDEBUG", "-Wall" ], + #| description: "A DSL fixture module", + #| exclude: [ "target/**", "**/*.tmp" ], + #| include: [ "src/**", "README.mbt.md" ], + #| keywords: [ "dsl", "fixture" ], + #| license: "Apache-2.0", + #| "link-flags": [ "-lm" ], + #| "preferred-target": "wasm-gc", + #| readme: "README.mbt.md", + #| repository: "https://example.com/repo", + #| scripts: { "prebuild": "node ./prebuild.js", "postbuild": "echo done" }, + #| source: "src", + #|) + #| + json_inspect(legacy_parse_mod(source).to_json(), content={ + "name": "example/dsl_only", + "version": "0.1.0", + "deps": { "moonbitlang/x": "0.4.6", "../dep": "" }, + "bin-deps": { "tool/cli": { "bin-pkg": ["cli"], "path": "../bin" } }, + "compile-flags": ["-DDEBUG", "-Wall"], + "description": "A DSL fixture module", + "exclude": ["target/**", "**/*.tmp"], + "include": ["src/**", "README.mbt.md"], + "keywords": ["dsl", "fixture"], + "license": "Apache-2.0", + "link-flags": ["-lm"], + "preferred-target": "wasm-gc", + "readme": "README.mbt.md", + "repository": "https://example.com/repo", + "scripts": { "prebuild": "node ./prebuild.js", "postbuild": "echo done" }, + "source": "src", + }) +} + +///| +test "legacy JSON parses moon.work output" { + let source = + #|members = ["./app", "./shared"] + #|preferred_target = "wasm-gc" + #| + json_inspect(legacy_parse_work(source).to_json(), content={ + "members": ["./app", "./shared"], + "preferred_target": "wasm-gc", + }) +} + +///| +test "legacy JSON folds duplicate moon.mod keys" { + let source = + #|name = "user/first" + #|name = "user/second" + #| + json_inspect(legacy_parse_mod(source).to_json(), content={ + "name": ["user/first", "user/second"], + }) +} diff --git a/moon_config/moon.pkg b/moon_config/moon.pkg index ab13673a..b915c15c 100644 --- a/moon_config/moon.pkg +++ b/moon_config/moon.pkg @@ -2,12 +2,17 @@ import { "moonbitlang/parser/basic", "moonbitlang/parser/tokens", "moonbitlang/parser/lexer", - "moonbitlang/x/fs", "moonbitlang/core/immut/vector", + "moonbitlang/core/json", } import { "moonbitlang/core/test", - "moonbitlang/core/json", "moonbitlang/core/debug", } for "test" + +import { + "moonbitlang/core/test", + "moonbitlang/core/debug", + "moonbitlang/core/json", +} for "wbtest" diff --git a/moon_config/moon_config_test.mbt b/moon_config/moon_config_test.mbt index 67df4e63..0066bbd5 100644 --- a/moon_config/moon_config_test.mbt +++ b/moon_config/moon_config_test.mbt @@ -1,418 +1,427 @@ ///| -fn parse_pkg(source : String) -> (Ast, Array[@basic.Report]) { - parse_string(source, name="moon.pkg") -} +struct AstNoLoc(Ast) ///| -fn ast_summary(ast : Ast) -> Json { - match ast { - Null(_) => Json::null() - Bool(value, ..) => Json::boolean(value) - Str(str, ..) => Json::string(str) - Float(float, ..) => - Json::object(Map::from_array([("$number", Json::string(float))])) - Arr(content, ..) => Json::array(content.to_array().map(ast_summary)) - Obj(fields, ..) => { - let object : Map[String, Json] = Map([]) - for field in fields { - let (key, value) = field - object[key] = ast_summary(value) - } - Json::object(object) +impl @debug.Debug for AstNoLoc with to_repr(self) { + fn go(ast : Ast) -> @debug.Repr { + match ast { + Null(_) => @debug.Repr::ctor("Null", []) + Bool(value, ..) => + @debug.Repr::ctor("Bool", [(None, @debug.to_repr(value))]) + Str(str, ..) => @debug.Repr::ctor("Str", [(None, @debug.to_repr(str))]) + Float(float, ..) => + @debug.Repr::ctor("Float", [(None, @debug.to_repr(float))]) + Arr(content, ..) => + @debug.Repr::ctor("Arr", [ + (None, @debug.Repr::array(content.to_array().map(item => go(item)))), + ]) + Obj(fields, ..) => + @debug.Repr::ctor("Obj", [ + ( + None, + @debug.Repr::array( + fields + .to_array() + .map(field => { + let (key, value) = field + @debug.Repr::tuple([@debug.to_repr(key), go(value)]) + }), + ), + ), + ]) } } -} -///| -fn report_contains(reports : Array[@basic.Report], needle : String) -> Bool { - for report in reports { - if report.msg.contains(needle) { - return true - } + match self { + AstNoLoc(ast) => go(ast) } - false } ///| -fn report_checks( - reports : Array[@basic.Report], - needles : Array[String], -) -> Json { - Json::object( - Map::from_array([ - ("count", Json::number(reports.length().to_double())), - ( - "contains", - Json::array( - needles.map(needle => Json::boolean(report_contains(reports, needle))), - ), - ), - ]), - ) -} - -///| -test "moon_config parse nested options" { +test "parse_moon_pkg postprocesses options and repeatable fields" { let source = + #|warnings = "+w1" + #|supported_targets = "+wasm" #|options( - #| nested: { "a": ["x", "y", 123], "b": { "c": false } } + #| "is-main": true, #|) + #|rule(name: "rule1", command: "exe") + #|rule(name: "rule2", command: "exe") + #|dev_build(rule: "rule1", input: "a", output: "b") + #|dev_build(rule: "rule2", input: "c", output: "d") #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, []), content={ - "count": 0, - "contains": [], - }) - json_inspect(ast_summary(ast), content={ - "options": { - "nested": { "a": ["x", "y", { "$number": "123" }], "b": { "c": false } }, - }, - }) -} - -///| -test "moon_config parse import for wbtest" { - let source = - #|import { "a/pkg", "b/pkg" } for "wbtest" - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, []), content={ - "count": 0, - "contains": [], - }) - json_inspect(ast_summary(ast), content={ "wbtest-import": ["a/pkg", "b/pkg"] }) -} - -///| -test "moon_config rejects legacy import kind syntax" { - let source = - #|import "test" { "path/to/pkg", "p" } - #| - let (ast, reports) = parse_pkg(source) - json_inspect( - report_checks(reports, ["Old import syntax is no longer supported"]), - content={ "count": 1, "contains": [true] }, + let (ast, reports) = parse_moon_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), ) - json_inspect(ast_summary(ast), content={ "import": ["path/to/pkg", "p"] }) -} - -///| -test "moon_config rejects legacy import alias syntax" { - let source = - #|import { "a" as @alias, "b" @ok } - #| - let (ast, reports) = parse_pkg(source) - json_inspect( - report_checks(reports, ["Old import alias syntax is no longer supported"]), - content={ "count": 1, "contains": [true] }, + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ("warn-list", Str("+w1")), + #| ("supported-targets", Str("+wasm")), + #| ("is-main", Bool(true)), + #| ("rule", Obj([("name", Str("rule1")), ("command", Str("exe"))])), + #| ("rule", Obj([("name", Str("rule2")), ("command", Str("exe"))])), + #| ( + #| "dev_build", + #| Obj([("rule", Str("rule1")), ("input", Str("a")), ("output", Str("b"))]), + #| ), + #| ( + #| "dev_build", + #| Obj([("rule", Str("rule2")), ("input", Str("c")), ("output", Str("d"))]), + #| ), + #| ], + #|) + ), ) - json_inspect(ast_summary(ast), content={ - "import": ["a", { "path": "b", "alias": "ok" }], - }) } ///| -test "moon_config avoids cascading after invalid import kind" { +test "parse_moon_pkg keeps repeated top-level rule entries in Ast" { let source = - #|import { "a" } for next = true + #|rule(name:"rule1", command:"exe") + #|rule(name:"rule2", command:"exe") #| - let (ast, reports) = parse_pkg(source) - json_inspect( - report_checks(reports, [ - "string literal \"test\" or \"wbtest\"", "`;` or EOF", - ]), - content={ "count": 2, "contains": [true, true] }, + let (ast, reports) = parse_moon_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ("rule", Obj([("name", Str("rule1")), ("command", Str("exe"))])), + #| ("rule", Obj([("name", Str("rule2")), ("command", Str("exe"))])), + #| ], + #|) + ), ) - json_inspect(ast_summary(ast), content={ "import": ["a"] }) -} - -///| -test "moon_config reports positional argument" { - let source = - #|apply("positional") - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["labeled argument"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "apply": { "": "positional" } }) -} - -///| -test "moon_config parse assignments" { - let source = - #|warnings = "+w1-w1" - #|supported_targets = "+wasm" - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, []), content={ - "count": 0, - "contains": [], - }) - json_inspect(ast_summary(ast), content={ - "warnings": "+w1-w1", - "supported_targets": "+wasm", - }) -} - -///| -test "moon_config requires statement separators" { - let source = - #|a = "x" b = "y" - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`;` or EOF"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "a": "x" }) } ///| -test "moon_config keeps next statement after missing assignment value before semicolon" { +test "parse_moon_pkg handles package imports aliases and targets" { let source = - #|a = ; - #|b = 1 + #|import { + #| "moonbit-community/fullstack-one-project-doc/shared" @shared, + #| "moonbitlang/core/json" @json, + #| "moonbitlang/async", + #|} #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["expression", "`;` or EOF"]), content={ - "count": 1, - "contains": [true, false], - }) - json_inspect(ast_summary(ast), content={ "a": null, "b": { "$number": "1" } }) -} - -///| -test "moon_config keeps next statement after missing assignment value on next line" { - let source = - #|a = - #|b = 1 + #|import { + #| "moonbitlang/core/test", + #|} for "test" #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["expression", "`;` or EOF"]), content={ - "count": 1, - "contains": [true, false], - }) - json_inspect(ast_summary(ast), content={ "a": null, "b": { "$number": "1" } }) -} - -///| -test "moon_config rejects unsupported numeric literals" { - let source = - #|unsigned = 1U - #|floaty = 1.5 + #|warnings = "-test_unqualified_package" + #|supported_targets = "native" #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["suffixes", "expression"]), content={ - "count": 2, - "contains": [true, true], - }) - json_inspect(ast_summary(ast), content={ "unsigned": null, "floaty": null }) -} - -///| -test "moon_config accepts bigint literal in pkg values" { - let source = - #|big = 1N + #|options( + #| "is-main": true, + #| targets: { + #| "main.mbt": [ "native" ], + #| "browser.mbt": [ "js" ], + #| }, + #|) #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, []), content={ - "count": 0, - "contains": [], - }) - json_inspect(ast_summary(ast), content={ "big": { "$number": "1N" } }) -} - -///| -test "moon_config preserves duplicate object and statement entries" { - fn duplicate_entry_summary(ast : Ast) -> Array[(String, String?)] raise { - match ast { - Obj(root_fields, ..) => { - let (_, options_value) = root_fields[0] - match options_value { - Obj(options_fields, ..) => { - let (_, dup_value) = options_fields[0] - match dup_value { - Obj(dup_fields, ..) => - dup_fields - .to_array() - .map(field => { - let (key, value) = field - (key, value.as_number()) - }) - _ => fail("expected duplicate entry value to be an object") - } - } - _ => fail("expected options argument list to be an object") - } - } - _ => fail("expected root to be an object") - } - } - - fn root_entry_summary(ast : Ast) -> Array[(String, String?)] raise { - match ast { - Obj(root_fields, ..) => - root_fields - .to_array() - .map(field => { - let (key, value) = field - let value_name = match value { - Obj(fields, ..) => - match fields[0] { - ("name", Str(str, ..)) => Some(str) - _ => None - } - _ => None - } - (key, value_name) - }) - _ => fail("expected root to be an object") - } - } - - let source = - #|options(dup: { "a": 1, "a": 2 }) - #|rule(name:"rule1", command:"exe") - #|rule(name:"rule2", command:"exe") - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, []), content={ - "count": 0, - "contains": [], - }) + let (ast, reports) = parse_moon_pkg(source) debug_inspect( - duplicate_entry_summary(ast), + reports, content=( - #|[("a", Some("1")), ("a", Some("2"))] + #|[] ), ) debug_inspect( - root_entry_summary(ast), + AstNoLoc(ast), content=( - #|[("options", None), ("rule", Some("rule1")), ("rule", Some("rule2"))] + #|Obj( + #| [ + #| ( + #| "import", + #| Arr( + #| [ + #| Obj( + #| [ + #| ("path", Str("moonbit-community/fullstack-one-project-doc/shared")), + #| ("alias", Str("shared")), + #| ], + #| ), + #| Obj([("path", Str("moonbitlang/core/json")), ("alias", Str("json"))]), + #| Str("moonbitlang/async"), + #| ], + #| ), + #| ), + #| ("test-import", Arr([Str("moonbitlang/core/test")])), + #| ("warn-list", Str("-test_unqualified_package")), + #| ("supported-targets", Str("native")), + #| ("is-main", Bool(true)), + #| ( + #| "targets", + #| Obj( + #| [ + #| ("main.mbt", Arr([Str("native")])), + #| ("browser.mbt", Arr([Str("js")])), + #| ], + #| ), + #| ), + #| ], + #|) ), ) } ///| -test "moon_config recovers map value after missing colon" { - let source = - #|options(nested: { "a" 1 }) - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["\":\""]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ - "options": { "nested": { "a": { "$number": "1" } } }, - }) -} - -///| -test "moon_config keeps next statement after missing array right bracket" { - let source = - #|a = [1 - #|b = 2 - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`]`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ - "a": [{ "$number": "1" }], - "b": { "$number": "2" }, - }) -} - -///| -test "moon_config reports missing import right brace at eof" { +test "parse_moon_pkg handles link options" { let source = - #|import { + #|options( + #| link: { + #| "wasm-gc": { + #| "use-js-builtin-string": true, + #| "import-memory": { "module": "env", "name": "memory" }, + #| "memory-limits": { "min": 1, "max": 4 }, + #| "shared-memory": true, + #| }, + #| }, + #|) #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`}`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "import": [] }) + let (ast, reports) = parse_moon_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ( + #| "link", + #| Obj( + #| [ + #| ( + #| "wasm-gc", + #| Obj( + #| [ + #| ("use-js-builtin-string", Bool(true)), + #| ( + #| "import-memory", + #| Obj([("module", Str("env")), ("name", Str("memory"))]), + #| ), + #| ("memory-limits", Obj([("min", Float("1")), ("max", Float("4"))])), + #| ("shared-memory", Bool(true)), + #| ], + #| ), + #| ), + #| ], + #| ), + #| ), + #| ], + #|) + ), + ) } ///| -test "moon_config reports missing apply right paren at eof" { +test "parse_moon_mod postprocesses imports and options" { let source = + #|name = "user/mod" + #|version = "0.1.0" + #|import { "dep/a@1.2.3", "dep/b" } + #|warnings = "+w1" + #|supported_targets = "+wasm" #|options( + #| license: "MIT", + #| repository: "https://example.com/repo", + #|) + #|rule(name: "rule1", command: "exe") + #|rule(name: "rule2", command: "exe") #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`)`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "options": {} }) + let (ast, reports) = parse_moon_mod(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ("name", Str("user/mod")), + #| ("version", Str("0.1.0")), + #| ("deps", Obj([("dep/a", Str("1.2.3")), ("dep/b", Str(""))])), + #| ("warn-list", Str("+w1")), + #| ("supported-targets", Str("+wasm")), + #| ("license", Str("MIT")), + #| ("repository", Str("https://example.com/repo")), + #| ("rule", Obj([("name", Str("rule1")), ("command", Str("exe"))])), + #| ("rule", Obj([("name", Str("rule2")), ("command", Str("exe"))])), + #| ], + #|) + ), + ) } ///| -test "moon_config reports missing array right bracket at eof" { +test "parse_moon_mod handles metadata options" { let source = - #|a = [ + #|name = "example/dsl_only" + #|version = "0.1.0" + #|import { "moonbitlang/x@0.4.6", "../dep" } + #|options( + #| "bin-deps": { + #| "tool/cli": { "bin-pkg": [ "cli" ], "path": "../bin" }, + #| }, + #| "compile-flags": [ "-DDEBUG", "-Wall" ], + #| description: "A DSL fixture module", + #| exclude: [ "target/**", "**/*.tmp" ], + #| include: [ "src/**", "README.mbt.md" ], + #| keywords: [ "dsl", "fixture" ], + #| license: "Apache-2.0", + #| "link-flags": [ "-lm" ], + #| "preferred-target": "wasm-gc", + #| readme: "README.mbt.md", + #| repository: "https://example.com/repo", + #| scripts: { "prebuild": "node ./prebuild.js", "postbuild": "echo done" }, + #| source: "src", + #|) #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`]`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "a": [] }) + let (ast, reports) = parse_moon_mod(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ("name", Str("example/dsl_only")), + #| ("version", Str("0.1.0")), + #| ("deps", Obj([("moonbitlang/x", Str("0.4.6")), ("../dep", Str(""))])), + #| ( + #| "bin-deps", + #| Obj( + #| [ + #| ( + #| "tool/cli", + #| Obj([("bin-pkg", Arr([Str("cli")])), ("path", Str("../bin"))]), + #| ), + #| ], + #| ), + #| ), + #| ("compile-flags", Arr([Str("-DDEBUG"), Str("-Wall")])), + #| ("description", Str("A DSL fixture module")), + #| ("exclude", Arr([Str("target/**"), Str("**/*.tmp")])), + #| ("include", Arr([Str("src/**"), Str("README.mbt.md")])), + #| ("keywords", Arr([Str("dsl"), Str("fixture")])), + #| ("license", Str("Apache-2.0")), + #| ("link-flags", Arr([Str("-lm")])), + #| ("preferred-target", Str("wasm-gc")), + #| ("readme", Str("README.mbt.md")), + #| ("repository", Str("https://example.com/repo")), + #| ( + #| "scripts", + #| Obj( + #| [ + #| ("prebuild", Str("node ./prebuild.js")), + #| ("postbuild", Str("echo done")), + #| ], + #| ), + #| ), + #| ("source", Str("src")), + #| ], + #|) + ), + ) } ///| -test "moon_config keeps next statement after missing apply left paren" { +test "parse_moon_work parses workspace config" { let source = - #|options nested: true - #|next = false + #|members = ["./app", "./shared"] + #|preferred_target = "wasm-gc" #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["\"(\""]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "options": {}, "next": false }) + let (ast, reports) = parse_moon_work(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ("members", Arr([Str("./app"), Str("./shared")])), + #| ("preferred_target", Str("wasm-gc")), + #| ], + #|) + ), + ) } ///| -test "moon_config keeps next statement after missing import left brace" { +test "parse_moon_work handles multiline members" { let source = - #|import [ "a/pkg" ] - #|next = false + #|members = [ + #| "./mod_a", + #| "./mod_b", + #|] #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["\"{\""]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "import": [], "next": false }) + let (ast, reports) = parse_moon_work(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("members", Arr([Str("./mod_a"), Str("./mod_b")]))]) + ), + ) } ///| -test "moon_config root loc stays ordered for layout-only source" { +test "parse_moon_mod reports duplicate non-repeatable keys" { let source = + #|name = "user/first" + #|name = "user/second" #| - #| - let (ast, reports) = parse_pkg(source) - let loc = ast.loc() - json_inspect( - Json::object( - Map::from_array([ - ("ast", ast_summary(ast)), - ("reports", report_checks(reports, [])), - ("ordered", Json::boolean(loc.start.compare(loc.end) <= 0)), - ]), + let (ast, reports) = parse_moon_mod(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.mod", lnum: 2, bol: 20, cnum: 27 }, + #| end: { fname: "moon.mod", lnum: 2, bol: 20, cnum: 40 }, + #| }, + #| msg: "Duplicate key `name` found in moon.mod.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("name", Str("user/first")), ("name", Str("user/second"))]) ), - content={ - "ast": {}, - "reports": { "count": 0, "contains": [] }, - "ordered": true, - }, ) } diff --git a/moon_config/moon_config_wbtest.mbt b/moon_config/moon_config_wbtest.mbt new file mode 100644 index 00000000..17a6d2c8 --- /dev/null +++ b/moon_config/moon_config_wbtest.mbt @@ -0,0 +1,702 @@ +///| +fn parse_pkg(source : String) -> (Ast, Array[@basic.Report]) { + parse_string(source, name="moon.pkg") +} + +///| +struct AstNoLoc(Ast) + +///| +impl @debug.Debug for AstNoLoc with to_repr(self) { + fn go(ast : Ast) -> @debug.Repr { + match ast { + Null(_) => @debug.Repr::ctor("Null", []) + Bool(value, ..) => + @debug.Repr::ctor("Bool", [(None, @debug.to_repr(value))]) + Str(str, ..) => @debug.Repr::ctor("Str", [(None, @debug.to_repr(str))]) + Float(float, ..) => + @debug.Repr::ctor("Float", [(None, @debug.to_repr(float))]) + Arr(content, ..) => + @debug.Repr::ctor("Arr", [ + (None, @debug.Repr::array(content.to_array().map(item => go(item)))), + ]) + Obj(fields, ..) => + @debug.Repr::ctor("Obj", [ + ( + None, + @debug.Repr::array( + fields + .to_array() + .map(field => { + let (key, value) = field + @debug.Repr::tuple([@debug.to_repr(key), go(value)]) + }), + ), + ), + ]) + } + } + + match self { + AstNoLoc(ast) => go(ast) + } +} + +///| +test "Ast::to_json folds repeated top-level rule entries into arrays" { + fn loc() -> Location { + Location::{ start: dummy_pos, end: dummy_pos } + } + + let loc = loc() + let ast = Obj( + Vector([ + ( + "rule", + Obj( + Vector([ + ("name", Str("rule1", loc~)), + ("command", Str("exe", loc~)), + ( + "env", + Obj( + Vector([("k", Str("first", loc~)), ("k", Str("second", loc~))]), + loc~, + ), + ), + ]), + loc~, + ), + ), + ( + "rule", + Obj( + Vector([("name", Str("rule2", loc~)), ("command", Str("exe", loc~))]), + loc~, + ), + ), + ]), + loc~, + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ( + #| "rule", + #| Obj( + #| [ + #| ("name", Str("rule1")), + #| ("command", Str("exe")), + #| ("env", Obj([("k", Str("first")), ("k", Str("second"))])), + #| ], + #| ), + #| ), + #| ("rule", Obj([("name", Str("rule2")), ("command", Str("exe"))])), + #| ], + #|) + ), + ) +} + +///| +test "moon_config parse nested options" { + let source = + #|options( + #| nested: { "a": ["x", "y", 123], "b": { "c": false } } + #|) + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ( + #| "options", + #| Obj( + #| [ + #| ( + #| "nested", + #| Obj( + #| [ + #| ("a", Arr([Str("x"), Str("y"), Float("123")])), + #| ("b", Obj([("c", Bool(false))])), + #| ], + #| ), + #| ), + #| ], + #| ), + #| ), + #| ], + #|) + ), + ) +} + +///| +test "moon_config parse import for wbtest" { + let source = + #|import { "a/pkg", "b/pkg" } for "wbtest" + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("wbtest-import", Arr([Str("a/pkg"), Str("b/pkg")]))]) + ), + ) +} + +///| +test "moon_config rejects legacy import kind syntax" { + let source = + #|import "test" { "path/to/pkg", "p" } + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 7 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 13 }, + #| }, + #| msg: "Old import syntax is no longer supported; use `import { ... }`, `import { ... } for \"test\"`, or `import { ... } for \"wbtest\"`.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("import", Arr([Str("path/to/pkg"), Str("p")]))]) + ), + ) +} + +///| +test "moon_config rejects legacy import alias syntax" { + let source = + #|import { "a" as @alias, "b" @ok } + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 13 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 15 }, + #| }, + #| msg: "Old import alias syntax is no longer supported; use `\"...\" @alias` instead of `\"...\" as @alias`.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ( + #| "import", + #| Arr([Str("a"), Obj([("path", Str("b")), ("alias", Str("ok"))])]), + #| ), + #| ], + #|) + ), + ) +} + +///| +test "moon_config avoids cascading after invalid import kind" { + let source = + #|import { "a" } for next = true + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 19 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 23 }, + #| }, + #| msg: "Unexpected token id (lowercase start), you may expect string literal \"test\" or \"wbtest\".", + #| }, + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 19 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 23 }, + #| }, + #| msg: "Unexpected token id (lowercase start), you may expect `;` or EOF.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("import", Arr([Str("a")]))]) + ), + ) +} + +///| +test "moon_config reports positional argument" { + let source = + #|apply("positional") + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 6 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 18 }, + #| }, + #| msg: "Unexpected token string, you may expect labeled argument.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("apply", Obj([("", Str("positional"))]))]) + ), + ) +} + +///| +test "moon_config parse assignments" { + let source = + #|warnings = "+w1-w1" + #|supported_targets = "+wasm" + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("warnings", Str("+w1-w1")), ("supported_targets", Str("+wasm"))]) + ), + ) +} + +///| +test "moon_config requires statement separators" { + let source = + #|a = "x" b = "y" + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 8 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 9 }, + #| }, + #| msg: "Unexpected token id (lowercase start), you may expect `;` or EOF.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("a", Str("x"))]) + ), + ) +} + +///| +test "moon_config keeps next statement after missing assignment value before semicolon" { + let source = + #|a = ; + #|b = 1 + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 4 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 5 }, + #| }, + #| msg: "Unexpected token `;`, you may expect expression.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("a", Null), ("b", Float("1"))]) + ), + ) +} + +///| +test "moon_config keeps next statement after missing assignment value on next line" { + let source = + #|a = + #|b = 1 + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 2, bol: 4, cnum: 4 }, + #| end: { fname: "moon.pkg", lnum: 2, bol: 4, cnum: 5 }, + #| }, + #| msg: "Unexpected token id (lowercase start), you may expect expression.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("a", Null), ("b", Float("1"))]) + ), + ) +} + +///| +test "moon_config rejects unsupported numeric literals" { + let source = + #|unsigned = 1U + #|floaty = 1.5 + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 11 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 13 }, + #| }, + #| msg: "Unexpected token int, you may expect integer literals without U or L suffixes.", + #| }, + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 2, bol: 14, cnum: 23 }, + #| end: { fname: "moon.pkg", lnum: 2, bol: 14, cnum: 26 }, + #| }, + #| msg: "Unexpected token double, you may expect expression.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("unsigned", Null), ("floaty", Null)]) + ), + ) +} + +///| +test "moon_config accepts bigint literal in pkg values" { + let source = + #|big = 1N + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("big", Float("1N"))]) + ), + ) +} + +///| +test "moon_config preserves duplicate object and statement entries" { + let source = + #|options(dup: { "a": 1, "a": 2 }) + #|rule(name:"rule1", command:"exe") + #|rule(name:"rule2", command:"exe") + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj( + #| [ + #| ( + #| "options", + #| Obj([("dup", Obj([("a", Float("1")), ("a", Float("2"))]))]), + #| ), + #| ("rule", Obj([("name", Str("rule1")), ("command", Str("exe"))])), + #| ("rule", Obj([("name", Str("rule2")), ("command", Str("exe"))])), + #| ], + #|) + ), + ) +} + +///| +test "moon_config recovers map value after missing colon" { + let source = + #|options(nested: { "a" 1 }) + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 22 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 23 }, + #| }, + #| msg: "Unexpected token int, you may expect \":\".", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("options", Obj([("nested", Obj([("a", Float("1"))]))]))]) + ), + ) +} + +///| +test "moon_config keeps next statement after missing array right bracket" { + let source = + #|a = [1 + #|b = 2 + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 6 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 6 }, + #| }, + #| msg: "Unexpected token `;`, you may expect `]`.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("a", Arr([Float("1")])), ("b", Float("2"))]) + ), + ) +} + +///| +test "moon_config reports missing import right brace at eof" { + let source = + #|import { + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 2, bol: 9, cnum: 9 }, + #| end: { fname: "moon.pkg", lnum: 2, bol: 9, cnum: 9 }, + #| }, + #| msg: "Unexpected end of file, missing `}` here.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("import", Arr([]))]) + ), + ) +} + +///| +test "moon_config reports missing apply right paren at eof" { + let source = + #|options( + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 2, bol: 9, cnum: 9 }, + #| end: { fname: "moon.pkg", lnum: 2, bol: 9, cnum: 9 }, + #| }, + #| msg: "Unexpected end of file, missing `)` here.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("options", Obj([]))]) + ), + ) +} + +///| +test "moon_config reports missing array right bracket at eof" { + let source = + #|a = [ + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 2, bol: 6, cnum: 6 }, + #| end: { fname: "moon.pkg", lnum: 2, bol: 6, cnum: 6 }, + #| }, + #| msg: "Unexpected end of file, missing `]` here.", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("a", Arr([]))]) + ), + ) +} + +///| +test "moon_config keeps next statement after missing apply left paren" { + let source = + #|options nested: true + #|next = false + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 8 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 14 }, + #| }, + #| msg: "Unexpected token id (lowercase start), you may expect \"(\".", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("options", Obj([])), ("next", Bool(false))]) + ), + ) +} + +///| +test "moon_config keeps next statement after missing import left brace" { + let source = + #|import [ "a/pkg" ] + #|next = false + #| + let (ast, reports) = parse_pkg(source) + debug_inspect( + reports, + content=( + #|[ + #| { + #| loc: { + #| start: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 7 }, + #| end: { fname: "moon.pkg", lnum: 1, bol: 0, cnum: 8 }, + #| }, + #| msg: "Unexpected token `[`, you may expect \"{\".", + #| }, + #|] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([("import", Arr([])), ("next", Bool(false))]) + ), + ) +} + +///| +test "moon_config root loc stays ordered for layout-only source" { + let source = + #| + #| + let (ast, reports) = parse_pkg(source) + let loc = ast.loc() + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|Obj([]) + ), + ) + debug_inspect(loc.start.compare(loc.end) <= 0, content="true") +} diff --git a/moon_config/parser.mbt b/moon_config/parser.mbt index d04b418c..7705e909 100644 --- a/moon_config/parser.mbt +++ b/moon_config/parser.mbt @@ -609,7 +609,7 @@ fn parse_statements(state : State) -> Array[(String, Ast)] { } ///| -pub fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { +fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { if tokens.is_empty() { let loc = Location::{ start: dummy_pos, end: dummy_pos } return (Obj(Vector([]), loc~), []) @@ -629,10 +629,7 @@ pub fn moon_pkg_of_tokens(tokens : Triples) -> (Ast, Array[Report]) { } ///| -pub fn parse_string( - source : String, - name? : String = "", -) -> (Ast, Array[Report]) { +fn parse_string(source : String, name? : String = "") -> (Ast, Array[Report]) { let lex_result = @lexer.tokens_from_string(source, comment=false, name~) let (ast, reports) = moon_pkg_of_tokens(lex_result.tokens) let diagnostics = [] @@ -645,7 +642,3 @@ pub fn parse_string( } ///| -pub fn parse_file(path : String) -> (Ast, Array[Report]) raise @fs.IOError { - let source = @fs.read_file_to_string(path) - parse_string(source, name=path) -} diff --git a/moon_config/pkg.generated.mbti b/moon_config/pkg.generated.mbti index b23ac671..c551408f 100644 --- a/moon_config/pkg.generated.mbti +++ b/moon_config/pkg.generated.mbti @@ -5,16 +5,14 @@ import { "moonbitlang/core/debug", "moonbitlang/core/immut/vector", "moonbitlang/parser/basic", - "moonbitlang/parser/tokens", - "moonbitlang/x/fs", } // Values -pub fn moon_pkg_of_tokens(Array[(@tokens.Token, @basic.Position, @basic.Position)]) -> (Ast, Array[@basic.Report]) +pub fn parse_moon_mod(name? : String, String) -> (Ast, Array[@basic.Report]) -pub fn parse_file(String) -> (Ast, Array[@basic.Report]) raise @fs.IOError +pub fn parse_moon_pkg(name? : String, String) -> (Ast, Array[@basic.Report]) -pub fn parse_string(String, name? : String) -> (Ast, Array[@basic.Report]) +pub fn parse_moon_work(name? : String, String) -> (Ast, Array[@basic.Report]) // Errors @@ -27,11 +25,6 @@ pub(all) enum Ast { Arr(@vector.Vector[Ast], loc~ : @basic.Location) Obj(@vector.Vector[(String, Ast)], loc~ : @basic.Location) } derive(Eq, @debug.Debug) -pub fn Ast::as_array(Self) -> @vector.Vector[Self]? -pub fn Ast::as_bool(Self) -> Bool? -pub fn Ast::as_number(Self) -> String? -pub fn Ast::as_object(Self) -> @vector.Vector[(String, Self)]? -pub fn Ast::as_string(Self) -> String? pub fn Ast::loc(Self) -> @basic.Location pub impl ToJson for Ast diff --git a/moon_config/post_process.mbt b/moon_config/post_process.mbt new file mode 100644 index 00000000..8ccecd7c --- /dev/null +++ b/moon_config/post_process.mbt @@ -0,0 +1,205 @@ +///| +fn post_process_moon_pkg(ast : Ast, diagnostics : Array[Report]) -> Ast { + match ast { + Obj(fields, loc~) => { + let emit_invalid_config = fn(key : String, value : Ast) { + diagnostics.push(Report::{ + loc: value.loc(), + msg: "Invalid moon.pkg config: unexpected key `\{key}`.", + }) + } + let allowed_duplicate = fn(key : String) -> Bool { + key == "dev_build" || key == "rule" + } + let seen : Map[String, Bool] = Map([]) + let check_unique_toplevel = fn(key : String, value : Ast) { + if seen.contains(key) && !allowed_duplicate(key) { + diagnostics.push(Report::{ + loc: value.loc(), + msg: "Duplicate key `\{key}` found in moon.pkg.", + }) + } + seen[key] = true + } + let results : Array[(String, Ast)] = [] + for field in fields { + let (key, value) = field + check_unique_toplevel(key, value) + match key { + "import" | "wbtest-import" | "test-import" | "dev_build" | "rule" => + results.push(field) + "warnings" => + match value { + Str(_, ..) => results.push(("warn-list", value)) + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + "supported_targets" => + match value { + Str(_, ..) => results.push(("supported-targets", value)) + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + "options" => + match value { + Obj(option_fields, ..) => + for option_field in option_fields { + let (option_key, option_value) = option_field + check_unique_toplevel(option_key, option_value) + results.push(option_field) + } + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + } + Obj(Vector(results), loc~) + } + _ => ast + } +} + +///| +fn post_process_moon_mod(ast : Ast, diagnostics : Array[Report]) -> Ast { + match ast { + Obj(fields, loc~) => { + fn emit_invalid_config(key : String, value : Ast) { + diagnostics.push({ + loc: value.loc(), + msg: "Invalid moon.mod config: unexpected key `\{key}`.", + }) + } + let seen : Map[String, Bool] = Map([]) + let check_unique_toplevel = fn(key : String, value : Ast) { + if seen.contains(key) && key != "rule" { + diagnostics.push({ + loc: value.loc(), + msg: "Duplicate key `\{key}` found in moon.mod.", + }) + } + seen[key] = true + } + let split_import_spec = fn(spec : String) -> (String, String) { + match spec.rev_find("@") { + Some(index) if index > 0 => + ( + spec.unsafe_substring(start=0, end=index), + spec.unsafe_substring(start=index + 1, end=spec.length()), + ) + _ => (spec, "") + } + } + let results : Array[(String, Ast)] = [] + for field in fields { + let (key, value) = field + check_unique_toplevel(key, value) + match key { + "name" | "version" | "rule" => results.push(field) + "warnings" => + match value { + Str(_, ..) => results.push(("warn-list", value)) + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + "supported_targets" => + match value { + Str(_, ..) => results.push(("supported-targets", value)) + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + "import" => + match value { + Arr(imports, loc=import_loc) => { + let deps : Array[(String, Ast)] = [] + for item in imports { + match item { + Str(spec, loc~) => { + let (name, version) = split_import_spec(spec) + deps.push((name, Str(version, loc~))) + } + _ => () + } + } + results.push(("deps", Obj(Vector(deps), loc=import_loc))) + } + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + "options" => + match value { + Obj(option_fields, ..) => + for option_field in option_fields { + let (option_key, option_value) = option_field + check_unique_toplevel(option_key, option_value) + results.push(option_field) + } + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + } + Obj(Vector(results), loc~) + } + _ => ast + } +} + +///| +fn post_process_moon_work(ast : Ast, diagnostics : Array[Report]) -> Ast { + match ast { + Obj(fields, loc~) => { + let emit_invalid_config = fn(key : String, value : Ast) { + diagnostics.push(Report::{ + loc: value.loc(), + msg: "Invalid moon.work config: unexpected key `\{key}`.", + }) + } + let seen : Map[String, Bool] = Map([]) + let check_unique_toplevel = fn(key : String, value : Ast) { + if seen.contains(key) { + diagnostics.push(Report::{ + loc: value.loc(), + msg: "Duplicate key `\{key}` found in moon.work.", + }) + } + seen[key] = true + } + let results : Array[(String, Ast)] = [] + for field in fields { + let (key, value) = field + check_unique_toplevel(key, value) + match key { + "members" | "preferred_target" => results.push(field) + _ => { + emit_invalid_config(key, value) + results.push(field) + } + } + } + Obj(Vector(results), loc~) + } + _ => ast + } +} From 712ae5da0df17740580e916aad84518a6d02bcef Mon Sep 17 00:00:00 2001 From: Yorkin Date: Thu, 14 May 2026 11:31:03 +0800 Subject: [PATCH 5/6] add mq command --- cmd/mq/README.md | 27 ++++ cmd/mq/cli.mbt | 227 +++++++++++++++++++++++++++++++++ cmd/mq/main.mbt | 100 +++++++++++++++ cmd/mq/moon.pkg | 15 +++ cmd/mq/pkg.generated.mbti | 12 ++ cmd/wasm/mq/README.md | 44 +++++++ cmd/wasm/mq/cli.mbt | 227 +++++++++++++++++++++++++++++++++ cmd/wasm/mq/main.mbt | 107 ++++++++++++++++ cmd/wasm/mq/moon.pkg | 12 ++ cmd/wasm/mq/pkg.generated.mbti | 17 +++ moon.mod.json | 4 +- 11 files changed, 791 insertions(+), 1 deletion(-) create mode 100644 cmd/mq/README.md create mode 100644 cmd/mq/cli.mbt create mode 100644 cmd/mq/main.mbt create mode 100644 cmd/mq/moon.pkg create mode 100644 cmd/mq/pkg.generated.mbti create mode 100644 cmd/wasm/mq/README.md create mode 100644 cmd/wasm/mq/cli.mbt create mode 100644 cmd/wasm/mq/main.mbt create mode 100644 cmd/wasm/mq/moon.pkg create mode 100644 cmd/wasm/mq/pkg.generated.mbti diff --git a/cmd/mq/README.md b/cmd/mq/README.md new file mode 100644 index 00000000..8ce64751 --- /dev/null +++ b/cmd/mq/README.md @@ -0,0 +1,27 @@ +# mq + +`mq` is the native command for parsing MoonBit configuration DSL files. It +lives at `moonbitlang/parser/cmd/mq` and uses `moonbitlang/async`. + +The WASI wasm package is documented separately in `cmd/mq_wasm`. + +The `legacy` subcommand prints the post-processed JSON form that is compatible +with the old JSON configuration format. + +```bash +mq legacy moon.pkg +mq legacy moon.mod +mq legacy moon.work +mq legacy moon.pkg -o moon.pkg.json +mq legacy --file-type mod -c 'name = "demo/mod"' +cat moon.work | mq legacy - --file-type work +``` + +When reading from stdin or `-c/--code`, pass `--file-type pkg`, +`--file-type mod`, or `--file-type work` to the `legacy` subcommand. + +install: + +```bash +moon install moonbitlang/parser/cmd/mq +``` diff --git a/cmd/mq/cli.mbt b/cmd/mq/cli.mbt new file mode 100644 index 00000000..13f1a608 --- /dev/null +++ b/cmd/mq/cli.mbt @@ -0,0 +1,227 @@ +///| +fn ends_with(str : String, suffix : String) -> Bool { + let str_len = str.length() + let suffix_len = suffix.length() + str_len >= suffix_len && str[str_len - suffix_len:].to_owned() == suffix +} + +///| +fn config_kind(name : String) -> String? { + if ends_with(name, "moon.pkg") { + Some("pkg") + } else if ends_with(name, "moon.mod") { + Some("mod") + } else if ends_with(name, "moon.work") { + Some("work") + } else { + None + } +} + +///| +fn file_type_kind(file_type : String) -> String? { + match file_type { + "pkg" => Some("pkg") + "mod" => Some("mod") + "work" => Some("work") + _ => None + } +} + +///| +fn kind_name(kind : String) -> String { + match kind { + "pkg" => "moon.pkg" + "mod" => "moon.mod" + "work" => "moon.work" + _ => "moon.pkg" + } +} + +///| +fn parse_config(kind : String, source : String) -> @config.Ast { + let name = kind_name(kind) + let (ast, _) = match kind { + "pkg" => @config.parse_moon_pkg(name~, source) + "mod" => @config.parse_moon_mod(name~, source) + "work" => @config.parse_moon_work(name~, source) + _ => @config.parse_moon_pkg(name~, source) + } + ast +} + +///| +enum InputSource { + File(String) + Stdin + Code(String) +} + +///| +struct LegacyOptions { + source : InputSource + file_type : String? + output : String? +} + +///| +enum CliAction { + RunLegacy(LegacyOptions) + Help(String) + Error(String) +} + +///| +fn root_usage() -> String { + ( + #|Usage: mq + #| + #|Mooncakes.io backend MoonBit config tool + #| + #|Commands: + #| legacy Convert Moon config DSL to legacy Mooncakes.io JSON output + #| help Print help for the subcommand. + #| + #|Options: + #| -h, --help Show help information. + ) +} + +///| +fn legacy_usage() -> String { + ( + #|Usage: mq legacy [options] [input] + #| + #|Convert Moon config DSL to legacy Mooncakes.io JSON output + #| + #|Arguments: + #| input Path to moon.pkg/moon.mod/moon.work, or - for stdin + #| + #|Options: + #| -h, --help Show help information. + #| --file-type File type for stdin or -c input: pkg, mod, or work + #| -c, --code Read config source from command line + #| -o, --output Write output to file instead of stdout + ) +} + +///| +fn cli_error(message : String, usage : String) -> CliAction { + Error("error: \{message}\n\n\{usage}") +} + +///| +fn is_help_arg(arg : String) -> Bool { + arg == "-h" || arg == "--help" +} + +///| +fn parse_cli(args : ArrayView[String]) -> CliAction { + if args.length() == 0 { + return cli_error("expected subcommand: legacy", root_usage()) + } + let first = args[0] + if is_help_arg(first) { + return Help(root_usage()) + } + if first == "help" { + if args.length() == 1 { + return Help(root_usage()) + } else if args.length() == 2 && args[1] == "legacy" { + return Help(legacy_usage()) + } else { + return cli_error("unknown help topic", root_usage()) + } + } + if first != "legacy" { + return cli_error("unknown subcommand: \{first}", root_usage()) + } + parse_legacy(args[1:]) +} + +///| +fn parse_legacy(args : ArrayView[String]) -> CliAction { + let mut input : String? = None + let mut code : String? = None + let mut file_type : String? = None + let mut output : String? = None + let mut i = 0 + while i < args.length() { + let arg = args[i] + if is_help_arg(arg) { + return Help(legacy_usage()) + } else if arg == "--file-type" { + if i + 1 >= args.length() { + return cli_error("missing value for --file-type", legacy_usage()) + } + file_type = Some(args[i + 1]) + i = i + 2 + continue + } else if arg.has_prefix("--file-type=") { + file_type = Some(arg["--file-type=".length():].to_owned()) + i = i + 1 + continue + } else if arg == "-c" || arg == "--code" { + if i + 1 >= args.length() { + return cli_error("missing value for \{arg}", legacy_usage()) + } + code = Some(args[i + 1]) + i = i + 2 + continue + } else if arg.has_prefix("--code=") { + code = Some(arg["--code=".length():].to_owned()) + i = i + 1 + continue + } else if arg == "-o" || arg == "--output" { + if i + 1 >= args.length() { + return cli_error("missing value for \{arg}", legacy_usage()) + } + output = Some(args[i + 1]) + i = i + 2 + continue + } else if arg.has_prefix("--output=") { + output = Some(arg["--output=".length():].to_owned()) + i = i + 1 + continue + } else if arg.has_prefix("-") && arg != "-" { + return cli_error("unknown option: \{arg}", legacy_usage()) + } else { + if input is Some(_) { + return cli_error("unexpected argument: \{arg}", legacy_usage()) + } + input = Some(arg) + i = i + 1 + } + } + if code is Some(source) { + if input is Some(_) { + return cli_error( + "-c/--code cannot be used with an input path", + legacy_usage(), + ) + } + return RunLegacy(LegacyOptions::{ source: Code(source), file_type, output }) + } + let source = match input { + Some("-") => Stdin + Some(path) => File(path) + None => return cli_error("missing input", legacy_usage()) + } + RunLegacy(LegacyOptions::{ source, file_type, output }) +} + +///| +fn kind_from_options(options : LegacyOptions) -> String? { + match options.file_type { + Some(file_type) => + match file_type_kind(file_type) { + Some(kind) => Some(kind) + None => None + } + None => + match options.source { + File(path) => config_kind(path) + Stdin | Code(_) => None + } + } +} diff --git a/cmd/mq/main.mbt b/cmd/mq/main.mbt new file mode 100644 index 00000000..a3abf4fc --- /dev/null +++ b/cmd/mq/main.mbt @@ -0,0 +1,100 @@ +///| +async fn die(message : String) -> Unit { + @stdio.stderr.write("\{message}\n") catch { + _ => () + } + @sys.exit(1) +} + +///| +async fn write_stdout(message : String) -> Unit { + try @stdio.stdout.write(message) catch { + err => die("error: failed to write stdout: \{err}") + } noraise { + _ => () + } +} + +///| +fn program_args() -> ArrayView[String] { + let args = @sys.get_cli_args() + if args.length() > 1 { + args[1:] + } else { + [] + } +} + +///| +async fn read_file(path : String) -> String { + try @fs.read_file(path).text() catch { + err => { + die("error: failed to read \{path}: \{err}") + "" + } + } noraise { + source => source + } +} + +///| +async fn read_stdin() -> String { + try @stdio.stdin.read_all().text() catch { + err => { + die("error: failed to read stdin: \{err}") + "" + } + } noraise { + source => source + } +} + +///| +async fn write_result(output : String?, content : String) -> Unit { + let content = "\{content}\n" + match output { + Some(path) => + try + @fs.write_file( + path, + content, + create_mode=@fs.CreateMode::CreateOrTruncate, + ) + catch { + err => die("error: failed to write \{path}: \{err}") + } noraise { + _ => () + } + None => write_stdout(content) + } +} + +///| +async fn run_legacy(options : LegacyOptions) -> Unit { + let kind = match kind_from_options(options) { + Some(kind) => kind + None => { + die("error: --file-type pkg|mod|work is required for stdin or -c input") + "pkg" + } + } + let source = match options.source { + File(path) => read_file(path) + Stdin => read_stdin() + Code(source) => source + } + let ast = parse_config(kind, source) + write_result(options.output, ast.to_json().stringify()) +} + +///| +async fn main { + match parse_cli(program_args()) { + Help(text) => { + write_stdout("\{text}\n") + @sys.exit(0) + } + Error(message) => die(message) + RunLegacy(options) => run_legacy(options) + } +} diff --git a/cmd/mq/moon.pkg b/cmd/mq/moon.pkg new file mode 100644 index 00000000..33531ed4 --- /dev/null +++ b/cmd/mq/moon.pkg @@ -0,0 +1,15 @@ +import { + "moonbitlang/parser/moon_config" @config, + "moonbitlang/async", + "moonbitlang/async/fs", + "moonbitlang/async/stdio", + "moonbitlang/async/io", + "moonbitlang/x/sys", + "moonbitlang/core/json", +} + +supported_targets = "+native" + +options( + "is-main": true, +) diff --git a/cmd/mq/pkg.generated.mbti b/cmd/mq/pkg.generated.mbti new file mode 100644 index 00000000..f67809c9 --- /dev/null +++ b/cmd/mq/pkg.generated.mbti @@ -0,0 +1,12 @@ +// Generated using `moon info`, DON'T EDIT IT +package "moonbitlang/parser/cmd/mq" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits diff --git a/cmd/wasm/mq/README.md b/cmd/wasm/mq/README.md new file mode 100644 index 00000000..f06611e9 --- /dev/null +++ b/cmd/wasm/mq/README.md @@ -0,0 +1,44 @@ +# mq (Wasm) + +This is the WASI wasm build of `mq` for parsing MoonBit configuration DSL +files. It lives at `moonbitlang/parser/cmd/wasm/mq` and uses +`moonbit-community/miniio`. + +The `legacy` subcommand prints the post-processed JSON form that is compatible +with the old JSON configuration format. + +Run commands from the `moonbitlang/parser` module root: + +```bash +moon build --target wasm cmd/wasm/mq +moon run --target wasm cmd/wasm/mq legacy moon.pkg +moon run --target wasm cmd/wasm/mq legacy moon.mod +moon run --target wasm cmd/wasm/mq legacy moon.work +moon run --target wasm cmd/wasm/mq legacy --file-type mod -c 'name = "demo/mod"' +cat moon.mod | moon run --target wasm cmd/wasm/mq legacy - --file-type mod +``` + +When reading from stdin or `-c/--code`, pass `--file-type pkg`, +`--file-type mod`, or `--file-type work` to the `legacy` subcommand. + +Moon names the raw build artifact from the package directory, so this package +builds as `mq.wasm`. + +Run it directly with `moonrun`: + +```bash +moonrun mq.wasm legacy moon.pkg +moonrun mq.wasm legacy --file-type mod -c 'name = "demo/mod"' +cat moon.mod | moonrun mq.wasm legacy - --file-type mod +``` + +Or with `wasmtime`: + +```bash +wasmtime run --dir . mq.wasm legacy moon.pkg +wasmtime run mq.wasm legacy --file-type mod -c 'name = "demo/mod"' +cat moon.mod | wasmtime run mq.wasm legacy - --file-type mod +``` + +Under WASI, `-o` can only write to paths inside directories preopened by the +runtime. diff --git a/cmd/wasm/mq/cli.mbt b/cmd/wasm/mq/cli.mbt new file mode 100644 index 00000000..13f1a608 --- /dev/null +++ b/cmd/wasm/mq/cli.mbt @@ -0,0 +1,227 @@ +///| +fn ends_with(str : String, suffix : String) -> Bool { + let str_len = str.length() + let suffix_len = suffix.length() + str_len >= suffix_len && str[str_len - suffix_len:].to_owned() == suffix +} + +///| +fn config_kind(name : String) -> String? { + if ends_with(name, "moon.pkg") { + Some("pkg") + } else if ends_with(name, "moon.mod") { + Some("mod") + } else if ends_with(name, "moon.work") { + Some("work") + } else { + None + } +} + +///| +fn file_type_kind(file_type : String) -> String? { + match file_type { + "pkg" => Some("pkg") + "mod" => Some("mod") + "work" => Some("work") + _ => None + } +} + +///| +fn kind_name(kind : String) -> String { + match kind { + "pkg" => "moon.pkg" + "mod" => "moon.mod" + "work" => "moon.work" + _ => "moon.pkg" + } +} + +///| +fn parse_config(kind : String, source : String) -> @config.Ast { + let name = kind_name(kind) + let (ast, _) = match kind { + "pkg" => @config.parse_moon_pkg(name~, source) + "mod" => @config.parse_moon_mod(name~, source) + "work" => @config.parse_moon_work(name~, source) + _ => @config.parse_moon_pkg(name~, source) + } + ast +} + +///| +enum InputSource { + File(String) + Stdin + Code(String) +} + +///| +struct LegacyOptions { + source : InputSource + file_type : String? + output : String? +} + +///| +enum CliAction { + RunLegacy(LegacyOptions) + Help(String) + Error(String) +} + +///| +fn root_usage() -> String { + ( + #|Usage: mq + #| + #|Mooncakes.io backend MoonBit config tool + #| + #|Commands: + #| legacy Convert Moon config DSL to legacy Mooncakes.io JSON output + #| help Print help for the subcommand. + #| + #|Options: + #| -h, --help Show help information. + ) +} + +///| +fn legacy_usage() -> String { + ( + #|Usage: mq legacy [options] [input] + #| + #|Convert Moon config DSL to legacy Mooncakes.io JSON output + #| + #|Arguments: + #| input Path to moon.pkg/moon.mod/moon.work, or - for stdin + #| + #|Options: + #| -h, --help Show help information. + #| --file-type File type for stdin or -c input: pkg, mod, or work + #| -c, --code Read config source from command line + #| -o, --output Write output to file instead of stdout + ) +} + +///| +fn cli_error(message : String, usage : String) -> CliAction { + Error("error: \{message}\n\n\{usage}") +} + +///| +fn is_help_arg(arg : String) -> Bool { + arg == "-h" || arg == "--help" +} + +///| +fn parse_cli(args : ArrayView[String]) -> CliAction { + if args.length() == 0 { + return cli_error("expected subcommand: legacy", root_usage()) + } + let first = args[0] + if is_help_arg(first) { + return Help(root_usage()) + } + if first == "help" { + if args.length() == 1 { + return Help(root_usage()) + } else if args.length() == 2 && args[1] == "legacy" { + return Help(legacy_usage()) + } else { + return cli_error("unknown help topic", root_usage()) + } + } + if first != "legacy" { + return cli_error("unknown subcommand: \{first}", root_usage()) + } + parse_legacy(args[1:]) +} + +///| +fn parse_legacy(args : ArrayView[String]) -> CliAction { + let mut input : String? = None + let mut code : String? = None + let mut file_type : String? = None + let mut output : String? = None + let mut i = 0 + while i < args.length() { + let arg = args[i] + if is_help_arg(arg) { + return Help(legacy_usage()) + } else if arg == "--file-type" { + if i + 1 >= args.length() { + return cli_error("missing value for --file-type", legacy_usage()) + } + file_type = Some(args[i + 1]) + i = i + 2 + continue + } else if arg.has_prefix("--file-type=") { + file_type = Some(arg["--file-type=".length():].to_owned()) + i = i + 1 + continue + } else if arg == "-c" || arg == "--code" { + if i + 1 >= args.length() { + return cli_error("missing value for \{arg}", legacy_usage()) + } + code = Some(args[i + 1]) + i = i + 2 + continue + } else if arg.has_prefix("--code=") { + code = Some(arg["--code=".length():].to_owned()) + i = i + 1 + continue + } else if arg == "-o" || arg == "--output" { + if i + 1 >= args.length() { + return cli_error("missing value for \{arg}", legacy_usage()) + } + output = Some(args[i + 1]) + i = i + 2 + continue + } else if arg.has_prefix("--output=") { + output = Some(arg["--output=".length():].to_owned()) + i = i + 1 + continue + } else if arg.has_prefix("-") && arg != "-" { + return cli_error("unknown option: \{arg}", legacy_usage()) + } else { + if input is Some(_) { + return cli_error("unexpected argument: \{arg}", legacy_usage()) + } + input = Some(arg) + i = i + 1 + } + } + if code is Some(source) { + if input is Some(_) { + return cli_error( + "-c/--code cannot be used with an input path", + legacy_usage(), + ) + } + return RunLegacy(LegacyOptions::{ source: Code(source), file_type, output }) + } + let source = match input { + Some("-") => Stdin + Some(path) => File(path) + None => return cli_error("missing input", legacy_usage()) + } + RunLegacy(LegacyOptions::{ source, file_type, output }) +} + +///| +fn kind_from_options(options : LegacyOptions) -> String? { + match options.file_type { + Some(file_type) => + match file_type_kind(file_type) { + Some(kind) => Some(kind) + None => None + } + None => + match options.source { + File(path) => config_kind(path) + Stdin | Code(_) => None + } + } +} diff --git a/cmd/wasm/mq/main.mbt b/cmd/wasm/mq/main.mbt new file mode 100644 index 00000000..9be496f7 --- /dev/null +++ b/cmd/wasm/mq/main.mbt @@ -0,0 +1,107 @@ +///| +fn die(message : String) -> Unit { + write_stderr("\{message}\n") + @miniio.exit(1) +} + +///| +fn write_stderr(message : String) -> Unit { + @miniio.stderr.write_text(message) catch { + _ => () + } +} + +///| +fn write_stdout(message : String) -> Unit { + try @miniio.stdout.write_text(message) catch { + err => die("error: failed to write stdout: \{err}") + } noraise { + _ => () + } +} + +///| +fn program_args() -> ArrayView[String] { + try @miniio.args_get() catch { + err => { + die("error: failed to read argv: \{err}") + [] + } + } noraise { + args => if args.length() > 1 { args[1:] } else { [] } + } +} + +///| +fn read_file(path : String) -> String { + try @miniio.read_text_file(path) catch { + err => { + die("error: failed to read \{path}: \{err}") + "" + } + } noraise { + source => source + } +} + +///| +fn read_stdin() -> String { + try @miniio.stdin.read_all().text() catch { + err => { + die("error: failed to read stdin: \{err}") + "" + } + } noraise { + source => source + } +} + +///| +fn write_result(output : String?, content : String) -> Unit { + let content = "\{content}\n" + match output { + Some(path) => + try + @miniio.write_text_file( + path, + content, + create_mode=@miniio.CreateMode::CreateOrTruncate, + ) + catch { + err => die("error: failed to write \{path}: \{err}") + } noraise { + _ => () + } + None => write_stdout(content) + } +} + +///| +fn run_legacy(options : LegacyOptions) -> Unit { + let kind = match kind_from_options(options) { + Some(kind) => kind + None => { + die("error: --file-type pkg|mod|work is required for stdin or -c input") + "pkg" + } + } + let source = match options.source { + File(path) => read_file(path) + Stdin => read_stdin() + Code(source) => source + } + let ast = parse_config(kind, source) + write_result(options.output, ast.to_json().stringify()) +} + +///| +fn main { + match parse_cli(program_args()) { + Help(text) => { + write_stdout("\{text}\n") + @miniio.exit(0) + } + Error(message) => die(message) + RunLegacy(options) => run_legacy(options) + } +} diff --git a/cmd/wasm/mq/moon.pkg b/cmd/wasm/mq/moon.pkg new file mode 100644 index 00000000..a38831ba --- /dev/null +++ b/cmd/wasm/mq/moon.pkg @@ -0,0 +1,12 @@ +import { + "moonbitlang/parser/moon_config" @config, + "moonbit-community/miniio", + "moonbit-community/miniio/io", + "moonbitlang/core/json", +} + +supported_targets = "wasm" + +options( + "is-main": true, +) diff --git a/cmd/wasm/mq/pkg.generated.mbti b/cmd/wasm/mq/pkg.generated.mbti new file mode 100644 index 00000000..513c05e1 --- /dev/null +++ b/cmd/wasm/mq/pkg.generated.mbti @@ -0,0 +1,17 @@ +// Generated using `moon info`, DON'T EDIT IT +package "moonbitlang/parser/cmd/wasm/mq" + +// Values + +// Errors + +// Types and methods +type CliAction + +type InputSource + +type LegacyOptions + +// Type aliases + +// Traits diff --git a/moon.mod.json b/moon.mod.json index 513ead8b..8ef75a7f 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -3,7 +3,9 @@ "version": "0.2.6", "deps": { "moonbitlang/x": "0.4.39", - "moonbitlang/yacc": "0.7.13" + "moonbitlang/yacc": "0.7.13", + "moonbit-community/miniio": "0.1.0", + "moonbitlang/async": "0.19.0" }, "bin-deps": { "moonbitlang/yacc": "0.7.12" From d8854446ad3906c400b28526cb3897af5ceee089 Mon Sep 17 00:00:00 2001 From: Yorkin Date: Thu, 14 May 2026 14:06:57 +0800 Subject: [PATCH 6/6] chore: bump version to 0.3.0 --- moon.mod.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moon.mod.json b/moon.mod.json index 8ef75a7f..b8b02e3f 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,6 +1,6 @@ { "name": "moonbitlang/parser", - "version": "0.2.6", + "version": "0.3.0", "deps": { "moonbitlang/x": "0.4.39", "moonbitlang/yacc": "0.7.13", @@ -23,4 +23,4 @@ "exclude": [ "test" ] -} \ No newline at end of file +}