Skip to main content

probar_js_gen/
codegen.rs

1//! Code generation from HIR to JavaScript.
2//!
3//! # Design
4//!
5//! The codegen is deterministic: the same HIR always produces the same output.
6//! This is critical for immutability verification via hashing.
7//!
8//! # References
9//! - ECMA-262 (ES2022) - JavaScript language specification
10//! - Lattner & Adve (2004) "LLVM: A Compilation Framework"
11
12use crate::hir::*;
13use std::fmt::Write;
14
15/// Generate JavaScript code from a module.
16///
17/// # Panics
18///
19/// This function does not panic. All errors are handled gracefully.
20pub fn generate(module: &JsModule) -> String {
21    let mut output = String::new();
22
23    // Write generation metadata as header comment
24    if let Some(ref meta) = module.metadata {
25        write_metadata_header(&mut output, meta);
26    }
27
28    // Write statements
29    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    // Class declaration
244    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    // Constructor
254    if let Some(ref body) = class.constructor {
255        out.push_str(&inner_pad);
256        out.push_str("constructor() {\n");
257        // Auto-insert super() if extends
258        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    // Methods
272    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// Note: write! to String never fails, so we use `let _ =` to suppress warnings
332#[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            // Format integers without decimal point
341            #[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}