From d0c2f291e9d06c9813ea223f34a7451684377b2f Mon Sep 17 00:00:00 2001 From: UditDey Date: Tue, 13 Oct 2026 11:52:02 +0000 Subject: [PATCH] Mid rewrite --- compiler/src/emit/calls.rs | 132 ---- compiler/src/emit/classes.rs | 181 ------ compiler/src/emit/conv.rs | 282 --------- compiler/src/emit/instrs.rs | 221 ------- compiler/src/emit/mod.rs | 310 --------- compiler/src/emit/objects.rs | 231 ------- compiler/src/emit/reps.rs | 234 ------- compiler/src/flatten.rs | 27 +- compiler/src/infer/cells.rs | 256 -------- compiler/src/infer/mod.rs | 586 ++---------------- compiler/src/infer/shape.rs | 9 + compiler/src/infer/types.rs | 208 +------ compiler/src/libqjs/parse.rs | 10 +- compiler/src/main.rs | 82 +-- compiler/src/native.rs | 177 ++---- compiler/src/regify/mod.rs | 375 +---------- compiler/src/regify/walk.rs | 259 ++++++++ compiler/src/transform/args.rs | 18 +- compiler/src/transform/bindings.rs | 216 ------- .../src/transform/{declass.rs => class.rs} | 70 ++- compiler/src/transform/closure.rs | 36 ++ compiler/src/transform/expand.rs | 167 +++-- compiler/src/transform/mod.rs | 114 ++-- compiler/src/transform/new.rs | 78 ++- 24 files changed, 752 insertions(+), 3527 deletions(-) delete mode 100644 compiler/src/emit/calls.rs delete mode 100644 compiler/src/emit/classes.rs delete mode 100644 compiler/src/emit/conv.rs delete mode 100644 compiler/src/emit/instrs.rs delete mode 100644 compiler/src/emit/mod.rs delete mode 100644 compiler/src/emit/objects.rs delete mode 100644 compiler/src/emit/reps.rs delete mode 100644 compiler/src/infer/cells.rs create mode 100644 compiler/src/infer/shape.rs create mode 100644 compiler/src/regify/walk.rs delete mode 100644 compiler/src/transform/bindings.rs rename compiler/src/transform/{declass.rs => class.rs} (70%) create mode 100644 compiler/src/transform/closure.rs diff --git a/compiler/src/emit/calls.rs b/compiler/src/emit/calls.rs deleted file mode 100644 index d36b186..0000000 --- a/compiler/src/emit/calls.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Calls of fns, natives and proto links - -use super::*; - -impl<'a> Emitter<'a> { - /// A call from `place`. The callee's cell tells which code runs, and the - /// callee is its own env. A callee that is not a fn throws - pub(super) fn call(&mut self, res: &InferResult, place: Place, instr: &RegInstr, out: &mut String) { - let arg_types: Vec = instr.args.iter().map(|&a| res.val_types[a].clone()).collect(); - let operands: Vec<(String, Rep)> = (instr.args.iter().zip(&arg_types)) - .map(|(a, ty)| (format!("v{a}"), self.rep(ty))) - .collect(); - let dst = (instr.dsts[0], self.rep(&res.val_types[instr.dsts[0]])); - - self.dispatch(&operands[0].0, &arg_types[0], "jsrt_type_error();", out, |e, member, callee, rep| { - let Type::Obj(cell) = member else { - return None; - }; - - let mut arg_types = arg_types.clone(); - let mut operands = operands.clone(); - arg_types[0] = member.clone(); - operands[0] = (callee.into(), rep); - - let (call, ret) = e.call_code(cell.code()?, place, &arg_types, &operands); - Some(e.call_result(instr.op, &call, &ret, &operands[THIS_ARG as usize], dst)) - }); - } - - /// The statement that runs `call`, which gives a value of type `ret`, and - /// puts the call's result in `dst` (its value, and its rep). `new` gives - /// what the constructor returns if that is an object, else the new object - /// itself, `this` (see infer) - pub(super) fn call_result(&self, op: OpCode, call: &str, ret: &Type, this: &(String, Rep), dst: (usize, Rep)) -> String { - let (dst, dst_rep) = (format!("v{}", dst.0), dst.1); - let ret_rep = self.rep(ret); - let members = ret.members(); - let objects = members.iter().filter(|m| matches!(m, Type::Obj(_))).count(); - let this = self.conv(&this.0, this.1, dst_rep); - - match op { - _ if *ret == Type::Unknown => format!("{call};"), - OpCode::CallConstructor if objects == 0 => format!("{call}; {dst} = {this};"), - OpCode::CallConstructor if objects < members.len() => { - let is_object = match self.tagged("r", ret_rep) { - Some(a) => format!("__any_is_object({a})"), - None => format!("!{}", self.nullish("r", ret)), - }; - let value = self.conv("r", ret_rep, dst_rep); - format!("{{ {} r = {call}; {dst} = {is_object} ? {value} : {this}; }}", self.c_type(ret_rep)) - } - _ => format!("{dst} = {};", self.conv(call, ret_rep, dst_rep)), - } - } - - /// The C call of `code` with these operands (the callee, `this`, then the - /// args, each with its rep) whose types are `arg_types`, and the type it - /// returns - pub(super) fn call_code(&mut self, code: Code, place: Place, arg_types: &[Type], operands: &[(String, Rep)]) -> (String, Type) { - // An operand as a value of rep `to`. One that the call does not pass is - // undefined - let operand = |e: &Self, i: usize, to: Rep| match operands.get(i) { - Some((value, rep)) => e.conv(value, *rep, to), - None => e.conv("(undefined_t){0}", Rep::Undefined, to), - }; - - match code { - Code::Fn(fn_id) => { - let w = self.cache.find(fn_id, place, arg_types, self.fns); - self.use_walk(w); - - let walk = &self.cache.walks[w]; - let args: Vec = (walk.req.arg_types.iter().enumerate()) - .map(|(i, param)| operand(self, i, self.rep(param))) - .collect(); - - let ret = walk.result.as_ref().unwrap().ret_type.clone(); - (format!("{}({})", self.fn_name(w), args.join(", ")), ret) - } - - // A native whose args cannot be of its params' kinds throws - Code::Native(id) if native::ret_type(id, &arg_types[FIRST_ARG as usize..]) == Type::Unknown => { - ("jsrt_type_error()".into(), Type::Unknown) - } - Code::Native(id) => { - let native = &NATIVES[id]; - - let args: Vec = (native.args.iter().enumerate()) - .map(|(i, param)| operand(self, FIRST_ARG as usize + i, self.rep(param))) - .collect(); - - (format!("{}({})", native.name, args.join(", ")), native.ret.clone()) - } - - // Keeps the prototype where field reads on primitives find it - Code::ProtoLink(kind) => { - let value = operand(self, FIRST_ARG as usize, self.rep(&self.cache.protos[kind as usize])); - (format!("({} = {value}, (undefined_t){{0}})", proto_global(kind)), Type::Undefined) - } - - // Keeps the fn where `jsrt_complete` finds it - Code::HostLink => { - let value = operand(self, FIRST_ARG as usize, self.rep(&self.cache.host_complete)); - (format!("({HOST_COMPLETE_GLOBAL} = {value}, (undefined_t){{0}})"), Type::Undefined) - } - } - } - - /// The body of `jsrt_complete(h)`: a call of the fn that the stdlib links - /// for the host, with op `h` (see `infer::host_calls()`) - pub(super) fn host_complete(&mut self) -> String { - let ty = self.cache.host_complete.clone(); - let mut out = String::new(); - - if ty == Type::Unknown { - return out; - } - - let rep = self.rep(&ty); - let operands = [(HOST_COMPLETE_GLOBAL.to_string(), rep), ("(undefined_t){0}".into(), Rep::Undefined), ("h".into(), Rep::Int)]; - - self.dispatch(HOST_COMPLETE_GLOBAL, &ty, "jsrt_type_error();", &mut out, |e, member, callee, rep| { - let Type::Obj(cell) = member else { return None }; - let mut operands = operands.clone(); - operands[0] = (callee.into(), rep); - let arg_types = [member.clone(), Type::Undefined, Type::Int]; - Some(format!("{};", e.call_code(cell.code()?, HOST_COMPLETE, &arg_types, &operands).0)) - }); - - out - } -} diff --git a/compiler/src/emit/classes.rs b/compiler/src/emit/classes.rs deleted file mode 100644 index a560d08..0000000 --- a/compiler/src/emit/classes.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Which cells and walks C cannot tell apart - -use super::*; - -/// Groups the cells and the walks that C cannot tell apart. Two cells are the -/// same in C if their kinds, code and fields are, with each cell in a field's -/// type replaced by its class. Two walks are the same if their fns, the types -/// of their values and instrs, their reachable blocks and the classes of the -/// walks they call are. Both start in one class each, which splits until no -/// class splits. Gives the class of each cell, a cell of each class, and for -/// each walk, the first walk of its class -pub(super) fn classes(fns: &[RegFn], cache: &TypeCache) -> (HashMap, Vec, Vec) { - let cells: Vec = cache.cells().cloned().collect(); - let mut class: HashMap = cells.iter().map(|c| (c.clone(), 0)).collect(); - let mut sig = vec![]; - - loop { - let mut ids: HashMap, CellNum> = HashMap::new(); - let next: Vec = cells - .iter() - .map(|cell| { - sig.clear(); - sig.push(class[cell]); - cell_sig(cell, &class, &mut sig); - let n = ids.len() as CellNum; - *ids.entry(sig.clone()).or_insert(n) - }) - .collect(); - - let done = ids.len() == class.values().collect::>().len(); - cells.iter().zip(next).for_each(|(cell, n)| *class.get_mut(cell).unwrap() = n); - - if done { - break; - } - } - - let mut reps: Vec> = vec![None; class.values().max().map_or(0, |&m| m as usize + 1)]; - cells.iter().for_each(|cell| _ = reps[class[cell] as usize].get_or_insert_with(|| cell.clone())); - - // Walks. A walk that no round reached has no fn, and its own class - let walks = &cache.walks; - let mut wclass: Vec = (0..walks.len()).map(|w| if walks[w].result.is_some() { 0 } else { w as u32 + 1 }).collect(); - let callees: Vec> = (0..walks.len()).map(|w| callees_of(fns, cache, w)).collect(); - let mut count = 0; - - loop { - let mut ids: HashMap, u32> = HashMap::new(); - let next: Vec = (0..walks.len()) - .map(|w| { - sig.clear(); - sig.push(wclass[w]); - - if let Some(res) = &walks[w].result { - sig.push(walks[w].req.fn_id as u32); - walks[w].req.arg_types.iter().chain(&res.val_types).chain(&res.instr_types).for_each(|t| type_sig(t, &class, &mut sig)); - type_sig(&res.ret_type, &class, &mut sig); - sig.extend(res.reachable.iter().map(|&r| r as u32)); - sig.extend(callees[w].iter().map(|&c| wclass[c])); - } - - let n = ids.len() as u32; - *ids.entry(sig.clone()).or_insert(n) - }) - .collect(); - - wclass = next; - - if ids.len() == count { - break; - } - - count = ids.len(); - } - - let mut first: HashMap = HashMap::new(); - let walk_rep = (0..walks.len()).map(|w| *first.entry(wclass[w]).or_insert(w)).collect(); - - (class, reps.into_iter().map(Option::unwrap).collect(), walk_rep) -} - -/// The walks that walk `w` calls, in the order that emit writes the calls -pub(super) fn callees_of(fns: &[RegFn], cache: &TypeCache, w: usize) -> Vec { - let Some(res) = &cache.walks[w].result else { return vec![] }; - let fn_id = cache.walks[w].req.fn_id; - let f = &fns[fn_id]; - let mut out = vec![]; - - let blocks = f.blocks.iter().enumerate().filter(|(b, _)| res.reachable[*b]); - let code = blocks.flat_map(|(_, block): (usize, &BasicBlock)| (block.start..block.end).map(|idx| (idx, &f.code[idx]))); - - for (idx, instr) in code { - if !matches!(instr.op, OpCode::Call | OpCode::CallMethod | OpCode::CallConstructor) - || instr.args.iter().any(|&a| res.val_types[a] == Type::Unknown) - { - continue; - } - - let mut args: Vec = instr.args.iter().map(|&a| res.val_types[a].clone()).collect(); - - for member in res.val_types[instr.args[0]].members() { - if let Type::Obj(cell) = member - && let Some(Code::Fn(callee)) = cell.code() - { - args[0] = member.clone(); - out.push(cache.find(callee, (fn_id, idx), &args, fns)); - } - } - } - - out -} - -/// Append what C sees of `cell` to `sig` -pub(super) fn cell_sig(cell: &Cell, class: &HashMap, sig: &mut Vec) { - let info = cell.info(); - - match &info.kind { - ObjKind::Plain => sig.push(0), - ObjKind::Fn(code) => sig.extend(match *code { - Code::Fn(id) => [1, id as u32], - Code::Native(id) => [2, id as u32], - Code::ProtoLink(kind) => [3, kind as u32], - Code::HostLink => [5, 0], - }), - ObjKind::Array { elems } => { - sig.push(4); - type_sig(elems, class, sig); - } - } - - sig.push(info.fields.len() as u32); - - for (name, field) in &info.fields { - atom_sig(*name, sig); - sig.push(field.defined as u32); - type_sig(&field.ty, class, sig); - } -} - -/// Append `ty` to `sig`, with each cell as its class. No encoding is a prefix -/// of another, so a list of them reads back one way -pub(super) fn type_sig(ty: &Type, class: &HashMap, sig: &mut Vec) { - match ty { - Type::Unknown => sig.push(0), - Type::Undefined => sig.push(1), - Type::Null => sig.push(2), - Type::Bool(b) => sig.push(3 + b.map_or(0, |b| 1 + b as u32)), - Type::Int => sig.push(6), - Type::Float => sig.push(7), - Type::Str(None) => sig.extend([8, u32::MAX]), - Type::Str(Some(atom)) => { - sig.push(8); - atom_sig(*atom, sig); - } - Type::Obj(cell) => sig.extend([10, class[cell]]), - Type::Sum(members) => { - let mut parts: Vec> = members - .iter() - .map(|m| { - let mut part = vec![]; - type_sig(m, class, &mut part); - part - }) - .collect(); - parts.sort_unstable(); - parts.dedup(); - sig.extend([11, parts.len() as u32]); - parts.iter().for_each(|p| sig.extend(p)); - } - Type::Any => sig.push(12), - } -} - -pub(super) fn atom_sig(atom: AtomRef, sig: &mut Vec) { - sig.extend(match atom { - AtomRef::TaggedInt(n) => [0, n], - AtomRef::Predefined(n) => [1, n], - AtomRef::FileLocal(n) => [2, n as u32], - }); -} diff --git a/compiler/src/emit/conv.rs b/compiler/src/emit/conv.rs deleted file mode 100644 index e43bb6f..0000000 --- a/compiler/src/emit/conv.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Conversions between representations, `dispatch` over the members of a -//! sum, and the tests on values that C needs - -use super::*; - -impl<'a> Emitter<'a> { - /// `e`, a value of rep `from`, as a value of rep `to`. `e` is evaluated - /// once, even where its value is not needed, because it can be a call - /// - /// Types grow in a join, so most conversions go to a rep that holds more: - /// to a sum, or from `Int` to `Double`. A conversion to a rep that holds - /// less checks the kind, and a failed check is a `TypeError`: for example - /// a string that can be undefined, passed to a native's string param. A - /// value of a param that the caller does not pass is undefined - pub(super) fn conv(&self, e: &str, from: Rep, to: Rep) -> String { - if from == to { - return e.into(); - } - - // A tagged union is made in place from a value of one kind that it - // has. Else it goes through `any_t`, which the C compiler removes - match (from, to) { - (Rep::Sum(_), _) => return self.conv(&self.tagged(e, from).unwrap(), Rep::Any, to), - (_, Rep::Sum(k)) if let Some(made) = self.make_sum(e, from, k) => return made, - (_, Rep::Sum(_)) => return format!("any_{}({})", self.c_type(to), self.conv(e, from, Rep::Any)), - _ => {} - } - - let c_to = self.c_type(to); - let after_e = |value: String| format!("((void)({e}), {value})"); - - match (from, to) { - (_, Rep::Any) => match from { - Rep::Undefined => after_e("__any_undefined()".into()), - Rep::Null => after_e("__any_null()".into()), - Rep::Bool => format!("__any_from_bool({e})"), - Rep::Int => format!("__any_from_int({e})"), - Rep::Double => format!("__any_from_float({e})"), - Rep::Str | Rep::OptStr => format!("__any_from_string({e})"), - Rep::Obj(_) | Rep::OptObj(_) | Rep::Objs => format!("__any_from_object({e})"), - Rep::Sum(_) | Rep::Any => unreachable!(), - }, - (Rep::Any, _) => match to { - Rep::Undefined => after_e("(undefined_t){0}".into()), - Rep::Null => after_e("(null_t){0}".into()), - Rep::Bool => format!("__any_as_bool({e})"), - Rep::Int => format!("__any_as_int({e})"), - Rep::Double => format!("__any_as_float({e})"), - Rep::Str => format!("(({c_to})jsrt_some(__any_as_string({e})))"), - Rep::Obj(_) => format!("(({c_to})jsrt_some(__any_as_object({e})))"), - Rep::OptStr => format!("__any_as_string({e})"), - Rep::OptObj(_) | Rep::Objs => format!("(({c_to})__any_as_object({e}))"), - Rep::Sum(_) | Rep::Any => unreachable!(), - }, - (Rep::Int, Rep::Double) => format!("(double){e}"), - (Rep::Double, Rep::Int) => format!("(int32_t){e}"), - (Rep::Str, Rep::OptStr) | (Rep::Obj(_), Rep::OptObj(_)) => e.into(), - (Rep::OptStr, Rep::Str) | (Rep::OptObj(_), Rep::Obj(_)) => format!("(({c_to})jsrt_some({e}))"), - (Rep::Obj(_) | Rep::OptObj(_), Rep::Objs) => format!("((jsrt_header *){e})"), - (Rep::Undefined, Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => after_e(format!("(({c_to})JSRT_UNDEFINED)")), - (Rep::Null, Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => after_e(format!("(({c_to})NULL)")), - - // Reps that correct types never make meet - _ => after_e(format!("({c_to}){{0}} /* mismatch */")), - } - } - - /// `dst = expr`, where `expr` is a value of rep `from` - pub(super) fn assign(&self, out: &mut String, dst: &str, expr: &str, from: Rep, to: Rep) { - let _ = writeln!(out, " {dst} = {};", self.conv(expr, from, to)); - } - - /// `e`, a value of rep `from`, as a tagged union of kinds `k`, if `from` - /// has one kind and `k` has it, with no sentinels to tell apart - pub(super) fn make_sum(&self, e: &str, from: Rep, k: Kinds) -> Option { - let (kind, member) = match from { - Rep::Undefined => (1, None), - Rep::Null => (2, None), - Rep::Bool => (3, Some("b")), - Rep::Int => (4, Some("i")), - Rep::Double => (5, Some("f")), - Rep::Str => (6, Some("s")), - Rep::Obj(_) => (7, Some("o")), - _ => return None, - }; - - if k & 1 << kind == 0 { - return None; - } - - let t = self.c_type(Rep::Sum(k)); - let kind = KINDS[kind].0; - - Some(match member { - Some(m) => format!("(({t}){{ .kind = JSRT_KIND_{kind}, .as.{m} = {e} }})"), - None => format!("((void)({e}), ({t}){{ .kind = JSRT_KIND_{kind} }})"), - }) - } - - /// `e`, a value of rep `rep`, as an `any_t`, if `rep` is a tagged value - pub(super) fn tagged(&self, e: &str, rep: Rep) -> Option { - match rep { - Rep::Any => Some(e.into()), - Rep::Sum(_) => Some(format!("{}_any({e})", self.c_type(rep))), - _ => None, - } - } - - /// The absent value of a field of rep `rep` - pub(super) fn absent(&self, rep: Rep) -> String { - match rep { - Rep::Any | Rep::Sum(_) => self.conv("__any_absent()", Rep::Any, rep), - _ => format!("(({})JSRT_ABSENT)", self.c_type(rep)), - } - } - - /// A C bool that is true if `e`, a field of rep `rep`, is not absent - pub(super) fn present(&self, e: &str, rep: Rep) -> String { - match self.tagged(e, rep) { - Some(a) => format!("!__any_is_absent({a})"), - None => format!("((void *){e} != JSRT_ABSENT)"), - } - } - - /// `e`, an `any_t` that holds an object of cell `id`, as a pointer to its - /// struct - pub(super) fn any_pointer(&self, e: &str, id: CellNum) -> String { - format!("(({} *)__any_as_object({e}))", struct_name(id)) - } - - /// A C bool that is true if `e`, a value of type `ty`, is null or - /// undefined - pub(super) fn nullish(&self, e: &str, ty: &Type) -> String { - let can_be = ty.members().iter().any(Type::is_nullish); - - match self.rep(ty) { - Rep::Undefined | Rep::Null => "true".into(), - rep @ (Rep::Any | Rep::Sum(_)) => format!("__any_is_nullish({})", self.tagged(e, rep).unwrap()), - Rep::OptStr | Rep::OptObj(_) | Rep::Objs if can_be => { - format!("((void *){e} == NULL || (void *){e} == JSRT_UNDEFINED)") - } - _ => "false".into(), - } - } - - /// Writes `body`'s statement for each member of `ty`, the type of `e`. A - /// statement gets the member, and `e` as a value of that member and its - /// rep. At runtime, a member is told apart by its kind, and an object by - /// the cell in its header. `body` gives `None` for a member on which the - /// op throws. A value that no statement takes runs `otherwise` - pub(super) fn dispatch( - &mut self, - e: &str, - ty: &Type, - otherwise: &str, - out: &mut String, - mut body: impl FnMut(&mut Self, &Type, &str, Rep) -> Option, - ) { - let rep = self.rep(ty); - - if let [member] = ty.members() { - let stmt = body(self, member, e, rep); - let _ = writeln!(out, " {}", stmt.as_deref().unwrap_or(otherwise)); - return; - } - - let _ = write!(out, " "); - let mut objects = vec![]; - - for member in ty.members() { - let (value, member_rep) = self.member_value(e, rep, member); - - match (member, body(self, member, &value, member_rep)) { - (_, None) => {} - // Cells of one class do the same - (Type::Obj(cell), Some(_)) if objects.iter().any(|(id, _)| *id == self.num(cell)) => {} - (Type::Obj(cell), Some(stmt)) => objects.push((self.num(cell), stmt)), - (_, Some(stmt)) => { - let _ = write!(out, "if ({}) {{ {stmt} }} else ", self.is_member(e, rep, member)); - } - } - } - - // The objects, by cell. A pointer to the objects of one cell needs no - // switch - let is_object = match self.tagged(e, rep) { - Some(a) => format!("__any_is_object({a})"), - None => format!("!{}", self.nullish(e, ty)), - }; - - match (rep, &objects[..]) { - (_, []) => {} - (Rep::OptObj(_), [(_, stmt)]) => { - let _ = write!(out, "if ({is_object}) {{ {stmt} }} else "); - } - _ => { - let cell = match self.tagged(e, rep) { - Some(a) => format!("((jsrt_header *)__any_as_object({a}))->cell"), - None => format!("{e}->cell"), - }; - let _ = writeln!(out, "if ({is_object}) switch ({cell}) {{"); - - for (id, stmt) in &objects { - let _ = writeln!(out, " case {id}: {{ {stmt} }} break;"); - } - - let _ = write!(out, " default: {otherwise}\n }} else "); - } - } - - let _ = writeln!(out, "{otherwise}"); - } - - /// `e`, a value of rep `rep`, as a value of `member`, a member of its type, - /// and that value's rep - pub(super) fn member_value(&self, e: &str, rep: Rep, member: &Type) -> (String, Rep) { - let member_rep = self.rep(member); - - let value = match (rep, member) { - (Rep::Any | Rep::Sum(_), Type::Obj(cell)) => self.any_pointer(&self.tagged(e, rep).unwrap(), self.num(cell)), - (Rep::Any | Rep::Sum(_), _) => self.conv(e, rep, member_rep), - (Rep::Objs, Type::Obj(cell)) => format!("(({} *){e})", struct_name(self.num(cell))), - (_, Type::Undefined | Type::Null) => format!("({}){{0}}", self.c_type(member_rep)), - _ => e.into(), - }; - - (value, member_rep) - } - - /// A C bool that is true if `e`, a value of rep `rep`, is of `member`, a - /// member of its type that is not an object - pub(super) fn is_member(&self, e: &str, rep: Rep, member: &Type) -> String { - let kind = match member { - Type::Undefined => "undefined", - Type::Null => "null", - Type::Bool(_) => "boolean", - Type::Int | Type::Float => "number", - _ => "string", - }; - - match (rep, member) { - (Rep::Any | Rep::Sum(_), _) => format!("__any_is_{kind}({})", self.tagged(e, rep).unwrap()), - (_, Type::Undefined) => format!("((void *){e} == JSRT_UNDEFINED)"), - (_, Type::Null) => format!("((void *){e} == NULL)"), - _ => format!("((void *){e} != NULL && (void *){e} != JSRT_UNDEFINED)"), - } - } - - /// A C bool that is JS's truthiness of `e`, a value of type `ty` - pub(super) fn truthy(&self, e: &str, ty: &Type) -> String { - match self.rep(ty) { - Rep::Bool => e.into(), - Rep::Int => format!("({e} != 0)"), - Rep::Double => format!("({e} != 0 && {e} == {e})"), - Rep::Str => format!("(jsrt_string_length({e}) != 0)"), - Rep::OptStr => format!("(!{} && jsrt_string_length({e}) != 0)", self.nullish(e, ty)), - Rep::Obj(_) => "true".into(), - Rep::OptObj(_) | Rep::Objs => format!("!{}", self.nullish(e, ty)), - Rep::Undefined | Rep::Null => "false".into(), - rep @ (Rep::Any | Rep::Sum(_)) => format!("__any_truthy({})", self.tagged(e, rep).unwrap()), - } - } - - /// `e`, a value of type `ty`, as a string, like the spec's ToString - pub(super) fn to_string(&self, e: &str, ty: &Type) -> String { - match self.rep(ty) { - Rep::Str => e.into(), - Rep::Int | Rep::Double => format!("__native_number_to_string((double){e})"), - Rep::Bool => { - let (t, f) = (self.string_lit("true"), self.string_lit("false")); - format!("({e} ? {t} : {f})") - } - Rep::Undefined => self.string_lit("undefined"), - Rep::Null => self.string_lit("null"), - rep => format!("__any_to_string({})", self.conv(e, rep, Rep::Any)), - } - } - - pub(super) fn string_lit(&self, s: &str) -> String { - format!("jsrt_string_lit({})", c_str(s)) - } -} diff --git a/compiler/src/emit/instrs.rs b/compiler/src/emit/instrs.rs deleted file mode 100644 index 499b2a1..0000000 --- a/compiler/src/emit/instrs.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! The C of each instr - -use super::*; - -impl<'a> Emitter<'a> { - pub(super) fn emit_instr(&mut self, fn_id: usize, res: &InferResult, idx: usize, instr: &RegInstr, out: &mut String) { - use OpCode::*; - - if matches!(instr.op, Nop | Drop) { - return; - } - - // No value reaches the instr, so it cannot run. An instr that values - // reach but that gives no value always throws, or is a call that - // never returns: its op's code below does that - if instr.args.iter().any(|&a| res.val_types[a] == Type::Unknown) { - let _ = writeln!(out, " abort(); /* not reached */"); - return; - } - - let ty = &res.instr_types[idx]; - let v = |i: usize| format!("v{}", instr.args[i]); - let vt = |i: usize| &res.val_types[instr.args[i]]; - let vr = |i: usize| self.rep(&res.val_types[instr.args[i]]); - let dst = instr.dsts.first().map(|d| format!("v{d}")).unwrap_or_default(); - let dst_rep = instr.dsts.first().map_or(Rep::Undefined, |&d| self.rep(&res.val_types[d])); - let rep = self.rep(ty); - - // A known value is its own literal. A call still runs, for what it - // does - let known = match ty { - Type::Bool(Some(b)) => Some(b.to_string()), - Type::Str(Some(atom)) => Some(self.string_lit(&atom_name(*atom, self.atoms))), - _ => None, - }; - let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef); - let is_call = matches!(instr.op, Call | CallMethod | CallConstructor); - - if let Some(lit) = known.filter(|_| !is_ret && !is_call) { - return self.assign(out, &dst, &lit, rep, dst_rep); - } - - // Binary ops: the lhs was pushed first, so it is popped last - let (l, r) = (instr.args.len() == 2).then(|| (v(1), v(0))).unwrap_or_default(); - let numbers = instr.args.len() == 2 && is_num(vr(0)) && is_num(vr(1)); - - let expr = match instr.op { - // ── Literals ── - PushMinus1 => "-1".into(), - Push0 => "0".into(), - Push1 => "1".into(), - Push2 => "2".into(), - Push3 => "3".into(), - Push4 => "4".into(), - Push5 => "5".into(), - Push6 => "6".into(), - Push7 => "7".into(), - PushI8 | PushI16 | PushI32 => match instr.imm { - ImmOperand::I8(n) => n.to_string(), - ImmOperand::I16(n) => n.to_string(), - ImmOperand::I32(n) => n.to_string(), - _ => unreachable!(), - }, - PushConst | PushConst8 => match &self.fns[fn_id].cpool[instr.imm.const_idx()] { - JsValue::Int(n) => n.to_string(), - JsValue::Float(x) if x.is_finite() => format!("{x:?}"), - JsValue::Float(x) if x.is_nan() => "NAN".into(), - JsValue::Float(x) => format!("{}INFINITY", if *x < 0.0 { "-" } else { "" }), - JsValue::String(s) => self.string_lit(&js_string_text(s)), - JsValue::NativeFn(id) => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::Native(*id))))), - JsValue::ProtoLink(kind) => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::ProtoLink(*kind))))), - JsValue::HostLink => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::HostLink)))), - _ => format!("({}){{0}}", self.c_type(rep)), - }, - Undefined | Null => format!("({}){{0}}", self.c_type(rep)), - Object | Fclosure | Fclosure8 => self.alloc(self.num_of(ty)), - // The elements are the operands, last first - ArrayFrom => { - let id = self.num_of(ty); - let (name, elem) = (struct_name(id), self.elem_rep(id)); - let _ = writeln!(out, " {dst} = {};\n set_length_{name}({dst}, {});", self.alloc(id), instr.args.len()); - - for (i, &v) in instr.args.iter().rev().enumerate() { - let value = self.conv(&format!("v{v}"), self.rep(&res.val_types[v]), elem); - let _ = writeln!(out, " {dst}->items[{i}] = {value};"); - } - - return; - } - GetArrayEl | PutArrayEl => return self.element(res, instr, out), - - // ── Arithmetic ── - Add if matches!(ty, Type::Str(_)) => { - let (ls, rs) = (self.to_string(&l, vt(1)), self.to_string(&r, vt(0))); - format!("__native_string_concat({ls}, {rs})") - } - Add | Sub | Mul if numbers => { - let op = match instr.op { Add => "+", Sub => "-", _ => "*" }; - format!("{l} {op} {r}") - } - Mod if numbers && rep == Rep::Int => format!("{l} % {r}"), - Mod if numbers => format!("fmod({l}, {r})"), - Pow if numbers => format!("pow({l}, {r})"), - Div if numbers => format!("(double){l} / {r}"), - // An operand that is not a number: JS's operator, at runtime - Add | Sub | Mul | Div | Mod | Pow => { - let name = match instr.op { - Add => "add", Sub => "sub", Mul => "mul", Div => "div", Mod => "mod", _ => "pow", - }; - let (la, ra) = (self.conv(&l, vr(1), Rep::Any), self.conv(&r, vr(0), Rep::Any)); - return self.assign(out, &dst, &format!("__any_{name}({la}, {ra})"), Rep::Any, dst_rep); - } - - Neg | Plus | Inc | Dec | PostInc | PostDec if is_num(vr(0)) => { - let new = match instr.op { - Neg => format!("-{}", v(0)), - Plus => v(0), - Inc | PostInc => format!("{} + 1", v(0)), - _ => format!("{} - 1", v(0)), - }; - - // Pushes the old value, then the new one - if matches!(instr.op, PostInc | PostDec) { - let new_dst = format!("v{}", instr.dsts[1]); - self.assign(out, &new_dst, &new, rep, self.rep(&res.val_types[instr.dsts[1]])); - v(0) - } else { - new - } - } - Neg | Plus | Inc | Dec | PostInc | PostDec => { - let name = match instr.op { - Neg => "neg", Plus => "plus", Inc | PostInc => "inc", _ => "dec", - }; - let operand = self.conv(&v(0), vr(0), Rep::Any); - - // Pushes the old value as a number, then the new one - if matches!(instr.op, PostInc | PostDec) { - let new_dst = format!("v{}", instr.dsts[1]); - let new = format!("__any_{name}({operand})"); - self.assign(out, &new_dst, &new, Rep::Any, self.rep(&res.val_types[instr.dsts[1]])); - return self.assign(out, &dst, &format!("__any_plus({operand})"), Rep::Any, dst_rep); - } - - return self.assign(out, &dst, &format!("__any_{name}({operand})"), Rep::Any, dst_rep); - } - Not => match vr(0) { - Rep::Int => format!("~{}", v(0)), - r => format!("~__any_to_int32({})", self.conv(&v(0), r, Rep::Any)), - }, - - // ── Comparisons ── - Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq => { - let op = match instr.op { - Lt => "<", Lte => "<=", Gt => ">", Gte => ">=", - Eq | StrictEq => "==", _ => "!=", - }; - let eq = matches!(instr.op, Eq | Neq | StrictEq | StrictNeq); - - match (vr(1), vr(0)) { - _ if numbers => format!("{l} {op} {r}"), - (Rep::Bool, Rep::Bool) => format!("{l} {op} {r}"), - (Rep::Str, Rep::Str) => match instr.op { - Eq | StrictEq => format!("jsrt_string_eq({l}, {r})"), - Neq | StrictNeq => format!("!jsrt_string_eq({l}, {r})"), - _ => format!("jsrt_string_cmp({l}, {r}) {op} 0"), - }, - // Two objects are equal if they are one object - (Rep::Obj(_), Rep::Obj(_)) if eq => format!("(void *){l} {op} (void *){r}"), - (lr, rr) => { - let name = match instr.op { - Lt => "lt", Lte => "lte", Gt => "gt", Gte => "gte", Eq => "eq", - Neq => "neq", StrictEq => "strict_eq", _ => "strict_neq", - }; - let (la, ra) = (self.conv(&l, lr, Rep::Any), self.conv(&r, rr, Rep::Any)); - format!("__any_{name}({la}, {ra})") - } - } - } - Lnot => format!("!{}", self.truthy(&v(0), vt(0))), - - // ── Types of values only known at runtime ── - Typeof => format!("__any_typeof({})", self.conv(&v(0), vr(0), Rep::Any)), - TypeofIsUndefined | TypeofIsFunction | IsUndefined | IsNull | IsUndefinedOrNull => { - let name = match instr.op { - TypeofIsFunction => "is_function", - IsNull => "is_null", - IsUndefinedOrNull => "is_nullish", - _ => "is_undefined", - }; - format!("__any_{name}({})", self.conv(&v(0), vr(0), Rep::Any)) - } - - // ── Movers ── - SetLoc | SetName | ToPropkey => v(0), - - // ── Objects ── - DefineField | PutField => return self.write_field(res, instr, out), - GetField | GetLength => return self.read_field(res, instr, out), - - // ── Calls ── - Call | CallMethod | CallConstructor => return self.call(res, (fn_id, idx), instr, out), - - // ── Returns ── - Return | ReturnAsync => { - let value = self.conv(&v(0), vr(0), self.rep(&res.ret_type)); - let _ = writeln!(out, " return {value};"); - return; - } - ReturnUndef => { - let value = self.conv("(undefined_t){0}", Rep::Undefined, self.rep(&res.ret_type)); - let _ = writeln!(out, " return {value};"); - return; - } - - _ => unimplemented!("emit {instr:?}"), - }; - - self.assign(out, &dst, &expr, rep, dst_rep); - } -} diff --git a/compiler/src/emit/mod.rs b/compiler/src/emit/mod.rs deleted file mode 100644 index 6301c57..0000000 --- a/compiler/src/emit/mod.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! Emits C: one fn for each walk that the program calls, one struct for each -//! cell (see `docs/model.md`, section 5). Cells and walks that C cannot tell -//! apart share one struct and one fn (see `classes()`) -//! -//! A value's C representation is a function of its type (`rep()`), one for -//! each kind of type. An object is a pointer to a struct of its cell. Each -//! object starts with a header that holds the number of its cell, and the -//! fields that can be absent start as absent -//! -//! The runtime (`lib/rt/`) gives the primitive types, the string ops, the -//! `any_t` ops and the allocator - -use std::{ - cell::RefCell, - collections::{BTreeSet, HashMap, HashSet}, - fmt::Write, -}; - -use crate::{ - infer::{ - HOST, HOST_COMPLETE, InferResult, Place, TypeCache, - cells::{Cell, Code, LENGTH_FIELD, ObjKind, read_value}, - types::Type, - }, - libqjs::parse::{Atom, AtomRef, ImmOperand, JsValue, opcodes::OpCode}, - native::{self, NATIVES, ProtoOf, atom_name, js_string_text}, - regify::{BasicBlock, RegFn, RegInstr}, - transform::{FIRST_ARG, PROTO_FIELD, THIS_ARG}, -}; - -mod calls; -mod classes; -mod conv; -mod instrs; -mod objects; -mod reps; - -use classes::*; -use reps::*; - -/// The number of a class of cells in C: the number in the header of their -/// objects, and the name of their struct -type CellNum = u32; - -pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom]) -> String { - let (nums, cells, walk_rep) = classes(fns, cache); - - let mut e = Emitter { - fns, - cache, - atoms, - cells, - nums, - walk_rep, - queued: vec![false; cache.walks.len()], - queue: vec![], - helpers: HashSet::new(), - sums: RefCell::new(BTreeSet::new()), - helper_protos: String::new(), - helper_defs: String::new(), - }; - - // Every struct is declared first, so that a field can point to any of - // them - let mut structs = String::new(); - - for id in 0..e.cells.len() as CellNum { - let _ = writeln!(structs, "typedef struct {0} {0};", struct_name(id)); - } - - for id in 0..e.cells.len() as CellNum { - e.struct_def(id, &mut structs); - } - - // Which cells are fns, by number, for the runtime's `typeof` - let fn_cells: Vec<&str> = e.cells.iter().map(|cell| if cell.code().is_some() { "1" } else { "0" }).collect(); - let mut statics = format!("const bool jsrt_fn_cells[] = {{ {} }};\n\n", fn_cells.join(", ")); - - // A built-in fn is one static object - let mut init = String::new(); - - for cell in cache.builtins() { - let id = e.num(cell); - let name = struct_name(id); - let _ = writeln!(statics, "static {name} {};", builtin_name(id)); - let _ = writeln!(init, " init_{name}(&{}, {id});", builtin_name(id)); - } - - for kind in ProtoOf::ALL { - let ty = &cache.protos[kind as usize]; - - if *ty != Type::Unknown { - let _ = writeln!(statics, "static {} {};", e.c_type(e.rep(ty)), proto_global(kind)); - } - } - - if cache.host_complete != Type::Unknown { - let _ = writeln!(statics, "static {} {HOST_COMPLETE_GLOBAL};", e.c_type(e.rep(&cache.host_complete))); - } - - // The walks that the code calls, from the host's calls. The host calls the - // entry with nothing: its env and `this` are undefined - let entry = cache.find(fns.len() - 1, HOST, &[], fns); - let entry_args = vec!["(undefined_t){0}"; cache.walks[entry].req.arg_types.len()].join(", "); - e.use_walk(entry); - let complete = e.host_complete(); - - let mut fn_protos = String::new(); - let mut bodies = String::new(); - - while let Some(w) = e.queue.pop() { - let res = cache.walks[w].result.as_ref().unwrap(); - let _ = writeln!(fn_protos, "{};", e.signature(w, res)); - e.emit_fn(w, res, &mut bodies); - } - - // The program is one file, so it also holds the runtime. The runtime goes - // first (see `lib/rt/jsrt.h`) - let mut out = String::from( - "#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n#include \n#include \n\n// Natives\n", - ); - - for n in NATIVES { - let params: Vec = n.args.iter().map(|t| e.c_type(e.rep(t))).collect(); - let _ = writeln!(out, "{} {}({});", e.c_type(e.rep(&n.ret)), n.name, params.join(", ")); - } - - let _ = write!( - out, - "\n{}\n{structs}\n{statics}\n{fn_protos}\n{}\n{}{bodies}", - sum_defs(&e.sums.borrow()), - e.helper_protos, - e.helper_defs - ); - // What the host calls (see `lib/rt/jsrt.h`) - let _ = writeln!(out, "void jsrt_start(void) {{\n{init} {}({entry_args});\n}}\n", e.fn_name(entry)); - let _ = writeln!(out, "void jsrt_complete(int32_t h) {{\n{complete}}}"); - out -} - -struct Emitter<'a> { - fns: &'a [RegFn], - cache: &'a TypeCache, - atoms: &'a [Atom], - cells: Vec, // a cell of each class, by number - nums: HashMap, // the class of each cell - walk_rep: Vec, // by walk: the walk whose fn it uses - queued: Vec, // the walks that the code calls, by walk - queue: Vec, // the walks that the code calls, which are not written yet - helpers: HashSet, // the helpers made so far, by name - sums: RefCell>, // the tagged unions that the C uses - helper_protos: String, - helper_defs: String, -} - -impl<'a> Emitter<'a> { - fn num(&self, cell: &Cell) -> CellNum { - self.nums[cell] - } - - /// The number of the cell of an `Obj` type - fn num_of(&self, ty: &Type) -> CellNum { - match ty { - Type::Obj(cell) => self.num(cell), - _ => unreachable!(), - } - } - - /// Walk `w` is called, so the walk whose fn it uses is written - fn use_walk(&mut self, w: usize) { - let w = self.walk_rep[w]; - - if !self.queued[w] { - self.queued[w] = true; - self.queue.push(w); - } - } - - fn signature(&self, w: usize, res: &InferResult) -> String { - let params: Vec = (self.cache.walks[w].req.arg_types.iter().enumerate()) - .map(|(i, ty)| format!("{} p{i}", self.c_type(self.rep(ty)))) - .collect(); - - let ret = self.c_type(self.rep(&res.ret_type)); - format!("static {ret} {}({})", self.fn_name(w), params.join(", ")) - } - - fn emit_fn(&mut self, w: usize, res: &InferResult, out: &mut String) { - let fn_id = self.cache.walks[w].req.fn_id; - let f = &self.fns[fn_id]; - let mut body = String::new(); - - // Args are values `var_count + i` (see regify) - for (i, ty) in self.cache.walks[w].req.arg_types.iter().enumerate() { - let arg = f.var_count + i; - self.assign(&mut body, &format!("v{arg}"), &format!("p{i}"), self.rep(ty), self.rep(&res.val_types[arg])); - } - - for (b, block) in f.blocks.iter().enumerate() { - let _ = writeln!(body, "L{b}:;"); - - if !res.reachable[b] { - let _ = writeln!(body, " abort();"); - continue; - } - - let code = &f.code[block.start..block.end]; - - let (instrs, last) = match code.split_last() { - Some((last, rest)) if is_terminator(last.op) => (rest, Some(last)), - _ => (code, None), - }; - - for (i, instr) in instrs.iter().enumerate() { - self.emit_instr(fn_id, res, block.start + i, instr, &mut body); - } - - // Postfix copies carry values into the next block's stack slots - for copy in &block.postfix { - let (src, dst) = (copy.args[0], copy.dsts[0]); - let (from, to) = (self.rep(&res.val_types[src]), self.rep(&res.val_types[dst])); - self.assign(&mut body, &format!("v{dst}"), &format!("v{src}"), from, to); - } - - let goto = |i: usize| format!("goto L{};", block.succ[i]); - - match last.map(|l| l.op) { - Some(OpCode::IfFalse | OpCode::IfFalse8 | OpCode::IfTrue | OpCode::IfTrue8) => { - let last = last.unwrap(); - let cond = self.truthy(&format!("v{}", last.args[0]), &res.val_types[last.args[0]]); - let not = if matches!(last.op, OpCode::IfFalse | OpCode::IfFalse8) { "!" } else { "" }; - let _ = writeln!(body, " if ({not}{cond}) {} else {}", goto(0), goto(1)); - } - Some(OpCode::Goto | OpCode::Goto8 | OpCode::Goto16) => { - let _ = writeln!(body, " {}", goto(0)); - } - Some(_) => self.emit_instr(fn_id, res, block.end - 1, last.unwrap(), &mut body), - None if !block.succ.is_empty() => { - let _ = writeln!(body, " {}", goto(0)); - } - None => {} - } - } - - let _ = writeln!(out, "{} {{", self.signature(w, res)); - - for (v, ty) in res.val_types.iter().enumerate() { - let _ = writeln!(out, " {} v{v};", self.c_type(self.rep(ty))); - } - - let _ = writeln!(out, "{body}}}\n"); - } - - /// A new object of cell `id`, on the heap - fn alloc(&self, id: CellNum) -> String { - let name = struct_name(id); - format!("init_{name}(jsrt_alloc(sizeof({name})), {id})") - } - - /// The C fn of walk `w`: the fn of the walk of its class - fn fn_name(&self, w: usize) -> String { - let w = self.walk_rep[w]; - format!("f{}_{w}", self.cache.walks[w].req.fn_id) - } -} - -/// `s` as a C string literal. Every byte that is not plain printable ASCII is -/// an octal escape, so UTF-8 passes through unchanged -fn c_str(s: &str) -> String { - let mut lit = String::from("\""); - - for b in s.bytes() { - match b { - b'"' | b'\\' => lit.extend(['\\', b as char]), - 0x20..0x7f => lit.push(b as char), - _ => lit += &format!("\\{b:03o}"), - } - } - - lit + "\"" -} - -fn is_num(rep: Rep) -> bool { - matches!(rep, Rep::Int | Rep::Double) -} - -/// The struct of the objects of cell `id` -fn struct_name(id: CellNum) -> String { - format!("S{id}") -} - -/// The static object of the built-in fn of cell `id` -fn builtin_name(id: CellNum) -> String { - format!("b{id}") -} - -/// The global that holds what the host calls when an IO op completes -const HOST_COMPLETE_GLOBAL: &str = "jsrt_host_complete"; - -fn proto_global(kind: ProtoOf) -> String { - format!("proto_{kind:?}").to_lowercase() -} - -fn is_terminator(op: OpCode) -> bool { - use OpCode::*; - matches!( - op, - Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8 | Return | ReturnAsync | ReturnUndef - ) -} diff --git a/compiler/src/emit/objects.rs b/compiler/src/emit/objects.rs deleted file mode 100644 index 163e6e7..0000000 --- a/compiler/src/emit/objects.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Field reads and writes, array elements, and prototype chains - -use super::*; - -impl<'a> Emitter<'a> { - /// `obj.field = value`. A primitive has no fields to write, and undefined - /// and null throw - pub(super) fn write_field(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) { - let (value, obj) = (instr.args[0], instr.args[1]); - let value_rep = self.rep(&res.val_types[value]); - let field = instr.imm.atom(); - let value = format!("v{value}"); - - self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, _| { - match member { - Type::Obj(cell) if field == LENGTH_FIELD && cell.is_array() => { - let name = struct_name(e.num(cell)); - Some(format!("set_length_{name}({obj}, {});", e.conv(&value, value_rep, Rep::Int))) - } - Type::Obj(cell) => { - let slot = e.slot_rep(&cell.field(field).unwrap().ty, cell.can_be_absent(field)); - Some(format!("{obj}->{} = {};", e.field_name(field), e.conv(&value, value_rep, slot))) - } - Type::Undefined | Type::Null => None, - _ => Some(";".into()), - } - }); - } - - /// `dst = obj.field`: on an object, the read through its prototype chain; - /// on a primitive, the read on its kind's prototype, except that a string's - /// `length` is its own. Undefined and null throw - pub(super) fn read_field(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) { - let (obj, dst) = (instr.args[0], instr.dsts[0]); - let dst_rep = self.rep(&res.val_types[dst]); - - let field = match instr.op { - OpCode::GetLength => LENGTH_FIELD, - _ => instr.imm.atom(), - }; - - self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, _| { - let (read, rep) = match member { - Type::Obj(cell) => e.read_on(cell, field, obj), - _ => e.read_primitive(member, obj, field)?, - }; - - Some(format!("v{dst} = {};", e.conv(&read, rep, dst_rep))) - }); - } - - /// `dst = a[i]` or `a[i] = value`, on an array, and `dst = s[i]` on a - /// string. A string cannot be written, and undefined and null throw - pub(super) fn element(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) { - let put = instr.op == OpCode::PutArrayEl; - let (index, obj) = (instr.args[put as usize], instr.args[put as usize + 1]); - let index = self.conv(&format!("v{index}"), self.rep(&res.val_types[index]), Rep::Int); - - // A put has the value, a get the dst - let (v, v_rep) = match put { - true => (instr.args[0], self.rep(&res.val_types[instr.args[0]])), - false => (instr.dsts[0], self.rep(&res.val_types[instr.dsts[0]])), - }; - - self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, _| { - let id = match member { - Type::Obj(cell) => e.num(cell), - Type::Str(_) if !put => { - let c = format!("jsrt_string_at({obj}, {index})"); - return Some(format!("v{v} = {};", e.conv(&c, Rep::OptStr, v_rep))); - } - _ => return None, - }; - - let (name, elem) = (struct_name(id), e.elem_rep(id)); - - Some(match put { - true => format!("set_elem_{name}({obj}, {index}, {});", e.conv(&format!("v{v}"), v_rep, elem)), - false => format!("v{v} = {};", e.conv(&format!("elem_{name}({obj}, {index})"), elem, v_rep)), - }) - }); - } - - /// A read of `field` on the object of `cell` at `ptr`, and its rep. An own - /// field that is never absent is read in place. Any other read goes - /// through the prototype chain - pub(super) fn read_on(&mut self, cell: &Cell, field: AtomRef, ptr: &str) -> (String, Rep) { - if field == LENGTH_FIELD && cell.is_array() { - return (format!("{ptr}->length"), Rep::Int); - } - - match cell.field(field) { - Some(f) if !cell.can_be_absent(field) => (format!("{ptr}->{}", self.field_name(field)), self.rep(&f.ty)), - _ => { - let (reader, rep) = self.field_reader(cell, field); - (format!("{reader}({ptr})"), rep) - } - } - } - - /// A read of `field` on `e`, a primitive of type `ty`, and its rep: a - /// string's length, or a read on its kind's prototype. `None` if it throws - pub(super) fn read_primitive(&mut self, ty: &Type, e: &str, field: AtomRef) -> Option<(String, Rep)> { - let kind = ProtoOf::of(ty)?; - - if kind == ProtoOf::String && field == LENGTH_FIELD { - return Some((format!("jsrt_string_length({e})"), Rep::Int)); - } - - Some(self.read_proto(kind, field)) - } - - /// A read of `field` on the prototype that the stdlib links for `kind`, - /// and its rep - pub(super) fn read_proto(&mut self, kind: ProtoOf, field: AtomRef) -> (String, Rep) { - match &self.cache.protos[kind as usize] { - Type::Unknown => ("(undefined_t){0}".into(), Rep::Undefined), - Type::Obj(cell) => self.read_on(&cell.clone(), field, &proto_global(kind)), - _ => self.proto_reader(kind, field), - } - } - - /// The helper that reads `field` on the prototype that the stdlib links - /// for `kind`, where the prototype's type is a sum, and the rep it gives. - /// It reads on each member of the type - pub(super) fn proto_reader(&mut self, kind: ProtoOf, field: AtomRef) -> (String, Rep) { - let name = format!("get_{}_{}", proto_global(kind), self.field_name(field)); - let proto = &self.cache.protos[kind as usize]; - let ret = self.rep(&read_value(proto, field, &self.cache.protos)); - - if !self.helpers.insert(name.clone()) { - return (format!("{name}()"), ret); - } - - let mut body = String::new(); - - self.dispatch(&proto_global(kind), proto, "jsrt_type_error();", &mut body, |e, member, proto, _| { - let Type::Obj(cell) = member else { - return None; - }; - - let (read, rep) = e.read_on(cell, field, proto); - Some(format!("return {};", e.conv(&read, rep, ret))) - }); - - self.helper(format!("static {} {name}(void)", self.c_type(ret)), body); - (format!("{name}()"), ret) - } - - /// The helper that reads `field` on an object of `cell`, and the rep it - /// gives. It reads the own field, and where that can be absent, goes on to - /// the prototype. At the end of the chain, the field is undefined - pub(super) fn field_reader(&mut self, cell: &Cell, field: AtomRef) -> (String, Rep) { - let name = format!("get_{}_{}", struct_name(self.num(cell)), self.field_name(field)); - let read = cell.read(field, &self.cache.protos); - let ret = self.rep(&read); - - if !self.helpers.insert(name.clone()) { - return (name, ret); - } - - let own = cell.field(field); - let absent = cell.can_be_absent(field); - let mut body = String::new(); - - if let Some(own) = &own { - let slot = self.slot_rep(&own.ty, absent); - let member = format!("o->{}", self.field_name(field)); - let value = self.conv(&member, slot, ret); - - match absent { - false => { - let _ = writeln!(body, " return {value};"); - } - true => { - let _ = writeln!(body, " if ({}) return {value};", self.present(&member, slot)); - } - } - } - - // An array's prototype is the one that the stdlib links for arrays - if absent && cell.is_array() { - let (read, rep) = self.read_proto(ProtoOf::Array, field); - let _ = writeln!(body, " return {};", self.conv(&read, rep, ret)); - } else if absent { - let proto_ty = cell.field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty); - let proto_absent = cell.can_be_absent(PROTO_FIELD); - let slot = self.slot_rep(&proto_ty, proto_absent); - let member = format!("o->{}", self.field_name(PROTO_FIELD)); - let mut chain = String::new(); - - // On a prototype that is not an object, the chain ends - self.dispatch(&member, &proto_ty, "{}", &mut chain, |e, member, proto, _| { - let Type::Obj(proto_cell) = member else { - return None; - }; - - let (reader, rep) = e.field_reader(proto_cell, field); - Some(format!("return {};", e.conv(&format!("{reader}({proto})"), rep, ret))) - }); - - match (&proto_ty, proto_absent) { - (Type::Unknown, _) => {} - (_, true) => { - let _ = writeln!(body, " if ({}) {{\n{chain} }}", self.present(&member, slot)); - } - (_, false) => body += &chain, - } - - // The end of the chain. If the read cannot give undefined, the chain - // always finds the field before it - match read.members().contains(&Type::Undefined) { - true => { - let undefined = self.conv("(undefined_t){0}", Rep::Undefined, ret); - let _ = writeln!(body, " return {undefined};"); - } - false => body += " abort();\n", - } - } - - // A field that is defined, but that no value reaches: the read cannot - // run - if own.is_none() && !absent { - let _ = writeln!(body, " abort();"); - } - - let sig = format!("static {} {name}({} *o)", self.c_type(ret), struct_name(self.num(cell))); - self.helper(sig, body); - (name, ret) - } -} diff --git a/compiler/src/emit/reps.rs b/compiler/src/emit/reps.rs deleted file mode 100644 index aced9f3..0000000 --- a/compiler/src/emit/reps.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! How each type looks in C: its representation, the C type of that, -//! the struct of each class of cells, and the tagged union of each sum - -use super::*; - -/// A C representation -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum Rep { - Undefined, - Null, - Bool, - Int, - Double, - Str, // a string, or in a field, the sentinel for absent - Obj(CellNum), // a pointer to an object of the cell, or in a field, the sentinel for absent - OptStr, // a sum of a string and null or undefined: `NULL` is null, and `JSRT_UNDEFINED` is undefined - OptObj(CellNum), // a sum of the objects of one cell and null or undefined: a pointer, with the same sentinels - Objs, // a sum of the objects of several cells, maybe with null or undefined: a pointer to the header - Sum(Kinds), // any other sum: a tagged union of its kinds (see `sum_defs()`) - Any, // any value: an `any_t`. The `Any` type, and the args of the runtime's ops on sums -} - -/// A set of kinds of value, one bit for each `jsrt_kind` (see `lib/rt/any.h`) -pub(super) type Kinds = u8; - -pub(super) const ABSENT: Kinds = 1 << 0; - -/// The `jsrt_kind` of each kind, and its member in a tagged union, if it has a -/// value besides its kind -pub(super) const KINDS: [(&str, Option<(&str, &str)>); 8] = [ - ("ABSENT", None), - ("UNDEFINED", None), - ("NULL", None), - ("BOOL", Some(("bool", "b"))), - ("INT", Some(("int32_t", "i"))), - ("FLOAT", Some(("double", "f"))), - ("STRING", Some(("string_t", "s"))), - ("OBJECT", Some(("void *", "o"))), -]; - -/// The kinds of the values of type `ty` -pub(super) fn kinds(ty: &Type) -> Kinds { - ty.members().iter().fold(0, |all, member| { - all | 1 << match member { - Type::Undefined => 1, - Type::Null => 2, - Type::Bool(_) => 3, - Type::Int => 4, - Type::Float => 5, - Type::Str(_) => 6, - _ => 7, - } - }) -} - -/// The C of the tagged union of each set of kinds in `sums`, and its -/// conversions to and from `any_t`. Every member of a union is at its start, -/// so a conversion copies the payload's bytes. A union has one kind of number -/// (see `join()`): the other converts. A kind that the union lacks is a -/// `TypeError` -pub(super) fn sum_defs(sums: &BTreeSet) -> String { - let mut out = String::new(); - - for &k in sums { - let has = |i: usize| k & 1 << i != 0; - let union: String = (0..8).filter(|&i| has(i)).filter_map(|i| KINDS[i].1).map(|(t, m)| format!(" {t} {m};")).collect(); - let copy = |to: &str, from: &str| match union.is_empty() { - true => String::new(), - false => format!(" __builtin_memcpy(&{to}.as, &{from}.as, sizeof {to}.as);"), - }; - let number = match (has(4), has(5)) { - (false, true) => " if (a.kind == JSRT_KIND_INT) { v.kind = JSRT_KIND_FLOAT; v.as.f = a.as.i; return v; }", - (true, false) => " if (a.kind == JSRT_KIND_FLOAT) { v.kind = JSRT_KIND_INT; v.as.i = (int32_t)a.as.f; return v; }", - _ => "", - }; - let union = if union.is_empty() { String::new() } else { format!(" union {{{union} }} as;") }; - - let _ = writeln!(out, "typedef struct {{ jsrt_kind kind;{union} }} sum_{k};"); - let _ = writeln!(out, "static inline any_t sum_{k}_any(sum_{k} v) {{ any_t a = {{ .kind = v.kind }};{} return a; }}", copy("a", "v")); - let _ = writeln!( - out, - "static inline sum_{k} any_sum_{k}(any_t a) {{ sum_{k} v = {{ .kind = a.kind }};{number} if (!({k}u >> a.kind & 1)) jsrt_type_error();{} return v; }}", - copy("v", "a") - ); - } - - out -} - -impl<'a> Emitter<'a> { - /// The representation of a value of type `ty` - pub(super) fn rep(&self, ty: &Type) -> Rep { - match ty { - Type::Unknown | Type::Undefined => Rep::Undefined, - Type::Null => Rep::Null, - Type::Bool(_) => Rep::Bool, - Type::Int => Rep::Int, - Type::Float => Rep::Double, - Type::Str(_) => Rep::Str, - Type::Obj(cell) => Rep::Obj(self.num(cell)), - Type::Sum(_) => self.sum_rep(ty), - Type::Any => Rep::Any, - } - } - - /// The representation of a sum. A string or the objects of one cell, with - /// null or undefined, is that pointer with sentinels. The objects of - /// several cells are a pointer to their header. Any other sum is an - /// `any_t`, which holds every kind - pub(super) fn sum_rep(&self, ty: &Type) -> Rep { - let others: Vec<&Type> = ty.members().iter().filter(|m| !m.is_nullish()).collect(); - let mut classes: Vec = others.iter().filter_map(|m| self.cell_num(m)).collect(); - classes.dedup(); - - match (&others[..], &classes[..]) { - ([], _) => Rep::Sum(kinds(ty)), - ([Type::Str(_)], _) => Rep::OptStr, - (_, [id]) if classes.len() == 1 && others.iter().all(|m| matches!(m, Type::Obj(_))) => Rep::OptObj(*id), - _ if others.iter().all(|m| matches!(m, Type::Obj(_))) => Rep::Objs, - _ => Rep::Sum(kinds(ty)), - } - } - - /// The class of an `Obj` type - pub(super) fn cell_num(&self, ty: &Type) -> Option { - match ty { - Type::Obj(cell) => Some(self.num(cell)), - _ => None, - } - } - - /// The representation of a field of type `ty`. A field that can be absent - /// needs a sentinel for absent: a pointer has one, and any other rep is a - /// tagged union that can also be absent - pub(super) fn slot_rep(&self, ty: &Type, absent: bool) -> Rep { - match self.rep(ty) { - rep @ (Rep::Str | Rep::Obj(_) | Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => rep, - _ if absent => Rep::Sum(kinds(ty) | ABSENT), - rep => rep, - } - } - - /// The representation of an element of an array of cell `id`: its type, - /// or undefined for a hole - pub(super) fn elem_rep(&self, id: CellNum) -> Rep { - self.rep(&self.cells[id as usize].read_elem()) - } - - pub(super) fn c_type(&self, rep: Rep) -> String { - match rep { - Rep::Undefined => "undefined_t".into(), - Rep::Null => "null_t".into(), - Rep::Bool => "bool".into(), - Rep::Int => "int32_t".into(), - Rep::Double => "double".into(), - Rep::Str | Rep::OptStr => "string_t".into(), - Rep::Obj(id) | Rep::OptObj(id) => format!("{} *", struct_name(id)), - Rep::Objs => "jsrt_header *".into(), - Rep::Sum(k) => { - self.sums.borrow_mut().insert(k); - format!("sum_{k}") - } - Rep::Any => "any_t".into(), - } - } - - pub(super) fn field_name(&self, name: AtomRef) -> String { - let text = atom_name(name, self.atoms); - let text = text.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }); - format!("f_{}", text.collect::()) - } - - /// The struct of cell `id`, and its helpers: `init_S`, which sets the - /// header and makes each field that can be absent absent, and for an - /// array, the helpers of its elements - pub(super) fn struct_def(&mut self, id: CellNum, out: &mut String) { - let cell = self.cells[id as usize].clone(); - let name = struct_name(id); - let mut members = String::from("jsrt_header h;"); - let mut body = String::from(" o->h.cell = cell;\n"); - - // An array's elements come after its header. `items` has room for - // `cap` of them. An element holds undefined for a hole - let elem = cell.is_array().then(|| self.elem_rep(id)); - - if let Some(elem) = elem { - let _ = write!(members, " int32_t length; int32_t cap; {} *items;", self.c_type(elem)); - } - - for (field, f) in &cell.info().fields { - let absent = cell.can_be_absent(*field); - let rep = self.slot_rep(&f.ty, absent); - let member = self.field_name(*field); - let _ = write!(members, " {} {member};", self.c_type(rep)); - - if absent { - let _ = writeln!(body, " o->{member} = {};", self.absent(rep)); - } - } - - let _ = writeln!(out, "struct {name} {{ {members} }};"); - self.helper( - format!("static {name} *init_{name}({name} *o, uint32_t cell)"), - format!("{body} return o;\n"), - ); - - if let Some(elem) = elem { - let (t, hole) = (self.c_type(elem), self.conv("(undefined_t){0}", Rep::Undefined, elem)); - self.helper( - format!("static void set_length_{name}({name} *a, int32_t n)"), - format!( - " if (n < 0) jsrt_type_error();\n\ - \x20 a->items = jsrt_grow(a->items, &a->cap, n, sizeof *a->items);\n\ - \x20 for (int32_t i = a->length; i < n; i++) a->items[i] = {hole};\n\ - \x20 a->length = n;\n" - ), - ); - self.helper( - format!("static {t} elem_{name}({name} *a, int32_t i)"), - format!(" return i >= 0 && i < a->length ? a->items[i] : {hole};\n"), - ); - self.helper( - format!("static void set_elem_{name}({name} *a, int32_t i, {t} v)"), - " if (i < 0) jsrt_type_error();\n if (i >= a->length) set_length_{NAME}(a, i + 1);\n a->items[i] = v;\n" - .replace("{NAME}", &name), - ); - } - } - - pub(super) fn helper(&mut self, sig: String, body: String) { - let _ = writeln!(self.helper_protos, "{sig};"); - let _ = writeln!(self.helper_defs, "{sig} {{\n{body}}}\n"); - } -} diff --git a/compiler/src/flatten.rs b/compiler/src/flatten.rs index d57c7d2..d9090aa 100644 --- a/compiler/src/flatten.rs +++ b/compiler/src/flatten.rs @@ -1,3 +1,14 @@ +//! QuickJS bytecode represents the program as a hierarchy. Each function has a set of child functions +//! that reference captured variables in their parent +//! +//! This module: +//! - flattens the hierarchy +//! - assigns an integer idx to each function +//! - converts child ownership to a reference by-idx +//! - extracts closure captures into a separate vec +//! +//! Also adds a synthetic entry point function that calls the top level fn generated by QuickJS + use crate::libqjs::parse::{ Atom, AtomRef, ClosureVar, Function, ImmOperand, Instr, JsValue, QuickJSBinary, opcodes::OpCode, }; @@ -6,11 +17,16 @@ use crate::libqjs::parse::{ pub struct FlatFn { pub name: AtomRef, pub arg_count: usize, - pub var_count: usize, + pub local_count: usize, pub cpool: Vec, pub code: Vec, } +/// Flattens the binary +/// +/// Returns: +/// - flat_fns[i] = the i'th flattened function +/// - captures[i] = the i'th fn's captures from its parent pub fn flatten(bin: QuickJSBinary) -> (Vec, Vec>, Vec) { let mut fns = vec![]; let mut captures = vec![]; @@ -27,18 +43,19 @@ fn walk(func: Function, flat_fns: &mut Vec, captures: &mut Vec JsValue::FnById(walk(child, flat_fns, captures)), + JsValue::Function(child) => JsValue::FnByIdx(walk(child, flat_fns, captures)), other => other, }) .collect(); @@ -82,8 +99,8 @@ fn add_entry_point(fns: &mut Vec, captures: &mut Vec>) { fns.push(FlatFn { name: AtomRef::Predefined(0), arg_count: 0, - var_count: 1, - cpool: vec![JsValue::FnById(0)], + local_count: 1, + cpool: vec![JsValue::FnByIdx(0)], // First fn is the top level code, }); diff --git a/compiler/src/infer/cells.rs b/compiler/src/infer/cells.rs deleted file mode 100644 index 0606ad2..0000000 --- a/compiler/src/infer/cells.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! Cells: the types of objects, which every holder of an object shares (see -//! `docs/model.md`, section 3) - -use std::{ - cell::{Ref, RefCell}, - fmt, - hash::{Hash, Hasher}, - rc::Rc, -}; - -use crate::{ - libqjs::parse::AtomRef, - native::ProtoOf, - transform::{BOX_FIELD, PROTO_FIELD}, -}; - -use super::types::{Type, join}; - -pub const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length - -/// The prototype of each `ProtoOf`, as the stdlib links it -pub type Protos = [Type; ProtoOf::ALL.len()]; - -/// The code a fn object runs -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub enum Code { - Fn(usize), - Native(usize), - ProtoLink(ProtoOf), - HostLink, -} - -/// What the objects of a cell are, besides objects with named fields -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ObjKind { - Plain, - Fn(Code), // a fn, which runs the code - Array { elems: Type }, // an array, whose elements are the values written to them, or holes -} - -/// A field of the objects of a cell -#[derive(Debug, Clone)] -pub struct Field { - pub ty: Type, - pub defined: bool, // every object of the cell has it from the start (see `write()`) -} - -/// What a cell knows about its objects -#[derive(Debug)] -pub struct CellInfo { - pub kind: ObjKind, - pub fields: Vec<(AtomRef, Field)>, // sorted by name -} - -/// The type of an object. Every copy of an `Obj` type holds the same cell, so -/// a write through one holder is seen by all. A cell is its `Rc`: two cells -/// are equal only if they are one cell, and they are ordered by address -#[derive(Clone)] -pub struct Cell(Rc>); - -impl Cell { - pub fn new(kind: ObjKind) -> Self { - Cell(Rc::new(RefCell::new(CellInfo { kind, fields: vec![] }))) - } - - pub fn info(&self) -> Ref<'_, CellInfo> { - self.0.borrow() - } - - /// The address of the cell, which is its identity - fn addr(&self) -> usize { - Rc::as_ptr(&self.0) as usize - } - - /// The code that its objects run, if they are fns - pub fn code(&self) -> Option { - match self.info().kind { - ObjKind::Fn(code) => Some(code), - _ => None, - } - } - - pub fn is_array(&self) -> bool { - matches!(self.info().kind, ObjKind::Array { .. }) - } - - /// The type of its elements, for an array - pub fn elems(&self) -> Type { - match &self.info().kind { - ObjKind::Array { elems } => elems.clone(), - _ => unreachable!(), - } - } - - pub fn field(&self, name: AtomRef) -> Option { - let info = self.info(); - let i = info.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?; - Some(info.fields[i].1.clone()) - } - - /// Whether one of its objects can lack its own `field`. A box's value - /// never can: a read of a binding before its first write throws (its - /// TDZ), so a read that gives a value comes after a write (see transform) - pub fn can_be_absent(&self, field: AtomRef) -> bool { - field != BOX_FIELD && !self.field(field).is_some_and(|f| f.defined) - } - - /// Join `ty` into its `field`. A field is defined if a `define_field` - /// makes it: that is a write into an object before it goes anywhere (see - /// regify). A field that another write makes can be absent. An array's - /// `length` is not a field: a write of it sets the array's length. - /// Whether the cell changed - pub fn write(&self, field: AtomRef, ty: &Type, defined: bool) -> bool { - if field == LENGTH_FIELD && self.is_array() { - return false; - } - - // The join is done before the borrow: `ty` can hold this cell - let old = self.field(field); - let new = join(old.as_ref().map_or(&Type::Unknown, |f| &f.ty), ty); - let fields = &mut self.0.borrow_mut().fields; - - match fields.binary_search_by_key(&field, |(n, _)| *n) { - Ok(i) if fields[i].1.ty == new => false, - Ok(i) => { - fields[i].1.ty = new; - true - } - Err(i) => { - fields.insert(i, (field, Field { ty: new, defined })); - true - } - } - } - - /// Join `ty` into the type of its elements. Whether the cell changed - pub fn write_elem(&self, ty: &Type) -> bool { - let new = join(&self.elems(), ty); - - match &mut self.0.borrow_mut().kind { - ObjKind::Array { elems } if *elems != new => { - *elems = new; - true - } - _ => false, - } - } - - /// A read of an element of one of its arrays. An index can be a hole, or - /// past the end, which gives undefined - pub fn read_elem(&self) -> Type { - join(&self.elems(), &Type::Undefined) - } - - /// A read of `field` on one of its objects: its own field, then, where - /// that can be absent, the read on its prototype. At the end of the - /// chain, an absent field is undefined. An array's prototype is the one - /// that the stdlib links for arrays, in `protos`, and its `length` is not a - /// field - pub fn read(&self, field: AtomRef, protos: &Protos) -> Type { - self.read_chain(field, protos, &mut vec![]) - } - - /// `read()`, on a chain that came through the cells `chain`. Objects of - /// one cell can be each other's prototypes, so a chain of cells can come - /// back to a cell, which a chain of objects cannot. The read on that cell - /// is already in the chain, so it gives nothing more - fn read_chain(&self, field: AtomRef, protos: &Protos, chain: &mut Vec) -> Type { - let array = self.is_array(); - - if array && field == LENGTH_FIELD { - return Type::Int; - } - - let own = self.field(field).map_or(Type::Unknown, |f| f.ty); - - if !self.can_be_absent(field) || chain.contains(self) { - return own; - } - - // The chain ends at a prototype that is absent or not an object - let (proto, proto_absent) = match array { - true => (protos[ProtoOf::Array as usize].clone(), false), - false => (self.field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty), self.can_be_absent(PROTO_FIELD)), - }; - let mut ty = own; - - if proto_absent || proto == Type::Unknown { - ty = join(&ty, &Type::Undefined); - } - - chain.push(self.clone()); - - for member in proto.members() { - let read = match member { - Type::Obj(proto) => proto.read_chain(field, protos, chain), - _ => Type::Undefined, - }; - - ty = join(&ty, &read); - } - - chain.pop(); - ty - } -} - -/// A read of `field` on a value of type `ty`. A primitive reads its kind's -/// prototype in `protos`. Undefined and null have no fields: the read throws, -/// so it gives nothing. A sum reads on each of its members -pub fn read_value(ty: &Type, field: AtomRef, protos: &Protos) -> Type { - match ty { - Type::Obj(cell) => cell.read(field, protos), - Type::Sum(members) => { - let reads = members.iter().map(|member| read_value(member, field, protos)); - reads.fold(Type::Unknown, |all, read| join(&all, &read)) - } - ty => match ProtoOf::of(ty).map(|kind| &protos[kind as usize]) { - Some(Type::Unknown) | None => Type::Unknown, - Some(proto) => read_value(proto, field, protos), - }, - } -} - -impl PartialEq for Cell { - fn eq(&self, other: &Self) -> bool { - self.addr() == other.addr() - } -} - -impl Eq for Cell {} - -impl PartialOrd for Cell { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Cell { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.addr().cmp(&other.addr()) - } -} - -impl Hash for Cell { - fn hash(&self, state: &mut H) { - self.addr().hash(state); - } -} - -/// A cell prints as its address only: its fields can hold the cell itself -impl fmt::Debug for Cell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "cell {:#x}", self.addr()) - } -} diff --git a/compiler/src/infer/mod.rs b/compiler/src/infer/mod.rs index cbeb44d..eea30f0 100644 --- a/compiler/src/infer/mod.rs +++ b/compiler/src/infer/mod.rs @@ -1,567 +1,53 @@ -//! Infers a type for every value (see `docs/model.md`, section 4) -//! -//! A request is a fn, the call place that calls it, and the types of its args. -//! Its result is the type of each of its values. An object is named by the -//! instr that makes it and the call place of the request that runs the instr, -//! and each name has one cell for the whole program. Both come from the -//! program text, so there are finitely many requests and names, and infer -//! ends. A walk reads cells, protos and the returns of other requests, which -//! only grow, so infer walks the program in rounds until one changes nothing +mod shape; +mod types; -pub mod cells; -pub mod types; +use std::cell::{RefCell, RefMut}; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; -use std::collections::{BTreeMap, HashMap}; +pub use types::Type; -use crate::{ - libqjs::parse::{AtomRef, JsValue, opcodes::OpCode}, - native::{self, NATIVES, ProtoOf}, - regify::RegFn, - transform::{FIRST_ARG, THIS_ARG}, -}; +/// TypeCells are owned by the site (value inside a InferResult) where it was created +/// +/// Deep clone vs shallow clone is used to model by-ref and by-value semantics when passing +/// to callees or writing into objects +#[derive(Debug)] +pub struct TypeCell(Rc>); -use cells::{Cell, Code, LENGTH_FIELD, ObjKind, Protos, read_value}; -use types::{BaseType, EMPTY_STRING, Type, is_function, join, loose_eq, strict_eq, test, truthiness, typeof_name}; +impl TypeCell { + fn shallow_clone(&self) -> Self { + todo!() + } -/// A call place: a fn and the index of its call instr -pub type Place = (usize, usize); + fn deep_clone(&self) -> Self { + todo!() + } -/// The call place of the entry fn, which the host calls -pub const HOST: Place = (usize::MAX, 0); + fn get(&self) -> RefMut<'_, Type> { + self.0.borrow_mut() + } +} -/// The call place of the fn that the host calls when an IO op completes (see -/// `Code::HostLink`) -pub const HOST_COMPLETE: Place = (usize::MAX, 1); +impl Hash for TypeCell { + fn hash(&self, state: &mut H) { + self.0.as_ptr().hash(state); // TODO: hash by ptr identity is correct right? + } +} -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Hash)] pub struct InferRequest { - pub fn_id: usize, - pub place: Place, - pub arg_types: Vec, // one for each param + pub fn_idx: usize, + pub arg_types: Vec, } #[derive(Debug)] pub struct InferResult { - pub val_types: Vec, - pub instr_types: Vec, // the type each instr gives, before it is joined into its dst - pub ret_type: Type, - pub reachable: Vec, // the blocks that can run + pub local_types: Vec, + pub reg_types: Vec, + pub ret_type: TypeCell, } -/// A request and its result -#[derive(Debug)] -pub struct Walk { - pub req: InferRequest, - ret: Type, // what it returned in the rounds so far: a recursive call gives that - walking: bool, - pub result: Option, // in the current round, once it is done -} - -#[derive(Debug)] pub struct TypeCache { - pub walks: Vec, - ids: HashMap, // the walk of each request - objects: BTreeMap<(usize, usize, Place), Cell>, // the cell of each name: the fn and instr that make the objects, and the call place - builtins: Vec, // the cells of the natives, then of the proto links - pub protos: Protos, // the prototype of each `ProtoOf`, as the stdlib links it - pub host_complete: Type, // what the host calls when an IO op completes, as the stdlib links it - version: usize, // goes up on every change of a cell, a return or a proto, and on every new cell or walk -} - -impl TypeCache { - /// The request of a call of `fn_id` from `place` with args of these types. - /// An arg it does not pass is undefined, and an arg it passes beyond its - /// params is never read. Only an arg's kind crosses a call, not a known - /// value, else every literal passed would make a new walk - fn request(fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> InferRequest { - let params = (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen)); - InferRequest { fn_id, place, arg_types: params.collect() } - } - - /// The walk of a call that infer walked - pub fn find(&self, fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> usize { - self.ids[&Self::request(fn_id, place, arg_types, fns)] - } - - /// The cell of a built-in fn - pub fn builtin(&self, code: Code) -> &Cell { - match code { - Code::Native(id) => &self.builtins[id], - Code::ProtoLink(kind) => &self.builtins[NATIVES.len() + kind as usize], - Code::HostLink => &self.builtins[NATIVES.len() + ProtoOf::ALL.len()], - Code::Fn(_) => unreachable!(), - } - } - - /// The cells of the built-in fns - pub fn builtins(&self) -> &[Cell] { - &self.builtins - } - - /// Every cell: the built-in ones, then one for each name - pub fn cells(&self) -> impl Iterator { - self.builtins.iter().chain(self.objects.values()) - } - - /// The cell of the objects of `name`, made the first time - fn object(&mut self, name: (usize, usize, Place), kind: ObjKind) -> Cell { - if let Some(cell) = self.objects.get(&name) { - return cell.clone(); - } - - self.version += 1; - self.objects.entry(name).or_insert(Cell::new(kind)).clone() - } - - /// Count a change, if there was one - fn changed(&mut self, changed: bool) { - self.version += changed as usize; - } - - /// Infer a call of `fn_id` from `place`, walking it unless its request is - /// done in this round. Gives what it returns - fn call(&mut self, fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> Type { - let req = Self::request(fn_id, place, arg_types, fns); - - let w = match self.ids.get(&req) { - Some(&w) => w, - None => { - self.version += 1; - self.ids.insert(req.clone(), self.walks.len()); - self.walks.push(Walk { req, ret: Type::Unknown, walking: false, result: None }); - self.walks.len() - 1 - } - }; - - match &self.walks[w] { - Walk { walking: true, ret, .. } => return ret.clone(), - Walk { result: Some(res), .. } => return res.ret_type.clone(), - _ => {} - } - - self.walks[w].walking = true; - let res = walk_fn(w, fns, self); - let ret = res.ret_type.clone(); - - let walk = &mut self.walks[w]; - let joined = join(&walk.ret, &ret); - let grew = joined != walk.ret; - walk.walking = false; - walk.ret = joined; - walk.result = Some(res); - self.changed(grew); - ret - } -} - -/// The join of `f` over the members of `ty` -fn map(ty: &Type, mut f: impl FnMut(&Type) -> Type) -> Type { - ty.members().iter().fold(Type::Unknown, |all, member| join(&all, &f(member))) -} - -/// What `f` gives for every member of `ty`, if all members give the same -fn agree(ty: &Type, f: impl Fn(&Type) -> Option) -> Option { - let mut members = ty.members().iter().map(f); - let first = members.next()??; - members.all(|t| t.as_ref() == Some(&first)).then_some(first) -} - -/// What `f` gives for every pair of members of `a` and `b`, if all pairs give -/// the same -fn agree2(a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option) -> Option { - let pairs = a.members().iter().flat_map(|x| b.members().iter().map(move |y| (x, y))); - let mut results = pairs.map(|(x, y)| f(x, y)); - let first = results.next()??; - results.all(|t| t.as_ref() == Some(&first)).then_some(first) -} - -/// A call of `code` from `place` with operands of these types (see the call -/// rule in `walk_fn()`). Gives what it returns -fn call_code(cache: &mut TypeCache, code: Code, arg_types: &[Type], fns: &[RegFn], place: Place) -> Type { - let args = &arg_types[FIRST_ARG as usize..]; - - match code { - Code::Fn(fn_id) => cache.call(fn_id, place, arg_types, fns), - Code::Native(id) => native::ret_type(id, args), - Code::ProtoLink(kind) => { - let linked = &cache.protos[kind as usize]; - let joined = join(linked, args.first().unwrap_or(&Type::Undefined)); - cache.changed(joined != *linked); - cache.protos[kind as usize] = joined; - Type::Undefined - } - Code::HostLink => { - let joined = join(&cache.host_complete, args.first().unwrap_or(&Type::Undefined)); - cache.changed(joined != cache.host_complete); - cache.host_complete = joined; - Type::Undefined - } - } -} - -/// The calls that the host makes: the entry, with no args, then the fn that -/// it calls when an IO op completes, with the op's handle. Each member of its -/// type is its own env, and `this` is undefined -fn host_calls(cache: &mut TypeCache, fns: &[RegFn]) { - // The synthetic program entry is appended last by `flatten` - cache.call(fns.len() - 1, HOST, &[], fns); - - for member in cache.host_complete.clone().members() { - if let Type::Obj(cell) = member - && let Some(Code::Fn(fn_id)) = cell.code() - { - cache.call(fn_id, HOST_COMPLETE, &[member.clone(), Type::Undefined, Type::Int], fns); - } - } -} - -/// An array index must be a number. Any other key is a computed field name, -/// which is not supported -fn check_index(index: &Type) { - for member in index.members() { - if !matches!(member, Type::Int | Type::Float) { - unimplemented!("`a[k]` with a key of type {member:?}: computed field names"); - } - } -} - -/// Walk the program from the entry, in rounds, until a round changes -/// nothing. The results are those of the last round -pub fn infer_types(fns: &[RegFn]) -> TypeCache { - let mut cache = TypeCache { - walks: vec![], - ids: HashMap::new(), - objects: BTreeMap::new(), - builtins: vec![], - protos: std::array::from_fn(|_| Type::Unknown), - host_complete: Type::Unknown, - version: 0, - }; - - let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink)).chain([Code::HostLink]); - cache.builtins = codes.map(|code| Cell::new(ObjKind::Fn(code))).collect(); - - loop { - let version = cache.version; - cache.walks.iter_mut().for_each(|w| w.result = None); - - host_calls(&mut cache, fns); - - if cache.version == version { - return cache; - } - } -} - -fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult { - use OpCode::*; - - let (fn_id, place) = (cache.walks[w].req.fn_id, cache.walks[w].req.place); - let f = ®_fns[fn_id]; - let mut val_types = vec![Type::Unknown; f.val_count]; - let mut instr_types = vec![Type::Unknown; f.code.len()]; - let mut ret_type = Type::Unknown; - - // arg `i` is value id `var_count + i` (see regify) - for (i, ty) in cache.walks[w].req.arg_types.iter().enumerate() { - val_types[f.var_count + i] = ty.clone(); - } - - // The blocks that can run, given what the types say about each condition - let mut reachable = vec![false; f.blocks.len()]; - reachable[0] = true; - - // Scratch for calls, kept across instrs - let mut arg_types: Vec = vec![]; - - // Walk instrs till nothing changes. A return only changes `ret_type` once - // its block is reachable, and a field read only gives more once a cell - // changes, so the values, the blocks and the version are enough to watch - loop { - let version = cache.version; - let mut changed = false; - - for (block_idx, block) in f.blocks.iter().enumerate() { - if !reachable[block_idx] { - continue; - } - - let code = &f.code[block.start..block.end]; - let code = code.iter().chain(&block.postfix); - - for (offset, instr) in code.enumerate() { - // Jumps are the block's to walk (see below). The others give - // nothing - if matches!(instr.op, Nop | Drop | Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8) { - continue; - } - - // A rule with an `Unknown` operand gives `Unknown`: no value has - // reached the instr yet - if instr.args.iter().any(|&v| val_types[v] == Type::Unknown) { - continue; - } - - // The instr's index in the code (postfix copies have none) - let idx = block.start + offset; - let arg = |i: usize| val_types[instr.args[i]].clone(); - - // The cell of the objects that this instr makes in this - // request: the one of its name - let cell_here = |cache: &mut TypeCache, kind: ObjKind| cache.object((fn_id, idx, place), kind); - - let ty = match instr.op { - // ────────────────────────────────────────── Literals ─────────────────────────────────────────────────────── - PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 - | PushI8 | PushI16 | PushI32 => Type::Int, - - PushTrue => Type::Bool(Some(true)), - PushFalse => Type::Bool(Some(false)), - PushAtomValue => Type::Str(Some(instr.imm.atom())), - PushEmptyString => Type::Str(Some(EMPTY_STRING)), - - PushConst | PushConst8 => match f.cpool[instr.imm.const_idx()] { - JsValue::Int(_) => Type::Int, - JsValue::Float(_) => Type::Float, - JsValue::String(_) => Type::Str(None), - JsValue::BoolTrue => Type::Bool(Some(true)), - JsValue::BoolFalse => Type::Bool(Some(false)), - JsValue::Undefined => Type::Undefined, - JsValue::NativeFn(id) => Type::Obj(cache.builtin(Code::Native(id)).clone()), - JsValue::ProtoLink(kind) => Type::Obj(cache.builtin(Code::ProtoLink(kind)).clone()), - JsValue::HostLink => Type::Obj(cache.builtin(Code::HostLink).clone()), - _ => unreachable!(), - }, - - Undefined => Type::Undefined, - Null => Type::Null, - - // ──────────────────────────────────────── Arithmetic ─────────────────────────────────────────────────────── - // `+` concatenates if either side becomes a string, which - // a string or an object does. Else it adds numbers - Add => { - let r = arg(0); - - map(&arg(1), |l| map(&r, |r| match (l, r) { - (l, r) if [l, r].iter().any(|t| matches!(t, Type::Str(_) | Type::Obj(_))) => Type::Str(None), - (Type::Int, Type::Int) => Type::Int, - _ => Type::Float, - })) - } - // Any other operator converts its operands to numbers - Sub | Mul | Mod | Pow => { - let r = arg(0); - - map(&arg(1), |l| map(&r, |r| match (l, r) { - (Type::Int, Type::Int) => Type::Int, - _ => Type::Float, - })) - } - Neg | Plus | Inc | Dec | PostInc | PostDec => map(&arg(0), |t| match t { - Type::Int => Type::Int, - _ => Type::Float, - }), - Div => Type::Float, - // Bitwise NOT gives an int32 - Not => Type::Int, - - Lt | Lte | Gt | Gte => Type::Bool(None), - - // ─────────────────────────────── Conditions the types may decide ────────────────────────────────────────── - // A known value lets infer skip the branch that cannot run - // (see `reachable` below) - StrictEq | StrictNeq | Eq | Neq => { - let eq = match instr.op { - StrictEq | StrictNeq => agree2(&arg(0), &arg(1), strict_eq), - _ => agree2(&arg(0), &arg(1), loose_eq), - }; - - Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq))) - } - Lnot => Type::Bool(agree(&arg(0), truthiness).map(|t| !t)), - Typeof => match agree(&arg(0), typeof_name) { - Some(name) => Type::Str(Some(AtomRef::Predefined(name))), - None => Type::Str(None), - }, - IsUndefined | TypeofIsUndefined => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined]))), - IsNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Null]))), - IsUndefinedOrNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))), - TypeofIsFunction => Type::Bool(agree(&arg(0), is_function)), - - // ─────────────────────────────────────── Value Movers ────────────────────────────────────────────────────── - SetLoc | SetName => arg(0), - - // ───────────────────────────────────────── Objects ───────────────────────────────────────────────────────── - Object => Type::Obj(cell_here(cache, ObjKind::Plain)), - Fclosure | Fclosure8 => { - let JsValue::FnById(child) = f.cpool[instr.imm.const_idx()] else { - unreachable!() - }; - - Type::Obj(cell_here(cache, ObjKind::Fn(Code::Fn(child)))) - } - // The elements are its operands - ArrayFrom => { - let cell = cell_here(cache, ObjKind::Array { elems: Type::Unknown }); - - for &v in &instr.args { - let grew = cell.write_elem(&val_types[v]); - cache.changed(grew); - } - - Type::Obj(cell) - } - // `a[i]` and `a[i] = x`, on arrays, and `s[i]` on strings: - // an index is a number. Other objects need computed field - // names, which are not supported. A string cannot be - // written (module code is strict), and undefined and null - // throw - GetArrayEl => { - let index = arg(0); - - map(&arg(1), |obj| match obj { - Type::Obj(cell) if cell.is_array() => { - check_index(&index); - cell.read_elem() - } - Type::Str(_) => { - check_index(&index); - join(&Type::Str(None), &Type::Undefined) - } - Type::Undefined | Type::Null => Type::Unknown, - obj => unimplemented!("`o[k]` on {obj:?}: computed field names"), - }) - } - PutArrayEl => { - let (value, index) = (arg(0), arg(1)); - - for obj in arg(2).members() { - match obj { - Type::Obj(cell) if cell.is_array() => { - check_index(&index); - let grew = cell.write_elem(&value); - cache.changed(grew); - } - Type::Undefined | Type::Null | Type::Str(_) => {} - obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"), - } - } - - Type::Unknown - } - // A key stays as it is: a number is an index - ToPropkey => arg(0), - // The write goes into the object's cell, which every holder - // of the object sees. A primitive has no fields to write, - // and undefined and null throw - DefineField | PutField => { - let (field, value) = (instr.imm.atom(), arg(0)); - - for obj in arg(1).members() { - if let Type::Obj(cell) = obj { - let grew = cell.write(field, &value, instr.op == DefineField); - cache.changed(grew); - } - } - - Type::Unknown - } - GetField => read_value(&arg(0), instr.imm.atom(), &cache.protos), - // A string's length is its own. Anything else reads a - // `length` field - GetLength => map(&arg(0), |obj| match obj { - Type::Str(_) => Type::Int, - obj => read_value(obj, LENGTH_FIELD, &cache.protos), - }), - - // ───────────────────────────────────── Function Calls ────────────────────────────────────────────────────── - // Operands are the callee's params: the callee (its env), - // `this`, then the args (see regify). A callee that is not - // a fn throws, so it gives nothing. A call of each member - // of a sum passes that member as the env - Call | CallMethod | CallConstructor => { - let ret = map(&arg(0), |callee| { - let Type::Obj(cell) = callee else { - return Type::Unknown; - }; - let Some(code) = cell.code() else { - return Type::Unknown; - }; - - arg_types.clear(); - arg_types.extend(instr.args.iter().map(|&v| val_types[v].clone())); - arg_types[0] = callee.clone(); - call_code(cache, code, &arg_types, reg_fns, (fn_id, idx)) - }); - - // `new` gives what the constructor returns if that is an - // object, else the new object itself, `this` - match instr.op { - CallConstructor => map(&ret, |ret| match ret { - Type::Obj(_) => ret.clone(), - _ => arg(THIS_ARG as usize), - }), - _ => ret, - } - } - - // ──────────────────────────────────────── Returns ────────────────────────────────────────────────────────── - Return | ReturnAsync => arg(0), - ReturnUndef => Type::Undefined, - - _ => unreachable!("{instr:?}"), - }; - - // Postfix copies come after the block's own instrs, and have no - // entry: a copy gives the type of what it copies - if idx < block.end { - instr_types[idx] = join(&instr_types[idx], &ty); - } - - if matches!(instr.op, Return | ReturnAsync | ReturnUndef) { - ret_type = join(&ret_type, &ty); - continue; - } - - for &dst in &instr.dsts { - let joined = join(&val_types[dst], &ty); - changed |= joined != val_types[dst]; - val_types[dst] = joined; - } - } // instr - - // Mark the blocks this one can go to. A conditional jump whose - // condition the types decide goes one way only. One whose - // condition nothing has reached yet goes nowhere yet - // (A block that only carries copies on an edge has no instrs) - let jumps = match f.code[block.start..block.end].last() { - Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => { - match &val_types[last.args[0]] { - Type::Unknown => continue, - cond => agree(cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)), - } - } - _ => None, - }; - - // succ[0] is the jump target, succ[1] the fall-through - for (i, &succ) in block.succ.iter().enumerate() { - if jumps.is_none_or(|jumps| jumps == (i == 0)) && !reachable[succ] { - reachable[succ] = true; - changed = true; - } - } - } // block - - if !changed && cache.version == version { - break; - } - } // loop - - InferResult { - val_types, - instr_types, - ret_type, - reachable, - } + results: HashMap, } diff --git a/compiler/src/infer/shape.rs b/compiler/src/infer/shape.rs new file mode 100644 index 0000000..f527664 --- /dev/null +++ b/compiler/src/infer/shape.rs @@ -0,0 +1,9 @@ +use super::types::Type; +use crate::libqjs::parse::AtomRef; + +#[derive(Debug, Hash)] +pub enum Shape { + Obj(Vec<(AtomRef, Type)>), + Sum(Vec), + Array(Box), +} diff --git a/compiler/src/infer/types.rs b/compiler/src/infer/types.rs index e6369d0..7fc462a 100644 --- a/compiler/src/infer/types.rs +++ b/compiler/src/infer/types.rs @@ -1,209 +1,15 @@ -//! Types: the values that a place can hold, their joins, and what a type -//! decides about the values in it (see `docs/model.md`, section 2) - -use std::rc::Rc; - +use super::shape::Shape; use crate::libqjs::parse::AtomRef; -use super::cells::Cell; - -pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // "" - -/// A type is one of these. Join rules: -/// Unknown * T = T -/// Any * T = Any -/// T * T = T -/// Bool(a) * Bool(b) = Bool, the value no longer known. The same for Str -/// Int * Float = Float -/// T * U = T | U, a sum -/// -/// A sum is a value of one of a list of types, its members. It has two or more -/// members, none of them `Unknown`, `Sum` or `Any`, sorted in the order of the -/// variants below. It has at most one member of each base type, except -/// objects: one `Obj` for each cell -#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Hash)] pub enum Type { - Unknown, // no value has reached it yet + Unknown, Undefined, Null, - Bool(Option), // the value, if it is known + Bool(Option), Int, Float, - Str(Option), // the value, if it is known. Atoms are interned: equal atoms are equal strings - Obj(Cell), // an object, which the cell describes. A fn is an object whose cell has code - Sum(Rc<[Type]>), // see above - Any, // any value. No rule gives it now: a join gives a sum instead -} - -/// A `Type` without its detail (a known value, a cell): the kinds of value -/// that JS tells apart -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BaseType { - Undefined, - Null, - Bool, - Number, - String, - Object, -} - -impl Type { - /// The base type of every value of the type. `None` for `Unknown`, which - /// has no values, and for `Sum` and `Any`, which can have more than one - pub fn base(&self) -> Option { - match self { - Type::Unknown | Type::Sum(_) | Type::Any => None, - Type::Undefined => Some(BaseType::Undefined), - Type::Null => Some(BaseType::Null), - Type::Bool(_) => Some(BaseType::Bool), - Type::Int | Type::Float => Some(BaseType::Number), - Type::Str(_) => Some(BaseType::String), - Type::Obj(_) => Some(BaseType::Object), - } - } - - pub fn is_nullish(&self) -> bool { - matches!(self, Type::Undefined | Type::Null) - } - - /// The types that a value of the type can have: the members of a sum, - /// else the type itself. `Unknown` has none - pub fn members(&self) -> &[Type] { - match self { - Type::Unknown => &[], - Type::Sum(members) => members, - ty => std::slice::from_ref(ty), - } - } - - /// The type with every known value dropped - pub fn widen(&self) -> Type { - match self { - Type::Bool(_) => Type::Bool(None), - Type::Str(_) => Type::Str(None), - Type::Sum(members) => sum_of(members.iter().map(Type::widen).collect()), - ty => ty.clone(), - } - } -} - -/// The join of two types (see `Type`) -pub fn join(a: &Type, b: &Type) -> Type { - match (a, b) { - (Type::Unknown, t) | (t, Type::Unknown) => t.clone(), - (a, b) if a == b => a.clone(), - (Type::Any, _) | (_, Type::Any) => Type::Any, - _ => { - let joined = sum_of(a.members().iter().chain(b.members()).cloned().collect()); - - // Keep the sum that exists, so that its members are not copied - match () { - _ if joined == *a => a.clone(), - _ if joined == *b => b.clone(), - _ => joined, - } - } - } -} - -/// The type whose members are `list`: sorted, and two of one base type -/// joined -fn sum_of(mut list: Vec) -> Type { - list.sort_unstable(); - list.dedup_by(|b, a| { - let joins = a.base() == b.base() && a.base() != Some(BaseType::Object); - - if joins { - *a = join_base(a, b); - } - - joins || a == b - }); - - match list.len() { - 1 => list.pop().unwrap(), - _ => Type::Sum(list.into()), - } -} - -/// The join of two types of one base type, neither of them `Obj` -fn join_base(a: &Type, b: &Type) -> Type { - match (a, b) { - (Type::Bool(x), Type::Bool(y)) => Type::Bool(if x == y { *x } else { None }), - (Type::Str(x), Type::Str(y)) => Type::Str(if x == y { *x } else { None }), - (Type::Int, Type::Int) => Type::Int, - (Type::Int | Type::Float, Type::Int | Type::Float) => Type::Float, - (a, _) => a.clone(), - } -} - -/// The type, if every value of it is one value that the type tells -pub(super) fn single(ty: &Type) -> Option { - match ty { - Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(ty.clone()), - _ => None, - } -} - -// The tests below take one member of a type each (see `agree()` in infer) - -/// Whether `a === b`, if the types decide it -pub(super) fn strict_eq(a: &Type, b: &Type) -> Option { - let disjoint = matches!((a.base(), b.base()), (Some(x), Some(y)) if x != y); - - match (single(a), single(b)) { - _ if disjoint => Some(false), - (Some(x), Some(y)) => Some(x == y), - _ => None, - } -} - -/// Whether `a == b`, if the types decide it. Undefined and null are equal to -/// each other and to nothing else. Two values of one kind compare as `===` -/// does -pub(super) fn loose_eq(a: &Type, b: &Type) -> Option { - match (a, b) { - _ if a.is_nullish() && b.is_nullish() => Some(true), - _ if a.is_nullish() || b.is_nullish() => Some(false), - _ if a.base() == b.base() => strict_eq(a, b), - _ => None, - } -} - -/// Whether a value of type `ty` has one of the base types `bases` -pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option { - ty.base().map(|base| bases.contains(&base)) -} - -/// Whether a value of type `ty` is a fn -pub(super) fn is_function(ty: &Type) -> Option { - match ty { - Type::Obj(cell) => Some(cell.code().is_some()), - _ => Some(false), - } -} - -/// Whether a value of type `ty` is truthy, if the type decides it -pub(super) fn truthiness(ty: &Type) -> Option { - match ty { - Type::Undefined | Type::Null => Some(false), - Type::Bool(b) => *b, - Type::Str(s) => s.map(|atom| atom != EMPTY_STRING), - Type::Obj(_) => Some(true), - _ => None, - } -} - -/// The atom of what `typeof` gives for a value of type `ty` -pub(super) fn typeof_name(ty: &Type) -> Option { - Some(match ty { - Type::Undefined => 73, // "undefined" - Type::Null => 77, // "object" - Type::Bool(_) => 75, // "boolean" - Type::Int | Type::Float => 74, // "number" - Type::Str(_) => 76, // "string" - Type::Obj(cell) if cell.code().is_some() => 27, // "function" - Type::Obj(_) => 77, // "object" - _ => return None, - }) + Str(Option), + Shape(Shape), + Closure { fn_idx: usize, fields: Shape }, } diff --git a/compiler/src/libqjs/parse.rs b/compiler/src/libqjs/parse.rs index 761d405..c05efe6 100644 --- a/compiler/src/libqjs/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -168,7 +168,7 @@ pub enum JsValue { _raw: Box, }, Function(Function), - FnById(usize), + FnByIdx(usize), /// A C function, by its index in `native::NATIVES` NativeFn(usize), /// Makes its arg the prototype of a primitive kind (see `native`) @@ -810,7 +810,13 @@ impl ExportInfo { 0 => { let (bin, var_idx) = leb128(bin)?; let (bin, name) = AtomRef::parse(bin)?; - (bin, Self::Local { _var_idx: var_idx, _name: name }) + ( + bin, + Self::Local { + _var_idx: var_idx, + _name: name, + }, + ) } 1 => (bin, Self::Alias), _ => unreachable!(), diff --git a/compiler/src/main.rs b/compiler/src/main.rs index f8ec29a..8e08d2b 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -1,4 +1,3 @@ -mod emit; mod flatten; mod infer; mod libqjs; @@ -9,40 +8,7 @@ mod transform; use std::fs::File; use std::io::Read; -use flatten::flatten; -use infer::infer_types; use libqjs::compile; -use regify::regify; -use transform::transform; - -/// The stdlib, in the order that it runs in, in front of the program -const STDLIB: &[&[u8]] = &[ - include_bytes!("../../lib/stdlib/globals.js"), - include_bytes!("../../lib/stdlib/object.js"), - include_bytes!("../../lib/stdlib/string.js"), - include_bytes!("../../lib/stdlib/number.js"), - include_bytes!("../../lib/stdlib/boolean.js"), - include_bytes!("../../lib/stdlib/array.js"), - include_bytes!("../../lib/stdlib/loop.js"), - include_bytes!("../../lib/stdlib/promise.js"), - include_bytes!("../../lib/stdlib/timers.js"), - include_bytes!("../../lib/stdlib/io.js"), - include_bytes!("../../lib/stdlib/console.js"), -]; - -/// Runs after the program -const STDLIB_END: &[u8] = include_bytes!("../../lib/stdlib/end.js"); - -/// The runtime. The emitted C includes `jsrt.h`, which includes the rest -const RUNTIME: &[(&str, &[u8])] = &[ - ("jsrt.h", include_bytes!("../../lib/rt/jsrt.h")), - ("core.h", include_bytes!("../../lib/rt/core.h")), - ("str.h", include_bytes!("../../lib/rt/str.h")), - ("number.h", include_bytes!("../../lib/rt/number.h")), - ("any.h", include_bytes!("../../lib/rt/any.h")), - ("io.h", include_bytes!("../../lib/rt/io.h")), - ("uring.h", include_bytes!("../../lib/rt/uring.h")), -]; fn main() { // Load source file @@ -53,55 +19,11 @@ fn main() { let metadata = std::fs::metadata(filename).unwrap(); assert!(metadata.is_file()); - // The stdlib is compiled with the program, in front of it. The program is - // in a fn of its own, so that its names can shadow the stdlib's, as a - // script's names shadow JS's globals (node does the same with a script). - // `})` is on a line of its own, in case the program ends in a comment - let mut src = STDLIB.concat(); - src.reserve(metadata.len() as usize + STDLIB_END.len() + 32); - src.extend_from_slice(b"(function () {\n"); + let mut src = Vec::with_capacity(metadata.len() as usize); file.read_to_end(&mut src).unwrap(); - src.extend_from_slice(b"\n})();\n"); - src.extend_from_slice(STDLIB_END); // Compile to bytecode - let binary = compile(&src, filename); + let bin = compile(&src, filename); // Process it - let (flat_fns, captures, atoms) = flatten(binary); - let flat_fns = transform(flat_fns, &captures, &atoms); - let reg_fns = regify(flat_fns); - let ty_cache = infer_types(®_fns); - - let c = emit::emit(®_fns, &ty_cache, &atoms); - - // Compile the C, with the runtime, to `a.out` - let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - - for (name, src) in RUNTIME { - std::fs::write(dir.join(name), src).unwrap(); - } - - std::fs::write(dir.join("program.c"), c).unwrap(); - - let cc = cc::Build::new() - .target(env!("TARGET")) - .host(env!("TARGET")) - .opt_level(2) - .cargo_metadata(false) - .get_compiler(); - - // Each fn and global in a section of its own, so that the linker drops the - // runtime's that the program never uses - let status = cc - .to_command() - .args(["-std=c11", "-w", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections"]) - .arg(dir.join("program.c")) - .args(["-o", "a.out", "-lm"]) - .status() - .unwrap(); - - std::fs::remove_dir_all(&dir).unwrap(); - std::process::exit(status.code().unwrap_or(1)); } diff --git a/compiler/src/native.rs b/compiler/src/native.rs index f14f77b..b47165b 100644 --- a/compiler/src/native.rs +++ b/compiler/src/native.rs @@ -1,159 +1,65 @@ -//! What the stdlib (`lib/stdlib/`) can reach below JS: the globals it may -//! read, and nothing else may -//! -//! Natives are the common layer of operations on primitive values. The stdlib -//! builds the JS-visible functions and methods on top of them, and the -//! compiler's own code for operators (`+` on strings and so on) uses them -//! directly -//! -//! Proto links tell the compiler which object is the prototype of a primitive -//! kind. A field read on a primitive borrows its kind's prototype - use crate::{ - infer::types::{BaseType, Type}, + infer::Type, libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS}, }; /// A native C function callable by JS /// -/// A native is monomorphic: it takes one kind for each arg, and returns one -/// fixed type. Anything more general is written in JS, in the stdlib, on top -/// of natives +/// Natives are strictly monomorphic with a single set of expected arg types and fixed +/// return type pub struct NativeFn { pub name: &'static str, - pub args: &'static [Type], - pub ret: Type, + pub arg_types: &'static [Type], + pub ret_type: Type, } -macro_rules! native { - ($name:literal ( $($arg:ident),* ) -> $ret:ident) => { - NativeFn { - name: $name, - args: &[$(ty!($arg)),*], - ret: ty!($ret), - } +macro_rules! ty { + (Undefined) => { + Type::Undefined + }; + (Int) => { + Type::Int + }; + (Float) => { + Type::Float + }; + (String) => { + Type::Str(None) }; } -/// A native's param or return type: any value of its kind -macro_rules! ty { - (Undefined) => { Type::Undefined }; - (Int) => { Type::Int }; - (Float) => { Type::Float }; - (String) => { Type::Str(None) }; +macro_rules! natives { + ($(fn $name:ident( $($arg:ident),* ) -> $ret:ident;)*) => { + &[ + $( + NativeFn { + name: stringify!($name), + arg_types: &[$(ty!($arg)),*], + ret_type: ty!($ret), + } + ),* + ] + }; } -#[rustfmt::skip] -pub const NATIVES: &[NativeFn] = &[ - // IO - native!("__native_print_string"(String) -> Undefined), +/// All native functions accessible to JS +const NATIVES: &[NativeFn] = natives! { + // String ops + fn __native_string_concat(String, String) -> String; + fn __native_string_slice(String, Int, Int) -> String; +}; - // Async IO (see `lib/rt/io.h`). A queue gives the op's handle. A wait gives - // the handle of an op that is done, or -1 if none is left. The getters - // read that op's result, until the next wait - native!("__native_io_queue_timeout"(Float) -> Int), - native!("__native_io_queue_open"(String, Int, Int) -> Int), - native!("__native_io_queue_read"(Int, Int, Float) -> Int), - native!("__native_io_queue_write"(Int, String, Float) -> Int), - native!("__native_io_queue_close"(Int) -> Int), - native!("__native_io_queue_stat"(String) -> Int), - native!("__native_io_queue_cancel"(Int) -> Int), - native!("__native_io_status"(Int) -> Int), - native!("__native_io_result_string"(Int) -> String), - native!("__native_io_result_number"(Int) -> Float), - - // Numbers. JS has one number type, so an Int and a Float arg both pass - // (see `ret_type()`) - native!("__native_number_to_string"(Float) -> String), - native!("__native_number_to_fixed"(Float, Int) -> String), - native!("__native_string_to_number"(String) -> Float), - - // Strings. Indices and lengths count UTF-16 code units, as in JS - native!("__native_string_concat"(String, String) -> String), - native!("__native_string_slice"(String, Int, Int) -> String), - native!("__native_string_char_code_at"(String, Int) -> Int), - native!("__native_string_from_char_code"(Int) -> String), -]; - -/// A kind of value whose prototype the stdlib makes and links (see -/// `PROTO_LINKS`): a primitive kind, or arrays. A field read that its own -/// fields do not answer goes on to that prototype -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum ProtoOf { - String, - Number, - Boolean, - Array, -} - -impl ProtoOf { - pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array]; - - /// The primitive kind of every value of type `ty`, if it has a prototype - pub fn of(ty: &Type) -> Option { - match ty.base()? { - BaseType::String => Some(ProtoOf::String), - BaseType::Number => Some(ProtoOf::Number), - BaseType::Bool => Some(ProtoOf::Boolean), - _ => None, - } - } -} - -/// `__link_proto_(proto)` makes `proto` the prototype of ``. It -/// does nothing at runtime -static PROTO_LINKS: &[(&str, ProtoOf)] = &[ - ("__link_proto_string", ProtoOf::String), - ("__link_proto_number", ProtoOf::Number), - ("__link_proto_boolean", ProtoOf::Boolean), - ("__link_proto_array", ProtoOf::Array), -]; - -/// `__link_host_complete(f)` makes `f` what the host calls when an IO op -/// completes, with the op's handle (see `jsrt_complete` in `lib/rt/jsrt.h`). -/// The host owns the event loop: a flavor of the runtime runs it, and the -/// program only queues ops and handles their completions -pub const HOST_LINK: &str = "__link_host_complete"; - -/// The value of the global `name`. The only globals are the ones the stdlib -/// may read, anything else is an error pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue { + // TODO: write an Eq between Atom and &str let name = atom_name(name, atoms); - // A global property in JS, but a constant - if name == "undefined" { - return JsValue::Undefined; - } - - if let Some(idx) = NATIVES.iter().position(|n| name == n.name) { - return JsValue::NativeFn(idx); - } - - if name == HOST_LINK { - return JsValue::HostLink; - } - - match PROTO_LINKS.iter().find(|(link, _)| name == *link) { - Some(&(_, kind)) => JsValue::ProtoLink(kind), - None => panic!("unknown global `{name}`"), + match NATIVES.iter().position(|n| name == n.name) { + Some(idx) => JsValue::NativeFn(idx), + None => panic!(), } } -/// What a call of native `idx` returns, with args of these types, in source -/// order. An arg that cannot be of its param's kind makes the call throw, so -/// the call gives nothing. An arg of a sum is checked at runtime -pub fn ret_type(idx: usize, args: &[Type]) -> Type { - let native = &NATIVES[idx]; - let missing = std::iter::repeat(&Type::Undefined); - let can_pass = |(param, arg): (&Type, &Type)| arg.members().iter().any(|a| a.base() == param.base()); - - match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) { - true => native.ret.clone(), - false => Type::Unknown, - } -} - -pub fn atom_name(name: AtomRef, atoms: &[Atom]) -> String { +fn atom_name(name: AtomRef, atoms: &[Atom]) -> String { let js_string = match name { AtomRef::FileLocal(idx) => match &atoms[idx] { Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s, @@ -166,8 +72,7 @@ pub fn atom_name(name: AtomRef, atoms: &[Atom]) -> String { js_string_text(js_string) } -pub fn js_string_text(js_string: &JsString) -> String { - // TODO: Avoid string alloc +fn js_string_text(js_string: &JsString) -> String { match js_string { JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(), JsString::UTF16(b) => { diff --git a/compiler/src/regify/mod.rs b/compiler/src/regify/mod.rs index 57aa120..74096b2 100644 --- a/compiler/src/regify/mod.rs +++ b/compiler/src/regify/mod.rs @@ -1,25 +1,34 @@ mod basic_block; +mod walk; use crate::{ flatten::FlatFn, - libqjs::parse::{ImmOperand, Instr, JsValue, opcodes::OpCode}, + libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode}, }; pub use basic_block::BasicBlock; use basic_block::find_basic_blocks; +use walk::walk_block; + +#[derive(Debug, Clone, Copy)] +pub enum Operand { + Local(usize), + Arg(usize), + Reg(usize), +} #[derive(Debug, Clone)] pub struct RegInstr { - pub dsts: Vec, + pub dsts: Vec, pub op: OpCode, pub imm: ImmOperand, - pub args: Vec, + pub args: Vec, } impl RegInstr { fn nop() -> Self { Self { - dsts: vec![], // TODO: do we ever have more than one dst? + dsts: vec![], op: OpCode::Nop, imm: ImmOperand::None, args: vec![], @@ -30,10 +39,8 @@ impl RegInstr { #[derive(Debug)] pub struct RegFn { pub arg_count: usize, - pub var_count: usize, - /// Locals and args are not renamed: local `k` is value id `k`, and arg `k` - /// is value id `var_count + k`. - pub val_count: usize, + pub local_count: usize, + pub reg_count: usize, pub cpool: Vec, pub code: Vec, pub blocks: Vec, @@ -41,18 +48,12 @@ pub struct RegFn { impl RegFn { fn from_flat(f: FlatFn) -> Self { - let mut blocks = find_basic_blocks(&f.code); + let blocks = find_basic_blocks(&f.code); let mut out = vec![RegInstr::nop(); f.code.len()]; - // Reserve values for locals, then args - let mut next_val = f.var_count + f.arg_count; - // The stack each block recieves at entry from its predecessors - let mut entry_stacks: Vec>> = vec![None; blocks.len()]; - - // How many blocks lead to each block - let mut n_preds = vec![0; blocks.len()]; - blocks.iter().flat_map(|b| &b.succ).for_each(|&s| n_preds[s] += 1); + let mut entry_stacks: Vec>> = vec![None; blocks.len()]; + let mut next_reg = 0; // Walk blocks in CFG order, starting from block 0 let mut worklist = vec![]; @@ -65,348 +66,14 @@ impl RegFn { walk_block( &blocks[block_idx], &f.code, - f.var_count, &mut stack, - &mut next_val, + &mut next_reg, &mut out, ); - for i in 0..blocks[block_idx].succ.len() { - let succ_idx = blocks[block_idx].succ[i]; - - // A join's stack slots are new values, which every block that - // leads to it copies its own values into. Naming a slot after - // one predecessor's value would give that value a second - // meaning (this does the job of phi nodes in SSA) - let succ_stack = match &entry_stacks[succ_idx] { - Some(succ_stack) => succ_stack.clone(), - None => { - let succ_stack: Vec = match n_preds[succ_idx] { - 1 => stack.clone(), - _ => stack.iter().map(|_| fresh_val(&mut next_val)).collect(), - }; - - entry_stacks[succ_idx] = Some(succ_stack.clone()); - worklist.push(succ_idx); - succ_stack - } - }; - - let copies = edge_copies(&stack, &succ_stack); - - if copies.is_empty() { - continue; - } - - // The copies belong to this one edge. A block that can go - // elsewhere too gets them on a block of their own on this - // edge, so the other edge does not run them - if blocks[block_idx].succ.len() == 1 { - blocks[block_idx].postfix.extend(copies); - } else { - let at = blocks[succ_idx].start; - - blocks.push(BasicBlock { - start: at, - end: at, - succ: vec![succ_idx], - postfix: copies, - }); - - blocks[block_idx].succ[i] = blocks.len() - 1; - } - } + // TODO: handle sucessors } - // A define_field into an object that no op of this fn made (a class - // field on `this`, a static field on the class) is a write like any - // other: a put_field. So a define_field is always a write into an - // object before it goes anywhere, and its field is never absent (see - // `Cell::write()` in infer) - let mut created = vec![false; next_val]; - - for instr in &out { - if matches!(instr.op, OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8) { - created[instr.dsts[0]] = true; - } - } - - for instr in &mut out { - if instr.op == OpCode::DefineField && !created[instr.args[1]] { - instr.op = OpCode::PutField; - } - } - - Self { - arg_count: f.arg_count, - var_count: f.var_count, - val_count: next_val, - cpool: f.cpool, - code: out, - blocks, - } + todo!() } } - -/// Walk a basic block's instructions, converting each instr to register form -fn walk_block( - block: &BasicBlock, - code: &[Instr], - var_count: usize, - stack: &mut Vec, - next_val: &mut usize, - out: &mut [RegInstr], -) { - let mut fresh_val = || fresh_val(next_val); - - for instr_idx in block.start..block.end { - let instr = &code[instr_idx]; - - let mut out_instr = RegInstr { - dsts: vec![], - op: instr.op, - imm: instr.imm, - args: vec![], - }; - - let info = instr.op.info(); - let n_pop = info.n_pop as usize + extra_pops(instr); - let n_push = info.n_push as usize; - - // Handle this instr's pops - for _ in 0..n_pop { - let v = stack.pop().unwrap(); - out_instr.args.push(v); - } - - if matches!( - instr.op, - OpCode::Call | OpCode::CallMethod | OpCode::CallConstructor - ) { - out_instr.args = call_operands(instr.op, out_instr.args); - } - - // Locals and args are not renamed, so their read/writes need special handling - // Handle read of a local or arg - if let Some(var) = is_var_read(instr, var_count) { - let v = fresh_val(); - stack.push(v); - out_instr.op = OpCode::SetLoc; - out_instr.imm = ImmOperand::None; - out_instr.dsts.push(v); - out_instr.args.push(var); - out[instr_idx] = out_instr; - continue; - } - - // Handle write of a local or arg value - if let Some(var) = is_var_write(instr, var_count) { - out_instr.op = OpCode::SetLoc; - out_instr.imm = ImmOperand::None; - out_instr.dsts.push(var); - - // set_loc also leaves the value it stored on the stack. - if n_push == 1 { - stack.push(out_instr.args[0]); // args set by previos pops loop - } - - out[instr_idx] = out_instr; - continue; - } - - // Some more special cases: - // set_home_object binds [[HomeObject]], which only `super` reads, - // `super` is unsupported currently, It returns both its operands unchanged - if instr.op == OpCode::SetHomeObject { - stack.push(out_instr.args[1]); - stack.push(out_instr.args[0]); - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - - // Handle `Dup`, `Dup2`, `Swap` and `ToPropkey2`, which just push values - if instr.op == OpCode::Dup { - stack.push(out_instr.args[0]); - stack.push(out_instr.args[0]); - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - - if instr.op == OpCode::Dup2 { - let (a, b) = (out_instr.args[1], out_instr.args[0]); - stack.extend([a, b, a, b]); - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - - // to_propkey2 makes the key on top a property key, and keeps the - // object under it. A key is a number, which it keeps as it is (see - // infer), so it only gives both back - if instr.op == OpCode::ToPropkey2 { - stack.push(out_instr.args[1]); - stack.push(out_instr.args[0]); - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - - if instr.op == OpCode::Swap { - stack.push(out_instr.args[0]); - stack.push(out_instr.args[1]); - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - - // define_field leaves the object it wrote on the stack: the same value, - // not a new one - if instr.op == OpCode::DefineField { - stack.push(out_instr.args[1]); - out[instr_idx] = out_instr; - continue; - } - - // Normal case: handle this instr's pushes - for _ in 0..n_push { - let v = fresh_val(); - stack.push(v); - out_instr.dsts.push(v); - } - - out[instr_idx] = out_instr; - } -} - -/// Some instrs have extra pops not reflected by instr.op.info().n_pop -fn extra_pops(instr: &Instr) -> usize { - match instr.imm { - ImmOperand::Npop(npop) => npop as usize, - ImmOperand::NpopU16 { npop, .. } => npop as usize, - _ => 0, - } -} - -/// Put a call's popped operands in the order of the callee's params: the -/// callee (which is its env), `this`, then the args in source order -/// -/// `call_method` has `this` at the bottom of its frame, below the callee. -/// `call_constructor` has the callee at the bottom and `this` above it (see -/// transform). `call` has `this` on top, where transform pushed it -fn call_operands(op: OpCode, mut popped: Vec) -> Vec { - let (callee, this) = match op { - OpCode::CallMethod => { - let this = popped.pop().unwrap(); - (popped.pop().unwrap(), this) - } - OpCode::CallConstructor => { - let callee = popped.pop().unwrap(); - (callee, popped.pop().unwrap()) - } - _ => { - let callee = popped.pop().unwrap(); - (callee, popped.remove(0)) - } - }; - - // What remains are the args, last arg first - popped.reverse(); - - [callee, this].into_iter().chain(popped).collect() -} - -/// The value id of the local or arg that `instr` reads, if any. -pub fn is_var_read(instr: &Instr, var_count: usize) -> Option { - let arg = |k: usize| Some(var_count + k); - - match instr.op { - OpCode::GetLoc0 => Some(0), - OpCode::GetLoc1 => Some(1), - OpCode::GetLoc2 => Some(2), - OpCode::GetLoc3 => Some(3), - - OpCode::GetLoc | OpCode::GetLoc8 | OpCode::GetLocCheck => match instr.imm { - ImmOperand::Loc(k) => Some(k as usize), - _ => unreachable!(), - }, - - OpCode::GetArg0 => arg(0), - OpCode::GetArg1 => arg(1), - OpCode::GetArg2 => arg(2), - OpCode::GetArg3 => arg(3), - - OpCode::GetArg => match instr.imm { - ImmOperand::Arg(k) => arg(k as usize), - _ => unreachable!(), - }, - - _ => None, - } -} - -/// The value id of the local or arg that `instr` writes, if any. -pub fn is_var_write(instr: &Instr, var_count: usize) -> Option { - let arg = |k: usize| Some(var_count + k); - - match instr.op { - OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0), - OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1), - OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2), - OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3), - - OpCode::PutLoc - | OpCode::PutLoc8 - | OpCode::PutLocCheck - | OpCode::PutLocCheckInit - | OpCode::SetLoc - | OpCode::SetLoc8 => match instr.imm { - ImmOperand::Loc(k) => Some(k as usize), - _ => unreachable!(), - }, - - OpCode::PutArg0 | OpCode::SetArg0 => arg(0), - OpCode::PutArg1 | OpCode::SetArg1 => arg(1), - OpCode::PutArg2 | OpCode::SetArg2 => arg(2), - OpCode::PutArg3 | OpCode::SetArg3 => arg(3), - - OpCode::PutArg | OpCode::SetArg => match instr.imm { - ImmOperand::Arg(k) => arg(k as usize), - _ => unreachable!(), - }, - - _ => None, - } -} - -/// The copies that carry the values on a block's stack into its successor's -/// entry stack -fn edge_copies(stack: &[usize], succ_stack: &[usize]) -> Vec { - let overlap = stack.iter().rev().zip(succ_stack.iter().rev()); - - overlap - .filter(|(val, succ_val)| val != succ_val) - .map(|(&val, &succ_val)| RegInstr { - dsts: vec![succ_val], - op: OpCode::SetLoc, - imm: ImmOperand::None, - args: vec![val], - }) - .collect() -} - -fn fresh_val(next_val: &mut usize) -> usize { - let v = *next_val; - *next_val += 1; - v -} - -pub fn regify(flat_fns: Vec) -> Vec { - flat_fns.into_iter().map(RegFn::from_flat).collect() -} diff --git a/compiler/src/regify/walk.rs b/compiler/src/regify/walk.rs new file mode 100644 index 0000000..42ef8b9 --- /dev/null +++ b/compiler/src/regify/walk.rs @@ -0,0 +1,259 @@ +//! This module converts one basic block's instrs from stack to register form using a simulated stack +//! +//! The output reg form instrs are structured as: +//! `dsts = op(imm, args)` +//! +//! Every instr's pushes allocates new register operands which are pushed on the stack and form the +//! output instr's `dsts`, the instr's pops pop operands from the stack and form the output instr's +//! `args` +//! +//! Each instr's `op` and `imm` is preserved except for some special cases +//! +//! `set_loc` is henceforth used to move data from one operand (local, arg, reg) to another + +#![allow(non_snake_case)] + +use super::{BasicBlock, Operand, RegInstr}; +use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode}; + +/// Walk a basic block's instructions, converting each instr to register form +pub fn walk_block( + block: &BasicBlock, + code: &[Instr], + stack: &mut Vec, + next_reg: &mut usize, + out: &mut [RegInstr], +) { + let mut fresh_reg = || { + let r = *next_reg; + *next_reg += 1; + Operand::Reg(r) + }; + + for instr_idx in block.start..block.end { + let instr = &code[instr_idx]; + let out_instr = &mut out[instr_idx]; + + if try_handle_named_read(instr, stack, &mut fresh_reg, out_instr) { + continue; + } + + if try_handle_named_write(instr, stack, out_instr) { + continue; + } + + if try_handle_misc(instr, stack, out_instr) { + continue; + } + + handle_normal_case(instr, stack, &mut fresh_reg, out_instr); + } +} + +/// Handles a "named read" i.e. a read of a local or arg (and not a stack value) +/// +/// We simulate pushing the operand to stack, and replace the instr with `SetLoc`, which +/// is our catch-all for a data-move instr +fn try_handle_named_read( + instr: &Instr, + stack: &mut Vec, + mut fresh_reg: impl FnMut() -> Operand, + out: &mut RegInstr, +) -> bool { + use OpCode::*; + + let arg = match instr.op { + GetLoc0 => Operand::Local(0), + GetLoc1 => Operand::Local(1), + GetLoc2 => Operand::Local(2), + GetLoc3 => Operand::Local(3), + + GetLoc | GetLoc8 | GetLocCheck => match instr.imm { + ImmOperand::Loc(k) => Operand::Local(k as usize), + _ => unreachable!(), + }, + + GetArg0 => Operand::Arg(0), + GetArg1 => Operand::Arg(1), + GetArg2 => Operand::Arg(2), + GetArg3 => Operand::Arg(3), + + GetArg => match instr.imm { + ImmOperand::Arg(k) => Operand::Arg(k as usize), + _ => unreachable!(), + }, + + _ => return false, + }; + + let dst = fresh_reg(); + + // Converted SetLoc, reading from arg, writing to dst + *out = RegInstr { + dsts: vec![dst], + op: SetLoc, + imm: ImmOperand::None, + args: vec![arg], + }; + + // Simulate stack push + stack.push(dst); + true +} + +/// Handles a "named write" i.e. a write of a local or arg (and not a stack value) +/// +/// We simulate popping the operand off stack, and replace the instr with `SetLoc`, which +/// is our catch-all for a data-move instr +/// +/// A distinction needs to be made between `Set*` and `Put*` since `Set*` instrs leave +/// the value-to-write on the stack and `Put*` pops it off +fn try_handle_named_write(instr: &Instr, stack: &mut Vec, out: &mut RegInstr) -> bool { + use OpCode::*; + + let (dst, preserved) = match instr.op { + PutLoc0 => (Operand::Local(0), false), + PutLoc1 => (Operand::Local(1), false), + PutLoc2 => (Operand::Local(2), false), + PutLoc3 => (Operand::Local(3), false), + + PutLoc | PutLoc8 | PutLocCheck | PutLocCheckInit => match instr.imm { + ImmOperand::Loc(k) => (Operand::Local(k as usize), false), + _ => unreachable!(), + }, + + PutArg0 => (Operand::Arg(0), false), + PutArg1 => (Operand::Arg(1), false), + PutArg2 => (Operand::Arg(2), false), + PutArg3 => (Operand::Arg(3), false), + + SetLoc0 => (Operand::Local(0), true), + SetLoc1 => (Operand::Local(1), true), + SetLoc2 => (Operand::Local(2), true), + SetLoc3 => (Operand::Local(3), true), + + SetLoc | SetLoc8 => match instr.imm { + ImmOperand::Loc(k) => (Operand::Local(k as usize), true), + _ => unreachable!(), + }, + + SetArg0 => (Operand::Arg(0), true), + SetArg1 => (Operand::Arg(1), true), + SetArg2 => (Operand::Arg(2), true), + SetArg3 => (Operand::Arg(3), true), + + _ => return false, + }; + + // Simulate stack pop or peek, depending on instr + let arg = if preserved { + stack.last().unwrap().clone() + } else { + stack.pop().unwrap() + }; + + // Converted SetLoc, reading from arg, writing to dst + *out = RegInstr { + dsts: vec![dst], + op: SetLoc, + imm: ImmOperand::None, + args: vec![arg], + }; + + true +} + +fn try_handle_misc(instr: &Instr, stack: &mut Vec, out_instr: &mut RegInstr) -> bool { + use OpCode::*; + + *out_instr = match instr.op { + // Duplicate stack top, out_instr becomes `nop` + Dup => { + let top = stack[stack.len() - 1].clone(); + stack.push(top); + + RegInstr { + dsts: vec![], + op: Nop, + imm: ImmOperand::None, + args: vec![], + } + } + + // Duplicate the top 2 elems in stack, out_instr becomes `nop` + Dup2 => { + let a = stack[stack.len() - 1].clone(); + let b = stack[stack.len() - 2].clone(); + + stack.push(b); + stack.push(a); + + RegInstr { + dsts: vec![], + op: Nop, + imm: ImmOperand::None, + args: vec![], + } + } + + // Swap top 2 elems in stack, out_instr becomes `nop` + Swap => { + let a = stack.pop().unwrap(); + let b = stack.pop().unwrap(); + + stack.push(a); + stack.push(b); + + RegInstr { + dsts: vec![], + op: Nop, + imm: ImmOperand::None, + args: vec![], + } + } + + _ => return false, + }; + + true +} + +/// Handle a normal instruction with simulated stack pushes/pops +fn handle_normal_case( + instr: &Instr, + stack: &mut Vec, + mut fresh_reg: impl FnMut() -> Operand, + out_instr: &mut RegInstr, +) { + // op and imm carried as-is + *out_instr = RegInstr { + dsts: vec![], + op: instr.op, + imm: instr.imm, + args: vec![], + }; + + // Some instrs have extra pops not reflected in info.n_pop + let extra_pops = match instr.imm { + ImmOperand::Npop(npop) => npop as usize, + ImmOperand::NpopU16 { npop, .. } => npop as usize, + _ => 0, + }; + + let info = instr.op.info(); + let n_pop = info.n_pop as usize + extra_pops; + let n_push = info.n_push as usize; + + // Simulate this instr's pops + for _ in 0..n_pop { + let x = stack.pop().unwrap(); + out_instr.args.push(x); + } + + // Simulate this instr's pushes + for _ in 0..n_push { + let x = fresh_reg(); + stack.push(x); + out_instr.dsts.push(x); + } +} diff --git a/compiler/src/transform/args.rs b/compiler/src/transform/args.rs index 995a0e9..8741196 100644 --- a/compiler/src/transform/args.rs +++ b/compiler/src/transform/args.rs @@ -1,9 +1,12 @@ -use super::{FIRST_ARG, THIS_ARG, pusher}; +//! Replace all accesses to args, accounting for the added env and `this` args +//! +//! After this only `get_arg` and `set_arg` are used + +use super::{FIRST_ARG, THIS_ARG}; use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode}; -/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`, -/// and `this` with a read of its param -pub fn replace_arg_access(instr: &Instr, out: &mut Vec) -> bool { +#[allow(non_snake_case)] +pub fn try_replace_arg_access(instr: &Instr, out: &mut Vec) -> bool { use OpCode::*; let (op, arg) = match (instr.op, instr.imm) { @@ -24,6 +27,11 @@ pub fn replace_arg_access(instr: &Instr, out: &mut Vec) -> bool { _ => return false, }; - pusher(instr.pc, out)(op, ImmOperand::Arg(arg)); + out.push(Instr { + pc: instr.pc, + op, + imm: ImmOperand::Arg(arg), + }); + true } diff --git a/compiler/src/transform/bindings.rs b/compiler/src/transform/bindings.rs deleted file mode 100644 index 1273e8d..0000000 --- a/compiler/src/transform/bindings.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::collections::HashMap; - -use super::{BOX_FIELD, ENV_ARG, FIRST_ARG, pusher}; -use crate::{ - libqjs::parse::{ - AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode, - }, - regify::{is_var_read, is_var_write}, -}; - -/// Put every binding that a closure captures in a box: an object whose -/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The -/// owner and the closures read and write the box. A closure keeps the boxes -/// it captured in its own fields, one for each of its `closure_vars`: field `i` -/// holds the box of capture `i` -/// -/// The fn makes a box for each binding that its children capture, at entry. -/// A captured local holds its box in its own slot. A captured arg keeps its -/// box in a new local, and the box starts with the arg's value. A `var` starts -/// as undefined. A `let` or `const` has no value until it is written (its TDZ): -/// object; undefined; define_field ; put_loc k each captured `var` -/// object; put_loc k each captured `let` -/// object; get_arg a; define_field ; put_loc box each captured arg -/// -/// A scope that is entered again is a loop body. QuickJS closes each captured -/// binding of the scope (`close_loc`) at every way out of it, so a new box -/// there gives each iteration its own binding. `set_loc_uninitialized` only -/// marks the TDZ, which is not checked, and hoisted fns are made before it, so -/// it does not make a box -/// -/// These instrs take a pc that no jump goes to, so a jump to the fn's first -/// instr does not make them again -/// -/// Then it replaces, for a captured binding: -/// get_loc k get_loc box; get_field -/// put_loc k get_loc box; swap; put_field -/// set_loc k dup; get_loc box; swap; put_field -/// close_loc k object; get_loc k; get_field ; -/// define_field ; put_loc k -/// a way out of a loop body: a new binding, with -/// the current value -/// -/// For any binding: -/// get_var_ref i get_arg env; get_field i; get_field -/// put_var_ref i get_arg env; get_field i; swap; put_field -/// set_var_ref i dup; get_arg env; get_field i; swap; put_field -/// fclosure f fclosure f; (push_box; define_field i)* -/// -/// where `push_box` is the box of capture `i` of `f`: the local that holds it, -/// or field `j` of this fn's env for capture `j` of this fn. A binding with no -/// frame (a module binding) gets its box here, from `object` -/// -/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure -/// captures, do nothing. Gives the code and the new number of locals -pub fn replace_bindings( - code: &[Instr], - cpool: &[JsValue], - captures: &[Vec], - var_count: usize, -) -> (Vec, usize) { - use OpCode::*; - - let children = cpool.iter().filter_map(|c| match c { - JsValue::FnById(child) => Some(*child), - _ => None, - }); - - let mut locals = vec![]; - let mut args = vec![]; - - for c in children.flat_map(|child| &captures[child]) { - match c.flags.closure_kind { - ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)), - ClosureKind::Arg => args.push(c.var_idx), - _ => {} - } - } - - locals.sort_unstable(); - locals.dedup(); - args.sort_unstable(); - args.dedup(); - - // Value ids of bindings, as `is_var_read` gives them: args after every local - let new_var_count = var_count + args.len(); - let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k; - - // The local that holds the box of each captured binding, by value id - let boxes: HashMap = (locals.iter().map(|&(k, _)| (k, k))) - .chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i))) - .collect(); - - let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32)); - let value = ImmOperand::Atom(BOX_FIELD); - let none = ImmOperand::None; - let loc = |k: usize| ImmOperand::Loc(k as u16); - - let mut out = Vec::with_capacity(code.len()); - - { - let mut push = pusher(usize::MAX, &mut out); - - for &(k, lexical) in &locals { - push(Object, none); - - if !lexical { - push(Undefined, none); - push(DefineField, value); - } - - push(PutLoc, loc(k)); - } - - for (i, &k) in args.iter().enumerate() { - push(Object, none); - push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16)); - push(DefineField, value); - push(PutLoc, loc(var_count + i)); - } - } - - for instr in code { - let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v)); - let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v)); - let own_box = |k: ImmOperand| match k { - ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(), - _ => None, - }; - let mut push = pusher(instr.pc, &mut out); - - if let Some(&b) = read { - push(GetLoc, loc(b)); - push(GetField, value); - } else if let Some(&b) = write { - if instr.op.info().n_push == 1 { - push(Dup, none); - } - - push(GetLoc, loc(b)); - push(Swap, none); - push(PutField, value); - } else if let Some((i, writes, leaves)) = var_ref_access(instr) { - if leaves { - push(Dup, none); - } - - push(GetArg, ImmOperand::Arg(ENV_ARG)); - push(GetField, slot(i)); - - if writes { - push(Swap, none); - push(PutField, value); - } else { - push(GetField, value); - } - } else if instr.op == SetLocUninitialized { - push(Nop, none); - } else if instr.op == CloseLoc { - match own_box(instr.imm) { - Some(b) => { - push(Object, none); - push(GetLoc, loc(b)); - push(GetField, value); - push(DefineField, value); - push(PutLoc, loc(b)); - } - None => push(Nop, none), - } - } else if matches!(instr.op, Fclosure8 | Fclosure) { - let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else { - unreachable!() - }; - - push(Fclosure, instr.imm); - - for (i, c) in captures[child].iter().enumerate() { - match c.flags.closure_kind { - ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])), - ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])), - ClosureKind::Ref => { - push(GetArg, ImmOperand::Arg(ENV_ARG)); - push(GetField, slot(c.var_idx)); - } - _ => push(Object, none), - } - - push(DefineField, slot(i)); - } - } else { - push(instr.op, instr.imm); - } - } - - (out, new_var_count) -} - -/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes, -/// and whether a write also leaves the value on the stack -fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> { - use OpCode::*; - - let i = match instr.imm { - ImmOperand::VarRef(i) => i as usize, - _ => 0, - }; - - match instr.op { - GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)), - PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)), - SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)), - GetVarRef | GetVarRefCheck => Some((i, false, false)), - PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)), - SetVarRef => Some((i, true, true)), - _ => None, - } -} diff --git a/compiler/src/transform/declass.rs b/compiler/src/transform/class.rs similarity index 70% rename from compiler/src/transform/declass.rs rename to compiler/src/transform/class.rs index 54fe175..5253ba5 100644 --- a/compiler/src/transform/declass.rs +++ b/compiler/src/transform/class.rs @@ -1,4 +1,25 @@ -use super::{PROTOTYPE_FIELD, pusher}; +//! Convert ES6 classes to old-school prototype form +//! +//! Class form: +//! ```js +//! class Class { +//! constructor() { } +//! m1() { } +//! } +//! ``` +//! +//! Becomes prototype form: +//! ```js +//! function Class() { } +//! Class.prototype.m1 = () => { }; +//! ``` +//! +//! We first pattern-match a class declaration sequence in the input bytecode, +//! and replace it with equivalent prototype form in the emitted output +//! +//! TODO: field initializer unhandled + +use super::PROTOTYPE_FIELD; use crate::libqjs::parse::{AtomRef, ImmOperand, Instr, opcodes::OpCode}; #[derive(Debug)] @@ -15,19 +36,34 @@ pub struct ClassSequence { } impl ClassSequence { - /// fclosure ctor [C] - /// dup [C, C] - /// object [C, C, P] + /// Bytecode for prototype form looks like: + /// ``` + /// fclosure ctor + /// dup + /// object /// (fclosure method; define_field name)* - /// put_field prototype [C], C.prototype = P - /// dup [C, C] - /// get_field prototype [C, P] + /// put_field prototype + /// dup + /// get_field prototype + /// ``` /// - /// JS gives every fn a `prototype` object, but a closure here starts with - /// no fields, so the prototype is created explicitly. `define_class` left - /// [C, P] on the stack, and the code after it expects that - pub fn emit_prototype_form(&self, out: &mut Vec) { - let mut push = pusher(self.start_pc, out); + /// i.e. + /// - make `ctor` function + /// - make prototype obj + /// - assign methods to prototype + /// - assign prototype to `ctor` + /// + /// The original class sequence leaves `[class, prototype]` on top of the stack, + /// the last `get_field` ensures that stack condition is identical in this form + pub fn emit_proto_form(&self, out: &mut Vec) { + let mut push = move |op, imm| { + out.push(Instr { + pc: self.start_pc, + op, + imm, + }) + }; + let prototype = ImmOperand::Atom(PROTOTYPE_FIELD); push(OpCode::Fclosure8, self.ctor_const); @@ -45,13 +81,17 @@ impl ClassSequence { } } +/// Declass one function's bytecode +/// +/// Try to pattern-match a class decl, emitting prototype form in its place +/// and advancing the slice as we go pub fn declass(mut code: &[Instr]) -> Vec { let mut out = Vec::with_capacity(code.len()); while !code.is_empty() { match match_class_seq(code) { Ok((rest, seq)) => { - seq.emit_prototype_form(&mut out); + seq.emit_proto_form(&mut out); code = rest; } Err(rest) => { @@ -82,7 +122,7 @@ fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> { // Bit 0 of `arg` is set when the class has a superclass let is_derived = matches!(define_class.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1); - assert!(!is_derived, "derived classes are not supported"); + assert!(!is_derived, "TODO: derived classes"); let seq = ClassSequence { start_pc: pc, @@ -93,7 +133,7 @@ fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> { Ok((ins, seq)) } -// TODO: accessors are not handled +// TODO: accessor fns are not handled fn match_method_seq(ins: &[Instr]) -> PResult<'_, MethodSequence> { let (ins, fclosure) = match_instr(ins, |i| i.op == OpCode::Fclosure8)?; let (ins, define_method) = match_instr(ins, |i| i.op == OpCode::DefineMethod)?; diff --git a/compiler/src/transform/closure.rs b/compiler/src/transform/closure.rs new file mode 100644 index 0000000..4447c27 --- /dev/null +++ b/compiler/src/transform/closure.rs @@ -0,0 +1,36 @@ +//! This module lowers closures by making implicit environment captures explicit +//! +//! Replace: +//! ```js +//! let x = 0; +//! let y = true; +//! +//! function fn() { +//! f(x) +//! f(y) +//! } +//! ``` +//! +//! With: +//! ```js +//! let x = 0; +//! let y = true; +//! +//! function fn() { +//! f(fn..x); +//! f(fn..y); +//! } +//! +//! fn..x = x; // by-ref +//! fn..y = y; // by-ref +//! ``` +//! +//! i.e. captured fields are turned into fields on the function +//! +//! A special `define_field_ref` instr is introduced by `c_parse.py` to define a field on an +//! object specifically by-reference. This signals to `escape.rs` to promote those values to +//! the heap regardless if its a primitive or an object. This is required because normally +//! primitives are only passed by-value, but closure captures must always be by-reference +//! +//! In the emitted C, every underlying "plain" function is called as: +//! `fn(this, fields, arg_0, arg_1, ... arg_n)` diff --git a/compiler/src/transform/expand.rs b/compiler/src/transform/expand.rs index 777f811..7e3d03a 100644 --- a/compiler/src/transform/expand.rs +++ b/compiler/src/transform/expand.rs @@ -1,59 +1,136 @@ -use super::{ENV_ARG, PROTO_FIELD, pusher}; +//! Process instrs that are a simple sequence of other instrs +//! +//! Useless instrs are replaced with `nop` since simply dropping instrs can break jump targets + +use super::{ENV_ARG, PROTO_FIELD}; + use crate::{ - libqjs::parse::{Atom, ImmOperand, Instr, JsValue, opcodes::OpCode}, + libqjs::parse::{Atom, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode}, native, }; -/// Replace ops that are a fixed sequence of simpler ones -/// -/// A sequence is never empty: jumps find their target by its pc (see regify), -/// so every instr must keep at least one instr at its pc -pub fn replace_expansion(instr: &Instr, out: &mut Vec) -> bool { +#[allow(non_snake_case)] +pub fn try_replace_expansion(instr: &Instr, out: &mut Vec) -> bool { use OpCode::*; - let none = ImmOperand::None; - let imm = instr.imm; - - // A plain call has no `this`, so it passes `undefined`. It is pushed last, - // on top of the args, and the call pops it as one more arg (see regify) - let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))]; - let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)]; - - let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) { - (Call0, _) => &plain_call(0), - (Call1, _) => &plain_call(1), - (Call2, _) => &plain_call(2), - (Call3, _) => &plain_call(3), - (Call, ImmOperand::Npop(n)) => &plain_call(n), - (TailCall, ImmOperand::Npop(n)) => &tail_call(n), - (TailCallMethod, _) => &[(CallMethod, imm), (Return, none)], - (GetField2, _) => &[(Dup, none), (GetField, imm)], - (GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)], - (IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)], - (DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)], + let seq = match (instr.op, instr.imm) { + (Call0, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Undefined, Imm::None), + (Call, Imm::Npop(1)), + ], + (Call1, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Undefined, Imm::None), + (Call, Imm::Npop(2)), + ], + (Call2, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Undefined, Imm::None), + (Call, Imm::Npop(3)), + ], + (Call3, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Undefined, Imm::None), + (Call, Imm::Npop(4)), + ], + (Call, Imm::Npop(n)) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Undefined, Imm::None), + (Call, Imm::Npop(n + 1)), + ], + (TailCall, Imm::Npop(n)) => [ + (Nop, Imm::None), + (Undefined, Imm::None), + (Call, Imm::Npop(n + 1)), + (Return, Imm::None), + ], + (TailCallMethod, imm) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (CallMethod, imm), + (Return, Imm::None), + ], + (GetField2, imm) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Dup, Imm::None), + (GetField, imm), + ], + (GetLoc0Loc1, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (GetLoc0, Imm::None), + (GetLoc1, Imm::None), + ], + (IncLoc, imm) => [ + (Nop, Imm::None), + (GetLoc, imm), + (Inc, Imm::None), + (PutLoc, imm), + ], + (DecLoc, imm) => [ + (Nop, Imm::None), + (GetLoc, imm), + (Dec, Imm::None), + (PutLoc, imm), + ], // The added value is on the stack, the local is the left operand - (AddLoc, _) => &[(GetLoc, imm), (Swap, none), (Add, none), (PutLoc, imm)], - (SetProto, _) => &[(DefineField, ImmOperand::Atom(PROTO_FIELD))], - // The low two bits of `arg`: 0 method, 1 getter, 2 setter - (DefineMethod, ImmOperand::AtomU8 { atom, arg }) => { + (AddLoc, imm) => [ + (GetLoc, imm), + (Swap, Imm::None), + (Add, Imm::None), + (PutLoc, imm), + ], + (SetProto, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Nop, Imm::None), + (DefineField, Imm::Atom(PROTO_FIELD)), + ], + (DefineMethod, Imm::AtomU8 { atom, arg }) => { assert!(arg & 3 == 0, "getters and setters are not supported"); - &[(DefineField, ImmOperand::Atom(atom))] + [ + (Nop, Imm::None), + (Nop, Imm::None), + (Nop, Imm::None), + (DefineField, Imm::Atom(atom)), + ] } // The fn itself, which is its env - (SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))], + (SpecialObject, Imm::U8(2)) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Nop, Imm::None), + (GetArg, Imm::Arg(ENV_ARG)), + ], // [[HomeObject]], which only `super` reads. `super` is not supported - // (see set_home_object in regify) - (SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)], - // A class constructor called without `new` throws. That is not - // checked - (CheckCtor, _) => &[(Nop, none)], + (SpecialObject, Imm::U8(4)) => unimplemented!(), + // A class constructor called without `new` throws. That is not checked for the moment + (CheckCtor, _) => [ + (Nop, Imm::None), + (Nop, Imm::None), + (Nop, Imm::None), + (Nop, Imm::None), + ], // arguments, new.target, import.meta - (SpecialObject, _) => unimplemented!("special_object {imm:?}"), + (SpecialObject, _) => unimplemented!(), _ => return false, }; - let mut push = pusher(instr.pc, out); - seq.iter().for_each(|&(op, imm)| push(op, imm)); + for (op, imm) in seq { + out.push(Instr { + pc: instr.pc, + op, + imm, + }); + } + true } @@ -69,6 +146,12 @@ pub fn replace_get_var( } cpool.push(native::global(instr.imm.atom(), atoms)); - pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1)); + + out.push(Instr { + pc: instr.pc, + op: OpCode::PushConst, + imm: Imm::Const(cpool.len() as u32 - 1), + }); + true } diff --git a/compiler/src/transform/mod.rs b/compiler/src/transform/mod.rs index 4af907c..9fe53b7 100644 --- a/compiler/src/transform/mod.rs +++ b/compiler/src/transform/mod.rs @@ -1,39 +1,74 @@ +//! This module applies a set of transforms on the bytecode to lower them to a simpler +//! form more suitable for type inference and emission +//! +//! After transformation, the following instructions are eliminated: +//! - call{0, 1, 2, 3} +//! - tail_call, tail_call_method +//! - get_field2 +//! - get_loc0_loc1 +//! - inc_loc, dec_loc, add_loc +//! - set_proto +//! - define_method +//! - check_ctor +//! - special_object +//! - get_var +//! - push_this +//! - get_arg{0, 1, 2, 3} +//! - put_arg{0, 1, 2, 3} +//! - set_arg{0, 1, 2, 3} +//! - get_var_ref{0, 1, 2, 3}, get_var_ref_check +//! - put_var_ref{0, 1, 2, 3}, put_var_ref_check, put_var_ref_check_init +//! - set_var_ref{0, 1, 2, 3}, set_var_ref +//! - set_loc_uninitialized +//! - close_loc +//! - fclosure8 +//! - define_class +//! +//! +//! And the following instructions take on a different meaning: +//! - call_constructor (see new.rs) + mod args; -mod bindings; -mod declass; +mod class; +mod closure; mod expand; mod new; -use args::replace_arg_access; -use bindings::replace_bindings; -use declass::declass; -use expand::{replace_expansion, replace_get_var}; -use new::{is_new, replace_new}; - use crate::{ flatten::FlatFn, - libqjs::parse::{Atom, AtomRef, ClosureVar, ImmOperand, Instr, opcodes::OpCode}, + libqjs::parse::{Atom, AtomRef, ClosureVar}, }; -/// The field an object's prototype is kept in. In JS, `o.__proto__` is the -/// prototype of an ordinary object, so user code that reads or writes it -/// gets the right behaviour with no extra work. -pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(72); // __proto__ +use args::try_replace_arg_access; +use class::declass; +use expand::{replace_get_var, try_replace_expansion}; +use new::try_replace_new; -/// The field a constructor keeps the prototype of its instances in -pub const PROTOTYPE_FIELD: AtomRef = AtomRef::Predefined(63); // prototype +// Object Prototype Rules: +// - The `__proto__` field is the JS spec's `[[Prototype]]`, ie the link between an object and +// its parent prototype +// +// - The `prototype` field is the normal `.prototype` field used by classes, ie it holds the +// object that `new F()` will link +// +// So objects are created by: +// ``` +// Object.create = function (proto) { +// return { __proto__: proto }; +// }; +// ``` +/// `__proto__` +pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(72); +/// `prototype` +pub const PROTOTYPE_FIELD: AtomRef = AtomRef::Predefined(63); -/// Every fn takes its env and `this` as its first two params, and its declared -/// params after them. So the params of a call do not depend on how many args -/// the callee declares: a missing arg is not passed, and an extra one is not -/// read. +// Arg Rules: +// After transformation, every fn takes its capture env and `this` as the first two args +// and all the declared args after them pub const ENV_ARG: u16 = 0; pub const THIS_ARG: u16 = 1; pub const FIRST_ARG: u16 = 2; -/// The field a box keeps its binding's value in (see replace_bindings()) -pub const BOX_FIELD: AtomRef = AtomRef::Predefined(86); // - pub fn transform( flat_fns: Vec, captures: &[Vec], @@ -46,28 +81,31 @@ pub fn transform( } fn transform_one(mut f: FlatFn, captures: &[Vec], atoms: &[Atom]) -> FlatFn { + // Transform class declarations let code = declass(&f.code); - // Temp locals, after the fn's own, for the args of a `new` (see - // replace_new()). Each `new` has its own: a local has one type for its - // whole fn, so shared temps would join the args of every `new` in it - let temps = f.var_count; - let n_temps: usize = code.iter().filter_map(is_new).sum(); - let mut next_temp = temps; + // Transforms may need to allocate new local vars + let mut next_local = f.local_count; + let mut new_local = || { + let n = next_local; + next_local += 1; + n + }; + + // Apply transformations let mut out = Vec::with_capacity(code.len()); for instr in &code { - if replace_new(instr, next_temp, &mut out) { - next_temp += is_new(instr).unwrap(); + if try_replace_new(instr, &mut new_local, &mut out) { continue; } - if replace_arg_access(instr, &mut out) { + if try_replace_arg_access(instr, &mut out) { continue; } - if replace_expansion(instr, &mut out) { + if try_replace_expansion(instr, &mut out) { continue; } @@ -75,22 +113,16 @@ fn transform_one(mut f: FlatFn, captures: &[Vec], atoms: &[Atom]) -> continue; } + // TODO: replace closures/bindings + out.push(instr.clone()); } - // Last, so that it also sees the var accesses that the expansions make - let (code, var_count) = replace_bindings(&out, &f.cpool, captures, temps + n_temps); - FlatFn { name: f.name, arg_count: f.arg_count + FIRST_ARG as usize, - var_count, + local_count: f.local_count, cpool: f.cpool, code, } } - -/// Pushes instrs at the pc of the instr they replace -fn pusher(pc: usize, out: &mut Vec) -> impl FnMut(OpCode, ImmOperand) + '_ { - move |op, imm| out.push(Instr { pc, op, imm }) -} diff --git a/compiler/src/transform/new.rs b/compiler/src/transform/new.rs index 6b717dc..74fd5fc 100644 --- a/compiler/src/transform/new.rs +++ b/compiler/src/transform/new.rs @@ -1,39 +1,51 @@ -use super::{PROTO_FIELD, PROTOTYPE_FIELD, pusher}; +//! QuickJS emits `call_constructor` which is a complex instruction so we need +//! to lower it first, and simplify its meaning +//! +//! We replace: +//! call_constructor n [F, new_target, a0 .. an-1] +//! +//! With: +//! put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top +//! drop new_target, always a copy of F here +//! dup [F, F] +//! get_field prototype [F, proto] +//! object [F, proto, obj] +//! swap [F, obj, proto] +//! define_field __proto__ [F, obj], obj is { __proto__: F.prototype } +//! get_loc t0 .. get_loc t(n-1) the args back +//! call_constructor n [F, obj, a0 .. an-1], a method call of F on obj +//! +//! The temp locals are needed because of where F sits on the stack. At call_constructor, +//! the stack is [F, new_target, a0 .. an-1], with the arguments on top. To make the object, +//! transform needs F on top, so that it can read F.prototype + +use super::{PROTO_FIELD, PROTOTYPE_FIELD}; use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode}; -/// If `instr` is a `new`, the number of args it passes -pub fn is_new(instr: &Instr) -> Option { - match (instr.op, instr.imm) { - (OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize), - _ => None, - } -} - -/// Replace: -/// call_constructor n [F, new_target, a0 .. an-1] -/// -/// With: -/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top -/// drop new_target, always a copy of F here -/// dup [F, F] -/// get_field prototype [F, proto] -/// object [F, proto, obj] -/// swap [F, obj, proto] -/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype } -/// get_loc t0 .. get_loc t(n-1) the args back -/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj -/// -/// Whether `new` gives the call's result or `obj` depends on types (see infer) -pub fn replace_new(instr: &Instr, temps: usize, out: &mut Vec) -> bool { - let Some(n) = is_new(instr) else { - return false; +pub fn try_replace_new( + instr: &Instr, + mut new_local: impl FnMut() -> usize, + out: &mut Vec, +) -> bool { + let num_args = match (instr.op, instr.imm) { + (OpCode::CallConstructor, ImmOperand::Npop(n)) => n, + _ => return false, }; - let mut push = pusher(instr.pc, out); - let temp = |i: usize| ImmOperand::Loc((temps + i) as u16); + let mut push = move |op, imm| { + out.push(Instr { + pc: instr.pc, + op, + imm, + }) + }; - for i in (0..n).rev() { - push(OpCode::PutLoc, temp(i)); + let mut new_locals = vec![]; + + for _ in 0..num_args { + let n = new_local() as u16; + new_locals.push(n); + push(OpCode::PutLoc, ImmOperand::Loc(n)); } push(OpCode::Drop, ImmOperand::None); @@ -43,8 +55,8 @@ pub fn replace_new(instr: &Instr, temps: usize, out: &mut Vec) -> bool { push(OpCode::Swap, ImmOperand::None); push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD)); - for i in 0..n { - push(OpCode::GetLoc, temp(i)); + for &n in new_locals.iter().rev() { + push(OpCode::GetLoc, ImmOperand::Loc(n)); } push(OpCode::CallConstructor, instr.imm);