@tishlang/tish-format 1.0.12 → 1.0.13

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 (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,781 @@
1
+ //! Tish CLI - run, REPL, build to native or other targets.
2
+
3
+ mod cargo_native_registry;
4
+ mod cli_help;
5
+ mod repl_completion;
6
+
7
+ use std::collections::HashSet;
8
+ use std::fs;
9
+ use std::io::{self, IsTerminal, Read, Write};
10
+ use std::path::{Path, PathBuf};
11
+ use tishlang_core::VmRef;
12
+
13
+ use clap::FromArgMatches;
14
+ use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
15
+
16
+ use cli_help::{Cli, Commands};
17
+
18
+ /// Normalize `--feature` / `--feature http,timers,fs` / `--feature full` for VM runs and native builds.
19
+ fn normalize_capability_flags(features: &[String]) -> HashSet<String> {
20
+ let mut out = HashSet::new();
21
+ for s in features {
22
+ for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
23
+ if part == "full" {
24
+ for name in ["http", "timers", "fs", "process", "regex", "ws"] {
25
+ out.insert(name.to_string());
26
+ }
27
+ } else {
28
+ out.insert(part.to_string());
29
+ }
30
+ }
31
+ }
32
+ out
33
+ }
34
+
35
+ /// VM capabilities for `run` / `repl` / stdin with the bytecode VM.
36
+ ///
37
+ /// If the user passes no `--feature`, enable **everything linked into this `tish` binary**
38
+ /// (so `cargo run --bin tish --features full -- script.tish` does not need `--feature full`).
39
+ /// If they pass `--feature …`, use **only** that set (e.g. restrict a full build to `http` only).
40
+ fn vm_capabilities_for_cli_run(cli_features: &[String]) -> HashSet<String> {
41
+ if cli_features.is_empty() {
42
+ tishlang_vm::all_compiled_capabilities()
43
+ } else {
44
+ normalize_capability_flags(cli_features)
45
+ }
46
+ }
47
+
48
+ /// `--feature` list for `tish build --target native`: same default as `tish run` (all linked-in caps).
49
+ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
50
+ if cli_features.is_empty() {
51
+ let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities()
52
+ .into_iter()
53
+ .collect();
54
+ v.sort();
55
+ v
56
+ } else {
57
+ cli_features.to_vec()
58
+ }
59
+ }
60
+
61
+ /// `tish script.tish` → insert `run` so it matches `tish run script.tish` (npx / npm UX).
62
+ fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
63
+ if argv.len() >= 2 {
64
+ let first = argv[1].as_str();
65
+ const SUBCOMMANDS: &[&str] = &["run", "repl", "build", "dump-ast"];
66
+ let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.iter().any(|&s| s == first);
67
+ if looks_like_file {
68
+ argv.insert(1, "run".to_string());
69
+ }
70
+ }
71
+ argv
72
+ }
73
+
74
+ fn main() {
75
+ let no_opt_env = std::env::var_os("TISH_NO_OPTIMIZE")
76
+ .map(|v| v == "1" || v == "true" || v == "yes")
77
+ .unwrap_or(false);
78
+
79
+ // `tish -` (like `node -` / `bun -`); clap would treat `-` as an invalid subcommand.
80
+ let argv: Vec<String> = std::env::args().collect();
81
+ if argv.len() == 2 && argv[1] == "-" {
82
+ let result = run_stdin_pipe("vm", &[], no_opt_env, true);
83
+ if let Err(e) = result {
84
+ eprintln!("Error: {}", e);
85
+ std::process::exit(1);
86
+ }
87
+ return;
88
+ }
89
+
90
+ if cli_help::argv_requests_help(&argv) {
91
+ cli_help::print_banner_with_help(&argv);
92
+ std::process::exit(0);
93
+ }
94
+
95
+ let argv = argv_with_implicit_run(argv);
96
+ let matches = cli_help::build_command().get_matches_from(&argv);
97
+ let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
98
+ let result = match cli.command {
99
+ Some(Commands::Run(a)) => run_file(
100
+ &a.file,
101
+ &a.backend,
102
+ &a.features,
103
+ a.no_optimize || no_opt_env,
104
+ ),
105
+ 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),
118
+ None => {
119
+ if io::stdin().is_terminal() {
120
+ run_repl("vm", no_opt_env, &[])
121
+ } else {
122
+ // `echo '...' | tish` — run script from stdin (Bun-style)
123
+ run_stdin_pipe("vm", &[], no_opt_env, false)
124
+ }
125
+ }
126
+ };
127
+
128
+ if let Err(e) = result {
129
+ eprintln!("Error: {}", e);
130
+ std::process::exit(1);
131
+ }
132
+ }
133
+
134
+ /// Read stdin and run as Tish. If `fail_on_empty`, `tish run -` / `tish -` get an error; if false, empty stdin exits 0.
135
+ fn run_stdin_pipe(
136
+ backend: &str,
137
+ features: &[String],
138
+ no_optimize: bool,
139
+ fail_on_empty: bool,
140
+ ) -> Result<(), String> {
141
+ let mut source = String::new();
142
+ io::stdin()
143
+ .read_to_string(&mut source)
144
+ .map_err(|e| format!("Cannot read stdin: {}", e))?;
145
+ if source.trim().is_empty() {
146
+ if fail_on_empty {
147
+ return Err(
148
+ "No source on stdin. Example: echo 'console.log(1)' | tish or tish run -"
149
+ .into(),
150
+ );
151
+ }
152
+ return Ok(());
153
+ }
154
+ run_stdin_source(&source, backend, features, no_optimize)
155
+ }
156
+
157
+ fn run_stdin_source(
158
+ source: &str,
159
+ backend: &str,
160
+ features: &[String],
161
+ no_optimize: bool,
162
+ ) -> Result<(), String> {
163
+ let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
164
+ let modules = tishlang_compile::resolve_project_from_stdin(source, &cwd)?;
165
+ tishlang_compile::detect_cycles(&modules)?;
166
+ let prog = tishlang_compile::merge_modules(modules)?.program;
167
+ let program = if no_optimize {
168
+ prog
169
+ } else {
170
+ tishlang_opt::optimize(&prog)
171
+ };
172
+ run_program(&program, backend, no_optimize, features)
173
+ }
174
+
175
+ fn run_file(
176
+ path: &str,
177
+ backend: &str,
178
+ features: &[String],
179
+ no_optimize: bool,
180
+ ) -> Result<(), String> {
181
+ let program = if path == "-" {
182
+ return run_stdin_pipe(backend, features, no_optimize, true);
183
+ } else {
184
+ let path = Path::new(path)
185
+ .canonicalize()
186
+ .map_err(|e| format!("Cannot resolve {}: {}", path, e))?;
187
+ let project_root = path.parent().and_then(|p| {
188
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
189
+ p.parent()
190
+ } else {
191
+ Some(p)
192
+ }
193
+ });
194
+
195
+ if path.extension().map(|e| e == "js") == Some(true) {
196
+ let prog = tishlang_js_to_tish::convert(
197
+ &fs::read_to_string(&path).map_err(|e| format!("{}", e))?,
198
+ )
199
+ .map_err(|e| format!("{}", e))?;
200
+ if no_optimize {
201
+ prog
202
+ } else {
203
+ tishlang_opt::optimize(&prog)
204
+ }
205
+ } else {
206
+ let modules = tishlang_compile::resolve_project(&path, project_root)?;
207
+ tishlang_compile::detect_cycles(&modules)?;
208
+ let prog = tishlang_compile::merge_modules(modules)?.program;
209
+ if no_optimize {
210
+ prog
211
+ } else {
212
+ tishlang_opt::optimize(&prog)
213
+ }
214
+ }
215
+ };
216
+
217
+ run_program(&program, backend, no_optimize, features)
218
+ }
219
+
220
+ fn run_program(
221
+ program: &tishlang_ast::Program,
222
+ backend: &str,
223
+ no_optimize: bool,
224
+ features: &[String],
225
+ ) -> Result<(), String> {
226
+ if backend == "interp" {
227
+ let mut eval = tishlang_eval::Evaluator::new();
228
+ let value = eval.eval_program(program)?;
229
+ #[cfg(feature = "timers")]
230
+ {
231
+ let _ = eval.run_timer_phase();
232
+ }
233
+ if !matches!(value, tishlang_eval::Value::Null) {
234
+ println!(
235
+ "{}",
236
+ tishlang_eval::format_value_for_console(
237
+ &value,
238
+ tishlang_core::use_console_colors()
239
+ )
240
+ );
241
+ }
242
+ return Ok(());
243
+ }
244
+
245
+ let chunk = if no_optimize {
246
+ tishlang_bytecode::compile_unoptimized(program).map_err(|e| e.to_string())?
247
+ } else {
248
+ tishlang_bytecode::compile(program).map_err(|e| e.to_string())?
249
+ };
250
+ let caps = vm_capabilities_for_cli_run(features);
251
+ let mut vm = tishlang_vm::Vm::with_capabilities(caps);
252
+ cargo_native_registry::register_bytecode_native_modules(&mut vm);
253
+ let value = vm.run_with_options(&chunk, false)?;
254
+ if !matches!(value, tishlang_core::Value::Null) {
255
+ println!(
256
+ "{}",
257
+ tishlang_core::format_value_styled(&value, tishlang_core::use_console_colors())
258
+ );
259
+ }
260
+ Ok(())
261
+ }
262
+
263
+ fn run_repl(backend: &str, no_optimize: bool, features: &[String]) -> Result<(), String> {
264
+ cli_help::print_tish_banner();
265
+ println!("Tish REPL (Ctrl-D to exit)");
266
+ let mut buffer = String::new();
267
+
268
+ if backend == "interp" {
269
+ let mut eval = tishlang_eval::Evaluator::new();
270
+ let mut multiline = String::new();
271
+ loop {
272
+ let prompt = repl_prompt(multiline.is_empty());
273
+ print!("{}", prompt);
274
+ io::stdout().flush().map_err(|e| e.to_string())?;
275
+ buffer.clear();
276
+ if io::stdin()
277
+ .read_line(&mut buffer)
278
+ .map_err(|e| e.to_string())?
279
+ == 0
280
+ {
281
+ if !multiline.is_empty() {
282
+ let _ = tishlang_parser::parse(multiline.trim());
283
+ }
284
+ break;
285
+ }
286
+ let line = buffer.trim_end();
287
+ if multiline.is_empty() && line.is_empty() {
288
+ continue;
289
+ }
290
+ if multiline.is_empty() {
291
+ multiline = line.to_string();
292
+ } else {
293
+ multiline.push('\n');
294
+ multiline.push_str(line);
295
+ }
296
+ match tishlang_parser::parse(multiline.trim()) {
297
+ Ok(program) => {
298
+ match eval.eval_program(&program) {
299
+ Ok(v) => {
300
+ #[cfg(feature = "timers")]
301
+ {
302
+ let _ = eval.run_timer_phase();
303
+ }
304
+ if !matches!(v, tishlang_eval::Value::Null) {
305
+ println!(
306
+ "{}",
307
+ tishlang_eval::format_value_for_console(
308
+ &v,
309
+ tishlang_core::use_console_colors()
310
+ )
311
+ );
312
+ }
313
+ }
314
+ Err(e) => eprintln!("{}", e),
315
+ }
316
+ multiline.clear();
317
+ }
318
+ Err(e) => {
319
+ if e.to_lowercase().contains("eof") {
320
+ // Incomplete: keep reading
321
+ } else {
322
+ eprintln!("Parse error: {}", e);
323
+ multiline.clear();
324
+ }
325
+ }
326
+ }
327
+ }
328
+ return Ok(());
329
+ }
330
+
331
+ // VM backend with tab completion (e.g. a. -> properties/methods)
332
+ if !std::io::stdin().is_terminal() {
333
+ eprintln!("Note: Tab completion and grey preview require an interactive terminal (TTY).");
334
+ }
335
+ let mut vm0 = tishlang_vm::Vm::with_capabilities(vm_capabilities_for_cli_run(features));
336
+ cargo_native_registry::register_bytecode_native_modules(&mut vm0);
337
+ let vm = VmRef::new(vm0);
338
+ let completer = repl_completion::ReplCompleter {
339
+ vm: vm.clone(),
340
+ no_optimize,
341
+ };
342
+ let config = Config::builder()
343
+ .completion_type(CompletionType::List)
344
+ .completion_show_all_if_ambiguous(true)
345
+ .color_mode(ColorMode::Forced)
346
+ .behavior(Behavior::PreferTerm)
347
+ .build();
348
+ let mut rl: Editor<repl_completion::ReplCompleter, _> =
349
+ Editor::with_config(config).map_err(|e| e.to_string())?;
350
+ rl.set_helper(Some(completer));
351
+
352
+ if let Some(ref path) = tish_history_path() {
353
+ let _ = rl.load_history(path);
354
+ }
355
+
356
+ println!("Tab after 'obj.' for completions (grey preview); press Tab again for full list.");
357
+ println!("Multi-line: type until the statement is complete; use ... continuation prompt.");
358
+
359
+ let mut buffer = String::new();
360
+
361
+ loop {
362
+ let prompt = repl_prompt(buffer.is_empty());
363
+ let line = match rl.readline(&prompt) {
364
+ Ok(l) => l,
365
+ Err(rustyline::error::ReadlineError::Eof) => {
366
+ if buffer.is_empty() {
367
+ break;
368
+ }
369
+ match tishlang_parser::parse(buffer.trim()) {
370
+ Ok(program) => {
371
+ let compile_fn = if no_optimize {
372
+ tishlang_bytecode::compile_for_repl_unoptimized
373
+ } else {
374
+ tishlang_bytecode::compile_for_repl
375
+ };
376
+ if let Ok(chunk) = compile_fn(&program) {
377
+ let _ = vm.borrow_mut().run_with_options(&chunk, true);
378
+ }
379
+ }
380
+ Err(e) => eprintln!("Parse error: {}", e),
381
+ }
382
+ break;
383
+ }
384
+ Err(rustyline::error::ReadlineError::Interrupted) => {
385
+ buffer.clear();
386
+ continue;
387
+ }
388
+ Err(e) => return Err(e.to_string()),
389
+ };
390
+ let line = line.trim_end();
391
+ if buffer.is_empty() && line.is_empty() {
392
+ continue;
393
+ }
394
+ if buffer.is_empty() {
395
+ buffer = line.to_string();
396
+ } else {
397
+ buffer.push('\n');
398
+ buffer.push_str(line);
399
+ }
400
+ match tishlang_parser::parse(buffer.trim()) {
401
+ Ok(program) => {
402
+ let compile_fn = if no_optimize {
403
+ tishlang_bytecode::compile_for_repl_unoptimized
404
+ } else {
405
+ tishlang_bytecode::compile_for_repl
406
+ };
407
+ match compile_fn(&program) {
408
+ Ok(chunk) => match vm.borrow_mut().run_with_options(&chunk, true) {
409
+ Ok(v) => {
410
+ if !matches!(v, tishlang_core::Value::Null) {
411
+ println!(
412
+ "{}",
413
+ tishlang_core::format_value_styled(
414
+ &v,
415
+ tishlang_core::use_console_colors()
416
+ )
417
+ );
418
+ }
419
+ }
420
+ Err(e) => eprintln!("{}", e),
421
+ },
422
+ Err(e) => eprintln!("Compile error: {}", e),
423
+ }
424
+ let _ = rl.add_history_entry(buffer.trim());
425
+ buffer.clear();
426
+ }
427
+ Err(e) => {
428
+ if e.to_lowercase().contains("eof") {
429
+ // Incomplete: keep accumulating (Python-style ... prompt)
430
+ } else {
431
+ eprintln!("Parse error: {}", e);
432
+ buffer.clear();
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ if let Some(ref path) = tish_history_path() {
439
+ let _ = rl.save_history(path);
440
+ }
441
+ Ok(())
442
+ }
443
+
444
+ /// REPL prompt with green caret when stdout is a TTY (platform-style).
445
+ fn repl_prompt(primary: bool) -> String {
446
+ if tishlang_core::use_console_colors() {
447
+ if primary {
448
+ "\x1b[32m> \x1b[0m".to_string()
449
+ } else {
450
+ "\x1b[32m... \x1b[0m".to_string()
451
+ }
452
+ } else if primary {
453
+ "> ".to_string()
454
+ } else {
455
+ "... ".to_string()
456
+ }
457
+ }
458
+
459
+ /// Path to REPL history file (Python-style: ~/.tish_history).
460
+ fn tish_history_path() -> Option<PathBuf> {
461
+ let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"));
462
+ home.map(|h| PathBuf::from(h).join(".tish_history"))
463
+ }
464
+
465
+ fn compile_to_js(
466
+ input_path: &Path,
467
+ output_path: &str,
468
+ optimize: bool,
469
+ source_map: bool,
470
+ ) -> Result<(), String> {
471
+ if source_map && optimize {
472
+ return Err(
473
+ "tish build --target js --source-map requires --no-optimize (mappings follow unmerged statement order)."
474
+ .into(),
475
+ );
476
+ }
477
+ if source_map
478
+ && (input_path.extension().map(|e| e == "jsx") == Some(true)
479
+ || input_path.extension().map(|e| e == "js") == Some(true))
480
+ {
481
+ return Err(
482
+ "tish build --target js --source-map is only supported for .tish project builds (not single-file .jsx / .js inputs)."
483
+ .into(),
484
+ );
485
+ }
486
+ let project_root = input_path.parent().and_then(|p| {
487
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
488
+ p.parent()
489
+ } else {
490
+ Some(p)
491
+ }
492
+ });
493
+ let out_path = Path::new(output_path);
494
+ let out_path = if out_path.extension().is_none()
495
+ || out_path.extension() == Some(std::ffi::OsStr::new(""))
496
+ {
497
+ out_path.with_extension("js")
498
+ } else {
499
+ out_path.to_path_buf()
500
+ };
501
+ let out_js_name = out_path
502
+ .file_name()
503
+ .and_then(|s| s.to_str())
504
+ .unwrap_or("out.js");
505
+
506
+ let (js, map_json) = if input_path.extension().map(|e| e == "jsx") == Some(true) {
507
+ let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
508
+ let wrapped = format!(
509
+ "export fn __TishJsxRoot() {{\n return (\n{}\n )\n}}",
510
+ source.trim()
511
+ );
512
+ let program =
513
+ tishlang_parser::parse(&wrapped).map_err(|e| format!("JSX wrapper parse: {}", e))?;
514
+ let p = if optimize {
515
+ tishlang_opt::optimize(&program)
516
+ } else {
517
+ program
518
+ };
519
+ let js =
520
+ tishlang_compile_js::compile_with_jsx(&p, optimize).map_err(|e| format!("{}", e))?;
521
+ (js, None)
522
+ } else if input_path.extension().map(|e| e == "js") == Some(true) {
523
+ let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
524
+ let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
525
+ let js = tishlang_compile_js::compile_with_jsx(&program, optimize)
526
+ .map_err(|e| format!("{}", e))?;
527
+ (js, None)
528
+ } else if source_map {
529
+ let bundle = tishlang_compile_js::compile_project_with_jsx_and_source_map(
530
+ input_path,
531
+ project_root,
532
+ out_js_name,
533
+ )
534
+ .map_err(|e| format!("{}", e))?;
535
+ (bundle.js, bundle.source_map_json)
536
+ } else {
537
+ let js = tishlang_compile_js::compile_project_with_jsx(input_path, project_root, optimize)
538
+ .map_err(|e| format!("{}", e))?;
539
+ (js, None)
540
+ };
541
+
542
+ if let Some(parent) = out_path.parent() {
543
+ fs::create_dir_all(parent)
544
+ .map_err(|e| format!("Cannot create output directory {}: {}", parent.display(), e))?;
545
+ }
546
+ let mut js_out = js;
547
+ if let Some(map) = &map_json {
548
+ let map_path = out_path.with_extension("js.map");
549
+ fs::write(&map_path, map)
550
+ .map_err(|e| format!("Cannot write {}: {}", map_path.display(), e))?;
551
+ let map_url = map_path
552
+ .file_name()
553
+ .and_then(|s| s.to_str())
554
+ .unwrap_or("out.js.map");
555
+ js_out.push_str(&format!("\n//# sourceMappingURL={map_url}\n"));
556
+ println!("Built: {}", map_path.display());
557
+ }
558
+ fs::write(&out_path, js_out)
559
+ .map_err(|e| format!("Cannot write {}: {}", out_path.display(), e))?;
560
+ println!("Built: {}", out_path.display());
561
+ Ok(())
562
+ }
563
+
564
+ #[allow(clippy::vec_init_then_push)]
565
+ fn build_file(
566
+ input_path: &str,
567
+ output_path: &str,
568
+ target: &str,
569
+ native_backend: &str,
570
+ cli_features: &[String],
571
+ no_optimize: bool,
572
+ source_map: bool,
573
+ ios_triple: Option<&str>,
574
+ crate_type: &str,
575
+ ) -> Result<(), String> {
576
+ let optimize = !no_optimize;
577
+ let input_path = Path::new(input_path)
578
+ .canonicalize()
579
+ .map_err(|e| format!("Cannot resolve {}: {}", input_path, e))?;
580
+
581
+ let is_js = input_path.extension().map(|e| e == "js") == Some(true);
582
+
583
+ if target == "js" {
584
+ return compile_to_js(&input_path, output_path, optimize, source_map);
585
+ }
586
+
587
+ if target == "wasm" && is_js {
588
+ let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
589
+ let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
590
+ return tishlang_wasm::compile_program_to_wasm(&program, Path::new(output_path), optimize)
591
+ .map_err(|e| format!("{}", e));
592
+ }
593
+
594
+ if target == "wasm" {
595
+ let project_root = input_path.parent().and_then(|p| {
596
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
597
+ p.parent()
598
+ } else {
599
+ Some(p)
600
+ }
601
+ });
602
+ return tishlang_wasm::compile_to_wasm(
603
+ &input_path,
604
+ project_root,
605
+ Path::new(output_path),
606
+ optimize,
607
+ )
608
+ .map_err(|e| e.to_string());
609
+ }
610
+
611
+ if target == "wasi" {
612
+ let project_root = input_path.parent().and_then(|p| {
613
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
614
+ p.parent()
615
+ } else {
616
+ Some(p)
617
+ }
618
+ });
619
+ let features = native_build_features_from_cli(cli_features);
620
+ return tishlang_wasm::compile_to_wasi(
621
+ &input_path,
622
+ project_root,
623
+ Path::new(output_path),
624
+ optimize,
625
+ &features,
626
+ )
627
+ .map_err(|e| e.to_string());
628
+ }
629
+
630
+ if target == "bytecode" {
631
+ let project_root = input_path.parent().and_then(|p| {
632
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
633
+ p.parent()
634
+ } else {
635
+ Some(p)
636
+ }
637
+ });
638
+ return tishlang_wasm::compile_to_bytecode(
639
+ &input_path,
640
+ project_root,
641
+ Path::new(output_path),
642
+ optimize,
643
+ )
644
+ .map_err(|e| e.to_string());
645
+ }
646
+
647
+ if target != "native" {
648
+ return Err(format!(
649
+ "Unknown target: {}. Use 'native', 'js', 'wasm', 'wasi', or 'bytecode'.",
650
+ target
651
+ ));
652
+ }
653
+
654
+ let project_root = input_path.parent().map(|p| {
655
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
656
+ p.parent().unwrap_or(p)
657
+ } else {
658
+ p
659
+ }
660
+ });
661
+ let features: Vec<String> = native_build_features_from_cli(cli_features);
662
+
663
+ let build_config = if let Some(triple) = ios_triple {
664
+ tishlang_native::NativeBuildConfig::ios_staticlib(triple)
665
+ } else if crate_type == "staticlib" {
666
+ tishlang_native::NativeBuildConfig {
667
+ artifact: tishlang_native::NativeArtifact::StaticLib,
668
+ cargo_target: None,
669
+ emit_mode: tishlang_compile::NativeEmitMode::EmbeddedLib,
670
+ }
671
+ } else if crate_type != "bin" {
672
+ return Err(format!(
673
+ "Unknown --crate-type: {}. Use 'bin' or 'staticlib'.",
674
+ crate_type
675
+ ));
676
+ } else {
677
+ tishlang_native::NativeBuildConfig::desktop()
678
+ };
679
+
680
+ if is_js {
681
+ let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
682
+ let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
683
+ if build_config.artifact != tishlang_native::NativeArtifact::Bin {
684
+ return Err(
685
+ "--crate-type staticlib / --ios-triple require a .tish entry file.".to_string(),
686
+ );
687
+ }
688
+ tishlang_native::compile_program_to_native(
689
+ &program,
690
+ project_root,
691
+ Path::new(output_path),
692
+ &features,
693
+ native_backend,
694
+ optimize,
695
+ )
696
+ .map_err(|e| e.to_string())?;
697
+ } else {
698
+ tishlang_native::compile_to_native_with_config(
699
+ &input_path,
700
+ project_root,
701
+ Path::new(output_path),
702
+ &features,
703
+ native_backend,
704
+ optimize,
705
+ &build_config,
706
+ )
707
+ .map_err(|e| e.to_string())?;
708
+ }
709
+
710
+ let out_name = Path::new(output_path)
711
+ .file_stem()
712
+ .and_then(|s| s.to_str())
713
+ .unwrap_or("tish_out");
714
+ let built_path = if build_config.artifact == tishlang_native::NativeArtifact::StaticLib {
715
+ if output_path.ends_with('/') || Path::new(output_path).is_dir() {
716
+ Path::new(output_path).join(format!("lib{out_name}.a"))
717
+ } else if output_path.ends_with(".a") {
718
+ Path::new(output_path).to_path_buf()
719
+ } else {
720
+ Path::new(output_path).with_extension("a")
721
+ }
722
+ } else if output_path.ends_with('/') || Path::new(output_path).is_dir() {
723
+ Path::new(output_path).join(out_name)
724
+ } else {
725
+ Path::new(output_path).to_path_buf()
726
+ };
727
+ println!("Built: {}", built_path.display());
728
+ Ok(())
729
+ }
730
+
731
+ #[cfg(test)]
732
+ mod cli_tests {
733
+ use clap::Parser;
734
+
735
+ use crate::cli_help::{Cli, Commands};
736
+
737
+ use super::argv_with_implicit_run;
738
+
739
+ #[test]
740
+ fn implicit_run_inserts_run_before_file() {
741
+ let argv = argv_with_implicit_run(vec!["tish".to_string(), "hello.tish".to_string()]);
742
+ let cli = Cli::try_parse_from(argv).unwrap();
743
+ match cli.command {
744
+ Some(Commands::Run(a)) => assert_eq!(a.file, "hello.tish"),
745
+ _ => panic!("expected Run"),
746
+ }
747
+ }
748
+
749
+ #[test]
750
+ fn explicit_subcommand_not_treated_as_file() {
751
+ let argv = argv_with_implicit_run(vec!["tish".to_string(), "repl".to_string()]);
752
+ let cli = Cli::try_parse_from(argv).unwrap();
753
+ assert!(matches!(cli.command, Some(Commands::Repl(_))));
754
+ }
755
+
756
+ #[test]
757
+ fn build_js_target_parses() {
758
+ let cli = Cli::try_parse_from(["tish", "build", "m.tish", "--target", "js", "-o", "x.js"])
759
+ .unwrap();
760
+ match cli.command {
761
+ Some(Commands::Build(a)) => assert_eq!(a.file, "m.tish"),
762
+ _ => panic!("expected Build"),
763
+ }
764
+ }
765
+
766
+ #[test]
767
+ fn run_stdin_marker_parses_as_file() {
768
+ let cli = Cli::try_parse_from(["tish", "run", "-"]).unwrap();
769
+ match cli.command {
770
+ Some(Commands::Run(a)) => assert_eq!(a.file, "-"),
771
+ _ => panic!("expected Run"),
772
+ }
773
+ }
774
+ }
775
+
776
+ fn dump_ast(path: &str) -> Result<(), String> {
777
+ let source = fs::read_to_string(path).map_err(|e| format!("Cannot read {}: {}", path, e))?;
778
+ let program = tishlang_parser::parse(&source)?;
779
+ println!("{:#?}", program);
780
+ Ok(())
781
+ }