@tishlang/tish-lsp 2.16.13 → 2.35.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.
Files changed (50) hide show
  1. package/bin/tish-lsp +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fs_parity.rs +103 -0
  7. package/crates/tish/tests/shortcircuit.rs +3 -3
  8. package/crates/tish_ast/src/ast.rs +77 -3
  9. package/crates/tish_builtins/Cargo.toml +1 -0
  10. package/crates/tish_builtins/src/array.rs +195 -16
  11. package/crates/tish_builtins/src/collections.rs +37 -2
  12. package/crates/tish_builtins/src/globals.rs +215 -25
  13. package/crates/tish_builtins/src/math.rs +65 -7
  14. package/crates/tish_builtins/src/number.rs +173 -0
  15. package/crates/tish_builtins/src/string.rs +168 -15
  16. package/crates/tish_bytecode/src/compiler.rs +56 -4
  17. package/crates/tish_bytecode/src/opcode.rs +4 -2
  18. package/crates/tish_compile/src/codegen.rs +513 -44
  19. package/crates/tish_compile/src/infer.rs +26 -11
  20. package/crates/tish_compile/src/resolve.rs +87 -10
  21. package/crates/tish_compile_js/src/codegen.rs +82 -24
  22. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  23. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  24. package/crates/tish_core/src/json.rs +5 -1
  25. package/crates/tish_core/src/lib.rs +48 -2
  26. package/crates/tish_core/src/uri.rs +45 -0
  27. package/crates/tish_core/src/value.rs +6 -0
  28. package/crates/tish_eval/Cargo.toml +2 -1
  29. package/crates/tish_eval/src/eval.rs +655 -27
  30. package/crates/tish_eval/src/natives.rs +264 -18
  31. package/crates/tish_eval/src/regex.rs +47 -1
  32. package/crates/tish_fmt/src/lib.rs +23 -4
  33. package/crates/tish_lsp/Cargo.toml +1 -1
  34. package/crates/tish_opt/src/lib.rs +20 -1
  35. package/crates/tish_parser/src/lib.rs +41 -0
  36. package/crates/tish_parser/src/parser.rs +180 -11
  37. package/crates/tish_resolve/src/lib.rs +66 -5
  38. package/crates/tish_runtime/Cargo.toml +5 -1
  39. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  40. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  41. package/crates/tish_runtime/src/lib.rs +128 -9
  42. package/crates/tish_ui/src/jsx.rs +18 -3
  43. package/crates/tish_vm/src/jit.rs +1 -1
  44. package/crates/tish_vm/src/vm.rs +270 -60
  45. package/package.json +1 -1
  46. package/platform/darwin-arm64/tish-lsp +0 -0
  47. package/platform/darwin-x64/tish-lsp +0 -0
  48. package/platform/linux-arm64/tish-lsp +0 -0
  49. package/platform/linux-x64/tish-lsp +0 -0
  50. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -175,9 +175,13 @@ pub fn map_get(map: &Value, key: &Value) -> Value {
175
175
  }
176
176
 
177
177
  /// Direct `Map.prototype.set` (mutates in place; returns `undefined` = `Null`).
178
- pub fn map_set(map: &Value, key: Value, val: Value) -> Value {
178
+ ///
179
+ /// Takes the key/val by reference and clones on insert (the map owns its entries). Passing by
180
+ /// reference lets the native emitter BORROW the caller's place instead of moving it, so
181
+ /// `m.set(k, v); …use k…` no longer produces `E0382 use of moved value` in the generated Rust (#442).
182
+ pub fn map_set(map: &Value, key: &Value, val: &Value) -> Value {
179
183
  if let Some(s) = map_store(map) {
180
- s.borrow_mut().insert(Key(key), val);
184
+ s.borrow_mut().insert(Key(key.clone()), val.clone());
181
185
  }
182
186
  Value::Null
183
187
  }
@@ -320,6 +324,20 @@ pub fn set_instance(initial: &[Value]) -> Value {
320
324
  }),
321
325
  );
322
326
  }
