//! The program, as C: its types, its statics and its functions //! //! `Program::new` finds all of them before any C is written. Writing a function only reads //! the program, so the functions can be written in any order. use std::collections::{BTreeMap, BTreeSet}; use super::functions::Function; use super::types::{CType, ClassField, ClassKind, Member, Types}; use crate::{ flatten::ModuleBindings, infer::{LENGTH_FIELD, Type, TypeCache, join, module_binding}, libqjs::parse::{Atom, AtomRef, JsValue, opcodes::OpCode}, regify::{Operand, RegFn, RegInstr}, transform::{PROTO_FIELD, PROTOTYPE_FIELD}, }; /// Strings that the C code can need for any program: the names that `typeof` gives, and the /// results of converting undefined, null and the bools to strings const FIXED_STRINGS: &[&str] = &[ "undefined", "null", "true", "false", "boolean", "number", "string", "function", "object", ]; pub struct Program<'a> { // The input: the JS functions, and what infer found pub fns: &'a [RegFn], pub cache: &'a TypeCache, pub atoms: &'a [Atom], bindings: &'a ModuleBindings, pub entry: usize, // the walk of the entry: the JS function that runs the program // The output pub types: Types, pub strings: Vec, // statics: the text of each string literal pub globals: BTreeMap, // statics: the module bindings, which are locals of the entry, and their C types pub functions: Vec, } impl<'a> Program<'a> { pub fn new( fns: &'a [RegFn], cache: &'a TypeCache, atoms: &'a [Atom], bindings: &'a ModuleBindings, ) -> Self { let mut program = Program { fns, cache, atoms, bindings, entry: cache.find(fns.len() - 1, None, &[], fns), types: Types::new(&cache.memory), strings: vec![], globals: BTreeMap::new(), functions: vec![], }; program.add_globals(); program.add_functions(); program.add_strings(); program.add_unions(); program } /// Each module binding is a C global, so that any function can use the stdlib's fn add_globals(&mut self) { let entry = self.fns.len() - 1; for &local in self.bindings.values() { let ty = self.cache.memory.get(module_binding(entry, local)); let ctype = self.types.add(ty); self.globals.insert(local, ctype); } } /// One C function for each walk of a JS function in infer's last round, one for each /// lookup, and the host's two entry points fn add_functions(&mut self) { let walks = (0..self.cache.walks.len()).filter(|&walk| self.cache.is_live(walk)); let mut lookups = BTreeSet::new(); for walk in walks.clone() { self.add_lookups(walk, &mut lookups); } let lookups = lookups .into_iter() .map(|(class, field)| Function::Lookup(class, field)); self.functions.extend(walks.map(Function::Js)); self.functions.extend(lookups); self.functions.extend([Function::Start, Function::Complete]); } /// The lookups that a JS function's walk needs: one for each field that it gets, on each /// class that can lack the field fn add_lookups(&self, walk: usize, lookups: &mut BTreeSet<(usize, AtomRef)>) { for instr in self.instrs(walk) { let field = match instr.op { OpCode::GetField => instr.imm.atom(), OpCode::GetLength => LENGTH_FIELD, _ => continue, }; let ty = self.slot_type(walk, instr.args[0]); for member in self.types.type_members(&ty) { if let Some(class) = self.field_class(member, field) { self.add_lookup(class, field, lookups); } } } } /// The lookup of `field` on `class`, if its objects can lack the field. If they can, the /// lookup goes on to their prototypes, so those need lookups too fn add_lookup(&self, class: usize, field: AtomRef, lookups: &mut BTreeSet<(usize, AtomRef)>) { let defined = self.types.field(class, field).is_some_and(|f| f.defined); if defined || !lookups.insert((class, field)) { return; } for proto_class in self.proto_classes(class) { self.add_lookup(proto_class, field, lookups); } } /// The text of every string literal: the fixed ones, and the ones in the JS functions fn add_strings(&mut self) { let mut strings: Vec = FIXED_STRINGS.iter().map(|s| s.to_string()).collect(); for &function in &self.functions { let Function::Js(walk) = function else { continue; }; let cpool = &self.fns[self.cache.walks[walk].req.fn_idx].cpool; for instr in self.instrs(walk) { let text = match instr.op { OpCode::PushAtomValue => instr.imm.atom().text(self.atoms), OpCode::PushEmptyString => String::new(), OpCode::PushConst | OpCode::PushConst8 => match &cpool[instr.imm.const_idx()] { JsValue::String(s) => s.text(), _ => continue, }, _ => continue, }; strings.push(text); } } for text in strings { if !self.strings.contains(&text) { self.strings.push(text); } } } /// The union of every C type that the functions use, if it has several members. The /// classes' fields have theirs already (see `Types::new`) fn add_unions(&mut self) { let types: Vec = (self.functions.iter()) .flat_map(|function| self.value_types(function)) .collect(); for ty in &types { self.types.add(ty); } } /// The types of the values in a function: the locals, args and regs of a JS function, /// and what it returns, or what a lookup gives fn value_types(&self, function: &Function) -> Vec { match *function { Function::Js(walk) => { let f = &self.fns[self.cache.walks[walk].req.fn_idx]; let slots = slots(f).into_iter().map(|slot| self.slot_type(walk, slot)); slots.chain([self.cache.walks[walk].ret.clone()]).collect() } Function::Lookup(class, field) => vec![self.lookup_type(class, field)], Function::Start | Function::Complete => vec![], } } /// The instrs of a JS function that can run in a walk fn instrs(&self, walk: usize) -> impl Iterator + use<'a> { let (cache, fns) = (self.cache, self.fns); let walk = &cache.walks[walk]; let f = &fns[walk.req.fn_idx]; let blocks = f.blocks.iter().zip(&walk.reachable); let blocks = blocks.filter(|(_, reachable)| **reachable); blocks.flat_map(|(block, _)| &f.code[block.start..block.end]) } } // ────────────────────────────────────── C values ─────────────────────────────────────── /// One combination of members of some operands. When an operand is a union, the C code tests /// its tag, and then uses the member that the tag says #[derive(Clone)] pub struct Case { pub operands: Vec<(String, Member)>, // each operand, as C, as a value of one member pub tests: Vec, // the C tests that the operands are these members: one for each union pub first: bool, // the first case of its operands: an `if`. The others are `else if` } impl Program<'_> { /// The type of a local, arg or reg of a JS function, in one walk pub fn slot_type(&self, walk: usize, slot: Operand) -> Type { let addr = self.cache.addr(walk, slot, self.fns); self.cache.memory.get(addr).clone() } /// The C type of a local, arg or reg of a JS function, in one walk pub fn slot_ctype(&self, walk: usize, slot: Operand) -> CType { self.types.of(&self.slot_type(walk, slot)) } pub fn class_kind(&self, member: Member) -> ClassKind { match member { Member::Obj(class) => self.types.classes[class].kind, _ => ClassKind::Obj, } } /// Whether a value of `member` is a function pub fn is_function(&self, member: Member) -> bool { let closure = matches!(self.class_kind(member), ClassKind::Closure(_)); closure || matches!(member, Member::Native(_)) } /// Every combination of the members of `operands`. Each operand is C, and its C type pub fn cases(&self, operands: &[(String, CType)]) -> Vec { let mut cases = vec![Case { operands: vec![], tests: vec![], first: true, }]; for (value, ctype) in operands { let mut next = vec![]; for case in &cases { for (tag, member) in self.types.members_of(*ctype).into_iter().enumerate() { let mut case = case.clone(); match ctype { CType::One(_) => case.operands.push((value.clone(), member)), CType::Union(_) => { case.operands.push((format!("{value}.as.m{tag}"), member)); case.tests.push(format!("{value}.tag == {tag}")); } } case.first = next.is_empty(); next.push(case); } } cases = next; } cases } /// `value`, of `member`, as a value of C type `to`. Types only grow, so `to` holds /// `member`, or holds a float if `member` is an int pub fn convert(&self, value: &str, member: Member, to: CType) -> String { let members = self.types.members_of(to); let same = members.iter().position(|&m| m == member); let float = members.iter().position(|&m| m == Member::Float); let float = float.filter(|_| member == Member::Int); let Some(tag) = same.or(float) else { panic!("emit: a value of {member:?} where {members:?} was inferred"); }; let value = match members[tag] == member { true => value.to_string(), false => format!("(double){value}"), }; match to { CType::One(_) => value, CType::Union(id) => format!("(u{id}){{.tag = {tag}, .as.m{tag} = {value}}}"), } } /// A string literal, as C: a pointer to its static pub fn string(&self, text: &str) -> String { match self.strings.iter().position(|s| s == text) { Some(id) => format!("(&str{id})"), None => panic!("emit: no static for the string {text:?}"), } } /// `value`, of `member`, as a string, as JS's ToString does. `None` for an object: its /// conversion calls its methods, which is not supported pub fn to_string(&self, value: &str, member: Member) -> Option { Some(match member { Member::Undefined => self.string("undefined"), Member::Null => self.string("null"), Member::Bool => { let (t, f) = (self.string("true"), self.string("false")); format!("({value} ? {t} : {f})") } Member::Int => format!("__rt_number_to_string((double){value})"), Member::Float => format!("__rt_number_to_string({value})"), Member::Str => value.into(), Member::Obj(_) | Member::Native(_) => return None, }) } /// The value of the stdlib's module binding `name`, as C, and its C type pub fn binding(&self, name: &str) -> Option<(String, CType)> { let local = *self.bindings.get(name)?; Some((format!("(*g{local})"), self.globals[&local])) } } // ─────────────────────────────────────── Fields ──────────────────────────────────────── impl Program<'_> { /// The C name of a field pub fn field_name(&self, field: AtomRef) -> String { let text: String = (field.text(self.atoms).chars().take(24)) .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) .collect(); match field { AtomRef::TaggedInt(n) => format!("f_{text}_i{n}"), AtomRef::Predefined(n) => format!("f_{text}_p{n}"), AtomRef::FileLocal(n) => format!("f_{text}_a{n}"), } } /// A field of `obj`, as C to get or set it. A field that shares a location holds a /// pointer to it pub fn field_access(&self, obj: &str, field: &ClassField) -> String { let name = self.field_name(field.name); match field.shared { true => format!("(*{obj}->{name})"), false => format!("{obj}->{name}"), } } /// The value of `field` on `obj`, an object of `class`, and its C type. If every object /// of the class has the field, that is the field itself. Else it is a call of the field's /// lookup pub fn field_value(&self, class: usize, field: AtomRef, obj: &str) -> (String, CType) { match self.types.field(class, field) { Some(f) if f.defined => (self.field_access(obj, f), f.ctype), _ => { let lookup = Function::Lookup(class, field).name(self); let ctype = self.types.of(&self.lookup_type(class, field)); (format!("{lookup}({obj})"), ctype) } } } /// The class of the object that has `field`, for a value of `member`. That is the value /// itself if it is an object, or the stdlib's prototype of its kind if it is a primitive /// (see `builtin_proto`). A string's length and an array's are not fields, so they have /// none pub fn field_class(&self, member: Member, field: AtomRef) -> Option { let length = field == LENGTH_FIELD; match (member, self.class_kind(member)) { (Member::Str, _) | (Member::Obj(_), ClassKind::Array) if length => None, (Member::Obj(class), _) => Some(class), (Member::Undefined | Member::Null | Member::Native(_), _) => None, _ => self.builtin_proto(member).map(|(_, class)| class), } } /// The type that the lookup of `field` on `class` gives: the join of the field's types on /// the class's objects, and on their prototypes (see `Memory::read_field`) pub fn lookup_type(&self, class: usize, field: AtomRef) -> Type { let objs = &self.types.classes[class].objs; let memory = &self.cache.memory; let types = objs .iter() .map(|&obj| memory.read_field(&Type::Ref(obj), field)); types.fold(Type::Unknown, |all, ty| join(&all, &ty)) } /// The prototype of a primitive or an array, as C, and its class: the `prototype` field of /// the stdlib's String, Number, Boolean or Array (see `Memory::builtin_proto`) pub fn builtin_proto(&self, member: Member) -> Option<(String, usize)> { let name = match (member, self.class_kind(member)) { (Member::Str, _) => "String", (Member::Int | Member::Float, _) => "Number", (Member::Bool, _) => "Boolean", (_, ClassKind::Array) => "Array", _ => return None, }; let (stdlib_class, ctype) = self.binding(name)?; let proto = self.types.field(ctype.class()?, PROTOTYPE_FIELD)?; let value = self.field_access(&stdlib_class, proto); Some((value, proto.ctype.class()?)) } /// The classes that the prototype of an object of `class` can be. An array's prototype /// is the stdlib's. Another object's is its `__proto__` field, if it has one pub fn proto_classes(&self, class: usize) -> Vec { if self.types.classes[class].kind == ClassKind::Array { let proto = self.builtin_proto(Member::Obj(class)); return proto.map(|(_, class)| class).into_iter().collect(); } let Some(proto) = self.types.field(class, PROTO_FIELD) else { return vec![]; }; let members = self.types.members_of(proto.ctype); let classes = members.into_iter().map(|member| CType::One(member).class()); classes.flatten().collect() } } /// Every local, arg and reg of a JS function pub fn slots(f: &RegFn) -> Vec { let locals = (0..f.local_count).map(Operand::Local); let args = (0..f.arg_count).map(Operand::Arg); let regs = (0..f.reg_count).map(Operand::Reg); locals.chain(args).chain(regs).collect() }