@tishlang/tish 1.0.7 → 1.0.11

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 (127) hide show
  1. package/Cargo.toml +43 -0
  2. package/LICENSE +13 -0
  3. package/README.md +66 -0
  4. package/crates/js_to_tish/Cargo.toml +9 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +61 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +608 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +474 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +44 -0
  13. package/crates/tish/src/main.rs +585 -0
  14. package/crates/tish/src/repl_completion.rs +200 -0
  15. package/crates/tish/tests/integration_test.rs +726 -0
  16. package/crates/tish_ast/Cargo.toml +7 -0
  17. package/crates/tish_ast/src/ast.rs +494 -0
  18. package/crates/tish_ast/src/lib.rs +5 -0
  19. package/crates/tish_build_utils/Cargo.toml +5 -0
  20. package/crates/tish_build_utils/src/lib.rs +175 -0
  21. package/crates/tish_builtins/Cargo.toml +12 -0
  22. package/crates/tish_builtins/src/array.rs +410 -0
  23. package/crates/tish_builtins/src/globals.rs +197 -0
  24. package/crates/tish_builtins/src/helpers.rs +38 -0
  25. package/crates/tish_builtins/src/lib.rs +14 -0
  26. package/crates/tish_builtins/src/math.rs +80 -0
  27. package/crates/tish_builtins/src/object.rs +36 -0
  28. package/crates/tish_builtins/src/string.rs +253 -0
  29. package/crates/tish_bytecode/Cargo.toml +15 -0
  30. package/crates/tish_bytecode/src/chunk.rs +97 -0
  31. package/crates/tish_bytecode/src/compiler.rs +1361 -0
  32. package/crates/tish_bytecode/src/encoding.rs +100 -0
  33. package/crates/tish_bytecode/src/lib.rs +19 -0
  34. package/crates/tish_bytecode/src/opcode.rs +110 -0
  35. package/crates/tish_bytecode/src/peephole.rs +159 -0
  36. package/crates/tish_bytecode/src/serialize.rs +163 -0
  37. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  38. package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
  39. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  40. package/crates/tish_compile/Cargo.toml +21 -0
  41. package/crates/tish_compile/src/codegen.rs +3316 -0
  42. package/crates/tish_compile/src/lib.rs +71 -0
  43. package/crates/tish_compile/src/resolve.rs +631 -0
  44. package/crates/tish_compile/src/types.rs +304 -0
  45. package/crates/tish_compile_js/Cargo.toml +16 -0
  46. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  47. package/crates/tish_compile_js/src/codegen.rs +794 -0
  48. package/crates/tish_compile_js/src/error.rs +20 -0
  49. package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
  50. package/crates/tish_compile_js/src/lib.rs +27 -0
  51. package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +19 -0
  53. package/crates/tish_compiler_wasm/src/lib.rs +55 -0
  54. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
  55. package/crates/tish_core/Cargo.toml +11 -0
  56. package/crates/tish_core/src/console_style.rs +128 -0
  57. package/crates/tish_core/src/json.rs +327 -0
  58. package/crates/tish_core/src/lib.rs +15 -0
  59. package/crates/tish_core/src/macros.rs +37 -0
  60. package/crates/tish_core/src/uri.rs +115 -0
  61. package/crates/tish_core/src/value.rs +376 -0
  62. package/crates/tish_cranelift/Cargo.toml +17 -0
  63. package/crates/tish_cranelift/src/lib.rs +41 -0
  64. package/crates/tish_cranelift/src/link.rs +120 -0
  65. package/crates/tish_cranelift/src/lower.rs +77 -0
  66. package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
  67. package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
  68. package/crates/tish_eval/Cargo.toml +26 -0
  69. package/crates/tish_eval/src/eval.rs +3205 -0
  70. package/crates/tish_eval/src/http.rs +122 -0
  71. package/crates/tish_eval/src/lib.rs +59 -0
  72. package/crates/tish_eval/src/natives.rs +301 -0
  73. package/crates/tish_eval/src/promise.rs +173 -0
  74. package/crates/tish_eval/src/regex.rs +298 -0
  75. package/crates/tish_eval/src/timers.rs +111 -0
  76. package/crates/tish_eval/src/value.rs +224 -0
  77. package/crates/tish_eval/src/value_convert.rs +85 -0
  78. package/crates/tish_fmt/Cargo.toml +16 -0
  79. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  80. package/crates/tish_fmt/src/lib.rs +884 -0
  81. package/crates/tish_jsx_web/Cargo.toml +7 -0
  82. package/crates/tish_jsx_web/README.md +18 -0
  83. package/crates/tish_jsx_web/src/lib.rs +157 -0
  84. package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
  85. package/crates/tish_lexer/Cargo.toml +7 -0
  86. package/crates/tish_lexer/src/lib.rs +430 -0
  87. package/crates/tish_lexer/src/token.rs +155 -0
  88. package/crates/tish_lint/Cargo.toml +17 -0
  89. package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
  90. package/crates/tish_lint/src/lib.rs +278 -0
  91. package/crates/tish_llvm/Cargo.toml +11 -0
  92. package/crates/tish_llvm/src/lib.rs +106 -0
  93. package/crates/tish_lsp/Cargo.toml +22 -0
  94. package/crates/tish_lsp/README.md +26 -0
  95. package/crates/tish_lsp/src/main.rs +615 -0
  96. package/crates/tish_native/Cargo.toml +14 -0
  97. package/crates/tish_native/src/build.rs +102 -0
  98. package/crates/tish_native/src/lib.rs +237 -0
  99. package/crates/tish_opt/Cargo.toml +11 -0
  100. package/crates/tish_opt/src/lib.rs +896 -0
  101. package/crates/tish_parser/Cargo.toml +9 -0
  102. package/crates/tish_parser/src/lib.rs +123 -0
  103. package/crates/tish_parser/src/parser.rs +1714 -0
  104. package/crates/tish_runtime/Cargo.toml +26 -0
  105. package/crates/tish_runtime/src/http.rs +308 -0
  106. package/crates/tish_runtime/src/http_fetch.rs +453 -0
  107. package/crates/tish_runtime/src/lib.rs +1004 -0
  108. package/crates/tish_runtime/src/native_promise.rs +26 -0
  109. package/crates/tish_runtime/src/promise.rs +77 -0
  110. package/crates/tish_runtime/src/promise_io.rs +41 -0
  111. package/crates/tish_runtime/src/timers.rs +125 -0
  112. package/crates/tish_runtime/src/ws.rs +725 -0
  113. package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
  114. package/crates/tish_vm/Cargo.toml +31 -0
  115. package/crates/tish_vm/src/lib.rs +39 -0
  116. package/crates/tish_vm/src/vm.rs +1399 -0
  117. package/crates/tish_wasm/Cargo.toml +13 -0
  118. package/crates/tish_wasm/src/lib.rs +358 -0
  119. package/crates/tish_wasm_runtime/Cargo.toml +25 -0
  120. package/crates/tish_wasm_runtime/src/lib.rs +36 -0
  121. package/justfile +260 -0
  122. package/package.json +8 -3
  123. package/platform/darwin-arm64/tish +0 -0
  124. package/platform/darwin-x64/tish +0 -0
  125. package/platform/linux-arm64/tish +0 -0
  126. package/platform/linux-x64/tish +0 -0
  127. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,585 @@
