@tishlang/tish 2.10.1 → 2.16.13

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 (75) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +16 -0
  5. package/crates/tish/src/main.rs +24 -4
  6. package/crates/tish/tests/integration_test.rs +149 -0
  7. package/crates/tish_ast/src/ast.rs +10 -0
  8. package/crates/tish_builtins/src/array.rs +529 -56
  9. package/crates/tish_builtins/src/collections.rs +114 -40
  10. package/crates/tish_builtins/src/string.rs +95 -8
  11. package/crates/tish_bytecode/src/chunk.rs +7 -0
  12. package/crates/tish_bytecode/src/compiler.rs +560 -64
  13. package/crates/tish_bytecode/src/lib.rs +1 -1
  14. package/crates/tish_bytecode/src/opcode.rs +154 -2
  15. package/crates/tish_bytecode/src/serialize.rs +2 -0
  16. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  17. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  18. package/crates/tish_compile/src/codegen.rs +17736 -5269
  19. package/crates/tish_compile/src/infer.rs +1707 -190
  20. package/crates/tish_compile/src/lib.rs +29 -4
  21. package/crates/tish_compile/src/resolve.rs +66 -5
  22. package/crates/tish_compile/src/types.rs +224 -28
  23. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  24. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  25. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  26. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  27. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  28. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  29. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  30. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  31. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  32. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  33. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  34. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  35. package/crates/tish_compile_js/src/codegen.rs +91 -4
  36. package/crates/tish_compile_js/src/lib.rs +5 -2
  37. package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
  38. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  39. package/crates/tish_core/Cargo.toml +4 -0
  40. package/crates/tish_core/src/json.rs +104 -5
  41. package/crates/tish_core/src/lib.rs +174 -0
  42. package/crates/tish_core/src/shape.rs +4 -2
  43. package/crates/tish_core/src/value.rs +565 -35
  44. package/crates/tish_core/src/vmref.rs +14 -0
  45. package/crates/tish_eval/src/eval.rs +675 -76
  46. package/crates/tish_eval/src/natives.rs +19 -0
  47. package/crates/tish_eval/src/value.rs +76 -21
  48. package/crates/tish_ffi/src/lib.rs +11 -1
  49. package/crates/tish_ffi/tests/double_free.rs +35 -0
  50. package/crates/tish_fmt/src/lib.rs +75 -1
  51. package/crates/tish_lexer/src/lib.rs +76 -0
  52. package/crates/tish_lexer/src/token.rs +4 -0
  53. package/crates/tish_lint/src/lib.rs +126 -0
  54. package/crates/tish_lsp/README.md +2 -1
  55. package/crates/tish_lsp/src/main.rs +378 -28
  56. package/crates/tish_native/src/build.rs +41 -0
  57. package/crates/tish_parser/Cargo.toml +4 -0
  58. package/crates/tish_parser/src/lib.rs +27 -0
  59. package/crates/tish_parser/src/parser.rs +302 -20
  60. package/crates/tish_resolve/src/lib.rs +9 -0
  61. package/crates/tish_runtime/Cargo.toml +5 -0
  62. package/crates/tish_runtime/src/http.rs +28 -10
  63. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  64. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  65. package/crates/tish_runtime/src/lib.rs +523 -42
  66. package/crates/tish_runtime/src/timers.rs +49 -2
  67. package/crates/tish_ui/src/jsx.rs +253 -0
  68. package/crates/tish_vm/src/jit.rs +2514 -117
  69. package/crates/tish_vm/src/vm.rs +1227 -182
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish +0 -0
  72. package/platform/darwin-x64/tish +0 -0
  73. package/platform/linux-arm64/tish +0 -0
  74. package/platform/linux-x64/tish +0 -0
  75. package/platform/win32-x64/tish.exe +0 -0
@@ -54,38 +54,65 @@ impl PartialEq for Key {
54
54
  }
55
55
  impl Eq for Key {}
56
56
 
