@tishlang/tish 2.2.7 → 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 +6 -2
- 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 +1 -1
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +4 -4
- 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 +17 -0
- 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 +45 -1
- 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 +6 -0
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +856 -140
- 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 +15 -26
- package/crates/tish_resolve/src/lib.rs +188 -18
- 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
|
@@ -558,7 +558,7 @@ pub(crate) fn pi_mentions(e: &Expr, name: &str) -> bool {
|
|
|
558
558
|
}
|
|
559
559
|
}),
|
|
560
560
|
Object { props, .. } => props.iter().any(|p| match p {
|
|
561
|
-
tishlang_ast::ObjectProp::KeyValue(_, v) => pi_mentions(v, name),
|
|
561
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => pi_mentions(v, name),
|
|
562
562
|
tishlang_ast::ObjectProp::Spread(x) => pi_mentions(x, name),
|
|
563
563
|
}),
|
|
564
564
|
TemplateLiteral { exprs, .. } => exprs.iter().any(|x| pi_mentions(x, name)),
|
|
@@ -634,7 +634,7 @@ fn infer_object_shape(
|
|
|
634
634
|
let mut fields = Vec::with_capacity(props.len());
|
|
635
635
|
for p in props {
|
|
636
636
|
match p {
|
|
637
|
-
tishlang_ast::ObjectProp::KeyValue(k, v) => {
|
|
637
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
|
|
638
638
|
let ty = infer_expr_type(v, ctx)?;
|
|
639
639
|
// Only primitive field types in this conservative version.
|
|
640
640
|
if !matches!(&ty, TypeAnnotation::Simple(s, _)
|
|
@@ -1303,7 +1303,7 @@ fn arr_expr_safe(e: &Expr, name: &str) -> bool {
|
|
|
1303
1303
|
}
|
|
1304
1304
|
}),
|
|
1305
1305
|
Object { props, .. } => props.iter().all(|p| match p {
|
|
1306
|
-
tishlang_ast::ObjectProp::KeyValue(_, v) => arr_expr_safe(v, name),
|
|
1306
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => arr_expr_safe(v, name),
|
|
1307
1307
|
tishlang_ast::ObjectProp::Spread(v) => arr_expr_safe(v, name),
|
|
1308
1308
|
}),
|
|
1309
1309
|
// Anything else that mentions `name`: be safe, bail.
|
|
@@ -1423,7 +1423,7 @@ fn expr_name_safe(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>)
|
|
|
1423
1423
|
}
|
|
1424
1424
|
}),
|
|
1425
1425
|
Object { props, .. } => props.iter().all(|p| match p {
|
|
1426
|
-
tishlang_ast::ObjectProp::KeyValue(_, v) => expr_name_safe(v, name, keys),
|
|
1426
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => expr_name_safe(v, name, keys),
|
|
1427
1427
|
tishlang_ast::ObjectProp::Spread(e) => expr_name_safe(e, name, keys),
|
|
1428
1428
|
}),
|
|
1429
1429
|
TemplateLiteral { exprs, .. } => exprs.iter().all(|e| expr_name_safe(e, name, keys)),
|
|
@@ -186,6 +186,44 @@ fn factory() {
|
|
|
186
186
|
"expected outerVar to be inferred as f64 (Copy, no clone needed)"
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
/// i32-loop-var lowering: an FNV-style integer/bitwise hash accumulator declared before a `for`
|
|
191
|
+
/// and reassigned only by `>>> 0`/bitwise ops lives in an `i32` register across the loop — no
|
|
192
|
+
/// per-op `to_int32(h)`↔`f64` round-trip — with a single `f64` excursion for the `h * C`
|
|
193
|
+
/// multiply (which exceeds 2^53, so it must round in f64 before `>>> 0`, exactly as V8 does).
|
|
194
|
+
#[test]
|
|
195
|
+
fn fnv_accumulator_lowers_to_i32_register() {
|
|
196
|
+
let src = r#"
|
|
197
|
+
let h = 2166136261
|
|
198
|
+
for (let i = 0; i < 100; i++) {
|
|
199
|
+
h = h ^ (i & 255)
|
|
200
|
+
h = (h * 16777619) >>> 0
|
|
201
|
+
h = ((h << 13) | (h >>> 19)) >>> 0
|
|
202
|
+
}
|
|
203
|
+
let check = h >>> 0
|
|
204
|
+
console.log(check)
|
|
205
|
+
"#;
|
|
206
|
+
let rust = compile(&parse(src).unwrap()).unwrap();
|
|
207
|
+
// The accumulator is an i32 register, initialized via the u32 reinterpretation so the
|
|
208
|
+
// > i32::MAX seed keeps its JS ToInt32 bit-pattern.
|
|
209
|
+
assert!(
|
|
210
|
+
rust.contains("let mut h: i32 = (2166136261u32) as i32;"),
|
|
211
|
+
"expected `h` to be an i32 register seeded via u32 reinterpretation; got:\n{}",
|
|
212
|
+
rust.lines().filter(|l| l.contains("h")).take(8).collect::<Vec<_>>().join("\n")
|
|
213
|
+
);
|
|
214
|
+
// The per-iteration `to_int32(h)` round-trips are gone: `h` is read straight from the
|
|
215
|
+
// register inside the bitwise chain (no `to_int32(h)` substring referencing the accumulator).
|
|
216
|
+
assert!(
|
|
217
|
+
!rust.contains("to_int32(h)"),
|
|
218
|
+
"expected NO per-op `to_int32(h)` round-trip on the i32 accumulator"
|
|
219
|
+
);
|
|
220
|
+
// The only f64 excursion is the multiply, lowered as an unchecked truncation of the
|
|
221
|
+
// provably-finite product (`h as f64 * 16777619`).
|
|
222
|
+
assert!(
|
|
223
|
+
rust.contains(".to_int_unchecked::<i64>()") && rust.contains("16777619"),
|
|
224
|
+
"expected the `h * 16777619` excursion to lower to an unchecked f64 truncation"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
189
227
|
}
|
|
190
228
|
|
|
191
229
|
#[cfg(test)]
|
|
@@ -173,7 +173,7 @@ pub fn program_uses_document(program: &Program) -> bool {
|
|
|
173
173
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_uses_document(e),
|
|
174
174
|
}),
|
|
175
175
|
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
176
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_uses_document(e),
|
|
176
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_uses_document(e),
|
|
177
177
|
}),
|
|
178
178
|
Expr::Assign { value, .. }
|
|
179
179
|
| Expr::CompoundAssign { value, .. }
|
|
@@ -1601,7 +1601,7 @@ fn rewrite_expr_scope(expr: &mut Expr, active: &HashMap<String, Arc<str>>) {
|
|
|
1601
1601
|
Expr::Object { props, .. } => {
|
|
1602
1602
|
for p in props {
|
|
1603
1603
|
match p {
|
|
1604
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
|
|
1604
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
|
|
1605
1605
|
rewrite_expr_scope(e, active) // key is a property name; value recurses
|
|
1606
1606
|
}
|
|
1607
1607
|
}
|
|
@@ -1819,6 +1819,7 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1819
1819
|
name: Arc::from(v.clone()),
|
|
1820
1820
|
span: *span,
|
|
1821
1821
|
},
|
|
1822
|
+
*name_span,
|
|
1822
1823
|
));
|
|
1823
1824
|
}
|
|
1824
1825
|
merge_push(
|
|
@@ -13,6 +13,12 @@ pub enum RustType {
|
|
|
13
13
|
Value,
|
|
14
14
|
/// f64 (for number)
|
|
15
15
|
F64,
|
|
16
|
+
/// i32 — a `number` local PROVEN to always hold an integer reinterpretable as a JS ToInt32
|
|
17
|
+
/// bit-pattern, kept in an integer register across a bitwise/hash hot loop (bun/JSC-style)
|
|
18
|
+
/// instead of round-tripping `f64`↔`i32` on every op. The value is the signed int32
|
|
19
|
+
/// (= JS `ToInt32`) view; `>>> 0` results are uint32 reinterpreted into this i32. Only the
|
|
20
|
+
/// codegen's i32-loop-var lowering produces this type — never `from_annotation`.
|
|
21
|
+
I32,
|
|
16
22
|
/// String (for string)
|
|
17
23
|
String,
|
|
18
24
|
/// bool (for boolean)
|
|
@@ -222,6 +228,7 @@ impl RustType {
|
|
|
222
228
|
match self {
|
|
223
229
|
RustType::Value => "Value".to_string(),
|
|
224
230
|
RustType::F64 => "f64".to_string(),
|
|
231
|
+
RustType::I32 => "i32".to_string(),
|
|
225
232
|
RustType::String => "String".to_string(),
|
|
226
233
|
RustType::Bool => "bool".to_string(),
|
|
227
234
|
RustType::Unit => "()".to_string(),
|
|
@@ -250,6 +257,7 @@ impl RustType {
|
|
|
250
257
|
match self {
|
|
251
258
|
RustType::Value => "Value::Null".to_string(),
|
|
252
259
|
RustType::F64 => "0.0".to_string(),
|
|
260
|
+
RustType::I32 => "0i32".to_string(),
|
|
253
261
|
RustType::String => "String::new()".to_string(),
|
|
254
262
|
RustType::Bool => "false".to_string(),
|
|
255
263
|
RustType::Unit => "()".to_string(),
|
|
@@ -306,6 +314,12 @@ impl RustType {
|
|
|
306
314
|
"match &{} {{ Value::Number(n) => *n, _ => panic!(\"expected number\") }}",
|
|
307
315
|
value_expr
|
|
308
316
|
),
|
|
317
|
+
// A `Value::Number` narrowed to its JS ToInt32 bit-pattern (NaN/±Inf → 0, exactly as
|
|
318
|
+
// the bitwise/shift path coerces). Only reached for an `I32`-typed binding boundary.
|
|
319
|
+
RustType::I32 => format!(
|
|
320
|
+
"match &{} {{ Value::Number(n) => tishlang_runtime::to_int32(*n), _ => panic!(\"expected number\") }}",
|
|
321
|
+
value_expr
|
|
322
|
+
),
|
|
309
323
|
RustType::String => format!(
|
|
310
324
|
"match &{} {{ Value::String(s) => s.to_string(), _ => panic!(\"expected string\") }}",
|
|
311
325
|
value_expr
|
|
@@ -363,6 +377,9 @@ impl RustType {
|
|
|
363
377
|
}
|
|
364
378
|
RustType::Value => native_expr.to_string(),
|
|
365
379
|
RustType::F64 => format!("Value::Number({})", native_expr),
|
|
380
|
+
// The signed int32 view boxes as a JS Number (every i32 is exactly representable in
|
|
381
|
+
// f64). The uint32 `>>> 0` reinterpretation is applied at the boxing site, not here.
|
|
382
|
+
RustType::I32 => format!("Value::Number(({}) as f64)", native_expr),
|
|
366
383
|
RustType::String => format!("Value::String({}.clone().into())", native_expr),
|
|
367
384
|
RustType::Bool => format!("Value::Bool({})", native_expr),
|
|
368
385
|
RustType::Unit => "Value::Null".to_string(),
|
|
@@ -487,9 +487,29 @@ impl Codegen {
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
/// Is `expr` the `null` literal? Used to lower `x === null` / `x !== null` to JS `== null` /
|
|
491
|
+
/// `!= null` so the check catches `undefined` too (tish treats a missing/undefined value as
|
|
492
|
+
/// null on every other backend) — see the StrictEq/StrictNe arms below.
|
|
493
|
+
fn is_null_literal(expr: &Expr) -> bool {
|
|
494
|
+
matches!(
|
|
495
|
+
expr,
|
|
496
|
+
Expr::Literal {
|
|
497
|
+
value: Literal::Null,
|
|
498
|
+
..
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
490
503
|
fn emit_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
|
|
491
504
|
Ok(match expr {
|
|
492
505
|
Expr::Literal { value, .. } => match value {
|
|
506
|
+
// Rust's `{}` prints non-finite f64 as `inf` / `-inf` / `NaN`; only `NaN` is valid JS.
|
|
507
|
+
// Emit the JS spellings so a folded `1/0` / `-1/0` doesn't become an undefined `inf`
|
|
508
|
+
// identifier in the output.
|
|
509
|
+
Literal::Number(n) if n.is_nan() => "NaN".to_string(),
|
|
510
|
+
Literal::Number(n) if n.is_infinite() => {
|
|
511
|
+
if *n < 0.0 { "-Infinity".to_string() } else { "Infinity".to_string() }
|
|
512
|
+
}
|
|
493
513
|
Literal::Number(n) => format!("{}", n),
|
|
494
514
|
Literal::String(s) => format!("{:?}", s.as_ref()),
|
|
495
515
|
Literal::Bool(b) => format!("{}", b),
|
|
@@ -510,8 +530,26 @@ impl Codegen {
|
|
|
510
530
|
BinOp::Pow => "**",
|
|
511
531
|
BinOp::Eq => "==",
|
|
512
532
|
BinOp::Ne => "!=",
|
|
513
|
-
|
|
514
|
-
|
|
533
|
+
// tish has no `undefined`: a missing/absent value reads back as `null`, so
|
|
534
|
+
// `x === null` means "is nullish" — exactly how interp/vm/native behave (a
|
|
535
|
+
// missing property is null there). In the JS runtime a missing property is
|
|
536
|
+
// `undefined`, so lower `=== null` / `!== null` to loose `== null` / `!= null`,
|
|
537
|
+
// which match BOTH null and undefined — keeping `=== null` mean the same thing on
|
|
538
|
+
// every target. Strict equality between non-null operands is unaffected.
|
|
539
|
+
BinOp::StrictEq => {
|
|
540
|
+
if Self::is_null_literal(left) || Self::is_null_literal(right) {
|
|
541
|
+
"=="
|
|
542
|
+
} else {
|
|
543
|
+
"==="
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
BinOp::StrictNe => {
|
|
547
|
+
if Self::is_null_literal(left) || Self::is_null_literal(right) {
|
|
548
|
+
"!="
|
|
549
|
+
} else {
|
|
550
|
+
"!=="
|
|
551
|
+
}
|
|
552
|
+
}
|
|
515
553
|
BinOp::Lt => "<",
|
|
516
554
|
BinOp::Le => "<=",
|
|
517
555
|
BinOp::Gt => ">",
|
|
@@ -563,6 +601,15 @@ impl Codegen {
|
|
|
563
601
|
..
|
|
564
602
|
} => {
|
|
565
603
|
let obj = self.emit_expr(object)?;
|
|
604
|
+
// `255.toString()` is a JS syntax error — the lexer reads `255.` as a float and
|
|
605
|
+
// then chokes on the method name. Parenthesize a numeric-literal object so member
|
|
606
|
+
// access / method calls stay valid: `(255).toString()`. (Folded constants reach
|
|
607
|
+
// codegen as number literals too, so this covers e.g. `(100 * 2).toString()`.)
|
|
608
|
+
let obj = if matches!(&**object, Expr::Literal { value: Literal::Number(_), .. }) {
|
|
609
|
+
format!("({})", obj)
|
|
610
|
+
} else {
|
|
611
|
+
obj
|
|
612
|
+
};
|
|
566
613
|
let expr = match prop {
|
|
567
614
|
MemberProp::Name { name, .. } => {
|
|
568
615
|
if name.parse::<u32>().is_ok()
|
|
@@ -629,7 +676,7 @@ impl Codegen {
|
|
|
629
676
|
let parts: Result<Vec<_>, _> = props
|
|
630
677
|
.iter()
|
|
631
678
|
.map(|p| match p {
|
|
632
|
-
ObjectProp::KeyValue(k, v) => {
|
|
679
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
633
680
|
let key = k.as_ref();
|
|
634
681
|
let val = self.emit_expr(v)?;
|
|
635
682
|
Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
|
@@ -650,7 +697,11 @@ impl Codegen {
|
|
|
650
697
|
}
|
|
651
698
|
Expr::TypeOf { operand, .. } => {
|
|
652
699
|
let o = self.emit_expr(operand)?;
|
|
653
|
-
|
|
700
|
+
// tish `typeof null` is "null" (interp/vm/native all agree — null is a first-class
|
|
701
|
+
// type, not JS's `typeof null === "object"` wart). tish has no `undefined`, so any
|
|
702
|
+
// nullish operand (incl. a JS-runtime `undefined`) maps to "null". Evaluate the
|
|
703
|
+
// operand once via the arrow arg so side effects don't run twice.
|
|
704
|
+
format!("((__v) => __v == null ? \"null\" : typeof __v)({})", o)
|
|
654
705
|
}
|
|
655
706
|
Expr::Delete { target, .. } => {
|
|
656
707
|
// Emit the raw property *reference*, not a value: `emit_expr` wraps Index /
|
|
@@ -411,4 +411,48 @@ fn factory() {
|
|
|
411
411
|
);
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
|
+
|
|
415
|
+
// tish `=== null` / `!== null` lower to JS `== null` / `!= null` so the nullish check catches the
|
|
416
|
+
// JS-runtime `undefined` (missing props / holes) too — matching interp/vm/native, which read a
|
|
417
|
+
// missing property back as null. Strict equality between non-null operands stays strict.
|
|
418
|
+
#[test]
|
|
419
|
+
fn strict_eq_null_lowers_to_loose_null() {
|
|
420
|
+
let program = parse("let x = 1\nconsole.log(x === null)\nconsole.log(x !== null)\n").unwrap();
|
|
421
|
+
let js = crate::compile(&program, false).unwrap();
|
|
422
|
+
assert!(!js.contains("=== null"), "`=== null` must lower to `== null`:\n{js}");
|
|
423
|
+
assert!(!js.contains("!== null"), "`!== null` must lower to `!= null`:\n{js}");
|
|
424
|
+
assert!(
|
|
425
|
+
js.contains("== null") && js.contains("!= null"),
|
|
426
|
+
"expected loose null checks:\n{js}"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
fn strict_eq_between_non_null_operands_stays_strict() {
|
|
432
|
+
let program = parse("let a = 1\nlet b = 2\nconsole.log(a === b)\nconsole.log(a !== b)\n").unwrap();
|
|
433
|
+
let js = crate::compile(&program, false).unwrap();
|
|
434
|
+
assert!(js.contains("==="), "non-null `===` must stay strict:\n{js}");
|
|
435
|
+
assert!(js.contains("!=="), "non-null `!==` must stay strict:\n{js}");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// `typeof null` is "null" in tish (interp/vm/native agree — null is a first-class type), not JS's
|
|
439
|
+
// `typeof null === "object"` wart. The JS backend must map a nullish operand to "null".
|
|
440
|
+
#[test]
|
|
441
|
+
fn typeof_null_emits_null_not_object() {
|
|
442
|
+
let program = parse("console.log(typeof null)\n").unwrap();
|
|
443
|
+
let js = crate::compile(&program, false).unwrap();
|
|
444
|
+
assert!(!js.contains("(typeof null)"), "must not emit raw `typeof null`:\n{js}");
|
|
445
|
+
assert!(js.contains("\"null\""), "typeof of a nullish value must yield \"null\":\n{js}");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 1/0 and -1/0 fold to Infinity / -Infinity; emit the JS spellings, not Rust's `inf` / `-inf`
|
|
449
|
+
// (which would be undefined identifiers in the output).
|
|
450
|
+
#[test]
|
|
451
|
+
fn non_finite_number_literals_use_js_spellings() {
|
|
452
|
+
let pos = crate::compile(&parse("console.log(1 / 0)\n").unwrap(), true).unwrap();
|
|
453
|
+
assert!(pos.contains("Infinity"), "1/0 must emit Infinity:\n{pos}");
|
|
454
|
+
assert!(!pos.contains("inf"), "must not emit Rust's lowercase `inf`:\n{pos}");
|
|
455
|
+
let neg = crate::compile(&parse("console.log(-1 / 0)\n").unwrap(), true).unwrap();
|
|
456
|
+
assert!(neg.contains("-Infinity"), "-1/0 must emit -Infinity:\n{neg}");
|
|
457
|
+
}
|
|
414
458
|
}
|
|
@@ -23,6 +23,8 @@ ahash = "0.8.11"
|
|
|
23
23
|
arcstr = "1"
|
|
24
24
|
smallvec = "1"
|
|
25
25
|
indexmap = "2"
|
|
26
|
+
# Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
|
|
27
|
+
itoa = "1"
|
|
26
28
|
fancy-regex = { version = "0.17.0", optional = true }
|
|
27
29
|
# Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
|
|
28
30
|
# single atomic with no pthread syscall — profiled as a top cost on object/array access.
|
|
@@ -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
|
+
}
|