327
+ // `__drain__`: the default-iteration array a Set yields (its elements). `drain_iterator`
328
+ // (and the interp/native equivalents) check `__drain__` FIRST, so exposing it here makes a
329
+ // Set directly iterable — `[...set]`, `for (x of set)`, `Array.from(set)` — across all three
330
+ // backends with no changes to any spread/for-of site. (#440)
331
+ {
332
+ let s = store.clone();
333
+ m.insert(
334
+ Arc::from("__drain__"),
335
+ Value::native(move |_args: &[Value]| {
336
+ let out: Vec<Value> = s.borrow().keys().map(|k| k.0.clone()).collect();
337
+ Value::Array(VmRef::new(out))
338
+ }),
339
+ );
340
+ }
323
341
 
324
342
  Value::object(m)
325
343
  }
@@ -442,6 +460,23 @@ pub fn map_instance(pairs: &[Value]) -> Value {
442
460
  }),
443
461
  );
444
462
  }
463
+ // `__drain__`: a Map's default iteration yields `[key, value]` entries. Exposed so the shared
464
+ // `drain_iterator` (checked first) makes a Map directly iterable — `[...map]`, `for (x of map)`,
465
+ // `Array.from(map)` — across interp/vm/native with no spread/for-of site changes. (#440)
466
+ {
467
+ let s = store.clone();
468
+ m.insert(
469
+ Arc::from("__drain__"),
470
+ Value::native(move |_args: &[Value]| {
471
+ let pairs: Vec<Value> = s
472
+ .borrow()
473
+ .iter()
474
+ .map(|(k, v)| Value::Array(VmRef::new(vec![k.0.clone(), v.clone()])))
475
+ .collect();
476
+ Value::Array(VmRef::new(pairs))
477
+ }),
478
+ );
479
+ }
445
480
 
446
481
  Value::object(m)
447
482
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  use std::sync::Arc;
7
7
  use tishlang_core::VmRef;
8
- use tishlang_core::{percent_decode, percent_encode, ObjectMap, Value};
8
+ use tishlang_core::{percent_decode, percent_decode_component, percent_encode, percent_encode_component, ObjectMap, Value};
9
9
 
10
10
  /// Boolean(value) - coerce to bool
11
11
  pub fn boolean(args: &[Value]) -> Value {
@@ -13,7 +13,7 @@ pub fn boolean(args: &[Value]) -> Value {
13
13
  Value::Bool(v.is_truthy())
14
14
  }
15
15
 
16
- /// decodeURI(str)
16
+ /// decodeURI(str) — preserves the URI reserved delimiters (the inverse of encodeURI).
17
17
  pub fn decode_uri(args: &[Value]) -> Value {
18
18
  let s = args
19
19
  .first()
@@ -31,19 +31,34 @@ pub fn encode_uri(args: &[Value]) -> Value {
31
31
  Value::String(percent_encode(&s).into())
32
32
  }
33
33
 
34
- /// isFinite(value)
34
+ /// encodeURIComponent(str) — stricter than encodeURI (also escapes the URI reserved delimiters).
35
+ pub fn encode_uri_component(args: &[Value]) -> Value {
36
+ let s = args
37
+ .first()
38
+ .map(Value::to_display_string)
39
+ .unwrap_or_default();
40
+ Value::String(percent_encode_component(&s).into())
41
+ }
42
+
43
+ /// decodeURIComponent(str) — decodes ALL `%XX` escapes (including the reserved delimiters).
44
+ pub fn decode_uri_component(args: &[Value]) -> Value {
45
+ let s = args
46
+ .first()
47
+ .map(Value::to_display_string)
48
+ .unwrap_or_default();
49
+ Value::String(percent_decode_component(&s).unwrap_or(s).into())
50
+ }
51
+
52
+ /// isFinite(value) — coerces via ToNumber (like `Number()`), then tests finiteness. `isFinite("3")`
53
+ /// is `true`, `isFinite("x")`/`isFinite()` is `false`. (The non-coercing form is `Number.isFinite`.)
35
54
  pub fn is_finite(args: &[Value]) -> Value {
36
- Value::Bool(
37
- args.first()
38
- .is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())),
39
- )
55
+ Value::Bool(args.first().map_or(f64::NAN, to_number).is_finite())
40
56
  }
41
57
 
