use crate::ast::{
BinOp, Decl, Expr, Literal, MatchArm, Pattern, Program, Spanned, Stmt, Type, UnaryOp,
UnwrapMode,
};
pub fn emit(program: &Program) -> String {
let mut out = String::new();
out.push_str("// Generated by ilo --emit js\n\n");
for decl in &program.declarations {
emit_decl(&mut out, decl, 0);
out.push('\n');
}
out.trim_end().to_string()
}
fn indent(out: &mut String, level: usize) {
for _ in 0..level {
out.push_str(" ");
}
}
fn js_name(name: &str) -> String {
let parts: Vec<&str> = name.split('-').collect();
if parts.len() == 1 {
return name.to_string();
}
let mut result = parts[0].to_string();
for part in &parts[1..] {
let mut chars = part.chars();
if let Some(first) = chars.next() {
result.push_str(&first.to_uppercase().to_string());
result.push_str(chars.as_str());
}
}
result
}
fn emit_type_comment(ty: &Type) -> String {
match ty {
Type::Number => "number".to_string(),
Type::Text => "string".to_string(),
Type::Bool => "boolean".to_string(),
Type::Any => "any".to_string(),
Type::Optional(inner) => format!("{} | null", emit_type_comment(inner)),
Type::List(inner) => format!("{}[]", emit_type_comment(inner)),
Type::Map(k, v) => format!("Map<{}, {}>", emit_type_comment(k), emit_type_comment(v)),
Type::Sum(_) => "string".to_string(),
Type::Result(ok, _err) => format!(
"{{ ok: true, value: {} }} | {{ ok: false, error: string }}",
emit_type_comment(ok)
),
Type::Fn(params, ret) => {
let ps: Vec<_> = params.iter().map(emit_type_comment).collect();
format!("({}) => {}", ps.join(", "), emit_type_comment(ret))
}
Type::Named(name) => name.clone(),
Type::U32 | Type::U64 | Type::I64 => "number".to_string(),
}
}
fn emit_decl(out: &mut String, decl: &Decl, level: usize) {
match decl {
Decl::Function {
name,
params,
return_type,
body,
..
} => {
indent(out, level);
out.push_str("/**\n");
for p in params {
indent(out, level);
out.push_str(&format!(
" * @param {{{}}} {}\n",
emit_type_comment(&p.ty),
js_name(&p.name)
));
}
indent(out, level);
out.push_str(&format!(
" * @returns {{{}}}\n",
emit_type_comment(return_type)
));
indent(out, level);
out.push_str(" */\n");
indent(out, level);
out.push_str(&format!("const {} = (", js_name(name)));
for (i, p) in params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&js_name(&p.name));
}
out.push_str(") => {\n");
emit_body(out, body, level + 1, true);
indent(out, level);
out.push_str("};\n");
}
Decl::TypeDef { name, fields, .. } => {
indent(out, level);
out.push_str(&format!("// type {} = {{", name));
for (i, f) in fields.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&format!("{}: {}", f.name, emit_type_comment(&f.ty)));
}
out.push_str("}\n");
}
Decl::Tool {
name,
description,
params,
return_type,
..
} => {
indent(out, level);
out.push_str("/**\n");
indent(out, level);
out.push_str(&format!(" * {}\n", description));
for p in params {
indent(out, level);
out.push_str(&format!(
" * @param {{{}}} {}\n",
emit_type_comment(&p.ty),
js_name(&p.name)
));
}
indent(out, level);
out.push_str(&format!(
" * @returns {{{}}}\n",
emit_type_comment(return_type)
));
indent(out, level);
out.push_str(" */\n");
indent(out, level);
out.push_str(&format!("const {} = (", js_name(name)));
for (i, p) in params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&js_name(&p.name));
}
out.push_str(") => { throw new Error(\"not implemented\"); };\n");
}
Decl::Alias { name, target, .. } => {
indent(out, level);
out.push_str(&format!(
"// alias {} = {}\n",
name,
emit_type_comment(target)
));
}
Decl::Use { .. } => {}
Decl::VersionPragma { .. } => {}
Decl::Error { .. } => {}
Decl::SumType { name, variants, .. } => {
indent(out, level);
out.push_str(&format!("// sum type: {}\n", name));
for v in variants {
indent(out, level);
if v.payload.is_some() {
out.push_str(&format!(
"const {} = (payload) => [\"{}\", payload];\n",
js_name(&v.name),
v.name
));
} else {
out.push_str(&format!(
"const {} = [\"{}\", null];\n",
js_name(&v.name),
v.name
));
}
}
}
}
}
fn emit_body(out: &mut String, stmts: &[Spanned<Stmt>], level: usize, is_fn_body: bool) {
if stmts.is_empty() {
indent(out, level);
out.push_str("return undefined;\n");
return;
}
for (i, spanned) in stmts.iter().enumerate() {
let is_last = i == stmts.len() - 1;
emit_stmt(out, &spanned.node, level, is_fn_body && is_last);
}
}
fn emit_stmt(out: &mut String, stmt: &Stmt, level: usize, implicit_return: bool) {
match stmt {
Stmt::Let { name, value } => {
let val = emit_expr(out, level, value);
indent(out, level);
out.push_str(&format!("const {} = {};\n", js_name(name), val));
}
Stmt::Destructure { bindings, value } => {
let record = emit_expr(out, level, value);
for binding in bindings {
indent(out, level);
out.push_str(&format!(
"const {} = {}[\"{}\"];\n",
js_name(binding),
record,
binding
));
}
}
Stmt::Guard {
condition,
negated,
body,
else_body,
..
} => {
let cond = emit_expr(out, level, condition);
indent(out, level);
let is_ternary = else_body.is_some();
if *negated {
out.push_str(&format!("if (!({})) {{\n", cond));
} else {
out.push_str(&format!("if ({}) {{\n", cond));
}
emit_body(out, body, level + 1, !is_ternary);
indent(out, level);
out.push_str("}\n");
if let Some(eb) = else_body {
indent(out, level);
out.push_str("else {\n");
emit_body(out, eb, level + 1, false);
indent(out, level);
out.push_str("}\n");
}
}
Stmt::Match { subject, arms } => {
emit_match_stmt(out, subject, arms, level);
}
Stmt::ForEach {
binding,
collection,
body,
} => {
let coll = emit_expr(out, level, collection);
indent(out, level);
out.push_str(&format!(
"for (const {} of {}) {{\n",
js_name(binding),
coll
));
emit_body(out, body, level + 1, false);
indent(out, level);
out.push_str("}\n");
}
Stmt::ForRange {
binding,
start,
end,
step,
body,
} => {
let s = emit_expr(out, level, start);
let e = emit_expr(out, level, end);
indent(out, level);
if let Some(step_expr) = step {
let st = emit_expr(out, level, step_expr);
out.push_str(&format!(
"for (let {} = {}; {} < {}; {} += {}) {{\n",
js_name(binding),
s,
js_name(binding),
e,
js_name(binding),
st
));
} else {
out.push_str(&format!(
"for (let {} = {}; {} < {}; {}++) {{\n",
js_name(binding),
s,
js_name(binding),
e,
js_name(binding)
));
}
emit_body(out, body, level + 1, false);
indent(out, level);
out.push_str("}\n");
}
Stmt::While { condition, body } => {
let cond = emit_expr(out, level, condition);
indent(out, level);
out.push_str(&format!("while ({}) {{\n", cond));
emit_body(out, body, level + 1, false);
indent(out, level);
out.push_str("}\n");
}
Stmt::Return(expr) => {
let val = emit_expr(out, level, expr);
indent(out, level);
out.push_str(&format!("return {};\n", val));
}
Stmt::Break(Some(expr)) => {
let val = emit_expr(out, level, expr);
indent(out, level);
out.push_str(&format!("__breakVal = {};\n", val));
indent(out, level);
out.push_str("break;\n");
}
Stmt::Break(None) => {
indent(out, level);
out.push_str("break;\n");
}
Stmt::Continue => {
indent(out, level);
out.push_str("continue;\n");
}
Stmt::Expr(expr) => {
let val = emit_expr(out, level, expr);
indent(out, level);
if implicit_return {
out.push_str(&format!("return {};\n", val));
} else {
out.push_str(&format!("{};\n", val));
}
}
Stmt::Defer { expr, .. } => {
let val = emit_expr(out, level, expr);
indent(out, level);
out.push_str(&format!("{};\n", val));
}
}
}
fn emit_match_stmt(out: &mut String, subject: &Option<Expr>, arms: &[MatchArm], level: usize) {
let subj_str = match subject {
Some(e) => emit_expr(out, level, e),
None => "_subject".to_string(),
};
for (i, arm) in arms.iter().enumerate() {
indent(out, level);
match &arm.pattern {
Pattern::Wildcard => {
if i == 0 {
emit_body(out, &arm.body, level, true);
return;
}
out.push_str("else {\n");
emit_body(out, &arm.body, level + 1, true);
indent(out, level);
out.push_str("}\n");
return;
}
Pattern::Ok(binding) => {
let kw = if i == 0 { "if" } else { "else if" };
out.push_str(&format!(
"{} ({} && {}[0] === \"ok\") {{\n",
kw, subj_str, subj_str
));
indent(out, level + 1);
out.push_str(&format!("const {} = {}[1];\n", js_name(binding), subj_str));
}
Pattern::Err(binding) => {
let kw = if i == 0 { "if" } else { "else if" };
out.push_str(&format!(
"{} ({} && {}[0] === \"err\") {{\n",
kw, subj_str, subj_str
));
indent(out, level + 1);
out.push_str(&format!("const {} = {}[1];\n", js_name(binding), subj_str));
}
Pattern::Literal(lit) => {
let kw = if i == 0 { "if" } else { "else if" };
out.push_str(&format!(
"{} ({} === {}) {{\n",
kw,
subj_str,
emit_literal(lit)
));
}
Pattern::TypeIs { ty, binding } => {
let kw = if i == 0 { "if" } else { "else if" };
let type_check = match ty {
Type::Number => format!("typeof {} === \"number\"", subj_str),
Type::Text => format!("typeof {} === \"string\"", subj_str),
Type::Bool => format!("typeof {} === \"boolean\"", subj_str),
_ => format!("true /* type check for {} */", emit_type_comment(ty)),
};
out.push_str(&format!("{} ({}) {{\n", kw, type_check));
indent(out, level + 1);
out.push_str(&format!("const {} = {};\n", js_name(binding), subj_str));
}
Pattern::Or(alts) => {
let kw = if i == 0 { "if" } else { "else if" };
let conds: Vec<String> = alts
.iter()
.map(|alt| match alt {
Pattern::Literal(lit) => {
format!("{} === {}", subj_str, emit_literal(lit))
}
Pattern::Wildcard => "true".to_string(),
_ => "false".to_string(),
})
.collect();
out.push_str(&format!("{} ({}) {{\n", kw, conds.join(" || ")));
}
Pattern::Variant { tag, binding } => {
let kw = if i == 0 { "if" } else { "else if" };
out.push_str(&format!(
"{} (Array.isArray({}) && {}[0] === \"{}\") {{\n",
kw, subj_str, subj_str, tag
));
if let Some(b) = binding {
indent(out, level + 1);
out.push_str(&format!("const {} = {}[1];\n", js_name(b), subj_str));
}
}
}
emit_body(out, &arm.body, level + 1, true);
indent(out, level);
out.push_str("}\n");
}
}
fn emit_expr(out: &mut String, level: usize, expr: &Expr) -> String {
match expr {
Expr::Literal(lit) => emit_literal(lit),
Expr::Ref(name) => js_name(name),
Expr::Field {
object,
field,
safe,
} => {
let obj = emit_expr(out, level, object);
if *safe {
format!("({}?.[\"{}\"])", obj, field)
} else {
format!("{}[\"{}\"]", obj, field)
}
}
Expr::Index {
object,
index,
safe,
} => {
let obj = emit_expr(out, level, object);
if *safe {
format!("({}?.[{}])", obj, index)
} else {
format!("{}[{}]", obj, index)
}
}
Expr::Call {
function,
args,
unwrap,
} => emit_call(out, level, function, args, unwrap),
Expr::BinOp { op, left, right } => {
let op_str = match op {
BinOp::Add => "+",
BinOp::Subtract => "-",
BinOp::Multiply => "*",
BinOp::Divide => "/",
BinOp::Equals => "===",
BinOp::NotEquals => "!==",
BinOp::GreaterThan => ">",
BinOp::LessThan => "<",
BinOp::GreaterOrEqual => ">=",
BinOp::LessOrEqual => "<=",
BinOp::And => "&&",
BinOp::Or => "||",
BinOp::Append => {
let l = emit_expr(out, level, left);
let r = emit_expr(out, level, right);
return format!("[...{}, {}]", l, r);
}
};
let l = emit_expr(out, level, left);
let r = emit_expr(out, level, right);
format!("({} {} {})", l, op_str, r)
}
Expr::UnaryOp { op, operand } => {
let val = emit_expr(out, level, operand);
match op {
UnaryOp::Not => format!("(!{})", val),
UnaryOp::Negate => format!("(-{})", val),
}
}
Expr::Ok(inner) => format!("[\"ok\", {}]", emit_expr(out, level, inner)),
Expr::Err(inner) => format!("[\"err\", {}]", emit_expr(out, level, inner)),
Expr::List(items) => {
let items_str: Vec<String> = items.iter().map(|i| emit_expr(out, level, i)).collect();
format!("[{}]", items_str.join(", "))
}
Expr::AnonRecord { fields } => {
let mut parts = Vec::new();
for (name, val) in fields {
parts.push(format!("\"{}\": {}", name, emit_expr(out, level, val)));
}
format!("{{{}}}", parts.join(", "))
}
Expr::Record { type_name, fields } => {
let mut parts = vec![format!("\"_type\": \"{}\"", type_name)];
for (name, val) in fields {
parts.push(format!("\"{}\": {}", name, emit_expr(out, level, val)));
}
format!("{{{}}}", parts.join(", "))
}
Expr::Match { subject, arms } => emit_match_expr(out, level, subject, arms),
Expr::NilCoalesce { value, default } => {
let v = emit_expr(out, level, value);
let d = emit_expr(out, level, default);
format!("({} ?? {})", v, d)
}
Expr::Ternary {
condition,
then_expr,
else_expr,
} => {
let c = emit_expr(out, level, condition);
let t = emit_expr(out, level, then_expr);
let e = emit_expr(out, level, else_expr);
format!("({} ? {} : {})", c, t, e)
}
Expr::With { object, updates } => {
let obj = emit_expr(out, level, object);
let mut parts = vec![format!("...{}", obj)];
for (name, val) in updates {
parts.push(format!("\"{}\": {}", name, emit_expr(out, level, val)));
}
format!("{{{}}}", parts.join(", "))
}
Expr::MakeClosure { fn_name, captures } => {
let caps: Vec<String> = captures.iter().map(|c| emit_expr(out, level, c)).collect();
let cap_str = if caps.is_empty() {
String::new()
} else {
format!(", {}", caps.join(", "))
};
format!("((..._a) => {}(..._a{}))", js_name(fn_name), cap_str)
}
Expr::Todo(reason) => {
let msg = emit_expr(out, level, reason);
format!("(() => {{ throw new Error('TODO: ' + {}); }})()", msg)
}
Expr::Panic(reason) => {
let msg = emit_expr(out, level, reason);
format!("(() => {{ throw new Error('PANIC: ' + {}); }})()", msg)
}
}
}
fn emit_call(
out: &mut String,
level: usize,
function: &str,
args: &[Expr],
unwrap: &UnwrapMode,
) -> String {
if function == "len" && args.len() == 1 {
let arg = emit_expr(out, level, &args[0]);
return format!("{}.length", arg);
}
if function == "str" && args.len() == 1 {
let arg = emit_expr(out, level, &args[0]);
return format!("String({})", arg);
}
if function == "num" && args.len() == 1 {
let arg = emit_expr(out, level, &args[0]);
return format!(
"((v) => {{ const n = Number(v); return isNaN(n) ? [\"err\", String(v)] : [\"ok\", n]; }})({})",
arg
);
}
if function == "abs" && args.len() == 1 {
return format!("Math.abs({})", emit_expr(out, level, &args[0]));
}
if function == "min" && args.len() == 2 {
let a = emit_expr(out, level, &args[0]);
let b = emit_expr(out, level, &args[1]);
return format!("Math.min({}, {})", a, b);
}
if function == "max" && args.len() == 2 {
let a = emit_expr(out, level, &args[0]);
let b = emit_expr(out, level, &args[1]);
return format!("Math.max({}, {})", a, b);
}
if function == "flr" && args.len() == 1 {
return format!("Math.floor({})", emit_expr(out, level, &args[0]));
}
if function == "cel" && args.len() == 1 {
return format!("Math.ceil({})", emit_expr(out, level, &args[0]));
}
if function == "rou" && args.len() == 1 {
return format!("Math.round({})", emit_expr(out, level, &args[0]));
}
if function == "sqrt" && args.len() == 1 {
return format!("Math.sqrt({})", emit_expr(out, level, &args[0]));
}
if function == "pi" && args.is_empty() {
return "Math.PI".to_string();
}
if function == "tau" && args.is_empty() {
return "(2 * Math.PI)".to_string();
}
if function == "e" && args.is_empty() {
return "Math.E".to_string();
}
if function == "now" && args.is_empty() {
return "(Date.now() / 1000)".to_string();
}
if function == "now-ms" && args.is_empty() {
return "Date.now()".to_string();
}
if function == "rnd" && args.is_empty() {
return "Math.random()".to_string();
}
if function == "rnd" && args.len() == 2 {
let lo = emit_expr(out, level, &args[0]);
let hi = emit_expr(out, level, &args[1]);
return format!("Math.floor(Math.random() * ({} - {} + 1) + {})", hi, lo, lo);
}
if function == "trm" && args.len() == 1 {
return format!("{}.trim()", emit_expr(out, level, &args[0]));
}
if function == "prnt" && args.len() == 1 {
let arg = emit_expr(out, level, &args[0]);
return format!("((v) => {{ console.log(v); return v; }})({})", arg);
}
if function == "unq" && args.len() == 1 {
let xs = emit_expr(out, level, &args[0]);
return format!("[...new Set({})]", xs);
}
if function == "srt" && args.len() == 2 {
let key_fn = emit_expr(out, level, &args[0]);
let xs = emit_expr(out, level, &args[1]);
return format!(
"[...{}].sort((a, b) => {{const ka = {}(a); const kb = {}(b); return ka < kb ? -1 : ka > kb ? 1 : 0;}})",
xs, key_fn, key_fn
);
}
if function == "fmt" && !args.is_empty() {
let tmpl = emit_expr(out, level, &args[0]);
if args.len() == 1 {
return tmpl;
}
let rest: Vec<String> = args[1..].iter().map(|a| emit_expr(out, level, a)).collect();
let replacements: Vec<String> = rest
.iter()
.enumerate()
.map(|(i, val)| format!(".replace(\"{{{}}}\", {})", i, val))
.collect();
return format!("{}{}", tmpl, replacements.join(""));
}
if function == "jdmp" && args.len() == 1 {
return format!("JSON.stringify({})", emit_expr(out, level, &args[0]));
}
if function == "jpar" && args.len() == 1 {
let arg = emit_expr(out, level, &args[0]);
let call = format!(
"((s) => {{ try {{ return [\"ok\", JSON.parse(s)]; }} catch(e) {{ return [\"err\", e.message]; }} }})({})",
arg
);
return if unwrap.is_any() {
format!("_iloUnwrap({})", call)
} else {
call
};
}
if function == "env" && args.len() == 1 {
let arg = emit_expr(out, level, &args[0]);
let call = format!(
"((k) => {{ const v = typeof process !== 'undefined' && process.env[k]; return v !== undefined ? [\"ok\", v] : [\"err\", `env var '${{k}}' not set`]; }})({})",
arg
);
return if unwrap.is_any() {
format!("_iloUnwrap({})", call)
} else {
call
};
}
if function == "prod" && args.len() == 1 {
let xs = emit_expr(out, level, &args[0]);
return format!("{}.reduce((a, b) => a * b, 1)", xs);
}
if function == "argmax" && args.len() == 1 {
let xs = emit_expr(out, level, &args[0]);
return format!(
"((xs) => xs.reduce((mi, _, i) => xs[i] > xs[mi] ? i : mi, 0))({})",
xs
);
}
if function == "argmin" && args.len() == 1 {
let xs = emit_expr(out, level, &args[0]);
return format!(
"((xs) => xs.reduce((mi, _, i) => xs[i] < xs[mi] ? i : mi, 0))({})",
xs
);
}
let args_str: Vec<String> = args.iter().map(|a| emit_expr(out, level, a)).collect();
let call = format!("{}({})", js_name(function), args_str.join(", "));
if unwrap.is_any() {
format!("_iloUnwrap({})", call)
} else {
call
}
}
fn emit_match_expr(
out: &mut String,
level: usize,
subject: &Option<Box<Expr>>,
arms: &[MatchArm],
) -> String {
let subj = match subject {
Some(e) => emit_expr(out, level, e),
None => "_subject".to_string(),
};
let can_ternary = arms.iter().all(|arm| {
arm.body.len() == 1
&& matches!(arm.body[0].node, Stmt::Expr(_))
&& matches!(arm.pattern, Pattern::Wildcard | Pattern::Literal(_))
});
if can_ternary {
let mut parts: Vec<String> = Vec::new();
let mut default = "undefined".to_string();
for arm in arms {
let arm_val = match &arm.body[0].node {
Stmt::Expr(e) => emit_expr(out, level, e),
_ => "undefined".to_string(),
};
match &arm.pattern {
Pattern::Wildcard => default = arm_val,
Pattern::Literal(lit) => {
parts.push(format!("{} === {} ? {}", subj, emit_literal(lit), arm_val));
}
_ => {}
}
}
if parts.is_empty() {
return default;
}
return format!("({} : {})", parts.join(" : "), default);
}
let mut body = String::new();
body.push_str("(() => {\n");
let subj_var = "_s";
indent(&mut body, level + 1);
body.push_str(&format!("const {} = {};\n", subj_var, subj));
for (i, arm) in arms.iter().enumerate() {
match &arm.pattern {
Pattern::Wildcard => {
if i == 0 {
} else {
indent(&mut body, level + 1);
body.push_str("{\n");
}
emit_body(&mut body, &arm.body, level + 2, true);
if i > 0 {
indent(&mut body, level + 1);
body.push_str("}\n");
}
}
Pattern::Ok(binding) => {
let kw = if i == 0 { "if" } else { "else if" };
indent(&mut body, level + 1);
body.push_str(&format!(
"{} ({} && {}[0] === \"ok\") {{\n",
kw, subj_var, subj_var
));
indent(&mut body, level + 2);
body.push_str(&format!("const {} = {}[1];\n", js_name(binding), subj_var));
emit_body(&mut body, &arm.body, level + 2, true);
indent(&mut body, level + 1);
body.push_str("}\n");
}
Pattern::Err(binding) => {
let kw = if i == 0 { "if" } else { "else if" };
indent(&mut body, level + 1);
body.push_str(&format!(
"{} ({} && {}[0] === \"err\") {{\n",
kw, subj_var, subj_var
));
indent(&mut body, level + 2);
body.push_str(&format!("const {} = {}[1];\n", js_name(binding), subj_var));
emit_body(&mut body, &arm.body, level + 2, true);
indent(&mut body, level + 1);
body.push_str("}\n");
}
Pattern::Literal(lit) => {
let kw = if i == 0 { "if" } else { "else if" };
indent(&mut body, level + 1);
body.push_str(&format!(
"{} ({} === {}) {{\n",
kw,
subj_var,
emit_literal(lit)
));
emit_body(&mut body, &arm.body, level + 2, true);
indent(&mut body, level + 1);
body.push_str("}\n");
}
Pattern::TypeIs { ty, binding } => {
let kw = if i == 0 { "if" } else { "else if" };
let type_check = match ty {
Type::Number => format!("typeof {} === \"number\"", subj_var),
Type::Text => format!("typeof {} === \"string\"", subj_var),
Type::Bool => format!("typeof {} === \"boolean\"", subj_var),
_ => "true".to_string(),
};
indent(&mut body, level + 1);
body.push_str(&format!("{} ({}) {{\n", kw, type_check));
indent(&mut body, level + 2);
body.push_str(&format!("const {} = {};\n", js_name(binding), subj_var));
emit_body(&mut body, &arm.body, level + 2, true);
indent(&mut body, level + 1);
body.push_str("}\n");
}
Pattern::Or(alts) => {
let kw = if i == 0 { "if" } else { "else if" };
let conds: Vec<String> = alts
.iter()
.map(|alt| match alt {
Pattern::Literal(lit) => {
format!("{} === {}", subj_var, emit_literal(lit))
}
Pattern::Wildcard => "true".to_string(),
_ => "false".to_string(),
})
.collect();
indent(&mut body, level + 1);
body.push_str(&format!("{} ({}) {{\n", kw, conds.join(" || ")));
emit_body(&mut body, &arm.body, level + 2, true);
indent(&mut body, level + 1);
body.push_str("}\n");
}
Pattern::Variant { tag, binding } => {
let kw = if i == 0 { "if" } else { "else if" };
indent(&mut body, level + 1);
body.push_str(&format!(
"{} (Array.isArray({}) && {}[0] === \"{}\") {{\n",
kw, subj_var, subj_var, tag
));
if let Some(b) = binding {
indent(&mut body, level + 2);
body.push_str(&format!("const {} = {}[1];\n", js_name(b), subj_var));
}
emit_body(&mut body, &arm.body, level + 2, true);
indent(&mut body, level + 1);
body.push_str("}\n");
}
}
}
indent(&mut body, level);
body.push_str("})()");
body
}
fn emit_literal(lit: &Literal) -> String {
match lit {
Literal::Number(n) => {
if *n == (*n as i64) as f64 {
format!("{}", *n as i64)
} else {
format!("{}", n)
}
}
Literal::Text(s) => {
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r");
format!("\"{}\"", escaped)
}
Literal::Bool(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
Literal::Nil => "null".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer;
use crate::parser;
fn parse_and_emit(source: &str) -> String {
let tokens: Vec<crate::lexer::Token> = lexer::lex(source)
.unwrap()
.into_iter()
.map(|(t, _)| t)
.collect();
let program = parser::parse_tokens(tokens).unwrap();
emit(&program)
}
fn parse_file_and_emit(path: &str) -> String {
let source = std::fs::read_to_string(path).unwrap();
parse_and_emit(&source)
}
#[test]
fn emit_js_fac() {
let js = parse_file_and_emit("examples/recursion.ilo");
assert!(js.contains("const fac = ("), "got: {}", js);
assert!(js.contains("const fib = ("), "got: {}", js);
assert!(js.contains("fac("), "got: {}", js);
assert!(js.contains("fib("), "got: {}", js);
}
#[test]
fn emit_js_simple_function() {
let js = parse_and_emit("tot p:n q:n r:n>n;s=*p q;t=*s r;+s t");
assert!(js.contains("const tot = (p, q, r) => {"), "got: {}", js);
assert!(js.contains("const s = (p * q);"), "got: {}", js);
assert!(js.contains("return (s + t);"), "got: {}", js);
}
#[test]
fn emit_js_guard() {
let js = parse_and_emit(r#"cls sp:n>t;>=sp 1000{"gold"};>=sp 500{"silver"};"bronze""#);
assert!(js.contains("const cls = (sp) => {"), "got: {}", js);
assert!(js.contains("if ((sp >= 1000))"), "got: {}", js);
assert!(js.contains("\"gold\""), "got: {}", js);
assert!(js.contains("\"bronze\""), "got: {}", js);
}
#[test]
fn emit_js_binops() {
let js = parse_and_emit("f a:n b:n>b;=a b");
assert!(js.contains("(a === b)"), "got: {}", js);
let js = parse_and_emit("f a:n b:n>b;!=a b");
assert!(js.contains("(a !== b)"), "got: {}", js);
}
#[test]
fn emit_js_len_builtin() {
let js = parse_and_emit("f s:t>n;len s");
assert!(js.contains(".length"), "got: {}", js);
}
#[test]
fn emit_js_kebab_to_camel() {
let js = parse_and_emit("f>t;make-id()");
assert!(js.contains("makeId()"), "got: {}", js);
}
#[test]
fn emit_js_ok_err() {
let js = parse_and_emit("f x:n>R n t;~x");
assert!(js.contains("[\"ok\", x]"), "got: {}", js);
}
#[test]
fn emit_js_foreach() {
let js = parse_and_emit("f xs:L n>n;@x xs{+x 1}");
assert!(js.contains("for (const x of xs)"), "got: {}", js);
}
#[test]
fn emit_js_bool_literals() {
let js = parse_and_emit("f>b;true");
assert!(js.contains("true"), "got: {}", js);
let js = parse_and_emit("f>b;false");
assert!(js.contains("false"), "got: {}", js);
}
#[test]
fn emit_js_list_append() {
let js = parse_and_emit("f xs:L n>L n;+=xs 1");
assert!(js.contains("[...xs, 1]"), "got: {}", js);
}
#[test]
fn emit_js_nil_coalesce() {
let js = parse_and_emit("f x:O n>n;x??0");
assert!(js.contains("??"), "got: {}", js);
}
#[test]
fn emit_js_match_stmt() {
let js = parse_and_emit("f x:n>n;?x{1:10;_:0}");
assert!(js.contains("if (x === 1)"), "got: {}", js);
assert!(js.contains("return 10"), "got: {}", js);
assert!(js.contains("return 0"), "got: {}", js);
}
}