@tishlang/tish 2.10.1 → 2.16.13

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 (75) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +16 -0
  5. package/crates/tish/src/main.rs +24 -4
  6. package/crates/tish/tests/integration_test.rs +149 -0
  7. package/crates/tish_ast/src/ast.rs +10 -0
  8. package/crates/tish_builtins/src/array.rs +529 -56
  9. package/crates/tish_builtins/src/collections.rs +114 -40
  10. package/crates/tish_builtins/src/string.rs +95 -8
  11. package/crates/tish_bytecode/src/chunk.rs +7 -0
  12. package/crates/tish_bytecode/src/compiler.rs +560 -64
  13. package/crates/tish_bytecode/src/lib.rs +1 -1
  14. package/crates/tish_bytecode/src/opcode.rs +154 -2
  15. package/crates/tish_bytecode/src/serialize.rs +2 -0
  16. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  17. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  18. package/crates/tish_compile/src/codegen.rs +17736 -5269
  19. package/crates/tish_compile/src/infer.rs +1707 -190
  20. package/crates/tish_compile/src/lib.rs +29 -4
  21. package/crates/tish_compile/src/resolve.rs +66 -5
  22. package/crates/tish_compile/src/types.rs +224 -28
  23. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  24. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  25. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  26. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  27. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  28. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  29. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  30. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  31. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  32. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  33. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  34. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  35. package/crates/tish_compile_js/src/codegen.rs +91 -4
  36. package/crates/tish_compile_js/src/lib.rs +5 -2
  37. package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
  38. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  39. package/crates/tish_core/Cargo.toml +4 -0
  40. package/crates/tish_core/src/json.rs +104 -5
  41. package/crates/tish_core/src/lib.rs +174 -0
  42. package/crates/tish_core/src/shape.rs +4 -2
  43. package/crates/tish_core/src/value.rs +565 -35
  44. package/crates/tish_core/src/vmref.rs +14 -0
  45. package/crates/tish_eval/src/eval.rs +675 -76
  46. package/crates/tish_eval/src/natives.rs +19 -0
  47. package/crates/tish_eval/src/value.rs +76 -21
  48. package/crates/tish_ffi/src/lib.rs +11 -1
  49. package/crates/tish_ffi/tests/double_free.rs +35 -0
  50. package/crates/tish_fmt/src/lib.rs +75 -1
  51. package/crates/tish_lexer/src/lib.rs +76 -0
  52. package/crates/tish_lexer/src/token.rs +4 -0
  53. package/crates/tish_lint/src/lib.rs +126 -0
  54. package/crates/tish_lsp/README.md +2 -1
  55. package/crates/tish_lsp/src/main.rs +378 -28
  56. package/crates/tish_native/src/build.rs +41 -0
  57. package/crates/tish_parser/Cargo.toml +4 -0
  58. package/crates/tish_parser/src/lib.rs +27 -0
  59. package/crates/tish_parser/src/parser.rs +302 -20
  60. package/crates/tish_resolve/src/lib.rs +9 -0
  61. package/crates/tish_runtime/Cargo.toml +5 -0
  62. package/crates/tish_runtime/src/http.rs +28 -10
  63. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  64. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  65. package/crates/tish_runtime/src/lib.rs +523 -42
  66. package/crates/tish_runtime/src/timers.rs +49 -2
  67. package/crates/tish_ui/src/jsx.rs +253 -0
  68. package/crates/tish_vm/src/jit.rs +2514 -117
  69. package/crates/tish_vm/src/vm.rs +1227 -182
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish +0 -0
  72. package/platform/darwin-x64/tish +0 -0
  73. package/platform/linux-arm64/tish +0 -0
  74. package/platform/linux-x64/tish +0 -0
  75. package/platform/win32-x64/tish.exe +0 -0
