@tishlang/tish 2.2.5 → 2.8.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.
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- package/crates/tish_ast/src/ast.rs +12 -5
- package/crates/tish_build_utils/src/lib.rs +37 -0
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +11 -10
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +16 -16
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +26 -9
- package/crates/tish_compile_js/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +46 -2
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +7 -1
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +913 -145
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +35 -35
- package/crates/tish_resolve/src/lib.rs +567 -17
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- 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
|
@@ -3,13 +3,70 @@
|
|
|
3
3
|
use crate::{Value, VmRef};
|
|
4
4
|
use std::sync::Arc;
|
|
5
5
|
|
|
6
|
+
/// Per-`json_parse`-call cache of object-key text → shared `Arc<str>`. A JSON array of records
|
|
7
|
+
/// repeats the same handful of keys across thousands of objects; interning allocates each key
|
|
8
|
+
/// once and `Arc::clone`s it thereafter, instead of a fresh `Arc<str>` allocation per occurrence.
|
|
9
|
+
type KeyCache = ahash::AHashMap<Box<str>, Arc<str>>;
|
|
10
|
+
|
|
11
|
+
#[inline]
|
|
12
|
+
fn intern_key(cache: &mut KeyCache, s: &str) -> Arc<str> {
|
|
13
|
+
if let Some(existing) = cache.get(s) {
|
|
14
|
+
return Arc::clone(existing);
|
|
15
|
+
}
|
|
16
|
+
let arc: Arc<str> = Arc::from(s);
|
|
17
|
+
cache.insert(Box::from(s), Arc::clone(&arc));
|
|
18
|
+
arc
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Append `n` to `buf` exactly as JS `JSON.stringify` formats a number. Integer-valued finite
|
|
22
|
+
/// numbers within the safe-integer range (`|n| < 2^53`) take a fast `i64` path — bit-identical to
|
|
23
|
+
/// JS for every such value (verified against Node over 100k values) and far cheaper than the `f64`
|
|
24
|
+
/// formatter. Everything else uses the ECMAScript `Number::toString` (`js_number_to_string`),
|
|
25
|
+
/// which matches JS where Rust's `{}` Display would not (e.g. `1e21`, `1e-7`). NaN/∞ → `null`.
|
|
26
|
+
/// Public so the interpreter's separate JSON path formats numbers identically (single source of
|
|
27
|
+
/// truth → interp/vm/native/node agree).
|
|
28
|
+
#[inline]
|
|
29
|
+
pub fn write_json_number(buf: &mut String, n: f64) {
|
|
30
|
+
if n.is_nan() || n.is_infinite() {
|
|
31
|
+
buf.push_str("null");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if n.fract() == 0.0 && n.abs() < 9_007_199_254_740_992.0 {
|
|
35
|
+
let mut b = itoa::Buffer::new();
|
|
36
|
+
buf.push_str(b.format(n as i64));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
crate::js_number_to_string_into(buf, n);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Scan a string body (the input with its opening `"` already removed) for the closing quote.
|
|
43
|
+
/// Returns `Ok(Some((body, rest)))` when the string is escape-free — `body` is the raw contents and
|
|
44
|
+
/// `rest` is the input just past the closing quote, so the value is built in a single allocation
|
|
45
|
+
/// with no per-char decode. Returns `Ok(None)` on the first backslash (caller uses the escape
|
|
46
|
+
/// decoder) and `Err` if unterminated. Only stops on the ASCII bytes `"`/`\\`, so the byte index
|
|
47
|
+
/// always lands on a UTF-8 char boundary — multi-byte sequences pass through inside `body` intact.
|
|
48
|
+
#[inline]
|
|
49
|
+
fn scan_escape_free(body: &str) -> Result<Option<(&str, &str)>, String> {
|
|
50
|
+
let bytes = body.as_bytes();
|
|
51
|
+
let mut i = 0;
|
|
52
|
+
while i < bytes.len() {
|
|
53
|
+
match bytes[i] {
|
|
54
|
+
b'\\' => return Ok(None),
|
|
55
|
+
b'"' => return Ok(Some((&body[..i], &body[i + 1..]))),
|
|
56
|
+
_ => i += 1,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
Err("Unterminated string".to_string())
|
|
60
|
+
}
|
|
61
|
+
|
|
6
62
|
/// Parse JSON string into a Value.
|
|
7
63
|
pub fn json_parse(json: &str) -> Result<Value, String> {
|
|
8
64
|
let json = json.trim();
|
|
9
65
|
if json.is_empty() {
|
|
10
66
|
return Err("SyntaxError: Unexpected end of JSON input".to_string());
|
|
11
67
|
}
|
|
12
|
-
let
|
|
68
|
+
let mut cache = KeyCache::default();
|
|
69
|
+
let (value, rest) = parse_value(json, 0, &mut cache)?;
|
|
13
70
|
if !rest.trim().is_empty() {
|
|
14
71
|
return Err("SyntaxError: Unexpected token at end of JSON".to_string());
|
|
15
72
|
}
|
|
@@ -39,17 +96,7 @@ pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
|
39
96
|
Value::Null => buf.push_str("null"),
|
|
40
97
|
Value::Bool(true) => buf.push_str("true"),
|
|
41
98
|
Value::Bool(false) => buf.push_str("false"),
|
|
42
|
-
Value::Number(n) =>
|
|
43
|
-
if n.is_nan() || n.is_infinite() {
|
|
44
|
-
buf.push_str("null");
|
|
45
|
-
} else {
|
|
46
|
-
// `write!` avoids the heap allocation that `to_string`
|
|
47
|
-
// produces. The f64 → decimal formatter is the same
|
|
48
|
-
// either way (`std::fmt::Display`).
|
|
49
|
-
use std::fmt::Write;
|
|
50
|
-
let _ = write!(buf, "{}", n);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
99
|
+
Value::Number(n) => write_json_number(buf, *n),
|
|
53
100
|
Value::String(s) => {
|
|
54
101
|
buf.push('"');
|
|
55
102
|
escape_json_string_into(buf, s);
|
|
@@ -69,16 +116,11 @@ pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
|
69
116
|
Value::NumberArray(arr) => {
|
|
70
117
|
let borrowed = arr.borrow();
|
|
71
118
|
buf.push('[');
|
|
72
|
-
use std::fmt::Write;
|
|
73
119
|
for (i, n) in borrowed.iter().enumerate() {
|
|
74
120
|
if i > 0 {
|
|
75
121
|
buf.push(',');
|
|
76
122
|
}
|
|
77
|
-
|
|
78
|
-
buf.push_str("null");
|
|
79
|
-
} else {
|
|
80
|
-
let _ = write!(buf, "{}", n);
|
|
81
|
-
}
|
|
123
|
+
write_json_number(buf, *n);
|
|
82
124
|
}
|
|
83
125
|
buf.push(']');
|
|
84
126
|
}
|
|
@@ -153,7 +195,11 @@ fn escape_json_string_into(buf: &mut String, s: &str) {
|
|
|
153
195
|
/// whole process (uncatchable, SIGABRT). 128 matches serde_json's default limit.
|
|
154
196
|
const MAX_JSON_DEPTH: usize = 128;
|
|
155
197
|
|
|
156
|
-
fn parse_value
|
|
198
|
+
fn parse_value<'a>(
|
|
199
|
+
input: &'a str,
|
|
200
|
+
depth: usize,
|
|
201
|
+
cache: &mut KeyCache,
|
|
202
|
+
) -> Result<(Value, &'a str), String> {
|
|
157
203
|
let input = input.trim_start();
|
|
158
204
|
if input.is_empty() {
|
|
159
205
|
return Err("Unexpected end of JSON input".to_string());
|
|
@@ -163,8 +209,8 @@ fn parse_value(input: &str, depth: usize) -> Result<(Value, &str), String> {
|
|
|
163
209
|
'n' => parse_null(input),
|
|
164
210
|
't' | 'f' => parse_bool(input),
|
|
165
211
|
'"' => parse_string(input),
|
|
166
|
-
'[' => parse_array(input, depth),
|
|
167
|
-
'{' => parse_object(input, depth),
|
|
212
|
+
'[' => parse_array(input, depth, cache),
|
|
213
|
+
'{' => parse_object(input, depth, cache),
|
|
168
214
|
c if c == '-' || c.is_ascii_digit() => parse_number(input),
|
|
169
215
|
c => Err(format!("Unexpected character '{}' in JSON", c)),
|
|
170
216
|
}
|
|
@@ -189,6 +235,31 @@ fn parse_bool(input: &str) -> Result<(Value, &str), String> {
|
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
fn parse_string(input: &str) -> Result<(Value, &str), String> {
|
|
238
|
+
// Fast path: an escape-free string is a direct slice of the input — one allocation, no decode.
|
|
239
|
+
let body = &input[1..]; // skip opening quote (ASCII, safe byte index)
|
|
240
|
+
if let Some((s, rest)) = scan_escape_free(body)? {
|
|
241
|
+
return Ok((Value::String(s.into()), rest));
|
|
242
|
+
}
|
|
243
|
+
parse_string_escaped(input)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Read an object key, interning it through `cache` so repeated keys share one `Arc<str>` instead
|
|
247
|
+
/// of allocating a fresh one per occurrence. Escape-free keys (the common case) look up the cache
|
|
248
|
+
/// with the borrowed slice — zero allocations on a hit.
|
|
249
|
+
fn parse_key<'a>(input: &'a str, cache: &mut KeyCache) -> Result<(Arc<str>, &'a str), String> {
|
|
250
|
+
let body = &input[1..];
|
|
251
|
+
if let Some((s, rest)) = scan_escape_free(body)? {
|
|
252
|
+
return Ok((intern_key(cache, s), rest));
|
|
253
|
+
}
|
|
254
|
+
let (val, rest) = parse_string_escaped(input)?;
|
|
255
|
+
match val {
|
|
256
|
+
Value::String(s) => Ok((intern_key(cache, s.as_str()), rest)),
|
|
257
|
+
_ => unreachable!("parse_string_escaped always yields Value::String"),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Decode a JSON string that contains at least one escape (the slow path).
|
|
262
|
+
fn parse_string_escaped(input: &str) -> Result<(Value, &str), String> {
|
|
192
263
|
let input = &input[1..]; // skip opening quote
|
|
193
264
|
let mut result = String::new();
|
|
194
265
|
let mut chars = input.chars().peekable();
|
|
@@ -267,19 +338,25 @@ fn parse_number(input: &str) -> Result<(Value, &str), String> {
|
|
|
267
338
|
let bytes = input.as_bytes();
|
|
268
339
|
let mut end = 0;
|
|
269
340
|
|
|
270
|
-
|
|
341
|
+
let neg = bytes.first() == Some(&b'-');
|
|
342
|
+
if neg {
|
|
271
343
|
end += 1;
|
|
272
344
|
}
|
|
345
|
+
let int_start = end;
|
|
273
346
|
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
274
347
|
end += 1;
|
|
275
348
|
}
|
|
349
|
+
let int_len = end - int_start;
|
|
350
|
+
let mut is_integer = true;
|
|
276
351
|
if bytes.get(end) == Some(&b'.') {
|
|
352
|
+
is_integer = false;
|
|
277
353
|
end += 1;
|
|
278
354
|
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
279
355
|
end += 1;
|
|
280
356
|
}
|
|
281
357
|
}
|
|
282
358
|
if matches!(bytes.get(end), Some(&b'e') | Some(&b'E')) {
|
|
359
|
+
is_integer = false;
|
|
283
360
|
end += 1;
|
|
284
361
|
if matches!(bytes.get(end), Some(&b'+') | Some(&b'-')) {
|
|
285
362
|
end += 1;
|
|
@@ -291,13 +368,27 @@ fn parse_number(input: &str) -> Result<(Value, &str), String> {
|
|
|
291
368
|
|
|
292
369
|
// `end` lands on an ASCII boundary, so slicing `input` by byte index is valid.
|
|
293
370
|
let num_str = &input[..end];
|
|
371
|
+
// Integer fast-path: a pure integer of ≤15 digits fits `i64` and is exact in `f64`
|
|
372
|
+
// (|value| < 10^15 < 2^53), so `i64 as f64` equals the full float parse but is far cheaper —
|
|
373
|
+
// and JSON arrays of records are integer-heavy. Negative zero is excluded: `i64` loses the
|
|
374
|
+
// sign, but `JSON.parse("-0")` must yield `-0.0` (matches std `f64` parse and Node).
|
|
375
|
+
if is_integer && (1..=15).contains(&int_len) {
|
|
376
|
+
if let Ok(i) = num_str.parse::<i64>() {
|
|
377
|
+
let n = if neg && i == 0 { -0.0 } else { i as f64 };
|
|
378
|
+
return Ok((Value::Number(n), &input[end..]));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
294
381
|
num_str
|
|
295
382
|
.parse::<f64>()
|
|
296
383
|
.map(|n| (Value::Number(n), &input[end..]))
|
|
297
384
|
.map_err(|_| format!("Invalid number: {}", num_str))
|
|
298
385
|
}
|
|
299
386
|
|
|
300
|
-
fn parse_array
|
|
387
|
+
fn parse_array<'a>(
|
|
388
|
+
input: &'a str,
|
|
389
|
+
depth: usize,
|
|
390
|
+
cache: &mut KeyCache,
|
|
391
|
+
) -> Result<(Value, &'a str), String> {
|
|
301
392
|
if depth >= MAX_JSON_DEPTH {
|
|
302
393
|
return Err("JSON nesting too deep".to_string());
|
|
303
394
|
}
|
|
@@ -310,7 +401,7 @@ fn parse_array(input: &str, depth: usize) -> Result<(Value, &str), String> {
|
|
|
310
401
|
}
|
|
311
402
|
|
|
312
403
|
loop {
|
|
313
|
-
let (value, rest) = parse_value(input, depth + 1)?;
|
|
404
|
+
let (value, rest) = parse_value(input, depth + 1, cache)?;
|
|
314
405
|
items.push(value);
|
|
315
406
|
input = rest.trim_start();
|
|
316
407
|
|
|
@@ -322,17 +413,28 @@ fn parse_array(input: &str, depth: usize) -> Result<(Value, &str), String> {
|
|
|
322
413
|
}
|
|
323
414
|
}
|
|
324
415
|
|
|
325
|
-
fn parse_object
|
|
416
|
+
fn parse_object<'a>(
|
|
417
|
+
input: &'a str,
|
|
418
|
+
depth: usize,
|
|
419
|
+
cache: &mut KeyCache,
|
|
420
|
+
) -> Result<(Value, &'a str), String> {
|
|
326
421
|
if depth >= MAX_JSON_DEPTH {
|
|
327
422
|
return Err("JSON nesting too deep".to_string());
|
|
328
423
|
}
|
|
329
424
|
let mut input = &input[1..]; // skip '{'
|
|
330
|
-
|
|
425
|
+
// Build the insertion-ordered `PropMap` directly. The old path collected into an `AHashMap`
|
|
426
|
+
// and then re-inserted every pair into a `PropMap` via `from_strings` — a wasted map + rehash
|
|
427
|
+
// per object, AND the `AHashMap` iteration order scrambled JSON key order (its `RandomState`
|
|
428
|
+
// reseeds per process), so `Object.keys` after `JSON.parse` came out in a non-spec order.
|
|
429
|
+
let mut map = crate::PropMap::new();
|
|
331
430
|
|
|
332
431
|
input = input.trim_start();
|
|
333
432
|
if let Some(rest) = input.strip_prefix('}') {
|
|
334
433
|
return Ok((
|
|
335
|
-
Value::Object(VmRef::new(crate::ObjectData
|
|
434
|
+
Value::Object(VmRef::new(crate::ObjectData {
|
|
435
|
+
strings: map,
|
|
436
|
+
symbols: None,
|
|
437
|
+
})),
|
|
336
438
|
rest,
|
|
337
439
|
));
|
|
338
440
|
}
|
|
@@ -343,11 +445,7 @@ fn parse_object(input: &str, depth: usize) -> Result<(Value, &str), String> {
|
|
|
343
445
|
return Err("Expected string key in object".to_string());
|
|
344
446
|
}
|
|
345
447
|
|
|
346
|
-
let (
|
|
347
|
-
let key: Arc<str> = match key_val {
|
|
348
|
-
Value::String(s) => Arc::from(s.as_str()),
|
|
349
|
-
_ => unreachable!(),
|
|
350
|
-
};
|
|
448
|
+
let (key, rest) = parse_key(input, cache)?;
|
|
351
449
|
|
|
352
450
|
input = rest.trim_start();
|
|
353
451
|
if !input.starts_with(':') {
|
|
@@ -355,7 +453,7 @@ fn parse_object(input: &str, depth: usize) -> Result<(Value, &str), String> {
|
|
|
355
453
|
}
|
|
356
454
|
input = &input[1..];
|
|
357
455
|
|
|
358
|
-
let (value, rest) = parse_value(input, depth + 1)?;
|
|
456
|
+
let (value, rest) = parse_value(input, depth + 1, cache)?;
|
|
359
457
|
map.insert(key, value);
|
|
360
458
|
input = rest.trim_start();
|
|
361
459
|
|
|
@@ -363,7 +461,10 @@ fn parse_object(input: &str, depth: usize) -> Result<(Value, &str), String> {
|
|
|
363
461
|
Some(',') => input = &input[1..],
|
|
364
462
|
Some('}') => {
|
|
365
463
|
return Ok((
|
|
366
|
-
Value::Object(VmRef::new(crate::ObjectData
|
|
464
|
+
Value::Object(VmRef::new(crate::ObjectData {
|
|
465
|
+
strings: map,
|
|
466
|
+
symbols: None,
|
|
467
|
+
})),
|
|
367
468
|
&input[1..],
|
|
368
469
|
));
|
|
369
470
|
}
|
|
@@ -12,9 +12,31 @@ mod value;
|
|
|
12
12
|
mod vmref;
|
|
13
13
|
|
|
14
14
|
pub use console_style::{format_value_styled, format_values_for_console, use_console_colors};
|
|
15
|
-
pub use json::{json_parse, json_stringify, json_stringify_into};
|
|
15
|
+
pub use json::{json_parse, json_stringify, json_stringify_into, write_json_number};
|
|
16
16
|
pub use shape::{ShapeId, DICT_SHAPE, EMPTY_SHAPE};
|
|
17
17
|
pub use uri::{percent_decode, percent_encode};
|
|
18
18
|
pub use arcstr::ArcStr;
|
|
19
19
|
pub use value::*;
|
|
20
20
|
pub use vmref::{VmReadGuard, VmRef, VmWriteGuard};
|
|
21
|
+
|
|
22
|
+
/// `process.argv` for the interpreter / VM. Defaults to the host process's own `std::env::args()`,
|
|
23
|
+
/// but `tish run <file> [args...]` overrides it (via [`set_process_argv`]) with a node-shaped argv
|
|
24
|
+
/// `[tish-exe, <file>, args...]` so a script sees its own args — not the `run` subcommand. Compiled
|
|
25
|
+
/// native binaries don't touch this; they read `std::env::args()` directly (which is already their
|
|
26
|
+
/// own argv). #88
|
|
27
|
+
static PROCESS_ARGV: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
|
|
28
|
+
|
|
29
|
+
/// Override `process.argv` for the interpreter/VM (set once by `tish run` before executing). A
|
|
30
|
+
/// later call is ignored (the first override wins), matching the single-program-per-process model.
|
|
31
|
+
pub fn set_process_argv(argv: Vec<String>) {
|
|
32
|
+
let _ = PROCESS_ARGV.set(argv);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// The argv a script should see as `process.argv`: the [`set_process_argv`] override if present,
|
|
36
|
+
/// else the host process's `std::env::args()`.
|
|
37
|
+
pub fn process_argv() -> Vec<String> {
|
|
38
|
+
match PROCESS_ARGV.get() {
|
|
39
|
+
Some(v) => v.clone(),
|
|
40
|
+
None => std::env::args().collect(),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -834,6 +834,31 @@ pub fn to_uint32(x: f64) -> u32 {
|
|
|
834
834
|
}
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
/// `ToNumber` for the native boxed runtime: a `Value::Number` passes through, every other variant
|
|
838
|
+
/// coerces to `NaN`. This is the same `as_number().unwrap_or(NaN)` convention used by the
|
|
839
|
+
/// interpreter (`binop_number` / `to_int32`), the VM (`eval_binop`), and `ops::add`/`sub`/`mul`/`div`
|
|
840
|
+
/// — so the native backend stays bit-for-bit in lock-step with them at runtime (a string/bool/null
|
|
841
|
+
/// operand is `NaN`, hence `0` for bitwise). The compiler constant-folds literal cases like
|
|
842
|
+
/// `"5" | 0 === 5` separately, so this only governs runtime (non-constant) operands.
|
|
843
|
+
#[inline]
|
|
844
|
+
pub fn to_number_value(v: &Value) -> f64 {
|
|
845
|
+
v.as_number().unwrap_or(f64::NAN)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/// `ToInt32` of an arbitrary [`Value`] (coerce via [`to_number_value`], then [`to_int32`]). Designed
|
|
849
|
+
/// to compose in generated code: unlike a `let Value::Number(a) = &(..) else { panic!() }` block,
|
|
850
|
+
/// it binds no name, so nested bitwise/shift operands can never shadow each other.
|
|
851
|
+
#[inline]
|
|
852
|
+
pub fn to_int32_value(v: &Value) -> i32 {
|
|
853
|
+
to_int32(to_number_value(v))
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/// `ToUint32` companion to [`to_int32_value`].
|
|
857
|
+
#[inline]
|
|
858
|
+
pub fn to_uint32_value(v: &Value) -> u32 {
|
|
859
|
+
to_uint32(to_number_value(v))
|
|
860
|
+
}
|
|
861
|
+
|
|
837
862
|
/// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
|
|
838
863
|
pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
839
864
|
match callee {
|
|
@@ -910,20 +935,36 @@ impl std::fmt::Debug for Value {
|
|
|
910
935
|
/// We take the shortest round-tripping digits from Rust's `{:e}` (a Ryū/Grisu-class shortest
|
|
911
936
|
/// formatter, matching V8's digit choice) and lay them out per the ECMAScript rule: plain
|
|
912
937
|
/// decimal when the point position `n` is in `(-6, 21]`, otherwise `d[.ddd]e±E` with `E = n-1`
|
|
913
|
-
/// (sign always shown, no leading zeros in the exponent).
|
|
914
|
-
/// `
|
|
938
|
+
/// (sign always shown, no leading zeros in the exponent). This is the ECMAScript `Number::toString`
|
|
939
|
+
/// used by `.toString()`, `String(n)`, `+` concatenation, and template literals, so `-0` renders as
|
|
940
|
+
/// `"0"` (the sign is dropped — `(-0).toString() === "0"`). The *inspect* form (`console.log` of a
|
|
941
|
+
/// bare number / array element) keeps `"-0"`; that distinction lives in [`Value::to_display_string`].
|
|
915
942
|
pub fn js_number_to_string(value: f64) -> String {
|
|
943
|
+
let mut out = String::new();
|
|
944
|
+
js_number_to_string_into(&mut out, value);
|
|
945
|
+
out
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/// Append the ECMAScript `Number::toString` of `value` to `out` — identical result to
|
|
949
|
+
/// [`js_number_to_string`] but with no intermediate `String` allocation. The JSON.stringify
|
|
950
|
+
/// hot path appends millions of numbers into a single buffer, so the alloc-free form matters.
|
|
951
|
+
pub fn js_number_to_string_into(out: &mut String, value: f64) {
|
|
916
952
|
if value.is_nan() {
|
|
917
|
-
|
|
953
|
+
out.push_str("NaN");
|
|
954
|
+
return;
|
|
918
955
|
}
|
|
919
956
|
if value == f64::INFINITY {
|
|
920
|
-
|
|
957
|
+
out.push_str("Infinity");
|
|
958
|
+
return;
|
|
921
959
|
}
|
|
922
960
|
if value == f64::NEG_INFINITY {
|
|
923
|
-
|
|
961
|
+
out.push_str("-Infinity");
|
|
962
|
+
return;
|
|
924
963
|
}
|
|
925
964
|
if value == 0.0 {
|
|
926
|
-
|
|
965
|
+
// ECMAScript `Number::toString`: both `+0` and `-0` stringify to `"0"`.
|
|
966
|
+
out.push('0');
|
|
967
|
+
return;
|
|
927
968
|
}
|
|
928
969
|
|
|
929
970
|
let negative = value < 0.0;
|
|
@@ -939,7 +980,6 @@ pub fn js_number_to_string(value: f64) -> String {
|
|
|
939
980
|
let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
|
|
940
981
|
let point = exp + 1; // ECMAScript's `n`: value = digits × 10^(point − k)
|
|
941
982
|
|
|
942
|
-
let mut out = String::new();
|
|
943
983
|
if negative {
|
|
944
984
|
out.push('-');
|
|
945
985
|
}
|
|
@@ -969,13 +1009,15 @@ pub fn js_number_to_string(value: f64) -> String {
|
|
|
969
1009
|
out.push(if e >= 0 { '+' } else { '-' });
|
|
970
1010
|
out.push_str(&e.abs().to_string());
|
|
971
1011
|
}
|
|
972
|
-
out
|
|
973
1012
|
}
|
|
974
1013
|
|
|
975
1014
|
impl Value {
|
|
976
1015
|
/// Convert value to display string (for console output).
|
|
977
1016
|
pub fn to_display_string(&self) -> String {
|
|
978
1017
|
match self {
|
|
1018
|
+
// Inspect form keeps the sign of negative zero (`console.log(-0)` → `-0`), unlike the
|
|
1019
|
+
// ECMAScript ToString used by `to_js_string`. See `js_number_to_string`. (#247)
|
|
1020
|
+
Value::Number(n) if *n == 0.0 && n.is_sign_negative() => "-0".to_string(),
|
|
979
1021
|
Value::Number(n) => js_number_to_string(*n),
|
|
980
1022
|
Value::String(s) => s.to_string(),
|
|
981
1023
|
Value::Bool(b) => b.to_string(),
|
|
@@ -1046,6 +1088,9 @@ impl Value {
|
|
|
1046
1088
|
.collect::<Vec<_>>()
|
|
1047
1089
|
.join(","),
|
|
1048
1090
|
Value::Object(_) => "[object Object]".to_string(),
|
|
1091
|
+
// ECMAScript ToString of a number (drops `-0`'s sign), distinct from the inspect form
|
|
1092
|
+
// that `to_display_string` would give for `-0`. (#247)
|
|
1093
|
+
Value::Number(n) => js_number_to_string(*n),
|
|
1049
1094
|
// Primitives (and the remaining cases) coincide with the display form.
|
|
1050
1095
|
_ => self.to_display_string(),
|
|
1051
1096
|
}
|
|
@@ -1148,12 +1193,18 @@ impl Value {
|
|
|
1148
1193
|
// -------------------------------------------------------------------------
|
|
1149
1194
|
|
|
1150
1195
|
/// Whether packed f64 arrays are enabled this run. Default: **off** (`TISH_PACKED_ARRAYS=1`
|
|
1151
|
-
/// opts in).
|
|
1196
|
+
/// opts in). Read once per process and cached — the VM calls this once per executed array
|
|
1197
|
+
/// literal, and a `std::env::var` there is a libc env lock plus a `String` allocation per
|
|
1198
|
+
/// `NewArray` for a flag that never changes after startup (#166). Set the variable before the
|
|
1199
|
+
/// process starts (as the CI sweep does); mid-process toggling is not observed by design.
|
|
1152
1200
|
/// The flag is intentionally backwards from the slot/JIT flags (those were default-on) to
|
|
1153
1201
|
/// keep the default binary behaviour byte-identical while we validate coverage.
|
|
1154
1202
|
#[inline]
|
|
1155
1203
|
pub fn packed_arrays_enabled() -> bool {
|
|
1156
|
-
std::
|
|
1204
|
+
static PACKED_ARRAYS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
|
1205
|
+
*PACKED_ARRAYS.get_or_init(|| {
|
|
1206
|
+
std::env::var("TISH_PACKED_ARRAYS").map(|v| v == "1").unwrap_or(false)
|
|
1207
|
+
})
|
|
1157
1208
|
}
|
|
1158
1209
|
|
|
1159
1210
|
/// Wrap a `Vec<f64>` as a `Value::NumberArray`. Only call when `packed_arrays_enabled()`.
|
|
@@ -1310,9 +1361,11 @@ mod number_to_string_tests {
|
|
|
1310
1361
|
#[test]
|
|
1311
1362
|
fn matches_javascript_number_tostring() {
|
|
1312
1363
|
// (value, expected) — every `expected` is what Node's `String(value)` produces.
|
|
1364
|
+
// `String(-0) === "0"`: ToString drops the sign of negative zero (the inspect form keeps
|
|
1365
|
+
// it, but that path is `Value::to_display_string`, not this function). (#247)
|
|
1313
1366
|
let cases: &[(f64, &str)] = &[
|
|
1314
1367
|
(0.0, "0"),
|
|
1315
|
-
(-0.0, "
|
|
1368
|
+
(-0.0, "0"),
|
|
1316
1369
|
(123.0, "123"),
|
|
1317
1370
|
(123.456, "123.456"),
|
|
1318
1371
|
(0.5, "0.5"),
|