@tishlang/tish 1.13.1 → 2.0.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 +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -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 +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- 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/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -86,6 +86,12 @@ fn combined_mvp_native_inputs_hash(paths: &[PathBuf]) -> u64 {
|
|
|
86
86
|
if value_rs.is_file() {
|
|
87
87
|
file_content_hash(&value_rs).hash(&mut h);
|
|
88
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
|
+
}
|
|
89
95
|
h.finish()
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -142,7 +148,22 @@ fn file_content_hash(path: &Path) -> u64 {
|
|
|
142
148
|
/// executing a binary still being written).
|
|
143
149
|
fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
144
150
|
let stem = path.file_stem().unwrap().to_string_lossy();
|
|
145
|
-
|
|
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
|
+
};
|
|
146
167
|
let hash8 = &format!("{:016x}", hash)[..8];
|
|
147
168
|
let cache_base = integration_compile_cache_dir().join(backend);
|
|
148
169
|
let _ = std::fs::create_dir_all(&cache_base);
|
|
@@ -473,31 +494,185 @@ fn test_async_await_run() {
|
|
|
473
494
|
}
|
|
474
495
|
}
|
|
475
496
|
|
|
476
|
-
///
|
|
477
|
-
///
|
|
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.
|
|
478
502
|
#[test]
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
);
|
|
497
588
|
}
|
|
498
589
|
}
|
|
499
590
|
|
|
500
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
|
+
|
|
501
676
|
/// Ignored: tishlang_eval::run() does not run the event loop.
|
|
502
677
|
#[test]
|
|
503
678
|
#[cfg(feature = "http")]
|
|
@@ -558,6 +733,109 @@ fn test_vm_date_now() {
|
|
|
558
733
|
}
|
|
559
734
|
}
|
|
560
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
|
+
|
|
561
839
|
/// VM run with parse+compile only (no resolve/merge) - isolates bytecode IndexAssign.
|
|
562
840
|
#[test]
|
|
563
841
|
fn test_vm_index_assign_direct() {
|
|
@@ -637,51 +915,92 @@ fn test_full_stack_parse() {
|
|
|
637
915
|
}
|
|
638
916
|
}
|
|
639
917
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
"
|
|
647
|
-
"
|
|
648
|
-
"
|
|
649
|
-
"
|
|
650
|
-
"
|
|
651
|
-
"
|
|
652
|
-
"
|
|
653
|
-
"
|
|
654
|
-
"
|
|
655
|
-
"
|
|
656
|
-
"
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
"
|
|
660
|
-
"
|
|
661
|
-
"
|
|
662
|
-
"
|
|
663
|
-
"
|
|
664
|
-
"
|
|
665
|
-
"math.tish",
|
|
666
|
-
"optional_chaining.tish",
|
|
667
|
-
"void.tish",
|
|
668
|
-
"rest_params.tish",
|
|
669
|
-
"json.tish",
|
|
670
|
-
"uri.tish",
|
|
671
|
-
"in_op.tish",
|
|
672
|
-
"arrow_functions.tish",
|
|
673
|
-
"template_literals.tish",
|
|
674
|
-
"compound_assign.tish",
|
|
675
|
-
"mutation.tish",
|
|
676
|
-
"string_methods.tish",
|
|
677
|
-
"array_methods.tish",
|
|
678
|
-
"object_methods.tish",
|
|
679
|
-
"types.tish",
|
|
680
|
-
"logical_assign.tish",
|
|
681
|
-
"spread.tish",
|
|
682
|
-
"fn_param_destructuring.tish",
|
|
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",
|
|
683
943
|
];
|
|
684
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
|
+
|
|
685
1004
|
/// Run each .tish file with interpreter and compare stdout to static expected.
|
|
686
1005
|
/// Set REGENERATE_EXPECTED=1 to write .expected files from interpreter output (run once, then commit).
|
|
687
1006
|
#[test]
|
|
@@ -694,7 +1013,7 @@ fn test_mvp_programs_interpreter() {
|
|
|
694
1013
|
bin.display()
|
|
695
1014
|
);
|
|
696
1015
|
let regenerate = std::env::var("REGENERATE_EXPECTED").as_deref() == Ok("1");
|
|
697
|
-
for name in
|
|
1016
|
+
for name in &discover_core_tests() {
|
|
698
1017
|
let path = core_dir.join(name);
|
|
699
1018
|
if !path.exists() {
|
|
700
1019
|
continue;
|
|
@@ -741,7 +1060,10 @@ fn test_mvp_programs_interp_vm_stdout_parity() {
|
|
|
741
1060
|
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
742
1061
|
bin.display()
|
|
743
1062
|
);
|
|
744
|
-
for name in
|
|
1063
|
+
for name in &discover_core_tests() {
|
|
1064
|
+
if VM_PARITY_SKIP.contains(&name.as_str()) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
745
1067
|
let path = core_dir.join(name);
|
|
746
1068
|
if !path.exists() {
|
|
747
1069
|
continue;
|
|
@@ -792,7 +1114,7 @@ fn test_mvp_programs_native() {
|
|
|
792
1114
|
bin.display()
|
|
793
1115
|
);
|
|
794
1116
|
|
|
795
|
-
let mut paths: Vec<PathBuf> =
|
|
1117
|
+
let mut paths: Vec<PathBuf> = discover_core_tests()
|
|
796
1118
|
.iter()
|
|
797
1119
|
.filter_map(|name| {
|
|
798
1120
|
let p = core_dir.join(name);
|
|
@@ -899,26 +1221,14 @@ fn test_mvp_programs_native() {
|
|
|
899
1221
|
assert!(errors.is_empty(), "native failures:\n{}", errors.join("\n"));
|
|
900
1222
|
}
|
|
901
1223
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
"json.tish",
|
|
911
|
-
"math.tish",
|
|
912
|
-
"builtins.tish",
|
|
913
|
-
"uri.tish",
|
|
914
|
-
"inc_dec.tish",
|
|
915
|
-
"exponentiation.tish",
|
|
916
|
-
"void.tish",
|
|
917
|
-
"rest_params.tish",
|
|
918
|
-
"arrow_functions.tish",
|
|
919
|
-
"array_methods.tish",
|
|
920
|
-
"types.tish",
|
|
921
|
-
];
|
|
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.
|
|
922
1232
|
|
|
923
1233
|
/// Compile each .tish file with Cranelift backend, run, and compare stdout to static expected (parallelized).
|
|
924
1234
|
#[test]
|
|
@@ -930,7 +1240,7 @@ fn test_mvp_programs_cranelift() {
|
|
|
930
1240
|
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
931
1241
|
bin.display()
|
|
932
1242
|
);
|
|
933
|
-
let errors: Vec<String> =
|
|
1243
|
+
let errors: Vec<String> = discover_core_tests()
|
|
934
1244
|
.par_iter()
|
|
935
1245
|
.filter_map(|name| {
|
|
936
1246
|
let path = core_dir.join(name);
|
|
@@ -994,7 +1304,7 @@ fn test_mvp_programs_wasi() {
|
|
|
994
1304
|
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
995
1305
|
bin.display()
|
|
996
1306
|
);
|
|
997
|
-
let errors: Vec<String> =
|
|
1307
|
+
let errors: Vec<String> = discover_core_tests()
|
|
998
1308
|
.par_iter()
|
|
999
1309
|
.filter_map(|name| {
|
|
1000
1310
|
let path = core_dir.join(name);
|
|
@@ -1057,8 +1367,9 @@ fn test_mvp_programs_js() {
|
|
|
1057
1367
|
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
|
|
1058
1368
|
bin.display()
|
|
1059
1369
|
);
|
|
1060
|
-
for name in
|
|
1061
|
-
|
|
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) {
|
|
1062
1373
|
continue;
|
|
1063
1374
|
}
|
|
1064
1375
|
let path = core_dir.join(name);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//! Issue #60: `try/catch` must catch VM-internal runtime errors (property/method access on
|
|
2
|
+
//! null, calling a non-function) AND `throw`s that propagate across function-call frames,
|
|
3
|
+
//! preserving the thrown value. Verified on the bytecode VM and the tree-walk interpreter
|
|
4
|
+
//! (the two backends #60 targets — the embedded/browser target is the VM, the interpreter is
|
|
5
|
+
//! its oracle). The native backend surfaces these as Rust panics and is tracked separately.
|
|
6
|
+
|
|
7
|
+
use std::path::PathBuf;
|
|
8
|
+
use std::process::Command;
|
|
9
|
+
|
|
10
|
+
const EXPECTED: &str = "\
|
|
11
|
+
A caught
|
|
12
|
+
B caught: boom
|
|
13
|
+
C caught
|
|
14
|
+
D done
|
|
15
|
+
E caught: rethrown
|
|
16
|
+
F ok
|
|
17
|
+
G total: 206
|
|
18
|
+
";
|
|
19
|
+
|
|
20
|
+
fn run(backend: &str, fixture: &str) -> String {
|
|
21
|
+
let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
|
|
22
|
+
let out = Command::new(&tish)
|
|
23
|
+
.args(["run", "--backend", backend, fixture])
|
|
24
|
+
.output()
|
|
25
|
+
.expect("spawn tish run");
|
|
26
|
+
assert!(
|
|
27
|
+
out.status.success(),
|
|
28
|
+
"tish run --backend {backend} failed: {}",
|
|
29
|
+
String::from_utf8_lossy(&out.stderr)
|
|
30
|
+
);
|
|
31
|
+
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn trycatch_catches_runtime_and_cross_frame_errors() {
|
|
36
|
+
let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
37
|
+
.join("tests")
|
|
38
|
+
.join("fixtures")
|
|
39
|
+
.join("trycatch_runtime_errors.tish");
|
|
40
|
+
assert!(fixture.is_file(), "missing fixture {}", fixture.display());
|
|
41
|
+
let fixture = fixture.to_str().unwrap();
|
|
42
|
+
|
|
43
|
+
assert_eq!(run("vm", fixture), EXPECTED, "vm output mismatch");
|
|
44
|
+
assert_eq!(run("interp", fixture), EXPECTED, "interp output mismatch");
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//! Issue #101: the `tish:tty` capability (raw mode, terminal size, key/resize events) is
|
|
2
|
+
//! available on every backend behind the `tty` feature. CI has no real terminal, so this
|
|
3
|
+
//! asserts the module loads and degrades gracefully (size → null, isTTY → a boolean, the
|
|
4
|
+
//! event/raw-mode primitives are functions). Interactive behavior is covered manually.
|
|
5
|
+
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
use std::process::Command;
|
|
8
|
+
|
|
9
|
+
const EXPECTED: &str = "\
|
|
10
|
+
isTTY: boolean
|
|
11
|
+
size: null
|
|
12
|
+
setRawMode: function
|
|
13
|
+
read: function
|
|
14
|
+
readLine: function
|
|
15
|
+
alt: function function
|
|
16
|
+
";
|
|
17
|
+
|
|
18
|
+
fn run(backend: &str, fixture: &str) -> String {
|
|
19
|
+
let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
|
|
20
|
+
let out = Command::new(&tish)
|
|
21
|
+
.args(["run", "--backend", backend, "--feature", "tty", fixture])
|
|
22
|
+
.output()
|
|
23
|
+
.expect("spawn tish run");
|
|
24
|
+
assert!(
|
|
25
|
+
out.status.success(),
|
|
26
|
+
"tish run --backend {backend} failed: {}",
|
|
27
|
+
String::from_utf8_lossy(&out.stderr)
|
|
28
|
+
);
|
|
29
|
+
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn tty_module_loads_and_degrades_without_a_terminal() {
|
|
34
|
+
let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
35
|
+
.join("tests")
|
|
36
|
+
.join("fixtures")
|
|
37
|
+
.join("tty_capability.tish");
|
|
38
|
+
assert!(fixture.is_file(), "missing fixture {}", fixture.display());
|
|
39
|
+
let fixture = fixture.to_str().unwrap();
|
|
40
|
+
|
|
41
|
+
assert_eq!(run("vm", fixture), EXPECTED, "vm output mismatch");
|
|
42
|
+
assert_eq!(run("interp", fixture), EXPECTED, "interp output mismatch");
|
|
43
|
+
}
|