@tishlang/tish-format 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.
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +77 -3
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +195 -16
- package/crates/tish_builtins/src/collections.rs +37 -2
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +168 -15
- package/crates/tish_bytecode/src/compiler.rs +56 -4
- package/crates/tish_bytecode/src/opcode.rs +4 -2
- package/crates/tish_compile/src/codegen.rs +513 -44
- package/crates/tish_compile/src/infer.rs +26 -11
- package/crates/tish_compile/src/resolve.rs +87 -10
- package/crates/tish_compile_js/src/codegen.rs +82 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
- package/crates/tish_core/src/json.rs +5 -1
- package/crates/tish_core/src/lib.rs +48 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +6 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +655 -27
- package/crates/tish_eval/src/natives.rs +264 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +23 -4
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/src/lib.rs +41 -0
- package/crates/tish_parser/src/parser.rs +180 -11
- package/crates/tish_resolve/src/lib.rs +66 -5
- package/crates/tish_runtime/Cargo.toml +5 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/lib.rs +128 -9
- package/crates/tish_ui/src/jsx.rs +18 -3
- package/crates/tish_vm/src/jit.rs +1 -1
- package/crates/tish_vm/src/vm.rs +270 -60
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.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
|
-
|
|
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
|
-
///
|
|
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().
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
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)
|
|
141
|
-
let b = extract_num(args.get(1)).unwrap_or(0.0)
|
|
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
|