@tishlang/tish-format 1.0.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. package/README.md +0 -138
@@ -41,6 +41,8 @@ pub enum RustType {
41
41
  params: Vec<RustType>,
42
42
  returns: Box<RustType>,
43
43
  },
44
+ /// Tuple `(T0, T1, …)` for `[T0, T1]` tuple types — a native Rust tuple.
45
+ Tuple(Vec<RustType>),
44
46
  }
45
47
 
46
48
  impl RustType {
@@ -124,6 +126,38 @@ impl RustType {
124
126
  // Other unions fall back to Value
125
127
  RustType::Value
126
128
  }
129
+ // `[T0, T1]` -> a native Rust tuple `(T0, T1)`.
130
+ TypeAnnotation::Tuple(elems) => RustType::Tuple(
131
+ elems
132
+ .iter()
133
+ .map(|e| Self::from_annotation_with_aliases(e, aliases))
134
+ .collect(),
135
+ ),
136
+ // A literal type lowers to its base primitive.
137
+ TypeAnnotation::Literal(lit) => match lit {
138
+ tishlang_ast::TypeLiteral::Str(_) => RustType::String,
139
+ tishlang_ast::TypeLiteral::Num(_) => RustType::F64,
140
+ tishlang_ast::TypeLiteral::Bool(_) => RustType::Bool,
141
+ },
142
+ // Intersection of object shapes (e.g. `interface X extends Y { … }` → `Y & { … }`):
143
+ // merge the fields into one shape. Registered as a `type` alias, this becomes a native
144
+ // struct. Any non-object member → can't merge → fall back to boxed `Value`.
145
+ TypeAnnotation::Intersection(parts) => {
146
+ let mut fields: Vec<(Arc<str>, RustType)> = Vec::new();
147
+ for p in parts {
148
+ match Self::from_annotation_with_aliases(p, aliases) {
149
+ RustType::Object(fs) | RustType::Named { fields: fs, .. } => {
150
+ for (k, v) in fs {
151
+ if !fields.iter().any(|(ek, _)| *ek == k) {
152
+ fields.push((k, v));
153
+ }
154
+ }
155
+ }
156
+ _ => return RustType::Value,
157
+ }
158
+ }
159
+ RustType::Object(fields)
160
+ }
127
161
  }
128
162
  }
129
163
 
@@ -145,6 +179,15 @@ impl RustType {
145
179
  BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
146
180
  Some(RustType::F64)
147
181
  }
182
+ // Bitwise / shift ops: JS coerces both sides to int32, computes, and
183
+ // returns a Number — so the native result is still F64. Big win for
184
+ // crypto/hashing loops that would otherwise box every `^`/`>>>`.
185
+ BinOp::BitAnd
186
+ | BinOp::BitOr
187
+ | BinOp::BitXor
188
+ | BinOp::Shl
189
+ | BinOp::Shr
190
+ | BinOp::UShr => Some(RustType::F64),
148
191
  BinOp::Lt
149
192
  | BinOp::Le
150
193
  | BinOp::Gt
@@ -159,6 +202,16 @@ impl RustType {
159
202
  BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
160
203
  _ => None,
161
204
  }
205
+ } else if lhs == &RustType::String && rhs == &RustType::String {
206
+ // M2: native string concat + value equality. `+` concatenates; `===`/`!==` compare by
207
+ // value (byte-identical to JS and to the boxed `Value::String` path). Relational
208
+ // `< <= > >=` deliberately stay on the boxed path: JS orders strings by UTF-16 code
209
+ // units while Rust `String` orders by UTF-8 bytes — they diverge outside the BMP.
210
+ match op {
211
+ BinOp::Add => Some(RustType::String),
212
+ BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
213
+ _ => None,
214
+ }
162
215
  } else {
163
216
  None
164
217
  }
@@ -188,6 +241,7 @@ impl RustType {
188
241
  returns.to_rust_type_str()
189
242
  )
190
243
  }
