@tishlang/tish-format 2.43.8 → 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 (35) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
  3. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
  4. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
  5. package/crates/tish/tests/regr_typed_externs.rs +59 -0
  6. package/crates/tish_builtins/src/globals.rs +27 -0
  7. package/crates/tish_compile/src/codegen.rs +1285 -116
  8. package/crates/tish_compile/src/resolve.rs +53 -7
  9. package/crates/tish_compile/src/schemes.rs +60 -5
  10. package/crates/tish_compile/src/types.rs +64 -32
  11. package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
  12. package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
  13. package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
  14. package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
  15. package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
  16. package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
  17. package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
  18. package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
  19. package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
  20. package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
  21. package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
  22. package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
  23. package/crates/tish_core/src/console_style.rs +4 -0
  24. package/crates/tish_core/src/json.rs +8 -0
  25. package/crates/tish_core/src/value.rs +123 -0
  26. package/crates/tish_fmt/Cargo.toml +1 -1
  27. package/crates/tish_runtime/src/lib.rs +1 -0
  28. package/crates/tish_runtime_gba/src/lib.rs +188 -4
  29. package/crates/tish_vm/src/vm.rs +48 -0
  30. package/package.json +1 -1
  31. package/platform/darwin-arm64/tish-fmt +0 -0
  32. package/platform/darwin-x64/tish-fmt +0 -0
  33. package/platform/linux-arm64/tish-fmt +0 -0
  34. package/platform/linux-x64/tish-fmt +0 -0
  35. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -165,6 +165,35 @@ pub trait TishOpaque {
165
165
  fn as_any(&self) -> &dyn core::any::Any;
166
166
  }
167
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;
195
+ }
196
+
168
197
  /// Trait for Promise-like values that can be awaited (block until settled).
169
198
  /// Implemented by the runtime for native compile; interpreter uses its own Promise.
170
199
  pub trait TishPromise: Send + Sync {
@@ -467,8 +496,24 @@ pub enum Value {
467
496
  Promise(Arc<dyn TishPromise>),
468
497
  /// Opaque handle to a native Rust type (e.g. Polars DataFrame).
469
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),
470
509
  }
471
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
+
472
517
  // Size guard. `Value` is 24 bytes: `String` is thin (`ArcStr`, 8B), but `Function`/`Promise`/`Opaque`
473
518
  // are fat `Arc<dyn …>` (data+vtable, 16B) ⇒ 16B payload + discriminant = 24.
474
519
  //
@@ -513,6 +558,8 @@ pub enum ValueTag {
513
558
  RegExp,
514
559
  Promise,
515
560
  Opaque,
561
+ #[cfg(feature = "portable")]
562
+ Struct,
516
563
  }
517
564
 
518
565
  /// Borrowed view of a [`Value`]'s payload, mirroring the enum variants. Lets call sites
@@ -533,6 +580,8 @@ pub enum ValueRef<'a> {
533
580
  RegExp(&'a VmRef<TishRegExp>),
534
581
  Promise(&'a Arc<dyn TishPromise>),
535
582
  Opaque(&'a Arc<dyn TishOpaque>),
583
+ #[cfg(feature = "portable")]
584
+ Struct(&'a StructRef),
536
585
  }
537
586
 
538
587
  impl Value {
@@ -578,6 +627,8 @@ impl Value {
578
627
  Value::RegExp(r) => ValueRef::RegExp(r),
579
628
  Value::Promise(p) => ValueRef::Promise(p),
580
629
  Value::Opaque(o) => ValueRef::Opaque(o),
630
+ #[cfg(feature = "portable")]
631
+ Value::Struct(s) => ValueRef::Struct(s),
581
632
  }
582
633
  }
583
634
 
@@ -598,6 +649,8 @@ impl Value {
598
649
  Value::RegExp(_) => ValueTag::RegExp,
599
650
  Value::Promise(_) => ValueTag::Promise,
600
651
  Value::Opaque(_) => ValueTag::Opaque,
652
+ #[cfg(feature = "portable")]
653
+ Value::Struct(_) => ValueTag::Struct,
601
654
  }
602
655
  }
603
656
 
@@ -1256,6 +1309,8 @@ impl core::fmt::Debug for Value {
1256
1309
  ),
1257
1310
  Value::Promise(_) => write!(f, "Promise"),
1258
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()),
1259
1314
  }
