@tishlang/tish-lsp 2.39.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) 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/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
  6. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
  7. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
  8. package/crates/tish/tests/regr_typed_externs.rs +59 -0
  9. package/crates/tish/tests/scope_merge.rs +79 -0
  10. package/crates/tish_build_utils/src/lib.rs +27 -1
  11. package/crates/tish_builtins/Cargo.toml +14 -6
  12. package/crates/tish_builtins/src/array.rs +130 -54
  13. package/crates/tish_builtins/src/collections.rs +9 -5
  14. package/crates/tish_builtins/src/construct.rs +7 -1
  15. package/crates/tish_builtins/src/date.rs +15 -1
  16. package/crates/tish_builtins/src/globals.rs +65 -3
  17. package/crates/tish_builtins/src/helpers.rs +7 -3
  18. package/crates/tish_builtins/src/iterator.rs +5 -1
  19. package/crates/tish_builtins/src/lib.rs +3 -0
  20. package/crates/tish_builtins/src/math.rs +14 -0
  21. package/crates/tish_builtins/src/number.rs +72 -6
  22. package/crates/tish_builtins/src/object.rs +6 -1
  23. package/crates/tish_builtins/src/string.rs +13 -2
  24. package/crates/tish_builtins/src/symbol.rs +17 -17
  25. package/crates/tish_builtins/src/typedarrays.rs +9 -2
  26. package/crates/tish_bytecode/src/compiler.rs +76 -2
  27. package/crates/tish_bytecode/src/lib.rs +1 -1
  28. package/crates/tish_bytecode/src/opcode.rs +77 -2
  29. package/crates/tish_compile/src/codegen.rs +2154 -225
  30. package/crates/tish_compile/src/infer.rs +37 -41
  31. package/crates/tish_compile/src/lib.rs +13 -1
  32. package/crates/tish_compile/src/platform_resolve.rs +361 -0
  33. package/crates/tish_compile/src/resolve.rs +195 -11
  34. package/crates/tish_compile/src/schemes.rs +333 -0
  35. package/crates/tish_compile/src/types.rs +205 -34
  36. package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
  37. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
  38. package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
  39. package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
  40. package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
  41. package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
  42. package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
  43. package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
  44. package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
  45. package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
  46. package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
  47. package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
  48. package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
  49. package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
  50. package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
  51. package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +1 -0
  53. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
  54. package/crates/tish_core/Cargo.toml +39 -4
  55. package/crates/tish_core/src/compat.rs +506 -0
  56. package/crates/tish_core/src/console_style.rs +23 -1
  57. package/crates/tish_core/src/json.rs +42 -10
  58. package/crates/tish_core/src/lib.rs +121 -25
  59. package/crates/tish_core/src/macros.rs +1 -1
  60. package/crates/tish_core/src/shape.rs +4 -2
  61. package/crates/tish_core/src/uri.rs +8 -1
  62. package/crates/tish_core/src/value.rs +305 -54
  63. package/crates/tish_core/src/vmref.rs +17 -5
  64. package/crates/tish_eval/Cargo.toml +2 -0
  65. package/crates/tish_eval/src/eval.rs +90 -3
  66. package/crates/tish_eval/src/value.rs +8 -0
  67. package/crates/tish_eval/src/value_convert.rs +316 -21
  68. package/crates/tish_lsp/Cargo.toml +3 -1
  69. package/crates/tish_lsp/src/import_goto.rs +41 -14
  70. package/crates/tish_lsp/src/main.rs +18 -0
  71. package/crates/tish_native/src/build.rs +246 -0
  72. package/crates/tish_native/src/config.rs +20 -0
  73. package/crates/tish_native/src/lib.rs +16 -6
  74. package/crates/tish_parser/src/lib.rs +28 -0
  75. package/crates/tish_parser/src/parser.rs +25 -4
  76. package/crates/tish_runtime/src/lib.rs +55 -9
  77. package/crates/tish_runtime_gba/Cargo.lock +837 -0
  78. package/crates/tish_runtime_gba/Cargo.toml +16 -0
  79. package/crates/tish_runtime_gba/src/gba.rs +211 -0
  80. package/crates/tish_runtime_gba/src/lib.rs +804 -0
  81. package/crates/tish_vm/src/jit.rs +1446 -164
  82. package/crates/tish_vm/src/vm.rs +583 -173
  83. package/justfile +9 -0
  84. package/package.json +1 -1
  85. package/platform/darwin-arm64/tish-lsp +0 -0
  86. package/platform/darwin-x64/tish-lsp +0 -0
  87. package/platform/linux-arm64/tish-lsp +0 -0
  88. package/platform/linux-x64/tish-lsp +0 -0
  89. 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,42 @@ 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;
