use crate::hir::*;
use std::fmt::Write;
pub fn generate(module: &JsModule) -> String {
let mut output = String::new();
if let Some(ref meta) = module.metadata {
write_metadata_header(&mut output, meta);
}
for stmt in &module.statements {
write_stmt(&mut output, stmt, 0);
output.push('\n');
}
output
}
fn write_metadata_header(out: &mut String, meta: &GenerationMetadata) {
out.push_str("/**\n");
out.push_str(" * @generated - This file is auto-generated.\n");
out.push_str(" * Do not edit manually. To update, run:\n");
out.push_str(" *\n");
let _ = writeln!(out, " * {}", meta.regenerate_cmd);
out.push_str(" *\n");
let _ = writeln!(out, " * Generated by: {} v{}", meta.tool, meta.version);
let _ = writeln!(out, " * Timestamp: {}", meta.timestamp);
let _ = writeln!(out, " * Input hash: {}", meta.input_hash);
out.push_str(" */\n\n");
}
fn write_stmt(out: &mut String, stmt: &Stmt, indent: usize) {
let pad = " ".repeat(indent);
match stmt {
Stmt::Let { name, value } => {
out.push_str(&pad);
out.push_str("let ");
out.push_str(name.as_str());
out.push_str(" = ");
write_expr(out, value);
out.push(';');
}
Stmt::Const { name, value } => {
out.push_str(&pad);
out.push_str("const ");
out.push_str(name.as_str());
out.push_str(" = ");
write_expr(out, value);
out.push(';');
}
Stmt::Assign { name, value } => {
out.push_str(&pad);
out.push_str(name.as_str());
out.push_str(" = ");
write_expr(out, value);
out.push(';');
}
Stmt::MemberAssign {
object,
member,
value,
} => {
out.push_str(&pad);
write_expr(out, object);
out.push('.');
out.push_str(member.as_str());
out.push_str(" = ");
write_expr(out, value);
out.push(';');
}
Stmt::AddAssign { target, value } => {
out.push_str(&pad);
write_expr(out, target);
out.push_str(" += ");
write_expr(out, value);
out.push(';');
}
Stmt::PostIncrement(expr) => {
out.push_str(&pad);
write_expr(out, expr);
out.push_str("++;");
}
Stmt::Expr(expr) => {
out.push_str(&pad);
write_expr(out, expr);
out.push(';');
}
Stmt::Return(None) => {
out.push_str(&pad);
out.push_str("return;");
}
Stmt::Return(Some(expr)) => {
out.push_str(&pad);
out.push_str("return ");
write_expr(out, expr);
out.push(';');
}
Stmt::If {
condition,
then_branch,
else_branch,
} => {
out.push_str(&pad);
out.push_str("if (");
write_expr(out, condition);
out.push_str(") {\n");
for s in then_branch {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push('}');
if let Some(else_stmts) = else_branch {
out.push_str(" else {\n");
for s in else_stmts {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push('}');
}
}
Stmt::For {
var,
start,
end,
body,
} => {
out.push_str(&pad);
out.push_str("for (let ");
out.push_str(var.as_str());
out.push_str(" = ");
write_expr(out, start);
out.push_str("; ");
out.push_str(var.as_str());
out.push_str(" < ");
write_expr(out, end);
out.push_str("; ");
out.push_str(var.as_str());
out.push_str("++) {\n");
for s in body {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push('}');
}
Stmt::While { condition, body } => {
out.push_str(&pad);
out.push_str("while (");
write_expr(out, condition);
out.push_str(") {\n");
for s in body {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push('}');
}
Stmt::TryCatch {
body,
catch_var,
handler,
} => {
out.push_str(&pad);
out.push_str("try {\n");
for s in body {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push_str("} catch (");
out.push_str(catch_var.as_str());
out.push_str(") {\n");
for s in handler {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push('}');
}
Stmt::Block(stmts) => {
out.push_str(&pad);
out.push_str("{\n");
for s in stmts {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push('}');
}
Stmt::Comment(text) => {
out.push_str(&pad);
out.push_str("// ");
out.push_str(text);
}
Stmt::Class(class) => {
write_class(out, class, indent);
}
Stmt::Switch(switch) => {
write_switch(out, switch, indent);
}
Stmt::OnMessage(body) => {
out.push_str(&pad);
out.push_str("self.onmessage = async function(e) {\n");
for s in body {
write_stmt(out, s, indent + 1);
out.push('\n');
}
out.push_str(&pad);
out.push_str("};");
}
Stmt::RegisterProcessor { name, class } => {
out.push_str(&pad);
let _ = write!(out, "registerProcessor(\"{}\", {});", name, class.as_str());
}
}
}
fn write_class(out: &mut String, class: &JsClass, indent: usize) {
let pad = " ".repeat(indent);
let inner_pad = " ".repeat(indent + 1);
out.push_str(&pad);
out.push_str("class ");
out.push_str(class.name.as_str());
if let Some(ref parent) = class.extends {
out.push_str(" extends ");
out.push_str(parent.as_str());
}
out.push_str(" {\n");
if let Some(ref body) = class.constructor {
out.push_str(&inner_pad);
out.push_str("constructor() {\n");
if class.extends.is_some() {
let body_pad = " ".repeat(indent + 2);
out.push_str(&body_pad);
out.push_str("super();\n");
}
for s in body {
write_stmt(out, s, indent + 2);
out.push('\n');
}
out.push_str(&inner_pad);
out.push_str("}\n");
}
for method in &class.methods {
out.push_str(&inner_pad);
out.push_str(method.name.as_str());
out.push('(');
for (i, param) in method.params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(param.as_str());
}
out.push_str(") {\n");
for s in &method.body {
write_stmt(out, s, indent + 2);
out.push('\n');
}
out.push_str(&inner_pad);
out.push_str("}\n");
}
out.push_str(&pad);
out.push('}');
}
fn write_switch(out: &mut String, switch: &JsSwitch, indent: usize) {
let pad = " ".repeat(indent);
let case_pad = " ".repeat(indent + 1);
let body_pad = " ".repeat(indent + 2);
out.push_str(&pad);
out.push_str("switch (");
write_expr(out, &switch.expr);
out.push_str(") {\n");
for (value, body) in &switch.cases {
out.push_str(&case_pad);
out.push_str("case ");
write_expr(out, value);
out.push_str(":\n");
for s in body {
write_stmt(out, s, indent + 2);
out.push('\n');
}
out.push_str(&body_pad);
out.push_str("break;\n");
}
if let Some(ref body) = switch.default {
out.push_str(&case_pad);
out.push_str("default:\n");
for s in body {
write_stmt(out, s, indent + 2);
out.push('\n');
}
}
out.push_str(&pad);
out.push('}');
}
#[allow(clippy::unwrap_used)]
fn write_expr(out: &mut String, expr: &Expr) {
match expr {
Expr::Null => out.push_str("null"),
Expr::Bool(b) => {
let _ = write!(out, "{b}");
}
Expr::Num(n) => {
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
let _ = write!(out, "{}", *n as i64);
} else {
let _ = write!(out, "{n}");
}
}
Expr::Str(s) => {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out.push('"');
}
Expr::Ident(id) => out.push_str(id.as_str()),
Expr::This => out.push_str("this"),
Expr::Member { object, property } => {
write_expr(out, object);
out.push('.');
out.push_str(property.as_str());
}
Expr::Index { object, index } => {
write_expr(out, object);
out.push('[');
write_expr(out, index);
out.push(']');
}
Expr::Call { callee, args } => {
write_expr(out, callee);
out.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_expr(out, arg);
}
out.push(')');
}
Expr::New { constructor, args } => {
out.push_str("new ");
write_expr(out, constructor);
out.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_expr(out, arg);
}
out.push(')');
}
Expr::Await(inner) => {
out.push_str("await ");
write_expr(out, inner);
}
Expr::Import(path) => {
out.push_str("import(");
write_expr(out, path);
out.push(')');
}
Expr::Binary { left, op, right } => {
out.push('(');
write_expr(out, left);
out.push(' ');
out.push_str(op.as_str());
out.push(' ');
write_expr(out, right);
out.push(')');
}
Expr::Unary { op, operand } => {
out.push_str(op.as_str());
write_expr(out, operand);
}
Expr::Ternary {
condition,
then_expr,
else_expr,
} => {
out.push('(');
write_expr(out, condition);
out.push_str(" ? ");
write_expr(out, then_expr);
out.push_str(" : ");
write_expr(out, else_expr);
out.push(')');
}
Expr::Object(pairs) => {
out.push_str("{ ");
for (i, (key, value)) in pairs.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(key);
out.push_str(": ");
write_expr(out, value);
}
out.push_str(" }");
}
Expr::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_expr(out, item);
}
out.push(']');
}
Expr::Arrow { params, body } => {
if params.len() == 1 {
out.push_str(params[0].as_str());
} else {
out.push('(');
for (i, p) in params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(p.as_str());
}
out.push(')');
}
out.push_str(" => ");
write_expr(out, body);
}
Expr::ArrowBlock { params, body } => {
if params.len() == 1 {
out.push_str(params[0].as_str());
} else {
out.push('(');
for (i, p) in params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(p.as_str());
}
out.push(')');
}
out.push_str(" => {\n");
for s in body {
write_stmt(out, s, 1);
out.push('\n');
}
out.push('}');
}
Expr::Assign { target, value } => {
write_expr(out, target);
out.push_str(" = ");
write_expr(out, value);
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::builder::*;
#[test]
fn generate_empty_module() {
let module = JsModuleBuilder::new().build();
let js = generate(&module);
assert_eq!(js, "");
}
#[test]
fn generate_let_declaration() {
let module = JsModuleBuilder::new()
.let_decl("x", Expr::num(42))
.unwrap()
.build();
let js = generate(&module);
assert!(js.contains("let x = 42;"));
}
#[test]
fn generate_class() {
let class = JsClassBuilder::new("Foo")
.unwrap()
.extends("Bar")
.unwrap()
.constructor(vec![])
.method("test", &[], vec![Stmt::ret()])
.unwrap()
.build();
let module = JsModuleBuilder::new().class(class).build();
let js = generate(&module);
assert!(js.contains("class Foo extends Bar"));
assert!(js.contains("super();"));
assert!(js.contains("test()"));
}
#[test]
fn generate_if_else() {
let stmt = Stmt::if_else(
Expr::ident("x").unwrap().lt(Expr::num(10)),
vec![Stmt::expr(
Expr::ident("console")
.unwrap()
.dot("log")
.unwrap()
.call(vec![Expr::str("small")]),
)],
vec![Stmt::expr(
Expr::ident("console")
.unwrap()
.dot("log")
.unwrap()
.call(vec![Expr::str("big")]),
)],
);
let module = JsModuleBuilder::new().stmt(stmt).build();
let js = generate(&module);
assert!(js.contains("if ((x < 10))"));
assert!(js.contains("} else {"));
}
#[test]
fn generate_for_loop() {
let stmt = Stmt::for_loop("i", Expr::num(0), Expr::num(10), vec![]).unwrap();
let module = JsModuleBuilder::new().stmt(stmt).build();
let js = generate(&module);
assert!(js.contains("for (let i = 0; i < 10; i++)"));
}
#[test]
fn generate_string_escaping() {
let module = JsModuleBuilder::new()
.const_decl("s", Expr::str("hello\n\"world\""))
.unwrap()
.build();
let js = generate(&module);
assert!(js.contains(r#""hello\n\"world\"""#));
}
#[test]
fn generate_with_metadata() {
let module = JsModuleBuilder::new()
.metadata(GenerationMetadata {
tool: "probar-js-gen".to_string(),
version: "0.1.0".to_string(),
input_hash: "abc123".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
regenerate_cmd: "probar gen js".to_string(),
})
.let_decl("x", Expr::num(1))
.unwrap()
.build();
let js = generate(&module);
assert!(js.contains("@generated"));
assert!(js.contains("Do not edit manually"));
assert!(js.contains("probar-js-gen"));
assert!(js.contains("abc123"));
}
#[test]
fn deterministic_output() {
let module = JsModuleBuilder::new()
.let_decl("x", Expr::num(1))
.unwrap()
.const_decl("y", Expr::str("hello"))
.unwrap()
.build();
let js1 = generate(&module);
let js2 = generate(&module);
assert_eq!(js1, js2, "Same HIR must produce same output");
}
}