@tishlang/tish-format 2.12.0 → 2.35.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 +3 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -29,6 +29,10 @@ pub enum RustType {
|
|
|
29
29
|
Vec(Box<RustType>),
|
|
30
30
|
/// Option<T> for nullable types (T | null)
|
|
31
31
|
Option(Box<RustType>),
|
|
32
|
+
/// Box<T> — heap indirection required to make recursive structs finite-sized.
|
|
33
|
+
/// Only the recursive-struct native pass (#178) produces this, for child fields
|
|
34
|
+
/// like `Option<Box<TishRec_Node>>`; never from `from_annotation`.
|
|
35
|
+
Boxed(Box<RustType>),
|
|
32
36
|
/// Inline object shape — used during inference / annotation lowering
|
|
33
37
|
/// before a `Named` alias has been registered. Once a corresponding
|
|
34
38
|
/// `type Foo = { ... }` declaration is found in the program, occurrences
|
|
@@ -49,6 +53,15 @@ pub enum RustType {
|
|
|
49
53
|
},
|
|
50
54
|
/// Tuple `(T0, T1, …)` for `[T0, T1]` tuple types — a native Rust tuple.
|
|
51
55
|
Tuple(Vec<RustType>),
|
|
56
|
+
/// #179 Stage B: a closed set of 2..=8 distinct primitive-record shapes read at one site — the
|
|
57
|
+
/// "megamorphic" array-of-heterogeneous-objects pattern. Emitted as a generated Rust enum
|
|
58
|
+
/// `TishUnion_<name> { V0(TishStruct_<v0>), … }` (one variant per shape); a `.field` read present
|
|
59
|
+
/// in EVERY variant lowers to a `match` with a direct field load per arm (no hash, no IC). `name`
|
|
60
|
+
/// is the union alias; each `variants` entry is `(per-variant struct alias, its fields)`.
|
|
61
|
+
ShapeUnion {
|
|
62
|
+
name: Arc<str>,
|
|
63
|
+
variants: Vec<(Arc<str>, Vec<(Arc<str>, RustType)>)>,
|
|
64
|
+
},
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
impl RustType {
|
|
@@ -95,6 +108,12 @@ impl RustType {
|
|
|
95
108
|
RustType::Vec(Box::new(Self::from_annotation_with_aliases(elem, aliases)))
|
|
96
109
|
}
|
|
97
110
|
TypeAnnotation::Object(fields) => {
|
|
111
|
+
// Security #379: a field key that is not a valid Rust identifier must not drive a
|
|
112
|
+
// native struct (it would be interpolated into generated Rust). Keep the whole object
|
|
113
|
+
// on the boxed `Value` path — always correct, just unspecialized.
|
|
114
|
+
if fields.iter().any(|(k, _)| !is_struct_field_safe(k)) {
|
|
115
|
+
return RustType::Value;
|
|
116
|
+
}
|
|
98
117
|
let typed_fields: Vec<_> = fields
|
|
99
118
|
.iter()
|
|
100
119
|
.map(|(k, v)| (k.clone(), Self::from_annotation_with_aliases(v, aliases)))
|
|
@@ -234,6 +253,7 @@ impl RustType {
|
|
|
234
253
|
RustType::Unit => "()".to_string(),
|
|
235
254
|
RustType::Vec(inner) => format!("Vec<{}>", inner.to_rust_type_str()),
|
|
236
255
|
RustType::Option(inner) => format!("Option<{}>", inner.to_rust_type_str()),
|
|
256
|
+
RustType::Boxed(inner) => format!("Box<{}>", inner.to_rust_type_str()),
|
|
237
257
|
RustType::Object(_) => {
|
|
238
258
|
// Anonymous inline shapes don't have a Rust struct; fall
|
|
239
259
|
// back to the dynamic Value path.
|
|
@@ -249,6 +269,7 @@ impl RustType {
|
|
|
249
269
|
)
|
|
250
270
|
}
|
|
251
271
|
RustType::Tuple(elems) => tuple_text(&elems.iter().map(|e| e.to_rust_type_str()).collect::<Vec<_>>()),
|
|
272
|
+
RustType::ShapeUnion { name, .. } => shape_union_enum_ident(name),
|
|
252
273
|
}
|
|
253
274
|
}
|
|
254
275
|
|
|
@@ -263,6 +284,7 @@ impl RustType {
|
|
|
263
284
|
RustType::Unit => "()".to_string(),
|
|
264
285
|
RustType::Vec(_) => "Vec::new()".to_string(),
|
|
265
286
|
RustType::Option(_) => "None".to_string(),
|
|
287
|
+
RustType::Boxed(inner) => format!("Box::new({})", inner.default_value()),
|
|
266
288
|
RustType::Object(_) => "Value::Null".to_string(),
|
|
267
289
|
RustType::Named { fields, .. } => {
|
|
268
290
|
// Build a literal struct with each field at its own default,
|
|
@@ -285,6 +307,22 @@ impl RustType {
|
|
|
285
307
|
RustType::Tuple(elems) => {
|
|
286
308
|
tuple_text(&elems.iter().map(|e| e.default_value()).collect::<Vec<_>>())
|
|
287
309
|
}
|
|
310
|
+
RustType::ShapeUnion { name, variants } => {
|
|
311
|
+
// Default = the first variant with each of its fields defaulted.
|
|
312
|
+
let (v0_alias, v0_fields) = &variants[0];
|
|
313
|
+
let init = v0_fields
|
|
314
|
+
.iter()
|
|
315
|
+
.map(|(k, t)| format!("{}: {}", field_ident(k), t.default_value()))
|
|
316
|
+
.collect::<Vec<_>>()
|
|
317
|
+
.join(", ");
|
|
318
|
+
format!(
|
|
319
|
+
"{}::{}({} {{ {} }})",
|
|
320
|
+
shape_union_enum_ident(name),
|
|
321
|
+
shape_union_variant_ident(0),
|
|
322
|
+
named_struct_ident(v0_alias),
|
|
323
|
+
init
|
|
324
|
+
)
|
|
325
|
+
}
|
|
288
326
|
}
|
|
289
327
|
}
|
|
290
328
|
|
|
@@ -344,20 +382,34 @@ impl RustType {
|
|
|
344
382
|
)
|
|
345
383
|
}
|
|
346
384
|
RustType::Named { name, fields } => {
|
|
347
|
-
// Each field is fetched out of the Value::Object via
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
// (
|
|
385
|
+
// Each field is fetched out of the Value::Object via `get_prop` and converted to its
|
|
386
|
+
// native type. The source Value is bound ONCE to `_src` so a non-trivial `value_expr`
|
|
387
|
+
// (an object literal, a call, …) is evaluated a single time instead of being textually
|
|
388
|
+
// re-inlined per field (which would re-allocate the whole object N times). Missing
|
|
389
|
+
// fields fall back to `default_value()` (rare — usually these come from JSON or PG).
|
|
351
390
|
let field_assigns = fields
|
|
352
391
|
.iter()
|
|
353
392
|
.map(|(k, ty)| {
|
|
354
|
-
let fetch =
|
|
355
|
-
format!("tishlang_runtime::get_prop(&{}, {:?})", value_expr, k.as_ref());
|
|
393
|
+
let fetch = format!("tishlang_runtime::get_prop(&_src, {:?})", k.as_ref());
|
|
356
394
|
format!("{}: {}", field_ident(k), ty.from_value_expr(&fetch))
|
|
357
395
|
})
|
|
358
396
|
.collect::<Vec<_>>()
|
|
359
397
|
.join(", ");
|
|
360
|
-
format!(
|
|
398
|
+
format!(
|
|
399
|
+
"{{ let _src = {}; {} {{ {} }} }}",
|
|
400
|
+
value_expr,
|
|
401
|
+
named_struct_ident(name),
|
|
402
|
+
field_assigns
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
RustType::ShapeUnion { .. } => {
|
|
406
|
+
// #179 Stage B: converting a boxed Value INTO a ShapeUnion is a boundary the safety
|
|
407
|
+
// walk forbids (a typed union element never crosses to/from boxed), so codegen must
|
|
408
|
+
// never emit this. Present for match-completeness; fails loud if gating is violated.
|
|
409
|
+
format!(
|
|
410
|
+
"{{ let _ = {}; unreachable!(\"ShapeUnion from Value is a forbidden boundary\") }}",
|
|
411
|
+
value_expr
|
|
412
|
+
)
|
|
361
413
|
}
|
|
362
414
|
_ => value_expr.to_string(), // Fallback
|
|
363
415
|
}
|
|
@@ -403,12 +455,15 @@ impl RustType {
|
|
|
403
455
|
)
|
|
404
456
|
}
|
|
405
457
|
RustType::Named { fields, .. } => {
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
458
|
+
// Build the boxed Value with an ORDERED PropMap via `object_from_pairs` (no
|
|
459
|
+
// intermediate `AHashMap`), so key order == field-declaration order == JS
|
|
460
|
+
// insertion order. The old `ObjectMap::default()` (`AHashMap`) + insert path
|
|
461
|
+
// scrambled key order NON-DETERMINISTICALLY per run (ahash seed) — a shipped
|
|
462
|
+
// `JSON.stringify` / `Object.keys` / `for..in` divergence from node whenever a
|
|
463
|
+
// native struct crosses back into untyped Tish. This boundary is paid only on
|
|
464
|
+
// that crossing (JSON.stringify, calling a Value::Function, etc.); direct
|
|
465
|
+
// Rust-to-Rust paths between two Named values stay as plain struct moves.
|
|
466
|
+
let pairs = fields
|
|
412
467
|
.iter()
|
|
413
468
|
.map(|(k, ty)| {
|
|
414
469
|
let access = format!("{}.{}", native_expr, field_ident(k));
|
|
@@ -420,22 +475,35 @@ impl RustType {
|
|
|
420
475
|
} else {
|
|
421
476
|
ty.to_value_expr(&access)
|
|
422
477
|
};
|
|
478
|
+
format!("(::std::sync::Arc::from({:?}), {})", k.as_ref(), v_expr)
|
|
479
|
+
})
|
|
480
|
+
.collect::<Vec<_>>()
|
|
481
|
+
.join(", ");
|
|
482
|
+
// `object_from_pairs::<N>` builds the `PropMap` in one ordered pass (N = field
|
|
483
|
+
// count, a codegen-time constant) — no AHashMap, so key order is preserved.
|
|
484
|
+
format!("Value::object_from_pairs([{}])", pairs)
|
|
485
|
+
}
|
|
486
|
+
RustType::ShapeUnion { name, variants } => {
|
|
487
|
+
// union → boxed Value::Object: one match arm per variant, delegating to that
|
|
488
|
+
// variant's Named glue (ordered `object_from_pairs`, preserving JS key order).
|
|
489
|
+
let arms = variants
|
|
490
|
+
.iter()
|
|
491
|
+
.enumerate()
|
|
492
|
+
.map(|(i, (alias, fields))| {
|
|
493
|
+
let named = RustType::Named {
|
|
494
|
+
name: alias.clone(),
|
|
495
|
+
fields: fields.clone(),
|
|
496
|
+
};
|
|
423
497
|
format!(
|
|
424
|
-
"
|
|
425
|
-
|
|
426
|
-
|
|
498
|
+
"{}::{}(__u) => {}",
|
|
499
|
+
shape_union_enum_ident(name),
|
|
500
|
+
shape_union_variant_ident(i),
|
|
501
|
+
named.to_value_expr("__u")
|
|
427
502
|
)
|
|
428
503
|
})
|
|
429
504
|
.collect::<Vec<_>>()
|
|
430
|
-
.join(" ");
|
|
431
|
-
|
|
432
|
-
// actually holds) — emitting `Value::Object(VmRef::new(_om))` directly is a type
|
|
433
|
-
// error (`ObjectMap` != `ObjectData`); only surfaced once a whole struct is boxed
|
|
434
|
-
// into a `Value`, e.g. passing it to a function.
|
|
435
|
-
format!(
|
|
436
|
-
"{{ let mut _om = ObjectMap::default(); {} Value::object(_om) }}",
|
|
437
|
-
inserts
|
|
438
|
-
)
|
|
505
|
+
.join(", ");
|
|
506
|
+
format!("(match &{} {{ {} }})", native_expr, arms)
|
|
439
507
|
}
|
|
440
508
|
_ => native_expr.to_string(), // Fallback
|
|
441
509
|
}
|
|
@@ -457,16 +525,75 @@ pub fn named_struct_ident(tish_name: &str) -> String {
|
|
|
457
525
|
format!("TishStruct_{}", tish_name)
|
|
458
526
|
}
|
|
459
527
|
|
|
528
|
+
/// #179 Stage B: map a shape-union alias to the generated Rust enum identifier.
|
|
529
|
+
pub fn shape_union_enum_ident(tish_name: &str) -> String {
|
|
530
|
+
format!("TishUnion_{}", tish_name)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/// #179 Stage B: the Rust variant identifier for the `idx`-th shape of a union (`V0`, `V1`, …).
|
|
534
|
+
pub fn shape_union_variant_ident(idx: usize) -> String {
|
|
535
|
+
format!("V{}", idx)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/// Keywords that cannot be written as a raw identifier (`r#self` etc. are a hard Rust error), so an
|
|
539
|
+
/// object key equal to one of these must NOT drive struct/shape inference — it stays boxed.
|
|
540
|
+
const NON_RAWABLE_KEYWORDS: &[&str] = &["self", "Self", "super", "crate"];
|
|
541
|
+
|
|
542
|
+
/// True iff `key` can be safely emitted as a generated Rust struct field name via [`field_ident`]:
|
|
543
|
+
/// a plain ASCII identifier (`^[A-Za-z_][A-Za-z0-9_]*$`), excluding the bare wildcard `_` and the
|
|
544
|
+
/// four non-rawable keywords. Any other key — one containing spaces/punctuation, empty, non-ASCII,
|
|
545
|
+
/// etc. — is REJECTED so the object it belongs to falls back to the boxed `Value` path instead of
|
|
546
|
+
/// being lowered to a native struct.
|
|
547
|
+
///
|
|
548
|
+
/// This is the front-line guard for the native-codegen key-injection class (security #379): an
|
|
549
|
+
/// object literal key is arbitrary tish/JS text, and interpolating it verbatim into generated Rust
|
|
550
|
+
/// (`pub {key}: {ty},`) is arbitrary-code injection. Inference/annotation lowering call this to keep
|
|
551
|
+
/// unsafe-keyed objects on the always-correct boxed path; [`field_ident`] additionally sanitizes as a
|
|
552
|
+
/// last-resort choke point so no path can ever emit a non-identifier verbatim.
|
|
553
|
+
pub fn is_struct_field_safe(key: &str) -> bool {
|
|
554
|
+
if key == "_" || NON_RAWABLE_KEYWORDS.contains(&key) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
let mut chars = key.chars();
|
|
558
|
+
match chars.next() {
|
|
559
|
+
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
|
|
560
|
+
_ => return false,
|
|
561
|
+
}
|
|
562
|
+
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/// FNV-1a 64-bit — a tiny, dependency-free hash used only to synthesize a deterministic safe field
|
|
566
|
+
/// identifier for an (unexpected) non-identifier key. Deterministic so a struct's field definition,
|
|
567
|
+
/// its constructor, its accessors, and its serializer all agree on the same name.
|
|
568
|
+
fn fnv1a_64(s: &str) -> u64 {
|
|
569
|
+
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
|
570
|
+
for b in s.as_bytes() {
|
|
571
|
+
h ^= *b as u64;
|
|
572
|
+
h = h.wrapping_mul(0x0000_0100_0000_01b3);
|
|
573
|
+
}
|
|
574
|
+
h
|
|
575
|
+
}
|
|
576
|
+
|
|
460
577
|
/// Map a Tish field name (`randomNumber`) to a valid Rust identifier
|
|
461
578
|
/// (kept identical here — non-snake-case is allowed via
|
|
462
579
|
/// `#[allow(non_snake_case)]` on the struct, so JS-style camelCase keys
|
|
463
580
|
/// stay readable in the generated source).
|
|
581
|
+
///
|
|
582
|
+
/// A key that is not a valid Rust identifier is NEVER emitted verbatim (that would be code injection,
|
|
583
|
+
/// security #379): inference/annotation lowering should already have kept such an object boxed via
|
|
584
|
+
/// [`is_struct_field_safe`], so this is a defense-in-depth choke point — an unsafe key is replaced by
|
|
585
|
+
/// a deterministic `__tish_field_<hash>` name. The worst case if a producer is missed is thus a wrong
|
|
586
|
+
/// field name (a compile error / correctness bug), never injected Rust.
|
|
464
587
|
pub fn field_ident(tish_name: &str) -> String {
|
|
465
|
-
|
|
588
|
+
if !is_struct_field_safe(tish_name) {
|
|
589
|
+
return format!("__tish_field_{:016x}", fnv1a_64(tish_name));
|
|
590
|
+
}
|
|
591
|
+
// Reserve Rust keywords that would otherwise conflict. (The non-rawable keywords are already
|
|
592
|
+
// excluded by `is_struct_field_safe` above, so every keyword reaching here has a valid `r#` form.)
|
|
466
593
|
match tish_name {
|
|
467
|
-
"type" | "ref" | "fn" | "match" | "move" | "mod" | "
|
|
594
|
+
"type" | "ref" | "fn" | "match" | "move" | "mod" | "use"
|
|
468
595
|
| "where" | "loop" | "yield" | "async" | "await" | "dyn" | "impl" | "trait" | "in"
|
|
469
|
-
| "as" | "box" | "
|
|
596
|
+
| "as" | "box" | "const" | "extern" | "let" | "mut" | "pub" | "static"
|
|
470
597
|
| "unsafe" | "abstract" | "become" | "do" | "final" | "macro" | "override" | "priv"
|
|
471
598
|
| "typeof" | "unsized" | "virtual" => format!("r#{}", tish_name),
|
|
472
599
|
_ => tish_name.to_string(),
|
|
@@ -571,6 +698,75 @@ mod tests {
|
|
|
571
698
|
);
|
|
572
699
|
}
|
|
573
700
|
|
|
701
|
+
// ---- security #379: object-literal key injection into generated Rust ----
|
|
702
|
+
|
|
703
|
+
#[test]
|
|
704
|
+
fn is_struct_field_safe_accepts_plain_and_camel_and_keywords() {
|
|
705
|
+
for k in ["x", "y", "randomNumber", "_priv", "a1", "TishAnon_0", "type", "fn", "match"] {
|
|
706
|
+
assert!(is_struct_field_safe(k), "{k:?} should be a safe field");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#[test]
|
|
711
|
+
fn is_struct_field_safe_rejects_injection_and_nonrawable() {
|
|
712
|
+
for k in [
|
|
713
|
+
// the #379 injection shapes: anything with punctuation/space/braces
|
|
714
|
+
"x: i32 } pub fn pwned() {} struct Z { pub y",
|
|
715
|
+
"a, b",
|
|
716
|
+
"0abc",
|
|
717
|
+
"has space",
|
|
718
|
+
"unicodé",
|
|
719
|
+
"",
|
|
720
|
+
// non-rawable keywords would become invalid `r#self` etc.
|
|
721
|
+
"self", "Self", "super", "crate",
|
|
722
|
+
// bare wildcard is not a legal field name
|
|
723
|
+
"_",
|
|
724
|
+
] {
|
|
725
|
+
assert!(!is_struct_field_safe(k), "{k:?} must NOT be a safe field");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#[test]
|
|
730
|
+
fn field_ident_never_emits_non_identifier_for_unsafe_key() {
|
|
731
|
+
// The core RCE guarantee: whatever key comes in, field_ident() out is ALWAYS a legal Rust
|
|
732
|
+
// identifier (ASCII ident chars, or a leading `r#`) — never verbatim injectable text.
|
|
733
|
+
let malicious = "x: i32 } pub fn PWNED() -> i32 { 1337 } struct S { pub y";
|
|
734
|
+
let out = field_ident(malicious);
|
|
735
|
+
assert!(out.starts_with("__tish_field_"), "unsafe key must be sanitized, got {out:?}");
|
|
736
|
+
let body = out.trim_start_matches("r#");
|
|
737
|
+
assert!(
|
|
738
|
+
body.chars().enumerate().all(|(i, c)| {
|
|
739
|
+
if i == 0 { c == '_' || c.is_ascii_alphabetic() } else { c == '_' || c.is_ascii_alphanumeric() }
|
|
740
|
+
}),
|
|
741
|
+
"field_ident output {out:?} is not a legal Rust identifier"
|
|
742
|
+
);
|
|
743
|
+
// deterministic: the def, the accessor, and the serializer must agree.
|
|
744
|
+
assert_eq!(field_ident(malicious), field_ident(malicious));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#[test]
|
|
748
|
+
fn field_ident_preserves_legit_and_raws_keywords() {
|
|
749
|
+
assert_eq!(field_ident("randomNumber"), "randomNumber");
|
|
750
|
+
assert_eq!(field_ident("x"), "x");
|
|
751
|
+
assert_eq!(field_ident("type"), "r#type");
|
|
752
|
+
// a non-rawable keyword is sanitized, NOT emitted as the invalid `r#self`.
|
|
753
|
+
assert!(field_ident("self").starts_with("__tish_field_"));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
#[test]
|
|
757
|
+
fn annotation_object_with_unsafe_key_stays_boxed() {
|
|
758
|
+
// A `type` alias whose object shape has a non-identifier key must fall back to boxed Value,
|
|
759
|
+
// never a native struct with an injected field name.
|
|
760
|
+
let obj = TypeAnnotation::Object(vec![
|
|
761
|
+
("safe".into(), TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())),
|
|
762
|
+
(
|
|
763
|
+
"evil } fn pwned() {".into(),
|
|
764
|
+
TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default()),
|
|
765
|
+
),
|
|
766
|
+
]);
|
|
767
|
+
assert_eq!(RustType::from_annotation(&obj), RustType::Value);
|
|
768
|
+
}
|
|
769
|
+
|
|
574
770
|
#[test]
|
|
575
771
|
fn test_nullable_type() {
|
|
576
772
|
let nullable = TypeAnnotation::Union(vec![
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//! Dump perf fixtures to target/dump_*.rs for inspection.
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
use tishlang_compile::compile_project_full;
|
|
4
|
+
|
|
5
|
+
fn compile(rel: &str) -> String {
|
|
6
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
7
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
8
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
9
|
+
rust
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn dump_perf_fixtures() {
|
|
14
|
+
let out = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target");
|
|
15
|
+
std::fs::write(out.join("dump_mandelbrot.rs"), compile("tests/perf/mandelbrot.tish")).unwrap();
|
|
16
|
+
std::fs::write(out.join("dump_fasta.rs"), compile("tests/perf/fasta.tish")).unwrap();
|
|
17
|
+
std::fs::write(out.join("dump_fannkuch.rs"), compile("tests/perf/fannkuch.tish")).unwrap();
|
|
18
|
+
std::fs::write(out.join("dump_fnv_hash.rs"), compile("tests/perf/fnv_hash.tish")).unwrap();
|
|
19
|
+
std::fs::write(out.join("dump_spectral_norm.rs"), compile("tests/perf/spectral_norm.tish")).unwrap();
|
|
20
|
+
std::fs::write(out.join("dump_queens.rs"), compile("tests/perf/queens.tish")).unwrap();
|
|
21
|
+
}
|
|
@@ -11,17 +11,6 @@ use tishlang_parser::parse;
|
|
|
11
11
|
|
|
12
12
|
/// Enable every dark-shipped typed-native flag, matching `scripts/run_perf_gauntlet.sh`.
|
|
13
13
|
fn enable_typed_flags() {
|
|
14
|
-
for k in [
|
|
15
|
-
"TISH_PARAM_NATIVE",
|
|
16
|
-
"TISH_PARAM_INFER",
|
|
17
|
-
"TISH_NATIVE_FN",
|
|
18
|
-
"TISH_STRUCT_INFER",
|
|
19
|
-
"TISH_FUSED_HOF",
|
|
20
|
-
"TISH_NATIVE_HOF",
|
|
21
|
-
"TISH_AGGREGATE_INFER",
|
|
22
|
-
] {
|
|
23
|
-
std::env::set_var(k, "1");
|
|
24
|
-
}
|
|
25
14
|
}
|
|
26
15
|
|
|
27
16
|
fn compile_typed(src: &str) -> String {
|
|
@@ -130,10 +119,12 @@ fn issue_173_adversarial_cases_do_not_fuse() {
|
|
|
130
119
|
!rust.contains("repeat((i * 2"),
|
|
131
120
|
"non-constant push arg must not be fused into a repeat-fill"
|
|
132
121
|
);
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
122
|
+
// The three canonical fills DO fuse (boolFill=8, sieve=n, oobGrow=3); the adversarial cases keep
|
|
123
|
+
// their push loops. Assert each sound fill is present rather than a brittle exact total: with the
|
|
124
|
+
// typing stack on by default, a fn that has a native `_nv` form (e.g. `numFillSieve`) may emit its
|
|
125
|
+
// fill in both the native and the boxed closure form, so the same fill can legitimately appear
|
|
126
|
+
// more than once.
|
|
127
|
+
assert!(rust.contains("repeat(true).take((8"), "boolFill (8) should fuse");
|
|
128
|
+
assert!(rust.contains("repeat(1_f64).take((n"), "sieve (n) should fuse");
|
|
129
|
+
assert!(rust.contains("repeat(1_f64).take((3"), "oobGrow (3) should fuse");
|
|
139
130
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//! Generated-Rust assertions for #173 part 3 — in-bounds index elision.
|
|
2
|
+
//!
|
|
3
|
+
//! When a native `Vec` is filled to a fixed length and an index is proven `< len` (an enclosing
|
|
4
|
+
//! `i < len` guard) and `>= 0`, the store drops its OOB-growth `resize` branch and the read drops its
|
|
5
|
+
//! `.get().unwrap_or(..)` branch — a direct `a[i]` like V8/Bun emit after range-proving a loop. Any
|
|
6
|
+
//! array that can't be proven (escapes, is reassigned, or the index isn't a guarded counter) keeps
|
|
7
|
+
//! the OOB-safe lowering. Soundness across backends is covered by the `tests/core` parity corpus.
|
|
8
|
+
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
|
|
11
|
+
use tishlang_compile::compile_project_full;
|
|
12
|
+
|
|
13
|
+
fn enable_typed_flags() {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn compile_fixture_typed(rel: &str) -> String {
|
|
17
|
+
enable_typed_flags();
|
|
18
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
19
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
20
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
21
|
+
rust
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn nsieve_inbounds_store_and_read_elide_their_guards() {
|
|
26
|
+
// `isPrime` is filled to length `n` and only indexed; the read `isPrime[i]` (guard `i < n`) and
|
|
27
|
+
// the strided store `isPrime[k] = false` (guard `k < n`, `k` non-negative, stored before its own
|
|
28
|
+
// `k = k + i` reassignment) are both provably in-bounds.
|
|
29
|
+
let rust = compile_fixture_typed("tests/perf/nsieve.tish");
|
|
30
|
+
// Read: direct index, NOT `.get(..).copied().unwrap_or(false)`.
|
|
31
|
+
assert!(
|
|
32
|
+
rust.contains("isPrime[(i) as usize]"),
|
|
33
|
+
"in-bounds read should be a direct index:\n{}",
|
|
34
|
+
rust.lines().filter(|l| l.contains("isPrime")).take(8).collect::<Vec<_>>().join("\n")
|
|
35
|
+
);
|
|
36
|
+
// Store: direct index, NO `resize` grow branch. The strided `isPrime[k] = false` is a discarded
|
|
37
|
+
// statement, so it lowers to the bare side-effect form (no `{ …; Value::Null }` value wrapper);
|
|
38
|
+
// the point is it is a direct indexed store, not a resize-grow.
|
|
39
|
+
assert!(
|
|
40
|
+
rust.contains("isPrime[(k) as usize] = false;"),
|
|
41
|
+
"in-bounds store should be a direct indexed store (no resize-grow branch):\n{}",
|
|
42
|
+
rust.lines().filter(|l| l.contains("isPrime")).take(8).collect::<Vec<_>>().join("\n")
|
|
43
|
+
);
|
|
44
|
+
assert!(
|
|
45
|
+
!rust.contains("isPrime.resize") && !rust.contains("isPrime.get("),
|
|
46
|
+
"the proven-fixed-length isPrime must have no resize/get fallbacks left:\n{}",
|
|
47
|
+
rust.lines().filter(|l| l.contains("isPrime")).take(8).collect::<Vec<_>>().join("\n")
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn escaping_array_keeps_oob_safe_lowering() {
|
|
53
|
+
// `a` is passed to a function, so it could be mutated/shrunk out of line — the fixed-length fact
|
|
54
|
+
// must NOT apply and the store must keep its OOB-growth `resize` branch.
|
|
55
|
+
let rust = compile_fixture_typed("tests/core/inbounds_index.tish");
|
|
56
|
+
assert!(
|
|
57
|
+
rust.contains("resize"),
|
|
58
|
+
"an escaping array must retain the OOB-safe resize store:\n{}",
|
|
59
|
+
rust.lines().filter(|l| l.contains("resize") || l.contains("esc")).take(8).collect::<Vec<_>>().join("\n")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
//! Generated-Rust assertions for the #174 Int32/U32 range-lattice round-trip erasure.
|
|
2
|
+
//!
|
|
3
|
+
//! #174 erases redundant `tishlang_runtime::to_int32` / `to_uint32` calls in the typed bitwise/shift
|
|
4
|
+
//! emitter when an operand is PROVABLY a finite integer — either an integer literal (a compile-time
|
|
5
|
+
//! constant) or a value the integer-range lattice bounds within `(-2^53, 2^53)`. The guarded
|
|
6
|
+
//! `to_int32` (NaN/±Infinity → 0) is retained for every unproven leaf, so the lowering is purely
|
|
7
|
+
//! additive and behaviour-identical (the gauntlet's `typed == boxed == node` checksum is the gate).
|
|
8
|
+
//!
|
|
9
|
+
//! These run in their own test binary so any env flags set here never leak into other tests.
|
|
10
|
+
|
|
11
|
+
use tishlang_compile::compile;
|
|
12
|
+
use tishlang_parser::parse;
|
|
13
|
+
|
|
14
|
+
fn enable_typed_flags() {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// FNV-style hash loop: the canonical bitwise hot loop. The accumulator `h` lives in an i32 register
|
|
18
|
+
/// (existing lowering); #174 additionally folds the constant mask / shift counts and keeps the
|
|
19
|
+
/// `(h * C) >>> 0` f64-rounding excursion intact.
|
|
20
|
+
const FNV: &str = r#"
|
|
21
|
+
let h = 2166136261
|
|
22
|
+
for (let i = 0; i < 100; i++) {
|
|
23
|
+
h = h ^ (i & 255)
|
|
24
|
+
h = (h * 16777619) >>> 0
|
|
25
|
+
h = ((h << 13) | (h >>> 19)) >>> 0
|
|
26
|
+
}
|
|
27
|
+
let check = h >>> 0
|
|
28
|
+
console.log(check)
|
|
29
|
+
"#;
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn issue_174_constant_mask_and_shift_counts_fold() {
|
|
33
|
+
// `i & 255`, `h << 13`, `h >>> 19`, `… >>> 0`: every integer-literal operand is a compile-time
|
|
34
|
+
// ToInt32 constant, so it must emit as a bare `i32` literal — NOT a runtime `to_int32(255_f64)`.
|
|
35
|
+
let rust = compile(&parse(FNV).unwrap()).unwrap();
|
|
36
|
+
assert!(
|
|
37
|
+
rust.contains("255i32"),
|
|
38
|
+
"the `& 255` mask must fold to an i32 constant:\n{}",
|
|
39
|
+
rust.lines().filter(|l| l.contains("255")).take(4).collect::<Vec<_>>().join("\n")
|
|
40
|
+
);
|
|
41
|
+
assert!(
|
|
42
|
+
rust.contains("13i32") && rust.contains("19i32"),
|
|
43
|
+
"shift counts 13 / 19 must fold to i32 constants"
|
|
44
|
+
);
|
|
45
|
+
// No runtime ToInt32/ToUint32 call on an integer LITERAL remains.
|
|
46
|
+
assert!(
|
|
47
|
+
!rust.contains("to_int32(255") && !rust.contains("to_uint32(13")
|
|
48
|
+
&& !rust.contains("to_uint32(19") && !rust.contains("to_uint32(0"),
|
|
49
|
+
"integer-literal operands must not round-trip through a runtime to_int32/to_uint32 call:\n{}",
|
|
50
|
+
rust.lines().filter(|l| l.contains("to_uint32(") || l.contains("to_int32(")).take(8).collect::<Vec<_>>().join("\n")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn issue_174_fnv_accumulator_still_i32_register_no_regression() {
|
|
56
|
+
// The i32-register accumulator lowering (and its `(h * C)` f64-rounding excursion) is preserved:
|
|
57
|
+
// `h` is an i32 register, there is no per-op `to_int32(h)` round-trip, and the multiply excursion
|
|
58
|
+
// lowers to an unchecked f64 truncation. (Mirror of `fnv_accumulator_lowers_to_i32_register`.)
|
|
59
|
+
let rust = compile(&parse(FNV).unwrap()).unwrap();
|
|
60
|
+
assert!(
|
|
61
|
+
rust.contains("let mut h: i32 = (2166136261u32) as i32;"),
|
|
62
|
+
"h must stay an i32 register seeded via u32 reinterpretation"
|
|
63
|
+
);
|
|
64
|
+
assert!(
|
|
65
|
+
!rust.contains("to_int32(h)"),
|
|
66
|
+
"no per-op to_int32(h) round-trip on the i32 accumulator"
|
|
67
|
+
);
|
|
68
|
+
assert!(
|
|
69
|
+
rust.contains(".to_int_unchecked::<i64>()") && rust.contains("16777619"),
|
|
70
|
+
"the `h * 16777619` excursion stays an f64 multiply with an unchecked truncation"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[test]
|
|
75
|
+
fn issue_174_range_proven_counter_drops_is_finite_guard() {
|
|
76
|
+
// A loop counter masked into a bitwise op is range-proven integral, so its ToInt32 must not
|
|
77
|
+
// round-trip through the guarded `to_int32(i)` call. The guard-free lowering is the saturating
|
|
78
|
+
// `(i) as i64 as i32` cast — #380 (PR #387) replaced the original `to_int_unchecked::<i64>()`
|
|
79
|
+
// here because `int_valued_locals` proves integrality but not magnitude, making the unchecked
|
|
80
|
+
// truncation UB on an overflowing accumulator; the saturating cast is defined for all f64 at
|
|
81
|
+
// the same hot-path cost (fnv_hash/fannkuch/mandelbrot measured at baseline in #387).
|
|
82
|
+
let src = r#"
|
|
83
|
+
let acc = 0
|
|
84
|
+
for (let i = 0; i < 64; i = i + 1) {
|
|
85
|
+
acc = acc ^ (i & 15)
|
|
86
|
+
}
|
|
87
|
+
console.log(acc >>> 0)
|
|
88
|
+
"#;
|
|
89
|
+
let rust = compile(&parse(src).unwrap()).unwrap();
|
|
90
|
+
assert!(
|
|
91
|
+
rust.contains("as i64 as i32"),
|
|
92
|
+
"a range-proven counter `i` in `i & 15` must lower to the guard-free saturating cast:\n{}",
|
|
93
|
+
rust.lines().filter(|l| l.contains("acc") || l.contains("to_int")).take(8).collect::<Vec<_>>().join("\n")
|
|
94
|
+
);
|
|
95
|
+
assert!(
|
|
96
|
+
!rust.contains("to_int32(i)"),
|
|
97
|
+
"a range-proven counter must not round-trip through the guarded to_int32 call:\n{}",
|
|
98
|
+
rust.lines().filter(|l| l.contains("to_int32(")).take(8).collect::<Vec<_>>().join("\n")
|
|
99
|
+
);
|
|
100
|
+
// Nothing in this program is the bounded `(h * C) >>> 0` excursion shape, so the UB-prone
|
|
101
|
+
// unchecked truncation must not appear at all (#380).
|
|
102
|
+
assert!(
|
|
103
|
+
!rust.contains("to_int_unchecked"),
|
|
104
|
+
"no unchecked truncation may appear for an unbounded-magnitude program"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn fnv_value_fn_uses_usize_for_loop() {
|
|
110
|
+
let src = r#"
|
|
111
|
+
function fnv1a(n) {
|
|
112
|
+
let h = 2166136261
|
|
113
|
+
for (let i = 0; i < n; i++) {
|
|
114
|
+
h = h ^ (i & 255)
|
|
115
|
+
h = (h * 16777619) >>> 0
|
|
116
|
+
h = ((h << 13) | (h >>> 19)) >>> 0
|
|
117
|
+
}
|
|
118
|
+
return h >>> 0
|
|
119
|
+
}
|
|
120
|
+
let check = fnv1a(100)
|
|
121
|
+
console.log(check)
|
|
122
|
+
"#;
|
|
123
|
+
enable_typed_flags();
|
|
124
|
+
let rust = compile(&parse(src).unwrap()).unwrap();
|
|
125
|
+
assert!(
|
|
126
|
+
rust.contains("for _usize_i_0 in 0..(n as usize)"),
|
|
127
|
+
"fnv1a hot loop should lower to a usize `for` over param n:\n{}",
|
|
128
|
+
rust.lines().filter(|l| l.contains("usize") || l.contains("fnv1a")).take(6).collect::<Vec<_>>().join("\n")
|
|
129
|
+
);
|
|
130
|
+
assert!(
|
|
131
|
+
rust.contains("let mut h: i32 = (2166136261u32) as i32;"),
|
|
132
|
+
"accumulator h must stay on the i32 register path"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn issue_174_unproven_leaf_keeps_guarded_to_int32() {
|
|
138
|
+
// SOUNDNESS: an f64 value that is NOT provably a finite integer (here a fractional accumulator —
|
|
139
|
+
// the range lattice drops it because `n = n + 0.5` is not integer-preserving) must KEEP the
|
|
140
|
+
// guarded `tishlang_runtime::to_int32`, never the unchecked truncation (a non-integer/NaN/±Inf
|
|
141
|
+
// input must map through the JS ToInt32 path, not a raw `to_int_unchecked`).
|
|
142
|
+
let src = r#"
|
|
143
|
+
let n = 0.0
|
|
144
|
+
for (let i = 0; i < 10; i = i + 1) { n = n + 0.5 }
|
|
145
|
+
let m = n & 7
|
|
146
|
+
console.log(m >>> 0)
|
|
147
|
+
"#;
|
|
148
|
+
let rust = compile(&parse(src).unwrap()).unwrap();
|
|
149
|
+
// `n` is a top-level numeric used only at top level, so per #313 it is a native `let mut n: f64`
|
|
150
|
+
// local (not a `thread_local Cell` — no function references it). The soundness invariant is
|
|
151
|
+
// unchanged: the unproven fractional value must STILL pass through the guarded `to_int32` (not an
|
|
152
|
+
// unchecked truncation) before the `& 7` mask.
|
|
153
|
+
assert!(
|
|
154
|
+
rust.contains("tishlang_runtime::to_int32(n) & 7i32"),
|
|
155
|
+
"an unproven f64 leaf must keep the guarded to_int32 before the mask:\n{}",
|
|
156
|
+
rust.lines().filter(|l| l.contains("& ") || l.contains("to_int")).take(6).collect::<Vec<_>>().join("\n")
|
|
157
|
+
);
|
|
158
|
+
// The literal `7` still folds; only the unproven `n` keeps the guard.
|
|
159
|
+
assert!(rust.contains("7i32"), "the literal mask `7` still folds to a constant");
|
|
160
|
+
}
|