42
- /// isNaN(value)
58
+ /// isNaN(value) — coerces via ToNumber, then tests NaN. `isNaN("3")` is `false`; an absent arg
59
+ /// (undefined) coerces to NaN → `true`. (The non-coercing form is `Number.isNaN`.)
43
60
  pub fn is_nan(args: &[Value]) -> Value {
44
- Value::Bool(args.first().is_none_or(|v| {
45
- matches!(v, Value::Number(n) if n.is_nan()) || !matches!(v, Value::Number(_))
46
- }))
61
+ Value::Bool(args.first().map_or(f64::NAN, to_number).is_nan())
47
62
  }
48
63
 
49
64
  /// Array.isArray(value)
@@ -51,6 +66,142 @@ pub fn array_is_array(args: &[Value]) -> Value {
51
66
  Value::Bool(matches!(args.first(), Some(Value::Array(_)) | Some(Value::NumberArray(_))))
52
67
  }
53
68
 
69
+ /// `Array.of(...items)` — build an array from the args (unlike `Array(n)`, a single number is an
70
+ /// element, not a length: `Array.of(7)` is `[7]`).
71
+ pub fn array_of(args: &[Value]) -> Value {
72
+ Value::Array(VmRef::new(args.to_vec()))
73
+ }
74
+
75
+ /// `Array.from(source, mapFn?)` — build an array from an iterable (array, string, Set/Map, or any
76
+ /// `__drain__`/`next` iterator — via the shared iterator protocol) or an array-like `{ length }`
77
+ /// object, optionally mapping each element through `mapFn(item, index)`.
78
+ pub fn array_from(args: &[Value]) -> Value {
79
+ let source = args.first().cloned().unwrap_or(Value::Null);
80
+ let items: Vec<Value> = match &source {
81
+ Value::Array(a) => a.borrow().clone(),
82
+ Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
83
+ // `drain_iterator` covers String (chars), Set/Map, and any iterator object.
84
+ other => {
85
+ if let Some(d) = tishlang_core::drain_iterator(other) {
86
+ d
87
+ } else if matches!(other, Value::Object(_)) {
88
+ let len = tishlang_core::object_get(other, &Value::String("length".into()))
89
+ .and_then(|v| match v {
90
+ Value::Number(n) if n.is_finite() && n >= 0.0 => Some(n as usize),
91
+ _ => None,
92
+ })
93
+ .unwrap_or(0);
94
+ (0..len)
95
+ .map(|i| {
96
+ tishlang_core::object_get(other, &Value::String(i.to_string().into()))
97
+ .unwrap_or(Value::Null)
98
+ })
99
+ .collect()
100
+ } else {
101
+ Vec::new()
102
+ }
103
+ }
104
+ };
105
+ let out: Vec<Value> = match args.get(1) {
106
+ Some(Value::Function(f)) => items
107
+ .into_iter()
108
+ .enumerate()
109
+ .map(|(i, x)| f.call(&[x, Value::Number(i as f64)]))
110
+ .collect(),
111
+ _ => items,
112
+ };
113
+ Value::Array(VmRef::new(out))
114
+ }
115
+
116
+ /// `structuredClone(value)` — a recursive deep copy of arrays, objects, and primitives, with cycles
117
+ /// preserved (a repeated reference clones to the same new node). Functions and exotic types (Date,
118
+ /// RegExp, Map, Set) are shared/shallow — a pragmatic subset covering plain data structures.
119
+ pub fn structured_clone(args: &[Value]) -> Value {
120
+ let mut seen: Vec<(*const (), Value)> = Vec::new();
121
+ deep_clone(args.first().unwrap_or(&Value::Null), &mut seen)
122
+ }
123
+
124
+ fn deep_clone(v: &Value, seen: &mut Vec<(*const (), Value)>) -> Value {
125
+ match v {
126
+ Value::Array(a) => {
127
+ let ptr = a.as_ptr();
128
+ if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
129
+ return c.clone();
130
+ }
131
+ let new: VmRef<Vec<Value>> = VmRef::new(Vec::new());
132
+ seen.push((ptr, Value::Array(new.clone())));
133
+ let src = a.borrow().clone();
134
+ let cloned: Vec<Value> = src.iter().map(|e| deep_clone(e, seen)).collect();
135
+ *new.borrow_mut() = cloned;
136
+ Value::Array(new)
137
+ }
138
+ Value::NumberArray(a) => Value::NumberArray(VmRef::new(a.borrow().clone())),
139
+ Value::Object(o) => {
140
+ let ptr = o.as_ptr();
141
+ if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
142
+ return c.clone();
143
+ }
144
+ let entries: Vec<(Arc<str>, Value)> = o
145
+ .borrow()
146
+ .strings
147
+ .iter()
148
+ .map(|(k, val)| (k.clone(), val.clone()))
149
+ .collect();
150
+ let new_obj = Value::object(ObjectMap::default());
151
+ // register the (empty) new object before recursing so cycles resolve back to it
152
+ if let Value::Object(no) = &new_obj {
153
+ seen.push((ptr, new_obj.clone()));
154
+ for (k, val) in entries {
155
+ let cloned = deep_clone(&val, seen);
156
+ no.borrow_mut().strings.insert(k, cloned);
157
+ }
158
+ }
159
+ new_obj
160
+ }
161
+ other => other.clone(),
162
+ }
163
+ }
164
+
165
+ /// `Object.hasOwn(obj, key)` (ES2022) — true if `key` is an OWN property of `obj`. For arrays an
166
+ /// in-range integer index or `"length"` counts; for plain objects, a key present in the string map.
167
+ pub fn object_has_own(args: &[Value]) -> Value {
168
+ let key = match args.get(1) {
169
+ Some(Value::String(s)) => s.to_string(),
170
+ Some(Value::Number(n)) => tishlang_core::js_number_to_string(*n),
171
+ Some(v) => v.to_display_string(),
172
+ None => return Value::Bool(false),
173
+ };
174
+ match args.first() {
175
+ Some(Value::Object(obj)) => Value::Bool(obj.borrow().strings.contains_key(key.as_str())),
176
+ Some(Value::Array(arr)) => Value::Bool(
177
+ key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
178
+ ),
179
+ Some(Value::NumberArray(arr)) => Value::Bool(
180
+ key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
181
+ ),
182
+ _ => Value::Bool(false),
183
+ }
184
+ }
185
+
186
+ /// `Object.is(a, b)` — SameValue: like `===`, but NaN equals NaN and `+0` is NOT equal to `-0`.
187
+ pub fn object_is(args: &[Value]) -> Value {
188
+ let a = args.first().unwrap_or(&Value::Null);
189
+ let b = args.get(1).unwrap_or(&Value::Null);
190
+ let same = match (a, b) {
191
+ (Value::Number(x), Value::Number(y)) => {
192
+ if x.is_nan() && y.is_nan() {
193
+ true
194
+ } else if *x == 0.0 && *y == 0.0 {
195
+ x.is_sign_negative() == y.is_sign_negative()
196
+ } else {
197
+ x == y
198
+ }
199
+ }
200
+ _ => a.strict_eq(b),
201
+ };
202
+ Value::Bool(same)
203
+ }
204
+
54
205
  /// String(value) — convert value to string (JS String constructor as function).
