@tishlang/tish-format 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.
Files changed (76) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-format +0 -0
  3. package/crates/tish/src/cli_help.rs +28 -0
  4. package/crates/tish/src/main.rs +41 -5
  5. package/crates/tish/tests/scope_merge.rs +79 -0
  6. package/crates/tish_build_utils/src/lib.rs +27 -1
  7. package/crates/tish_builtins/Cargo.toml +14 -6
  8. package/crates/tish_builtins/src/array.rs +130 -54
  9. package/crates/tish_builtins/src/collections.rs +9 -5
  10. package/crates/tish_builtins/src/construct.rs +7 -1
  11. package/crates/tish_builtins/src/date.rs +15 -1
  12. package/crates/tish_builtins/src/globals.rs +38 -3
  13. package/crates/tish_builtins/src/helpers.rs +7 -3
  14. package/crates/tish_builtins/src/iterator.rs +5 -1
  15. package/crates/tish_builtins/src/lib.rs +3 -0
  16. package/crates/tish_builtins/src/math.rs +14 -0
  17. package/crates/tish_builtins/src/number.rs +72 -6
  18. package/crates/tish_builtins/src/object.rs +6 -1
  19. package/crates/tish_builtins/src/string.rs +13 -2
  20. package/crates/tish_builtins/src/symbol.rs +17 -17
  21. package/crates/tish_builtins/src/typedarrays.rs +9 -2
  22. package/crates/tish_bytecode/src/compiler.rs +76 -2
  23. package/crates/tish_bytecode/src/lib.rs +1 -1
  24. package/crates/tish_bytecode/src/opcode.rs +77 -2
  25. package/crates/tish_compile/src/codegen.rs +812 -47
  26. package/crates/tish_compile/src/infer.rs +37 -41
  27. package/crates/tish_compile/src/lib.rs +13 -1
  28. package/crates/tish_compile/src/platform_resolve.rs +361 -0
  29. package/crates/tish_compile/src/resolve.rs +148 -10
  30. package/crates/tish_compile/src/schemes.rs +278 -0
  31. package/crates/tish_compile/src/types.rs +141 -2
  32. package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
  33. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
  34. package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
  35. package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
  36. package/crates/tish_compiler_wasm/Cargo.toml +1 -0
  37. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
  38. package/crates/tish_core/Cargo.toml +39 -4
  39. package/crates/tish_core/src/compat.rs +506 -0
  40. package/crates/tish_core/src/console_style.rs +19 -1
  41. package/crates/tish_core/src/json.rs +34 -10
  42. package/crates/tish_core/src/lib.rs +121 -25
  43. package/crates/tish_core/src/macros.rs +1 -1
  44. package/crates/tish_core/src/shape.rs +4 -2
  45. package/crates/tish_core/src/uri.rs +8 -1
  46. package/crates/tish_core/src/value.rs +182 -54
  47. package/crates/tish_core/src/vmref.rs +17 -5
  48. package/crates/tish_eval/Cargo.toml +2 -0
  49. package/crates/tish_eval/src/eval.rs +90 -3
  50. package/crates/tish_eval/src/value.rs +8 -0
  51. package/crates/tish_eval/src/value_convert.rs +316 -21
  52. package/crates/tish_fmt/Cargo.toml +1 -1
  53. package/crates/tish_lsp/Cargo.toml +2 -0
  54. package/crates/tish_lsp/src/import_goto.rs +41 -14
  55. package/crates/tish_lsp/src/main.rs +18 -0
  56. package/crates/tish_native/src/build.rs +246 -0
  57. package/crates/tish_native/src/config.rs +20 -0
  58. package/crates/tish_native/src/lib.rs +16 -6
  59. package/crates/tish_parser/src/lib.rs +28 -0
  60. package/crates/tish_parser/src/parser.rs +25 -4
  61. package/crates/tish_runtime/src/http_hyper.rs +70 -0
  62. package/crates/tish_runtime/src/lib.rs +55 -10
  63. package/crates/tish_runtime/src/ws.rs +94 -21
  64. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  65. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  66. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  67. package/crates/tish_runtime_gba/src/lib.rs +620 -0
  68. package/crates/tish_vm/src/jit.rs +1446 -164
  69. package/crates/tish_vm/src/vm.rs +535 -173
  70. package/justfile +9 -0
  71. package/package.json +1 -1
  72. package/platform/darwin-arm64/tish-fmt +0 -0
  73. package/platform/darwin-x64/tish-fmt +0 -0
  74. package/platform/linux-arm64/tish-fmt +0 -0
  75. package/platform/linux-x64/tish-fmt +0 -0
  76. package/platform/win32-x64/tish-fmt.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
- use std::any::Any;
27
- use std::hash::{Hash, Hasher};
28
- use std::sync::Arc;
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, ahash::RandomState>>;
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().iter().map(|n| Value::Number(*n)).collect(),
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
- use std::sync::Arc;
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
- use std::sync::Arc;
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
- use std::sync::Arc;
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().iter().map(|n| Value::Number(*n)).collect(),
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::with_capacity(entries_borrow.len());
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
- use std::sync::Arc;
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 std::fmt::Display) -> Value {
24
- let mut obj = ObjectMap::with_capacity(1);
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
- use std::sync::Arc;
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. Rust's `{:.*}` rounds half-to-even (`(2.5).toFixed(0)` → "2"); JS rounds half away
29
- /// from zero (→ "3"), so pre-round the scaled value with `.round()` (half-away) before formatting. #247
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
- let factor = 10f64.powi(digits as i32);
42
- let rounded = (num * factor).round() / factor;
43
- format!("{:.*}", digits, rounded)
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(std::str::from_utf8(&int_digits).unwrap());
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
- use std::sync::Arc;
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 std::cell::RefCell;
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 std::ptr::eq(c.s.as_bytes().as_ptr(), s.as_bytes().as_ptr()) && c.s.len() == s.len());
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
- use std::collections::hash_map::Entry;
4
- use std::sync::{Arc, Mutex, OnceLock};
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 std::collections::HashMap;
7
+ use tishlang_core::sync::{Mutex, OnceLock};
8
+ use tishlang_core::{alloc_symbol_id, AHashMap, Arc, ObjectMap, TishSymbol, Value};
7
9
 
8
- use tishlang_core::{alloc_symbol_id, ObjectMap, TishSymbol, Value};
10
+ static SYMBOL_FOR_REGISTRY: OnceLock<Mutex<AHashMap<Arc<str>, Arc<TishSymbol>>>> = OnceLock::new();
9
11
 
10
- static SYMBOL_FOR_REGISTRY: OnceLock<Mutex<HashMap<Arc<str>, Arc<TishSymbol>>>> = OnceLock::new();
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
- let sym = match reg.entry(Arc::clone(&k)) {
20
- Entry::Occupied(e) => e.get().clone(),
21
- Entry::Vacant(v) => {
22
- let id = alloc_symbol_id();
23
- let sym = TishSymbol::new_registry(id, Arc::clone(&k), None);
24
- v.insert(Arc::clone(&sym));
25
- sym
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
- use std::sync::Arc;
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().iter().map(|n| Value::Number(*n)).collect(),
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
- compiler.math_is_global = !program.statements.iter().any(|s| stmt_rebinds(s, "Math"));
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};