@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,2 @@
1
+ let cmd = "a"
2
+ if (cmd === "a" || cmd === "b") { console.log("hit") } else { console.log("miss") }
@@ -0,0 +1,34 @@
1
+ //! `DeclareVar` + block scopes: function `let` shadows script-level names (bytecode VM).
2
+
3
+ use tishlang_bytecode::compile;
4
+ use tishlang_vm::run;
5
+
6
+ #[test]
7
+ fn declare_var_shadows_script_let_inside_fn() {
8
+ let src = r#"
9
+ let x = 1
10
+ fn f() {
11
+ let x = 2
12
+ return x
13
+ }
14
+ let r = f()
15
+ console.log("script", x, "fn", r)
16
+ "#;
17
+ let program = tishlang_parser::parse(src).expect("parse");
18
+ let chunk = compile(&program).expect("compile");
19
+ run(&chunk).expect("run");
20
+ }
21
+
22
+ #[test]
23
+ fn block_let_restores_outer_binding() {
24
+ let src = r#"
25
+ let x = 1
26
+ {
27
+ let x = 2
28
+ }
29
+ console.log(x)
30
+ "#;
31
+ let program = tishlang_parser::parse(src).expect("parse");
32
+ let chunk = compile(&program).expect("compile");
33
+ run(&chunk).expect("run");
34
+ }
@@ -0,0 +1,150 @@
1
+ //! Regression: bytecode peephole `chain_jumps` must not follow `JumpIfFalse` as if it were an
2
+ //! unconditional `Jump`. Doing so broke `===` + `||` when nested as the condition of an outer `if`
3
+ //! (default VM differed from `--backend interp` / `--no-optimize`).
4
+ //!
5
+ //! CLI parity for the same source is covered in `crates/tish/tests/run_optimize_stdout_parity.rs`.
6
+
7
+ use std::path::PathBuf;
8
+
9
+ use tishlang_bytecode::{
10
+ compile, compile_for_repl, compile_for_repl_unoptimized, compile_unoptimized,
11
+ };
12
+ use tishlang_core::Value;
13
+
14
+ fn run_chunk(chunk: &tishlang_bytecode::Chunk) -> Value {
15
+ tishlang_vm::run(chunk).expect("vm run")
16
+ }
17
+
18
+ /// `tish run` ends with trailing `null` when the last statement is not a REPL-style expr; use
19
+ /// `compile_for_repl` so the VM return value reflects the `||` result (catches peephole/AST bugs).
20
+ #[test]
21
+ fn string_strict_eq_logical_or_repl_last_expr_is_true() {
22
+ let src = "let cmd = \"a\"\ncmd === \"a\" || cmd === \"b\"";
23
+ let opt = tishlang_opt::optimize(&tishlang_parser::parse(src).expect("parse"));
24
+ let v_peep = run_chunk(&compile_for_repl(&opt).expect("compile repl"));
25
+ let v_unopt = run_chunk(&compile_for_repl_unoptimized(&opt).expect("compile repl unopt"));
26
+ assert!(
27
+ v_peep.strict_eq(&v_unopt),
28
+ "peephole vs unopt repl: peep={v_peep:?} unopt={v_unopt:?}"
29
+ );
30
+ assert!(
31
+ matches!(&v_peep, Value::Bool(true)),
32
+ "expected true for cmd===a||cmd===b with cmd=a, got {v_peep:?}"
33
+ );
34
+ }
35
+
36
+ /// `?:` uses different codegen than `if`; both must agree with unoptimized bytecode.
37
+ #[test]
38
+ fn string_strict_eq_logical_or_inside_ternary_repl_last_expr() {
39
+ // Statement boundary: without `;` or `;`-like ASI, the parser can tie the `(` line to `let`.
40
+ let src = "let cmd = \"a\"\n;(cmd === \"a\" || cmd === \"b\") ? 1 : 0";
41
+ let opt = tishlang_opt::optimize(&tishlang_parser::parse(src).expect("parse"));
42
+ let v_peep = run_chunk(&compile_for_repl(&opt).expect("compile repl"));
43
+ let v_unopt = run_chunk(&compile_for_repl_unoptimized(&opt).expect("compile repl unopt"));
44
+ assert!(
45
+ v_peep.strict_eq(&v_unopt),
46
+ "peep={v_peep:?} unopt={v_unopt:?}"
47
+ );
48
+ assert!(
49
+ matches!(&v_peep, Value::Number(n) if *n == 1.0),
50
+ "expected 1, got {v_peep:?}"
51
+ );
52
+ }
53
+
54
+ #[test]
55
+ fn logical_or_strict_eq_peephole_matches_unoptimized() {
56
+ let src = "let a = 1\nlet b = 2\na === 1 || b === 2";
57
+ let program = tishlang_parser::parse(src).expect("parse");
58
+ let program = tishlang_opt::optimize(&program);
59
+
60
+ let v_peep = run_chunk(&compile(&program).expect("compile"));
61
+ let v_raw = run_chunk(&compile_unoptimized(&program).expect("compile unopt"));
62
+ assert!(
63
+ v_peep.strict_eq(&v_raw),
64
+ "peephole changed semantics: peep={v_peep:?} raw={v_raw:?}"
65
+ );
66
+
67
+ let v_peep_repl = run_chunk(&compile_for_repl(&program).expect("compile repl"));
68
+ let v_raw_repl =
69
+ run_chunk(&compile_for_repl_unoptimized(&program).expect("compile repl unopt"));
70
+ assert!(
71
+ v_peep_repl.strict_eq(&v_raw_repl),
72
+ "repl: peep={v_peep_repl:?} raw={v_raw_repl:?}"
73
+ );
74
+ }
75
+
76
+ #[test]
77
+ fn logical_or_inside_if_condition_peephole_matches_unoptimized() {
78
+ let src = "let a = 1\nlet b = 2\nif (a === 1 || b === 2) { 1 } else { 0 }";
79
+ let program = tishlang_parser::parse(src).expect("parse");
80
+ let program = tishlang_opt::optimize(&program);
81
+
82
+ let v_peep = run_chunk(&compile(&program).expect("compile"));
83
+ let v_raw = run_chunk(&compile_unoptimized(&program).expect("compile unopt"));
84
+ assert!(
85
+ v_peep.strict_eq(&v_raw),
86
+ "if + || : peep={v_peep:?} raw={v_raw:?}"
87
+ );
88
+ }
89
+
90
+ #[test]
91
+ fn string_strict_eq_logical_or_ast_opt_matches_unoptimized_bytecode() {
92
+ let src = "let cmd = \"a\"\nif (cmd === \"a\" || cmd === \"b\") { 1 } else { 0 }";
93
+ let raw = tishlang_parser::parse(src).expect("parse");
94
+ let opt = tishlang_opt::optimize(&raw);
95
+ let v_raw = run_chunk(&compile_unoptimized(&raw).expect("raw"));
96
+ let v_opt = run_chunk(&compile_unoptimized(&opt).expect("opt"));
97
+ assert!(
98
+ v_raw.strict_eq(&v_opt),
99
+ "AST optimizer changed semantics: raw={v_raw:?} opt={v_opt:?}"
100
+ );
101
+ }
102
+
103
+ #[test]
104
+ fn string_strict_eq_logical_or_peephole_matches_unoptimized() {
105
+ let src = "let cmd = \"a\"\nif (cmd === \"a\" || cmd === \"b\") { 1 } else { 0 }";
106
+ let program = tishlang_opt::optimize(&tishlang_parser::parse(src).expect("parse"));
107
+ let v_peep = run_chunk(&compile(&program).expect("compile"));
108
+ let v_raw = run_chunk(&compile_unoptimized(&program).expect("unopt"));
109
+ assert!(
110
+ v_peep.strict_eq(&v_raw),
111
+ "peephole + strings: peep={v_peep:?} raw={v_raw:?}"
112
+ );
113
+ }
114
+
115
+ /// `tish run path/to/file.tish` uses merge_modules; ensure that matches plain parse for the fixture.
116
+ #[test]
117
+ fn merged_module_program_bytecode_matches_parse_for_string_or_fixture() {
118
+ let fixture =
119
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/or_string_cmd.tish");
120
+ let src = std::fs::read_to_string(&fixture).expect("read fixture");
121
+ let modules = tishlang_compile::resolve_project(&fixture, Some(fixture.parent().unwrap()))
122
+ .expect("resolve");
123
+ let merged = tishlang_compile::merge_modules(modules)
124
+ .expect("merge")
125
+ .program;
126
+ let flat = tishlang_parser::parse(&src).expect("parse");
127
+ let m_opt = tishlang_opt::optimize(&merged);
128
+ let f_opt = tishlang_opt::optimize(&flat);
129
+ let c_m = compile(&m_opt).expect("compile merged");
130
+ let c_f = compile(&f_opt).expect("compile flat");
131
+ assert_eq!(
132
+ c_m.code, c_f.code,
133
+ "merge_modules vs parse produced different bytecode"
134
+ );
135
+ }
136
+
137
+ /// `if (cmd === "a" || cmd === "b")` must match unoptimized VM semantics (Nop padding from other
138
+ /// peepholes must not confuse `chain_jumps`).
139
+ #[test]
140
+ fn string_eq_or_in_if_stmt_matches_unoptimized_repl() {
141
+ let src = "let cmd = \"a\"\nlet ok = false\nif (cmd === \"a\" || cmd === \"b\") { ok = true } else { ok = false }\nok";
142
+ let program = tishlang_opt::optimize(&tishlang_parser::parse(src).expect("parse"));
143
+ let v_peep = run_chunk(&compile_for_repl(&program).expect("compile repl"));
144
+ let v_raw = run_chunk(&compile_for_repl_unoptimized(&program).expect("compile repl unopt"));
145
+ assert!(v_peep.strict_eq(&v_raw), "peep={v_peep:?} raw={v_raw:?}");
146
+ assert!(
147
+ matches!(&v_peep, Value::Bool(true)),
148
+ "expected ok=true, got {v_peep:?}"
149
+ );
150
+ }
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "tishlang_wasm"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "WebAssembly backend for Tish - compiles to real .wasm"
6
+
7
+ license-file = { workspace = true }
8
+ repository = { workspace = true }
9
+ [dependencies]
10
+ tishlang_build_utils = { path = "../tish_build_utils", version = ">=0.1" }
11
+ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
12
+ tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
13
+ tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
14
+ tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
15
+ base64 = "0.22"
@@ -0,0 +1,424 @@
1
+ //! WebAssembly backend for Tish.
2
+ //!
3
+ //! Compiles Tish to bytecode, then produces a .wasm VM binary + loader.
4
+ //! The VM runs in the browser; your program runs as serialized bytecode.
5
+
6
+ use std::collections::BTreeSet;
7
+ use std::path::Path;
8
+ use std::process::Command;
9
+
10
+ use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
11
+
12
+ use tishlang_ast::Program;
13
+ use tishlang_bytecode::{serialize, Chunk};
14
+ use tishlang_compile::{
15
+ detect_cycles, extract_native_import_features, has_external_native_imports, merge_modules,
16
+ resolve_project,
17
+ };
18
+
19
+ /// Error from WASM compilation.
20
+ #[derive(Debug)]
21
+ pub struct WasmError {
22
+ pub message: String,
23
+ }
24
+
25
+ impl std::fmt::Display for WasmError {
26
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27
+ write!(f, "{}", self.message)
28
+ }
29
+ }
30
+
31
+ impl std::error::Error for WasmError {}
32
+
33
+ /// Map CLI / import capability names to `tishlang_wasm_runtime` Cargo features for wasm32-wasip1.
34
+ /// The full `http` stack (tokio/socket2/…) does not build on WASI here; `http` maps to `promise`
35
+ /// so `Promise` / `await` work. `ws` is skipped for the same reason.
36
+ fn insert_wasi_runtime_cap(out: &mut BTreeSet<String>, cap: &str) {
37
+ match cap {
38
+ "http" => {
39
+ out.insert("promise".to_string());
40
+ }
41
+ "ws" => {}
42
+ "fs" | "process" | "promise" | "timers" | "regex" => {
43
+ out.insert(cap.to_string());
44
+ }
45
+ _ => {}
46
+ }
47
+ }
48
+
49
+ /// Resolve project, merge modules, and compile to bytecode chunk.
50
+ /// Returns (Chunk, Program) so WASI can extract features for the runtime.
51
+ fn resolve_and_compile_to_chunk(
52
+ entry_path: &Path,
53
+ project_root: Option<&Path>,
54
+ optimize: bool,
55
+ ) -> Result<(Chunk, Program), WasmError> {
56
+ let modules = resolve_project(entry_path, project_root).map_err(|e| WasmError {
57
+ message: e.to_string(),
58
+ })?;
59
+ detect_cycles(&modules).map_err(|e| WasmError {
60
+ message: e.to_string(),
61
+ })?;
62
+ let program = {
63
+ let prog = merge_modules(modules)
64
+ .map(|m| m.program)
65
+ .map_err(|e| WasmError {
66
+ message: e.to_string(),
67
+ })?;
68
+ if optimize {
69
+ tishlang_opt::optimize(&prog)
70
+ } else {
71
+ prog
72
+ }
73
+ };
74
+ let chunk = if optimize {
75
+ tishlang_bytecode::compile(&program).map_err(|e| WasmError {
76
+ message: e.to_string(),
77
+ })?
78
+ } else {
79
+ tishlang_bytecode::compile_unoptimized(&program).map_err(|e| WasmError {
80
+ message: e.to_string(),
81
+ })?
82
+ };
83
+ Ok((chunk, program))
84
+ }
85
+
86
+ /// Compile a single Program (e.g. from tishlang_js_to_tish) for WebAssembly.
87
+ pub fn compile_program_to_wasm(
88
+ program: &Program,
89
+ output_path: &Path,
90
+ optimize: bool,
91
+ ) -> Result<(), WasmError> {
92
+ let program = if optimize {
93
+ tishlang_opt::optimize(program)
94
+ } else {
95
+ program.clone()
96
+ };
97
+ let chunk = if optimize {
98
+ tishlang_bytecode::compile(&program).map_err(|e| WasmError {
99
+ message: e.to_string(),
100
+ })?
101
+ } else {
102
+ tishlang_bytecode::compile_unoptimized(&program).map_err(|e| WasmError {
103
+ message: e.to_string(),
104
+ })?
105
+ };
106
+ emit_wasm_from_chunk(&chunk, output_path)
107
+ }
108
+
109
+ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmError> {
110
+ let chunk_bytes = serialize(chunk);
111
+ let chunk_b64 = BASE64.encode(&chunk_bytes);
112
+ let stem = output_path
113
+ .file_stem()
114
+ .and_then(|s| s.to_str())
115
+ .unwrap_or("main");
116
+ let out_dir = output_path
117
+ .parent()
118
+ .filter(|p| !p.as_os_str().is_empty())
119
+ .unwrap_or(Path::new("."));
120
+ let out_dir_abs = out_dir
121
+ .canonicalize()
122
+ .or_else(|_| std::env::current_dir().map(|cwd| cwd.join(out_dir)))
123
+ .map_err(|e| WasmError {
124
+ message: format!("Cannot resolve output dir: {}", e),
125
+ })?;
126
+ std::fs::create_dir_all(&out_dir_abs).map_err(|e| WasmError {
127
+ message: format!("Cannot create output directory: {}", e),
128
+ })?;
129
+ let workspace_root =
130
+ tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
131
+ let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
132
+ let build_status = Command::new(&cargo)
133
+ .current_dir(&workspace_root)
134
+ .args([
135
+ "build",
136
+ "-p",
137
+ "tishlang_wasm_runtime",
138
+ "--target",
139
+ "wasm32-unknown-unknown",
140
+ "--release",
141
+ "--features",
142
+ "browser",
143
+ ])
144
+ .status()
145
+ .map_err(|e| WasmError {
146
+ message: format!("Failed to run cargo: {}", e),
147
+ })?;
148
+ if !build_status.success() {
149
+ return Err(WasmError {
150
+ message: "Failed to build wasm runtime. Run: rustup target add wasm32-unknown-unknown"
151
+ .to_string(),
152
+ });
153
+ }
154
+ let wasm_artifact =
155
+ workspace_root.join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
156
+ if !wasm_artifact.exists() {
157
+ return Err(WasmError {
158
+ message: format!("Wasm artifact not found: {}", wasm_artifact.display()),
159
+ });
160
+ }
161
+ let wasm_bindgen = std::env::var("WASM_BINDGEN").unwrap_or_else(|_| "wasm-bindgen".to_string());
162
+ let out_name = stem.to_string();
163
+ let bindgen_status = Command::new(&wasm_bindgen)
164
+ .args([
165
+ "--target",
166
+ "web",
167
+ "--out-dir",
168
+ out_dir_abs.to_str().unwrap(),
169
+ "--out-name",
170
+ &out_name,
171
+ wasm_artifact.to_str().unwrap(),
172
+ ])
173
+ .status()
174
+ .map_err(|e| WasmError {
175
+ message: format!(
176
+ "Failed to run wasm-bindgen: {}. Install with: cargo install wasm-bindgen-cli",
177
+ e
178
+ ),
179
+ })?;
180
+ if !bindgen_status.success() {
181
+ return Err(WasmError {
182
+ message: "wasm-bindgen failed".to_string(),
183
+ });
184
+ }
185
+ let js_name = format!("{}.js", stem);
186
+ let html = format!(
187
+ r#"<!DOCTYPE html>
188
+ <html>
189
+ <head><meta charset="utf-8"><title>{}</title></head>
190
+ <body>
191
+ <script type="module">
192
+ const CHUNK_B64 = "{}";
193
+ const chunk = Uint8Array.from(atob(CHUNK_B64), c => c.charCodeAt(0));
194
+ import init, {{ run }} from './{}';
195
+ await init();
196
+ run(chunk);
197
+ </script>
198
+ </body>
199
+ </html>
200
+ "#,
201
+ stem, chunk_b64, js_name
202
+ );
203
+ let html_path = out_dir_abs.join(format!("{}.html", stem));
204
+ std::fs::write(&html_path, html).map_err(|e| WasmError {
205
+ message: format!("Cannot write {}: {}", html_path.display(), e),
206
+ })?;
207
+ println!(
208
+ "Built: {}_bg.wasm, {}.js, {}",
209
+ stem,
210
+ stem,
211
+ html_path.display()
212
+ );
213
+ Ok(())
214
+ }
215
+
216
+ /// Compile a Tish project for WebAssembly.
217
+ ///
218
+ /// Produces:
219
+ /// - `{output}.wasm` — VM binary (runs your program)
220
+ /// - `{output}.js` — wasm-bindgen glue
221
+ /// - `{output}.html` — loader (open in browser)
222
+ ///
223
+ /// Requires: `rustup target add wasm32-unknown-unknown`, `wasm-bindgen-cli`
224
+ pub fn compile_to_wasm(
225
+ entry_path: &Path,
226
+ project_root: Option<&Path>,
227
+ output_path: &Path,
228
+ optimize: bool,
229
+ ) -> Result<(), WasmError> {
230
+ let (chunk, _) = resolve_and_compile_to_chunk(entry_path, project_root, optimize)?;
231
+ emit_wasm_from_chunk(&chunk, output_path)
232
+ }
233
+
234
+ /// Compile a Tish project to a raw serialized bytecode chunk.
235
+ ///
236
+ /// Writes a single `{output}` file of the exact bytes that the wasm/WASI runtime entry points
237
+ /// (`start` / `run`) deserialize directly — the same chunk `--target wasm` embeds as base64 in
238
+ /// its generated HTML loader, but written raw with no VM binary, JS glue, or HTML wrapper. Lets a
239
+ /// host that already ships the VM runtime (e.g. a bundler) consume the bytecode without the
240
+ /// throwaway standalone build.
241
+ pub fn compile_to_bytecode(
242
+ entry_path: &Path,
243
+ project_root: Option<&Path>,
244
+ output_path: &Path,
245
+ optimize: bool,
246
+ ) -> Result<(), WasmError> {
247
+ let (chunk, _) = resolve_and_compile_to_chunk(entry_path, project_root, optimize)?;
248
+ let bytes = serialize(&chunk);
249
+ if let Some(parent) = output_path.parent().filter(|p| !p.as_os_str().is_empty()) {
250
+ std::fs::create_dir_all(parent).map_err(|e| WasmError {
251
+ message: format!("Cannot create output directory: {}", e),
252
+ })?;
253
+ }
254
+ std::fs::write(output_path, &bytes).map_err(|e| WasmError {
255
+ message: format!("Cannot write {}: {}", output_path.display(), e),
256
+ })?;
257
+ println!("Built: {} ({} bytes)", output_path.display(), bytes.len());
258
+ Ok(())
259
+ }
260
+
261
+ /// Compile a Tish project for Wasmtime/WASI.
262
+ ///
263
+ /// Produces a single `{output}.wasm` with embedded bytecode. Run with:
264
+ /// `wasmtime {output}.wasm`
265
+ ///
266
+ /// Requires: `rustup target add wasm32-wasip1`
267
+ ///
268
+ /// `capabilities` is the same capability list as `tish build --target native` (e.g. from
269
+ /// `native_build_features_from_cli`): merged with `import`-inferred features so globals like
270
+ /// `Promise` / `fetch` work without a top-level `import … from 'http'`.
271
+ pub fn compile_to_wasi(
272
+ entry_path: &Path,
273
+ project_root: Option<&Path>,
274
+ output_path: &Path,
275
+ optimize: bool,
276
+ capabilities: &[String],
277
+ ) -> Result<(), WasmError> {
278
+ let (chunk, program) = resolve_and_compile_to_chunk(entry_path, project_root, optimize)?;
279
+ if has_external_native_imports(&program) {
280
+ return Err(WasmError {
281
+ message: "WASI backend does not support external native imports (tish:egui, @scope/pkg). Built-in tish:fs, tish:http, tish:process, tish:timers are supported.".to_string(),
282
+ });
283
+ }
284
+ let mut wasi_feature_set: BTreeSet<String> = BTreeSet::new();
285
+ for f in extract_native_import_features(&program) {
286
+ insert_wasi_runtime_cap(&mut wasi_feature_set, f.as_str());
287
+ }
288
+ for f in capabilities {
289
+ insert_wasi_runtime_cap(&mut wasi_feature_set, f.as_str());
290
+ }
291
+ // Many scripts use global setTimeout without `import` from timers.
292
+ wasi_feature_set.insert("timers".to_string());
293
+
294
+ let chunk_bytes = serialize(&chunk);
295
+
296
+ let stem = output_path
297
+ .file_stem()
298
+ .and_then(|s| s.to_str())
299
+ .unwrap_or("main");
300
+ let out_dir = output_path
301
+ .parent()
302
+ .filter(|p| !p.as_os_str().is_empty())
303
+ .unwrap_or(Path::new("."));
304
+ let out_dir_abs = out_dir
305
+ .canonicalize()
306
+ .or_else(|_| std::env::current_dir().map(|cwd| cwd.join(out_dir)))
307
+ .map_err(|e| WasmError {
308
+ message: format!("Cannot resolve output dir: {}", e),
309
+ })?;
310
+
311
+ std::fs::create_dir_all(&out_dir_abs).map_err(|e| WasmError {
312
+ message: format!("Cannot create output directory: {}", e),
313
+ })?;
314
+
315
+ let workspace_root =
316
+ tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
317
+
318
+ // Create generated project: wasi_build/{stem}/
319
+ let build_dir = out_dir_abs.join("wasi_build").join(stem);
320
+ std::fs::create_dir_all(build_dir.join("src")).map_err(|e| WasmError {
321
+ message: format!("Cannot create build dir: {}", e),
322
+ })?;
323
+
324
+ // Write chunk.bin
325
+ std::fs::write(build_dir.join("chunk.bin"), &chunk_bytes).map_err(|e| WasmError {
326
+ message: format!("Cannot write chunk: {}", e),
327
+ })?;
328
+
329
+ // Cargo.toml - path to tishlang_wasm_runtime (crate in crates/tish_wasm_runtime)
330
+ let runtime_path = workspace_root.join("crates").join("tish_wasm_runtime");
331
+ let runtime_path_str = runtime_path
332
+ .canonicalize()
333
+ .unwrap_or(runtime_path)
334
+ .to_string_lossy()
335
+ .replace('\\', "/");
336
+
337
+ let features_str = format!(
338
+ ", features = [{}]",
339
+ wasi_feature_set
340
+ .iter()
341
+ .map(|f| format!("{:?}", f))
342
+ .collect::<Vec<_>>()
343
+ .join(", ")
344
+ );
345
+ let cargo_toml = format!(
346
+ r#"[package]
347
+ name = "tish_wasi_{stem}"
348
+ version = "0.1.0"
349
+ edition = "2021"
350
+
351
+ [workspace]
352
+
353
+ [[bin]]
354
+ name = "tish_wasi_{stem}"
355
+ path = "src/main.rs"
356
+
357
+ [dependencies]
358
+ tishlang_wasm_runtime = {{ path = "{runtime_path_str}"{features_str} }}
359
+ "#,
360
+ stem = stem,
361
+ runtime_path_str = runtime_path_str,
362
+ features_str = features_str
363
+ );
364
+ std::fs::write(build_dir.join("Cargo.toml"), cargo_toml).map_err(|e| WasmError {
365
+ message: format!("Cannot write Cargo.toml: {}", e),
366
+ })?;
367
+
368
+ // main.rs
369
+ let main_rs = r#"
370
+ fn main() {
371
+ let chunk = include_bytes!("../chunk.bin");
372
+ if let Err(e) = tishlang_wasm_runtime::run_wasi(chunk) {
373
+ eprintln!("Runtime error: {}", e);
374
+ std::process::exit(1);
375
+ }
376
+ }
377
+ "#;
378
+ std::fs::write(build_dir.join("src").join("main.rs"), main_rs).map_err(|e| WasmError {
379
+ message: format!("Cannot write main.rs: {}", e),
380
+ })?;
381
+
382
+ // Build - use explicit target-dir so we know where the artifact is
383
+ let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
384
+ let bin_name = format!("tish_wasi_{}", stem);
385
+ let target_dir = build_dir.join("target");
386
+ let build_status = Command::new(&cargo)
387
+ .current_dir(&build_dir)
388
+ .env("CARGO_TARGET_DIR", &target_dir)
389
+ .args(["build", "--target", "wasm32-wasip1", "--release"])
390
+ .status()
391
+ .map_err(|e| WasmError {
392
+ message: format!("Failed to run cargo: {}", e),
393
+ })?;
394
+
395
+ if !build_status.success() {
396
+ return Err(WasmError {
397
+ message: "Failed to build WASI binary. Run: rustup target add wasm32-wasip1"
398
+ .to_string(),
399
+ });
400
+ }
401
+
402
+ let wasm_artifact = target_dir
403
+ .join("wasm32-wasip1")
404
+ .join("release")
405
+ .join(format!("{}.wasm", bin_name));
406
+
407
+ if !wasm_artifact.exists() {
408
+ return Err(WasmError {
409
+ message: format!("WASI artifact not found: {}", wasm_artifact.display()),
410
+ });
411
+ }
412
+
413
+ let final_wasm = out_dir_abs.join(format!("{}.wasm", stem));
414
+ std::fs::copy(&wasm_artifact, &final_wasm).map_err(|e| WasmError {
415
+ message: format!("Cannot copy wasm: {}", e),
416
+ })?;
417
+
418
+ println!(
419
+ "Built: {} (run with: wasmtime {})",
420
+ final_wasm.display(),
421
+ final_wasm.display()
422
+ );
423
+ Ok(())
424
+ }
@@ -0,0 +1,37 @@
1
+ [package]
2
+ name = "tishlang_wasm_runtime"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Tish VM compiled to WebAssembly (browser + Wasmtime/WASI)"
6
+ license-file = { workspace = true }
7
+ repository = { workspace = true }
8
+
9
+ [lib]
10
+ crate-type = ["cdylib", "rlib"]
11
+
12
+ [features]
13
+ # For wasm32-unknown-unknown (browser): wasm-bindgen, console output
14
+ browser = ["dep:wasm-bindgen", "tishlang_vm/wasm"]
15
+ # Browser WebGPU / JS-interop FFI + requestAnimationFrame render loop (the
16
+ # `start(chunk, env)` entry). Reflection-based bridge over js-sys; no web-sys
17
+ # WebGPU bindings needed since the WebGPU command API is synchronous.
18
+ gpu = ["browser", "dep:js-sys"]
19
+ # Built-in modules for WASI (wasm32-wasip1): align with `tishlang_cranelift_runtime` / CLI caps
20
+ fs = ["tishlang_vm/fs"]
21
+ process = ["tishlang_vm/process"]
22
+ http = ["tishlang_vm/http"]
23
+ promise = ["tishlang_vm/promise"]
24
+ timers = ["tishlang_vm/timers"]
25
+ regex = ["tishlang_vm/regex"]
26
+ ws = ["tishlang_vm/ws"]
27
+
28
+ [dependencies]
29
+ tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
30
+ tishlang_vm = { path = "../tish_vm", version = ">=0.1" }
31
+ tishlang_core = { path = "../tish_core", version = ">=0.1" }
32
+ wasm-bindgen = { version = "0.2", optional = true }
33
+ js-sys = { version = "0.3", optional = true }
34
+
35
+ # rand_core → getrandom 0.4 needs wasm_js on wasm32-unknown-unknown (browser VM build).
36
+ [target.'cfg(target_arch = "wasm32")'.dependencies]
37
+ getrandom = { version = "0.4", features = ["wasm_js"] }