@@ -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
+ }
@@ -0,0 +1,190 @@
1
+ //! Generated-Rust assertions for #175 — native plain-array free fns.
2
+ //!
3
+ //! A top-level fn over `number[]`/`boolean[]` params (used only by index/length, no escape) whose
4
+ //! call sites pass pairwise-distinct array idents is de-virtualized to `fn name_nv(<f64..>,
5
+ //! <&/&mut Vec<T>..>) -> f64 | ()`; calls route there passing arrays by reference. A fn that calls a
6
+ //! boxed (non-native) closure, or whose array args can't be proven distinct/native, falls back to the
7
+ //! boxed closure (no `_nv`). Cross-backend soundness is covered by `tests/core/native_vec_params`.
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
+ fn compile_fixture_embedded_lib(rel: &str) -> String {
25
+ compile_fixture_embedded_lib_with_features(rel, &[])
26
+ }
27
+
28
+ fn compile_fixture_embedded_lib_with_features(rel: &str, features: &[String]) -> String {
29
+ enable_typed_flags();
30
+ let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
31
+ let path = manifest.join("../..").join(rel).canonicalize().unwrap();
32
+ let (rust, _, _, _) = tishlang_compile::compile_project_full_emit(
33
+ &path,
34
+ path.parent(),
35
+ features,
36
+ true,
37
+ tishlang_compile::NativeEmitMode::EmbeddedLib,
38
+ None,
39
+ )
40
+ .unwrap();
41
+ rust
42
+ }
43
+
44
+ #[test]
45
+ fn native_vec_params_emit_ref_signatures_and_route() {
46
+ let rust = compile_fixture_typed("tests/core/native_vec_params.tish");
47
+ // Recursive `&mut boolean[]` fn.
48
+ assert!(
49
+ rust.contains("fn mark_nv(mut n: f64, mut row: f64, a: &mut Vec<bool>, b: &mut Vec<bool>)"),
50
+ "mark lowers to a native free fn over &mut Vec<bool> params:\n{}",
51
+ rust.lines().filter(|l| l.contains("mark_nv")).take(4).collect::<Vec<_>>().join("\n")
52
+ );
53
+ // Recursion reborrows the ref params through.
54
+ assert!(
55
+ rust.contains("mark_nv(n, (row + 1_f64), &mut *a, &mut *b)"),
56
+ "recursive call reborrows the &mut params:\n{}",
57
+ rust.lines().filter(|l| l.contains("mark_nv(")).take(4).collect::<Vec<_>>().join("\n")
58
+ );
59
+ // Read-only `&Vec<f64>` + write `&mut Vec<f64>`.
60
+ assert!(
61
+ rust.contains("fn scaleInto_nv(mut n: f64, src: &Vec<f64>, dst: &mut Vec<f64>)"),
62
+ "scaleInto distinguishes read (&) vs written (&mut) array params:\n{}",
63
+ rust.lines().filter(|l| l.contains("scaleInto_nv")).take(4).collect::<Vec<_>>().join("\n")
64
+ );
65
+ // Top-level call sites address-of native `Vec` locals.
66
+ assert!(
67
+ rust.contains("mark_nv(5_f64, 0_f64, &mut a, &mut b)"),
68
+ "the entry call passes the local Vecs by &mut:\n{}",
69
+ rust.lines().filter(|l| l.contains("mark_nv(5")).take(2).collect::<Vec<_>>().join("\n")
70
+ );
71
+ assert!(
72
+ rust.contains("scaleInto_nv(6_f64, &src, &mut dst)"),
73
+ "scaleInto call passes & / &mut:\n{}",
74
+ rust.lines().filter(|l| l.contains("scaleInto_nv(6")).take(2).collect::<Vec<_>>().join("\n")
75
+ );
76
+ // The local arrays became native Vecs (the escape into the native-vec fn is not a boxing escape).
77
+ assert!(
78
+ rust.contains("let mut a: Vec<bool>") && rust.contains("let mut src: Vec<f64>"),
79
+ "caller arrays are unboxed native Vecs"
80
+ );
81
+ }
82
+
83
+ #[test]
84
+ fn queens_place_devirtualizes_to_native_vec_fn() {
85
+ let rust = compile_fixture_typed("tests/perf/queens.tish");
86
+ assert!(
87
+ rust.contains("fn place_nv(mut n: f64, mut row: f64, cols: &mut Vec<bool>, diag1: &mut Vec<bool>, diag2: &mut Vec<bool>) -> f64"),
88
+ "queens' place lowers to a native free fn over three &mut Vec<bool> params:\n{}",
89
+ rust.lines().filter(|l| l.contains("place_nv")).take(3).collect::<Vec<_>>().join("\n")
90
+ );
91
+ assert!(
92
+ rust.contains("place_nv(5_f64, 0_f64") || rust.contains("place_nv(match") || rust.contains("place_nv("),
93
+ "the place call routes to place_nv"
94
+ );
95
+ }
96
+
97
+ #[test]
98
+ fn spectral_norm_devirtualizes_with_inlined_evala() {
99
+ // `multiplyAv`/`multiplyAtv` (over `&Vec<f64>` + `&mut Vec<f64>`) call `evalA`, a numeric leaf fn.
100
+ // `evalA` inlines at the native-f64 call site (no dispatch, no boxed reference), so the native-vec
101
+ // group de-virtualizes. The boxed `evalA` closure is left intact for any non-f64 callers.
102
+ let rust = compile_fixture_typed("tests/perf/spectral_norm.tish");
103
+ assert!(
104
+ rust.contains("fn multiplyAv_nv(") && (rust.contains("v: &Vec<f64>") || rust.contains("av: &mut Vec<f64>")),
105
+ "multiplyAv lowers to a native-vec fn:\n{}",
106
+ rust.lines().filter(|l| l.contains("multiplyAv_nv")).take(3).collect::<Vec<_>>().join("\n")
107
+ );
108
+ assert!(
109
+ rust.contains("fn multiplyAtAv_nv("),
110
+ "multiplyAtAv should forward to native-vec callees:\n{}",
111
+ rust.lines().filter(|l| l.contains("multiplyAtAv_nv")).take(3).collect::<Vec<_>>().join("\n")
112
+ );
113
+ // evalA is inlined: the native-vec body has the substituted body (a `_inl…` temp) and does NOT
114
+ // call evalA (no `value_call`/`evalA(` inside multiplyAv_nv).
115
+ let mav = rust
116
+ .lines()
117
+ .skip_while(|l| !l.contains("fn multiplyAv_nv("))
118
+ .take_while(|l| !l.trim_start().starts_with("fn ") || l.contains("multiplyAv_nv"))
119
+ .collect::<Vec<_>>()
120
+ .join("\n");
121
+ assert!(
122
+ mav.contains("_inl") && !mav.contains("value_call"),
123
+ "evalA must be inlined into multiplyAv_nv (substituted temps, no value_call):\n{}",
124
+ mav
125
+ );
126
+ if rust.contains("fn spectralNorm_nv(") {
127
+ let sn = rust.split("fn spectralNorm_nv(").nth(1).unwrap();
128
+ let sn = sn.split("fn run()").next().unwrap_or(sn);
129
+ assert!(
130
+ sn.contains("let mut u: Vec<f64>"),
131
+ "spectralNorm_nv should keep u/v/w as native Vec<f64> locals"
132
+ );
133
+ assert!(
134
+ sn.contains("multiplyAtAv_nv(") && !sn.contains("multiplyAtAv_native("),
135
+ "spectralNorm_nv should call multiplyAtAv_nv, not the boxed native shim"
136
+ );
137
+ }
138
+ }
139
+
140
+ #[test]
141
+ fn spectral_norm_embedded_lib_devirtualizes_to_native_vec_fns() {
142
+ // EmbeddedLib mode fully de-virtualizes the `multiply*` group to native-vec (`_nv`) fns over
143
+ // `Vec<f64>` params (#350-352 native struct-array codegen). Once the whole group de-virtualizes,
144
+ // the old boxed `*_native` shims this test used to assert are correctly no longer emitted —
145
+ // matching the typed-mode `spectral_norm_devirtualizes_with_inlined_evala` check above (which
146
+ // asserts `multiplyAtAv` routes to `_nv`, not the boxed `_native` shim). Cross-backend soundness
147
+ // of the de-virtualized code is covered by the gauntlet + `tests/core/native_vec_params`.
148
+ let rust = compile_fixture_embedded_lib("tests/perf/spectral_norm.tish");
149
+ assert!(
150
+ rust.contains("fn multiplyAv_nv(")
151
+ && rust.contains("fn multiplyAtv_nv(")
152
+ && rust.contains("fn multiplyAtAv_nv("),
153
+ "multiply* should de-virtualize to native-vec fns:\n{}",
154
+ rust.lines().filter(|l| l.contains("_nv(")).take(6).collect::<Vec<_>>().join("\n")
155
+ );
156
+ assert!(
157
+ rust.contains("v: &Vec<f64>") && rust.contains("av: &mut Vec<f64>"),
158
+ "native-vec fns take Vec<f64> params by &/&mut ref"
159
+ );
160
+ assert!(
161
+ !rust.contains("fn multiplyAv_native(")
162
+ && !rust.contains("fn multiplyAtv_native(")
163
+ && !rust.contains("fn multiplyAtAv_native("),
164
+ "the boxed `*_native` shims must be elided once the group fully de-virtualizes:\n{}",
165
+ rust.lines().filter(|l| l.contains("_native(")).take(6).collect::<Vec<_>>().join("\n")
166
+ );
167
+ }
168
+
169
+ #[test]
170
+ fn spectral_norm_embedded_lib_with_runtime_features_devirtualizes() {
171
+ // The same de-virtualization holds with the runtime features enabled — they swap the embedded
172
+ // runtime in, but don't change the native-vec lowering of the user fns (still no boxed shims).
173
+ let features: Vec<String> = [
174
+ "http", "timers", "fs", "process", "regex", "ws", "tty",
175
+ ]
176
+ .into_iter()
177
+ .map(String::from)
178
+ .collect();
179
+ let rust =
180
+ compile_fixture_embedded_lib_with_features("tests/perf/spectral_norm.tish", &features);
181
+ assert!(
182
+ rust.contains("fn multiplyAv_nv(") && rust.contains("fn multiplyAtAv_nv("),
183
+ "runtime-feature build still de-virtualizes multiply* to native-vec fns:\n{}",
184
+ rust.lines().filter(|l| l.contains("_nv(")).take(6).collect::<Vec<_>>().join("\n")
185
+ );
186
+ assert!(
187
+ !rust.contains("fn multiplyAv_native(") && !rust.contains("fn multiplyAtAv_native("),
188
+ "no boxed `*_native` shims under runtime features either"
189
+ );
190
+ }
@@ -0,0 +1,60 @@
1
+ //! Generated-Rust assertions for #176 — native numeric globals (`thread_local Cell<f64>`).
2
+
3
+ use std::path::PathBuf;
4
+
5
+ use tishlang_compile::compile_project_full;
6
+
7
+ fn enable_typed_flags() {
8
+ }
9
+
10
+ fn compile_fixture_typed(rel: &str) -> String {
11
+ enable_typed_flags();
12
+ let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
13
+ let path = manifest.join("../..").join(rel).canonicalize().unwrap();
14
+ let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
15
+ rust
16
+ }
17
+
18
+ #[test]
19
+ fn fasta_seed_lowers_to_thread_local_and_genrandom_native() {
20
+ let rust = compile_fixture_typed("tests/perf/fasta.tish");
21
+ assert!(
22
+ rust.contains("thread_local!"),
23
+ "fasta emits thread_local globals:\n{}",
24
+ rust.lines().filter(|l| l.contains("thread_local") || l.contains("G_SEED")).take(6).collect::<Vec<_>>().join("\n")
25
+ );
26
+ assert!(
27
+ rust.contains("static G_SEED: std::cell::Cell<f64>"),
28
+ "seed lowers to G_SEED Cell:\n{}",
29
+ rust.lines().filter(|l| l.contains("G_SEED")).take(4).collect::<Vec<_>>().join("\n")
30
+ );
31
+ assert!(
32
+ rust.contains("fn genRandom_native("),
33
+ "genRandom becomes M5-eligible after native global seed:\n{}",
34
+ rust.lines().filter(|l| l.contains("genRandom")).take(6).collect::<Vec<_>>().join("\n")
35
+ );
36
+ assert!(
37
+ rust.contains("G_SEED.with(|c| c.set("),
38
+ "genRandom_native mutates G_SEED:\n{}",
39
+ rust.lines().filter(|l| l.contains("G_SEED.with")).take(6).collect::<Vec<_>>().join("\n")
40
+ );
41
+ }
42
+
43
+ #[test]
44
+ fn native_numeric_global_fixture_interleaved_state() {
45
+ let rust = compile_fixture_typed("tests/core/native_numeric_global.tish");
46
+ assert!(
47
+ rust.contains("static G_COUNTER: std::cell::Cell<f64>"),
48
+ "counter global lowers:\n{}",
49
+ rust.lines().filter(|l| l.contains("G_COUNTER")).take(4).collect::<Vec<_>>().join("\n")
50
+ );
51
+ assert!(
52
+ rust.contains("G_COUNTER.with(|c| c.get())") && rust.contains("G_COUNTER.with(|c| c.set("),
53
+ "boxed + native paths share G_COUNTER:\n{}",
54
+ rust.lines()
55
+ .filter(|l| l.contains("G_COUNTER::with"))
56
+ .take(8)
57
+ .collect::<Vec<_>>()
58
+ .join("\n")
59
+ );
60
+ }
@@ -0,0 +1,167 @@
1
+ //! Generated-Rust assertions for #177 follow-on — Vec<f64> returns, mandel native, fasta cum path.
2
+
3
+ use std::path::PathBuf;
4
+
5
+ use tishlang_compile::compile_project_full;
6
+
7
+ fn enable_typed_flags() {
8
+ }
9
+
10
+ fn compile_fixture_typed(rel: &str) -> String {
11
+ enable_typed_flags();
12
+ let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
13
+ let path = manifest.join("../..").join(rel).canonicalize().unwrap();
14
+ let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
15
+ rust
16
+ }
17
+
18
+ #[test]
19
+ fn fasta_lowers_cumulative_and_native_arrays() {
20
+ let rust = compile_fixture_typed("tests/perf/fasta.tish");
21
+ assert!(rust.contains("fn cumulative_nv("), "cumulative_nv missing");
22
+ assert!(
23
+ rust.contains("const G_CODES") || rust.contains("let mut codes: Vec<f64>"),
24
+ "codes not lowered to module const or native vec"
25
+ );
26
+ assert!(
27
+ rust.contains("const G_PROBS_CUM"),
28
+ "precomputed cumulative array missing"
29
+ );
30
+ if rust.contains("fn fastaRandom_native(") {
31
+ assert!(
32
+ rust.contains("fastaRandom_native("),
33
+ "fastaRandom_native declared but not called"
34
+ );
35
+ assert!(
36
+ rust.contains("G_CODES["),
37
+ "fastaRandom_native should index G_CODES directly"
38
+ );
39
+ assert!(
40
+ rust.contains("_lcg_seed"),
41
+ "fastaRandom_native should hoist LCG seed to a local"
42
+ );
43
+ assert!(
44
+ rust.contains("_lcg_seed: i64") && rust.contains("3877i64"),
45
+ "fastaRandom_native should use integer LCG arithmetic"
46
+ );
47
+ }
48
+ }
49
+
50
+ #[test]
51
+ fn mandelbrot_lowers_mandel_native() {
52
+ let rust = compile_fixture_typed("tests/perf/mandelbrot.tish");
53
+ // Prefer top-level `mandel_native` when M5-eligible; the hot loop is already native f64
54
+ // inside the closure even when this is absent.
55
+ if rust.contains("fn mandel_native(") {
56
+ assert!(
57
+ rust.contains("mandel_native("),
58
+ "mandel_native declared but not called"
59
+ );
60
+ assert!(
61
+ rust.contains("for _usize_iter") && rust.contains("0..100"),
62
+ "mandel_native should use usize bounded escape loop for maxIter=100"
63
+ );
64
+ assert!(
65
+ rust.contains("_stayed_") && rust.contains("if _stayed_"),
66
+ "mandel_native should fuse iter===maxIter into stayed flag"
67
+ );
68
+ assert!(
69
+ rust.contains("count += 1_f64"),
70
+ "mandel_native should increment count with +="
71
+ );
72
+ if let Some(native) = rust.split("fn mandel_native(").nth(1) {
73
+ let native = native.split("fn run()").next().unwrap_or(native);
74
+ assert!(
75
+ !native.contains("iter = (iter + 1_f64)"),
76
+ "mandel_native should skip iter increment in usize escape loop"
77
+ );
78
+ assert!(
79
+ native.contains("0.0025_f64") && !native.contains("let mut py:"),
80
+ "mandel_native should fuse py/h and px/w into reciprocal coord init"
81
+ );
82
+ assert!(
83
+ !native.contains("let mut xt:"),
84
+ "mandel_native should fuse x2/y2/xy iteration temps"
85
+ );
86
+ assert!(
87
+ native.contains("let x2 =") && native.contains("let xy ="),
88
+ "mandel_native should emit fused iteration temps"
89
+ );
90
+ }
91
+ }
92
+ }
93
+
94
+ #[test]
95
+ fn fannkuch_nv_uses_direct_flip_indexing() {
96
+ let rust = compile_fixture_typed("tests/perf/fannkuch.tish");
97
+ if rust.contains("fn fannkuch_nv(") {
98
+ let nv = rust.split("fn fannkuch_nv(").nth(1).unwrap();
99
+ let nv = nv.split("fn run()").next().unwrap_or(nv);
100
+ assert!(
101
+ nv.contains("let ku =") && nv.contains("for _usize_flip_"),
102
+ "fannkuch_nv flip loop should fuse to ku half-loop"
103
+ );
104
+ assert!(
105
+ !nv.contains("let mut k2:") && !nv.contains("let mut temp:"),
106
+ "fannkuch_nv fused flip should not emit k2/temp"
107
+ );
108
+ assert!(
109
+ !nv.contains("perm.get(((k - i))"),
110
+ "fannkuch_nv should not use perm.get for k-i sub-index"
111
+ );
112
+ assert!(
113
+ nv.contains("copy_within(1..((r as usize) + 1), 0)")
114
+ || nv.contains("copy_within(1..((r) as usize + 1), 0)")
115
+ || nv.contains("copy_within(1..(_ru + 1), 0)"),
116
+ "fannkuch_nv rotation should shift perm1 via copy_within"
117
+ );
118
+ assert!(
119
+ nv.contains("perm1[(r) as usize] = perm0")
120
+ || nv.contains("perm1[r as usize] = perm0")
121
+ || nv.contains("perm1[(r as usize)] = perm0")
122
+ || nv.contains("perm1[(r) as usize] = (perm0) as i32"),
123
+ "fannkuch_nv rotation should assign perm1[r] without resize"
124
+ );
125
+ assert!(
126
+ nv.contains("count[(r) as usize] = (count[(r) as usize] - 1)")
127
+ || nv.contains("count[r as usize] = (count[r as usize] - 1)")
128
+ || nv.contains("count[(r) as usize] = (count[(r) as usize] - 1i32)")
129
+ || nv.contains("count[r as usize] = (count[r as usize] - 1i32)")
130
+ || nv.contains("count[(r) as usize] = (count[(r) as usize] - 1_f64)"),
131
+ "fannkuch_nv should decrement count[r] via direct indexing"
132
+ );
133
+ assert!(
134
+ nv.contains("perm = std::iter::repeat(0i32).take(10)")
135
+ || nv.contains("perm = std::iter::repeat(0_f64).take(10)")
136
+ || nv.contains("perm.extend(std::iter::repeat(0i32).take(10))"),
137
+ "fannkuch_nv should bulk-init perm array"
138
+ );
139
+ assert!(
140
+ nv.contains("perm1 = (0..10).collect()")
141
+ || nv.contains("perm1 = (0..10).map(|j| j as f64).collect()"),
142
+ "fannkuch_nv should iota-init perm1"
143
+ );
144
+ assert!(
145
+ nv.contains(".copy_from_slice(&perm1)"),
146
+ "fannkuch_nv should copy perm1 into perm via copy_from_slice"
147
+ );
148
+ assert!(
149
+ nv.contains("while r != 1_f64") && nv.contains("count[ri - 1] = r"),
150
+ "fannkuch_nv should fill count via while r!=1 loop (not bulk 1..n init)"
151
+ );
152
+ assert!(
153
+ !nv.contains("_usize_shift_"),
154
+ "fannkuch_nv rotation should use copy_within, not per-element shift loop"
155
+ );
156
+ assert!(
157
+ (nv.contains("if ((permCount as i64) & 1) == 0")
158
+ || (nv.contains("to_int_unchecked") && nv.contains("& 1"))),
159
+ "fannkuch_nv checksum parity should use fast int parity"
160
+ );
161
+ assert!(
162
+ nv.contains("count[ri - 1] = r as i32")
163
+ || nv.contains("count[ri - 1] = r"),
164
+ "fannkuch_nv count[r-1] init should use direct indexing in while loop"
165
+ );
166
+ }
167
+ }