@tishlang/tish 1.13.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +61 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
package/Cargo.toml CHANGED
@@ -21,6 +21,8 @@ members = [
21
21
  "crates/tish_compiler_wasm",
22
22
  "crates/tish_vm",
23
23
  "crates/tish_llvm",
24
+ "crates/tish_ffi",
25
+ "examples/ffi/mathext",
24
26
 
25
27
  "crates/tish_native",
26
28
  "crates/tish_wasm",
package/bin/tish CHANGED
Binary file
@@ -477,6 +477,7 @@ fn convert_bin_op(op: &oxc::ast::ast::BinaryOperator) -> Result<BinOp, ConvertEr
477
477
  oxc::ast::ast::BinaryOperator::BitwiseXOR => BinOp::BitXor,
478
478
  oxc::ast::ast::BinaryOperator::ShiftLeft => BinOp::Shl,
479
479
  oxc::ast::ast::BinaryOperator::ShiftRight => BinOp::Shr,
480
+ oxc::ast::ast::BinaryOperator::ShiftRightZeroFill => BinOp::UShr,
480
481
  _ => {
481
482
  return Err(ConvertError::new(ConvertErrorKind::Unsupported {
482
483
  what: format!("binary operator: {op:?}"),
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.13.2"
3
+ version = "2.0.1"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -16,10 +16,15 @@ path = "src/main.rs"
16
16
  # (see workspace `justfile`). `tish run --feature …` / `tish build --feature …` gate what runs
17
17
  # or what gets linked into a *native output* binary — they are not the primary way to ship a
18
18
  # “full” vs “empty” CLI.
19
- default = ["full"]
19
+ default = ["full", "fast-alloc"]
20
20
  # Alias: same dependency edges as `default` (kept for `--features full` in CI and docs).
21
21
  # Includes `pg` so `tish run` can resolve `import … from '@tishlang/pg'` (`cargo:tish_pg`) on the bytecode VM.
22
- full = ["http", "fs", "process", "regex", "ws", "timers", "pg"]
22
+ full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg"]
23
+ # Fast allocator (mimalloc) as the process `#[global_allocator]`. tish's object/array/string workloads
24
+ # are allocation-bound (a sampling profile of object-heavy code spends ~14%+ in system malloc/free);
25
+ # mimalloc is markedly faster for the many-small-allocations pattern — the same reason JSC ships bmalloc.
26
+ # Semantically transparent (just a faster malloc); `--no-default-features` drops it for the system allocator.
27
+ fast-alloc = ["dep:mimalloc"]
23
28
  # Opt-out: build without Postgres (`cargo build -p tishlang --no-default-features --features http,...`).
24
29
  pg = ["dep:tishlang_pg"]
25
30
  # Individual capability flags
@@ -29,6 +34,7 @@ fs = ["tishlang_eval/fs", "tishlang_runtime/fs", "tishlang_compil
29
34
  process = ["tishlang_eval/process", "tishlang_runtime/process", "tishlang_compile/process", "tishlang_vm/process"]
30
35
  regex = ["tishlang_eval/regex", "tishlang_runtime/regex", "tishlang_compile/regex", "tishlang_vm/regex"]
31
36
  ws = ["tishlang_eval/ws", "tishlang_runtime/ws", "tishlang_compile/ws", "tishlang_vm/ws"]
37
+ tty = ["tishlang_eval/tty", "tishlang_runtime/tty", "tishlang_compile/tty", "tishlang_vm/tty"]
32
38
  # GPU compute — Apple Silicon only
33
39
  [dependencies]
34
40
  rustyline = { version = "17", features = ["with-file-history"] }
@@ -41,6 +47,7 @@ tishlang_compile_js = { path = "../tish_compile_js", version = ">=0.1" }
41
47
  tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
42
48
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
43
49
  tishlang_vm = { path = "../tish_vm", version = ">=0.1" }
50
+ tishlang_ffi = { path = "../tish_ffi", version = ">=0.1" }
44
51
  tishlang_native = { path = "../tish_native", version = ">=0.1" }
45
52
  tishlang_llvm = { path = "../tish_llvm", version = ">=0.1" }
46
53
  tishlang_wasm = { path = "../tish_wasm", version = ">=0.1" }
@@ -49,6 +56,7 @@ tishlang_core = { path = "../tish_core", version = ">=0.1" }
49
56
  tishlang_js_to_tish = { path = "../js_to_tish", version = ">=0.1" }
50
57
  clap = { version = "4.6.0", features = ["derive", "color"] }
51
58
  tishlang_pg = { path = "../tish_pg", version = ">=0.1", optional = true }
59
+ mimalloc = { version = "0.1", optional = true }
52
60
 
53
61
  [dev-dependencies]
54
62
  rayon = "1.11"
@@ -0,0 +1,21 @@
1
+ //! Export the `tish_value_*` C-ABI accessors (from `tishlang_ffi`) in the `tish` binary's dynamic
2
+ //! symbol table, so a `dlopen`'d native extension (`ffi:`) can resolve them at load time. This is
3
+ //! the **decoupled** FFI model: the extension declares the accessors `extern "C"` and does NOT link
4
+ //! `tish_core`, so there is a single value representation and no host/extension layout matching.
5
+ //! Without this flag the host's accessor symbols aren't visible to the loaded library.
6
+
7
+ fn main() {
8
+ let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
9
+ match target_os.as_str() {
10
+ // macOS/iOS: export all global symbols (so the two-level-namespace binary exposes them).
11
+ "macos" | "ios" => {
12
+ println!("cargo:rustc-link-arg-bins=-Wl,-export_dynamic");
13
+ }
14
+ // Windows PE export tables work differently; the host-export FFI model isn't wired there yet.
15
+ "windows" => {}
16
+ // Linux/BSD: -rdynamic puts all symbols in the dynamic table.
17
+ _ => {
18
+ println!("cargo:rustc-link-arg-bins=-rdynamic");
19
+ }
20
+ }
21
+ }
@@ -72,8 +72,7 @@ fn write_tish_banner_frame(out: &mut impl Write, reveal_t: f32, color_frame: usi
72
72
  let visible = ((len as f32) * reveal_t).round() as usize;
73
73
  let visible = visible.min(len);
74
74
 
75
- for col in 0..len {
76
- let ch = chars[col];
75
+ for (col, &ch) in chars.iter().enumerate() {
77
76
  if col >= visible || ch == ' ' {
78
77
  let _ = write!(out, " ");
79
78
  } else {
@@ -321,6 +320,8 @@ pub fn cli_after_help() -> String {
321
320
  {oh}Environment variables:{r}
322
321
  {t}TISH_NO_OPTIMIZE=1{r}
323
322
  Disable AST and bytecode optimizations for run/build
323
+ {t}TISH_IGNORE_INDENT=1{r}
324
+ Ignore indentation syntax: parse blocks by braces only (debug nested-block transpilation)
324
325
 
325
326
  See {t}tish run --help{r} and {t}tish build --help{r} for backend and feature options."
326
327
  )
@@ -348,8 +349,10 @@ fn capabilities_section(oh: &str, t: &str, r: &str) -> String {
348
349
  RegExp
349
350
  {t}ws{r}
350
351
  WebSocket client / server
352
+ {t}tty{r}
353
+ Interactive terminal: raw mode, key/resize events, size, alt screen (`import from \"tish:tty\"`)
351
354
  {t}full{r}
352
- All of the above (http, timers, fs, process, regex, ws)
355
+ All of the above (http, timers, fs, process, regex, ws, tty)
353
356
 
354
357
  Omit --feature to allow every capability compiled into this `tish` binary; pass flags to restrict what scripts may use. The CLI is normally built with all of them (Cargo default on `tishlang`)."
355
358
  )
@@ -429,8 +432,10 @@ pub fn build_after_help() -> String {
429
432
  RegExp
430
433
  {t}ws{r}
431
434
  WebSocket client / server
435
+ {t}tty{r}
436
+ Interactive terminal: raw mode, key/resize events, size, alt screen (`import from \"tish:tty\"`)
432
437
  {t}full{r}
433
- All of the above (http, timers, fs, process, regex, ws)
438
+ All of the above (http, timers, fs, process, regex, ws, tty)
434
439
 
435
440
  For `--target native`, these choose what is linked into the **output** executable (omit = same set as this `tish` binary was built with). Minimal native outputs still use a full `tish` CLI unless you built it with `cargo build -p tishlang --no-default-features`."
436
441
  )
@@ -543,6 +548,9 @@ pub(crate) struct BuildArgs {
543
548
  /// For `--target js` project builds: emit `OUTPUT.js.map` and `//# sourceMappingURL=…` so JS/TS tools can jump to original `.tish` (implies `--no-optimize` for that build).
544
549
  #[arg(long, help_heading = "Options")]
545
550
  pub source_map: bool,
551
+ /// Run the gradual type checker: `warn` prints `line:col` type diagnostics; `error` also fails the build on them. (Equivalent to setting `TISH_CHECK`.)
552
+ #[arg(long, value_name = "MODE", help_heading = "Options")]
553
+ pub check: Option<String>,
546
554
  /// Entry `.tish` file (or `.js` where supported).
547
555
  #[arg(required = true, value_name = "FILE", help_heading = "Arguments")]
548
556
  pub file: String,
@@ -561,5 +569,8 @@ pub(crate) enum Commands {
561
569
  DumpAst {
562
570
  #[arg(required = true, value_name = "FILE", help_heading = "Arguments")]
563
571
  file: String,
572
+ /// Ignore indentation syntax: parse blocks by braces only (same as TISH_IGNORE_INDENT=1).
573
+ #[arg(long, help_heading = "Options")]
574
+ ignore_indent: bool,
564
575
  },
565
576
  }
@@ -1,5 +1,14 @@
1
1
  //! Tish CLI - run, REPL, build to native or other targets.
2
2
 
3
+ // Fast allocator for the whole process. tish's object/array/string workloads are allocation-bound
4
+ // (sampling profiles spend a large fraction in system malloc/free + Arc drops); mimalloc is much
5
+ // faster for the many-small-allocations pattern — the technique JSC uses with bmalloc. Transparent:
6
+ // it only changes WHICH malloc backs every allocation, never program behaviour. `fast-alloc` is in
7
+ // `default`; `--no-default-features` falls back to the system allocator.
8
+ #[cfg(feature = "fast-alloc")]
9
+ #[global_allocator]
10
+ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
11
+
3
12
  mod cargo_native_registry;
4
13
  mod cli_help;
5
14
  mod repl_completion;
@@ -21,7 +30,7 @@ fn normalize_capability_flags(features: &[String]) -> HashSet<String> {
21
30
  for s in features {
22
31
  for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
23
32
  if part == "full" {
24
- for name in ["http", "timers", "fs", "process", "regex", "ws"] {
33
+ for name in ["http", "timers", "fs", "process", "regex", "ws", "tty"] {
25
34
  out.insert(name.to_string());
26
35
  }
27
36
  } else {
@@ -63,7 +72,7 @@ fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
63
72
  if argv.len() >= 2 {
64
73
  let first = argv[1].as_str();
65
74
  const SUBCOMMANDS: &[&str] = &["run", "repl", "build", "dump-ast"];
66
- let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.iter().any(|&s| s == first);
75
+ let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.contains(&first);
67
76
  if looks_like_file {
68
77
  argv.insert(1, "run".to_string());
69
78
  }
@@ -103,18 +112,28 @@ fn main() {
103
112
  a.no_optimize || no_opt_env,
104
113
  ),
105
114
  Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env, &a.features),
106
- Some(Commands::Build(a)) => build_file(
107
- &a.file,
108
- &a.output,
109
- &a.target,
110
- &a.native_backend,
111
- &a.features,
112
- a.no_optimize || no_opt_env,
113
- a.source_map,
114
- a.ios_triple.as_deref(),
115
- &a.crate_type,
116
- ),
117
- Some(Commands::DumpAst { file }) => dump_ast(&file),
115
+ Some(Commands::Build(a)) => {
116
+ // `--check warn|error` drives the gradual type checker via the same channel as the
117
+ // `TISH_CHECK` env var that `tishlang_compile::run_type_check` reads.
118
+ if let Some(mode) = &a.check {
119
+ std::env::set_var("TISH_CHECK", mode);
120
+ }
121
+ build_file(
122
+ &a.file,
123
+ &a.output,
124
+ &a.target,
125
+ &a.native_backend,
126
+ &a.features,
127
+ a.no_optimize || no_opt_env,
128
+ a.source_map,
129
+ a.ios_triple.as_deref(),
130
+ &a.crate_type,
131
+ )
132
+ }
133
+ Some(Commands::DumpAst {
134
+ file,
135
+ ignore_indent,
136
+ }) => dump_ast(&file, ignore_indent),
118
137
  None => {
119
138
  if io::stdin().is_terminal() {
120
139
  run_repl("vm", no_opt_env, &[])
@@ -169,7 +188,7 @@ fn run_stdin_source(
169
188
  } else {
170
189
  tishlang_opt::optimize(&prog)
171
190
  };
172
- run_program(&program, backend, no_optimize, features)
191
+ run_program(&program, &cwd, backend, no_optimize, features, None)
173
192
  }
174
193
 
175
194
  fn run_file(
@@ -214,16 +233,55 @@ fn run_file(
214
233
  }
215
234
  };
216
235
 
217
- run_program(&program, backend, no_optimize, features)
236
+ let ffi_base = Path::new(path)
237
+ .parent()
238
+ .map(|p| p.to_path_buf())
239
+ .unwrap_or_else(|| std::path::PathBuf::from("."));
240
+ run_program(&program, &ffi_base, backend, no_optimize, features, Some(path))
241
+ }
242
+
243
+ /// Load every `ffi:<path>` cdylib the program imports, resolving each path relative to `base_dir`
244
+ /// (the importing file's directory). Returns `(spec, exports)` pairs the backends register as
245
+ /// native modules so `import { f } from "ffi:./lib.dylib"` resolves to the extension's exports.
246
+ fn load_ffi_modules(
247
+ program: &tishlang_ast::Program,
248
+ base_dir: &Path,
249
+ ) -> Result<Vec<(String, tishlang_core::ObjectMap)>, String> {
250
+ let mut out = Vec::new();
251
+ for spec in tishlang_compile::ffi_native_specs(program) {
252
+ let rel = spec.strip_prefix("ffi:").unwrap_or(spec.as_str());
253
+ let lib_path = base_dir.join(rel);
254
+ let path_str = lib_path
255
+ .to_str()
256
+ .ok_or_else(|| format!("ffi: non-UTF-8 path: {}", lib_path.display()))?;
257
+ let exports = tishlang_ffi::load_module(path_str)?;
258
+ out.push((spec, exports));
259
+ }
260
+ Ok(out)
218
261
  }
219
262
 
220
263
  fn run_program(
221
264
  program: &tishlang_ast::Program,
265
+ base_dir: &Path,
222
266
  backend: &str,
223
267
  no_optimize: bool,
224
268
  features: &[String],
269
+ source_name: Option<&str>,
225
270
  ) -> Result<(), String> {
271
+ // FFI: load each `ffi:<path>` cdylib (resolved relative to the importing file) into a
272
+ // name→export map the backends register as a native module. Built once, shared across backends.
273
+ let ffi_modules = load_ffi_modules(program, base_dir)?;
274
+
226
275
  if backend == "interp" {
276
+ if !ffi_modules.is_empty() {
277
+ // The interpreter runs on `EvalValue`, but the FFI shim is built over core `Value`;
278
+ // bridging the two is a follow-up. `ffi:` works on the default VM backend today.
279
+ return Err(
280
+ "ffi: native extensions currently require the default (VM) backend; \
281
+ run without `--backend interp`"
282
+ .to_string(),
283
+ );
284
+ }
227
285
  let mut eval = tishlang_eval::Evaluator::new();
228
286
  let value = eval.eval_program(program)?;
229
287
  #[cfg(feature = "timers")]
@@ -242,14 +300,22 @@ fn run_program(
242
300
  return Ok(());
243
301
  }
244
302
 
303
+ let source_arc: Option<std::sync::Arc<str>> = source_name.map(std::sync::Arc::from);
245
304
  let chunk = if no_optimize {
246
- tishlang_bytecode::compile_unoptimized(program).map_err(|e| e.to_string())?
305
+ // `--no-optimize` keeps the simpler path; line info is most useful with optimization on.
306
+ let mut c = tishlang_bytecode::compile_unoptimized(program).map_err(|e| e.to_string())?;
307
+ c.source = source_arc.clone();
308
+ c
247
309
  } else {
248
- tishlang_bytecode::compile(program).map_err(|e| e.to_string())?
310
+ tishlang_bytecode::compile_with_source(program, source_arc.clone())
311
+ .map_err(|e| e.to_string())?
249
312
  };
250
313
  let caps = vm_capabilities_for_cli_run(features);
251
314
  let mut vm = tishlang_vm::Vm::with_capabilities(caps);
252
315
  cargo_native_registry::register_bytecode_native_modules(&mut vm);
316
+ for (spec, exports) in ffi_modules {
317
+ vm.register_native_module(spec, exports);
318
+ }
253
319
  let value = vm.run_with_options(&chunk, false)?;
254
320
  if !matches!(value, tishlang_core::Value::Null) {
255
321
  println!(
@@ -561,7 +627,7 @@ fn compile_to_js(
561
627
  Ok(())
562
628
  }
563
629
 
564
- #[allow(clippy::vec_init_then_push)]
630
+ #[allow(clippy::vec_init_then_push, clippy::too_many_arguments)] // build_file maps CLI build flags 1:1
565
631
  fn build_file(
566
632
  input_path: &str,
567
633
  output_path: &str,
@@ -773,9 +839,15 @@ mod cli_tests {
773
839
  }
774
840
  }
775
841
 
776
- fn dump_ast(path: &str) -> Result<(), String> {
842
+ fn dump_ast(path: &str, ignore_indent: bool) -> Result<(), String> {
777
843
  let source = fs::read_to_string(path).map_err(|e| format!("Cannot read {}: {}", path, e))?;
778
- let program = tishlang_parser::parse(&source)?;
844
+ // The `--ignore-indent` flag ORs with the `TISH_IGNORE_INDENT` env var, mirroring how
845
+ // `--no-optimize` combines with `TISH_NO_OPTIMIZE`.
846
+ let ignore_indent = ignore_indent || tishlang_parser::LexerOptions::from_env().ignore_indent;
847
+ let program = tishlang_parser::parse_with_options(
848
+ &source,
849
+ tishlang_parser::LexerOptions { ignore_indent },
850
+ )?;
779
851
  println!("{:#?}", program);
780
852
  Ok(())
781
853
  }
@@ -13,7 +13,6 @@ use rustyline::Context;
13
13
  use rustyline::Helper;
14
14
 
15
15
  use tishlang_bytecode::{compile_for_repl, compile_for_repl_unoptimized};
16
- use tishlang_parser;
17
16
  use tishlang_vm::Vm;
18
17
 
19
18
  /// Tish keywords for bare-word completion (Python-style).
@@ -0,0 +1,36 @@
1
+ //! Issue #74: a runtime error from the bytecode VM carries its source location (`file:line`)
2
+ //! instead of a bare message, so embedders/users can find where it came from.
3
+
4
+ use std::path::PathBuf;
5
+ use std::process::Command;
6
+
7
+ #[test]
8
+ fn runtime_error_reports_source_file_and_line() {
9
+ let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
10
+ let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
11
+ .join("tests")
12
+ .join("fixtures")
13
+ .join("runtime_error_location.tish");
14
+ assert!(fixture.is_file(), "missing fixture {}", fixture.display());
15
+
16
+ // `obj.field.deep` reads `.field` of `null` on line 4 of the fixture.
17
+ let out = Command::new(&tish)
18
+ .args(["run", "--backend", "vm", fixture.to_str().unwrap()])
19
+ .output()
20
+ .expect("spawn tish run");
21
+ assert!(!out.status.success(), "expected a runtime error");
22
+ let stderr = String::from_utf8_lossy(&out.stderr);
23
+
24
+ assert!(
25
+ stderr.contains(":4"),
26
+ "error should report the source line (4):\n{stderr}"
27
+ );
28
+ assert!(
29
+ stderr.contains("runtime_error_location.tish"),
30
+ "error should report the source file:\n{stderr}"
31
+ );
32
+ assert!(
33
+ stderr.contains("Cannot read property 'field' of null"),
34
+ "error should keep the original message:\n{stderr}"
35
+ );
36
+ }
@@ -0,0 +1,5 @@
1
+ let a = 1
2
+ let b = 2
3
+ let obj = null
4
+ let x = obj.field.deep
5
+ console.log(x)
@@ -0,0 +1,15 @@
1
+ // try/catch catches runtime errors (null property/method access, "not a function")
2
+ // AND throws that cross function-call frames, with the thrown value preserved (issue #60).
3
+ try { let x = null; x.foo() } catch (e) { console.log("A caught") }
4
+ function f() { throw new Error("boom") }
5
+ try { f() } catch (e) { console.log("B caught:", e.message) }
6
+ function g() { let y = null; y.baz() }
7
+ try { g() } catch (e) { console.log("C caught") }
8
+ console.log("D done")
9
+ try { try { throw "inner" } catch (e) { throw "rethrown" } } catch (e) { console.log("E caught:", e) }
10
+ try { console.log("F ok") } catch (e) { console.log("F should not happen") }
11
+ let total = 0
12
+ function risky(n) { if (n > 3) { throw n }; return n }
13
+ let i = 0
14
+ while (i < 6) { try { total = total + risky(i) } catch (e) { total = total + 100 }; i = i + 1 }
15
+ console.log("G total:", total)
@@ -0,0 +1,9 @@
1
+ // tish:tty capability (issue #101). Asserts the module loads and degrades gracefully when
2
+ // stdout/stdin is not a terminal (CI), so the output is deterministic without a real TTY.
3
+ import { isTTY, size, setRawMode, enterAltScreen, leaveAltScreen, read, readLine } from 'tish:tty'
4
+ console.log("isTTY:", typeof isTTY())
5
+ console.log("size:", size() ? "object" : "null")
6
+ console.log("setRawMode:", typeof setRawMode)
7
+ console.log("read:", typeof read)
8
+ console.log("readLine:", typeof readLine)
9
+ console.log("alt:", typeof enterAltScreen, typeof leaveAltScreen)