@tishlang/tish-format 1.0.12 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- 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
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
//! Full-stack integration tests: run .tish files with interpreter or each backend and compare
|
|
2
|
+
//! stdout to static expected files (e.g. `fn_any.tish.expected`).
|
|
3
|
+
//!
|
|
4
|
+
//! - Run: `cargo test -p tishlang` (or `cargo nextest run -p tishlang`).
|
|
5
|
+
//! - Generate/update expected files: `REGENERATE_EXPECTED=1 cargo test -p tishlangtest_mvp_programs_interpreter`
|
|
6
|
+
//! then commit the new/updated `tests/core/*.tish.expected` files.
|
|
7
|
+
//! - Compiled outputs are cached under `target/integration_compile_cache/` per backend.
|
|
8
|
+
//! MVP native tests use `native_many/<hash>/` plus one batched nested Cargo build.
|
|
9
|
+
|
|
10
|
+
use std::collections::hash_map::DefaultHasher;
|
|
11
|
+
use std::ffi::OsString;
|
|
12
|
+
use std::hash::{Hash, Hasher};
|
|
13
|
+
use std::io::Read;
|
|
14
|
+
use std::path::{Path, PathBuf};
|
|
15
|
+
use std::process::Command;
|
|
16
|
+
|
|
17
|
+
use rayon::prelude::*;
|
|
18
|
+
use tishlang_native::compile_many_to_native;
|
|
19
|
+
|
|
20
|
+
fn workspace_root() -> PathBuf {
|
|
21
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
22
|
+
.join("..")
|
|
23
|
+
.join("..")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn core_dir() -> PathBuf {
|
|
27
|
+
workspace_root().join("tests").join("core")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Path to the static expected stdout for a .tish file (e.g. fn_any.tish -> fn_any.tish.expected).
|
|
31
|
+
fn expected_path(path: &Path) -> PathBuf {
|
|
32
|
+
path.with_file_name(format!(
|
|
33
|
+
"{}.expected",
|
|
34
|
+
path.file_name().unwrap().to_string_lossy()
|
|
35
|
+
))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Read static expected stdout for a test file. Returns None if the file does not exist.
|
|
39
|
+
fn get_expected(path: &Path) -> Option<String> {
|
|
40
|
+
let p = expected_path(path);
|
|
41
|
+
std::fs::read_to_string(&p).ok()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn target_dir() -> PathBuf {
|
|
45
|
+
std::env::var("CARGO_TARGET_DIR")
|
|
46
|
+
.map(PathBuf::from)
|
|
47
|
+
.unwrap_or_else(|_| workspace_root().join("target"))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Cache dir for tish build outputs (under target/ so CI rust-cache restores it).
|
|
51
|
+
fn integration_compile_cache_dir() -> PathBuf {
|
|
52
|
+
target_dir().join("integration_compile_cache")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Match `tish build` with no `--feature`: link every capability compiled into this `tish` binary.
|
|
56
|
+
fn native_build_features_for_integration_test() -> Vec<String> {
|
|
57
|
+
let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities()
|
|
58
|
+
.into_iter()
|
|
59
|
+
.collect();
|
|
60
|
+
v.sort();
|
|
61
|
+
v
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn combined_mvp_native_inputs_hash(paths: &[PathBuf]) -> u64 {
|
|
65
|
+
let mut h = DefaultHasher::new();
|
|
66
|
+
let feats = native_build_features_for_integration_test();
|
|
67
|
+
feats.len().hash(&mut h);
|
|
68
|
+
for f in &feats {
|
|
69
|
+
f.hash(&mut h);
|
|
70
|
+
}
|
|
71
|
+
paths.len().hash(&mut h);
|
|
72
|
+
for p in paths {
|
|
73
|
+
p.file_name()
|
|
74
|
+
.unwrap_or_default()
|
|
75
|
+
.to_string_lossy()
|
|
76
|
+
.hash(&mut h);
|
|
77
|
+
file_content_hash(p).hash(&mut h);
|
|
78
|
+
}
|
|
79
|
+
// Native batch cache must invalidate when the emitter or `value_call` changes — not only
|
|
80
|
+
// when `.tish` sources change; otherwise CI/rust-cache can keep stale nested binaries.
|
|
81
|
+
let codegen_rs = workspace_root().join("crates/tish_compile/src/codegen.rs");
|
|
82
|
+
if codegen_rs.is_file() {
|
|
83
|
+
file_content_hash(&codegen_rs).hash(&mut h);
|
|
84
|
+
}
|
|
85
|
+
let value_rs = workspace_root().join("crates/tish_core/src/value.rs");
|
|
86
|
+
if value_rs.is_file() {
|
|
87
|
+
file_content_hash(&value_rs).hash(&mut h);
|
|
88
|
+
}
|
|
89
|
+
// Inference (struct/param/return typing — M1/M4/M5) also drives native emission, so the
|
|
90
|
+
// native batch cache must invalidate when it changes too.
|
|
91
|
+
let infer_rs = workspace_root().join("crates/tish_compile/src/infer.rs");
|
|
92
|
+
if infer_rs.is_file() {
|
|
93
|
+
file_content_hash(&infer_rs).hash(&mut h);
|
|
94
|
+
}
|
|
95
|
+
h.finish()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn mvp_native_batch_cache_dir(combined: u64) -> PathBuf {
|
|
99
|
+
integration_compile_cache_dir()
|
|
100
|
+
.join("native_many")
|
|
101
|
+
.join(format!("{:016x}", combined))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Restores the previous process env when dropped (for `TISH_FAST_NATIVE_BUILD` in batch tests).
|
|
105
|
+
struct EnvVarGuard {
|
|
106
|
+
key: &'static str,
|
|
107
|
+
previous: Option<std::ffi::OsString>,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl EnvVarGuard {
|
|
111
|
+
fn set(key: &'static str, value: &str) -> Self {
|
|
112
|
+
let previous = std::env::var_os(key);
|
|
113
|
+
std::env::set_var(key, value);
|
|
114
|
+
Self { key, previous }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
impl Drop for EnvVarGuard {
|
|
119
|
+
fn drop(&mut self) {
|
|
120
|
+
match &self.previous {
|
|
121
|
+
None => std::env::remove_var(self.key),
|
|
122
|
+
Some(v) => std::env::set_var(self.key, v),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn file_content_hash(path: &Path) -> u64 {
|
|
128
|
+
let mut f = std::fs::File::open(path).expect("open file for hash");
|
|
129
|
+
let mut content = Vec::new();
|
|
130
|
+
f.read_to_end(&mut content).expect("read file for hash");
|
|
131
|
+
let mut h = DefaultHasher::new();
|
|
132
|
+
path.to_string_lossy().hash(&mut h);
|
|
133
|
+
content.hash(&mut h);
|
|
134
|
+
h.finish()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Compile a .tish file with the given backend, using a persistent cache so we only run
|
|
138
|
+
/// `tish build` when the file or backend changed. Returns path to the compiled artifact
|
|
139
|
+
/// (binary, .js, or .wasm) in a temp dir; caller may run it and then delete it.
|
|
140
|
+
///
|
|
141
|
+
/// Cache is keyed by backend (native, cranelift, js, wasi) so e.g. cranelift and wasi
|
|
142
|
+
/// compiles of the same file do not overwrite each other: .../cranelift/<stem>_<hash> vs .../wasi/<stem>_<hash>.wasm.
|
|
143
|
+
///
|
|
144
|
+
/// The artifact **basename** must be unique per `(stem, hash, backend)`: nested `tish build`
|
|
145
|
+
/// uses it as the Cargo binary name under the workspace `target/release/`. If native and
|
|
146
|
+
/// cranelift both used `strict_equality_<hash>`, parallel `cargo nextest` could run those
|
|
147
|
+
/// tests concurrently and corrupt the same `target/release/...` output (Linux: ETXTBSY when
|
|
148
|
+
/// executing a binary still being written).
|
|
149
|
+
fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
150
|
+
let stem = path.file_stem().unwrap().to_string_lossy();
|
|
151
|
+
// The cache must invalidate when the COMPILER changes, not only the `.tish` source: cranelift/wasi
|
|
152
|
+
// (and native) bake the VM/codegen/runtime into the artifact, so a stale cached binary built before
|
|
153
|
+
// a VM fix would silently use old behaviour. The `tish` binary's mtime is the precise signal — any
|
|
154
|
+
// VM/compiler/runtime/codegen source edit rebuilds it. Mix it into the key alongside the file hash.
|
|
155
|
+
let bin_stamp = std::fs::metadata(bin)
|
|
156
|
+
.and_then(|m| m.modified())
|
|
157
|
+
.ok()
|
|
158
|
+
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
|
159
|
+
.map(|d| d.as_nanos() as u64)
|
|
160
|
+
.unwrap_or(0);
|
|
161
|
+
let hash = {
|
|
162
|
+
let mut h = DefaultHasher::new();
|
|
163
|
+
file_content_hash(path).hash(&mut h);
|
|
164
|
+
bin_stamp.hash(&mut h);
|
|
165
|
+
h.finish()
|
|
166
|
+
};
|
|
167
|
+
let hash8 = &format!("{:016x}", hash)[..8];
|
|
168
|
+
let cache_base = integration_compile_cache_dir().join(backend);
|
|
169
|
+
let _ = std::fs::create_dir_all(&cache_base);
|
|
170
|
+
// Include `backend` in the leaf name so nested cargo bin names never collide across backends.
|
|
171
|
+
let leaf = format!("{}__{}__{}", stem, backend, hash8);
|
|
172
|
+
|
|
173
|
+
let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
|
|
174
|
+
"native" => {
|
|
175
|
+
let ext = if cfg!(target_os = "windows") {
|
|
176
|
+
".exe"
|
|
177
|
+
} else {
|
|
178
|
+
""
|
|
179
|
+
};
|
|
180
|
+
let cached = cache_base.join(format!("{}{}", leaf, ext));
|
|
181
|
+
let args = vec![
|
|
182
|
+
OsString::from("build"),
|
|
183
|
+
OsString::from(path),
|
|
184
|
+
OsString::from("-o"),
|
|
185
|
+
OsString::from(&cached),
|
|
186
|
+
];
|
|
187
|
+
(cached, args)
|
|
188
|
+
}
|
|
189
|
+
"cranelift" => {
|
|
190
|
+
let ext = if cfg!(target_os = "windows") {
|
|
191
|
+
".exe"
|
|
192
|
+
} else {
|
|
193
|
+
""
|
|
194
|
+
};
|
|
195
|
+
let cached = cache_base.join(format!("{}{}", leaf, ext));
|
|
196
|
+
let args = vec![
|
|
197
|
+
OsString::from("build"),
|
|
198
|
+
OsString::from(path),
|
|
199
|
+
OsString::from("-o"),
|
|
200
|
+
OsString::from(&cached),
|
|
201
|
+
OsString::from("--native-backend"),
|
|
202
|
+
OsString::from("cranelift"),
|
|
203
|
+
];
|
|
204
|
+
(cached, args)
|
|
205
|
+
}
|
|
206
|
+
"js" => {
|
|
207
|
+
let cached = cache_base.join(format!("{}.js", leaf));
|
|
208
|
+
let args = vec![
|
|
209
|
+
OsString::from("build"),
|
|
210
|
+
OsString::from(path),
|
|
211
|
+
OsString::from("--target"),
|
|
212
|
+
OsString::from("js"),
|
|
213
|
+
OsString::from("-o"),
|
|
214
|
+
OsString::from(&cached),
|
|
215
|
+
];
|
|
216
|
+
(cached, args)
|
|
217
|
+
}
|
|
218
|
+
"wasi" => {
|
|
219
|
+
let out_base = cache_base.join(&leaf);
|
|
220
|
+
let artifact = out_base.with_extension("wasm");
|
|
221
|
+
let args = vec![
|
|
222
|
+
OsString::from("build"),
|
|
223
|
+
OsString::from(path),
|
|
224
|
+
OsString::from("-o"),
|
|
225
|
+
OsString::from(&out_base),
|
|
226
|
+
OsString::from("--target"),
|
|
227
|
+
OsString::from("wasi"),
|
|
228
|
+
];
|
|
229
|
+
(artifact, args)
|
|
230
|
+
}
|
|
231
|
+
_ => panic!("unknown backend {}", backend),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if !artifact_path.exists() {
|
|
235
|
+
let out = Command::new(bin)
|
|
236
|
+
.args(compile_args)
|
|
237
|
+
.current_dir(workspace_root())
|
|
238
|
+
.output()
|
|
239
|
+
.expect("run tish build");
|
|
240
|
+
assert!(
|
|
241
|
+
out.status.success(),
|
|
242
|
+
"Compile failed for {} ({}): {}",
|
|
243
|
+
path.display(),
|
|
244
|
+
backend,
|
|
245
|
+
String::from_utf8_lossy(&out.stderr)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Copy to temp so caller can run and delete without touching cache.
|
|
250
|
+
let ext = artifact_path
|
|
251
|
+
.extension()
|
|
252
|
+
.map(|e| e.to_string_lossy().to_string())
|
|
253
|
+
.unwrap_or_default();
|
|
254
|
+
let temp_dest =
|
|
255
|
+
std::env::temp_dir().join(format!("tish_cached_{}_{}_{}", backend, stem, hash8));
|
|
256
|
+
let temp_dest = if ext.is_empty() {
|
|
257
|
+
temp_dest
|
|
258
|
+
} else {
|
|
259
|
+
temp_dest.with_extension(ext)
|
|
260
|
+
};
|
|
261
|
+
std::fs::copy(&artifact_path, &temp_dest).expect("copy cached artifact to temp");
|
|
262
|
+
temp_dest
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Path to the tish CLI binary. When running under cargo-llvm-cov, the build goes to
|
|
266
|
+
/// target/llvm-cov-target and CARGO_TARGET_DIR may not be set for the test process.
|
|
267
|
+
fn tish_bin() -> PathBuf {
|
|
268
|
+
let bin_name = if cfg!(target_os = "windows") {
|
|
269
|
+
"tish.exe"
|
|
270
|
+
} else {
|
|
271
|
+
"tish"
|
|
272
|
+
};
|
|
273
|
+
let default = target_dir().join("debug").join(bin_name);
|
|
274
|
+
if default.exists() {
|
|
275
|
+
return default;
|
|
276
|
+
}
|
|
277
|
+
let llvm_cov = workspace_root()
|
|
278
|
+
.join("target")
|
|
279
|
+
.join("llvm-cov-target")
|
|
280
|
+
.join("debug")
|
|
281
|
+
.join(bin_name);
|
|
282
|
+
if llvm_cov.exists() {
|
|
283
|
+
return llvm_cov;
|
|
284
|
+
}
|
|
285
|
+
default
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// tish -V and --version print the version.
|
|
289
|
+
#[test]
|
|
290
|
+
fn test_tish_version_flag() {
|
|
291
|
+
let bin = tish_bin();
|
|
292
|
+
assert!(
|
|
293
|
+
bin.exists(),
|
|
294
|
+
"tish binary not found. Run `cargo build -p tishlang` first."
|
|
295
|
+
);
|
|
296
|
+
let out = Command::new(&bin).arg("-V").output().expect("run tish -V");
|
|
297
|
+
assert!(
|
|
298
|
+
out.status.success(),
|
|
299
|
+
"tish -V failed: {}",
|
|
300
|
+
String::from_utf8_lossy(&out.stderr)
|
|
301
|
+
);
|
|
302
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
303
|
+
assert!(
|
|
304
|
+
stdout.contains(env!("CARGO_PKG_VERSION")),
|
|
305
|
+
"tish -V should print version {}; got: {}",
|
|
306
|
+
env!("CARGO_PKG_VERSION"),
|
|
307
|
+
stdout
|
|
308
|
+
);
|
|
309
|
+
let out2 = Command::new(&bin)
|
|
310
|
+
.arg("--version")
|
|
311
|
+
.output()
|
|
312
|
+
.expect("run tish --version");
|
|
313
|
+
assert!(out2.status.success());
|
|
314
|
+
let stdout2 = String::from_utf8_lossy(&out2.stdout);
|
|
315
|
+
assert!(
|
|
316
|
+
stdout2.contains(env!("CARGO_PKG_VERSION")),
|
|
317
|
+
"tish --version should print version"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Parse async-await example (validates async fn parsing).
|
|
322
|
+
#[test]
|
|
323
|
+
fn test_async_await_parse() {
|
|
324
|
+
let path = workspace_root()
|
|
325
|
+
.join("examples")
|
|
326
|
+
.join("async-await")
|
|
327
|
+
.join("src")
|
|
328
|
+
.join("main.tish");
|
|
329
|
+
if path.exists() {
|
|
330
|
+
let source = std::fs::read_to_string(&path).unwrap();
|
|
331
|
+
let result = tishlang_parser::parse(&source);
|
|
332
|
+
assert!(
|
|
333
|
+
result.is_ok(),
|
|
334
|
+
"Parse failed for {}: {:?}",
|
|
335
|
+
path.display(),
|
|
336
|
+
result.err()
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Invoke tish binary to compile async-await and run compiled output (validates non-blocking pipeline).
|
|
342
|
+
#[test]
|
|
343
|
+
#[cfg(feature = "http")]
|
|
344
|
+
fn test_async_await_compile_via_binary() {
|
|
345
|
+
let bin = tish_bin();
|
|
346
|
+
let path = workspace_root()
|
|
347
|
+
.join("examples")
|
|
348
|
+
.join("async-await")
|
|
349
|
+
.join("src")
|
|
350
|
+
.join("main.tish");
|
|
351
|
+
if path.exists() && bin.exists() {
|
|
352
|
+
let out = std::env::temp_dir().join("tish_async_test_out");
|
|
353
|
+
let compile_result = Command::new(&bin)
|
|
354
|
+
.args([
|
|
355
|
+
"build",
|
|
356
|
+
path.to_string_lossy().as_ref(),
|
|
357
|
+
"-o",
|
|
358
|
+
out.to_string_lossy().as_ref(),
|
|
359
|
+
])
|
|
360
|
+
.current_dir(workspace_root())
|
|
361
|
+
.output();
|
|
362
|
+
let compile_out = compile_result.expect("run tish build");
|
|
363
|
+
assert!(
|
|
364
|
+
compile_out.status.success(),
|
|
365
|
+
"tish build failed: {}",
|
|
366
|
+
String::from_utf8_lossy(&compile_out.stderr)
|
|
367
|
+
);
|
|
368
|
+
// Run compiled binary to validate non-blocking fetchAll executes correctly
|
|
369
|
+
let run_result = Command::new(&out).current_dir(workspace_root()).output();
|
|
370
|
+
let run_out = run_result.expect("run compiled async binary");
|
|
371
|
+
assert!(
|
|
372
|
+
run_out.status.success(),
|
|
373
|
+
"compiled async binary failed: {}",
|
|
374
|
+
String::from_utf8_lossy(&run_out.stderr)
|
|
375
|
+
);
|
|
376
|
+
let stdout = String::from_utf8_lossy(&run_out.stdout);
|
|
377
|
+
assert!(
|
|
378
|
+
stdout.contains("Fetching"),
|
|
379
|
+
"expected output to mention fetching"
|
|
380
|
+
);
|
|
381
|
+
assert!(stdout.contains("Done"), "expected output to contain Done");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// DEFINITIVE VALIDATION: Parallel fetches must be faster than sequential.
|
|
386
|
+
/// Uses httpbin.org/delay/1 (1s each). 3 parallel ≈ 1s, 3 sequential ≈ 3s.
|
|
387
|
+
#[test]
|
|
388
|
+
#[cfg(feature = "http")]
|
|
389
|
+
#[ignore = "timing and network sensitive; run manually: cargo test test_async_parallel_vs_sequential_timing -p tishlang--features http -- --ignored"]
|
|
390
|
+
fn test_async_parallel_vs_sequential_timing() {
|
|
391
|
+
let bin = tish_bin();
|
|
392
|
+
let parallel_src = workspace_root()
|
|
393
|
+
.join("examples")
|
|
394
|
+
.join("async-await")
|
|
395
|
+
.join("src")
|
|
396
|
+
.join("parallel.tish");
|
|
397
|
+
let sequential_src = workspace_root()
|
|
398
|
+
.join("examples")
|
|
399
|
+
.join("async-await")
|
|
400
|
+
.join("src")
|
|
401
|
+
.join("sequential.tish");
|
|
402
|
+
if !parallel_src.exists() || !sequential_src.exists() || !bin.exists() {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
let out_parallel = std::env::temp_dir().join("tish_parallel_timing");
|
|
406
|
+
let out_sequential = std::env::temp_dir().join("tish_sequential_timing");
|
|
407
|
+
|
|
408
|
+
// Compile both
|
|
409
|
+
let compile_par = Command::new(&bin)
|
|
410
|
+
.args([
|
|
411
|
+
"build",
|
|
412
|
+
parallel_src.to_string_lossy().as_ref(),
|
|
413
|
+
"-o",
|
|
414
|
+
out_parallel.to_string_lossy().as_ref(),
|
|
415
|
+
])
|
|
416
|
+
.current_dir(workspace_root())
|
|
417
|
+
.output();
|
|
418
|
+
assert!(
|
|
419
|
+
compile_par.as_ref().unwrap().status.success(),
|
|
420
|
+
"compile parallel: {}",
|
|
421
|
+
String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr)
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
let compile_seq = Command::new(&bin)
|
|
425
|
+
.args([
|
|
426
|
+
"build",
|
|
427
|
+
sequential_src.to_string_lossy().as_ref(),
|
|
428
|
+
"-o",
|
|
429
|
+
out_sequential.to_string_lossy().as_ref(),
|
|
430
|
+
])
|
|
431
|
+
.current_dir(workspace_root())
|
|
432
|
+
.output();
|
|
433
|
+
assert!(
|
|
434
|
+
compile_seq.as_ref().unwrap().status.success(),
|
|
435
|
+
"compile sequential: {}",
|
|
436
|
+
String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr)
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// Run parallel and time
|
|
440
|
+
let t_parallel = std::time::Instant::now();
|
|
441
|
+
let run_par = Command::new(&out_parallel)
|
|
442
|
+
.current_dir(workspace_root())
|
|
443
|
+
.output();
|
|
444
|
+
let elapsed_parallel = t_parallel.elapsed();
|
|
445
|
+
assert!(
|
|
446
|
+
run_par.as_ref().unwrap().status.success(),
|
|
447
|
+
"run parallel: {}",
|
|
448
|
+
String::from_utf8_lossy(&run_par.as_ref().unwrap().stderr)
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Run sequential and time
|
|
452
|
+
let t_sequential = std::time::Instant::now();
|
|
453
|
+
let run_seq = Command::new(&out_sequential)
|
|
454
|
+
.current_dir(workspace_root())
|
|
455
|
+
.output();
|
|
456
|
+
let elapsed_sequential = t_sequential.elapsed();
|
|
457
|
+
assert!(
|
|
458
|
+
run_seq.as_ref().unwrap().status.success(),
|
|
459
|
+
"run sequential: {}",
|
|
460
|
+
String::from_utf8_lossy(&run_seq.as_ref().unwrap().stderr)
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// PARALLEL MUST BE FASTER: parallel < sequential * 0.6 (parallel ~1s, sequential ~3s)
|
|
464
|
+
let parallel_secs = elapsed_parallel.as_secs_f64();
|
|
465
|
+
let sequential_secs = elapsed_sequential.as_secs_f64();
|
|
466
|
+
assert!(
|
|
467
|
+
parallel_secs < sequential_secs * 0.6,
|
|
468
|
+
"Async NOT validated: parallel took {:.2}s but sequential took {:.2}s. Parallel must be < 60% of sequential to prove non-blocking.",
|
|
469
|
+
parallel_secs,
|
|
470
|
+
sequential_secs
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// Run async-await example via tishlang_eval (same path as `tish run`).
|
|
475
|
+
/// Ignored: tishlang_eval::run() is synchronous and does not run the event loop.
|
|
476
|
+
#[test]
|
|
477
|
+
#[cfg(feature = "http")]
|
|
478
|
+
#[ignore = "requires async runtime; use test_async_await_compile_via_binary for CI"]
|
|
479
|
+
fn test_async_await_run() {
|
|
480
|
+
let path = workspace_root()
|
|
481
|
+
.join("examples")
|
|
482
|
+
.join("async-await")
|
|
483
|
+
.join("src")
|
|
484
|
+
.join("main.tish");
|
|
485
|
+
if path.exists() {
|
|
486
|
+
let source = std::fs::read_to_string(&path).unwrap();
|
|
487
|
+
let result = tishlang_eval::run(&source);
|
|
488
|
+
assert!(
|
|
489
|
+
result.is_ok(),
|
|
490
|
+
"Run failed for {}: {:?}",
|
|
491
|
+
path.display(),
|
|
492
|
+
result.err()
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// `promise.tish` — full Promise API including `new Promise(executor)`, `.then`, `.catch`,
|
|
498
|
+
/// `all`, `race`, `any`, `allSettled`. Runs via the binary (vm + interp), asserts exact output.
|
|
499
|
+
/// This was previously `#[ignore]`'d and only checked "doesn't error" — the old fixture also
|
|
500
|
+
/// avoided `new Promise` entirely, hiding a bug where the executor never ran on the VM. Now
|
|
501
|
+
/// CI-gated and output-asserting. vm ≡ interp.
|
|
502
|
+
#[test]
|
|
503
|
+
fn test_promise_core() {
|
|
504
|
+
let bin = tish_bin();
|
|
505
|
+
if !bin.exists() {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
let path = workspace_root()
|
|
509
|
+
.join("tests")
|
|
510
|
+
.join("modules")
|
|
511
|
+
.join("promise.tish");
|
|
512
|
+
if !path.exists() {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
let expected = "\
|
|
516
|
+
new Promise: new-ctor
|
|
517
|
+
Promise sync resolve: 42
|
|
518
|
+
Promise.resolve: 100
|
|
519
|
+
Promise.reject caught: true
|
|
520
|
+
.then chain: 4
|
|
521
|
+
.catch: handled: fail
|
|
522
|
+
Promise.all: 1 2 3
|
|
523
|
+
Promise.race: fast
|
|
524
|
+
Promise.any: any-win
|
|
525
|
+
Promise.allSettled: fulfilled rejected reason
|
|
526
|
+
Promise tests completed
|
|
527
|
+
";
|
|
528
|
+
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
|
|
529
|
+
let mut args = backend_args.clone();
|
|
530
|
+
args.push(path.to_string_lossy().to_string().leak());
|
|
531
|
+
let out = Command::new(&bin)
|
|
532
|
+
.args(&args)
|
|
533
|
+
.current_dir(workspace_root())
|
|
534
|
+
.output()
|
|
535
|
+
.expect("run tish binary");
|
|
536
|
+
assert!(
|
|
537
|
+
out.status.success(),
|
|
538
|
+
"promise.tish ({:?}) failed: stderr={}",
|
|
539
|
+
backend_args,
|
|
540
|
+
String::from_utf8_lossy(&out.stderr)
|
|
541
|
+
);
|
|
542
|
+
assert_eq!(
|
|
543
|
+
String::from_utf8_lossy(&out.stdout),
|
|
544
|
+
expected,
|
|
545
|
+
"Promise output mismatch on backend {:?} — check new Promise/any/allSettled regressions",
|
|
546
|
+
backend_args
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/// Import aliasing — `{ x as y }` rename and `* as M` namespace. `as` is a dedicated keyword
|
|
552
|
+
/// token (shared with type casts), so the import-specifier parser must accept it in that position.
|
|
553
|
+
/// Regression: previously `import { a as b }` failed with "Expected Comma, got As". vm ≡ interp.
|
|
554
|
+
#[test]
|
|
555
|
+
fn test_import_alias() {
|
|
556
|
+
let bin = tish_bin();
|
|
557
|
+
if !bin.exists() {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
let path = workspace_root()
|
|
561
|
+
.join("tests")
|
|
562
|
+
.join("modules")
|
|
563
|
+
.join("import_alias.tish");
|
|
564
|
+
if !path.exists() {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
let expected = "42\nhi there\n42\n1.0\n";
|
|
568
|
+
for backend_args in [vec!["run"], vec!["run", "--backend", "vm"]] {
|
|
569
|
+
let mut args = backend_args.clone();
|
|
570
|
+
args.push(path.to_string_lossy().to_string().leak());
|
|
571
|
+
let out = Command::new(&bin)
|
|
572
|
+
.args(&args)
|
|
573
|
+
.current_dir(workspace_root())
|
|
574
|
+
.output()
|
|
575
|
+
.expect("run tish binary");
|
|
576
|
+
assert!(
|
|
577
|
+
out.status.success(),
|
|
578
|
+
"import_alias.tish ({:?}) failed: stderr={}",
|
|
579
|
+
backend_args,
|
|
580
|
+
String::from_utf8_lossy(&out.stderr)
|
|
581
|
+
);
|
|
582
|
+
assert_eq!(
|
|
583
|
+
String::from_utf8_lossy(&out.stdout),
|
|
584
|
+
expected,
|
|
585
|
+
"import alias output mismatch on backend {:?}",
|
|
586
|
+
backend_args
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// Combined validation: async/await + Promise + setTimeout + multiple HTTP requests.
|
|
592
|
+
/// #97: a module's non-exported top-level bindings stay private — a same-named binding in
|
|
593
|
+
/// another module must not overwrite them (runtime), and the `--target js` bundle must not
|
|
594
|
+
/// emit duplicate `let` declarations (SyntaxError). Verified identical across interp / VM /
|
|
595
|
+
/// native / node, including parameter and inner-`let` shadowing.
|
|
596
|
+
#[test]
|
|
597
|
+
fn test_module_private_binding_isolation() {
|
|
598
|
+
let bin = tish_bin();
|
|
599
|
+
if !bin.exists() {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
let path = workspace_root()
|
|
603
|
+
.join("tests")
|
|
604
|
+
.join("modules")
|
|
605
|
+
.join("private_isolation.tish");
|
|
606
|
+
if !path.exists() {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
let expected = "from-a:helper-a from-b:helper-b\narg inner\nhelper-b\n";
|
|
610
|
+
|
|
611
|
+
// Interpreter + VM via `tish run`.
|
|
612
|
+
for backend in ["interp", "vm"] {
|
|
613
|
+
let out = Command::new(&bin)
|
|
614
|
+
.args(["run", "--backend", backend])
|
|
615
|
+
.arg(&path)
|
|
616
|
+
.current_dir(workspace_root())
|
|
617
|
+
.output()
|
|
618
|
+
.expect("run tish binary");
|
|
619
|
+
assert!(
|
|
620
|
+
out.status.success(),
|
|
621
|
+
"private_isolation.tish ({backend}) failed: stderr={}",
|
|
622
|
+
String::from_utf8_lossy(&out.stderr)
|
|
623
|
+
);
|
|
624
|
+
assert_eq!(
|
|
625
|
+
String::from_utf8_lossy(&out.stdout),
|
|
626
|
+
expected,
|
|
627
|
+
"module private-binding isolation mismatch on backend {backend}"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Native: compile + run.
|
|
632
|
+
let native_bin = compile_cached(&bin, &path, "native");
|
|
633
|
+
let out = Command::new(&native_bin)
|
|
634
|
+
.current_dir(workspace_root())
|
|
635
|
+
.output()
|
|
636
|
+
.expect("run native binary");
|
|
637
|
+
let _ = std::fs::remove_file(&native_bin);
|
|
638
|
+
assert!(
|
|
639
|
+
out.status.success(),
|
|
640
|
+
"private_isolation native run failed: stderr={}",
|
|
641
|
+
String::from_utf8_lossy(&out.stderr)
|
|
642
|
+
);
|
|
643
|
+
assert_eq!(
|
|
644
|
+
String::from_utf8_lossy(&out.stdout),
|
|
645
|
+
expected,
|
|
646
|
+
"module private-binding isolation mismatch on native backend"
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// JS target: compile + run through Node (also asserts no duplicate-`let` SyntaxError).
|
|
650
|
+
let node_available = Command::new("node")
|
|
651
|
+
.arg("--version")
|
|
652
|
+
.output()
|
|
653
|
+
.map(|o| o.status.success())
|
|
654
|
+
.unwrap_or(false);
|
|
655
|
+
if node_available {
|
|
656
|
+
let out_js = compile_cached(&bin, &path, "js");
|
|
657
|
+
let out = Command::new("node")
|
|
658
|
+
.arg(&out_js)
|
|
659
|
+
.current_dir(workspace_root())
|
|
660
|
+
.output()
|
|
661
|
+
.expect("run node");
|
|
662
|
+
let _ = std::fs::remove_file(&out_js);
|
|
663
|
+
assert!(
|
|
664
|
+
out.status.success(),
|
|
665
|
+
"private_isolation JS run failed: stderr={}",
|
|
666
|
+
String::from_utf8_lossy(&out.stderr)
|
|
667
|
+
);
|
|
668
|
+
assert_eq!(
|
|
669
|
+
String::from_utf8_lossy(&out.stdout),
|
|
670
|
+
expected,
|
|
671
|
+
"module private-binding isolation mismatch on JS target"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/// Ignored: tishlang_eval::run() does not run the event loop.
|
|
677
|
+
#[test]
|
|
678
|
+
#[cfg(feature = "http")]
|
|
679
|
+
#[ignore = "requires async runtime"]
|
|
680
|
+
fn test_async_promise_settimeout_combined() {
|
|
681
|
+
let path = workspace_root()
|
|
682
|
+
.join("tests")
|
|
683
|
+
.join("modules")
|
|
684
|
+
.join("async_promise_settimeout.tish");
|
|
685
|
+
if path.exists() {
|
|
686
|
+
let source = std::fs::read_to_string(&path).unwrap();
|
|
687
|
+
let result = tishlang_eval::run(&source);
|
|
688
|
+
assert!(
|
|
689
|
+
result.is_ok(),
|
|
690
|
+
"Failed to run async_promise_settimeout: {:?}",
|
|
691
|
+
result.err()
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/// VM run with Date global (resolve+merge+bytecode+run pipeline).
|
|
697
|
+
#[test]
|
|
698
|
+
fn test_vm_date_now() {
|
|
699
|
+
let path = workspace_root()
|
|
700
|
+
.join("tests")
|
|
701
|
+
.join("core")
|
|
702
|
+
.join("date.tish");
|
|
703
|
+
if !path.exists() {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
// Library path
|
|
707
|
+
let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
|
|
708
|
+
tishlang_compile::detect_cycles(&modules).expect("cycles");
|
|
709
|
+
let program = tishlang_compile::merge_modules(modules)
|
|
710
|
+
.expect("merge")
|
|
711
|
+
.program;
|
|
712
|
+
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
713
|
+
let result = tishlang_vm::run(&chunk);
|
|
714
|
+
assert!(
|
|
715
|
+
result.is_ok(),
|
|
716
|
+
"VM run (library) failed: {:?}",
|
|
717
|
+
result.err()
|
|
718
|
+
);
|
|
719
|
+
// Binary path - same flow as `tish run <file>`
|
|
720
|
+
let bin = tish_bin();
|
|
721
|
+
if bin.exists() {
|
|
722
|
+
let out = Command::new(&bin)
|
|
723
|
+
.args(["run", path.to_string_lossy().as_ref()])
|
|
724
|
+
.current_dir(workspace_root())
|
|
725
|
+
.output()
|
|
726
|
+
.expect("run tish binary");
|
|
727
|
+
assert!(
|
|
728
|
+
out.status.success(),
|
|
729
|
+
"tish run failed: stdout={} stderr={}",
|
|
730
|
+
String::from_utf8_lossy(&out.stdout),
|
|
731
|
+
String::from_utf8_lossy(&out.stderr)
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/// `Promise.any`, `Promise.allSettled`, fixed `Promise.race` — cross-backend, network-free, CI-gated.
|
|
737
|
+
/// vm ≡ interp exact match.
|
|
738
|
+
#[test]
|
|
739
|
+
fn test_promise_combinators() {
|
|
740
|
+
let bin = tish_bin();
|
|
741
|
+
if !bin.exists() {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let path = workspace_root()
|
|
745
|
+
.join("tests")
|
|
746
|
+
.join("modules")
|
|
747
|
+
.join("promise_combinators.tish");
|
|
748
|
+
if !path.exists() {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
let expected = "\
|
|
752
|
+
any first-fulfilled: win
|
|
753
|
+
any all-rejected: [\"e1\",\"e2\"]
|
|
754
|
+
allSettled[0] ok: 10
|
|
755
|
+
allSettled[1] rejected: boom
|
|
756
|
+
allSettled[2] ok: 30
|
|
757
|
+
race winner: A
|
|
758
|
+
any passthrough: 42
|
|
759
|
+
";
|
|
760
|
+
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
|
|
761
|
+
let mut args = backend_args.clone();
|
|
762
|
+
args.push(path.to_string_lossy().to_string().leak());
|
|
763
|
+
let out = Command::new(&bin)
|
|
764
|
+
.args(&args)
|
|
765
|
+
.current_dir(workspace_root())
|
|
766
|
+
.output()
|
|
767
|
+
.expect("run tish binary");
|
|
768
|
+
assert!(
|
|
769
|
+
out.status.success(),
|
|
770
|
+
"promise_combinators ({:?}) failed: stderr={}",
|
|
771
|
+
backend_args,
|
|
772
|
+
String::from_utf8_lossy(&out.stderr)
|
|
773
|
+
);
|
|
774
|
+
assert_eq!(
|
|
775
|
+
String::from_utf8_lossy(&out.stdout),
|
|
776
|
+
expected,
|
|
777
|
+
"Promise.any/allSettled/race divergence on backend {:?}",
|
|
778
|
+
backend_args
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/// Pins tish's DOCUMENTED async/Promise ordering (docs/concurrency-model.md) — network-free, so it
|
|
784
|
+
/// runs in CI (unlike the `#[ignore]`'d network async tests). Asserts Promise.all order + non-promise
|
|
785
|
+
/// passthrough, `.then` chaining, await-reject catch, Promise.all reject short-circuit, AND the
|
|
786
|
+
/// deliberate blocking signature: a 0ms `setTimeout` queued before an `await` fires LAST (a JS event
|
|
787
|
+
/// loop would interleave it earlier — tish's `await` blocks instead of yielding). NOT compared to node
|
|
788
|
+
/// on purpose; this is tish's own contract. vm ≡ interp guards cross-backend agreement.
|
|
789
|
+
#[test]
|
|
790
|
+
fn test_async_ordering_documented() {
|
|
791
|
+
let bin = tish_bin();
|
|
792
|
+
if !bin.exists() {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
let path = workspace_root()
|
|
796
|
+
.join("tests")
|
|
797
|
+
.join("modules")
|
|
798
|
+
.join("async_ordering.tish");
|
|
799
|
+
if !path.exists() {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
// tish's documented, deliberately-non-JS ordering (timer drains last because await blocks).
|
|
803
|
+
let expected = "\
|
|
804
|
+
1: sync-start
|
|
805
|
+
2: await = 42
|
|
806
|
+
2b: new Promise = ctor-ran
|
|
807
|
+
3: all = a b c
|
|
808
|
+
4: chain = 13
|
|
809
|
+
5: caught = boom
|
|
810
|
+
6: all-reject = rej
|
|
811
|
+
7: post-await = after-timer-was-queued
|
|
812
|
+
9: sync-end
|
|
813
|
+
8: timer-fires-LAST (await did not yield)
|
|
814
|
+
";
|
|
815
|
+
// Both the bytecode VM and the tree-walking interpreter must agree on this contract.
|
|
816
|
+
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
|
|
817
|
+
let mut args = backend_args.clone();
|
|
818
|
+
args.push(path.to_string_lossy().to_string().leak());
|
|
819
|
+
let out = Command::new(&bin)
|
|
820
|
+
.args(&args)
|
|
821
|
+
.current_dir(workspace_root())
|
|
822
|
+
.output()
|
|
823
|
+
.expect("run tish binary");
|
|
824
|
+
assert!(
|
|
825
|
+
out.status.success(),
|
|
826
|
+
"async_ordering ({:?}) failed: stderr={}",
|
|
827
|
+
backend_args,
|
|
828
|
+
String::from_utf8_lossy(&out.stderr)
|
|
829
|
+
);
|
|
830
|
+
assert_eq!(
|
|
831
|
+
String::from_utf8_lossy(&out.stdout),
|
|
832
|
+
expected,
|
|
833
|
+
"async ordering divergence on backend {:?} — the documented blocking-await/timer contract changed",
|
|
834
|
+
backend_args
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/// VM run with parse+compile only (no resolve/merge) - isolates bytecode IndexAssign.
|
|
840
|
+
#[test]
|
|
841
|
+
fn test_vm_index_assign_direct() {
|
|
842
|
+
let source = r#"let arr = [1, 2, 3]; arr[1] = 99; console.log(arr[1]);"#;
|
|
843
|
+
let program = tishlang_parser::parse(source).expect("parse");
|
|
844
|
+
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
845
|
+
let result = tishlang_vm::run(&chunk);
|
|
846
|
+
assert!(result.is_ok(), "VM IndexAssign failed: {:?}", result.err());
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/// VM run via resolve+merge (same as tish run) - must also pass.
|
|
850
|
+
#[test]
|
|
851
|
+
fn test_vm_index_assign_via_resolve() {
|
|
852
|
+
let path = workspace_root()
|
|
853
|
+
.join("tests")
|
|
854
|
+
.join("core")
|
|
855
|
+
.join("array_sort_minimal.tish");
|
|
856
|
+
let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
|
|
857
|
+
tishlang_compile::detect_cycles(&modules).expect("cycles");
|
|
858
|
+
let program = tishlang_compile::merge_modules(modules)
|
|
859
|
+
.expect("merge")
|
|
860
|
+
.program;
|
|
861
|
+
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
862
|
+
let result = tishlang_vm::run(&chunk);
|
|
863
|
+
assert!(
|
|
864
|
+
result.is_ok(),
|
|
865
|
+
"VM IndexAssign via resolve failed: {:?}",
|
|
866
|
+
result.err()
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/// tish run binary must pass array_sort_minimal (ensures CLI works).
|
|
871
|
+
#[test]
|
|
872
|
+
fn test_tish_run_index_assign() {
|
|
873
|
+
let bin = tish_bin();
|
|
874
|
+
let path = workspace_root()
|
|
875
|
+
.join("tests")
|
|
876
|
+
.join("core")
|
|
877
|
+
.join("array_sort_minimal.tish");
|
|
878
|
+
if !bin.exists() {
|
|
879
|
+
eprintln!("Skipping: tish binary not built");
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
let out = Command::new(&bin)
|
|
883
|
+
.args(["run", path.to_string_lossy().as_ref()])
|
|
884
|
+
.current_dir(workspace_root())
|
|
885
|
+
.output()
|
|
886
|
+
.expect("run tish");
|
|
887
|
+
assert!(
|
|
888
|
+
out.status.success(),
|
|
889
|
+
"tish run failed: stdout={} stderr={}",
|
|
890
|
+
String::from_utf8_lossy(&out.stdout),
|
|
891
|
+
String::from_utf8_lossy(&out.stderr)
|
|
892
|
+
);
|
|
893
|
+
assert!(
|
|
894
|
+
String::from_utf8_lossy(&out.stdout).contains("pass"),
|
|
895
|
+
"Expected 'pass' in output"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/// Full stack: lex + parse each .tish file and assert no parse error.
|
|
900
|
+
#[test]
|
|
901
|
+
fn test_full_stack_parse() {
|
|
902
|
+
let core_dir = core_dir();
|
|
903
|
+
for entry in std::fs::read_dir(&core_dir).unwrap() {
|
|
904
|
+
let path = entry.unwrap().path();
|
|
905
|
+
if path.extension().map(|e| e == "tish").unwrap_or(false) {
|
|
906
|
+
let source = std::fs::read_to_string(&path).unwrap();
|
|
907
|
+
let result = tishlang_parser::parse(&source);
|
|
908
|
+
assert!(
|
|
909
|
+
result.is_ok(),
|
|
910
|
+
"Parse failed for {}: {:?}",
|
|
911
|
+
path.display(),
|
|
912
|
+
result.err()
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// (The hand-maintained `MVP_TEST_FILES` allowlist was removed in favor of `discover_core_tests()`
|
|
919
|
+
// below — every `tests/core/*.tish` with a `.expected` now gets cross-backend coverage automatically.)
|
|
920
|
+
|
|
921
|
+
/// Tests whose `.expected` embeds elapsed-ms timings (perf/stress/probe) — nondeterministic, so
|
|
922
|
+
/// excluded from exact-output comparison. Run them via `just perf-*` instead.
|
|
923
|
+
const TIMING_NONDETERMINISTIC: &[&str] = &[
|
|
924
|
+
"array_stress.tish",
|
|
925
|
+
"array_stress_01_large_array_creation.tish",
|
|
926
|
+
"array_stress_02_iteration.tish",
|
|
927
|
+
"array_stress_03_map_filter_reduce.tish",
|
|
928
|
+
"array_stress_04_chained.tish",
|
|
929
|
+
"array_stress_05_sorting.tish",
|
|
930
|
+
"array_stress_06_search.tish",
|
|
931
|
+
"array_stress_07_splice_slice.tish",
|
|
932
|
+
"array_stress_08_concat_spread.tish",
|
|
933
|
+
"array_stress_09_flat.tish",
|
|
934
|
+
"array_stress_10_objects.tish",
|
|
935
|
+
"basic_types.tish",
|
|
936
|
+
"benchmark_granular.tish",
|
|
937
|
+
"new_features_perf.tish",
|
|
938
|
+
"object_stress.tish",
|
|
939
|
+
"objects_perf.tish",
|
|
940
|
+
"string_methods_perf.tish",
|
|
941
|
+
"recursion_stress.tish",
|
|
942
|
+
"jit_probe.tish",
|
|
943
|
+
];
|
|
944
|
+
|
|
945
|
+
/// Known cross-backend gaps skipped on the interp↔vm parity check, with reason + a tracking note.
|
|
946
|
+
/// Each is a real divergence to fix, not a permanent exclusion.
|
|
947
|
+
///
|
|
948
|
+
/// Empty: the former `nested_complex.tish` gap (the VM's fixed 2-level `enclosing` lost captures
|
|
949
|
+
/// >2 levels deep — `level4` couldn't see `level1`'s `a`) is fixed. The VM now captures the full
|
|
950
|
+
/// lexical chain (`Vm.enclosing: Vec<ScopeMap>`), so closures nested arbitrarily deep resolve
|
|
951
|
+
/// every ancestor's locals. interp↔vm parity holds for all discovered tests.
|
|
952
|
+
const VM_PARITY_SKIP: &[&str] = &[];
|
|
953
|
+
|
|
954
|
+
/// Discover every `tests/core/*.tish` that has a `.expected` sibling, minus timing-nondeterministic
|
|
955
|
+
/// ones. Replaces the hand-maintained `MVP_TEST_FILES` allowlist so a new `*.tish` + `*.expected`
|
|
956
|
+
/// gets cross-backend coverage automatically (the allowlist silently left ~38 tests running nowhere).
|
|
957
|
+
fn discover_core_tests() -> Vec<String> {
|
|
958
|
+
let mut v: Vec<String> = std::fs::read_dir(core_dir())
|
|
959
|
+
.expect("read tests/core")
|
|
960
|
+
.filter_map(|e| {
|
|
961
|
+
let p = e.ok()?.path();
|
|
962
|
+
if p.extension().map(|x| x == "tish").unwrap_or(false) && expected_path(&p).exists() {
|
|
963
|
+
Some(p.file_name()?.to_string_lossy().into_owned())
|
|
964
|
+
} else {
|
|
965
|
+
None
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
.filter(|n| !TIMING_NONDETERMINISTIC.contains(&n.as_str()))
|
|
969
|
+
.collect();
|
|
970
|
+
v.sort();
|
|
971
|
+
v
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/// True if `name` has a sibling `.js` (so it can run through the Node oracle).
|
|
975
|
+
fn has_js_sibling(name: &str) -> bool {
|
|
976
|
+
core_dir()
|
|
977
|
+
.join(name)
|
|
978
|
+
.with_extension("js")
|
|
979
|
+
.exists()
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/// The gradual type checker must produce ZERO diagnostics on the (valid) corpus — any diagnostic is
|
|
983
|
+
/// a false positive. Lists every offender so they can be inspected/fixed.
|
|
984
|
+
#[test]
|
|
985
|
+
fn checker_no_false_positives_on_corpus() {
|
|
986
|
+
let mut flagged: Vec<String> = Vec::new();
|
|
987
|
+
for name in discover_core_tests() {
|
|
988
|
+
let src = std::fs::read_to_string(core_dir().join(&name)).unwrap();
|
|
989
|
+
if let Ok(prog) = tishlang_parser::parse(&src) {
|
|
990
|
+
let diags = tishlang_compile::check_program(&prog);
|
|
991
|
+
if !diags.is_empty() {
|
|
992
|
+
let msgs: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();
|
|
993
|
+
flagged.push(format!("{name}: {}", msgs.join(" | ")));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
assert!(
|
|
998
|
+
flagged.is_empty(),
|
|
999
|
+
"type checker flagged valid corpus programs (false positives):\n{}",
|
|
1000
|
+
flagged.join("\n")
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/// Run each .tish file with interpreter and compare stdout to static expected.
|
|
1005
|
+
/// Set REGENERATE_EXPECTED=1 to write .expected files from interpreter output (run once, then commit).
|
|
1006
|
+
#[test]
|
|
1007
|
+
fn test_mvp_programs_interpreter() {
|
|
1008
|
+
let core_dir = core_dir();
|
|
1009
|
+
let bin = tish_bin();
|
|
1010
|
+
assert!(
|
|
1011
|
+
bin.exists(),
|
|
1012
|
+
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1013
|
+
bin.display()
|
|
1014
|
+
);
|
|
1015
|
+
let regenerate = std::env::var("REGENERATE_EXPECTED").as_deref() == Ok("1");
|
|
1016
|
+
for name in &discover_core_tests() {
|
|
1017
|
+
let path = core_dir.join(name);
|
|
1018
|
+
if !path.exists() {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
let path_str = path.to_string_lossy();
|
|
1022
|
+
let out = Command::new(&bin)
|
|
1023
|
+
.args(["run", path_str.as_ref(), "--backend", "interp"])
|
|
1024
|
+
.current_dir(workspace_root())
|
|
1025
|
+
.output()
|
|
1026
|
+
.expect("run tish interpreter");
|
|
1027
|
+
assert!(
|
|
1028
|
+
out.status.success(),
|
|
1029
|
+
"Interpreter failed for {}: {}",
|
|
1030
|
+
path.display(),
|
|
1031
|
+
String::from_utf8_lossy(&out.stderr)
|
|
1032
|
+
);
|
|
1033
|
+
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
|
1034
|
+
if regenerate {
|
|
1035
|
+
std::fs::write(expected_path(&path), &stdout).expect("write expected");
|
|
1036
|
+
} else {
|
|
1037
|
+
let expected = get_expected(&path).unwrap_or_else(|| {
|
|
1038
|
+
panic!(
|
|
1039
|
+
"missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
|
|
1040
|
+
path.display()
|
|
1041
|
+
)
|
|
1042
|
+
});
|
|
1043
|
+
assert_eq!(
|
|
1044
|
+
stdout,
|
|
1045
|
+
expected,
|
|
1046
|
+
"Interpreter output mismatch for {}",
|
|
1047
|
+
path.display()
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/// Default bytecode VM must match the tree-walking interpreter for every MVP program.
|
|
1054
|
+
#[test]
|
|
1055
|
+
fn test_mvp_programs_interp_vm_stdout_parity() {
|
|
1056
|
+
let core_dir = core_dir();
|
|
1057
|
+
let bin = tish_bin();
|
|
1058
|
+
assert!(
|
|
1059
|
+
bin.exists(),
|
|
1060
|
+
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1061
|
+
bin.display()
|
|
1062
|
+
);
|
|
1063
|
+
for name in &discover_core_tests() {
|
|
1064
|
+
if VM_PARITY_SKIP.contains(&name.as_str()) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
let path = core_dir.join(name);
|
|
1068
|
+
if !path.exists() {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
let path_str = path.to_string_lossy();
|
|
1072
|
+
let out_interp = Command::new(&bin)
|
|
1073
|
+
.args(["run", path_str.as_ref(), "--backend", "interp"])
|
|
1074
|
+
.current_dir(workspace_root())
|
|
1075
|
+
.output()
|
|
1076
|
+
.expect("run tish interpreter");
|
|
1077
|
+
assert!(
|
|
1078
|
+
out_interp.status.success(),
|
|
1079
|
+
"Interpreter failed for {}: {}",
|
|
1080
|
+
path.display(),
|
|
1081
|
+
String::from_utf8_lossy(&out_interp.stderr)
|
|
1082
|
+
);
|
|
1083
|
+
let out_vm = Command::new(&bin)
|
|
1084
|
+
.args(["run", path_str.as_ref()])
|
|
1085
|
+
.current_dir(workspace_root())
|
|
1086
|
+
.output()
|
|
1087
|
+
.expect("run tish VM");
|
|
1088
|
+
assert!(
|
|
1089
|
+
out_vm.status.success(),
|
|
1090
|
+
"VM failed for {}: {}",
|
|
1091
|
+
path.display(),
|
|
1092
|
+
String::from_utf8_lossy(&out_vm.stderr)
|
|
1093
|
+
);
|
|
1094
|
+
let s_interp = String::from_utf8_lossy(&out_interp.stdout);
|
|
1095
|
+
let s_vm = String::from_utf8_lossy(&out_vm.stdout);
|
|
1096
|
+
assert_eq!(
|
|
1097
|
+
s_interp,
|
|
1098
|
+
s_vm,
|
|
1099
|
+
"interp vs VM stdout mismatch for {}",
|
|
1100
|
+
path.display()
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/// Compile each .tish file to native, run, and compare stdout to static expected (parallelized).
|
|
1106
|
+
#[test]
|
|
1107
|
+
fn test_mvp_programs_native() {
|
|
1108
|
+
let _fast_native = EnvVarGuard::set("TISH_FAST_NATIVE_BUILD", "1");
|
|
1109
|
+
let core_dir = core_dir();
|
|
1110
|
+
let bin = tish_bin();
|
|
1111
|
+
assert!(
|
|
1112
|
+
bin.exists(),
|
|
1113
|
+
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1114
|
+
bin.display()
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
let mut paths: Vec<PathBuf> = discover_core_tests()
|
|
1118
|
+
.iter()
|
|
1119
|
+
.filter_map(|name| {
|
|
1120
|
+
let p = core_dir.join(name);
|
|
1121
|
+
if p.exists() {
|
|
1122
|
+
Some(p)
|
|
1123
|
+
} else {
|
|
1124
|
+
None
|
|
1125
|
+
}
|
|
1126
|
+
})
|
|
1127
|
+
.collect();
|
|
1128
|
+
paths.sort();
|
|
1129
|
+
|
|
1130
|
+
if paths.is_empty() {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
let combined = combined_mvp_native_inputs_hash(&paths);
|
|
1135
|
+
let cache_dir = mvp_native_batch_cache_dir(combined);
|
|
1136
|
+
let _ = std::fs::create_dir_all(&cache_dir);
|
|
1137
|
+
|
|
1138
|
+
let ext = if cfg!(target_os = "windows") {
|
|
1139
|
+
".exe"
|
|
1140
|
+
} else {
|
|
1141
|
+
""
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
let entries_owned: Vec<(PathBuf, PathBuf)> = paths
|
|
1145
|
+
.iter()
|
|
1146
|
+
.map(|p| {
|
|
1147
|
+
let stem = p.file_stem().unwrap().to_string_lossy();
|
|
1148
|
+
let cached = cache_dir.join(format!("{}{}", stem, ext));
|
|
1149
|
+
(p.clone(), cached)
|
|
1150
|
+
})
|
|
1151
|
+
.collect();
|
|
1152
|
+
|
|
1153
|
+
let need_build = entries_owned.iter().any(|(_, o)| !o.exists());
|
|
1154
|
+
if need_build {
|
|
1155
|
+
let refs: Vec<(&Path, &Path)> = entries_owned
|
|
1156
|
+
.iter()
|
|
1157
|
+
.map(|(a, b)| (a.as_path(), b.as_path()))
|
|
1158
|
+
.collect();
|
|
1159
|
+
let feats = native_build_features_for_integration_test();
|
|
1160
|
+
compile_many_to_native(&refs, Some(workspace_root().as_path()), &feats, true)
|
|
1161
|
+
.unwrap_or_else(|e| panic!("compile_many_to_native: {}", e.message));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Run each binary sequentially. Parallel `fs::copy` + `exec` caused Linux ETXTBSY (errno 26)
|
|
1165
|
+
// in CI when several threads replaced/ran temp executables under load.
|
|
1166
|
+
let errors: Vec<String> = entries_owned
|
|
1167
|
+
.iter()
|
|
1168
|
+
.enumerate()
|
|
1169
|
+
.filter_map(|(run_index, (path, cached_bin))| {
|
|
1170
|
+
let expected = match get_expected(path) {
|
|
1171
|
+
Some(e) => e,
|
|
1172
|
+
None => return Some(format!("missing expected: {}", path.display())),
|
|
1173
|
+
};
|
|
1174
|
+
if !cached_bin.exists() {
|
|
1175
|
+
return Some(format!("missing cached binary: {}", cached_bin.display()));
|
|
1176
|
+
}
|
|
1177
|
+
let stem = path.file_stem().unwrap().to_string_lossy();
|
|
1178
|
+
let ext_bin = cached_bin
|
|
1179
|
+
.extension()
|
|
1180
|
+
.map(|e| e.to_string_lossy().to_string())
|
|
1181
|
+
.unwrap_or_default();
|
|
1182
|
+
let temp_dest = std::env::temp_dir().join(format!(
|
|
1183
|
+
"tish_mvp_native_{}_{:x}_{}_{}",
|
|
1184
|
+
stem,
|
|
1185
|
+
file_content_hash(path),
|
|
1186
|
+
std::process::id(),
|
|
1187
|
+
run_index
|
|
1188
|
+
));
|
|
1189
|
+
let temp_dest = if ext_bin.is_empty() {
|
|
1190
|
+
temp_dest
|
|
1191
|
+
} else {
|
|
1192
|
+
temp_dest.with_extension(&ext_bin)
|
|
1193
|
+
};
|
|
1194
|
+
std::fs::copy(cached_bin, &temp_dest).expect("copy cached native bin to temp");
|
|
1195
|
+
let out_bin = temp_dest;
|
|
1196
|
+
let out = match Command::new(&out_bin)
|
|
1197
|
+
.current_dir(workspace_root())
|
|
1198
|
+
.output()
|
|
1199
|
+
{
|
|
1200
|
+
Ok(o) => o,
|
|
1201
|
+
Err(e) => {
|
|
1202
|
+
let _ = std::fs::remove_file(&out_bin);
|
|
1203
|
+
return Some(format!("{}: run failed: {}", path.display(), e));
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
let _ = std::fs::remove_file(&out_bin);
|
|
1207
|
+
if !out.status.success() {
|
|
1208
|
+
return Some(format!(
|
|
1209
|
+
"{}: {}",
|
|
1210
|
+
path.display(),
|
|
1211
|
+
String::from_utf8_lossy(&out.stderr)
|
|
1212
|
+
));
|
|
1213
|
+
}
|
|
1214
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
1215
|
+
if stdout != expected {
|
|
1216
|
+
return Some(format!("{}: output mismatch", path.display()));
|
|
1217
|
+
}
|
|
1218
|
+
None
|
|
1219
|
+
})
|
|
1220
|
+
.collect();
|
|
1221
|
+
assert!(errors.is_empty(), "native failures:\n{}", errors.join("\n"));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// cranelift + wasi now use `discover_core_tests()` (full file discovery), like interp/vm/native —
|
|
1225
|
+
// the former curated `CRANELIFT_TEST_FILES` allowlist is gone. They embed the bytecode VM, which
|
|
1226
|
+
// has full interp↔vm parity (`VM_PARITY_SKIP` empty), so they inherit it: a disk-safe sweep confirmed
|
|
1227
|
+
// cranelift 66/66 and wasi 66/66. The old blocker was build COST — each backend build used to emit a
|
|
1228
|
+
// per-program `target/` (~2-5 GB) that accumulated to ~130 GB; now `tish_cranelift`/`tish_wasm` build
|
|
1229
|
+
// into a SHARED target dir so the deps compile once (610 MB / 85 MB total for the whole sweep, and a
|
|
1230
|
+
// repeat build is ~1 s). If a construct ever regresses on a backend, add it to a documented skip set
|
|
1231
|
+
// rather than re-introducing an allowlist.
|
|
1232
|
+
|
|
1233
|
+
/// Compile each .tish file with Cranelift backend, run, and compare stdout to static expected (parallelized).
|
|
1234
|
+
#[test]
|
|
1235
|
+
fn test_mvp_programs_cranelift() {
|
|
1236
|
+
let core_dir = core_dir();
|
|
1237
|
+
let bin = tish_bin();
|
|
1238
|
+
assert!(
|
|
1239
|
+
bin.exists(),
|
|
1240
|
+
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1241
|
+
bin.display()
|
|
1242
|
+
);
|
|
1243
|
+
let errors: Vec<String> = discover_core_tests()
|
|
1244
|
+
.par_iter()
|
|
1245
|
+
.filter_map(|name| {
|
|
1246
|
+
let path = core_dir.join(name);
|
|
1247
|
+
if !path.exists() {
|
|
1248
|
+
return None;
|
|
1249
|
+
}
|
|
1250
|
+
let expected = match get_expected(&path) {
|
|
1251
|
+
Some(e) => e,
|
|
1252
|
+
None => return Some(format!("missing expected: {}", path.display())),
|
|
1253
|
+
};
|
|
1254
|
+
let out_bin = compile_cached(&bin, &path, "cranelift");
|
|
1255
|
+
let out = match Command::new(&out_bin)
|
|
1256
|
+
.current_dir(workspace_root())
|
|
1257
|
+
.output()
|
|
1258
|
+
{
|
|
1259
|
+
Ok(o) => o,
|
|
1260
|
+
Err(e) => {
|
|
1261
|
+
let _ = std::fs::remove_file(&out_bin);
|
|
1262
|
+
return Some(format!("{}: run failed: {}", path.display(), e));
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
let _ = std::fs::remove_file(&out_bin);
|
|
1266
|
+
if !out.status.success() {
|
|
1267
|
+
return Some(format!(
|
|
1268
|
+
"{}: {}",
|
|
1269
|
+
path.display(),
|
|
1270
|
+
String::from_utf8_lossy(&out.stderr)
|
|
1271
|
+
));
|
|
1272
|
+
}
|
|
1273
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
1274
|
+
if stdout != expected {
|
|
1275
|
+
return Some(format!("{}: output mismatch", path.display()));
|
|
1276
|
+
}
|
|
1277
|
+
None
|
|
1278
|
+
})
|
|
1279
|
+
.collect();
|
|
1280
|
+
assert!(
|
|
1281
|
+
errors.is_empty(),
|
|
1282
|
+
"cranelift failures:\n{}",
|
|
1283
|
+
errors.join("\n")
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/// Compile each .tish file to WASI, run with wasmtime, and compare stdout to static expected (parallelized).
|
|
1288
|
+
/// Skips if wasmtime is not available.
|
|
1289
|
+
#[test]
|
|
1290
|
+
fn test_mvp_programs_wasi() {
|
|
1291
|
+
let wasmtime_available = Command::new("wasmtime")
|
|
1292
|
+
.arg("--version")
|
|
1293
|
+
.output()
|
|
1294
|
+
.map(|o| o.status.success())
|
|
1295
|
+
.unwrap_or(false);
|
|
1296
|
+
if !wasmtime_available {
|
|
1297
|
+
eprintln!("Skipping test_mvp_programs_wasi: wasmtime not found");
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
let core_dir = core_dir();
|
|
1301
|
+
let bin = tish_bin();
|
|
1302
|
+
assert!(
|
|
1303
|
+
bin.exists(),
|
|
1304
|
+
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1305
|
+
bin.display()
|
|
1306
|
+
);
|
|
1307
|
+
let errors: Vec<String> = discover_core_tests()
|
|
1308
|
+
.par_iter()
|
|
1309
|
+
.filter_map(|name| {
|
|
1310
|
+
let path = core_dir.join(name);
|
|
1311
|
+
if !path.exists() {
|
|
1312
|
+
return None;
|
|
1313
|
+
}
|
|
1314
|
+
let expected = match get_expected(&path) {
|
|
1315
|
+
Some(e) => e,
|
|
1316
|
+
None => return Some(format!("missing expected: {}", path.display())),
|
|
1317
|
+
};
|
|
1318
|
+
let out_wasm = compile_cached(&bin, &path, "wasi");
|
|
1319
|
+
let out = match Command::new("wasmtime")
|
|
1320
|
+
.arg(out_wasm.as_os_str())
|
|
1321
|
+
.current_dir(workspace_root())
|
|
1322
|
+
.output()
|
|
1323
|
+
{
|
|
1324
|
+
Ok(o) => o,
|
|
1325
|
+
Err(e) => {
|
|
1326
|
+
let _ = std::fs::remove_file(&out_wasm);
|
|
1327
|
+
return Some(format!("{}: wasmtime failed: {}", path.display(), e));
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
let _ = std::fs::remove_file(&out_wasm);
|
|
1331
|
+
if !out.status.success() {
|
|
1332
|
+
return Some(format!(
|
|
1333
|
+
"{}: {}",
|
|
1334
|
+
path.display(),
|
|
1335
|
+
String::from_utf8_lossy(&out.stderr)
|
|
1336
|
+
));
|
|
1337
|
+
}
|
|
1338
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
1339
|
+
if stdout != expected {
|
|
1340
|
+
return Some(format!("{}: output mismatch", path.display()));
|
|
1341
|
+
}
|
|
1342
|
+
None
|
|
1343
|
+
})
|
|
1344
|
+
.collect();
|
|
1345
|
+
assert!(errors.is_empty(), "wasi failures:\n{}", errors.join("\n"));
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/// Files where Tish intentionally differs from JavaScript (typeof, void); skip in JS test since we compare to Tish expected.
|
|
1349
|
+
const JS_SKIP_FILES: &[&str] = &["typeof.tish", "void.tish"];
|
|
1350
|
+
|
|
1351
|
+
/// Compile each .tish file to JS, run with Node, and compare stdout to static expected.
|
|
1352
|
+
#[test]
|
|
1353
|
+
fn test_mvp_programs_js() {
|
|
1354
|
+
let node_available = Command::new("node")
|
|
1355
|
+
.args(["--version"])
|
|
1356
|
+
.output()
|
|
1357
|
+
.map(|o| o.status.success())
|
|
1358
|
+
.unwrap_or(false);
|
|
1359
|
+
if !node_available {
|
|
1360
|
+
eprintln!("Skipping test_mvp_programs_js: Node.js not found");
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
let core_dir = core_dir();
|
|
1364
|
+
let bin = tish_bin();
|
|
1365
|
+
assert!(
|
|
1366
|
+
bin.exists(),
|
|
1367
|
+
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1368
|
+
bin.display()
|
|
1369
|
+
);
|
|
1370
|
+
for name in &discover_core_tests() {
|
|
1371
|
+
// intentional JS divergences + tests without a `.js` sibling can't use the Node oracle
|
|
1372
|
+
if JS_SKIP_FILES.contains(&name.as_str()) || !has_js_sibling(name) {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
let path = core_dir.join(name);
|
|
1376
|
+
if !path.exists() {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
let expected = get_expected(&path).unwrap_or_else(|| {
|
|
1380
|
+
panic!(
|
|
1381
|
+
"missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
|
|
1382
|
+
path.display()
|
|
1383
|
+
)
|
|
1384
|
+
});
|
|
1385
|
+
let out_js = compile_cached(&bin, &path, "js");
|
|
1386
|
+
let out = Command::new("node")
|
|
1387
|
+
.arg(&out_js)
|
|
1388
|
+
.current_dir(workspace_root())
|
|
1389
|
+
.output()
|
|
1390
|
+
.expect("run node");
|
|
1391
|
+
let _ = std::fs::remove_file(&out_js);
|
|
1392
|
+
assert!(
|
|
1393
|
+
out.status.success(),
|
|
1394
|
+
"Node failed for {}: {}",
|
|
1395
|
+
path.display(),
|
|
1396
|
+
String::from_utf8_lossy(&out.stderr)
|
|
1397
|
+
);
|
|
1398
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
1399
|
+
assert_eq!(
|
|
1400
|
+
stdout,
|
|
1401
|
+
expected,
|
|
1402
|
+
"JS output mismatch for {}",
|
|
1403
|
+
path.display()
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
}
|