55
206
  /// Uses JS `ToString` (arrays comma-join recursively, objects → "[object Object]"),
56
207
  /// not the inspect/display form.
@@ -64,8 +215,14 @@ pub fn string_convert(args: &[Value]) -> Value {
64
215
  /// otherwise NaN); arrays/objects go via their string form (so `Number([5])` → 5,
65
216
  /// `Number([])` → 0, objects → NaN).
66
217
  pub fn number_convert(args: &[Value]) -> Value {
67
- let v = args.first().unwrap_or(&Value::Null);
68
- let n = match v {
218
+ Value::Number(to_number(args.first().unwrap_or(&Value::Null)))
219
+ }
220
+
221
+ /// JS `ToNumber(v)`: Number → itself; Bool → 1/0; Null → 0; String → parsed (trimmed, `0x`/`0b`/`0o`
222
+ /// and `Infinity`; `""` → 0, else NaN); anything else via its string form. The single ToNumber path
223
+ /// shared by `Number()`, `isNaN`/`isFinite`, etc.
224
+ pub fn to_number(v: &Value) -> f64 {
225
+ match v {
69
226
  Value::Number(n) => *n,
70
227
  Value::Bool(b) => {
71
228
  if *b {
@@ -77,8 +234,29 @@ pub fn number_convert(args: &[Value]) -> Value {
77
234
  Value::Null => 0.0,
78
235
  Value::String(s) => parse_numeric_string(s),
79
236
  other => parse_numeric_string(&other.to_js_string()),
80
- };
81
- Value::Number(n)
237
+ }
238
+ }
239
+
240
+ /// `Number.isInteger(v)` — true iff `v` IS a finite Number with no fractional part. No coercion
241
+ /// (`Number.isInteger("3")` is `false`, unlike a bare `isNaN`-style coercion).
242
+ pub fn number_is_integer(args: &[Value]) -> Value {
243
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0))
244
+ }
245
+
246
+ /// `Number.isSafeInteger(v)` — `isInteger` AND exactly representable (|v| ≤ 2^53 − 1).
247
+ pub fn number_is_safe_integer(args: &[Value]) -> Value {
248
+ Value::Bool(matches!(args.first(),
249
+ Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0 && n.abs() <= 9_007_199_254_740_991.0))
250
+ }
251
+
252
+ /// `Number.isNaN(v)` — true iff `v` IS the number NaN. No coercion (unlike the global `isNaN`).
253
+ pub fn number_is_nan(args: &[Value]) -> Value {
254
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_nan()))
255
+ }
256
+
257
+ /// `Number.isFinite(v)` — true iff `v` is a finite Number. No coercion (unlike the global `isFinite`).
258
+ pub fn number_is_finite(args: &[Value]) -> Value {
259
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite()))
82
260
  }
