502 lines
16 KiB
Rust
502 lines
16 KiB
Rust
//! 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<Type>), 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 <stdbool.h>\n#include <stdint.h>\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<usize, usize> = 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<String> = 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
|
|
}
|