@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
|
@@ -16,7 +16,9 @@ pub use tishlang_core::ArcStr;
|
|
|
16
16
|
/// Used by native codegen for `f()` / `obj()` dispatch (`Value::Function` or `__call` on objects).
|
|
17
17
|
pub use tishlang_core::value_call;
|
|
18
18
|
/// JS ToInt32/ToUint32 for the emitted bitwise/shift code (modulo 2³², NaN/±Infinity → 0).
|
|
19
|
-
pub use tishlang_core::{
|
|
19
|
+
pub use tishlang_core::{
|
|
20
|
+
to_int32, to_int32_value, to_number_value, to_uint32, to_uint32_value,
|
|
21
|
+
};
|
|
20
22
|
// Re-export the shared-mutable wrapper so the Rust code emitted by
|
|
21
23
|
// `tishlang_compile::codegen` can write `VmRef::new(...)` without needing
|
|
22
24
|
// a direct dependency on `tishlang_core` from the generated crate.
|
|
@@ -57,8 +59,9 @@ pub use tishlang_builtins::collections::{
|
|
|
57
59
|
|
|
58
60
|
// Re-export array methods from tishlang_builtins
|
|
59
61
|
pub use tishlang_builtins::array::{
|
|
60
|
-
concat as array_concat_impl, every as array_every, filter as array_filter,
|
|
61
|
-
|
|
62
|
+
at as array_at, concat as array_concat_impl, every as array_every, filter as array_filter,
|
|
63
|
+
find as array_find, find_index as array_find_index, find_last as array_find_last,
|
|
64
|
+
find_last_index as array_find_last_index, flat as array_flat_impl, flat_map as array_flat_map,
|
|
62
65
|
for_each as array_for_each, includes as array_includes_impl, index_of as array_index_of_impl,
|
|
63
66
|
join as array_join_impl, map as array_map, pop as array_pop, push as array_push_impl,
|
|
64
67
|
fill as array_fill, reduce as array_reduce, reverse as array_reverse, shift as array_shift,
|
|
@@ -71,13 +74,13 @@ pub use tishlang_builtins::array::{
|
|
|
71
74
|
|
|
72
75
|
// Re-export string methods from tishlang_builtins
|
|
73
76
|
pub use tishlang_builtins::string::{
|
|
74
|
-
char_at as string_char_at_impl, char_code_at as string_char_code_at_impl,
|
|
77
|
+
at as string_at_impl, char_at as string_char_at_impl, char_code_at as string_char_code_at_impl,
|
|
75
78
|
ends_with as string_ends_with_impl, escape_html as string_escape_html_impl,
|
|
76
79
|
includes as string_includes_impl, index_of as string_index_of_impl,
|
|
77
80
|
last_index_of as string_last_index_of_impl, pad_end as string_pad_end_impl,
|
|
78
81
|
pad_start as string_pad_start_impl, repeat as string_repeat_impl,
|
|
79
82
|
replace as string_replace_impl, replace_all as string_replace_all_impl,
|
|
80
|
-
slice as string_slice_impl, split as string_split_impl,
|
|
83
|
+
slice as string_slice_impl, split as string_split_impl, split_limit as string_split_limit_impl,
|
|
81
84
|
starts_with as string_starts_with_impl, substr as string_substr_impl,
|
|
82
85
|
substring as string_substring_impl,
|
|
83
86
|
to_lower_case as string_to_lower_case, to_upper_case as string_to_upper_case,
|
|
@@ -141,13 +144,23 @@ pub fn string_substr(s: &Value, start: &Value, length: &Value) -> Value {
|
|
|
141
144
|
string_substr_impl(s, start, length)
|
|
142
145
|
}
|
|
143
146
|
pub fn string_split(s: &Value, sep: &Value) -> Value {
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
string_split_limit(s, sep, &Value::Null)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// `split(sep, limit)` honoring the optional `limit` argument (passed as a `Value` so the VM and
|
|
151
|
+
/// native codegen can forward the raw call argument). A non-numeric / null `limit` means "no limit".
|
|
152
|
+
/// Routes a RegExp separator to the regex splitter (matching string_replace's regex handling and
|
|
153
|
+
/// the interpreter), so `"a1b2c".split(RegExp("\\d",""))` works on the rust backend.
|
|
154
|
+
pub fn string_split_limit(s: &Value, sep: &Value, limit: &Value) -> Value {
|
|
155
|
+
let max = match limit {
|
|
156
|
+
Value::Number(n) if *n >= 0.0 => Some(*n as usize),
|
|
157
|
+
_ => None,
|
|
158
|
+
};
|
|
146
159
|
#[cfg(feature = "regex")]
|
|
147
160
|
if matches!(sep, Value::RegExp(_)) {
|
|
148
|
-
return string_split_regex(s, sep,
|
|
161
|
+
return string_split_regex(s, sep, max);
|
|
149
162
|
}
|
|
150
|
-
|
|
163
|
+
string_split_limit_impl(s, sep, max)
|
|
151
164
|
}
|
|
152
165
|
pub fn string_starts_with(s: &Value, search: &Value) -> Value {
|
|
153
166
|
string_starts_with_impl(s, search)
|
|
@@ -168,6 +181,17 @@ pub fn string_replace_all(s: &Value, search: &Value, replacement: &Value) -> Val
|
|
|
168
181
|
pub fn string_char_at(s: &Value, idx: &Value) -> Value {
|
|
169
182
|
string_char_at_impl(s, idx)
|
|
170
183
|
}
|
|
184
|
+
pub fn string_at(s: &Value, idx: &Value) -> Value {
|
|
185
|
+
string_at_impl(s, idx)
|
|
186
|
+
}
|
|
187
|
+
/// `.at(i)` dispatched on the runtime value — `at` exists on both String and Array, and the native
|
|
188
|
+
/// method match is by name (not receiver type), so route here at runtime. #247
|
|
189
|
+
pub fn value_at(recv: &Value, idx: &Value) -> Value {
|
|
190
|
+
match recv {
|
|
191
|
+
Value::String(_) => string_at_impl(recv, idx),
|
|
192
|
+
_ => array_at(recv, idx),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
171
195
|
pub fn string_char_code_at(s: &Value, idx: &Value) -> Value {
|
|
172
196
|
string_char_code_at_impl(s, idx)
|
|
173
197
|
}
|
|
@@ -509,6 +533,9 @@ pub use tishlang_builtins::math::{
|
|
|
509
533
|
round as tish_math_round_impl, sign as tish_math_sign_impl, sin as tish_math_sin_impl,
|
|
510
534
|
imul as tish_math_imul_impl,
|
|
511
535
|
sqrt as tish_math_sqrt_impl, tan as tish_math_tan_impl, trunc as tish_math_trunc_impl,
|
|
536
|
+
// hypot/atan2/asin/acos/atan were missing on the native Math but present on the vm (#247).
|
|
537
|
+
hypot as tish_math_hypot_impl, atan2 as tish_math_atan2_impl, asin as tish_math_asin_impl,
|
|
538
|
+
acos as tish_math_acos_impl, atan as tish_math_atan_impl,
|
|
512
539
|
};
|
|
513
540
|
|
|
514
541
|
// Wrapper functions to maintain API (existing callers use math_* naming)
|
|
@@ -533,6 +560,21 @@ pub fn math_min(args: &[Value]) -> Value {
|
|
|
533
560
|
pub fn math_max(args: &[Value]) -> Value {
|
|
534
561
|
tish_math_max_impl(args)
|
|
535
562
|
}
|
|
563
|
+
pub fn math_hypot(args: &[Value]) -> Value {
|
|
564
|
+
tish_math_hypot_impl(args)
|
|
565
|
+
}
|
|
566
|
+
pub fn math_atan2(args: &[Value]) -> Value {
|
|
567
|
+
tish_math_atan2_impl(args)
|
|
568
|
+
}
|
|
569
|
+
pub fn math_asin(args: &[Value]) -> Value {
|
|
570
|
+
tish_math_asin_impl(args)
|
|
571
|
+
}
|
|
572
|
+
pub fn math_acos(args: &[Value]) -> Value {
|
|
573
|
+
tish_math_acos_impl(args)
|
|
574
|
+
}
|
|
575
|
+
pub fn math_atan(args: &[Value]) -> Value {
|
|
576
|
+
tish_math_atan_impl(args)
|
|
577
|
+
}
|
|
536
578
|
pub fn math_sin(args: &[Value]) -> Value {
|
|
537
579
|
tish_math_sin_impl(args)
|
|
538
580
|
}
|
|
@@ -1288,33 +1330,31 @@ pub fn string_split_regex(s: &Value, separator: &Value, limit: Option<usize>) ->
|
|
|
1288
1330
|
match separator {
|
|
1289
1331
|
Value::RegExp(re) => {
|
|
1290
1332
|
let re = re.borrow();
|
|
1333
|
+
// JS semantics: split fully, then truncate to `limit` (don't keep the unsplit remainder
|
|
1334
|
+
// in the last slot). `truncate(usize::MAX)` is a no-op, so the no-limit path is unchanged.
|
|
1291
1335
|
let mut result = Vec::new();
|
|
1292
1336
|
let mut last_end = 0;
|
|
1293
1337
|
|
|
1294
1338
|
for mat in re.regex.find_iter(input) {
|
|
1295
1339
|
match mat {
|
|
1296
1340
|
Ok(m) => {
|
|
1297
|
-
if result.len() >= max - 1 {
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
1341
|
result.push(Value::String(input[last_end..m.start()].into()));
|
|
1301
1342
|
last_end = m.end();
|
|
1302
1343
|
}
|
|
1303
1344
|
Err(_) => break,
|
|
1304
1345
|
}
|
|
1305
1346
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
result.push(Value::String(input[last_end..].into()));
|
|
1309
|
-
}
|
|
1347
|
+
result.push(Value::String(input[last_end..].into()));
|
|
1348
|
+
result.truncate(max);
|
|
1310
1349
|
|
|
1311
1350
|
Value::Array(VmRef::new(result))
|
|
1312
1351
|
}
|
|
1313
1352
|
Value::String(sep) => {
|
|
1314
|
-
let parts: Vec<Value> = input
|
|
1315
|
-
.
|
|
1353
|
+
let mut parts: Vec<Value> = input
|
|
1354
|
+
.split(sep.as_str())
|
|
1316
1355
|
.map(|s| Value::String(s.into()))
|
|
1317
1356
|
.collect();
|
|
1357
|
+
parts.truncate(max);
|
|
1318
1358
|
Value::Array(VmRef::new(parts))
|
|
1319
1359
|
}
|
|
1320
1360
|
_ => Value::Array(VmRef::new(vec![Value::String(input.into())])),
|
|
@@ -302,7 +302,7 @@ fn collect_fun_decl_names_expr(expr: &Expr, names: &mut HashSet<String>) {
|
|
|
302
302
|
Expr::Object { props, .. } => {
|
|
303
303
|
for p in props {
|
|
304
304
|
match p {
|
|
305
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
|
|
305
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
|
|
306
306
|
collect_fun_decl_names_expr(e, names);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
@@ -650,7 +650,7 @@ fn expr_contains_jsx(expr: &Expr) -> bool {
|
|
|
650
650
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_contains_jsx(e),
|
|
651
651
|
}),
|
|
652
652
|
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
653
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_contains_jsx(e),
|
|
653
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_contains_jsx(e),
|
|
654
654
|
}),
|
|
655
655
|
Expr::ArrowFunction { body, .. } => match body {
|
|
656
656
|
tishlang_ast::ArrowBody::Expr(e) => expr_contains_jsx(e),
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
//! frame slot and a block per bytecode jump target; handles for/while/nested loops, if/else,
|
|
10
10
|
//! early return, break, continue. Enabled by slot-based locals making such functions `slot_based`.
|
|
11
11
|
//!
|
|
12
|
-
//! Anything unsupported (member/index, calls, arrays/objects, non-number constants, pow
|
|
12
|
+
//! Anything unsupported (member/index, calls, arrays/objects, non-number constants, pow,
|
|
13
13
|
//! booleans in slots, a ternary inside a loop) makes compilation return `None`, and any non-number
|
|
14
14
|
//! argument at call time falls back to the interpreter — so this is purely ADDITIVE and can never
|
|
15
15
|
//! change behaviour (a miss runs the VM). Only a logic bug here could, hence the differential
|
|
16
|
-
//! validation (vm-JIT ≡ interp ≡ node) + `tests/core/jit_loops.tish`.
|
|
16
|
+
//! validation (vm-JIT ≡ interp ≡ node) + `tests/core/jit_loops.tish`, `tests/core/jit_shifts.tish`.
|
|
17
|
+
//! Bitwise `& | ^ ~` and shifts `<< >> >>>` are lowered (JS ToInt32/ToUint32 semantics, bit-exact
|
|
18
|
+
//! with the VM's `eval_binop`); only `**` (pow) still falls back.
|
|
17
19
|
//!
|
|
18
20
|
//! Not compiled for wasm targets (cranelift-jit emits host code).
|
|
19
21
|
|
|
@@ -179,9 +181,15 @@ impl NumericFn {
|
|
|
179
181
|
|
|
180
182
|
struct JitGlobal {
|
|
181
183
|
module: JITModule,
|
|
182
|
-
/// Keyed by the address of the
|
|
183
|
-
///
|
|
184
|
-
cache
|
|
184
|
+
/// Keyed by the address of the nested `Chunk`, with a content **fingerprint** alongside the
|
|
185
|
+
/// result. Within one program run a chunk lives for the whole run, so the address is stable and
|
|
186
|
+
/// unique. But this cache is a process-global that is never cleared, and a `Chunk` is dropped when
|
|
187
|
+
/// its program is — so a long-lived process that compiles/drops/recompiles programs (the REPL;
|
|
188
|
+
/// embedders running multiple scripts) can allocate a *different* chunk at a freed address that is
|
|
189
|
+
/// still cached. We therefore verify the fingerprint on every hit: a mismatch means the address was
|
|
190
|
+
/// reused by a different chunk, so we recompile (and overwrite) instead of returning stale native
|
|
191
|
+
/// code. `None` still caches "not JIT-eligible". See [`chunk_fingerprint`].
|
|
192
|
+
cache: HashMap<usize, (u64, Option<NumericFn>)>,
|
|
185
193
|
counter: usize,
|
|
186
194
|
}
|
|
187
195
|
|
|
@@ -228,6 +236,65 @@ fn read_u16(code: &[u8], ip: &mut usize) -> Option<u16> {
|
|
|
228
236
|
Some((a << 8) | b)
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/// Content fingerprint of everything `compile_chunk` reads, so a cache entry can be validated against
|
|
240
|
+
/// the chunk currently at a (possibly reused) address. FNV-1a over the compile-relevant fields:
|
|
241
|
+
/// shape (`param_count`, `num_slots`, `rest_param_index`, `slot_based`), the full `code` bytes, and
|
|
242
|
+
/// the `constants` (the JIT emits `f64const`/bool from `LoadConst`, so their values matter). The JIT
|
|
243
|
+
/// makes no cross-chunk calls (`op_size` allows only `SelfCall`, which recurses into *this* function),
|
|
244
|
+
/// so nothing outside the chunk affects the result — this fingerprint is complete. Deterministic
|
|
245
|
+
/// within a process (fixed FNV constants, not a randomized hasher), which is all the cache needs.
|
|
246
|
+
fn chunk_fingerprint(chunk: &Chunk) -> u64 {
|
|
247
|
+
// Mixes a u64 at a time (FNV-prime multiply + an avalanche shift). Eight bytes per round keeps
|
|
248
|
+
// this cheap on the hot closure-creation path; correctness only needs determinism + good
|
|
249
|
+
// distinction, not cryptographic strength.
|
|
250
|
+
#[inline]
|
|
251
|
+
fn mix(h: &mut u64, v: u64) {
|
|
252
|
+
*h ^= v;
|
|
253
|
+
*h = h.wrapping_mul(0x0000_0100_0000_01b3);
|
|
254
|
+
*h ^= *h >> 29;
|
|
255
|
+
}
|
|
256
|
+
#[inline]
|
|
257
|
+
fn mix_bytes(h: &mut u64, bytes: &[u8]) {
|
|
258
|
+
let mut it = bytes.chunks_exact(8);
|
|
259
|
+
for w in &mut it {
|
|
260
|
+
mix(h, u64::from_le_bytes(w.try_into().unwrap()));
|
|
261
|
+
}
|
|
262
|
+
let rem = it.remainder();
|
|
263
|
+
if !rem.is_empty() {
|
|
264
|
+
let mut buf = [0u8; 8];
|
|
265
|
+
buf[..rem.len()].copy_from_slice(rem);
|
|
266
|
+
mix(h, u64::from_le_bytes(buf));
|
|
267
|
+
}
|
|
268
|
+
mix(h, bytes.len() as u64);
|
|
269
|
+
}
|
|
270
|
+
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
|
271
|
+
mix(&mut h, chunk.param_count as u64);
|
|
272
|
+
mix(&mut h, chunk.num_slots as u64);
|
|
273
|
+
mix(&mut h, chunk.rest_param_index as u64);
|
|
274
|
+
mix(&mut h, chunk.slot_based as u64);
|
|
275
|
+
mix_bytes(&mut h, &chunk.code);
|
|
276
|
+
mix(&mut h, chunk.constants.len() as u64);
|
|
277
|
+
for c in &chunk.constants {
|
|
278
|
+
match c {
|
|
279
|
+
Constant::Number(n) => {
|
|
280
|
+
mix(&mut h, 1);
|
|
281
|
+
mix(&mut h, n.to_bits());
|
|
282
|
+
}
|
|
283
|
+
Constant::String(s) => {
|
|
284
|
+
mix(&mut h, 2);
|
|
285
|
+
mix_bytes(&mut h, s.as_bytes());
|
|
286
|
+
}
|
|
287
|
+
Constant::Bool(b) => mix(&mut h, if *b { 4 } else { 3 }),
|
|
288
|
+
Constant::Null => mix(&mut h, 5),
|
|
289
|
+
Constant::Closure(idx) => {
|
|
290
|
+
mix(&mut h, 6);
|
|
291
|
+
mix(&mut h, *idx as u64);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
h
|
|
296
|
+
}
|
|
297
|
+
|
|
231
298
|
/// Get (or compile, then cache) the native numeric function for `chunk`.
|
|
232
299
|
/// Returns `None` if the chunk isn't a straight-line numeric function.
|
|
233
300
|
pub fn try_compile_numeric(chunk: &Chunk) -> Option<NumericFn> {
|
|
@@ -239,13 +306,18 @@ pub fn try_compile_numeric(chunk: &Chunk) -> Option<NumericFn> {
|
|
|
239
306
|
return None;
|
|
240
307
|
}
|
|
241
308
|
let key = chunk as *const Chunk as usize;
|
|
309
|
+
let fp = chunk_fingerprint(chunk);
|
|
242
310
|
let lock = jit()?;
|
|
243
311
|
let mut g = lock.lock().ok()?;
|
|
244
|
-
if
|
|
245
|
-
|
|
312
|
+
// Hit only counts if the fingerprint matches: otherwise this address was freed and reused by a
|
|
313
|
+
// *different* chunk, and the cached `NumericFn` is native code for the old one (a miscompile).
|
|
314
|
+
if let Some(&(cached_fp, cached)) = g.cache.get(&key) {
|
|
315
|
+
if cached_fp == fp {
|
|
316
|
+
return cached;
|
|
317
|
+
}
|
|
246
318
|
}
|
|
247
319
|
let result = compile_chunk(&mut g, chunk);
|
|
248
|
-
g.cache.insert(key, result);
|
|
320
|
+
g.cache.insert(key, (fp, result));
|
|
249
321
|
result
|
|
250
322
|
}
|
|
251
323
|
|
|
@@ -561,7 +633,6 @@ fn emit_simple_op(
|
|
|
561
633
|
bcx.ins().fsub(l, p)
|
|
562
634
|
}
|
|
563
635
|
// Bitwise AND/OR/XOR via JS ToInt32 (modulo 2³², NaN/±∞ → 0) — see [`js_to_int32`].
|
|
564
|
-
// Shifts / `>>>` stay on the VM (shift-amount edge cases).
|
|
565
636
|
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => {
|
|
566
637
|
let li = js_to_int32(bcx, l);
|
|
567
638
|
let ri = js_to_int32(bcx, r);
|
|
@@ -573,7 +644,36 @@ fn emit_simple_op(
|
|
|
573
644
|
};
|
|
574
645
|
bcx.ins().fcvt_from_sint(types::F64, res)
|
|
575
646
|
}
|
|
576
|
-
//
|
|
647
|
+
// Shifts. JS masks the count to the low 5 bits (`& 31`); the low 5 bits of `ToInt32(r)`
|
|
648
|
+
// equal `ToUint32(r)`, so `js_to_int32(r)` carries the right amount. We mask explicitly
|
|
649
|
+
// (`& 31`) so correctness never depends on Cranelift's own amount-masking convention.
|
|
650
|
+
// `<<`/`>>` are signed-domain (ToInt32 → i32 → signed→f64); `>>>` is logical on the
|
|
651
|
+
// unsigned bits with an UNSIGNED→f64 convert (result may exceed 2³¹). Bit-for-bit with
|
|
652
|
+
// vm.rs `eval_binop`: Shl/Shr = `to_int32(l).wrapping_sh*(to_uint32(r))`,
|
|
653
|
+
// UShr = `to_uint32(l).wrapping_shr(to_uint32(r))`.
|
|
654
|
+
BinOp::Shl | BinOp::Shr | BinOp::UShr => {
|
|
655
|
+
let li = js_to_int32(bcx, l);
|
|
656
|
+
let amt = js_to_int32(bcx, r);
|
|
657
|
+
let mask = bcx.ins().iconst(types::I32, 31);
|
|
658
|
+
let amt = bcx.ins().band(amt, mask);
|
|
659
|
+
match bop {
|
|
660
|
+
BinOp::Shl => {
|
|
661
|
+
let res = bcx.ins().ishl(li, amt);
|
|
662
|
+
bcx.ins().fcvt_from_sint(types::F64, res)
|
|
663
|
+
}
|
|
664
|
+
BinOp::Shr => {
|
|
665
|
+
let res = bcx.ins().sshr(li, amt);
|
|
666
|
+
bcx.ins().fcvt_from_sint(types::F64, res)
|
|
667
|
+
}
|
|
668
|
+
// UShr: logical shift on the same 32-bit value bits as ToUint32(l), then
|
|
669
|
+
// unsigned→f64 so a result with bit 31 set stays a positive number (JS `>>>`).
|
|
670
|
+
_ => {
|
|
671
|
+
let res = bcx.ins().ushr(li, amt);
|
|
672
|
+
bcx.ins().fcvt_from_uint(types::F64, res)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Pow/In/And/Or: fall back to the VM.
|
|
577
677
|
_ => return SimpleOp::Unsupported,
|
|
578
678
|
};
|
|
579
679
|
stack.push((v, is_cmp));
|
|
@@ -1048,3 +1148,105 @@ fn build_body(
|
|
|
1048
1148
|
bcx.finalize();
|
|
1049
1149
|
result
|
|
1050
1150
|
}
|
|
1151
|
+
|
|
1152
|
+
#[cfg(all(test, not(target_arch = "wasm32")))]
|
|
1153
|
+
mod tests {
|
|
1154
|
+
use super::*;
|
|
1155
|
+
use tishlang_core::{to_int32, to_uint32};
|
|
1156
|
+
|
|
1157
|
+
/// First arity-2 slot-based numeric nested chunk in compiled `src` (cloned, so the caller owns it).
|
|
1158
|
+
fn fn_chunk(src: &str) -> Chunk {
|
|
1159
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
1160
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
1161
|
+
let top = tishlang_bytecode::compile(&opt).expect("compile");
|
|
1162
|
+
fn find(c: &Chunk) -> Option<Chunk> {
|
|
1163
|
+
for n in &c.nested {
|
|
1164
|
+
if n.slot_based && n.param_count == 2 && n.rest_param_index == NO_REST_PARAM {
|
|
1165
|
+
return Some(n.clone());
|
|
1166
|
+
}
|
|
1167
|
+
if let Some(x) = find(n) {
|
|
1168
|
+
return Some(x);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
None
|
|
1172
|
+
}
|
|
1173
|
+
find(&top).expect("expected an arity-2 slot-based fn chunk")
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/// Compile `src`, then return the first arity-2 pure-numeric function the JIT accepts. Panics if
|
|
1177
|
+
/// none compiles — so a change that makes the JIT silently *stop* compiling the target (the exact
|
|
1178
|
+
/// "vacuous fixture" miss that motivated this guard) fails loudly instead of passing emptily.
|
|
1179
|
+
///
|
|
1180
|
+
/// Bypasses [`try_compile_numeric`]'s cache and calls [`compile_chunk`] directly: the cache is
|
|
1181
|
+
/// keyed by chunk address, unique-and-stable in a real run but reused across this test's transient
|
|
1182
|
+
/// chunks. Compiling fresh is correct here and still exercises the real lowering path.
|
|
1183
|
+
fn jit_arity2(src: &str) -> NumericFn {
|
|
1184
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
1185
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
1186
|
+
let chunk = tishlang_bytecode::compile(&opt).expect("compile");
|
|
1187
|
+
fn compile_uncached(c: &Chunk) -> Option<NumericFn> {
|
|
1188
|
+
if !c.slot_based
|
|
1189
|
+
|| c.rest_param_index != NO_REST_PARAM
|
|
1190
|
+
|| c.param_count == 0
|
|
1191
|
+
|| c.param_count > 8
|
|
1192
|
+
{
|
|
1193
|
+
return None;
|
|
1194
|
+
}
|
|
1195
|
+
let lock = jit()?;
|
|
1196
|
+
let mut g = lock.lock().ok()?;
|
|
1197
|
+
compile_chunk(&mut g, c)
|
|
1198
|
+
}
|
|
1199
|
+
fn find(c: &Chunk) -> Option<NumericFn> {
|
|
1200
|
+
for n in &c.nested {
|
|
1201
|
+
if let Some(f) = compile_uncached(n) {
|
|
1202
|
+
if f.arity == 2 && f.array_param_mask == 0 {
|
|
1203
|
+
return Some(f);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if let Some(f) = find(n) {
|
|
1207
|
+
return Some(f);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
None
|
|
1211
|
+
}
|
|
1212
|
+
find(&chunk).expect("the JIT must compile this arity-2 numeric fn (did it start bailing?)")
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/// Regression for the address-reuse stale hit (#247): compile one function, then overwrite the SAME
|
|
1216
|
+
/// heap `Chunk` (same address = the cache key) with a *different* function — what a long-lived
|
|
1217
|
+
/// process (REPL / multi-script embedder) does when a freed chunk address is reused. Before the
|
|
1218
|
+
/// fingerprint check the cache returned the first function's native code for the second.
|
|
1219
|
+
#[test]
|
|
1220
|
+
fn jit_cache_detects_address_reuse() {
|
|
1221
|
+
let mut boxed: Box<Chunk> = Box::new(fn_chunk("const f = (a, b) => a - b\nf(0, 0)\n"));
|
|
1222
|
+
let sub = try_compile_numeric(&boxed).expect("a - b must JIT");
|
|
1223
|
+
assert_eq!(sub.call(&[10.0, 3.0]), 7.0, "sub baseline");
|
|
1224
|
+
|
|
1225
|
+
*boxed = fn_chunk("const f = (a, b) => a * b\nf(0, 0)\n"); // same address, different content
|
|
1226
|
+
let mul = try_compile_numeric(&boxed).expect("a * b must JIT");
|
|
1227
|
+
assert_eq!(
|
|
1228
|
+
mul.call(&[10.0, 3.0]),
|
|
1229
|
+
30.0,
|
|
1230
|
+
"stale cache hit: reused address returned the old (sub) fn instead of mul"
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/// Permanent guard for #168: shifts must (a) actually JIT-compile and (b) be bit-exact with the
|
|
1235
|
+
/// VM's `eval_binop` (`to_int32`/`to_uint32` + wrapping shift). Breaking either fails this test.
|
|
1236
|
+
#[test]
|
|
1237
|
+
fn jit_lowers_shifts_bit_exact_to_vm() {
|
|
1238
|
+
let shl = jit_arity2("const f = (a, b) => a << b\nf(0, 0)\n");
|
|
1239
|
+
let shr = jit_arity2("const f = (a, b) => a >> b\nf(0, 0)\n");
|
|
1240
|
+
let ushr = jit_arity2("const f = (a, b) => a >>> b\nf(0, 0)\n");
|
|
1241
|
+
let cases = [
|
|
1242
|
+
(1.0, 4.0), (1.0, 32.0), (1.0, 33.0), (1.0, -1.0), (-8.0, 1.0), (-1.0, 0.0),
|
|
1243
|
+
(-2.0, 1.0), (4294967295.0, 0.0), (3.9, 0.0), (4294967297.0, 0.0),
|
|
1244
|
+
(-123456789.0, 5.0), (65535.0, 16.0),
|
|
1245
|
+
];
|
|
1246
|
+
for (a, b) in cases {
|
|
1247
|
+
assert_eq!(shl.call(&[a, b]), to_int32(a).wrapping_shl(to_uint32(b)) as f64, "<< {a} {b}");
|
|
1248
|
+
assert_eq!(shr.call(&[a, b]), to_int32(a).wrapping_shr(to_uint32(b)) as f64, ">> {a} {b}");
|
|
1249
|
+
assert_eq!(ushr.call(&[a, b]), to_uint32(a).wrapping_shr(to_uint32(b)) as f64, ">>> {a} {b}");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -264,7 +264,7 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
264
264
|
tishlang_runtime::process_exec(args)
|
|
265
265
|
})),
|
|
266
266
|
"argv" => Some(Value::Array(VmRef::new(
|
|
267
|
-
|
|
267
|
+
tishlang_core::process_argv().into_iter().map(|s| Value::String(s.into())).collect(),
|
|
268
268
|
))),
|
|
269
269
|
"env" => Some(value_object_from_map(
|
|
270
270
|
std::env::vars()
|
|
@@ -288,7 +288,7 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
288
288
|
m.insert(
|
|
289
289
|
"argv".into(),
|
|
290
290
|
Value::Array(VmRef::new(
|
|
291
|
-
|
|
291
|
+
tishlang_core::process_argv().into_iter().map(|s| Value::String(s.into())).collect(),
|
|
292
292
|
)),
|
|
293
293
|
);
|
|
294
294
|
m.insert(
|
|
@@ -462,12 +462,11 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
462
462
|
Value::Number(n.ceil())
|
|
463
463
|
}),
|
|
464
464
|
);
|
|
465
|
+
// round/min/max delegate to the shared builtins (JS round-half-to-+∞, empty → ±∞, NaN
|
|
466
|
+
// propagation) so the vm never diverges from interp/native on Math semantics (#247).
|
|
465
467
|
math.insert(
|
|
466
468
|
"round".into(),
|
|
467
|
-
Value::native(|args: &[Value]|
|
|
468
|
-
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
469
|
-
Value::Number(n.round())
|
|
470
|
-
}),
|
|
469
|
+
Value::native(|args: &[Value]| math_builtins::round(args)),
|
|
471
470
|
);
|
|
472
471
|
math.insert(
|
|
473
472
|
"random".into(),
|
|
@@ -475,17 +474,11 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
475
474
|
);
|
|
476
475
|
math.insert(
|
|
477
476
|
"min".into(),
|
|
478
|
-
Value::native(|args: &[Value]|
|
|
479
|
-
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
|
|
480
|
-
Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.min(b)))
|
|
481
|
-
}),
|
|
477
|
+
Value::native(|args: &[Value]| math_builtins::min(args)),
|
|
482
478
|
);
|
|
483
479
|
math.insert(
|
|
484
480
|
"max".into(),
|
|
485
|
-
Value::native(|args: &[Value]|
|
|
486
|
-
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
|
|
487
|
-
Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.max(b)))
|
|
488
|
-
}),
|
|
481
|
+
Value::native(|args: &[Value]| math_builtins::max(args)),
|
|
489
482
|
);
|
|
490
483
|
math.insert(
|
|
491
484
|
"pow".into(),
|
|
@@ -795,7 +788,7 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
795
788
|
process_obj.insert(
|
|
796
789
|
"argv".into(),
|
|
797
790
|
Value::Array(VmRef::new(
|
|
798
|
-
|
|
791
|
+
tishlang_core::process_argv().into_iter().map(|s| Value::String(s.into())).collect(),
|
|
799
792
|
)),
|
|
800
793
|
);
|
|
801
794
|
process_obj.insert(
|
|
@@ -2969,6 +2962,9 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2969
2962
|
"forEach" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::for_each(&bv, &cb) }),
|
|
2970
2963
|
"find" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::find(&bv, &cb) }),
|
|
2971
2964
|
"findIndex" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::find_index(&bv, &cb) }),
|
|
2965
|
+
"findLast" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::find_last(&bv, &cb) }),
|
|
2966
|
+
"findLastIndex" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::find_last_index(&bv, &cb) }),
|
|
2967
|
+
"at" => make_native_fn(move |args| { let i = args.first().cloned().unwrap_or(Value::Null); arr_builtins::at(&bv, &i) }),
|
|
2972
2968
|
"some" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::some(&bv, &cb) }),
|
|
2973
2969
|
"every" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::every(&bv, &cb) }),
|
|
2974
2970
|
"join" => make_native_fn(move |args| { let sep = args.first().cloned().unwrap_or(Value::Null); arr_builtins::join(&bv, &sep) }),
|
|
@@ -3073,6 +3069,18 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3073
3069
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3074
3070
|
arr_builtins::find_index(&Value::Array(a_clone.clone()), &cb)
|
|
3075
3071
|
}),
|
|
3072
|
+
"findLast" => make_native_fn(move |args: &[Value]| {
|
|
3073
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3074
|
+
arr_builtins::find_last(&Value::Array(a_clone.clone()), &cb)
|
|
3075
|
+
}),
|
|
3076
|
+
"findLastIndex" => make_native_fn(move |args: &[Value]| {
|
|
3077
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3078
|
+
arr_builtins::find_last_index(&Value::Array(a_clone.clone()), &cb)
|
|
3079
|
+
}),
|
|
3080
|
+
"at" => make_native_fn(move |args: &[Value]| {
|
|
3081
|
+
let i = args.first().cloned().unwrap_or(Value::Null);
|
|
3082
|
+
arr_builtins::at(&Value::Array(a_clone.clone()), &i)
|
|
3083
|
+
}),
|
|
3076
3084
|
"some" => make_native_fn(move |args: &[Value]| {
|
|
3077
3085
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3078
3086
|
arr_builtins::some(&Value::Array(a_clone.clone()), &cb)
|
|
@@ -3159,15 +3167,24 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3159
3167
|
}),
|
|
3160
3168
|
"split" => make_native_fn(move |args: &[Value]| {
|
|
3161
3169
|
let sep = args.first().unwrap_or(&Value::Null);
|
|
3170
|
+
let limit = args.get(1).unwrap_or(&Value::Null);
|
|
3171
|
+
let max = match limit {
|
|
3172
|
+
Value::Number(n) if *n >= 0.0 => Some(*n as usize),
|
|
3173
|
+
_ => None,
|
|
3174
|
+
};
|
|
3175
|
+
// A RegExp separator needs the runtime's regex path — but a `Value::RegExp` can
|
|
3176
|
+
// only exist with the `regex` feature, which is also what pulls in the optional
|
|
3177
|
+
// `tishlang_runtime`. String separators use the always-available builtin, so
|
|
3178
|
+
// `tish_vm` still compiles (and tests) without the optional runtime crate.
|
|
3162
3179
|
#[cfg(feature = "regex")]
|
|
3163
3180
|
if matches!(sep, Value::RegExp(_)) {
|
|
3164
|
-
return tishlang_runtime::
|
|
3181
|
+
return tishlang_runtime::string_split_limit(
|
|
3165
3182
|
&Value::String(s_clone.clone()),
|
|
3166
3183
|
sep,
|
|
3167
|
-
|
|
3184
|
+
limit,
|
|
3168
3185
|
);
|
|
3169
3186
|
}
|
|
3170
|
-
str_builtins::
|
|
3187
|
+
str_builtins::split_limit(&Value::String(s_clone.clone()), sep, max)
|
|
3171
3188
|
}),
|
|
3172
3189
|
"trim" => make_native_fn(move |_args: &[Value]| {
|
|
3173
3190
|
str_builtins::trim(&Value::String(s_clone.clone()))
|
|
@@ -3224,6 +3241,10 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3224
3241
|
let idx = args.first().unwrap_or(&Value::Null);
|
|
3225
3242
|
str_builtins::char_at(&Value::String(s_clone.clone()), idx)
|
|
3226
3243
|
}),
|
|
3244
|
+
"at" => make_native_fn(move |args: &[Value]| {
|
|
3245
|
+
let idx = args.first().unwrap_or(&Value::Null);
|
|
3246
|
+
str_builtins::at(&Value::String(s_clone.clone()), idx)
|
|
3247
|
+
}),
|
|
3227
3248
|
"charCodeAt" => make_native_fn(move |args: &[Value]| {
|
|
3228
3249
|
let idx = args.first().unwrap_or(&Value::Null);
|
|
3229
3250
|
str_builtins::char_code_at(&Value::String(s_clone.clone()), idx)
|