1260
1315
  }
1261
1316
  }
@@ -1466,6 +1521,9 @@ impl Value {
1466
1521
  Value::Function(_) => "[Function]".to_string(),
1467
1522
  Value::Promise(_) => "[object Promise]".to_string(),
1468
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),
1469
1527
  #[cfg(feature = "regex")]
1470
1528
  Value::RegExp(re) => {
1471
1529
  let re = re.borrow();
@@ -1517,6 +1575,8 @@ impl Value {
1517
1575
  .collect::<Vec<_>>()
1518
1576
  .join(","),
1519
1577
  Value::Object(_) => "[object Object]".to_string(),
1578
+ #[cfg(feature = "portable")]
1579
+ Value::Struct(_) => "[object Object]".to_string(),
1520
1580
  // ECMAScript ToString of a number (drops `-0`'s sign), distinct from the inspect form
1521
1581
  // that `to_display_string` would give for `-0`. (#247)
1522
1582
  Value::Number(n) => js_number_to_string(*n),
@@ -1561,6 +1621,10 @@ impl Value {
1561
1621
  (Value::Promise(a), Value::Promise(b)) => Arc::ptr_eq(a, b),
1562
1622
  (Value::Opaque(a), Value::Opaque(b)) => Arc::ptr_eq(a, b),
1563
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),
1564
1628
  _ => false,
1565
1629
  }
1566
1630
  }
@@ -1613,6 +1677,63 @@ impl Value {
1613
1677
  }))
1614
1678
  }
1615
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
+
1616
1737
  /// Create an empty array Value.
1617
1738
  pub fn empty_array() -> Self {
1618
1739
  Value::Array(VmRef::new(Vec::new()))
@@ -1700,6 +1821,8 @@ impl Value {
1700
1821
  Value::Promise(_) => "object",
1701
1822
  Value::Opaque(o) => o.type_name(),
1702
1823
  Value::Symbol(_) => "symbol",
1824
+ #[cfg(feature = "portable")]
1825
+ Value::Struct(_) => "object",
1703
1826
  }
1704
1827
  }
1705
1828
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_fmt"
3
- version = "2.43.8"
3
+ version = "3.0.0"
4
4
  edition = "2021"
5
5
  description = "Opinionated formatter for Tish source (parse → pretty-print)"
6
6
  license-file = { workspace = true }
@@ -16,6 +16,7 @@ pub use tishlang_core::ObjectMap;
16
16
  // pass) instead of materializing an intermediate `AHashMap` and rebuilding from it.
17
17
  pub use tishlang_core::{ObjectData, PropMap};
18
18
  pub use tishlang_core::Value;
19
+ pub use tishlang_core::TishStruct;
19
20
  pub use tishlang_core::NumArrayBacking;
20
21
  pub use tishlang_core::ArcStr;
21
22
  /// Used by native codegen for `f()` / `obj()` dispatch (`Value::Function` or `__call` on objects).
@@ -17,8 +17,8 @@ use alloc::vec::Vec;
17
17
  // ── Core value vocabulary (single source of truth in tishlang_core) ──────────
18
18
  pub use tishlang_core::{
19
19
  js_number_to_string_into, to_int32, to_int32_value, to_number_value, to_uint32,
20
- to_uint32_value, ArcStr, NumArrayBacking, ObjectData, ObjectMap, PropMap, Value, VmReadGuard,
21
- VmRef, VmWriteGuard,
20
+ to_uint32_value, ArcStr, NumArrayBacking, ObjectData, ObjectMap, PropMap, TishStruct, Value,
21
+ VmReadGuard, VmRef, VmWriteGuard,
22
22
  };
23
23
  /// `Arc` on GBA is `Rc` (single-core). Emitted code writes `Arc::from(..)`.
24
24
  pub use tishlang_core::Arc;
