@tishlang/tish-format 2.16.13 → 2.36.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/fixtures/process_capture.tish +16 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/process_capture.rs +40 -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 +519 -46
- 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 +661 -27
- package/crates/tish_eval/src/natives.rs +318 -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 +236 -10
- 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 +294 -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
|
@@ -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
|
|
@@ -317,6 +317,59 @@ pub fn trim(s: &Value) -> Value {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
pub fn trim_start(s: &Value) -> Value {
|
|
321
|
+
if let Value::String(s) = s {
|
|
322
|
+
Value::String(s.trim_start().into())
|
|
323
|
+
} else {
|
|
324
|
+
Value::Null
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Pure `String.prototype.normalize` core (ECMA-262 §22.1.3.13). `form` is one of NFC/NFD/NFKC/NFKD;
|
|
329
|
+
/// returns `None` for an unrecognized form so the caller can surface a `RangeError`. Shared by every
|
|
330
|
+
/// backend so interp/vm/native stay byte-identical.
|
|
331
|
+
pub fn normalize_form(s: &str, form: &str) -> Option<String> {
|
|
332
|
+
use unicode_normalization::UnicodeNormalization;
|
|
333
|
+
match form {
|
|
334
|
+
"NFC" => Some(s.nfc().collect()),
|
|
335
|
+
"NFD" => Some(s.nfd().collect()),
|
|
336
|
+
"NFKC" => Some(s.nfkc().collect()),
|
|
337
|
+
"NFKD" => Some(s.nfkd().collect()),
|
|
338
|
+
_ => None,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// `String.prototype.normalize(form?)` — core-`Value` entry (vm/native). An omitted form defaults to
|
|
343
|
+
/// NFC; an invalid form parks a catchable `RangeError`.
|
|
344
|
+
pub fn normalize(s: &Value, form: &Value) -> Value {
|
|
345
|
+
let input = match s {
|
|
346
|
+
Value::String(s) => s.as_ref(),
|
|
347
|
+
_ => return Value::Null,
|
|
348
|
+
};
|
|
349
|
+
let f: String = match form {
|
|
350
|
+
Value::Null => "NFC".to_string(),
|
|
351
|
+
Value::String(f) => f.to_string(),
|
|
352
|
+
v => v.to_display_string(),
|
|
353
|
+
};
|
|
354
|
+
match normalize_form(input, &f) {
|
|
355
|
+
Some(out) => Value::String(out.into()),
|
|
356
|
+
None => {
|
|
357
|
+
tishlang_core::set_pending_throw(tishlang_core::range_error(
|
|
358
|
+
"The normalization form should be one of NFC, NFD, NFKC, NFKD.",
|
|
359
|
+
));
|
|
360
|
+
Value::Null
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
pub fn trim_end(s: &Value) -> Value {
|
|
366
|
+
if let Value::String(s) = s {
|
|
367
|
+
Value::String(s.trim_end().into())
|
|
368
|
+
} else {
|
|
369
|
+
Value::Null
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
320
373
|
pub fn to_upper_case(s: &Value) -> Value {
|
|
321
374
|
if let Value::String(s) = s {
|
|
322
375
|
Value::String(s.to_uppercase().into())
|
|
@@ -333,22 +386,74 @@ pub fn to_lower_case(s: &Value) -> Value {
|
|
|
333
386
|
}
|
|
334
387
|
}
|
|
335
388
|
|
|
336
|
-
pub fn starts_with(s: &Value, search: &Value) -> Value {
|
|
389
|
+
pub fn starts_with(s: &Value, search: &Value, position: Option<&Value>) -> Value {
|
|
337
390
|
if let (Value::String(s), Value::String(search)) = (s, search) {
|
|
338
|
-
|
|
391
|
+
// `position`: test as if the string began at char `position` (clamped to [0, len]). Absent → 0.
|
|
392
|
+
let pos = match position {
|
|
393
|
+
Some(Value::Number(n)) if *n > 0.0 => *n as usize,
|
|
394
|
+
_ => 0,
|
|
395
|
+
};
|
|
396
|
+
let byte_start = s.char_indices().nth(pos).map(|(b, _)| b).unwrap_or(s.len());
|
|
397
|
+
Value::Bool(s[byte_start..].starts_with(search.as_str()))
|
|
339
398
|
} else {
|
|
340
399
|
Value::Bool(false)
|
|
341
400
|
}
|
|
342
401
|
}
|
|
343
402
|
|
|
344
|
-
pub fn ends_with(s: &Value, search: &Value) -> Value {
|
|
403
|
+
pub fn ends_with(s: &Value, search: &Value, end_position: Option<&Value>) -> Value {
|
|
345
404
|
if let (Value::String(s), Value::String(search)) = (s, search) {
|
|
346
|
-
|
|
405
|
+
// `endPosition`: test as if the string ended at char `endPosition` (clamped to [0, len]).
|
|
406
|
+
// Absent → the full length.
|
|
407
|
+
let char_count = s.chars().count();
|
|
408
|
+
let end = match end_position {
|
|
409
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(char_count),
|
|
410
|
+
Some(Value::Number(_)) => 0,
|
|
411
|
+
_ => char_count,
|
|
412
|
+
};
|
|
413
|
+
let byte_end = s.char_indices().nth(end).map(|(b, _)| b).unwrap_or(s.len());
|
|
414
|
+
Value::Bool(s[..byte_end].ends_with(search.as_str()))
|
|
347
415
|
} else {
|
|
348
416
|
Value::Bool(false)
|
|
349
417
|
}
|
|
350
418
|
}
|
|
351
419
|
|
|
420
|
+
/// Expand a string replacement's `$` patterns for one match: `$$`→`$`, `$&`→the match, `` $` ``→the
|
|
421
|
+
/// text before it, `$'`→the text after it. Any other `$x` is literal. (String-search `replace`; the
|
|
422
|
+
/// numbered `$1` captures only apply to regex replacements.)
|
|
423
|
+
pub fn expand_replacement(repl: &str, matched: &str, before: &str, after: &str) -> String {
|
|
424
|
+
if !repl.contains('$') {
|
|
425
|
+
return repl.to_string();
|
|
426
|
+
}
|
|
427
|
+
let mut out = String::with_capacity(repl.len());
|
|
428
|
+
let mut chars = repl.chars().peekable();
|
|
429
|
+
while let Some(c) = chars.next() {
|
|
430
|
+
if c == '$' {
|
|
431
|
+
match chars.peek() {
|
|
432
|
+
Some('$') => {
|
|
433
|
+
out.push('$');
|
|
434
|
+
chars.next();
|
|
435
|
+
}
|
|
436
|
+
Some('&') => {
|
|
437
|
+
out.push_str(matched);
|
|
438
|
+
chars.next();
|
|
439
|
+
}
|
|
440
|
+
Some('`') => {
|
|
441
|
+
out.push_str(before);
|
|
442
|
+
chars.next();
|
|
443
|
+
}
|
|
444
|
+
Some('\'') => {
|
|
445
|
+
out.push_str(after);
|
|
446
|
+
chars.next();
|
|
447
|
+
}
|
|
448
|
+
_ => out.push('$'),
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
out.push(c);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
out
|
|
455
|
+
}
|
|
456
|
+
|
|
352
457
|
fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Value {
|
|
353
458
|
if let Value::String(s) = s {
|
|
354
459
|
let search_str = match search {
|
|
@@ -359,12 +464,37 @@ fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Va
|
|
|
359
464
|
Value::String(ss) => ss.as_str(),
|
|
360
465
|
_ => "",
|
|
361
466
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
467
|
+
// Fast path: no `$` in the replacement (and non-empty search) → the library replace.
|
|
468
|
+
if !repl_str.contains('$') || search_str.is_empty() {
|
|
469
|
+
let result = if all {
|
|
470
|
+
s.replace(search_str, repl_str)
|
|
471
|
+
} else {
|
|
472
|
+
s.replacen(search_str, repl_str, 1)
|
|
473
|
+
};
|
|
474
|
+
return Value::String(result.into());
|
|
475
|
+
}
|
|
476
|
+
// `$`-expansion path: expand per match against the original string's before/after context.
|
|
477
|
+
let sref = s.as_str();
|
|
478
|
+
let mut out = String::with_capacity(sref.len());
|
|
479
|
+
let mut last = 0usize;
|
|
480
|
+
let mut start = 0usize;
|
|
481
|
+
while let Some(pos) = sref[start..].find(search_str) {
|
|
482
|
+
let m = start + pos;
|
|
483
|
+
out.push_str(&sref[last..m]);
|
|
484
|
+
out.push_str(&expand_replacement(
|
|
485
|
+
repl_str,
|
|
486
|
+
search_str,
|
|
487
|
+
&sref[..m],
|
|
488
|
+
&sref[m + search_str.len()..],
|
|
489
|
+
));
|
|
490
|
+
last = m + search_str.len();
|
|
491
|
+
start = last;
|
|
492
|
+
if !all {
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
out.push_str(&sref[last..]);
|
|
497
|
+
Value::String(out.into())
|
|
368
498
|
} else {
|
|
369
499
|
Value::Null
|
|
370
500
|
}
|
|
@@ -507,6 +637,23 @@ pub fn char_code_at(s: &Value, idx: &Value) -> Value {
|
|
|
507
637
|
}
|
|
508
638
|
}
|
|
509
639
|
|
|
640
|
+
/// `codePointAt(i)` — the Unicode code point at char index `i`, or `null` (JS `undefined`) if out of
|
|
641
|
+
/// range. tish strings are code-point sequences, so this returns the same value as `charCodeAt` for
|
|
642
|
+
/// an in-range index, but reads `null` (not `NaN`) past the end.
|
|
643
|
+
pub fn code_point_at(s: &Value, idx: &Value) -> Value {
|
|
644
|
+
if let Value::String(s) = s {
|
|
645
|
+
let idx = match idx {
|
|
646
|
+
Value::Number(n) if *n >= 0.0 => *n as usize,
|
|
647
|
+
_ => return Value::Null,
|
|
648
|
+
};
|
|
649
|
+
char_at_idx(s, idx)
|
|
650
|
+
.map(|c| Value::Number(c as u32 as f64))
|
|
651
|
+
.unwrap_or(Value::Null)
|
|
652
|
+
} else {
|
|
653
|
+
Value::Null
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
510
657
|
pub fn repeat(s: &Value, count: &Value) -> Value {
|
|
511
658
|
if let Value::String(s) = s {
|
|
512
659
|
let count = match count {
|
|
@@ -525,12 +672,15 @@ fn pad_impl(s: &Value, target_len: &Value, pad: &Value, at_start: bool) -> Value
|
|
|
525
672
|
Value::Number(n) => *n as usize,
|
|
526
673
|
_ => return Value::String(s.clone()),
|
|
527
674
|
};
|
|
675
|
+
// An *explicit* empty fill string means "no padding" (spec: `padStart(n, "")` → original,
|
|
676
|
+
// unchanged); only an ABSENT pad arg (`Value::Null`) defaults to a space. The old
|
|
677
|
+
// `if !p.is_empty()` guard conflated the two, space-padding on an explicit `""`.
|
|
528
678
|
let pad_str = match pad {
|
|
529
|
-
Value::String(p)
|
|
679
|
+
Value::String(p) => p.as_str(),
|
|
530
680
|
_ => " ",
|
|
531
681
|
};
|
|
532
682
|
let char_count = s.chars().count();
|
|
533
|
-
if char_count >= target_len {
|
|
683
|
+
if char_count >= target_len || pad_str.is_empty() {
|
|
534
684
|
return Value::String(s.clone());
|
|
535
685
|
}
|
|
536
686
|
let needed = target_len - char_count;
|
|
@@ -696,9 +846,12 @@ mod tests {
|
|
|
696
846
|
fn case_and_prefix_suffix() {
|
|
697
847
|
assert_same!(to_upper_case(&s("aB")), s("AB"));
|
|
698
848
|
assert_same!(to_lower_case(&s("aB")), s("ab"));
|
|
699
|
-
assert_same!(starts_with(&s("/api"), &s("/api")), Value::Bool(true));
|
|
700
|
-
assert_same!(ends_with(&s("x.js"), &s(".js")), Value::Bool(true));
|
|
701
|
-
assert_same!(starts_with(&n(1.0), &s("")), Value::Bool(false));
|
|
849
|
+
assert_same!(starts_with(&s("/api"), &s("/api"), None), Value::Bool(true));
|
|
850
|
+
assert_same!(ends_with(&s("x.js"), &s(".js"), None), Value::Bool(true));
|
|
851
|
+
assert_same!(starts_with(&n(1.0), &s(""), None), Value::Bool(false));
|
|
852
|
+
// 2nd-arg: position / endPosition.
|
|
853
|
+
assert_same!(starts_with(&s("abc"), &s("bc"), Some(&n(1.0))), Value::Bool(true));
|
|
854
|
+
assert_same!(ends_with(&s("abc"), &s("ab"), Some(&n(2.0))), Value::Bool(true));
|
|
702
855
|
}
|
|
703
856
|
|
|
704
857
|
#[test]
|
|
@@ -334,10 +334,16 @@ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
|
|
|
334
334
|
}
|
|
335
335
|
Statement::ForOf {
|
|
336
336
|
name: n,
|
|
337
|
-
iterable,
|
|
337
|
+
iterable: e,
|
|
338
338
|
body,
|
|
339
339
|
..
|
|
340
|
-
}
|
|
340
|
+
}
|
|
341
|
+
| Statement::ForIn {
|
|
342
|
+
name: n,
|
|
343
|
+
object: e,
|
|
344
|
+
body,
|
|
345
|
+
..
|
|
346
|
+
} => n.as_ref() == name || expr_rebinds(e, name) || stmt_rebinds(body, name),
|
|
341
347
|
Statement::Switch {
|
|
342
348
|
expr,
|
|
343
349
|
cases,
|
|
@@ -504,10 +510,16 @@ fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
|
|
|
504
510
|
}
|
|
505
511
|
Statement::ForOf {
|
|
506
512
|
name: n,
|
|
507
|
-
iterable,
|
|
513
|
+
iterable: e,
|
|
508
514
|
body,
|
|
509
515
|
..
|
|
510
|
-
}
|
|
516
|
+
}
|
|
517
|
+
| Statement::ForIn {
|
|
518
|
+
name: n,
|
|
519
|
+
object: e,
|
|
520
|
+
body,
|
|
521
|
+
..
|
|
522
|
+
} => n.as_ref() == name || expr_rebinds(e, name) || name_rebinds_in_stmt(body, name),
|
|
511
523
|
Statement::Switch {
|
|
512
524
|
expr,
|
|
513
525
|
cases,
|
|
@@ -1803,6 +1815,46 @@ impl<'a> Compiler<'a> {
|
|
|
1803
1815
|
self.emit(Opcode::LoopVarsEnd);
|
|
1804
1816
|
self.exit_block_scope();
|
|
1805
1817
|
}
|
|
1818
|
+
Statement::ForIn {
|
|
1819
|
+
name,
|
|
1820
|
+
name_span,
|
|
1821
|
+
object,
|
|
1822
|
+
body,
|
|
1823
|
+
span,
|
|
1824
|
+
} => {
|
|
1825
|
+
// Lower `for (let k in obj)` to `for (let k of Object.keys(obj))` (#413): Object.keys
|
|
1826
|
+
// yields exactly the own enumerable keys the interpreter enumerates — insertion order
|
|
1827
|
+
// for objects, index strings for arrays, and `[]` for a non-object (so `for (k in
|
|
1828
|
+
// null)` no-ops). Reuses the whole for-of iteration path.
|
|
1829
|
+
let dummy = Span {
|
|
1830
|
+
start: (0, 0),
|
|
1831
|
+
end: (0, 0),
|
|
1832
|
+
};
|
|
1833
|
+
let keys_call = Expr::Call {
|
|
1834
|
+
callee: Box::new(Expr::Member {
|
|
1835
|
+
object: Box::new(Expr::Ident {
|
|
1836
|
+
name: Arc::from("Object"),
|
|
1837
|
+
span: dummy,
|
|
1838
|
+
}),
|
|
1839
|
+
prop: MemberProp::Name {
|
|
1840
|
+
name: Arc::from("keys"),
|
|
1841
|
+
span: dummy,
|
|
1842
|
+
},
|
|
1843
|
+
optional: false,
|
|
1844
|
+
span: dummy,
|
|
1845
|
+
}),
|
|
1846
|
+
args: vec![CallArg::Expr(object.clone())],
|
|
1847
|
+
span: dummy,
|
|
1848
|
+
};
|
|
1849
|
+
let lowered = Statement::ForOf {
|
|
1850
|
+
name: Arc::clone(name),
|
|
1851
|
+
name_span: *name_span,
|
|
1852
|
+
iterable: keys_call,
|
|
1853
|
+
body: body.clone(),
|
|
1854
|
+
span: *span,
|
|
1855
|
+
};
|
|
1856
|
+
self.compile_statement(&lowered)?;
|
|
1857
|
+
}
|
|
1806
1858
|
Statement::Return { value, .. } => {
|
|
1807
1859
|
// Evaluate the return value first (JS order), then run any enclosing `finally`
|
|
1808
1860
|
// blocks (they're stack-neutral, so the value stays on top), then return.
|
|
@@ -258,7 +258,9 @@ impl MathUnaryFn {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
MathUnaryFn::Trunc => x.trunc(),
|
|
261
|
-
// JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1
|
|
261
|
+
// JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1; and for
|
|
262
|
+
// ±0 return `x` unchanged so `Math.sign(-0)` is `-0` (`x < 0.0` is false for -0, so it
|
|
263
|
+
// lands in the else — bare `0.0` would drop the sign).
|
|
262
264
|
MathUnaryFn::Sign => {
|
|
263
265
|
if x.is_nan() {
|
|
264
266
|
f64::NAN
|
|
@@ -267,7 +269,7 @@ impl MathUnaryFn {
|
|
|
267
269
|
} else if x < 0.0 {
|
|
268
270
|
-1.0
|
|
269
271
|
} else {
|
|
270
|
-
|
|
272
|
+
x
|
|
271
273
|
}
|
|
272
274
|
}
|
|
273
275
|
MathUnaryFn::Sin => x.sin(),
|