// Copyright (C) 2025 International Digital Economy Academy
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; version 2.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see .
///|
async fn println_to_stderr(msg : String) -> Unit noraise {
@stdio.stderr.write(msg + "\n") catch {
_ => ()
}
}
///|
#cfg(target="wasm")
fn runtime_exit(code : Int32) -> Unit = "wasi_snapshot_preview1" "proc_exit"
///|
#cfg(target="native")
extern "c" fn runtime_exit(code : Int32) -> Unit = "exit"
///|
fn[T] exit(code : Int32) -> T {
runtime_exit(code)
panic()
}
///|
async fn write_text_file(file : String, content : String) -> Unit {
@fs.write_file(file, content, create_mode=CreateOrTruncate)
}
///|
priv suberror CliError {
UnsupportedMode(String)
UnsupportedInputMode(String)
UnsupportedInputLayout(String)
UnsupportedBackend(String)
UnsupportedOptimizationLevel(String)
ConflictingBackendOptions
}
///|
priv struct CliOptions {
input_file : String
output_file : String
output_map_file : String?
mode : @driver.Mode
input_mode : @driver.InputMode
input_layout : @codegen.InputLayout
external_tokens : Bool
no_comments : Bool
no_std : Bool
direct : Bool
table : Bool
compress : Bool
optimization_level : @codegen.OptimizationLevel
token_payload_rewrite : @driver.TokenPayloadRewrite
force_int_position : Bool
print_as_mly_without_actions : Bool
}
///|
fn cli_command() -> @argparse.Command {
Command(
"moonyacc",
version=MODULE_VERSION,
disable_version_flag=true,
flags=[
FlagArg(
"external-tokens",
long="external-tokens",
about="Use external tokens",
),
FlagArg(
"no-comments",
long="no-comments",
about="Do not include comments in the output",
),
FlagArg("no-std", long="no-std", about="Do not include standard library"),
FlagArg("table", long="table", about="Use table engine"),
FlagArg(
"compress-table",
long="compress-table",
about="Use table engine with compression",
),
FlagArg(
"version",
short='v',
long="version",
action=Version,
about="Show version",
),
FlagArg(
"print-as-mly-without-actions",
long="print-as-mly-without-actions",
about="Print as mly without actions",
),
FlagArg(
"force-token-json-payload",
long="force-token-json-payload",
about="Force token to use JSON payload",
),
FlagArg(
"force-token-no-payload",
long="force-token-no-payload",
about="Force token to not use payload",
),
FlagArg(
"force-int-position",
long="force-int-position",
about="Overwrite the position type to int",
),
],
options=[
OptionArg(
"optimization-level",
short='O',
long="optimization-level",
action=Append,
about="Direct backend optimization level (0, 1, 2; default: 1). Level 2 specializes states and optimizes runtime storage; level 0 currently uses level 1",
),
OptionArg(
"backend",
long="backend",
about="Specify backend (code, direct, table). Default is code",
),
OptionArg(
"output-file",
short='o',
long="output-file",
about="Output file",
),
OptionArg(
"output-map-file",
long="output-map-file",
about="Output source map file, if not specified, it will be the output file with .map.json extension",
),
OptionArg(
"mode",
long="mode",
about="Specify mode (default, json-cst, only-tokens)",
),
OptionArg(
"input-mode",
long="input-mode",
about="Specify input mode (array, pull). Default is array",
),
OptionArg(
"input-layout",
long="input-layout",
about="Input representation (located, token-only). Token-only requires Unit positions; default: located",
),
],
positionals=[
PositionArg("input-file", num_args=@argparse.ValueRange::single()),
],
)
}
///|
fn string_value(
matches : @argparse.Matches,
name : String,
default : String,
) -> String {
match matches.values.get(name) {
Some(values) => values[0]
None => default
}
}
///|
fn string_option(matches : @argparse.Matches, name : String) -> String? {
matches.values.get(name).map(values => values[0])
}
///|
fn flag_value(matches : @argparse.Matches, name : String) -> Bool {
matches.flags.get(name).unwrap_or(false)
}
///|
fn parse_mode(mode_sym : String) -> @driver.Mode raise CliError {
match mode_sym {
"default" => Default
"json-cst" => JsonCst
"only-tokens" => OnlyTokens
_ => raise UnsupportedMode(mode_sym)
}
}
///|
fn parse_input_mode(
input_mode_sym : String,
) -> @driver.InputMode raise CliError {
match input_mode_sym {
"array" => Array
"pull" => Pull
_ => raise UnsupportedInputMode(input_mode_sym)
}
}
///|
fn parse_input_layout(layout : String) -> @codegen.InputLayout raise CliError {
match layout {
"located" => Located
"token-only" => TokenOnly
_ => raise UnsupportedInputLayout(layout)
}
}
///|
fn parse_token_payload_rewrite(
argv : ArrayView[String],
) -> @driver.TokenPayloadRewrite {
let mut rewrite : @driver.TokenPayloadRewrite = NoRewrite
for arg in argv {
match arg {
"--force-token-json-payload" => rewrite = JsonPayload
"--force-token-no-payload" => rewrite = NoPayload
_ => ()
}
}
rewrite
}
///|
fn parse_optimization_level(
matches : @argparse.Matches,
) -> @codegen.OptimizationLevel raise CliError {
let mut level : @codegen.OptimizationLevel = O1
if matches.values.get("optimization-level") is Some(values) {
for value in values {
level = match value {
"0" => O0
"1" => O1
"2" => O2
_ => raise UnsupportedOptimizationLevel(value)
}
}
}
level
}
///|
fn parse_cli_args(argv : ArrayView[String]) -> CliOptions raise {
let matches = cli_command().parse(argv~, env={})
let compress = flag_value(matches, "compress-table")
let legacy_table = compress || flag_value(matches, "table")
let (direct, table) = match string_option(matches, "backend") {
None => (false, legacy_table)
Some("table") => (false, true)
Some("direct") | Some("code") as backend => {
if legacy_table {
raise ConflictingBackendOptions
}
(backend == Some("direct"), false)
}
Some(name) => raise UnsupportedBackend(name)
}
{
input_file: string_value(matches, "input-file", ""),
output_file: string_value(matches, "output-file", ""),
output_map_file: string_option(matches, "output-map-file"),
mode: parse_mode(string_value(matches, "mode", "default")),
input_mode: parse_input_mode(string_value(matches, "input-mode", "array")),
input_layout: parse_input_layout(
string_value(matches, "input-layout", "located"),
),
external_tokens: flag_value(matches, "external-tokens"),
no_comments: flag_value(matches, "no-comments"),
no_std: flag_value(matches, "no-std"),
direct,
table,
compress,
optimization_level: parse_optimization_level(matches),
token_payload_rewrite: parse_token_payload_rewrite(argv),
force_int_position: flag_value(matches, "force-int-position"),
print_as_mly_without_actions: flag_value(
matches, "print-as-mly-without-actions",
),
}
}
///|
async fn main {
let argv = @env.args()
let options = parse_cli_args(if argv.length() > 1 { argv[1:] } else { [] }) catch {
CliError::UnsupportedMode(mode_sym) => {
println_to_stderr("Unsupported mode: \{mode_sym}")
exit(1)
}
CliError::UnsupportedInputMode(input_mode_sym) => {
println_to_stderr("Unsupported input mode: \{input_mode_sym}")
exit(1)
}
CliError::UnsupportedInputLayout(layout) => {
println_to_stderr(
"Unsupported input layout: \{layout} (expected located or token-only)",
)
exit(1)
}
CliError::UnsupportedBackend(name) => {
println_to_stderr("Unsupported backend: \{name}")
exit(1)
}
CliError::UnsupportedOptimizationLevel(level) => {
println_to_stderr(
"Unsupported optimization level: \{level} (expected 0, 1, or 2)",
)
exit(1)
}
CliError::ConflictingBackendOptions => {
println_to_stderr(
"--backend code/direct cannot be combined with --table or --compress-table",
)
exit(1)
}
err => {
println_to_stderr(Show::to_string(err))
exit(1)
}
}
let parser_spec_src = @fs.read_file(options.input_file).text() catch {
err => abort(Show::to_string(err))
}
if options.print_as_mly_without_actions {
let out = StringBuilder()
@driver.print(parser_spec_src, filename=options.input_file, out~)
exit(0)
}
let source_map = @codegen.SourceMap::new()
let output = @driver.compile(
parser_spec_src,
mode=options.mode,
source_map_builder=source_map,
input_mode=options.input_mode,
input_layout=options.input_layout,
filename=options.input_file,
external_tokens=options.external_tokens,
no_comments=options.no_comments,
no_std=options.no_std,
token_payload_rewrite=options.token_payload_rewrite,
force_int_position=options.force_int_position,
generator=if options.direct {
@gen_mbt_direct.new(optimization_level=options.optimization_level)
} else if options.table {
if options.compress {
@gen_mbt_table.compress_generator
} else {
@gen_mbt_table.generator
}
} else {
@gen_mbt.generator
},
)
if options.output_file == "" {
@stdio.stdout.write(output + "\n")
if options.output_map_file is Some(file) {
try! write_text_file(
file,
ToJson::to_json(source_map).stringify(indent=2),
)
}
} else {
try! ({
write_text_file(options.output_file, output)
write_text_file(
options.output_map_file.unwrap_or("\{options.output_file}.map.json"),
ToJson::to_json(source_map).stringify(indent=2),
)
})
}
}