166
+ }
167
+
168
+ /// A typed native struct (from an `interface` / typed object literal in native codegen) carried inside
169
+ /// a [`Value::Struct`] WITHOUT serialising to a hashmap object. Each generated `TishStruct_*` implements
170
+ /// it: a boxed function that returns/passes a typed struct keeps it by reference, property access
171
+ /// dispatches here (a small `match` on the field name — no string hash, no allocation), and it
172
+ /// materialises to a real `Value::Object` only when a boxed `Value::Struct` (a struct whose static
173
+ /// type was erased) crosses into genuinely dynamic territory (`JSON.stringify`, display,
174
+ /// `Object.keys`/`values`/`entries`, `for..in`, `in`, `Object.assign`, object-literal spread `{...s}`).
175
+ /// This is the RUNTIME fallback: when the struct's static type IS known at a reflection site, GBA
176
+ /// codegen lowers `Object.keys`/`values`/`entries`/`in`/`{...s}` to native field reads instead — the
177
+ /// struct is never boxed and this materialisation never runs. No `Send + Sync` bound: the `Value::Struct` variant is
178
+ /// `cfg(portable)` and `portable` excludes `send-values` (they are mutually exclusive), so the ref is
179
+ /// single-threaded `Rc` — mirroring the single-threaded [`TishOpaque`].
180
+ pub trait TishStruct {
181
+ /// Read a field as a boxed `Value` (`Null` if `key` is not a field). Backs `get_prop`.
182
+ fn tish_get(&self, key: &str) -> Value;
183
+ /// Write a field from a boxed `Value` (no-op if `key` is not a field). Backs `set_prop`.
184
+ fn tish_set(&mut self, key: &str, value: Value);
185
+ /// Materialise an ordered `Value::Object` copy (field-declaration order, JS insertion order). Used
186
+ /// for JSON / display / `Object`-interop, and as the non-portable `Value::from_struct` fallback so
187
+ /// off-GBA backends behave EXACTLY as the historical `object_from_pairs` boundary did.
188
+ fn tish_to_object(&self) -> Value;
189
+ /// `typeof` result — always "object".
190
+ fn tish_type_name(&self) -> &'static str {
191
+ "object"
192
+ }
193
+ /// Downcast hook — recover the concrete `TishStruct_*` (e.g. for a future typed fast path).
194
+ fn as_any(&self) -> &dyn core::any::Any;
158
195
  }
159
196
 
160
197
  /// Trait for Promise-like values that can be awaited (block until settled).
161
198
  /// Implemented by the runtime for native compile; interpreter uses its own Promise.
