@tishlang/tish 2.12.0 → 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.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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().
|
|
200
|
-
Value::Bool(s.borrow().contains_key(&
|
|
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().
|
|
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(&
|
|
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().
|
|
303
|
-
s.borrow().get(&
|
|
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().
|
|
313
|
-
Value::Bool(s.borrow().contains_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().
|
|
323
|
-
Value::Bool(s.borrow_mut().shift_remove(&
|
|
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
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
let idx = if i < 0 {
|
|
402
|
-
if idx >= 0
|
|
403
|
-
|
|
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
|
|