@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.
Files changed (73) hide show
  1. package/Cargo.toml +4 -1
  2. package/bin/tish-lsp +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 +806 -46
  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_lsp/Cargo.toml +3 -1
  53. package/crates/tish_lsp/src/import_goto.rs +41 -14
  54. package/crates/tish_lsp/src/main.rs +18 -0
  55. package/crates/tish_native/src/build.rs +246 -0
  56. package/crates/tish_native/src/config.rs +20 -0
  57. package/crates/tish_native/src/lib.rs +16 -6
  58. package/crates/tish_parser/src/lib.rs +28 -0
  59. package/crates/tish_parser/src/parser.rs +25 -4
  60. package/crates/tish_runtime/src/lib.rs +54 -9
  61. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  62. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  63. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  64. package/crates/tish_runtime_gba/src/lib.rs +620 -0
  65. package/crates/tish_vm/src/jit.rs +1446 -164
  66. package/crates/tish_vm/src/vm.rs +535 -173
  67. package/justfile +9 -0
  68. package/package.json +1 -1
  69. package/platform/darwin-arm64/tish-lsp +0 -0
  70. package/platform/darwin-x64/tish-lsp +0 -0
  71. package/platform/linux-arm64/tish-lsp +0 -0
  72. package/platform/linux-x64/tish-lsp +0 -0
  73. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -1,12 +1,20 @@
1
1
  //! Unified Value type for Tish runtime values.
2
2
 
3
- use std::sync::atomic::{AtomicU64, Ordering};
4
- use std::sync::Arc;
5
-
6
- use ahash::AHashMap;
3
+ use crate::compat::{AHashMap, ArcStr, AtomicU64, Arc, Ordering, RandomState};
7
4
  use indexmap::IndexMap;
8
5
  use smallvec::SmallVec;
9
6
 
7
+ #[cfg(feature = "portable")]
8
+ use alloc::{
9
+ boxed::Box,
10
+ format,
11
+ string::{String, ToString},
12
+ vec,
13
+ vec::Vec,
14
+ };
15
+ #[cfg(feature = "portable")]
16
+ use crate::compat::FloatExt;
17
+
10
18
  use crate::vmref::VmRef;
11
19
 
12
20
  /// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
@@ -75,12 +83,12 @@ use fancy_regex::Regex;
75
83
  pub trait Callable: Send + Sync {
76
84
  fn call(&self, args: &[Value]) -> Value;
77
85
  /// Downcast hook for the VM frame path; native adapters return themselves (downcast fails).
78
- fn as_any(&self) -> &dyn std::any::Any;
86
+ fn as_any(&self) -> &dyn core::any::Any;
79
87
  }
80
88
  #[cfg(not(feature = "send-values"))]
81
89
  pub trait Callable {
82
90
  fn call(&self, args: &[Value]) -> Value;
83
- fn as_any(&self) -> &dyn std::any::Any;
91
+ fn as_any(&self) -> &dyn core::any::Any;
84
92
  }
85
93
 
86
94
  /// Adapter wrapping a plain `Fn` closure (every native builtin) as a [`Callable`].
@@ -91,7 +99,7 @@ impl<F: Fn(&[Value]) -> Value + Send + Sync + 'static> Callable for FnCallable<F
91
99
  fn call(&self, args: &[Value]) -> Value {
92
100
  (self.0)(args)
93
101
  }
94
- fn as_any(&self) -> &dyn std::any::Any {
102
+ fn as_any(&self) -> &dyn core::any::Any {
95
103
  self
96
104
  }
97
105
  }
@@ -101,7 +109,7 @@ impl<F: Fn(&[Value]) -> Value + 'static> Callable for FnCallable<F> {
101
109
  fn call(&self, args: &[Value]) -> Value {
102
110
  (self.0)(args)
103
111
  }
104
- fn as_any(&self) -> &dyn std::any::Any {
112
+ fn as_any(&self) -> &dyn core::any::Any {
105
113
  self
106
114
  }
107
115
  }
@@ -109,7 +117,7 @@ impl<F: Fn(&[Value]) -> Value + 'static> Callable for FnCallable<F> {
109
117
  #[cfg(feature = "send-values")]
110
118
  pub type NativeFn = Arc<dyn Callable>;
