@tishlang/tish 1.13.2 → 2.0.1

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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +61 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,481 @@
1
+ //! `Set` and `Map` — real constructors + methods for the non-JS targets (interpreter, VM, native).
2
+ //!
3
+ //! Like [`crate::date`], an instance is a plain `Value::Object` whose methods are per-instance
4
+ //! `Value::native` closures that capture a shared backing store, so the one implementation works
5
+ //! across every backend with no new `Value` variant. The backing store is an **insertion-ordered hash
6
+ //! map** ([`indexmap::IndexMap`]) keyed by [`Key`] (SameValueZero), so `get`/`set`/`has`/`add` are
7
+ //! O(1) average rather than the O(n) linear scan a `Vec`-of-pairs would force (a `Map` doing N
8
+ //! operations was previously O(N²) — see `docs/perf-benchmark-suite.md`). `delete` uses `shift_remove`
9
+ //! to preserve iteration order. Each instance also carries a hidden [`SIZE_SLOT`] opaque
10
+ //! ([`SizeProbe`]) wired to the live store so the runtimes can answer the computed `.size` property
11
+ //! via [`collection_size`].
12
+ //!
13
+ //! ## v1 scope / known gaps (documented in tishlang-web)
14
+ //! - `add`/`set` return `undefined` (no method chaining yet — native object methods receive no
15
+ //! `this`).
16
+ //! - `.values()` / `.keys()` / `.entries()` return real **iterators** (objects with a `next()`
17
+ //! that yields `{ value, done }`), so they work both directly (`it.next()`) and in `for…of` /
18
+ //! spread / `Array.from` via [`tishlang_core::drain_iterator`]. The iterator snapshots the
19
+ //! collection when created; mutating the source mid-iteration is not reflected (a live iterator
20
+ //! is a follow-up). Direct `for (x of set)` (iterating the collection itself) and `forEach`
21
+ //! (a callback the interpreter's closures cannot cross into a core native fn) are still
22
+ //! follow-ups — iterate via `.values()` / `.entries()`.
23
+ //! - Keys/values use **SameValueZero** equality (NaN equals NaN; `+0`/`-0` are the same; objects by
24
+ //! identity).
25
+
26
+ use std::any::Any;
27
+ use std::hash::{Hash, Hasher};
28
+ use std::sync::Arc;
29
+
30
+ use indexmap::IndexMap;
31
+ use tishlang_core::{NativeFn, ObjectMap, TishOpaque, Value, VmRef};
32
+
33
+ const CONSTRUCT: &str = "__construct";
34
+
35
+ /// Hidden instance slot holding a [`SizeProbe`] opaque so every runtime can answer `.size`. An
36
+ /// `Opaque` is *shared* (`Arc::clone`, not deep-copied) across the interpreter's core↔eval value
37
+ /// bridge, so the probe stays wired to the live backing store even after an instance is bridged —
38
+ /// a hidden `Value::Array` would be copied and go stale on the first mutation.
39
+ pub const SIZE_SLOT: &str = "__tish_size__";
40
+
41
+ /// SameValueZero-keyed entry wrapper, so a `Value` can key an insertion-ordered hash map with JS
42
+ /// `Map`/`Set` semantics: NaN equals NaN, `+0`/`-0` unify, primitives by value, references by
43
+ /// identity. `Hash` is kept consistent with this `Eq` (equal keys hash equal). Primitive keys
44
+ /// (number / string / bool / null — the common case) hash by value → true O(1). Reference keys hash
45
+ /// by a per-variant tag only and are disambiguated by `ptr_eq` in `Eq`; that keeps object-keyed maps
46
+ /// *correct* (if same-bucket within a variant) without needing a stable address out of `VmRef`.
47
+ #[derive(Clone)]
48
+ struct Key(Value);
49
+
50
+ impl PartialEq for Key {
51
+ fn eq(&self, other: &Self) -> bool {
52
+ same_value_zero(&self.0, &other.0)
53
+ }
54
+ }
55
+ impl Eq for Key {}
56
+
57
+ impl Hash for Key {
58
+ fn hash<H: Hasher>(&self, state: &mut H) {
59
+ match &self.0 {
60
+ Value::Number(n) => {
61
+ 0u8.hash(state);
62
+ // Canonicalize so `Eq` partners hash together: `+0` and `-0` share a key, and every
63
+ // NaN is one key.
64
+ let bits = if *n == 0.0 {
65
+ 0u64
66
+ } else if n.is_nan() {
67
+ 0x7ff8_0000_0000_0000
68
+ } else {
69
+ n.to_bits()
70
+ };
71
+ bits.hash(state);
72
+ }
73
+ Value::String(s) => {
74
+ 1u8.hash(state);
75
+ s.as_str().hash(state);
76
+ }
77
+ Value::Bool(b) => {
78
+ 2u8.hash(state);
79
+ b.hash(state);
80
+ }
81
+ Value::Null => 3u8.hash(state),
82
+ // Reference / identity values: per-variant tag only; `ptr_eq` in `Eq` does the rest.
83
+ Value::Symbol(_) => 4u8.hash(state),
84
+ Value::Array(_) => 5u8.hash(state),
85
+ Value::NumberArray(_) => 6u8.hash(state),
86
+ Value::Object(_) => 7u8.hash(state),
87
+ _ => 8u8.hash(state),
88
+ }
89
+ }
90
+ }
91
+
92
+ /// Shared backing for both `Set` and `Map`: an insertion-ordered hash map keyed by [`Key`]. A `Set`
93
+ /// stores `Value::Null` values and iterates its keys; a `Map` stores the mapped value. Uses `ahash`
94
+ /// (the same fast hasher `tish_core`'s `PropMap` uses) rather than the default SipHash — iteration
95
+ /// order is insertion order regardless of the hasher, so this is a pure constant-factor speedup.
96
+ type Store = VmRef<IndexMap<Key, Value, ahash::RandomState>>;
97
+
98
+ /// Opaque whose length reports a collection's live element count. Stored under [`SIZE_SLOT`] on every
99
+ /// `Set`/`Map` instance.
100
+ struct SizeProbe(Store);
101
+
102
+ impl TishOpaque for SizeProbe {
103
+ fn type_name(&self) -> &'static str {
104
+ "CollectionSize"
105
+ }
106
+ fn get_method(&self, _name: &str) -> Option<NativeFn> {
107
+ None
108
+ }
109
+ fn as_any(&self) -> &dyn Any {
110
+ self
111
+ }
112
+ }
113
+
114
+ /// If `op` is a [`SizeProbe`], its live length. Lets the interpreter (which holds the probe as an
115
+ /// already-bridged `Opaque`) read `.size` without depending on this module's private `SizeProbe`.
116
+ pub fn size_probe_len(op: &dyn TishOpaque) -> Option<f64> {
117
+ op.as_any()
118
+ .downcast_ref::<SizeProbe>()
119
+ .map(|p| p.0.borrow().len() as f64)
120
+ }
121
+
122
+ /// Wrap a backing store as the hidden [`SIZE_SLOT`] opaque. `Value::Opaque`'s payload is always
123
+ /// `Arc<dyn TishOpaque>`, so on the single-threaded build (`Rc`-based `VmRef`) clippy's
124
+ /// `arc_with_non_send_sync` fires spuriously — the `Arc` is mandated by the API, not a thread choice.
125
+ #[allow(clippy::arc_with_non_send_sync)]
126
+ fn size_slot(store: &Store) -> Value {
127
+ Value::Opaque(Arc::new(SizeProbe(store.clone())))
128
+ }
129
+
130
+ /// SameValueZero — the equality `Set`/`Map` use for membership. NaN equals NaN; `+0` and `-0` are
131
+ /// equal; primitives compare by value; reference types compare by identity.
132
+ fn same_value_zero(a: &Value, b: &Value) -> bool {
133
+ use Value::*;
134
+ match (a, b) {
135
+ (Number(x), Number(y)) => x == y || (x.is_nan() && y.is_nan()),
136
+ (String(x), String(y)) => x == y,
137
+ (Bool(x), Bool(y)) => x == y,
138
+ (Null, Null) => true,
139
+ (Symbol(x), Symbol(y)) => Arc::ptr_eq(x, y),
140
+ (Array(x), Array(y)) => VmRef::ptr_eq(x, y),
141
+ (NumberArray(x), NumberArray(y)) => VmRef::ptr_eq(x, y),
142
+ (Object(x), Object(y)) => VmRef::ptr_eq(x, y),
143
+ _ => false,
144
+ }
145
+ }
146
+
147
+ /// Iterate a `Value` as a sequence: arrays yield their elements; everything else yields nothing.
148
+ fn iter_elements(v: &Value) -> Vec<Value> {
149
+ match v {
150
+ Value::Array(a) => a.borrow().iter().cloned().collect(),
151
+ Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
152
+ _ => Vec::new(),
153
+ }
154
+ }
155
+
156
+ /// The live `.size` of a `Set`/`Map` instance, or `None` for any other value. Runtimes call this
157
+ /// from `get_prop` so `set.size` / `map.size` read as a plain number property.
158
+ pub fn collection_size(obj: &Value) -> Option<f64> {
159
+ if let Value::Object(o) = obj {
160
+ if let Some(Value::Opaque(op)) = o.borrow().strings.get(SIZE_SLOT) {
161
+ return size_probe_len(op.as_ref());
162
+ }
163
+ }
164
+ None
165
+ }
166
+
167
+ // ─────────────────────────────────────────── Set ───────────────────────────────────────────────
168
+
169
+ /// Build a `Set` instance object over a fresh backing store seeded from `initial`.
170
+ pub fn set_instance(initial: &[Value]) -> Value {
171
+ let store: Store = VmRef::new(IndexMap::default());
172
+ {
173
+ let mut b = store.borrow_mut();
174
+ for v in initial {
175
+ // `or_insert` dedups and keeps first-insertion order.
176
+ b.entry(Key(v.clone())).or_insert(Value::Null);
177
+ }
178
+ }
179
+ let mut m = ObjectMap::default();
180
+ // Hidden slot that drives `.size` (shared across the interp value bridge — see SIZE_SLOT).
181
+ m.insert(Arc::from(SIZE_SLOT), size_slot(&store));
182
+
183
+ {
184
+ let s = store.clone();
185
+ m.insert(
186
+ Arc::from("add"),
187
+ Value::native(move |args: &[Value]| {
188
+ let v = args.first().cloned().unwrap_or(Value::Null);
189
+ s.borrow_mut().entry(Key(v)).or_insert(Value::Null);
190
+ Value::Null
191
+ }),
192
+ );
193
+ }
194
+ {
195
+ let s = store.clone();
196
+ m.insert(
197
+ Arc::from("has"),
198
+ Value::native(move |args: &[Value]| {
199
+ let v = args.first().cloned().unwrap_or(Value::Null);
200
+ Value::Bool(s.borrow().contains_key(&Key(v)))
201
+ }),
202
+ );
203
+ }
204
+ {
205
+ let s = store.clone();
206
+ m.insert(
207
+ Arc::from("delete"),
208
+ Value::native(move |args: &[Value]| {
209
+ let v = args.first().cloned().unwrap_or(Value::Null);
210
+ // `shift_remove` preserves iteration order (vs `swap_remove`).
211
+ Value::Bool(s.borrow_mut().shift_remove(&Key(v)).is_some())
212
+ }),
213
+ );
214
+ }
215
+ {
216
+ let s = store.clone();
217
+ m.insert(
218
+ Arc::from("clear"),
219
+ Value::native(move |_args: &[Value]| {
220
+ s.borrow_mut().clear();
221
+ Value::Null
222
+ }),
223
+ );
224
+ }
225
+ // values()/keys() are identical for a Set (the elements are the keys); entries() yields [v, v].
226
+ {
227
+ let s = store.clone();
228
+ let values = move |_args: &[Value]| {
229
+ let out: Vec<Value> = s.borrow().keys().map(|k| k.0.clone()).collect();
230
+ crate::iterator::array_iterator(out)
231
+ };
232
+ m.insert(Arc::from("values"), Value::native(values.clone()));
233
+ m.insert(Arc::from("keys"), Value::native(values));
234
+ }
235
+ {
236
+ let s = store.clone();
237
+ m.insert(
238
+ Arc::from("entries"),
239
+ Value::native(move |_args: &[Value]| {
240
+ let pairs: Vec<Value> = s
241
+ .borrow()
242
+ .keys()
243
+ .map(|k| Value::Array(VmRef::new(vec![k.0.clone(), k.0.clone()])))
244
+ .collect();
245
+ crate::iterator::array_iterator(pairs)
246
+ }),
247
+ );
248
+ }
249
+
250
+ Value::object(m)
251
+ }
252
+
253
+ /// The global `Set` constructor (`new Set()` / `new Set([1, 2, 2])`).
254
+ pub fn set_constructor_value() -> Value {
255
+ let mut m = ObjectMap::default();
256
+ m.insert(
257
+ Arc::from(CONSTRUCT),
258
+ Value::native(|args: &[Value]| {
259
+ let initial = args.first().map(iter_elements).unwrap_or_default();
260
+ set_instance(&initial)
261
+ }),
262
+ );
263
+ Value::object(m)
264
+ }
265
+
266
+ // ─────────────────────────────────────────── Map ───────────────────────────────────────────────
267
+
268
+ /// Build a `Map` instance object over a fresh backing store seeded from `pairs` (each element an
269
+ /// iterable `[k, v]`).
270
+ pub fn map_instance(pairs: &[Value]) -> Value {
271
+ let store: Store = VmRef::new(IndexMap::default());
272
+ {
273
+ let mut b = store.borrow_mut();
274
+ for p in pairs {
275
+ let kv = iter_elements(p);
276
+ let k = kv.first().cloned().unwrap_or(Value::Null);
277
+ let v = kv.get(1).cloned().unwrap_or(Value::Null);
278
+ b.insert(Key(k), v);
279
+ }
280
+ }
281
+ let mut m = ObjectMap::default();
282
+ m.insert(Arc::from(SIZE_SLOT), size_slot(&store));
283
+
284
+ {
285
+ let s = store.clone();
286
+ m.insert(
287
+ Arc::from("set"),
288
+ Value::native(move |args: &[Value]| {
289
+ let key = args.first().cloned().unwrap_or(Value::Null);
290
+ let val = args.get(1).cloned().unwrap_or(Value::Null);
291
+ // `insert` updates an existing key in place (keeps its position) or appends a new one.
292
+ s.borrow_mut().insert(Key(key), val);
293
+ Value::Null
294
+ }),
295
+ );
296
+ }
297
+ {
298
+ let s = store.clone();
299
+ m.insert(
300
+ Arc::from("get"),
301
+ Value::native(move |args: &[Value]| {
302
+ let key = args.first().cloned().unwrap_or(Value::Null);
303
+ s.borrow().get(&Key(key)).cloned().unwrap_or(Value::Null)
304
+ }),
305
+ );
306
+ }
307
+ {
308
+ let s = store.clone();
309
+ m.insert(
310
+ Arc::from("has"),
311
+ Value::native(move |args: &[Value]| {
312
+ let key = args.first().cloned().unwrap_or(Value::Null);
313
+ Value::Bool(s.borrow().contains_key(&Key(key)))
314
+ }),
315
+ );
316
+ }
317
+ {
318
+ let s = store.clone();
319
+ m.insert(
320
+ Arc::from("delete"),
321
+ Value::native(move |args: &[Value]| {
322
+ let key = args.first().cloned().unwrap_or(Value::Null);
323
+ Value::Bool(s.borrow_mut().shift_remove(&Key(key)).is_some())
324
+ }),
325
+ );
326
+ }
327
+ {
328
+ let s = store.clone();
329
+ m.insert(
330
+ Arc::from("clear"),
331
+ Value::native(move |_args: &[Value]| {
332
+ s.borrow_mut().clear();
333
+ Value::Null
334
+ }),
335
+ );
336
+ }
337
+ {
338
+ let s = store.clone();
339
+ m.insert(
340
+ Arc::from("keys"),
341
+ Value::native(move |_args: &[Value]| {
342
+ let out: Vec<Value> = s.borrow().keys().map(|k| k.0.clone()).collect();
343
+ crate::iterator::array_iterator(out)
344
+ }),
345
+ );
346
+ }
347
+ {
348
+ let s = store.clone();
349
+ m.insert(
350
+ Arc::from("values"),
351
+ Value::native(move |_args: &[Value]| {
352
+ let out: Vec<Value> = s.borrow().values().cloned().collect();
353
+ crate::iterator::array_iterator(out)
354
+ }),
355
+ );
356
+ }
357
+ {
358
+ let s = store.clone();
359
+ m.insert(
360
+ Arc::from("entries"),
361
+ Value::native(move |_args: &[Value]| {
362
+ let pairs: Vec<Value> = s
363
+ .borrow()
364
+ .iter()
365
+ .map(|(k, v)| Value::Array(VmRef::new(vec![k.0.clone(), v.clone()])))
366
+ .collect();
367
+ crate::iterator::array_iterator(pairs)
368
+ }),
369
+ );
370
+ }
371
+
372
+ Value::object(m)
373
+ }
374
+
375
+ /// The global `Map` constructor (`new Map()` / `new Map([[k, v], …])`).
376
+ pub fn map_constructor_value() -> Value {
377
+ let mut m = ObjectMap::default();
378
+ m.insert(
379
+ Arc::from(CONSTRUCT),
380
+ Value::native(|args: &[Value]| {
381
+ let pairs = args.first().map(iter_elements).unwrap_or_default();
382
+ map_instance(&pairs)
383
+ }),
384
+ );
385
+ Value::object(m)
386
+ }
387
+
388
+ #[cfg(test)]
389
+ mod tests {
390
+ use super::*;
391
+
392
+ fn num(v: &Value) -> f64 {
393
+ match v {
394
+ Value::Number(n) => *n,
395
+ _ => f64::NAN,
396
+ }
397
+ }
398
+
399
+ #[test]
400
+ fn set_dedups_and_counts() {
401
+ let s = set_instance(&[Value::Number(1.0), Value::Number(2.0), Value::Number(2.0)]);
402
+ assert_eq!(collection_size(&s), Some(2.0));
403
+ }
404
+
405
+ #[test]
406
+ fn set_nan_is_one_member() {
407
+ let s = set_instance(&[Value::Number(f64::NAN), Value::Number(f64::NAN)]);
408
+ assert_eq!(collection_size(&s), Some(1.0));
409
+ }
410
+
411
+ #[test]
412
+ fn map_size_via_hook() {
413
+ let m = map_instance(&[
414
+ Value::Array(VmRef::new(vec![Value::String("x".into()), Value::Number(1.0)])),
415
+ Value::Array(VmRef::new(vec![Value::String("y".into()), Value::Number(2.0)])),
416
+ ]);
417
+ assert_eq!(collection_size(&m), Some(2.0));
418
+ }
419
+
420
+ #[test]
421
+ fn key_same_value_zero_semantics() {
422
+ // `+0` and `-0` are one key; NaN is one key.
423
+ let mut map: IndexMap<Key, Value> = IndexMap::default();
424
+ map.insert(Key(Value::Number(0.0)), Value::Number(1.0));
425
+ map.insert(Key(Value::Number(-0.0)), Value::Number(2.0)); // updates the same entry
426
+ assert_eq!(map.len(), 1);
427
+ assert_eq!(num(map.get(&Key(Value::Number(0.0))).unwrap()), 2.0);
428
+
429
+ map.insert(Key(Value::Number(f64::NAN)), Value::Number(7.0));
430
+ map.insert(Key(Value::Number(f64::NAN)), Value::Number(8.0)); // same NaN key
431
+ assert_eq!(num(map.get(&Key(Value::Number(f64::NAN))).unwrap()), 8.0);
432
+ assert_eq!(map.len(), 2);
433
+ }
434
+
435
+ #[test]
436
+ fn map_insert_updates_in_place_and_keeps_order() {
437
+ let store: Store = VmRef::new(IndexMap::default());
438
+ store
439
+ .borrow_mut()
440
+ .insert(Key(Value::String("a".into())), Value::Number(1.0));
441
+ store
442
+ .borrow_mut()
443
+ .insert(Key(Value::String("a".into())), Value::Number(9.0)); // update, not insert
444
+ store
445
+ .borrow_mut()
446
+ .insert(Key(Value::String("b".into())), Value::Number(2.0));
447
+ let b = store.borrow();
448
+ assert_eq!(b.len(), 2);
449
+ assert_eq!(num(b.get(&Key(Value::String("a".into()))).unwrap()), 9.0);
450
+ let order: Vec<&str> = b
451
+ .keys()
452
+ .map(|k| match &k.0 {
453
+ Value::String(s) => s.as_str(),
454
+ _ => "?",
455
+ })
456
+ .collect();
457
+ assert_eq!(order, vec!["a", "b"]); // insertion order preserved
458
+ }
459
+
460
+ #[test]
461
+ fn delete_preserves_order() {
462
+ let store: Store = VmRef::new(IndexMap::default());
463
+ for (k, v) in [("a", 1.0), ("b", 2.0), ("c", 3.0)] {
464
+ store
465
+ .borrow_mut()
466
+ .insert(Key(Value::String(k.into())), Value::Number(v));
467
+ }
468
+ store
469
+ .borrow_mut()
470
+ .shift_remove(&Key(Value::String("b".into())));
471
+ let b = store.borrow();
472
+ let order: Vec<&str> = b
473
+ .keys()
474
+ .map(|k| match &k.0 {
475
+ Value::String(s) => s.as_str(),
476
+ _ => "?",
477
+ })
478
+ .collect();
479
+ assert_eq!(order, vec!["a", "c"]); // "b" removed, order intact
480
+ }
481
+ }
@@ -6,16 +6,26 @@ use tishlang_core::{ObjectMap, Value, VmRef};
6
6
 
