@tishlang/tish-format 1.0.12 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -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 +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,1046 @@
1
+ //! AST optimization pass for Tish.
2
+ //!
3
+ //! Applies constant folding, short-circuit evaluation, conditional simplification,
4
+ //! and dead code elimination. Benefits all backends: bytecode VM, native, Rust codegen, JS transpilation.
5
+
6
+ use std::sync::Arc;
7
+
8
+ use tishlang_ast::{ArrowBody, BinOp, Expr, Literal, Program, Statement, UnaryOp};
9
+
10
+ /// Optimize a Tish program. Returns a new program with transformations applied.
11
+ pub fn optimize(program: &Program) -> Program {
12
+ Program {
13
+ statements: program.statements.iter().map(optimize_statement).collect(),
14
+ }
15
+ }
16
+
17
+ fn optimize_statement(stmt: &Statement) -> Statement {
18
+ match stmt {
19
+ Statement::Block { statements, span } => {
20
+ let optimized = optimize_block(statements);
21
+ Statement::Block {
22
+ statements: optimized,
23
+ span: *span,
24
+ }
25
+ }
26
+ Statement::Multi { statements, span } => Statement::Multi {
27
+ statements: statements.iter().map(optimize_statement).collect(),
28
+ span: *span,
29
+ },
30
+ Statement::VarDecl {
31
+ name,
32
+ name_span,
33
+ mutable,
34
+ type_ann,
35
+ init,
36
+ span,
37
+ } => Statement::VarDecl {
38
+ name: Arc::clone(name),
39
+ name_span: *name_span,
40
+ mutable: *mutable,
41
+ type_ann: type_ann.clone(),
42
+ init: init.as_ref().map(optimize_expr),
43
+ span: *span,
44
+ },
45
+ Statement::VarDeclDestructure {
46
+ pattern,
47
+ mutable,
48
+ init,
49
+ span,
50
+ } => Statement::VarDeclDestructure {
51
+ pattern: pattern.clone(),
52
+ mutable: *mutable,
53
+ init: optimize_expr(init),
54
+ span: *span,
55
+ },
56
+ Statement::ExprStmt { expr, span } => Statement::ExprStmt {
57
+ expr: optimize_expr(expr),
58
+ span: *span,
59
+ },
60
+ Statement::If {
61
+ cond,
62
+ then_branch,
63
+ else_branch,
64
+ span,
65
+ } => {
66
+ let opt_cond = optimize_expr(cond);
67
+ // Conditional simplification: if cond is constant, take only the branch
68
+ if let Expr::Literal { value, .. } = &opt_cond {
69
+ let truthy = literal_is_truthy(value);
70
+ return if truthy {
71
+ optimize_statement(then_branch)
72
+ } else if let Some(else_b) = else_branch {
73
+ optimize_statement(else_b)
74
+ } else {
75
+ Statement::Block {
76
+ statements: vec![],
77
+ span: *span,
78
+ }
79
+ };
80
+ }
81
+ Statement::If {
82
+ cond: opt_cond,
83
+ then_branch: Box::new(optimize_statement(then_branch)),
84
+ else_branch: else_branch
85
+ .as_ref()
86
+ .map(|b| Box::new(optimize_statement(b))),
87
+ span: *span,
88
+ }
89
+ }
90
+ Statement::While { cond, body, span } => Statement::While {
91
+ cond: optimize_expr(cond),
92
+ body: Box::new(optimize_statement(body)),
93
+ span: *span,
94
+ },
95
+ Statement::For {
96
+ init,
97
+ cond,
98
+ update,
99
+ body,
100
+ span,
101
+ } => Statement::For {
102
+ init: init.as_ref().map(|i| Box::new(optimize_statement(i))),
103
+ cond: cond.as_ref().map(optimize_expr),
104
+ update: update.as_ref().map(optimize_expr),
105
+ body: Box::new(optimize_statement(body)),
106
+ span: *span,
107
+ },
108
+ Statement::ForOf {
109
+ name,
110
+ name_span,
111
+ iterable,
112
+ body,
113
+ span,
114
+ } => Statement::ForOf {
115
+ name: Arc::clone(name),
116
+ name_span: *name_span,
117
+ iterable: optimize_expr(iterable),
118
+ body: Box::new(optimize_statement(body)),
119
+ span: *span,
120
+ },
121
+ Statement::Return { value, span } => Statement::Return {
122
+ value: value.as_ref().map(optimize_expr),
123
+ span: *span,
124
+ },
125
+ Statement::Break { span } => Statement::Break { span: *span },
126
+ Statement::Continue { span } => Statement::Continue { span: *span },
127
+ Statement::FunDecl {
128
+ async_,
129
+ name,
130
+ name_span,
131
+ params,
132
+ rest_param,
133
+ return_type,
134
+ body,
135
+ span,
136
+ } => Statement::FunDecl {
137
+ async_: *async_,
138
+ name: Arc::clone(name),
139
+ name_span: *name_span,
140
+ params: params.clone(),
141
+ rest_param: rest_param.clone(),
142
+ return_type: return_type.clone(),
143
+ body: Box::new(optimize_statement(body)),
144
+ span: *span,
145
+ },
146
+ Statement::Switch {
147
+ expr,
148
+ cases,
149
+ default_body,
150
+ span,
151
+ } => Statement::Switch {
152
+ expr: optimize_expr(expr),
153
+ cases: cases
154
+ .iter()
155
+ .map(|(ce, stmts)| (ce.as_ref().map(optimize_expr), optimize_block(stmts)))
156
+ .collect(),
157
+ default_body: default_body.as_ref().map(|stmts| optimize_block(stmts)),
158
+ span: *span,
159
+ },
160
+ Statement::DoWhile { body, cond, span } => Statement::DoWhile {
161
+ body: Box::new(optimize_statement(body)),
162
+ cond: optimize_expr(cond),
163
+ span: *span,
164
+ },
165
+ Statement::Throw { value, span } => Statement::Throw {
166
+ value: optimize_expr(value),
167
+ span: *span,
168
+ },
169
+ Statement::Try {
170
+ body,
171
+ catch_param,
172
+ catch_param_span,
173
+ catch_body,
174
+ finally_body,
175
+ span,
176
+ } => Statement::Try {
177
+ body: Box::new(optimize_statement(body)),
178
+ catch_param: catch_param.clone(),
179
+ catch_param_span: *catch_param_span,
180
+ catch_body: catch_body.as_ref().map(|b| Box::new(optimize_statement(b))),
181
+ finally_body: finally_body
182
+ .as_ref()
183
+ .map(|b| Box::new(optimize_statement(b))),
184
+ span: *span,
185
+ },
186
+ Statement::Import { .. }
187
+ | Statement::Export { .. }
188
+ | Statement::TypeAlias { .. }
189
+ | Statement::DeclareVar { .. }
190
+ | Statement::DeclareFun { .. } => stmt.clone(),
191
+ }
192
+ }
193
+
194
+ /// Optimize block with dead code elimination: remove statements after return/throw.
195
+ fn optimize_block(statements: &[Statement]) -> Vec<Statement> {
196
+ let mut result = Vec::new();
197
+ for stmt in statements {
198
+ if let Some(last) = result.last() {
199
+ if stmt_always_returns_or_throws(last) {
200
+ // Dead code - skip
201
+ continue;
202
+ }
203
+ }
204
+ result.push(optimize_statement(stmt));
205
+ }
206
+ result
207
+ }
208
+
209
+ fn stmt_always_returns_or_throws(stmt: &Statement) -> bool {
210
+ match stmt {
211
+ Statement::Return { .. } | Statement::Throw { .. } => true,
212
+ Statement::If {
213
+ cond: Expr::Literal { value, .. },
214
+ then_branch,
215
+ else_branch,
216
+ ..
217
+ } => {
218
+ let truthy = literal_is_truthy(value);
219
+ if truthy {
220
+ stmt_always_returns_or_throws(then_branch)
221
+ } else if let Some(else_b) = else_branch {
222
+ stmt_always_returns_or_throws(else_b)
223
+ } else {
224
+ false
225
+ }
226
+ }
227
+ Statement::If { .. } => false,
228
+ _ => false,
229
+ }
230
+ }
231
+
232
+ fn optimize_expr(expr: &Expr) -> Expr {
233
+ match expr {
234
+ Expr::Literal { value, span } => Expr::Literal {
235
+ value: value.clone(),
236
+ span: *span,
237
+ },
238
+ Expr::Ident { name, span } => Expr::Ident {
239
+ name: Arc::clone(name),
240
+ span: *span,
241
+ },
242
+ Expr::Binary {
243
+ left,
244
+ op,
245
+ right,
246
+ span,
247
+ } => {
248
+ let opt_left = optimize_expr(left);
249
+ let opt_right = optimize_expr(right);
250
+
251
+ // Short-circuit for And/Or when left is constant
252
+ if *op == BinOp::And {
253
+ if let Expr::Literal { value, .. } = &opt_left {
254
+ return if literal_is_truthy(value) {
255
+ opt_right
256
+ } else {
257
+ Expr::Literal {
258
+ value: Literal::Bool(false),
259
+ span: *span,
260
+ }
261
+ };
262
+ }
263
+ }
264
+ if *op == BinOp::Or {
265
+ if let Expr::Literal { value, .. } = &opt_left {
266
+ return if literal_is_truthy(value) {
267
+ Expr::Literal {
268
+ value: Literal::Bool(true),
269
+ span: *span,
270
+ }
271
+ } else {
272
+ opt_right
273
+ };
274
+ }
275
+ }
276
+
277
+ // Constant folding when both are literals
278
+ if let (Expr::Literal { value: lv, .. }, Expr::Literal { value: rv, .. }) =
279
+ (&opt_left, &opt_right)
280
+ {
281
+ if let Some(folded) = try_fold_binop(lv, *op, rv) {
282
+ return Expr::Literal {
283
+ value: folded,
284
+ span: *span,
285
+ };
286
+ }
287
+ }
288
+
289
+ // A5: Algebraic simplification (x+0=x, x*1=x, etc.).
290
+ // Applied after constant folding so e.g. x*(1+0) → x*1 → x.
291
+ if let Some(simplified) = try_algebraic_simplify(*op, &opt_left, &opt_right, *span) {
292
+ return simplified;
293
+ }
294
+
295
+ Expr::Binary {
296
+ left: Box::new(opt_left),
297
+ op: *op,
298
+ right: Box::new(opt_right),
299
+ span: *span,
300
+ }
301
+ }
302
+ Expr::Unary { op, operand, span } => {
303
+ let opt_operand = optimize_expr(operand);
304
+ if let Expr::Literal { value, .. } = &opt_operand {
305
+ if let Some(folded) = try_fold_unary(*op, value) {
306
+ return Expr::Literal {
307
+ value: folded,
308
+ span: *span,
309
+ };
310
+ }
311
+ }
312
+ Expr::Unary {
313
+ op: *op,
314
+ operand: Box::new(opt_operand),
315
+ span: *span,
316
+ }
317
+ }
318
+ Expr::Conditional {
319
+ cond,
320
+ then_branch,
321
+ else_branch,
322
+ span,
323
+ } => {
324
+ let opt_cond = optimize_expr(cond);
325
+ if let Expr::Literal { value, .. } = &opt_cond {
326
+ return if literal_is_truthy(value) {
327
+ optimize_expr(then_branch)
328
+ } else {
329
+ optimize_expr(else_branch)
330
+ };
331
+ }
332
+ Expr::Conditional {
333
+ cond: Box::new(opt_cond),
334
+ then_branch: Box::new(optimize_expr(then_branch)),
335
+ else_branch: Box::new(optimize_expr(else_branch)),
336
+ span: *span,
337
+ }
338
+ }
339
+ Expr::Call { callee, args, span } => Expr::Call {
340
+ callee: Box::new(optimize_expr(callee)),
341
+ args: args
342
+ .iter()
343
+ .map(|a| match a {
344
+ tishlang_ast::CallArg::Expr(e) => tishlang_ast::CallArg::Expr(optimize_expr(e)),
345
+ tishlang_ast::CallArg::Spread(e) => {
346
+ tishlang_ast::CallArg::Spread(optimize_expr(e))
347
+ }
348
+ })
349
+ .collect(),
350
+ span: *span,
351
+ },
352
+ Expr::New { callee, args, span } => Expr::New {
353
+ callee: Box::new(optimize_expr(callee)),
354
+ args: args
355
+ .iter()
356
+ .map(|a| match a {
357
+ tishlang_ast::CallArg::Expr(e) => tishlang_ast::CallArg::Expr(optimize_expr(e)),
358
+ tishlang_ast::CallArg::Spread(e) => {
359
+ tishlang_ast::CallArg::Spread(optimize_expr(e))
360
+ }
361
+ })
362
+ .collect(),
363
+ span: *span,
364
+ },
365
+ Expr::Member {
366
+ object,
367
+ prop,
368
+ optional,
369
+ span,
370
+ } => {
371
+ let opt_obj = optimize_expr(object);
372
+ let opt_prop = match prop {
373
+ tishlang_ast::MemberProp::Name { name, span } => tishlang_ast::MemberProp::Name {
374
+ name: Arc::clone(name),
375
+ span: *span,
376
+ },
377
+ tishlang_ast::MemberProp::Expr(e) => {
378
+ tishlang_ast::MemberProp::Expr(Box::new(optimize_expr(e)))
379
+ }
380
+ };
381
+ Expr::Member {
382
+ object: Box::new(opt_obj),
383
+ prop: opt_prop,
384
+ optional: *optional,
385
+ span: *span,
386
+ }
387
+ }
388
+ Expr::Index {
389
+ object,
390
+ index,
391
+ optional,
392
+ span,
393
+ } => Expr::Index {
394
+ object: Box::new(optimize_expr(object)),
395
+ index: Box::new(optimize_expr(index)),
396
+ optional: *optional,
397
+ span: *span,
398
+ },
399
+ Expr::NullishCoalesce { left, right, span } => {
400
+ let opt_left = optimize_expr(left);
401
+ if let Expr::Literal {
402
+ value: Literal::Null,
403
+ ..
404
+ } = &opt_left
405
+ {
406
+ return optimize_expr(right);
407
+ }
408
+ Expr::NullishCoalesce {
409
+ left: Box::new(opt_left),
410
+ right: Box::new(optimize_expr(right)),
411
+ span: *span,
412
+ }
413
+ }
414
+ Expr::Array { elements, span } => Expr::Array {
415
+ elements: elements
416
+ .iter()
417
+ .map(|e| match e {
418
+ tishlang_ast::ArrayElement::Expr(ex) => {
419
+ tishlang_ast::ArrayElement::Expr(optimize_expr(ex))
420
+ }
421
+ tishlang_ast::ArrayElement::Spread(ex) => {
422
+ tishlang_ast::ArrayElement::Spread(optimize_expr(ex))
423
+ }
424
+ })
425
+ .collect(),
426
+ span: *span,
427
+ },
428
+ Expr::Object { props, span } => Expr::Object {
429
+ props: props
430
+ .iter()
431
+ .map(|p| match p {
432
+ tishlang_ast::ObjectProp::KeyValue(k, v) => {
433
+ tishlang_ast::ObjectProp::KeyValue(Arc::clone(k), optimize_expr(v))
434
+ }
435
+ tishlang_ast::ObjectProp::Spread(e) => {
436
+ tishlang_ast::ObjectProp::Spread(optimize_expr(e))
437
+ }
438
+ })
439
+ .collect(),
440
+ span: *span,
441
+ },
442
+ Expr::Assign { name, value, span } => Expr::Assign {
443
+ name: Arc::clone(name),
444
+ value: Box::new(optimize_expr(value)),
445
+ span: *span,
446
+ },
447
+ Expr::TypeOf { operand, span } => Expr::TypeOf {
448
+ operand: Box::new(optimize_expr(operand)),
449
+ span: *span,
450
+ },
451
+ Expr::Delete { target, span } => Expr::Delete {
452
+ target: Box::new(optimize_expr(target)),
453
+ span: *span,
454
+ },
455
+ Expr::PostfixInc { .. }
456
+ | Expr::PostfixDec { .. }
457
+ | Expr::PrefixInc { .. }
458
+ | Expr::PrefixDec { .. } => expr.clone(),
459
+ Expr::CompoundAssign {
460
+ name,
461
+ op,
462
+ value,
463
+ span,
464
+ } => Expr::CompoundAssign {
465
+ name: Arc::clone(name),
466
+ op: *op,
467
+ value: Box::new(optimize_expr(value)),
468
+ span: *span,
469
+ },
470
+ Expr::LogicalAssign {
471
+ name,
472
+ op,
473
+ value,
474
+ span,
475
+ } => Expr::LogicalAssign {
476
+ name: Arc::clone(name),
477
+ op: *op,
478
+ value: Box::new(optimize_expr(value)),
479
+ span: *span,
480
+ },
481
+ Expr::MemberAssign {
482
+ object,
483
+ prop,
484
+ value,
485
+ span,
486
+ } => Expr::MemberAssign {
487
+ object: Box::new(optimize_expr(object)),
488
+ prop: Arc::clone(prop),
489
+ value: Box::new(optimize_expr(value)),
490
+ span: *span,
491
+ },
492
+ Expr::IndexAssign {
493
+ object,
494
+ index,
495
+ value,
496
+ span,
497
+ } => Expr::IndexAssign {
498
+ object: Box::new(optimize_expr(object)),
499
+ index: Box::new(optimize_expr(index)),
500
+ value: Box::new(optimize_expr(value)),
501
+ span: *span,
502
+ },
503
+ Expr::ArrowFunction { params, body, span } => {
504
+ let opt_body = match body {
505
+ ArrowBody::Expr(e) => ArrowBody::Expr(Box::new(optimize_expr(e))),
506
+ ArrowBody::Block(s) => ArrowBody::Block(Box::new(optimize_statement(s))),
507
+ };
508
+ Expr::ArrowFunction {
509
+ params: params.clone(),
510
+ body: opt_body,
511
+ span: *span,
512
+ }
513
+ }
514
+ Expr::TemplateLiteral {
515
+ quasis,
516
+ exprs,
517
+ span,
518
+ } => Expr::TemplateLiteral {
519
+ quasis: quasis.iter().map(Arc::clone).collect(),
520
+ exprs: exprs.iter().map(optimize_expr).collect(),
521
+ span: *span,
522
+ },
523
+ Expr::Await { operand, span } => Expr::Await {
524
+ operand: Box::new(optimize_expr(operand)),
525
+ span: *span,
526
+ },
527
+ Expr::JsxElement { .. } | Expr::JsxFragment { .. } => expr.clone(),
528
+ Expr::NativeModuleLoad {
529
+ spec,
530
+ export_name,
531
+ span,
532
+ } => Expr::NativeModuleLoad {
533
+ spec: Arc::clone(spec),
534
+ export_name: Arc::clone(export_name),
535
+ span: *span,
536
+ },
537
+ }
538
+ }
539
+
540
+ fn literal_is_truthy(lit: &Literal) -> bool {
541
+ match lit {
542
+ Literal::Null => false,
543
+ Literal::Bool(b) => *b,
544
+ Literal::Number(n) => *n != 0.0 && !n.is_nan(),
545
+ Literal::String(s) => !s.is_empty(),
546
+ }
547
+ }
548
+
549
+ fn literal_strict_eq(a: &Literal, b: &Literal) -> bool {
550
+ match (a, b) {
551
+ (Literal::Number(x), Literal::Number(y)) => {
552
+ if x.is_nan() || y.is_nan() {
553
+ false
554
+ } else {
555
+ x == y
556
+ }
557
+ }
558
+ (Literal::String(x), Literal::String(y)) => x == y,
559
+ (Literal::Bool(x), Literal::Bool(y)) => x == y,
560
+ (Literal::Null, Literal::Null) => true,
561
+ _ => false,
562
+ }
563
+ }
564
+
565
+ /// JS `Number.prototype.toString` (radix 10). **Kept byte-for-byte in sync with
566
+ /// `tishlang_core::js_number_to_string`** so a constant-folded `"" + n` here matches the
567
+ /// runtime conversion there; `tish_opt` deliberately does not depend on `tish_core` (it is a
568
+ /// lean AST pass), hence the small duplication of this fixed-spec algorithm. See that function
569
+ /// for the full commentary.
570
+ fn js_number_to_string(value: f64) -> String {
571
+ if value.is_nan() {
572
+ return "NaN".to_string();
573
+ }
574
+ if value == f64::INFINITY {
575
+ return "Infinity".to_string();
576
+ }
577
+ if value == f64::NEG_INFINITY {
578
+ return "-Infinity".to_string();
579
+ }
580
+ if value == 0.0 {
581
+ return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
582
+ }
583
+ let negative = value < 0.0;
584
+ let sci = format!("{:e}", value.abs());
585
+ let (mantissa, exp_str) = sci
586
+ .split_once('e')
587
+ .expect("LowerExp formatting always contains 'e'");
588
+ let exp: i32 = exp_str
589
+ .parse()
590
+ .expect("LowerExp exponent is a valid integer");
591
+ let digits: String = mantissa.chars().filter(|&c| c != '.').collect();
592
+ let k = digits.len() as i32;
593
+ let point = exp + 1;
594
+
595
+ let mut out = String::new();
596
+ if negative {
597
+ out.push('-');
598
+ }
599
+ if k <= point && point <= 21 {
600
+ out.push_str(&digits);
601
+ out.push_str(&"0".repeat((point - k) as usize));
602
+ } else if 0 < point && point <= 21 {
603
+ out.push_str(&digits[..point as usize]);
604
+ out.push('.');
605
+ out.push_str(&digits[point as usize..]);
606
+ } else if -6 < point && point <= 0 {
607
+ out.push_str("0.");
608
+ out.push_str(&"0".repeat((-point) as usize));
609
+ out.push_str(&digits);
610
+ } else {
611
+ let e = point - 1;
612
+ out.push_str(&digits[..1]);
613
+ if k > 1 {
614
+ out.push('.');
615
+ out.push_str(&digits[1..]);
616
+ }
617
+ out.push('e');
618
+ out.push(if e >= 0 { '+' } else { '-' });
619
+ out.push_str(&e.abs().to_string());
620
+ }
621
+ out
622
+ }
623
+
624
+ fn literal_to_display_string(lit: &Literal) -> String {
625
+ match lit {
626
+ // Must match the runtime exactly so constant-folded `"" + n` agrees with the
627
+ // unfolded path (see `js_number_to_string` below).
628
+ Literal::Number(n) => js_number_to_string(*n),
629
+ Literal::String(s) => s.to_string(),
630
+ Literal::Bool(b) => b.to_string(),
631
+ Literal::Null => "null".to_string(),
632
+ }
633
+ }
634
+
635
+ fn literal_as_number(lit: &Literal) -> f64 {
636
+ match lit {
637
+ Literal::Number(n) => *n,
638
+ Literal::Bool(true) => 1.0,
639
+ Literal::Bool(false) => 0.0,
640
+ Literal::Null => 0.0,
641
+ Literal::String(s) => s.parse().unwrap_or(f64::NAN),
642
+ }
643
+ }
644
+
645
+ /// Algebraic simplification: x+0→x, x*1→x, etc.
646
+ /// Only applies when the literal is a clean 0 or 1 (no NaN/Inf).
647
+ fn try_algebraic_simplify(
648
+ op: BinOp,
649
+ left: &Expr,
650
+ right: &Expr,
651
+ span: tishlang_ast::Span,
652
+ ) -> Option<Expr> {
653
+ use BinOp::*;
654
+ fn num_is_zero(n: f64) -> bool {
655
+ n == 0.0 && !n.is_nan() && n.is_finite()
656
+ }
657
+ fn num_is_one(n: f64) -> bool {
658
+ (n - 1.0).abs() < f64::EPSILON && !n.is_nan() && n.is_finite()
659
+ }
660
+
661
+ match op {
662
+ Add => {
663
+ if let Expr::Literal {
664
+ value: Literal::Number(r),
665
+ ..
666
+ } = right
667
+ {
668
+ if num_is_zero(*r) {
669
+ return Some(left.clone());
670
+ }
671
+ }
672
+ if let Expr::Literal {
673
+ value: Literal::Number(l),
674
+ ..
675
+ } = left
676
+ {
677
+ if num_is_zero(*l) {
678
+ return Some(right.clone());
679
+ }
680
+ }
681
+ }
682
+ Sub => {
683
+ if let Expr::Literal {
684
+ value: Literal::Number(r),
685
+ ..
686
+ } = right
687
+ {
688
+ if num_is_zero(*r) {
689
+ return Some(left.clone());
690
+ }
691
+ }
692
+ }
693
+ Mul => {
694
+ if let Expr::Literal {
695
+ value: Literal::Number(r),
696
+ ..
697
+ } = right
698
+ {
699
+ if num_is_one(*r) {
700
+ return Some(left.clone());
701
+ }
702
+ if num_is_zero(*r) {
703
+ return Some(Expr::Literal {
704
+ value: Literal::Number(0.0),
705
+ span,
706
+ });
707
+ }
708
+ }
709
+ if let Expr::Literal {
710
+ value: Literal::Number(l),
711
+ ..
712
+ } = left
713
+ {
714
+ if num_is_one(*l) {
715
+ return Some(right.clone());
716
+ }
717
+ if num_is_zero(*l) {
718
+ return Some(Expr::Literal {
719
+ value: Literal::Number(0.0),
720
+ span,
721
+ });
722
+ }
723
+ }
724
+ }
725
+ Div => {
726
+ if let Expr::Literal {
727
+ value: Literal::Number(r),
728
+ ..
729
+ } = right
730
+ {
731
+ if num_is_one(*r) {
732
+ return Some(left.clone());
733
+ }
734
+ }
735
+ }
736
+ Pow => {
737
+ if let Expr::Literal {
738
+ value: Literal::Number(r),
739
+ ..
740
+ } = right
741
+ {
742
+ if num_is_one(*r) {
743
+ return Some(left.clone());
744
+ }
745
+ if num_is_zero(*r) {
746
+ return Some(Expr::Literal {
747
+ value: Literal::Number(1.0),
748
+ span,
749
+ });
750
+ }
751
+ }
752
+ if let Expr::Literal {
753
+ value: Literal::Number(l),
754
+ ..
755
+ } = left
756
+ {
757
+ if num_is_one(*l) {
758
+ return Some(Expr::Literal {
759
+ value: Literal::Number(1.0),
760
+ span,
761
+ });
762
+ }
763
+ }
764
+ }
765
+ _ => {}
766
+ }
767
+ None
768
+ }
769
+
770
+ // JS ToInt32/ToUint32 for the constant folder. NaN/±Infinity → 0 (`f64 as i64` saturates, so a
771
+ // folded `(1e308 * 10) | 0` would otherwise give -1 while the runtime gives 0). `tish_opt` has no
772
+ // `tish_core` dep, so these mirror `tishlang_core::to_int32`/`to_uint32` (kept in sync, pure spec).
773
+ #[inline]
774
+ fn fold_to_int32(x: f64) -> i32 {
775
+ if x.is_finite() {
776
+ x as i64 as i32
777
+ } else {
778
+ 0
779
+ }
780
+ }
781
+ #[inline]
782
+ fn fold_to_uint32(x: f64) -> u32 {
783
+ if x.is_finite() {
784
+ x as i64 as u32
785
+ } else {
786
+ 0
787
+ }
788
+ }
789
+
790
+ /// Constant-fold a relational comparison (`<` `<=` `>` `>=`). Two string literals
791
+ /// compare lexicographically; otherwise the numeric coercions `ln`/`rn` are used.
792
+ /// `pred` maps the `Ordering` to a bool; a NaN-involved numeric comparison has no
793
+ /// ordering and is `false` — matching the VM/interp/native runtime exactly.
794
+ fn fold_relational<F>(left: &Literal, right: &Literal, ln: f64, rn: f64, pred: F) -> bool
795
+ where
796
+ F: FnOnce(std::cmp::Ordering) -> bool,
797
+ {
798
+ let ord = match (left, right) {
799
+ (Literal::String(a), Literal::String(b)) => Some(a.as_ref().cmp(b.as_ref())),
800
+ _ => ln.partial_cmp(&rn),
801
+ };
802
+ ord.map(pred).unwrap_or(false)
803
+ }
804
+
805
+ fn try_fold_binop(left: &Literal, op: BinOp, right: &Literal) -> Option<Literal> {
806
+ use BinOp::*;
807
+ let ln = literal_as_number(left);
808
+ let rn = literal_as_number(right);
809
+
810
+ let result = match op {
811
+ Add => {
812
+ if matches!(left, Literal::String(_)) || matches!(right, Literal::String(_)) {
813
+ return Some(Literal::String(
814
+ format!(
815
+ "{}{}",
816
+ literal_to_display_string(left),
817
+ literal_to_display_string(right)
818
+ )
819
+ .into(),
820
+ ));
821
+ }
822
+ Literal::Number(ln + rn)
823
+ }
824
+ Sub => Literal::Number(ln - rn),
825
+ Mul => Literal::Number(ln * rn),
826
+ // IEEE division/remainder, matching JS + the VM's `eval_binop` + interp + rust-AOT:
827
+ // `5/0` → Infinity, `-5/0` → -Infinity, `0/0` → NaN, `5%0` → NaN. The former
828
+ // `if rn == 0.0 { NaN }` folded `5/0` to NaN at compile time, diverging from every runtime
829
+ // path (which all produce Infinity) — a constant-fold-vs-runtime inconsistency.
830
+ Div => Literal::Number(ln / rn),
831
+ Mod => Literal::Number(ln % rn),
832
+ Pow => Literal::Number(ln.powf(rn)),
833
+ Eq => Literal::Bool(literal_strict_eq(left, right)),
834
+ Ne => Literal::Bool(!literal_strict_eq(left, right)),
835
+ StrictEq => Literal::Bool(literal_strict_eq(left, right)),
836
+ StrictNe => Literal::Bool(!literal_strict_eq(left, right)),
837
+ // Relational ops fold lexicographically when BOTH operands are string
838
+ // literals (JS semantics — must match the VM/interp/native runtime), else
839
+ // numerically. A NaN-involved numeric comparison is always false.
840
+ Lt => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_lt())),
841
+ Le => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_le())),
842
+ Gt => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_gt())),
843
+ Ge => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_ge())),
844
+ And => Literal::Bool(literal_is_truthy(left) && literal_is_truthy(right)),
845
+ Or => Literal::Bool(literal_is_truthy(left) || literal_is_truthy(right)),
846
+ // ToInt32/ToUint32 (modulo 2³², NaN/±Infinity → 0), matching the VM/interp exactly.
847
+ BitAnd => Literal::Number((fold_to_int32(ln) & fold_to_int32(rn)) as f64),
848
+ BitOr => Literal::Number((fold_to_int32(ln) | fold_to_int32(rn)) as f64),
849
+ BitXor => Literal::Number((fold_to_int32(ln) ^ fold_to_int32(rn)) as f64),
850
+ Shl => Literal::Number(fold_to_int32(ln).wrapping_shl(fold_to_uint32(rn)) as f64),
851
+ Shr => Literal::Number(fold_to_int32(ln).wrapping_shr(fold_to_uint32(rn)) as f64),
852
+ UShr => Literal::Number(fold_to_uint32(ln).wrapping_shr(fold_to_uint32(rn)) as f64),
853
+ In => return None, // Requires object/array on right
854
+ };
855
+ Some(result)
856
+ }
857
+
858
+ #[cfg(test)]
859
+ mod tests {
860
+ use super::*;
861
+
862
+ fn program_from_source(src: &str) -> Program {
863
+ tishlang_parser::parse(src).expect("parse")
864
+ }
865
+
866
+ fn has_literal_number(expr: &Expr, n: f64) -> bool {
867
+ if let Expr::Literal {
868
+ value: Literal::Number(x),
869
+ ..
870
+ } = expr
871
+ {
872
+ (*x - n).abs() < f64::EPSILON
873
+ } else {
874
+ false
875
+ }
876
+ }
877
+
878
+ #[test]
879
+ fn constant_fold_add() {
880
+ let program = program_from_source("1 + 2");
881
+ let opt = optimize(&program);
882
+ let expr = match &opt.statements[..] {
883
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
884
+ _ => panic!("expected single expr stmt"),
885
+ };
886
+ assert!(has_literal_number(expr, 3.0), "expected 3, got {:?}", expr);
887
+ }
888
+
889
+ #[test]
890
+ fn constant_fold_unary_neg() {
891
+ let program = program_from_source("-42");
892
+ let opt = optimize(&program);
893
+ let expr = match &opt.statements[..] {
894
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
895
+ _ => panic!("expected single expr stmt"),
896
+ };
897
+ assert!(
898
+ has_literal_number(expr, -42.0),
899
+ "expected -42, got {:?}",
900
+ expr
901
+ );
902
+ }
903
+
904
+ #[test]
905
+ fn short_circuit_false_and() {
906
+ let program = program_from_source("false && foo");
907
+ let opt = optimize(&program);
908
+ let expr = match &opt.statements[..] {
909
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
910
+ _ => panic!("expected single expr stmt"),
911
+ };
912
+ assert!(
913
+ matches!(
914
+ expr,
915
+ Expr::Literal {
916
+ value: Literal::Bool(false),
917
+ ..
918
+ }
919
+ ),
920
+ "expected false, got {:?}",
921
+ expr
922
+ );
923
+ }
924
+
925
+ #[test]
926
+ fn conditional_simplify_true() {
927
+ let program = program_from_source("true ? 1 : 2");
928
+ let opt = optimize(&program);
929
+ let expr = match &opt.statements[..] {
930
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
931
+ _ => panic!("expected single expr stmt"),
932
+ };
933
+ assert!(has_literal_number(expr, 1.0), "expected 1, got {:?}", expr);
934
+ }
935
+
936
+ #[test]
937
+ fn algebraic_simplify_x_plus_zero() {
938
+ // x + 0 → x (after constant fold, 0 is literal)
939
+ let program = program_from_source("x + 0");
940
+ let opt = optimize(&program);
941
+ let expr = match &opt.statements[..] {
942
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
943
+ _ => panic!("expected single expr stmt"),
944
+ };
945
+ assert!(
946
+ matches!(expr, Expr::Ident { name, .. } if name.as_ref() == "x"),
947
+ "expected Ident(x), got {:?}",
948
+ expr
949
+ );
950
+ }
951
+
952
+ #[test]
953
+ fn algebraic_simplify_x_times_one() {
954
+ let program = program_from_source("x * 1");
955
+ let opt = optimize(&program);
956
+ let expr = match &opt.statements[..] {
957
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
958
+ _ => panic!("expected single expr stmt"),
959
+ };
960
+ assert!(
961
+ matches!(expr, Expr::Ident { name, .. } if name.as_ref() == "x"),
962
+ "expected Ident(x), got {:?}",
963
+ expr
964
+ );
965
+ }
966
+
967
+ #[test]
968
+ fn algebraic_simplify_chain() {
969
+ // x * (1 + 0) → constant fold 1+0=1 → x*1 → x
970
+ let program = program_from_source("x * (1 + 0)");
971
+ let opt = optimize(&program);
972
+ let expr = match &opt.statements[..] {
973
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
974
+ _ => panic!("expected single expr stmt"),
975
+ };
976
+ assert!(
977
+ matches!(expr, Expr::Ident { name, .. } if name.as_ref() == "x"),
978
+ "expected Ident(x) after x*(1+0) → x*1 → x, got {:?}",
979
+ expr
980
+ );
981
+ }
982
+
983
+ #[test]
984
+ fn algebraic_simplify_pow_one() {
985
+ let program = program_from_source("x ** 1");
986
+ let opt = optimize(&program);
987
+ let expr = match &opt.statements[..] {
988
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
989
+ _ => panic!("expected single expr stmt"),
990
+ };
991
+ assert!(
992
+ matches!(expr, Expr::Ident { name, .. } if name.as_ref() == "x"),
993
+ "expected Ident(x), got {:?}",
994
+ expr
995
+ );
996
+ }
997
+
998
+ #[test]
999
+ fn algebraic_simplify_pow_zero() {
1000
+ let program = program_from_source("x ** 0");
1001
+ let opt = optimize(&program);
1002
+ let expr = match &opt.statements[..] {
1003
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
1004
+ _ => panic!("expected single expr stmt"),
1005
+ };
1006
+ assert!(has_literal_number(expr, 1.0), "expected 1, got {:?}", expr);
1007
+ }
1008
+
1009
+ #[test]
1010
+ fn algebraic_simplify_one_pow_x() {
1011
+ let program = program_from_source("1 ** x");
1012
+ let opt = optimize(&program);
1013
+ let expr = match &opt.statements[..] {
1014
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
1015
+ _ => panic!("expected single expr stmt"),
1016
+ };
1017
+ assert!(has_literal_number(expr, 1.0), "expected 1, got {:?}", expr);
1018
+ }
1019
+
1020
+ #[test]
1021
+ fn nullish_coalesce_null_simplify() {
1022
+ let program = program_from_source("null ?? x");
1023
+ let opt = optimize(&program);
1024
+ let expr = match &opt.statements[..] {
1025
+ [tishlang_ast::Statement::ExprStmt { expr, .. }] => expr,
1026
+ _ => panic!("expected single expr stmt"),
1027
+ };
1028
+ assert!(
1029
+ matches!(expr, Expr::Ident { name, .. } if name.as_ref() == "x"),
1030
+ "expected Ident(x), got {:?}",
1031
+ expr
1032
+ );
1033
+ }
1034
+ }
1035
+
1036
+ fn try_fold_unary(op: UnaryOp, operand: &Literal) -> Option<Literal> {
1037
+ use UnaryOp::*;
1038
+ let result = match op {
1039
+ Not => Literal::Bool(!literal_is_truthy(operand)),
1040
+ Neg => Literal::Number(-literal_as_number(operand)),
1041
+ Pos => Literal::Number(literal_as_number(operand)),
1042
+ BitNot => Literal::Number(!fold_to_int32(literal_as_number(operand)) as f64),
1043
+ Void => Literal::Null,
1044
+ };
1045
+ Some(result)
1046
+ }