244
+ RustType::Tuple(elems) => tuple_text(&elems.iter().map(|e| e.to_rust_type_str()).collect::<Vec<_>>()),
191
245
  }
192
246
  }
193
247
 
@@ -220,12 +274,33 @@ impl RustType {
220
274
  )
221
275
  }
222
276
  RustType::Function { .. } => "Value::Null".to_string(),
277
+ RustType::Tuple(elems) => {
278
+ tuple_text(&elems.iter().map(|e| e.default_value()).collect::<Vec<_>>())
279
+ }
223
280
  }
224
281
  }
225
282
 
226
283
  /// Generate code to convert from Value to this native type.
227
284
  pub fn from_value_expr(&self, value_expr: &str) -> String {
228
285
  match self {
286
+ RustType::Tuple(elems) => {
287
+ // `Value::Array([..])` -> `(e0, e1, …)`, converting each slot from its `Value`.
288
+ let parts: Vec<String> = elems
289
+ .iter()
290
+ .enumerate()
291
+ .map(|(i, e)| {
292
+ e.from_value_expr(&format!(
293
+ "_t.get({}).cloned().unwrap_or(Value::Null)",
294
+ i
295
+ ))
296
+ })
297
+ .collect();
298
+ format!(
299
+ "match &{} {{ Value::Array(_a) => {{ let _t = _a.borrow(); {} }}, _ => panic!(\"expected tuple\") }}",
300
+ value_expr,
301
+ tuple_text(&parts)
302
+ )
303
+ }
229
304
  RustType::Value => value_expr.to_string(),
230
305
  RustType::F64 => format!(
231
306
  "match &{} {{ Value::Number(n) => *n, _ => panic!(\"expected number\") }}",
@@ -277,6 +352,15 @@ impl RustType {
277
352
  /// Generate code to convert from this native type to Value.
278
353
  pub fn to_value_expr(&self, native_expr: &str) -> String {
279
354
  match self {
355
+ RustType::Tuple(elems) => {
356
+ // `(e0, e1, …)` -> `Value::Array([e0.into_value(), …])`.
357
+ let parts: Vec<String> = elems
358
+ .iter()
359
+ .enumerate()
360
+ .map(|(i, e)| e.to_value_expr(&format!("{}.{}", native_expr, i)))
361
+ .collect();
362
+ format!("Value::Array(VmRef::new(vec![{}]))", parts.join(", "))
363
+ }
280
364
  RustType::Value => native_expr.to_string(),
281
365
  RustType::F64 => format!("Value::Number({})", native_expr),
282
366
  RustType::String => format!("Value::String({}.clone().into())", native_expr),
@@ -311,7 +395,14 @@ impl RustType {
311
395
  .iter()
312
396
  .map(|(k, ty)| {
313
397
  let access = format!("{}.{}", native_expr, field_ident(k));
314
- let v_expr = ty.to_value_expr(&access);
398
+ // A `Value`-typed field (e.g. from a generic struct `Box<T>`) accessed
399
+ // behind `&self` must be cloned — it isn't `Copy` and `to_value_expr(Value)`
400
+ // is identity. Native field types clone/copy inside their own `to_value_expr`.
401
+ let v_expr = if matches!(ty, RustType::Value) {
402
+ format!("{}.clone()", access)
403
+ } else {
404
+ ty.to_value_expr(&access)
405
+ };
315
406
  format!(
316
407
  "_om.insert(::std::sync::Arc::from({:?}), {});",
317
408
  k.as_ref(),
@@ -320,8 +411,12 @@ impl RustType {
320
411
  })
321
412
  .collect::<Vec<_>>()
322
413
  .join(" ");
414
+ // `Value::object` wraps the `ObjectMap` in an `ObjectData` (what `Value::Object`
415
+ // actually holds) — emitting `Value::Object(VmRef::new(_om))` directly is a type
416
+ // error (`ObjectMap` != `ObjectData`); only surfaced once a whole struct is boxed
417
+ // into a `Value`, e.g. passing it to a function.
323
418
  format!(
324
- "{{ let mut _om = ObjectMap::default(); {} Value::Object(VmRef::new(_om)) }}",
419
+ "{{ let mut _om = ObjectMap::default(); {} Value::object(_om) }}",
325
420
  inserts
326
421
  )
327
422
  }
@@ -332,6 +427,15 @@ impl RustType {
332
427
 
333
428
  /// Map a Tish type-alias name to the Rust struct identifier we emit.
334
429
  /// Prefixed so user names can never collide with runtime types like `Value`.
430
+ /// Render a Rust tuple type/value from its parts, using the `(T,)` form for 1-tuples.
431
+ fn tuple_text(parts: &[String]) -> String {
432
+ if parts.len() == 1 {
433
+ format!("({},)", parts[0])
434
+ } else {
435
+ format!("({})", parts.join(", "))
436
+ }
437
+ }
438
+
335
439
  pub fn named_struct_ident(tish_name: &str) -> String {
336
440
  format!("TishStruct_{}", tish_name)
337
441
  }
@@ -143,6 +143,13 @@ impl Codegen {
143
143
  self.indent -= 1;
144
144
  self.writeln("}");
145
145
  }
146
+ // Comma-declarators: emit each declarator as its own JS statement in the
147
+ // current scope (no wrapping braces).
148
+ Statement::Multi { statements, .. } => {
149
+ for s in statements {
150
+ self.emit_statement(s)?;
151
+ }
152
+ }
146
153
  Statement::VarDecl {
147
154
  name,
148
155
  mutable,
@@ -225,6 +232,34 @@ impl Codegen {
225
232
  let ex = self.emit_expr(expr)?;
226
233
  header.push_str(&ex);
227
234
  }
235
+ // Comma-declarators (`for (let i = 0, n = len; …)`): emit JS-native
236
+ // `let i = 0, n = len` — one keyword, declarators joined by `, `.
237
+ Statement::Multi { statements, .. } => {
238
+ let mut decl_kw = "let";
239
+ let mut parts: Vec<String> = Vec::new();
240
+ for (idx, st) in statements.iter().enumerate() {
241
+ let Statement::VarDecl {
242
+ name,
243
+ mutable,
244
+ init: opt_init,
245
+ ..
246
+ } = st
247
+ else {
248
+ return Err(CompileError::new("Unsupported for init"));
249
+ };
250
+ if idx == 0 {
251
+ decl_kw = if *mutable { "let" } else { "const" };
252
+ }
253
+ let escaped = Self::escape_ident(name.as_ref());
254
+ if let Some(e) = opt_init {
255
+ let ex = self.emit_expr(e)?;
256
+ parts.push(format!("{} = {}", escaped, ex));
257
+ } else {
258
+ parts.push(escaped);
259
+ }
260
+ }
261
+ header.push_str(&format!("{} {}", decl_kw, parts.join(", ")));
262
+ }
228
263
  _ => return Err(CompileError::new("Unsupported for init")),
229
264
  }
230
265
  }
@@ -488,6 +523,7 @@ impl Codegen {
488
523
  BinOp::BitXor => "^",
489
524
  BinOp::Shl => "<<",
490
525
  BinOp::Shr => ">>",
526
+ BinOp::UShr => ">>>",
491
527
  BinOp::In => {
492
528
  // key in object (property/index existence check)
493
529
  return Ok(format!("({} in {})", l, r));
@@ -616,6 +652,37 @@ impl Codegen {
616
652
  let o = self.emit_expr(operand)?;
617
653
  format!("(typeof {})", o)
618
654
  }
655
+ Expr::Delete { target, .. } => {
656
+ // Emit the raw property *reference*, not a value: `emit_expr` wraps Index /
657
+ // optional reads in `(… ?? null)`, and `delete (x ?? null)` is a no-op. So
658
+ // reconstruct `obj.name` / `obj[key]` directly here.
659
+ match target.as_ref() {
660
+ Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
661
+ let obj = self.emit_expr(object)?;
662
+ if name.parse::<u32>().is_ok()
663
+ || !name.chars().all(|c| c.is_alphanumeric() || c == '_')
664
+ {
665
+ format!("(delete {}[{:?}])", obj, name.as_ref())
666
+ } else {
667
+ format!("(delete {}.{})", obj, name.as_ref())
668
+ }
669
+ }
670
+ Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
671
+ let obj = self.emit_expr(object)?;
672
+ let k = self.emit_expr(key)?;
673
+ format!("(delete {}[{}])", obj, k)
674
+ }
675
+ Expr::Index { object, index, .. } => {
676
+ let obj = self.emit_expr(object)?;
677
+ let idx = self.emit_expr(index)?;
678
+ format!("(delete {}[{}])", obj, idx)
679
+ }
680
+ _ => {
681
+ let t = self.emit_expr(target)?;
682
+ format!("(delete {})", t)
683
+ }
684
+ }
685
+ }
619
686
  Expr::PostfixInc { name, .. } => {
620
687
  format!("{}++", Self::escape_ident(name.as_ref()))
621
688
  }
@@ -27,6 +27,70 @@ mod tests {
27
27
  assert!(js.contains("h(Fragment, null, ["));
28
28
  }
29
29
 
30
+ #[test]
31
+ fn jsx_keyword_text_after_child_element() {
32
+ // #108: a text run *after* a nested child element must be lexed as JSX text, so a bare
33
+ // reserved keyword (`as`, `in`, `if`, `return`, `let`) in that run is plain text, not a
34
+ // keyword token. Text-only children already worked; the bug was failing to re-enter
35
+ // JSX-text mode once a child element had closed.
36
+ for kw in ["as", "in", "if", "return", "let"] {
37
+ let src = format!("fn V() {{ return <div><span>x</span> {kw} JSON</div> }}");
38
+ let program =
39
+ parse(&src).unwrap_or_else(|e| panic!("parse failed for trailing `{kw}`: {e}"));
40
+ let js = compile_with_jsx(&program, false).unwrap();
41
+ let expected = format!("[h(\"span\", null, [\"x\"]), \" {kw} JSON\"]");
42
+ assert!(
43
+ js.contains(&expected),
44
+ "trailing `{kw}` after child element: expected {expected:?} in output, got: {js}"
45
+ );
46
+ }
47
+ }
48
+
49
+ #[test]
50
+ fn jsx_text_between_and_after_multiple_children() {
51
+ // Text both between and after child elements stays text (incl. a keyword run between).
52
+ let src = r#"fn V() { return <div><span>x</span> as <b>y</b> in z</div> }"#;
53
+ let program = parse(src).unwrap();
54
+ let js = compile_with_jsx(&program, false).unwrap();
55
+ assert!(
56
+ js.contains(
57
+ "[h(\"span\", null, [\"x\"]), \" as \", h(\"b\", null, [\"y\"]), \" in z\"]"
58
+ ),
59
+ "{js}"
60
+ );
61
+ }
62
+
63
+ #[test]
64
+ fn jsx_self_closing_child_then_keyword_text() {
65
+ // A self-closing child (`<br/>`) followed by keyword text must also re-enter text mode.
66
+ let src = r#"fn V() { return <div><br/> as JSON</div> }"#;
67
+ let program = parse(src).unwrap();
68
+ let js = compile_with_jsx(&program, false).unwrap();
69
+ assert!(
70
+ js.contains("[h(\"br\", null, []), \" as JSON\"]"),
71
+ "{js}"
72
+ );
73
+ }
74
+
75
+ #[test]
76
+ fn jsx_child_element_inside_expr_container_not_text_mode() {
77
+ // Re-entering JSX text mode after a child closes must NOT happen when that child lived
78
+ // inside a `{…}` expression container — otherwise the `)`/`,` that follows is swallowed as
79
+ // JsxText ("Expected RParen, got JsxText"). Regression for the #108 fix itself, caught by
80
+ // the downstream suite (tish-audio / tish-midi use `{items.map(x => <li>{x}</li>)}`).
81
+ let src = r#"fn V(items) { return <ul>{items.map(x => <li>{x}</li>)}</ul> }"#;
82
+ let program = parse(src).expect("map-in-container must parse");
83
+ let js = compile_with_jsx(&program, false).unwrap();
84
+ assert!(js.contains("h(\"ul\""), "{js}");
85
+ assert!(js.contains(".map("), "{js}");
86
+
87
+ // And the combined case: a `{…}` container followed by trailing keyword text.
88
+ let src2 = r#"fn V(items) { return <div>{items.map(x => <span>{x}</span>)} as JSON</div> }"#;
89
+ let program2 = parse(src2).expect("container-then-text must parse");
90
+ let js2 = compile_with_jsx(&program2, false).unwrap();
91
+ assert!(js2.contains("\" as JSON\""), "{js2}");
92
+ }
93
+
30
94
  #[test]
31
95
  fn jsx_text_whitespace_coalesced() {
32
96
  let src = r#"fn X() { return <p>First paragraph</p> }"#;
@@ -16,11 +16,17 @@ regex = ["dep:fancy-regex"]
16
16
  # most notably `tishlang_runtime/http`, which dispatches HTTP handlers
17
17
  # across a worker pool. Off by default so wasm / wasi / cranelift / llvm /
18
18
  # interpreter builds pay no atomic-ref-count or mutex overhead.
19
- send-values = []
19
+ send-values = ["dep:parking_lot"]
20
20
 
21
21
  [dependencies]
22
22
  ahash = "0.8.11"
23
+ arcstr = "1"
24
+ smallvec = "1"
25
+ indexmap = "2"
23
26
  fancy-regex = { version = "0.17.0", optional = true }
27
+ # Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
28
+ # single atomic with no pthread syscall — profiled as a top cost on object/array access.
29
+ parking_lot = { version = "0.12", optional = true }
24
30
 
25
31
  [target.wasm32-unknown-unknown.dependencies]
26
32
  getrandom = { version = "0.3", features = ["wasm_js"] }
@@ -48,6 +48,7 @@ pub fn format_value_styled(value: &Value, colors: bool) -> String {
48
48
 
49
49
  /// `quote_strings`: when true (REPL / inspect), strings render as quoted literals. When false
50
50
  /// (top-level `console.log` arguments), strings render raw like Node.
51
+ #[allow(clippy::only_used_in_recursion)] // `colors` is always true here; threaded for recursive symmetry
51
52
  fn format_value_styled_inner(value: &Value, colors: bool, quote_strings: bool) -> String {
52
53
  match value {
53
54
  Value::Number(n) => {
@@ -67,7 +68,7 @@ fn format_value_styled_inner(value: &Value, colors: bool, quote_strings: bool) -
67
68
  let escaped = escape_string_for_display(s);
68
69
  format!("{STRING}\"{escaped}\"{RESET}")
69
70
  } else {
70
- format!("{STRING}{}{RESET}", s.as_ref())
71
+ format!("{STRING}{}{RESET}", s.as_str())
71
72
  }
72
73
  }
73
74
  Value::Bool(b) => format!("{BOOLEAN}{b}{RESET}"),
@@ -81,6 +82,15 @@ fn format_value_styled_inner(value: &Value, colors: bool, quote_strings: bool) -
81
82
  let sep = format!("{PUNCT}, {RESET}");
82
83
  format!("{PUNCT}[{RESET}{}{PUNCT}]{RESET}", inner.join(&sep))
83
84
  }
85
+ Value::NumberArray(arr) => {
86
+ let sep = format!("{PUNCT}, {RESET}");
87
+ let inner: Vec<String> = arr
88
+ .borrow()
89
+ .iter()
90
+ .map(|n| format_value_styled_inner(&Value::Number(*n), colors, true))
91
+ .collect();
92
+ format!("{PUNCT}[{RESET}{}{PUNCT}]{RESET}", inner.join(&sep))
93
+ }
84
94
  Value::Object(obj) => {
85
95
  let inner: Vec<String> = obj
86
96
  .borrow()
@@ -9,7 +9,7 @@ pub fn json_parse(json: &str) -> Result<Value, String> {
9
9
  if json.is_empty() {
10
10
  return Err("SyntaxError: Unexpected end of JSON input".to_string());
11
11
  }
12
- let (value, rest) = parse_value(json)?;
12
+ let (value, rest) = parse_value(json, 0)?;
13
13
  if !rest.trim().is_empty() {
14
14
  return Err("SyntaxError: Unexpected token at end of JSON".to_string());
15
15
  }
@@ -66,22 +66,36 @@ pub fn json_stringify_into(buf: &mut String, value: &Value) {
66
66
  }
67
67
  buf.push(']');
68
68
  }
69
+ Value::NumberArray(arr) => {
70
+ let borrowed = arr.borrow();
71
+ buf.push('[');
72
+ use std::fmt::Write;
73
+ for (i, n) in borrowed.iter().enumerate() {
74
+ if i > 0 {
75
+ buf.push(',');
76
+ }
77
+ if n.is_nan() || n.is_infinite() {
78
+ buf.push_str("null");
79
+ } else {
80
+ let _ = write!(buf, "{}", n);
81
+ }
82
+ }
83
+ buf.push(']');
84
+ }
69
85
  Value::Object(obj) => {
70
86
  let borrowed = obj.borrow();
71
- // Sort keys for deterministic output. Pre-allocate to avoid
72
- // a fresh `Vec` realloc inside `keys().collect()`.
73
- let mut keys: Vec<&Arc<str>> = Vec::with_capacity(borrowed.strings.len());
74
- keys.extend(borrowed.strings.keys());
75
- keys.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
87
+ // Iterate in insertion order (PropMap preserves it) — matches JS/Node
88
+ // and `Object.keys`. No intermediate key Vec, no sort: one fewer
89
+ // allocation per object on the JSON hot path (e.g. TFB /json, /db).
76
90
  buf.push('{');
77
- for (i, key) in keys.into_iter().enumerate() {
91
+ for (i, (key, val)) in borrowed.strings.iter().enumerate() {
78
92
  if i > 0 {
79
93
  buf.push(',');
80
94
  }
81
95
  buf.push('"');
82
96
  escape_json_string_into(buf, key);
83
97
  buf.push_str("\":");
84
- json_stringify_into(buf, borrowed.strings.get(key).unwrap());
98
+ json_stringify_into(buf, val);
85
99
  }
86
100
  buf.push('}');
87
101
  }
@@ -134,14 +148,12 @@ fn escape_json_string_into(buf: &mut String, s: &str) {
134
148
  }
135
149
  }
136
150
 
137
- #[allow(dead_code)]
138
- fn escape_json_string(s: &str) -> String {
139
- let mut buf = String::with_capacity(s.len());
140
- escape_json_string_into(&mut buf, s);
141
- buf
142
- }
151
+ /// Max nesting depth for `JSON.parse`. Bounds recursion so deeply-nested untrusted
152
+ /// input errors instead of overflowing the stack — a Rust stack overflow aborts the
153
+ /// whole process (uncatchable, SIGABRT). 128 matches serde_json's default limit.
154
+ const MAX_JSON_DEPTH: usize = 128;
143
155
 
144
- fn parse_value(input: &str) -> Result<(Value, &str), String> {
156
+ fn parse_value(input: &str, depth: usize) -> Result<(Value, &str), String> {
145
157
  let input = input.trim_start();
146
158
  if input.is_empty() {
147
159
  return Err("Unexpected end of JSON input".to_string());
@@ -151,8 +163,8 @@ fn parse_value(input: &str) -> Result<(Value, &str), String> {
151
163
  'n' => parse_null(input),
152
164
  't' | 'f' => parse_bool(input),
153
165
  '"' => parse_string(input),
154
- '[' => parse_array(input),
155
- '{' => parse_object(input),
166
+ '[' => parse_array(input, depth),
167
+ '{' => parse_object(input, depth),
156
168
  c if c == '-' || c.is_ascii_digit() => parse_number(input),
157
169
  c => Err(format!("Unexpected character '{}' in JSON", c)),
158
170
  }
@@ -249,44 +261,46 @@ fn parse_string(input: &str) -> Result<(Value, &str), String> {
249
261
  }
250
262
 
251
263
  fn parse_number(input: &str) -> Result<(Value, &str), String> {
264
+ // Byte scan (all number chars are ASCII) — O(token), not O(remaining input).
265
+ // The old `input.chars().collect::<Vec<char>>()` per number made parsing an
266
+ // N-number array O(N^2): a CPU-exhaustion DoS on untrusted JSON.
267
+ let bytes = input.as_bytes();
252
268
  let mut end = 0;
253
- let chars: Vec<char> = input.chars().collect();
254
269
 
255
- if chars.get(end) == Some(&'-') {
270
+ if bytes.first() == Some(&b'-') {
256
271
  end += 1;
257
272
  }
258
-
259
- while end < chars.len() && chars[end].is_ascii_digit() {
273
+ while end < bytes.len() && bytes[end].is_ascii_digit() {
260
274
  end += 1;
261
275
  }
262
-
263
- if chars.get(end) == Some(&'.') {
276
+ if bytes.get(end) == Some(&b'.') {
264
277
  end += 1;
265
- while end < chars.len() && chars[end].is_ascii_digit() {
278
+ while end < bytes.len() && bytes[end].is_ascii_digit() {
266
279
  end += 1;
267
280
  }
268
281
  }
269
-
270
- if chars.get(end) == Some(&'e') || chars.get(end) == Some(&'E') {
282
+ if matches!(bytes.get(end), Some(&b'e') | Some(&b'E')) {
271
283
  end += 1;
272
- if chars.get(end) == Some(&'+') || chars.get(end) == Some(&'-') {
284
+ if matches!(bytes.get(end), Some(&b'+') | Some(&b'-')) {
273
285
  end += 1;
274
286
  }
275
- while end < chars.len() && chars[end].is_ascii_digit() {
287
+ while end < bytes.len() && bytes[end].is_ascii_digit() {
276
288
  end += 1;
277
289
  }
278
290
  }
279
291
 
280
- let num_str: String = chars[..end].iter().collect();
281
- let byte_len: usize = chars[..end].iter().map(|c| c.len_utf8()).sum();
282
-
292
+ // `end` lands on an ASCII boundary, so slicing `input` by byte index is valid.
293
+ let num_str = &input[..end];
283
294
  num_str
284
295
  .parse::<f64>()
285
- .map(|n| (Value::Number(n), &input[byte_len..]))
296
+ .map(|n| (Value::Number(n), &input[end..]))
286
297
  .map_err(|_| format!("Invalid number: {}", num_str))
287
298
  }
288
299
 
289
- fn parse_array(input: &str) -> Result<(Value, &str), String> {
300
+ fn parse_array(input: &str, depth: usize) -> Result<(Value, &str), String> {
301
+ if depth >= MAX_JSON_DEPTH {
302
+ return Err("JSON nesting too deep".to_string());
303
+ }
290
304
  let mut input = &input[1..]; // skip '['
291
305
  let mut items = Vec::new();
292
306
 
@@ -296,7 +310,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
296
310
  }
297
311
 
298
312
  loop {
299
- let (value, rest) = parse_value(input)?;
313
+ let (value, rest) = parse_value(input, depth + 1)?;
300
314
  items.push(value);
301
315
  input = rest.trim_start();
302
316
 
@@ -308,7 +322,10 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
308
322
  }
309
323
  }
310
324
 
311
- fn parse_object(input: &str) -> Result<(Value, &str), String> {
325
+ fn parse_object(input: &str, depth: usize) -> Result<(Value, &str), String> {
326
+ if depth >= MAX_JSON_DEPTH {
327
+ return Err("JSON nesting too deep".to_string());
328
+ }
312
329
  let mut input = &input[1..]; // skip '{'
313
330
  let mut map = crate::ObjectMap::default();
314
331
 
@@ -328,7 +345,7 @@ fn parse_object(input: &str) -> Result<(Value, &str), String> {
328
345
 
329
346
  let (key_val, rest) = parse_string(input)?;
330
347
  let key: Arc<str> = match key_val {
331
- Value::String(s) => s,
348
+ Value::String(s) => Arc::from(s.as_str()),
332
349
  _ => unreachable!(),
333
350
  };
334
351
 
@@ -338,7 +355,7 @@ fn parse_object(input: &str) -> Result<(Value, &str), String> {
338
355
  }
339
356
  input = &input[1..];
340
357
 
341
- let (value, rest) = parse_value(input)?;
358
+ let (value, rest) = parse_value(input, depth + 1)?;
342
359
  map.insert(key, value);
343
360
  input = rest.trim_start();
344
361
 
@@ -366,7 +383,7 @@ mod tests {
366
383
  assert!(matches!(json_parse("false").unwrap(), Value::Bool(false)));
367
384
  assert!(matches!(json_parse("42").unwrap(), Value::Number(n) if n == 42.0));
368
385
  assert!(
369
- matches!(json_parse("\"hello\"").unwrap(), Value::String(s) if s.as_ref() == "hello")
386
+ matches!(json_parse("\"hello\"").unwrap(), Value::String(s) if s.as_str() == "hello")
370
387
  );
371
388
  }
372
389
 
@@ -384,4 +401,30 @@ mod tests {
384
401
  _ => panic!("Expected objects"),
385
402
  }
386
403
  }
404
+
405
+ #[test]
406
+ fn deeply_nested_json_is_rejected_not_crash() {
407
+ // C1 regression: deeply-nested untrusted input must error at the depth limit,
408
+ // never recurse deep enough to overflow the stack (an uncatchable SIGABRT that
409
+ // would crash the whole process / HTTP worker).
410
+ let under = format!("{}{}", "[".repeat(100), "]".repeat(100));
411
+ assert!(json_parse(&under).is_ok(), "100 < limit should parse");
412
+ let over = format!("{}{}", "[".repeat(200), "]".repeat(200));
413
+ assert!(json_parse(&over).is_err(), "200 > limit must error");
414
+ // Pathological depth must still just error (fast), not overflow the stack.
415
+ let huge = format!("{}{}", "[".repeat(200_000), "]".repeat(200_000));
416
+ assert!(json_parse(&huge).is_err(), "huge depth must error, not crash");
417
+ }
418
+
419
+ #[test]
420
+ fn large_number_array_parses_correctly() {
421
+ // C2 regression: parse_number byte-scans (O(token)); the old chars().collect()
422
+ // over the whole remaining input made an N-number array O(N^2) — a CPU DoS.
423
+ let n = 50_000;
424
+ let body = format!("[{}]", vec!["7"; n].join(","));
425
+ match json_parse(&body).unwrap() {
426
+ Value::Array(arr) => assert_eq!(arr.borrow().len(), n),
427
+ _ => panic!("expected array"),
428
+ }
429
+ }
387
430
  }
@@ -6,12 +6,15 @@
6
6
  mod console_style;
7
7
  mod json;
8
8
  mod macros;
9
+ mod shape;
9
10
  mod uri;
10
11
  mod value;
11
12
  mod vmref;
12
13
 
13
14
  pub use console_style::{format_value_styled, format_values_for_console, use_console_colors};
14
15
  pub use json::{json_parse, json_stringify, json_stringify_into};
16
+ pub use shape::{ShapeId, DICT_SHAPE, EMPTY_SHAPE};
15
17
  pub use uri::{percent_decode, percent_encode};
18
+ pub use arcstr::ArcStr;
16
19
  pub use value::*;
17
20
  pub use vmref::{VmReadGuard, VmRef, VmWriteGuard};