1
+ //! Tish CLI - run, REPL, compile to native.
2
+
3
+ mod repl_completion;
4
+
5
+ use std::cell::RefCell;
6
+ use std::fs;
7
+ use std::io::{self, IsTerminal, Write};
8
+ use std::path::{Path, PathBuf};
9
+ use std::rc::Rc;
10
+
11
+ use clap::{Parser, Subcommand};
12
+ use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
13
+
14
+ #[derive(Parser)]
15
+ #[command(name = "tish")]
16
+ #[command(about = "Tish - minimal TS/JS-compatible language")]
17
+ #[command(after_help = "To disable optimizations: TISH_NO_OPTIMIZE=1")]
18
+ pub(crate) struct Cli {
19
+ #[command(subcommand)]
20
+ command: Option<Commands>,
21
+ }
22
+
23
+ #[derive(Parser)]
24
+ struct RunArgs {
25
+ #[arg(required = true)]
26
+ file: String,
27
+ #[arg(long, default_value = "vm")]
28
+ backend: String,
29
+ /// Enable capabilities (http, fs, process, regex, ws). Must match how tish was built.
30
+ /// E.g. cargo run -p tish --features http,fs -- run script.tish --feature http,fs
31
+ #[arg(long = "feature", action = clap::ArgAction::Append)]
32
+ features: Vec<String>,
33
+ /// Disable AST and bytecode optimizations (for debugging)
34
+ #[arg(long)]
35
+ no_optimize: bool,
36
+ }
37
+
38
+ #[derive(Parser)]
39
+ struct ReplArgs {
40
+ #[arg(long, default_value = "vm")]
41
+ backend: String,
42
+ #[arg(long)]
43
+ no_optimize: bool,
44
+ }
45
+
46
+ #[derive(Parser)]
47
+ struct CompileArgs {
48
+ #[arg(short, long, default_value = "tish_out")]
49
+ output: String,
50
+ #[arg(long, default_value = "native")]
51
+ target: String,
52
+ #[arg(long, default_value = "rust")]
53
+ native_backend: String,
54
+ #[arg(long = "feature", action = clap::ArgAction::Append)]
55
+ features: Vec<String>,
56
+ #[arg(long)]
57
+ no_optimize: bool,
58
+ /// JS target only: `lattish` (default), `vdom` (vnode + patch; use with Lattish createRoot).
59
+ #[arg(long = "jsx", value_name = "MODE", default_value = "lattish")]
60
+ jsx_mode: String,
61
+ #[arg(required = true)]
62
+ file: String,
63
+ }
64
+
65
+ #[derive(Subcommand)]
66
+ pub(crate) enum Commands {
67
+ /// Run a Tish file (interpret)
68
+ Run(RunArgs),
69
+ /// Interactive REPL
70
+ Repl(ReplArgs),
71
+ /// Compile to native binary or JavaScript
72
+ Compile(CompileArgs),
73
+ /// Parse and dump AST
74
+ #[command(name = "dump-ast")]
75
+ DumpAst {
76
+ #[arg(required = true)]
77
+ file: String,
78
+ },
79
+ }
80
+
81
+ fn main() {
82
+ let cli = Cli::parse();
83
+ let no_opt_env = std::env::var_os("TISH_NO_OPTIMIZE")
84
+ .map(|v| v == "1" || v == "true" || v == "yes")
85
+ .unwrap_or(false);
86
+ let result = match cli.command {
87
+ Some(Commands::Run(a)) => run_file(&a.file, &a.backend, &a.features, a.no_optimize || no_opt_env),
88
+ Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env),
89
+ Some(Commands::Compile(a)) => compile_file(
90
+ &a.file,
91
+ &a.output,
92
+ &a.target,
93
+ &a.native_backend,
94
+ &a.features,
95
+ a.no_optimize || no_opt_env,
96
+ &a.jsx_mode,
97
+ ),
98
+ Some(Commands::DumpAst { file }) => dump_ast(&file),
99
+ None => run_repl("vm", false), // No args = REPL
100
+ };
101
+
102
+ if let Err(e) = result {
103
+ eprintln!("Error: {}", e);
104
+ std::process::exit(1);
105
+ }
106
+ }
107
+
108
+ fn run_file(path: &str, backend: &str, _features: &[String], no_optimize: bool) -> Result<(), String> {
109
+ let path = Path::new(path).canonicalize().map_err(|e| format!("Cannot resolve {}: {}", path, e))?;
110
+ let project_root = path.parent().and_then(|p| {
111
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
112
+ p.parent()
113
+ } else {
114
+ Some(p)
115
+ }
116
+ });
117
+
118
+ let program = if path.extension().map(|e| e == "js") == Some(true) {
119
+ let prog = js_to_tish::convert(&fs::read_to_string(&path).map_err(|e| format!("{}", e))?)
120
+ .map_err(|e| format!("{}", e))?;
121
+ if no_optimize {
122
+ prog
123
+ } else {
124
+ tish_opt::optimize(&prog)
125
+ }
126
+ } else {
127
+ let modules = tish_compile::resolve_project(&path, project_root)?;
128
+ tish_compile::detect_cycles(&modules)?;
129
+ let prog = tish_compile::merge_modules(modules)?;
130
+ if no_optimize {
131
+ prog
132
+ } else {
133
+ tish_opt::optimize(&prog)
134
+ }
135
+ };
136
+
137
+ if backend == "interp" {
138
+ let mut eval = tish_eval::Evaluator::new();
139
+ let value = eval.eval_program(&program)?;
140
+ if !matches!(value, tish_eval::Value::Null) {
141
+ println!("{}", tish_eval::format_value_for_console(&value, tish_core::use_console_colors()));
142
+ }
143
+ return Ok(());
144
+ }
145
+
146
+ // VM backend (bytecode) - supports native imports when built with fs/http/process features
147
+ let chunk = if no_optimize {
148
+ tish_bytecode::compile_unoptimized(&program).map_err(|e| e.to_string())?
149
+ } else {
150
+ tish_bytecode::compile(&program).map_err(|e| e.to_string())?
151
+ };
152
+ let value = tish_vm::run(&chunk)?;
153
+ if !matches!(value, tish_core::Value::Null) {
154
+ println!("{}", tish_core::format_value_styled(&value, tish_core::use_console_colors()));
155
+ }
156
+ Ok(())
157
+ }
158
+
159
+ fn run_repl(backend: &str, no_optimize: bool) -> Result<(), String> {
160
+ println!("Tish REPL (Ctrl-D to exit)");
161
+ let mut buffer = String::new();
162
+
163
+ if backend == "interp" {
164
+ let mut eval = tish_eval::Evaluator::new();
165
+ let mut multiline = String::new();
166
+ loop {
167
+ let prompt = repl_prompt(multiline.is_empty());
168
+ print!("{}", prompt);
169
+ io::stdout().flush().map_err(|e| e.to_string())?;
170
+ buffer.clear();
171
+ if io::stdin().read_line(&mut buffer).map_err(|e| e.to_string())? == 0 {
172
+ if !multiline.is_empty() {
173
+ let _ = tish_parser::parse(multiline.trim());
174
+ }
175
+ break;
176
+ }
177
+ let line = buffer.trim_end();
178
+ if multiline.is_empty() && line.is_empty() {
179
+ continue;
180
+ }
181
+ if multiline.is_empty() {
182
+ multiline = line.to_string();
183
+ } else {
184
+ multiline.push('\n');
185
+ multiline.push_str(line);
186
+ }
187
+ match tish_parser::parse(multiline.trim()) {
188
+ Ok(program) => {
189
+ match eval.eval_program(&program) {
190
+ Ok(v) => {
191
+ if !matches!(v, tish_eval::Value::Null) {
192
+ println!("{}", tish_eval::format_value_for_console(&v, tish_core::use_console_colors()));
193
+ }
194
+ }
195
+ Err(e) => eprintln!("{}", e),
196
+ }
197
+ multiline.clear();
198
+ }
199
+ Err(e) => {
200
+ if e.to_lowercase().contains("eof") {
201
+ // Incomplete: keep reading
202
+ } else {
203
+ eprintln!("Parse error: {}", e);
204
+ multiline.clear();
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return Ok(());
210
+ }
211
+
212
+ // VM backend with tab completion (e.g. a. -> properties/methods)
213
+ if !std::io::stdin().is_terminal() {
214
+ eprintln!("Note: Tab completion and grey preview require an interactive terminal (TTY).");
215
+ }
216
+ let vm = Rc::new(RefCell::new(tish_vm::Vm::new()));
217
+ let completer = repl_completion::ReplCompleter {
218
+ vm: Rc::clone(&vm),
219
+ no_optimize,
220
+ };
221
+ let config = Config::builder()
222
+ .completion_type(CompletionType::List)
223
+ .completion_show_all_if_ambiguous(true)
224
+ .color_mode(ColorMode::Forced)
225
+ .behavior(Behavior::PreferTerm)
226
+ .build();
227
+ let mut rl: Editor<repl_completion::ReplCompleter, _> =
228
+ Editor::with_config(config).map_err(|e| e.to_string())?;
229
+ rl.set_helper(Some(completer));
230
+
231
+ if let Some(ref path) = tish_history_path() {
232
+ let _ = rl.load_history(path);
233
+ }
234
+
235
+ println!("Tab after 'obj.' for completions (grey preview); press Tab again for full list.");
236
+ println!("Multi-line: type until the statement is complete; use ... continuation prompt.");
237
+
238
+ let mut buffer = String::new();
239
+
240
+ loop {
241
+ let prompt = repl_prompt(buffer.is_empty());
242
+ let line = match rl.readline(&prompt) {
243
+ Ok(l) => l,
244
+ Err(rustyline::error::ReadlineError::Eof) => {
245
+ if buffer.is_empty() {
246
+ break;
247
+ }
248
+ match tish_parser::parse(buffer.trim()) {
249
+ Ok(program) => {
250
+ let compile_fn = if no_optimize {
251
+ tish_bytecode::compile_for_repl_unoptimized
252
+ } else {
253
+ tish_bytecode::compile_for_repl
254
+ };
255
+ if let Ok(chunk) = compile_fn(&program) {
256
+ let _ = vm.borrow_mut().run(&chunk);
257
+ }
258
+ }
259
+ Err(e) => eprintln!("Parse error: {}", e),
260
+ }
261
+ break;
262
+ }
263
+ Err(rustyline::error::ReadlineError::Interrupted) => {
264
+ buffer.clear();
265
+ continue;
266
+ }
267
+ Err(e) => return Err(e.to_string()),
268
+ };
269
+ let line = line.trim_end();
270
+ if buffer.is_empty() && line.is_empty() {
271
+ continue;
272
+ }
273
+ if buffer.is_empty() {
274
+ buffer = line.to_string();
275
+ } else {
276
+ buffer.push('\n');
277
+ buffer.push_str(line);
278
+ }
279
+ match tish_parser::parse(buffer.trim()) {
280
+ Ok(program) => {
281
+ let compile_fn = if no_optimize {
282
+ tish_bytecode::compile_for_repl_unoptimized
283
+ } else {
284
+ tish_bytecode::compile_for_repl
285
+ };
286
+ match compile_fn(&program) {
287
+ Ok(chunk) => {
288
+ match vm.borrow_mut().run(&chunk) {
289
+ Ok(v) => {
290
+ if !matches!(v, tish_core::Value::Null) {
291
+ println!("{}", tish_core::format_value_styled(&v, tish_core::use_console_colors()));
292
+ }
293
+ }
294
+ Err(e) => eprintln!("{}", e),
295
+ }
296
+ }
297
+ Err(e) => eprintln!("Compile error: {}", e),
298
+ }
299
+ let _ = rl.add_history_entry(buffer.trim());
300
+ buffer.clear();
301
+ }
302
+ Err(e) => {
303
+ if e.to_lowercase().contains("eof") {
304
+ // Incomplete: keep accumulating (Python-style ... prompt)
305
+ } else {
306
+ eprintln!("Parse error: {}", e);
307
+ buffer.clear();
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ if let Some(ref path) = tish_history_path() {
314
+ let _ = rl.save_history(path);
315
+ }
316
+ Ok(())
317
+ }
318
+
319
+ /// REPL prompt with green caret when stdout is a TTY (platform-style).
320
+ fn repl_prompt(primary: bool) -> String {
321
+ if tish_core::use_console_colors() {
322
+ if primary {
323
+ "\x1b[32m> \x1b[0m".to_string()
324
+ } else {
325
+ "\x1b[32m... \x1b[0m".to_string()
326
+ }
327
+ } else if primary {
328
+ "> ".to_string()
329
+ } else {
330
+ "... ".to_string()
331
+ }
332
+ }
333
+
334
+ /// Path to REPL history file (Python-style: ~/.tish_history).
335
+ fn tish_history_path() -> Option<PathBuf> {
336
+ let home = std::env::var_os("HOME")
337
+ .or_else(|| std::env::var_os("USERPROFILE"));
338
+ home.map(|h| PathBuf::from(h).join(".tish_history"))
339
+ }
340
+
341
+ fn parse_jsx_mode(s: &str) -> Result<tish_compile_js::JsxMode, String> {
342
+ match s {
343
+ "legacy" => Err(
344
+ "--jsx legacy was removed. Use --jsx lattish (default) with lattish merged into your \
345
+ bundle, or --jsx vdom with Lattish's createRoot."
346
+ .to_string(),
347
+ ),
348
+ "vdom" => Ok(tish_compile_js::JsxMode::Vdom),
349
+ "lattish" => Ok(tish_compile_js::JsxMode::LattishH),
350
+ other => Err(format!(
351
+ "Unknown --jsx {:?}: use lattish (default) or vdom.",
352
+ other
353
+ )),
354
+ }
355
+ }
356
+
357
+ fn compile_to_js(
358
+ input_path: &Path,
359
+ output_path: &str,
360
+ optimize: bool,
361
+ jsx: &str,
362
+ ) -> Result<(), String> {
363
+ let jsx_mode = parse_jsx_mode(jsx)?;
364
+ let project_root = input_path.parent().and_then(|p| {
365
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
366
+ p.parent()
367
+ } else {
368
+ Some(p)
369
+ }
370
+ });
371
+ let js = if input_path.extension().map(|e| e == "jsx") == Some(true) {
372
+ let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
373
+ let wrapped = format!(
374
+ "export fn __TishJsxRoot() {{\n return (\n{}\n )\n}}",
375
+ source.trim()
376
+ );
377
+ let program = tish_parser::parse(&wrapped)
378
+ .map_err(|e| format!("JSX wrapper parse: {}", e))?;
379
+ let p = if optimize {
380
+ tish_opt::optimize(&program)
381
+ } else {
382
+ program
383
+ };
384
+ tish_compile_js::compile_with_jsx(&p, optimize, jsx_mode).map_err(|e| format!("{}", e))?
385
+ } else if input_path.extension().map(|e| e == "js") == Some(true) {
386
+ let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
387
+ let program = js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
388
+ tish_compile_js::compile_with_jsx(&program, optimize, jsx_mode).map_err(|e| format!("{}", e))?
389
+ } else {
390
+ tish_compile_js::compile_project_with_jsx(input_path, project_root, optimize, jsx_mode)
391
+ .map_err(|e| format!("{}", e))?
392
+ };
393
+
394
+ let out_path = Path::new(output_path);
395
+ let out_path = if out_path.extension().is_none()
396
+ || out_path.extension() == Some(std::ffi::OsStr::new(""))
397
+ {
398
+ out_path.with_extension("js")
399
+ } else {
400
+ out_path.to_path_buf()
401
+ };
402
+
403
+ if let Some(parent) = out_path.parent() {
404
+ fs::create_dir_all(parent)
405
+ .map_err(|e| format!("Cannot create output directory {}: {}", parent.display(), e))?;
406
+ }
407
+ fs::write(&out_path, js).map_err(|e| format!("Cannot write {}: {}", out_path.display(), e))?;
408
+ println!("Built: {}", out_path.display());
409
+ Ok(())
410
+ }
411
+
412
+ #[allow(clippy::vec_init_then_push)]
413
+ fn compile_file(
414
+ input_path: &str,
415
+ output_path: &str,
416
+ target: &str,
417
+ native_backend: &str,
418
+ cli_features: &[String],
419
+ no_optimize: bool,
420
+ jsx: &str,
421
+ ) -> Result<(), String> {
422
+ let optimize = !no_optimize;
423
+ let input_path =
424
+ Path::new(input_path).canonicalize().map_err(|e| format!("Cannot resolve {}: {}", input_path, e))?;
425
+
426
+ let is_js = input_path.extension().map(|e| e == "js") == Some(true);
427
+
428
+ if target == "js" {
429
+ return compile_to_js(&input_path, output_path, optimize, jsx.trim());
430
+ }
431
+
432
+ if target == "wasm" && is_js {
433
+ let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
434
+ let program = js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
435
+ return tish_wasm::compile_program_to_wasm(&program, Path::new(output_path), optimize)
436
+ .map_err(|e| format!("{}", e));
437
+ }
438
+
439
+ if target == "wasm" {
440
+ let project_root = input_path.parent().and_then(|p| {
441
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
442
+ p.parent()
443
+ } else {
444
+ Some(p)
445
+ }
446
+ });
447
+ return tish_wasm::compile_to_wasm(&input_path, project_root, Path::new(output_path), optimize)
448
+ .map_err(|e| e.to_string());
449
+ }
450
+
451
+ if target == "wasi" {
452
+ let project_root = input_path.parent().and_then(|p| {
453
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
454
+ p.parent()
455
+ } else {
456
+ Some(p)
457
+ }
458
+ });
459
+ return tish_wasm::compile_to_wasi(&input_path, project_root, Path::new(output_path), optimize)
460
+ .map_err(|e| e.to_string());
461
+ }
462
+
463
+ if target != "native" {
464
+ return Err(format!(
465
+ "Unknown target: {}. Use 'native', 'js', 'wasm', or 'wasi'.",
466
+ target
467
+ ));
468
+ }
469
+
470
+ let project_root = input_path.parent().map(|p| {
471
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
472
+ p.parent().unwrap_or(p)
473
+ } else {
474
+ p
475
+ }
476
+ });
477
+ let features: Vec<String> = if cli_features.is_empty() {
478
+ #[allow(unused_mut)]
479
+ let mut f = Vec::new();
480
+ #[cfg(feature = "http")]
481
+ f.push("http".to_string());
482
+ #[cfg(feature = "fs")]
483
+ f.push("fs".to_string());
484
+ #[cfg(feature = "process")]
485
+ f.push("process".to_string());
486
+ #[cfg(feature = "regex")]
487
+ f.push("regex".to_string());
488
+ #[cfg(feature = "ws")]
489
+ f.push("ws".to_string());
490
+ f
491
+ } else {
492
+ cli_features.to_vec()
493
+ };
494
+
495
+ if is_js {
496
+ let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
497
+ let program = js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
498
+ tish_native::compile_program_to_native(
499
+ &program,
500
+ project_root,
501
+ Path::new(output_path),
502
+ &features,
503
+ native_backend,
504
+ optimize,
505
+ )
506
+ .map_err(|e| e.to_string())?;
507
+ } else {
508
+ tish_native::compile_to_native(
509
+ &input_path,
510
+ project_root,
511
+ Path::new(output_path),
512
+ &features,
513
+ native_backend,
514
+ optimize,
515
+ )
516
+ .map_err(|e| e.to_string())?;
517
+ }
518
+
519
+ let out_name = Path::new(output_path)
520
+ .file_stem()
521
+ .and_then(|s| s.to_str())
522
+ .unwrap_or("tish_out");
523
+ let built_path = if output_path.ends_with('/') || Path::new(output_path).is_dir() {
524
+ Path::new(output_path).join(out_name)
525
+ } else {
526
+ Path::new(output_path).to_path_buf()
527
+ };
528
+ println!("Built: {}", built_path.display());
529
+ Ok(())
530
+ }
531
+
532
+
533
+
534
+ #[cfg(test)]
535
+ mod cli_tests {
536
+ use clap::Parser;
537
+
538
+ use super::{Cli, Commands};
539
+
540
+ #[test]
541
+ fn compile_jsx_defaults_to_lattish() {
542
+ let cli = Cli::try_parse_from([
543
+ "tish",
544
+ "compile",
545
+ "m.tish",
546
+ "--target",
547
+ "js",
548
+ "-o",
549
+ "x.js",
550
+ ])
551
+ .unwrap();
552
+ match cli.command {
553
+ Some(Commands::Compile(a)) => assert_eq!(a.jsx_mode.as_str(), "lattish"),
554
+ _ => panic!("expected Compile"),
555
+ }
556
+ }
557
+
558
+ #[test]
559
+ fn compile_jsx_flag_vdom() {
560
+ let cli = Cli::try_parse_from([
561
+ "tish",
562
+ "compile",
563
+ "a.tish",
564
+ "--target",
565
+ "js",
566
+ "--jsx",
567
+ "vdom",
568
+ "-o",
569
+ "x.js",
570
+ ])
571
+ .unwrap();
572
+ match cli.command {
573
+ Some(Commands::Compile(a)) => assert_eq!(a.jsx_mode.as_str(), "vdom"),
574
+ _ => panic!("expected Compile"),
575
+ }
576
+ }
577
+ }
578
+
579
+ fn dump_ast(path: &str) -> Result<(), String> {
580
+ let source =
581
+ fs::read_to_string(path).map_err(|e| format!("Cannot read {}: {}", path, e))?;
582
+ let program = tish_parser::parse(&source)?;
583
+ println!("{:#?}", program);
584
+ Ok(())
585
+ }