@tishlang/tish-format 2.12.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
package/Cargo.toml
CHANGED
|
@@ -34,6 +34,9 @@ members = [
|
|
|
34
34
|
"crates/tish_resolve",
|
|
35
35
|
"crates/tishlang_cargo_bindgen",
|
|
36
36
|
]
|
|
37
|
+
# Generated native-build projects (`tish build`) land under `target/` when TMPDIR points there;
|
|
38
|
+
# never enroll anything beneath `target/` as a workspace member.
|
|
39
|
+
exclude = ["target"]
|
|
37
40
|
resolver = "2"
|
|
38
41
|
|
|
39
42
|
[workspace.package]
|
package/bin/tish-format
CHANGED
|
Binary file
|
|
@@ -176,7 +176,12 @@ pub fn convert_expr(expr: &OxcExpr<'_>, ctx: &Ctx<'_>) -> Result<Expr, ConvertEr
|
|
|
176
176
|
span: span_util::oxc_span_to_tish(ctx.1, &*arrow.body),
|
|
177
177
|
}))
|
|
178
178
|
};
|
|
179
|
-
Ok(Expr::ArrowFunction {
|
|
179
|
+
Ok(Expr::ArrowFunction {
|
|
180
|
+
async_: arrow.r#async,
|
|
181
|
+
params,
|
|
182
|
+
body,
|
|
183
|
+
span,
|
|
184
|
+
})
|
|
180
185
|
}
|
|
181
186
|
OxcExpr::AwaitExpression(a) => Ok(Expr::Await {
|
|
182
187
|
operand: Box::new(convert_expr(&a.argument, ctx)?),
|
|
@@ -229,7 +234,12 @@ pub fn convert_expr(expr: &OxcExpr<'_>, ctx: &Ctx<'_>) -> Result<Expr, ConvertEr
|
|
|
229
234
|
}))
|
|
230
235
|
}
|
|
231
236
|
};
|
|
232
|
-
Ok(Expr::ArrowFunction {
|
|
237
|
+
Ok(Expr::ArrowFunction {
|
|
238
|
+
async_: f.r#async,
|
|
239
|
+
params,
|
|
240
|
+
body,
|
|
241
|
+
span,
|
|
242
|
+
})
|
|
233
243
|
}
|
|
234
244
|
_ => Err(ConvertError::new(ConvertErrorKind::Unsupported {
|
|
235
245
|
what: format!("expression: {:?}", std::mem::discriminant(expr)),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Issue #122: Node callback form on the VM — `fs.writeFile(path, data, (err) => …)` and
|
|
2
|
+
// `fs.readFile(path, (err, data) => …)`, with (err, data) semantics and a non-null err on a
|
|
3
|
+
// missing file. Operates under $FS_TMP.
|
|
4
|
+
import { writeFile, readFile, stat, unlink } from "node:fs"
|
|
5
|
+
import { process } from "tish:process"
|
|
6
|
+
|
|
7
|
+
let d = process.env.FS_TMP
|
|
8
|
+
writeFile(d + "/cb.txt", "callback data", (err) => {
|
|
9
|
+
console.log("write-err-null=" + String(err === null))
|
|
10
|
+
readFile(d + "/cb.txt", (err2, data) => {
|
|
11
|
+
console.log("read=" + data)
|
|
12
|
+
stat(d + "/cb.txt", (err3, st) => {
|
|
13
|
+
console.log("size=" + String(st.size) + ",file=" + String(st.isFile()))
|
|
14
|
+
unlink(d + "/cb.txt", (err4) => {
|
|
15
|
+
console.log("unlink-err-null=" + String(err4 === null))
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
readFile(d + "/missing.txt", (err, data) => {
|
|
21
|
+
console.log("missing-err-set=" + String(!(err === null)) + ",data-null=" + String(data === null))
|
|
22
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Issue #122: the async `node:fs/promises` surface — await on the same methods, and `access`
|
|
2
|
+
// rejects on a missing path. Operates under $FS_TMP.
|
|
3
|
+
import { writeFile, readFile, stat, mkdir, readdir, rm, access } from "node:fs/promises"
|
|
4
|
+
import { process } from "tish:process"
|
|
5
|
+
|
|
6
|
+
async fn main() {
|
|
7
|
+
let d = process.env.FS_TMP
|
|
8
|
+
await writeFile(d + "/p.txt", "async hello")
|
|
9
|
+
console.log("read=" + (await readFile(d + "/p.txt")))
|
|
10
|
+
let st = await stat(d + "/p.txt")
|
|
11
|
+
console.log("size=" + String(st.size))
|
|
12
|
+
await mkdir(d + "/pd")
|
|
13
|
+
await writeFile(d + "/pd/x", "x")
|
|
14
|
+
console.log("dir=" + (await readdir(d + "/pd")).join(","))
|
|
15
|
+
let rejected = false
|
|
16
|
+
try { await access(d + "/missing") } catch (e) { rejected = true }
|
|
17
|
+
console.log("access-missing-rejected=" + String(rejected))
|
|
18
|
+
await rm(d + "/pd", { recursive: true })
|
|
19
|
+
console.log("done")
|
|
20
|
+
}
|
|
21
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Issue #122: Node-compatible fs sync surface via `node:fs`. Operates under $FS_TMP.
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, statSync, mkdirSync, readdirSync, renameSync, rmSync, appendFileSync, copyFileSync, constants } from "node:fs"
|
|
3
|
+
import { process } from "tish:process"
|
|
4
|
+
|
|
5
|
+
let d = process.env.FS_TMP
|
|
6
|
+
writeFileSync(d + "/a.txt", "hi")
|
|
7
|
+
appendFileSync(d + "/a.txt", "!")
|
|
8
|
+
console.log("read=" + readFileSync(d + "/a.txt"))
|
|
9
|
+
console.log("exists=" + String(existsSync(d + "/a.txt")) + "," + String(existsSync(d + "/no")))
|
|
10
|
+
let st = statSync(d + "/a.txt")
|
|
11
|
+
console.log("size=" + String(st.size) + ",file=" + String(st.isFile()) + ",dir=" + String(st.isDirectory()))
|
|
12
|
+
mkdirSync(d + "/sub")
|
|
13
|
+
copyFileSync(d + "/a.txt", d + "/sub/x.txt")
|
|
14
|
+
console.log("dir=" + readdirSync(d + "/sub").join(","))
|
|
15
|
+
renameSync(d + "/a.txt", d + "/b.txt")
|
|
16
|
+
console.log("renamed=" + String(existsSync(d + "/a.txt")) + "," + String(existsSync(d + "/b.txt")))
|
|
17
|
+
rmSync(d + "/sub", { recursive: true })
|
|
18
|
+
console.log("rm=" + String(existsSync(d + "/sub")))
|
|
19
|
+
console.log("const=" + String(constants.R_OK))
|
|
20
|
+
console.log("done")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//! Issue #122: Node `fs` parity on the bytecode VM (`tish run` default backend) — the sync
|
|
2
|
+
//! surface via `node:fs` and the async `node:fs/promises` surface (await + rejection).
|
|
3
|
+
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
use std::process::Command;
|
|
6
|
+
|
|
7
|
+
fn run_on(backend: &str, fixture: &str, tmp: &str) -> String {
|
|
8
|
+
let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
|
|
9
|
+
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
10
|
+
.join("tests")
|
|
11
|
+
.join("fixtures")
|
|
12
|
+
.join(fixture);
|
|
13
|
+
let out = Command::new(&tish)
|
|
14
|
+
.args([
|
|
15
|
+
"run",
|
|
16
|
+
"--backend",
|
|
17
|
+
backend,
|
|
18
|
+
"--feature",
|
|
19
|
+
"fs,process",
|
|
20
|
+
path.to_str().unwrap(),
|
|
21
|
+
])
|
|
22
|
+
.env("FS_TMP", tmp)
|
|
23
|
+
.output()
|
|
24
|
+
.expect("spawn tish run");
|
|
25
|
+
assert!(
|
|
26
|
+
out.status.success(),
|
|
27
|
+
"tish run --backend {backend} {fixture} failed: {}",
|
|
28
|
+
String::from_utf8_lossy(&out.stderr)
|
|
29
|
+
);
|
|
30
|
+
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn tmp_dir(tag: &str) -> String {
|
|
34
|
+
let nanos = std::time::SystemTime::now()
|
|
35
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
36
|
+
.unwrap()
|
|
37
|
+
.as_nanos();
|
|
38
|
+
let d = std::env::temp_dir().join(format!("tish_fs_parity_{tag}_{nanos:x}"));
|
|
39
|
+
std::fs::create_dir_all(&d).unwrap();
|
|
40
|
+
d.to_str().unwrap().to_string()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SYNC_EXPECTED: &str = "read=hi!\n\
|
|
44
|
+
exists=true,false\n\
|
|
45
|
+
size=3,file=true,dir=false\n\
|
|
46
|
+
dir=x.txt\n\
|
|
47
|
+
renamed=false,true\n\
|
|
48
|
+
rm=false\n\
|
|
49
|
+
const=4\n\
|
|
50
|
+
done\n";
|
|
51
|
+
|
|
52
|
+
const PROMISES_EXPECTED: &str = "read=async hello\n\
|
|
53
|
+
size=11\n\
|
|
54
|
+
dir=x\n\
|
|
55
|
+
access-missing-rejected=true\n\
|
|
56
|
+
done\n";
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn node_fs_sync_surface_on_every_backend() {
|
|
60
|
+
for backend in ["vm", "interp"] {
|
|
61
|
+
let tmp = tmp_dir(&format!("sync_{backend}"));
|
|
62
|
+
assert_eq!(
|
|
63
|
+
run_on(backend, "fs_parity_sync.tish", &tmp),
|
|
64
|
+
SYNC_EXPECTED,
|
|
65
|
+
"sync surface mismatch on {backend}"
|
|
66
|
+
);
|
|
67
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn node_fs_callback_form_on_every_backend() {
|
|
73
|
+
// Node callback form `readFile(path, (err, data) => …)` on every backend (#122): native's
|
|
74
|
+
// multi-arg-closure-to-native lowering was fixed upstream, and the interpreter runs the callback
|
|
75
|
+
// at the eval level (call_func's Native arm) instead of the self-less core bridge.
|
|
76
|
+
let expected = "write-err-null=true\n\
|
|
77
|
+
read=callback data\n\
|
|
78
|
+
size=13,file=true\n\
|
|
79
|
+
unlink-err-null=true\n\
|
|
80
|
+
missing-err-set=true,data-null=true\n";
|
|
81
|
+
for backend in ["vm", "interp", "native"] {
|
|
82
|
+
let tmp = tmp_dir(&format!("cb_{backend}"));
|
|
83
|
+
assert_eq!(
|
|
84
|
+
run_on(backend, "fs_parity_callbacks.tish", &tmp),
|
|
85
|
+
expected,
|
|
86
|
+
"callback form mismatch on {backend}"
|
|
87
|
+
);
|
|
88
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn node_fs_promises_surface_on_every_backend() {
|
|
94
|
+
for backend in ["vm", "interp"] {
|
|
95
|
+
let tmp = tmp_dir(&format!("prom_{backend}"));
|
|
96
|
+
assert_eq!(
|
|
97
|
+
run_on(backend, "fs_parity_promises.tish", &tmp),
|
|
98
|
+
PROMISES_EXPECTED,
|
|
99
|
+
"promises surface mismatch on {backend}"
|
|
100
|
+
);
|
|
101
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -52,6 +52,28 @@ fn integration_compile_cache_dir() -> PathBuf {
|
|
|
52
52
|
target_dir().join("integration_compile_cache")
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/// Remove cache entries under `dir` whose name starts with `prefix` but is not part of the current
|
|
56
|
+
/// generation `keep_leaf` (the current artifact may have an extension appended, e.g. `<leaf>.wasm`,
|
|
57
|
+
/// so we keep anything that *starts with* `keep_leaf`). Best-effort: caps the per-fixture compile
|
|
58
|
+
/// cache so a `tish` rebuild doesn't leave the previous generation behind forever. Parallel-safe —
|
|
59
|
+
/// see the call site in `compile_cached`.
|
|
60
|
+
fn prune_stale_cache_generations(dir: &Path, prefix: &str, keep_leaf: &str) {
|
|
61
|
+
if let Ok(rd) = std::fs::read_dir(dir) {
|
|
62
|
+
for ent in rd.flatten() {
|
|
63
|
+
let name = ent.file_name();
|
|
64
|
+
let name = name.to_string_lossy();
|
|
65
|
+
if name.starts_with(prefix) && !name.starts_with(keep_leaf) {
|
|
66
|
+
let p = ent.path();
|
|
67
|
+
if p.is_dir() {
|
|
68
|
+
let _ = std::fs::remove_dir_all(&p);
|
|
69
|
+
} else {
|
|
70
|
+
let _ = std::fs::remove_file(&p);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
/// Match `tish build` with no `--feature`: link every capability compiled into this `tish` binary.
|
|
56
78
|
fn native_build_features_for_integration_test() -> Vec<String> {
|
|
57
79
|
let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities()
|
|
@@ -170,6 +192,14 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
|
170
192
|
// Include `backend` in the leaf name so nested cargo bin names never collide across backends.
|
|
171
193
|
let leaf = format!("{}__{}__{}", stem, backend, hash8);
|
|
172
194
|
|
|
195
|
+
// Cap the cache: `hash8` mixes in the `tish` binary's mtime, so every recompile of `tish`
|
|
196
|
+
// orphans the previous generation of THIS (stem, backend). Drop those stale siblings now, so the
|
|
197
|
+
// cache holds ~one generation per fixture instead of growing without bound across rebuilds
|
|
198
|
+
// (it reached 14 GB in one session and exhausted the disk). Safe under parallel `nextest`: every
|
|
199
|
+
// live process for this (stem, backend) computes the SAME `hash8`, so only genuinely-stale
|
|
200
|
+
// generations (a different hash) are removed, never an artifact a concurrent run is using.
|
|
201
|
+
prune_stale_cache_generations(&cache_base, &format!("{}__{}__", stem, backend), &leaf);
|
|
202
|
+
|
|
173
203
|
let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
|
|
174
204
|
"native" => {
|
|
175
205
|
let ext = if cfg!(target_os = "windows") {
|
|
@@ -588,6 +618,46 @@ fn test_import_alias() {
|
|
|
588
618
|
}
|
|
589
619
|
}
|
|
590
620
|
|
|
621
|
+
/// #305: re-export — `export { foo, bar as inc } from "./dep"` and `export * from "./dep"` in a
|
|
622
|
+
/// middle module, consumed by a named import and a namespace import in the entry. Identical across
|
|
623
|
+
/// the interpreter and the VM (native/cranelift/js verified via the bundle path).
|
|
624
|
+
#[test]
|
|
625
|
+
fn test_reexport() {
|
|
626
|
+
let bin = tish_bin();
|
|
627
|
+
if !bin.exists() {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
let path = workspace_root()
|
|
631
|
+
.join("tests")
|
|
632
|
+
.join("modules")
|
|
633
|
+
.join("reexport.tish");
|
|
634
|
+
if !path.exists() {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
let expected = "42\n42\n1.0\n42\n";
|
|
638
|
+
for backend_args in [vec!["run"], vec!["run", "--backend", "vm"]] {
|
|
639
|
+
let mut args = backend_args.clone();
|
|
640
|
+
args.push(path.to_string_lossy().to_string().leak());
|
|
641
|
+
let out = Command::new(&bin)
|
|
642
|
+
.args(&args)
|
|
643
|
+
.current_dir(workspace_root())
|
|
644
|
+
.output()
|
|
645
|
+
.expect("run tish binary");
|
|
646
|
+
assert!(
|
|
647
|
+
out.status.success(),
|
|
648
|
+
"reexport.tish ({:?}) failed: stderr={}",
|
|
649
|
+
backend_args,
|
|
650
|
+
String::from_utf8_lossy(&out.stderr)
|
|
651
|
+
);
|
|
652
|
+
assert_eq!(
|
|
653
|
+
String::from_utf8_lossy(&out.stdout),
|
|
654
|
+
expected,
|
|
655
|
+
"reexport output mismatch on backend {:?}",
|
|
656
|
+
backend_args
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
591
661
|
/// Combined validation: async/await + Promise + setTimeout + multiple HTTP requests.
|
|
592
662
|
/// #97: a module's non-exported top-level bindings stay private — a same-named binding in
|
|
593
663
|
/// another module must not overwrite them (runtime), and the `--target js` bundle must not
|
|
@@ -1376,6 +1446,16 @@ fn test_mvp_programs_native() {
|
|
|
1376
1446
|
let combined = combined_mvp_native_inputs_hash(&paths);
|
|
1377
1447
|
let cache_dir = mvp_native_batch_cache_dir(combined);
|
|
1378
1448
|
let _ = std::fs::create_dir_all(&cache_dir);
|
|
1449
|
+
// Cap the native batch cache: a new combined hash (a codegen / inference / source change) orphans
|
|
1450
|
+
// the previous `native_many/<hash>` directory, each of which holds a full nested Cargo `target/`
|
|
1451
|
+
// (~GBs). Drop the stale siblings, keeping only the current generation. Only `test_mvp_programs_
|
|
1452
|
+
// native` builds here, so there is no cross-process race.
|
|
1453
|
+
if let (Some(parent), Some(keep)) = (
|
|
1454
|
+
cache_dir.parent(),
|
|
1455
|
+
cache_dir.file_name().map(|n| n.to_string_lossy().into_owned()),
|
|
1456
|
+
) {
|
|
1457
|
+
prune_stale_cache_generations(parent, "", &keep);
|
|
1458
|
+
}
|
|
1379
1459
|
|
|
1380
1460
|
let ext = if cfg!(target_os = "windows") {
|
|
1381
1461
|
".exe"
|
|
@@ -9,7 +9,7 @@ use tishlang_vm;
|
|
|
9
9
|
|
|
10
10
|
#[test]
|
|
11
11
|
fn test_and_shortcircuit_emits_jump() {
|
|
12
|
-
let source = "let x = null; let y = x
|
|
12
|
+
let source = "let x = null; let y = x !== null && x.foo;";
|
|
13
13
|
let program = parse(source).expect("parse");
|
|
14
14
|
let chunk = compile_unoptimized(&program).expect("compile");
|
|
15
15
|
let code = &chunk.code;
|
|
@@ -22,7 +22,7 @@ fn test_and_shortcircuit_emits_jump() {
|
|
|
22
22
|
|
|
23
23
|
#[test]
|
|
24
24
|
fn test_and_shortcircuit_runs_unoptimized() {
|
|
25
|
-
let source = "let x = null; let y = x
|
|
25
|
+
let source = "let x = null; let y = x !== null && x.foo;";
|
|
26
26
|
let program = parse(source).expect("parse");
|
|
27
27
|
let chunk = compile_unoptimized(&program).expect("compile");
|
|
28
28
|
let result = tishlang_vm::run(&chunk);
|
|
@@ -35,7 +35,7 @@ fn test_and_shortcircuit_runs_unoptimized() {
|
|
|
35
35
|
|
|
36
36
|
#[test]
|
|
37
37
|
fn test_and_shortcircuit_runs_optimized() {
|
|
38
|
-
let source = "let x = null; let y = x
|
|
38
|
+
let source = "let x = null; let y = x !== null && x.foo;";
|
|
39
39
|
let program = parse(source).expect("parse");
|
|
40
40
|
let program = tishlang_opt::optimize(&program);
|
|
41
41
|
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
@@ -156,6 +156,18 @@ pub enum ExportDeclaration {
|
|
|
156
156
|
Named(Box<Statement>),
|
|
157
157
|
/// export default expr
|
|
158
158
|
Default(Expr),
|
|
159
|
+
/// Re-export from another module (#305):
|
|
160
|
+
/// `export { foo, bar as inc } from "./m"` -> specifiers, all=false, from=Some
|
|
161
|
+
/// `export * from "./m"` -> specifiers=[], all=true, from=Some
|
|
162
|
+
/// `export { a, b as c }` -> specifiers, all=false, from=None (#415 —
|
|
163
|
+
/// a LOCAL named export of already-declared bindings)
|
|
164
|
+
/// Specifiers reuse `ImportSpecifier::Named` (`{ a as b }`).
|
|
165
|
+
ReExport {
|
|
166
|
+
specifiers: Vec<ImportSpecifier>,
|
|
167
|
+
all: bool,
|
|
168
|
+
from: Option<Arc<str>>,
|
|
169
|
+
span: Span,
|
|
170
|
+
},
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
#[derive(Debug, Clone)]
|
|
@@ -163,6 +175,64 @@ pub struct Program {
|
|
|
163
175
|
pub statements: Vec<Statement>,
|
|
164
176
|
}
|
|
165
177
|
|
|
178
|
+
/// #469: adopt a Go/Rust/Swift-style `main` entry point.
|
|
179
|
+
///
|
|
180
|
+
/// tish runs top-level statements (JS module semantics), so `fn main() { … }` on its own defines a
|
|
181
|
+
/// function that is never called — a footgun for devs coming from compiled languages (the native
|
|
182
|
+
/// server case: `fn main() { serve(...) }` silently exits). This appends a synthetic `main()`
|
|
183
|
+
/// invocation to the ENTRY program's top-level statements so a defined `main` actually runs: top-level
|
|
184
|
+
/// statements still execute first (module init), then `main`.
|
|
185
|
+
///
|
|
186
|
+
/// Deliberately conservative to avoid a double-invocation: it fires ONLY when a top-level `main`
|
|
187
|
+
/// function is declared AND the program does not already invoke `main()` itself at top level (the
|
|
188
|
+
/// JS-habit `fn main(){…}; main()` shape). This must be applied to the entry module only (not imported
|
|
189
|
+
/// modules) and identically for every backend — call it from `merge_modules` so interp/vm/native/js
|
|
190
|
+
/// stay consistent by construction.
|
|
191
|
+
///
|
|
192
|
+
/// The synthetic call is a plain `main()` (never `await main()`), even for `async fn main`: a blocking
|
|
193
|
+
/// server main never returns anyway, and the interpreter cannot `await` a user async function's result
|
|
194
|
+
/// (a separate pre-existing gap), so an unwrapped call is the one form that behaves identically on
|
|
195
|
+
/// every backend.
|
|
196
|
+
///
|
|
197
|
+
/// Note: this is a tish-specific divergence from JS/node (node does not auto-call `main`); test it via
|
|
198
|
+
/// interp==vm==native cross-checks, and pair any `.tish` fixture with a `.js` that calls `main()`
|
|
199
|
+
/// explicitly so the node reference matches.
|
|
200
|
+
pub fn append_main_entry(statements: &mut Vec<Statement>) {
|
|
201
|
+
// A top-level `fn main` / `async fn main` declaration.
|
|
202
|
+
let has_main = statements.iter().any(|s| {
|
|
203
|
+
matches!(s, Statement::FunDecl { name, .. } if &**name == "main")
|
|
204
|
+
});
|
|
205
|
+
if !has_main {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Already invoked at top level? Then the user is driving it — don't double-call.
|
|
209
|
+
fn is_main_call(expr: &Expr) -> bool {
|
|
210
|
+
match expr {
|
|
211
|
+
Expr::Call { callee, .. } => {
|
|
212
|
+
matches!(callee.as_ref(), Expr::Ident { name, .. } if &**name == "main")
|
|
213
|
+
}
|
|
214
|
+
Expr::Await { operand, .. } => is_main_call(operand),
|
|
215
|
+
_ => false,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let already_called = statements
|
|
219
|
+
.iter()
|
|
220
|
+
.any(|s| matches!(s, Statement::ExprStmt { expr, .. } if is_main_call(expr)));
|
|
221
|
+
if already_called {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let span = Span::default();
|
|
225
|
+
let call = Expr::Call {
|
|
226
|
+
callee: Box::new(Expr::Ident {
|
|
227
|
+
name: Arc::from("main"),
|
|
228
|
+
span,
|
|
229
|
+
}),
|
|
230
|
+
args: Vec::new(),
|
|
231
|
+
span,
|
|
232
|
+
};
|
|
233
|
+
statements.push(Statement::ExprStmt { expr: call, span });
|
|
234
|
+
}
|
|
235
|
+
|
|
166
236
|
#[derive(Debug, Clone, PartialEq)]
|
|
167
237
|
pub enum Statement {
|
|
168
238
|
Block {
|
|
@@ -221,6 +291,16 @@ pub enum Statement {
|
|
|
221
291
|
body: Box<Statement>,
|
|
222
292
|
span: Span,
|
|
223
293
|
},
|
|
294
|
+
/// `for (let k in object)` — iterates the object's own enumerable keys (JS `for-in` semantics).
|
|
295
|
+
/// Distinct from `ForOf` (which iterates values). Kept as a first-class node so every backend
|
|
296
|
+
/// enumerates keys consistently and `tish fmt` round-trips `for-in`.
|
|
297
|
+
ForIn {
|
|
298
|
+
name: Arc<str>,
|
|
299
|
+
name_span: Span,
|
|
300
|
+
object: Expr,
|
|
301
|
+
body: Box<Statement>,
|
|
302
|
+
span: Span,
|
|
303
|
+
},
|
|
224
304
|
Return {
|
|
225
305
|
value: Option<Expr>,
|
|
226
306
|
span: Span,
|
|
@@ -421,6 +501,9 @@ pub enum Expr {
|
|
|
421
501
|
},
|
|
422
502
|
/// Arrow function: (params) => body
|
|
423
503
|
ArrowFunction {
|
|
504
|
+
/// `true` for `async () => …` (#428) — the body runs in async context (`await` allowed) and
|
|
505
|
+
/// the JS target emits an `async` prefix. Non-async arrows have `async_: false`.
|
|
506
|
+
async_: bool,
|
|
424
507
|
params: Vec<FunParam>,
|
|
425
508
|
body: ArrowBody,
|
|
426
509
|
span: Span,
|
|
@@ -643,6 +726,7 @@ impl Statement {
|
|
|
643
726
|
| Statement::While { span, .. }
|
|
644
727
|
| Statement::For { span, .. }
|
|
645
728
|
| Statement::ForOf { span, .. }
|
|
729
|
+
| Statement::ForIn { span, .. }
|
|
646
730
|
| Statement::Return { span, .. }
|
|
647
731
|
| Statement::Break { span, .. }
|
|
648
732
|
| Statement::Continue { span, .. }
|