57
+ /// The single source of truth for hashing a key `Value`. Both [`Key`] (owned, the stored key) and
58
+ /// [`KeyRef`] (borrowed, the lookup probe) delegate here so an owned key and a borrowed probe of an
59
+ /// equal value hash identically — the invariant that makes the zero-clone borrow lookup sound.
60
+ #[inline]
61
+ fn hash_key_value<H: Hasher>(v: &Value, state: &mut H) {
62
+ match v {
63
+ Value::Number(n) => {
64
+ 0u8.hash(state);
65
+ // Canonicalize so `Eq` partners hash together: `+0` and `-0` share a key, and every
66
+ // NaN is one key.
67
+ let bits = if *n == 0.0 {
68
+ 0u64
69
+ } else if n.is_nan() {
70
+ 0x7ff8_0000_0000_0000
71
+ } else {
72
+ n.to_bits()
73
+ };
74
+ bits.hash(state);
75
+ }
76
+ Value::String(s) => {
77
+ 1u8.hash(state);
78
+ s.as_str().hash(state);
79
+ }
80
+ Value::Bool(b) => {
81
+ 2u8.hash(state);
82
+ b.hash(state);
83
+ }
84
+ Value::Null => 3u8.hash(state),
85
+ // Reference / identity values: per-variant tag only; `ptr_eq` in `Eq` does the rest.
86
+ Value::Symbol(_) => 4u8.hash(state),
87
+ Value::Array(_) => 5u8.hash(state),
88
+ Value::NumberArray(_) => 6u8.hash(state),
89
+ Value::Object(_) => 7u8.hash(state),
90
+ _ => 8u8.hash(state),
91
+ }
92
+ }
93
+
57
94
  impl Hash for Key {
58
95
  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
- }
96
+ hash_key_value(&self.0, state);
97
+ }
98
+ }
99
+
100
+ /// Borrowed look-up probe for [`Key`]: wraps `&Value` so `get`/`contains_key`/`shift_remove` can
101
+ /// query the store **without cloning the key into an owned `Key`** (no `Value` clone, no `Arc` bump).
102
+ /// It hashes via [`hash_key_value`] (identical to `Key`) and compares via [`same_value_zero`]
103
+ /// (identical to `Key`'s `Eq`), so it is `Equivalent<Key>` — the lookup is a drop-in for `&Key`.
104
+ /// Only `set`/`add` (which must *store* the key) still build an owned `Key`.
105
+ struct KeyRef<'a>(&'a Value);
106
+
107
+ impl Hash for KeyRef<'_> {
108
+ fn hash<H: Hasher>(&self, state: &mut H) {
109
+ hash_key_value(self.0, state);
110
+ }
111
+ }
112
+
113
+ impl indexmap::Equivalent<Key> for KeyRef<'_> {
114
+ fn equivalent(&self, key: &Key) -> bool {
115
+ same_value_zero(self.0, &key.0)
89
116
  }
90
117
  }
91
118
 
@@ -119,6 +146,53 @@ pub fn size_probe_len(op: &dyn TishOpaque) -> Option<f64> {
119
146
  .map(|p| p.0.borrow().len() as f64)
120
147
  }
121
148
 
149
+ fn map_store(map: &Value) -> Option<Store> {
150
+ if let Value::Object(o) = map {
151
+ if let Some(Value::Opaque(op)) = o.borrow().strings.get(SIZE_SLOT) {
152
+ if let Some(probe) = op.as_ref().as_any().downcast_ref::<SizeProbe>() {
153
+ return Some(probe.0.clone());
154
+ }
155
+ }
156
+ }
157
+ None
158
+ }
159
+
160
+ /// Direct `Map.prototype.has` — skips bound-method `get_prop` + `value_call` (k-nucleotide hot path).
161
+ /// Looks up via a borrowed [`KeyRef`]: no key clone, no owned `Key` allocation.
162
+ pub fn map_has(map: &Value, key: &Value) -> Value {
163
+ match map_store(map) {
164
+ Some(s) => Value::Bool(s.borrow().contains_key(&KeyRef(key))),
165
+ None => Value::Bool(false),
166
+ }
167
+ }
168
+
169
+ /// Direct `Map.prototype.get`. Looks up via a borrowed [`KeyRef`] (no key clone / no owned `Key`).
170
+ pub fn map_get(map: &Value, key: &Value) -> Value {
171
+ match map_store(map) {
172
+ Some(s) => s.borrow().get(&KeyRef(key)).cloned().unwrap_or(Value::Null),
173
+ None => Value::Null,
174
+ }
175
+ }
176
+
177
+ /// Direct `Map.prototype.set` (mutates in place; returns `undefined` = `Null`).
178
+ pub fn map_set(map: &Value, key: Value, val: Value) -> Value {
179
+ if let Some(s) = map_store(map) {
180
+ s.borrow_mut().insert(Key(key), val);
181
+ }
182
+ Value::Null
183
+ }
184
+
185
+ /// Direct `Map.prototype.values` — snapshot iterator (same as the bound native method).
186
+ pub fn map_values(map: &Value) -> Value {
187
+ match map_store(map) {
188
+ Some(s) => {
189
+ let out: Vec<Value> = s.borrow().values().cloned().collect();
190
+ crate::iterator::array_iterator(out)
191
+ }
192
+ None => crate::iterator::array_iterator(vec![]),
193
+ }
194
+ }
195
+
122
196
  /// Wrap a backing store as the hidden [`SIZE_SLOT`] opaque. `Value::Opaque`'s payload is always