111
119
  #[cfg(not(feature = "send-values"))]
112
- pub type NativeFn = std::rc::Rc<dyn Callable>;
120
+ pub type NativeFn = alloc::rc::Rc<dyn Callable>;
113
121
 
114
122
  /// Build a raw [`NativeFn`] from a plain closure (wraps it in [`FnCallable`]). For sites that
115
123
  /// need a `NativeFn` handle directly rather than a `Value::Function` (e.g. HTTP/promise/timer
@@ -120,7 +128,7 @@ pub fn native_fn<F: Fn(&[Value]) -> Value + Send + Sync + 'static>(f: F) -> Nati
120
128
  }
121
129
  #[cfg(not(feature = "send-values"))]
122
130
  pub fn native_fn<F: Fn(&[Value]) -> Value + 'static>(f: F) -> NativeFn {
123
- std::rc::Rc::new(FnCallable(f))
131
+ alloc::rc::Rc::new(FnCallable(f))
124
132
  }
125
133
 
126
134
  /// Trait for opaque Rust types exposed to Tish (e.g. Polars DataFrame).
@@ -141,7 +149,7 @@ pub trait TishOpaque: Send + Sync {
141
149
  fn get_method(&self, name: &str) -> Option<NativeFn>;
142
150
 
143
151
  /// For downcasting `Arc<dyn TishOpaque>` in native crates (e.g. Polars → `DataFrame`).
144
- fn as_any(&self) -> &dyn std::any::Any;
152
+ fn as_any(&self) -> &dyn core::any::Any;
145
153
  }
146
154
 
147
155
  /// Single-threaded variant (no `Send + Sync` bound); see the `send-values` doc above.
@@ -154,13 +162,13 @@ pub trait TishOpaque {
154
162
  fn get_method(&self, name: &str) -> Option<NativeFn>;
155
163
 
156
164
  /// For downcasting `Arc<dyn TishOpaque>` in native crates (e.g. Polars → `DataFrame`).
157
- fn as_any(&self) -> &dyn std::any::Any;
165
+ fn as_any(&self) -> &dyn core::any::Any;
158
166
  }
159
167
 
160
168
  /// Trait for Promise-like values that can be awaited (block until settled).
161
169
  /// Implemented by the runtime for native compile; interpreter uses its own Promise.