162
199
  pub trait TishPromise: Send + Sync {
163
- fn block_until_settled(&self) -> std::result::Result<Value, Value>;
200
+ fn block_until_settled(&self) -> core::result::Result<Value, Value>;
164
201
  /// Try to settle WITHOUT blocking. Returns `Some(result)` if the promise was already
165
202
  /// settled before this call; returns `None` if it is still pending (a background thread
166
203
  /// / I/O task has not completed yet). Default: always pending — implementors of async
@@ -169,7 +206,7 @@ pub trait TishPromise: Send + Sync {
169
206
  /// Used by `race`/`any`/`allSettled` to handle already-settled promises in input-order
170
207
  /// (deterministic, JS-compatible) before falling back to concurrent thread waiting for
171
208
  /// genuinely-pending ones.
172
- fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
209
+ fn try_settle(&self) -> Option<core::result::Result<Value, Value>> {
173
210
  None
174
211
  }
175
212
  }
@@ -236,8 +273,8 @@ impl RegExpFlags {
236
273
  }
237
274
 
238
275
  #[cfg(feature = "regex")]
239
- impl std::fmt::Display for RegExpFlags {
240
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276
+ impl core::fmt::Display for RegExpFlags {
277
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241
278
  if self.global {
242
279
  f.write_str("g")?;
243
280
  }
@@ -344,20 +381,111 @@ impl TishRegExp {
344
381
  /// **Thread safety**: `Value: Send + Sync`. Mutable payloads live inside
345
382
  /// [`VmRef`], a `Send + Sync` `Arc<Mutex<T>>` wrapper that preserves the
346
383
  /// `RefCell`-style borrow API. Functions are `Arc<dyn Fn + Send + Sync>`.
384
+ /// Backing store for a [`Value::NumberArray`] (#199 packed arrays). Starts `Packed`; a mutation
385
+ /// that would introduce a non-numeric element upgrades it to `Boxed` **in place** via
386
+ /// [`NumArrayBacking::deopt`], so all aliases sharing the `VmRef` observe the change (#506).
387
+ ///
388
+ /// Access rules that keep the two arms sound at every call site:
389
+ /// - single-element read → [`get`](Self::get) (O(1), no alloc);
390
+ /// - iterate/snapshot as `Value`s → [`to_values`](Self::to_values) (Packed boxes each f64, same
391
+ /// cost the old `materialize` path paid);
392
+ /// - hot packed-only fast path (sort/fold/`sum`) → [`as_packed`](Self::as_packed) /
393
+ /// [`as_packed_mut`](Self::as_packed_mut), which return `None` after a deopt so the caller falls
394
+ /// back to the boxed path instead of assuming `Vec<f64>`.
395
+ #[derive(Clone, Debug)]
396
+ pub enum NumArrayBacking {
397
+ Packed(Vec<f64>),
398
+ Boxed(Vec<Value>),
399
+ }
400
+
401
+ impl NumArrayBacking {
402
+ #[inline]
403
+ pub fn len(&self) -> usize {
404
+ match self {
405
+ NumArrayBacking::Packed(v) => v.len(),
406
+ NumArrayBacking::Boxed(v) => v.len(),
407
+ }
408
+ }
409
+ #[inline]
410
+ pub fn is_empty(&self) -> bool {
411
+ self.len() == 0
412
+ }
413
+ /// Element `i` as a `Value` (Packed: `Number(n)`, NaN kept as `Number(NaN)` — the hole-as-null
414
+ /// rule is applied by the specific read sites that want it, preserving current per-site
415
+ /// behavior). `None` if out of bounds.
416
+ #[inline]
417
+ pub fn get(&self, i: usize) -> Option<Value> {
418
+ match self {
419
+ NumArrayBacking::Packed(v) => v.get(i).map(|n| Value::Number(*n)),
420
+ NumArrayBacking::Boxed(v) => v.get(i).cloned(),
421
+ }
422
+ }
423
+ /// Snapshot the whole array as boxed `Value`s (Packed boxes each `f64`).
424
+ #[inline]
425
+ pub fn to_values(&self) -> Vec<Value> {
426
+ match self {
427
+ NumArrayBacking::Packed(v) => v.iter().map(|n| Value::Number(*n)).collect(),
428
+ NumArrayBacking::Boxed(v) => v.clone(),
429
+ }
430
+ }
431
+ /// The packed `f64` slice, iff still packed (hot fast paths); `None` once deopted.
432
+ #[inline]
433
+ pub fn as_packed(&self) -> Option<&Vec<f64>> {
434
+ match self {
435
+ NumArrayBacking::Packed(v) => Some(v),
436
+ NumArrayBacking::Boxed(_) => None,
437
+ }
438
+ }
439
+ #[inline]
440
+ pub fn as_packed_mut(&mut self) -> Option<&mut Vec<f64>> {
441
+ match self {
442
+ NumArrayBacking::Packed(v) => Some(v),
443
+ NumArrayBacking::Boxed(_) => None,
444
+ }
445
+ }
446
+ /// The boxed slice, iff already deopted.
447
+ #[inline]
448
+ pub fn as_boxed(&self) -> Option<&Vec<Value>> {
449
+ match self {
450
+ NumArrayBacking::Boxed(v) => Some(v),
451
+ NumArrayBacking::Packed(_) => None,
452
+ }
453
+ }
454
+ /// Upgrade to a boxed backing **in place** (idempotent) and return `&mut` to the boxed `Vec`.
455
+ /// Every alias sharing this cell now sees a boxed array (#506). Packed `f64`s become
456
+ /// `Number(n)` (NaN preserved), so a read after deopt matches what the packed form returned.
457
+ #[inline]
458
+ pub fn deopt(&mut self) -> &mut Vec<Value> {
459
+ if let NumArrayBacking::Packed(v) = self {
460
+ let boxed = v.iter().map(|n| Value::Number(*n)).collect();
461
+ *self = NumArrayBacking::Boxed(boxed);
462
+ }
463
+ match self {
464
+ NumArrayBacking::Boxed(v) => v,
465
+ NumArrayBacking::Packed(_) => unreachable!("just deopted"),
466
+ }
467
+ }
468
+ }
469
+
347
470
  #[derive(Clone, Default)]
348
471
  pub enum Value {
349
472
  Number(f64),
350
- String(arcstr::ArcStr),
473
+ String(ArcStr),
351
474
  Bool(bool),
352
475
  #[default]
353
476
  Null,
354
477
  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>>),
478
+ /// Numeric-origin array — `TISH_PACKED_ARRAYS` mode only. Starts `Packed` (a `Vec<f64>`, no
479
+ /// per-element boxing; enables direct `sort_unstable_by` and unboxed folds). A mutation that
480
+ /// introduces a **non-numeric** value ([`NumArrayBacking::deopt`]) upgrades the backing to
481
+ /// `Boxed` IN PLACE, so every alias sharing this `VmRef` observes the transition (#506) — the
482
+ /// value stays a `NumberArray` (aliases can't be rewritten to `Array`), but its backing is now
483
+ /// a `Vec<Value>` and it behaves exactly like a boxed array. So the tag means "numeric-origin,
484
+ /// possibly deopted"; access goes through the [`NumArrayBacking`] accessors, never a raw
485
+ /// `Vec<f64>` assumption. Created by all-numeric array literals, `new Array(n)` (zero-filled),
486
+ /// numeric HOF results, and typed arrays. Never created when `packed_arrays_enabled()` is
487
+ /// false — callers check before constructing.
488
+ NumberArray(VmRef<NumArrayBacking>),
361
489
  Object(VmRef<ObjectData>),
362
490
  /// ECMAScript-style primitive symbol (identity by `Arc`).
363
491
  Symbol(Arc<TishSymbol>),
@@ -368,8 +496,24 @@ pub enum Value {
368
496
  Promise(Arc<dyn TishPromise>),
369
497
  /// Opaque handle to a native Rust type (e.g. Polars DataFrame).
370
498
  Opaque(Arc<dyn TishOpaque>),
499
+ /// A typed native struct carried BY REFERENCE (portable / GBA native codegen). Lets a boxed
500
+ /// function return or pass a typed struct without serialising it to a hashmap `Object` —
501
+ /// property access dispatches through [`TishStruct`] (a small match, no hash lookup / alloc), and
502
+ /// it materialises to an `Object` only on JSON / display / `Object.keys`-family / `for..in` /
503
+ /// `in` / `Object.assign` / `{...s}` spread. Built via
504
+ /// [`Value::from_struct`]. Portable-only: the ref is a single-threaded `Rc<RefCell<dyn …>>` and
505
+ /// `portable` excludes `send-values`, so this never violates `Send` (which off-portable `Value`
506
+ /// may need). A fat pointer is 16 B, so the 24 B `Value` size guard still holds.
507
+ #[cfg(feature = "portable")]
508
+ Struct(StructRef),
371
509
  }
372
510
 
511
+ /// Reference to a boxed typed struct: `Rc<RefCell<dyn TishStruct>>`. Portable-only (see [`TishStruct`]
512
+ /// / [`Value::Struct`]). `RefCell` is OUTSIDE the trait object so `set_prop` can borrow it mutably; the
513
+ /// `Rc<RefCell<Concrete>> → Rc<RefCell<dyn TishStruct>>` unsizing is a stable coercion (no nightly).
514
+ #[cfg(feature = "portable")]
515
+ pub type StructRef = alloc::rc::Rc<core::cell::RefCell<dyn TishStruct>>;
516
+
373
517
  // Size guard. `Value` is 24 bytes: `String` is thin (`ArcStr`, 8B), but `Function`/`Promise`/`Opaque`
374
518
  // are fat `Arc<dyn …>` (data+vtable, 16B) ⇒ 16B payload + discriminant = 24.
375
519
  //
@@ -381,7 +525,7 @@ pub enum Value {
381
525
  // re-attempt the box trick; only a dispatch-level change (e.g. NaN-box's branch-free tag test, not
382
526
  // its size) could pay off. Gated to 64-bit: wasm32 (wasi) has 32-bit pointers, so size differs there.
383
527
  #[cfg(target_pointer_width = "64")]
384
- const _: () = assert!(std::mem::size_of::<Value>() == 24);
528
+ const _: () = assert!(core::mem::size_of::<Value>() == 24);
385
529
 
386
530
  // ─────────────────────────────────────────────────────────────────────────────
387
531
  // #201 Stage A — representation abstraction (behavior-preserving, zero perf effect).
@@ -414,6 +558,8 @@ pub enum ValueTag {
414
558
  RegExp,
415
559
  Promise,
416
560
  Opaque,
561
+ #[cfg(feature = "portable")]
562
+ Struct,
417
563
  }
418
564
 
419
565
  /// Borrowed view of a [`Value`]'s payload, mirroring the enum variants. Lets call sites
@@ -422,11 +568,11 @@ pub enum ValueTag {
422
568
  /// sites. Zero-cost — each arm borrows the existing payload in place.
423
569
  pub enum ValueRef<'a> {
424
570
  Number(f64),
425
- String(&'a arcstr::ArcStr),
571
+ String(&'a ArcStr),
426
572
  Bool(bool),
427
573
  Null,
428
574
  Array(&'a VmRef<Vec<Value>>),
429
- NumberArray(&'a VmRef<Vec<f64>>),
575
+ NumberArray(&'a VmRef<NumArrayBacking>),
430
576
  Object(&'a VmRef<ObjectData>),
431
577
  Symbol(&'a Arc<TishSymbol>),
432
578
  Function(&'a NativeFn),
@@ -434,6 +580,8 @@ pub enum ValueRef<'a> {
434
580
  RegExp(&'a VmRef<TishRegExp>),
435
581
  Promise(&'a Arc<dyn TishPromise>),
436
582
  Opaque(&'a Arc<dyn TishOpaque>),
583
+ #[cfg(feature = "portable")]
584
+ Struct(&'a StructRef),
437
585
  }
438
586
 
439
587
  impl Value {
@@ -452,7 +600,7 @@ impl Value {
452
600
  /// Named constructor for a string (#201 abstraction). Accepts anything that converts
453
601
  /// into the interned `ArcStr` (`&str`, `String`, `ArcStr`).
454
602
  #[inline]
455
- pub fn string(s: impl Into<arcstr::ArcStr>) -> Self {
603
+ pub fn string(s: impl Into<ArcStr>) -> Self {
456
604
  Value::String(s.into())
457
605
  }
458
606
 
@@ -479,6 +627,8 @@ impl Value {
479
627
  Value::RegExp(r) => ValueRef::RegExp(r),
480
628
  Value::Promise(p) => ValueRef::Promise(p),
481
629
  Value::Opaque(o) => ValueRef::Opaque(o),
630
+ #[cfg(feature = "portable")]
631
+ Value::Struct(s) => ValueRef::Struct(s),
482
632
  }
483
633
  }
484
634
 
@@ -499,6 +649,8 @@ impl Value {
499
649
  Value::RegExp(_) => ValueTag::RegExp,
500
650
  Value::Promise(_) => ValueTag::Promise,
501
651
  Value::Opaque(_) => ValueTag::Opaque,
652
+ #[cfg(feature = "portable")]
653
+ Value::Struct(_) => ValueTag::Struct,
502
654
  }
503
655
  }
504
656
 
@@ -540,7 +692,7 @@ impl Value {
540
692
 
541
693
  /// Borrow the packed-number-array payload, if this is a `NumberArray`.
542
694
  #[inline]
543
- pub fn as_number_array(&self) -> Option<&VmRef<Vec<f64>>> {
695
+ pub fn as_number_array(&self) -> Option<&VmRef<NumArrayBacking>> {
544
696
  match self {
545
697
  Value::NumberArray(a) => Some(a),
546
698
  _ => None,
@@ -571,7 +723,7 @@ const PROPMAP_INLINE: usize = 8;
571
723
  #[derive(Clone, Debug, Default)]
572
724
  pub struct PropMap {
573
725
  inline: SmallVec<[(Arc<str>, Value); PROPMAP_INLINE]>,
574
- map: Option<Box<IndexMap<Arc<str>, Value, ahash::RandomState>>>,
726
+ map: Option<Box<IndexMap<Arc<str>, Value, RandomState>>>,
575
727
  /// Hidden-class identity for this object's ordered key-set (JSC Structure). `EMPTY_SHAPE` (0)
576
728
  /// for `{}`. Maintained by `insert` (new key → `shape::transition`) and reset to `DICT_SHAPE` by
577
729
  /// `remove`. Lets the VM's inline caches compare a `u32` instead of hashing a key. INVARIANT: a
@@ -592,7 +744,7 @@ impl PropMap {
592
744
  inline: SmallVec::new(),
593
745
  map: Some(Box::new(IndexMap::with_capacity_and_hasher(
594
746
  n,
595
- ahash::RandomState::default(),
747
+ RandomState::default(),
596
748
  ))),
597
749
  shape: crate::shape::EMPTY_SHAPE,
598
750
  }
@@ -693,15 +845,15 @@ impl PropMap {
693
845
  }
694
846
  if let Some(slot) = self.inline.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref()) {
695
847
  // Update existing key → value changes, layout (shape) does not.
696
- return Some(std::mem::replace(&mut slot.1, val));
848
+ return Some(core::mem::replace(&mut slot.1, val));
697
849
  }
698
850
  // New key (inline storage) → transition the shape away from the current one.
699
851
  self.shape = crate::shape::transition(self.shape, &key);
700
852
  if self.inline.len() >= PROPMAP_INLINE {
701
853
  // Promote inline storage to an insertion-ordered map (keys + their order are preserved,
702
854
  // 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());
855
+ let mut m: IndexMap<Arc<str>, Value, RandomState> =
856
+ IndexMap::with_capacity_and_hasher(self.inline.len() + 1, RandomState::default());
705
857
  for (k, v) in self.inline.drain(..) {
706
858
  m.insert(k, v);
707
859
  }
@@ -778,9 +930,9 @@ impl PropMap {
778
930
  // If the combined worst-case size escapes inline storage, promote once up front so the
779
931
  // per-key inserts below never reallocate or re-promote.
780
932
  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(
933
+ let mut m: IndexMap<Arc<str>, Value, RandomState> = IndexMap::with_capacity_and_hasher(
782
934
  self.inline.len() + incoming,
783
- ahash::RandomState::default(),
935
+ RandomState::default(),
784
936
  );
785
937
  for (k, v) in self.inline.drain(..) {
786
938
  m.insert(k, v);
@@ -826,7 +978,7 @@ impl IntoIterator for PropMap {
826
978
 
827
979
  /// Zero-allocation borrowing iterator over [`PropMap`] entries (insertion order).
828
980
  pub enum PropMapIter<'a> {
829
- Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
981
+ Inline(core::slice::Iter<'a, (Arc<str>, Value)>),
830
982
  Map(indexmap::map::Iter<'a, Arc<str>, Value>),
831
983
  }
832
984
  impl<'a> Iterator for PropMapIter<'a> {
@@ -842,7 +994,7 @@ impl<'a> Iterator for PropMapIter<'a> {
842
994
 
843
995
  /// Zero-allocation key iterator over [`PropMap`] (insertion order).
844
996
  pub enum PropMapKeys<'a> {
845
- Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
997
+ Inline(core::slice::Iter<'a, (Arc<str>, Value)>),
846
998
  Map(indexmap::map::Keys<'a, Arc<str>, Value>),
847
999
  }
848
1000
  impl<'a> Iterator for PropMapKeys<'a> {
@@ -858,7 +1010,7 @@ impl<'a> Iterator for PropMapKeys<'a> {
858
1010
 
859
1011
  /// Zero-allocation value iterator over [`PropMap`] (insertion order).
860
1012
  pub enum PropMapValues<'a> {
861
- Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
1013
+ Inline(core::slice::Iter<'a, (Arc<str>, Value)>),
862
1014
  Map(indexmap::map::Values<'a, Arc<str>, Value>),
863
1015
  }
864
1016
  impl<'a> Iterator for PropMapValues<'a> {
@@ -894,6 +1046,10 @@ impl Iterator for PropMapIntoIter {
894
1046
  pub struct ObjectData {
895
1047
  pub strings: PropMap,
896
1048
  pub symbols: Option<AHashMap<u64, Value>>,
1049
+ /// `Object.freeze` marker (#437). When true, property writes must throw a catchable TypeError
1050
+ /// instead of mutating — enforced at every write chokepoint (interp assign, vm `set_member`,
1051
+ /// native `set_prop`). Reference-shared like the rest of the object; a fresh object is not frozen.
1052
+ pub frozen: bool,
897
1053
  }
898
1054
 
899
1055
  impl ObjectData {
@@ -902,9 +1058,16 @@ impl ObjectData {
902
1058
  Self {
903
1059
  strings: strings.into_iter().collect(),
904
1060
  symbols: None,
1061
+ frozen: false,
905
1062
  }
906
1063
  }
907
1064
 
1065
+ /// True if `Object.freeze` has been applied to this object.
1066
+ #[inline]
1067
+ pub fn is_frozen(&self) -> bool {
1068
+ self.frozen
1069
+ }
1070
+
908
1071
  #[inline]
909
1072
  pub fn len_entries(&self) -> usize {
910
1073
  self.strings.len() + self.symbols.as_ref().map(|s| s.len()).unwrap_or(0)
@@ -1084,15 +1247,15 @@ pub fn value_call(callee: &Value, args: &[Value]) -> Value {
1084
1247
  return value_call(&inner, args);
1085
1248
  }
1086
1249
  crate::set_pending_throw(crate::not_a_function_error(format!(
1087
- "{} is not a function",
1088
- callee.to_display_string()
1250
+ "Call of non-function: {}",
1251
+ callee.type_name()
1089
1252
  )));
1090
1253
  Value::Null
1091
1254
  }
1092
1255
  _ => {
1093
1256
  crate::set_pending_throw(crate::not_a_function_error(format!(
1094
- "{} is not a function",
1095
- callee.to_display_string()
1257
+ "Call of non-function: {}",
1258
+ callee.type_name()
1096
1259
  )));
1097
1260
  Value::Null
1098
1261
  }
@@ -1118,11 +1281,15 @@ pub fn merge_object_data(left: &VmRef<ObjectData>, right: &VmRef<ObjectData>) ->
1118
1281
  None => symbols = Some(rs.clone()),
1119
1282
  }
1120
1283
  }
1121
- ObjectData { strings, symbols }
1284
+ ObjectData {
1285
+ strings,
1286
+ symbols,
1287
+ frozen: false,
1288
+ }
1122
1289
  }
1123
1290
 
1124
- impl std::fmt::Debug for Value {
1125
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1291
+ impl core::fmt::Debug for Value {
1292
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1126
1293
  match self {
1127
1294
  Value::Number(n) => write!(f, "Number({})", n),
1128
1295
  Value::String(s) => write!(f, "String({:?})", s.as_str()),
@@ -1142,6 +1309,8 @@ impl std::fmt::Debug for Value {
1142
1309
  ),
1143
1310
  Value::Promise(_) => write!(f, "Promise"),
1144
1311
  Value::Opaque(o) => write!(f, "{}(opaque)", o.type_name()),
1312
+ #[cfg(feature = "portable")]
1313
+ Value::Struct(s) => write!(f, "Struct({})", s.borrow().tish_type_name()),
1145
1314
  }
1146
1315
  }
1147
1316
  }
@@ -1242,7 +1411,7 @@ pub fn js_number_to_string_into(out: &mut String, value: f64) {
1242
1411
  end -= 1;
1243
1412
  }
1244
1413
  let trail = (n - end) as i32;
1245
- let digits = std::str::from_utf8(&digits_buf[lead..end]).expect("ascii digits");
1414
+ let digits = core::str::from_utf8(&digits_buf[lead..end]).expect("ascii digits");
1246
1415
  let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
1247
1416
  let point = k + exp - frac_len + trail;
1248
1417
 
@@ -1318,8 +1487,12 @@ impl Value {
1318
1487
  Value::NumberArray(arr) => {
1319
1488
  let inner: Vec<String> = arr
1320
1489
  .borrow()
1490
+ .to_values()
1321
1491
  .iter()
1322
- .map(|&n| if n.is_nan() { "null".to_string() } else { Value::Number(n).to_display_string() })
1492
+ .map(|v| match v {
1493
+ Value::Number(n) if n.is_nan() => "null".to_string(),
1494
+ other => other.to_display_string(),
1495
+ })
1323
1496
  .collect();
1324
1497
  format!("[{}]", inner.join(", "))
1325
1498
  }
@@ -1348,6 +1521,9 @@ impl Value {
1348
1521
  Value::Function(_) => "[Function]".to_string(),
1349
1522
  Value::Promise(_) => "[object Promise]".to_string(),
1350
1523
  Value::Opaque(o) => format!("[object {}]", o.type_name()),
1524
+ // Display a boxed struct like the object it is (materialise once, then reuse the Object path).
1525
+ #[cfg(feature = "portable")]
1526
+ Value::Struct(s) => s.borrow().tish_to_object().to_display_string_guarded(ancestors),
1351
1527
  #[cfg(feature = "regex")]
1352
1528
  Value::RegExp(re) => {
1353
1529
  let re = re.borrow();
@@ -1393,11 +1569,14 @@ impl Value {
1393
1569
  }
1394
1570
  Value::NumberArray(arr) => arr
1395
1571
  .borrow()
1572
+ .to_values()
1396
1573
  .iter()
1397
- .map(|n| Value::Number(*n).to_js_string())
1574
+ .map(|v| v.to_js_string())
1398
1575
  .collect::<Vec<_>>()
1399
1576
  .join(","),
1400
1577
  Value::Object(_) => "[object Object]".to_string(),
1578
+ #[cfg(feature = "portable")]
1579
+ Value::Struct(_) => "[object Object]".to_string(),
1401
1580
  // ECMAScript ToString of a number (drops `-0`'s sign), distinct from the inspect form
1402
1581
  // that `to_display_string` would give for `-0`. (#247)
1403
1582
  Value::Number(n) => js_number_to_string(*n),
@@ -1436,12 +1615,16 @@ impl Value {
1436
1615
  #[cfg(feature = "send-values")]
1437
1616
  (Value::Function(a), Value::Function(b)) => Arc::ptr_eq(a, b),
1438
1617
  #[cfg(not(feature = "send-values"))]
1439
- (Value::Function(a), Value::Function(b)) => std::rc::Rc::ptr_eq(a, b),
1618
+ (Value::Function(a), Value::Function(b)) => alloc::rc::Rc::ptr_eq(a, b),
1440
1619
  #[cfg(feature = "regex")]
1441
1620
  (Value::RegExp(a), Value::RegExp(b)) => VmRef::ptr_eq(a, b),
1442
1621
  (Value::Promise(a), Value::Promise(b)) => Arc::ptr_eq(a, b),
1443
1622
  (Value::Opaque(a), Value::Opaque(b)) => Arc::ptr_eq(a, b),
1444
1623
  (Value::Symbol(a), Value::Symbol(b)) => Arc::ptr_eq(a, b),
1624
+ // Struct-Value identity is `Rc` pointer equality, exactly like `Object` — two distinct
1625
+ // boxings compare unequal, but a struct aliased through one `Value` is `=== ` itself.
1626
+ #[cfg(feature = "portable")]
1627
+ (Value::Struct(a), Value::Struct(b)) => alloc::rc::Rc::ptr_eq(a, b),
1445
1628
  _ => false,
1446
1629
  }
1447
1630
  }
@@ -1465,7 +1648,7 @@ impl Value {
1465
1648
  where
1466
1649
  F: Fn(&[Value]) -> Value + 'static,
1467
1650
  {
1468
- Value::Function(std::rc::Rc::new(FnCallable(f)))
1651
+ Value::Function(alloc::rc::Rc::new(FnCallable(f)))
1469
1652
  }
1470
1653
 
1471
1654
  /// Create a new array Value from a Vec.
@@ -1490,9 +1673,67 @@ impl Value {
1490
1673
  Value::Object(VmRef::new(ObjectData {
1491
1674
  strings,
1492
1675
  symbols: None,
1676
+ frozen: false,
1493
1677
  }))
1494
1678
  }
1495
1679
 
1680
+ /// Box a typed native struct (a generated `TishStruct_*`). On portable (GBA) it is kept BY
1681
+ /// REFERENCE as a cheap [`Value::Struct`] — one `Rc` allocation, no per-field string keys / hashmap;
1682
+ /// property access later dispatches through [`TishStruct`]. On every other backend it serialises to
1683
+ /// an ordered `Value::Object` (via [`TishStruct::tish_to_object`]) — byte-identical to the historical
1684
+ /// `object_from_pairs` boundary, so off-GBA behaviour is unchanged. Codegen's struct-to-`Value`
1685
+ /// boxing emits a single `Value::from_struct(..)` call for all native targets.
1686
+ #[cfg(feature = "portable")]
1687
+ #[inline]
1688
+ pub fn from_struct<T: TishStruct + 'static>(v: T) -> Value {
1689
+ Value::Struct(alloc::rc::Rc::new(core::cell::RefCell::new(v)))
1690
+ }
1691
+ /// See the portable variant above — off-GBA this is exactly the old `object_from_pairs` boundary.
1692
+ #[cfg(not(feature = "portable"))]
1693
+ #[inline]
1694
+ pub fn from_struct<T: TishStruct>(v: T) -> Value {
1695
+ v.tish_to_object()
1696
+ }
1697
+
1698
+ /// Box a typed native struct BY SHARED REFERENCE — the boxing path codegen actually emits, so the
1699
+ /// whole-struct clone is paid ONLY where it's genuinely needed. On portable (GBA) the struct must
1700
+ /// be OWNED by the `Rc`, so it clones (identical to `from_struct((v).clone())`). Off-GBA it
1701
+ /// serialises the borrow directly via [`TishStruct::tish_to_object`] with NO whole-struct clone —
1702
+ /// byte-identical to the historical `object_from_pairs` boundary that read fields straight off the
1703
+ /// place (#565: the old `from_struct((v).clone())` cloned the entire struct just to serialise it).
1704
+ #[cfg(feature = "portable")]
1705
+ #[inline]
1706
+ pub fn from_struct_ref<T: TishStruct + Clone + 'static>(v: &T) -> Value {
1707
+ Value::Struct(alloc::rc::Rc::new(core::cell::RefCell::new(v.clone())))
1708
+ }
1709
+ /// See the portable variant — off-GBA this serialises the borrow with no whole-struct clone.
1710
+ #[cfg(not(feature = "portable"))]
1711
+ #[inline]
1712
+ pub fn from_struct_ref<T: TishStruct>(v: &T) -> Value {
1713
+ v.tish_to_object()
1714
+ }
1715
+
1716
+ /// If this value is a by-reference boxed struct of concrete type `T` (a `Value::Struct`), return a
1717
+ /// CLONE of it — the native fast path for `let x: T = <boxed struct return>`, so subsequent field
1718
+ /// reads are plain Rust loads instead of `get_prop`. Portable only: off-GBA there is no `Value::Struct`
1719
+ /// variant, so this is always `None` and codegen's `from_value_expr` falls back to the field-by-field
1720
+ /// `get_prop` rebuild (which also handles a genuine `Value::Object`). Returns `None` for any non-struct
1721
+ /// value or a struct of a different concrete type.
1722
+ #[cfg(feature = "portable")]
1723
+ #[inline]
1724
+ pub fn downcast_struct<T: TishStruct + Clone + 'static>(&self) -> Option<T> {
1725
+ match self {
1726
+ Value::Struct(s) => s.borrow().as_any().downcast_ref::<T>().cloned(),
1727
+ _ => None,
1728
+ }
1729
+ }
1730
+ /// See the portable variant — off-GBA there is no struct variant, so always `None`.
1731
+ #[cfg(not(feature = "portable"))]
1732
+ #[inline]
1733
+ pub fn downcast_struct<T: TishStruct + Clone + 'static>(&self) -> Option<T> {
1734
+ None
1735
+ }
1736
+
1496
1737
  /// Create an empty array Value.
1497
1738
  pub fn empty_array() -> Self {
1498
1739
  Value::Array(VmRef::new(Vec::new()))
@@ -1509,6 +1750,7 @@ impl Value {
1509
1750
  /// process starts (as the CI sweep does); mid-process toggling is not observed by design.
1510
1751
  /// The flag is intentionally backwards from the slot/JIT flags (those were default-on) to
1511
1752
  /// keep the default binary behaviour byte-identical while we validate coverage.
1753
+ #[cfg(not(feature = "portable"))]
1512
1754
  #[inline]
1513
1755
  pub fn packed_arrays_enabled() -> bool {
1514
1756
  static PACKED_ARRAYS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
@@ -1517,10 +1759,18 @@ impl Value {
1517
1759
  })
1518
1760
  }
1519
1761
 
1762
+ /// Packed f64 arrays are off under `portable` (no env to opt in; keeps the
1763
+ /// generic array path, which the GBA runtime already exercises).
1764
+ #[cfg(feature = "portable")]
1765
+ #[inline]
1766
+ pub fn packed_arrays_enabled() -> bool {
1767
+ false
1768
+ }
1769
+
1520
1770
  /// Wrap a `Vec<f64>` as a `Value::NumberArray`. Only call when `packed_arrays_enabled()`.
1521
1771
  #[inline]
1522
1772
  pub fn number_array(items: Vec<f64>) -> Self {
1523
- Value::NumberArray(VmRef::new(items))
1773
+ Value::NumberArray(VmRef::new(NumArrayBacking::Packed(items)))
1524
1774
  }
1525
1775
 
1526
1776
  /// Materialize a `Value::NumberArray` into a boxed `Value::Array`.
@@ -1529,9 +1779,8 @@ impl Value {
1529
1779
  /// converts once and continues on the generic path. The original `NumberArray` VmRef
1530
1780
  /// is consumed; callers replace the `Value` in whatever container held it.
1531
1781
  #[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()))
1782
+ pub fn materialize_number_array(arr: &VmRef<NumArrayBacking>) -> Value {
1783
+ Value::Array(VmRef::new(arr.borrow().to_values()))
1535
1784
  }
1536
1785
 
1537
1786
  /// If `self` is a `NumberArray`, materialise and return `Value::Array`; otherwise
@@ -1572,6 +1821,8 @@ impl Value {
1572
1821
  Value::Promise(_) => "object",
1573
1822
  Value::Opaque(o) => o.type_name(),
1574
1823
  Value::Symbol(_) => "symbol",
1824
+ #[cfg(feature = "portable")]
1825
+ Value::Struct(_) => "object",
1575
1826
  }
1576
1827
  }
1577
1828