123
197
  /// `Arc<dyn TishOpaque>`, so on the single-threaded build (`Rc`-based `VmRef`) clippy's
124
198
  /// `arc_with_non_send_sync` fires spuriously — the `Arc` is mandated by the API, not a thread choice.
@@ -196,8 +270,8 @@ pub fn set_instance(initial: &[Value]) -> Value {
196
270
  m.insert(
197
271
  Arc::from("has"),
198
272
  Value::native(move |args: &[Value]| {
199
- let v = args.first().cloned().unwrap_or(Value::Null);
200
- Value::Bool(s.borrow().contains_key(&Key(v)))
273
+ let v = args.first().unwrap_or(&Value::Null);
274
+ Value::Bool(s.borrow().contains_key(&KeyRef(v)))
201
275
  }),
202
276
  );
203
277
  }
@@ -206,9 +280,9 @@ pub fn set_instance(initial: &[Value]) -> Value {
206
280
  m.insert(
207
281
  Arc::from("delete"),
208
282
  Value::native(move |args: &[Value]| {
209
- let v = args.first().cloned().unwrap_or(Value::Null);
283
+ let v = args.first().unwrap_or(&Value::Null);
210
284
  // `shift_remove` preserves iteration order (vs `swap_remove`).
211
- Value::Bool(s.borrow_mut().shift_remove(&Key(v)).is_some())
285
+ Value::Bool(s.borrow_mut().shift_remove(&KeyRef(v)).is_some())
212
286
  }),
213
287
  );
214
288
  }
@@ -299,8 +373,8 @@ pub fn map_instance(pairs: &[Value]) -> Value {
299
373
  m.insert(
300
374
  Arc::from("get"),
301
375
  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)
376
+ let key = args.first().unwrap_or(&Value::Null);
377
+ s.borrow().get(&KeyRef(key)).cloned().unwrap_or(Value::Null)
304
378
  }),
305
379
  );
306
380
  }
@@ -309,8 +383,8 @@ pub fn map_instance(pairs: &[Value]) -> Value {
309
383
  m.insert(
310
384
  Arc::from("has"),
311
385
  Value::native(move |args: &[Value]| {
312
- let key = args.first().cloned().unwrap_or(Value::Null);
313
- Value::Bool(s.borrow().contains_key(&Key(key)))
386
+ let key = args.first().unwrap_or(&Value::Null);
387
+ Value::Bool(s.borrow().contains_key(&KeyRef(key)))
314
388
  }),
315
389
  );
316
390
  }
@@ -319,8 +393,8 @@ pub fn map_instance(pairs: &[Value]) -> Value {
319
393
  m.insert(
320
394
  Arc::from("delete"),
321
395
  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())
396
+ let key = args.first().unwrap_or(&Value::Null);
397
+ Value::Bool(s.borrow_mut().shift_remove(&KeyRef(key)).is_some())
324
398
  }),
325
399
  );
326
400
  }
@@ -4,9 +4,64 @@
4
4
  //! JavaScript, matching .length and .charAt(). Byte offsets are never exposed.
