@tishlang/tish 1.0.7 → 1.0.10

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,1361 @@
1
+ //! AST to bytecode compiler.
2
+
3
+ use std::collections::HashMap;
4
+ use std::sync::Arc;
5
+
6
+ use tish_ast::{
7
+ ArrayElement, ArrowBody, BinOp, CallArg, DestructElement, DestructPattern, Expr,
8
+ JsxAttrValue, JsxChild, JsxProp, Literal, MemberProp, ObjectProp, Program, Span, Statement,
9
+ };
10
+
11
+ use crate::chunk::{Chunk, Constant};
12
+ use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
13
+ use crate::opcode::Opcode;
14
+
15
+ enum SimpleMapResult {
16
+ Identity,
17
+ BinOp(BinOp, Constant, bool), // op, constant, param_on_left
18
+ }
19
+
20
+ fn literal_to_constant(expr: &Expr) -> Option<Constant> {
21
+ if let Expr::Literal { value, .. } = expr {
22
+ Some(match value {
23
+ Literal::Number(n) => Constant::Number(*n),
24
+ Literal::String(s) => Constant::String(Arc::clone(s)),
25
+ Literal::Bool(b) => Constant::Bool(*b),
26
+ Literal::Null => Constant::Null,
27
+ })
28
+ } else {
29
+ None
30
+ }
31
+ }
32
+
33
+ #[derive(Debug)]
34
+ pub struct CompileError {
35
+ pub message: String,
36
+ }
37
+
38
+ impl std::fmt::Display for CompileError {
39
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40
+ write!(f, "{}", self.message)
41
+ }
42
+ }
43
+
44
+ impl std::error::Error for CompileError {}
45
+
46
+ /// Loop boundary for break/continue.
47
+ struct LoopInfo {
48
+ break_patches: Vec<usize>,
49
+ continue_patches: Vec<usize>,
50
+ }
51
+
52
+ /// Switch boundary: break exits the switch.
53
+ struct SwitchInfo {
54
+ break_patches: Vec<usize>,
55
+ }
56
+
57
+ struct Compiler<'a> {
58
+ chunk: &'a mut Chunk,
59
+ /// Current scope: variable name -> (depth, is_captured). Depth 0 = local.
60
+ scope: Vec<HashMap<Arc<str>, bool>>,
61
+ /// Stack of loop info for break/continue.
62
+ loop_stack: Vec<LoopInfo>,
63
+ switch_stack: Vec<SwitchInfo>,
64
+ /// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
65
+ retain_last_expr: bool,
66
+ }
67
+
68
+ impl<'a> Compiler<'a> {
69
+ fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
70
+ Self {
71
+ chunk,
72
+ scope: vec![HashMap::new()],
73
+ loop_stack: Vec::new(),
74
+ switch_stack: Vec::new(),
75
+ retain_last_expr,
76
+ }
77
+ }
78
+
79
+ fn name_idx(&mut self, name: &Arc<str>) -> u16 {
80
+ self.chunk.add_name(Arc::clone(name))
81
+ }
82
+
83
+ fn constant_idx(&mut self, c: Constant) -> u16 {
84
+ self.chunk.add_constant(c)
85
+ }
86
+
87
+ fn emit(&mut self, op: Opcode) {
88
+ self.chunk.write_u8(op as u8);
89
+ }
90
+
91
+ fn emit_u8(&mut self, op: Opcode, v: u8) {
92
+ self.chunk.write_u8(op as u8);
93
+ self.chunk.write_u16(v as u16);
94
+ }
95
+
96
+ fn emit_u16(&mut self, op: Opcode, v: u16) {
97
+ self.chunk.write_u8(op as u8);
98
+ self.chunk.write_u16(v);
99
+ }
100
+
101
+ fn emit_jump(&mut self, op: Opcode) -> usize {
102
+ let pos = self.chunk.code.len();
103
+ self.chunk.write_u8(op as u8);
104
+ self.chunk.write_u16(0); // placeholder
105
+ pos + 1
106
+ }
107
+
108
+ /// Emit JumpBack with placeholder distance; patch later with patch_jump_back.
109
+ fn emit_jump_back(&mut self) -> usize {
110
+ let pos = self.chunk.code.len();
111
+ self.chunk.write_u8(Opcode::JumpBack as u8);
112
+ self.chunk.write_u16(0);
113
+ pos + 1
114
+ }
115
+
116
+ fn patch_jump(&mut self, patch_pos: usize, target: usize) {
117
+ let base = patch_pos + 2;
118
+ let jump_offset = (target as i32).wrapping_sub(base as i32);
119
+ let bytes = (jump_offset as i16).to_be_bytes();
120
+ self.chunk.code[patch_pos] = bytes[0];
121
+ self.chunk.code[patch_pos + 1] = bytes[1];
122
+ }
123
+
124
+ /// Patch a JumpBack operand: distance from (patch_pos+3) back to target.
125
+ fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
126
+ let after_insn = patch_pos + 3;
127
+ let dist = after_insn.saturating_sub(target);
128
+ let bytes = (dist as u16).to_be_bytes();
129
+ self.chunk.code[patch_pos] = bytes[0];
130
+ self.chunk.code[patch_pos + 1] = bytes[1];
131
+ }
132
+
133
+ /// Detect property-based numeric sort: (a, b) => a.prop - b.prop or (a, b) => b.prop - a.prop.
134
+ /// Returns Some((prop_name, asc)) or None.
135
+ fn detect_property_sort_comparator(expr: &Expr) -> Option<(Arc<str>, bool)> {
136
+ if let Expr::ArrowFunction { params, body, .. } = expr {
137
+ if params.len() != 2 {
138
+ return None;
139
+ }
140
+ let param_a = params[0].name.as_ref();
141
+ let param_b = params[1].name.as_ref();
142
+ let body_expr = match body {
143
+ ArrowBody::Expr(e) => e.as_ref(),
144
+ ArrowBody::Block(stmt) => {
145
+ if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
146
+ e
147
+ } else {
148
+ return None;
149
+ }
150
+ }
151
+ };
152
+ if let Expr::Binary {
153
+ left,
154
+ op: BinOp::Sub,
155
+ right,
156
+ ..
157
+ } = body_expr
158
+ {
159
+ if let (Expr::Member { object: lo, prop: MemberProp::Name(p), .. }, Expr::Member { object: ro, prop: MemberProp::Name(pr), .. }) =
160
+ (left.as_ref(), right.as_ref())
161
+ {
162
+ if p != pr {
163
+ return None;
164
+ }
165
+ if let (Expr::Ident { name: ln, .. }, Expr::Ident { name: rn, .. }) =
166
+ (lo.as_ref(), ro.as_ref())
167
+ {
168
+ if ln.as_ref() == param_a && rn.as_ref() == param_b {
169
+ return Some((Arc::clone(p), true));
170
+ }
171
+ if ln.as_ref() == param_b && rn.as_ref() == param_a {
172
+ return Some((Arc::clone(p), false));
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ None
179
+ }
180
+
181
+ /// Detect numeric sort comparator: (a, b) => a - b (asc) or (a, b) => b - a (desc).
182
+ fn detect_numeric_sort_comparator(expr: &Expr) -> Option<bool> {
183
+ if let Expr::ArrowFunction { params, body, .. } = expr {
184
+ if params.len() != 2 {
185
+ return None;
186
+ }
187
+ let param_a = params[0].name.as_ref();
188
+ let param_b = params[1].name.as_ref();
189
+ let body_expr = match body {
190
+ ArrowBody::Expr(e) => e.as_ref(),
191
+ ArrowBody::Block(stmt) => {
192
+ if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
193
+ e
194
+ } else {
195
+ return None;
196
+ }
197
+ }
198
+ };
199
+ if let Expr::Binary {
200
+ left,
201
+ op: BinOp::Sub,
202
+ right,
203
+ ..
204
+ } = body_expr
205
+ {
206
+ if let (Expr::Ident { name: left_name, .. }, Expr::Ident { name: right_name, .. }) =
207
+ (left.as_ref(), right.as_ref())
208
+ {
209
+ if left_name.as_ref() == param_a && right_name.as_ref() == param_b {
210
+ return Some(true);
211
+ }
212
+ if left_name.as_ref() == param_b && right_name.as_ref() == param_a {
213
+ return Some(false);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ None
219
+ }
220
+
221
+ /// Detect simple map callback: x => x (identity) or x => x op const / x => const op x.
222
+ /// Returns SimpleMapResult for map optimization.
223
+ fn detect_simple_map_callback(expr: &Expr) -> Option<SimpleMapResult> {
224
+ let (params, body) = match expr {
225
+ Expr::ArrowFunction { params, body, .. } => (params, body),
226
+ _ => return None,
227
+ };
228
+ if params.len() != 1 {
229
+ return None;
230
+ }
231
+ let param_name = params[0].name.as_ref();
232
+ let expr_ref: &Expr = match body {
233
+ ArrowBody::Expr(e) => e.as_ref(),
234
+ ArrowBody::Block(stmt) => {
235
+ let s = stmt.as_ref();
236
+ if let Statement::Return { value: Some(ref e), .. } = s {
237
+ e
238
+ } else if let Statement::ExprStmt { expr: ref e, .. } = s {
239
+ e
240
+ } else {
241
+ return None;
242
+ }
243
+ }
244
+ };
245
+ // Identity: x => x
246
+ if let Expr::Ident { name, .. } = expr_ref {
247
+ if name.as_ref() == param_name {
248
+ return Some(SimpleMapResult::Identity);
249
+ }
250
+ }
251
+ // Binary: x op const or const op x
252
+ if let Expr::Binary { left, op, right, .. } = expr_ref {
253
+ let left_is_param = matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
254
+ let right_is_param = matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
255
+ let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
256
+ let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
257
+ if left_is_param && right_is_literal {
258
+ if let Some(c) = literal_to_constant(right.as_ref()) {
259
+ return Some(SimpleMapResult::BinOp(*op, c, true));
260
+ }
261
+ }
262
+ if left_is_literal && right_is_param {
263
+ if let Some(c) = literal_to_constant(left.as_ref()) {
264
+ return Some(SimpleMapResult::BinOp(*op, c, false));
265
+ }
266
+ }
267
+ }
268
+ None
269
+ }
270
+
271
+ /// Detect simple filter callback: x => x op const or x => const op x (comparison that returns bool).
272
+ fn detect_simple_filter_callback(expr: &Expr) -> Option<(BinOp, Constant, bool)> {
273
+ let (params, body) = match expr {
274
+ Expr::ArrowFunction { params, body, .. } => (params, body),
275
+ _ => return None,
276
+ };
277
+ if params.len() != 1 {
278
+ return None;
279
+ }
280
+ let param_name = params[0].name.as_ref();
281
+ let expr_ref: &Expr = match body {
282
+ ArrowBody::Expr(e) => e.as_ref(),
283
+ ArrowBody::Block(stmt) => {
284
+ let s = stmt.as_ref();
285
+ if let Statement::Return { value: Some(ref e), .. } = s {
286
+ e
287
+ } else if let Statement::ExprStmt { expr: ref e, .. } = s {
288
+ e
289
+ } else {
290
+ return None;
291
+ }
292
+ }
293
+ };
294
+ if let Expr::Binary { left, op, right, .. } = expr_ref {
295
+ if !matches!(op, BinOp::Eq | BinOp::Ne | BinOp::StrictEq | BinOp::StrictNe | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::And | BinOp::Or) {
296
+ return None;
297
+ }
298
+ let left_is_param = matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
299
+ let right_is_param = matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
300
+ let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
301
+ let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
302
+ if left_is_param && right_is_literal {
303
+ if let Some(c) = literal_to_constant(right.as_ref()) {
304
+ return Some((*op, c, true));
305
+ }
306
+ }
307
+ if left_is_literal && right_is_param {
308
+ if let Some(c) = literal_to_constant(left.as_ref()) {
309
+ return Some((*op, c, false));
310
+ }
311
+ }
312
+ }
313
+ None
314
+ }
315
+
316
+ fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
317
+ let stmts = &program.statements;
318
+ let last_is_expr = self.retain_last_expr
319
+ && stmts
320
+ .last()
321
+ .map(|s| matches!(s, Statement::ExprStmt { .. }))
322
+ .unwrap_or(false);
323
+
324
+ if last_is_expr {
325
+ let (rest, last) = stmts.split_at(stmts.len().saturating_sub(1));
326
+ for stmt in rest {
327
+ self.compile_statement(stmt)?;
328
+ }
329
+ if let Some(Statement::ExprStmt { expr, .. }) = last.first() {
330
+ self.compile_expr(expr)?;
331
+ }
332
+ } else {
333
+ for stmt in stmts {
334
+ self.compile_statement(stmt)?;
335
+ }
336
+ let idx = self.constant_idx(Constant::Null);
337
+ self.emit(Opcode::LoadConst);
338
+ self.chunk.write_u16(idx);
339
+ }
340
+ Ok(())
341
+ }
342
+
343
+ fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
344
+ match stmt {
345
+ Statement::Block { statements, .. } => {
346
+ self.scope.push(HashMap::new());
347
+ for s in statements {
348
+ self.compile_statement(s)?;
349
+ }
350
+ self.scope.pop();
351
+ }
352
+ Statement::VarDecl {
353
+ name,
354
+ init,
355
+ mutable: _,
356
+ ..
357
+ } => {
358
+ if let Some(expr) = init {
359
+ self.compile_expr(expr)?;
360
+ } else {
361
+ let idx = self.constant_idx(Constant::Null);
362
+ self.emit(Opcode::LoadConst);
363
+ self.chunk.write_u16(idx);
364
+ }
365
+ let idx = self.name_idx(name);
366
+ self.emit_u16(Opcode::StoreVar, idx);
367
+ self.scope
368
+ .last_mut()
369
+ .unwrap()
370
+ .insert(Arc::clone(name), false);
371
+ }
372
+ Statement::VarDeclDestructure { pattern, init, .. } => {
373
+ self.compile_expr(init)?;
374
+ self.compile_destructure(pattern, false)?;
375
+ }
376
+ Statement::ExprStmt { expr, .. } => {
377
+ self.compile_expr(expr)?;
378
+ self.emit(Opcode::Pop);
379
+ }
380
+ Statement::If {
381
+ cond,
382
+ then_branch,
383
+ else_branch,
384
+ ..
385
+ } => {
386
+ self.compile_expr(cond)?;
387
+ let jump_else = self.emit_jump(Opcode::JumpIfFalse);
388
+ self.compile_statement(then_branch)?;
389
+ let jump_end = self.emit_jump(Opcode::Jump);
390
+ self.patch_jump(jump_else, self.chunk.code.len());
391
+ if let Some(else_s) = else_branch {
392
+ self.compile_statement(else_s)?;
393
+ }
394
+ self.patch_jump(jump_end, self.chunk.code.len());
395
+ }
396
+ Statement::While { cond, body, .. } => {
397
+ let start = self.chunk.code.len();
398
+ self.loop_stack.push(LoopInfo {
399
+ break_patches: Vec::new(),
400
+ continue_patches: Vec::new(),
401
+ });
402
+ self.compile_expr(cond)?;
403
+ let jump_out = self.emit_jump(Opcode::JumpIfFalse);
404
+ // JumpIfFalse already pops condition when taking body
405
+ self.compile_statement(body)?;
406
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
407
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
408
+ let end = self.chunk.code.len();
409
+ self.patch_jump(jump_out, end);
410
+ let info = self.loop_stack.pop().unwrap();
411
+ for p in info.continue_patches {
412
+ self.patch_jump_back(p, start);
413
+ }
414
+ for p in info.break_patches {
415
+ self.patch_jump(p, end);
416
+ }
417
+ }
418
+ Statement::For {
419
+ init,
420
+ cond,
421
+ update,
422
+ body,
423
+ ..
424
+ } => {
425
+ self.scope.push(HashMap::new());
426
+ if let Some(i) = init {
427
+ self.compile_statement(i)?;
428
+ }
429
+ let cond_start = self.chunk.code.len();
430
+ if let Some(c) = cond {
431
+ self.compile_expr(c)?;
432
+ } else {
433
+ let idx = self.constant_idx(Constant::Bool(true));
434
+ self.emit(Opcode::LoadConst);
435
+ self.chunk.write_u16(idx);
436
+ }
437
+ let jump_out = self.emit_jump(Opcode::JumpIfFalse);
438
+ self.loop_stack.push(LoopInfo {
439
+ break_patches: Vec::new(),
440
+ continue_patches: Vec::new(),
441
+ });
442
+ self.compile_statement(body)?;
443
+ let update_start = self.chunk.code.len();
444
+ if let Some(u) = update {
445
+ self.compile_expr(u)?;
446
+ self.emit(Opcode::Pop);
447
+ }
448
+ let info = self.loop_stack.pop().unwrap();
449
+ for p in info.continue_patches {
450
+ self.patch_jump_back(p, update_start);
451
+ }
452
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
453
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
454
+ let end = self.chunk.code.len();
455
+ self.patch_jump(jump_out, end);
456
+ for p in info.break_patches {
457
+ self.patch_jump(p, end);
458
+ }
459
+ self.scope.pop();
460
+ }
461
+ Statement::ForOf { name, iterable, body, .. } => {
462
+ self.compile_expr(iterable)?;
463
+ self.scope.push(HashMap::new());
464
+ let arr_name = Arc::from("__forof_arr__");
465
+ let i_name = Arc::from("__forof_i__");
466
+ let len_name = Arc::from("__forof_len__");
467
+ let arr_idx = self.name_idx(&arr_name);
468
+ let i_idx = self.name_idx(&i_name);
469
+ let len_idx = self.name_idx(&len_name);
470
+ let name_idx = self.name_idx(name);
471
+ self.emit_u16(Opcode::StoreVar, arr_idx);
472
+ self.scope.last_mut().unwrap().insert(arr_name.clone(), false);
473
+ self.emit_u16(Opcode::LoadVar, arr_idx);
474
+ let len_name_idx = self.name_idx(&Arc::from("length"));
475
+ self.emit_u16(Opcode::GetMember, len_name_idx);
476
+ self.emit_u16(Opcode::StoreVar, len_idx);
477
+ self.scope.last_mut().unwrap().insert(len_name.clone(), false);
478
+ let zero_idx = self.constant_idx(Constant::Number(0.0));
479
+ self.emit(Opcode::LoadConst);
480
+ self.chunk.write_u16(zero_idx);
481
+ self.emit_u16(Opcode::StoreVar, i_idx);
482
+ self.scope.last_mut().unwrap().insert(i_name.clone(), false);
483
+ let loop_start = self.chunk.code.len();
484
+ self.loop_stack.push(LoopInfo {
485
+ break_patches: Vec::new(),
486
+ continue_patches: Vec::new(),
487
+ });
488
+ self.emit_u16(Opcode::LoadVar, arr_idx);
489
+ self.emit_u16(Opcode::LoadVar, i_idx);
490
+ self.emit(Opcode::GetIndex);
491
+ self.emit_u16(Opcode::StoreVar, name_idx);
492
+ self.scope.last_mut().unwrap().insert(Arc::clone(name), false);
493
+ self.compile_statement(body)?;
494
+ self.emit_u16(Opcode::LoadVar, i_idx);
495
+ let one_idx = self.constant_idx(Constant::Number(1.0));
496
+ self.emit(Opcode::LoadConst);
497
+ self.chunk.write_u16(one_idx);
498
+ self.emit_u8(Opcode::BinOp, 0);
499
+ self.emit_u16(Opcode::StoreVar, i_idx);
500
+ self.emit_u16(Opcode::LoadVar, i_idx);
501
+ self.emit_u16(Opcode::LoadVar, len_idx);
502
+ self.emit_u8(Opcode::BinOp, 10);
503
+ let jump_out = self.emit_jump(Opcode::JumpIfFalse);
504
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(loop_start);
505
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
506
+ let end = self.chunk.code.len();
507
+ self.patch_jump(jump_out, end);
508
+ let info = self.loop_stack.pop().unwrap();
509
+ for p in info.continue_patches {
510
+ self.patch_jump_back(p, loop_start);
511
+ }
512
+ for p in info.break_patches {
513
+ self.patch_jump(p, end);
514
+ }
515
+ self.scope.pop();
516
+ }
517
+ Statement::Return { value, .. } => {
518
+ if let Some(v) = value {
519
+ self.compile_expr(v)?;
520
+ } else {
521
+ let idx = self.constant_idx(Constant::Null);
522
+ self.emit(Opcode::LoadConst);
523
+ self.chunk.write_u16(idx);
524
+ }
525
+ self.emit(Opcode::Return);
526
+ }
527
+ Statement::Break { .. } => {
528
+ let pos = self.emit_jump(Opcode::Jump);
529
+ if let Some(sw) = self.switch_stack.last_mut() {
530
+ sw.break_patches.push(pos);
531
+ } else if let Some(lo) = self.loop_stack.last_mut() {
532
+ lo.break_patches.push(pos);
533
+ } else {
534
+ return Err(CompileError {
535
+ message: "break not inside a loop or switch".to_string(),
536
+ });
537
+ }
538
+ }
539
+ Statement::Continue { .. } => {
540
+ let pos = self.emit_jump_back();
541
+ self.loop_stack.last_mut().ok_or_else(|| CompileError {
542
+ message: "continue not inside a loop".to_string(),
543
+ })?.continue_patches.push(pos);
544
+ }
545
+ Statement::FunDecl {
546
+ name,
547
+ params,
548
+ body,
549
+ rest_param,
550
+ async_: _,
551
+ ..
552
+ } => {
553
+ let mut inner = Chunk::new();
554
+ let mut param_names: Vec<Arc<str>> =
555
+ params.iter().map(|p| Arc::clone(&p.name)).collect();
556
+ if let Some(rp) = rest_param {
557
+ param_names.push(rp.name.clone());
558
+ inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
559
+ }
560
+ for p in &param_names {
561
+ inner.add_name(Arc::clone(p));
562
+ }
563
+ inner.param_count = param_names.len() as u16;
564
+ let mut inner_comp = Compiler::new(&mut inner, false);
565
+ inner_comp.scope = vec![param_names
566
+ .iter()
567
+ .map(|n| (Arc::clone(n), false))
568
+ .collect::<HashMap<_, _>>()];
569
+ inner_comp.compile_statement(body)?;
570
+ inner_comp.emit(Opcode::LoadConst);
571
+ let idx = inner_comp.constant_idx(Constant::Null);
572
+ inner_comp.chunk.write_u16(idx);
573
+ inner_comp.emit(Opcode::Return);
574
+ let nested_idx = self.chunk.add_nested(inner);
575
+ self.emit(Opcode::LoadConst);
576
+ let idx = self.constant_idx(Constant::Closure(nested_idx));
577
+ self.chunk.write_u16(idx);
578
+ let idx = self.name_idx(name);
579
+ self.emit_u16(Opcode::StoreVar, idx);
580
+ self.scope.last_mut().unwrap().insert(Arc::clone(name), false);
581
+ }
582
+ Statement::DoWhile { body, cond, .. } => {
583
+ let start = self.chunk.code.len();
584
+ self.loop_stack.push(LoopInfo {
585
+ break_patches: Vec::new(),
586
+ continue_patches: Vec::new(),
587
+ });
588
+ self.compile_statement(body)?;
589
+ let cond_start = self.chunk.code.len();
590
+ self.compile_expr(cond)?;
591
+ let jump_back = self.emit_jump(Opcode::JumpIfFalse);
592
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
593
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
594
+ let end = self.chunk.code.len();
595
+ self.patch_jump(jump_back, end);
596
+ let info = self.loop_stack.pop().unwrap();
597
+ for p in info.continue_patches {
598
+ self.patch_jump_back(p, cond_start);
599
+ }
600
+ for p in info.break_patches {
601
+ self.patch_jump(p, end);
602
+ }
603
+ }
604
+ Statement::Switch { expr, cases, default_body, .. } => {
605
+ self.switch_stack.push(SwitchInfo {
606
+ break_patches: Vec::new(),
607
+ });
608
+ self.compile_expr(expr)?;
609
+ self.emit(Opcode::Dup);
610
+ let mut end_patches = Vec::new();
611
+ for (case_expr, case_body) in cases {
612
+ self.emit(Opcode::Dup);
613
+ if let Some(ce) = case_expr {
614
+ self.compile_expr(ce)?;
615
+ self.emit_u8(Opcode::BinOp, 8);
616
+ let jump_next = self.emit_jump(Opcode::JumpIfFalse);
617
+ // JumpIfFalse already pops the match result when taking this case
618
+ self.compile_statement(&Statement::Block {
619
+ statements: case_body.clone(),
620
+ span: Span {
621
+ start: (0, 0),
622
+ end: (0, 0),
623
+ },
624
+ })?;
625
+ let jump_end = self.emit_jump(Opcode::Jump);
626
+ end_patches.push(jump_end);
627
+ self.patch_jump(jump_next, self.chunk.code.len());
628
+ } else {
629
+ self.emit(Opcode::Pop);
630
+ self.compile_statement(&Statement::Block {
631
+ statements: case_body.clone(),
632
+ span: Span {
633
+ start: (0, 0),
634
+ end: (0, 0),
635
+ },
636
+ })?;
637
+ }
638
+ }
639
+ if let Some(body) = default_body {
640
+ self.emit(Opcode::Pop);
641
+ self.compile_statement(&Statement::Block {
642
+ statements: body.clone(),
643
+ span: Span {
644
+ start: (0, 0),
645
+ end: (0, 0),
646
+ },
647
+ })?;
648
+ } else {
649
+ self.emit(Opcode::Pop);
650
+ }
651
+ for p in end_patches {
652
+ self.patch_jump(p, self.chunk.code.len());
653
+ }
654
+ let sw = self.switch_stack.pop().unwrap();
655
+ for p in sw.break_patches {
656
+ self.patch_jump(p, self.chunk.code.len());
657
+ }
658
+ }
659
+ Statement::Throw { value, .. } => {
660
+ self.compile_expr(value)?;
661
+ self.emit(Opcode::Throw);
662
+ }
663
+ Statement::Try {
664
+ body,
665
+ catch_param,
666
+ catch_body,
667
+ finally_body,
668
+ ..
669
+ } => {
670
+ let catch_offset_pos = self.chunk.code.len();
671
+ self.emit(Opcode::EnterTry);
672
+ self.chunk.write_u16(0);
673
+ self.compile_statement(body)?;
674
+ self.emit(Opcode::ExitTry);
675
+ let jump_over_catch = self.emit_jump(Opcode::Jump);
676
+ let catch_start = self.chunk.code.len();
677
+ if let Some(catch_stmt) = catch_body {
678
+ if let Some(param) = catch_param {
679
+ let param_idx = self.name_idx(param);
680
+ self.emit_u16(Opcode::StoreVar, param_idx);
681
+ self.scope
682
+ .last_mut()
683
+ .unwrap()
684
+ .insert(Arc::clone(param), false);
685
+ } else {
686
+ self.emit(Opcode::Pop);
687
+ }
688
+ self.compile_statement(catch_stmt)?;
689
+ } else {
690
+ self.emit(Opcode::Throw);
691
+ }
692
+ let after_catch = self.chunk.code.len();
693
+ self.patch_jump(jump_over_catch, after_catch);
694
+ if let Some(finally) = finally_body {
695
+ self.compile_statement(finally)?;
696
+ }
697
+ let catch_offset = catch_start.wrapping_sub(catch_offset_pos).wrapping_sub(3) as u16;
698
+ self.chunk.code[catch_offset_pos + 1] = (catch_offset >> 8) as u8;
699
+ self.chunk.code[catch_offset_pos + 2] = (catch_offset & 0xff) as u8;
700
+ }
701
+ Statement::Import { .. } | Statement::Export { .. } => {
702
+ return Err(CompileError {
703
+ message: "Import/Export not supported in bytecode".to_string(),
704
+ });
705
+ }
706
+ }
707
+ Ok(())
708
+ }
709
+
710
+ fn compile_destructure(
711
+ &mut self,
712
+ pattern: &DestructPattern,
713
+ mutable: bool,
714
+ ) -> Result<(), CompileError> {
715
+ match pattern {
716
+ DestructPattern::Array(elements) => {
717
+ for (i, elem) in elements.iter().enumerate() {
718
+ match elem {
719
+ Some(DestructElement::Ident(name)) => {
720
+ self.emit(Opcode::Dup);
721
+ let idx = self.constant_idx(Constant::Number(i as f64));
722
+ self.emit(Opcode::LoadConst);
723
+ self.chunk.write_u16(idx);
724
+ self.emit(Opcode::GetIndex);
725
+ let idx = self.name_idx(name);
726
+ self.emit_u16(Opcode::StoreVar, idx);
727
+ self.scope
728
+ .last_mut()
729
+ .unwrap()
730
+ .insert(Arc::clone(name), false);
731
+ }
732
+ _ => {
733
+ return Err(CompileError {
734
+ message: "Complex destructuring not yet supported".to_string(),
735
+ });
736
+ }
737
+ }
738
+ }
739
+ self.emit(Opcode::Pop);
740
+ }
741
+ DestructPattern::Object(props) => {
742
+ for prop in props {
743
+ self.emit(Opcode::Dup);
744
+ let key_idx = self.constant_idx(Constant::String(Arc::clone(&prop.key)));
745
+ self.emit(Opcode::LoadConst);
746
+ self.chunk.write_u16(key_idx);
747
+ self.emit(Opcode::GetIndex); // GetIndex pops obj, index and uses get_member
748
+ match &prop.value {
749
+ DestructElement::Ident(name) => {
750
+ let idx = self.name_idx(name);
751
+ self.emit_u16(Opcode::StoreVar, idx);
752
+ if mutable {
753
+ self.scope
754
+ .last_mut()
755
+ .unwrap()
756
+ .insert(Arc::clone(name), false);
757
+ }
758
+ }
759
+ _ => {
760
+ return Err(CompileError {
761
+ message: "Nested object destructuring not yet supported"
762
+ .to_string(),
763
+ });
764
+ }
765
+ }
766
+ }
767
+ self.emit(Opcode::Pop);
768
+ }
769
+ }
770
+ Ok(())
771
+ }
772
+
773
+ fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
774
+ match expr {
775
+ Expr::Literal { value, .. } => {
776
+ let c = match value {
777
+ Literal::Number(n) => Constant::Number(*n),
778
+ Literal::String(s) => Constant::String(Arc::clone(s)),
779
+ Literal::Bool(b) => Constant::Bool(*b),
780
+ Literal::Null => Constant::Null,
781
+ };
782
+ let idx = self.constant_idx(c);
783
+ self.emit(Opcode::LoadConst);
784
+ self.chunk.write_u16(idx);
785
+ }
786
+ Expr::Ident { name, .. } => {
787
+ let idx = self.name_idx(name);
788
+ self.emit_u16(Opcode::LoadVar, idx);
789
+ }
790
+ Expr::Binary { left, op, right, .. } => {
791
+ match op {
792
+ BinOp::And => {
793
+ // Short-circuit: a && b => if !a then a else b
794
+ self.compile_expr(left)?;
795
+ self.emit(Opcode::Dup);
796
+ let jump_shortcut = self.emit_jump(Opcode::JumpIfFalse);
797
+ self.compile_expr(right)?; // left still on stack from Dup
798
+ self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::And));
799
+ let jump_end = self.emit_jump(Opcode::Jump);
800
+ self.patch_jump(jump_shortcut, self.chunk.code.len());
801
+ self.patch_jump(jump_end, self.chunk.code.len());
802
+ }
803
+ BinOp::Or => {
804
+ // Short-circuit: a || b => if a then a else b
805
+ self.compile_expr(left)?;
806
+ self.emit(Opcode::Dup);
807
+ let jump_eval_right = self.emit_jump(Opcode::JumpIfFalse);
808
+ let jump_end = self.emit_jump(Opcode::Jump);
809
+ self.patch_jump(jump_eval_right, self.chunk.code.len());
810
+ self.emit(Opcode::Pop); // discard falsy left
811
+ self.compile_expr(right)?;
812
+ self.patch_jump(jump_end, self.chunk.code.len());
813
+ }
814
+ _ => {
815
+ self.compile_expr(left)?;
816
+ self.compile_expr(right)?;
817
+ self.emit_u8(Opcode::BinOp, binop_to_u8(*op));
818
+ }
819
+ }
820
+ }
821
+ Expr::Unary { op, operand, .. } => {
822
+ self.compile_expr(operand)?;
823
+ self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
824
+ }
825
+ Expr::Call { callee, args, .. } => {
826
+ // Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
827
+ if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
828
+ && args.len() == 1
829
+ && matches!(args[0], CallArg::Expr(_))
830
+ {
831
+ if let (Expr::Member { object, prop: MemberProp::Name(key), optional: false, .. }, CallArg::Expr(cmp_expr)) =
832
+ (callee.as_ref(), &args[0])
833
+ {
834
+ if key.as_ref() == "sort" {
835
+ if let Some(ascending) = Self::detect_numeric_sort_comparator(cmp_expr) {
836
+ self.compile_expr(object)?;
837
+ self.emit_u8(Opcode::ArraySortNumeric, if ascending { 0 } else { 1 });
838
+ return Ok(());
839
+ }
840
+ if let Some((prop, ascending)) = Self::detect_property_sort_comparator(cmp_expr) {
841
+ self.compile_expr(object)?;
842
+ let prop_idx = self.constant_idx(Constant::String(prop));
843
+ self.emit(Opcode::ArraySortByProperty);
844
+ self.chunk.write_u16(prop_idx);
845
+ self.chunk.write_u16(if ascending { 0 } else { 1 });
846
+ return Ok(());
847
+ }
848
+ }
849
+ if key.as_ref() == "map" {
850
+ if let Some(simple) = Self::detect_simple_map_callback(cmp_expr) {
851
+ self.compile_expr(object)?;
852
+ match simple {
853
+ SimpleMapResult::Identity => {
854
+ self.emit(Opcode::ArrayMapIdentity);
855
+ }
856
+ SimpleMapResult::BinOp(op, c, param_left) => {
857
+ let const_idx = self.constant_idx(c);
858
+ self.emit(Opcode::ArrayMapBinOp);
859
+ self.chunk.write_u8(binop_to_u8(op));
860
+ self.chunk.write_u16(const_idx);
861
+ self.chunk.write_u8(if param_left { 0 } else { 1 });
862
+ }
863
+ }
864
+ return Ok(());
865
+ }
866
+ }
867
+ if key.as_ref() == "filter" {
868
+ if let Some((op, const_val, param_left)) =
869
+ Self::detect_simple_filter_callback(cmp_expr)
870
+ {
871
+ self.compile_expr(object)?;
872
+ let const_idx = self.constant_idx(const_val);
873
+ self.emit(Opcode::ArrayFilterBinOp);
874
+ self.chunk.write_u8(binop_to_u8(op));
875
+ self.chunk.write_u16(const_idx);
876
+ self.chunk.write_u8(if param_left { 0 } else { 1 });
877
+ return Ok(());
878
+ }
879
+ }
880
+ }
881
+ }
882
+ let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
883
+ if has_spread {
884
+ // Build args array [a, ...b, c], then callee, then CallSpread
885
+ self.emit_u16(Opcode::NewArray, 0);
886
+ for arg in args {
887
+ match arg {
888
+ CallArg::Expr(e) => {
889
+ self.compile_expr(e)?;
890
+ self.emit_u16(Opcode::NewArray, 1);
891
+ self.emit(Opcode::ConcatArray);
892
+ }
893
+ CallArg::Spread(expr) => {
894
+ self.compile_expr(expr)?;
895
+ self.emit(Opcode::ConcatArray);
896
+ }
897
+ }
898
+ }
899
+ self.compile_expr(callee)?;
900
+ self.emit(Opcode::CallSpread);
901
+ } else {
902
+ self.compile_expr(callee)?;
903
+ for arg in args {
904
+ if let CallArg::Expr(e) = arg {
905
+ self.compile_expr(e)?;
906
+ }
907
+ }
908
+ self.emit_u16(Opcode::Call, args.len() as u16);
909
+ }
910
+ }
911
+ Expr::Member {
912
+ object,
913
+ prop,
914
+ optional,
915
+ ..
916
+ } => {
917
+ self.compile_expr(object)?;
918
+ if *optional {
919
+ self.emit(Opcode::Dup);
920
+ let null_idx = self.constant_idx(Constant::Null);
921
+ self.emit(Opcode::LoadConst);
922
+ self.chunk.write_u16(null_idx);
923
+ self.emit_u8(Opcode::BinOp, 8);
924
+ let jump_to_null = self.emit_jump(Opcode::JumpIfFalse);
925
+ let jump_to_get_instr = self.chunk.code.len();
926
+ let jump_to_get = self.emit_jump(Opcode::Jump);
927
+ self.patch_jump(jump_to_null, jump_to_get_instr);
928
+ self.emit(Opcode::Pop);
929
+ self.emit(Opcode::LoadConst);
930
+ self.chunk.write_u16(null_idx);
931
+ let jump_end = self.emit_jump(Opcode::Jump);
932
+ self.patch_jump(jump_to_get, self.chunk.code.len());
933
+ match prop {
934
+ MemberProp::Name(key) => {
935
+ let idx = self.name_idx(key);
936
+ self.emit_u16(Opcode::GetMemberOptional, idx);
937
+ }
938
+ MemberProp::Expr(e) => {
939
+ self.compile_expr(e)?;
940
+ self.emit(Opcode::GetIndex);
941
+ }
942
+ }
943
+ self.patch_jump(jump_end, self.chunk.code.len());
944
+ } else {
945
+ match prop {
946
+ MemberProp::Name(key) => {
947
+ let idx = self.name_idx(key);
948
+ self.emit_u16(Opcode::GetMember, idx);
949
+ }
950
+ MemberProp::Expr(e) => {
951
+ self.compile_expr(e)?;
952
+ self.emit(Opcode::GetIndex);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ Expr::Index { object, index, .. } => {
958
+ self.compile_expr(object)?;
959
+ self.compile_expr(index)?;
960
+ self.emit(Opcode::GetIndex);
961
+ }
962
+ Expr::Conditional {
963
+ cond,
964
+ then_branch,
965
+ else_branch,
966
+ ..
967
+ } => {
968
+ self.compile_expr(cond)?;
969
+ let jump_else = self.emit_jump(Opcode::JumpIfFalse);
970
+ // JumpIfFalse pops condition when taking then; when taking else it also pops
971
+ self.compile_expr(then_branch)?;
972
+ let jump_end = self.emit_jump(Opcode::Jump);
973
+ self.patch_jump(jump_else, self.chunk.code.len());
974
+ // no Pop: condition was already popped by JumpIfFalse
975
+ self.compile_expr(else_branch)?;
976
+ self.patch_jump(jump_end, self.chunk.code.len());
977
+ }
978
+ Expr::NullishCoalesce { left, right, .. } => {
979
+ self.compile_expr(left)?;
980
+ self.emit(Opcode::Dup);
981
+ let idx = self.constant_idx(Constant::Null);
982
+ self.emit(Opcode::LoadConst);
983
+ self.chunk.write_u16(idx);
984
+ self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictNe));
985
+ let jump_to_right = self.emit_jump(Opcode::JumpIfFalse);
986
+ let jump_end = self.emit_jump(Opcode::Jump);
987
+ self.patch_jump(jump_to_right, self.chunk.code.len());
988
+ self.emit(Opcode::Pop);
989
+ self.compile_expr(right)?;
990
+ self.patch_jump(jump_end, self.chunk.code.len());
991
+ }
992
+ Expr::Array { elements, .. } => {
993
+ let has_spread = elements.iter().any(|e| matches!(e, ArrayElement::Spread(_)));
994
+ if has_spread {
995
+ // Build array incrementally: start with [], concat each element
996
+ self.emit_u16(Opcode::NewArray, 0);
997
+ for elem in elements {
998
+ match elem {
999
+ ArrayElement::Expr(e) => {
1000
+ self.compile_expr(e)?;
1001
+ self.emit_u16(Opcode::NewArray, 1);
1002
+ self.emit(Opcode::ConcatArray);
1003
+ }
1004
+ ArrayElement::Spread(expr) => {
1005
+ self.compile_expr(expr)?;
1006
+ self.emit(Opcode::ConcatArray);
1007
+ }
1008
+ }
1009
+ }
1010
+ } else {
1011
+ for elem in elements {
1012
+ if let ArrayElement::Expr(e) = elem {
1013
+ self.compile_expr(e)?;
1014
+ }
1015
+ }
1016
+ self.emit_u16(Opcode::NewArray, elements.len() as u16);
1017
+ }
1018
+ }
1019
+ Expr::Object { props, .. } => {
1020
+ let has_spread = props.iter().any(|p| matches!(p, ObjectProp::Spread(_)));
1021
+ if has_spread {
1022
+ self.emit_u16(Opcode::NewObject, 0); // start with {}
1023
+ for prop in props {
1024
+ match prop {
1025
+ ObjectProp::KeyValue(k, v) => {
1026
+ let idx = self.constant_idx(Constant::String(Arc::clone(k)));
1027
+ self.emit(Opcode::LoadConst);
1028
+ self.chunk.write_u16(idx);
1029
+ self.compile_expr(v)?;
1030
+ self.emit_u16(Opcode::NewObject, 1);
1031
+ self.emit(Opcode::MergeObject);
1032
+ }
1033
+ ObjectProp::Spread(expr) => {
1034
+ self.compile_expr(expr)?;
1035
+ self.emit(Opcode::MergeObject);
1036
+ }
1037
+ }
1038
+ }
1039
+ } else {
1040
+ for prop in props {
1041
+ if let ObjectProp::KeyValue(k, v) = prop {
1042
+ let idx = self.constant_idx(Constant::String(Arc::clone(k)));
1043
+ self.emit(Opcode::LoadConst);
1044
+ self.chunk.write_u16(idx);
1045
+ self.compile_expr(v)?;
1046
+ }
1047
+ }
1048
+ self.emit_u16(Opcode::NewObject, props.len() as u16);
1049
+ }
1050
+ }
1051
+ Expr::Assign { name, value, .. } => {
1052
+ self.compile_expr(value)?;
1053
+ let idx = self.name_idx(name);
1054
+ self.emit_u16(Opcode::StoreVar, idx);
1055
+ self.emit_u16(Opcode::LoadVar, idx); // assign yields value
1056
+ }
1057
+ Expr::TypeOf { operand, .. } => {
1058
+ let typeof_idx = self.name_idx(&Arc::from("typeof"));
1059
+ self.emit_u16(Opcode::LoadGlobal, typeof_idx);
1060
+ self.compile_expr(operand)?;
1061
+ self.emit_u16(Opcode::Call, 1);
1062
+ }
1063
+ Expr::ArrowFunction { params, body, .. } => {
1064
+ let mut inner = Chunk::new();
1065
+ let param_names: Vec<Arc<str>> =
1066
+ params.iter().map(|p| Arc::clone(&p.name)).collect();
1067
+ for p in &param_names {
1068
+ inner.add_name(Arc::clone(p));
1069
+ }
1070
+ inner.param_count = param_names.len() as u16;
1071
+ let mut inner_comp = Compiler::new(&mut inner, false);
1072
+ inner_comp.scope = vec![param_names
1073
+ .iter()
1074
+ .map(|n| (Arc::clone(n), false))
1075
+ .collect::<HashMap<_, _>>()];
1076
+ match body {
1077
+ ArrowBody::Expr(e) => {
1078
+ inner_comp.compile_expr(e)?;
1079
+ inner_comp.emit(Opcode::Return);
1080
+ }
1081
+ ArrowBody::Block(s) => {
1082
+ inner_comp.compile_statement(s)?;
1083
+ let idx = inner_comp.constant_idx(Constant::Null);
1084
+ inner_comp.emit(Opcode::LoadConst);
1085
+ inner_comp.chunk.write_u16(idx);
1086
+ inner_comp.emit(Opcode::Return);
1087
+ }
1088
+ }
1089
+ let nested_idx = self.chunk.add_nested(inner);
1090
+ let idx = self.constant_idx(Constant::Closure(nested_idx));
1091
+ self.emit(Opcode::LoadConst);
1092
+ self.chunk.write_u16(idx);
1093
+ }
1094
+ Expr::TemplateLiteral { quasis, exprs, .. } => {
1095
+ if exprs.is_empty() {
1096
+ let s = quasis[0].to_string();
1097
+ let idx = self.constant_idx(Constant::String(Arc::from(s)));
1098
+ self.emit(Opcode::LoadConst);
1099
+ self.chunk.write_u16(idx);
1100
+ } else {
1101
+ // Interleave quasis and exprs: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + quasi[n]
1102
+ let first = quasis[0].to_string();
1103
+ let idx = self.constant_idx(Constant::String(Arc::from(first)));
1104
+ self.emit(Opcode::LoadConst);
1105
+ self.chunk.write_u16(idx);
1106
+ for (i, expr) in exprs.iter().enumerate() {
1107
+ self.compile_expr(expr)?;
1108
+ self.emit_u8(Opcode::BinOp, 0); // Add (string concat)
1109
+ let quasi_s = quasis[i + 1].to_string();
1110
+ let qidx = self.constant_idx(Constant::String(Arc::from(quasi_s)));
1111
+ self.emit(Opcode::LoadConst);
1112
+ self.chunk.write_u16(qidx);
1113
+ self.emit_u8(Opcode::BinOp, 0); // Add
1114
+ }
1115
+ }
1116
+ }
1117
+ Expr::PostfixInc { name, .. } => {
1118
+ let idx = self.name_idx(name);
1119
+ let one = self.constant_idx(Constant::Number(1.0));
1120
+ self.emit_u16(Opcode::LoadVar, idx);
1121
+ self.emit(Opcode::Dup);
1122
+ self.emit(Opcode::LoadConst);
1123
+ self.chunk.write_u16(one);
1124
+ self.emit_u8(Opcode::BinOp, 0);
1125
+ self.emit_u16(Opcode::StoreVar, idx);
1126
+ }
1127
+ Expr::PostfixDec { name, .. } => {
1128
+ let idx = self.name_idx(name);
1129
+ let one = self.constant_idx(Constant::Number(1.0));
1130
+ self.emit_u16(Opcode::LoadVar, idx);
1131
+ self.emit(Opcode::Dup);
1132
+ self.emit(Opcode::LoadConst);
1133
+ self.chunk.write_u16(one);
1134
+ self.emit_u8(Opcode::BinOp, 1);
1135
+ self.emit_u16(Opcode::StoreVar, idx);
1136
+ }
1137
+ Expr::PrefixInc { name, .. } => {
1138
+ let idx = self.name_idx(name);
1139
+ let one = self.constant_idx(Constant::Number(1.0));
1140
+ self.emit_u16(Opcode::LoadVar, idx);
1141
+ self.emit(Opcode::LoadConst);
1142
+ self.chunk.write_u16(one);
1143
+ self.emit_u8(Opcode::BinOp, 0);
1144
+ self.emit(Opcode::Dup);
1145
+ self.emit_u16(Opcode::StoreVar, idx);
1146
+ }
1147
+ Expr::PrefixDec { name, .. } => {
1148
+ let idx = self.name_idx(name);
1149
+ let one = self.constant_idx(Constant::Number(1.0));
1150
+ self.emit_u16(Opcode::LoadVar, idx);
1151
+ self.emit(Opcode::LoadConst);
1152
+ self.chunk.write_u16(one);
1153
+ self.emit_u8(Opcode::BinOp, 1);
1154
+ self.emit(Opcode::Dup);
1155
+ self.emit_u16(Opcode::StoreVar, idx);
1156
+ }
1157
+ Expr::CompoundAssign { name, op, value, .. } => {
1158
+ let idx = self.name_idx(name);
1159
+ self.emit_u16(Opcode::LoadVar, idx);
1160
+ self.compile_expr(value)?;
1161
+ self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
1162
+ self.emit(Opcode::Dup);
1163
+ self.emit_u16(Opcode::StoreVar, idx);
1164
+ }
1165
+ Expr::MemberAssign { object, prop, value, .. } => {
1166
+ self.compile_expr(object)?;
1167
+ self.compile_expr(value)?;
1168
+ let idx = self.name_idx(prop);
1169
+ self.emit_u16(Opcode::SetMember, idx); // SetMember pops obj, val and pushes val back
1170
+ }
1171
+ Expr::IndexAssign { object, index, value, .. } => {
1172
+ self.compile_expr(object)?;
1173
+ self.compile_expr(index)?;
1174
+ self.compile_expr(value)?;
1175
+ self.emit(Opcode::Dup); // leave copy for assignment expression result
1176
+ self.emit(Opcode::SetIndex);
1177
+ }
1178
+ Expr::NativeModuleLoad { spec, export_name, .. } => {
1179
+ let spec_idx = self.constant_idx(Constant::String(Arc::clone(spec)));
1180
+ let export_idx = self.constant_idx(Constant::String(Arc::clone(export_name)));
1181
+ self.emit(Opcode::LoadNativeExport);
1182
+ self.chunk.write_u16(spec_idx);
1183
+ self.chunk.write_u16(export_idx);
1184
+ }
1185
+ Expr::JsxElement {
1186
+ tag, props, children, ..
1187
+ } => {
1188
+ self.compile_jsx_element(tag, props, children)?;
1189
+ }
1190
+ Expr::JsxFragment { children, .. } => {
1191
+ self.compile_jsx_fragment(children)?;
1192
+ }
1193
+ Expr::Await { operand, .. } => {
1194
+ // await expr => LoadNativeExport("tish:http","await"), compile(operand), Call(1)
1195
+ let spec_idx = self.constant_idx(Constant::String(Arc::from("tish:http")));
1196
+ let await_idx = self.constant_idx(Constant::String(Arc::from("await")));
1197
+ self.emit(Opcode::LoadNativeExport);
1198
+ self.chunk.write_u16(spec_idx);
1199
+ self.chunk.write_u16(await_idx);
1200
+ self.compile_expr(operand)?;
1201
+ self.emit_u16(Opcode::Call, 1);
1202
+ }
1203
+ Expr::LogicalAssign { .. } => {
1204
+ return Err(CompileError {
1205
+ message: "Logical assignment (&&=, ||=, ??=) not yet supported in bytecode".to_string(),
1206
+ });
1207
+ }
1208
+ }
1209
+ Ok(())
1210
+ }
1211
+
1212
+ fn compile_jsx_element(
1213
+ &mut self,
1214
+ tag: &Arc<str>,
1215
+ props: &[JsxProp],
1216
+ children: &[JsxChild],
1217
+ ) -> Result<(), CompileError> {
1218
+ let h_idx = self.name_idx(&Arc::from("h"));
1219
+ self.emit_u16(Opcode::LoadGlobal, h_idx);
1220
+ let tag_str = tag.as_ref();
1221
+ let is_component = tag_str.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
1222
+ if is_component {
1223
+ let tag_idx = self.name_idx(tag);
1224
+ self.emit_u16(Opcode::LoadGlobal, tag_idx);
1225
+ } else {
1226
+ let tag_const = self.constant_idx(Constant::String(Arc::from(tag_str)));
1227
+ self.emit(Opcode::LoadConst);
1228
+ self.chunk.write_u16(tag_const);
1229
+ }
1230
+ self.compile_jsx_props(props)?;
1231
+ self.compile_jsx_children(children)?;
1232
+ self.emit_u16(Opcode::Call, 3);
1233
+ Ok(())
1234
+ }
1235
+
1236
+ fn compile_jsx_fragment(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
1237
+ let h_idx = self.name_idx(&Arc::from("h"));
1238
+ self.emit_u16(Opcode::LoadGlobal, h_idx);
1239
+ let fragment_idx = self.name_idx(&Arc::from("Fragment"));
1240
+ self.emit_u16(Opcode::LoadGlobal, fragment_idx);
1241
+ let null_idx = self.constant_idx(Constant::Null);
1242
+ self.emit(Opcode::LoadConst);
1243
+ self.chunk.write_u16(null_idx);
1244
+ self.compile_jsx_children(children)?;
1245
+ self.emit_u16(Opcode::Call, 3);
1246
+ Ok(())
1247
+ }
1248
+
1249
+ fn compile_jsx_props(&mut self, props: &[JsxProp]) -> Result<(), CompileError> {
1250
+ if props.is_empty() {
1251
+ let null_idx = self.constant_idx(Constant::Null);
1252
+ self.emit(Opcode::LoadConst);
1253
+ self.chunk.write_u16(null_idx);
1254
+ return Ok(());
1255
+ }
1256
+ let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
1257
+ if has_spread {
1258
+ self.emit_u16(Opcode::NewObject, 0);
1259
+ for prop in props {
1260
+ match prop {
1261
+ JsxProp::Attr { name, value } => {
1262
+ let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
1263
+ self.emit(Opcode::LoadConst);
1264
+ self.chunk.write_u16(key_idx);
1265
+ match value {
1266
+ JsxAttrValue::String(s) => {
1267
+ let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
1268
+ self.emit(Opcode::LoadConst);
1269
+ self.chunk.write_u16(val_idx);
1270
+ }
1271
+ JsxAttrValue::Expr(e) => self.compile_expr(e)?,
1272
+ JsxAttrValue::ImplicitTrue => {
1273
+ let true_idx = self.constant_idx(Constant::Bool(true));
1274
+ self.emit(Opcode::LoadConst);
1275
+ self.chunk.write_u16(true_idx);
1276
+ }
1277
+ }
1278
+ self.emit_u16(Opcode::NewObject, 1);
1279
+ self.emit(Opcode::MergeObject);
1280
+ }
1281
+ JsxProp::Spread(expr) => {
1282
+ self.compile_expr(expr)?;
1283
+ self.emit(Opcode::MergeObject);
1284
+ }
1285
+ }
1286
+ }
1287
+ } else {
1288
+ for prop in props {
1289
+ if let JsxProp::Attr { name, value } = prop {
1290
+ let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
1291
+ self.emit(Opcode::LoadConst);
1292
+ self.chunk.write_u16(key_idx);
1293
+ match value {
1294
+ JsxAttrValue::String(s) => {
1295
+ let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
1296
+ self.emit(Opcode::LoadConst);
1297
+ self.chunk.write_u16(val_idx);
1298
+ }
1299
+ JsxAttrValue::Expr(e) => self.compile_expr(e)?,
1300
+ JsxAttrValue::ImplicitTrue => {
1301
+ let true_idx = self.constant_idx(Constant::Bool(true));
1302
+ self.emit(Opcode::LoadConst);
1303
+ self.chunk.write_u16(true_idx);
1304
+ }
1305
+ }
1306
+ }
1307
+ }
1308
+ self.emit_u16(Opcode::NewObject, props.len() as u16);
1309
+ }
1310
+ Ok(())
1311
+ }
1312
+
1313
+ fn compile_jsx_children(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
1314
+ for child in children {
1315
+ match child {
1316
+ JsxChild::Text(s) => {
1317
+ let idx = self.constant_idx(Constant::String(Arc::clone(s)));
1318
+ self.emit(Opcode::LoadConst);
1319
+ self.chunk.write_u16(idx);
1320
+ }
1321
+ JsxChild::Expr(e) => self.compile_expr(e)?,
1322
+ }
1323
+ }
1324
+ self.emit_u16(Opcode::NewArray, children.len() as u16);
1325
+ Ok(())
1326
+ }
1327
+ }
1328
+
1329
+ /// Compile a Tish program to bytecode (with peephole optimizations).
1330
+ pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
1331
+ compile_internal(program, true, false)
1332
+ }
1333
+
1334
+ /// Compile without peephole optimizations (for --no-optimize).
1335
+ pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
1336
+ compile_internal(program, false, false)
1337
+ }
1338
+
1339
+ /// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
1340
+ pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
1341
+ compile_internal(program, true, true)
1342
+ }
1343
+
1344
+ /// Compile for REPL without peephole optimizations.
1345
+ pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
1346
+ compile_internal(program, false, true)
1347
+ }
1348
+
1349
+ fn compile_internal(
1350
+ program: &Program,
1351
+ peephole: bool,
1352
+ retain_last_expr: bool,
1353
+ ) -> Result<Chunk, CompileError> {
1354
+ let mut chunk = Chunk::new();
1355
+ let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
1356
+ compiler.compile_program(program)?;
1357
+ if peephole {
1358
+ crate::peephole::optimize(&mut chunk);
1359
+ }
1360
+ Ok(chunk)
1361
+ }