Mid rewrite
This commit is contained in:
@@ -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<Type> = 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<String> = (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<String> = (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
|
||||
}
|
||||
}
|
||||
@@ -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<Cell, CellNum>, Vec<Cell>, Vec<usize>) {
|
||||
let cells: Vec<Cell> = cache.cells().cloned().collect();
|
||||
let mut class: HashMap<Cell, CellNum> = cells.iter().map(|c| (c.clone(), 0)).collect();
|
||||
let mut sig = vec![];
|
||||
|
||||
loop {
|
||||
let mut ids: HashMap<Vec<u32>, CellNum> = HashMap::new();
|
||||
let next: Vec<CellNum> = 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::<HashSet<_>>().len();
|
||||
cells.iter().zip(next).for_each(|(cell, n)| *class.get_mut(cell).unwrap() = n);
|
||||
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut reps: Vec<Option<Cell>> = 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<u32> = (0..walks.len()).map(|w| if walks[w].result.is_some() { 0 } else { w as u32 + 1 }).collect();
|
||||
let callees: Vec<Vec<usize>> = (0..walks.len()).map(|w| callees_of(fns, cache, w)).collect();
|
||||
let mut count = 0;
|
||||
|
||||
loop {
|
||||
let mut ids: HashMap<Vec<u32>, u32> = HashMap::new();
|
||||
let next: Vec<u32> = (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<u32, usize> = 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<usize> {
|
||||
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<Type> = 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<Cell, CellNum>, sig: &mut Vec<u32>) {
|
||||
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<Cell, CellNum>, sig: &mut Vec<u32>) {
|
||||
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<Vec<u32>> = 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<u32>) {
|
||||
sig.extend(match atom {
|
||||
AtomRef::TaggedInt(n) => [0, n],
|
||||
AtomRef::Predefined(n) => [1, n],
|
||||
AtomRef::FileLocal(n) => [2, n as u32],
|
||||
});
|
||||
}
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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<String>,
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 <math.h>\n#include <stdlib.h>\n\n// Natives\n",
|
||||
);
|
||||
|
||||
for n in NATIVES {
|
||||
let params: Vec<String> = 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<Cell>, // a cell of each class, by number
|
||||
nums: HashMap<Cell, CellNum>, // the class of each cell
|
||||
walk_rep: Vec<usize>, // by walk: the walk whose fn it uses
|
||||
queued: Vec<bool>, // the walks that the code calls, by walk
|
||||
queue: Vec<usize>, // the walks that the code calls, which are not written yet
|
||||
helpers: HashSet<String>, // the helpers made so far, by name
|
||||
sums: RefCell<BTreeSet<Kinds>>, // 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<String> = (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
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<Kinds>) -> 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<CellNum> = 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<CellNum> {
|
||||
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::<String>())
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
+22
-5
@@ -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<JsValue>,
|
||||
pub code: Vec<Instr>,
|
||||
}
|
||||
|
||||
/// 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<FlatFn>, Vec<Vec<ClosureVar>>, Vec<Atom>) {
|
||||
let mut fns = vec![];
|
||||
let mut captures = vec![];
|
||||
@@ -27,18 +43,19 @@ fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<Closu
|
||||
flat_fns.push(FlatFn {
|
||||
name: func.name,
|
||||
arg_count: func.arg_count,
|
||||
var_count: func.var_count,
|
||||
local_count: func.var_count,
|
||||
cpool: vec![],
|
||||
code: func.byte_code,
|
||||
});
|
||||
|
||||
captures.push(func.closure_vars);
|
||||
|
||||
// Every direct child ownership is replaced by a by-idx reference
|
||||
let cpool = func
|
||||
.cpool
|
||||
.into_iter()
|
||||
.map(|c| match c {
|
||||
JsValue::Function(child) => 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<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) {
|
||||
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 <eval>
|
||||
code,
|
||||
});
|
||||
|
||||
|
||||
@@ -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<RefCell<CellInfo>>);
|
||||
|
||||
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<Code> {
|
||||
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<Field> {
|
||||
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<Cell>) -> 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<std::cmp::Ordering> {
|
||||
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<H: Hasher>(&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())
|
||||
}
|
||||
}
|
||||
+36
-550
@@ -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<RefCell<Type>>);
|
||||
|
||||
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<H: Hasher>(&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<Type>, // one for each param
|
||||
pub fn_idx: usize,
|
||||
pub arg_types: Vec<TypeCell>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InferResult {
|
||||
pub val_types: Vec<Type>,
|
||||
pub instr_types: Vec<Type>, // the type each instr gives, before it is joined into its dst
|
||||
pub ret_type: Type,
|
||||
pub reachable: Vec<bool>, // the blocks that can run
|
||||
pub local_types: Vec<TypeCell>,
|
||||
pub reg_types: Vec<TypeCell>,
|
||||
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<InferResult>, // in the current round, once it is done
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TypeCache {
|
||||
pub walks: Vec<Walk>,
|
||||
ids: HashMap<InferRequest, usize>, // 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<Cell>, // 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<Item = &Cell> {
|
||||
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<T: PartialEq>(ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
|
||||
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<T: PartialEq>(a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T>) -> Option<T> {
|
||||
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<Type> = 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<InferRequest, InferResult>,
|
||||
}
|
||||
|
||||
@@ -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<Type>),
|
||||
Array(Box<Type>),
|
||||
}
|
||||
+7
-201
@@ -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<bool>), // the value, if it is known
|
||||
Bool(Option<bool>),
|
||||
Int,
|
||||
Float,
|
||||
Str(Option<AtomRef>), // 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<BaseType> {
|
||||
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>) -> 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<Type> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
ty.base().map(|base| bases.contains(&base))
|
||||
}
|
||||
|
||||
/// Whether a value of type `ty` is a fn
|
||||
pub(super) fn is_function(ty: &Type) -> Option<bool> {
|
||||
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<bool> {
|
||||
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<u32> {
|
||||
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<AtomRef>),
|
||||
Shape(Shape),
|
||||
Closure { fn_idx: usize, fields: Shape },
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ pub enum JsValue {
|
||||
_raw: Box<JsValue>,
|
||||
},
|
||||
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!(),
|
||||
|
||||
+2
-80
@@ -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));
|
||||
}
|
||||
|
||||
+41
-136
@@ -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<ProtoOf> {
|
||||
match ty.base()? {
|
||||
BaseType::String => Some(ProtoOf::String),
|
||||
BaseType::Number => Some(ProtoOf::Number),
|
||||
BaseType::Bool => Some(ProtoOf::Boolean),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `__link_proto_<kind>(proto)` makes `proto` the prototype of `<kind>`. 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) => {
|
||||
|
||||
+21
-354
@@ -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<usize>,
|
||||
pub dsts: Vec<Operand>,
|
||||
pub op: OpCode,
|
||||
pub imm: ImmOperand,
|
||||
pub args: Vec<usize>,
|
||||
pub args: Vec<Operand>,
|
||||
}
|
||||
|
||||
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<JsValue>,
|
||||
pub code: Vec<RegInstr>,
|
||||
pub blocks: Vec<BasicBlock>,
|
||||
@@ -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<Option<Vec<usize>>> = 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<Option<Vec<Operand>>> = 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<usize> = 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<usize>,
|
||||
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<usize>) -> Vec<usize> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<RegInstr> {
|
||||
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<FlatFn>) -> Vec<RegFn> {
|
||||
flat_fns.into_iter().map(RegFn::from_flat).collect()
|
||||
}
|
||||
|
||||
@@ -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<Operand>,
|
||||
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<Operand>,
|
||||
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<Operand>, 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<Operand>, 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<Operand>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<Instr>) -> bool {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn try_replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> 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<Instr>) -> bool {
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
|
||||
out.push(Instr {
|
||||
pc: instr.pc,
|
||||
op,
|
||||
imm: ImmOperand::Arg(arg),
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -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 <var>; put_loc k each captured `var`
|
||||
/// object; put_loc k each captured `let`
|
||||
/// object; get_arg a; define_field <var>; 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 <var>
|
||||
/// put_loc k get_loc box; swap; put_field <var>
|
||||
/// set_loc k dup; get_loc box; swap; put_field <var>
|
||||
/// close_loc k object; get_loc k; get_field <var>;
|
||||
/// define_field <var>; 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 <var>
|
||||
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
|
||||
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
|
||||
/// 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<ClosureVar>],
|
||||
var_count: usize,
|
||||
) -> (Vec<Instr>, 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<usize, usize> = (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,
|
||||
}
|
||||
}
|
||||
@@ -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<Instr>) {
|
||||
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<Instr>) {
|
||||
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<Instr> {
|
||||
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)?;
|
||||
@@ -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.<var>.x);
|
||||
//! f(fn.<var>.y);
|
||||
//! }
|
||||
//!
|
||||
//! fn.<var>.x = x; // by-ref
|
||||
//! fn.<var>.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)`
|
||||
@@ -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<Instr>) -> bool {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn try_replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> 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
|
||||
}
|
||||
|
||||
@@ -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); // <var>
|
||||
|
||||
pub fn transform(
|
||||
flat_fns: Vec<FlatFn>,
|
||||
captures: &[Vec<ClosureVar>],
|
||||
@@ -46,28 +81,31 @@ pub fn transform(
|
||||
}
|
||||
|
||||
fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], 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<ClosureVar>], 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<Instr>) -> impl FnMut(OpCode, ImmOperand) + '_ {
|
||||
move |op, imm| out.push(Instr { pc, op, imm })
|
||||
}
|
||||
|
||||
@@ -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<usize> {
|
||||
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<Instr>) -> 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<Instr>,
|
||||
) -> 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<Instr>) -> 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);
|
||||
|
||||
Reference in New Issue
Block a user