5
5
 
6
6
  use crate::helpers::normalize_index;
7
+ use std::cell::RefCell;
8
+ use tishlang_core::ArcStr;
7
9
  use tishlang_core::Value;
8
10
  use tishlang_core::VmRef;
9
11
 
12
+ // #203: a per-thread cursor cache that makes repeated character indexing (`charCodeAt(i)`, `s[i]`,
13
+ // `charAt(i)`) O(1)/near-O(1) instead of O(i). tish strings are UTF-8, so `chars().nth(i)` scans from
14
+ // the start — turning indexed/strided scans into O(n^2) (a strided checksum over a 1.3 MB string was
15
+ // 4939ms vs node 1ms). For each recently-indexed string we cache whether it is all-ASCII (then a
16
+ // character index equals a byte index → O(1) byte lookup) plus a forward cursor (so non-ASCII
17
+ // sequential/strided scans advance from the last position, not from 0). Safety: the entry holds an
18
+ // `ArcStr` CLONE, which keeps the backing allocation alive — so its data pointer can't be freed and
19
+ // reused by another string while cached (no ABA), and since strings are immutable the cached ASCII
20
+ // flag stays valid. Backends share this via `char_at_idx` (native + VM route through the builtin).
21
+ // Semantics are unchanged: still character (Unicode scalar) indexing, identical to `chars().nth(i)`.
22
+ struct CharCursor {
23
+ s: ArcStr,
24
+ ascii: bool,
25
+ /// Character (Unicode scalar) count, cached so `.length` is O(1) too — `for (i=0;i<s.length;i++)`
26
+ /// re-evaluates the bound every iteration, so an O(n) `chars().count()` there is itself O(n^2).
27
+ len_chars: usize,
28
+ char_idx: usize,
29
+ byte_off: usize,
30
+ }
31
+
32
+ thread_local! {
33
+ static INDEX_CURSOR: RefCell<Option<CharCursor>> = const { RefCell::new(None) };
34
+ }
35
+
36
+ /// Borrow the cursor entry for `s`, reseeding (compute ASCII flag + character count) if it currently
37
+ /// caches a different backing allocation, then run `f` against it. Centralises the pointer-keyed
38
+ /// reseed shared by [`char_at_idx`] and [`char_count`].
39
+ fn with_cursor<R>(s: &ArcStr, f: impl FnOnce(&mut CharCursor, &ArcStr) -> R) -> R {
40
+ INDEX_CURSOR.with(|cell| {
41
+ let mut slot = cell.borrow_mut();
42
+ let hit = matches!(slot.as_ref(), Some(c)
43
+ if std::ptr::eq(c.s.as_bytes().as_ptr(), s.as_bytes().as_ptr()) && c.s.len() == s.len());
44
+ if !hit {
45
+ let ascii = s.as_bytes().is_ascii();
46
+ // ASCII → character count equals byte length (free); otherwise count once.
47
+ let len_chars = if ascii { s.len() } else { s.chars().count() };
48
+ *slot = Some(CharCursor {
49
+ s: s.clone(),
50
+ ascii,
51
+ len_chars,
52
+ char_idx: 0,
53
+ byte_off: 0,
54
+ });
55
+ }
56
+ f(slot.as_mut().unwrap(), s)
57
+ })
58
+ }
59
+
60
+ /// Character (Unicode scalar) count of `s`, O(1) after the first call on a given string.
61
+ pub fn char_count(s: &ArcStr) -> usize {
62
+ with_cursor(s, |c, _| c.len_chars)
63
+ }
64
+
10
65
  /// Byte offset -> character index.
