@tishlang/tish 2.2.7 → 2.8.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 +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- 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 +1386 -42
- package/crates/tish_compile/src/infer.rs +4 -4
- 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/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- 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
|
@@ -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)]
|
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
|
|
@@ -652,14 +658,23 @@ fn build_file(
|
|
|
652
658
|
return compile_to_js(&input_path, output_path, optimize, source_map);
|
|
653
659
|
}
|
|
654
660
|
|
|
655
|
-
|
|
661
|
+
// `wasm-gpu` (#277): same pipeline as `wasm` but builds the `--features gpu` WebGPU runtime and
|
|
662
|
+
// emits a `start(chunk, host)` loader that bootstraps WebGPU into the `host` global.
|
|
663
|
+
let wasm_gpu = target == "wasm-gpu";
|
|
664
|
+
|
|
665
|
+
if (target == "wasm" || wasm_gpu) && is_js {
|
|
656
666
|
let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
|
|
657
667
|
let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
|
|
658
|
-
return tishlang_wasm::compile_program_to_wasm(
|
|
659
|
-
|
|
668
|
+
return tishlang_wasm::compile_program_to_wasm(
|
|
669
|
+
&program,
|
|
670
|
+
Path::new(output_path),
|
|
671
|
+
optimize,
|
|
672
|
+
wasm_gpu,
|
|
673
|
+
)
|
|
674
|
+
.map_err(|e| format!("{}", e));
|
|
660
675
|
}
|
|
661
676
|
|
|
662
|
-
if target == "wasm" {
|
|
677
|
+
if target == "wasm" || wasm_gpu {
|
|
663
678
|
let project_root = input_path.parent().and_then(|p| {
|
|
664
679
|
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
665
680
|
p.parent()
|
|
@@ -672,6 +687,7 @@ fn build_file(
|
|
|
672
687
|
project_root,
|
|
673
688
|
Path::new(output_path),
|
|
674
689
|
optimize,
|
|
690
|
+
wasm_gpu,
|
|
675
691
|
)
|
|
676
692
|
.map_err(|e| e.to_string());
|
|
677
693
|
}
|
|
@@ -714,7 +730,7 @@ fn build_file(
|
|
|
714
730
|
|
|
715
731
|
if target != "native" {
|
|
716
732
|
return Err(format!(
|
|
717
|
-
"Unknown target: {}. Use 'native', 'js', 'wasm', 'wasi', or 'bytecode'.",
|
|
733
|
+
"Unknown target: {}. Use 'native', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
|
|
718
734
|
target
|
|
719
735
|
));
|
|
720
736
|
}
|
|
@@ -814,6 +830,48 @@ mod cli_tests {
|
|
|
814
830
|
}
|
|
815
831
|
}
|
|
816
832
|
|
|
833
|
+
#[test]
|
|
834
|
+
fn run_collects_trailing_script_args() {
|
|
835
|
+
// `tish run main.tish a --flag` → file=main.tish, script_args=[a, --flag] (#88).
|
|
836
|
+
let cli = Cli::try_parse_from(vec![
|
|
837
|
+
"tish".to_string(),
|
|
838
|
+
"run".to_string(),
|
|
839
|
+
"main.tish".to_string(),
|
|
840
|
+
"a".to_string(),
|
|
841
|
+
"--flag".to_string(),
|
|
842
|
+
])
|
|
843
|
+
.unwrap();
|
|
844
|
+
match cli.command {
|
|
845
|
+
Some(Commands::Run(a)) => {
|
|
846
|
+
assert_eq!(a.file, "main.tish");
|
|
847
|
+
assert_eq!(a.script_args, vec!["a".to_string(), "--flag".to_string()]);
|
|
848
|
+
}
|
|
849
|
+
_ => panic!("expected Run"),
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
#[test]
|
|
854
|
+
fn run_options_before_file_still_parse() {
|
|
855
|
+
// tish options come BEFORE the file; everything after the file is the script's. (#88)
|
|
856
|
+
let cli = Cli::try_parse_from(vec![
|
|
857
|
+
"tish".to_string(),
|
|
858
|
+
"run".to_string(),
|
|
859
|
+
"--backend".to_string(),
|
|
860
|
+
"interp".to_string(),
|
|
861
|
+
"main.tish".to_string(),
|
|
862
|
+
"x".to_string(),
|
|
863
|
+
])
|
|
864
|
+
.unwrap();
|
|
865
|
+
match cli.command {
|
|
866
|
+
Some(Commands::Run(a)) => {
|
|
867
|
+
assert_eq!(a.backend, "interp");
|
|
868
|
+
assert_eq!(a.file, "main.tish");
|
|
869
|
+
assert_eq!(a.script_args, vec!["x".to_string()]);
|
|
870
|
+
}
|
|
871
|
+
_ => panic!("expected Run"),
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
817
875
|
#[test]
|
|
818
876
|
fn explicit_subcommand_not_treated_as_file() {
|
|
819
877
|
let argv = argv_with_implicit_run(vec!["tish".to_string(), "repl".to_string()]);
|
|
@@ -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.
|
|
@@ -215,15 +215,39 @@ pub fn substr(s: &Value, start: &Value, length: &Value) -> Value {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
pub fn split(s: &Value, sep: &Value) -> Value {
|
|
218
|
+
split_limit(s, sep, None)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// `String.prototype.split(sep, limit)` for a string separator. JS semantics: the string is split
|
|
222
|
+
/// completely on `sep`, then the result is truncated to `limit` elements (it does NOT keep the
|
|
223
|
+
/// unsplit remainder in the last slot, which is what Rust's `splitn` would do). `limit == 0` yields
|
|
224
|
+
/// an empty array. This is the single source of truth shared by the VM and rust/cranelift/wasi
|
|
225
|
+
/// backends; the interpreter mirrors it in `tish_eval::regex::string_split`.
|
|
226
|
+
pub fn split_limit(s: &Value, sep: &Value, limit: Option<usize>) -> Value {
|
|
218
227
|
if let Value::String(s) = s {
|
|
219
228
|
let separator = match sep {
|
|
220
229
|
Value::String(ss) => ss.as_str(),
|
|
221
230
|
_ => return Value::Array(VmRef::new(vec![Value::String(s.clone())])),
|
|
222
231
|
};
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
if limit == Some(0) {
|
|
233
|
+
return Value::Array(VmRef::new(Vec::new()));
|
|
234
|
+
}
|
|
235
|
+
// JS `split("")` is special: it yields the string's characters with NO surrounding empties
|
|
236
|
+
// (`"xyz".split("")` → `["x","y","z"]`, `"".split("")` → `[]`), unlike Rust's `str::split("")`
|
|
237
|
+
// which emits leading/trailing `""`. (tish splits on `char`s; lone-surrogate behavior for
|
|
238
|
+
// astral code points is out of scope.) #247
|
|
239
|
+
if separator.is_empty() {
|
|
240
|
+
let mut parts: Vec<Value> =
|
|
241
|
+
s.chars().map(|c| Value::String(c.to_string().into())).collect();
|
|
242
|
+
if let Some(max) = limit {
|
|
243
|
+
parts.truncate(max);
|
|
244
|
+
}
|
|
245
|
+
return Value::Array(VmRef::new(parts));
|
|
246
|
+
}
|
|
247
|
+
let mut parts: Vec<Value> = s.split(separator).map(|p| Value::String(p.into())).collect();
|
|
248
|
+
if let Some(max) = limit {
|
|
249
|
+
parts.truncate(max);
|
|
250
|
+
}
|
|
227
251
|
Value::Array(VmRef::new(parts))
|
|
228
252
|
} else {
|
|
229
253
|
Value::Null
|
|
@@ -364,6 +388,24 @@ pub fn char_at(s: &Value, idx: &Value) -> Value {
|
|
|
364
388
|
}
|
|
365
389
|
}
|
|
366
390
|
|
|
391
|
+
/// `String.prototype.at(index)` — like `charAt` but negative `index` counts from the end and an
|
|
392
|
+
/// out-of-range index yields `null` (JS `undefined`), not `""`. #247
|
|
393
|
+
pub fn at(s: &Value, index: &Value) -> Value {
|
|
394
|
+
if let Value::String(s) = s {
|
|
395
|
+
let i = match index {
|
|
396
|
+
Value::Number(n) => *n as i64,
|
|
397
|
+
_ => 0,
|
|
398
|
+
};
|
|
399
|
+
let chars: Vec<char> = s.chars().collect();
|
|
400
|
+
let len = chars.len() as i64;
|
|
401
|
+
let idx = if i < 0 { len + i } else { i };
|
|
402
|
+
if idx >= 0 && idx < len {
|
|
403
|
+
return Value::String(chars[idx as usize].to_string().into());
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
Value::Null
|
|
407
|
+
}
|
|
408
|
+
|
|
367
409
|
pub fn char_code_at(s: &Value, idx: &Value) -> Value {
|
|
368
410
|
if let Value::String(s) = s {
|
|
369
411
|
let idx = match idx {
|
|
@@ -541,6 +583,28 @@ mod tests {
|
|
|
541
583
|
assert_same!(trim(&n(1.0)), Value::Null);
|
|
542
584
|
}
|
|
543
585
|
|
|
586
|
+
#[test]
|
|
587
|
+
fn split_limit_js_semantics() {
|
|
588
|
+
let parts = |v: &Value| -> Vec<String> {
|
|
589
|
+
let Value::Array(a) = v else { panic!("not array") };
|
|
590
|
+
a.borrow()
|
|
591
|
+
.iter()
|
|
592
|
+
.map(|x| match x {
|
|
593
|
+
Value::String(s) => s.to_string(),
|
|
594
|
+
_ => panic!("not string"),
|
|
595
|
+
})
|
|
596
|
+
.collect()
|
|
597
|
+
};
|
|
598
|
+
// limit truncates to the first N pieces (does NOT keep the remainder, unlike `splitn`)
|
|
599
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), Some(2))), ["a", "b"]);
|
|
600
|
+
// limit 0 -> empty; limit beyond piece count -> full split; no limit -> full split
|
|
601
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), Some(0))).len(), 0);
|
|
602
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), Some(10))), ["a", "b", "c", "d"]);
|
|
603
|
+
assert_eq!(parts(&split_limit(&s("a,b,c,d"), &s(","), None)), ["a", "b", "c", "d"]);
|
|
604
|
+
// split() delegates with no limit
|
|
605
|
+
assert_eq!(parts(&split(&s("one two"), &s(" "))), ["one", "two"]);
|
|
606
|
+
}
|
|
607
|
+
|
|
544
608
|
#[test]
|
|
545
609
|
fn case_and_prefix_suffix() {
|
|
546
610
|
assert_same!(to_upper_case(&s("aB")), s("AB"));
|
|
@@ -136,7 +136,14 @@ fn construct(kind: Kind, args: &[Value]) -> Value {
|
|
|
136
136
|
/// construction-only-coercion gap for this one view). Any op without a packed fast path materialises
|
|
137
137
|
/// it back to a boxed array, so every array method keeps working.
|
|
138
138
|
pub fn float64_array_packed(args: &[Value]) -> Value {
|
|
139
|
-
|
|
139
|
+
float64_array_with(Value::packed_arrays_enabled(), args)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// `float64_array_packed` with the packed/boxed choice made explicit, so tests exercise both paths
|
|
143
|
+
/// without toggling the now-cached process env flag (#166). `packed = false` is byte-identical to
|
|
144
|
+
/// the generic boxed `Value::Array` constructor.
|
|
145
|
+
fn float64_array_with(packed: bool, args: &[Value]) -> Value {
|
|
146
|
+
if !packed {
|
|
140
147
|
// Byte-identical fallback to the generic boxed `Value::Array` backing.
|
|
141
148
|
return construct(Kind::F64, args);
|
|
142
149
|
}
|
|
@@ -261,25 +268,26 @@ mod tests {
|
|
|
261
268
|
assert_eq!(nums(&v), vec![0.0, 1.0]);
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
271
|
+
// The packed/boxed selection is passed explicitly via `float64_array_with`, so this exercises
|
|
272
|
+
// both paths without touching the now-cached process env flag (#166) — no env mutation, no
|
|
273
|
+
// mid-test toggle race, parallel-safe.
|
|
267
274
|
#[test]
|
|
268
275
|
fn float64_packed_respects_flag() {
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
let boxed = float64_array_packed(&[Value::Number(3.0)]);
|
|
276
|
+
// Packed off (the default): byte-identical boxed `Value::Array` fallback.
|
|
277
|
+
let boxed = float64_array_with(false, &[Value::Number(3.0)]);
|
|
272
278
|
assert!(matches!(boxed, Value::Array(_)), "packed-off must return boxed Array");
|
|
273
279
|
assert_eq!(nums(&boxed), vec![0.0, 0.0, 0.0]);
|
|
274
280
|
|
|
275
|
-
//
|
|
281
|
+
// Packed on: `Value::NumberArray`. F64 needs no coercion (exact), non-numeric → NaN
|
|
276
282
|
// (matching the boxed `from_values(F64, …)`), and the length form zero-fills.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Value::
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
let packed = float64_array_with(
|
|
284
|
+
true,
|
|
285
|
+
&[Value::Array(VmRef::new(vec![
|
|
286
|
+
Value::Number(1.1),
|
|
287
|
+
Value::Number(2.2),
|
|
288
|
+
Value::Null,
|
|
289
|
+
]))],
|
|
290
|
+
);
|
|
283
291
|
match &packed {
|
|
284
292
|
Value::NumberArray(a) => {
|
|
285
293
|
let v = a.borrow();
|
|
@@ -290,9 +298,8 @@ mod tests {
|
|
|
290
298
|
_ => panic!("packed-on must return NumberArray"),
|
|
291
299
|
}
|
|
292
300
|
assert!(matches!(
|
|
293
|
-
|
|
301
|
+
float64_array_with(true, &[Value::Number(2.0)]),
|
|
294
302
|
Value::NumberArray(_)
|
|
295
303
|
));
|
|
296
|
-
std::env::remove_var("TISH_PACKED_ARRAYS");
|
|
297
304
|
}
|
|
298
305
|
}
|