//! One walk: the types that one request gives the locals, args and regs of its fn //! //! The walk goes over the reachable blocks until nothing changes. Each instr has a rule, //! which gives the type of its result from the types of its operands. The result joins into //! the instr's dsts in memory. A rule with an `Unknown` operand gives nothing: no value has //! reached the instr yet use super::{ InferRequest, TypeCache, binding_addr, memory::{Addr, InstrRef, ObjAddr}, types::{ BaseType, EMPTY_STRING, LENGTH_FIELD, Shape, Type, decide_pair, is_base, join, join_all, loose_eq, pairs, strict_eq, truthiness, }, }; use crate::{ libqjs::parse::{JsValue, opcodes::OpCode}, native, regify::{BasicBlock, Operand, RegFn, RegInstr}, transform::{FIRST_ARG, THIS_ARG}, }; /// Walk `w`. Gives what it returns, and the blocks that can run pub fn walk_fn(w: usize, fns: &[RegFn], cache: &mut TypeCache) -> (Type, Vec) { let req = cache.walks[w].req.clone(); let f = &fns[req.fn_idx]; let walker = Walker { w, f, fns, captured: f.captured_slots(), req, cache, ret: Type::Unknown, reachable: vec![false; f.blocks.len()], }; walker.run() } struct Walker<'a> { w: usize, req: InferRequest, f: &'a RegFn, fns: &'a [RegFn], captured: Vec, // the slots of `f` that a closure shares cache: &'a mut TypeCache, ret: Type, reachable: Vec, } impl Walker<'_> { fn addr(&self, slot: Operand) -> Addr { binding_addr(self.w, &self.req, &self.captured, slot) } /// The address of the objects that the instr at `idx` makes in this walk fn here(&self, idx: usize) -> ObjAddr { ObjAddr { made_at: InstrRef { fn_idx: self.req.fn_idx, instr_idx: idx, }, in_call: self.req.in_call, } } fn run(mut self) -> (Type, Vec) { // The args get their values from the request for (i, ty) in self.req.arg_types.iter().enumerate() { self.cache.memory.join(self.addr(Operand::Arg(i)), ty); } self.reachable[0] = true; // A return only changes once its block is reachable, and a read only gives more // once memory changes, so the blocks and the count of changes are enough to watch loop { let before = (self.cache.progress(), self.reachable.clone()); let f = self.f; for (i, block) in f.blocks.iter().enumerate() { if self.reachable[i] { self.walk_block(block); } } if (self.cache.progress(), &self.reachable) == (before.0, &before.1) { return (self.ret, self.reachable); } } } fn walk_block(&mut self, block: &BasicBlock) { let f = self.f; let code = f.code[block.start..block.end].iter().chain(&block.postfix); // A postfix copy has no index in the code, but its rule never needs one for (offset, instr) in code.enumerate() { self.step(instr, block.start + offset); } self.mark_successors(block); } fn step(&mut self, instr: &RegInstr, idx: usize) { use OpCode::*; // Jumps are the block's to walk (see `mark_successors()`). A `close_loc` or a // `close_field` gives a binding a new location, which has the same address (see // transform/closure.rs) if matches!( instr.op, Nop | CloseLoc | CloseField | Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8 ) { return; } let args: Vec = instr .args .iter() .map(|&slot| self.cache.memory.get(self.addr(slot)).clone()) .collect(); // The first operand of `define_field_ref` is a location, not a value, and a binding // has no value before its first write let values = if instr.op == DefineFieldRef { &args[1..] } else { &args[..] }; if values.contains(&Type::Unknown) { return; } let ty = self.rule(instr, idx, &args); if matches!(instr.op, Return | ReturnAsync | ReturnUndef) { self.ret = join(&self.ret, &ty); } for &dst in &instr.dsts { self.cache.memory.join(self.addr(dst), &ty); } } /// The type that `instr` gives, from the types of its operands `args`. An instr that /// gives nothing gives `Unknown`. Operands that an instr pops come top first (see /// regify), so a binary operator has its right side in `args[0]` fn rule(&mut self, instr: &RegInstr, idx: usize, args: &[Type]) -> Type { use OpCode::*; let memory = &self.cache.memory; 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 => self.constant(instr), Undefined => Type::Undefined, Null => Type::Null, // Arithmetic. Every number is a double, and an int is a double that is known to // fit in an int32 (see emit). So an operator that can leave that range, or give -0 // or NaN, gives a float. `+` concatenates if either side becomes a string, which a // string or an object does Add => join_all(pairs(&args[1], &args[0]).map(|(l, r)| { let text = |t: &Type| matches!(t, Type::Str(_) | Type::Ref(_) | Type::Native(_)); match text(l) || text(r) { true => Type::Str(None), false => Type::Float, } })), Sub | Mul | Div | Mod | Pow | Neg | Inc | Dec | PostInc | PostDec => Type::Float, // `+x` is x as a number: an int stays an int Plus => join_all(args[0].members().iter().map(|t| match t { Type::Int => Type::Int, _ => Type::Float, })), // The bitwise operators give an int32, but `>>>` gives a uint32 Not | And | Or | Xor | Shl | Sar => Type::Int, Shr => Type::Float, Lt | Lte | Gt | Gte => Type::Bool(None), // Tests. A known result lets the walk skip a branch that cannot run StrictEq | StrictNeq | Eq | Neq => { let eq = match instr.op { StrictEq | StrictNeq => decide_pair(&args[0], &args[1], strict_eq), _ => decide_pair(&args[0], &args[1], loose_eq), }; Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq))) } Lnot => Type::Bool(args[0].decide(truthiness).map(|t| !t)), Typeof => Type::Str(args[0].decide(|t| memory.typeof_name(t))), IsUndefined | TypeofIsUndefined => { Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Undefined]))) } IsNull => Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Null]))), IsUndefinedOrNull => { Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Undefined, BaseType::Null]))) } TypeofIsFunction => Type::Bool(args[0].decide(|t| memory.is_function(t))), // Moves. A key stays as it is: a number is an index SetLoc | SetName | ToPropkey => args[0].clone(), // Objects Object => self.make(idx, Type::Shape(Shape::Obj(vec![]))), Fclosure => { let JsValue::FnByIdx(fn_idx) = self.f.cpool[instr.imm.const_idx()] else { unreachable!() }; self.make( idx, Type::Closure { fn_idx, fields: vec![], }, ) } ArrayFrom => { let array = self.make(idx, Type::Shape(Shape::Array(Box::new(Type::Unknown)))); for value in args { self.cache.memory.write_elem(self.here(idx), value); } array } GetArrayEl => self.get_array_el(&args[1], &args[0]), PutArrayEl => self.put_array_el(&args[2], &args[1], &args[0]), DefineField | PutField => self.write_field(instr, &args[1], &args[0]), DefineFieldRef => self.share_field(instr, args), GetField => memory.read_field(&args[0], instr.imm.atom()), // A string's length is its own. Anything else reads a `length` field GetLength => join_all(args[0].members().iter().map(|t| match t { Type::Str(_) => Type::Int, t => memory.read_field(t, LENGTH_FIELD), })), // Calls Call | CallMethod | CallConstructor => self.call(instr, idx, args), // Returns Return | ReturnAsync => args[0].clone(), ReturnUndef => Type::Undefined, _ => unreachable!("{instr:?}"), } } /// The object that the instr at `idx` makes in this walk, made with `entry` the first time fn make(&mut self, idx: usize, entry: Type) -> Type { self.cache.memory.make(self.here(idx), entry); Type::Ref(self.here(idx)) } /// A constant from the fn's constant pool fn constant(&self, instr: &RegInstr) -> Type { match &self.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(idx) => Type::Native(*idx), c => unimplemented!("the constant {c:?}"), } } /// `a[i]` on an array, or `s[i]` on a string: an index is a number. Other objects need /// computed field names, which are not supported. Undefined and null throw fn get_array_el(&self, obj: &Type, index: &Type) -> Type { join_all(obj.members().iter().map(|obj| match obj { Type::Ref(obj) if self.cache.memory.is_array(*obj) => { check_index(index); self.cache.memory.read_elem(*obj) } 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"), })) } /// `a[i] = v` on an array. A string cannot be written (module code is strict), and /// undefined and null throw fn put_array_el(&mut self, obj: &Type, index: &Type, value: &Type) -> Type { for obj in obj.members() { match obj { // A write past the end leaves holes before it, which hold undefined Type::Ref(obj) if self.cache.memory.is_array(*obj) => { check_index(index); self.cache.memory.write_elem(*obj, value); self.cache.memory.write_elem(*obj, &Type::Undefined); } Type::Undefined | Type::Null | Type::Str(_) => {} obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"), } } Type::Unknown } /// `define_field` and `put_field`. The write goes into the object's field, which every /// holder of the object sees. A primitive has no fields to write, and undefined and null /// throw fn write_field(&mut self, instr: &RegInstr, obj: &Type, value: &Type) -> Type { for obj in obj.members() { if let Type::Ref(obj) = obj { let defined = instr.op == OpCode::DefineField; self.cache .memory .write_field(*obj, instr.imm.atom(), value, defined); } } Type::Unknown } /// `define_field_ref`: the field becomes a location, which regify gives as the first /// operand. That is a binding, or the reg of an object whose field of the same name is /// the location (see regify/walk.rs) fn share_field(&mut self, instr: &RegInstr, args: &[Type]) -> Type { let name = instr.imm.atom(); let location = match instr.args[0] { Operand::Reg(_) => { let holders = args[0].members().iter().map(|holder| match holder { Type::Ref(holder) => self.cache.memory.field(*holder, name).map(|f| f.addr), holder => unreachable!("a capture held by {holder:?}"), }); let mut locations: Vec = holders.flatten().collect(); locations.dedup(); assert!( locations.len() <= 1, "a capture of one of several locations" ); locations.pop() } slot => Some(self.addr(slot)), }; // A holder's field can be missing until the walk that makes it gets there if let Some(location) = location { for obj in args[1].members() { if let Type::Ref(obj) = obj { self.cache.memory.share_field(*obj, name, location); } } } Type::Unknown } /// Operands are the callee's params: the callee (its env), `this`, then the args (see /// regify). A call of each member of a sum passes that member as the env. A callee that /// is not a fn throws, so it gives nothing /// /// `new` gives what the constructor returns if that is an object, else the new object /// itself, `this` fn call(&mut self, instr: &RegInstr, idx: usize, args: &[Type]) -> Type { let in_call = Some(InstrRef { fn_idx: self.req.fn_idx, instr_idx: idx, }); let rets: Vec = (args[0].members().iter()) .map(|callee| { // A native takes the args only, without its env and `this` if let Type::Native(idx) = callee { return native::ret_type(*idx, &args[FIRST_ARG as usize..]); } let Type::Ref(obj) = callee else { return Type::Unknown; }; let Some(fn_idx) = self.cache.memory.closure_fn(*obj) else { return Type::Unknown; }; let mut call_args = args.to_vec(); call_args[0] = callee.clone(); self.cache.call(fn_idx, in_call, &call_args, self.fns) }) .collect(); let ret = join_all(rets); if instr.op != OpCode::CallConstructor { return ret; } join_all(ret.members().iter().map(|ret| match ret { Type::Ref(_) => ret.clone(), _ => args[THIS_ARG as usize].clone(), })) } /// Mark the blocks that `block` 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 fn mark_successors(&mut self, block: &BasicBlock) { use OpCode::*; let last = &self.f.code[block.end - 1]; let jumps = match last.op { IfFalse | IfFalse8 | IfTrue | IfTrue8 => { match self.cache.memory.get(self.addr(last.args[0])) { Type::Unknown => return, cond => cond .decide(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)) { self.reachable[succ] = true; } } } } /// 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"); } } }