@tishlang/tish-lsp 2.38.0 → 2.43.8
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/Cargo.toml +4 -1
- package/bin/tish-lsp +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +38 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +812 -47
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +148 -10
- package/crates/tish_compile/src/schemes.rs +278 -0
- package/crates/tish_compile/src/types.rs +141 -2
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +19 -1
- package/crates/tish_core/src/json.rs +34 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +182 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_lsp/Cargo.toml +3 -1
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/http_hyper.rs +70 -0
- package/crates/tish_runtime/src/lib.rs +55 -10
- package/crates/tish_runtime/src/ws.rs +94 -21
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +620 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +535 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -693,6 +693,16 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
693
693
|
math.insert("fround".into(), Value::native(|a: &[Value]| math_builtins::fround(a)));
|
|
694
694
|
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
|
|
695
695
|
math.insert("E".into(), Value::Number(std::f64::consts::E));
|
|
696
|
+
// #539: the remaining ECMAScript Math value constants (were missing → `null`).
|
|
697
|
+
math.insert("LN2".into(), Value::Number(std::f64::consts::LN_2));
|
|
698
|
+
math.insert("LN10".into(), Value::Number(std::f64::consts::LN_10));
|
|
699
|
+
math.insert("LOG2E".into(), Value::Number(std::f64::consts::LOG2_E));
|
|
700
|
+
math.insert("LOG10E".into(), Value::Number(std::f64::consts::LOG10_E));
|
|
701
|
+
math.insert("SQRT2".into(), Value::Number(std::f64::consts::SQRT_2));
|
|
702
|
+
math.insert(
|
|
703
|
+
"SQRT1_2".into(),
|
|
704
|
+
Value::Number(std::f64::consts::FRAC_1_SQRT_2),
|
|
705
|
+
);
|
|
696
706
|
g.insert("Math".into(), value_object_from_map(math));
|
|
697
707
|
|
|
698
708
|
let mut json = ObjectMap::default();
|
|
@@ -860,6 +870,14 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
860
870
|
"fromEntries".into(),
|
|
861
871
|
Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
|
|
862
872
|
);
|
|
873
|
+
object_methods.insert(
|
|
874
|
+
"freeze".into(),
|
|
875
|
+
Value::native(|args: &[Value]| globals_builtins::object_freeze(args)),
|
|
876
|
+
);
|
|
877
|
+
object_methods.insert(
|
|
878
|
+
"isFrozen".into(),
|
|
879
|
+
Value::native(|args: &[Value]| globals_builtins::object_is_frozen(args)),
|
|
880
|
+
);
|
|
863
881
|
object_methods.insert(
|
|
864
882
|
"keys".into(),
|
|
865
883
|
Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
|
|
@@ -1382,7 +1400,14 @@ mod hof_fusion {
|
|
|
1382
1400
|
#[inline]
|
|
1383
1401
|
fn numeric_snapshot(arr: &Value) -> Option<Vec<f64>> {
|
|
1384
1402
|
match arr {
|
|
1385
|
-
Value::NumberArray(a) =>
|
|
1403
|
+
Value::NumberArray(a) => match a.borrow().as_packed() {
|
|
1404
|
+
Some(v) => Some(v.clone()),
|
|
1405
|
+
None => a
|
|
1406
|
+
.borrow()
|
|
1407
|
+
.as_boxed()
|
|
1408
|
+
.map(|vs| vs.iter().map(|v| v.as_number()).collect::<Option<Vec<f64>>>())
|
|
1409
|
+
.unwrap_or(None),
|
|
1410
|
+
},
|
|
1386
1411
|
Value::Array(a) => {
|
|
1387
1412
|
let b = a.borrow();
|
|
1388
1413
|
let mut out = Vec::with_capacity(b.len());
|
|
@@ -1671,19 +1696,27 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1671
1696
|
native_modules: self.native_modules.clone(),
|
|
1672
1697
|
};
|
|
1673
1698
|
#[cfg(not(target_arch = "wasm32"))]
|
|
1674
|
-
let
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
.unwrap_or(Value::Null)
|
|
1678
|
-
})
|
|
1679
|
-
};
|
|
1699
|
+
let run = stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
1700
|
+
vm.run_chunk(self.chunk.as_ref(), &self.chunk.nested, args, false)
|
|
1701
|
+
});
|
|
1680
1702
|
#[cfg(target_arch = "wasm32")]
|
|
1681
|
-
let
|
|
1682
|
-
vm.run_chunk(&self.chunk, &self.chunk.nested, args, false)
|
|
1683
|
-
.unwrap_or(Value::Null)
|
|
1684
|
-
};
|
|
1703
|
+
let run = vm.run_chunk(&self.chunk, &self.chunk.nested, args, false);
|
|
1685
1704
|
tishlang_core::dec_call_depth();
|
|
1686
|
-
|
|
1705
|
+
match run {
|
|
1706
|
+
Ok(v) => v,
|
|
1707
|
+
// A catchable throw is already parked in the pending slot; the caller's
|
|
1708
|
+
// take_pending_throw() surfaces it, so just yield Null here (issue #60).
|
|
1709
|
+
Err(ref e) if e == PENDING_THROW_SENTINEL || tishlang_core::has_pending_throw() => {
|
|
1710
|
+
Value::Null
|
|
1711
|
+
}
|
|
1712
|
+
// A plain runtime error inside the body (Undefined variable, type error, bad index,
|
|
1713
|
+
// call-of-non-function, …) must NOT vanish to null — park it as a catchable error so the
|
|
1714
|
+
// caller propagates it, matching the interpreter and top-level VM.
|
|
1715
|
+
Err(e) => {
|
|
1716
|
+
set_pending_throw(construct_builtin::error_object("Error", &e));
|
|
1717
|
+
Value::Null
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1687
1720
|
}
|
|
1688
1721
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
1689
1722
|
self
|
|
@@ -1891,15 +1924,91 @@ impl Vm {
|
|
|
1891
1924
|
_ => return OsrResult::LiveInMiss, // a live slot is non-numeric → deopt, keep interpreting
|
|
1892
1925
|
}
|
|
1893
1926
|
}
|
|
1927
|
+
// #203: array live-ins → a scratch `Vec<f64>` per array (the matmul lever). `scratch` OWNS the
|
|
1928
|
+
// extracted data; the [`crate::jit::ArrayHandle`]s point into it and are built only AFTER it is
|
|
1929
|
+
// fully populated so no inner buffer reallocates under a live pointer. A live-in that is not an
|
|
1930
|
+
// all-`Number` `Value::Array` (a `NumberArray`, a boxed non-number element, a non-array) bails
|
|
1931
|
+
// to the interpreter — so a misclassified array slot is always correct, just not JIT'd.
|
|
1932
|
+
let mut scratch: Vec<Vec<f64>> = Vec::with_capacity(loopfn.array_slots.len());
|
|
1933
|
+
for &(slot, _writable) in &loopfn.array_slots {
|
|
1934
|
+
match slots.get(slot_base + slot as usize) {
|
|
1935
|
+
Some(Value::Array(a)) => {
|
|
1936
|
+
let b = a.borrow();
|
|
1937
|
+
let mut ebuf: Vec<f64> = Vec::with_capacity(b.len());
|
|
1938
|
+
for el in b.iter() {
|
|
1939
|
+
match el {
|
|
1940
|
+
Value::Number(n) => ebuf.push(*n),
|
|
1941
|
+
_ => return OsrResult::LiveInMiss,
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
scratch.push(ebuf);
|
|
1945
|
+
}
|
|
1946
|
+
_ => return OsrResult::LiveInMiss,
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
// #203: a WRITABLE array must not alias another array live-in — the region reads/writes a
|
|
1950
|
+
// PRIVATE scratch copy, so aliasing would make one array's writes invisible to the other (the
|
|
1951
|
+
// interpreter shares one backing store). Bail to the interpreter on any such overlap.
|
|
1952
|
+
for (k, &(slot_k, writable_k)) in loopfn.array_slots.iter().enumerate() {
|
|
1953
|
+
if !writable_k {
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
let ak = match slots.get(slot_base + slot_k as usize) {
|
|
1957
|
+
Some(Value::Array(a)) => a,
|
|
1958
|
+
_ => return OsrResult::LiveInMiss,
|
|
1959
|
+
};
|
|
1960
|
+
for (j, &(slot_j, _)) in loopfn.array_slots.iter().enumerate() {
|
|
1961
|
+
if j == k {
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
if let Some(Value::Array(aj)) = slots.get(slot_base + slot_j as usize) {
|
|
1965
|
+
if VmRef::ptr_eq(ak, aj) {
|
|
1966
|
+
return OsrResult::LiveInMiss;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
let mut handles: Vec<crate::jit::ArrayHandle> = scratch
|
|
1972
|
+
.iter_mut()
|
|
1973
|
+
.map(|b| crate::jit::ArrayHandle {
|
|
1974
|
+
ptr: b.as_mut_ptr(),
|
|
1975
|
+
len: b.len(),
|
|
1976
|
+
})
|
|
1977
|
+
.collect();
|
|
1894
1978
|
let mut deopt: u8 = 0;
|
|
1895
|
-
// SAFETY: `buf` is a valid `[f64; used_slots.len()]
|
|
1896
|
-
//
|
|
1897
|
-
|
|
1979
|
+
// SAFETY: `buf` is a valid `[f64; used_slots.len()]`, `handles` a valid `[ArrayHandle;
|
|
1980
|
+
// array_slots.len()]` whose scratch outlives the call, `deopt` a valid `*mut u8`. The region was
|
|
1981
|
+
// compiled for exactly this chunk+header (fingerprint-checked in the cache) and its ABI matches
|
|
1982
|
+
// `array_slots` (2-pointer when empty, 3-pointer otherwise).
|
|
1983
|
+
let exit_id = loopfn.call(&mut buf, &mut handles, &mut deopt);
|
|
1984
|
+
if deopt != 0 {
|
|
1985
|
+
// An int-slot live-in wasn't an exact in-range integer (#514), OR an array index went
|
|
1986
|
+
// out of bounds mid-region (#203). Either way the region bailed WITHOUT committing — `buf`
|
|
1987
|
+
// and the array scratch are discarded here, the caller's arrays are untouched — so re-
|
|
1988
|
+
// interpreting from the pristine pre-loop slots is correct (no partial result escapes).
|
|
1989
|
+
return OsrResult::LiveInMiss;
|
|
1990
|
+
}
|
|
1898
1991
|
for (p, &slot) in loopfn.used_slots.iter().enumerate() {
|
|
1899
1992
|
if let Some(d) = slots.get_mut(slot_base + slot as usize) {
|
|
1900
1993
|
*d = Value::Number(buf[p]);
|
|
1901
1994
|
}
|
|
1902
1995
|
}
|
|
1996
|
+
// #203: copy each WRITABLE array's (possibly-mutated) scratch back into its `Value::Array`.
|
|
1997
|
+
// Length is unchanged (`SetIndex` can't grow the array; an OOB write deopts above), so this is
|
|
1998
|
+
// a same-length element overwrite — mirroring `try_call_array_jit`'s writeback.
|
|
1999
|
+
for (k, &(slot, writable)) in loopfn.array_slots.iter().enumerate() {
|
|
2000
|
+
if !writable {
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
if let Some(Value::Array(a)) = slots.get(slot_base + slot as usize) {
|
|
2004
|
+
let mut b = a.borrow_mut();
|
|
2005
|
+
for (j, &v) in scratch[k].iter().enumerate() {
|
|
2006
|
+
if let Some(el) = b.get_mut(j) {
|
|
2007
|
+
*el = Value::Number(v);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1903
2012
|
match loopfn.exits.get(exit_id as usize) {
|
|
1904
2013
|
Some(&ip) => OsrResult::Compiled(ip),
|
|
1905
2014
|
// Out-of-range exit id is impossible (the region returns an in-range id), but if it ever
|
|
@@ -2265,6 +2374,13 @@ impl Vm {
|
|
|
2265
2374
|
std::collections::HashMap::new();
|
|
2266
2375
|
#[cfg(not(target_arch = "wasm32"))]
|
|
2267
2376
|
let mut osr_hot: (usize, u32) = (usize::MAX, 0);
|
|
2377
|
+
// #203: `trig header → back-edge count at last OSR attempt`, so an outermost (expanded) region
|
|
2378
|
+
// fires on the FIRST sound entry past the threshold and then re-attempts only every `OSR_RETRY`
|
|
2379
|
+
// back-edges (a `LiveInMiss` cooldown). Its own back-edge is too rare for the modulo cadence the
|
|
2380
|
+
// flag-off path uses. Unused when `TISH_JIT_OSR_ARRAY` is off.
|
|
2381
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2382
|
+
let mut osr_last_attempt: std::collections::HashMap<usize, u32> =
|
|
2383
|
+
std::collections::HashMap::new();
|
|
2268
2384
|
// Frame-local string builder for `acc += x` on a slot local (Opcode::AppendLocal): keeps the
|
|
2269
2385
|
// accumulator in a growable `sb_buf` so appends are amortized O(1) instead of reallocating the
|
|
2270
2386
|
// whole string each time. `sb_slot` is the slot currently buffered (at most one). The slot's
|
|
@@ -2455,6 +2571,23 @@ impl Vm {
|
|
|
2455
2571
|
.ok_or_else(|| format!("Bad MathUnary id: {}", id))?;
|
|
2456
2572
|
self.stack.push(Value::Number(mfn.apply(x)));
|
|
2457
2573
|
}
|
|
2574
|
+
Opcode::MathBinary => {
|
|
2575
|
+
// #203 — `Math.<fn>(a, b)` on two numbers. Same reasoning as MathUnary (emitted
|
|
2576
|
+
// only when `Math` is the global builtin); each non-number coerces to NaN. Args
|
|
2577
|
+
// were pushed a-then-b, so pop b first.
|
|
2578
|
+
let id = Self::read_u16(code, &mut ip);
|
|
2579
|
+
let b = match self.stack.pop() {
|
|
2580
|
+
Some(Value::Number(n)) => n,
|
|
2581
|
+
Some(_) | None => f64::NAN,
|
|
2582
|
+
};
|
|
2583
|
+
let a = match self.stack.pop() {
|
|
2584
|
+
Some(Value::Number(n)) => n,
|
|
2585
|
+
Some(_) | None => f64::NAN,
|
|
2586
|
+
};
|
|
2587
|
+
let bfn = tishlang_bytecode::MathBinaryFn::from_u16(id)
|
|
2588
|
+
.ok_or_else(|| format!("Bad MathBinary id: {}", id))?;
|
|
2589
|
+
self.stack.push(Value::Number(bfn.apply(a, b)));
|
|
2590
|
+
}
|
|
2458
2591
|
Opcode::LoadConst => {
|
|
2459
2592
|
let idx = Self::read_u16(code, &mut ip);
|
|
2460
2593
|
let c = constants
|
|
@@ -2967,8 +3100,10 @@ impl Vm {
|
|
|
2967
3100
|
if chunk.slot_based && Self::osr_enabled() {
|
|
2968
3101
|
let region_end = ip;
|
|
2969
3102
|
let header_ip = ip.saturating_sub(dist);
|
|
2970
|
-
//
|
|
2971
|
-
//
|
|
3103
|
+
// Count on the RAW header via the single-entry fast slot — byte-identical to the
|
|
3104
|
+
// pre-#203 baseline. ALL #203 expansion work is gated behind the threshold below,
|
|
3105
|
+
// so a loop that is cold *per enclosing call* (e.g. nbody's `advance`, entered
|
|
3106
|
+
// 200k× with ~5-iteration loops) never reaches the gate and pays ZERO extra cost.
|
|
2972
3107
|
if header_ip != osr_hot.0 {
|
|
2973
3108
|
if osr_hot.0 != usize::MAX {
|
|
2974
3109
|
osr_counters.insert(osr_hot.0, osr_hot.1);
|
|
@@ -2980,25 +3115,87 @@ impl Vm {
|
|
|
2980
3115
|
}
|
|
2981
3116
|
if osr_hot.1 != u32::MAX {
|
|
2982
3117
|
osr_hot.1 = osr_hot.1.saturating_add(1);
|
|
2983
|
-
if osr_hot.1 >= OSR_THRESHOLD
|
|
2984
|
-
|
|
3118
|
+
if osr_hot.1 >= OSR_THRESHOLD {
|
|
3119
|
+
if crate::jit::osr_array_enabled() {
|
|
3120
|
+
// #203: this loop is confirmed hot. Find the OUTERMOST enclosing
|
|
3121
|
+
// array-indexing loop so an array nest (matmul) marshals its arrays
|
|
3122
|
+
// ONCE and runs the whole nest natively, instead of re-marshalling
|
|
3123
|
+
// O(array) per inner entry (a wash).
|
|
3124
|
+
let (th, te, has_arr, worthy) = crate::jit::osr_expand_cached(
|
|
3125
|
+
chunk, header_ip, region_end,
|
|
3126
|
+
);
|
|
3127
|
+
if th != header_ip {
|
|
3128
|
+
// Expanded to an enclosing array loop: THIS inner loop is a
|
|
3129
|
+
// marshalling wash. Give up on it and mark the OUTER hot, so the
|
|
3130
|
+
// outer fires at its OWN (sound) back-edge on a later iteration —
|
|
3131
|
+
// restarting the outer from this inner point would re-run
|
|
3132
|
+
// already-completed iterations.
|
|
3133
|
+
osr_counters.insert(th, OSR_THRESHOLD);
|
|
3134
|
+
osr_hot.1 = u32::MAX;
|
|
3135
|
+
} else if has_arr && !worthy {
|
|
3136
|
+
// An array loop nested in a NON-array outer loop (k_nucleotide's
|
|
3137
|
+
// `seq[i+j]` inner loop): re-entered per outer iteration, so
|
|
3138
|
+
// array-OSR would re-marshal the whole array each time — a wash.
|
|
3139
|
+
// Leave it to the interpreter, exactly as the flag-off path does
|
|
3140
|
+
// (its `GetIndex` bails the numeric OSR there).
|
|
3141
|
+
osr_hot.1 = u32::MAX;
|
|
3142
|
+
} else {
|
|
3143
|
+
// This loop IS the trigger region. An array-WORTHY (outermost)
|
|
3144
|
+
// region fires on the FIRST attempt past the threshold, then
|
|
3145
|
+
// re-attempts only every OSR_RETRY back-edges (a LiveInMiss
|
|
3146
|
+
// cooldown); a pure-numeric region keeps the original modulo
|
|
3147
|
+
// sampling cadence.
|
|
3148
|
+
let due = if worthy {
|
|
3149
|
+
let last = osr_last_attempt
|
|
3150
|
+
.get(&header_ip)
|
|
3151
|
+
.copied()
|
|
3152
|
+
.unwrap_or(0);
|
|
3153
|
+
last == 0
|
|
3154
|
+
|| osr_hot.1.saturating_sub(last) >= OSR_RETRY
|
|
3155
|
+
} else {
|
|
3156
|
+
osr_hot
|
|
3157
|
+
.1
|
|
3158
|
+
.saturating_sub(OSR_THRESHOLD)
|
|
3159
|
+
.is_multiple_of(OSR_RETRY)
|
|
3160
|
+
};
|
|
3161
|
+
if due {
|
|
3162
|
+
if worthy {
|
|
3163
|
+
osr_last_attempt.insert(header_ip, osr_hot.1);
|
|
3164
|
+
}
|
|
3165
|
+
match Self::run_osr(
|
|
3166
|
+
chunk, header_ip, te, &mut slot_locals, 0,
|
|
3167
|
+
) {
|
|
3168
|
+
OsrResult::Compiled(exit_ip) => {
|
|
3169
|
+
ip = exit_ip;
|
|
3170
|
+
continue;
|
|
3171
|
+
}
|
|
3172
|
+
OsrResult::LiveInMiss => {}
|
|
3173
|
+
OsrResult::NotCompilable => {
|
|
3174
|
+
osr_hot.1 = u32::MAX
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
} else if osr_hot
|
|
2985
3180
|
.1
|
|
2986
3181
|
.saturating_sub(OSR_THRESHOLD)
|
|
2987
3182
|
.is_multiple_of(OSR_RETRY)
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3183
|
+
{
|
|
3184
|
+
// Flag OFF: exact pre-#203 threshold+retry-modulo behavior.
|
|
3185
|
+
match Self::run_osr(
|
|
3186
|
+
chunk,
|
|
3187
|
+
header_ip,
|
|
3188
|
+
region_end,
|
|
3189
|
+
&mut slot_locals,
|
|
3190
|
+
0,
|
|
3191
|
+
) {
|
|
3192
|
+
OsrResult::Compiled(exit_ip) => {
|
|
3193
|
+
ip = exit_ip;
|
|
3194
|
+
continue;
|
|
3195
|
+
}
|
|
3196
|
+
OsrResult::LiveInMiss => {}
|
|
3197
|
+
OsrResult::NotCompilable => osr_hot.1 = u32::MAX,
|
|
2999
3198
|
}
|
|
3000
|
-
OsrResult::LiveInMiss => {}
|
|
3001
|
-
OsrResult::NotCompilable => osr_hot.1 = u32::MAX,
|
|
3002
3199
|
}
|
|
3003
3200
|
}
|
|
3004
3201
|
}
|
|
@@ -3174,6 +3371,7 @@ impl Vm {
|
|
|
3174
3371
|
self.stack.push(Value::Object(VmRef::new(ObjectData {
|
|
3175
3372
|
strings: map,
|
|
3176
3373
|
symbols: None,
|
|
3374
|
+
frozen: false,
|
|
3177
3375
|
})));
|
|
3178
3376
|
}
|
|
3179
3377
|
Opcode::EnterTry => {
|
|
@@ -3316,12 +3514,13 @@ impl Vm {
|
|
|
3316
3514
|
.unwrap_or(Value::Null);
|
|
3317
3515
|
let result = match &arr {
|
|
3318
3516
|
Value::NumberArray(a) => {
|
|
3319
|
-
//
|
|
3320
|
-
|
|
3321
|
-
let mapped: Vec<Value> =
|
|
3322
|
-
.
|
|
3323
|
-
.
|
|
3324
|
-
|
|
3517
|
+
// Fast path over the numeric-origin array (packed or deopted): map each
|
|
3518
|
+
// element through the const binop.
|
|
3519
|
+
let mapped: Vec<Value> = a
|
|
3520
|
+
.borrow()
|
|
3521
|
+
.to_values()
|
|
3522
|
+
.into_iter()
|
|
3523
|
+
.map(|elem| {
|
|
3325
3524
|
let (l, r) = if param_left {
|
|
3326
3525
|
(elem, const_val.clone())
|
|
3327
3526
|
} else {
|
|
@@ -3388,21 +3587,31 @@ impl Vm {
|
|
|
3388
3587
|
.unwrap_or(Value::Null);
|
|
3389
3588
|
let result = match &arr {
|
|
3390
3589
|
Value::NumberArray(a) => {
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
.
|
|
3395
|
-
|
|
3590
|
+
// Numeric-origin filter (packed or deopted): keep elements whose const
|
|
3591
|
+
// binop is truthy, preserving each element's actual Value.
|
|
3592
|
+
let kept: Vec<Value> = a
|
|
3593
|
+
.borrow()
|
|
3594
|
+
.to_values()
|
|
3595
|
+
.into_iter()
|
|
3596
|
+
.filter(|elem| {
|
|
3396
3597
|
let (l, r) = if param_left {
|
|
3397
|
-
(elem, const_val.clone())
|
|
3598
|
+
(elem.clone(), const_val.clone())
|
|
3398
3599
|
} else {
|
|
3399
|
-
(const_val.clone(), elem)
|
|
3600
|
+
(const_val.clone(), elem.clone())
|
|
3400
3601
|
};
|
|
3401
3602
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
3402
3603
|
})
|
|
3403
|
-
.copied()
|
|
3404
3604
|
.collect();
|
|
3405
|
-
|
|
3605
|
+
// Stay packed when every kept element is a plain number.
|
|
3606
|
+
if let Some(nums) = kept
|
|
3607
|
+
.iter()
|
|
3608
|
+
.map(|v| v.as_number())
|
|
3609
|
+
.collect::<Option<Vec<f64>>>()
|
|
3610
|
+
{
|
|
3611
|
+
Value::number_array(nums)
|
|
3612
|
+
} else {
|
|
3613
|
+
Value::Array(VmRef::new(kept))
|
|
3614
|
+
}
|
|
3406
3615
|
}
|
|
3407
3616
|
Value::Array(a) => {
|
|
3408
3617
|
let arr_borrow = a.borrow();
|
|
@@ -3718,6 +3927,13 @@ fn ic_set_member(
|
|
|
3718
3927
|
use std::sync::atomic::Ordering::Relaxed;
|
|
3719
3928
|
if let Value::Object(od) = obj {
|
|
3720
3929
|
let mut b = od.borrow_mut();
|
|
3930
|
+
if b.frozen {
|
|
3931
|
+
// #437: writes to a frozen object throw a catchable TypeError (the SetMember handler
|
|
3932
|
+
// routes this Err through `catchable!` → a TypeError).
|
|
3933
|
+
return Err(format!(
|
|
3934
|
+
"Cannot assign to read only property '{key}' of a frozen object"
|
|
3935
|
+
));
|
|
3936
|
+
}
|
|
3721
3937
|
let shape = b.strings.shape();
|
|
3722
3938
|
let cell = chunk.inline_caches.0.get(name_idx as usize);
|
|
3723
3939
|
if shape != tishlang_core::DICT_SHAPE {
|
|
@@ -3769,11 +3985,7 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3769
3985
|
let key_s = key.as_ref();
|
|
3770
3986
|
// Numeric index fast path.
|
|
3771
3987
|
if let Ok(idx) = key_s.parse::<usize>() {
|
|
3772
|
-
return Ok(a
|
|
3773
|
-
.borrow()
|
|
3774
|
-
.get(idx)
|
|
3775
|
-
.map(|&n| Value::Number(n))
|
|
3776
|
-
.unwrap_or(Value::Null));
|
|
3988
|
+
return Ok(a.borrow().get(idx).unwrap_or(Value::Null));
|
|
3777
3989
|
}
|
|
3778
3990
|
if key_s == "length" {
|
|
3779
3991
|
return Ok(Value::Number(a.borrow().len() as f64));
|
|
@@ -3782,135 +3994,177 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3782
3994
|
let a_clone = a.clone();
|
|
3783
3995
|
let method: ArrayMethodFn = match key_s {
|
|
3784
3996
|
"push" => make_native_fn(move |args: &[Value]| {
|
|
3785
|
-
let mut
|
|
3997
|
+
let mut b = a_clone.borrow_mut();
|
|
3786
3998
|
for v in args {
|
|
3787
|
-
match v {
|
|
3788
|
-
Value::Number(n) =>
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
}
|
|
3999
|
+
match (b.as_packed_mut(), v) {
|
|
4000
|
+
(Some(nums), Value::Number(n)) => nums.push(*n),
|
|
4001
|
+
// First non-numeric (or already deopted) → upgrade in place, push Value.
|
|
4002
|
+
_ => b.deopt().push(v.clone()),
|
|
3792
4003
|
}
|
|
3793
4004
|
}
|
|
3794
|
-
Value::Number(
|
|
4005
|
+
Value::Number(b.len() as f64)
|
|
3795
4006
|
}),
|
|
3796
4007
|
"pop" => make_native_fn(move |_: &[Value]| {
|
|
3797
|
-
a_clone
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
if n.is_nan() {
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
}
|
|
3806
|
-
})
|
|
3807
|
-
.unwrap_or(Value::Null)
|
|
4008
|
+
let mut b = a_clone.borrow_mut();
|
|
4009
|
+
match b.as_packed_mut() {
|
|
4010
|
+
Some(nums) => nums
|
|
4011
|
+
.pop()
|
|
4012
|
+
.map(|n| if n.is_nan() { Value::Null } else { Value::Number(n) })
|
|
4013
|
+
.unwrap_or(Value::Null),
|
|
4014
|
+
None => b.deopt().pop().unwrap_or(Value::Null),
|
|
4015
|
+
}
|
|
3808
4016
|
}),
|
|
3809
4017
|
"shift" => make_native_fn(move |_: &[Value]| {
|
|
3810
|
-
let mut
|
|
3811
|
-
if
|
|
3812
|
-
Value::Null
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
Value::Number(n)
|
|
4018
|
+
let mut b = a_clone.borrow_mut();
|
|
4019
|
+
if b.is_empty() {
|
|
4020
|
+
return Value::Null;
|
|
4021
|
+
}
|
|
4022
|
+
match b.as_packed_mut() {
|
|
4023
|
+
Some(nums) => {
|
|
4024
|
+
let n = nums.remove(0);
|
|
4025
|
+
if n.is_nan() { Value::Null } else { Value::Number(n) }
|
|
3819
4026
|
}
|
|
4027
|
+
None => b.deopt().remove(0),
|
|
3820
4028
|
}
|
|
3821
4029
|
}),
|
|
3822
4030
|
"unshift" => make_native_fn(move |args: &[Value]| {
|
|
3823
|
-
let mut
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4031
|
+
let mut b = a_clone.borrow_mut();
|
|
4032
|
+
let all_num = args.iter().all(|v| matches!(v, Value::Number(_)));
|
|
4033
|
+
if all_num && b.as_packed().is_some() {
|
|
4034
|
+
let nums = b.as_packed_mut().unwrap();
|
|
4035
|
+
for (i, v) in args.iter().enumerate() {
|
|
4036
|
+
if let Value::Number(n) = v {
|
|
4037
|
+
nums.insert(i, *n);
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
} else {
|
|
4041
|
+
let boxed = b.deopt();
|
|
4042
|
+
for (i, v) in args.iter().enumerate() {
|
|
4043
|
+
boxed.insert(i, v.clone());
|
|
4044
|
+
}
|
|
3830
4045
|
}
|
|
3831
|
-
Value::Number(
|
|
4046
|
+
Value::Number(b.len() as f64)
|
|
3832
4047
|
}),
|
|
4048
|
+
// #502: `fill` mutates IN PLACE, but the packed block routed it through the
|
|
4049
|
+
// materialize-to-boxed fallback below — which fills a THROWAWAY copy, so the
|
|
4050
|
+
// original array was untouched (a silent no-op). A numeric fill value writes
|
|
4051
|
+
// straight into the Vec<f64>; a non-numeric value can't live in a packed array
|
|
4052
|
+
// (the documented rep limit, #199) so it deopts: materialize, fill the boxed copy,
|
|
4053
|
+
// and write the numeric elements back (non-numeric slots become NaN holes — the
|
|
4054
|
+
// same lossy edge splice/set-index already have on packed).
|
|
4055
|
+
"fill" => {
|
|
4056
|
+
let a2 = a_clone.clone();
|
|
4057
|
+
make_native_fn(move |args: &[Value]| {
|
|
4058
|
+
let len = a2.borrow().len() as i64;
|
|
4059
|
+
let norm = |v: Option<&Value>, dflt: i64| -> i64 {
|
|
4060
|
+
match v {
|
|
4061
|
+
Some(Value::Number(n)) => {
|
|
4062
|
+
let i = *n as i64;
|
|
4063
|
+
if i < 0 { (len + i).max(0) } else { i.min(len) }
|
|
4064
|
+
}
|
|
4065
|
+
_ => dflt,
|
|
4066
|
+
}
|
|
4067
|
+
};
|
|
4068
|
+
let start = norm(args.get(1), 0);
|
|
4069
|
+
let end = norm(args.get(2), len);
|
|
4070
|
+
let fill_v = args.first().cloned().unwrap_or(Value::Null);
|
|
4071
|
+
let mut b = a2.borrow_mut();
|
|
4072
|
+
match (b.as_packed_mut(), &fill_v) {
|
|
4073
|
+
// Numeric fill stays packed — write straight into the Vec<f64>.
|
|
4074
|
+
(Some(nums), Value::Number(fv)) => {
|
|
4075
|
+
let mut i = start;
|
|
4076
|
+
while i < end && (i as usize) < nums.len() {
|
|
4077
|
+
nums[i as usize] = *fv;
|
|
4078
|
+
i += 1;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
// Non-numeric fill (or already deopted): upgrade in place and fill the
|
|
4082
|
+
// boxed Vec with the actual Value — no NaN-hole loss (#199 sound).
|
|
4083
|
+
_ => {
|
|
4084
|
+
let boxed = b.deopt();
|
|
4085
|
+
let mut i = start;
|
|
4086
|
+
while i < end && (i as usize) < boxed.len() {
|
|
4087
|
+
boxed[i as usize] = fill_v.clone();
|
|
4088
|
+
i += 1;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
drop(b);
|
|
4093
|
+
Value::NumberArray(a2.clone())
|
|
4094
|
+
})
|
|
4095
|
+
}
|
|
3833
4096
|
"reverse" => make_native_fn(move |_: &[Value]| {
|
|
3834
|
-
a_clone.borrow_mut()
|
|
4097
|
+
let mut b = a_clone.borrow_mut();
|
|
4098
|
+
match b.as_packed_mut() {
|
|
4099
|
+
Some(nums) => nums.reverse(),
|
|
4100
|
+
None => b.deopt().reverse(),
|
|
4101
|
+
}
|
|
4102
|
+
drop(b);
|
|
3835
4103
|
Value::NumberArray(a_clone.clone())
|
|
3836
4104
|
}),
|
|
3837
4105
|
"splice" => {
|
|
3838
4106
|
let a2 = a_clone.clone();
|
|
3839
4107
|
make_native_fn(move |args: &[Value]| {
|
|
3840
|
-
|
|
3841
|
-
let
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
// elements back to the original Vec<f64>. This preserves the VmRef
|
|
3849
|
-
// identity for subsequent accesses. The array may have non-numeric
|
|
3850
|
-
// elements after this splice — they become NaN holes in the VmRef.
|
|
3851
|
-
let boxed = Value::materialize_number_array(&a2);
|
|
3852
|
-
let result = arr_builtins::splice(
|
|
3853
|
-
&boxed,
|
|
3854
|
-
args.first().unwrap_or(&Value::Null),
|
|
3855
|
-
args.get(1),
|
|
3856
|
-
args.get(2..).unwrap_or(&[]),
|
|
3857
|
-
);
|
|
3858
|
-
// Sync the modified boxed Vec back into the original VmRef.
|
|
3859
|
-
if let Value::Array(boxed_vmref) = &boxed {
|
|
3860
|
-
let mut packed = a2.borrow_mut();
|
|
3861
|
-
*packed = boxed_vmref
|
|
3862
|
-
.borrow()
|
|
3863
|
-
.iter()
|
|
3864
|
-
.map(|v| match v {
|
|
3865
|
-
Value::Number(n) => *n,
|
|
3866
|
-
_ => f64::NAN,
|
|
3867
|
-
})
|
|
3868
|
-
.collect();
|
|
4108
|
+
let inserts = args.get(2..).unwrap_or(&[]);
|
|
4109
|
+
let all_num = inserts.iter().all(|v| matches!(v, Value::Number(_)));
|
|
4110
|
+
let mut b = a2.borrow_mut();
|
|
4111
|
+
let len = b.len() as i64;
|
|
4112
|
+
let start = match args.first() {
|
|
4113
|
+
Some(Value::Number(n)) => {
|
|
4114
|
+
let sn = *n as i64;
|
|
4115
|
+
if sn < 0 { (len + sn).max(0) as usize } else { (sn as usize).min(b.len()) }
|
|
3869
4116
|
}
|
|
3870
|
-
|
|
3871
|
-
}
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
_ => 0,
|
|
3884
|
-
};
|
|
3885
|
-
let del = match args.get(1) {
|
|
3886
|
-
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
3887
|
-
_ => arr.len().saturating_sub(start),
|
|
3888
|
-
};
|
|
3889
|
-
let del = del.min(arr.len().saturating_sub(start));
|
|
3890
|
-
let new_nums: Vec<f64> = args
|
|
3891
|
-
.get(2..)
|
|
3892
|
-
.unwrap_or(&[])
|
|
4117
|
+
_ => 0,
|
|
4118
|
+
};
|
|
4119
|
+
let del = match args.get(1) {
|
|
4120
|
+
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
4121
|
+
_ => b.len().saturating_sub(start),
|
|
4122
|
+
};
|
|
4123
|
+
let del = del.min(b.len().saturating_sub(start));
|
|
4124
|
+
if all_num && b.as_packed().is_some() {
|
|
4125
|
+
// Stay packed: splice the Vec<f64>, return the removed f64s.
|
|
4126
|
+
let nums = b.as_packed_mut().unwrap();
|
|
4127
|
+
let new_nums: Vec<f64> = inserts
|
|
3893
4128
|
.iter()
|
|
3894
|
-
.map(|v|
|
|
3895
|
-
Value::Number(n) => *n,
|
|
3896
|
-
_ => f64::NAN,
|
|
3897
|
-
})
|
|
4129
|
+
.map(|v| if let Value::Number(n) = v { *n } else { f64::NAN })
|
|
3898
4130
|
.collect();
|
|
3899
|
-
let removed: Vec<f64> =
|
|
3900
|
-
arr.splice(start..start + del, new_nums).collect();
|
|
4131
|
+
let removed: Vec<f64> = nums.splice(start..start + del, new_nums).collect();
|
|
3901
4132
|
Value::number_array(removed)
|
|
4133
|
+
} else {
|
|
4134
|
+
// #506: non-numeric inserts (or already deopted) — upgrade in place and
|
|
4135
|
+
// splice the boxed Vec. The array genuinely holds the inserted values and
|
|
4136
|
+
// the removed elements are correct (no NaN loss).
|
|
4137
|
+
let boxed = b.deopt();
|
|
4138
|
+
let removed: Vec<Value> =
|
|
4139
|
+
boxed.splice(start..start + del, inserts.iter().cloned()).collect();
|
|
4140
|
+
Value::Array(VmRef::new(removed))
|
|
3902
4141
|
}
|
|
3903
4142
|
})
|
|
3904
4143
|
}
|
|
3905
4144
|
"sort" => make_native_fn(move |args: &[Value]| {
|
|
3906
|
-
let arr_val = Value::NumberArray(a_clone.clone());
|
|
3907
4145
|
let cmp = args.first();
|
|
3908
4146
|
if let Some(Value::Function(_)) = cmp {
|
|
3909
4147
|
// Comparator sort: materialise first (comparator may return non-numeric).
|
|
3910
4148
|
let boxed = Value::materialize_number_array(&a_clone);
|
|
3911
4149
|
arr_builtins::sort_with_comparator(&boxed, cmp.unwrap())
|
|
3912
4150
|
} else {
|
|
3913
|
-
|
|
4151
|
+
// JS default `sort()` is LEXICOGRAPHIC — String(element), NOT numeric — so
|
|
4152
|
+
// `[10, 9, 100].sort()` is `[10, 100, 9]`. The boxed path already does this
|
|
4153
|
+
// (arr_builtins::sort_default); match it on the packed vec by comparing each
|
|
4154
|
+
// number's display string. (Pre-existing packed bug: it sorted numerically.)
|
|
4155
|
+
let mut b = a_clone.borrow_mut();
|
|
4156
|
+
match b.as_packed_mut() {
|
|
4157
|
+
Some(nums) => nums.sort_by(|x, y| {
|
|
4158
|
+
Value::Number(*x)
|
|
4159
|
+
.to_display_string()
|
|
4160
|
+
.cmp(&Value::Number(*y).to_display_string())
|
|
4161
|
+
}),
|
|
4162
|
+
None => b.deopt().sort_by(|x, y| {
|
|
4163
|
+
x.to_display_string().cmp(&y.to_display_string())
|
|
4164
|
+
}),
|
|
4165
|
+
}
|
|
4166
|
+
drop(b);
|
|
4167
|
+
Value::NumberArray(a_clone.clone())
|
|
3914
4168
|
}
|
|
3915
4169
|
}),
|
|
3916
4170
|
_ => {
|
|
@@ -4506,6 +4760,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
4506
4760
|
fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
|
|
4507
4761
|
match obj {
|
|
4508
4762
|
Value::Object(m) => {
|
|
4763
|
+
if m.borrow().frozen {
|
|
4764
|
+
return Err(format!(
|
|
4765
|
+
"Cannot assign to read only property '{key}' of a frozen object"
|
|
4766
|
+
));
|
|
4767
|
+
}
|
|
4509
4768
|
m.borrow_mut().strings.insert(Arc::clone(key), val);
|
|
4510
4769
|
Ok(())
|
|
4511
4770
|
}
|
|
@@ -4531,7 +4790,11 @@ fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
|
|
|
4531
4790
|
if key.as_ref() == "length" {
|
|
4532
4791
|
let new_len = array_length_arg(&val)?;
|
|
4533
4792
|
// NaN is the packed-array hole marker (read back as Null), matching get_index.
|
|
4534
|
-
a.borrow_mut()
|
|
4793
|
+
let mut b = a.borrow_mut();
|
|
4794
|
+
match b.as_packed_mut() {
|
|
4795
|
+
Some(nums) => nums.resize(new_len, f64::NAN),
|
|
4796
|
+
None => b.deopt().resize(new_len, Value::Null),
|
|
4797
|
+
}
|
|
4535
4798
|
return Ok(());
|
|
4536
4799
|
}
|
|
4537
4800
|
Err(format!("Cannot set property of {}", obj.type_name()))
|
|
@@ -4566,12 +4829,9 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
4566
4829
|
// NaN is used as the hole marker (sparse-array positions); reads return Null.
|
|
4567
4830
|
Ok(a.borrow()
|
|
4568
4831
|
.get(i)
|
|
4569
|
-
.map(
|
|
4570
|
-
if n.is_nan()
|
|
4571
|
-
|
|
4572
|
-
} else {
|
|
4573
|
-
Value::Number(n)
|
|
4574
|
-
}
|
|
4832
|
+
.map(|v| match v {
|
|
4833
|
+
Value::Number(n) if n.is_nan() => Value::Null,
|
|
4834
|
+
other => other,
|
|
4575
4835
|
})
|
|
4576
4836
|
.unwrap_or(Value::Null))
|
|
4577
4837
|
}
|
|
@@ -4661,6 +4921,23 @@ fn delete_index(obj: &Value, key: &Value) {
|
|
|
4661
4921
|
}
|
|
4662
4922
|
}
|
|
4663
4923
|
}
|
|
4924
|
+
// #504: `delete arr[i]` on a packed array — the Vec<f64> can't hold Null, but NaN is the
|
|
4925
|
+
// rep's own hole marker (GetIndex reads a NaN element back as Null, matching a boxed hole),
|
|
4926
|
+
// so a delete writes NaN. Length is unchanged, exactly like the boxed arm's `= Null`.
|
|
4927
|
+
Value::NumberArray(a) => {
|
|
4928
|
+
if let Value::Number(n) = key {
|
|
4929
|
+
let n = *n;
|
|
4930
|
+
if n >= 0.0 && n.fract() == 0.0 {
|
|
4931
|
+
let i = n as usize;
|
|
4932
|
+
let mut b = a.borrow_mut();
|
|
4933
|
+
if i < b.len() {
|
|
4934
|
+
// Deopt so the hole is a real Null (matches the boxed arm exactly, and keeps
|
|
4935
|
+
// a genuinely-stored NaN element distinct from a deleted slot).
|
|
4936
|
+
b.deopt()[i] = Value::Null;
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4664
4941
|
_ => {}
|
|
4665
4942
|
}
|
|
4666
4943
|
}
|
|
@@ -4688,27 +4965,23 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
4688
4965
|
// a sentinel error — the caller (SetIndex opcode) does NOT handle deopt.
|
|
4689
4966
|
// Instead we only do in-bounds-or-next-element numeric assignments here;
|
|
4690
4967
|
// anything that creates holes (i > len) or sets a non-number is unsupported.
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
while
|
|
4696
|
-
|
|
4968
|
+
let mut b = a.borrow_mut();
|
|
4969
|
+
match (b.as_packed_mut(), &val) {
|
|
4970
|
+
// Numeric assignment stays packed — NaN-extend to a sparse hole if past the end.
|
|
4971
|
+
(Some(nums), Value::Number(n)) => {
|
|
4972
|
+
while nums.len() <= i {
|
|
4973
|
+
nums.push(f64::NAN);
|
|
4697
4974
|
}
|
|
4698
|
-
|
|
4975
|
+
nums[i] = *n;
|
|
4699
4976
|
}
|
|
4700
|
-
// Non-numeric
|
|
4701
|
-
//
|
|
4702
|
-
// known limitation of NumberArray; the uncommon mixed-type path should not produce
|
|
4703
|
-
// a NumberArray in the first place. The caller will see the correct index reads for
|
|
4704
|
-
// numeric elements and Null for the NaN holes.
|
|
4977
|
+
// Non-numeric (or already deopted): upgrade in place and store the real Value —
|
|
4978
|
+
// the array genuinely becomes heterogeneous, no data loss (#199 sound).
|
|
4705
4979
|
_ => {
|
|
4706
|
-
let
|
|
4707
|
-
while
|
|
4708
|
-
|
|
4980
|
+
let boxed = b.deopt();
|
|
4981
|
+
while boxed.len() <= i {
|
|
4982
|
+
boxed.push(Value::Null);
|
|
4709
4983
|
}
|
|
4710
|
-
|
|
4711
|
-
// for the experimental TISH_PACKED_ARRAYS path.
|
|
4984
|
+
boxed[i] = val;
|
|
4712
4985
|
}
|
|
4713
4986
|
}
|
|
4714
4987
|
Ok(())
|
|
@@ -4735,7 +5008,15 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
4735
5008
|
arr[i] = val;
|
|
4736
5009
|
Ok(())
|
|
4737
5010
|
}
|
|
4738
|
-
Value::Object(
|
|
5011
|
+
Value::Object(m) => {
|
|
5012
|
+
if m.borrow().frozen {
|
|
5013
|
+
return Err(format!(
|
|
5014
|
+
"Cannot assign to read only property '{}' of a frozen object",
|
|
5015
|
+
idx.to_display_string()
|
|
5016
|
+
));
|
|
5017
|
+
}
|
|
5018
|
+
object_set(obj, idx, val)
|
|
5019
|
+
}
|
|
4739
5020
|
_ => Err(format!("Cannot set property of {}", obj.type_name())),
|
|
4740
5021
|
}
|
|
4741
5022
|
}
|
|
@@ -4837,3 +5118,84 @@ mod recursion_limit_tests_381 {
|
|
|
4837
5118
|
);
|
|
4838
5119
|
}
|
|
4839
5120
|
}
|
|
5121
|
+
|
|
5122
|
+
#[cfg(test)]
|
|
5123
|
+
mod undeclared_var_in_fn_tests {
|
|
5124
|
+
use tishlang_core::Value;
|
|
5125
|
+
|
|
5126
|
+
fn run_src(src: &str) -> Result<Value, String> {
|
|
5127
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
5128
|
+
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
5129
|
+
super::run(&chunk)
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
// A free/undeclared identifier read INSIDE a called function must surface as an error, not be
|
|
5133
|
+
// swallowed to null (regression: closure calls used to `run_chunk(..).unwrap_or(Value::Null)`,
|
|
5134
|
+
// so any error in a callee body vanished — the VM returned null where the interpreter throws).
|
|
5135
|
+
#[test]
|
|
5136
|
+
fn undeclared_read_in_fn_errors_not_null() {
|
|
5137
|
+
let out = run_src("fn f() { return zzznotdeclared }\nf()");
|
|
5138
|
+
assert!(
|
|
5139
|
+
out.is_err(),
|
|
5140
|
+
"undeclared read in a function body must error, got: {out:?}"
|
|
5141
|
+
);
|
|
5142
|
+
assert!(
|
|
5143
|
+
out.as_ref().unwrap_err().contains("zzznotdeclared"),
|
|
5144
|
+
"error must name the missing variable: {out:?}"
|
|
5145
|
+
);
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// `undefined` is just a specific undeclared identifier — same rejection, since tish has no
|
|
5149
|
+
// `undefined` value.
|
|
5150
|
+
#[test]
|
|
5151
|
+
fn undefined_ident_in_fn_errors_not_null() {
|
|
5152
|
+
let out = run_src("fn f() { return undefined }\nf()");
|
|
5153
|
+
assert!(
|
|
5154
|
+
out.is_err() && out.as_ref().unwrap_err().contains("undefined"),
|
|
5155
|
+
"free `undefined` in a function body must error: {out:?}"
|
|
5156
|
+
);
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
// The propagated error is catchable, matching the interpreter.
|
|
5160
|
+
// Object.freeze marks an object frozen and a subsequent property write throws a catchable
|
|
5161
|
+
// TypeError (#437 — tish's own stricter spec; node's sloppy mode silently ignores it).
|
|
5162
|
+
#[test]
|
|
5163
|
+
fn frozen_write_throws_catchable_typeerror() {
|
|
5164
|
+
// Uncaught: the frozen write propagates as an error.
|
|
5165
|
+
let uncaught = run_src("let o = { a: 1 }\nObject.freeze(o)\no.a = 2");
|
|
5166
|
+
assert!(
|
|
5167
|
+
uncaught.is_err(),
|
|
5168
|
+
"a write to a frozen object must throw, not silently succeed: {uncaught:?}"
|
|
5169
|
+
);
|
|
5170
|
+
// Caught: try/catch recovers it, and the value is unchanged.
|
|
5171
|
+
let caught = run_src(
|
|
5172
|
+
"let o = { a: 1 }\n\
|
|
5173
|
+
Object.freeze(o)\n\
|
|
5174
|
+
try { o.a = 2 } catch (e) { }\n\
|
|
5175
|
+
o.a",
|
|
5176
|
+
);
|
|
5177
|
+
assert!(
|
|
5178
|
+
caught.is_ok(),
|
|
5179
|
+
"the frozen write must be catchable (try/catch recovers): {caught:?}"
|
|
5180
|
+
);
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
// The propagated error is catchable, matching the interpreter (issue #437 bucket-A).
|
|
5184
|
+
#[test]
|
|
5185
|
+
fn undeclared_read_in_fn_is_catchable() {
|
|
5186
|
+
// If the callee error weren't catchable it would propagate as an Err from run_src; a caught
|
|
5187
|
+
// throw lets the program complete normally (Ok). An uncaught baseline (below) confirms the
|
|
5188
|
+
// try/catch is what recovers it, not that the error silently vanished.
|
|
5189
|
+
let caught = run_src(
|
|
5190
|
+
"fn f() { return zzz }\n\
|
|
5191
|
+
try { f() } catch (e) { }\n\
|
|
5192
|
+
1",
|
|
5193
|
+
);
|
|
5194
|
+
assert!(caught.is_ok(), "callee error must be catchable: {caught:?}");
|
|
5195
|
+
let uncaught = run_src("fn f() { return zzz }\nf()");
|
|
5196
|
+
assert!(
|
|
5197
|
+
uncaught.is_err(),
|
|
5198
|
+
"without a catch the same error must propagate, not vanish: {uncaught:?}"
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
}
|