11
66
  fn byte_to_char_index(s: &str, byte_offset: usize) -> usize {
12
67
  s.char_indices()
@@ -30,7 +85,7 @@ pub fn from_str(s: &str) -> Value {
30
85
  /// Get the length of a string (character count).
31
86
  pub fn len(s: &Value) -> Option<usize> {
32
87
  match s {
33
- Value::String(str) => Some(str.chars().count()),
88
+ Value::String(str) => Some(char_count(str)),
34
89
  _ => None,
35
90
  }
36
91
  }
@@ -370,8 +425,38 @@ pub fn escape_html(s: &Value) -> Value {
370
425
  Value::String(tishlang_core::ArcStr::from(out))
371
426
  }
372
427
 
373
- fn char_at_idx(s: &str, idx: usize) -> Option<char> {
374
- s.chars().nth(idx)
428
+ /// Character (Unicode scalar) at index `idx`, using the cursor cache (see [`CharCursor`]).
429
+ /// Equivalent to `s.chars().nth(idx)` but O(1) for ASCII strings and near-O(1) for forward/strided
430
+ /// scans of non-ASCII strings, instead of O(idx) every call.
431
+ fn char_at_idx(s: &ArcStr, idx: usize) -> Option<char> {
432
+ with_cursor(s, |c, s| {
433
+ if c.ascii {
434
+ // ASCII: character index == byte index, and every byte is its own scalar.
435
+ return s.as_bytes().get(idx).map(|&b| b as char);
436
+ }
437
+ // Non-ASCII: advance from the nearest known position (forward fast path); restart from 0 only
438
+ // when indexing backwards relative to the cursor.
439
+ let (base_idx, base_off) = if idx >= c.char_idx {
440
+ (c.char_idx, c.byte_off)
441
+ } else {
442
+ (0, 0)
443
+ };
444
+ match s[base_off..].char_indices().nth(idx - base_idx) {
445
+ Some((rel_off, ch)) => {
446
+ c.char_idx = idx;
447
+ c.byte_off = base_off + rel_off;
448
+ Some(ch)
449
+ }
450
+ None => None,
451
+ }
452
+ })
453
+ }
454
+
455
+ /// Character (Unicode scalar) at index `idx` via the cursor cache — the O(1)/near-O(1) primitive
456
+ /// behind `s[i]`. Returns `None` for an out-of-range index; each backend maps that to its own
457
+ /// out-of-bounds behaviour (interpreter/native → null, VM → error).
458
+ pub fn nth_char(s: &ArcStr, idx: usize) -> Option<char> {
459
+ char_at_idx(s, idx)
375
460
  }
376
461
 
377
462
  pub fn char_at(s: &Value, idx: &Value) -> Value {
@@ -396,11 +481,13 @@ pub fn at(s: &Value, index: &Value) -> Value {
396
481
  Value::Number(n) => *n as i64,
397
482
  _ => 0,
398
483
  };
399
- let chars: Vec<char> = s.chars().collect();
400
- let len = chars.len() as i64;
401
- let idx = if i < 0 { len + i } else { i };
402
- if idx >= 0 && idx < len {
403
- return Value::String(chars[idx as usize].to_string().into());
484
+ // Non-negative indices use the cursor cache directly; a negative index counts from the end,
485
+ // which needs the character length first (inherently O(n)).
486
+ let idx = if i < 0 { s.chars().count() as i64 + i } else { i };
487
+ if idx >= 0 {
488
+ if let Some(c) = char_at_idx(s, idx as usize) {
489
+ return Value::String(c.to_string().into());
490
+ }
404
491
  }
405
492
  }
406
493
  Value::Null
@@ -79,6 +79,12 @@ pub struct Chunk {
79
79
  pub lines: Vec<(u32, u32)>,
80
80
  /// Source file path for error messages (`file:line`); propagated to nested chunks. Runtime-only.
81
81
  pub source: Option<Arc<str>>,
82
+ /// #187: when `Some(name)`, this chunk is a top-level `function name` whose binding is provably
83
+ /// stable across the whole program (never reassigned/shadowed/redeclared). The numeric JIT
84
+ /// registers such a chunk under `name` so a caller's `name(args)` can lower to a direct native
85
+ /// call. `None` for anonymous/nested/unstable functions. Runtime-only; not serialized (a reloaded
86
+ /// program just forgoes the cross-function-call optimization).
87
+ pub global_name: Option<Arc<str>>,
82
88
  }
83
89
 
84
90
  impl Chunk {
@@ -95,6 +101,7 @@ impl Chunk {
95
101
  inline_caches: InlineCaches::default(),
96
102
  lines: Vec::new(),
97
103
  source: None,
104
+ global_name: None,
98
105
  }
99
106
  }
100
107