///|
/// Pre-extract every `--fixture ` triple from `args`, returning the
/// extracted pairs and the remaining argv (to hand to argparse).
///
/// `--fixture` takes two adjacent values per occurrence, which doesn't fit
/// argparse's one-value-per-OptionArg model. Pulling the triples out here
/// preserves the original adjacency contract (each `` always belongs to
/// the immediately-preceding ``), regardless of where in argv they sit.
fn extract_fixtures(
args : ArrayView[String],
) -> (Array[(String, String)], Array[String]) {
let fixtures : Array[(String, String)] = []
let rest : Array[String] = []
let mut i = 0
while i < args.length() {
if args[i] == "--" {
for j = i; j < args.length(); j = j + 1 {
rest.push(args[j])
}
return (fixtures, rest)
} else if args[i] == "--fixture" {
// `--fixture` always consumes the next two argv slots. Erroring here
// (rather than letting it fall through to argparse) is required because
// argparse also declares `--fixture` for help rendering, so a malformed
// single-value form would otherwise be silently mis-parsed.
guard i + 2 < args.length() else {
println("error: --fixture requires two values: --fixture ")
exit_with_error()
return (fixtures, rest)
}
fixtures.push((args[i + 1], args[i + 2]))
i = i + 3
} else if args[i].has_prefix("--fixture=") {
// argparse accepts the inline `--fixture=spec` form because we declare
// `fixture` as an OptionArg for --help discovery. Reject it loudly here
// — the two-value contract requires separate-token form.
println("error: --fixture requires two values: --fixture ")
exit_with_error()
return (fixtures, rest)
} else {
rest.push(args[i])
i = i + 1
}
}
(fixtures, rest)
}
///|
/// argparse cannot distinguish eval-mode arguments after `--` from the file
/// positional. Remove that delimiter only when an eval option precedes it.
fn split_eval_arguments(args : Array[String]) -> (Array[String], Array[String]) {
let mut separator = -1
let mut has_eval = false
for i = 0; i < args.length(); i = i + 1 {
if args[i] == "-e" || args[i] == "--eval" || args[i].has_prefix("--eval=") {
has_eval = true
}
if args[i] == "--" {
separator = i
break
}
}
if !has_eval || separator < 0 {
return (args, [])
}
let command_args : Array[String] = []
let script_args : Array[String] = []
for i = 0; i < separator; i = i + 1 {
command_args.push(args[i])
}
for i = separator + 1; i < args.length(); i = i + 1 {
script_args.push(args[i])
}
(command_args, script_args)
}
///|
fn resolve_shell_identity(request : String, referrer : String?) -> String {
let base = match referrer {
Some(path) => @path.Path::dirname(path).to_string()
None => @env.current_dir().unwrap_or(".")
}
if @path.Path::is_absolute(request) {
@path.Path::normalize(request).to_string()
} else {
@path.Path::normalize(@path.Path::join(base, request).to_string()).to_string()
}
}
///|
fn resolve_shell_script(
request : String,
referrer : String?,
) -> @lib.LoadedScript raise {
let identity = resolve_shell_identity(request, referrer)
let source = @fs.read_file_to_string(identity)
@lib.LoadedScript(identity~, source~)
}
///|
fn resolve_shell_bytes(request : String, referrer : String?) -> Bytes raise {
let identity = resolve_shell_identity(request, referrer)
@fs.read_file_to_bytes(identity)
}
///|
fn main {
let (fixtures, remaining) = extract_fixtures(program_args(@env.args()))
let (command_args, eval_arguments) = split_eval_arguments(remaining)
let cmd = @argparse.Command(
"js_engine",
flags=[
@argparse.FlagArg("module", about="Run input as an ES module"),
@argparse.FlagArg(
"annex-b",
about="Enable Annex B (web compat) semantics",
),
],
options=[
@argparse.OptionArg(
"eval",
short='e',
about="Evaluate JavaScript source instead of reading a script file",
),
// Documented for --help only; the actual `--fixture ` triples
// are pre-extracted from argv by extract_fixtures() before argparse runs,
// because argparse OptionArgs accept a single value per occurrence.
@argparse.OptionArg(
"fixture",
action=Append,
about=" — register a module fixture (may be repeated)",
),
@argparse.OptionArg(
"module-specifier",
about="specifier to register the main module under",
),
],
positionals=[
@argparse.PositionArg("script", about="JavaScript file to execute"),
@argparse.PositionArg(
"script_args",
num_args=@argparse.ValueRange(lower=0),
allow_hyphen_values=true,
about="Arguments exposed to the script as globalThis.arguments",
),
],
about="JavaScript interpreter",
version="0.9.0",
)
let matches = cmd.parse(argv=command_args[:], env=Map([])) catch {
err => {
println("\{err}")
exit_with_error()
return
}
}
let is_module = matches.flags.get_or_default("module", false)
let annex_b = matches.flags.get_or_default("annex-b", false)
let eval_source : String? = match matches.values.get("eval") {
Some(values) if values.length() > 0 => Some(values[0])
_ => None
}
let script : String? = match matches.values.get("script") {
Some(values) if values.length() > 0 => Some(values[0])
_ => None
}
let positional_script_args = match matches.values.get("script_args") {
Some(values) => values
None => []
}
let script_args = if eval_arguments.length() > 0 {
eval_arguments
} else {
positional_script_args
}
if eval_source is Some(_) && script is Some(_) {
println("error: --eval cannot be combined with a script file")
exit_with_error()
return
}
let main_module_specifier = match matches.values.get("module-specifier") {
Some(arr) if arr.length() > 0 => arr[0]
_ => ""
}
match (eval_source, script) {
(None, None) => {
println(
"Usage: js_engine [--module] [--annex-b] [--fixture ]... ",
)
println(" js_engine -e ")
}
_ =>
try {
let clock_origin = @bench.monotonic_clock_start()
let shell = @lib.Shell(
load_source=resolve_shell_script,
read_bytes=Some(resolve_shell_bytes),
now_millis=Some(() => {
@bench.monotonic_clock_end(clock_origin) / 1000.0
}),
annex_b~,
)
let script_name = match script {
Some(path) => path
None => "-e"
}
if script is Some(_) || script_args.length() > 0 {
shell.set_arguments(script_name, script_args)
}
let execution : Result[Unit, Error] = Ok(
{
if is_module {
let src = match (eval_source, script) {
(Some(source), _) => source
(_, Some(path)) => @fs.read_file_to_string(path)
_ => ""
}
let registered_specifiers : @set.Set[String] = Default::default()
for fixture in fixtures {
let (specifier, _) = fixture
if registered_specifiers.contains(specifier) {
println("error: duplicate module specifier '\{specifier}'")
exit_with_error()
return
}
registered_specifiers.add(specifier)
}
if registered_specifiers.contains(main_module_specifier) {
println(
"error: duplicate module specifier '\{main_module_specifier}'",
)
exit_with_error()
return
}
// Run fixture modules plus the main module through the graph
// runner in the same Realm that owns the Shell host globals.
let modules : Array[(String, String)] = []
for f in fixtures {
modules.push(f)
}
modules.push((main_module_specifier, src))
shell.run_modules(modules)
} else {
match (eval_source, script) {
(Some(source), _) => shell.eval(source)
(_, Some(path)) => shell.run_file(path)
_ => ()
}
}
shell.drain_jobs()
},
) catch {
err => Err(err)
}
for line in shell.take_output() {
println(line)
}
match execution {
Ok(_) => ()
Err(err) => raise err
}
} catch {
@lib.JavaScriptException(message) => {
println(message)
exit_with_error()
}
@runtime.JsException(value) => {
match value {
Object(data) if data.class_name.has_suffix("Error") => {
let name : String = match data.bag.properties.get("name") {
Some(String_(s)) => s
_ => data.class_name
}
let msg : String = match data.bag.properties.get("message") {
Some(String_(s)) => s
_ => ""
}
println("\{name}: \{msg}")
}
Object(data) if data.bag.properties.contains("message") => {
// Error-like object (e.g. new Test262Error()): find constructor name from prototype
let ctor_name : String? = match data.prototype {
Object(proto_data) =>
match proto_data.bag.properties.get("constructor") {
Some(Object(ctor_data)) =>
match ctor_data.callable {
Some(UserFunc(fd)) => fd.name
Some(UserFuncExt(fd)) => fd.name
_ => None
}
_ => None
}
_ => None
}
// Only format as "Name: message" if the constructor name ends with "Error"
match ctor_name {
Some(name) if name.has_suffix("Error") => {
let msg : String = match data.bag.properties.get("message") {
Some(String_(s)) => s
_ => ""
}
println("\{name}: \{msg}")
}
_ => println("\{value}")
}
}
_ => println("\{value}")
}
exit_with_error()
}
Failure::Failure(msg) => {
println("Error: \{msg}")
exit_with_error()
}
e => {
match @errors.format_if_js_error(e) {
Some(msg) => println(msg)
None => println("Error: \{e}")
}
exit_with_error()
}
}
}
}