83
261
 
84
262
  /// Parse a string as JS `Number` does: trimmed; `""` → 0; `0x`/`0o`/`0b` radix prefixes;
@@ -120,16 +298,28 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
120
298
 
121
299
  /// Object.keys(obj)
122
300
  pub fn object_keys(args: &[Value]) -> Value {
123
- if let Some(Value::Object(obj)) = args.first() {
124
- let obj_borrow = obj.borrow();
125
- let keys: Vec<Value> = obj_borrow
126
- .strings
127
- .keys()
128
- .map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
129
- .collect();
130
- Value::Array(VmRef::new(keys))
131
- } else {
132
- Value::Array(VmRef::new(Vec::new()))
301
+ match args.first() {
302
+ Some(Value::Object(obj)) => {
303
+ let obj_borrow = obj.borrow();
304
+ let keys: Vec<Value> = obj_borrow
305
+ .strings
306
+ .keys()
307
+ .map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
308
+ .collect();
309
+ Value::Array(VmRef::new(keys))
310
+ }
311
+ // `Object.keys(array)` yields the index strings "0".."len-1" (an array's own enumerable keys
312
+ // are its indices, per JS). Previously returned `[]` — a node divergence, and the reason
313
+ // `for (k in array)` enumerated nothing on the vm/native backends (they lower for-in to
314
+ // for-of over `Object.keys`).
315
+ Some(Value::Array(arr)) => {
316
+ let len = arr.borrow().len();
317
+ let keys: Vec<Value> = (0..len)
318
+ .map(|i| Value::String(tishlang_core::ArcStr::from(i.to_string().as_str())))
319
+ .collect();
320
+ Value::Array(VmRef::new(keys))
321
+ }
322
+ _ => Value::Array(VmRef::new(Vec::new())),
133
323
  }
134
324
  }
135
325
 
@@ -1,7 +1,7 @@
1
1
  //! Math builtin functions.
2
2
 
3
3
  use crate::helpers::extract_num;
4
- use tishlang_core::Value;
4
+ use tishlang_core::{to_int32, to_uint32, Value};
5
5
 
6
6
  macro_rules! math_unary {
7
7
  ($name:ident, $op:ident) => {
@@ -28,6 +28,20 @@ math_unary!(log2, log2);
28
28
  math_unary!(exp, exp);
29
29
  math_unary!(trunc, trunc);
30
30
  math_unary!(cbrt, cbrt);
31
+ math_unary!(expm1, exp_m1);
32
+ math_unary!(log1p, ln_1p);
33
+
34
+ /// `Math.clz32(x)` — count leading zero bits of `ToUint32(x)` (0..=32).
35
+ pub fn clz32(args: &[Value]) -> Value {
36
+ let n = to_uint32(extract_num(args.first()).unwrap_or(0.0));
37
+ Value::Number(n.leading_zeros() as f64)
38
+ }
39
+
40
+ /// `Math.fround(x)` — round `x` to the nearest 32-bit float, back to f64.
41
+ pub fn fround(args: &[Value]) -> Value {
42
+ let x = extract_num(args.first()).unwrap_or(f64::NAN);
43
+ Value::Number(x as f32 as f64)
44
+ }
31
45
 
32
46
  // --- f64-domain semantics (single source of truth) -------------------------------------------------
33
47
  // These hold the JS-specific Math rules so every backend agrees: the vm/native paths call the
@@ -119,7 +133,9 @@ pub fn sign(args: &[Value]) -> Value {
119
133
  } else if n < 0.0 {
120
134
  -1.0
121
135
  } else {
122
- 0.0
136
+ // ±0 → return the input unchanged so `Math.sign(-0)` is `-0` (per ES);
137
+ // `n < 0.0` is false for -0, so it lands here — bare `0.0` would drop the sign.
138
+ n
123
139
  })
124
140
  }
125
141
 
@@ -129,15 +145,57 @@ pub fn atan2(args: &[Value]) -> Value {
129
145
  Value::Number(y.atan2(x))
130
146
  }
131
147
 
148
+ /// ECMAScript `Math.hypot(...args)` core: sqrt of the sum of squares, VARIADIC and
149
+ /// NaN/Infinity-correct. Infinity takes precedence over NaN (any ±∞ arg → +∞, even alongside a NaN);
150
+ /// otherwise any NaN → NaN; the finite case is scaled by the max magnitude to avoid intermediate
151
+ /// overflow/underflow (matches V8). `Math.hypot()` (no args) → 0. Shared so interp/vm/native agree
152
+ /// (#247 — the interp/native path was 2-arg only, so `Math.hypot(3,4,12)` gave 5 not 13, and the vm
153
+ /// dropped non-number args instead of propagating NaN).
154
+ pub fn hypot_f64(nums: &[f64]) -> f64 {
155
+ let mut any_nan = false;
156
+ let mut max = 0.0_f64;
157
+ for &n in nums {
158
+ if n.is_infinite() {
159
+ return f64::INFINITY;
160
+ }
161
+ if n.is_nan() {
162
+ any_nan = true;
163
+ }
164
+ let a = n.abs();
165
+ if a > max {
166
+ max = a;
167
+ }
168
+ }
169
+ if any_nan {
170
+ return f64::NAN;
171
+ }
172
+ if max == 0.0 {
173
+ return 0.0;
174
+ }
175
+ let mut sum = 0.0_f64;
176
+ for &n in nums {
177
+ let r = n / max;
178
+ sum += r * r;
179
+ }
180
+ max * sum.sqrt()
181
+ }
182
+
132
183
  pub fn hypot(args: &[Value]) -> Value {
133
- let x = extract_num(args.first()).unwrap_or(0.0);
134
- let y = extract_num(args.get(1)).unwrap_or(0.0);
135
- Value::Number(x.hypot(y))
184
+ let nums: Vec<f64> = args
185
+ .iter()
186
+ .map(|v| extract_num(Some(v)).unwrap_or(f64::NAN))
187
+ .collect();
188
+ Value::Number(hypot_f64(&nums))
136
189
  }
137
190
 
138
191
  /// ES6 `Math.imul`: 32-bit integer multiply (used by xmur3 PRNG in juke-cards).
192
+ ///
193
+ /// Operands go through JS `ToInt32` (modulo 2³², NaN/±Infinity → 0), NOT a saturating `as i32`
194
+ /// cast: the latter clamps any argument ≥ 2³¹ (e.g. a full uint32 hash state) to `i32::MAX` and
195
+ /// diverges from ECMAScript / V8 (`Math.imul(3e9, 1)` = -1294967296, not 2147483647). The typed
196
+ /// native path inlines the same ToInt32 + wrapping multiply, so all backends agree.
139
197
  pub fn imul(args: &[Value]) -> Value {
140
- let a = extract_num(args.first()).unwrap_or(0.0) as i32;
141
- let b = extract_num(args.get(1)).unwrap_or(0.0) as i32;
198
+ let a = to_int32(extract_num(args.first()).unwrap_or(0.0));
199
+ let b = to_int32(extract_num(args.get(1)).unwrap_or(0.0));
142
200
  Value::Number(a.wrapping_mul(b) as f64)
143
201
  }
@@ -43,6 +43,179 @@ pub fn to_fixed_str(num: f64, digits: usize) -> String {
43
43
  format!("{:.*}", digits, rounded)
44
44
  }
45
45
 
46
+ /// `Number.prototype.toExponential(fractionDigits?)` — ECMA-262 §21.1.3.2. Exponential notation with
47
+ /// `fractionDigits` mantissa fraction digits (0–100), or the minimal digits needed when omitted. The
48
+ /// exponent always carries an explicit sign (`1.23e+4`, `1e-7`) to match V8.
49
+ pub fn to_exponential(n: &Value, digits: &Value) -> Value {
50
+ let num = match n {
51
+ Value::Number(x) => *x,
52
+ _ => f64::NAN,
53
+ };
54
+ let d = match digits {
55
+ Value::Number(x) => Some((*x as i32).clamp(0, 100) as usize),
56
+ _ => None,
57
+ };
58
+ Value::String(to_exponential_str(num, d).into())
59
+ }
60
+
61
+ /// f64-domain core of `toExponential`, shared with the tree-walk interpreter.
62
+ pub fn to_exponential_str(num: f64, digits: Option<usize>) -> String {
63
+ if num.is_nan() {
64
+ return "NaN".to_string();
65
+ }
66
+ if num.is_infinite() {
67
+ return if num < 0.0 { "-Infinity" } else { "Infinity" }.to_string();
68
+ }
69
+ let d = match digits {
70
+ // No fractionDigits: shortest round-trip mantissa (Rust's `{:e}` matches V8 here).
71
+ None => return fix_exponent_sign(&format!("{:e}", num)),
72
+ Some(d) => d,
73
+ };
74
+ if num == 0.0 {
75
+ let mant = if d == 0 {
76
+ "0".to_string()
77
+ } else {
78
+ format!("0.{}", "0".repeat(d))
79
+ };
80
+ return format!("{}e+0", mant);
81
+ }
82
+ let sign = if num < 0.0 { "-" } else { "" };
83
+ let (ds, e) = sig_digits(num.abs(), d + 1);
84
+ let mant = if d == 0 {
85
+ ds
86
+ } else {
87
+ format!("{}.{}", &ds[..1], &ds[1..])
88
+ };
89
+ let esign = if e >= 0 { "+" } else { "-" };
90
+ format!("{}{}e{}{}", sign, mant, esign, e.abs())
91
+ }
92
+
93
+ /// Round `x` (finite, `x > 0`) to `sig` significant decimal digits with **half-away-from-zero**
94
+ /// rounding — ECMA's "pick the larger n" tie rule (`2.5.toPrecision(1) === "3"`, not "2"). Returns the
95
+ /// `sig`-digit string and the base-10 exponent of the leading digit.
96
+ ///
97
+ /// The exact value of an f64 has a finite decimal expansion; scaling it through `f64` arithmetic
98
+ /// re-rounds and can land a genuinely-below-half value (e.g. `2.675` ≈ `2.67499…`) exactly on `.5`,
99
+ /// corrupting the tie test. So we format with many guard digits — `{:.Ne}` gives the correctly-rounded
100
+ /// exact-value expansion — capturing the TRUE digit at position `sig`, then round the digit string:
101
+ /// `digit[sig] >= 5` rounds up (half-away; `== 5` is a true tie only because the guard digits show no
102
+ /// nonzero remainder). Rust's `{:e}` alone rounds half-to-even, which is exactly what we must avoid.
103
+ fn sig_digits(x: f64, sig: usize) -> (String, i32) {
104
+ let sig = sig.max(1);
105
+ // Guard digits push the format's own (half-to-even) rounding far to the right of digit[sig], so
106
+ // digit[sig] is exact. A cascade into digit[sig] would need 16 consecutive 9s — not a real input.
107
+ let guard = sig + 16;
108
+ let raw = format!("{:.*e}", guard, x);
109
+ let (mant, exp_str) = raw.split_once('e').unwrap();
110
+ let mut e: i32 = exp_str.parse().unwrap_or(0);
111
+ let mut digits: Vec<u8> = mant
112
+ .bytes()
113
+ .filter(u8::is_ascii_digit)
114
+ .map(|b| b - b'0')
115
+ .collect();
116
+ if digits.len() > sig {
117
+ let round_up = digits[sig] >= 5;
118
+ digits.truncate(sig);
119
+ if round_up {
120
+ let mut i = sig;
121
+ loop {
122
+ if i == 0 {
123
+ // carry out of the leading digit: 9…9 → 1 0…0, one more place
124
+ digits.insert(0, 1);
125
+ digits.truncate(sig);
126
+ e += 1;
127
+ break;
128
+ }
129
+ i -= 1;
130
+ if digits[i] == 9 {
131
+ digits[i] = 0;
132
+ } else {
133
+ digits[i] += 1;
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ let s: String = digits.iter().map(|d| (d + b'0') as char).collect();
140
+ (s, e)
141
+ }
142
+
143
+ /// Rust's `{:e}` writes a bare positive exponent (`1.23e4`); JS/V8 always signs it (`1.23e+4`). Negative
144
+ /// exponents already carry `-`.
145
+ fn fix_exponent_sign(s: &str) -> String {
146
+ match s.split_once('e') {
147
+ Some((mantissa, exp)) if !exp.starts_with('-') && !exp.starts_with('+') => {
148
+ format!("{}e+{}", mantissa, exp)
149
+ }
150
+ _ => s.to_string(),
151
+ }
152
+ }
153
+
154
+ /// `Number.prototype.toPrecision(precision?)` — ECMA-262 §21.1.3.5. With `precision` significant digits,
155
+ /// choosing fixed or exponential per the spec; omitted precision falls back to `ToString`. An
156
+ /// out-of-range precision (`<1` or `>100`) parks a catchable `RangeError`.
157
+ pub fn to_precision(n: &Value, precision: &Value) -> Value {
158
+ let num = match n {
159
+ Value::Number(x) => *x,
160
+ _ => f64::NAN,
161
+ };
162
+ match precision {
163
+ Value::Number(p) => {
164
+ let p = *p as i32;
165
+ if !(1..=100).contains(&p) && num.is_finite() {
166
+ tishlang_core::set_pending_throw(tishlang_core::range_error(
167
+ "toPrecision() argument must be between 1 and 100",
168
+ ));
169
+ return Value::Null;
170
+ }
171
+ Value::String(to_precision_str(num, p).into())
172
+ }
173
+ // precision undefined → behave like ToString(number)
174
+ _ => Value::String(tishlang_core::js_number_to_string(num).into()),
175
+ }
176
+ }
177
+
178
+ /// f64-domain core of `toPrecision`, shared with the tree-walk interpreter. Assumes `precision` is in
179
+ /// range (the `&Value` entry point validates and parks a RangeError otherwise).
180
+ pub fn to_precision_str(num: f64, precision: i32) -> String {
181
+ if num.is_nan() {
182
+ return "NaN".to_string();
183
+ }
184
+ if num.is_infinite() {
185
+ return if num < 0.0 { "-Infinity" } else { "Infinity" }.to_string();
186
+ }
187
+ let p = precision.clamp(1, 100) as usize;
188
+ if num == 0.0 {
189
+ if p == 1 {
190
+ return "0".to_string();
191
+ }
192
+ return format!("0.{}", "0".repeat(p - 1));
193
+ }
194
+ let sign = if num < 0.0 { "-" } else { "" };
195
+ // `p` significant digits + base-10 exponent, half-away rounded (shared with toExponential).
196
+ let (digits, e) = sig_digits(num.abs(), p);
197
+ let body = if e < -6 || e >= p as i32 {
198
+ // exponential form
199
+ let mut m = String::new();
200
+ m.push(digits.as_bytes()[0] as char);
201
+ if p > 1 {
202
+ m.push('.');
203
+ m.push_str(&digits[1..]);
204
+ }
205
+ let esign = if e >= 0 { "+" } else { "-" };
206
+ format!("{}e{}{}", m, esign, e.abs())
207
+ } else if e == p as i32 - 1 {
208
+ digits
209
+ } else if e >= 0 {
210
+ let split = (e + 1) as usize;
211
+ format!("{}.{}", &digits[..split], &digits[split..])
212
+ } else {
213
+ let zeros = (-e - 1) as usize;
214
+ format!("0.{}{}", "0".repeat(zeros), digits)
215
+ };
216
+ format!("{}{}", sign, body)
217
+ }
218
+
46
219
  /// `Number.prototype.toString([radix])` — ECMA-262 §21.1.3.6.
47
220
  ///
48
221
  /// Radix defaults to 10 (canonical JS number formatting). For radix 2–36 the value is