@tishlang/tish-format 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.
Files changed (42) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/tish/Cargo.toml +4 -1
  4. package/crates/tish/src/cli_help.rs +84 -0
  5. package/crates/tish/src/main.rs +93 -1
  6. package/crates/tish/tests/scope_merge.rs +90 -0
  7. package/crates/tish/tests/test_runner.rs +673 -0
  8. package/crates/tish_bytecode/src/compiler.rs +25 -4
  9. package/crates/tish_compile/src/check.rs +178 -0
  10. package/crates/tish_compile/src/codegen.rs +383 -63
  11. package/crates/tish_compile/src/lib.rs +33 -1
  12. package/crates/tish_compile/src/resolve.rs +121 -19
  13. package/crates/tish_compile/src/types.rs +35 -1
  14. package/crates/tish_compile/tests/call_arity_641.rs +104 -0
  15. package/crates/tish_core/src/lib.rs +103 -11
  16. package/crates/tish_eval/src/eval.rs +16 -0
  17. package/crates/tish_eval/src/value_convert.rs +10 -0
  18. package/crates/tish_fmt/Cargo.toml +1 -1
  19. package/crates/tish_test/Cargo.toml +47 -0
  20. package/crates/tish_test/src/assert.rs +435 -0
  21. package/crates/tish_test/src/config.rs +85 -0
  22. package/crates/tish_test/src/coverage.rs +237 -0
  23. package/crates/tish_test/src/deep_equal.rs +261 -0
  24. package/crates/tish_test/src/discovery.rs +70 -0
  25. package/crates/tish_test/src/expect.rs +977 -0
  26. package/crates/tish_test/src/instrument.rs +457 -0
  27. package/crates/tish_test/src/isolation.rs +14 -0
  28. package/crates/tish_test/src/lib.rs +22 -0
  29. package/crates/tish_test/src/load.rs +152 -0
  30. package/crates/tish_test/src/mocks.rs +270 -0
  31. package/crates/tish_test/src/module.rs +318 -0
  32. package/crates/tish_test/src/registry.rs +235 -0
  33. package/crates/tish_test/src/report.rs +312 -0
  34. package/crates/tish_test/src/runner.rs +797 -0
  35. package/crates/tish_test/src/snapshots.rs +197 -0
  36. package/crates/tish_vm/src/vm.rs +17 -14
  37. package/package.json +1 -1
  38. package/platform/darwin-arm64/tish-fmt +0 -0
  39. package/platform/darwin-x64/tish-fmt +0 -0
  40. package/platform/linux-arm64/tish-fmt +0 -0
  41. package/platform/linux-x64/tish-fmt +0 -0
  42. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,457 @@