@@ -112,10 +112,32 @@ pub fn in_operator(key: &Value, obj: &Value) -> Value {
112
112
  Value::Object(_) => Value::Bool(tishlang_core::object_has(obj, key)),
113
113
  Value::Array(arr) => Value::Bool(array_in(key, arr.borrow().len())),
114
114
  Value::NumberArray(arr) => Value::Bool(array_in(key, arr.borrow().len())),
115
+ // A by-reference boxed typed struct: `"field" in s` materialises to the struct's object and
116
+ // asks it, so a boxed struct answers `in` like the object it stands for (was always `false`).
117
+ Value::Struct(s) => {
118
+ Value::Bool(tishlang_core::object_has(&s.borrow().tish_to_object(), key))
119
+ }
115
120
  _ => Value::Bool(false),
116
121
  }
117
122
  }
118
123
 
124
+ /// Spread source for `{ ...v }` (GBA emit only — codegen calls this from the Gba object-spread
125
+ /// lowering). Returns the source's own string-keyed props as an owned `PropMap`: a boxed typed struct
126
+ /// is materialised to its ordered object first, so `{ ...s }` carries the struct's fields instead of
127
+ /// dropping them; a non-object yields `None` (spread of `null`/a number contributes nothing, per JS).
128
+ /// Desktop emit keeps its inline `if let Value::Object` path, so this GBA-only helper adds no
129
+ /// non-GBA cost.
130
+ pub fn object_spread_props(v: &Value) -> Option<PropMap> {
131
+ match v {
132
+ Value::Object(o) => Some(o.borrow().strings.clone()),
133
+ Value::Struct(s) => match s.borrow().tish_to_object() {
134
+ Value::Object(o) => Some(o.borrow().strings.clone()),
135
+ _ => None,
136
+ },
137
+ _ => None,
138
+ }
139
+ }
140
+
119
141
  fn array_in(key: &Value, len: usize) -> bool {
120
142
  let key_str: Arc<str> = match key {
121
143
  Value::String(s) => Arc::from(s.as_str()),
@@ -175,6 +197,9 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
175
197
  }
176
198
  }
177
199
  Value::Opaque(o) => o.get_method(key).map(Value::Function).unwrap_or(Value::Null),
200
+ // A boxed typed struct: read the field natively (a small match, no hashmap). `size`/method
201
+ // sugar isn't relevant here — structs are plain field bags.
202
+ Value::Struct(s) => s.borrow().tish_get(key),
178
203
  Value::Null => {
179
204
  tishlang_core::set_pending_throw(tishlang_core::cannot_read_property_error(key));
180
205
  Value::Null
@@ -222,6 +247,13 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
222
247
  _ => Value::Null,
223
248
  },
224
249
  Value::Object(_) => tishlang_core::object_get(obj, index).unwrap_or(Value::Null),