7
7
  const CONSTRUCT: &str = "__construct";
8
8
 
9
- /// Host `new`: `Object` with `__construct`, `Function` as plain call, else `Null`.
9
+ /// Host `new`: `Object` with `__construct` (or, failing that, `__call`), `Function` as plain call,
10
+ /// else `Null`. The `__call` fallback matters because builtin objects like `Promise` expose their
11
+ /// constructor as `__call` (so `Promise(f)` works) but have no `__construct`; without this fallback
12
+ /// `new Promise((resolve, reject) => …)` returned `Null` and never ran the executor on the VM family,
13
+ /// while the interpreter (which routes `new` through the same callable) worked — a cross-backend
14
+ /// divergence on the most common promise idiom.
10
15
  pub fn construct(callee: &Value, args: &[Value]) -> Value {
11
16
  match callee {
12
- Value::Function(f) => f(args),
17
+ Value::Function(f) => f.call(args),
13
18
  Value::Object(o) => {
14
19
  let b = o.borrow();
15
- if let Some(Value::Function(ctor)) = b.strings.get(&Arc::from(CONSTRUCT)) {
20
+ if let Some(Value::Function(ctor)) = b.strings.get(CONSTRUCT) {
16
21
  let c = ctor.clone();
17
22
  drop(b);
18
- return c(args);
23
+ return c.call(args);
24
+ }
25
+ if let Some(Value::Function(call)) = b.strings.get("__call") {
26
+ let c = call.clone();
27
+ drop(b);
28
+ return c.call(args);
19
29
  }
20
30
  Value::Null
21
31
  }
@@ -23,6 +33,51 @@ pub fn construct(callee: &Value, args: &[Value]) -> Value {
23
33
  }
24
34
  }
25
35
 
36
+ /// A JS-style error object `{ name, message }` (issue #60).
37
+ pub fn error_object(name: &str, message: &str) -> Value {
38
+ let mut e = ObjectMap::default();
39
+ e.insert(Arc::from("name"), Value::String(name.into()));
40
+ e.insert(Arc::from("message"), Value::String(message.into()));
41
+ Value::object(e)
42
+ }
43
+
44
+ fn make_error_from_args(name: &str, args: &[Value]) -> Value {
45
+ let message = args.first().map(|v| v.to_js_string()).unwrap_or_default();
46
+ error_object(name, &message)
47
+ }
48
+
49
+ /// `Error(msg)` / `new Error(msg)` (and `TypeError` / `RangeError`) → `{ name, message }`
50
+ /// (issue #60). `__call` and `__construct` behave identically, matching JS where `Error(x)`
51
+ /// and `new Error(x)` produce the same object.
52
+ pub fn error_constructor_value(name: &'static str) -> Value {
53
+ let mut m = ObjectMap::default();
54
+ m.insert(
55
+ Arc::from(CONSTRUCT),
56
+ Value::native(move |args: &[Value]| make_error_from_args(name, args)),
57
+ );
58
+ m.insert(
59
+ Arc::from("__call"),
60
+ Value::native(move |args: &[Value]| make_error_from_args(name, args)),
61
+ );
62
+ Value::object(m)
63
+ }
64
+
65
+ /// `Array(...)` / `new Array(...)` (issue #72). A single non-negative integer argument is a
66
+ /// length — the array is filled with `null` holes (so `arr[i] = v` works and `Array(n).fill(v)`
67
+ /// can overwrite them, issue #76). Zero args → `[]`; any other args → an array of exactly those
68
+ /// elements (`new Array(1, 2, 3)` → `[1, 2, 3]`). A single non-integer / negative number is a
69
+ /// RangeError in JS; native builtins can't throw here, so it falls back to a one-element array —
70
+ /// the integer-length and element-list forms (the real uses) match JS exactly.
71
+ pub fn array_construct(args: &[Value]) -> Value {
72
+ if let [Value::Number(n)] = args {
73
+ let n = *n;
74
+ if n >= 0.0 && n.fract() == 0.0 && n <= 4_294_967_295.0 {
75
+ return Value::Array(VmRef::new(vec![Value::Null; n as usize]));
76
+ }
77
+ }
78
+ Value::Array(VmRef::new(args.to_vec()))
79
+ }
80
+
26
81
  fn param(initial: f64) -> Value {
27
82
  let mut m = ObjectMap::default();
28
83
  m.insert(Arc::from("value"), Value::Number(initial));
@@ -135,21 +190,6 @@ fn audio_context_instance() -> Value {
135
190
  Value::object(ctx)
136
191
  }
137
192
 
138
- /// Global `Uint8Array` for native/VM: `new Uint8Array(n)` → numeric array of zeros (not real bytes).
139
- pub fn uint8_array_constructor_value() -> Value {
140
- let ctor = Value::native(|args: &[Value]| {
141
- let len = args
142
- .first()
143
- .and_then(Value::as_number)
144
- .unwrap_or(0.0)
145
- .clamp(0.0, 1_000_000_000.0) as usize;
146
- Value::Array(VmRef::new(vec![Value::Number(0.0); len]))
147
- });
148
- let mut m = ObjectMap::default();
149
- m.insert(Arc::from(CONSTRUCT), ctor);
150
- Value::object(m)
151
- }
152
-
153
193
  /// Global `AudioContext` for native/VM: stub graph (no real audio).
154
194
  pub fn audio_context_constructor_value() -> Value {
155
195
  let ctor = Value::native(|_args: &[Value]| audio_context_instance());