//! C code generation. //! //! One struct per object shape, and one function per inferred //! function-and-argument pair. The body is walked the same way inference walks //! it — block by block, instruction by instruction — with one difference: //! inference skips an instruction that defines nothing, but jumps and returns //! define nothing and still have to be emitted. //! //! A function value is compile-time only: every call site knows the callee, so //! calls go straight to the monomorphized function by name. The function is //! never stored, so a closure's struct is its captured environment alone. use std::collections::HashMap; use std::fmt::Write; use crate::infer::types::Type; use crate::infer::{InferRequest, InferResult, TypeCache}; use crate::libqjs::parse::{Atom, AtomRef, ImmOperand, JsValue, atom_to_str, opcodes::OpCode}; use crate::model::{EXTRA_ARGS, FIELD_PROTO}; use crate::regify::{BasicBlock, RegFn, RegInstr}; /// The C name of each monomorphized variant, keyed by its function and argument /// types, so a call site and a definition agree. type Names = HashMap<(usize, Vec), String>; pub fn emit(cache: &TypeCache, reg_fns: &[RegFn], atoms: &[Atom]) -> String { let mut out = String::new(); out.push_str("// Generated by the jsrt compiler. Do not edit.\n\n"); out.push_str("#include \n#include \n\n"); out.push_str("// Placeholders, until the lattice can say more than this.\n"); out.push_str("typedef struct { char _; } undefined_t;\n"); out.push_str("typedef struct { char _; } any_t;\n"); out.push_str("typedef struct { char _; } empty_t;\n\n"); let names = mangle(cache, reg_fns, atoms); emit_shapes(cache, atoms, &mut out); out.push_str("// ── prototypes ──\n"); for (req, res) in sorted(cache) { let _ = writeln!(out, "{};", signature(req, res, cache, &names)); } out.push('\n'); for (req, res) in sorted(cache) { emit_fn(req, res, reg_fns, cache, atoms, &names, &mut out); } out } /// Results in a stable order, so the same program emits the same file. fn sorted(cache: &TypeCache) -> Vec<(&InferRequest, &InferResult)> { let mut rs: Vec<_> = cache.results().collect(); rs.sort_by_key(|(req, _)| (req.fn_id, format!("{:?}", req.arg_types))); rs } /// One C name per function-and-argument pair: the name, its id, and a per- /// function variant index. fn mangle(cache: &TypeCache, reg_fns: &[RegFn], atoms: &[Atom]) -> Names { let mut names = HashMap::new(); let mut seen: HashMap = HashMap::new(); for (req, _) in sorted(cache) { let variant = seen.entry(req.fn_id).or_insert(0); let base = sanitize(&atom_to_str(®_fns[req.fn_id].name, atoms)); let key = (req.fn_id, req.arg_types.clone()); names.insert(key, format!("{}_{}_{}", base, req.fn_id, variant)); *variant += 1; } names } fn name_of(fn_id: usize, arg_types: &[Type], names: &Names) -> String { names[&(fn_id, arg_types.to_vec())].clone() } fn signature(req: &InferRequest, res: &InferResult, cache: &TypeCache, names: &Names) -> String { let params: Vec = req .arg_types .iter() .enumerate() .map(|(i, ty)| format!("{} a{i}", c_type(ty, cache))) .collect(); format!( "{} {}({})", c_type(&res.ret_type, cache), name_of(req.fn_id, &req.arg_types, names), params.join(", ") ) } fn emit_fn( req: &InferRequest, res: &InferResult, reg_fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], names: &Names, out: &mut String, ) { let f = ®_fns[req.fn_id]; let _ = writeln!(out, "// {}", atom_to_str(&f.name, atoms)); let _ = writeln!(out, "{} {{", signature(req, res, cache, names)); // Every value gets a slot. Locals are not renamed, so a local and the // values flowing through it share one. for (v, ty) in res.val_types.iter().enumerate() { let _ = writeln!(out, " {} v{v};", c_type(ty, cache)); } if !res.val_types.is_empty() { out.push('\n'); } for (bi, block) in f.blocks.iter().enumerate() { let _ = writeln!(out, "L{bi}:;"); // Stack values that outlive the block are copied into the successor's // slots, and that has to happen before control leaves the block — so // the terminator is held back until the copies are out. let body = &f.code[block.start..block.end]; let (body, terminator) = match body.last() { Some(last) if is_terminator(last.op) => (&body[..body.len() - 1], Some(last)), _ => (body, None), }; for instr in body { emit_instr(instr, block, res, f, reg_fns, cache, atoms, names, out); } for instr in &block.postfix { emit_instr(instr, block, res, f, reg_fns, cache, atoms, names, out); } if let Some(instr) = terminator { emit_instr(instr, block, res, f, reg_fns, cache, atoms, names, out); } } out.push_str("}\n\n"); } #[allow(clippy::too_many_arguments)] fn emit_instr( instr: &RegInstr, block: &BasicBlock, res: &InferResult, f: &RegFn, reg_fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], names: &Names, out: &mut String, ) { use OpCode::*; let a = |i: usize| format!("v{}", instr.args[i]); // A binary operand sits under its partner on the stack, so the left-hand // side of the source expression is the *later* arg. let bin = |op: &str| format!("{} {} {}", a(1), op, a(0)); let set = |expr: String, out: &mut String| { for d in &instr.dsts { let _ = writeln!(out, " v{d} = {expr};"); } }; match instr.op { Nop | Drop => {} PushMinus1 => set("-1".into(), out), Push0 => set("0".into(), out), Push1 => set("1".into(), out), Push2 => set("2".into(), out), Push3 => set("3".into(), out), Push4 => set("4".into(), out), Push5 => set("5".into(), out), Push6 => set("6".into(), out), Push7 => set("7".into(), out), PushI8 | PushI16 | PushI32 => { let v = match instr.imm { ImmOperand::I8(n) => n as i64, ImmOperand::I16(n) => n as i64, ImmOperand::I32(n) => n as i64, _ => unreachable!(), }; set(v.to_string(), out); } PushTrue => set("true".into(), out), PushFalse => set("false".into(), out), PushConst | PushConst8 => { let ImmOperand::Const(k) = instr.imm else { unreachable!() }; let expr = match &f.cpool[k as usize] { JsValue::Int(n) => n.to_string(), JsValue::Float(x) => format!("{x:?}"), JsValue::BoolTrue => "true".into(), JsValue::BoolFalse => "false".into(), _ => unreachable!(), }; set(expr, out); } Add => set(bin("+"), out), Sub => set(bin("-"), out), Mul => set(bin("*"), out), Mod => set(bin("%"), out), Div => set(format!("(double){} / (double){}", a(1), a(0)), out), Neg => set(format!("-{}", a(0)), out), Plus => set(a(0), out), Inc => set(format!("{} + 1", a(0)), out), Dec => set(format!("{} - 1", a(0)), out), Lt => set(bin("<"), out), Lte => set(bin("<="), out), Gt => set(bin(">"), out), Gte => set(bin(">="), out), Eq | StrictEq => set(bin("=="), out), Neq | StrictNeq => set(bin("!="), out), Not => set(format!("!{}", a(0)), out), Dup | SetLoc | SetName => { let src = instr.args[0]; for d in &instr.dsts { emit_copy(*d, &res.val_types[*d], src, &res.val_types[src], atoms, out); } } Undefined => set("(undefined_t){0}".into(), out), GetArg0 => set("a0".into(), out), GetArg1 => set("a1".into(), out), GetArg2 => set("a2".into(), out), GetArg3 => set("a3".into(), out), GetArg => { let ImmOperand::Arg(k) = instr.imm else { unreachable!() }; set(format!("a{k}"), out); } // A new object, or a new closure: both start with no fields. A // closure's function is compile-time only, so it leaves no trace here. Object | Fclosure | Fclosure8 => { let d = instr.dsts[0]; let _ = writeln!( out, " v{d} = ({}){{0}};", c_type(&res.val_types[d], cache) ); } DefineField => { let ImmOperand::Atom(atom) = instr.imm else { unreachable!() }; let obj = instr.args[1]; let d = instr.dsts[0]; // A write-back names the object itself, so there is nothing to // carry over. Otherwise the result is a different shape, and C will // not assign one struct type to another — so the fields that // survive are copied one at a time. if d != obj { for (carried, _) in res.val_types[obj].fields() { if carried == atom { continue; } let cn = field_name(carried, atoms); let _ = writeln!(out, " v{d}.{cn} = v{obj}.{cn};"); } } let _ = writeln!(out, " v{d}.{} = {};", field_name(atom, atoms), a(0)); } GetField => { let ImmOperand::Atom(atom) = instr.imm else { unreachable!() }; set(format!("{}.{}", a(0), field_name(atom, atoms)), out); } Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod | TailCallMethod => { emit_call(instr, res, reg_fns, names, out); } Return | ReturnAsync => { let _ = writeln!(out, " return {};", a(0)); } ReturnUndef => { let _ = writeln!(out, " return ({}){{0}};", c_type(&res.ret_type, cache)); } Goto | Goto8 | Goto16 => { let _ = writeln!(out, " goto L{};", block.succ[0]); } IfFalse | IfFalse8 => { let _ = writeln!(out, " if (!{}) goto L{};", a(0), block.succ[0]); if let Some(fallthrough) = block.succ.get(1) { let _ = writeln!(out, " goto L{fallthrough};"); } } IfTrue | IfTrue8 => { let _ = writeln!(out, " if ({}) goto L{};", a(0), block.succ[0]); if let Some(fallthrough) = block.succ.get(1) { let _ = writeln!(out, " goto L{fallthrough};"); } } other => { let _ = writeln!(out, " /* TODO {other:?} */"); } } } /// Move one value into another's slot. /// /// A local's slot is declared with the type it ends up holding, so an earlier /// assignment to it can be a different shape. C will not assign one struct type /// to another, so the fields the two shapes share are carried over one at a /// time. Fields the destination does not have yet are simply not set. fn emit_copy( dst: usize, dst_ty: &Type, src: usize, src_ty: &Type, atoms: &[Atom], out: &mut String, ) { if dst_ty == src_ty || !is_object(dst_ty) || !is_object(src_ty) { let _ = writeln!(out, " v{dst} = v{src};"); return; } for (name, _) in src_ty.fields() { if !dst_ty.fields().any(|(n, _)| n == name) { continue; } let n = field_name(name, atoms); let _ = writeln!(out, " v{dst}.{n} = v{src}.{n};"); } } /// Control leaves the block at these, so nothing may be emitted after one. fn is_terminator(op: OpCode) -> bool { use OpCode::*; matches!( op, Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8 | Return | ReturnUndef | ReturnAsync ) } /// Something with a struct representation whose fields can be copied. A closure /// counts: its representation is its environment. fn is_object(ty: &Type) -> bool { matches!( ty, Type::Empty | Type::WithField { .. } | Type::Closure { .. } ) } /// The frame is the one inference reads: `[this,] callee, arg0 .. argN`, with /// `args` in pop order. The request is rebuilt exactly as inference built it, so /// the variant is an exact lookup, and the callee is emitted by name. fn emit_call( instr: &RegInstr, res: &InferResult, reg_fns: &[RegFn], names: &Names, out: &mut String, ) { use OpCode::*; let is_method = matches!(instr.op, CallMethod | TailCallMethod); let n = instr.args.len(); let (callee_idx, this, arg_end) = if is_method { (instr.args[n - 2], Some(instr.args[n - 1]), n - 2) } else { (instr.args[n - 1], None, n - 1) }; let Type::Closure { fn_id, .. } = res.val_types[callee_idx] else { unreachable!() }; let declared = reg_fns[fn_id].arg_count - EXTRA_ARGS; let mut arg_types = Vec::with_capacity(declared + EXTRA_ARGS); let mut args = Vec::with_capacity(declared + EXTRA_ARGS); let mut push = |ty: Type, expr: String| { arg_types.push(ty); args.push(expr); }; for i in 0..declared { match arg_end.checked_sub(i + 1) { Some(k) if i < arg_end => { let v = instr.args[k]; push(res.val_types[v].clone(), format!("v{v}")); } _ => push(Type::Undefined, "(undefined_t){0}".to_string()), } } match this { Some(v) => push(res.val_types[v].clone(), format!("v{v}")), None => push(Type::Undefined, "(undefined_t){0}".to_string()), } push(res.val_types[callee_idx].clone(), format!("v{callee_idx}")); let call = format!("{}({})", name_of(fn_id, &arg_types, names), args.join(", ")); if instr.dsts.is_empty() { let _ = writeln!(out, " {call};"); } else { for d in &instr.dsts { let _ = writeln!(out, " v{d} = {call};"); } } } fn emit_shapes(cache: &TypeCache, atoms: &[Atom], out: &mut String) { let mut shapes: Vec<_> = cache.shapes().collect(); shapes.sort_by_key(|(_, id)| id.0); for (ty, id) in shapes { let _ = writeln!(out, "typedef struct {{"); for (name, value) in ty.fields() { let _ = writeln!( out, " {} {};", c_type(value, cache), field_name(name, atoms) ); } let _ = writeln!(out, "}} Shape{};\n", id.0); } } fn c_type(ty: &Type, cache: &TypeCache) -> String { match ty { Type::Int => "int32_t".to_string(), Type::Float => "double".to_string(), Type::Bool => "bool".to_string(), Type::Undefined => "undefined_t".to_string(), // Unknown means the value was never inferred: dead code, or a known // gap. It has no real type, so it renders as undefined. Type::Unknown => "undefined_t".to_string(), Type::Any => "any_t".to_string(), // A closure is its environment; the function is compile-time only. Type::Closure { env, .. } => c_type(env, cache), // An object with no fields carries nothing, so it needs no struct. Type::Empty => "empty_t".to_string(), // A native is called by name, so its value carries nothing. Type::Native(_) => "empty_t".to_string(), Type::WithField { .. } => format!("Shape{}", cache.shape_id(ty).unwrap().0), } } fn field_name(name: AtomRef, atoms: &[Atom]) -> String { if name == FIELD_PROTO { return "proto".to_string(); } match name { AtomRef::TaggedInt(n) => format!("i{n}"), _ => sanitize(&atom_to_str(&name, atoms)), } } fn sanitize(s: &str) -> String { let mut out: String = s .chars() .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) .collect(); if !out.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') { out.insert(0, '_'); } out }