250
+ Value::Struct(s) => {
251
+ let key = match index {
252
+ Value::String(k) => k.to_string(),
253
+ other => other.to_js_string(),
254
+ };
255
+ s.borrow().tish_get(&key)
256
+ }
225
257
  Value::Null => {
226
258
  let key = match index {
227
259
  Value::String(s) => s.to_string(),
@@ -322,6 +354,10 @@ pub fn set_prop(obj: &Value, key: &str, val: Value) -> Value {
322
354
  }
323
355
  val
324
356
  }
357
+ Value::Struct(s) => {
358
+ s.borrow_mut().tish_set(key, val.clone());
359
+ val
360
+ }
325
361
  _ => {
326
362
  tishlang_core::set_pending_throw(tishlang_core::type_error("Cannot assign property on a non-object"));
327
363
  val
@@ -427,6 +463,44 @@ pub fn json_stringify(args: &[Value]) -> Value {
427
463
  Value::String(tishlang_core::json_stringify(&v).into())
428
464
  }
429
465
 
466
+ /// `json::{escape_into, write_json_number}` — the helpers codegen-emitted per-struct JSON serialisers
467
+ /// (`__tish_json_TishStruct_*`, #315) call. The std runtime exposes these under `tishlang_runtime::json`;
468
+ /// the GBA facade must mirror them so a game that uses a typed `interface`/`type` compiles. no_std.
469
+ pub mod json {
470
+ pub use tishlang_core::write_json_number;
471
+
472
+ /// Append the JSON-escaped contents of `s` (no surrounding quotes) to `buf`. Same escape rules as
473
+ /// the std runtime's `json::escape_into`; kept local (uses `core::fmt::Write` for `\uXXXX`).
474
+ pub fn escape_into(buf: &mut alloc::string::String, s: &str) {
475
+ use core::fmt::Write;
476
+ let bytes = s.as_bytes();
477
+ let mut start = 0usize;
478
+ for (i, &b) in bytes.iter().enumerate() {
479
+ if b < 0x20 || b == b'"' || b == b'\\' {
480
+ if start < i {
481
+ buf.push_str(&s[start..i]);
482
+ }
483
+ match b {
484
+ b'"' => buf.push_str("\\\""),
485
+ b'\\' => buf.push_str("\\\\"),
486
+ b'\n' => buf.push_str("\\n"),
487
+ b'\r' => buf.push_str("\\r"),
488
+ b'\t' => buf.push_str("\\t"),
489
+ b'\x08' => buf.push_str("\\b"),
490
+ b'\x0c' => buf.push_str("\\f"),
491
+ _ => {
492
+ let _ = write!(buf, "\\u{:04x}", b as u32);
493
+ }
494
+ }
495
+ start = i + 1;
496
+ }
497
+ }
498
+ if start < bytes.len() {
499
+ buf.push_str(&s[start..]);
500
+ }
501
+ }
502
+ }
503
+
430
504
  /// Build an `ObjectMap` from an array of key/value pairs. Generated object literals
431
505
  /// emit `ObjectMap::from([...])`; hashbrown's `From<[_; N]>` isn't available with a
432
506
  /// custom hasher, so the Gba post-pass rewrites those calls to this.
@@ -529,6 +603,58 @@ pub use tishlang_builtins::globals::{
529
603
  };
530
604
  pub use tishlang_builtins::string::escape_html as string_escape_html_impl;
531
605
 
606
+ // ── String methods (codegen lowers `.slice()`/`.indexOf()`/`.toLowerCase()`/… to these prelude
607
+ // names). Mirrors the std `tish_runtime` wrappers; the portable subset (no regex / unicode-normalize).
608
+ pub use tishlang_builtins::string::{
609
+ at as string_at_impl, char_at as string_char_at_impl,
610
+ char_code_at as string_char_code_at_impl, ends_with as string_ends_with_impl,
611
+ includes as string_includes_impl, index_of as string_index_of_impl,
612
+ slice as string_slice_impl, starts_with as string_starts_with_impl,
613
+ substr as string_substr_impl, substring as string_substring_impl,
614
+ to_lower_case as string_to_lower_case, to_upper_case as string_to_upper_case,
615
+ trim as string_trim, trim_end as string_trim_end, trim_start as string_trim_start,
616
+ };
617
+ #[inline]
618
+ pub fn string_index_of(s: &Value, search: &Value, from: &Value) -> Value {
619
+ string_index_of_impl(s, search, Some(from))
620
+ }
621
+ #[inline]
622
+ pub fn string_includes(s: &Value, search: &Value, from: &Value) -> Value {
623
+ string_includes_impl(s, search, Some(from))
624
+ }
625
+ #[inline]
626
+ pub fn string_slice(s: &Value, start: &Value, end: &Value) -> Value {
627
+ string_slice_impl(s, start, end)
628
+ }
629
+ #[inline]
630
+ pub fn string_substring(s: &Value, start: &Value, end: &Value) -> Value {
631
+ string_substring_impl(s, start, end)
632
+ }
633
+ #[inline]
634
+ pub fn string_substr(s: &Value, start: &Value, length: &Value) -> Value {
635
+ string_substr_impl(s, start, length)
636
+ }
637
+ #[inline]
638
+ pub fn string_starts_with(s: &Value, search: &Value, position: Option<&Value>) -> Value {
639
+ string_starts_with_impl(s, search, position)
640
+ }
641
+ #[inline]
642
+ pub fn string_ends_with(s: &Value, search: &Value, end_position: Option<&Value>) -> Value {
643
+ string_ends_with_impl(s, search, end_position)
644
+ }
645
+ #[inline]
646
+ pub fn string_char_at(s: &Value, idx: &Value) -> Value {
647
+ string_char_at_impl(s, idx)
648
+ }
649
+ #[inline]
650
+ pub fn string_at(s: &Value, idx: &Value) -> Value {
651
+ string_at_impl(s, idx)
652
+ }
653
+ #[inline]
654
+ pub fn string_char_code_at(s: &Value, idx: &Value) -> Value {
655
+ string_char_code_at_impl(s, idx)
656
+ }
657
+
532
658
  // ── Constructors, collections, typed arrays, symbol (re-export as prelude names) ──
533
659
  pub use tishlang_builtins::construct::{
534
660
  array_construct, audio_context_constructor_value as tish_audio_context_constructor,
@@ -539,8 +665,66 @@ pub use tishlang_builtins::collections::{
539
665
  map_constructor_value as tish_map_constructor, map_get, map_has, map_set, map_values,
540
666
  set_constructor_value as tish_set_constructor,
541
667
  };
542
- // Array mutators (portable) — `.push()` / `.pop()` lower to these `array_*` names.
543
- pub use tishlang_builtins::array::{pop as array_pop, push as array_push};
668
+ // Array methods — codegen lowers `.push()`/`.slice()`/`.map()`/… to these `array_*` names. Mirrors
669
+ // the std `tish_runtime` block (minus RNG-dependent `shuffle`). Portable: all operate on Vec<Value>.
670
+ pub use tishlang_builtins::array::{
671
+ at as array_at, concat as array_concat_impl, every as array_every, filter as array_filter,
672
+ find as array_find, find_index as array_find_index, find_last as array_find_last,
673
+ find_last_index as array_find_last_index, flat as array_flat_impl, flat_map as array_flat_map,
674
+ for_each as array_for_each, includes as array_includes_impl, index_of as array_index_of_impl,
675
+ join as array_join_impl, map as array_map, pop as array_pop, push as array_push_impl,
676
+ fill as array_fill, last_index_of as array_last_index_of, copy_within as array_copy_within,
677
+ reduce as array_reduce, reduce_right as array_reduce_right, reverse as array_reverse,
678
+ keys as array_keys, values as array_values, entries as array_entries, shift as array_shift,
679
+ slice as array_slice_impl, snapshot_values as array_snapshot_values,
680
+ as_f64_snapshot as array_as_f64_snapshot, some as array_some,
681
+ sort_by_keys as array_sort_by_keys, sort_default as array_sort_default,
682
+ sort_numeric_asc as array_sort_numeric_asc, sort_numeric_desc as array_sort_numeric_desc,
683
+ sort_with_comparator as array_sort_with_comparator, splice as array_splice_impl,
684
+ to_reversed as array_to_reversed, to_sorted as array_to_sorted,
685
+ to_spliced as array_to_spliced, with as array_with, unshift as array_unshift_impl,
686
+ };
687
+ #[inline]
688
+ pub fn array_push(arr: &Value, args: &[Value]) -> Value { array_push_impl(arr, args) }
689
+ #[inline]
690
+ pub fn array_unshift(arr: &Value, args: &[Value]) -> Value { array_unshift_impl(arr, args) }
691
+ #[inline]
692
+ pub fn array_index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
693
+ array_index_of_impl(arr, search, from)
694
+ }
695
+ #[inline]
696
+ pub fn array_includes(arr: &Value, search: &Value, from: &Value) -> Value {
697
+ array_includes_impl(arr, search, Some(from))
698
+ }
699
+ #[inline]
700
+ pub fn array_join(arr: &Value, sep: &Value) -> Value { array_join_impl(arr, sep) }
701
+ #[inline]
702
+ pub fn array_splice(arr: &Value, start: &Value, delete_count: Option<&Value>, items: &[Value]) -> Value {
703
+ array_splice_impl(arr, start, delete_count, items)
704
+ }
705
+ #[inline]
706
+ pub fn array_slice(arr: &Value, start: &Value, end: &Value) -> Value {
707
+ array_slice_impl(arr, start, end)
708
+ }
709
+ #[inline]
710
+ pub fn array_concat(arr: &Value, args: &[Value]) -> Value { array_concat_impl(arr, args) }
711
+ #[inline]
712
+ pub fn array_flat(arr: &Value, depth: &Value) -> Value { array_flat_impl(arr, depth) }
713
+ #[inline]
714
+ pub fn array_sort(arr: &Value, comparator: Option<&Value>) -> Value {
715
+ match comparator {
716
+ Some(cmp) => array_sort_with_comparator(arr, cmp),
717
+ None => array_sort_default(arr),
718
+ }
719
+ }
720
+ /// `.at(i)` dispatched on the runtime value — exists on both String and Array (#247).
721
+ #[inline]
722
+ pub fn value_at(recv: &Value, idx: &Value) -> Value {
723
+ match recv {
724
+ Value::String(_) => string_at_impl(recv, idx),
725
+ _ => array_at(recv, idx),
726
+ }
727
+ }
544
728
  pub use tishlang_builtins::symbol::symbol_object;
545
729
  pub use tishlang_builtins::typedarrays::{
546
730
  float32_array_constructor_value as tish_float32_array_constructor,
@@ -4093,6 +4093,54 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4093
4093
  Value::NumberArray(a2.clone())
4094
4094
  })
4095
4095
  }