162
170
  pub trait TishPromise: Send + Sync {
163
- fn block_until_settled(&self) -> std::result::Result<Value, Value>;
171
+ fn block_until_settled(&self) -> core::result::Result<Value, Value>;
164
172
  /// Try to settle WITHOUT blocking. Returns `Some(result)` if the promise was already
165
173
  /// settled before this call; returns `None` if it is still pending (a background thread
166
174
  /// / I/O task has not completed yet). Default: always pending — implementors of async
@@ -169,7 +177,7 @@ pub trait TishPromise: Send + Sync {
169
177
  /// Used by `race`/`any`/`allSettled` to handle already-settled promises in input-order
170
178
  /// (deterministic, JS-compatible) before falling back to concurrent thread waiting for
171
179
  /// genuinely-pending ones.
172
- fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
180
+ fn try_settle(&self) -> Option<core::result::Result<Value, Value>> {
173
181
  None
174
182
  }
175
183
  }
@@ -236,8 +244,8 @@ impl RegExpFlags {
236
244
  }
237
245
 
238
246
  #[cfg(feature = "regex")]
239
- impl std::fmt::Display for RegExpFlags {
240
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247
+ impl core::fmt::Display for RegExpFlags {
248
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241
249
  if self.global {
242
250
  f.write_str("g")?;
243
251
  }
@@ -344,20 +352,111 @@ impl TishRegExp {
344
352
  /// **Thread safety**: `Value: Send + Sync`. Mutable payloads live inside
345
353
  /// [`VmRef`], a `Send + Sync` `Arc<Mutex<T>>` wrapper that preserves the
346
354
  /// `RefCell`-style borrow API. Functions are `Arc<dyn Fn + Send + Sync>`.
355
+ /// Backing store for a [`Value::NumberArray`] (#199 packed arrays). Starts `Packed`; a mutation
356
+ /// that would introduce a non-numeric element upgrades it to `Boxed` **in place** via
357
+ /// [`NumArrayBacking::deopt`], so all aliases sharing the `VmRef` observe the change (#506).
358
+ ///
359
+ /// Access rules that keep the two arms sound at every call site:
360
+ /// - single-element read → [`get`](Self::get) (O(1), no alloc);
361
+ /// - iterate/snapshot as `Value`s → [`to_values`](Self::to_values) (Packed boxes each f64, same
362
+ /// cost the old `materialize` path paid);
363
+ /// - hot packed-only fast path (sort/fold/`sum`) → [`as_packed`](Self::as_packed) /
364
+ /// [`as_packed_mut`](Self::as_packed_mut), which return `None` after a deopt so the caller falls
365
+ /// back to the boxed path instead of assuming `Vec<f64>`.
366
+ #[derive(Clone, Debug)]
367
+ pub enum NumArrayBacking {
368
+ Packed(Vec<f64>),
369
+ Boxed(Vec<Value>),
370
+ }
371
+
372
+ impl NumArrayBacking {
373
+ #[inline]
374
+ pub fn len(&self) -> usize {
375
+ match self {
376
+ NumArrayBacking::Packed(v) => v.len(),
377
+ NumArrayBacking::Boxed(v) => v.len(),
378
+ }
379
+ }
380
+ #[inline]
381
+ pub fn is_empty(&self) -> bool {
382
+ self.len() == 0
383
+ }
384
+ /// Element `i` as a `Value` (Packed: `Number(n)`, NaN kept as `Number(NaN)` — the hole-as-null
385
+ /// rule is applied by the specific read sites that want it, preserving current per-site
386
+ /// behavior). `None` if out of bounds.
387
+ #[inline]
388
+ pub fn get(&self, i: usize) -> Option<Value> {
389
+ match self {
390
+ NumArrayBacking::Packed(v) => v.get(i).map(|n| Value::Number(*n)),
391
+ NumArrayBacking::Boxed(v) => v.get(i).cloned(),
392
+ }
393
+ }
394
+ /// Snapshot the whole array as boxed `Value`s (Packed boxes each `f64`).
395
+ #[inline]
396
+ pub fn to_values(&self) -> Vec<Value> {
397
+ match self {
398
+ NumArrayBacking::Packed(v) => v.iter().map(|n| Value::Number(*n)).collect(),
399
+ NumArrayBacking::Boxed(v) => v.clone(),
400
+ }
401
+ }
402
+ /// The packed `f64` slice, iff still packed (hot fast paths); `None` once deopted.
403
+ #[inline]
404
+ pub fn as_packed(&self) -> Option<&Vec<f64>> {
405
+ match self {
406
+ NumArrayBacking::Packed(v) => Some(v),
407
+ NumArrayBacking::Boxed(_) => None,
408
+ }
409
+ }
410
+ #[inline]
411
+ pub fn as_packed_mut(&mut self) -> Option<&mut Vec<f64>> {
412
+ match self {
413
+ NumArrayBacking::Packed(v) => Some(v),
414
+ NumArrayBacking::Boxed(_) => None,
415
+ }
416
+ }
417
+ /// The boxed slice, iff already deopted.
418
+ #[inline]
419
+ pub fn as_boxed(&self) -> Option<&Vec<Value>> {
420
+ match self {
421
+ NumArrayBacking::Boxed(v) => Some(v),
422
+ NumArrayBacking::Packed(_) => None,
423
+ }
424
+ }
425
+ /// Upgrade to a boxed backing **in place** (idempotent) and return `&mut` to the boxed `Vec`.
426
+ /// Every alias sharing this cell now sees a boxed array (#506). Packed `f64`s become
427
+ /// `Number(n)` (NaN preserved), so a read after deopt matches what the packed form returned.
428
+ #[inline]
429
+ pub fn deopt(&mut self) -> &mut Vec<Value> {
430
+ if let NumArrayBacking::Packed(v) = self {
431
+ let boxed = v.iter().map(|n| Value::Number(*n)).collect();
432
+ *self = NumArrayBacking::Boxed(boxed);
433
+ }
434
+ match self {
435
+ NumArrayBacking::Boxed(v) => v,
436
+ NumArrayBacking::Packed(_) => unreachable!("just deopted"),
437
+ }
438
+ }
439
+ }
440
+
347
441
  #[derive(Clone, Default)]
348
442
  pub enum Value {
349
443
  Number(f64),
350
- String(arcstr::ArcStr),
444
+ String(ArcStr),
351
445
  Bool(bool),
352
446
  #[default]
353
447
  Null,
354
448
  Array(VmRef<Vec<Value>>),
355
- /// Packed f64 array — `TISH_PACKED_ARRAYS` mode only. All elements are f64; a non-numeric
356
- /// push/set/op materializes to `Value::Array` first. Eliminates per-element boxing and
357
- /// enables direct `sort_unstable_by` without an unbox pass. Created by all-numeric array
358
- /// literals, `new Array(n)` (zero-filled), and from numeric HOF results. Never created
359
- /// when `packed_arrays_enabled()` is false — callers check before constructing.
360
- NumberArray(VmRef<Vec<f64>>),
449
+ /// Numeric-origin array — `TISH_PACKED_ARRAYS` mode only. Starts `Packed` (a `Vec<f64>`, no
450
+ /// per-element boxing; enables direct `sort_unstable_by` and unboxed folds). A mutation that
451
+ /// introduces a **non-numeric** value ([`NumArrayBacking::deopt`]) upgrades the backing to
452
+ /// `Boxed` IN PLACE, so every alias sharing this `VmRef` observes the transition (#506) — the
453
+ /// value stays a `NumberArray` (aliases can't be rewritten to `Array`), but its backing is now
454
+ /// a `Vec<Value>` and it behaves exactly like a boxed array. So the tag means "numeric-origin,
455
+ /// possibly deopted"; access goes through the [`NumArrayBacking`] accessors, never a raw
456
+ /// `Vec<f64>` assumption. Created by all-numeric array literals, `new Array(n)` (zero-filled),
457
+ /// numeric HOF results, and typed arrays. Never created when `packed_arrays_enabled()` is
458
+ /// false — callers check before constructing.
459
+ NumberArray(VmRef<NumArrayBacking>),
361
460
  Object(VmRef<ObjectData>),
362
461
  /// ECMAScript-style primitive symbol (identity by `Arc`).
363
462
  Symbol(Arc<TishSymbol>),
@@ -381,7 +480,7 @@ pub enum Value {
381
480
  // re-attempt the box trick; only a dispatch-level change (e.g. NaN-box's branch-free tag test, not
382
481
  // its size) could pay off. Gated to 64-bit: wasm32 (wasi) has 32-bit pointers, so size differs there.
383
482
  #[cfg(target_pointer_width = "64")]
384
- const _: () = assert!(std::mem::size_of::<Value>() == 24);
483
+ const _: () = assert!(core::mem::size_of::<Value>() == 24);
385
484
 
386
485
  // ─────────────────────────────────────────────────────────────────────────────
387
486
  // #201 Stage A — representation abstraction (behavior-preserving, zero perf effect).
@@ -422,11 +521,11 @@ pub enum ValueTag {
422
521
  /// sites. Zero-cost — each arm borrows the existing payload in place.
423
522
  pub enum ValueRef<'a> {
424
523
  Number(f64),
425
- String(&'a arcstr::ArcStr),
524
+ String(&'a ArcStr),
426
525
  Bool(bool),
427
526
  Null,
428
527
  Array(&'a VmRef<Vec<Value>>),
429
- NumberArray(&'a VmRef<Vec<f64>>),
528
+ NumberArray(&'a VmRef<NumArrayBacking>),
430
529
  Object(&'a VmRef<ObjectData>),
431
530
  Symbol(&'a Arc<TishSymbol>),
432
531
  Function(&'a NativeFn),
@@ -452,7 +551,7 @@ impl Value {
452
551
  /// Named constructor for a string (#201 abstraction). Accepts anything that converts
453
552
  /// into the interned `ArcStr` (`&str`, `String`, `ArcStr`).
454
553
  #[inline]
455
- pub fn string(s: impl Into<arcstr::ArcStr>) -> Self {
554
+ pub fn string(s: impl Into<ArcStr>) -> Self {
456
555
  Value::String(s.into())
457
556
  }
458
557
 
@@ -540,7 +639,7 @@ impl Value {
540
639
 
541
640
  /// Borrow the packed-number-array payload, if this is a `NumberArray`.
542
641
  #[inline]
543
- pub fn as_number_array(&self) -> Option<&VmRef<Vec<f64>>> {
642
+ pub fn as_number_array(&self) -> Option<&VmRef<NumArrayBacking>> {
544
643
  match self {
545
644
  Value::NumberArray(a) => Some(a),
546
645
  _ => None,
@@ -571,7 +670,7 @@ const PROPMAP_INLINE: usize = 8;
571
670
  #[derive(Clone, Debug, Default)]
572
671
  pub struct PropMap {
573
672
  inline: SmallVec<[(Arc<str>, Value); PROPMAP_INLINE]>,
574
- map: Option<Box<IndexMap<Arc<str>, Value, ahash::RandomState>>>,
673
+ map: Option<Box<IndexMap<Arc<str>, Value, RandomState>>>,
575
674
  /// Hidden-class identity for this object's ordered key-set (JSC Structure). `EMPTY_SHAPE` (0)
576
675
  /// for `{}`. Maintained by `insert` (new key → `shape::transition`) and reset to `DICT_SHAPE` by
577
676
  /// `remove`. Lets the VM's inline caches compare a `u32` instead of hashing a key. INVARIANT: a
@@ -592,7 +691,7 @@ impl PropMap {
592
691
  inline: SmallVec::new(),
593
692
  map: Some(Box::new(IndexMap::with_capacity_and_hasher(
594
693
  n,
595
- ahash::RandomState::default(),
694
+ RandomState::default(),
596
695
  ))),
597
696
  shape: crate::shape::EMPTY_SHAPE,
598
697
  }
@@ -693,15 +792,15 @@ impl PropMap {
693
792
  }
694
793
  if let Some(slot) = self.inline.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref()) {
695
794
  // Update existing key → value changes, layout (shape) does not.
696
- return Some(std::mem::replace(&mut slot.1, val));
795
+ return Some(core::mem::replace(&mut slot.1, val));
697
796
  }
698
797
  // New key (inline storage) → transition the shape away from the current one.
699
798
  self.shape = crate::shape::transition(self.shape, &key);
700
799
  if self.inline.len() >= PROPMAP_INLINE {
701
800
  // Promote inline storage to an insertion-ordered map (keys + their order are preserved,
702
801
  // so the shape stays valid).
703
- let mut m: IndexMap<Arc<str>, Value, ahash::RandomState> =
704
- IndexMap::with_capacity_and_hasher(self.inline.len() + 1, ahash::RandomState::default());
802
+ let mut m: IndexMap<Arc<str>, Value, RandomState> =
803
+ IndexMap::with_capacity_and_hasher(self.inline.len() + 1, RandomState::default());
705
804
  for (k, v) in self.inline.drain(..) {
706
805
  m.insert(k, v);
707
806
  }
@@ -778,9 +877,9 @@ impl PropMap {
778
877
  // If the combined worst-case size escapes inline storage, promote once up front so the
779
878
  // per-key inserts below never reallocate or re-promote.
780
879
  if self.map.is_none() && self.inline.len() + incoming > PROPMAP_INLINE {
781
- let mut m: IndexMap<Arc<str>, Value, ahash::RandomState> = IndexMap::with_capacity_and_hasher(
880
+ let mut m: IndexMap<Arc<str>, Value, RandomState> = IndexMap::with_capacity_and_hasher(
782
881
  self.inline.len() + incoming,
783
- ahash::RandomState::default(),
882
+ RandomState::default(),
784
883
  );
785
884
  for (k, v) in self.inline.drain(..) {
786
885
  m.insert(k, v);
@@ -826,7 +925,7 @@ impl IntoIterator for PropMap {
826
925
 
827
926
  /// Zero-allocation borrowing iterator over [`PropMap`] entries (insertion order).
828
927
  pub enum PropMapIter<'a> {
829
- Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
928
+ Inline(core::slice::Iter<'a, (Arc<str>, Value)>),
830
929
  Map(indexmap::map::Iter<'a, Arc<str>, Value>),
831
930
  }
832
931
  impl<'a> Iterator for PropMapIter<'a> {
@@ -842,7 +941,7 @@ impl<'a> Iterator for PropMapIter<'a> {
842
941
 
843
942
  /// Zero-allocation key iterator over [`PropMap`] (insertion order).
844
943
  pub enum PropMapKeys<'a> {
845
- Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
944
+ Inline(core::slice::Iter<'a, (Arc<str>, Value)>),
846
945
  Map(indexmap::map::Keys<'a, Arc<str>, Value>),
847
946
  }
848
947
  impl<'a> Iterator for PropMapKeys<'a> {
@@ -858,7 +957,7 @@ impl<'a> Iterator for PropMapKeys<'a> {
858
957
 
859
958
  /// Zero-allocation value iterator over [`PropMap`] (insertion order).
860
959
  pub enum PropMapValues<'a> {
861
- Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
960
+ Inline(core::slice::Iter<'a, (Arc<str>, Value)>),
862
961
  Map(indexmap::map::Values<'a, Arc<str>, Value>),
863
962
  }
864
963
  impl<'a> Iterator for PropMapValues<'a> {
@@ -894,6 +993,10 @@ impl Iterator for PropMapIntoIter {
894
993
  pub struct ObjectData {
895
994
  pub strings: PropMap,
896
995
  pub symbols: Option<AHashMap<u64, Value>>,
996
+ /// `Object.freeze` marker (#437). When true, property writes must throw a catchable TypeError
997
+ /// instead of mutating — enforced at every write chokepoint (interp assign, vm `set_member`,
998
+ /// native `set_prop`). Reference-shared like the rest of the object; a fresh object is not frozen.
999
+ pub frozen: bool,
897
1000
  }
898
1001
 
899
1002
  impl ObjectData {
@@ -902,9 +1005,16 @@ impl ObjectData {
902
1005
  Self {
903
1006
  strings: strings.into_iter().collect(),
904
1007
  symbols: None,
1008
+ frozen: false,
905
1009
  }
906
1010
  }
907
1011
 
1012
+ /// True if `Object.freeze` has been applied to this object.
1013
+ #[inline]
1014
+ pub fn is_frozen(&self) -> bool {
1015
+ self.frozen
1016
+ }
1017
+
908
1018
  #[inline]
909
1019
  pub fn len_entries(&self) -> usize {
910
1020
  self.strings.len() + self.symbols.as_ref().map(|s| s.len()).unwrap_or(0)
@@ -1084,15 +1194,15 @@ pub fn value_call(callee: &Value, args: &[Value]) -> Value {
1084
1194
  return value_call(&inner, args);
1085
1195
  }
1086
1196
  crate::set_pending_throw(crate::not_a_function_error(format!(
1087
- "{} is not a function",
1088
- callee.to_display_string()
1197
+ "Call of non-function: {}",
1198
+ callee.type_name()
1089
1199
  )));
1090
1200
  Value::Null
1091
1201
  }
1092
1202
  _ => {
1093
1203
  crate::set_pending_throw(crate::not_a_function_error(format!(
1094
- "{} is not a function",
1095
- callee.to_display_string()
1204
+ "Call of non-function: {}",
1205
+ callee.type_name()
1096
1206
  )));
1097
1207
  Value::Null
1098
1208
  }
@@ -1118,11 +1228,15 @@ pub fn merge_object_data(left: &VmRef<ObjectData>, right: &VmRef<ObjectData>) ->
1118
1228
  None => symbols = Some(rs.clone()),
1119
1229
  }
1120
1230
  }
1121
- ObjectData { strings, symbols }
1231
+ ObjectData {
1232
+ strings,
1233
+ symbols,
1234
+ frozen: false,
1235
+ }
1122
1236
  }
1123
1237
 
1124
- impl std::fmt::Debug for Value {
1125
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1238
+ impl core::fmt::Debug for Value {
1239
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1126
1240
  match self {
1127
1241
  Value::Number(n) => write!(f, "Number({})", n),
1128
1242
  Value::String(s) => write!(f, "String({:?})", s.as_str()),
@@ -1242,7 +1356,7 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
1242
1356
  end -= 1;
1243
1357
  }
1244
1358
  let trail = (n - end) as i32;
1245
- let digits = std::str::from_utf8(&digits_buf[lead..end]).expect("ascii digits");
1359
+ let digits = core::str::from_utf8(&digits_buf[lead..end]).expect("ascii digits");
1246
1360
  let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
1247
1361
  let point = k + exp - frac_len + trail;
1248
1362
 
@@ -1318,8 +1432,12 @@ impl Value {
1318
1432
  Value::NumberArray(arr) => {
1319
1433
  let inner: Vec<String> = arr
1320
1434
  .borrow()
1435
+ .to_values()
1321
1436
  .iter()
1322
- .map(|&n| if n.is_nan() { "null".to_string() } else { Value::Number(n).to_display_string() })
1437
+ .map(|v| match v {
1438
+ Value::Number(n) if n.is_nan() => "null".to_string(),
1439
+ other => other.to_display_string(),
1440
+ })
1323
1441
  .collect();
1324
1442
  format!("[{}]", inner.join(", "))
1325
1443
  }
@@ -1393,8 +1511,9 @@ impl Value {
1393
1511
  }
1394
1512
  Value::NumberArray(arr) => arr
1395
1513
  .borrow()
1514
+ .to_values()
1396
1515
  .iter()
1397
- .map(|n| Value::Number(*n).to_js_string())
1516
+ .map(|v| v.to_js_string())
1398
1517
  .collect::<Vec<_>>()
1399
1518
  .join(","),
1400
1519
  Value::Object(_) => "[object Object]".to_string(),
@@ -1436,7 +1555,7 @@ impl Value {
1436
1555
  #[cfg(feature = "send-values")]
1437
1556
  (Value::Function(a), Value::Function(b)) => Arc::ptr_eq(a, b),
1438
1557
  #[cfg(not(feature = "send-values"))]
1439
- (Value::Function(a), Value::Function(b)) => std::rc::Rc::ptr_eq(a, b),
1558
+ (Value::Function(a), Value::Function(b)) => alloc::rc::Rc::ptr_eq(a, b),
1440
1559
  #[cfg(feature = "regex")]
1441
1560
  (Value::RegExp(a), Value::RegExp(b)) => VmRef::ptr_eq(a, b),
1442
1561
  (Value::Promise(a), Value::Promise(b)) => Arc::ptr_eq(a, b),
@@ -1465,7 +1584,7 @@ impl Value {
1465
1584
  where
1466
1585
  F: Fn(&[Value]) -> Value + 'static,
1467
1586
  {
1468
- Value::Function(std::rc::Rc::new(FnCallable(f)))
1587
+ Value::Function(alloc::rc::Rc::new(FnCallable(f)))
1469
1588
  }
1470
1589
 
1471
1590
  /// Create a new array Value from a Vec.
@@ -1490,6 +1609,7 @@ impl Value {
1490
1609
  Value::Object(VmRef::new(ObjectData {
1491
1610
  strings,
1492
1611
  symbols: None,
1612
+ frozen: false,
1493
1613
  }))
1494
1614
  }
1495
1615
 
@@ -1509,6 +1629,7 @@ impl Value {
1509
1629
  /// process starts (as the CI sweep does); mid-process toggling is not observed by design.
1510
1630
  /// The flag is intentionally backwards from the slot/JIT flags (those were default-on) to
1511
1631
  /// keep the default binary behaviour byte-identical while we validate coverage.
1632
+ #[cfg(not(feature = "portable"))]
1512
1633
  #[inline]
1513
1634
  pub fn packed_arrays_enabled() -> bool {
1514
1635
  static PACKED_ARRAYS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
@@ -1517,10 +1638,18 @@ impl Value {
1517
1638
  })
1518
1639
  }
1519
1640
 
1641
+ /// Packed f64 arrays are off under `portable` (no env to opt in; keeps the
1642
+ /// generic array path, which the GBA runtime already exercises).
1643
+ #[cfg(feature = "portable")]
1644
+ #[inline]
1645
+ pub fn packed_arrays_enabled() -> bool {
1646
+ false
1647
+ }
1648
+
1520
1649
  /// Wrap a `Vec<f64>` as a `Value::NumberArray`. Only call when `packed_arrays_enabled()`.
1521
1650
  #[inline]
1522
1651
  pub fn number_array(items: Vec<f64>) -> Self {
1523
- Value::NumberArray(VmRef::new(items))
1652
+ Value::NumberArray(VmRef::new(NumArrayBacking::Packed(items)))
1524
1653
  }
1525
1654
 
1526
1655
  /// Materialize a `Value::NumberArray` into a boxed `Value::Array`.
@@ -1529,9 +1658,8 @@ impl Value {
1529
1658
  /// converts once and continues on the generic path. The original `NumberArray` VmRef
1530
1659
  /// is consumed; callers replace the `Value` in whatever container held it.
1531
1660
  #[inline]
1532
- pub fn materialize_number_array(arr: &VmRef<Vec<f64>>) -> Value {
1533
- let nums = arr.borrow();
1534
- Value::Array(VmRef::new(nums.iter().map(|&n| Value::Number(n)).collect()))
1661
+ pub fn materialize_number_array(arr: &VmRef<NumArrayBacking>) -> Value {
1662
+ Value::Array(VmRef::new(arr.borrow().to_values()))
1535
1663
  }
1536
1664
 
1537
1665
  /// If `self` is a `NumberArray`, materialise and return `Value::Array`; otherwise
@@ -49,24 +49,24 @@
49
49
  //! feature. They both `Deref` (and, for write guards, `DerefMut`) to `T`
50
50
  //! just like the underlying types.
51
51
 
52
- use std::fmt;
52
+ use core::fmt;
53
53
 
54
54
  // --------------------------------------------------------------------------
55
55
  // Single-threaded backing store (default): Rc<RefCell<T>>
56
56
  // --------------------------------------------------------------------------
57
57
  #[cfg(not(feature = "send-values"))]
58
58
  mod imp {
59
- use std::cell::RefCell;
60
- use std::rc::Rc;
59
+ use alloc::rc::Rc;
60
+ use core::cell::RefCell;
61
61
 
62
62
  #[derive(Default)]
63
63
  pub struct VmRef<T: ?Sized>(pub(super) Rc<RefCell<T>>);
64
64
 
65
65
  /// Read guard alias. On the single-threaded path this is a true
66
66
  /// `Ref<'_, T>`, so multiple readers can coexist.
67
- pub type ReadGuard<'a, T> = std::cell::Ref<'a, T>;
67
+ pub type ReadGuard<'a, T> = core::cell::Ref<'a, T>;
68
68
  /// Write guard alias. Exclusive, `DerefMut`.
69
- pub type WriteGuard<'a, T> = std::cell::RefMut<'a, T>;
69
+ pub type WriteGuard<'a, T> = core::cell::RefMut<'a, T>;
70
70
 
71
71
  impl<T> VmRef<T> {
72
72
  #[inline]
@@ -182,6 +182,7 @@ mod imp {
182
182
 
183
183
  pub use imp::{ReadGuard as VmReadGuard, VmRef, WriteGuard as VmWriteGuard};
184
184
 
185
+ #[cfg(not(feature = "portable"))]
185
186
  impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
186
187
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187
188
  // Match `RefCell`'s debug format so snapshot-test output stays
@@ -195,3 +196,14 @@ impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
195
196
  }
196
197
  }
197
198
  }
199
+
200
+ // No unwinding on GBA (`panic = abort`), so `catch_unwind` is unavailable.
201
+ // Debug is used for host snapshot tests, not the runtime hot path, so a direct
202
+ // borrow is fine (a live borrow during Debug would abort, which never happens in
203
+ // practice on the single-threaded target).
204
+ #[cfg(feature = "portable")]
205
+ impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
206
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207
+ write!(f, "RefCell {{ value: {:?} }}", &*self.borrow())
208
+ }
209
+ }
@@ -42,6 +42,8 @@ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
42
42
  tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
43
43
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
44
44
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
45
+ # Shared platform/surface file cascade (same as `tish build` / `resolve-id`).
46
+ tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
45
47
  tiny_http = { version = "0.12.0", optional = true }
46
48
  tokio = { version = "1.50.0", features = ["rt-multi-thread", "time", "sync"], optional = true }
47
49
  tishlang_runtime = { path = "../tish_runtime", version = ">=0.1", optional = true }