@tishlang/tish 2.12.0 → 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.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -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 +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- 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 +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- 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/src/lib.rs +75 -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_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -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_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//! #178 Bun-style fusion targets — AST shape checks + codegen assertions when fusion lands.
|
|
2
|
+
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
|
|
6
|
+
use tishlang_ast::{BinOp, Expr, Statement};
|
|
7
|
+
use tishlang_compile::compile_project_full;
|
|
8
|
+
use tishlang_opt::optimize;
|
|
9
|
+
use tishlang_parser::parse;
|
|
10
|
+
|
|
11
|
+
fn enable_typed_flags() {
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn compile_fixture_typed(rel: &str) -> String {
|
|
15
|
+
enable_typed_flags();
|
|
16
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
17
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
18
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
19
|
+
rust
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn stmt_slice_unwrapped(body: &Statement) -> Vec<&Statement> {
|
|
23
|
+
let mut cur = body;
|
|
24
|
+
loop {
|
|
25
|
+
match cur {
|
|
26
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
27
|
+
if statements.len() == 1 {
|
|
28
|
+
cur = &statements[0];
|
|
29
|
+
} else {
|
|
30
|
+
return statements.iter().collect();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
other => return vec![other],
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn find_flip_for_body(s: &Statement) -> Option<&Statement> {
|
|
39
|
+
match s {
|
|
40
|
+
Statement::For { cond, body, .. } => {
|
|
41
|
+
if let Some(cond) = cond.as_ref() {
|
|
42
|
+
if let Expr::Binary {
|
|
43
|
+
op: BinOp::Lt,
|
|
44
|
+
right,
|
|
45
|
+
..
|
|
46
|
+
} = cond
|
|
47
|
+
{
|
|
48
|
+
if matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "k2") {
|
|
49
|
+
return Some(body);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
None
|
|
54
|
+
}
|
|
55
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
56
|
+
for st in statements {
|
|
57
|
+
if let Some(b) = find_flip_for_body(st) {
|
|
58
|
+
return Some(b);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
None
|
|
62
|
+
}
|
|
63
|
+
Statement::While { body, .. } => find_flip_for_body(body),
|
|
64
|
+
Statement::If { then_branch, .. } => find_flip_for_body(then_branch),
|
|
65
|
+
_ => None,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn fannkuch_flip_for_body_is_three_index_assigns() {
|
|
71
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
72
|
+
let path = manifest
|
|
73
|
+
.join("../../tests/perf/fannkuch.tish")
|
|
74
|
+
.canonicalize()
|
|
75
|
+
.unwrap();
|
|
76
|
+
let program = optimize(&parse(&fs::read_to_string(&path).unwrap()).unwrap());
|
|
77
|
+
let body = program
|
|
78
|
+
.statements
|
|
79
|
+
.iter()
|
|
80
|
+
.find_map(|s| {
|
|
81
|
+
if let Statement::FunDecl { name, body, .. } = s {
|
|
82
|
+
if name.as_ref() == "fannkuch" {
|
|
83
|
+
return find_flip_for_body(body);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
None
|
|
87
|
+
})
|
|
88
|
+
.expect("flip for body");
|
|
89
|
+
let stmts = stmt_slice_unwrapped(body);
|
|
90
|
+
assert_eq!(stmts.len(), 3);
|
|
91
|
+
for st in &stmts[1..] {
|
|
92
|
+
let Statement::ExprStmt { expr, .. } = st else {
|
|
93
|
+
panic!("expected expr stmt");
|
|
94
|
+
};
|
|
95
|
+
assert!(matches!(expr, Expr::IndexAssign { .. }));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn fannkuch_nv_uses_usize_sub_index_in_flip() {
|
|
101
|
+
let rust = compile_fixture_typed("tests/perf/fannkuch.tish");
|
|
102
|
+
if rust.contains("fn fannkuch_nv(") {
|
|
103
|
+
let nv = rust.split("fn fannkuch_nv(").nth(1).unwrap();
|
|
104
|
+
let nv = nv.split("fn run()").next().unwrap_or(nv);
|
|
105
|
+
assert!(
|
|
106
|
+
nv.contains("let ku =") && nv.contains("for _usize_flip_"),
|
|
107
|
+
"flip loop should fuse to ku half-loop with usize swaps"
|
|
108
|
+
);
|
|
109
|
+
assert!(
|
|
110
|
+
!nv.contains("let mut k2:") && !nv.contains("let mut temp:"),
|
|
111
|
+
"fused flip loop should not emit k2/temp temps"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//! #178 — recursive-struct native lowering.
|
|
2
|
+
//!
|
|
3
|
+
//! A developer-shaped recursive binary tree (arbitrary identifiers, NOT the fixture names
|
|
4
|
+
//! bottomUpTree/itemCheck/binaryTrees) must lower to a native arena: an `i32`-indexed struct,
|
|
5
|
+
//! native `build`/`check` free fns threading `&mut Vec<Node>` / `&Vec<Node>`, and a top-level
|
|
6
|
+
//! routed call — with NO `object_from_pairs` / `get_prop` / `value_call` on the hot path. This is
|
|
7
|
+
//! the real, name-independent fix that makes the boxed `binary_trees` path fast under any names.
|
|
8
|
+
//! See docs/recursive-struct-native.md.
|
|
9
|
+
|
|
10
|
+
use std::path::PathBuf;
|
|
11
|
+
|
|
12
|
+
use tishlang_compile::compile_project_full;
|
|
13
|
+
|
|
14
|
+
fn enable_flags() {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SRC: &str = r#"
|
|
18
|
+
function buildNode(d) {
|
|
19
|
+
if (d > 0) { return { left: buildNode(d - 1), right: buildNode(d - 1) } }
|
|
20
|
+
return { left: null, right: null }
|
|
21
|
+
}
|
|
22
|
+
function sumNode(node) {
|
|
23
|
+
if (node.left === null) { return 1 }
|
|
24
|
+
return 1 + sumNode(node.left) + sumNode(node.right)
|
|
25
|
+
}
|
|
26
|
+
let t0 = Date.now()
|
|
27
|
+
let check = sumNode(buildNode(10))
|
|
28
|
+
console.log("GAUNTLET rec " + (Date.now() - t0) + " " + check)
|
|
29
|
+
"#;
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn recursive_tree_lowers_to_native_arena() {
|
|
33
|
+
enable_flags();
|
|
34
|
+
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
|
|
35
|
+
let path = dir.join("rec_tree_dev_178.tish");
|
|
36
|
+
std::fs::write(&path, SRC).unwrap();
|
|
37
|
+
|
|
38
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
39
|
+
|
|
40
|
+
// The synthesized arena node struct (i32 child indices, not Option<Box> / not boxed Value).
|
|
41
|
+
assert!(
|
|
42
|
+
rust.contains("struct TishStruct_TishRecNode"),
|
|
43
|
+
"expected a synthesized recursive node struct"
|
|
44
|
+
);
|
|
45
|
+
// Native builder + consumer free fns over the arena.
|
|
46
|
+
assert!(
|
|
47
|
+
rust.contains("fn buildNode_rec(") && rust.contains("__rec_arena: &mut Vec<"),
|
|
48
|
+
"expected a native arena builder fn"
|
|
49
|
+
);
|
|
50
|
+
assert!(
|
|
51
|
+
rust.contains("fn sumNode_rec(__rec_idx: i32, __rec_arena: &Vec<"),
|
|
52
|
+
"expected a native arena consumer fn"
|
|
53
|
+
);
|
|
54
|
+
// Top-level call routed through the arena, not the boxed closure.
|
|
55
|
+
assert!(
|
|
56
|
+
rust.contains("let __rec_root = buildNode_rec("),
|
|
57
|
+
"expected the top-level builder call to be routed to the native arena fn"
|
|
58
|
+
);
|
|
59
|
+
assert!(
|
|
60
|
+
rust.contains("sumNode_rec(__rec_root, &__rec_arena)"),
|
|
61
|
+
"expected the consumer to be invoked on the arena root index"
|
|
62
|
+
);
|
|
63
|
+
// No per-node boxed allocation on the recursive build path.
|
|
64
|
+
assert!(
|
|
65
|
+
rust.contains("__rec_arena.push(TishStruct_TishRecNode"),
|
|
66
|
+
"expected nodes to be bump-allocated into the arena Vec"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The FULL binary_trees shape (with a loop-bearing orchestrator) — developer identifiers.
|
|
71
|
+
const SRC_ORCH: &str = r#"
|
|
72
|
+
function makeTree(d) {
|
|
73
|
+
if (d > 0) { return { left: makeTree(d - 1), right: makeTree(d - 1) } }
|
|
74
|
+
return { left: null, right: null }
|
|
75
|
+
}
|
|
76
|
+
function countTree(node) {
|
|
77
|
+
if (node.left === null) { return 1 }
|
|
78
|
+
return 1 + countTree(node.left) + countTree(node.right)
|
|
79
|
+
}
|
|
80
|
+
function run(maxDepth) {
|
|
81
|
+
let minDepth = 4
|
|
82
|
+
let total = countTree(makeTree(maxDepth + 1))
|
|
83
|
+
let longLived = makeTree(maxDepth)
|
|
84
|
+
let depth = minDepth
|
|
85
|
+
while (depth <= maxDepth) {
|
|
86
|
+
let iterations = 1 << (maxDepth - depth + minDepth)
|
|
87
|
+
let sum = 0
|
|
88
|
+
for (let i = 0; i < iterations; i++) { sum = sum + countTree(makeTree(depth)) }
|
|
89
|
+
total = total + sum
|
|
90
|
+
depth = depth + 2
|
|
91
|
+
}
|
|
92
|
+
total = total + countTree(longLived)
|
|
93
|
+
return total
|
|
94
|
+
}
|
|
95
|
+
let t0 = Date.now()
|
|
96
|
+
let check = run(8)
|
|
97
|
+
console.log("GAUNTLET t " + (Date.now() - t0) + " " + check)
|
|
98
|
+
"#;
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn recursive_tree_orchestrator_lowers_to_native_arena() {
|
|
102
|
+
enable_flags();
|
|
103
|
+
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
|
|
104
|
+
let path = dir.join("rec_orch_dev_178.tish");
|
|
105
|
+
std::fs::write(&path, SRC_ORCH).unwrap();
|
|
106
|
+
|
|
107
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
108
|
+
|
|
109
|
+
// The orchestrator becomes a native fn threading the arena and returning f64.
|
|
110
|
+
assert!(
|
|
111
|
+
rust.contains("fn run_rec(") && rust.contains("__rec_arena: &mut Vec<"),
|
|
112
|
+
"expected the orchestrator to lower to a native arena fn"
|
|
113
|
+
);
|
|
114
|
+
// It calls builders/consumers natively (no value_call on the hot path).
|
|
115
|
+
assert!(
|
|
116
|
+
rust.contains("makeTree_rec(") && rust.contains("countTree_rec("),
|
|
117
|
+
"expected the orchestrator to call builder/consumer fns natively"
|
|
118
|
+
);
|
|
119
|
+
// Top-level call routed through a fresh arena.
|
|
120
|
+
assert!(
|
|
121
|
+
rust.contains("let mut __rec_arena") && rust.contains("run_rec("),
|
|
122
|
+
"expected the top-level orchestrator call to set up and thread the arena"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//! #181 — direct Map method dispatch for `new Map()` locals.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
use tishlang_compile::compile_project_full;
|
|
6
|
+
|
|
7
|
+
fn enable_typed_flags() {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn k_nucleotide_uses_direct_map_has_get_set() {
|
|
12
|
+
enable_typed_flags();
|
|
13
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
14
|
+
let path = manifest
|
|
15
|
+
.join("../../tests/perf/k_nucleotide.tish")
|
|
16
|
+
.canonicalize()
|
|
17
|
+
.unwrap();
|
|
18
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
19
|
+
assert!(
|
|
20
|
+
rust.contains("tish_map_has("),
|
|
21
|
+
"expected direct map_has dispatch:\n{}",
|
|
22
|
+
rust.lines()
|
|
23
|
+
.filter(|l| l.contains("map_has") || l.contains("map_set"))
|
|
24
|
+
.take(6)
|
|
25
|
+
.collect::<Vec<_>>()
|
|
26
|
+
.join("\n")
|
|
27
|
+
);
|
|
28
|
+
assert!(
|
|
29
|
+
rust.contains("tish_map_get(") && rust.contains("tish_map_set("),
|
|
30
|
+
"expected direct map_get/set dispatch"
|
|
31
|
+
);
|
|
32
|
+
assert!(
|
|
33
|
+
!rust.contains("get_prop(&(m).clone(), \"has\")"),
|
|
34
|
+
"should not use bound-method has on map local m"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
//! #320 — honest native-array lowering for fn-call-built numeric arrays.
|
|
2
|
+
//!
|
|
3
|
+
//! Two sound, name-independent inference fixes:
|
|
4
|
+
//! fix1: a 0-param numeric fn (`nextBase`, mutates a numeric global, returns number) is eligible
|
|
5
|
+
//! for native `-> f64` promotion (`collect_native_fns` no longer requires ≥1 param).
|
|
6
|
+
//! fix2: a fn PROVEN to always return a number lets `infer_expr_type(f(...))` be `number`, so
|
|
7
|
+
//! `seq.push(nextBase())` keeps `seq` a native `Vec<f64>` instead of a boxed `Value[]`.
|
|
8
|
+
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
|
|
11
|
+
use tishlang_compile::compile_project_full;
|
|
12
|
+
|
|
13
|
+
/// The general typed-native inference flags.
|
|
14
|
+
fn enable_typed_flags() {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Write `src` under the workspace `target/` (scratch lives there, per repo convention) and compile.
|
|
18
|
+
fn compile_src(stem: &str, src: &str) -> String {
|
|
19
|
+
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/perf_codegen_320");
|
|
20
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
21
|
+
let path = dir.join(format!("{stem}.tish"));
|
|
22
|
+
std::fs::write(&path, src).unwrap();
|
|
23
|
+
let path = path.canonicalize().unwrap();
|
|
24
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
25
|
+
rust
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NON_ESCAPING: &str = r#"
|
|
29
|
+
let seed = 7
|
|
30
|
+
function nextBase() {
|
|
31
|
+
seed = (seed * 3877 + 29573) % 139968
|
|
32
|
+
if (seed < 34992) { return 0 }
|
|
33
|
+
if (seed < 69984) { return 1 }
|
|
34
|
+
if (seed < 104976) { return 2 }
|
|
35
|
+
return 3
|
|
36
|
+
}
|
|
37
|
+
let seq = []
|
|
38
|
+
for (let i = 0; i < 100; i++) { seq.push(nextBase()) }
|
|
39
|
+
let sum = 0
|
|
40
|
+
for (let i = 0; i < seq.length; i++) { sum = sum + seq[i] }
|
|
41
|
+
console.log("R " + sum)
|
|
42
|
+
"#;
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn zero_param_numeric_fn_goes_native() {
|
|
46
|
+
enable_typed_flags();
|
|
47
|
+
let rust = compile_src("zero_param", NON_ESCAPING);
|
|
48
|
+
// fix1: the 0-param fn is promoted to a native `-> f64` form.
|
|
49
|
+
assert!(
|
|
50
|
+
rust.contains("fn nextBase_native() -> f64"),
|
|
51
|
+
"0-param numeric fn should get a native f64 form (fix1)"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn numeric_returning_fn_keeps_pushed_array_native() {
|
|
57
|
+
enable_typed_flags();
|
|
58
|
+
let rust = compile_src("native_push", NON_ESCAPING);
|
|
59
|
+
// fix2: `seq.push(nextBase())` keeps `seq` a native `Vec<f64>` (the pushed value infers numeric).
|
|
60
|
+
assert!(
|
|
61
|
+
rust.contains("let mut seq: Vec<f64>"),
|
|
62
|
+
"seq built by push(numericFn()) should be a native Vec<f64> (fix2)"
|
|
63
|
+
);
|
|
64
|
+
// And the push routes straight to the native fn (no boxed Value array push).
|
|
65
|
+
assert!(
|
|
66
|
+
rust.contains("seq.push(nextBase_native())"),
|
|
67
|
+
"push should call the native fn into the native Vec (fix1+fix2)"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A fn that can fall through to an implicit `undefined` (no trailing unconditional return) must NOT
|
|
72
|
+
/// be treated as number-returning — else `a.push(maybe())` could put `undefined` in a `Vec<f64>`.
|
|
73
|
+
#[test]
|
|
74
|
+
fn fallthrough_fn_is_not_number_returning() {
|
|
75
|
+
enable_typed_flags();
|
|
76
|
+
let src = r#"
|
|
77
|
+
let g = 0
|
|
78
|
+
function maybe() {
|
|
79
|
+
g = g + 1
|
|
80
|
+
if (g < 10) { return 1 }
|
|
81
|
+
}
|
|
82
|
+
let a = []
|
|
83
|
+
for (let i = 0; i < 100; i++) { a.push(maybe()) }
|
|
84
|
+
let sum = 0
|
|
85
|
+
for (let i = 0; i < a.length; i++) { sum = sum + 1 }
|
|
86
|
+
console.log("R " + sum)
|
|
87
|
+
"#;
|
|
88
|
+
let rust = compile_src("fallthrough", src);
|
|
89
|
+
// Unsound to type `a` as Vec<f64> here — it must stay a boxed array.
|
|
90
|
+
assert!(
|
|
91
|
+
!rust.contains("let mut a: Vec<f64>"),
|
|
92
|
+
"fall-through fn must not make the pushed array native (soundness)"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// #320 part 2: a read-only `number[]` param of a normal (boxed-body, boxed-return) fn is unboxed
|
|
97
|
+
/// once into an owned native `Vec<f64>`, so the body indexes it natively. This is the
|
|
98
|
+
/// cross-fn-boundary lever — `kNucleotide(seq, k)` indexes `seq` but builds & returns a boxed Map,
|
|
99
|
+
/// so it is NOT a native-vec fn.
|
|
100
|
+
fn enable_arr_param() {}
|
|
101
|
+
|
|
102
|
+
const ARR_PARAM: &str = r#"
|
|
103
|
+
let seed = 7
|
|
104
|
+
function nextBase() {
|
|
105
|
+
seed = (seed * 3877 + 29573) % 139968
|
|
106
|
+
if (seed < 34992) { return 0 }
|
|
107
|
+
if (seed < 69984) { return 1 }
|
|
108
|
+
if (seed < 104976) { return 2 }
|
|
109
|
+
return 3
|
|
110
|
+
}
|
|
111
|
+
function count(seq, k) {
|
|
112
|
+
let m = new Map()
|
|
113
|
+
let n = seq.length - k + 1
|
|
114
|
+
for (let i = 0; i < n; i++) {
|
|
115
|
+
let key = 0
|
|
116
|
+
for (let j = 0; j < k; j++) { key = key * 4 + seq[i + j] }
|
|
117
|
+
if (m.has(key)) { m.set(key, m.get(key) + 1) } else { m.set(key, 1) }
|
|
118
|
+
}
|
|
119
|
+
return m
|
|
120
|
+
}
|
|
121
|
+
let seq = []
|
|
122
|
+
for (let i = 0; i < 100; i++) { seq.push(nextBase()) }
|
|
123
|
+
let m = count(seq, 4)
|
|
124
|
+
let sum = 0
|
|
125
|
+
for (let v of m.values()) { sum = sum + v }
|
|
126
|
+
console.log("R " + sum)
|
|
127
|
+
"#;
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn readonly_arr_param_unboxed_to_native_vec() {
|
|
131
|
+
enable_arr_param();
|
|
132
|
+
let rust = compile_src("arr_param", ARR_PARAM);
|
|
133
|
+
// The `seq` param is unboxed once into an owned native Vec<f64> at the closure entry (a packed
|
|
134
|
+
// NumberArray clones its backing; a boxed Array is mapped element-wise).
|
|
135
|
+
assert!(
|
|
136
|
+
rust.contains("let mut seq: Vec<f64> = match args.get(")
|
|
137
|
+
&& rust.contains("Some(Value::NumberArray(a)) => a.borrow().clone()"),
|
|
138
|
+
"read-only number[] param should be unboxed to a native Vec<f64>"
|
|
139
|
+
);
|
|
140
|
+
// And the hot inner loop indexes it natively (f64 mul + native vec read), no boxed get_index.
|
|
141
|
+
assert!(
|
|
142
|
+
rust.contains("key * 4_f64") && rust.contains("seq.get("),
|
|
143
|
+
"inner loop should be native f64 arithmetic over a native Vec read"
|
|
144
|
+
);
|
|
145
|
+
assert!(
|
|
146
|
+
!rust.contains("tishlang_runtime::get_index(&seq")
|
|
147
|
+
&& !rust.contains("get_index(&(seq)"),
|
|
148
|
+
"seq must not be read via the boxed runtime get_index inside the fn"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn mutating_arr_param_stays_boxed() {
|
|
154
|
+
enable_arr_param();
|
|
155
|
+
// `a` is index-ASSIGNED → an owned native copy would silently drop the write, so the param must
|
|
156
|
+
// stay boxed (classify_vec_param reports is_mut, and the detection requires read-only).
|
|
157
|
+
let src = r#"
|
|
158
|
+
function bump(a) { a[0] = a[0] + 1; return a.length }
|
|
159
|
+
let arr = []
|
|
160
|
+
for (let i = 0; i < 10; i++) { arr.push(i) }
|
|
161
|
+
let r = bump(arr)
|
|
162
|
+
console.log("R " + r)
|
|
163
|
+
"#;
|
|
164
|
+
let rust = compile_src("mut_arr_param", src);
|
|
165
|
+
assert!(
|
|
166
|
+
!rust.contains("let mut a: Vec<f64>"),
|
|
167
|
+
"a mutated array param must stay boxed (soundness — owned copy would lose writes)"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn escaping_arr_param_stays_boxed() {
|
|
173
|
+
enable_arr_param();
|
|
174
|
+
// `seq.push` MUTATES the param via a method (not index-assign): an owned copy would lose the
|
|
175
|
+
// caller-visible push. `scan_param_use` flags this as an escape → the param stays boxed.
|
|
176
|
+
let src = r#"
|
|
177
|
+
function build(seq) {
|
|
178
|
+
let m = new Map()
|
|
179
|
+
for (let i = 0; i < seq.length; i++) { m.set(i, seq[i]) }
|
|
180
|
+
seq.push(99)
|
|
181
|
+
return m
|
|
182
|
+
}
|
|
183
|
+
let arr = []
|
|
184
|
+
for (let i = 0; i < 10; i++) { arr.push(i) }
|
|
185
|
+
let m = build(arr)
|
|
186
|
+
console.log("R " + arr.length)
|
|
187
|
+
"#;
|
|
188
|
+
let rust = compile_src("escape_arr_param", src);
|
|
189
|
+
assert!(
|
|
190
|
+
!rust.contains("let mut seq: Vec<f64>"),
|
|
191
|
+
"an escaping (pushed) array param must stay boxed (soundness)"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn non_number_array_arg_keeps_callee_boxed() {
|
|
197
|
+
enable_arr_param();
|
|
198
|
+
// `joinup` reads its param read-only, but the ONLY caller passes a NON-number[] array. Unboxing
|
|
199
|
+
// to Vec<f64> would turn "x" into NaN (vs JS string concat) — so the call-site check keeps the
|
|
200
|
+
// callee fully boxed.
|
|
201
|
+
let src = r#"
|
|
202
|
+
function joinup(seq) { let r = ""; for (let i = 0; i < seq.length; i++) { r = r + seq[i] } return r }
|
|
203
|
+
let arr = [1, "x", 3]
|
|
204
|
+
console.log(joinup(arr))
|
|
205
|
+
"#;
|
|
206
|
+
let rust = compile_src("nonnum_arr_arg", src);
|
|
207
|
+
assert!(
|
|
208
|
+
!rust.contains("let mut seq: Vec<f64>"),
|
|
209
|
+
"a read-only fn whose caller passes a non-number[] array must stay boxed (call-site soundness)"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//! Hoisted module-level f64 array literals (`const G_XS`) must compile when used in for-of / join.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
use tishlang_compile::compile_project_full;
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn typed_array_forof_compiles() {
|
|
9
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
10
|
+
let path = manifest
|
|
11
|
+
.join("../../tests/core/typed_array_forof.tish")
|
|
12
|
+
.canonicalize()
|
|
13
|
+
.unwrap();
|
|
14
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
15
|
+
assert!(rust.contains("const G_XS:"), "expected hoisted xs array");
|
|
16
|
+
assert!(
|
|
17
|
+
!rust.contains("normalize_for_of((xs)"),
|
|
18
|
+
"for-of should use native index loop over G_XS, not boxed xs reference"
|
|
19
|
+
);
|
|
20
|
+
assert!(
|
|
21
|
+
rust.contains("for _fof_i0 in 0..G_XS.len()"),
|
|
22
|
+
"expected native for-of over hoisted G_XS"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn template_literals_hoisted_nums_compiles() {
|
|
28
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
29
|
+
let path = manifest
|
|
30
|
+
.join("../../tests/core/template_literals.tish")
|
|
31
|
+
.canonicalize()
|
|
32
|
+
.unwrap();
|
|
33
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
34
|
+
assert!(rust.contains("const G_NUMS:"));
|
|
35
|
+
assert!(
|
|
36
|
+
rust.contains("array_join(&Value::NumberArray(VmRef::new(G_NUMS"),
|
|
37
|
+
"nums.join should box hoisted G_NUMS, not reference missing local nums"
|
|
38
|
+
);
|
|
39
|
+
assert!(!rust.contains("&nums,"));
|
|
40
|
+
}
|
|
@@ -1036,6 +1036,39 @@ impl Codegen {
|
|
|
1036
1036
|
let v = self.emit_expr(e)?;
|
|
1037
1037
|
self.writeln(&format!("export default {};", v));
|
|
1038
1038
|
}
|
|
1039
|
+
// #305: re-export in ESM mode — emit the native `export { … } from` / `export * from`,
|
|
1040
|
+
// rewriting the `.tish` specifier to the emitted `.js` module path.
|
|
1041
|
+
ExportDeclaration::ReExport {
|
|
1042
|
+
specifiers,
|
|
1043
|
+
all,
|
|
1044
|
+
from,
|
|
1045
|
+
..
|
|
1046
|
+
} => {
|
|
1047
|
+
let spec = rewrite_import_to_js(
|
|
1048
|
+
from.as_ref(),
|
|
1049
|
+
&self.module_path,
|
|
1050
|
+
&self.project_root,
|
|
1051
|
+
self.import_rewrite,
|
|
1052
|
+
)?;
|
|
1053
|
+
if *all {
|
|
1054
|
+
self.writeln(&format!("export * from {:?};", spec));
|
|
1055
|
+
} else {
|
|
1056
|
+
let mut parts: Vec<String> = Vec::new();
|
|
1057
|
+
for s in specifiers {
|
|
1058
|
+
if let ImportSpecifier::Named { name, alias, .. } = s {
|
|
1059
|
+
match alias {
|
|
1060
|
+
Some(a) => parts.push(format!(
|
|
1061
|
+
"{} as {}",
|
|
1062
|
+
Self::escape_ident(name.as_ref()),
|
|
1063
|
+
Self::escape_ident(a.as_ref())
|
|
1064
|
+
)),
|
|
1065
|
+
None => parts.push(Self::escape_ident(name.as_ref()).to_string()),
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
self.writeln(&format!("export {{ {} }} from {:?};", parts.join(", "), spec));
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1039
1072
|
}
|
|
1040
1073
|
Ok(())
|
|
1041
1074
|
}
|