1
+ //! AST instrumentation for line coverage — rewrite programs in `tish_test` only.
2
+
3
+ use std::path::Path;
4
+ use std::sync::Arc;
5
+
6
+ use tishlang_ast::{
7
+ ArrowBody, CallArg, ExportDeclaration, Expr, Literal, Program, Span, Statement,
8
+ };
9
+
10
+ use crate::coverage::{self, hit_global_name};
11
+
12
+ /// Instrument every resolved module's AST so execution of any backend records line hits.
13
+ pub fn instrument_program(program: &mut Program, path: &Path) {
14
+ if !coverage::is_enabled() {
15
+ return;
16
+ }
17
+ let file_id = coverage::intern_file(path);
18
+ program.statements = instrument_stmts(std::mem::take(&mut program.statements), file_id);
19
+ }
20
+
21
+ fn instrument_stmts(stmts: Vec<Statement>, file_id: u32) -> Vec<Statement> {
22
+ let mut out = Vec::with_capacity(stmts.len() * 2);
23
+ for stmt in stmts {
24
+ match stmt {
25
+ Statement::TypeAlias { .. }
26
+ | Statement::DeclareVar { .. }
27
+ | Statement::DeclareFun { .. }
28
+ | Statement::Import { .. } => {
29
+ out.push(stmt);
30
+ }
31
+ Statement::Export { declaration, span } => {
32
+ let declaration = Box::new(match *declaration {
33
+ ExportDeclaration::Named(inner) => {
34
+ ExportDeclaration::Named(Box::new(instrument_stmt(*inner, file_id)))
35
+ }
36
+ ExportDeclaration::Default(expr) => {
37
+ ExportDeclaration::Default(instrument_expr(expr, file_id))
38
+ }
39
+ other => other,
40
+ });
41
+ out.push(hit_stmt(file_id, span.start.0 as u32));
42
+ out.push(Statement::Export { declaration, span });
43
+ }
44
+ Statement::Multi { statements, span } => {
45
+ out.push(Statement::Multi {
46
+ statements: instrument_stmts(statements, file_id),
47
+ span,
48
+ });
49
+ }
50
+ Statement::Block { statements, span } => {
51
+ out.push(Statement::Block {
52
+ statements: instrument_stmts(statements, file_id),
53
+ span,
54
+ });
55
+ }
56
+ other => {
57
+ let line = other.span().start.0 as u32;
58
+ out.push(hit_stmt(file_id, line));
59
+ out.push(instrument_stmt(other, file_id));
60
+ }
61
+ }
62
+ }
63
+ out
64
+ }
65
+
66
+ fn instrument_stmt(stmt: Statement, file_id: u32) -> Statement {
67
+ match stmt {
68
+ Statement::Block { statements, span } => Statement::Block {
69
+ statements: instrument_stmts(statements, file_id),
70
+ span,
71
+ },
72
+ Statement::Multi { statements, span } => Statement::Multi {
73
+ statements: instrument_stmts(statements, file_id),
74
+ span,
75
+ },
76
+ Statement::If {
77
+ cond,
78
+ then_branch,
79
+ else_branch,
80
+ span,
81
+ } => Statement::If {
82
+ cond: instrument_expr(cond, file_id),
83
+ then_branch: Box::new(instrument_stmt(*then_branch, file_id)),
84
+ else_branch: else_branch.map(|b| Box::new(instrument_stmt(*b, file_id))),
85
+ span,
86
+ },
87
+ Statement::While { cond, body, span } => Statement::While {
88
+ cond: instrument_expr(cond, file_id),
89
+ body: Box::new(instrument_stmt(*body, file_id)),
90
+ span,
91
+ },
92
+ Statement::DoWhile { body, cond, span } => Statement::DoWhile {
93
+ body: Box::new(instrument_stmt(*body, file_id)),
94
+ cond: instrument_expr(cond, file_id),
95
+ span,
96
+ },
97
+ Statement::For {
98
+ init,
99
+ cond,
100
+ update,
101
+ body,
102
+ span,
103
+ } => Statement::For {
104
+ init: init.map(|i| Box::new(instrument_stmt(*i, file_id))),
105
+ cond: cond.map(|c| instrument_expr(c, file_id)),
106
+ update: update.map(|u| instrument_expr(u, file_id)),
107
+ body: Box::new(instrument_stmt(*body, file_id)),
108
+ span,
109
+ },
110
+ Statement::ForOf {
111
+ name,
112
+ name_span,
113
+ iterable,
114
+ body,
115
+ span,
116
+ } => Statement::ForOf {
117
+ name,
118
+ name_span,
119
+ iterable: instrument_expr(iterable, file_id),
120
+ body: Box::new(instrument_stmt(*body, file_id)),
121
+ span,
122
+ },
123
+ Statement::ForIn {
124
+ name,
125
+ name_span,
126
+ object,
127
+ body,
128
+ span,
129
+ } => Statement::ForIn {
130
+ name,
131
+ name_span,
132
+ object: instrument_expr(object, file_id),
133
+ body: Box::new(instrument_stmt(*body, file_id)),
134
+ span,
135
+ },
136
+ Statement::FunDecl {
137
+ async_,
138
+ name,
139
+ name_span,
140
+ params,
141
+ rest_param,
142
+ return_type,
143
+ body,
144
+ span,
145
+ } => Statement::FunDecl {
146
+ async_,
147
+ name,
148
+ name_span,
149
+ params,
150
+ rest_param,
151
+ return_type,
152
+ body: Box::new(instrument_stmt(*body, file_id)),
153
+ span,
154
+ },
155
+ Statement::Try {
156
+ body,
157
+ catch_param,
158
+ catch_param_span,
159
+ catch_body,
160
+ finally_body,
161
+ span,
162
+ } => Statement::Try {
163
+ body: Box::new(instrument_stmt(*body, file_id)),
164
+ catch_param,
165
+ catch_param_span,
166
+ catch_body: catch_body.map(|b| Box::new(instrument_stmt(*b, file_id))),
167
+ finally_body: finally_body.map(|b| Box::new(instrument_stmt(*b, file_id))),
168
+ span,
169
+ },
170
+ Statement::Switch {
171
+ expr,
172
+ cases,
173
+ default_body,
174
+ span,
175
+ } => Statement::Switch {
176
+ expr: instrument_expr(expr, file_id),
177
+ cases: cases
178
+ .into_iter()
179
+ .map(|(c, body)| {
180
+ (
181
+ c.map(|e| instrument_expr(e, file_id)),
182
+ instrument_stmts(body, file_id),
183
+ )
184
+ })
185
+ .collect(),
186
+ default_body: default_body.map(|b| instrument_stmts(b, file_id)),
187
+ span,
188
+ },
189
+ Statement::ExprStmt { expr, span } => Statement::ExprStmt {
190
+ expr: instrument_expr(expr, file_id),
191
+ span,
192
+ },
193
+ Statement::VarDecl {
194
+ name,
195
+ name_span,
196
+ mutable,
197
+ type_ann,
198
+ init,
199
+ span,
200
+ } => Statement::VarDecl {
201
+ name,
202
+ name_span,
203
+ mutable,
204
+ type_ann,
205
+ init: init.map(|e| instrument_expr(e, file_id)),
206
+ span,
207
+ },
208
+ Statement::VarDeclDestructure {
209
+ pattern,
210
+ mutable,
211
+ init,
212
+ span,
213
+ } => Statement::VarDeclDestructure {
214
+ pattern,
215
+ mutable,
216
+ init: instrument_expr(init, file_id),
217
+ span,
218
+ },
219
+ Statement::Return { value, span } => Statement::Return {
220
+ value: value.map(|e| instrument_expr(e, file_id)),
221
+ span,
222
+ },
223
+ Statement::Throw { value, span } => Statement::Throw {
224
+ value: instrument_expr(value, file_id),
225
+ span,
226
+ },
227
+ other => other,
228
+ }
229
+ }
230
+
231
+ fn instrument_expr(expr: Expr, file_id: u32) -> Expr {
232
+ match expr {
233
+ Expr::ArrowFunction {
234
+ async_,
235
+ params,
236
+ body,
237
+ span,
238
+ } => Expr::ArrowFunction {
239
+ async_,
240
+ params,
241
+ body: match body {
242
+ ArrowBody::Expr(e) => ArrowBody::Expr(Box::new(instrument_expr(*e, file_id))),
243
+ ArrowBody::Block(b) => ArrowBody::Block(Box::new(instrument_stmt(*b, file_id))),
244
+ },
245
+ span,
246
+ },
247
+ Expr::Call { callee, args, span } => Expr::Call {
248
+ callee: Box::new(instrument_expr(*callee, file_id)),
249
+ args: args
250
+ .into_iter()
251
+ .map(|a| match a {
252
+ CallArg::Expr(e) => CallArg::Expr(instrument_expr(e, file_id)),
253
+ CallArg::Spread(e) => CallArg::Spread(instrument_expr(e, file_id)),
254
+ })
255
+ .collect(),
256
+ span,
257
+ },
258
+ Expr::Binary {
259
+ left,
260
+ op,
261
+ right,
262
+ span,
263
+ } => Expr::Binary {
264
+ left: Box::new(instrument_expr(*left, file_id)),
265
+ op,
266
+ right: Box::new(instrument_expr(*right, file_id)),
267
+ span,
268
+ },
269
+ Expr::Unary { op, operand, span } => Expr::Unary {
270
+ op,
271
+ operand: Box::new(instrument_expr(*operand, file_id)),
272
+ span,
273
+ },
274
+ Expr::Member {
275
+ object,
276
+ prop,
277
+ optional,
278
+ span,
279
+ } => Expr::Member {
280
+ object: Box::new(instrument_expr(*object, file_id)),
281
+ prop,
282
+ optional,
283
+ span,
284
+ },
285
+ Expr::Assign { name, value, span } => Expr::Assign {
286
+ name,
287
+ value: Box::new(instrument_expr(*value, file_id)),
288
+ span,
289
+ },
290
+ Expr::Array { elements, span } => Expr::Array {
291
+ elements: elements
292
+ .into_iter()
293
+ .map(|el| match el {
294
+ tishlang_ast::ArrayElement::Expr(e) => {
295
+ tishlang_ast::ArrayElement::Expr(instrument_expr(e, file_id))
296
+ }
297
+ tishlang_ast::ArrayElement::Spread(e) => {
298
+ tishlang_ast::ArrayElement::Spread(instrument_expr(e, file_id))
299
+ }
300
+ })
301
+ .collect(),
302
+ span,
303
+ },
304
+ Expr::Object { props, span } => Expr::Object {
305
+ props: props
306
+ .into_iter()
307
+ .map(|p| match p {
308
+ tishlang_ast::ObjectProp::KeyValue(k, v, s) => {
309
+ tishlang_ast::ObjectProp::KeyValue(k, instrument_expr(v, file_id), s)
310
+ }
311
+ tishlang_ast::ObjectProp::Spread(e) => {
312
+ tishlang_ast::ObjectProp::Spread(instrument_expr(e, file_id))
313
+ }
314
+ })
315
+ .collect(),
316
+ span,
317
+ },
318
+ // Every remaining expression form that can nest an arrow function. Missing one does not
319
+ // fail loudly: the arrow's body is simply never marked executable, so it drops out of
320
+ // the denominator and inflates the reported percentage.
321
+ Expr::New { callee, args, span } => Expr::New {
322
+ callee: Box::new(instrument_expr(*callee, file_id)),
323
+ args: args
324
+ .into_iter()
325
+ .map(|a| match a {
326
+ CallArg::Expr(e) => CallArg::Expr(instrument_expr(e, file_id)),
327
+ CallArg::Spread(e) => CallArg::Spread(instrument_expr(e, file_id)),
328
+ })
329
+ .collect(),
330
+ span,
331
+ },
332
+ Expr::Index {
333
+ object,
334
+ index,
335
+ optional,
336
+ span,
337
+ } => Expr::Index {
338
+ object: Box::new(instrument_expr(*object, file_id)),
339
+ index: Box::new(instrument_expr(*index, file_id)),
340
+ optional,
341
+ span,
342
+ },
343
+ Expr::Conditional {
344
+ cond,
345
+ then_branch,
346
+ else_branch,
347
+ span,
348
+ } => Expr::Conditional {
349
+ cond: Box::new(instrument_expr(*cond, file_id)),
350
+ then_branch: Box::new(instrument_expr(*then_branch, file_id)),
351
+ else_branch: Box::new(instrument_expr(*else_branch, file_id)),
352
+ span,
353
+ },
354
+ Expr::NullishCoalesce { left, right, span } => Expr::NullishCoalesce {
355
+ left: Box::new(instrument_expr(*left, file_id)),
356
+ right: Box::new(instrument_expr(*right, file_id)),
357
+ span,
358
+ },
359
+ Expr::Await { operand, span } => Expr::Await {
360
+ operand: Box::new(instrument_expr(*operand, file_id)),
361
+ span,
362
+ },
363
+ Expr::TypeOf { operand, span } => Expr::TypeOf {
364
+ operand: Box::new(instrument_expr(*operand, file_id)),
365
+ span,
366
+ },
367
+ Expr::Delete { target, span } => Expr::Delete {
368
+ target: Box::new(instrument_expr(*target, file_id)),
369
+ span,
370
+ },
371
+ Expr::CompoundAssign {
372
+ name,
373
+ op,
374
+ value,
375
+ span,
376
+ } => Expr::CompoundAssign {
377
+ name,
378
+ op,
379
+ value: Box::new(instrument_expr(*value, file_id)),
380
+ span,
381
+ },
382
+ Expr::LogicalAssign {
383
+ name,
384
+ op,
385
+ value,
386
+ span,
387
+ } => Expr::LogicalAssign {
388
+ name,
389
+ op,
390
+ value: Box::new(instrument_expr(*value, file_id)),
391
+ span,
392
+ },
393
+ Expr::MemberAssign {
394
+ object,
395
+ prop,
396
+ value,
397
+ span,
398
+ } => Expr::MemberAssign {
399
+ object: Box::new(instrument_expr(*object, file_id)),
400
+ prop,
401
+ value: Box::new(instrument_expr(*value, file_id)),
402
+ span,
403
+ },
404
+ Expr::IndexAssign {
405
+ object,
406
+ index,
407
+ value,
408
+ span,
409
+ } => Expr::IndexAssign {
410
+ object: Box::new(instrument_expr(*object, file_id)),
411
+ index: Box::new(instrument_expr(*index, file_id)),
412
+ value: Box::new(instrument_expr(*value, file_id)),
413
+ span,
414
+ },
415
+ Expr::TemplateLiteral {
416
+ quasis,
417
+ exprs,
418
+ span,
419
+ } => Expr::TemplateLiteral {
420
+ quasis,
421
+ exprs: exprs
422
+ .into_iter()
423
+ .map(|e| instrument_expr(e, file_id))
424
+ .collect(),
425
+ span,
426
+ },
427
+ // Literals, identifiers, increments/decrements and JSX carry no nested arrow bodies
428
+ // that statement-level instrumentation would otherwise miss.
429
+ other => other,
430
+ }
431
+ }
432
+
433
+ fn hit_stmt(file_id: u32, line: u32) -> Statement {
434
+ coverage::mark_executable(file_id, line);
435
+ let span = Span {
436
+ start: (line as usize, 1),
437
+ end: (line as usize, 1),
438
+ };
439
+ let name: Arc<str> = Arc::from(hit_global_name());
440
+ Statement::ExprStmt {
441
+ expr: Expr::Call {
442
+ callee: Box::new(Expr::Ident { name, span }),
443
+ args: vec![
444
+ CallArg::Expr(Expr::Literal {
445
+ value: Literal::Number(file_id as f64),
446
+ span,
447
+ }),
448
+ CallArg::Expr(Expr::Literal {
449
+ value: Literal::Number(line as f64),
450
+ span,
451
+ }),
452
+ ],
453
+ span,
454
+ },
455
+ span,
456
+ }
457
+ }
@@ -0,0 +1,14 @@
1
+ //! Per-file isolation hooks for multi-file `tish test` runs.
2
+ //!
3
+ //! Clears pending throws and the mock registry between files. Timers / HTTP static routes /
4
+ //! sockets are **not** reset in-process — suites that leak host state should use separate
5
+ //! processes.
6
+
7
+ use tishlang_core::take_pending_throw;
8
+
9
+ /// Reset host state between test files in the same process.
10
+ pub fn reset_between_files() {
11
+ let _ = take_pending_throw();
12
+ // Otherwise `restoreAllMocks()` in one file drains handles registered by another.
13
+ crate::mocks::reset_mock_registry();
14
+ }
@@ -0,0 +1,22 @@
1
+ //! Native `tish test` runner, Bun/Jest-shaped `tish:test`, and Node-mirrored `tish:assert`.
2
+
3
+ pub mod assert;
4
+ pub mod config;
5
+ pub mod coverage;
6
+ pub mod deep_equal;
7
+ pub mod discovery;
8
+ pub mod expect;
9
+ pub mod instrument;
10
+ pub mod isolation;
11
+ pub mod mocks;
12
+ pub mod module;
13
+ pub mod registry;
14
+ pub mod report;
15
+ pub mod runner;
16
+ pub mod snapshots;
17
+
18
+ #[cfg(feature = "runner")]
19
+ pub mod load;
20
+
21
+ pub use module::{assert_module, test_module};
22
+ pub use runner::{run_tests, run_tests_watch, TestOptions, TestRunResult};
@@ -0,0 +1,152 @@
1
+ //! Load a test file into the suite registry (requires `runner` feature).
2
+
3
+ use std::collections::HashSet;
4
+ use std::path::{Path, PathBuf};
5
+ use std::sync::Arc;
6
+
7
+ use crate::coverage::{self, hit_global_name, hit_value};
8
+ use crate::instrument::instrument_program;
9
+ use crate::module::{assert_module, test_module};
10
+ use crate::registry::{begin_collect, end_collect, take_collect_errors, SuiteNode};
11
+
12
+ /// A loaded file plus any errors thrown while collecting its suites.
13
+ pub struct LoadedSuite {
14
+ pub suite: SuiteNode,
15
+ /// Throws that escaped a `describe` body. These silently truncate registration, so the
16
+ /// runner reports them as failures instead of letting the file look green.
17
+ pub collect_errors: Vec<String>,
18
+ }
19
+
20
+ /// Resolve, merge, optimize, and execute `path` so `describe`/`test` callbacks register
21
+ /// into the collect-then-run suite tree. Suppresses printing the program's last value.
22
+ ///
23
+ /// `preload` modules run first **on the same VM instance**, so anything they define or
24
+ /// register (globals, `beforeEach` hooks) is visible to the test file. `features` restricts VM
25
+ /// capabilities exactly as `tish run --feature` does; an empty list means "everything linked
26
+ /// into this binary".
27
+ pub fn load_and_collect_tests(
28
+ path: &Path,
29
+ preload: &[PathBuf],
30
+ backend: &str,
31
+ no_optimize: bool,
32
+ features: &[String],
33
+ ) -> Result<LoadedSuite, String> {
34
+ let path = path
35
+ .canonicalize()
36
+ .map_err(|e| format!("Cannot resolve {}: {}", path.display(), e))?;
37
+
38
+ begin_collect(&path.to_string_lossy());
39
+
40
+ let mut programs = Vec::with_capacity(preload.len() + 1);
41
+ for pre in preload {
42
+ let pre = pre
43
+ .canonicalize()
44
+ .map_err(|e| format!("Cannot resolve preload {}: {}", pre.display(), e))?;
45
+ let program = compile_program(&pre, no_optimize)
46
+ .map_err(|e| format!("Error loading preload {}: {}", pre.display(), e))?;
47
+ programs.push((pre, program));
48
+ }
49
+ programs.push((path.clone(), compile_program(&path, no_optimize)?));
50
+
51
+ let result = match backend {
52
+ "vm" => run_vm(&programs, features),
53
+ other => {
54
+ return Err(format!(
55
+ "unsupported --backend `{other}` for tish test (use `vm`)"
56
+ ));
57
+ }
58
+ };
59
+
60
+ let suite = end_collect();
61
+ let collect_errors = take_collect_errors();
62
+ result?;
63
+ Ok(LoadedSuite {
64
+ suite,
65
+ collect_errors,
66
+ })
67
+ }
68
+
69
+ fn compile_program(path: &Path, no_optimize: bool) -> Result<tishlang_ast::Program, String> {
70
+ let project_root = path.parent().and_then(|p| {
71
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
72
+ p.parent()
73
+ } else {
74
+ Some(p)
75
+ }
76
+ });
77
+
78
+ let mut modules = tishlang_compile::resolve_project(path, project_root)?;
79
+ tishlang_compile::detect_cycles(&modules)?;
80
+
81
+ // Instrument each module's AST in this crate so vm / interp / future backends share hits.
82
+ // A module reached from more than one test file must only be instrumented once, or its
83
+ // statements would be double-counted (and re-wrapped) on the second load.
84
+ if coverage::is_enabled() {
85
+ for m in &mut modules {
86
+ if coverage::mark_instrumented(&m.path) {
87
+ instrument_program(&mut m.program, &m.path);
88
+ }
89
+ }
90
+ }
91
+
92
+ let prog = tishlang_compile::merge_modules(modules)?.program;
93
+ Ok(if no_optimize {
94
+ prog
95
+ } else {
96
+ tishlang_opt::optimize(&prog)
97
+ })
98
+ }
99
+
100
+ /// VM capability set, matching `tish run`: empty `--feature` means every capability linked
101
+ /// into this binary; otherwise exactly the requested set (`full` expands).
102
+ fn vm_capabilities(features: &[String]) -> HashSet<String> {
103
+ if features.is_empty() {
104
+ return tishlang_vm::all_compiled_capabilities();
105
+ }
106
+ let mut out = HashSet::new();
107
+ for s in features {
108
+ for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
109
+ if part == "full" {
110
+ for name in ["http", "timers", "fs", "process", "regex", "ws", "tty"] {
111
+ out.insert(name.to_string());
112
+ }
113
+ } else {
114
+ out.insert(part.to_string());
115
+ }
116
+ }
117
+ }
118
+ out
119
+ }
120
+
121
+ fn run_vm(
122
+ programs: &[(PathBuf, tishlang_ast::Program)],
123
+ features: &[String],
124
+ ) -> Result<(), String> {
125
+ let mut vm = tishlang_vm::Vm::with_capabilities(vm_capabilities(features));
126
+ // `node:assert` / `assert/strict` are normalized to `tish:assert` during resolution, so
127
+ // only the canonical specs need registering.
128
+ vm.register_native_module("tish:test", test_module());
129
+ vm.register_native_module("tish:assert", assert_module());
130
+ if coverage::is_enabled() {
131
+ vm.set_global(Arc::from(hit_global_name()), hit_value());
132
+ }
133
+
134
+ for (path, program) in programs {
135
+ let source_arc: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
136
+ let mut chunk = tishlang_bytecode::compile_with_source(program, Some(source_arc.clone()))
137
+ .map_err(|e| e.to_string())?;
138
+ chunk.source = Some(source_arc);
139
+
140
+ vm.run_with_options(&chunk, false)
141
+ .map_err(|e| format!("Error loading {}: {}", path.display(), e))?;
142
+ if tishlang_core::has_pending_throw() {
143
+ let err = tishlang_core::take_pending_throw().unwrap_or(tishlang_core::Value::Null);
144
+ return Err(format!(
145
+ "Error loading {}: {}",
146
+ path.display(),
147
+ err.to_display_string()
148
+ ));
149
+ }
150
+ }
151
+ Ok(())
152
+ }