diff --git a/.gitmodules b/.gitmodules index d1c4023..a5c351f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "third_party/quickjs"] path = third_party/quickjs url = https://github.com/quickjs-ng/quickjs.git +[submodule "third_party/stb"] + path = third_party/stb + url = https://github.com/nothings/stb.git diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index 89df6eb..b5f1012 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -4,5 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] +cc = "1.5.1" nom = "8.0.0" num_enum = "0.7.6" diff --git a/compiler/build.rs b/compiler/build.rs index 1f59433..3daeef4 100644 --- a/compiler/build.rs +++ b/compiler/build.rs @@ -1,6 +1,10 @@ const QUICKJS: &str = "../third_party/quickjs/build"; fn main() { + // The compiler compiles the C it emits for the target it is built for (see + // main.rs) + println!("cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap()); + println!("cargo:rustc-link-search=native={QUICKJS}"); println!("cargo:rustc-link-lib=static=qjs"); diff --git a/compiler/src/analyze/escape.rs b/compiler/src/analyze/escape.rs index 19e352d..45f1d58 100644 --- a/compiler/src/analyze/escape.rs +++ b/compiler/src/analyze/escape.rs @@ -12,13 +12,13 @@ pub enum Escape { Escapes, } -/// The escape of each value that `object` or `fclosure` gives. Every other +/// The escape of each value that `object`, `fclosure` or `array_from` gives. Every other /// value gives `Escapes`, which nothing reads pub fn analyze(f: &RegFn, flow: &mut Flow) -> Vec { let mut escape = vec![Escape::Escapes; f.val_count]; for (block, instr) in instrs(f) { - if matches!(instr.op, Object | Fclosure) { + if matches!(instr.op, Object | Fclosure | ArrayFrom) { let cell = instr.dsts[0]; flow.new_walk(); @@ -34,7 +34,8 @@ pub fn analyze(f: &RegFn, flow: &mut Flow) -> Vec { /// Whether the cell that `v` holds can be reached after the frame returns, or /// when its creating op runs again (`looped`: the op is in a loop). A var or a /// join slot can keep the cell until the next run, so in a loop any copy -/// escapes. A field, a callee and a return can keep it beyond the frame +/// escapes. A field, an element, a callee and a return can keep it beyond the +/// frame fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool { flow.visit(v); @@ -43,7 +44,8 @@ fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool { let out = match u.op { Call | CallMethod | CallConstructor | Return | ReturnAsync => true, - DefineField | PutField if u.operand == 0 => true, + DefineField | PutField | PutArrayEl if u.operand == 0 => true, + ArrayFrom => true, SetLoc | SetName if looped => true, SetLoc | SetName => { let dst = u.dst.unwrap() as usize; diff --git a/compiler/src/emit.rs b/compiler/src/emit.rs index d2525dd..24fc602 100644 --- a/compiler/src/emit.rs +++ b/compiler/src/emit.rs @@ -1,12 +1,16 @@ -//! Emits C: one fn for each variant infer walked, one struct for each shape -//! (see `docs/model.md`, section 4) +//! Emits C: one fn for each variant infer walked, one struct for each site //! -//! A value's C representation is a function of its type (`rep()`). An object -//! of a value shape is a struct, copied like a primitive. Any other object is -//! a pointer to its cell. Each object starts with a header that holds the site -//! that created it, and the fields that can be absent start as absent +//! A value's C representation is a function of its type (`rep()`), one for +//! each kind of type. An object of a value shape is a struct, copied like a +//! primitive. Any other object is a pointer to its cell. Each object starts +//! with a header that holds the site that created it, and the fields that can +//! be absent start as absent //! -//! The runtime (`lib/jsrt.h`) gives the primitive types, the string ops, the +//! An `Any` holds an object as a pointer. A read, a write or a call on an `Any` +//! is a switch over the sites of the objects that can be in an `Any` (see +//! `any_getter()`, `any_setter()` and `any_caller()`) +//! +//! The runtime (`lib/rt/`) gives the primitive types, the string ops, the //! `any_t` ops and the allocator use std::{ @@ -17,16 +21,16 @@ use std::{ use crate::{ analyze::{Analysis, Escape}, infer::{ - InferResult, TypeCache, - types::{Code, Field, Heap, Number, Site, SiteId, Type}, + InferResult, TypeCache, call_any_variant, + sites::{Code, CtxId, Field, LENGTH_FIELD, ObjKind, Place, SiteId, SiteTable}, + types::Type, }, libqjs::parse::{Atom, AtomRef, ImmOperand, JsValue, opcodes::OpCode}, - native::{NATIVES, Primitive, atom_name, js_string_text}, + native::{self, NATIVES, ProtoOf, atom_name, js_string_text}, regify::{RegFn, RegInstr}, transform::{FIRST_ARG, PROTO_FIELD, THIS_ARG}, }; -const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length /// A C representation #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -36,26 +40,56 @@ enum Rep { Bool, Int, Double, - /// A string, or a sentinel for null, undefined or absent + /// A string, or in a field, the sentinel for absent Str, - /// A pointer to an object of the shape of the site, or a sentinel + /// A pointer to an object of the site, or in a field, the sentinel for + /// absent Obj(SiteId), - /// An object of the value shape of the site, or a header tag for null, - /// undefined or absent + /// An object of the site, which has a value shape, or in a field, one + /// whose header is the tag for absent Val(SiteId), + /// A sum of a string and null or undefined: `NULL` is null, and + /// `JSRT_UNDEFINED` is undefined + OptStr, + /// A sum of the objects of one site and null or undefined: a pointer, with + /// the same sentinels + OptObj(SiteId), + /// A sum of the objects of several sites, which can also have null or + /// undefined: a pointer to the header, with the same sentinels + Objs, Any, } pub fn emit(fns: &[RegFn], cache: &TypeCache, analysis: &[Analysis], atoms: &[Atom]) -> String { - let heap = &cache.heap; - let shapes: Vec = heap.live_shapes().collect(); + let site_table = &cache.site_table; + let shapes: Vec = (0..site_table.sites.len() as SiteId).filter(|&s| site_table.site(s).live).collect(); - // The value shapes (see `docs/model.md`, section 5). A value cannot contain - // itself, so a shape that reaches itself through value fields is a pointer - let candidates: HashSet = shapes.iter().copied().filter(|s| heap.shape(*s).is_value()).collect(); + // The value shapes (see `docs/model.md`, section 5). A site in a sum is a + // pointer, because one C type holds every member of the sum. A value + // cannot contain itself, so a shape that reaches itself through value + // fields is a pointer + // An array is a pointer too, and so is an object that can be an element: + // a read of an element is in a sum with undefined + let elems = site_table.sites.iter().filter_map(|s| match s.kind { + ObjKind::Array { elems } => Some(elems), + _ => None, + }); + let elem_members: Vec = elems.flat_map(|e| site_table.members(&e).to_vec()).collect(); + let in_sums: HashSet = (site_table.sums().flatten().chain(&elem_members)) + .filter_map(|m| match m { + Type::Obj(site) => Some(*site), + _ => None, + }) + .collect(); + let candidates: HashSet = (shapes.iter().copied()) + .filter(|s| site_table.site(*s).is_value() && !site_table.site(*s).is_array() && !in_sums.contains(s)) + .collect(); let value_fields = |site: SiteId| { - let fields = heap.shape(site).fields.iter(); - fields.filter_map(|(_, f)| f.ty.object.map(|s| heap.find(s))).filter(|s| candidates.contains(s)) + let fields = site_table.site(site).fields.iter(); + fields.filter_map(|(_, f)| match f.ty { + Type::Obj(s) if candidates.contains(&s) => Some(s), + _ => None, + }) }; let reaches_itself = |site: SiteId| { let mut seen = HashSet::new(); @@ -80,7 +114,8 @@ pub fn emit(fns: &[RegFn], cache: &TypeCache, analysis: &[Analysis], atoms: &[At cache, analysis, atoms, - heap: heap.clone(), + site_table: site_table.clone(), + any_sites: shapes.iter().copied().filter(|&s| site_table.site(s).in_any).collect(), values, helpers: HashSet::new(), helper_protos: String::new(), @@ -101,22 +136,30 @@ pub fn emit(fns: &[RegFn], cache: &TypeCache, analysis: &[Analysis], atoms: &[At e.struct_defs(shape, &mut defined, &mut structs); } + // Which sites make fns, by header number, for the runtime's `typeof` + let fn_sites: Vec<&str> = (0..3) + .map(|_| "0") + .chain(site_table.sites.iter().map(|site| if site.code().is_some() { "1" } else { "0" })) + .collect(); + let mut statics = format!("const bool jsrt_fn_sites[] = {{ {} }};\n\n", fn_sites.join(", ")); + // A built-in fn that the program uses is one static cell - let mut statics = String::new(); let mut init = String::new(); - for site in heap.builtin(Site::Native(0))..heap.sites.len() as SiteId { - if heap.shape(site).live { + for site in 0..site_table.sites.len() as SiteId { + let place = site_table.places[site_table.site(site).place as usize].place; + + if site_table.site(site).live && !matches!(place, Place::Op { .. }) { let shape = e.struct_name(site); let _ = writeln!(statics, "static {shape} {};", e.builtin_name(site)); let _ = writeln!(init, " init_{shape}(&{}, {});", e.builtin_name(site), header(site)); } } - for kind in Primitive::ALL { + for kind in ProtoOf::ALL { let ty = cache.protos[kind as usize]; - if !ty.is_empty() { + if ty != Type::Unknown { let _ = writeln!(statics, "static {} {};", e.c_type(e.rep(&ty)), proto_global(kind)); } } @@ -135,7 +178,11 @@ pub fn emit(fns: &[RegFn], cache: &TypeCache, analysis: &[Analysis], atoms: &[At let entry = cache.find(fns.len() - 1, &[], fns); let entry_args = vec!["(undefined_t){0}"; cache.variants[entry].arg_types.len()].join(", "); - let mut out = String::from("#include \n#include \n#include \"jsrt.h\"\n\n// Natives\n"); + // The program is one file, so it also holds the runtime. The runtime goes + // first (see `lib/rt/jsrt.h`) + let mut out = String::from( + "#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n#include \n#include \n\n// Natives\n", + ); for n in NATIVES { let params: Vec = n.args.iter().map(|t| e.c_type(e.rep(t))).collect(); @@ -158,10 +205,12 @@ struct Emitter<'a> { atoms: &'a [Atom], /// The shapes, for the reads that helpers make. They are the final ones, /// so a read changes nothing in them - heap: Heap, - /// The value shapes, by the site that keeps each one + site_table: SiteTable, + /// The sites whose objects can be in an `Any`, that a creating op reached + any_sites: Vec, + /// The sites whose objects are values values: HashSet, - /// The field readers made so far (see `field_reader()`) + /// The helpers made so far, by name helpers: HashSet, helper_protos: String, helper_defs: String, @@ -170,31 +219,61 @@ struct Emitter<'a> { impl<'a> Emitter<'a> { // ─────────────────────────────────────────────── Types ─────────────────────────────────────────────── - /// The representation of a value of type `ty` (see `docs/model.md`, section 4) + /// The representation of a value of type `ty` fn rep(&self, ty: &Type) -> Rep { - self.slot_rep(ty, false) + 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(site) if self.values.contains(&site) => Rep::Val(site), + Type::Obj(site) => Rep::Obj(site), + Type::Sum(_) => self.sum_rep(ty), + Type::Any => Rep::Any, + } } - /// The representation of a field of type `ty`, which can also be absent - fn slot_rep(&self, ty: &Type, absent: bool) -> Rep { - let core = ty.non_nullish(); - let simple = !absent && !ty.has_nullish(); + /// The representation of a sum. A string or the objects of one site, with + /// null or undefined, is that pointer with sentinels. The objects of + /// several sites are a pointer to their header. Any other sum is an + /// `any_t`, which holds every kind + fn sum_rep(&self, ty: &Type) -> Rep { + let members = self.site_table.members(ty); + let others: Vec<&Type> = members.iter().filter(|m| !m.is_nullish()).collect(); - match core { - _ if !absent && (ty.is_empty() || *ty == Type::UNDEFINED) => Rep::Undefined, - _ if !absent && *ty == Type::NULL => Rep::Null, - _ if simple && core.is_only(Type::bool(None)) => Rep::Bool, - _ if simple && core == Type::number(Number::Int) => Rep::Int, - _ if simple && core == Type::number(Number::Float) => Rep::Double, - _ if core.is_only(Type::string(None)) => Rep::Str, - Type { object: Some(site), .. } if core == Type::object(site) => match self.heap.find(site) { - site if self.values.contains(&site) => Rep::Val(site), - site => Rep::Obj(site), - }, + match others[..] { + [] => Rep::Any, + [Type::Str(_)] => Rep::OptStr, + [Type::Obj(site)] => Rep::OptObj(*site), + _ if others.iter().all(|m| matches!(m, Type::Obj(_))) => Rep::Objs, _ => Rep::Any, } } + fn members(&self, ty: &Type) -> Vec { + self.site_table.members(ty).to_vec() + } + + /// The representation of a field of type `ty`. A field that can be absent + /// needs a sentinel for absent: a pointer and a struct have one, and any + /// other rep is an `any_t` + fn slot_rep(&self, ty: &Type, absent: bool) -> Rep { + match self.rep(ty) { + rep @ (Rep::Str | Rep::Obj(_) | Rep::Val(_) | Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => rep, + _ if absent => Rep::Any, + rep => rep, + } + } + + /// The representation of an element of an array of `site`: its type, or + /// undefined for a hole + fn elem_rep(&mut self, site: SiteId) -> Rep { + let elem = self.site_table.read_elem(site); + self.rep(&elem) + } + fn c_type(&self, rep: Rep) -> String { match rep { Rep::Undefined => "undefined_t".into(), @@ -202,27 +281,28 @@ impl<'a> Emitter<'a> { Rep::Bool => "bool".into(), Rep::Int => "int32_t".into(), Rep::Double => "double".into(), - Rep::Str => "string_t".into(), - Rep::Obj(site) => format!("{} *", self.struct_name(site)), + Rep::Str | Rep::OptStr => "string_t".into(), + Rep::Obj(site) | Rep::OptObj(site) => format!("{} *", self.struct_name(site)), Rep::Val(site) => self.struct_name(site), + Rep::Objs => "jsrt_header *".into(), Rep::Any => "any_t".into(), } } - /// The name of the struct of the shape of `site`, after the site that - /// keeps the shape + /// The name of the struct of the objects of `site` fn struct_name(&self, site: SiteId) -> String { - site_name(self.heap.sites[self.heap.find(site) as usize].site) + let info = self.site_table.site(site); + site_name(self.site_table.places[info.place as usize].place, info.ctx) } /// The static cell of the built-in fn of `site` fn builtin_name(&self, site: SiteId) -> String { - format!("b_{}", site_name(self.heap.sites[site as usize].site)) + format!("b_{}", self.struct_name(site)) } - /// The built-in fn of `site`, as a value of rep `rep` - fn builtin(&self, site: Site, rep: Rep) -> String { - let name = self.builtin_name(self.heap.builtin(site)); + /// The built-in fn made at `place`, as a value of rep `rep` + fn builtin(&self, place: Place, rep: Rep) -> String { + let name = self.builtin_name(self.site_table.builtin(place)); match rep { Rep::Val(_) => name, @@ -250,10 +330,10 @@ impl<'a> Emitter<'a> { return; } - let fields = self.heap.shape(site).fields.clone(); + let fields = self.site_table.site(site).fields.clone(); for ty in fields.iter().map(|(_, f)| f.ty) { - if let Rep::Val(held) = self.rep(&ty.non_nullish()) { + if let Rep::Val(held) = self.rep(&ty) { self.struct_defs(held, defined, out); } } @@ -262,8 +342,17 @@ impl<'a> Emitter<'a> { let mut members = String::from("jsrt_header h;"); let mut body = String::from(" o->h.site = site;\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 = self.site_table.site(site).is_array().then(|| self.elem_rep(site)); + + if let Some(elem) = elem { + let _ = write!(members, " int32_t length; int32_t cap; {} *items;", self.c_type(elem)); + body += " o->length = 0;\n o->cap = 0;\n o->items = NULL;\n"; + } + for (field, Field { ty, .. }) in &fields { - let absent = self.heap.can_be_absent(site, *field); + let absent = self.site_table.can_be_absent(site, *field); let rep = self.slot_rep(ty, absent); let member = self.field_name(*field); let _ = write!(members, " {} {member};", self.c_type(rep)); @@ -279,30 +368,38 @@ impl<'a> Emitter<'a> { 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), + ); + } + if self.values.contains(&site) { self.helper( format!("static {name} make_{name}(uint32_t site)"), format!(" {name} o;\n init_{name}(&o, site);\n return o;\n"), ); + // An object in an `Any` is never a value (see `SiteInfo::is_value()`), + // so a boxed copy is only read: for its kind, or to print it self.helper( format!("static any_t box_{name}({name} v)"), - format!( - " if (v.h.site == JSRT_SITE_UNDEFINED) return __any_undefined();\n\ - \x20 if (v.h.site == JSRT_SITE_NULL) return __any_null();\n\ - \x20 if (v.h.site == JSRT_SITE_ABSENT) return __any_absent();\n\ - \x20 {name} *o = jsrt_alloc(sizeof *o);\n\ - \x20 *o = v;\n\ - \x20 return __any_from_object(o);\n" - ), - ); - self.helper( - format!("static {name} unbox_{name}(any_t a)"), - format!( - " if (__any_is_object(a)) return *({name} *)__any_as_object(a);\n\ - \x20 {name} v;\n\ - \x20 v.h.site = __any_is_absent(a) ? JSRT_SITE_ABSENT : __any_is_null(a) ? JSRT_SITE_NULL : JSRT_SITE_UNDEFINED;\n\ - \x20 return v;\n" - ), + format!(" {name} *o = jsrt_alloc(sizeof *o);\n *o = v;\n return __any_from_object(o);\n"), ); } } @@ -314,46 +411,57 @@ impl<'a> Emitter<'a> { // ──────────────────────────────────────────── Conversions ──────────────────────────────────────────── - /// `e`, a value of rep `from`, as a value of rep `to` + /// `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 fn conv(&self, e: &str, from: Rep, to: Rep) -> String { if from == to { return e.into(); } 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 => "__any_undefined()".into(), - Rep::Null => "__any_null()".into(), + 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 => format!("__any_from_string({e})"), - Rep::Obj(_) => format!("__any_from_object({e})"), + Rep::Str | Rep::OptStr => format!("__any_from_string({e})"), + Rep::Obj(_) | Rep::OptObj(_) | Rep::Objs => format!("__any_from_object({e})"), Rep::Val(site) => format!("box_{}({e})", self.struct_name(site)), Rep::Any => unreachable!(), }, (Rep::Any, _) => match to { - Rep::Undefined => "(undefined_t){0}".into(), - Rep::Null => "(null_t){0}".into(), + 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!("__any_as_string({e})"), - Rep::Obj(_) => format!("(({c_to})__any_as_object({e}))"), - Rep::Val(site) => format!("unbox_{}({e})", self.struct_name(site)), + Rep::Str => format!("(({c_to})jsrt_some(__any_as_string({e})))"), + Rep::Obj(_) => format!("(({c_to})jsrt_some(__any_as_object({e})))"), + Rep::Val(site) => format!("*({} *)jsrt_some(__any_as_object({e}))", self.struct_name(site)), + Rep::OptStr => format!("__any_as_string({e})"), + Rep::OptObj(_) | Rep::Objs => format!("(({c_to})__any_as_object({e}))"), Rep::Any => unreachable!(), }, (Rep::Int, Rep::Double) => format!("(double){e}"), (Rep::Double, Rep::Int) => format!("(int32_t){e}"), - (Rep::Undefined, Rep::Str | Rep::Obj(_)) => format!("(({c_to})JSRT_UNDEFINED)"), - (Rep::Null, Rep::Str | Rep::Obj(_)) => format!("(({c_to})NULL)"), - (Rep::Undefined, Rep::Val(_)) => format!("(({c_to}){{ .h = {{ JSRT_SITE_UNDEFINED }} }})"), - (Rep::Null, Rep::Val(_)) => format!("(({c_to}){{ .h = {{ JSRT_SITE_NULL }} }})"), + (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 - _ => format!("({c_to}){{0}} /* mismatch */"), + _ => after_e(format!("({c_to}){{0}} /* mismatch */")), } } @@ -380,61 +488,138 @@ impl<'a> Emitter<'a> { } } - /// For a value `e` of type `ty`: a C bool that is true if it is an object, - /// and the object as a pointer to its shape's struct. `None` if `ty` has no - /// object. A value object must be an lvalue, so that it has an address - fn object(&self, e: &str, ty: &Type) -> Option<(String, String)> { - let site = ty.object?; - let rep = self.rep(ty); - let is_obj = || match ty.has_nullish() { - true => format!("!{}", self.nullish(e, ty)), - false => "true".into(), - }; - - Some(self.pointer(e, rep, site, is_obj)) - } - - /// For `e`, of rep `rep`, that can hold an object of the shape of `site`: - /// when it is one (from `is_obj`, unless `rep` decides it), and the object - /// as a pointer - fn pointer(&self, e: &str, rep: Rep, site: SiteId, is_obj: impl FnOnce() -> String) -> (String, String) { - let name = self.struct_name(self.heap.find(site)); - + /// `e`, an object of rep `rep`, as a pointer to its struct. A value + /// object must be an lvalue, so that it has an address + fn pointer(&self, e: &str, rep: Rep) -> String { match rep { - Rep::Any => (format!("__any_is_object({e})"), format!("(({name} *)__any_as_object({e}))")), - Rep::Val(_) => (is_obj(), format!("(&{e})")), - _ => (is_obj(), e.into()), + Rep::Val(_) => format!("(&{e})"), + _ => e.into(), } } - /// A C bool that is true if `e`, a value of type `ty`, is of primitive - /// kind `kind` - fn is_kind(&self, e: &str, ty: &Type, kind: Primitive) -> String { - let only = match kind { - Primitive::String => Type::string(None), - Primitive::Number => Type::number(Number::Int), - Primitive::Boolean => Type::bool(None), - }; - - match self.rep(ty) { - _ if ty.is_only(only) => "true".into(), - Rep::Str => format!("!{}", self.nullish(e, ty)), - _ => format!("__any_is_{}({e})", format!("{kind:?}").to_lowercase()), - } + /// `e`, an `any_t` that holds an object of `site`, as a pointer to its + /// struct + fn any_pointer(&self, e: &str, site: SiteId) -> String { + format!("(({} *)__any_as_object({e}))", self.struct_name(site)) } /// A C bool that is true if `e`, a value of type `ty`, is null or /// undefined fn nullish(&self, e: &str, ty: &Type) -> String { + let can_be = self.site_table.members(ty).iter().any(Type::is_nullish); + match self.rep(ty) { - Rep::Str | Rep::Obj(_) => format!("((void *){e} == NULL || (void *){e} == JSRT_UNDEFINED)"), - Rep::Val(_) => format!("({e}.h.site == JSRT_SITE_UNDEFINED || {e}.h.site == JSRT_SITE_NULL)"), Rep::Undefined | Rep::Null => "true".into(), Rep::Any => format!("__any_is_nullish({e})"), + 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 site in its header. `body` gives `None` for a member on which the + /// op throws. A value that no statement takes runs `otherwise` + fn dispatch( + &mut self, + e: &str, + ty: &Type, + otherwise: &str, + out: &mut String, + mut body: impl FnMut(&mut Self, Type, &str, Rep) -> Option, + ) { + let rep = self.rep(ty); + let members = self.members(ty); + + if let [member] = 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 &members { + let (value, member_rep) = self.member_value(e, rep, member); + + match (member, body(self, member, &value, member_rep)) { + (_, None) => {} + (Type::Obj(site), Some(stmt)) => objects.push((site, stmt)), + (_, Some(stmt)) => { + let _ = write!(out, "if ({}) {{ {stmt} }} else ", self.is_member(e, rep, member)); + } + } + } + + // The objects, by site. A pointer to the objects of one site needs no + // switch + let is_object = match rep { + Rep::Any => format!("__any_is_object({e})"), + _ => format!("!{}", self.nullish(e, ty)), + }; + + match (rep, &objects[..]) { + (_, []) => {} + (Rep::OptObj(_), [(_, stmt)]) => { + let _ = write!(out, "if ({is_object}) {{ {stmt} }} else "); + } + _ => { + let site = match rep { + Rep::Any => format!("((jsrt_header *)__any_as_object({e}))->site"), + _ => format!("{e}->site"), + }; + let _ = writeln!(out, "if ({is_object}) switch ({site}) {{"); + + for (site, stmt) in &objects { + let _ = writeln!(out, " case {}: {{ {stmt} }} break;", header(*site)); + } + + 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 + 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, Type::Obj(site)) => self.any_pointer(e, site), + (Rep::Any, _) => self.conv(e, Rep::Any, member_rep), + (Rep::Objs, Type::Obj(site)) => format!("(({} *){e})", self.struct_name(site)), + (_, 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 + 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, _) => format!("__any_is_{kind}({e})"), + (_, Type::Undefined) => format!("((void *){e} == JSRT_UNDEFINED)"), + (_, Type::Null) => format!("((void *){e} == NULL)"), + _ => format!("((void *){e} != NULL && (void *){e} != JSRT_UNDEFINED)"), + } + } + // ───────────────────────────────────────────── Functions ───────────────────────────────────────────── fn signature(&self, v: usize, res: &InferResult) -> String { @@ -447,7 +632,7 @@ impl<'a> Emitter<'a> { } fn emit_fn(&mut self, v: usize, res: &InferResult, out: &mut String) { - let fn_id = self.cache.variants[v].fn_id; + let (fn_id, ctx) = (self.cache.variants[v].fn_id, self.cache.variants[v].ctx); let f = &self.fns[fn_id]; let mut body = String::new(); let mut cells = String::new(); @@ -474,7 +659,7 @@ impl<'a> Emitter<'a> { }; for (i, instr) in instrs.iter().enumerate() { - self.emit_instr(fn_id, res, block.start + i, instr, &mut body, &mut cells); + self.emit_instr(fn_id, ctx, res, block.start + i, instr, &mut body, &mut cells); } // Postfix copies carry values into the next block's stack slots @@ -496,7 +681,7 @@ impl<'a> Emitter<'a> { 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, &mut cells), + Some(_) => self.emit_instr(fn_id, ctx, res, block.end - 1, last.unwrap(), &mut body, &mut cells), None if !block.succ.is_empty() => { let _ = writeln!(body, " {}", goto(0)); } @@ -513,15 +698,21 @@ impl<'a> Emitter<'a> { let _ = writeln!(out, "{cells}{body}}}\n"); } - /// A new object for the one that instr `idx` of fn `fn_id` creates into - /// value `dst`. A value object is a struct value. A cell's place comes from - /// escape analysis: the stack of the fn, or the heap - fn alloc(&self, fn_id: usize, idx: usize, dst: usize, cells: &mut String) -> String { - let site = self.heap.site_at(fn_id, idx).unwrap(); + /// The site that instr `idx` of fn `fn_id` makes, in the variant of + /// context `ctx` + fn site_at(&self, fn_id: usize, idx: usize, ctx: CtxId) -> SiteId { + let place = self.site_table.place_id(Place::Op { fn_id, instr: idx }); + self.site_table.find_site(place, ctx).unwrap() + } + + /// A new object of `site`, for the one that instr `idx` of fn `fn_id` + /// makes into value `dst`. A value object is a struct value. A cell's + /// place comes from escape analysis: the stack of the fn, or the heap + fn alloc(&self, site: SiteId, fn_id: usize, idx: usize, dst: usize, cells: &mut String) -> String { let shape = self.struct_name(site); let id = header(site); - if self.values.contains(&self.heap.find(site)) { + if self.values.contains(&site) { return format!("make_{shape}({id})"); } @@ -539,6 +730,7 @@ impl<'a> Emitter<'a> { fn emit_instr( &mut self, fn_id: usize, + ctx: CtxId, res: &InferResult, idx: usize, instr: &RegInstr, @@ -555,8 +747,10 @@ impl<'a> Emitter<'a> { let ty = res.instr_types[idx]; - // No value reaches the instr, so it cannot run - if ty.is_empty() { + // 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; } @@ -568,16 +762,16 @@ impl<'a> Emitter<'a> { 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 - let known = match (ty.bool.flatten(), ty.string.flatten()) { - (Some(b), _) if ty.is_only(Type::bool(None)) => Some(b.to_string()), - (_, Some(atom)) if ty.is_only(Type::string(None)) => { - Some(self.string_lit(&atom_name(atom, self.atoms))) - } + // 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_call = matches!(instr.op, Call | CallMethod | CallConstructor); - if let Some(lit) = known.filter(|_| !is_ret) { + if let Some(lit) = known.filter(|_| !is_ret && !is_call) { return self.assign(out, &dst, &lit, rep, dst_rep); } @@ -608,15 +802,30 @@ impl<'a> Emitter<'a> { 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) => self.builtin(Site::Native(*id), rep), - JsValue::ProtoLink(kind) => self.builtin(Site::ProtoLink(*kind), rep), + JsValue::NativeFn(id) => self.builtin(Place::Native(*id), rep), + JsValue::ProtoLink(kind) => self.builtin(Place::ProtoLink(*kind), rep), _ => format!("({}){{0}}", self.c_type(rep)), }, Undefined | Null => format!("({}){{0}}", self.c_type(rep)), - Object | Fclosure | Fclosure8 => self.alloc(fn_id, idx, instr.dsts[0], cells), + Object | Fclosure | Fclosure8 => self.alloc(self.site_at(fn_id, idx, ctx), fn_id, idx, instr.dsts[0], cells), + // The elements are the operands, last first + ArrayFrom => { + let site = self.site_at(fn_id, idx, ctx); + let (name, elem) = (self.struct_name(site), self.elem_rep(site)); + let array = self.alloc(site, fn_id, idx, instr.dsts[0], cells); + let _ = writeln!(out, " {dst} = {array};\n set_length_{name}({dst}, {});", 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 ty.is_only(Type::string(None)) => { + 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})") } @@ -682,36 +891,17 @@ impl<'a> Emitter<'a> { Eq | StrictEq => "==", _ => "!=", }; let eq = matches!(instr.op, Eq | Neq | StrictEq | StrictNeq); - let strict = matches!(instr.op, StrictEq | StrictNeq); - let nullish = vt(0).has_nullish() || vt(1).has_nullish(); match (vr(1), vr(0)) { _ if numbers => format!("{l} {op} {r}"), (Rep::Bool, Rep::Bool) => format!("{l} {op} {r}"), - (Rep::Str, Rep::Str) if !nullish => match instr.op { + (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"), }, - // A value object against undefined or null: its header tag - (Rep::Val(_), tag @ (Rep::Undefined | Rep::Null)) - | (tag @ (Rep::Undefined | Rep::Null), Rep::Val(_)) - if eq => - { - let (val, val_ty) = if matches!(vr(1), Rep::Val(_)) { (&l, vt(1)) } else { (&r, vt(0)) }; - let tag = if tag == Rep::Null { "JSRT_SITE_NULL" } else { "JSRT_SITE_UNDEFINED" }; - let test = match strict { - true => format!("({val}.h.site == {tag})"), - false => self.nullish(val, &val_ty), - }; - - if op == "==" { test } else { format!("!{test}") } - } - // Two objects are equal if they are one cell. A sentinel is - // one cell too, but `==` also makes null and undefined equal - (Rep::Obj(_), Rep::Obj(_)) if eq && (strict || !nullish) => { - format!("(void *){l} {op} (void *){r}") - } + // Two objects are equal if they are one cell + (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", @@ -737,7 +927,7 @@ impl<'a> Emitter<'a> { } // ── Movers ── - SetLoc | SetName => v(0), + SetLoc | SetName | ToPropkey => v(0), // ── Objects ── DefineField | PutField => return self.write_field(res, instr, out), @@ -768,21 +958,30 @@ impl<'a> Emitter<'a> { /// and null throw fn write_field(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) { let (value, obj) = (instr.args[0], instr.args[1]); - let (value_ty, obj_ty) = (res.val_types[value], res.val_types[obj]); + let (value_rep, obj_ty) = (self.rep(&res.val_types[value]), res.val_types[obj]); let field = instr.imm.atom(); + let value = format!("v{value}"); - if let Some((is_obj, ptr)) = self.object(&format!("v{obj}"), &obj_ty) { - let site = self.heap.find(obj_ty.object.unwrap()); - let field_ty = self.heap.shape(site).field(field).unwrap().ty; - let slot = self.slot_rep(&field_ty, self.heap.can_be_absent(site, field)); - let target = format!("{ptr}->{}", self.field_name(field)); - let value = self.conv(&format!("v{value}"), self.rep(&value_ty), slot); - let _ = writeln!(out, " {}{target} = {value};", guard(&is_obj)); - } - - if obj_ty.has_nullish() { - let _ = writeln!(out, " if ({}) jsrt_type_error();", self.nullish(&format!("v{obj}"), &obj_ty)); - } + self.dispatch(&format!("v{obj}"), &obj_ty, "jsrt_type_error();", out, |e, member, obj, obj_rep| { + match member { + Type::Obj(site) if field == LENGTH_FIELD && e.site_table.site(site).is_array() => { + let ptr = e.pointer(obj, obj_rep); + Some(format!("set_length_{}({ptr}, {});", e.struct_name(site), e.conv(&value, value_rep, Rep::Int))) + } + Type::Obj(site) => { + let field_ty = e.site_table.site(site).field(field).unwrap().ty; + let slot = e.slot_rep(&field_ty, e.site_table.can_be_absent(site, field)); + let ptr = e.pointer(obj, obj_rep); + Some(format!("{ptr}->{} = {};", e.field_name(field), e.conv(&value, value_rep, slot))) + } + Type::Any => { + let setter = e.any_setter(field); + Some(format!("{setter}({obj}, {});", e.conv(&value, value_rep, Rep::Any))) + } + Type::Undefined | Type::Null => None, + _ => Some(";".into()), + } + }); } /// `dst = obj.field`: on an object, the read through its prototype chain; @@ -791,7 +990,6 @@ impl<'a> Emitter<'a> { fn read_field(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) { let (obj, dst) = (instr.args[0], instr.dsts[0]); let obj_ty = res.val_types[obj]; - let e = format!("v{obj}"); let dst_rep = self.rep(&res.val_types[dst]); let field = match instr.op { @@ -799,74 +997,131 @@ impl<'a> Emitter<'a> { _ => instr.imm.atom(), }; - // One case for each kind the object can be: when, the value, its rep - let mut cases: Vec<(String, String, Rep)> = vec![]; + self.dispatch(&format!("v{obj}"), &obj_ty, "jsrt_type_error();", out, |e, member, obj, obj_rep| { + let (read, rep) = match member { + Type::Obj(site) => e.read_on(site, field, &e.pointer(obj, obj_rep)), + Type::Any => (format!("{}({obj})", e.any_getter(field)), Rep::Any), + _ => e.read_primitive(&member, obj, field)?, + }; - // An own field that is never absent is read in place. Any other read - // goes through the prototype chain - if let Some((is_obj, ptr)) = self.object(&e, &obj_ty) { - let site = self.heap.find(obj_ty.object.unwrap()); - - match self.heap.shape(site).field(field).map(|f| &f.ty) { - Some(ty) if !self.heap.can_be_absent(site, field) => { - cases.push((is_obj, format!("{ptr}->{}", self.field_name(field)), self.rep(ty))); - } - _ => { - let (reader, rep) = self.field_reader(site, field); - cases.push((is_obj, format!("{reader}({ptr})"), rep)); - } - } - } - - for kind in Primitive::ALL.into_iter().filter(|kind| kind.is_in(&obj_ty)) { - let is_kind = self.is_kind(&e, &obj_ty, kind); - - if kind == Primitive::String && field == LENGTH_FIELD { - let s = self.conv(&e, self.rep(&obj_ty), Rep::Str); - cases.push((is_kind, format!("jsrt_string_length({s})"), Rep::Int)); - continue; - } - - let proto = self.cache.protos[kind as usize]; - - if let Some((_, ptr)) = self.object(&proto_global(kind), &proto) { - let (reader, rep) = self.field_reader(proto.object.unwrap(), field); - cases.push((is_kind, format!("{reader}({ptr})"), rep)); - } - } - - let _ = write!(out, " "); - - for (cond, value, rep) in &cases { - let value = self.conv(value, *rep, dst_rep); - - if cond == "true" { - let _ = writeln!(out, "v{dst} = {value};"); - return; - } - - let _ = write!(out, "if ({cond}) v{dst} = {value}; else "); - } - - let _ = writeln!(out, "jsrt_type_error();"); + Some(format!("v{dst} = {};", e.conv(&read, rep, dst_rep))) + }); } - /// The helper that reads `field` on an object of the shape of `site`, 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 + /// `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 + 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); + let dst = (instr.dsts[0], self.rep(&res.val_types[instr.dsts[0]])); + let value = (instr.args[0], self.rep(&res.val_types[instr.args[0]])); + + self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, rep| { + let site = match member { + Type::Obj(site) => site, + Type::Str(_) if !put => { + let c = format!("jsrt_string_at({obj}, {index})"); + return Some(format!("v{} = {};", dst.0, e.conv(&c, Rep::OptStr, dst.1))); + } + _ => return None, + }; + + let (name, elem, ptr) = (e.struct_name(site), e.elem_rep(site), e.pointer(obj, rep)); + + Some(match put { + true => format!("set_elem_{name}({ptr}, {index}, {});", e.conv(&format!("v{}", value.0), value.1, elem)), + false => format!("v{} = {};", dst.0, e.conv(&format!("elem_{name}({ptr}, {index})"), elem, dst.1)), + }) + }); + } + + /// A read of `field` on the object of `site` at `ptr`, and its rep. An own + /// field that is never absent is read in place. Any other read goes + /// through the prototype chain + fn read_on(&mut self, site: SiteId, field: AtomRef, ptr: &str) -> (String, Rep) { + if field == LENGTH_FIELD && self.site_table.site(site).is_array() { + return (format!("{ptr}->length"), Rep::Int); + } + + match self.site_table.site(site).field(field) { + Some(f) if !self.site_table.can_be_absent(site, field) => { + (format!("{ptr}->{}", self.field_name(field)), self.rep(&f.ty)) + } + _ => { + let (reader, rep) = self.field_reader(site, 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 + 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 + fn read_proto(&mut self, kind: ProtoOf, field: AtomRef) -> (String, Rep) { + let (proto, global) = (self.cache.protos[kind as usize], proto_global(kind)); + + match proto { + Type::Unknown => ("(undefined_t){0}".into(), Rep::Undefined), + Type::Obj(site) => self.read_on(site, field, &self.pointer(&global, self.rep(&proto))), + _ => self.proto_reader(kind, field), + } + } + + /// The helper that reads `field` on the prototype of primitive kind + /// `kind`, where the prototype's type is a sum or `Any`, and the rep it + /// gives. It reads on each member of the type + fn proto_reader(&mut self, kind: ProtoOf, field: AtomRef) -> (String, Rep) { + let name = format!("get_{}_{}", proto_global(kind), self.field_name(field)); + let (proto, global) = (self.cache.protos[kind as usize], proto_global(kind)); + let read = self.site_table.read_value(&proto, field, &self.cache.protos); + let ret = self.rep(&read); + + if !self.helpers.insert(name.clone()) { + return (format!("{name}()"), ret); + } + + let mut body = String::new(); + + self.dispatch(&global, &proto, "jsrt_type_error();", &mut body, |e, member, proto, proto_rep| { + let (read, rep) = match member { + Type::Obj(site) => e.read_on(site, field, &e.pointer(proto, proto_rep)), + Type::Any => (format!("{}({proto})", e.any_getter(field)), Rep::Any), + _ => return None, + }; + + 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 `site`, 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 fn field_reader(&mut self, site: SiteId, field: AtomRef) -> (String, Rep) { - let site = self.heap.find(site); let name = format!("get_{}_{}", self.struct_name(site), self.field_name(field)); - let read = self.heap.read(site, field); + let read = self.site_table.read(site, field, &self.cache.protos); let ret = self.rep(&read); if !self.helpers.insert(name.clone()) { return (name, ret); } - let shape = self.heap.shape(site).clone(); - let absent = self.heap.can_be_absent(site, field); + let shape = self.site_table.site(site).clone(); + let absent = self.site_table.can_be_absent(site, field); let mut body = String::new(); if let Some(ty) = shape.field(field).map(|f| &f.ty) { @@ -884,113 +1139,128 @@ impl<'a> Emitter<'a> { } } - if absent { - let proto_ty = shape.field(PROTO_FIELD).map_or(Type::EMPTY, |f| f.ty); - let slot = self.slot_rep(&proto_ty, self.heap.can_be_absent(site, PROTO_FIELD)); + // An array's prototype is the one that the stdlib links for arrays + if absent && shape.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 = shape.field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty); + let proto_absent = self.site_table.can_be_absent(site, 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(); - if let Some(proto) = proto_ty.object { - let (reader, rep) = self.field_reader(proto, field); - let (is_obj, ptr) = self.pointer(&member, slot, proto, || match slot { - Rep::Val(_) => format!("({member}.h.site > JSRT_SITE_ABSENT)"), - _ => format!("((void *){member} != NULL && (void *){member} != JSRT_UNDEFINED && {})", self.present(&member, slot)), - }); - let value = self.conv(&format!("{reader}({ptr})"), rep, ret); - let _ = writeln!(body, " if ({is_obj}) return {value};"); + // On a prototype that is not an object, the chain ends + self.dispatch(&member, &proto_ty, "{}", &mut chain, |e, member, proto, proto_rep| match member { + Type::Obj(site) => { + let (reader, rep) = e.field_reader(site, field); + let read = format!("{reader}({})", e.pointer(proto, proto_rep)); + Some(format!("return {};", e.conv(&read, rep, ret))) + } + Type::Any => { + let read = format!("{}({proto})", e.any_getter(field)); + Some(format!("if (__any_is_object({proto})) return {};", e.conv(&read, Rep::Any, ret))) + } + _ => None, + }); + + match (proto_ty, proto_absent) { + (Type::Unknown, _) => {} + (_, true) => { + let _ = writeln!(body, " if ({}) {{\n{chain} }}", self.present(&member, slot)); + } + (_, false) => body += &chain, } - let undefined = self.conv("(undefined_t){0}", Rep::Undefined, ret); - let _ = writeln!(body, " return {undefined};"); + // The end of the chain. If the read cannot give undefined, the chain + // always finds the field before it + match read == Type::Any || self.members(&read).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 shape.field(field).map(|f| &f.ty).is_none() && !absent { + if shape.field(field).is_none() && !absent { let _ = writeln!(body, " abort();"); } let sig = format!("static {} {name}({} *o)", self.c_type(ret), self.struct_name(site)); - let _ = writeln!(self.helper_protos, "{sig};"); - let _ = writeln!(self.helper_defs, "{sig} {{\n{body}}}\n"); + self.helper(sig, body); (name, ret) } - /// A call: one case for each fn the callee can be, chosen by the site in - /// the callee's header. A callee that is not a fn throws + /// A call. The callee's site tells which code runs, and the callee is its + /// own env. On an `Any`, a helper finds the code at runtime. A callee that + /// is not a fn throws fn call(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) { let arg_types: Vec = instr.args.iter().map(|&a| res.val_types[a]).collect(); - let dst = instr.dsts[0]; - let dst_rep = self.rep(&res.val_types[dst]); - let callee_ty = arg_types[0]; - - let Some((is_obj, callee)) = self.object(&format!("v{}", instr.args[0]), &callee_ty) else { - let _ = writeln!(out, " jsrt_type_error();"); - return; - }; - - // The sites in the callee's shape whose objects are fns, and their code - let callee_site = self.heap.find(callee_ty.object.unwrap()); - let cases: Vec<(SiteId, Code)> = (0..self.heap.sites.len() as SiteId) - .filter(|&site| self.heap.find(site) == callee_site) - .filter_map(|site| self.heap.sites[site as usize].code.map(|code| (site, code))) + 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]])); - let codes = &self.heap.shape(callee_site).code.clone(); - let switch = codes.len() > 1; - - if is_obj != "true" { - let _ = writeln!(out, " if (!({is_obj})) jsrt_type_error();"); - } - - if switch { - let _ = writeln!(out, " switch ({callee}->h.site) {{"); - } - - for &code in codes { - if switch { - for (site, _) in cases.iter().filter(|(_, c)| *c == code) { - let _ = writeln!(out, " case {}:", header(*site)); + self.dispatch(&operands[0].0.clone(), &arg_types[0], "jsrt_type_error();", out, |e, member, callee, rep| { + let (call, ret) = match member { + Type::Obj(site) => { + let mut arg_types = arg_types.clone(); + let mut operands = operands.clone(); + arg_types[0] = member; + operands[0] = (callee.into(), rep); + e.call_code(e.site_table.site(site).code()?, &arg_types, &operands) } - } - - let (call, ret_ty) = self.call_code(code, &arg_types, instr); - - // `new` gives what the constructor returns if that is an object, - // else the new object itself, `this` (see infer) - let non_objects = Type { object: None, ..ret_ty }; - - if instr.op == OpCode::CallConstructor && !non_objects.is_empty() { - let this = instr.args[THIS_ARG as usize]; - let this_value = self.conv(&format!("v{this}"), self.rep(&arg_types[THIS_ARG as usize]), dst_rep); - - match self.object("r", &ret_ty) { - None => { - let _ = writeln!(out, " {call}; v{dst} = {this_value};"); - } - Some((is_obj, _)) => { - let ret_obj = self.conv("r", self.rep(&ret_ty), dst_rep); - let c_ret = self.c_type(self.rep(&ret_ty)); - let _ = writeln!(out, " {{ {c_ret} r = {call}; v{dst} = {is_obj} ? {ret_obj} : {this_value}; }}"); - } + Type::Any => { + let caller = e.any_caller(instr.args.len() - FIRST_ARG as usize); + let args: Vec = operands.iter().map(|(o, rep)| e.conv(o, *rep, Rep::Any)).collect(); + (format!("{caller}({})", args.join(", ")), Type::Any) } - } else { - let value = self.conv(&call, self.rep(&ret_ty), dst_rep); - let _ = writeln!(out, " v{dst} = {value};"); - } + _ => return None, + }; - if switch { - let _ = writeln!(out, " break;"); - } - } + Some(e.call_result(instr.op, &call, &ret, &operands[THIS_ARG as usize], dst)) + }); + } - if switch { - let _ = writeln!(out, " default: jsrt_type_error();\n }}"); + /// 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) + 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 = self.members(ret); + let objects = members.iter().filter(|m| matches!(m, Type::Obj(_) | Type::Any)).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() || *ret == Type::Any => { + let is_object = match ret_rep { + Rep::Any => "__any_is_object(r)".into(), + _ => 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 the call's operands, and the type it returns - fn call_code(&mut self, code: Code, arg_types: &[Type], instr: &RegInstr) -> (String, Type) { - let operand = |i: usize| format!("v{}", instr.args[i]); + /// 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 + fn call_code(&mut self, code: Code, 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 = |i: usize, to: Rep| match operands.get(i) { + Some((e, rep)) => self.conv(e, *rep, to), + None => self.conv("(undefined_t){0}", Rep::Undefined, to), + }; match code { Code::Fn(fn_id) => { @@ -999,23 +1269,21 @@ impl<'a> Emitter<'a> { let ret = variant.result.as_ref().unwrap().ret_type; let args: Vec = (variant.arg_types.iter().enumerate()) - .map(|(i, param)| match instr.args.get(i) { - Some(_) => self.conv(&operand(i), self.rep(&arg_types[i]), self.rep(param)), - None => "(undefined_t){0}".into(), - }) + .map(|(i, param)| operand(i, self.rep(param))) .collect(); (format!("{}({})", self.fn_name(v), 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..], &self.site_table) == Type::Unknown => { + ("jsrt_type_error()".into(), Type::Unknown) + } Code::Native(id) => { let native = &NATIVES[id]; let args: Vec = (native.args.iter().enumerate()) - .map(|(i, param)| { - let a = FIRST_ARG as usize + i; - self.conv(&operand(a), self.rep(&arg_types[a]), self.rep(param)) - }) + .map(|(i, param)| operand(FIRST_ARG as usize + i, self.rep(param))) .collect(); (format!("{}({})", native.name, args.join(", ")), native.ret) @@ -1023,14 +1291,113 @@ impl<'a> Emitter<'a> { // Keeps the prototype where field reads on primitives find it Code::ProtoLink(kind) => { - let arg = FIRST_ARG as usize; - let proto = self.cache.protos[kind as usize]; - let value = self.conv(&operand(arg), self.rep(&arg_types[arg]), self.rep(&proto)); - (format!("({} = {value}, (undefined_t){{0}})", proto_global(kind)), Type::UNDEFINED) + let value = operand(FIRST_ARG as usize, self.rep(&self.cache.protos[kind as usize])); + (format!("({} = {value}, (undefined_t){{0}})", proto_global(kind)), Type::Undefined) } } } + // ───────────────────────────────────────────── Any ─────────────────────────────────────────────────── + + /// The helper that reads `field` on an `Any`: on an object, by its site, + /// and on a primitive, by its kind. Undefined and null throw + fn any_getter(&mut self, field: AtomRef) -> String { + let name = format!("get_any_{}", self.field_name(field)); + + if !self.helpers.insert(name.clone()) { + return name; + } + + let mut body = String::from(" if (__any_is_object(o)) switch (((jsrt_header *)__any_as_object(o))->site) {\n"); + + for site in self.any_sites.clone() { + let (value, rep) = self.read_on(site, field, &self.any_pointer("o", site)); + let _ = writeln!(body, " case {}: return {};", header(site), self.conv(&value, rep, Rep::Any)); + } + + body += " default: abort();\n }\n"; + + for kind in ProtoOf::PRIMITIVES { + let ty = match kind { + ProtoOf::String => Type::Str(None), + ProtoOf::Number => Type::Float, + _ => Type::Bool(None), + }; + let e = self.conv("o", Rep::Any, self.rep(&ty)); + let (value, rep) = self.read_primitive(&ty, &e, field).unwrap(); + let is_kind = format!("{kind:?}").to_lowercase(); + let _ = writeln!(body, " if (__any_is_{is_kind}(o)) return {};", self.conv(&value, rep, Rep::Any)); + } + + body += " jsrt_type_error();\n"; + self.helper(format!("static any_t {name}(any_t o)"), body); + name + } + + /// The helper that writes `field` on an `Any`. Every object in an `Any` + /// has the field (see the write rule in infer). A primitive has no fields + /// to write, and undefined and null throw + fn any_setter(&mut self, field: AtomRef) -> String { + let name = format!("set_any_{}", self.field_name(field)); + + if !self.helpers.insert(name.clone()) { + return name; + } + + let mut body = String::from(" if (__any_is_object(o)) switch (((jsrt_header *)__any_as_object(o))->site) {\n"); + + for site in self.any_sites.clone() { + let Some(f) = self.site_table.site(site).field(field) else { + continue; + }; + + let slot = self.slot_rep(&f.ty, self.site_table.can_be_absent(site, field)); + let target = format!("{}->{}", self.any_pointer("o", site), self.field_name(field)); + let _ = writeln!(body, " case {}: {target} = {}; break;", header(site), self.conv("v", Rep::Any, slot)); + } + + body += " default: abort();\n }\n else if (__any_is_nullish(o)) jsrt_type_error();\n"; + self.helper(format!("static void {name}(any_t o, any_t v)"), body); + name + } + + /// The helper that calls an `Any` with `n` args: the fn object's site + /// tells which code runs, with the variant that infer walked for a call on + /// `Any` (see `call_any_variant()`). A callee that is not a fn throws + fn any_caller(&mut self, n: usize) -> String { + let name = format!("call_any_{n}"); + + if !self.helpers.insert(name.clone()) { + return name; + } + + let params: Vec = (0..FIRST_ARG as usize + n).map(|i| format!("any_t a{i}")).collect(); + let mut body = String::from( + " if (!__any_is_object(a0)) jsrt_type_error();\n switch (((jsrt_header *)__any_as_object(a0))->site) {\n", + ); + + for site in self.any_sites.clone() { + let Some(code) = self.site_table.site(site).code() else { + continue; + }; + + let arg_types = call_any_variant(site, n); + let operands: Vec<(String, Rep)> = (0..arg_types.len()) + .map(|i| match i { + 0 => (self.any_pointer("a0", site), self.rep(&Type::Obj(site))), + i => (format!("a{i}"), Rep::Any), + }) + .collect(); + + let (call, ret) = self.call_code(code, &arg_types, &operands); + let _ = writeln!(body, " case {}: return {};", header(site), self.conv(&call, self.rep(&ret), Rep::Any)); + } + + body += " default: jsrt_type_error();\n }\n"; + self.helper(format!("static any_t {name}({})", params.join(", ")), body); + name + } + // ───────────────────────────────────────────── Helpers ─────────────────────────────────────────────── /// A C bool that is JS's truthiness of `e`, a value of type `ty` @@ -1039,10 +1406,10 @@ impl<'a> Emitter<'a> { Rep::Bool => e.into(), Rep::Int => format!("({e} != 0)"), Rep::Double => format!("({e} != 0 && {e} == {e})"), - Rep::Str if ty.has_nullish() => format!("(!{} && jsrt_string_length({e}) != 0)", self.nullish(e, ty)), Rep::Str => format!("(jsrt_string_length({e}) != 0)"), - Rep::Obj(_) | Rep::Val(_) if ty.has_nullish() => format!("!{}", self.nullish(e, ty)), + Rep::OptStr => format!("(!{} && jsrt_string_length({e}) != 0)", self.nullish(e, ty)), Rep::Obj(_) | Rep::Val(_) => "true".into(), + Rep::OptObj(_) | Rep::Objs => format!("!{}", self.nullish(e, ty)), Rep::Undefined | Rep::Null => "false".into(), Rep::Any => format!("__any_truthy({e})"), } @@ -1051,7 +1418,7 @@ impl<'a> Emitter<'a> { /// `e`, a value of type `ty`, as a string, like the spec's ToString fn to_string(&self, e: &str, ty: &Type) -> String { match self.rep(ty) { - Rep::Str if !ty.has_nullish() => e.into(), + 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")); @@ -1084,33 +1451,26 @@ fn c_str(s: &str) -> String { lit + "\"" } -/// `if (cond) ` before a statement, or nothing if `cond` is always true -fn guard(cond: &str) -> String { - match cond { - "true" => String::new(), - cond => format!("if ({cond}) "), - } -} - fn is_num(rep: Rep) -> bool { matches!(rep, Rep::Int | Rep::Double) } -fn site_name(site: Site) -> String { - match site { - Site::Op { fn_id, instr } => format!("S{fn_id}_{instr}"), - Site::Native(id) => format!("SN{id}"), - Site::ProtoLink(kind) => format!("SL{kind:?}"), +/// The name of a site: its place, and for a place in code, its context +fn site_name(place: Place, ctx: CtxId) -> String { + match place { + Place::Op { fn_id, instr } => format!("S{fn_id}_{instr}_{ctx}"), + Place::Native(id) => format!("SN{id}"), + Place::ProtoLink(kind) => format!("SL{kind:?}"), } } /// The number in the header of an object made at `site`, after the tags -/// `JSRT_SITE_*` (see `lib/jsrt.h`) +/// `JSRT_SITE_*` (see `lib/rt/core.h`) fn header(site: SiteId) -> u32 { site + 3 } -fn proto_global(kind: Primitive) -> String { +fn proto_global(kind: ProtoOf) -> String { format!("proto_{kind:?}").to_lowercase() } diff --git a/compiler/src/infer/mod.rs b/compiler/src/infer/mod.rs index 8f351fa..828f396 100644 --- a/compiler/src/infer/mod.rs +++ b/compiler/src/infer/mod.rs @@ -1,28 +1,30 @@ //! Infers a type for every value (see `docs/model.md`, section 3) +pub mod sites; pub mod types; use std::collections::HashMap; use crate::{ libqjs::parse::{AtomRef, JsValue, opcodes::OpCode}, - native, + native::{self, ProtoOf}, regify::RegFn, transform::{FIRST_ARG, THIS_ARG}, }; -use types::{Code, Heap, Number, Site, Type}; +use sites::{Code, CtxId, LENGTH_FIELD, Place, Protos, SiteId, SiteTable}; +use types::{BaseType, EMPTY_STRING, Type, is_function, loose_eq, strict_eq, test, truthiness, typeof_name}; -const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length -const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // "" /// One walk of a fn with one tuple of arg types (see `docs/model.md`, -/// section 3.4) +/// section 3.5) #[derive(Debug)] pub struct Variant { pub fn_id: usize, /// One for each of the fn's params (see `TypeCache::variant()`) pub arg_types: Vec, + /// The context of the sites that it makes (see `SiteTable::context()`) + pub ctx: CtxId, /// The walk in the current round, once it is done pub result: Option, /// What it returned in the rounds so far, which is what a call of it gives @@ -48,9 +50,9 @@ pub struct TypeCache { by_args: Vec, usize>>, /// Scratch for a call's arg types key: Vec, - pub heap: Heap, + pub site_table: SiteTable, /// The prototype of each primitive kind, as the stdlib links it, by kind - pub protos: [Type; 3], + pub protos: Protos, /// Goes up when a variant's return or a proto changes (see `infer_types()`) changes: usize, } @@ -62,8 +64,9 @@ impl TypeCache { /// arg's kind crosses a call, not a known value, else every literal passed /// would make a new variant pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize { + let site_table = &mut self.site_table; self.key.clear(); - self.key.extend(params(fn_id, arg_types, fns)); + self.key.extend(params(fn_id, arg_types, fns).map(|ty| site_table.widen(&ty))); if let Some(&v) = self.by_args[fn_id].get(&self.key[..]) { return v; @@ -74,8 +77,9 @@ impl TypeCache { self.variants.push(Variant { fn_id, arg_types: self.key.clone(), + ctx: self.site_table.context(&self.key), result: None, - ret: Type::EMPTY, + ret: Type::Unknown, walking: false, }); @@ -84,10 +88,47 @@ impl TypeCache { /// The variant for a call that infer walked pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize { - let key: Vec = params(fn_id, arg_types, fns).collect(); + let site_table = &self.site_table; + let key: Vec = params(fn_id, arg_types, fns).map(|ty| site_table.widened(&ty)).collect(); self.by_args[fn_id][&key] } + /// The join of `f` over the members of `ty` (see `SiteTable::members()`). + /// The lists of sums only grow, so a member stays where it is while `f` + /// adds sums + fn map(&mut self, ty: Type, mut f: impl FnMut(&mut Self, Type) -> Type) -> Type { + let mut out = Type::Unknown; + + for i in 0..self.site_table.members(&ty).len() { + let member = self.site_table.members(&ty)[i]; + let ty = f(self, member); + out = self.site_table.join(&out, &ty); + } + + out + } + + /// The join of `f` over each pair of members of `a` and `b` + fn map2(&mut self, a: Type, b: Type, mut f: impl FnMut(&mut Self, Type, Type) -> Type) -> Type { + self.map(a, |cache, a| cache.map(b, |cache, b| f(cache, a, b))) + } + + /// What `f` gives for every member of `ty`, if all members give the same + fn agree(&self, ty: &Type, f: impl Fn(&Type) -> Option) -> Option { + let mut members = self.site_table.members(ty).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(&self, a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option) -> Option { + let members = |t| self.site_table.members(t).iter(); + let mut pairs = members(a).flat_map(|x| members(b).map(move |y| (x, y))).map(|(x, y)| f(x, y)); + let first = pairs.next()??; + pairs.all(|t| t.as_ref() == Some(&first)).then_some(first) + } + /// Infer a call of `fn_id`, walking it unless an identical call was /// already walked in this round. Gives what it returns fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type { @@ -104,7 +145,7 @@ impl TypeCache { let ret = res.ret_type; let last = self.variants[v].ret; - let joined = self.heap.join(&last, &ret); + let joined = self.site_table.join(&last, &ret); self.changes += (joined != last) as usize; let variant = &mut self.variants[v]; @@ -116,9 +157,73 @@ impl TypeCache { } /// The type of each of fn `fn_id`'s params, for a call with args of these -/// types (see `TypeCache::variant()`) +/// types, before known values are dropped (see `TypeCache::variant()`) fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator + 'a { - (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::UNDEFINED, Type::widen)) + (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).copied().unwrap_or(Type::Undefined)) +} + +/// A call of `code` 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]) -> Type { + let args = &arg_types[FIRST_ARG as usize..]; + + match code { + Code::Fn(fn_id) => cache.call(fn_id, arg_types, fns), + Code::Native(id) => native::ret_type(id, args, &cache.site_table), + Code::ProtoLink(kind) => { + let linked = cache.protos[kind as usize]; + let joined = cache.site_table.join(&linked, args.first().unwrap_or(&Type::Undefined)); + cache.changes += (joined != linked) as usize; + cache.protos[kind as usize] = joined; + Type::Undefined + } + } +} + +/// A call of a callee of type `Any`, with operands of these types: a call of +/// any fn object in an `Any`, with every operand an `Any`, except that the +/// callee is its own env. That is the variant that emit's dispatch on `Any` +/// calls (see `call_any_variant()`). Gives an `Any` +fn call_any(cache: &mut TypeCache, arg_types: &mut [Type], fns: &[RegFn]) -> Type { + for ty in arg_types.iter_mut() { + *ty = cache.site_table.to_any(ty); + } + + for site in 0..cache.site_table.sites.len() as SiteId { + let Some(code) = cache.site_table.site(site).code().filter(|_| cache.site_table.site(site).in_any) else { + continue; + }; + + arg_types[0] = Type::Obj(site); + let ret = call_code(cache, code, arg_types, fns); + cache.site_table.to_any(&ret); + } + + Type::Any +} + +/// The site that `place` makes in context `ctx`, made if it is new +fn site_at(cache: &mut TypeCache, place: Place, ctx: CtxId) -> SiteId { + let place = cache.site_table.place_id(place); + cache.site_table.site_at(place, ctx) +} + +/// An array index must be a number. Any other key is a computed field name, +/// which is not supported +fn check_index(cache: &TypeCache, index: &Type) { + for member in cache.site_table.members(index) { + if !matches!(member, Type::Int | Type::Float) { + unimplemented!("`a[k]` with a key of type {member:?}: computed field names"); + } + } +} + +/// The operand types of the variant that a call on `Any`, with `n_args` +/// args, runs for a fn object of `site` +pub fn call_any_variant(site: SiteId, n_args: usize) -> Vec { + let mut arg_types = vec![Type::Any; FIRST_ARG as usize + n_args]; + arg_types[0] = Type::Obj(site); + arg_types } /// Walk every variant from the entry, in rounds, until a round changes @@ -130,20 +235,20 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache { variants: vec![], by_args: fns.iter().map(|_| HashMap::new()).collect(), key: vec![], - heap: Heap::new(fns), - protos: [Type::EMPTY; 3], + site_table: SiteTable::new(fns), + protos: [Type::Unknown; ProtoOf::ALL.len()], changes: 0, }; loop { - let before = (cache.heap.version, cache.changes); + let before = (cache.site_table.version, cache.changes); cache.variants.iter_mut().for_each(|v| v.result = None); // The synthetic program entry is appended last by `flatten`. It is // called by the host with no args cache.call(fns.len() - 1, &[], fns); - if before == (cache.heap.version, cache.changes) { + if before == (cache.site_table.version, cache.changes) { return cache; } } @@ -152,11 +257,11 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache { fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult { use OpCode::*; - let fn_id = cache.variants[variant].fn_id; + let (fn_id, ctx) = (cache.variants[variant].fn_id, cache.variants[variant].ctx); let f = ®_fns[fn_id]; - let mut val_types = vec![Type::EMPTY; f.val_count]; - let mut instr_types = vec![Type::EMPTY; f.code.len()]; - let mut ret_type = Type::EMPTY; + 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) // Init them with the arg types passed in @@ -170,13 +275,13 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes // Scratch for calls, kept across instrs let mut arg_types: Vec = vec![]; - let mut codes: Vec = vec![]; // Walk instrs till nothing changes. A return only changes `ret_type` once - // its block is reachable, and a field read only gives more once the heap - // changes, so the values, the blocks and the heap are enough to watch + // its block is reachable, and a field read only gives more once the site + // table changes, so the values, the blocks and the site table are enough + // to watch loop { - let version = cache.heap.version; + let version = cache.site_table.version; let mut changed = false; for (block_idx, block) in f.blocks.iter().enumerate() { @@ -190,9 +295,9 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes for (offset, instr) in code.enumerate() { let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef); - // A rule with an empty operand gives an empty result: no value - // has reached the instr yet - let waits = instr.args.iter().any(|&v| val_types[v].is_empty()); + // A rule with an `Unknown` operand gives `Unknown`: no value has + // reached the instr yet + let waits = instr.args.iter().any(|&v| val_types[v] == Type::Unknown); if (instr.dsts.is_empty() && !is_ret) || waits { continue; @@ -205,162 +310,231 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes let ty = match instr.op { // ────────────────────────────────────────── Literals ─────────────────────────────────────────────────────── PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 - | PushI8 | PushI16 | PushI32 => Type::number(Number::Int), + | PushI8 | PushI16 | PushI32 => Type::Int, - PushTrue => Type::bool(Some(true)), - PushFalse => Type::bool(Some(false)), - PushAtomValue => Type::string(Some(instr.imm.atom())), - PushEmptyString => Type::string(Some(EMPTY_STRING)), + 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::number(Number::Int), - JsValue::Float(_) => Type::number(Number::Float), - JsValue::String(_) => Type::string(None), - JsValue::BoolTrue => Type::bool(Some(true)), - JsValue::BoolFalse => Type::bool(Some(false)), - JsValue::Undefined => Type::UNDEFINED, - JsValue::NativeFn(id) => cache.heap.create(cache.heap.builtin(Site::Native(id))), - JsValue::ProtoLink(kind) => cache.heap.create(cache.heap.builtin(Site::ProtoLink(kind))), + 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) => cache.site_table.create(cache.site_table.builtin(Place::Native(id))), + JsValue::ProtoLink(kind) => cache.site_table.create(cache.site_table.builtin(Place::ProtoLink(kind))), _ => unreachable!(), }, - Undefined => Type::UNDEFINED, - Null => Type::NULL, + Undefined => Type::Undefined, + Null => Type::Null, // ──────────────────────────────────────── Arithmetic ─────────────────────────────────────────────────────── // `+` concatenates if either side becomes a string, which - // only a string or an object can - Add => { - let (l, r) = (arg(1), arg(0)); - let is_string = |t: Type| t.is_only(Type::string(None)); - let can_be_string = |t: Type| t.string.is_some() || t.object.is_some(); - - if is_number(l) && is_number(r) { - Type::number(l.number.max(r.number).unwrap()) - } else if is_string(l) || is_string(r) { - Type::string(None) - } else if can_be_string(l) || can_be_string(r) { - Type { string: Some(None), ..Type::number(Number::Float) } - } else { - Type::number(Number::Float) - } - } + // a string or an object does. Else it adds numbers + Add => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) { + (Type::Any, _) | (_, Type::Any) => Type::Any, + (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 => match (arg(1), arg(0)) { - (l, r) if is_int(l) && is_int(r) => Type::number(Number::Int), - _ => Type::number(Number::Float), - }, - Neg | Plus | Inc | Dec | PostInc | PostDec => match arg(0) { - t if is_int(t) => Type::number(Number::Int), - _ => Type::number(Number::Float), - }, - Div => Type::number(Number::Float), + Sub | Mul | Mod | Pow => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) { + (Type::Int, Type::Int) => Type::Int, + _ => Type::Float, + }), + Neg | Plus | Inc | Dec | PostInc | PostDec => cache.map(arg(0), |_, t| match t { + Type::Int => Type::Int, + _ => Type::Float, + }), + Div => Type::Float, // Bitwise NOT gives an int32 - Not => Type::number(Number::Int), + Not => Type::Int, - Lt | Lte | Gt | Gte => Type::bool(None), + 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) - // An identity test of two objects (see `docs/model.md`, section 5) - StrictEq | StrictNeq | Eq | Neq if arg(0).object.is_some() && arg(1).object.is_some() => { - for obj in [arg(0).object, arg(1).object].into_iter().flatten() { - cache.heap.mark(obj, |shape| &mut shape.compared); + StrictEq | StrictNeq | Eq | Neq => { + let (a, b) = (arg(0), arg(1)); + + // An identity test of an object with what can be an + // object (see `docs/model.md`, section 5) + let members = |t: &Type| cache.site_table.members(t).to_vec(); + let (a_members, b_members) = (members(&a), members(&b)); + let can_be_object = |ms: &[Type]| ms.iter().any(|t| matches!(t, Type::Obj(_) | Type::Any)); + + if can_be_object(&a_members) && can_be_object(&b_members) { + for t in a_members.iter().chain(&b_members) { + if let Type::Obj(site) = t { + cache.site_table.mark(*site, |shape| &mut shape.compared); + } + } } - Type::bool(None) + let eq = match instr.op { + StrictEq | StrictNeq => cache.agree2(&a, &b, strict_eq), + _ => cache.agree2(&a, &b, loose_eq), + }; + + Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq))) } - StrictEq => Type::bool(strict_eq(&arg(0), &arg(1))), - StrictNeq => Type::bool(strict_eq(&arg(0), &arg(1)).map(|eq| !eq)), - Eq => Type::bool(loose_eq(&arg(0), &arg(1))), - Neq => Type::bool(loose_eq(&arg(0), &arg(1)).map(|eq| !eq)), - Lnot => Type::bool(truthiness(&arg(0)).map(|t| !t)), - Typeof => typeof_of(&arg(0), &cache.heap), - IsUndefined | TypeofIsUndefined => Type::bool(test(&arg(0), Type::UNDEFINED)), - IsNull => Type::bool(test(&arg(0), Type::NULL)), - IsUndefinedOrNull => Type::bool(test(&arg(0), Type { null: true, ..Type::UNDEFINED })), - TypeofIsFunction => Type::bool(is_function(&arg(0), &cache.heap)), + Lnot => Type::Bool(cache.agree(&arg(0), truthiness).map(|t| !t)), + Typeof => match cache.agree(&arg(0), |t| typeof_name(t, &cache.site_table)) { + Some(name) => Type::Str(Some(AtomRef::Predefined(name))), + None => Type::Str(None), + }, + IsUndefined | TypeofIsUndefined => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined]))), + IsNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Null]))), + IsUndefinedOrNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))), + TypeofIsFunction => Type::Bool(cache.agree(&arg(0), |t| is_function(t, &cache.site_table))), // ─────────────────────────────────────── Value Movers ────────────────────────────────────────────────────── SetLoc | SetName => arg(0), // ───────────────────────────────────────── Objects ───────────────────────────────────────────────────────── - Object | Fclosure | Fclosure8 => cache.heap.create(cache.heap.site_at(fn_id, idx).unwrap()), - // The write goes into the object's shape, which every value - // that holds the object sees. A primitive has no fields to - // write, and undefined and null throw - DefineField | PutField => { - if let Some(obj) = arg(1).object { - cache.heap.write(obj, instr.imm.atom(), &arg(0)); + Object | Fclosure | Fclosure8 => { + let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx); + cache.site_table.create(site) + } + // The elements are its operands, last first + ArrayFrom => { + let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx); - if !cache.heap.is_creation_write(fn_id, idx) { - cache.heap.mark(obj, |shape| &mut shape.written); - } + for &v in &instr.args { + cache.site_table.write_elem(site, &val_types[v]); } - cache.heap.canon(&arg(1)) + cache.site_table.create(site) } - GetField => cache.heap.read_value(&arg(0), instr.imm.atom(), &cache.protos), + // `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); + + cache.map(arg(1), |cache, obj| match obj { + Type::Obj(site) if cache.site_table.site(site).is_array() => { + check_index(cache, &index); + cache.site_table.read_elem(site) + } + Type::Str(_) => { + check_index(cache, &index); + cache.site_table.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)); + + cache.map(arg(2), |cache, obj| { + match obj { + Type::Obj(site) if cache.site_table.site(site).is_array() => { + check_index(cache, &index); + cache.site_table.write_elem(site, &value); + cache.site_table.mark(site, |s| &mut s.written); + } + Type::Undefined | Type::Null | Type::Str(_) => {} + obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"), + } + + Type::Unknown + }); + + arg(2) + } + // A key stays as it is: a number is an index + ToPropkey => arg(0), + // The write goes into the fields of the object's site, which + // every value that holds an object of the site sees. A write + // on `Any` can reach any object in an `Any`. A primitive has + // no fields to write, and undefined and null throw + DefineField | PutField => { + let (field, value) = (instr.imm.atom(), arg(0)); + // A define_field builds the object before it goes + // anywhere (see regify) + let creation = instr.op == DefineField; + + cache.map(arg(1), |cache, obj| { + match obj { + Type::Obj(site) => { + cache.site_table.write(site, field, &value); + + if !creation { + cache.site_table.mark(site, |shape| &mut shape.written); + } + } + Type::Any => { + for site in 0..cache.site_table.sites.len() as SiteId { + if cache.site_table.site(site).in_any { + cache.site_table.write(site, field, &value); + cache.site_table.mark(site, |shape| &mut shape.written); + } + } + } + _ => {} + } + + Type::Unknown + }); + + arg(1) + } + GetField => cache.site_table.read_value(&arg(0), instr.imm.atom(), &cache.protos), // A string's length is its own. Anything else reads a // `length` field - GetLength => { - let others = Type { string: None, ..arg(0) }; - let read = cache.heap.read_value(&others, LENGTH_FIELD, &cache.protos); - let own = Type { number: arg(0).string.map(|_| Number::Int), ..Type::EMPTY }; - cache.heap.join(&read, &own) - } + GetLength => cache.map(arg(0), |cache, obj| match obj { + Type::Str(_) => Type::Int, + obj => cache.site_table.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). The call runs one of - // the callee's fns. A callee that is not a fn throws, so it - // gives nothing + // `this`, then the args (see regify). A callee that is not + // a fn throws, so it gives nothing + // The callee's env is the fn object itself, so a call of + // each member of a sum passes that member as the env Call | CallMethod | CallConstructor => { - arg_types.clear(); - arg_types.extend(instr.args.iter().map(|&v| val_types[v])); - let args = &arg_types[FIRST_ARG as usize..]; + let this = arg(THIS_ARG as usize); - codes.clear(); + let ret = cache.map(arg(0), |cache, callee| { + arg_types.clear(); + arg_types.extend(instr.args.iter().map(|&v| val_types[v])); + arg_types[0] = callee; - if let Some(callee) = arg_types[0].object { - codes.extend(&cache.heap.shape(callee).code); - } - - let mut ret = Type::EMPTY; - - for &code in &codes { - let ty = match code { - Code::Fn(fn_id) => cache.call(fn_id, &arg_types, reg_fns), - Code::Native(id) => native::ret_type(id, args), - Code::ProtoLink(kind) => { - let linked = cache.protos[kind as usize]; - let joined = cache.heap.join(&linked, &args[0]); - cache.changes += (joined != linked) as usize; - cache.protos[kind as usize] = joined; - Type::UNDEFINED - } - }; - - ret = cache.heap.join(&ret, &ty); - } + match callee { + Type::Obj(site) => match cache.site_table.site(site).code() { + Some(code) => call_code(cache, code, &arg_types, reg_fns), + None => Type::Unknown, + }, + Type::Any => call_any(cache, &mut arg_types, reg_fns), + _ => Type::Unknown, + } + }); // `new` gives what the constructor returns if that is an // object, else the new object itself, `this` - let others = Type { object: None, ..ret }; - match instr.op { - CallConstructor if !others.is_empty() => { - let this = val_types[instr.args[THIS_ARG as usize]]; - cache.heap.join(&Type { object: ret.object, ..Type::EMPTY }, &this) - } + CallConstructor => cache.map(ret, |cache, ret| match ret { + Type::Obj(_) => ret, + Type::Any => cache.site_table.join(&ret, &this), + _ => this, + }), _ => ret, } } // ──────────────────────────────────────── Returns ────────────────────────────────────────────────────────── Return | ReturnAsync => arg(0), - ReturnUndef => Type::UNDEFINED, + ReturnUndef => Type::Undefined, _ => unreachable!("{instr:?}"), }; @@ -368,16 +542,16 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes // 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] = cache.heap.join(&instr_types[idx], &ty); + instr_types[idx] = cache.site_table.join(&instr_types[idx], &ty); } if is_ret { - ret_type = cache.heap.join(&ret_type, &ty); + ret_type = cache.site_table.join(&ret_type, &ty); continue; } for &dst in &instr.dsts { - let joined = cache.heap.join(&val_types[dst], &ty); + let joined = cache.site_table.join(&val_types[dst], &ty); changed |= joined != val_types[dst]; val_types[dst] = joined; } @@ -390,8 +564,8 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes 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]] { - cond if cond.is_empty() => continue, - cond => truthiness(&cond).map(|t| t == matches!(last.op, IfTrue | IfTrue8)), + Type::Unknown => continue, + cond => cache.agree(&cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)), } } _ => None, @@ -406,17 +580,11 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes } } // block - if !changed && cache.heap.version == version { + if !changed && cache.site_table.version == version { break; } } // loop - // A type names the site its shape was kept under at the time: make it the - // one now - for ty in val_types.iter_mut().chain(&mut instr_types).chain([&mut ret_type]) { - *ty = cache.heap.canon(ty); - } - InferResult { val_types, instr_types, @@ -425,115 +593,3 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes } } -/// Whether every value of type `ty` is a number -fn is_number(ty: Type) -> bool { - ty.is_only(Type::number(Number::Int)) -} - -/// Whether every value of type `ty` is an int32 -fn is_int(ty: Type) -> bool { - ty == Type::number(Number::Int) -} - -/// The type, if every value of it is one value that its kind and refinement -/// tell -fn single(ty: &Type) -> Option { - let one = match ty.kinds() { - [true, false, false, false, false, false] | [false, true, false, false, false, false] => true, - [false, false, true, false, false, false] => ty.bool.flatten().is_some(), - [false, false, false, false, true, false] => ty.string.flatten().is_some(), - _ => false, - }; - - one.then_some(*ty) -} - -/// Whether `a === b`, if the types decide it -fn strict_eq(a: &Type, b: &Type) -> Option { - let disjoint = !a.kinds().iter().zip(b.kinds()).any(|(x, y)| *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 -fn loose_eq(a: &Type, b: &Type) -> Option { - let nullish = |t: &Type| t.has_nullish() && t.non_nullish().is_empty(); - - match (a, b) { - _ if nullish(a) && nullish(b) => Some(true), - _ if (nullish(a) && !b.has_nullish()) || (!a.has_nullish() && nullish(b)) => Some(false), - _ if a.kinds() == b.kinds() && single(a).is_some() => strict_eq(a, b), - _ => None, - } -} - -/// Whether every value of type `ty` is of a kind in `kinds`: true if every -/// value is, false if none is, `None` if the type does not decide it -fn test(ty: &Type, kinds: Type) -> Option { - let [inside, outside] = [true, false].map(|want| { - ty.kinds().iter().zip(kinds.kinds()).any(|(has, of)| *has && of == want) - }); - - match (inside, outside) { - (true, false) => Some(true), - (false, true) => Some(false), - _ => None, - } -} - -/// Whether every value of type `ty` is a fn, if the type decides it -fn is_function(ty: &Type, heap: &Heap) -> Option { - let shape = ty.object.map(|site| heap.shape(site)); - let fns = shape.is_some_and(|shape| !shape.code.is_empty()); - let others = ty.kinds()[..5].contains(&true) || shape.is_some_and(|shape| shape.plain); - - match (fns, others) { - (true, false) => Some(true), - (false, _) => Some(false), - _ => None, - } -} - -/// Whether a value of type `ty` is truthy, if the type decides it -fn truthiness(ty: &Type) -> Option { - let truthy = [ - ty.undefined.then_some(Some(false)), - ty.null.then_some(Some(false)), - ty.bool, - ty.number.map(|_| None), - ty.string.map(|s| s.map(|atom| atom != EMPTY_STRING)), - ty.object.map(|_| Some(true)), - ]; - - let mut kinds = truthy.into_iter().flatten(); - let first = kinds.next()?; - kinds.all(|t| t == first).then_some(first).flatten() -} - -/// What `typeof` gives for a value of type `ty` -fn typeof_of(ty: &Type, heap: &Heap) -> Type { - let shape = ty.object.map(|site| heap.shape(site)); - - let names = [ - ty.undefined.then_some(73), // "undefined" - ty.null.then_some(77), // "object" - ty.bool.map(|_| 75), // "boolean" - ty.number.map(|_| 74), // "number" - ty.string.map(|_| 76), // "string" - shape.filter(|shape| shape.plain).map(|_| 77), // "object" - shape.filter(|shape| !shape.code.is_empty()).map(|_| 27), // "function" - ]; - - let mut names = names.into_iter().flatten(); - - match names.next() { - Some(name) if names.all(|n| n == name) => Type::string(Some(AtomRef::Predefined(name))), - _ => Type::string(None), - } -} diff --git a/compiler/src/infer/sites.rs b/compiler/src/infer/sites.rs new file mode 100644 index 0000000..cf1a4bf --- /dev/null +++ b/compiler/src/infer/sites.rs @@ -0,0 +1,498 @@ +//! What infer knows about the objects of each site: the type of each field, +//! and flags (see `docs/model.md`, section 3.3) + +use crate::{ + libqjs::parse::{AtomRef, JsValue, opcodes::OpCode}, + native::{NATIVES, ProtoOf}, + regify::RegFn, + transform::{BOX_FIELD, PROTO_FIELD}, +}; + +use super::types::Type; + +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()]; + +/// Where an object is made: the op that creates it, or a built-in fn. Places +/// are numbered in this order (see `SiteTable::new()`) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Place { + Op { fn_id: usize, instr: usize }, + Native(usize), + ProtoLink(ProtoOf), +} + +/// The number of a place: its index in `SiteTable::places` +pub type PlaceId = u32; + +/// The number of a context: its index in `SiteTable::contexts` +pub type CtxId = u32; + +/// The number of a site: its index in `SiteTable::sites` +pub type SiteId = u32; + +/// The code a fn object runs +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum Code { + Fn(usize), + Native(usize), + ProtoLink(ProtoOf), +} + +/// A field of the objects of a site +#[derive(Debug, Clone, Copy)] +pub struct Field { + pub ty: Type, + pub defined: bool, // every object of the site gets it when it is made, so it is never absent +} + +/// What the objects of a site are, besides objects with named fields +#[derive(Debug, Clone, Copy, 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 place, and what every site of it starts with +#[derive(Debug, Clone)] +pub struct PlaceInfo { + pub place: Place, + kind: ObjKind, // the kind of its sites, before any element is written + defined: Vec, // the fields that its creation writes write, sorted + sites: Vec<(CtxId, SiteId)>, // its sites so far, by context +} + +/// A part of a context (see `SiteTable::context()`) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CtxPart { + Member(Type), // a member of a param's type that is not an object + Place(PlaceId), // an object member, by the place that makes it + End, // the end of a param's type +} + +/// What infer knows about the objects of one site +#[derive(Debug, Clone)] +pub struct SiteInfo { + pub place: PlaceId, + pub ctx: CtxId, + pub kind: ObjKind, + pub fields: Vec<(AtomRef, Field)>, // sorted by name + pub written: bool, // a field of one of its objects is written after the object is made + pub compared: bool, // one of its objects is in an identity test with an object + pub in_any: bool, // one of its objects went into an `Any` (see `SiteTable::to_any()`) + pub live: bool, // an op that makes one of its objects has run (see infer) +} + +impl SiteInfo { + /// The code that its objects run, if they are fns + pub fn code(&self) -> Option { + match self.kind { + ObjKind::Fn(code) => Some(code), + _ => None, + } + } + + pub fn is_array(&self) -> bool { + matches!(self.kind, ObjKind::Array { .. }) + } + + pub fn field(&self, name: AtomRef) -> Option<&Field> { + let i = self.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?; + Some(&self.fields[i].1) + } + + /// Whether a copy of one of its objects cannot be told from the object: + /// no write after creation can show the copy, and no identity test can + /// (see `docs/model.md`, section 5). An object in an `Any` value can be + /// written and compared by code that does not know its site + pub fn is_value(&self) -> bool { + !self.written && !self.compared && !self.in_any + } +} + +/// Every place, every site, and every sum +/// +/// A site is a place and a context: the objects that one place makes in the +/// variants that have one context. So the objects follow the monomorphization +/// of the fns that make them (see `docs/model.md`, section 3.3) +/// +/// The sums are stored flat: the members of sum `i` are +/// `sum_members[sum_starts[i]..sum_starts[i + 1]]`. So a search over all sums +/// reads one array in order. The contexts are stored the same way +#[derive(Debug, Clone)] +pub struct SiteTable { + pub places: Vec, // place id -> where it is, sorted by `place` + pub sites: Vec, // site id -> what infer knows about its objects + ctx_parts: Vec, // context id -> its parts, see above + ctx_starts: Vec, + pub(super) sum_members: Vec, // sum id -> its members, see above + pub(super) sum_starts: Vec, + pub version: usize, // goes up on every change, so a walk can tell that a read may give more +} + +impl SiteTable { + /// One place for each creating op, in the order of fns and instrs, then + /// the natives, then the proto links. A place starts its sites with the + /// fields that its creation writes write: every `define_field` writes into + /// the object that a creating op gives (see regify). The built-in fns have + /// one site each, in the empty context (context 0) + pub fn new(fns: &[RegFn]) -> Self { + let mut places: Vec = vec![]; + let place = |place, kind| PlaceInfo { place, kind, defined: vec![], sites: vec![] }; + + // The place that made each value of the fn, if a creating op gave it + let mut created: Vec> = vec![]; + + for (fn_id, f) in fns.iter().enumerate() { + created.clear(); + created.resize(f.val_count, None); + + for (instr, op) in f.code.iter().enumerate() { + match op.op { + OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 | OpCode::ArrayFrom => { + let kind = match op.op { + OpCode::Object => ObjKind::Plain, + OpCode::ArrayFrom => ObjKind::Array { elems: Type::Unknown }, + _ => match f.cpool[op.imm.const_idx()] { + JsValue::FnById(id) => ObjKind::Fn(Code::Fn(id)), + _ => unreachable!(), + }, + }; + + created[op.dsts[0]] = Some(places.len()); + places.push(place(Place::Op { fn_id, instr }, kind)); + } + OpCode::DefineField => { + let defined = &mut places[created[op.args[1]].unwrap()].defined; + + if let Err(i) = defined.binary_search(&op.imm.atom()) { + defined.insert(i, op.imm.atom()); + } + } + _ => {} + } + } + } + + places.extend((0..NATIVES.len()).map(|id| place(Place::Native(id), ObjKind::Fn(Code::Native(id))))); + places.extend(ProtoOf::ALL.map(|kind| place(Place::ProtoLink(kind), ObjKind::Fn(Code::ProtoLink(kind))))); + + let mut table = Self { + places, + sites: vec![], + ctx_parts: vec![], + ctx_starts: vec![0], + sum_members: vec![], + sum_starts: vec![0], + version: 0, + }; + + let empty = table.context(&[]); + + for at in table.places.len() - NATIVES.len() - ProtoOf::ALL.len()..table.places.len() { + table.site_at(at as PlaceId, empty); + } + + table + } + + /// The place `place` + pub fn place_id(&self, place: Place) -> PlaceId { + self.places.binary_search_by_key(&place, |p| p.place).unwrap() as PlaceId + } + + /// The site of a built-in fn + pub fn builtin(&self, place: Place) -> SiteId { + self.places[self.place_id(place) as usize].sites[0].1 + } + + /// The context of a variant whose params have these types: the types, + /// where each object member is named only by its place. So there are + /// finitely many contexts, although a site's objects can be in a param of + /// a variant whose own sites are new (recursion) + pub fn context(&mut self, params: &[Type]) -> CtxId { + let start = self.ctx_parts.len(); + + for ty in params { + let members_start = self.ctx_parts.len(); + + for i in 0..self.members(ty).len() { + let part = match self.members(ty)[i] { + Type::Obj(site) => CtxPart::Place(self.sites[site as usize].place), + member => CtxPart::Member(member), + }; + + if !self.ctx_parts[members_start..].contains(&part) { + self.ctx_parts.push(part); + } + } + + self.ctx_parts.push(CtxPart::End); + } + + let new = &self.ctx_parts[start..]; + let starts = self.ctx_starts.windows(2); + + match starts.map(|w| &self.ctx_parts[w[0] as usize..w[1] as usize]).position(|ctx| ctx == new) { + Some(id) => { + self.ctx_parts.truncate(start); + id as CtxId + } + None => { + self.ctx_starts.push(self.ctx_parts.len() as u32); + (self.ctx_starts.len() - 2) as CtxId + } + } + } + + /// The site of `place` in context `ctx`, made if it is new + pub fn site_at(&mut self, place: PlaceId, ctx: CtxId) -> SiteId { + if let Some(site) = self.find_site(place, ctx) { + return site; + } + + let id = self.sites.len() as SiteId; + let info = &mut self.places[place as usize]; + let fields = info.defined.iter().map(|&n| (n, Field { ty: Type::Unknown, defined: true })).collect(); + + info.sites.push((ctx, id)); + self.sites.push(SiteInfo { + place, + ctx, + kind: info.kind, + fields, + written: false, + compared: false, + in_any: false, + live: false, + }); + + self.version += 1; + id + } + + /// The site of `place` in context `ctx`, if infer made it + pub fn find_site(&self, place: PlaceId, ctx: CtxId) -> Option { + let sites = &self.places[place as usize].sites; + sites.iter().find(|(c, _)| *c == ctx).map(|(_, site)| *site) + } + + pub fn site(&self, site: SiteId) -> &SiteInfo { + &self.sites[site as usize] + } + + /// An object of the site, which a creating op gives + pub fn create(&mut self, site: SiteId) -> Type { + self.mark(site, |s| &mut s.live); + Type::Obj(site) + } + + /// Set a flag of `site`. Whether it was not set before + pub fn mark(&mut self, site: SiteId, flag: fn(&mut SiteInfo) -> &mut bool) -> bool { + let flag = flag(&mut self.sites[site as usize]); + + if *flag { + return false; + } + + *flag = true; + self.version += 1; + true + } + + /// Whether an object of `site` can lack its own `field`. A box's value + /// never can: JS gives a binding a value before any read of it that does + /// not throw (see transform) + pub fn can_be_absent(&self, site: SiteId, field: AtomRef) -> bool { + field != BOX_FIELD && !self.site(site).field(field).is_some_and(|f| f.defined) + } + + /// A value of type `ty` goes into an `Any` value. Code that holds the + /// `Any` does not know the site of an object in it, so the object's site + /// is marked, and so are the sites of the objects in its fields, which + /// that code can read + pub fn to_any(&mut self, ty: &Type) -> Type { + let mut stack = vec![]; + self.push_sites(ty, &mut stack); + + while let Some(site) = stack.pop() { + if self.mark(site, |s| &mut s.in_any) { + for i in 0..self.site(site).fields.len() { + let field = self.site(site).fields[i].1.ty; + self.push_sites(&field, &mut stack); + } + + if let ObjKind::Array { elems } = self.site(site).kind { + self.push_sites(&elems, &mut stack); + } + } + } + + Type::Any + } + + /// Push the site of each object member of `ty` + fn push_sites(&self, ty: &Type, stack: &mut Vec) { + stack.extend(self.members(ty).iter().filter_map(|m| match m { + Type::Obj(site) => Some(*site), + _ => None, + })); + } + + // ─────────────────────────────────────────── Fields ────────────────────────────────────────────────── + + /// Join `ty` into `field` of the objects of `site`. A field that the site + /// did not have can be absent. An array's `length` is not a field: a write + /// of it sets the array's length (see `docs/model.md`, section 3.3) + pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) { + if field == LENGTH_FIELD && self.site(site).is_array() { + return; + } + + let old = self.site(site).field(field).map_or(Type::Unknown, |f| f.ty); + let new = self.join(&old, ty); + + // Code that holds one of its objects in an `Any` can read the field + if self.site(site).in_any { + self.to_any(&new); + } + + let fields = &mut self.sites[site as usize].fields; + + match fields.binary_search_by_key(&field, |(n, _)| *n) { + Ok(i) if fields[i].1.ty == new => return, + Ok(i) => fields[i].1.ty = new, + Err(i) => fields.insert(i, (field, Field { ty: new, defined: false })), + } + + self.version += 1; + } + + /// Join `ty` into the element type of the arrays of `site` + pub fn write_elem(&mut self, site: SiteId, ty: &Type) { + let ObjKind::Array { elems } = self.site(site).kind else { + unreachable!() + }; + + let new = self.join(&elems, ty); + + // Code that holds one of its arrays in an `Any` can read the elements + if self.site(site).in_any { + self.to_any(&new); + } + + if new != elems { + self.sites[site as usize].kind = ObjKind::Array { elems: new }; + self.version += 1; + } + } + + /// A read of an element of an array of `site`. An index can be a hole, or + /// past the end, which gives undefined + pub fn read_elem(&mut self, site: SiteId) -> Type { + let ObjKind::Array { elems } = self.site(site).kind else { + unreachable!() + }; + + self.join(&elems, &Type::Undefined) + } + + /// A read of `field` on an object of `site`: 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(&mut self, site: SiteId, field: AtomRef, protos: &Protos) -> Type { + self.read_chain(site, field, protos, self.sites.len()) + } + + /// `read()`, with at most `depth` more prototypes. Objects of one site can + /// be each other's prototypes, so the chain of sites can loop, which a + /// chain of objects cannot. A chain longer than the number of sites has + /// looped, and stops + fn read_chain(&mut self, site: SiteId, field: AtomRef, protos: &Protos, depth: usize) -> Type { + let array = self.site(site).is_array(); + + if array && field == LENGTH_FIELD { + return Type::Int; + } + + let own = self.site(site).field(field).map_or(Type::Unknown, |f| f.ty); + + if !self.can_be_absent(site, field) || depth == 0 { + 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], false), + false => { + let proto = self.site(site).field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty); + (proto, self.can_be_absent(site, PROTO_FIELD)) + } + }; + let mut ty = own; + + if proto_absent || proto == Type::Unknown { + ty = self.join(&ty, &Type::Undefined); + } + + for i in 0..self.members(&proto).len() { + let read = match self.members(&proto)[i] { + Type::Obj(proto) => self.read_chain(proto, field, protos, depth - 1), + Type::Any => Type::Any, + _ => Type::Undefined, + }; + + ty = self.join(&ty, &read); + } + + 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 + /// + /// A read on `Any` can read any object in an `Any`, or any primitive. What + /// it reads goes into the `Any` that it gives + pub fn read_value(&mut self, ty: &Type, field: AtomRef, protos: &Protos) -> Type { + match *ty { + Type::Obj(site) => self.read(site, field, protos), + Type::Sum(_) => { + let mut read = Type::Unknown; + + for i in 0..self.members(ty).len() { + let member = self.members(ty)[i]; + let ty = self.read_value(&member, field, protos); + read = self.join(&read, &ty); + } + + read + } + Type::Any => { + for site in 0..self.sites.len() as SiteId { + if self.site(site).in_any { + let read = self.read(site, field, protos); + self.to_any(&read); + } + } + + for kind in ProtoOf::ALL { + if let Type::Obj(proto) = protos[kind as usize] { + let read = self.read(proto, field, protos); + self.to_any(&read); + } + } + + Type::Any + } + ty => match ProtoOf::of(&ty).map(|kind| protos[kind as usize]) { + Some(Type::Unknown) | None => Type::Unknown, + Some(proto) => self.read_value(&proto, field, protos), + }, + } + } +} diff --git a/compiler/src/infer/types.rs b/compiler/src/infer/types.rs index 1ca3c99..6b9cefb 100644 --- a/compiler/src/infer/types.rs +++ b/compiler/src/infer/types.rs @@ -1,493 +1,275 @@ -//! Types and shapes (see `docs/model.md`, section 3) +//! 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 3.1) -use std::cell::Cell; +use crate::libqjs::parse::AtomRef; -use crate::{ - libqjs::parse::{AtomRef, JsValue, opcodes::OpCode}, - native::{NATIVES, Primitive}, - regify::RegFn, - transform::{BOX_FIELD, PROTO_FIELD}, -}; +use super::sites::{SiteId, SiteTable}; -/// Where an object comes from: the op that creates it, or a built-in fn -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Site { - Op { fn_id: usize, instr: usize }, - Native(usize), - ProtoLink(Primitive), -} +pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // "" -/// The number of a site: its index in `Heap::sites` -pub type SiteId = u32; +/// The number of a sum (see `SiteTable::members()`) +pub type SumId = u32; -/// The code a fn object runs +/// 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 site #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub enum Code { - Fn(usize), - Native(usize), - ProtoLink(Primitive), -} - -/// A number is an int32 or a double. An int32 is also a double, so `Int` -/// joins with `Float` to give `Float` -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub enum Number { +pub enum Type { + Unknown, // no value has reached it yet + Undefined, + Null, + Bool(Option), // the value, if it is known Int, Float, + Str(Option), // the value, if it is known. Atoms are interned: equal atoms are equal strings + Obj(SiteId), // an object that the site makes. A fn is an object whose site has code + Sum(SumId), // see above + Any, // any value. No rule gives it now: a join gives a sum instead } -/// The set of values that a place can hold: a set of kinds, with a refinement -/// for each kind in the set. The default is the empty set -#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)] -pub struct Type { - pub undefined: bool, - pub null: bool, - /// The value, if it is known - pub bool: Option>, - pub number: Option, - /// The value, if it is known. Atoms are interned, so two equal atoms are - /// two equal strings - pub string: Option>, - /// The shape, by a site in it (see `Heap`) - pub object: Option, +/// A `Type` without its detail (a known value, a site): 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 { - pub const UNDEFINED: Type = Type { undefined: true, ..Type::EMPTY }; - pub const NULL: Type = Type { null: true, ..Type::EMPTY }; - pub const EMPTY: Type = Type { - undefined: false, - null: false, - bool: None, - number: None, - string: None, - object: None, - }; - - pub const fn bool(value: Option) -> Type { - Type { bool: Some(value), ..Type::EMPTY } - } - - pub const fn number(number: Number) -> Type { - Type { number: Some(number), ..Type::EMPTY } - } - - pub const fn string(value: Option) -> Type { - Type { string: Some(value), ..Type::EMPTY } - } - - pub const fn object(site: SiteId) -> Type { - Type { object: Some(site), ..Type::EMPTY } - } - - pub fn is_empty(&self) -> bool { - *self == Type::EMPTY - } - - /// The type with the known values dropped + /// The type with the known value dropped pub fn widen(&self) -> Type { - Type { - bool: self.bool.map(|_| None), - string: self.string.map(|_| None), - ..*self + match self { + Type::Bool(_) => Type::Bool(None), + Type::Str(_) => Type::Str(None), + ty => *ty, } } - /// The type without undefined and null - pub fn non_nullish(&self) -> Type { - Type { - undefined: false, - null: false, - ..*self + /// The base type of every value of the type. `None` for `Unknown`, which + /// has no values, and for `Sum` and `Any`, which can have more than one + pub fn base(&self) -> Option { + match self { + Type::Unknown | Type::Sum(_) | Type::Any => None, + Type::Undefined => Some(BaseType::Undefined), + Type::Null => Some(BaseType::Null), + Type::Bool(_) => Some(BaseType::Bool), + Type::Int | Type::Float => Some(BaseType::Number), + Type::Str(_) => Some(BaseType::String), + Type::Obj(_) => Some(BaseType::Object), } } - pub fn has_nullish(&self) -> bool { - self.undefined || self.null - } - - /// The kinds in the type, in the order undefined, null, bool, number, - /// string, object - pub fn kinds(&self) -> [bool; 6] { - [ - self.undefined, - self.null, - self.bool.is_some(), - self.number.is_some(), - self.string.is_some(), - self.object.is_some(), - ] - } - - /// Whether the type has exactly the kinds of `kind` - pub fn is_only(&self, kind: Type) -> bool { - self.kinds() == kind.kinds() + pub fn is_nullish(&self) -> bool { + matches!(self, Type::Undefined | Type::Null) } } -/// The join of two refinements that are a value if it is known -fn known(a: Option>, b: Option>) -> Option> { +/// The join of two types of one base type, neither of them `Obj` +fn join_kind(a: Type, b: Type) -> Type { match (a, b) { - (Some(a), Some(b)) => Some(if a == b { a } else { None }), - (a, b) => a.or(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, } } -/// A field of a shape -#[derive(Debug, Clone, Copy)] -pub struct Field { - pub ty: Type, - /// Every object of the shape gets the field when it is created, so the - /// field is never absent - pub defined: bool, -} - -/// The type of a set of objects: what `docs/model.md` calls a shape -#[derive(Debug, Clone, Default)] -pub struct Shape { - /// Sorted by name - pub fields: Vec<(AtomRef, Field)>, - /// The code that its objects can run, sorted - pub code: Vec, - /// Some of its objects are not fns - pub plain: bool, - /// A field of one of its objects is written after the object is created - pub written: bool, - /// One of its objects is in an identity test with an object - pub compared: bool, - /// An op that creates one of its objects has run (see infer) - pub live: bool, -} - -impl Shape { - pub fn field(&self, name: AtomRef) -> Option<&Field> { - let i = self.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?; - Some(&self.fields[i].1) +/// The sums, and the join, which makes them. The sums are stored in the site +/// table, because a join also marks sites (see `SiteTable::to_any()`) +impl SiteTable { + /// The types that a value of type `ty` can have: the members of a sum, + /// else `ty` itself. `Unknown` has none + pub fn members<'a>(&'a self, ty: &'a Type) -> &'a [Type] { + match ty { + Type::Unknown => &[], + Type::Sum(id) => { + let id = *id as usize; + &self.sum_members[self.sum_starts[id] as usize..self.sum_starts[id + 1] as usize] + } + ty => std::slice::from_ref(ty), + } } - /// Whether a copy of one of its objects cannot be told from the object: - /// no write after creation can show the copy, and no identity test can - /// (see `docs/model.md`, section 5) - pub fn is_value(&self) -> bool { - !self.written && !self.compared + /// The members of every sum + pub fn sums(&self) -> impl Iterator { + self.sum_starts.windows(2).map(|w| &self.sum_members[w[0] as usize..w[1] as usize]) } -} -/// A site, and the code that its objects run, if they are fns -#[derive(Debug, Clone, Copy)] -pub struct SiteInfo { - pub site: Site, - pub code: Option, -} + /// The type whose members are the members of `a` and of `b` + fn union(&mut self, a: &Type, b: &Type) -> Type { + let start = self.sum_members.len(); -/// What `Heap::new` finds about one instr -#[derive(Debug, Clone, Copy, Default)] -struct InstrInfo { - /// The site of the object it creates - site: Option, - /// A `define_field` into the value that a creating op gives, before the - /// object goes anywhere. Every other field write is a write after creation - creation_write: bool, -} + for ty in [a, b] { + for i in 0..self.members(ty).len() { + let member = self.members(ty)[i]; + self.sum_members.push(member); + } + } -/// Every shape. Objects that flow into one place have one shape: their shapes -/// are merged. A merged site points to the site it was merged into, and only -/// a site that points to itself keeps a shape -#[derive(Debug, Clone)] -pub struct Heap { - /// Every site, by number: the creating ops of every fn in order, then the - /// natives, then the proto links - pub sites: Vec, - shapes: Vec, - parent: Vec>, - /// Each fn's first entry in `instrs`, which has one entry for each instr - fn_starts: Vec, - instrs: Vec, - /// The number of the first native's site - natives: SiteId, - /// Goes up on every change, so a walk can tell that a read may give more - pub version: usize, -} + self.sum_of_new(start) + } -impl Heap { - /// Number every site, and give each one a shape with the fields its - /// creation writes write - pub fn new(fns: &[RegFn]) -> Self { - let mut heap = Self { - sites: vec![], - shapes: vec![], - parent: vec![], - fn_starts: vec![], - instrs: vec![], - natives: 0, - version: 0, + /// `ty` with every known value dropped + pub fn widen(&mut self, ty: &Type) -> Type { + let start = self.sum_members.len(); + + for i in 0..self.members(ty).len() { + let member = self.members(ty)[i].widen(); + self.sum_members.push(member); + } + + self.sum_of_new(start) + } + + /// `widen()` for a type that `widen()` was already given + pub fn widened(&self, ty: &Type) -> Type { + let list: Vec = self.members(ty).iter().map(Type::widen).collect(); + + match list[..] { + [one] => one, + _ => Type::Sum(self.sums().position(|sum| sum == list).unwrap() as SumId), + } + } + + /// The type whose members are the types at the end of `sum_members`, from + /// `start` on. They are sorted, and two of one kind join. If they are one + /// type, or a sum that exists, they are removed again + fn sum_of_new(&mut self, start: usize) -> Type { + let list = &mut self.sum_members[start..]; + list.sort_unstable(); + + let mut n = 0; + + for i in 0..list.len() { + let ty = list[i]; + let joins = n > 0 && (list[n - 1] == ty || (ty.base() == list[n - 1].base() && ty.base() != Some(BaseType::Object))); + + if joins { + list[n - 1] = join_kind(list[n - 1], ty); + } else { + list[n] = ty; + n += 1; + } + } + + self.sum_members.truncate(start + n); + + let new = &self.sum_members[start..]; + let found = match new { + [one] => Some(*one), + _ => self.sums().position(|sum| sum == new).map(|id| Type::Sum(id as SumId)), }; - // The site that each value of the fn holds, if a creating op gave it - let mut created: Vec> = vec![]; - - for (fn_id, f) in fns.iter().enumerate() { - heap.fn_starts.push(heap.instrs.len()); - created.clear(); - created.resize(f.val_count, None); - - for (instr, op) in f.code.iter().enumerate() { - let mut info = InstrInfo::default(); - - match op.op { - OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 => { - let code = match op.op { - OpCode::Object => None, - _ => match f.cpool[op.imm.const_idx()] { - JsValue::FnById(id) => Some(Code::Fn(id)), - _ => unreachable!(), - }, - }; - - let site = heap.add(Site::Op { fn_id, instr }, code); - created[op.dsts[0]] = Some(site); - info.site = Some(site); - } - OpCode::DefineField => { - if let Some(site) = created[op.args[1]] { - let field = Field { ty: Type::EMPTY, defined: true }; - let fields = &mut heap.shapes[site as usize].fields; - - if let Err(i) = fields.binary_search_by_key(&op.imm.atom(), |(n, _)| *n) { - fields.insert(i, (op.imm.atom(), field)); - } - - info.creation_write = true; - } - } - _ => {} - } - - heap.instrs.push(info); + match found { + Some(ty) => { + self.sum_members.truncate(start); + ty + } + None => { + self.sum_starts.push(self.sum_members.len() as u32); + Type::Sum((self.sum_starts.len() - 2) as SumId) } } - - heap.natives = heap.sites.len() as SiteId; - - for id in 0..NATIVES.len() { - heap.add(Site::Native(id), Some(Code::Native(id))); - } - - for kind in Primitive::ALL { - heap.add(Site::ProtoLink(kind), Some(Code::ProtoLink(kind))); - } - - heap } - fn add(&mut self, site: Site, code: Option) -> SiteId { - let id = self.sites.len() as SiteId; - - self.sites.push(SiteInfo { site, code }); - self.parent.push(Cell::new(id)); - self.shapes.push(Shape { - code: code.into_iter().collect(), - plain: code.is_none(), - ..Shape::default() - }); - - id - } - - /// The site that instr `instr` of fn `fn_id` creates, if it is a creating - /// op - pub fn site_at(&self, fn_id: usize, instr: usize) -> Option { - self.instrs[self.fn_starts[fn_id] + instr].site - } - - pub fn is_creation_write(&self, fn_id: usize, instr: usize) -> bool { - self.instrs[self.fn_starts[fn_id] + instr].creation_write - } - - /// The site of a built-in fn - pub fn builtin(&self, site: Site) -> SiteId { - self.natives - + match site { - Site::Native(id) => id as SiteId, - Site::ProtoLink(kind) => (NATIVES.len() + kind as usize) as SiteId, - Site::Op { .. } => unreachable!(), - } - } - - /// The site that `site` is merged into, or `site` itself - pub fn find(&self, mut site: SiteId) -> SiteId { - loop { - let parent = self.parent[site as usize].get(); - - if parent == site { - return site; - } - - // Path halving: skip a step for the next find - let grandparent = self.parent[parent as usize].get(); - self.parent[site as usize].set(grandparent); - site = grandparent; - } - } - - /// `ty` with its object's site the one that keeps its shape - pub fn canon(&self, ty: &Type) -> Type { - Type { - object: ty.object.map(|site| self.find(site)), - ..*ty - } - } - - pub fn shape(&self, site: SiteId) -> &Shape { - &self.shapes[self.find(site) as usize] - } - - /// Every site that keeps a shape, and that a creating op has reached - pub fn live_shapes(&self) -> impl Iterator + '_ { - (0..self.sites.len() as SiteId).filter(|&site| self.find(site) == site && self.shapes[site as usize].live) - } - - /// An object of the site, which a creating op gives - pub fn create(&mut self, site: SiteId) -> Type { - let root = self.find(site); - self.mark(root, |shape| &mut shape.live); - Type::object(root) - } - - /// Set a flag of the shape of `site` - pub fn mark(&mut self, site: SiteId, flag: fn(&mut Shape) -> &mut bool) { - let root = self.find(site); - let flag = flag(&mut self.shapes[root as usize]); - - if !*flag { - *flag = true; - self.version += 1; - } - } - - /// Whether an object of the shape of `site` can lack its own `field`. A - /// box's value never can: JS gives a binding a value before any read of it - /// that does not throw (see transform) - pub fn can_be_absent(&self, site: SiteId, field: AtomRef) -> bool { - field != BOX_FIELD && !self.shape(site).field(field).is_some_and(|f| f.defined) - } - - /// Type lattice join: the union, kind by kind. Two different known values - /// give an unknown one, and two shapes are merged + /// The join of two types (see `Type`) pub fn join(&mut self, a: &Type, b: &Type) -> Type { - Type { - undefined: a.undefined || b.undefined, - null: a.null || b.null, - bool: known(a.bool, b.bool), - number: a.number.max(b.number), - string: known(a.string, b.string), - object: match (a.object, b.object) { - (Some(x), Some(y)) => Some(self.merge(x, y)), - (x, y) => x.or(y).map(|site| self.find(site)), - }, + use Type::*; + + match (*a, *b) { + (Unknown, t) | (t, Unknown) => t, + (a, b) if a == b => a, + (Any, t) | (t, Any) => self.to_any(&t), + (a, b) => self.union(&a, &b), } } +} - /// Merge the shape of `from` into the shape of `into`. A field that only - /// one of them has can be absent in the merged shape - fn merge(&mut self, into: SiteId, from: SiteId) -> SiteId { - let (into, from) = (self.find(into), self.find(from)); - - if into == from { - return into; - } - - let from_shape = std::mem::take(&mut self.shapes[from as usize]); - self.parent[from as usize].set(into); - self.version += 1; - - let shape = &mut self.shapes[into as usize]; - shape.code.extend(&from_shape.code); - shape.code.sort_unstable(); - shape.code.dedup(); - shape.plain |= from_shape.plain; - shape.written |= from_shape.written; - shape.compared |= from_shape.compared; - shape.live |= from_shape.live; - - for (name, field) in &mut shape.fields { - field.defined &= from_shape.field(*name).is_some_and(|f| f.defined); - } +/// The type, if every value of it is one value that the type tells +pub(super) fn single(ty: &Type) -> Option { + match ty { + Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(*ty), + _ => None, + } +} - for (name, field) in &from_shape.fields { - let defined = self.shape(into).field(*name).is_some_and(|f| f.defined) && field.defined; - self.write(into, *name, &field.ty); +// The tests below take one member of a type each (see `TypeCache::agree()`) - let root = self.find(into) as usize; - let i = self.shapes[root].fields.binary_search_by_key(name, |(n, _)| *n).unwrap(); - self.shapes[root].fields[i].1.defined = defined; - } +/// Whether `a === b`, if the types decide it +pub(super) fn strict_eq(a: &Type, b: &Type) -> Option { + let disjoint = matches!((a.base(), b.base()), (Some(x), Some(y)) if x != y); - self.find(into) + match (single(a), single(b)) { + _ if disjoint => Some(false), + (Some(x), Some(y)) => Some(x == y), + _ => None, } - - /// Join `ty` into `field` of the shape of `site`. A field that the shape - /// did not have can be absent - pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) { - let old = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty); - let new = self.join(&old, ty); - - // The join may have merged the shape into another one - let root = self.find(site) as usize; - let fields = &mut self.shapes[root].fields; - - match fields.binary_search_by_key(&field, |(n, _)| *n) { - Ok(i) if fields[i].1.ty == new => return, - Ok(i) => fields[i].1.ty = new, - Err(i) => fields.insert(i, (field, Field { ty: new, defined: false })), - } - - self.version += 1; +} + +/// 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 { + let other = |t: &Type| !t.is_nullish() && *t != Type::Any; + + match (a, b) { + _ if a.is_nullish() && b.is_nullish() => Some(true), + _ if (a.is_nullish() && other(b)) || (other(a) && b.is_nullish()) => Some(false), + _ if a.base().is_some() && a.base() == b.base() => strict_eq(a, b), + _ => None, } - - /// A read of `field` on an object of the shape of `site`: 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 - /// - /// Merged shapes can make a chain loop, which a real object cannot. A chain - /// longer than the number of sites has looped, and stops - pub fn read(&mut self, site: SiteId, field: AtomRef) -> Type { - let mut site = self.find(site); - let mut ty = Type::EMPTY; - - for _ in 0..self.sites.len() { - let own = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty); - ty = self.join(&ty, &own); - - if !self.can_be_absent(site, field) { - return ty; - } - - // The chain ends at a prototype that is null or absent - let proto = self.shape(site).field(PROTO_FIELD).map_or(Type::EMPTY, |f| f.ty); - ty.undefined |= proto.null || self.can_be_absent(site, PROTO_FIELD); - - match proto.object { - Some(proto) => site = self.find(proto), - None => break, - } - } - - ty +} + +/// Whether a value of type `ty` has one of the base types `bases`, if the type decides it +pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option { + ty.base().map(|base| bases.contains(&base)) +} + +/// Whether a value of type `ty` is a fn, if the type decides it +pub(super) fn is_function(ty: &Type, site_table: &SiteTable) -> Option { + match ty { + Type::Obj(site) => Some(site_table.site(*site).code().is_some()), + Type::Any => None, + _ => Some(false), } - - /// 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 they give nothing - pub fn read_value(&mut self, ty: &Type, field: AtomRef, protos: &[Type; 3]) -> Type { - let proto = |kind: Primitive| protos[kind as usize].object.filter(|_| kind.is_in(ty)); - let mut read = Type::EMPTY; - - for site in Primitive::ALL.map(proto).into_iter().chain([ty.object]).flatten() { - let ty = self.read(site, field); - read = self.join(&read, &ty); - } - - read +} + +/// Whether a value of type `ty` is truthy, if the type decides it +pub(super) fn truthiness(ty: &Type) -> Option { + match ty { + Type::Undefined | Type::Null => Some(false), + Type::Bool(b) => *b, + Type::Str(s) => s.map(|atom| atom != EMPTY_STRING), + Type::Obj(_) => Some(true), + _ => None, } +} + +/// The atom of what `typeof` gives for a value of type `ty`, if the type +/// decides it +pub(super) fn typeof_name(ty: &Type, site_table: &SiteTable) -> Option { + Some(match ty { + Type::Undefined => 73, // "undefined" + Type::Null => 77, // "object" + Type::Bool(_) => 75, // "boolean" + Type::Int | Type::Float => 74, // "number" + Type::Str(_) => 76, // "string" + Type::Obj(site) if site_table.site(*site).code().is_some() => 27, // "function" + Type::Obj(_) => 77, // "object" + _ => return None, + }) } diff --git a/compiler/src/libqjs/parse.rs b/compiler/src/libqjs/parse.rs index 546ccc6..2b8c4d7 100644 --- a/compiler/src/libqjs/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -72,7 +72,8 @@ impl Atom { let (bin, header) = leb128(bin)?; let wide_char = header & 1 == 1; let len = header >> 1; - let (bin, data) = take(len)(bin)?; + let byte_len = if wide_char { len * 2 } else { len }; + let (bin, data) = take(byte_len)(bin)?; let data = data.to_vec(); let string = if wide_char { @@ -171,7 +172,7 @@ pub enum JsValue { /// A C function, by its index in `native::NATIVES` NativeFn(usize), /// Makes its arg the prototype of a primitive kind (see `native`) - ProtoLink(crate::native::Primitive), + ProtoLink(crate::native::ProtoOf), Module(Module), TypedArray, ArrayBuffer, diff --git a/compiler/src/main.rs b/compiler/src/main.rs index 6e2d496..8411be2 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -17,7 +17,34 @@ use libqjs::compile; use regify::regify; use transform::transform; -const STDLIB: &[u8] = include_bytes!("../../lib/stdlib.js"); +/// 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 @@ -28,10 +55,16 @@ fn main() { let metadata = std::fs::metadata(filename).unwrap(); assert!(metadata.is_file()); - // The stdlib is compiled with the program, in front of it - let mut src = STDLIB.to_vec(); - src.reserve(metadata.len() as usize); + // 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"); 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); @@ -43,5 +76,33 @@ fn main() { let ty_cache = infer_types(®_fns); let analysis = analyze(®_fns); - print!("{}", emit::emit(®_fns, &ty_cache, &analysis, &atoms)); + let c = emit::emit(®_fns, &ty_cache, &analysis, &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(); + + let status = cc + .to_command() + .args(["-std=c11", "-w"]) + .arg(dir.join("program.c")) + .args(["-o", "a.out", "-lm"]) + .status() + .unwrap(); + + std::fs::remove_dir_all(&dir).unwrap(); + std::process::exit(status.code().unwrap_or(1)); } diff --git a/compiler/src/native.rs b/compiler/src/native.rs index 29f7b3b..fd00f27 100644 --- a/compiler/src/native.rs +++ b/compiler/src/native.rs @@ -1,4 +1,4 @@ -//! What the stdlib (`lib/stdlib.js`) can reach below JS: the globals it may +//! 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 @@ -10,7 +10,10 @@ //! kind. A field read on a primitive borrows its kind's prototype use crate::{ - infer::types::{Number, Type}, + infer::{ + sites::SiteTable, + types::{BaseType, Type}, + }, libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS}, }; @@ -37,10 +40,10 @@ macro_rules! native { /// A native's param or return type: any value of its kind macro_rules! ty { - (Undefined) => { Type::UNDEFINED }; - (Int) => { Type::number(Number::Int) }; - (Float) => { Type::number(Number::Float) }; - (String) => { Type::string(None) }; + (Undefined) => { Type::Undefined }; + (Int) => { Type::Int }; + (Float) => { Type::Float }; + (String) => { Type::Str(None) }; } #[rustfmt::skip] @@ -48,7 +51,23 @@ pub static NATIVES: &[NativeFn] = &[ // IO native!("__native_print_string"(String) -> Undefined), - // Numbers. JS has one number type, see `accepts()` + // 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_wait"() -> 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), @@ -60,33 +79,39 @@ pub static NATIVES: &[NativeFn] = &[ native!("__native_string_from_char_code"(Int) -> String), ]; -/// A primitive kind whose field reads borrow a prototype from the stdlib +/// 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 Primitive { +pub enum ProtoOf { String, Number, Boolean, + Array, } -impl Primitive { - pub const ALL: [Primitive; 3] = [Primitive::String, Primitive::Number, Primitive::Boolean]; +impl ProtoOf { + pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array]; + pub const PRIMITIVES: [ProtoOf; 3] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean]; - /// Whether `ty` can hold a value of this kind - pub fn is_in(self, ty: &Type) -> bool { - match self { - Primitive::String => ty.string.is_some(), - Primitive::Number => ty.number.is_some(), - Primitive::Boolean => ty.bool.is_some(), + /// The primitive kind of every value of type `ty`, if it has a prototype + pub fn of(ty: &Type) -> Option { + match ty.base()? { + BaseType::String => Some(ProtoOf::String), + BaseType::Number => Some(ProtoOf::Number), + BaseType::Bool => Some(ProtoOf::Boolean), + _ => None, } } } /// `__link_proto_(proto)` makes `proto` the prototype of ``. It /// does nothing at runtime -static PROTO_LINKS: &[(&str, Primitive)] = &[ - ("__link_proto_string", Primitive::String), - ("__link_proto_number", Primitive::Number), - ("__link_proto_boolean", Primitive::Boolean), +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), ]; /// The value of the global `name`. The only globals are the ones the stdlib @@ -111,17 +136,17 @@ pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue { /// 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 -pub fn ret_type(idx: usize, args: &[Type]) -> Type { +/// the call gives nothing. An arg of a sum or `Any` is checked at runtime +pub fn ret_type(idx: usize, args: &[Type], site_table: &SiteTable) -> Type { let native = &NATIVES[idx]; - let missing = std::iter::repeat(&Type::UNDEFINED); + let missing = std::iter::repeat(&Type::Undefined); let can_pass = |(param, arg): (&Type, &Type)| { - param.kinds().iter().zip(arg.kinds()).any(|(p, a)| *p && a) + *arg == Type::Any || site_table.members(arg).iter().any(|a| a.base() == param.base()) }; match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) { true => native.ret, - false => Type::EMPTY, + false => Type::Unknown, } } diff --git a/compiler/src/regify/mod.rs b/compiler/src/regify/mod.rs index b3f92b1..48f8c3b 100644 --- a/compiler/src/regify/mod.rs +++ b/compiler/src/regify/mod.rs @@ -2,7 +2,7 @@ mod basic_block; use crate::{ flatten::FlatFn, - libqjs::parse::{AtomRef, ImmOperand, Instr, JsValue, opcodes::OpCode}, + libqjs::parse::{ImmOperand, Instr, JsValue, opcodes::OpCode}, }; pub use basic_block::BasicBlock; @@ -29,7 +29,6 @@ impl RegInstr { #[derive(Debug)] pub struct RegFn { - pub name: AtomRef, pub arg_count: usize, pub var_count: usize, /// Locals and args are not renamed: local `k` is value id `k`, and arg `k` @@ -119,8 +118,25 @@ impl RegFn { } } + // A define_field into an object that no creating op of this fn gave + // (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 + // that builds an object before it goes anywhere (see 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 { - name: f.name, arg_count: f.arg_count, var_count: f.var_count, val_count: next_val, @@ -209,7 +225,7 @@ fn walk_block( continue; } - // Handle `Dup` and `Swap`, which just push values + // 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]); @@ -219,6 +235,27 @@ fn walk_block( 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]); @@ -228,11 +265,11 @@ fn walk_block( continue; } - // A field write lands back in the object's own value: the object with - // the field is the same object. define_field also leaves it on the - // stack, put_field does not - if matches!(instr.op, OpCode::PutField | OpCode::DefineField) { - let obj = out_instr.args[1]; + // A field or element write lands back in the object's own value: the + // object with the field is the same object. define_field also leaves + // it on the stack, put_field and put_array_el do not + if matches!(instr.op, OpCode::PutField | OpCode::DefineField | OpCode::PutArrayEl) { + let obj = *out_instr.args.last().unwrap(); if n_push == 1 { stack.push(obj); diff --git a/compiler/src/transform/args.rs b/compiler/src/transform/args.rs new file mode 100644 index 0000000..995a0e9 --- /dev/null +++ b/compiler/src/transform/args.rs @@ -0,0 +1,29 @@ +use super::{FIRST_ARG, THIS_ARG, pusher}; +use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode}; + +/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`, +/// and `this` with a read of its param +pub fn replace_arg_access(instr: &Instr, out: &mut Vec) -> bool { + use OpCode::*; + + let (op, arg) = match (instr.op, instr.imm) { + (PushThis, _) => (GetArg, THIS_ARG), + (GetArg0, _) => (GetArg, FIRST_ARG), + (GetArg1, _) => (GetArg, FIRST_ARG + 1), + (GetArg2, _) => (GetArg, FIRST_ARG + 2), + (GetArg3, _) => (GetArg, FIRST_ARG + 3), + (PutArg0, _) => (PutArg, FIRST_ARG), + (PutArg1, _) => (PutArg, FIRST_ARG + 1), + (PutArg2, _) => (PutArg, FIRST_ARG + 2), + (PutArg3, _) => (PutArg, FIRST_ARG + 3), + (SetArg0, _) => (SetArg, FIRST_ARG), + (SetArg1, _) => (SetArg, FIRST_ARG + 1), + (SetArg2, _) => (SetArg, FIRST_ARG + 2), + (SetArg3, _) => (SetArg, FIRST_ARG + 3), + (GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k), + _ => return false, + }; + + pusher(instr.pc, out)(op, ImmOperand::Arg(arg)); + true +} diff --git a/compiler/src/transform/bindings.rs b/compiler/src/transform/bindings.rs new file mode 100644 index 0000000..1273e8d --- /dev/null +++ b/compiler/src/transform/bindings.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; + +use super::{BOX_FIELD, ENV_ARG, FIRST_ARG, pusher}; +use crate::{ + libqjs::parse::{ + AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode, + }, + regify::{is_var_read, is_var_write}, +}; + +/// Put every binding that a closure captures in a box: an object whose +/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The +/// owner and the closures read and write the box. A closure keeps the boxes +/// it captured in its own fields, one for each of its `closure_vars`: field `i` +/// holds the box of capture `i` +/// +/// The fn makes a box for each binding that its children capture, at entry. +/// A captured local holds its box in its own slot. A captured arg keeps its +/// box in a new local, and the box starts with the arg's value. A `var` starts +/// as undefined. A `let` or `const` has no value until it is written (its TDZ): +/// object; undefined; define_field ; put_loc k each captured `var` +/// object; put_loc k each captured `let` +/// object; get_arg a; define_field ; put_loc box each captured arg +/// +/// A scope that is entered again is a loop body. QuickJS closes each captured +/// binding of the scope (`close_loc`) at every way out of it, so a new box +/// there gives each iteration its own binding. `set_loc_uninitialized` only +/// marks the TDZ, which is not checked, and hoisted fns are made before it, so +/// it does not make a box +/// +/// These instrs take a pc that no jump goes to, so a jump to the fn's first +/// instr does not make them again +/// +/// Then it replaces, for a captured binding: +/// get_loc k get_loc box; get_field +/// put_loc k get_loc box; swap; put_field +/// set_loc k dup; get_loc box; swap; put_field +/// close_loc k object; get_loc k; get_field ; +/// define_field ; put_loc k +/// a way out of a loop body: a new binding, with +/// the current value +/// +/// For any binding: +/// get_var_ref i get_arg env; get_field i; get_field +/// put_var_ref i get_arg env; get_field i; swap; put_field +/// set_var_ref i dup; get_arg env; get_field i; swap; put_field +/// fclosure f fclosure f; (push_box; define_field i)* +/// +/// where `push_box` is the box of capture `i` of `f`: the local that holds it, +/// or field `j` of this fn's env for capture `j` of this fn. A binding with no +/// frame (a module binding) gets its box here, from `object` +/// +/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure +/// captures, do nothing. Gives the code and the new number of locals +pub fn replace_bindings( + code: &[Instr], + cpool: &[JsValue], + captures: &[Vec], + var_count: usize, +) -> (Vec, usize) { + use OpCode::*; + + let children = cpool.iter().filter_map(|c| match c { + JsValue::FnById(child) => Some(*child), + _ => None, + }); + + let mut locals = vec![]; + let mut args = vec![]; + + for c in children.flat_map(|child| &captures[child]) { + match c.flags.closure_kind { + ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)), + ClosureKind::Arg => args.push(c.var_idx), + _ => {} + } + } + + locals.sort_unstable(); + locals.dedup(); + args.sort_unstable(); + args.dedup(); + + // Value ids of bindings, as `is_var_read` gives them: args after every local + let new_var_count = var_count + args.len(); + let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k; + + // The local that holds the box of each captured binding, by value id + let boxes: HashMap = (locals.iter().map(|&(k, _)| (k, k))) + .chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i))) + .collect(); + + let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32)); + let value = ImmOperand::Atom(BOX_FIELD); + let none = ImmOperand::None; + let loc = |k: usize| ImmOperand::Loc(k as u16); + + let mut out = Vec::with_capacity(code.len()); + + { + let mut push = pusher(usize::MAX, &mut out); + + for &(k, lexical) in &locals { + push(Object, none); + + if !lexical { + push(Undefined, none); + push(DefineField, value); + } + + push(PutLoc, loc(k)); + } + + for (i, &k) in args.iter().enumerate() { + push(Object, none); + push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16)); + push(DefineField, value); + push(PutLoc, loc(var_count + i)); + } + } + + for instr in code { + let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v)); + let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v)); + let own_box = |k: ImmOperand| match k { + ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(), + _ => None, + }; + let mut push = pusher(instr.pc, &mut out); + + if let Some(&b) = read { + push(GetLoc, loc(b)); + push(GetField, value); + } else if let Some(&b) = write { + if instr.op.info().n_push == 1 { + push(Dup, none); + } + + push(GetLoc, loc(b)); + push(Swap, none); + push(PutField, value); + } else if let Some((i, writes, leaves)) = var_ref_access(instr) { + if leaves { + push(Dup, none); + } + + push(GetArg, ImmOperand::Arg(ENV_ARG)); + push(GetField, slot(i)); + + if writes { + push(Swap, none); + push(PutField, value); + } else { + push(GetField, value); + } + } else if instr.op == SetLocUninitialized { + push(Nop, none); + } else if instr.op == CloseLoc { + match own_box(instr.imm) { + Some(b) => { + push(Object, none); + push(GetLoc, loc(b)); + push(GetField, value); + push(DefineField, value); + push(PutLoc, loc(b)); + } + None => push(Nop, none), + } + } else if matches!(instr.op, Fclosure8 | Fclosure) { + let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else { + unreachable!() + }; + + push(Fclosure, instr.imm); + + for (i, c) in captures[child].iter().enumerate() { + match c.flags.closure_kind { + ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])), + ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])), + ClosureKind::Ref => { + push(GetArg, ImmOperand::Arg(ENV_ARG)); + push(GetField, slot(c.var_idx)); + } + _ => push(Object, none), + } + + push(DefineField, slot(i)); + } + } else { + push(instr.op, instr.imm); + } + } + + (out, new_var_count) +} + +/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes, +/// and whether a write also leaves the value on the stack +fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> { + use OpCode::*; + + let i = match instr.imm { + ImmOperand::VarRef(i) => i as usize, + _ => 0, + }; + + match instr.op { + GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)), + PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)), + SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)), + GetVarRef | GetVarRefCheck => Some((i, false, false)), + PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)), + SetVarRef => Some((i, true, true)), + _ => None, + } +} diff --git a/compiler/src/transform/expand.rs b/compiler/src/transform/expand.rs new file mode 100644 index 0000000..bee51d5 --- /dev/null +++ b/compiler/src/transform/expand.rs @@ -0,0 +1,71 @@ +use super::{ENV_ARG, PROTO_FIELD, pusher}; +use crate::{ + libqjs::parse::{Atom, ImmOperand, Instr, JsValue, opcodes::OpCode}, + native, +}; + +/// Replace ops that are a fixed sequence of simpler ones +/// +/// A sequence is never empty: jumps find their target by its pc (see regify), +/// so every instr must keep at least one instr at its pc +pub fn replace_expansion(instr: &Instr, out: &mut Vec) -> bool { + 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)], + // 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 }) => { + assert!(arg & 3 == 0, "getters and setters are not supported"); + &[(DefineField, ImmOperand::Atom(atom))] + } + // The fn itself, which is its env + (SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))], + // [[HomeObject]], which only `super` reads. `super` is not supported + // (see set_home_object in regify) + (SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)], + // arguments, new.target, import.meta + (SpecialObject, _) => unimplemented!("special_object {imm:?}"), + _ => return false, + }; + + let mut push = pusher(instr.pc, out); + seq.iter().for_each(|&(op, imm)| push(op, imm)); + true +} + +/// Replace a read of a global with the constant it is (see `native`) +pub fn replace_get_var( + instr: &Instr, + atoms: &[Atom], + cpool: &mut Vec, + out: &mut Vec, +) -> bool { + if instr.op != OpCode::GetVar { + return false; + } + + cpool.push(native::global(instr.imm.atom(), atoms)); + pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1)); + true +} diff --git a/compiler/src/transform/mod.rs b/compiler/src/transform/mod.rs index 629e376..d299af2 100644 --- a/compiler/src/transform/mod.rs +++ b/compiler/src/transform/mod.rs @@ -1,16 +1,18 @@ +mod args; +mod bindings; mod declass; +mod expand; +mod new; -use std::collections::HashMap; - +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, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode, - }, - native, - regify::{is_var_read, is_var_write}, + libqjs::parse::{Atom, AtomRef, ClosureVar, ImmOperand, Instr, opcodes::OpCode}, }; /// The field an object's prototype is kept in. In JS, `o.__proto__` is the @@ -88,361 +90,3 @@ fn transform_one(mut f: FlatFn, captures: &[Vec], atoms: &[Atom]) -> fn pusher(pc: usize, out: &mut Vec) -> impl FnMut(OpCode, ImmOperand) + '_ { move |op, imm| out.push(Instr { pc, op, imm }) } - -// ───────────────────────────────────────────────── New Processing ────────────────────────────────────────────────────────────── - -/// If `instr` is a `new`, the number of args it passes -fn is_new(instr: &Instr) -> Option { - match (instr.op, instr.imm) { - (OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize), - _ => None, - } -} - -/// Replace: -/// call_constructor n [F, new_target, a0 .. an-1] -/// -/// With: -/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top -/// drop new_target, always a copy of F here -/// dup [F, F] -/// get_field prototype [F, proto] -/// object [F, proto, obj] -/// swap [F, obj, proto] -/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype } -/// get_loc t0 .. get_loc t(n-1) the args back -/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj -/// -/// Whether `new` gives the call's result or `obj` depends on types (see infer) -fn replace_new(instr: &Instr, temps: usize, out: &mut Vec) -> bool { - let Some(n) = is_new(instr) else { - return false; - }; - - let mut push = pusher(instr.pc, out); - let temp = |i: usize| ImmOperand::Loc((temps + i) as u16); - - for i in (0..n).rev() { - push(OpCode::PutLoc, temp(i)); - } - - push(OpCode::Drop, ImmOperand::None); - push(OpCode::Dup, ImmOperand::None); - push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD)); - push(OpCode::Object, ImmOperand::None); - push(OpCode::Swap, ImmOperand::None); - push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD)); - - for i in 0..n { - push(OpCode::GetLoc, temp(i)); - } - - push(OpCode::CallConstructor, instr.imm); - true -} - -// ─────────────────────────────────────────────── Binding Processing ──────────────────────────────────────────────────────────── - -/// Put every binding that a closure captures in a box: an object whose -/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The -/// owner and the closures read and write the box. A closure keeps the boxes -/// it captured in its own fields, one for each of its `closure_vars`: field `i` -/// holds the box of capture `i` -/// -/// The fn makes a box for each binding that its children capture, at entry. -/// A captured local holds its box in its own slot. A captured arg keeps its -/// box in a new local, and the box starts with the arg's value. A `var` starts -/// as undefined. A `let` or `const` has no value until it is written (its TDZ): -/// object; undefined; define_field ; put_loc k each captured `var` -/// object; put_loc k each captured `let` -/// object; get_arg a; define_field ; put_loc box each captured arg -/// -/// A scope that is entered again is a loop body. QuickJS closes each captured -/// binding of the scope (`close_loc`) at every way out of it, so a new box -/// there gives each iteration its own binding. `set_loc_uninitialized` only -/// marks the TDZ, which is not checked, and hoisted fns are made before it, so -/// it does not make a box -/// -/// These instrs take a pc that no jump goes to, so a jump to the fn's first -/// instr does not make them again -/// -/// Then it replaces, for a captured binding: -/// get_loc k get_loc box; get_field -/// put_loc k get_loc box; swap; put_field -/// set_loc k dup; get_loc box; swap; put_field -/// close_loc k object; get_loc k; get_field ; -/// define_field ; put_loc k -/// a way out of a loop body: a new binding, with -/// the current value -/// -/// For any binding: -/// get_var_ref i get_arg env; get_field i; get_field -/// put_var_ref i get_arg env; get_field i; swap; put_field -/// set_var_ref i dup; get_arg env; get_field i; swap; put_field -/// fclosure f fclosure f; (push_box; define_field i)* -/// -/// where `push_box` is the box of capture `i` of `f`: the local that holds it, -/// or field `j` of this fn's env for capture `j` of this fn. A binding with no -/// frame (a module binding) gets its box here, from `object` -/// -/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure -/// captures, do nothing. Gives the code and the new number of locals -fn replace_bindings( - code: &[Instr], - cpool: &[JsValue], - captures: &[Vec], - var_count: usize, -) -> (Vec, usize) { - use OpCode::*; - - let children = cpool.iter().filter_map(|c| match c { - JsValue::FnById(child) => Some(*child), - _ => None, - }); - - let mut locals = vec![]; - let mut args = vec![]; - - for c in children.flat_map(|child| &captures[child]) { - match c.flags.closure_kind { - ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)), - ClosureKind::Arg => args.push(c.var_idx), - _ => {} - } - } - - locals.sort_unstable(); - locals.dedup(); - args.sort_unstable(); - args.dedup(); - - // Value ids of bindings, as `is_var_read` gives them: args after every local - let new_var_count = var_count + args.len(); - let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k; - - // The local that holds the box of each captured binding, by value id - let boxes: HashMap = (locals.iter().map(|&(k, _)| (k, k))) - .chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i))) - .collect(); - - let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32)); - let value = ImmOperand::Atom(BOX_FIELD); - let none = ImmOperand::None; - let loc = |k: usize| ImmOperand::Loc(k as u16); - - let mut out = Vec::with_capacity(code.len()); - - { - let mut push = pusher(usize::MAX, &mut out); - - for &(k, lexical) in &locals { - push(Object, none); - - if !lexical { - push(Undefined, none); - push(DefineField, value); - } - - push(PutLoc, loc(k)); - } - - for (i, &k) in args.iter().enumerate() { - push(Object, none); - push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16)); - push(DefineField, value); - push(PutLoc, loc(var_count + i)); - } - } - - for instr in code { - let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v)); - let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v)); - let own_box = |k: ImmOperand| match k { - ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(), - _ => None, - }; - let mut push = pusher(instr.pc, &mut out); - - if let Some(&b) = read { - push(GetLoc, loc(b)); - push(GetField, value); - } else if let Some(&b) = write { - if instr.op.info().n_push == 1 { - push(Dup, none); - } - - push(GetLoc, loc(b)); - push(Swap, none); - push(PutField, value); - } else if let Some((i, writes, leaves)) = var_ref_access(instr) { - if leaves { - push(Dup, none); - } - - push(GetArg, ImmOperand::Arg(ENV_ARG)); - push(GetField, slot(i)); - - if writes { - push(Swap, none); - push(PutField, value); - } else { - push(GetField, value); - } - } else if instr.op == SetLocUninitialized { - push(Nop, none); - } else if instr.op == CloseLoc { - match own_box(instr.imm) { - Some(b) => { - push(Object, none); - push(GetLoc, loc(b)); - push(GetField, value); - push(DefineField, value); - push(PutLoc, loc(b)); - } - None => push(Nop, none), - } - } else if matches!(instr.op, Fclosure8 | Fclosure) { - let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else { - unreachable!() - }; - - push(Fclosure, instr.imm); - - for (i, c) in captures[child].iter().enumerate() { - match c.flags.closure_kind { - ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])), - ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])), - ClosureKind::Ref => { - push(GetArg, ImmOperand::Arg(ENV_ARG)); - push(GetField, slot(c.var_idx)); - } - _ => push(Object, none), - } - - push(DefineField, slot(i)); - } - } else { - push(instr.op, instr.imm); - } - } - - (out, new_var_count) -} - -/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes, -/// and whether a write also leaves the value on the stack -fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> { - use OpCode::*; - - let i = match instr.imm { - ImmOperand::VarRef(i) => i as usize, - _ => 0, - }; - - match instr.op { - GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)), - PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)), - SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)), - GetVarRef | GetVarRefCheck => Some((i, false, false)), - PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)), - SetVarRef => Some((i, true, true)), - _ => None, - } -} - -// ──────────────────────────────────────────────── Arg Access Processing ──────────────────────────────────────────────────────── - -/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`, -/// and `this` with a read of its param -fn replace_arg_access(instr: &Instr, out: &mut Vec) -> bool { - use OpCode::*; - - let (op, arg) = match (instr.op, instr.imm) { - (PushThis, _) => (GetArg, THIS_ARG), - (GetArg0, _) => (GetArg, FIRST_ARG), - (GetArg1, _) => (GetArg, FIRST_ARG + 1), - (GetArg2, _) => (GetArg, FIRST_ARG + 2), - (GetArg3, _) => (GetArg, FIRST_ARG + 3), - (PutArg0, _) => (PutArg, FIRST_ARG), - (PutArg1, _) => (PutArg, FIRST_ARG + 1), - (PutArg2, _) => (PutArg, FIRST_ARG + 2), - (PutArg3, _) => (PutArg, FIRST_ARG + 3), - (SetArg0, _) => (SetArg, FIRST_ARG), - (SetArg1, _) => (SetArg, FIRST_ARG + 1), - (SetArg2, _) => (SetArg, FIRST_ARG + 2), - (SetArg3, _) => (SetArg, FIRST_ARG + 3), - (GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k), - _ => return false, - }; - - pusher(instr.pc, out)(op, ImmOperand::Arg(arg)); - true -} - -// ───────────────────────────────────────────────── Misc Expansion ────────────────────────────────────────────────────────────── - -/// 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 -fn replace_expansion(instr: &Instr, out: &mut Vec) -> bool { - use OpCode::*; - - let none = ImmOperand::None; - let imm = instr.imm; - - // A plain call has no `this`, so it passes `undefined`. It is pushed last, - // on top of the args, and the call pops it as one more arg (see regify) - let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))]; - let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)]; - - let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) { - (Call0, _) => &plain_call(0), - (Call1, _) => &plain_call(1), - (Call2, _) => &plain_call(2), - (Call3, _) => &plain_call(3), - (Call, ImmOperand::Npop(n)) => &plain_call(n), - (TailCall, ImmOperand::Npop(n)) => &tail_call(n), - (TailCallMethod, _) => &[(CallMethod, imm), (Return, none)], - (GetField2, _) => &[(Dup, none), (GetField, imm)], - (GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)], - (IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)], - (DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)], - // 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 }) => { - assert!(arg & 3 == 0, "getters and setters are not supported"); - &[(DefineField, ImmOperand::Atom(atom))] - } - // The fn itself, which is its env - (SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))], - // [[HomeObject]], which only `super` reads. `super` is not supported - // (see set_home_object in regify) - (SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)], - // arguments, new.target, import.meta - (SpecialObject, _) => unimplemented!("special_object {imm:?}"), - _ => return false, - }; - - let mut push = pusher(instr.pc, out); - seq.iter().for_each(|&(op, imm)| push(op, imm)); - true -} - -/// Replace a read of a global with the constant it is (see `native`) -fn replace_get_var( - instr: &Instr, - atoms: &[Atom], - cpool: &mut Vec, - out: &mut Vec, -) -> bool { - if instr.op != OpCode::GetVar { - return false; - } - - cpool.push(native::global(instr.imm.atom(), atoms)); - pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1)); - true -} diff --git a/compiler/src/transform/new.rs b/compiler/src/transform/new.rs new file mode 100644 index 0000000..6b717dc --- /dev/null +++ b/compiler/src/transform/new.rs @@ -0,0 +1,52 @@ +use super::{PROTO_FIELD, PROTOTYPE_FIELD, pusher}; +use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode}; + +/// If `instr` is a `new`, the number of args it passes +pub fn is_new(instr: &Instr) -> Option { + match (instr.op, instr.imm) { + (OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize), + _ => None, + } +} + +/// Replace: +/// call_constructor n [F, new_target, a0 .. an-1] +/// +/// With: +/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top +/// drop new_target, always a copy of F here +/// dup [F, F] +/// get_field prototype [F, proto] +/// object [F, proto, obj] +/// swap [F, obj, proto] +/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype } +/// get_loc t0 .. get_loc t(n-1) the args back +/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj +/// +/// Whether `new` gives the call's result or `obj` depends on types (see infer) +pub fn replace_new(instr: &Instr, temps: usize, out: &mut Vec) -> bool { + let Some(n) = is_new(instr) else { + return false; + }; + + let mut push = pusher(instr.pc, out); + let temp = |i: usize| ImmOperand::Loc((temps + i) as u16); + + for i in (0..n).rev() { + push(OpCode::PutLoc, temp(i)); + } + + push(OpCode::Drop, ImmOperand::None); + push(OpCode::Dup, ImmOperand::None); + push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD)); + push(OpCode::Object, ImmOperand::None); + push(OpCode::Swap, ImmOperand::None); + push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD)); + + for i in 0..n { + push(OpCode::GetLoc, temp(i)); + } + + push(OpCode::CallConstructor, instr.imm); + true +} diff --git a/docs/model.md b/docs/model.md index 7fa427e..ddd4bd6 100644 --- a/docs/model.md +++ b/docs/model.md @@ -36,7 +36,7 @@ A representation is correct if no program can see a difference from these semant With no types and no analysis, this representation is correct for every program that uses only supported ops. -- **Every value is an `any_t`:** a 64-bit NaN-boxed word. It holds a double, or a tag and a payload. An object in an `any_t` is a pointer to its cell, so it keeps its identity. +- **Every value is an `any_t`:** a tagged union of the kinds. An object in an `any_t` is a pointer to its cell, so it keeps its identity. - **Every object is a cell on the heap.** Each object starts with a header: the site that created it. The site tells the object's shape and, for a fn, its code. Each field starts as absent. - **Every captured binding is a box:** a cell on the heap with one slot. - The box is made when the fn is entered. @@ -49,81 +49,121 @@ With no types and no analysis, this representation is correct for every program ## 3. Types -A type is the set of values that a place can hold at runtime. Infer computes one type for each value id in each variant (section 3.4). +A type tells which values a place can hold at runtime. Infer computes one type for each value id in each variant (section 3.5). -### 3.1 The lattice +### 3.1 The types -A type is a set of kinds, with a refinement for each kind that is in the set: +A type is one of these: -| kind | refinement | +| type | values | |---|---| -| undefined | none (one value) | -| null | none (one value) | -| bool | the value, if it is known | -| number | int32 or double (provisional: the number type is not decided) | -| string | the value, if it is known | -| object | its shape | +| `Unknown` | none: no value has reached the place yet | +| `Undefined` | undefined | +| `Null` | null | +| `Bool(v)` | a bool. `v` is the value, if it is known | +| `Int` | an int32 (provisional: the number type is not decided) | +| `Float` | a number | +| `Str(v)` | a string. `v` is the value, if it is known | +| `Obj(site)` | an object that `site` creates. A fn is an object whose site has code | +| `Sum(id)` | a value of one of the types in a list: `Int \| Str`, `Obj(A) \| Null`, `Obj(A) \| Obj(B)` | +| `Any` | any value. No rule gives it now (section 3.4) | -- **Join** is the union, kind by kind. Two different known values join to "not known". int32 joins with double to give double. Two shapes join by a merge (section 3.3). -- **Bottom** is the empty set. No value reaches the place, so the code cannot run. +**Join:** + +``` + Unknown * T = T + Any * T = Any + T * T = T + Bool(a) * Bool(b) = Bool, the value not known. The same for Str + Int * Float = Float + T * U = T | U +``` + +**Sums.** A sum is a list of the types above, except `Unknown`, `Sum` and `Any`. It has at most one type of each kind, except objects, which have one `Obj` for each site. A join of two sums is the union of their lists, where two types of one kind join by the rules above. So `Int | Str` joined with `Float` is `Float | Str`. Each list is kept once, by number, in the site table (`SiteTable`), which also holds a row for each site. There are finitely many kinds and sites, so there are finitely many sums. + +**Members.** The members of a sum are the types in its list. The only member of any other type is the type itself, and `Unknown` has none. Every rule in infer works member by member, and joins the results (section 3.3). ### 3.2 The invariant At every program point, every runtime value of a place is in the place's type. -Every rule in infer must keep this invariant. Every rule is monotone, and a rule with an empty operand gives an empty result. Infer starts from bottom and computes a fixed point of the rules. The invariant needs a fixed point, not the least one, so a merge (section 3.3), which can give more than the least result, is safe. +Every rule in infer must keep this invariant. Every rule is monotone, and a rule with an `Unknown` operand gives `Unknown`. Infer starts from `Unknown` everywhere and computes a fixed point of the rules. -### 3.3 Shapes +### 3.3 Objects -- **A site** is the op that creates an object. Each site starts with its own shape. -- **A shape** gives, for its objects: the type of each own field, the type of the prototype, and the set of fns that they can run (empty if they are not fns). -- **A field type includes absent,** an internal marker that is not a JS value, unless definite initialization removes it (section 7). A field that no write reaches is only absent. -- **A merge** is the result when two object types join. The two shapes become one. Each field type, the prototypes and the fn sets are joined. A merge costs precision, but it keeps the invariant. -- **A field read** gives the own field's type without absent. If the field can be absent, it also gives the read on the prototype. At the end of the chain, an absent field gives undefined. -- **A field write** joins the value's type into the field's type in the shape. So every value that holds an object of the shape sees the write. +- **A site** is an op that creates an object (`object`, `fclosure`), or a built-in fn. All objects of a site share one type for each field, for the whole program. Two sites are never merged: objects of two sites in one place give `Any`. +- **A field's type** is the join of every value written to it. A field can be absent, unless every object of the site gets it when it is created (section 7). A field that no write reaches is only absent. +- **A field read** on `Obj(site)` gives the own field's type. If the field can be absent, it also gives the read on the prototype (the `__proto__` field). The chain ends at a prototype that is absent or not an object, where the read gives `Undefined`. So a field that can be absent reads as its type `| Undefined`. +- **A field write** on `Obj(site)` joins the value's type into the site's field. So every value that holds an object of the site sees the write. +- **A primitive** has no own fields. A read on it reads its kind's prototype, which the stdlib links with `__link_proto_*`. +- **A field read or write on undefined or null** throws (section 8), so it gives `Unknown`. +- **An array** is an object too, of a site whose kind is "array": `array_from` makes it. Besides its fields, the site has an **element type**: the join of every value written to an element. So the type of an array is `Obj(site)`, and its site says what its elements can be (written `[T]` for short). + - `a[i]` gives `T | Undefined`: an index can be a hole, or past the end. + - `a[i] = x` joins the type of `x` into `T`. An index past the end makes the array longer, and the indexes between are holes. + - `a.length` is an `Int`, and it is not a field. A write of it truncates the array, or makes it longer with holes. + - An index is a number. Any other key is a computed field name (section 8). + - An array's prototype is the one that the stdlib links for arrays (`__link_proto_array`), as for a primitive kind. Its methods are JS in the stdlib, on top of the three ops above. +- **On a sum,** a read, a write or a call works on each member, and joins the results. So a read on `Obj(A) | Null` gives the read on `A`: the `Null` member throws, and gives nothing. -### 3.4 Calls +### 3.4 `Any` -- **A call** of an object runs one of the fns in its shape's fn set. Infer walks each fn in the set with the arg types, and joins the results. A call of a value that is not a fn throws (section 8). -- **The program is closed.** Every fn comes from an `fclosure`, or it is a native. So the fn set of a call is always known. -- **A variant** is one walk of a fn with one tuple of arg types. The lattice has finite height, so there are finitely many variants. -- **Rounds:** infer walks every variant from the entry again, until a round changes nothing in the shapes, the prototypes or the returns. A call of a variant that is still being walked (recursion) gives what that variant returned in the round before. When a round changes nothing, these returns are the fixed point. +Since sums, no join gives `Any`, so no value has the type `Any`. The rules for it stay, so that it can be used again (for example, as a limit on the size of a sum). + +Code that holds an `Any` does not know the site of an object in it. So infer tracks which objects can be in an `Any`: + +- **The mark.** When a value of type `Obj(site)` goes into an `Any`, the site is marked `in_any`. A value goes into an `Any` in a join that gives `Any`, and when it is an operand or a result of a read, write or call on `Any`. The mark spreads to the sites of the objects in the site's fields, because code that holds the `Any` can read them. +- **A read on `Any`** reads the field on every `in_any` site and on every primitive prototype. What it reads goes into the `Any` that it gives. +- **A write on `Any`** writes the field on every `in_any` site, and sets their `written` flag (section 5). +- **A call on `Any`** calls the code of every `in_any` site that has code, in the variant whose args are all `Any`, except the env, which is the fn object itself (`Obj(site)`). It gives `Any`. + +So every object that code with an `Any` can reach is of an `in_any` site, and the C can choose the code by the site in the object's header. + +### 3.5 Calls + +- **A call** of `Obj(site)` runs the site's code, with the fn object itself as its env. A call of a sum calls each `Obj` member with its own env, and joins the results. A call of a value that is not a fn throws (section 8). A call of `Any` is in section 3.4. +- **The program is closed.** Every fn comes from an `fclosure`, or it is a native. So the code of every site is known. +- **A variant** is one walk of a fn with one tuple of arg types. A known value does not cross a call (`Bool(v)` becomes `Bool`), so there are finitely many variants. +- **Rounds:** infer walks every variant from the entry again, until a round changes nothing in the fields, the flags, the prototypes or the returns. A call of a variant that is still being walked (recursion) gives what that variant returned in the round before. When a round changes nothing, these returns are the fixed point. ## 4. Representation -The C representation of a value is a function of its type and of the value rule (section 5) only. +The C representation of a value is a function of its type and of the value rule (section 5) only. Each type has one: | type | C | |---|---| -| empty | the code cannot run | -| only undefined, or only null | nothing (a unit type) | -| bool | `bool` | -| number, int32 only | `int32_t` | -| number | `double` | -| string | `string_t` | -| object of a value shape (section 5) | `S`, the struct of its shape | -| object of a value shape, plus undefined, null or both | `S`, with a tag in its header for undefined and null | -| object of any other shape | `S *`, a pointer to the struct of its shape | -| object of any other shape, plus undefined, null or both | `S *`. `NULL` is null, and the address of a sentinel is undefined | -| string, plus undefined, null or both | `string_t`, with the same two values | -| any other set of kinds | `any_t` | +| `Unknown` | the code cannot run | +| `Undefined`, `Null` | `undefined_t`, `null_t`: unit types | +| `Bool` | `bool` | +| `Int` | `int32_t` | +| `Float` | `double` | +| `Str` | `string_t` | +| `Obj(site)`, a value shape (section 5) | `S`, the struct of the site | +| `Obj(site)`, any other shape | `S *`, a pointer to the struct of the site | +| `Str`, plus `Undefined` and/or `Null` | `string_t`. `NULL` is null, and `JSRT_UNDEFINED` is undefined | +| `Obj(site)`, plus `Undefined` and/or `Null` | `S *`, with the same two sentinels | +| `Obj` of several sites, with or without `Undefined` and `Null` | `jsrt_header *`, a pointer to the object's header, with the same two sentinels | +| any other sum | `any_t`. It is the tagged union of every kind, so it holds every sum | +| `Any` | `any_t`, a tagged union. An object in it is the pointer to its cell | -- **Undefined and null are kinds in the types, and immediates in the representation.** -- **Absent** is a field state, not a value. A field whose type includes absent has a representation with one more immediate for absent: a second sentinel address, or an `any_t` tag. -- **A widening conversion** (to a representation that holds more kinds) always exists and cannot fail. **A narrowing conversion** checks the kind. A failed check is a `TypeError` (section 8). No conversion copies an object. +- **An array** is a pointer to the struct of its site, whose header is followed by `length`, `cap` and `items`: an array of `cap` elements, of the representation of `T | Undefined`. A hole holds undefined. +- **Absent** is a field state, not a value. A field that can be absent needs one more immediate for absent: a sentinel address for `string_t` and `S *`, a header tag for `S`, and an `any_t` tag for every other type. +- **Conversions.** A type grows in a join, so most conversions go to a representation that holds more: a member into its sum, or `int32_t` into `double`. These cannot fail. A conversion to a representation that holds less checks the value, and a failed check is a `TypeError` (section 8). For example, a `string_t` that can be undefined, passed to a native's string param. +- **An op on a sum** tests which member the value is: a kind test on an `any_t`, a sentinel test for null and undefined, and a `switch` on the site in the header for objects. Each case then works on one member, as for a single type. A member on which the op throws (a field read on null) has no case, so it throws. +- **A read, a write or a call on `any_t`** of type `Any` is a helper that emit writes: a `switch` on the site in the object's header, with one case for each `in_any` site (section 3.4). Every field name is known at compile time, so no hash map is needed. One comes with computed field names (section 8). ## 5. Values: immutable shapes **The rule.** Section 1 gives the two ways to see a copy of an object: a write through one holder that another holder reads later, and an identity test. An object that is never written after its creation, and never in an identity test, has neither. So it behaves like a primitive: a copy of it and a shared pointer to it cannot be told apart. It can be a C struct value, copied freely, with any number of holders. -**The flags.** Infer sets two flags on each shape. Both only become true, and a merge ORs them. +**The flags.** Infer sets three flags on each site. Each flag only becomes true. -- **written:** a field write that is not a creation write reaches an object of the shape. A creation write is a `define_field` into the value that a creating op gives, before the object goes anywhere: an object literal's fields, a closure's captured boxes. Every `put_field` is a write after creation, and so is a `define_field` into anything else. -- **compared:** an object of the shape is an operand of `===`, `!==`, `==` or `!=`, and the other operand can also be an object. A test against undefined or null does not compare identity. +- **written:** a field write that is not a creation write reaches an object of the site. A creation write is a `define_field` into the value that a creating op gives, before the object goes anywhere: an object literal's fields, a closure's captured boxes. Regify turns a `define_field` into anything else (a class field on `this`, a static field on the class) into a `put_field`. So every `define_field` is a creation write, and every `put_field` is a write after creation. +- **compared:** an object of the site is an operand of `===`, `!==`, `==` or `!=`, and the other operand can also be an object (`Obj` or `Any`). +- **in_any:** an object of the site can be in an `Any` (section 3.4). Code that does not know the site can write it or compare it. -**Value shapes.** A shape is a value shape if neither flag is set, and it does not contain itself through value fields. A struct cannot contain itself, so a shape that does stays a pointer. An object of an immutable shape cannot hold itself, but objects of one shape can hold each other, as in a list. +**Value shapes.** A site has a value shape if no flag is set, it is not an array, it is not a member of a sum (an element of an array is in the sum `T | Undefined`), and it does not contain itself through value fields. A sum is one C type for all its members, so its object members are pointers. A struct cannot contain itself, so a site that does stays a pointer. An object of an immutable site cannot hold itself, but objects of one site can hold each other, as in a list. -**Representation.** An object of a value shape is a struct value (`S`), in its holder: a C local, a param, a return value, or the struct of the object whose field holds it. Its header's site is a tag for undefined, null or absent (`JSRT_SITE_*`), as the sentinel pointers are for other objects. In an `any_t`, it is a heap copy. +**Representation.** An object of a value shape is a struct value (`S`), in its holder: a C local, a param, a return value, or the struct of the object whose field holds it. In a field that can be absent, its header's site is the tag for absent (`JSRT_SITE_ABSENT`). It is not in a sum and not `in_any`, so it is never an `any_t` value. A runtime operation that only reads it (ToString, `<`) gets a heap copy. **Default.** An object of any other shape is a pointer to a cell (`S *`). @@ -161,16 +201,18 @@ A box is a cell, so the same rules apply to boxes. | narrowing | condition | if not proved | |---|---|---| -| no absent in a field | a write to the field comes before every read of it (definite initialization). Object-literal fields have this: QuickJS fills them with `define_field` before the object goes anywhere | the field's type includes absent | +| no absent in a field | a write to the field comes before every read of it (definite initialization). Object-literal fields have this: QuickJS fills them with `define_field` before the object goes anywhere | the field can be absent (section 3.3) | | a captured binding as a value | no write reaches the binding after its first capture. Most `const` bindings have this. A `const` that a hoisted fn captures before its declaration does not | a box | -| a direct call | the fn set has one fn | a `switch` on the fn | +| a direct call | the callee's type is `Obj(site)`: its code is known | a call on `Any`: a `switch` on the site (section 3.4) | ## 8. Out of scope These are not part of this model, and they need their own design: -- `throw`, `try`, `catch` and `finally`. These include the `TypeError`s above: a call of a value that is not a fn, and a failed narrowing check. +- narrowing in branches: in `if (x !== null) { x.f }`, `x` keeps its whole type. This is correct, because the `Null` member of the read throws, but the C still tests for null +- `throw`, `try`, `catch` and `finally`. These include the `TypeError`s above: a call of a value that is not a fn, and a failed check of a conversion from `any_t`. - the TDZ (a read of a `let` before its declaration) - fields with computed names (`o[k]`), `delete`, `in`. The absent state is the base that `in` and `delete` need. -- arrays. An array is an object whose `length` follows its integer fields, so it needs computed field names first. +- typed arrays, `ArrayBuffer` and `DataView` +- iterators: `for…of`, spread and destructuring - getters and setters diff --git a/lib/jsrt.h b/lib/jsrt.h deleted file mode 100644 index aa4c98f..0000000 --- a/lib/jsrt.h +++ /dev/null @@ -1,105 +0,0 @@ -// The runtime that emitted C is compiled against. Declarations only for now. -// The natives are declared by the emitted C itself, from compiler/src/native.rs -// See docs/model.md for the representation of values -#pragma once - -#include -#include -#include - -typedef struct { char _; } undefined_t; -typedef struct { char _; } null_t; - -typedef struct jsrt_string *string_t; - -// ────────────────────────────────────────────── cells ────────────────────────────────────────────── - -// The start of every object: the site that created it, which tells its shape -// and, for a fn, its code -typedef struct { uint32_t site; } jsrt_header; - -// The sites below 3 are tags. An object of a value shape is a struct, not a -// pointer, so its header tells if it is undefined, null or absent instead -#define JSRT_SITE_UNDEFINED 0 -#define JSRT_SITE_NULL 1 -#define JSRT_SITE_ABSENT 2 - -// A pointer to an object or a string can also be one of these. NULL is null -extern jsrt_header jsrt_undefined_cell, jsrt_absent_cell; -#define JSRT_UNDEFINED ((void *)&jsrt_undefined_cell) -#define JSRT_ABSENT ((void *)&jsrt_absent_cell) // a field that an object lacks - -// A new heap cell of `size` bytes -void *jsrt_alloc(size_t size); - -// Throws a TypeError. Exceptions are not supported yet, so it stops the program -_Noreturn void jsrt_type_error(void); - -// ───────────────────────────────────────────── strings ───────────────────────────────────────────── - -string_t jsrt_string_lit(const char *s); -int32_t jsrt_string_length(string_t s); -bool jsrt_string_eq(string_t a, string_t b); -int32_t jsrt_string_cmp(string_t a, string_t b); - -// ─────────────────────────────────────────────── any ─────────────────────────────────────────────── -// A value of any kind: a NaN-boxed 64-bit word. An object in it is the pointer -// to its cell, so it keeps its identity. A field can also hold absent -typedef uint64_t any_t; - -// Box a value. A pointer that is a sentinel (NULL, JSRT_UNDEFINED, -// JSRT_ABSENT) boxes as null, undefined or absent -any_t __any_undefined(void); -any_t __any_null(void); -any_t __any_absent(void); -any_t __any_from_bool(bool b); -any_t __any_from_int(int32_t n); -any_t __any_from_float(double x); -any_t __any_from_string(string_t s); -any_t __any_from_object(void *o); - -// Unbox a value. A value of another kind throws. A string or an object unboxes -// null, undefined and absent to their sentinels -bool __any_as_bool(any_t a); -int32_t __any_as_int(any_t a); -double __any_as_float(any_t a); // an int too -string_t __any_as_string(any_t a); -void *__any_as_object(any_t a); - -// The kind of a value -bool __any_is_absent(any_t a); -bool __any_is_undefined(any_t a); -bool __any_is_null(any_t a); -bool __any_is_nullish(any_t a); // undefined or null -bool __any_is_boolean(any_t a); -bool __any_is_number(any_t a); -bool __any_is_string(any_t a); -bool __any_is_object(any_t a); // a fn too -bool __any_is_function(any_t a); - -// JS's operators, with JS's conversions -any_t __any_add(any_t a, any_t b); -any_t __any_sub(any_t a, any_t b); -any_t __any_mul(any_t a, any_t b); -any_t __any_div(any_t a, any_t b); -any_t __any_mod(any_t a, any_t b); -any_t __any_pow(any_t a, any_t b); -any_t __any_neg(any_t a); -any_t __any_plus(any_t a); // ToNumber -any_t __any_inc(any_t a); -any_t __any_dec(any_t a); - -bool __any_lt(any_t a, any_t b); -bool __any_lte(any_t a, any_t b); -bool __any_gt(any_t a, any_t b); -bool __any_gte(any_t a, any_t b); -bool __any_eq(any_t a, any_t b); -bool __any_neq(any_t a, any_t b); -bool __any_strict_eq(any_t a, any_t b); -bool __any_strict_neq(any_t a, any_t b); - -// JS's abstract operations -bool __any_truthy(any_t a); // ToBoolean -int32_t __any_to_int32(any_t a); // ToInt32 -string_t __any_to_string(any_t a); // ToString -string_t __any_typeof(any_t a); diff --git a/lib/rt/any.h b/lib/rt/any.h new file mode 100644 index 0000000..7436777 --- /dev/null +++ b/lib/rt/any.h @@ -0,0 +1,299 @@ +// any_t: a value of any kind, and JS's operators on it +#pragma once + +#include "number.h" + +// A value of any kind. An object in it is the pointer to its cell, so it keeps +// its identity. A field can also hold absent + +typedef enum { + JSRT_KIND_ABSENT, + JSRT_KIND_UNDEFINED, + JSRT_KIND_NULL, + JSRT_KIND_BOOL, + JSRT_KIND_INT, + JSRT_KIND_FLOAT, + JSRT_KIND_STRING, + JSRT_KIND_OBJECT, +} jsrt_kind; + +typedef struct { + jsrt_kind kind; + union { + bool b; + int32_t i; + double f; + string_t s; + void *o; + } as; +} any_t; + +// Box a value. A pointer that is a sentinel (NULL, JSRT_UNDEFINED, +// JSRT_ABSENT) boxes as null, undefined or absent +any_t __any_undefined(void); +any_t __any_null(void); +any_t __any_absent(void); +any_t __any_from_bool(bool b); +any_t __any_from_int(int32_t n); +any_t __any_from_float(double x); +any_t __any_from_string(string_t s); +any_t __any_from_object(void *o); + +// Unbox a value. A value of another kind throws. A string or an object unboxes +// null, undefined and absent to their sentinels +bool __any_as_bool(any_t a); +int32_t __any_as_int(any_t a); +double __any_as_float(any_t a); // an int too +string_t __any_as_string(any_t a); +void *__any_as_object(any_t a); + +// The kind of a value +bool __any_is_absent(any_t a); +bool __any_is_undefined(any_t a); +bool __any_is_null(any_t a); +bool __any_is_nullish(any_t a); // undefined or null +bool __any_is_boolean(any_t a); +bool __any_is_number(any_t a); +bool __any_is_string(any_t a); +bool __any_is_object(any_t a); // a fn too +bool __any_is_function(any_t a); + +// JS's operators, with JS's conversions +any_t __any_add(any_t a, any_t b); +any_t __any_sub(any_t a, any_t b); +any_t __any_mul(any_t a, any_t b); +any_t __any_div(any_t a, any_t b); +any_t __any_mod(any_t a, any_t b); +any_t __any_pow(any_t a, any_t b); +any_t __any_neg(any_t a); +any_t __any_plus(any_t a); // ToNumber +any_t __any_inc(any_t a); +any_t __any_dec(any_t a); + +bool __any_lt(any_t a, any_t b); +bool __any_lte(any_t a, any_t b); +bool __any_gt(any_t a, any_t b); +bool __any_gte(any_t a, any_t b); +bool __any_eq(any_t a, any_t b); +bool __any_neq(any_t a, any_t b); +bool __any_strict_eq(any_t a, any_t b); +bool __any_strict_neq(any_t a, any_t b); + +// JS's abstract operations +bool __any_truthy(any_t a); // ToBoolean +int32_t __any_to_int32(any_t a); // ToInt32 +string_t __any_to_string(any_t a); // ToString +string_t __any_typeof(any_t a); + +#ifdef JSRT_IMPLEMENTATION +#include + +// ── any: boxing ── + +any_t __any_undefined(void) { return (any_t){ .kind = JSRT_KIND_UNDEFINED }; } +any_t __any_null(void) { return (any_t){ .kind = JSRT_KIND_NULL }; } +any_t __any_absent(void) { return (any_t){ .kind = JSRT_KIND_ABSENT }; } +any_t __any_from_bool(bool b) { return (any_t){ .kind = JSRT_KIND_BOOL, .as.b = b }; } +any_t __any_from_int(int32_t n) { return (any_t){ .kind = JSRT_KIND_INT, .as.i = n }; } +any_t __any_from_float(double x) { return (any_t){ .kind = JSRT_KIND_FLOAT, .as.f = x }; } + +// A pointer that can be a sentinel, as the kind it stands for, else as `kind` +static any_t jsrt_from_ptr(void *p, jsrt_kind kind) { + if (p == NULL) return __any_null(); + if (p == JSRT_UNDEFINED) return __any_undefined(); + if (p == JSRT_ABSENT) return __any_absent(); + return (any_t){ .kind = kind, .as.o = p }; +} + +any_t __any_from_string(string_t s) { return jsrt_from_ptr(s, JSRT_KIND_STRING); } +any_t __any_from_object(void *o) { return jsrt_from_ptr(o, JSRT_KIND_OBJECT); } + +bool __any_as_bool(any_t a) { + if (a.kind != JSRT_KIND_BOOL) jsrt_type_error(); + return a.as.b; +} + +double __any_as_float(any_t a) { + if (a.kind == JSRT_KIND_INT) return a.as.i; + if (a.kind != JSRT_KIND_FLOAT) jsrt_type_error(); + return a.as.f; +} + +int32_t __any_as_int(any_t a) { + return a.kind == JSRT_KIND_INT ? a.as.i : (int32_t)__any_as_float(a); +} + +// A pointer of kind `kind`, or the sentinel of a nullish or absent value +static void *jsrt_as_ptr(any_t a, jsrt_kind kind) { + switch (a.kind) { + case JSRT_KIND_NULL: return NULL; + case JSRT_KIND_UNDEFINED: return JSRT_UNDEFINED; + case JSRT_KIND_ABSENT: return JSRT_ABSENT; + default: + if (a.kind != kind) jsrt_type_error(); + return a.as.o; + } +} + +string_t __any_as_string(any_t a) { return jsrt_as_ptr(a, JSRT_KIND_STRING); } +void *__any_as_object(any_t a) { return jsrt_as_ptr(a, JSRT_KIND_OBJECT); } + +// ── any: kinds ── + +bool __any_is_absent(any_t a) { return a.kind == JSRT_KIND_ABSENT; } +bool __any_is_undefined(any_t a) { return a.kind == JSRT_KIND_UNDEFINED || a.kind == JSRT_KIND_ABSENT; } +bool __any_is_null(any_t a) { return a.kind == JSRT_KIND_NULL; } +bool __any_is_nullish(any_t a) { return __any_is_undefined(a) || __any_is_null(a); } +bool __any_is_boolean(any_t a) { return a.kind == JSRT_KIND_BOOL; } +bool __any_is_number(any_t a) { return a.kind == JSRT_KIND_INT || a.kind == JSRT_KIND_FLOAT; } +bool __any_is_string(any_t a) { return a.kind == JSRT_KIND_STRING; } +bool __any_is_object(any_t a) { return a.kind == JSRT_KIND_OBJECT; } + +bool __any_is_function(any_t a) { + return a.kind == JSRT_KIND_OBJECT && jsrt_fn_sites[((jsrt_header *)a.as.o)->site]; +} + +// ── any: abstract operations ── + +// ToNumber. An object would call its `valueOf` or `toString` first, which the +// runtime cannot do, so it gives NaN +static double jsrt_to_number(any_t a) { + switch (a.kind) { + case JSRT_KIND_NULL: return 0; + case JSRT_KIND_BOOL: return a.as.b; + case JSRT_KIND_INT: return a.as.i; + case JSRT_KIND_FLOAT: return a.as.f; + case JSRT_KIND_STRING: return __native_string_to_number(a.as.s); + default: return NAN; + } +} + +bool __any_truthy(any_t a) { + switch (a.kind) { + case JSRT_KIND_BOOL: return a.as.b; + case JSRT_KIND_INT: return a.as.i != 0; + case JSRT_KIND_FLOAT: return a.as.f != 0 && !isnan(a.as.f); + case JSRT_KIND_STRING: return a.as.s->length != 0; + case JSRT_KIND_OBJECT: return true; + default: return false; + } +} + +int32_t __any_to_int32(any_t a) { + double x = jsrt_to_number(a); + + if (isnan(x) || isinf(x)) { + return 0; + } + + x = fmod(trunc(x), 4294967296.0); + return (int32_t)(uint32_t)(int64_t)(x < 0 ? x + 4294967296.0 : x); +} + +// ToString. An object would call its `toString`, which the runtime cannot do +string_t __any_to_string(any_t a) { + switch (a.kind) { + case JSRT_KIND_UNDEFINED: case JSRT_KIND_ABSENT: return jsrt_string_lit("undefined"); + case JSRT_KIND_NULL: return jsrt_string_lit("null"); + case JSRT_KIND_BOOL: return jsrt_string_lit(a.as.b ? "true" : "false"); + case JSRT_KIND_INT: return __native_number_to_string(a.as.i); + case JSRT_KIND_FLOAT: return __native_number_to_string(a.as.f); + case JSRT_KIND_STRING: return a.as.s; + default: return jsrt_string_lit(__any_is_function(a) ? "function" : "[object Object]"); + } +} + +string_t __any_typeof(any_t a) { + switch (a.kind) { + case JSRT_KIND_UNDEFINED: case JSRT_KIND_ABSENT: return jsrt_string_lit("undefined"); + case JSRT_KIND_BOOL: return jsrt_string_lit("boolean"); + case JSRT_KIND_INT: case JSRT_KIND_FLOAT: return jsrt_string_lit("number"); + case JSRT_KIND_STRING: return jsrt_string_lit("string"); + case JSRT_KIND_OBJECT: return jsrt_string_lit(__any_is_function(a) ? "function" : "object"); + default: return jsrt_string_lit("object"); + } +} + +// ── any: operators ── + +any_t __any_add(any_t a, any_t b) { + // An object becomes a string (see `__any_to_string`), so it concatenates + bool strings = a.kind == JSRT_KIND_STRING || b.kind == JSRT_KIND_STRING + || a.kind == JSRT_KIND_OBJECT || b.kind == JSRT_KIND_OBJECT; + + if (strings) { + return __any_from_string(__native_string_concat(__any_to_string(a), __any_to_string(b))); + } + + return __any_from_float(jsrt_to_number(a) + jsrt_to_number(b)); +} + +any_t __any_sub(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) - jsrt_to_number(b)); } +any_t __any_mul(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) * jsrt_to_number(b)); } +any_t __any_div(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) / jsrt_to_number(b)); } +any_t __any_mod(any_t a, any_t b) { return __any_from_float(fmod(jsrt_to_number(a), jsrt_to_number(b))); } +any_t __any_pow(any_t a, any_t b) { return __any_from_float(pow(jsrt_to_number(a), jsrt_to_number(b))); } +any_t __any_neg(any_t a) { return __any_from_float(-jsrt_to_number(a)); } +any_t __any_plus(any_t a) { return __any_from_float(jsrt_to_number(a)); } +any_t __any_inc(any_t a) { return __any_from_float(jsrt_to_number(a) + 1); } +any_t __any_dec(any_t a) { return __any_from_float(jsrt_to_number(a) - 1); } + +// ── any: comparisons ── + +// Two strings compare by code units. Anything else compares as numbers, where +// NaN makes every comparison false +bool __any_lt(any_t a, any_t b) { + bool strings = a.kind == JSRT_KIND_STRING && b.kind == JSRT_KIND_STRING; + return strings ? jsrt_string_cmp(a.as.s, b.as.s) < 0 : jsrt_to_number(a) < jsrt_to_number(b); +} + +bool __any_lte(any_t a, any_t b) { + bool strings = a.kind == JSRT_KIND_STRING && b.kind == JSRT_KIND_STRING; + return strings ? jsrt_string_cmp(a.as.s, b.as.s) <= 0 : jsrt_to_number(a) <= jsrt_to_number(b); +} + +bool __any_gt(any_t a, any_t b) { return __any_lt(b, a); } +bool __any_gte(any_t a, any_t b) { return __any_lte(b, a); } + +bool __any_strict_eq(any_t a, any_t b) { + if (__any_is_number(a) && __any_is_number(b)) { + return jsrt_to_number(a) == jsrt_to_number(b); + } + + if (__any_is_undefined(a) || __any_is_undefined(b)) { + return __any_is_undefined(a) && __any_is_undefined(b); + } + + if (a.kind != b.kind) { + return false; + } + + switch (a.kind) { + case JSRT_KIND_NULL: return true; + case JSRT_KIND_BOOL: return a.as.b == b.as.b; + case JSRT_KIND_STRING: return jsrt_string_eq(a.as.s, b.as.s); + default: return a.as.o == b.as.o; + } +} + +bool __any_strict_neq(any_t a, any_t b) { return !__any_strict_eq(a, b); } + +// Undefined and null equal each other and nothing else. Values of one kind, +// and objects, compare as `===` does. Other primitives compare as numbers +bool __any_eq(any_t a, any_t b) { + if (__any_is_nullish(a) || __any_is_nullish(b)) { + return __any_is_nullish(a) && __any_is_nullish(b); + } + + bool same = a.kind == b.kind || (__any_is_number(a) && __any_is_number(b)); + + if (same || a.kind == JSRT_KIND_OBJECT || b.kind == JSRT_KIND_OBJECT) { + return __any_strict_eq(a, b); + } + + return jsrt_to_number(a) == jsrt_to_number(b); +} + +bool __any_neq(any_t a, any_t b) { return !__any_eq(a, b); } + +#endif // JSRT_IMPLEMENTATION diff --git a/lib/rt/core.h b/lib/rt/core.h new file mode 100644 index 0000000..3a35ddf --- /dev/null +++ b/lib/rt/core.h @@ -0,0 +1,95 @@ +// Values and objects: the types that every other part uses, the object +// header and its tags, the allocator and errors. See docs/model.md +#pragma once + +#include +#include +#include + +typedef struct { char _; } undefined_t; +typedef struct { char _; } null_t; + +// A string: UTF-16 code units, as JS counts them +typedef struct jsrt_string *string_t; + +// The start of every object: the site that created it, which tells its shape +// and, for a fn, its code +typedef struct { uint32_t site; } jsrt_header; + +// The sites below 3 are tags. An object of a value shape is a struct, not a +// pointer, so its header tells if it is undefined, null or absent instead +#define JSRT_SITE_UNDEFINED 0 +#define JSRT_SITE_NULL 1 +#define JSRT_SITE_ABSENT 2 + +// A pointer to an object or a string can also be one of these. NULL is null +extern jsrt_header jsrt_undefined_cell, jsrt_absent_cell; +#define JSRT_UNDEFINED ((void *)&jsrt_undefined_cell) +#define JSRT_ABSENT ((void *)&jsrt_absent_cell) // a field that an object lacks + +// Whether the objects of each site are fns, by site. The emitted C defines it +extern const bool jsrt_fn_sites[]; + +// A new heap cell of `size` bytes, all zero +void *jsrt_alloc(size_t size); + +// `items`, an array's elements of `size` bytes each, with room for at least +// `need` of them. `*cap` is the room that it has, and is set to the new room +void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size); + +// Throws a TypeError. Exceptions are not supported yet, so it stops the program +_Noreturn void jsrt_type_error(void); + +// `p`, a pointer to an object or a string. A sentinel for null, undefined or +// absent throws a TypeError +void *jsrt_some(void *p); + +#ifdef JSRT_IMPLEMENTATION +#include +#include + +jsrt_header jsrt_undefined_cell = { JSRT_SITE_UNDEFINED }; +jsrt_header jsrt_absent_cell = { JSRT_SITE_ABSENT }; + +void *jsrt_alloc(size_t size) { + void *p = calloc(1, size); + + if (!p) { + fputs("jsrt: out of memory\n", stderr); + abort(); + } + + return p; +} + +void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size) { + if (need <= *cap) { + return items; + } + + int32_t room = *cap * 2 > need ? *cap * 2 : need < 4 ? 4 : need; + items = realloc(items, (size_t)room * size); + + if (!items) { + fputs("jsrt: out of memory\n", stderr); + abort(); + } + + *cap = room; + return items; +} + +_Noreturn void jsrt_type_error(void) { + fputs("TypeError\n", stderr); + exit(1); +} + +void *jsrt_some(void *p) { + if (p == NULL || p == JSRT_UNDEFINED || p == JSRT_ABSENT) { + jsrt_type_error(); + } + + return p; +} + +#endif // JSRT_IMPLEMENTATION diff --git a/lib/rt/io.h b/lib/rt/io.h new file mode 100644 index 0000000..8c57bc4 --- /dev/null +++ b/lib/rt/io.h @@ -0,0 +1,455 @@ +// IO natives. `__native_print_string` writes at once. The rest are async, on +// io_uring (see uring.h) +// +// A `__native_io_queue_` queues an op and gives its handle. It makes no +// syscall. `__native_io_wait` puts queued ops into the SQ, submits them, +// waits until one op is done, and gives its handle. The getters then read +// that op's result, until the next wait +// +// At most `cq_entries` ops are in flight, so the CQ cannot overflow. The rest +// stay queued until a later wait. So a queue never fails, and the program +// never sees a limit +// +// The ring is made at the first wait that has ops, so a program that does no +// async IO never makes it +#pragma once + +#include "str.h" + +#ifdef JSRT_IMPLEMENTATION +#include +#include +#include +#include +#include +#include +#include + +#include "uring.h" + +// ── sync ── + +undefined_t __native_print_string(string_t s) { + char *utf8 = jsrt_string_utf8(s); + puts(utf8); + free(utf8); + return (undefined_t){0}; +} + +// ── ops ── + +typedef enum { + JSRT_IO_TIMEOUT, + JSRT_IO_OPEN, + JSRT_IO_READ, + JSRT_IO_WRITE, + JSRT_IO_CLOSE, + JSRT_IO_STAT, + JSRT_IO_CANCEL, +} jsrt_io_kind; + +typedef enum { + JSRT_IO_FREE, // in the free list + JSRT_IO_QUEUED, // in the backlog, not in the SQ yet + JSRT_IO_FLIGHT, // in the SQ or in the kernel + JSRT_IO_DONE, // in the done list, or the last op a wait gave +} jsrt_io_state; + +// An op. Its handle is its index in the table, and its SQE's user_data +// +// The table moves when it grows, which only a queue does. So the kernel +// must not hold a pointer into it after a submit returns: `ts` is read +// before (SUBMIT_STABLE), and everything that the kernel reads or writes +// later is in `buf`, which does not move +typedef struct { + uint8_t kind, state; + bool cancelled; // cancelled while queued: never goes to the SQ + uint32_t gen; // counts reuses of this slot (see CANCEL) + int32_t next; // the next op in its list, or -1 + int32_t res; // the CQE's res: >= 0 on success, -errno on failure + + int32_t fd; // READ, WRITE, CLOSE + int32_t flags, mode; // OPEN + uint32_t len; // READ, WRITE: the size of `buf` + uint64_t off; // READ, WRITE: -1 is the file's position + int32_t target; // CANCEL: the op to cancel... + uint32_t target_gen; // ...while it is still this use of its slot + struct __kernel_timespec ts; // TIMEOUT + void *buf; // OPEN: path. READ, WRITE: data. STAT: statx, then path +} jsrt_io_op; + +// A FIFO of ops, linked through `next` +typedef struct { int32_t head, tail; } jsrt_io_list; + +static struct { + jsrt_uring ring; + bool ring_ready; + unsigned in_flight; + + jsrt_io_op *ops; + int32_t cap; + int32_t free; // a stack, linked through `next` + jsrt_io_list backlog, done; + int32_t given; // the op the last wait gave, or -1 +} jsrt_io = { + .free = -1, + .backlog = { -1, -1 }, + .done = { -1, -1 }, + .given = -1, +}; + +static void jsrt_io_push(jsrt_io_list *l, int32_t h) { + jsrt_io.ops[h].next = -1; + + if (l->tail < 0) { + l->head = h; + } else { + jsrt_io.ops[l->tail].next = h; + } + + l->tail = h; +} + +static int32_t jsrt_io_pop(jsrt_io_list *l) { + int32_t h = l->head; + + if (h >= 0) { + l->head = jsrt_io.ops[h].next; + + if (l->head < 0) { + l->tail = -1; + } + } + + return h; +} + +// A new op of `kind`, not in any list yet +static int32_t jsrt_io_new(jsrt_io_kind kind) { + if (jsrt_io.free < 0) { + int32_t cap = jsrt_io.cap ? jsrt_io.cap * 2 : 64; + jsrt_io_op *ops = realloc(jsrt_io.ops, (size_t)cap * sizeof *ops); + + if (!ops) { + fputs("jsrt: out of memory\n", stderr); + abort(); + } + + // The new slots go on the free stack, the lowest on top + for (int32_t h = cap - 1; h >= jsrt_io.cap; h--) { + ops[h] = (jsrt_io_op){ .state = JSRT_IO_FREE, .next = jsrt_io.free }; + jsrt_io.free = h; + } + + jsrt_io.ops = ops; + jsrt_io.cap = cap; + } + + int32_t h = jsrt_io.free; + jsrt_io_op *op = &jsrt_io.ops[h]; + jsrt_io.free = op->next; + *op = (jsrt_io_op){ .kind = kind, .state = JSRT_IO_QUEUED, .gen = op->gen + 1, .next = -1 }; + return h; +} + +static int32_t jsrt_io_queue(int32_t h) { + jsrt_io_push(&jsrt_io.backlog, h); + return h; +} + +// Ends op `h` with `res`, without the kernel +static int32_t jsrt_io_finish(int32_t h, int32_t res) { + jsrt_io.ops[h].state = JSRT_IO_DONE; + jsrt_io.ops[h].res = res; + jsrt_io_push(&jsrt_io.done, h); + return h; +} + +static void jsrt_io_release(int32_t h) { + jsrt_io_op *op = &jsrt_io.ops[h]; + free(op->buf); + op->buf = NULL; + op->state = JSRT_IO_FREE; + op->next = jsrt_io.free; + jsrt_io.free = h; +} + +// The op that the last wait gave. A handle of any other op is an error +static jsrt_io_op *jsrt_io_given(int32_t h) { + if (h < 0 || h != jsrt_io.given) { + jsrt_type_error(); + } + + return &jsrt_io.ops[h]; +} + +// 0 if op `h` still has work for the kernel, else the -errno it ends with +// +// An op that was cancelled while queued has none. Nor has a cancel whose +// target ended, because then the target's slot can hold a new op +static int32_t jsrt_io_dead(int32_t h) { + jsrt_io_op *op = &jsrt_io.ops[h]; + + if (op->cancelled) { + return -ECANCELED; + } + + if (op->kind == JSRT_IO_CANCEL) { + jsrt_io_op *t = &jsrt_io.ops[op->target]; + + if (t->gen != op->target_gen || t->state != JSRT_IO_FLIGHT) { + return -ENOENT; + } + } + + return 0; +} + +static void jsrt_io_prep(struct io_uring_sqe *sqe, int32_t h) { + jsrt_io_op *op = &jsrt_io.ops[h]; + sqe->user_data = (uint64_t)h; + + switch (op->kind) { + case JSRT_IO_TIMEOUT: + // A plain timer: `off` 0 means that no count of CQEs ends it + sqe->opcode = IORING_OP_TIMEOUT; + sqe->fd = -1; + sqe->addr = (uint64_t)(uintptr_t)&op->ts; + sqe->len = 1; + break; + + case JSRT_IO_OPEN: + sqe->opcode = IORING_OP_OPENAT; + sqe->fd = AT_FDCWD; + sqe->addr = (uint64_t)(uintptr_t)op->buf; + sqe->len = (uint32_t)op->mode; + sqe->open_flags = (uint32_t)op->flags; + break; + + case JSRT_IO_READ: + case JSRT_IO_WRITE: + sqe->opcode = op->kind == JSRT_IO_READ ? IORING_OP_READ : IORING_OP_WRITE; + sqe->fd = op->fd; + sqe->addr = (uint64_t)(uintptr_t)op->buf; + sqe->len = op->len; + sqe->off = op->off; + break; + + case JSRT_IO_CLOSE: + sqe->opcode = IORING_OP_CLOSE; + sqe->fd = op->fd; + break; + + case JSRT_IO_STAT: + sqe->opcode = IORING_OP_STATX; + sqe->fd = AT_FDCWD; + sqe->addr = (uint64_t)(uintptr_t)((char *)op->buf + sizeof(struct statx)); + sqe->len = STATX_SIZE; + sqe->addr2 = (uint64_t)(uintptr_t)op->buf; + break; + + case JSRT_IO_CANCEL: + // A timeout is removed with its own op + sqe->opcode = jsrt_io.ops[op->target].kind == JSRT_IO_TIMEOUT + ? IORING_OP_TIMEOUT_REMOVE + : IORING_OP_ASYNC_CANCEL; + sqe->fd = -1; + sqe->addr = (uint64_t)op->target; + break; + } +} + +// Moves queued ops into the SQ while fewer than `cq_entries` are in flight, +// and submits them +static void jsrt_io_drain(void) { + if (jsrt_io.backlog.head < 0) { + return; + } + + jsrt_uring *r = &jsrt_io.ring; + + if (!jsrt_io.ring_ready) { + jsrt_uring_init(r, 128); + jsrt_io.ring_ready = true; + } + + while (jsrt_io.backlog.head >= 0 && jsrt_io.in_flight < r->cq_entries) { + int32_t h = jsrt_io.backlog.head; + int32_t dead = jsrt_io_dead(h); + + if (dead) { + jsrt_io_pop(&jsrt_io.backlog); + jsrt_io_finish(h, dead); + continue; + } + + struct io_uring_sqe *sqe = jsrt_uring_sqe(r); + + // The SQ is full: a submit empties it + if (!sqe) { + jsrt_uring_submit(r, false); + continue; + } + + jsrt_io_pop(&jsrt_io.backlog); + jsrt_io_prep(sqe, h); + jsrt_io.ops[h].state = JSRT_IO_FLIGHT; + jsrt_io.in_flight++; + } + + // No SQE may stay unsubmitted when the wait returns, because a queue can + // then move the table (see jsrt_io_op) + jsrt_uring_submit(r, false); +} + +// ── natives ── + +int32_t __native_io_queue_timeout(double ms) { + int32_t h = jsrt_io_new(JSRT_IO_TIMEOUT); + + if (!(ms > 0)) { + ms = 0; + } + + jsrt_io.ops[h].ts.tv_sec = (int64_t)(ms / 1000); + jsrt_io.ops[h].ts.tv_nsec = (int64_t)(fmod(ms, 1000) * 1e6); + return jsrt_io_queue(h); +} + +// `flags` and `mode` are open(2)'s, as numbers +int32_t __native_io_queue_open(string_t path, int32_t flags, int32_t mode) { + int32_t h = jsrt_io_new(JSRT_IO_OPEN); + jsrt_io.ops[h].buf = jsrt_string_utf8(path); + jsrt_io.ops[h].flags = flags | O_CLOEXEC; + jsrt_io.ops[h].mode = mode; + return jsrt_io_queue(h); +} + +// Reads at most `len` bytes at `offset`, or at the file's position if +// `offset` is -1. The status is the number of bytes read +int32_t __native_io_queue_read(int32_t fd, int32_t len, double offset) { + int32_t h = jsrt_io_new(JSRT_IO_READ); + jsrt_io_op *op = &jsrt_io.ops[h]; + op->fd = fd; + op->len = len > 0 ? (uint32_t)len : 0; + op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset; + op->buf = jsrt_alloc(op->len ? op->len : 1); + return jsrt_io_queue(h); +} + +// Writes `data` as UTF-8, at `offset` as for a read. The status is the number +// of bytes written, which can be fewer than all +int32_t __native_io_queue_write(int32_t fd, string_t data, double offset) { + int32_t h = jsrt_io_new(JSRT_IO_WRITE); + jsrt_io_op *op = &jsrt_io.ops[h]; + op->fd = fd; + op->buf = jsrt_string_utf8(data); + op->len = (uint32_t)strlen(op->buf); + op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset; + return jsrt_io_queue(h); +} + +int32_t __native_io_queue_close(int32_t fd) { + int32_t h = jsrt_io_new(JSRT_IO_CLOSE); + jsrt_io.ops[h].fd = fd; + return jsrt_io_queue(h); +} + +// Gives the file's size, as `__native_io_result_number` +int32_t __native_io_queue_stat(string_t path) { + int32_t h = jsrt_io_new(JSRT_IO_STAT); + char *utf8 = jsrt_string_utf8(path); + size_t size = strlen(utf8) + 1; + char *buf = jsrt_alloc(sizeof(struct statx) + size); + memcpy(buf + sizeof(struct statx), utf8, size); + free(utf8); + jsrt_io.ops[h].buf = buf; + return jsrt_io_queue(h); +} + +// Cancels op `target`, which then ends with -ECANCELED if it did not end +// first. The cancel's own status is 0 if it found the op, else -ENOENT or +// -EALREADY +int32_t __native_io_queue_cancel(int32_t target) { + if (target < 0 || target >= jsrt_io.cap) { + jsrt_type_error(); + } + + int32_t h = jsrt_io_new(JSRT_IO_CANCEL); + jsrt_io_op *t = &jsrt_io.ops[target]; + + switch (t->state) { + case JSRT_IO_QUEUED: + t->cancelled = true; + return jsrt_io_finish(h, 0); + case JSRT_IO_FLIGHT: + jsrt_io.ops[h].target = target; + jsrt_io.ops[h].target_gen = t->gen; + return jsrt_io_queue(h); + default: + return jsrt_io_finish(h, -ENOENT); + } +} + +// The handle of an op that is done, or -1 if no op is queued or in flight. +// The op that the last wait gave is freed now +int32_t __native_io_wait(void) { + if (jsrt_io.given >= 0) { + jsrt_io_release(jsrt_io.given); + jsrt_io.given = -1; + } + + jsrt_io_drain(); + + int32_t h = jsrt_io_pop(&jsrt_io.done); + + while (h < 0) { + if (jsrt_io.in_flight == 0) { + return -1; + } + + uint64_t user_data; + int32_t res; + + if (!jsrt_uring_cqe(&jsrt_io.ring, &user_data, &res)) { + jsrt_uring_submit(&jsrt_io.ring, true); + continue; + } + + jsrt_io.in_flight--; + h = (int32_t)user_data; + + // A timer that runs out ends with -ETIME, which is its success + bool timer = jsrt_io.ops[h].kind == JSRT_IO_TIMEOUT; + jsrt_io.ops[h].state = JSRT_IO_DONE; + jsrt_io.ops[h].res = timer && res == -ETIME ? 0 : res; + } + + jsrt_io.given = h; + return h; +} + +// >= 0 if the op succeeded (a count of bytes, an fd, or 0), else -errno +int32_t __native_io_status(int32_t h) { + return jsrt_io_given(h)->res; +} + +// The data of a read, decoded as UTF-8. Empty for any other op, or a failure +string_t __native_io_result_string(int32_t h) { + jsrt_io_op *op = jsrt_io_given(h); + bool data = op->kind == JSRT_IO_READ && op->res > 0; + return jsrt_string_from_utf8(op->buf, data ? (size_t)op->res : 0); +} + +// The size of a stat's file. The status for any other op +double __native_io_result_number(int32_t h) { + jsrt_io_op *op = jsrt_io_given(h); + + if (op->kind == JSRT_IO_STAT && op->res >= 0) { + return (double)((struct statx *)op->buf)->stx_size; + } + + return op->res; +} + +#endif // JSRT_IMPLEMENTATION diff --git a/lib/rt/jsrt.h b/lib/rt/jsrt.h new file mode 100644 index 0000000..d70283c --- /dev/null +++ b/lib/rt/jsrt.h @@ -0,0 +1,26 @@ +// The runtime that emitted C is compiled with. See docs/model.md for the +// representation of values +// +// Each header declares its part, and if JSRT_IMPLEMENTATION is defined before +// this is included, also defines it. The emitted C is one file, which does +// that, and includes this before any other header. The natives are declared +// by the emitted C itself, from compiler/src/native.rs, and defined here +// +// core.h values, the object header, the allocator, errors +// str.h strings and the string natives +// number.h the number natives +// any.h any_t and JS's operators on it +// io.h IO natives, async on io_uring (uring.h) +#pragma once + +// io.h needs Linux's syscall() and mmap(), which -std=c11 hides. This must +// come before the first system header +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "core.h" +#include "str.h" +#include "number.h" +#include "any.h" +#include "io.h" diff --git a/lib/rt/number.h b/lib/rt/number.h new file mode 100644 index 0000000..baa3b6b --- /dev/null +++ b/lib/rt/number.h @@ -0,0 +1,93 @@ +// The number natives: conversions between numbers and strings +#pragma once + +#include "str.h" + +#ifdef JSRT_IMPLEMENTATION +#include +#include +#include +#include + +// JS's Number::toString: the fewest digits that give the number back. Plain +// notation while the decimal point is within 21 places, else exponential +string_t __native_number_to_string(double x) { + if (isnan(x)) return jsrt_string_lit("NaN"); + if (isinf(x)) return jsrt_string_lit(x < 0 ? "-Infinity" : "Infinity"); + if (x == 0) return jsrt_string_lit("0"); + + // The fewest digits: buf is [-]d[.ddd]e±xx + char buf[64]; + + for (int precision = 0; precision < 17; precision++) { + snprintf(buf, sizeof buf, "%.*e", precision, x); + + if (strtod(buf, NULL) == x) { + break; + } + } + + char digits[32], out[64]; + char *p = buf, *o = out; + int n = 0; + + if (*p == '-') { + *o++ = *p++; + } + + for (; *p != 'e'; p++) { + if (*p != '.') { + digits[n++] = *p; + } + } + + digits[n] = 0; + + // The decimal point goes after the first `point` digits + int point = atoi(p + 1) + 1; + + if (n <= point && point <= 21) { + o += sprintf(o, "%s", digits); + for (int i = n; i < point; i++) *o++ = '0'; + *o = 0; + } else if (0 < point && point <= 21) { + sprintf(o, "%.*s.%s", point, digits, digits + point); + } else if (-6 < point && point <= 0) { + o += sprintf(o, "0."); + for (int i = point; i < 0; i++) *o++ = '0'; + sprintf(o, "%s", digits); + } else { + int e = point - 1; + sprintf(o, "%c%s%se%c%d", digits[0], n > 1 ? "." : "", digits + 1, e < 0 ? '-' : '+', abs(e)); + } + + return jsrt_string_lit(out); +} + +string_t __native_number_to_fixed(double x, int32_t digits) { + char buf[400]; + snprintf(buf, sizeof buf, "%.*f", digits, x); + return jsrt_string_lit(buf); +} + +// JS's ToNumber on a string: a decimal number with whitespace around it, or +// only whitespace, which is 0. Anything else is NaN +double __native_string_to_number(string_t s) { + char *utf8 = jsrt_string_utf8(s); + char *start = utf8, *end; + + while (*start == ' ' || (*start >= '\t' && *start <= '\r')) start++; + + double x = 0; + + if (*start) { + x = strtod(start, &end); + while (*end == ' ' || (*end >= '\t' && *end <= '\r')) end++; + if (*end || start == end) x = NAN; + } + + free(utf8); + return x; +} + +#endif // JSRT_IMPLEMENTATION diff --git a/lib/rt/str.h b/lib/rt/str.h new file mode 100644 index 0000000..482dfa3 --- /dev/null +++ b/lib/rt/str.h @@ -0,0 +1,155 @@ +// Strings: UTF-16 code units, as JS counts them, and the string natives +#pragma once + +#include "core.h" + +string_t jsrt_string_lit(const char *utf8); +string_t jsrt_string_from_utf8(const char *utf8, size_t size); +int32_t jsrt_string_length(string_t s); +bool jsrt_string_eq(string_t a, string_t b); +int32_t jsrt_string_cmp(string_t a, string_t b); + +// `s[i]`: the code unit at `i`, as a string, or JSRT_UNDEFINED past the end +string_t jsrt_string_at(string_t s, int32_t i); + +#ifdef JSRT_IMPLEMENTATION +#include +#include + +struct jsrt_string { + int32_t length; + uint16_t units[]; +}; + +static string_t jsrt_string_new(int32_t length) { + string_t s = jsrt_alloc(sizeof *s + (size_t)length * sizeof(uint16_t)); + s->length = length; + return s; +} + +string_t jsrt_string_lit(const char *utf8) { + return jsrt_string_from_utf8(utf8, strlen(utf8)); +} + +// The `size` bytes at `utf8`, which need not end in a 0 +string_t jsrt_string_from_utf8(const char *utf8, size_t size) { + const unsigned char *p = (const unsigned char *)utf8, *end = p + size; + string_t s = jsrt_string_new((int32_t)size * 2); + int32_t n = 0; + + while (p < end) { + uint32_t c = *p++; + int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0; + c &= extra ? 0x3F >> extra : 0x7F; + + for (; extra && p < end; extra--) { + c = (c << 6) | (*p++ & 0x3F); + } + + if (c >= 0x10000) { + c -= 0x10000; + s->units[n++] = (uint16_t)(0xD800 + (c >> 10)); + s->units[n++] = (uint16_t)(0xDC00 + (c & 0x3FF)); + } else { + s->units[n++] = (uint16_t)c; + } + } + + s->length = n; + return s; +} + +int32_t jsrt_string_length(string_t s) { + return s->length; +} + +bool jsrt_string_eq(string_t a, string_t b) { + return a->length == b->length && !memcmp(a->units, b->units, (size_t)a->length * sizeof(uint16_t)); +} + +int32_t jsrt_string_cmp(string_t a, string_t b) { + int32_t n = a->length < b->length ? a->length : b->length; + + for (int32_t i = 0; i < n; i++) { + if (a->units[i] != b->units[i]) { + return a->units[i] < b->units[i] ? -1 : 1; + } + } + + return a->length < b->length ? -1 : a->length > b->length; +} + +string_t jsrt_string_at(string_t s, int32_t i) { + if (i < 0 || i >= s->length) { + return JSRT_UNDEFINED; + } + + string_t c = jsrt_string_new(1); + c->units[0] = s->units[i]; + return c; +} + +// `s` as UTF-8, in a new buffer for the caller to free +static char *jsrt_string_utf8(string_t s) { + char *utf8 = jsrt_alloc((size_t)s->length * 3 + 1); + char *out = utf8; + + for (int32_t i = 0; i < s->length; i++) { + uint32_t c = s->units[i]; + bool pair = c >= 0xD800 && c < 0xDC00 && i + 1 < s->length + && s->units[i + 1] >= 0xDC00 && s->units[i + 1] < 0xE000; + + if (pair) { + c = 0x10000 + ((c - 0xD800) << 10) + (s->units[++i] - 0xDC00); + } + + if (c < 0x80) { + *out++ = (char)c; + } else if (c < 0x800) { + *out++ = (char)(0xC0 | c >> 6); + *out++ = (char)(0x80 | (c & 0x3F)); + } else if (c < 0x10000) { + *out++ = (char)(0xE0 | c >> 12); + *out++ = (char)(0x80 | (c >> 6 & 0x3F)); + *out++ = (char)(0x80 | (c & 0x3F)); + } else { + *out++ = (char)(0xF0 | c >> 18); + *out++ = (char)(0x80 | (c >> 12 & 0x3F)); + *out++ = (char)(0x80 | (c >> 6 & 0x3F)); + *out++ = (char)(0x80 | (c & 0x3F)); + } + } + + *out = 0; + return utf8; +} + +// ── natives ── + +string_t __native_string_concat(string_t a, string_t b) { + string_t s = jsrt_string_new(a->length + b->length); + memcpy(s->units, a->units, (size_t)a->length * sizeof(uint16_t)); + memcpy(s->units + a->length, b->units, (size_t)b->length * sizeof(uint16_t)); + return s; +} + +string_t __native_string_slice(string_t s, int32_t start, int32_t end) { + start = start < 0 ? 0 : start > s->length ? s->length : start; + end = end < start ? start : end > s->length ? s->length : end; + + string_t r = jsrt_string_new(end - start); + memcpy(r->units, s->units + start, (size_t)(end - start) * sizeof(uint16_t)); + return r; +} + +int32_t __native_string_char_code_at(string_t s, int32_t i) { + return s->units[i]; +} + +string_t __native_string_from_char_code(int32_t code) { + string_t s = jsrt_string_new(1); + s->units[0] = (uint16_t)code; + return s; +} + +#endif // JSRT_IMPLEMENTATION diff --git a/lib/rt/uring.h b/lib/rt/uring.h new file mode 100644 index 0000000..adac5be --- /dev/null +++ b/lib/rt/uring.h @@ -0,0 +1,158 @@ +// A minimal io_uring ring, on the raw syscalls. Only io.h uses it +// +// We write SQEs into the SQ and the kernel takes them when we submit. The +// kernel writes CQEs into the CQ and we take them. Each side moves only its +// own index, and reads the other's with acquire, so it sees the entries that +// came before +#pragma once + +#ifdef JSRT_IMPLEMENTATION +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + int fd; + + // The SQ: we own the tail, the kernel owns the head. `array` maps each + // slot of the ring to an SQE, and we keep it the identity + unsigned *sq_head, *sq_tail, *sq_array; + unsigned sq_mask, sq_entries; + struct io_uring_sqe *sqes; + unsigned tail; // our copy of the SQ tail, ahead of it until submit + unsigned to_submit; // SQEs written that the kernel did not take yet + + // The CQ: the kernel owns the tail, we own the head + unsigned *cq_head, *cq_tail; + unsigned cq_mask, cq_entries; + struct io_uring_cqe *cqes; +} jsrt_uring; + +_Noreturn static void jsrt_uring_fail(const char *what) { + fprintf(stderr, "jsrt: %s: %s\n", what, strerror(errno)); + exit(1); +} + +// The features that io.h depends on, all from Linux 5.5: +// SINGLE_MMAP the SQ and the CQ rings are one mapping +// NODROP the kernel keeps CQEs that do not fit the CQ, never drops them +// SUBMIT_STABLE the kernel copies what an SQE points to (a timespec, a path) +// before submit returns, so it needs to stay only until then +static void jsrt_uring_init(jsrt_uring *r, unsigned entries) { + struct io_uring_params p; + memset(&p, 0, sizeof p); + + int fd = (int)syscall(__NR_io_uring_setup, entries, &p); + + if (fd < 0) { + jsrt_uring_fail("io_uring_setup"); + } + + unsigned need = IORING_FEAT_SINGLE_MMAP | IORING_FEAT_NODROP | IORING_FEAT_SUBMIT_STABLE; + + if ((p.features & need) != need) { + fputs("jsrt: io_uring: the kernel is older than Linux 5.5\n", stderr); + exit(1); + } + + size_t sq_size = p.sq_off.array + p.sq_entries * sizeof(unsigned); + size_t cq_size = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe); + size_t size = sq_size > cq_size ? sq_size : cq_size; + int prot = PROT_READ | PROT_WRITE, flags = MAP_SHARED | MAP_POPULATE; + + char *ring = mmap(NULL, size, prot, flags, fd, IORING_OFF_SQ_RING); + void *sqes = mmap(NULL, p.sq_entries * sizeof(struct io_uring_sqe), prot, flags, fd, IORING_OFF_SQES); + + if (ring == MAP_FAILED || sqes == MAP_FAILED) { + jsrt_uring_fail("io_uring mmap"); + } + + *r = (jsrt_uring){ + .fd = fd, + .sq_head = (unsigned *)(ring + p.sq_off.head), + .sq_tail = (unsigned *)(ring + p.sq_off.tail), + .sq_array = (unsigned *)(ring + p.sq_off.array), + .sq_mask = *(unsigned *)(ring + p.sq_off.ring_mask), + .sq_entries = p.sq_entries, + .sqes = sqes, + .tail = *(unsigned *)(ring + p.sq_off.tail), + .cq_head = (unsigned *)(ring + p.cq_off.head), + .cq_tail = (unsigned *)(ring + p.cq_off.tail), + .cq_mask = *(unsigned *)(ring + p.cq_off.ring_mask), + .cq_entries = p.cq_entries, + .cqes = (struct io_uring_cqe *)(ring + p.cq_off.cqes), + }; +} + +// A new SQE, all zero, or NULL if the SQ is full. The kernel sees it at the +// next submit +static struct io_uring_sqe *jsrt_uring_sqe(jsrt_uring *r) { + unsigned head = __atomic_load_n(r->sq_head, __ATOMIC_ACQUIRE); + + if (r->tail - head == r->sq_entries) { + return NULL; + } + + unsigned i = r->tail & r->sq_mask; + r->sq_array[i] = i; + r->tail++; + r->to_submit++; + + memset(&r->sqes[i], 0, sizeof r->sqes[i]); + return &r->sqes[i]; +} + +static bool jsrt_uring_cq_empty(jsrt_uring *r) { + return *r->cq_head == __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE); +} + +// Submits every SQE written, and if `wait`, waits until the CQ has a CQE +// +// The kernel can take fewer SQEs than we give, for example when one fails +// before it starts. That one still gets a CQE, and we give the rest again +static void jsrt_uring_submit(jsrt_uring *r, bool wait) { + __atomic_store_n(r->sq_tail, r->tail, __ATOMIC_RELEASE); + + for (;;) { + bool get = wait && jsrt_uring_cq_empty(r); + + if (!r->to_submit && !get) { + return; + } + + unsigned flags = get ? IORING_ENTER_GETEVENTS : 0; + int n = (int)syscall(__NR_io_uring_enter, r->fd, r->to_submit, get ? 1 : 0, flags, NULL, 0); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + + jsrt_uring_fail("io_uring_enter"); + } + + r->to_submit -= (unsigned)n; + } +} + +// Takes the next CQE, if there is one +static bool jsrt_uring_cqe(jsrt_uring *r, uint64_t *user_data, int32_t *res) { + if (jsrt_uring_cq_empty(r)) { + return false; + } + + unsigned head = *r->cq_head; + struct io_uring_cqe *cqe = &r->cqes[head & r->cq_mask]; + *user_data = cqe->user_data; + *res = cqe->res; + + __atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE); + return true; +} + +#endif // JSRT_IMPLEMENTATION diff --git a/lib/stdlib/array.js b/lib/stdlib/array.js new file mode 100644 index 0000000..84d9d4f --- /dev/null +++ b/lib/stdlib/array.js @@ -0,0 +1,153 @@ +// Arrays. The compiler gives only `a[i]`, `a[i] = x` and `a.length`, which can +// also be written. Every method here is built on those +// +// Not as the spec says: +// - A method takes one value where the spec takes any number (`push(x)`, +// `unshift(x)`, `concat(other)`), because more need rest params +// - A callback gets the element and its index, not the array too +// - Holes are not skipped: a callback sees undefined for a hole + +// `Array(n)` and `new Array(n)` make an array of `n` holes +function Array(n) { + const a = []; + if (n !== undefined) a.length = n; + return a; +} + +Array.prototype = { + // ── Change the array ── + push(x) { + this[this.length] = x; + return this.length; + }, + pop() { + if (this.length === 0) return undefined; + const x = this[this.length - 1]; + this.length = this.length - 1; + return x; + }, + shift() { + if (this.length === 0) return undefined; + const x = this[0]; + for (let i = 1; i < this.length; i++) this[i - 1] = this[i]; + this.length = this.length - 1; + return x; + }, + unshift(x) { + for (let i = this.length; i > 0; i--) this[i] = this[i - 1]; + this[0] = x; + return this.length; + }, + reverse() { + for (let i = 0, j = this.length - 1; i < j; i++, j--) { + const x = this[i]; + this[i] = this[j]; + this[j] = x; + } + return this; + }, + fill(x, start, end) { + const len = this.length; + const s = relativeIndex(start === undefined ? 0 : start, len); + const e = relativeIndex(end === undefined ? len : end, len); + for (let i = s; i < e; i++) this[i] = x; + return this; + }, + + // ── Read ── + at(i) { + if (i < 0) i += this.length; + return this[i]; + }, + indexOf(x, from) { + for (let i = from === undefined ? 0 : relativeIndex(from, this.length); i < this.length; i++) { + if (this[i] === x) return i; + } + return -1; + }, + lastIndexOf(x) { + for (let i = this.length - 1; i >= 0; i--) { + if (this[i] === x) return i; + } + return -1; + }, + includes(x) { + return this.indexOf(x) !== -1; + }, + join(sep) { + const s = sep === undefined ? "," : sep; + let out = ""; + for (let i = 0; i < this.length; i++) { + if (i > 0) out = out + s; + const x = this[i]; + if (x !== undefined && x !== null) out = out + String(x); + } + return out; + }, + toString() { + return this.join(","); + }, + + // ── Make a new array ── + slice(start, end) { + const len = this.length; + const s = relativeIndex(start === undefined ? 0 : start, len); + const e = relativeIndex(end === undefined ? len : end, len); + const out = []; + for (let i = s; i < e; i++) out[i - s] = this[i]; + return out; + }, + concat(other) { + const out = this.slice(); + for (let i = 0; i < other.length; i++) out[out.length] = other[i]; + return out; + }, + + // ── With a callback ── + forEach(f) { + for (let i = 0; i < this.length; i++) f(this[i], i); + }, + map(f) { + const out = []; + for (let i = 0; i < this.length; i++) out[i] = f(this[i], i); + return out; + }, + filter(f) { + const out = []; + for (let i = 0; i < this.length; i++) { + if (f(this[i], i)) out[out.length] = this[i]; + } + return out; + }, + reduce(f, init) { + let acc = init; + for (let i = 0; i < this.length; i++) acc = f(acc, this[i], i); + return acc; + }, + find(f) { + for (let i = 0; i < this.length; i++) { + if (f(this[i], i)) return this[i]; + } + return undefined; + }, + findIndex(f) { + for (let i = 0; i < this.length; i++) { + if (f(this[i], i)) return i; + } + return -1; + }, + some(f) { + for (let i = 0; i < this.length; i++) { + if (f(this[i], i)) return true; + } + return false; + }, + every(f) { + for (let i = 0; i < this.length; i++) { + if (!f(this[i], i)) return false; + } + return true; + }, +}; + +__link_proto_array(Array.prototype); diff --git a/lib/stdlib/boolean.js b/lib/stdlib/boolean.js new file mode 100644 index 0000000..c41f12e --- /dev/null +++ b/lib/stdlib/boolean.js @@ -0,0 +1,12 @@ +// Converts any value to a boolean, like the spec's ToBoolean +function Boolean(x) { + return !!x; +} + +// In these methods `this` is the primitive boolean itself +Boolean.prototype = { + toString() { return this ? "true" : "false"; }, + valueOf() { return this; }, +}; + +__link_proto_boolean(Boolean.prototype); diff --git a/lib/stdlib/console.js b/lib/stdlib/console.js new file mode 100644 index 0000000..dd7221a --- /dev/null +++ b/lib/stdlib/console.js @@ -0,0 +1,7 @@ +// Not as the spec says: it prints its first arg only, because more args need +// rest params, which need arrays +const console = { + log(x) { + __native_print_string(String(x)); + }, +}; diff --git a/lib/stdlib/end.js b/lib/stdlib/end.js new file mode 100644 index 0000000..5032297 --- /dev/null +++ b/lib/stdlib/end.js @@ -0,0 +1,2 @@ +// Runs after the program: the event loop (see loop.js) +__run_loop(); diff --git a/lib/stdlib/globals.js b/lib/stdlib/globals.js new file mode 100644 index 0000000..1931f9d --- /dev/null +++ b/lib/stdlib/globals.js @@ -0,0 +1,19 @@ +// The stdlib, compiled in front of every program. Its files are joined in the +// order of `STDLIB` in compiler/src/main.rs, and this one is first +// +// Built on the natives in compiler/src/native.rs, the common layer of +// operations on primitive values. The compiler's own code for operators (`+` on +// strings and so on) uses those natives directly, not these functions. +// +// Restrictions: +// - The prototypes linked with `__link_proto_*` are fixed. User code may not +// change them (no monkey-patching) +// - `new String(x)`, `new Number(x)` and `new Boolean(x)` (wrapper objects) are +// not supported. These functions only convert +// +// Needs `typeof`. `String.prototype.concat` is not here, because it needs rest +// params, which need arrays. `console.log` prints one arg only, for the same +// reason + +const NaN = 0 / 0; +const Infinity = 1 / 0; diff --git a/lib/stdlib/io.js b/lib/stdlib/io.js new file mode 100644 index 0000000..4c12b5f --- /dev/null +++ b/lib/stdlib/io.js @@ -0,0 +1,72 @@ +// Files, with promises, on the IO natives (see lib/rt/io.h). They are like +// node's `fs.promises`, but only for UTF-8 text: +// - `fs.readFile(path)` gives the text of the file +// - `fs.writeFile(path, text)` makes or replaces the file +// A failure rejects with the negative errno, not an `Error`. A read or a write +// that the kernel does not finish in one op is not continued + +// A promise of what op `h` gives when it completes: its status, its data, or +// a number. A result can only be read before the next wait, so these read it +// when the op completes +function __io_status(h) { + return new Promise((resolve) => { + __pending[h] = (h) => resolve(__native_io_status(h)); + }); +} + +function __io_string(h) { + return new Promise((resolve, reject) => { + __pending[h] = (h) => { + const status = __native_io_status(h); + if (status < 0) reject(status); + else resolve(__native_io_result_string(h)); + }; + }); +} + +function __io_number(h) { + return new Promise((resolve, reject) => { + __pending[h] = (h) => { + const status = __native_io_status(h); + if (status < 0) reject(status); + else resolve(__native_io_result_number(h)); + }; + }); +} + +// A promise of the status of op `h`, rejected if it failed +function __io_ok(h) { + return __io_status(h).then((status) => (status < 0 ? Promise.reject(status) : status)); +} + +const fs = { + readFile(path) { + let fd = -1; + let text = ""; + + return __io_ok(__native_io_queue_open(path, 0, 0)) + .then((f) => { + fd = f; + return __io_number(__native_io_queue_stat(path)); + }) + .then((size) => __io_string(__native_io_queue_read(fd, size, 0))) + .then((s) => { + text = s; + return __io_ok(__native_io_queue_close(fd)); + }) + .then(() => text); + }, + + writeFile(path, text) { + let fd = -1; + + // O_WRONLY | O_CREAT | O_TRUNC, mode 0644 + return __io_ok(__native_io_queue_open(path, 577, 420)) + .then((f) => { + fd = f; + return __io_ok(__native_io_queue_write(fd, text, 0)); + }) + .then(() => __io_ok(__native_io_queue_close(fd))) + .then(() => undefined); + }, +}; diff --git a/lib/stdlib/loop.js b/lib/stdlib/loop.js new file mode 100644 index 0000000..4f2cf68 --- /dev/null +++ b/lib/stdlib/loop.js @@ -0,0 +1,46 @@ +// The event loop. A macrotask is an IO op or a timer, which completes in C +// (see lib/rt/io.h): `__native_io_wait()` gives the handle of the next one +// that completes. A microtask is a promise job, in `__microtasks`. After the +// program, end.js runs the loop: all microtasks, then one macrotask, then all +// microtasks again, until nothing is pending + +// What to do when IO op `h` completes: `__pending[h]`, a fn that gets `h`. +// Handles are small ints, and a handle is used again after its op completes +const __pending = []; + +// Promise jobs, in order. `head` is the next one, so that taking a job does +// not move the others. A job is a closure, which keeps its own data: the +// handler and the value that it is called with. So each handler gets only the +// values of its own promise (see `docs/model.md`, section 3.3) +const __microtasks = { jobs: [], head: 0 }; + +function __queue_job(job) { + __microtasks.jobs.push(job); +} + +function __drain_microtasks() { + const q = __microtasks; + + while (q.head < q.jobs.length) { + const job = q.jobs[q.head]; + q.jobs[q.head] = undefined; + q.head++; + job(); + } + + q.jobs.length = 0; + q.head = 0; +} + +function __run_loop() { + for (;;) { + __drain_microtasks(); + + const h = __native_io_wait(); + if (h < 0) return; + + const done = __pending[h]; + __pending[h] = undefined; + if (done !== undefined) done(h); + } +} diff --git a/lib/stdlib/number.js b/lib/stdlib/number.js new file mode 100644 index 0000000..d0070e8 --- /dev/null +++ b/lib/stdlib/number.js @@ -0,0 +1,49 @@ +// Converts any value to a number, like the spec's ToNumber +function Number(x) { + switch (typeof x) { + case "number": return x; + case "string": return __native_string_to_number(x); + case "boolean": return x ? 1 : 0; + case "undefined": return NaN; + case "object": return x === null ? 0 : NaN; + default: return NaN; + } +} + +Number.NaN = NaN; +Number.POSITIVE_INFINITY = Infinity; +Number.NEGATIVE_INFINITY = -Infinity; +Number.MAX_SAFE_INTEGER = 9007199254740991; +Number.MIN_SAFE_INTEGER = -9007199254740991; + +Number.isNaN = function (x) { return typeof x === "number" && x !== x; }; +Number.isFinite = function (x) { return typeof x === "number" && x === x && x !== Infinity && x !== -Infinity; }; +Number.isInteger = function (x) { return Number.isFinite(x) && x % 1 === 0; }; + +// In these methods `this` is the primitive number itself +Number.prototype = { + toString(radix) { + if (radix === undefined || radix === 10) return __native_number_to_string(this); + return integerToString(this, radix); + }, + toFixed(digits) { return __native_number_to_fixed(this, digits === undefined ? 0 : digits); }, + valueOf() { return this; }, +}; + +__link_proto_number(Number.prototype); + +// An integer in `radix` (2 to 36). Only integers: the fraction is dropped +function integerToString(n, radix) { + let negative = n < 0; + if (negative) n = -n; + n = n - (n % 1); + + let out = ""; + do { + let digit = n % radix; + out = __native_string_concat(__native_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out); + n = (n - digit) / radix; + } while (n > 0); + + return negative ? __native_string_concat("-", out) : out; +} diff --git a/lib/stdlib/object.js b/lib/stdlib/object.js new file mode 100644 index 0000000..c71f7f8 --- /dev/null +++ b/lib/stdlib/object.js @@ -0,0 +1,10 @@ +// `new Object()` makes an empty object. Called without `new`, it does the same +function Object() { + return {}; +} + +Object.prototype = {}; + +Object.create = function (proto) { + return { __proto__: proto }; +}; diff --git a/lib/stdlib/promise.js b/lib/stdlib/promise.js new file mode 100644 index 0000000..da6f75a --- /dev/null +++ b/lib/stdlib/promise.js @@ -0,0 +1,165 @@ +// Promises, as the spec says, except: +// - A throw in a handler or an executor does not reject, because there is no +// `try`. `reject()` and a returned rejected promise do +// - An unhandled rejection does nothing. Node prints it and stops +// - `Promise.all` takes an array, not any iterable + +// The states of a promise +const __PENDING = 0; +const __FULFILLED = 1; +const __REJECTED = 2; + +class Promise { + constructor(executor) { + this.state = __PENDING; + this.value = undefined; + this.resolved = false; // `resolve()` or `reject()` was called, maybe with a thenable + this.reactions = []; + + if (executor !== undefined) { + executor((value) => __resolve(this, value), (reason) => __reject(this, reason)); + } + } + + then(onFulfilled, onRejected) { + const derived = new Promise(); + const reaction = { onFulfilled: onFulfilled, onRejected: onRejected, derived: derived }; + + if (this.state === __PENDING) { + this.reactions.push(reaction); + } else { + __queue_reaction(reaction, this.state, this.value); + } + + return derived; + } + + catch(onRejected) { + return this.then(undefined, onRejected); + } + + finally(f) { + return this.then( + (value) => { + f(); + return value; + }, + (reason) => { + f(); + return Promise.reject(reason); + }, + ); + } + + static resolve(value) { + if (__is_promise(value)) return value; + const p = new Promise(); + __resolve(p, value); + return p; + } + + static reject(reason) { + const p = new Promise(); + __reject(p, reason); + return p; + } + + static all(promises) { + const p = new Promise(); + const values = []; + let left = promises.length; + + if (left === 0) __resolve(p, values); + + for (let i = 0; i < promises.length; i++) { + Promise.resolve(promises[i]).then( + (value) => { + values[i] = value; + left--; + if (left === 0) __resolve(p, values); + }, + (reason) => __reject(p, reason), + ); + } + + return p; + } +} + +function __is_promise(x) { + return typeof x === "object" && x !== null && x.reactions !== undefined; +} + +// Resolve `p` with `value`, unless it was resolved or rejected before +function __resolve(p, value) { + if (p.resolved) return; + p.resolved = true; + __fulfil_or_adopt(p, value); +} + +// Fulfil `p` with `value`, or if `value` is a thenable, adopt its state. That +// is in a job of its own, as the spec says, so it takes a tick more +function __fulfil_or_adopt(p, value) { + if ((typeof value === "object" && value !== null) || typeof value === "function") { + const then = value.then; + + if (typeof then === "function") { + __queue_job(() => __adopt(p, value)); + return; + } + } + + __settle(p, __FULFILLED, value); +} + +function __reject(p, reason) { + if (p.resolved) return; + p.resolved = true; + __settle(p, __REJECTED, reason); +} + +function __settle(p, state, value) { + p.state = state; + p.value = value; + + const reactions = p.reactions; + p.reactions = []; + + for (let i = 0; i < reactions.length; i++) { + __queue_reaction(reactions[i], state, value); + } +} + +function __queue_reaction(reaction, state, value) { + const handler = state === __FULFILLED ? reaction.onFulfilled : reaction.onRejected; + const derived = reaction.derived; + __queue_job(() => __react(handler, value, derived, state)); +} + +// Call `handler` with the value of a promise that is in `state`, and resolve +// `derived` with the result. With no handler, `derived` gets the same state +function __react(handler, value, derived, state) { + if (typeof handler === "function") { + __resolve(derived, handler(value)); + } else if (state === __FULFILLED) { + __resolve(derived, value); + } else { + __reject(derived, value); + } +} + +// Make `p` follow the thenable `thenable` +function __adopt(p, thenable) { + thenable.then( + (value) => { + if (p.state === __PENDING) __fulfil_or_adopt(p, value); + }, + (reason) => { + if (p.state === __PENDING) __settle(p, __REJECTED, reason); + }, + ); +} + +function queueMicrotask(f) { + __queue_job(f); +} diff --git a/lib/stdlib.js b/lib/stdlib/string.js similarity index 51% rename from lib/stdlib.js rename to lib/stdlib/string.js index c24d343..47f7b17 100644 --- a/lib/stdlib.js +++ b/lib/stdlib/string.js @@ -1,36 +1,3 @@ -// The stdlib, compiled in front of every program -// -// Built on the natives in compiler/src/native.rs, the common layer of -// operations on primitive values. The compiler's own code for operators (`+` on -// strings and so on) uses those natives directly, not these functions. -// -// Restrictions: -// - The prototypes linked with `__link_proto_*` are fixed. User code may not -// change them (no monkey-patching) -// - `new String(x)`, `new Number(x)` and `new Boolean(x)` (wrapper objects) are -// not supported. These functions only convert -// -// Needs `typeof`. `console.log` and `String.prototype.concat` also need rest -// params, which need arrays - -const NaN = 0 / 0; -const Infinity = 1 / 0; - -// ─────────────────────────────────────────────── Object ─────────────────────────────────────────────── - -// `new Object()` makes an empty object. Called without `new`, it does the same -function Object() { - return {}; -} - -Object.prototype = {}; - -Object.create = function (proto) { - return { __proto__: proto }; -}; - -// ─────────────────────────────────────────────── String ─────────────────────────────────────────────── - // Converts any value to a string, like the spec's ToString function String(x) { switch (typeof x) { @@ -55,14 +22,6 @@ String.prototype = { toString() { return this; }, valueOf() { return this; }, - concat(...parts) { - let s = this; - for (let i = 0; i < parts.length; i++) { - s = __native_string_concat(s, String(parts[i])); - } - return s; - }, - // ── Characters ── charAt(i) { if (i < 0 || i >= this.length) return ""; @@ -182,83 +141,3 @@ function pad(s, length, fill, atStart) { return atStart ? __native_string_concat(padding, s) : __native_string_concat(s, padding); } - -// ─────────────────────────────────────────────── Number ─────────────────────────────────────────────── - -// Converts any value to a number, like the spec's ToNumber -function Number(x) { - switch (typeof x) { - case "number": return x; - case "string": return __native_string_to_number(x); - case "boolean": return x ? 1 : 0; - case "undefined": return NaN; - case "object": return x === null ? 0 : NaN; - default: return NaN; - } -} - -Number.NaN = NaN; -Number.POSITIVE_INFINITY = Infinity; -Number.NEGATIVE_INFINITY = -Infinity; -Number.MAX_SAFE_INTEGER = 9007199254740991; -Number.MIN_SAFE_INTEGER = -9007199254740991; - -Number.isNaN = function (x) { return typeof x === "number" && x !== x; }; -Number.isFinite = function (x) { return typeof x === "number" && x === x && x !== Infinity && x !== -Infinity; }; -Number.isInteger = function (x) { return Number.isFinite(x) && x % 1 === 0; }; - -// In these methods `this` is the primitive number itself -Number.prototype = { - toString(radix) { - if (radix === undefined || radix === 10) return __native_number_to_string(this); - return integerToString(this, radix); - }, - toFixed(digits) { return __native_number_to_fixed(this, digits === undefined ? 0 : digits); }, - valueOf() { return this; }, -}; - -__link_proto_number(Number.prototype); - -// An integer in `radix` (2 to 36). Only integers: the fraction is dropped -function integerToString(n, radix) { - let negative = n < 0; - if (negative) n = -n; - n = n - (n % 1); - - let out = ""; - do { - let digit = n % radix; - out = __native_string_concat(__native_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out); - n = (n - digit) / radix; - } while (n > 0); - - return negative ? __native_string_concat("-", out) : out; -} - -// ─────────────────────────────────────────────── Boolean ────────────────────────────────────────────── - -// Converts any value to a boolean, like the spec's ToBoolean -function Boolean(x) { - return !!x; -} - -// In these methods `this` is the primitive boolean itself -Boolean.prototype = { - toString() { return this ? "true" : "false"; }, - valueOf() { return this; }, -}; - -__link_proto_boolean(Boolean.prototype); - -// ─────────────────────────────────────────────── console ────────────────────────────────────────────── - -const console = { - log(...args) { - let line = ""; - for (let i = 0; i < args.length; i++) { - if (i > 0) line = __native_string_concat(line, " "); - line = __native_string_concat(line, String(args[i])); - } - __native_print_string(line); - }, -}; diff --git a/lib/stdlib/timers.js b/lib/stdlib/timers.js new file mode 100644 index 0000000..72bb971 --- /dev/null +++ b/lib/stdlib/timers.js @@ -0,0 +1,25 @@ +// Timers. `setTimeout` gives a record, as node does, not an int: an IO handle +// is used again after its op completes, so `clearTimeout` of an old int could +// cancel another op. A record stays the timer's own +// +// Not as the spec says: `setTimeout` takes no args for the callback + +function setTimeout(f, ms) { + // As in node, a delay below 1 ms is 1 ms + const delay = ms === undefined || !(ms >= 1) ? 1 : ms; + const timer = { handle: __native_io_queue_timeout(delay), active: true }; + + __pending[timer.handle] = (h) => { + if (!timer.active) return; + timer.active = false; + f(); + }; + + return timer; +} + +function clearTimeout(timer) { + if (timer === undefined || timer === null || !timer.active) return; + timer.active = false; + __native_io_queue_cancel(timer.handle); +} diff --git a/third_party/stb b/third_party/stb new file mode 160000 index 0000000..2c980bb --- /dev/null +++ b/third_party/stb @@ -0,0 +1 @@ +Subproject commit 2c980bb59875b0d32144a71867fbdebb2f77cd20