1use crate::hir::*;
13use std::fmt::Write;
14
15pub fn generate(module: &JsModule) -> String {
21 let mut output = String::new();
22
23 if let Some(ref meta) = module.metadata {
25 write_metadata_header(&mut output, meta);
26 }
27
28 for stmt in &module.statements {
30 write_stmt(&mut output, stmt, 0);
31 output.push('\n');
32 }
33
34 output
35}
36
37fn write_metadata_header(out: &mut String, meta: &GenerationMetadata) {
38 out.push_str("/**\n");
39 out.push_str(" * @generated - This file is auto-generated.\n");
40 out.push_str(" * Do not edit manually. To update, run:\n");
41 out.push_str(" *\n");
42 let _ = writeln!(out, " * {}", meta.regenerate_cmd);
43 out.push_str(" *\n");
44 let _ = writeln!(out, " * Generated by: {} v{}", meta.tool, meta.version);
45 let _ = writeln!(out, " * Timestamp: {}", meta.timestamp);
46 let _ = writeln!(out, " * Input hash: {}", meta.input_hash);
47 out.push_str(" */\n\n");
48}
49
50fn write_stmt(out: &mut String, stmt: &Stmt, indent: usize) {
51 let pad = " ".repeat(indent);
52
53 match stmt {
54 Stmt::Let { name, value } => {
55 out.push_str(&pad);
56 out.push_str("let ");
57 out.push_str(name.as_str());
58 out.push_str(" = ");
59 write_expr(out, value);
60 out.push(';');
61 }
62 Stmt::Const { name, value } => {
63 out.push_str(&pad);
64 out.push_str("const ");
65 out.push_str(name.as_str());
66 out.push_str(" = ");
67 write_expr(out, value);
68 out.push(';');
69 }
70 Stmt::Assign { name, value } => {
71 out.push_str(&pad);
72 out.push_str(name.as_str());
73 out.push_str(" = ");
74 write_expr(out, value);
75 out.push(';');
76 }
77 Stmt::MemberAssign {
78 object,
79 member,
80 value,
81 } => {
82 out.push_str(&pad);
83 write_expr(out, object);
84 out.push('.');
85 out.push_str(member.as_str());
86 out.push_str(" = ");
87 write_expr(out, value);
88 out.push(';');
89 }
90 Stmt::AddAssign { target, value } => {
91 out.push_str(&pad);
92 write_expr(out, target);
93 out.push_str(" += ");
94 write_expr(out, value);
95 out.push(';');
96 }
97 Stmt::PostIncrement(expr) => {
98 out.push_str(&pad);
99 write_expr(out, expr);
100 out.push_str("++;");
101 }
102 Stmt::Expr(expr) => {
103 out.push_str(&pad);
104 write_expr(out, expr);
105 out.push(';');
106 }
107 Stmt::Return(None) => {
108 out.push_str(&pad);
109 out.push_str("return;");
110 }
111 Stmt::Return(Some(expr)) => {
112 out.push_str(&pad);
113 out.push_str("return ");
114 write_expr(out, expr);
115 out.push(';');
116 }
117 Stmt::If {
118 condition,
119 then_branch,
120 else_branch,
121 } => {
122 out.push_str(&pad);
123 out.push_str("if (");
124 write_expr(out, condition);
125 out.push_str(") {\n");
126 for s in then_branch {
127 write_stmt(out, s, indent + 1);
128 out.push('\n');
129 }
130 out.push_str(&pad);
131 out.push('}');
132 if let Some(else_stmts) = else_branch {
133 out.push_str(" else {\n");
134 for s in else_stmts {
135 write_stmt(out, s, indent + 1);
136 out.push('\n');
137 }
138 out.push_str(&pad);
139 out.push('}');
140 }
141 }
142 Stmt::For {
143 var,
144 start,
145 end,
146 body,
147 } => {
148 out.push_str(&pad);
149 out.push_str("for (let ");
150 out.push_str(var.as_str());
151 out.push_str(" = ");
152 write_expr(out, start);
153 out.push_str("; ");
154 out.push_str(var.as_str());
155 out.push_str(" < ");
156 write_expr(out, end);
157 out.push_str("; ");
158 out.push_str(var.as_str());
159 out.push_str("++) {\n");
160 for s in body {
161 write_stmt(out, s, indent + 1);
162 out.push('\n');
163 }
164 out.push_str(&pad);
165 out.push('}');
166 }
167 Stmt::While { condition, body } => {
168 out.push_str(&pad);
169 out.push_str("while (");
170 write_expr(out, condition);
171 out.push_str(") {\n");
172 for s in body {
173 write_stmt(out, s, indent + 1);
174 out.push('\n');
175 }
176 out.push_str(&pad);
177 out.push('}');
178 }
179 Stmt::TryCatch {
180 body,
181 catch_var,
182 handler,
183 } => {
184 out.push_str(&pad);
185 out.push_str("try {\n");
186 for s in body {
187 write_stmt(out, s, indent + 1);
188 out.push('\n');
189 }
190 out.push_str(&pad);
191 out.push_str("} catch (");
192 out.push_str(catch_var.as_str());
193 out.push_str(") {\n");
194 for s in handler {
195 write_stmt(out, s, indent + 1);
196 out.push('\n');
197 }
198 out.push_str(&pad);
199 out.push('}');
200 }
201 Stmt::Block(stmts) => {
202 out.push_str(&pad);
203 out.push_str("{\n");
204 for s in stmts {
205 write_stmt(out, s, indent + 1);
206 out.push('\n');
207 }
208 out.push_str(&pad);
209 out.push('}');
210 }
211 Stmt::Comment(text) => {
212 out.push_str(&pad);
213 out.push_str("// ");
214 out.push_str(text);
215 }
216 Stmt::Class(class) => {
217 write_class(out, class, indent);
218 }
219 Stmt::Switch(switch) => {
220 write_switch(out, switch, indent);
221 }
222 Stmt::OnMessage(body) => {
223 out.push_str(&pad);
224 out.push_str("self.onmessage = async function(e) {\n");
225 for s in body {
226 write_stmt(out, s, indent + 1);
227 out.push('\n');
228 }
229 out.push_str(&pad);
230 out.push_str("};");
231 }
232 Stmt::RegisterProcessor { name, class } => {
233 out.push_str(&pad);
234 let _ = write!(out, "registerProcessor(\"{}\", {});", name, class.as_str());
235 }
236 }
237}
238
239fn write_class(out: &mut String, class: &JsClass, indent: usize) {
240 let pad = " ".repeat(indent);
241 let inner_pad = " ".repeat(indent + 1);
242
243 out.push_str(&pad);
245 out.push_str("class ");
246 out.push_str(class.name.as_str());
247 if let Some(ref parent) = class.extends {
248 out.push_str(" extends ");
249 out.push_str(parent.as_str());
250 }
251 out.push_str(" {\n");
252
253 if let Some(ref body) = class.constructor {
255 out.push_str(&inner_pad);
256 out.push_str("constructor() {\n");
257 if class.extends.is_some() {
259 let body_pad = " ".repeat(indent + 2);
260 out.push_str(&body_pad);
261 out.push_str("super();\n");
262 }
263 for s in body {
264 write_stmt(out, s, indent + 2);
265 out.push('\n');
266 }
267 out.push_str(&inner_pad);
268 out.push_str("}\n");
269 }
270
271 for method in &class.methods {
273 out.push_str(&inner_pad);
274 out.push_str(method.name.as_str());
275 out.push('(');
276 for (i, param) in method.params.iter().enumerate() {
277 if i > 0 {
278 out.push_str(", ");
279 }
280 out.push_str(param.as_str());
281 }
282 out.push_str(") {\n");
283 for s in &method.body {
284 write_stmt(out, s, indent + 2);
285 out.push('\n');
286 }
287 out.push_str(&inner_pad);
288 out.push_str("}\n");
289 }
290
291 out.push_str(&pad);
292 out.push('}');
293}
294
295fn write_switch(out: &mut String, switch: &JsSwitch, indent: usize) {
296 let pad = " ".repeat(indent);
297 let case_pad = " ".repeat(indent + 1);
298 let body_pad = " ".repeat(indent + 2);
299
300 out.push_str(&pad);
301 out.push_str("switch (");
302 write_expr(out, &switch.expr);
303 out.push_str(") {\n");
304
305 for (value, body) in &switch.cases {
306 out.push_str(&case_pad);
307 out.push_str("case ");
308 write_expr(out, value);
309 out.push_str(":\n");
310 for s in body {
311 write_stmt(out, s, indent + 2);
312 out.push('\n');
313 }
314 out.push_str(&body_pad);
315 out.push_str("break;\n");
316 }
317
318 if let Some(ref body) = switch.default {
319 out.push_str(&case_pad);
320 out.push_str("default:\n");
321 for s in body {
322 write_stmt(out, s, indent + 2);
323 out.push('\n');
324 }
325 }
326
327 out.push_str(&pad);
328 out.push('}');
329}
330
331#[allow(clippy::unwrap_used)]
333fn write_expr(out: &mut String, expr: &Expr) {
334 match expr {
335 Expr::Null => out.push_str("null"),
336 Expr::Bool(b) => {
337 let _ = write!(out, "{b}");
338 }
339 Expr::Num(n) => {
340 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
342 if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
343 let _ = write!(out, "{}", *n as i64);
344 } else {
345 let _ = write!(out, "{n}");
346 }
347 }
348 Expr::Str(s) => {
349 out.push('"');
350 for c in s.chars() {
351 match c {
352 '"' => out.push_str("\\\""),
353 '\\' => out.push_str("\\\\"),
354 '\n' => out.push_str("\\n"),
355 '\r' => out.push_str("\\r"),
356 '\t' => out.push_str("\\t"),
357 c => out.push(c),
358 }
359 }
360 out.push('"');
361 }
362 Expr::Ident(id) => out.push_str(id.as_str()),
363 Expr::This => out.push_str("this"),
364 Expr::Member { object, property } => {
365 write_expr(out, object);
366 out.push('.');
367 out.push_str(property.as_str());
368 }
369 Expr::Index { object, index } => {
370 write_expr(out, object);
371 out.push('[');
372 write_expr(out, index);
373 out.push(']');
374 }
375 Expr::Call { callee, args } => {
376 write_expr(out, callee);
377 out.push('(');
378 for (i, arg) in args.iter().enumerate() {
379 if i > 0 {
380 out.push_str(", ");
381 }
382 write_expr(out, arg);
383 }
384 out.push(')');
385 }
386 Expr::New { constructor, args } => {
387 out.push_str("new ");
388 write_expr(out, constructor);
389 out.push('(');
390 for (i, arg) in args.iter().enumerate() {
391 if i > 0 {
392 out.push_str(", ");
393 }
394 write_expr(out, arg);
395 }
396 out.push(')');
397 }
398 Expr::Await(inner) => {
399 out.push_str("await ");
400 write_expr(out, inner);
401 }
402 Expr::Import(path) => {
403 out.push_str("import(");
404 write_expr(out, path);
405 out.push(')');
406 }
407 Expr::Binary { left, op, right } => {
408 out.push('(');
409 write_expr(out, left);
410 out.push(' ');
411 out.push_str(op.as_str());
412 out.push(' ');
413 write_expr(out, right);
414 out.push(')');
415 }
416 Expr::Unary { op, operand } => {
417 out.push_str(op.as_str());
418 write_expr(out, operand);
419 }
420 Expr::Ternary {
421 condition,
422 then_expr,
423 else_expr,
424 } => {
425 out.push('(');
426 write_expr(out, condition);
427 out.push_str(" ? ");
428 write_expr(out, then_expr);
429 out.push_str(" : ");
430 write_expr(out, else_expr);
431 out.push(')');
432 }
433 Expr::Object(pairs) => {
434 out.push_str("{ ");
435 for (i, (key, value)) in pairs.iter().enumerate() {
436 if i > 0 {
437 out.push_str(", ");
438 }
439 out.push_str(key);
440 out.push_str(": ");
441 write_expr(out, value);
442 }
443 out.push_str(" }");
444 }
445 Expr::Array(items) => {
446 out.push('[');
447 for (i, item) in items.iter().enumerate() {
448 if i > 0 {
449 out.push_str(", ");
450 }
451 write_expr(out, item);
452 }
453 out.push(']');
454 }
455 Expr::Arrow { params, body } => {
456 if params.len() == 1 {
457 out.push_str(params[0].as_str());
458 } else {
459 out.push('(');
460 for (i, p) in params.iter().enumerate() {
461 if i > 0 {
462 out.push_str(", ");
463 }
464 out.push_str(p.as_str());
465 }
466 out.push(')');
467 }
468 out.push_str(" => ");
469 write_expr(out, body);
470 }
471 Expr::ArrowBlock { params, body } => {
472 if params.len() == 1 {
473 out.push_str(params[0].as_str());
474 } else {
475 out.push('(');
476 for (i, p) in params.iter().enumerate() {
477 if i > 0 {
478 out.push_str(", ");
479 }
480 out.push_str(p.as_str());
481 }
482 out.push(')');
483 }
484 out.push_str(" => {\n");
485 for s in body {
486 write_stmt(out, s, 1);
487 out.push('\n');
488 }
489 out.push('}');
490 }
491 Expr::Assign { target, value } => {
492 write_expr(out, target);
493 out.push_str(" = ");
494 write_expr(out, value);
495 }
496 }
497}
498
499#[cfg(test)]
500#[allow(clippy::unwrap_used)]
501mod tests {
502 use super::*;
503 use crate::builder::*;
504
505 #[test]
506 fn generate_empty_module() {
507 let module = JsModuleBuilder::new().build();
508 let js = generate(&module);
509 assert_eq!(js, "");
510 }
511
512 #[test]
513 fn generate_let_declaration() {
514 let module = JsModuleBuilder::new()
515 .let_decl("x", Expr::num(42))
516 .unwrap()
517 .build();
518 let js = generate(&module);
519 assert!(js.contains("let x = 42;"));
520 }
521
522 #[test]
523 fn generate_class() {
524 let class = JsClassBuilder::new("Foo")
525 .unwrap()
526 .extends("Bar")
527 .unwrap()
528 .constructor(vec![])
529 .method("test", &[], vec![Stmt::ret()])
530 .unwrap()
531 .build();
532
533 let module = JsModuleBuilder::new().class(class).build();
534 let js = generate(&module);
535
536 assert!(js.contains("class Foo extends Bar"));
537 assert!(js.contains("super();"));
538 assert!(js.contains("test()"));
539 }
540
541 #[test]
542 fn generate_if_else() {
543 let stmt = Stmt::if_else(
544 Expr::ident("x").unwrap().lt(Expr::num(10)),
545 vec![Stmt::expr(
546 Expr::ident("console")
547 .unwrap()
548 .dot("log")
549 .unwrap()
550 .call(vec![Expr::str("small")]),
551 )],
552 vec![Stmt::expr(
553 Expr::ident("console")
554 .unwrap()
555 .dot("log")
556 .unwrap()
557 .call(vec![Expr::str("big")]),
558 )],
559 );
560
561 let module = JsModuleBuilder::new().stmt(stmt).build();
562 let js = generate(&module);
563
564 assert!(js.contains("if ((x < 10))"));
565 assert!(js.contains("} else {"));
566 }
567
568 #[test]
569 fn generate_for_loop() {
570 let stmt = Stmt::for_loop("i", Expr::num(0), Expr::num(10), vec![]).unwrap();
571 let module = JsModuleBuilder::new().stmt(stmt).build();
572 let js = generate(&module);
573
574 assert!(js.contains("for (let i = 0; i < 10; i++)"));
575 }
576
577 #[test]
578 fn generate_string_escaping() {
579 let module = JsModuleBuilder::new()
580 .const_decl("s", Expr::str("hello\n\"world\""))
581 .unwrap()
582 .build();
583 let js = generate(&module);
584
585 assert!(js.contains(r#""hello\n\"world\"""#));
586 }
587
588 #[test]
589 fn generate_with_metadata() {
590 let module = JsModuleBuilder::new()
591 .metadata(GenerationMetadata {
592 tool: "probar-js-gen".to_string(),
593 version: "0.1.0".to_string(),
594 input_hash: "abc123".to_string(),
595 timestamp: "2024-01-01T00:00:00Z".to_string(),
596 regenerate_cmd: "probar gen js".to_string(),
597 })
598 .let_decl("x", Expr::num(1))
599 .unwrap()
600 .build();
601
602 let js = generate(&module);
603
604 assert!(js.contains("@generated"));
605 assert!(js.contains("Do not edit manually"));
606 assert!(js.contains("probar-js-gen"));
607 assert!(js.contains("abc123"));
608 }
609
610 #[test]
611 fn deterministic_output() {
612 let module = JsModuleBuilder::new()
613 .let_decl("x", Expr::num(1))
614 .unwrap()
615 .const_decl("y", Expr::str("hello"))
616 .unwrap()
617 .build();
618
619 let js1 = generate(&module);
620 let js2 = generate(&module);
621
622 assert_eq!(js1, js2, "Same HIR must produce same output");
623 }
624}