@tishlang/tish 1.13.2 → 2.0.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/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- 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
|
@@ -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
|
-
|
|
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::
|
|
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.
|
|
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
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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 <
|
|
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
|
|
284
|
+
if matches!(bytes.get(end), Some(&b'+') | Some(&b'-')) {
|
|
273
285
|
end += 1;
|
|
274
286
|
}
|
|
275
|
-
while end <
|
|
287
|
+
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
276
288
|
end += 1;
|
|
277
289
|
}
|
|
278
290
|
}
|
|
279
291
|
|
|
280
|
-
|
|
281
|
-
let
|
|
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[
|
|
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.
|
|
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};
|