4096
+ // #560: `copyWithin` MUTATES in place, but — unlike fill/reverse/splice — it had no
4097
+ // packed arm, so it fell to the `_ =>` materialise-to-boxed fallback below and copied
4098
+ // within a THROWAWAY boxed copy, leaving the packed array untouched (a silent no-op
4099
+ // that diverged from interp). copyWithin only relocates existing elements, so the
4100
+ // array stays packed: block-copy straight on the Vec<f64> (deopt only if already
4101
+ // boxed). Index clamping mirrors `arr_builtins::copy_within` (`normalize_index`).
4102
+ "copyWithin" => {
4103
+ let a2 = a_clone.clone();
4104
+ make_native_fn(move |args: &[Value]| {
4105
+ let len = a2.borrow().len() as i64;
4106
+ let norm = |v: Option<&Value>, dflt: i64| -> i64 {
4107
+ match v {
4108
+ Some(Value::Number(n)) => {
4109
+ let i = *n as i64;
4110
+ if i < 0 { (len + i).max(0) } else { i.min(len) }
4111
+ }
4112
+ _ => dflt,
4113
+ }
4114
+ };
4115
+ let target = norm(args.first(), 0) as usize;
4116
+ let start = norm(args.get(1), 0) as usize;
4117
+ let end = norm(args.get(2), len) as usize;
4118
+ let mut b = a2.borrow_mut();
4119
+ match b.as_packed_mut() {
4120
+ Some(nums) => {
4121
+ if start < end && target < nums.len() {
4122
+ let count = (end - start).min(nums.len() - target);
4123
+ let block: Vec<f64> = nums[start..start + count].to_vec();
4124
+ for (k, val) in block.into_iter().enumerate() {
4125
+ nums[target + k] = val;
4126
+ }
4127
+ }
4128
+ }
4129
+ None => {
4130
+ let boxed = b.deopt();
4131
+ if start < end && target < boxed.len() {
4132
+ let count = (end - start).min(boxed.len() - target);
4133
+ let block: Vec<Value> = boxed[start..start + count].to_vec();
4134
+ for (k, val) in block.into_iter().enumerate() {
4135
+ boxed[target + k] = val;
4136
+ }
4137
+ }
4138
+ }
4139
+ }
4140
+ drop(b);
4141
+ Value::NumberArray(a2.clone())
4142
+ })
4143
+ }
4096
4144
  "reverse" => make_native_fn(move |_: &[Value]| {
4097
4145
  let mut b = a_clone.borrow_mut();
4098
4146
  match b.as_packed_mut() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-format",
3
- "version": "2.43.8",
3
+ "version": "3.0.0",
4
4
  "description": "Tish code formatter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file