@tishlang/tish-lsp 3.3.3 → 3.7.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 +1 -0
- package/bin/tish-lsp +0 -0
- package/crates/tish/Cargo.toml +4 -1
- package/crates/tish/src/cli_help.rs +84 -0
- package/crates/tish/src/main.rs +93 -1
- package/crates/tish/tests/scope_merge.rs +90 -0
- package/crates/tish/tests/test_runner.rs +673 -0
- package/crates/tish_bytecode/src/compiler.rs +25 -4
- package/crates/tish_compile/src/check.rs +178 -0
- package/crates/tish_compile/src/codegen.rs +383 -63
- package/crates/tish_compile/src/lib.rs +33 -1
- package/crates/tish_compile/src/resolve.rs +121 -19
- package/crates/tish_compile/src/types.rs +35 -1
- package/crates/tish_compile/tests/call_arity_641.rs +104 -0
- package/crates/tish_core/src/lib.rs +103 -11
- package/crates/tish_eval/src/eval.rs +16 -0
- package/crates/tish_eval/src/value_convert.rs +10 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_test/Cargo.toml +47 -0
- package/crates/tish_test/src/assert.rs +435 -0
- package/crates/tish_test/src/config.rs +85 -0
- package/crates/tish_test/src/coverage.rs +237 -0
- package/crates/tish_test/src/deep_equal.rs +261 -0
- package/crates/tish_test/src/discovery.rs +70 -0
- package/crates/tish_test/src/expect.rs +977 -0
- package/crates/tish_test/src/instrument.rs +457 -0
- package/crates/tish_test/src/isolation.rs +14 -0
- package/crates/tish_test/src/lib.rs +22 -0
- package/crates/tish_test/src/load.rs +152 -0
- package/crates/tish_test/src/mocks.rs +270 -0
- package/crates/tish_test/src/module.rs +318 -0
- package/crates/tish_test/src/registry.rs +235 -0
- package/crates/tish_test/src/report.rs +312 -0
- package/crates/tish_test/src/runner.rs +797 -0
- package/crates/tish_test/src/snapshots.rs +197 -0
- package/crates/tish_vm/src/vm.rs +17 -14
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
package/Cargo.toml
CHANGED
|
@@ -33,6 +33,7 @@ members = [
|
|
|
33
33
|
"crates/tish_lsp",
|
|
34
34
|
"crates/tish_resolve",
|
|
35
35
|
"crates/tishlang_cargo_bindgen",
|
|
36
|
+
"crates/tish_test",
|
|
36
37
|
]
|
|
37
38
|
# Generated native-build projects (`tish build`) land under `target/` when TMPDIR points there;
|
|
38
39
|
# never enroll anything beneath `target/` as a workspace member.
|
package/bin/tish-lsp
CHANGED
|
Binary file
|
package/crates/tish/Cargo.toml
CHANGED
|
@@ -25,7 +25,7 @@ path = "src/main.rs"
|
|
|
25
25
|
default = ["full", "fast-alloc"]
|
|
26
26
|
# Alias: same dependency edges as `default` (kept for `--features full` in CI and docs).
|
|
27
27
|
# Includes `pg` so `tish run` can resolve `import … from '@tishlang/pg'` (`cargo:tish_pg`) on the bytecode VM.
|
|
28
|
-
full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg"]
|
|
28
|
+
full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg", "test-runner"]
|
|
29
29
|
# Fast allocator (mimalloc) as the process `#[global_allocator]`. tish's object/array/string workloads
|
|
30
30
|
# are allocation-bound (a sampling profile of object-heavy code spends ~14%+ in system malloc/free);
|
|
31
31
|
# mimalloc is markedly faster for the many-small-allocations pattern — the same reason JSC ships bmalloc.
|
|
@@ -33,6 +33,8 @@ full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg"]
|
|
|
33
33
|
fast-alloc = ["dep:mimalloc"]
|
|
34
34
|
# Opt-out: build without Postgres (`cargo build -p tishlang --no-default-features --features http,...`).
|
|
35
35
|
pg = ["dep:tishlang_pg"]
|
|
36
|
+
# Native `tish test` runner (`tish:test` / `tish:assert`).
|
|
37
|
+
test-runner = ["dep:tishlang_test"]
|
|
36
38
|
# Individual capability flags
|
|
37
39
|
http = ["tishlang_eval/http", "tishlang_runtime/http", "tishlang_compile/http", "tishlang_vm/http"]
|
|
38
40
|
timers = ["tishlang_eval/timers", "tishlang_compile/timers", "tishlang_vm/timers"]
|
|
@@ -58,6 +60,7 @@ tishlang_wasm = { path = "../tish_wasm", version = ">=0.1" }
|
|
|
58
60
|
tishlang_runtime = { path = "../tish_runtime", version = ">=0.1" }
|
|
59
61
|
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
60
62
|
tishlang_js_to_tish = { path = "../js_to_tish", version = ">=0.1" }
|
|
63
|
+
tishlang_test = { path = "../tish_test", optional = true, features = ["runner", "fs", "regex", "promise", "watch"] }
|
|
61
64
|
clap = { version = "4.6.0", features = ["derive", "color"] }
|
|
62
65
|
serde_json = "1"
|
|
63
66
|
tishlang_pg = { path = "../tish_pg", version = ">=0.1", optional = true }
|
|
@@ -631,12 +631,96 @@ pub(crate) struct CompileModuleArgs {
|
|
|
631
631
|
pub file: String,
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
#[derive(Parser)]
|
|
635
|
+
pub(crate) struct TestArgs {
|
|
636
|
+
/// File or directory filters (paths / substrings). Omit to discover under the current directory.
|
|
637
|
+
#[arg(num_args = 0.., value_name = "FILTER", help_heading = "Arguments")]
|
|
638
|
+
pub filters: Vec<String>,
|
|
639
|
+
/// Execution backend. Only `vm` runs tests today.
|
|
640
|
+
#[arg(
|
|
641
|
+
long,
|
|
642
|
+
default_value = "vm",
|
|
643
|
+
value_name = "NAME",
|
|
644
|
+
help_heading = "Options"
|
|
645
|
+
)]
|
|
646
|
+
pub backend: String,
|
|
647
|
+
/// Restrict which platform APIs tests may use, as `tish run --feature` does
|
|
648
|
+
/// (omit = all capabilities compiled into this `tish`). Enforced on `--backend vm`.
|
|
649
|
+
#[arg(
|
|
650
|
+
long = "feature",
|
|
651
|
+
value_name = "NAME",
|
|
652
|
+
action = clap::ArgAction::Append,
|
|
653
|
+
help_heading = "Options"
|
|
654
|
+
)]
|
|
655
|
+
pub features: Vec<String>,
|
|
656
|
+
/// Disable AST and bytecode optimizations.
|
|
657
|
+
#[arg(long, help_heading = "Options")]
|
|
658
|
+
pub no_optimize: bool,
|
|
659
|
+
/// Only run tests whose full name matches this regex.
|
|
660
|
+
#[arg(short = 't', long = "test-name-pattern", value_name = "REGEX", help_heading = "Options")]
|
|
661
|
+
pub test_name_pattern: Option<String>,
|
|
662
|
+
/// Per-test timeout in milliseconds (default 5000). Loops are interrupted at the deadline;
|
|
663
|
+
/// a body blocked inside one native call is only caught once it returns.
|
|
664
|
+
#[arg(long, default_value = "5000", value_name = "MS", help_heading = "Options")]
|
|
665
|
+
pub timeout: u64,
|
|
666
|
+
/// Update snapshot files on mismatch (`-u`).
|
|
667
|
+
#[arg(short = 'u', long = "update-snapshots", help_heading = "Options")]
|
|
668
|
+
pub update_snapshots: bool,
|
|
669
|
+
/// Fail on a missing snapshot instead of writing it (also on when `CI` is set).
|
|
670
|
+
#[arg(long, help_heading = "Options")]
|
|
671
|
+
pub ci: bool,
|
|
672
|
+
/// Stop after the first failing test file / suite.
|
|
673
|
+
#[arg(long, help_heading = "Options")]
|
|
674
|
+
pub bail: bool,
|
|
675
|
+
/// Retry failed tests this many times.
|
|
676
|
+
#[arg(long, default_value = "0", value_name = "N", help_heading = "Options")]
|
|
677
|
+
pub retry: u32,
|
|
678
|
+
/// Only run tests marked `.only`.
|
|
679
|
+
#[arg(long, help_heading = "Options")]
|
|
680
|
+
pub only: bool,
|
|
681
|
+
/// Reporter: `console` (default) or `dots`.
|
|
682
|
+
#[arg(long, default_value = "console", value_name = "NAME", help_heading = "Options")]
|
|
683
|
+
pub reporter: String,
|
|
684
|
+
/// Write a JUnit XML report to this path.
|
|
685
|
+
#[arg(long = "reporter-outfile", value_name = "PATH", help_heading = "Options")]
|
|
686
|
+
pub reporter_outfile: Option<String>,
|
|
687
|
+
/// Re-run tests when files under discovery roots change (requires notify).
|
|
688
|
+
#[arg(long, help_heading = "Options")]
|
|
689
|
+
pub watch: bool,
|
|
690
|
+
/// Shuffle test file order.
|
|
691
|
+
#[arg(long, help_heading = "Options")]
|
|
692
|
+
pub randomize: bool,
|
|
693
|
+
/// Seed for `--randomize` (implies randomize).
|
|
694
|
+
#[arg(long, value_name = "N", help_heading = "Options")]
|
|
695
|
+
pub seed: Option<u64>,
|
|
696
|
+
/// Run the suite this many times; any failing pass fails the command (`--rerun-each`).
|
|
697
|
+
#[arg(long = "rerun-each", default_value = "1", value_name = "N", help_heading = "Options")]
|
|
698
|
+
pub rerun_each: u32,
|
|
699
|
+
/// Shard files as `i/n` (1-based index).
|
|
700
|
+
#[arg(long, value_name = "I/N", help_heading = "Options")]
|
|
701
|
+
pub shard: Option<String>,
|
|
702
|
+
/// Only run tests that include this tag (repeatable).
|
|
703
|
+
#[arg(long = "tag", value_name = "TAG", action = clap::ArgAction::Append, help_heading = "Options")]
|
|
704
|
+
pub tags: Vec<String>,
|
|
705
|
+
/// Preload module paths (evaluated before each test file, on the same backend instance).
|
|
706
|
+
#[arg(long = "preload", value_name = "FILE", action = clap::ArgAction::Append, help_heading = "Options")]
|
|
707
|
+
pub preload: Vec<String>,
|
|
708
|
+
/// Collect line coverage via AST instrumentation.
|
|
709
|
+
#[arg(long, help_heading = "Options")]
|
|
710
|
+
pub coverage: bool,
|
|
711
|
+
/// Write `lcov.info` under this directory (implies `--coverage`). Default: `coverage/`.
|
|
712
|
+
#[arg(long = "coverage-dir", value_name = "DIR", help_heading = "Options")]
|
|
713
|
+
pub coverage_dir: Option<String>,
|
|
714
|
+
}
|
|
715
|
+
|
|
634
716
|
#[derive(Subcommand)]
|
|
635
717
|
pub(crate) enum Commands {
|
|
636
718
|
/// Run a Tish file (interpret)
|
|
637
719
|
Run(RunArgs),
|
|
638
720
|
/// Interactive REPL
|
|
639
721
|
Repl(ReplArgs),
|
|
722
|
+
/// Run tests (`*.test.tish`, `*.spec.tish`, …)
|
|
723
|
+
Test(TestArgs),
|
|
640
724
|
/// Build native binary, wasm, wasi, or JavaScript output
|
|
641
725
|
Build(BuildArgs),
|
|
642
726
|
/// Compile a single `.tish` module to one ES module (for Vite dev / HMR); prints to stdout
|
package/crates/tish/src/main.rs
CHANGED
|
@@ -74,7 +74,7 @@ fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
|
|
|
74
74
|
if argv.len() >= 2 {
|
|
75
75
|
let first = argv[1].as_str();
|
|
76
76
|
const SUBCOMMANDS: &[&str] =
|
|
77
|
-
&["run", "repl", "build", "compile-module", "resolve-id", "dump-ast"];
|
|
77
|
+
&["run", "repl", "test", "build", "compile-module", "resolve-id", "dump-ast"];
|
|
78
78
|
let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.contains(&first);
|
|
79
79
|
if looks_like_file {
|
|
80
80
|
argv.insert(1, "run".to_string());
|
|
@@ -120,6 +120,98 @@ fn main() {
|
|
|
120
120
|
tishlang_core::set_process_argv(argv);
|
|
121
121
|
run_file(&a.file, &a.backend, &a.features, a.no_optimize || no_opt_env)
|
|
122
122
|
}
|
|
123
|
+
Some(Commands::Test(a)) => {
|
|
124
|
+
#[cfg(feature = "test-runner")]
|
|
125
|
+
{
|
|
126
|
+
use tishlang_test::{run_tests, TestOptions};
|
|
127
|
+
use tishlang_test::report::ReporterKind;
|
|
128
|
+
|
|
129
|
+
let reporter = match a.reporter.as_str() {
|
|
130
|
+
"dots" => ReporterKind::Dots,
|
|
131
|
+
_ => ReporterKind::Console,
|
|
132
|
+
};
|
|
133
|
+
let (roots, filters) = if a.filters.is_empty() {
|
|
134
|
+
(vec![std::path::PathBuf::from(".")], Vec::new())
|
|
135
|
+
} else {
|
|
136
|
+
// Path-like args that exist become discovery roots; others are substring filters.
|
|
137
|
+
let mut roots = Vec::new();
|
|
138
|
+
let mut filters = Vec::new();
|
|
139
|
+
for f in &a.filters {
|
|
140
|
+
let p = std::path::PathBuf::from(f);
|
|
141
|
+
if p.exists() {
|
|
142
|
+
roots.push(p);
|
|
143
|
+
} else {
|
|
144
|
+
filters.push(f.clone());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if roots.is_empty() {
|
|
148
|
+
roots.push(std::path::PathBuf::from("."));
|
|
149
|
+
}
|
|
150
|
+
(roots, filters)
|
|
151
|
+
};
|
|
152
|
+
let shard = a.shard.as_ref().and_then(|s| {
|
|
153
|
+
let parts: Vec<_> = s.split('/').collect();
|
|
154
|
+
if parts.len() == 2 {
|
|
155
|
+
let i = parts[0].parse().ok()?;
|
|
156
|
+
let n = parts[1].parse().ok()?;
|
|
157
|
+
Some((i, n))
|
|
158
|
+
} else {
|
|
159
|
+
None
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
let backend = a.backend.as_str();
|
|
163
|
+
if backend != "vm" {
|
|
164
|
+
// The interpreter cannot host test callbacks: its closures are bound to the
|
|
165
|
+
// `Evaluator`, which is dropped before the collected suite runs. `--backend
|
|
166
|
+
// interp` used to be accepted and then fail on the simplest test file.
|
|
167
|
+
let hint = if backend == "interp" {
|
|
168
|
+
"`--backend interp` cannot run tests: interpreter closures are bound to the evaluator and cannot be called back after collection"
|
|
169
|
+
} else {
|
|
170
|
+
"`--backend native` is not implemented yet"
|
|
171
|
+
};
|
|
172
|
+
eprintln!("error: unsupported --backend `{backend}` for tish test (use `vm`); {hint}");
|
|
173
|
+
std::process::exit(2);
|
|
174
|
+
}
|
|
175
|
+
let opts = TestOptions {
|
|
176
|
+
roots,
|
|
177
|
+
filters,
|
|
178
|
+
name_pattern: a.test_name_pattern.clone(),
|
|
179
|
+
timeout_ms: a.timeout,
|
|
180
|
+
bail: a.bail,
|
|
181
|
+
retry: a.retry,
|
|
182
|
+
update_snapshots: a.update_snapshots,
|
|
183
|
+
reporter,
|
|
184
|
+
junit_path: a.reporter_outfile.as_ref().map(std::path::PathBuf::from),
|
|
185
|
+
only: a.only,
|
|
186
|
+
backend: a.backend.clone(),
|
|
187
|
+
no_optimize: a.no_optimize || no_opt_env,
|
|
188
|
+
features: a.features.clone(),
|
|
189
|
+
shard,
|
|
190
|
+
randomize: a.randomize || a.seed.is_some(),
|
|
191
|
+
seed: a.seed,
|
|
192
|
+
rerun_each: a.rerun_each,
|
|
193
|
+
tags: a.tags.clone(),
|
|
194
|
+
preload: a.preload.iter().map(std::path::PathBuf::from).collect(),
|
|
195
|
+
ci: a.ci,
|
|
196
|
+
coverage: a.coverage || a.coverage_dir.is_some(),
|
|
197
|
+
coverage_dir: a.coverage_dir.as_ref().map(std::path::PathBuf::from),
|
|
198
|
+
};
|
|
199
|
+
if a.watch {
|
|
200
|
+
let code = tishlang_test::run_tests_watch(opts);
|
|
201
|
+
std::process::exit(code);
|
|
202
|
+
}
|
|
203
|
+
let result = run_tests(opts);
|
|
204
|
+
std::process::exit(result.exit_code);
|
|
205
|
+
}
|
|
206
|
+
#[cfg(not(feature = "test-runner"))]
|
|
207
|
+
{
|
|
208
|
+
let _ = a;
|
|
209
|
+
Err(
|
|
210
|
+
"tish test requires the test-runner feature (included in --features full)"
|
|
211
|
+
.into(),
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
123
215
|
Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env, &a.features),
|
|
124
216
|
Some(Commands::Build(a)) => {
|
|
125
217
|
// `--check warn|error` drives the gradual type checker via the same channel as the
|
|
@@ -108,3 +108,93 @@ fn scope_tish_crate_routes_native() {
|
|
|
108
108
|
"@scope package with tish.crate must NOT be merged as Tish source"
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
// ── #587: two modules exporting the SAME name, flattened into one scope ──────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// `isolate_private_top_level_bindings` renamed colliding PRIVATE top-level names but skipped
|
|
115
|
+
// exported ones, on the reasoning that "exported names stay stable so imports keep resolving".
|
|
116
|
+
// `module_exports` is an indirection from export name to source symbol, so that was stricter than
|
|
117
|
+
// necessary — and leaving the collision produced a silently WRONG program rather than a slower one.
|
|
118
|
+
//
|
|
119
|
+
// The barrel pattern is where it bites: `import { greet as _greet }` + a local `export fn greet`
|
|
120
|
+
// that calls `_greet`. Flattened, that emits two `function greet` declarations, and because JS
|
|
121
|
+
// function declarations HOIST, the second overwrote the first BEFORE `const _greet = greet` ran.
|
|
122
|
+
// `_greet` then pointed at the wrapper and the program recursed until the stack blew.
|
|
123
|
+
//
|
|
124
|
+
// The entry module is excluded from renaming on purpose: its exports are the bundle's public
|
|
125
|
+
// surface, so moving those would change the API the caller sees. Only the dep is renamed here.
|
|
126
|
+
|
|
127
|
+
/// dep exports `greet`; barrel imports it aliased and re-exports its own `greet` wrapper.
|
|
128
|
+
fn setup_barrel() -> tempfile::TempDir {
|
|
129
|
+
let dir = tempfile::tempdir().unwrap();
|
|
130
|
+
let root = dir.path();
|
|
131
|
+
fs::create_dir_all(root.join("dep")).unwrap();
|
|
132
|
+
fs::create_dir_all(root.join("app")).unwrap();
|
|
133
|
+
fs::write(
|
|
134
|
+
root.join("dep/lib.tish"),
|
|
135
|
+
"export fn greet(name) { return \"hi \" + name }\n",
|
|
136
|
+
)
|
|
137
|
+
.unwrap();
|
|
138
|
+
fs::write(
|
|
139
|
+
root.join("app/barrel.tish"),
|
|
140
|
+
"import { greet as _greet } from \"../dep/lib.tish\"\n\
|
|
141
|
+
export fn greet(name) { return _greet(name) }\n",
|
|
142
|
+
)
|
|
143
|
+
.unwrap();
|
|
144
|
+
fs::write(
|
|
145
|
+
root.join("main.tish"),
|
|
146
|
+
"import { greet } from \"./app/barrel.tish\"\nconsole.log(greet(\"world\"))\n",
|
|
147
|
+
)
|
|
148
|
+
.unwrap();
|
|
149
|
+
dir
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn colliding_exported_names_are_isolated_across_modules() {
|
|
154
|
+
let dir = setup_barrel();
|
|
155
|
+
let root = dir.path();
|
|
156
|
+
let modules = resolve_project(&root.join("main.tish"), Some(root)).expect("resolve");
|
|
157
|
+
let merged = merge_modules(modules).expect("merge");
|
|
158
|
+
|
|
159
|
+
// The two `greet` declarations must not both be named `greet` in the flat program — that is the
|
|
160
|
+
// collision, and on the JS target hoisting silently resolves it the wrong way.
|
|
161
|
+
let greet_decls: Vec<&str> = merged
|
|
162
|
+
.program
|
|
163
|
+
.statements
|
|
164
|
+
.iter()
|
|
165
|
+
.filter_map(|s| match s {
|
|
166
|
+
Statement::FunDecl { name, .. } => Some(name.as_ref()),
|
|
167
|
+
_ => None,
|
|
168
|
+
})
|
|
169
|
+
.filter(|n| n.starts_with("greet"))
|
|
170
|
+
.collect();
|
|
171
|
+
assert!(
|
|
172
|
+
greet_decls.len() >= 2,
|
|
173
|
+
"expected both modules' greet fns in the merged program, got {:?}",
|
|
174
|
+
greet_decls
|
|
175
|
+
);
|
|
176
|
+
let bare = greet_decls.iter().filter(|n| **n == "greet").count();
|
|
177
|
+
assert!(
|
|
178
|
+
bare <= 1,
|
|
179
|
+
"two top-level `greet` declarations survived the merge — on the JS target the later one \
|
|
180
|
+
hoists over the former and the barrel's alias captures the wrapper (infinite recursion). \
|
|
181
|
+
Got {:?}",
|
|
182
|
+
greet_decls
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// The alias must bind to the DEP's symbol, not to the local wrapper.
|
|
186
|
+
let alias_target = merged.program.statements.iter().find_map(|s| match s {
|
|
187
|
+
Statement::VarDecl {
|
|
188
|
+
name,
|
|
189
|
+
init: Some(Expr::Ident { name: src, .. }),
|
|
190
|
+
..
|
|
191
|
+
} if name.as_ref() == "_greet" => Some(src.to_string()),
|
|
192
|
+
_ => None,
|
|
193
|
+
});
|
|
194
|
+
if let Some(target) = alias_target {
|
|
195
|
+
assert_ne!(
|
|
196
|
+
target, "greet",
|
|
197
|
+
"`_greet` must not bind to the ambiguous bare `greet`; it should name the dep's symbol"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|