@tishlang/tish-lsp 2.39.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 +806 -46
- 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/lib.rs +54 -9
- 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
|
@@ -23,9 +23,13 @@
|
|
|
23
23
|
//! - Keys/values use **SameValueZero** equality (NaN equals NaN; `+0`/`-0` are the same; objects by
|
|
24
24
|
//! identity).
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
use
|
|
26
|
+
#[cfg(feature = "portable")]
|
|
27
|
+
#[allow(unused_imports)]
|
|
28
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
29
|
+
|
|
30
|
+
use core::any::Any;
|
|
31
|
+
use core::hash::{Hash, Hasher};
|
|
32
|
+
use tishlang_core::Arc;
|
|
29
33
|
|
|
30
34
|
use indexmap::IndexMap;
|
|
31
35
|
use tishlang_core::{NativeFn, ObjectMap, TishOpaque, Value, VmRef};
|
|
@@ -120,7 +124,7 @@ impl indexmap::Equivalent<Key> for KeyRef<'_> {
|
|
|
120
124
|
/// stores `Value::Null` values and iterates its keys; a `Map` stores the mapped value. Uses `ahash`
|
|
121
125
|
/// (the same fast hasher `tish_core`'s `PropMap` uses) rather than the default SipHash — iteration
|
|
122
126
|
/// order is insertion order regardless of the hasher, so this is a pure constant-factor speedup.
|
|
123
|
-
type Store = VmRef<IndexMap<Key, Value,
|
|
127
|
+
type Store = VmRef<IndexMap<Key, Value, tishlang_core::RandomState>>;
|
|
124
128
|
|
|
125
129
|
/// Opaque whose length reports a collection's live element count. Stored under [`SIZE_SLOT`] on every
|
|
126
130
|
/// `Set`/`Map` instance.
|
|
@@ -226,7 +230,7 @@ fn same_value_zero(a: &Value, b: &Value) -> bool {
|
|
|
226
230
|
fn iter_elements(v: &Value) -> Vec<Value> {
|
|
227
231
|
match v {
|
|
228
232
|
Value::Array(a) => a.borrow().iter().cloned().collect(),
|
|
229
|
-
Value::NumberArray(a) => a.borrow().
|
|
233
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
230
234
|
_ => Vec::new(),
|
|
231
235
|
}
|
|
232
236
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
//! `new` lowering for non-JS targets: `construct(callee, args)` approximates JS `[[Construct]]`.
|
|
2
2
|
//! Browser-exact behavior remains on `tish build --target js`.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
#[cfg(feature = "portable")]
|
|
5
|
+
#[allow(unused_imports)]
|
|
6
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
7
|
+
#[cfg(feature = "portable")]
|
|
8
|
+
use tishlang_core::FloatExt;
|
|
9
|
+
|
|
10
|
+
use tishlang_core::Arc;
|
|
5
11
|
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
6
12
|
|
|
7
13
|
const CONSTRUCT: &str = "__construct";
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
//! is no timezone database in the core builtins (keeps them lean + wasm-friendly). `getTime`,
|
|
12
12
|
//! `valueOf`, the `getUTC*` family and `toISOString` are therefore fully deterministic everywhere.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
#[cfg(feature = "portable")]
|
|
15
|
+
#[allow(unused_imports)]
|
|
16
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
17
|
+
#[cfg(feature = "portable")]
|
|
18
|
+
use tishlang_core::FloatExt;
|
|
19
|
+
|
|
20
|
+
use tishlang_core::Arc;
|
|
15
21
|
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
16
22
|
|
|
17
23
|
const CONSTRUCT: &str = "__construct";
|
|
@@ -23,6 +29,7 @@ const MONTHS: [&str; 12] = [
|
|
|
23
29
|
];
|
|
24
30
|
|
|
25
31
|
/// Current wall-clock time as epoch milliseconds.
|
|
32
|
+
#[cfg(not(feature = "portable"))]
|
|
26
33
|
fn now_ms() -> f64 {
|
|
27
34
|
std::time::SystemTime::now()
|
|
28
35
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -30,6 +37,13 @@ fn now_ms() -> f64 {
|
|
|
30
37
|
.unwrap_or(0.0)
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
/// No OS clock on GBA: read the runtime-installed clock hook (defaults to 0 until
|
|
41
|
+
/// the facade wires up an agb timer). See `tishlang_core::install_clock`.
|
|
42
|
+
#[cfg(feature = "portable")]
|
|
43
|
+
fn now_ms() -> f64 {
|
|
44
|
+
tishlang_core::now_ms()
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/// A `(method name, field extractor)` pair for the numeric `Date` getters. The explicit
|
|
34
48
|
/// `fn(&Civil) -> i64` coerces the (non-capturing) extractor closures to a single fn-pointer type so
|
|
35
49
|
/// they can live in one array.
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
//! Used by both tishlang_vm (bytecode) and tishlang_runtime (compiled). Keeps tishlang_vm
|
|
4
4
|
//! independent of tishlang_runtime.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
#[cfg(feature = "portable")]
|
|
7
|
+
#[allow(unused_imports)]
|
|
8
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
9
|
+
#[cfg(feature = "portable")]
|
|
10
|
+
use tishlang_core::FloatExt;
|
|
11
|
+
|
|
12
|
+
use tishlang_core::Arc;
|
|
7
13
|
use tishlang_core::VmRef;
|
|
8
14
|
use tishlang_core::{percent_decode, percent_decode_component, percent_encode, percent_encode_component, ObjectMap, Value};
|
|
9
15
|
|
|
@@ -79,7 +85,7 @@ pub fn array_from(args: &[Value]) -> Value {
|
|
|
79
85
|
let source = args.first().cloned().unwrap_or(Value::Null);
|
|
80
86
|
let items: Vec<Value> = match &source {
|
|
81
87
|
Value::Array(a) => a.borrow().clone(),
|
|
82
|
-
Value::NumberArray(a) => a.borrow().
|
|
88
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
83
89
|
// `drain_iterator` covers String (chars), Set/Map, and any iterator object.
|
|
84
90
|
other => {
|
|
85
91
|
if let Some(d) = tishlang_core::drain_iterator(other) {
|
|
@@ -296,6 +302,25 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
|
|
|
296
302
|
Value::String(s.into())
|
|
297
303
|
}
|
|
298
304
|
|
|
305
|
+
/// Object.freeze(obj) — mark the object frozen so later property writes throw a catchable
|
|
306
|
+
/// TypeError (#437). Returns the same object; non-objects pass through unchanged (JS semantics).
|
|
307
|
+
pub fn object_freeze(args: &[Value]) -> Value {
|
|
308
|
+
if let Some(Value::Object(obj)) = args.first() {
|
|
309
|
+
obj.borrow_mut().frozen = true;
|
|
310
|
+
}
|
|
311
|
+
args.first().cloned().unwrap_or(Value::Null)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Object.isFrozen(obj) — true if the object was frozen. Primitives are considered frozen (JS).
|
|
315
|
+
pub fn object_is_frozen(args: &[Value]) -> Value {
|
|
316
|
+
match args.first() {
|
|
317
|
+
Some(Value::Object(obj)) => Value::Bool(obj.borrow().frozen),
|
|
318
|
+
// Arrays are mutable and not freezable in this slice → not frozen; other primitives are.
|
|
319
|
+
Some(Value::Array(_)) => Value::Bool(false),
|
|
320
|
+
_ => Value::Bool(true),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
299
324
|
/// Object.keys(obj)
|
|
300
325
|
pub fn object_keys(args: &[Value]) -> Value {
|
|
301
326
|
match args.first() {
|
|
@@ -319,6 +344,16 @@ pub fn object_keys(args: &[Value]) -> Value {
|
|
|
319
344
|
.collect();
|
|
320
345
|
Value::Array(VmRef::new(keys))
|
|
321
346
|
}
|
|
347
|
+
// A packed `NumberArray` has the same own enumerable keys (its index strings) — without
|
|
348
|
+
// this, `for (k in arr)` (lowered to `for-of Object.keys`) enumerated NOTHING under
|
|
349
|
+
// TISH_PACKED_ARRAYS=1. Mirrors the Array arm.
|
|
350
|
+
Some(Value::NumberArray(arr)) => {
|
|
351
|
+
let len = arr.borrow().len();
|
|
352
|
+
let keys: Vec<Value> = (0..len)
|
|
353
|
+
.map(|i| Value::String(tishlang_core::ArcStr::from(i.to_string().as_str())))
|
|
354
|
+
.collect();
|
|
355
|
+
Value::Array(VmRef::new(keys))
|
|
356
|
+
}
|
|
322
357
|
_ => Value::Array(VmRef::new(Vec::new())),
|
|
323
358
|
}
|
|
324
359
|
}
|
|
@@ -495,7 +530,7 @@ pub fn js_parse_float(s: &str) -> f64 {
|
|
|
495
530
|
pub fn object_from_entries(args: &[Value]) -> Value {
|
|
496
531
|
if let Some(Value::Array(entries)) = args.first() {
|
|
497
532
|
let entries_borrow = entries.borrow();
|
|
498
|
-
let mut obj: ObjectMap = ObjectMap::
|
|
533
|
+
let mut obj: ObjectMap = ObjectMap::with_capacity_and_hasher(entries_borrow.len(), Default::default());
|
|
499
534
|
|
|
500
535
|
for entry in entries_borrow.iter() {
|
|
501
536
|
if let Value::Array(pair) = entry {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
//! Common helper functions used across builtin implementations.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
#[cfg(feature = "portable")]
|
|
4
|
+
#[allow(unused_imports)]
|
|
5
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
6
|
+
|
|
7
|
+
use tishlang_core::Arc;
|
|
4
8
|
use tishlang_core::{ObjectMap, Value};
|
|
5
9
|
|
|
6
10
|
/// Normalize an array index, handling negative indices.
|
|
@@ -20,8 +24,8 @@ pub fn normalize_index(idx: &Value, len: i64, default: usize) -> usize {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/// Create an error object with a single "error" field.
|
|
23
|
-
pub fn make_error_value(e: impl
|
|
24
|
-
let mut obj = ObjectMap::
|
|
27
|
+
pub fn make_error_value(e: impl core::fmt::Display) -> Value {
|
|
28
|
+
let mut obj = ObjectMap::with_capacity_and_hasher(1, Default::default());
|
|
25
29
|
obj.insert(Arc::from("error"), Value::String(e.to_string().into()));
|
|
26
30
|
Value::object(obj)
|
|
27
31
|
}
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
//! array-returning version behaved; mid-iteration mutation of the source is not reflected (a
|
|
13
13
|
//! live iterator is a follow-up).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
#[cfg(feature = "portable")]
|
|
16
|
+
#[allow(unused_imports)]
|
|
17
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
18
|
+
|
|
19
|
+
use tishlang_core::Arc;
|
|
16
20
|
|
|
17
21
|
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
18
22
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
//! Used by the compiled runtime (tishlang_runtime) and bytecode VM (tishlang_vm). The
|
|
4
4
|
//! interpreter (tishlang_eval) implements builtins inline due to different Value
|
|
5
5
|
//! and native signatures.
|
|
6
|
+
#![cfg_attr(feature = "portable", no_std)]
|
|
7
|
+
|
|
8
|
+
extern crate alloc;
|
|
6
9
|
|
|
7
10
|
pub mod array;
|
|
8
11
|
pub mod collections;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
//! Math builtin functions.
|
|
2
2
|
|
|
3
|
+
#[cfg(feature = "portable")]
|
|
4
|
+
#[allow(unused_imports)]
|
|
5
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
6
|
+
#[cfg(feature = "portable")]
|
|
7
|
+
use tishlang_core::FloatExt;
|
|
8
|
+
|
|
3
9
|
use crate::helpers::extract_num;
|
|
4
10
|
use tishlang_core::{to_int32, to_uint32, Value};
|
|
5
11
|
|
|
@@ -120,10 +126,18 @@ pub fn pow(args: &[Value]) -> Value {
|
|
|
120
126
|
Value::Number(base.powf(exp))
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
#[cfg(not(feature = "portable"))]
|
|
123
130
|
pub fn random(_args: &[Value]) -> Value {
|
|
124
131
|
Value::Number(rand::random::<f64>())
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
/// No OS entropy on GBA: read the runtime-installed RNG hook (a seedable xorshift
|
|
135
|
+
/// fallback until the facade wires up `agb::rng`). See `tishlang_core::install_rng`.
|
|
136
|
+
#[cfg(feature = "portable")]
|
|
137
|
+
pub fn random(_args: &[Value]) -> Value {
|
|
138
|
+
Value::Number(tishlang_core::random_f64())
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
pub fn sign(args: &[Value]) -> Value {
|
|
128
142
|
let n = extract_num(args.first()).unwrap_or(f64::NAN);
|
|
129
143
|
Value::Number(if n.is_nan() {
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
//! and the tree-walk interpreter all route through here so every backend produces
|
|
6
6
|
//! byte-identical output — see `tish/docs/full-backend-parity-plan.md` (Workstream A).
|
|
7
7
|
|
|
8
|
+
#[cfg(feature = "portable")]
|
|
9
|
+
#[allow(unused_imports)]
|
|
10
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
11
|
+
#[cfg(feature = "portable")]
|
|
12
|
+
use tishlang_core::FloatExt;
|
|
13
|
+
|
|
8
14
|
use tishlang_core::Value;
|
|
9
15
|
|
|
10
16
|
/// `Number.prototype.toFixed(digits)` — ECMA-262 §21.1.3.3.
|
|
@@ -25,8 +31,16 @@ pub fn to_fixed(n: &Value, digits: &Value) -> Value {
|
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/// f64-domain `Number.prototype.toFixed` so the tree-walk interpreter (distinct `Value`) shares the
|
|
28
|
-
/// exact behavior.
|
|
29
|
-
/// from zero
|
|
34
|
+
/// exact behavior. tish's rule (#438): round the number's *true* decimal value to `digits` places,
|
|
35
|
+
/// ties away from zero — matching V8/node's observable `toFixed`.
|
|
36
|
+
///
|
|
37
|
+
/// The obvious `(num * 10^digits).round()` is wrong: float scaling nudges values that are actually
|
|
38
|
+
/// just below a tie up over it, so `(2.675).toFixed(2)` came out `2.68` where node gives `2.67`
|
|
39
|
+
/// (2.675 is stored as 2.67499999…). Instead we render the double's exact decimal expansion to well
|
|
40
|
+
/// beyond `digits` — f64 fractions terminate, so a generous precision captures the real digits — then
|
|
41
|
+
/// round that digit string: if the first dropped digit is ≥ 5, increment with decimal carry; else
|
|
42
|
+
/// truncate. On the magnitude, "first dropped ≥ 5" is round-half-up = round-half-away-from-zero, and
|
|
43
|
+
/// because the expansion is exact, non-ties round the way the true value dictates. #438
|
|
30
44
|
pub fn to_fixed_str(num: f64, digits: usize) -> String {
|
|
31
45
|
if num.is_nan() {
|
|
32
46
|
return "NaN".to_string();
|
|
@@ -38,9 +52,61 @@ pub fn to_fixed_str(num: f64, digits: usize) -> String {
|
|
|
38
52
|
if num.abs() >= 1e21 {
|
|
39
53
|
return format!("{}", num);
|
|
40
54
|
}
|
|
41
|
-
|
|
42
|
-
let
|
|
43
|
-
|
|
55
|
+
|
|
56
|
+
let negative = num.is_sign_negative() && num != 0.0;
|
|
57
|
+
let magnitude = num.abs();
|
|
58
|
+
|
|
59
|
+
// Exact-enough decimal expansion. The digit just past `digits` then reflects the double's TRUE
|
|
60
|
+
// value; +25 guard digits sit far beyond any position we inspect, so the half-to-even rounding
|
|
61
|
+
// Rust applies at the cutoff can't perturb our decision (it would take 25 consecutive 9s to carry
|
|
62
|
+
// back that far, and such a value already rounds up under the exact rule anyway).
|
|
63
|
+
let guard = digits + 25;
|
|
64
|
+
let rendered = format!("{:.*}", guard, magnitude);
|
|
65
|
+
let dot = rendered.find('.').expect("fixed formatting always has a decimal point");
|
|
66
|
+
let int_str = &rendered[..dot];
|
|
67
|
+
let frac = rendered[dot + 1..].as_bytes();
|
|
68
|
+
|
|
69
|
+
// Kept digits: the whole integer part, then `digits` fractional digits.
|
|
70
|
+
let mut ds: Vec<u8> = int_str
|
|
71
|
+
.bytes()
|
|
72
|
+
.chain(frac[..digits].iter().copied())
|
|
73
|
+
.map(|b| b - b'0')
|
|
74
|
+
.collect();
|
|
75
|
+
|
|
76
|
+
// Round half-away-from-zero on the magnitude: increment iff the first dropped digit is >= 5.
|
|
77
|
+
if frac[digits] - b'0' >= 5 {
|
|
78
|
+
let mut i = ds.len();
|
|
79
|
+
loop {
|
|
80
|
+
if i == 0 {
|
|
81
|
+
ds.insert(0, 1);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
i -= 1;
|
|
85
|
+
if ds[i] == 9 {
|
|
86
|
+
ds[i] = 0;
|
|
87
|
+
} else {
|
|
88
|
+
ds[i] += 1;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Split back into integer (all but the last `digits`) and fractional digits.
|
|
95
|
+
let int_len = ds.len() - digits;
|
|
96
|
+
let mut out = String::with_capacity(ds.len() + 2);
|
|
97
|
+
if negative {
|
|
98
|
+
out.push('-');
|
|
99
|
+
}
|
|
100
|
+
for &d in &ds[..int_len] {
|
|
101
|
+
out.push((d + b'0') as char);
|
|
102
|
+
}
|
|
103
|
+
if digits > 0 {
|
|
104
|
+
out.push('.');
|
|
105
|
+
for &d in &ds[int_len..] {
|
|
106
|
+
out.push((d + b'0') as char);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
out
|
|
44
110
|
}
|
|
45
111
|
|
|
46
112
|
/// `Number.prototype.toExponential(fractionDigits?)` — ECMA-262 §21.1.3.2. Exponential notation with
|
|
@@ -270,7 +336,7 @@ pub fn number_to_string_radix(num: f64, radix: i64) -> Option<String> {
|
|
|
270
336
|
if negative {
|
|
271
337
|
out.push('-');
|
|
272
338
|
}
|
|
273
|
-
out.push_str(
|
|
339
|
+
out.push_str(core::str::from_utf8(&int_digits).unwrap());
|
|
274
340
|
|
|
275
341
|
// Fractional part: multiply-by-radix, emitting the integer overflow each step.
|
|
276
342
|
if frac > 0.0 {
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
//! This module will contain shared object method implementations.
|
|
4
4
|
//! Functions will be migrated here from tishlang_runtime and tishlang_eval.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
#[cfg(feature = "portable")]
|
|
7
|
+
#[allow(unused_imports)]
|
|
8
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
9
|
+
|
|
10
|
+
use tishlang_core::Arc;
|
|
7
11
|
use tishlang_core::{ObjectData, PropMap, Value, VmRef};
|
|
8
12
|
|
|
9
13
|
/// Create a new empty object Value.
|
|
@@ -16,6 +20,7 @@ pub fn with_capacity(capacity: usize) -> Value {
|
|
|
16
20
|
Value::Object(VmRef::new(ObjectData {
|
|
17
21
|
strings: PropMap::with_capacity(capacity),
|
|
18
22
|
symbols: None,
|
|
23
|
+
frozen: false,
|
|
19
24
|
}))
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
//! All indices use character (Unicode scalar) positions for consistency with
|
|
4
4
|
//! JavaScript, matching .length and .charAt(). Byte offsets are never exposed.
|
|
5
5
|
|
|
6
|
+
#[cfg(feature = "portable")]
|
|
7
|
+
#[allow(unused_imports)]
|
|
8
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
9
|
+
|
|
6
10
|
use crate::helpers::normalize_index;
|
|
7
|
-
use
|
|
11
|
+
use core::cell::RefCell;
|
|
8
12
|
use tishlang_core::ArcStr;
|
|
9
13
|
use tishlang_core::Value;
|
|
10
14
|
use tishlang_core::VmRef;
|
|
15
|
+
#[cfg(feature = "portable")]
|
|
16
|
+
use tishlang_core::FloatExt;
|
|
11
17
|
|
|
12
18
|
// #203: a per-thread cursor cache that makes repeated character indexing (`charCodeAt(i)`, `s[i]`,
|
|
13
19
|
// `charAt(i)`) O(1)/near-O(1) instead of O(i). tish strings are UTF-8, so `chars().nth(i)` scans from
|
|
@@ -29,9 +35,14 @@ struct CharCursor {
|
|
|
29
35
|
byte_off: usize,
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
#[cfg(not(feature = "portable"))]
|
|
32
39
|
thread_local! {
|
|
33
40
|
static INDEX_CURSOR: RefCell<Option<CharCursor>> = const { RefCell::new(None) };
|
|
34
41
|
}
|
|
42
|
+
// Single-core: one shared cursor cache with the same `.with()` API.
|
|
43
|
+
#[cfg(feature = "portable")]
|
|
44
|
+
static INDEX_CURSOR: tishlang_core::SingleCore<RefCell<Option<CharCursor>>> =
|
|
45
|
+
tishlang_core::SingleCore::new(RefCell::new(None));
|
|
35
46
|
|
|
36
47
|
/// Borrow the cursor entry for `s`, reseeding (compute ASCII flag + character count) if it currently
|
|
37
48
|
/// caches a different backing allocation, then run `f` against it. Centralises the pointer-keyed
|
|
@@ -40,7 +51,7 @@ fn with_cursor<R>(s: &ArcStr, f: impl FnOnce(&mut CharCursor, &ArcStr) -> R) ->
|
|
|
40
51
|
INDEX_CURSOR.with(|cell| {
|
|
41
52
|
let mut slot = cell.borrow_mut();
|
|
42
53
|
let hit = matches!(slot.as_ref(), Some(c)
|
|
43
|
-
if
|
|
54
|
+
if core::ptr::eq(c.s.as_bytes().as_ptr(), s.as_bytes().as_ptr()) && c.s.len() == s.len());
|
|
44
55
|
if !hit {
|
|
45
56
|
let ascii = s.as_bytes().is_ascii();
|
|
46
57
|
// ASCII → character count equals byte length (free); otherwise count once.
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
//! ECMAScript-style `Symbol`, `Symbol.for`, `Symbol.keyFor`.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
#[cfg(feature = "portable")]
|
|
4
|
+
#[allow(unused_imports)]
|
|
5
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
5
6
|
|
|
6
|
-
use
|
|
7
|
+
use tishlang_core::sync::{Mutex, OnceLock};
|
|
8
|
+
use tishlang_core::{alloc_symbol_id, AHashMap, Arc, ObjectMap, TishSymbol, Value};
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
static SYMBOL_FOR_REGISTRY: OnceLock<Mutex<AHashMap<Arc<str>, Arc<TishSymbol>>>> = OnceLock::new();
|
|
9
11
|
|
|
10
|
-
static
|
|
11
|
-
|
|
12
|
-
fn symbol_registry() -> &'static Mutex<HashMap<Arc<str>, Arc<TishSymbol>>> {
|
|
13
|
-
SYMBOL_FOR_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
|
|
12
|
+
fn symbol_registry() -> &'static Mutex<AHashMap<Arc<str>, Arc<TishSymbol>>> {
|
|
13
|
+
SYMBOL_FOR_REGISTRY.get_or_init(|| Mutex::new(AHashMap::default()))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
fn symbol_for_impl(key: &str) -> Value {
|
|
17
17
|
let k: Arc<str> = key.into();
|
|
18
18
|
let mut reg = symbol_registry().lock().unwrap();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
// get-then-insert (rather than the `Entry` API) so this compiles against both
|
|
20
|
+
// std `HashMap` and the portable `hashbrown` map without a hasher-specific
|
|
21
|
+
// `Entry` import. `Symbol.for` is not a hot path.
|
|
22
|
+
if let Some(existing) = reg.get(&k) {
|
|
23
|
+
return Value::Symbol(existing.clone());
|
|
24
|
+
}
|
|
25
|
+
let id = alloc_symbol_id();
|
|
26
|
+
let sym = TishSymbol::new_registry(id, Arc::clone(&k), None);
|
|
27
|
+
reg.insert(k, Arc::clone(&sym));
|
|
28
28
|
Value::Symbol(sym)
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
//! - A typed array is a regular array at runtime, so `Array.isArray(ta)` is `true` and there is no
|
|
18
18
|
//! `instanceof` distinction.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
#[cfg(feature = "portable")]
|
|
21
|
+
#[allow(unused_imports)]
|
|
22
|
+
use alloc::{borrow::ToOwned, boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
|
|
23
|
+
#[cfg(feature = "portable")]
|
|
24
|
+
use tishlang_core::FloatExt;
|
|
25
|
+
|
|
26
|
+
use tishlang_core::Arc;
|
|
21
27
|
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
22
28
|
|
|
23
29
|
const CONSTRUCT: &str = "__construct";
|
|
@@ -93,7 +99,7 @@ fn bytes_per_element(kind: Kind) -> f64 {
|
|
|
93
99
|
fn elements(value: &Value) -> Vec<Value> {
|
|
94
100
|
match value {
|
|
95
101
|
Value::Array(a) => a.borrow().clone(),
|
|
96
|
-
Value::NumberArray(a) => a.borrow().
|
|
102
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
97
103
|
_ => Vec::new(),
|
|
98
104
|
}
|
|
99
105
|
}
|
|
@@ -291,6 +297,7 @@ mod tests {
|
|
|
291
297
|
match &packed {
|
|
292
298
|
Value::NumberArray(a) => {
|
|
293
299
|
let v = a.borrow();
|
|
300
|
+
let v = v.as_packed().expect("packed backing");
|
|
294
301
|
assert_eq!(v[0], 1.1);
|
|
295
302
|
assert_eq!(v[1], 2.2);
|
|
296
303
|
assert!(v[2].is_nan());
|
|
@@ -11,7 +11,7 @@ use tishlang_ast::{
|
|
|
11
11
|
|
|
12
12
|
use crate::chunk::{Chunk, Constant};
|
|
13
13
|
use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
|
|
14
|
-
use crate::opcode::{MathUnaryFn, Opcode};
|
|
14
|
+
use crate::opcode::{MathBinaryFn, MathUnaryFn, Opcode};
|
|
15
15
|
|
|
16
16
|
enum SimpleMapResult {
|
|
17
17
|
Identity,
|
|
@@ -452,6 +452,23 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
|
452
452
|
/// so it can't run program-wide), this has an explicit `FunDecl` arm: a `function name` is itself a
|
|
453
453
|
/// binding, and every function body is recursed into. `true` (or any node it can't analyze) ⇒ NOT
|
|
454
454
|
/// stable — which only forgoes the cross-function-call optimization, never risks a miscompile.
|
|
455
|
+
/// #203: the f64 value of a `Math.<name>` constant, or `None` if `name` isn't one. The values are the
|
|
456
|
+
/// IEEE doubles ECMAScript specifies (identical to Rust's `std::f64::consts` and to the `Math` object
|
|
457
|
+
/// built on every backend, #539), so emitting them as a `LoadConst` is byte-identical to `Math.PI` etc.
|
|
458
|
+
fn math_constant(name: &str) -> Option<f64> {
|
|
459
|
+
Some(match name {
|
|
460
|
+
"PI" => std::f64::consts::PI,
|
|
461
|
+
"E" => std::f64::consts::E,
|
|
462
|
+
"LN2" => std::f64::consts::LN_2,
|
|
463
|
+
"LN10" => std::f64::consts::LN_10,
|
|
464
|
+
"LOG2E" => std::f64::consts::LOG2_E,
|
|
465
|
+
"LOG10E" => std::f64::consts::LOG10_E,
|
|
466
|
+
"SQRT2" => std::f64::consts::SQRT_2,
|
|
467
|
+
"SQRT1_2" => std::f64::consts::FRAC_1_SQRT_2,
|
|
468
|
+
_ => return None,
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
455
472
|
fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
|
|
456
473
|
match s {
|
|
457
474
|
Statement::FunDecl {
|
|
@@ -1968,6 +1985,9 @@ impl<'a> Compiler<'a> {
|
|
|
1968
1985
|
}
|
|
1969
1986
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
1970
1987
|
inner_comp.stable_globals = Arc::clone(&self.stable_globals);
|
|
1988
|
+
// #203: `Math` unshadowed is a whole-program property; thread it so `Math.<fn>` in this
|
|
1989
|
+
// function body lowers to the MathUnary intrinsic (else the numeric JIT bails).
|
|
1990
|
+
inner_comp.math_is_global = self.math_is_global;
|
|
1971
1991
|
// Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
|
|
1972
1992
|
// param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
|
|
1973
1993
|
// compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
|
|
@@ -2399,6 +2419,31 @@ impl<'a> Compiler<'a> {
|
|
|
2399
2419
|
}
|
|
2400
2420
|
}
|
|
2401
2421
|
}
|
|
2422
|
+
// #203: `Math.<binfn>(a, b)` → `<a>; <b>; MathBinary(id)` — the same unshadowed-global
|
|
2423
|
+
// gate, for exactly two positional args (variadic `Math.max(a,b,c)` falls through to a
|
|
2424
|
+
// generic call). Lets the numeric JIT lower clamp/pow kernels instead of bailing.
|
|
2425
|
+
if self.math_is_global && args.len() == 2 {
|
|
2426
|
+
if let Expr::Member {
|
|
2427
|
+
object,
|
|
2428
|
+
prop: MemberProp::Name { name: fname, .. },
|
|
2429
|
+
optional: false,
|
|
2430
|
+
..
|
|
2431
|
+
} = callee.as_ref()
|
|
2432
|
+
{
|
|
2433
|
+
if let (Expr::Ident { name: obj, .. }, CallArg::Expr(a0), CallArg::Expr(a1)) =
|
|
2434
|
+
(object.as_ref(), &args[0], &args[1])
|
|
2435
|
+
{
|
|
2436
|
+
if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
|
|
2437
|
+
if let Some(bfn) = MathBinaryFn::from_name(fname.as_ref()) {
|
|
2438
|
+
self.compile_expr(a0)?;
|
|
2439
|
+
self.compile_expr(a1)?;
|
|
2440
|
+
self.emit_u16(Opcode::MathBinary, bfn as u16);
|
|
2441
|
+
return Ok(());
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2402
2447
|
// Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
|
|
2403
2448
|
if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
|
|
2404
2449
|
&& args.len() == 1
|
|
@@ -2520,6 +2565,26 @@ impl<'a> Compiler<'a> {
|
|
|
2520
2565
|
optional,
|
|
2521
2566
|
..
|
|
2522
2567
|
} => {
|
|
2568
|
+
// #203: `Math.<CONST>` (PI / E / LN2 / …) when `Math` is the provably-unshadowed
|
|
2569
|
+
// global → a plain `LoadConst`, so the numeric JIT sees a constant instead of bailing
|
|
2570
|
+
// on a `GetMember`. The values match the `Math` object's on every backend (both use
|
|
2571
|
+
// `std::f64::consts`, #539), so this is byte-identical. Same gate as the intrinsics.
|
|
2572
|
+
if !*optional && self.math_is_global {
|
|
2573
|
+
if let (
|
|
2574
|
+
Expr::Ident { name: obj, .. },
|
|
2575
|
+
MemberProp::Name { name: p, .. },
|
|
2576
|
+
) = (object.as_ref(), prop)
|
|
2577
|
+
{
|
|
2578
|
+
if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
|
|
2579
|
+
if let Some(v) = math_constant(p.as_ref()) {
|
|
2580
|
+
let idx = self.constant_idx(Constant::Number(v));
|
|
2581
|
+
self.emit(Opcode::LoadConst);
|
|
2582
|
+
self.chunk.write_u16(idx);
|
|
2583
|
+
return Ok(());
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2523
2588
|
self.compile_expr(object)?;
|
|
2524
2589
|
if *optional {
|
|
2525
2590
|
self.emit(Opcode::Dup);
|
|
@@ -2686,6 +2751,7 @@ impl<'a> Compiler<'a> {
|
|
|
2686
2751
|
}
|
|
2687
2752
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
2688
2753
|
inner_comp.stable_globals = Arc::clone(&self.stable_globals); // #187
|
|
2754
|
+
inner_comp.math_is_global = self.math_is_global; // #203 (see other call site)
|
|
2689
2755
|
if let Some(map) = simple_slots {
|
|
2690
2756
|
inner_comp.slot_ctx = Some(map);
|
|
2691
2757
|
} else {
|
|
@@ -3116,7 +3182,15 @@ fn compile_internal(
|
|
|
3116
3182
|
// #186 — `Math` intrinsics are sound only if `Math` is never rebound anywhere in the program.
|
|
3117
3183
|
// `stmt_rebinds` is conservative (any rebind, destructure, FunDecl, or unknown node → true), so a
|
|
3118
3184
|
// `false` here only forgoes the optimization, never risks a miscompile.
|
|
3119
|
-
|
|
3185
|
+
// #203: whole-program scan (recurses into function bodies/params, unlike `stmt_rebinds` which is
|
|
3186
|
+
// conservatively `true` on any FunDecl) — so `Math` is provably-global even when the program
|
|
3187
|
+
// defines functions, letting `Math.<fn>` inside a function lower to the MathUnary intrinsic (the
|
|
3188
|
+
// numeric/array JIT then compiles the kernel instead of bailing on a generic call). Errs toward
|
|
3189
|
+
// "rebound" ⇒ never falsely global ⇒ no miscompile. Threaded into nested compilers below.
|
|
3190
|
+
compiler.math_is_global = !program
|
|
3191
|
+
.statements
|
|
3192
|
+
.iter()
|
|
3193
|
+
.any(|s| name_rebinds_in_stmt(s, "Math"));
|
|
3120
3194
|
// #187 — the set of top-level functions safe to call directly from JIT'd code (never reassigned/
|
|
3121
3195
|
// shadowed/redeclared anywhere). Conservative: a name absent here only forgoes the optimization.
|
|
3122
3196
|
compiler.stable_globals = Arc::new(compute_stable_globals(program));
|
|
@@ -16,5 +16,5 @@ pub use compiler::{
|
|
|
16
16
|
compile_with_source, CompileError,
|
|
17
17
|
};
|
|
18
18
|
pub use encoding::{binop_to_u8, compound_op_to_u8, u8_to_binop, u8_to_unaryop, unaryop_to_u8};
|
|
19
|
-
pub use opcode::{MathUnaryFn, Opcode};
|
|
19
|
+
pub use opcode::{MathBinaryFn, MathUnaryFn, Opcode};
|
|
20
20
|
pub use serialize::{deserialize, serialize};
|