@tishlang/tish 2.2.7 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +2 -1
- package/crates/tish/src/cli_help.rs +15 -0
- package/crates/tish/src/main.rs +139 -12
- package/crates/tish/tests/integration_test.rs +65 -0
- package/crates/tish_ast/src/ast.rs +6 -2
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +1 -1
- package/crates/tish_compile/src/codegen.rs +3133 -582
- package/crates/tish_compile/src/infer.rs +1950 -56
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +17 -0
- package/crates/tish_compile_js/Cargo.toml +3 -0
- package/crates/tish_compile_js/src/codegen.rs +365 -9
- package/crates/tish_compile_js/src/lib.rs +2 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +229 -1
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +45 -1
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +6 -0
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +856 -140
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +15 -26
- package/crates/tish_resolve/src/lib.rs +188 -18
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
package/bin/tish
CHANGED
|
Binary file
|
|
@@ -350,7 +350,11 @@ fn convert_object_prop(
|
|
|
350
350
|
}
|
|
351
351
|
});
|
|
352
352
|
let value = convert_expr(&prop.value, ctx)?;
|
|
353
|
-
Ok(ObjectProp::KeyValue(
|
|
353
|
+
Ok(ObjectProp::KeyValue(
|
|
354
|
+
Arc::from(key.as_str()),
|
|
355
|
+
value,
|
|
356
|
+
tishlang_ast::Span::default(),
|
|
357
|
+
))
|
|
354
358
|
}
|
|
355
359
|
}
|
|
356
360
|
}
|
package/crates/tish/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tishlang"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.9.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Tish CLI - run, REPL, compile to native"
|
|
6
6
|
license-file = { workspace = true }
|
|
@@ -64,3 +64,4 @@ mimalloc = { version = "0.1", optional = true }
|
|
|
64
64
|
|
|
65
65
|
[dev-dependencies]
|
|
66
66
|
rayon = "1.11"
|
|
67
|
+
tempfile = "3"
|
|
@@ -406,6 +406,9 @@ pub fn build_after_help() -> String {
|
|
|
406
406
|
JavaScript bundle
|
|
407
407
|
{t}wasm{r}
|
|
408
408
|
WebAssembly (.tish project; .js source supported on some paths)
|
|
409
|
+
{t}wasm-gpu{r}
|
|
410
|
+
WebAssembly + the WebGPU reflection bridge — emits a start(chunk, host) loader that
|
|
411
|
+
bootstraps WebGPU (device/queue/context/canvas) into the `host` global your program reads
|
|
409
412
|
{t}wasi{r}
|
|
410
413
|
WASI WebAssembly
|
|
411
414
|
{t}bytecode{r}
|
|
@@ -479,6 +482,15 @@ pub(crate) struct RunArgs {
|
|
|
479
482
|
/// Disable AST and bytecode optimizations (for debugging).
|
|
480
483
|
#[arg(long, help_heading = "Options")]
|
|
481
484
|
pub no_optimize: bool,
|
|
485
|
+
/// Arguments passed through to the script as `process.argv` (after the file). Like
|
|
486
|
+
/// `node main.js a b`, options for `tish` itself must come BEFORE the file. #88
|
|
487
|
+
#[arg(
|
|
488
|
+
trailing_var_arg = true,
|
|
489
|
+
allow_hyphen_values = true,
|
|
490
|
+
value_name = "ARGS",
|
|
491
|
+
help_heading = "Arguments"
|
|
492
|
+
)]
|
|
493
|
+
pub script_args: Vec<String>,
|
|
482
494
|
}
|
|
483
495
|
|
|
484
496
|
#[derive(Parser)]
|
|
@@ -548,6 +560,9 @@ pub(crate) struct BuildArgs {
|
|
|
548
560
|
/// For `--target js` project builds: emit `OUTPUT.js.map` and `//# sourceMappingURL=…` so JS/TS tools can jump to original `.tish` (implies `--no-optimize` for that build).
|
|
549
561
|
#[arg(long, help_heading = "Options")]
|
|
550
562
|
pub source_map: bool,
|
|
563
|
+
/// For `--target js`: `bundle` (default) merges all modules into one file; `esm` emits one `.js` per `.tish` module with real `import`/`export` (so `-o` is a directory) for Vite/Rollup tree-shaking.
|
|
564
|
+
#[arg(long, default_value = "bundle", value_name = "FORMAT", help_heading = "Options")]
|
|
565
|
+
pub format: String,
|
|
551
566
|
/// Run the gradual type checker: `warn` prints `line:col` type diagnostics; `error` also fails the build on them. (Equivalent to setting `TISH_CHECK`.)
|
|
552
567
|
#[arg(long, value_name = "MODE", help_heading = "Options")]
|
|
553
568
|
pub check: Option<String>,
|
package/crates/tish/src/main.rs
CHANGED
|
@@ -107,12 +107,18 @@ fn main() {
|
|
|
107
107
|
let matches = cli_help::build_command().get_matches_from(&argv);
|
|
108
108
|
let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
|
|
109
109
|
let result = match cli.command {
|
|
110
|
-
Some(Commands::Run(a)) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
Some(Commands::Run(a)) => {
|
|
111
|
+
// Expose a node-shaped `process.argv` to the script: `[tish-exe, <file>, args...]`,
|
|
112
|
+
// so `tish run main.tish a b` gives the script `["…/tish", "main.tish", "a", "b"]`
|
|
113
|
+
// (not the `run` subcommand). Interp/VM read this via `tishlang_core::process_argv`. #88
|
|
114
|
+
let exe = std::env::args().next().unwrap_or_else(|| "tish".to_string());
|
|
115
|
+
let mut argv = Vec::with_capacity(a.script_args.len() + 2);
|
|
116
|
+
argv.push(exe);
|
|
117
|
+
argv.push(a.file.clone());
|
|
118
|
+
argv.extend(a.script_args.iter().cloned());
|
|
119
|
+
tishlang_core::set_process_argv(argv);
|
|
120
|
+
run_file(&a.file, &a.backend, &a.features, a.no_optimize || no_opt_env)
|
|
121
|
+
}
|
|
116
122
|
Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env, &a.features),
|
|
117
123
|
Some(Commands::Build(a)) => {
|
|
118
124
|
// `--check warn|error` drives the gradual type checker via the same channel as the
|
|
@@ -130,6 +136,7 @@ fn main() {
|
|
|
130
136
|
a.source_map,
|
|
131
137
|
a.ios_triple.as_deref(),
|
|
132
138
|
&a.crate_type,
|
|
139
|
+
&a.format,
|
|
133
140
|
)
|
|
134
141
|
}
|
|
135
142
|
Some(Commands::DumpAst {
|
|
@@ -535,7 +542,14 @@ fn compile_to_js(
|
|
|
535
542
|
output_path: &str,
|
|
536
543
|
optimize: bool,
|
|
537
544
|
source_map: bool,
|
|
545
|
+
format: &str,
|
|
538
546
|
) -> Result<(), String> {
|
|
547
|
+
if format != "bundle" && format != "esm" {
|
|
548
|
+
return Err(format!(
|
|
549
|
+
"Unknown --format '{}' for --target js. Use 'bundle' (single merged file) or 'esm' (one file per module).",
|
|
550
|
+
format
|
|
551
|
+
));
|
|
552
|
+
}
|
|
539
553
|
if source_map && optimize {
|
|
540
554
|
return Err(
|
|
541
555
|
"tish build --target js --source-map requires --no-optimize (mappings follow unmerged statement order)."
|
|
@@ -558,6 +572,9 @@ fn compile_to_js(
|
|
|
558
572
|
Some(p)
|
|
559
573
|
}
|
|
560
574
|
});
|
|
575
|
+
if format == "esm" {
|
|
576
|
+
return compile_to_js_esm(input_path, output_path, optimize, source_map, project_root);
|
|
577
|
+
}
|
|
561
578
|
let out_path = Path::new(output_path);
|
|
562
579
|
let out_path = if out_path.extension().is_none()
|
|
563
580
|
|| out_path.extension() == Some(std::ffi::OsStr::new(""))
|
|
@@ -629,6 +646,63 @@ fn compile_to_js(
|
|
|
629
646
|
Ok(())
|
|
630
647
|
}
|
|
631
648
|
|
|
649
|
+
/// `--target js --format esm`: emit one `.js` per `.tish` module under the output directory,
|
|
650
|
+
/// preserving the source tree layout, with real ES `import`/`export` so a bundler (Vite/Rollup)
|
|
651
|
+
/// can tree-shake and code-split. `-o` is treated as a directory.
|
|
652
|
+
fn compile_to_js_esm(
|
|
653
|
+
input_path: &Path,
|
|
654
|
+
output_path: &str,
|
|
655
|
+
optimize: bool,
|
|
656
|
+
source_map: bool,
|
|
657
|
+
project_root: Option<&Path>,
|
|
658
|
+
) -> Result<(), String> {
|
|
659
|
+
if source_map {
|
|
660
|
+
return Err(
|
|
661
|
+
"tish build --target js --format esm does not yet support --source-map; use --format bundle for source maps."
|
|
662
|
+
.into(),
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if input_path.extension().map(|e| e == "jsx") == Some(true)
|
|
666
|
+
|| input_path.extension().map(|e| e == "js") == Some(true)
|
|
667
|
+
{
|
|
668
|
+
return Err(
|
|
669
|
+
"tish build --target js --format esm is only supported for .tish entry files; use --format bundle for .jsx / .js inputs."
|
|
670
|
+
.into(),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
let modules = tishlang_compile_js::compile_project_esm(input_path, project_root, optimize)
|
|
674
|
+
.map_err(|e| format!("{}", e))?;
|
|
675
|
+
let out_dir = Path::new(output_path);
|
|
676
|
+
fs::create_dir_all(out_dir)
|
|
677
|
+
.map_err(|e| format!("Cannot create output directory {}: {}", out_dir.display(), e))?;
|
|
678
|
+
for module in &modules {
|
|
679
|
+
let out = out_dir.join(&module.relative_path);
|
|
680
|
+
if let Some(parent) = out.parent() {
|
|
681
|
+
fs::create_dir_all(parent)
|
|
682
|
+
.map_err(|e| format!("Cannot create output directory {}: {}", parent.display(), e))?;
|
|
683
|
+
}
|
|
684
|
+
fs::write(&out, &module.js)
|
|
685
|
+
.map_err(|e| format!("Cannot write {}: {}", out.display(), e))?;
|
|
686
|
+
}
|
|
687
|
+
println!("Built {} module(s) to {}", modules.len(), out_dir.display());
|
|
688
|
+
// The entry may sit in a subtree (the output is rooted at the directory common to every module,
|
|
689
|
+
// which can be an ancestor of the entry when deps live in sibling packages / node_modules), so
|
|
690
|
+
// point the user at the actual entry `.js` they should hand to a bundler.
|
|
691
|
+
let entry_js = input_path
|
|
692
|
+
.canonicalize()
|
|
693
|
+
.unwrap_or_else(|_| input_path.to_path_buf())
|
|
694
|
+
.with_extension("js");
|
|
695
|
+
if let Some(entry_rel) = modules
|
|
696
|
+
.iter()
|
|
697
|
+
.filter(|m| entry_js.ends_with(&m.relative_path))
|
|
698
|
+
.max_by_key(|m| m.relative_path.components().count())
|
|
699
|
+
.map(|m| m.relative_path.clone())
|
|
700
|
+
{
|
|
701
|
+
println!("Entry: {}", out_dir.join(entry_rel).display());
|
|
702
|
+
}
|
|
703
|
+
Ok(())
|
|
704
|
+
}
|
|
705
|
+
|
|
632
706
|
#[allow(clippy::vec_init_then_push, clippy::too_many_arguments)] // build_file maps CLI build flags 1:1
|
|
633
707
|
fn build_file(
|
|
634
708
|
input_path: &str,
|
|
@@ -640,6 +714,7 @@ fn build_file(
|
|
|
640
714
|
source_map: bool,
|
|
641
715
|
ios_triple: Option<&str>,
|
|
642
716
|
crate_type: &str,
|
|
717
|
+
format: &str,
|
|
643
718
|
) -> Result<(), String> {
|
|
644
719
|
let optimize = !no_optimize;
|
|
645
720
|
let input_path = Path::new(input_path)
|
|
@@ -649,17 +724,26 @@ fn build_file(
|
|
|
649
724
|
let is_js = input_path.extension().map(|e| e == "js") == Some(true);
|
|
650
725
|
|
|
651
726
|
if target == "js" {
|
|
652
|
-
return compile_to_js(&input_path, output_path, optimize, source_map);
|
|
727
|
+
return compile_to_js(&input_path, output_path, optimize, source_map, format);
|
|
653
728
|
}
|
|
654
729
|
|
|
655
|
-
|
|
730
|
+
// `wasm-gpu` (#277): same pipeline as `wasm` but builds the `--features gpu` WebGPU runtime and
|
|
731
|
+
// emits a `start(chunk, host)` loader that bootstraps WebGPU into the `host` global.
|
|
732
|
+
let wasm_gpu = target == "wasm-gpu";
|
|
733
|
+
|
|
734
|
+
if (target == "wasm" || wasm_gpu) && is_js {
|
|
656
735
|
let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
|
|
657
736
|
let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
|
|
658
|
-
return tishlang_wasm::compile_program_to_wasm(
|
|
659
|
-
|
|
737
|
+
return tishlang_wasm::compile_program_to_wasm(
|
|
738
|
+
&program,
|
|
739
|
+
Path::new(output_path),
|
|
740
|
+
optimize,
|
|
741
|
+
wasm_gpu,
|
|
742
|
+
)
|
|
743
|
+
.map_err(|e| format!("{}", e));
|
|
660
744
|
}
|
|
661
745
|
|
|
662
|
-
if target == "wasm" {
|
|
746
|
+
if target == "wasm" || wasm_gpu {
|
|
663
747
|
let project_root = input_path.parent().and_then(|p| {
|
|
664
748
|
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
665
749
|
p.parent()
|
|
@@ -672,6 +756,7 @@ fn build_file(
|
|
|
672
756
|
project_root,
|
|
673
757
|
Path::new(output_path),
|
|
674
758
|
optimize,
|
|
759
|
+
wasm_gpu,
|
|
675
760
|
)
|
|
676
761
|
.map_err(|e| e.to_string());
|
|
677
762
|
}
|
|
@@ -714,7 +799,7 @@ fn build_file(
|
|
|
714
799
|
|
|
715
800
|
if target != "native" {
|
|
716
801
|
return Err(format!(
|
|
717
|
-
"Unknown target: {}. Use 'native', 'js', 'wasm', 'wasi', or 'bytecode'.",
|
|
802
|
+
"Unknown target: {}. Use 'native', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
|
|
718
803
|
target
|
|
719
804
|
));
|
|
720
805
|
}
|
|
@@ -814,6 +899,48 @@ mod cli_tests {
|
|
|
814
899
|
}
|
|
815
900
|
}
|
|
816
901
|
|
|
902
|
+
#[test]
|
|
903
|
+
fn run_collects_trailing_script_args() {
|
|
904
|
+
// `tish run main.tish a --flag` → file=main.tish, script_args=[a, --flag] (#88).
|
|
905
|
+
let cli = Cli::try_parse_from(vec![
|
|
906
|
+
"tish".to_string(),
|
|
907
|
+
"run".to_string(),
|
|
908
|
+
"main.tish".to_string(),
|
|
909
|
+
"a".to_string(),
|
|
910
|
+
"--flag".to_string(),
|
|
911
|
+
])
|
|
912
|
+
.unwrap();
|
|
913
|
+
match cli.command {
|
|
914
|
+
Some(Commands::Run(a)) => {
|
|
915
|
+
assert_eq!(a.file, "main.tish");
|
|
916
|
+
assert_eq!(a.script_args, vec!["a".to_string(), "--flag".to_string()]);
|
|
917
|
+
}
|
|
918
|
+
_ => panic!("expected Run"),
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
#[test]
|
|
923
|
+
fn run_options_before_file_still_parse() {
|
|
924
|
+
// tish options come BEFORE the file; everything after the file is the script's. (#88)
|
|
925
|
+
let cli = Cli::try_parse_from(vec![
|
|
926
|
+
"tish".to_string(),
|
|
927
|
+
"run".to_string(),
|
|
928
|
+
"--backend".to_string(),
|
|
929
|
+
"interp".to_string(),
|
|
930
|
+
"main.tish".to_string(),
|
|
931
|
+
"x".to_string(),
|
|
932
|
+
])
|
|
933
|
+
.unwrap();
|
|
934
|
+
match cli.command {
|
|
935
|
+
Some(Commands::Run(a)) => {
|
|
936
|
+
assert_eq!(a.backend, "interp");
|
|
937
|
+
assert_eq!(a.file, "main.tish");
|
|
938
|
+
assert_eq!(a.script_args, vec!["x".to_string()]);
|
|
939
|
+
}
|
|
940
|
+
_ => panic!("expected Run"),
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
817
944
|
#[test]
|
|
818
945
|
fn explicit_subcommand_not_treated_as_file() {
|
|
819
946
|
let argv = argv_with_implicit_run(vec!["tish".to_string(), "repl".to_string()]);
|
|
@@ -673,6 +673,71 @@ fn test_module_private_binding_isolation() {
|
|
|
673
673
|
}
|
|
674
674
|
}
|
|
675
675
|
|
|
676
|
+
/// #282: `--target js --format esm` emits one ES module per `.tish` file with real
|
|
677
|
+
/// `import`/`export`, so two modules exporting the same name (`export fn activate`) keep separate
|
|
678
|
+
/// scopes instead of colliding. The merge-based pipeline (interp/VM/native and `--format bundle`)
|
|
679
|
+
/// still flattens both into one scope — a separately-tracked collision — so this test pins the ESM
|
|
680
|
+
/// JS target, which is the path that isolates them. Builds the module tree and runs it under Node.
|
|
681
|
+
#[test]
|
|
682
|
+
fn test_js_esm_export_collision() {
|
|
683
|
+
let bin = tish_bin();
|
|
684
|
+
if !bin.exists() {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
let main = workspace_root()
|
|
688
|
+
.join("tests")
|
|
689
|
+
.join("modules")
|
|
690
|
+
.join("esm")
|
|
691
|
+
.join("export_collision")
|
|
692
|
+
.join("main.tish");
|
|
693
|
+
if !main.exists() {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
let expected = "ab\n";
|
|
697
|
+
|
|
698
|
+
// JS ESM target: build the module tree, then load + run the entry under Node.
|
|
699
|
+
let node_available = Command::new("node")
|
|
700
|
+
.arg("--version")
|
|
701
|
+
.output()
|
|
702
|
+
.map(|o| o.status.success())
|
|
703
|
+
.unwrap_or(false);
|
|
704
|
+
if node_available {
|
|
705
|
+
let tmp = tempfile::tempdir().expect("tempdir");
|
|
706
|
+
let out_dir = tmp.path().join("esm_out");
|
|
707
|
+
let build = Command::new(&bin)
|
|
708
|
+
.args(["build"])
|
|
709
|
+
.arg(&main)
|
|
710
|
+
.args(["-o"])
|
|
711
|
+
.arg(&out_dir)
|
|
712
|
+
.args(["--target", "js", "--format", "esm"])
|
|
713
|
+
.current_dir(workspace_root())
|
|
714
|
+
.output()
|
|
715
|
+
.expect("build esm");
|
|
716
|
+
assert!(
|
|
717
|
+
build.status.success(),
|
|
718
|
+
"esm build failed: stderr={}",
|
|
719
|
+
String::from_utf8_lossy(&build.stderr)
|
|
720
|
+
);
|
|
721
|
+
let entry = out_dir.join("main.js");
|
|
722
|
+
assert!(entry.exists(), "esm entry {} not emitted", entry.display());
|
|
723
|
+
let out = Command::new("node")
|
|
724
|
+
.arg(&entry)
|
|
725
|
+
.current_dir(workspace_root())
|
|
726
|
+
.output()
|
|
727
|
+
.expect("run node");
|
|
728
|
+
assert!(
|
|
729
|
+
out.status.success(),
|
|
730
|
+
"esm JS run failed (collision not isolated?): stderr={}",
|
|
731
|
+
String::from_utf8_lossy(&out.stderr)
|
|
732
|
+
);
|
|
733
|
+
assert_eq!(
|
|
734
|
+
String::from_utf8_lossy(&out.stdout),
|
|
735
|
+
expected,
|
|
736
|
+
"esm export_collision mismatch on JS ESM target"
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
676
741
|
/// Ignored: tishlang_eval::run() does not run the event loop.
|
|
677
742
|
#[test]
|
|
678
743
|
#[cfg(feature = "http")]
|
|
@@ -541,10 +541,14 @@ pub enum ArrayElement {
|
|
|
541
541
|
Spread(Expr),
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
-
/// Object property: either a regular key-value pair or spread
|
|
544
|
+
/// Object property: either a regular key-value pair or spread.
|
|
545
|
+
///
|
|
546
|
+
/// The `Span` on `KeyValue` is the key token's source span, so diagnostics (e.g. duplicate-key)
|
|
547
|
+
/// can point at the key rather than the enclosing `{` (#143). Synthesized object literals use a
|
|
548
|
+
/// zero span. Per AST convention the span participates in PartialEq.
|
|
545
549
|
#[derive(Debug, Clone, PartialEq)]
|
|
546
550
|
pub enum ObjectProp {
|
|
547
|
-
KeyValue(Arc<str>, Expr),
|
|
551
|
+
KeyValue(Arc<str>, Expr, Span),
|
|
548
552
|
Spread(Expr),
|
|
549
553
|
}
|
|
550
554
|
|
|
@@ -135,7 +135,11 @@ pub fn includes(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
|
|
|
135
135
|
_ => 0,
|
|
136
136
|
};
|
|
137
137
|
for v in arr_borrow.iter().skip(start) {
|
|
138
|
-
|
|
138
|
+
// SameValueZero: like `===` but NaN matches NaN (JS `Array.prototype.includes`, unlike
|
|
139
|
+
// `indexOf` which stays strict). #247
|
|
140
|
+
if v.strict_eq(search)
|
|
141
|
+
|| matches!((v, search), (Value::Number(a), Value::Number(b)) if a.is_nan() && b.is_nan())
|
|
142
|
+
{
|
|
139
143
|
return Value::Bool(true);
|
|
140
144
|
}
|
|
141
145
|
}
|
|
@@ -462,6 +466,83 @@ pub fn find_index(arr: &Value, callback: &Value) -> Value {
|
|
|
462
466
|
Value::Number(-1.0)
|
|
463
467
|
}
|
|
464
468
|
|
|
469
|
+
/// `Array.prototype.findLast` — like [`find`] but iterates from the end; the callback still receives
|
|
470
|
+
/// the original index. Returns `null` (JS `undefined`) when nothing matches. #247
|
|
471
|
+
pub fn find_last(arr: &Value, callback: &Value) -> Value {
|
|
472
|
+
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
473
|
+
for i in (0..data.len()).rev() {
|
|
474
|
+
if cb
|
|
475
|
+
.call(&[Value::Number(data[i]), Value::Number(i as f64)])
|
|
476
|
+
.is_truthy()
|
|
477
|
+
{
|
|
478
|
+
return Value::Number(data[i]);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return Value::Null;
|
|
482
|
+
}
|
|
483
|
+
let arr = as_boxed_array(arr);
|
|
484
|
+
let arr = &*arr;
|
|
485
|
+
if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
|
|
486
|
+
let arr_borrow = arr.borrow();
|
|
487
|
+
for i in (0..arr_borrow.len()).rev() {
|
|
488
|
+
let v = &arr_borrow[i];
|
|
489
|
+
if cb.call(&[v.clone(), Value::Number(i as f64)]).is_truthy() {
|
|
490
|
+
return v.clone();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
Value::Null
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// `Array.prototype.findLastIndex` — like [`find_index`] but from the end; `-1` if no match. #247
|
|
498
|
+
pub fn find_last_index(arr: &Value, callback: &Value) -> Value {
|
|
499
|
+
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
500
|
+
for i in (0..data.len()).rev() {
|
|
501
|
+
if cb
|
|
502
|
+
.call(&[Value::Number(data[i]), Value::Number(i as f64)])
|
|
503
|
+
.is_truthy()
|
|
504
|
+
{
|
|
505
|
+
return Value::Number(i as f64);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return Value::Number(-1.0);
|
|
509
|
+
}
|
|
510
|
+
let arr = as_boxed_array(arr);
|
|
511
|
+
let arr = &*arr;
|
|
512
|
+
if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
|
|
513
|
+
let arr_borrow = arr.borrow();
|
|
514
|
+
for i in (0..arr_borrow.len()).rev() {
|
|
515
|
+
if cb
|
|
516
|
+
.call(&[arr_borrow[i].clone(), Value::Number(i as f64)])
|
|
517
|
+
.is_truthy()
|
|
518
|
+
{
|
|
519
|
+
return Value::Number(i as f64);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
Value::Number(-1.0)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/// `Array.prototype.at(index)` — negative `index` counts from the end; out of range → `null`
|
|
527
|
+
/// (JS `undefined`). A non-numeric index is `ToInteger`'d to 0, like JS. #247
|
|
528
|
+
pub fn at(arr: &Value, index: &Value) -> Value {
|
|
529
|
+
let i = match index {
|
|
530
|
+
Value::Number(n) => *n as i64,
|
|
531
|
+
_ => 0,
|
|
532
|
+
};
|
|
533
|
+
let arr = as_boxed_array(arr);
|
|
534
|
+
let arr = &*arr;
|
|
535
|
+
if let Value::Array(arr) = arr {
|
|
536
|
+
let arr_borrow = arr.borrow();
|
|
537
|
+
let len = arr_borrow.len() as i64;
|
|
538
|
+
let idx = if i < 0 { len + i } else { i };
|
|
539
|
+
if idx >= 0 && idx < len {
|
|
540
|
+
return arr_borrow[idx as usize].clone();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
Value::Null
|
|
544
|
+
}
|
|
545
|
+
|
|
465
546
|
pub fn some(arr: &Value, callback: &Value) -> Value {
|
|
466
547
|
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
467
548
|
for (i, &n) in data.iter().enumerate() {
|
|
@@ -206,25 +206,59 @@ pub fn parse_int(args: &[Value]) -> Value {
|
|
|
206
206
|
.first()
|
|
207
207
|
.map(Value::to_display_string)
|
|
208
208
|
.unwrap_or_default();
|
|
209
|
-
let
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
})
|
|
216
|
-
.unwrap_or(10);
|
|
209
|
+
let radix = args.get(1).and_then(|v| match v {
|
|
210
|
+
Value::Number(n) => Some(*n as i32),
|
|
211
|
+
_ => None,
|
|
212
|
+
});
|
|
213
|
+
Value::Number(js_parse_int(&s, radix))
|
|
214
|
+
}
|
|
217
215
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
216
|
+
/// JS `parseInt(string, radix)` semantics — shared so the tree-walk interpreter (distinct `Value`)
|
|
217
|
+
/// matches the vm/native exactly (#247). Skips leading whitespace + an optional sign, then strips a
|
|
218
|
+
/// leading `0x`/`0X` when radix is 16 (or omitted and the string is hex-prefixed); an omitted radix
|
|
219
|
+
/// otherwise defaults to 10. Reads the longest valid digit prefix (`parseInt("12px") === 12`),
|
|
220
|
+
/// accumulating in f64 to avoid the old i64-overflow `NaN`. `parseInt("0x1F", 16) === 31`.
|
|
221
|
+
pub fn js_parse_int(input: &str, radix_arg: Option<i32>) -> f64 {
|
|
222
|
+
let s = input.trim_start();
|
|
223
|
+
let (neg, rest) = if let Some(r) = s.strip_prefix('-') {
|
|
224
|
+
(true, r)
|
|
225
|
+
} else if let Some(r) = s.strip_prefix('+') {
|
|
226
|
+
(false, r)
|
|
227
|
+
} else {
|
|
228
|
+
(false, s)
|
|
229
|
+
};
|
|
230
|
+
let mut radix = radix_arg.unwrap_or(0);
|
|
231
|
+
let mut digits = rest;
|
|
232
|
+
if radix == 16 || radix == 0 {
|
|
233
|
+
if let Some(r) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
|
|
234
|
+
radix = 16;
|
|
235
|
+
digits = r;
|
|
225
236
|
}
|
|
226
237
|
}
|
|
227
|
-
|
|
238
|
+
if radix == 0 {
|
|
239
|
+
radix = 10;
|
|
240
|
+
}
|
|
241
|
+
if !(2..=36).contains(&radix) {
|
|
242
|
+
return f64::NAN;
|
|
243
|
+
}
|
|
244
|
+
let mut acc = 0.0_f64;
|
|
245
|
+
let mut any = false;
|
|
246
|
+
for c in digits.chars() {
|
|
247
|
+
match c.to_digit(radix as u32) {
|
|
248
|
+
Some(d) => {
|
|
249
|
+
acc = acc * radix as f64 + d as f64;
|
|
250
|
+
any = true;
|
|
251
|
+
}
|
|
252
|
+
None => break,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if !any {
|
|
256
|
+
f64::NAN
|
|
257
|
+
} else if neg {
|
|
258
|
+
-acc
|
|
259
|
+
} else {
|
|
260
|
+
acc
|
|
261
|
+
}
|
|
228
262
|
}
|
|
229
263
|
|
|
230
264
|
/// parseFloat(string)
|
|
@@ -16,7 +16,6 @@ math_unary!(abs, abs);
|
|
|
16
16
|
math_unary!(sqrt, sqrt);
|
|
17
17
|
math_unary!(floor, floor);
|
|
18
18
|
math_unary!(ceil, ceil);
|
|
19
|
-
math_unary!(round, round);
|
|
20
19
|
math_unary!(sin, sin);
|
|
21
20
|
math_unary!(cos, cos);
|
|
22
21
|
math_unary!(tan, tan);
|
|
@@ -30,20 +29,75 @@ math_unary!(exp, exp);
|
|
|
30
29
|
math_unary!(trunc, trunc);
|
|
31
30
|
math_unary!(cbrt, cbrt);
|
|
32
31
|
|
|
32
|
+
// --- f64-domain semantics (single source of truth) -------------------------------------------------
|
|
33
|
+
// These hold the JS-specific Math rules so every backend agrees: the vm/native paths call the
|
|
34
|
+
// `&[Value]` wrappers below, and the interpreter (which uses a different `Value` type) calls these f64
|
|
35
|
+
// helpers directly after extracting its own args. Keeping the rules here means they can't drift. #247
|
|
36
|
+
|
|
37
|
+
/// JS `Math.round`: ties round toward +∞ (not Rust's `.round()`, which rounds half away from zero),
|
|
38
|
+
/// and values in `[-0.5, 0)` return `-0`. `floor(n + 0.5)` gives the +∞-tie behavior; the guards keep
|
|
39
|
+
/// NaN/±∞ and the sign-of-zero edges correct (`Math.round(-2.5) === -2`, `Math.round(-0.5) === -0`).
|
|
40
|
+
pub fn round_f64(n: f64) -> f64 {
|
|
41
|
+
if n.is_nan() || n.is_infinite() || n == 0.0 {
|
|
42
|
+
n
|
|
43
|
+
} else if (-0.5..0.5).contains(&n) {
|
|
44
|
+
if n < 0.0 {
|
|
45
|
+
-0.0
|
|
46
|
+
} else {
|
|
47
|
+
0.0
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
(n + 0.5).floor()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// JS `Math.min` over already-extracted f64s: empty → `+∞`, any `NaN` → `NaN` (unlike `f64::min`,
|
|
55
|
+
/// which ignores NaN), and `-0` is preferred over `+0`.
|
|
56
|
+
pub fn min_f64(nums: &[f64]) -> f64 {
|
|
57
|
+
let mut acc = f64::INFINITY;
|
|
58
|
+
for &n in nums {
|
|
59
|
+
if n.is_nan() {
|
|
60
|
+
return f64::NAN;
|
|
61
|
+
}
|
|
62
|
+
if n < acc || (n == 0.0 && acc == 0.0 && n.is_sign_negative()) {
|
|
63
|
+
acc = n;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
acc
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// JS `Math.max` over already-extracted f64s: empty → `-∞`, any `NaN` → `NaN`, `+0` preferred over `-0`.
|
|
70
|
+
pub fn max_f64(nums: &[f64]) -> f64 {
|
|
71
|
+
let mut acc = f64::NEG_INFINITY;
|
|
72
|
+
for &n in nums {
|
|
73
|
+
if n.is_nan() {
|
|
74
|
+
return f64::NAN;
|
|
75
|
+
}
|
|
76
|
+
if n > acc || (n == 0.0 && acc == 0.0 && n.is_sign_positive()) {
|
|
77
|
+
acc = n;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
acc
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn round(args: &[Value]) -> Value {
|
|
84
|
+
Value::Number(round_f64(extract_num(args.first()).unwrap_or(f64::NAN)))
|
|
85
|
+
}
|
|
86
|
+
|
|
33
87
|
pub fn min(args: &[Value]) -> Value {
|
|
34
|
-
let
|
|
88
|
+
let nums: Vec<f64> = args
|
|
35
89
|
.iter()
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
Value::Number(
|
|
90
|
+
.map(|v| extract_num(Some(v)).unwrap_or(f64::NAN))
|
|
91
|
+
.collect();
|
|
92
|
+
Value::Number(min_f64(&nums))
|
|
39
93
|
}
|
|
40
94
|
|
|
41
95
|
pub fn max(args: &[Value]) -> Value {
|
|
42
|
-
let
|
|
96
|
+
let nums: Vec<f64> = args
|
|
43
97
|
.iter()
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
Value::Number(
|
|
98
|
+
.map(|v| extract_num(Some(v)).unwrap_or(f64::NAN))
|
|
99
|
+
.collect();
|
|
100
|
+
Value::Number(max_f64(&nums))
|
|
47
101
|
}
|
|
48
102
|
|
|
49
103
|
pub fn pow(args: &[Value]) -> Value {
|
|
@@ -21,7 +21,26 @@ pub fn to_fixed(n: &Value, digits: &Value) -> Value {
|
|
|
21
21
|
Value::Number(x) => (*x as i32).clamp(0, 20),
|
|
22
22
|
_ => 0,
|
|
23
23
|
} as usize;
|
|
24
|
-
Value::String(
|
|
24
|
+
Value::String(to_fixed_str(num, d).into())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// f64-domain `Number.prototype.toFixed` so the tree-walk interpreter (distinct `Value`) shares the
|
|
28
|
+
/// exact behavior. Rust's `{:.*}` rounds half-to-even (`(2.5).toFixed(0)` → "2"); JS rounds half away
|
|
29
|
+
/// from zero (→ "3"), so pre-round the scaled value with `.round()` (half-away) before formatting. #247
|
|
30
|
+
pub fn to_fixed_str(num: f64, digits: usize) -> String {
|
|
31
|
+
if num.is_nan() {
|
|
32
|
+
return "NaN".to_string();
|
|
33
|
+
}
|
|
34
|
+
if num.is_infinite() {
|
|
35
|
+
return if num < 0.0 { "-Infinity" } else { "Infinity" }.to_string();
|
|
36
|
+
}
|
|
37
|
+
// JS switches to exponential form for |num| >= 1e21; defer to default formatting there.
|
|
38
|
+
if num.abs() >= 1e21 {
|
|
39
|
+
return format!("{}", num);
|
|
40
|
+
}
|
|
41
|
+
let factor = 10f64.powi(digits as i32);
|
|
42
|
+
let rounded = (num * factor).round() / factor;
|
|
43
|
+
format!("{:.*}", digits, rounded)
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
/// `Number.prototype.toString([radix])` — ECMA-262 §21.1.3.6.
|