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..b8b02e3f 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,9 +1,11 @@ { "name": "moonbitlang/parser", - "version": "0.2.6", + "version": "0.3.0", "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" @@ -21,4 +23,4 @@ "exclude": [ "test" ] -} \ No newline at end of file +} 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 new file mode 100644 index 00000000..733da1ca --- /dev/null +++ b/moon_config/ast.mbt @@ -0,0 +1,21 @@ +///| +pub(all) enum Ast { + 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 + Bool(_, loc~) => loc + Str(_, loc~) => loc + Float(_, loc~) => loc + Arr(_, loc~) => loc + Obj(_, loc~) => loc + } +} diff --git a/pkg_parser/imports.mbt b/moon_config/imports.mbt similarity index 80% rename from pkg_parser/imports.mbt rename to moon_config/imports.mbt index dd0f9fd6..22550cf6 100644 --- a/pkg_parser/imports.mbt +++ b/moon_config/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/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/pkg_parser/moon.pkg b/moon_config/moon.pkg similarity index 51% rename from pkg_parser/moon.pkg rename to moon_config/moon.pkg index 40731568..b915c15c 100644 --- a/pkg_parser/moon.pkg +++ b/moon_config/moon.pkg @@ -2,10 +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 new file mode 100644 index 00000000..0066bbd5 --- /dev/null +++ b/moon_config/moon_config_test.mbt @@ -0,0 +1,427 @@ +///| +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 "parse_moon_pkg postprocesses options and repeatable fields" { + 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") + #| + let (ast, reports) = parse_moon_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + 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"))]), + #| ), + #| ], + #|) + ), + ) +} + +///| +test "parse_moon_pkg keeps repeated top-level rule entries in Ast" { + let source = + #|rule(name:"rule1", command:"exe") + #|rule(name:"rule2", command:"exe") + #| + 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"))])), + #| ], + #|) + ), + ) +} + +///| +test "parse_moon_pkg 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" ], + #| }, + #|) + #| + let (ast, reports) = parse_moon_pkg(source) + debug_inspect( + reports, + content=( + #|[] + ), + ) + debug_inspect( + AstNoLoc(ast), + content=( + #|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 "parse_moon_pkg 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, + #| }, + #| }, + #|) + #| + 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 "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_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 "parse_moon_mod handles 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", + #|) + #| + 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 "parse_moon_work parses workspace config" { + let source = + #|members = ["./app", "./shared"] + #|preferred_target = "wasm-gc" + #| + 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 "parse_moon_work handles multiline members" { + let source = + #|members = [ + #| "./mod_a", + #| "./mod_b", + #|] + #| + 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 "parse_moon_mod reports duplicate non-repeatable keys" { + let source = + #|name = "user/first" + #|name = "user/second" + #| + 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"))]) + ), + ) +} 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/pkg_parser/parser.mbt b/moon_config/parser.mbt similarity index 92% rename from pkg_parser/parser.mbt rename to moon_config/parser.mbt index 759c0a2d..7705e909 100644 --- a/pkg_parser/parser.mbt +++ b/moon_config/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(map=Map::from_array(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(map=Map([]), loc~)) + return (name, Obj(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(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( - map=Map::from_array([ - ("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~)) } } } @@ -609,10 +609,10 @@ 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(map=Map([]), loc~), []) + return (Obj(Vector([]), loc~), []) } let start = tokens[0].1 let state = State::{ @@ -624,18 +624,12 @@ 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( - map=Map::from_array(statements), - loc=state.loc_start_with(object_start), - ) + let ast = Obj(Vector(statements), loc=state.loc_start_with(object_start)) (ast, state.diagnostics) } ///| -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 = [] @@ -648,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 new file mode 100644 index 00000000..c551408f --- /dev/null +++ b/moon_config/pkg.generated.mbti @@ -0,0 +1,34 @@ +// Generated using `moon info`, DON'T EDIT IT +package "moonbitlang/parser/moon_config" + +import { + "moonbitlang/core/debug", + "moonbitlang/core/immut/vector", + "moonbitlang/parser/basic", +} + +// Values +pub fn parse_moon_mod(name? : String, String) -> (Ast, Array[@basic.Report]) + +pub fn parse_moon_pkg(name? : String, String) -> (Ast, Array[@basic.Report]) + +pub fn parse_moon_work(name? : String, String) -> (Ast, Array[@basic.Report]) + +// Errors + +// Types and methods +pub(all) enum Ast { + 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::loc(Self) -> @basic.Location +pub impl ToJson for Ast + +// Type aliases + +// Traits + 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 + } +} diff --git a/pkg_parser/ast.mbt b/pkg_parser/ast.mbt deleted file mode 100644 index 7bd55b28..00000000 --- a/pkg_parser/ast.mbt +++ /dev/null @@ -1,77 +0,0 @@ -///| -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(map~ : Map[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 - } -} - -///| -pub fn Ast::as_bool(self : Self) -> Bool? { - match self { - True(_) => Some(true) - False(_) => Some(false) - _ => 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) -> Array[Ast]? { - match self { - Arr(content~, ..) => Some(content) - _ => None - } -} - -///| -pub fn Ast::as_object(self : Self) -> Map[String, Ast]? { - match self { - Obj(map~, ..) => Some(map) - _ => None - } -} - -///| -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(map~, loc~) => { "type": "Obj", "map": map, "loc": loc } - } -} diff --git a/pkg_parser/pkg.generated.mbti b/pkg_parser/pkg.generated.mbti deleted file mode 100644 index 0e71b779..00000000 --- a/pkg_parser/pkg.generated.mbti +++ /dev/null @@ -1,41 +0,0 @@ -// Generated using `moon info`, DON'T EDIT IT -package "moonbitlang/parser/pkg_parser" - -import { - "moonbitlang/core/debug", - "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_file(String) -> (Ast, Array[@basic.Report]) raise @fs.IOError - -pub fn parse_string(String, name? : String) -> (Ast, Array[@basic.Report]) - -// Errors - -// 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(map~ : Map[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_string(Self) -> String? -pub fn Ast::loc(Self) -> @basic.Location -pub impl ToJson for Ast - -// Type aliases - -// Traits - diff --git a/pkg_parser/pkg_parser_test.mbt b/pkg_parser/pkg_parser_test.mbt deleted file mode 100644 index 86bb9dd7..00000000 --- a/pkg_parser/pkg_parser_test.mbt +++ /dev/null @@ -1,347 +0,0 @@ -///| -fn parse_pkg(source : String) -> (Ast, Array[@basic.Report]) { - parse_string(source, name="moon.pkg") -} - -///| -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~, ..) => - Json::object(Map::from_array([("$number", Json::string(float))])) - Arr(content~, ..) => Json::array(content.map(ast_summary)) - Obj(map~, ..) => { - let object : Map[String, Json] = Map([]) - for key, value in map { - object[key] = ast_summary(value) - } - Json::object(object) - } - } -} - -///| -fn report_contains(reports : Array[@basic.Report], needle : String) -> Bool { - for report in reports { - if report.msg.contains(needle) { - return true - } - } - 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 "pkg_parser parse nested options" { - let source = - #|options( - #| nested: { "a": ["x", "y", 123], "b": { "c": false } } - #|) - #| - 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 "pkg_parser 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 "pkg_parser 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] }, - ) - json_inspect(ast_summary(ast), content={ "import": ["path/to/pkg", "p"] }) -} - -///| -test "pkg_parser 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] }, - ) - json_inspect(ast_summary(ast), content={ - "import": ["a", { "path": "b", "alias": "ok" }], - }) -} - -///| -test "pkg_parser avoids cascading after invalid import kind" { - let source = - #|import { "a" } for next = true - #| - let (ast, reports) = parse_pkg(source) - json_inspect( - report_checks(reports, [ - "string literal \"test\" or \"wbtest\"", "`;` or EOF", - ]), - content={ "count": 2, "contains": [true, true] }, - ) - json_inspect(ast_summary(ast), content={ "import": ["a"] }) -} - -///| -test "pkg_parser 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 "pkg_parser 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 "pkg_parser 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 "pkg_parser keeps next statement after missing assignment value before semicolon" { - let source = - #|a = ; - #|b = 1 - #| - 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 "pkg_parser keeps next statement after missing assignment value on next line" { - let source = - #|a = - #|b = 1 - #| - 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 "pkg_parser rejects unsupported numeric literals" { - let source = - #|unsigned = 1U - #|floaty = 1.5 - #| - 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 "pkg_parser accepts bigint literal in pkg values" { - let source = - #|big = 1N - #| - 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 "pkg_parser 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 "pkg_parser 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 "pkg_parser reports missing import right brace at eof" { - let source = - #|import { - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`}`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "import": [] }) -} - -///| -test "pkg_parser reports missing apply right paren at eof" { - let source = - #|options( - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`)`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "options": {} }) -} - -///| -test "pkg_parser reports missing array right bracket at eof" { - let source = - #|a = [ - #| - let (ast, reports) = parse_pkg(source) - json_inspect(report_checks(reports, ["`]`"]), content={ - "count": 1, - "contains": [true], - }) - json_inspect(ast_summary(ast), content={ "a": [] }) -} - -///| -test "pkg_parser keeps next statement after missing apply left paren" { - let source = - #|options nested: true - #|next = false - #| - 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 }) -} - -///| -test "pkg_parser keeps next statement after missing import left brace" { - let source = - #|import [ "a/pkg" ] - #|next = false - #| - 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 }) -} - -///| -test "pkg_parser root loc stays ordered for layout-only source" { - let source = - #| - #| - 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)), - ]), - ), - content={ - "ast": {}, - "reports": { "count": 0, "contains": [] }, - "ordered": true, - }, - ) -}