# The core model This file is the spec for how the compiler represents JS values, objects, bindings and functions in C. Each part of the code must agree with it. A difference between the code and this file is a bug in one of them. The method: 1. State the JS semantics that the C code must keep (section 1). 2. Define a default representation that keeps all of it with no analysis (section 2). 3. Let types (sections 3 and 4), the value rule (section 5) and analyses (sections 6 and 7) narrow the representation. Each narrowing has one condition. If the condition is not proved, the default stays. So a hole in an analysis costs speed. It never makes wrong code. Only a wrong rule can make wrong code. ## 1. Semantics JS has four core concepts. **Value.** A value is one of: undefined, null, bool, number, string, object. A value cannot change. A value is always copied: an assignment, an argument, a return and a field write each copy it. An object value is a reference to an object, so a copy of it copies the reference. **Object.** An object is a cell with identity. It has own fields and a prototype (an object or null). A field read looks at the own fields. If the field is absent, the read goes on to the prototype, and at the end of the chain it gives undefined. An absent field and a field that holds undefined are different. Two object values are equal (`===`, `==`) if and only if they refer to the same object. **Binding.** A binding is the storage of a variable. Each entry into a scope makes new bindings. A loop body is entered once for each iteration. `for (let …)` copies the values of the loop vars into the new bindings of the next iteration. A closure captures bindings, not values. So a write to a captured binding, by the owner or by any closure, is seen by all of them. **Function.** A function is an object that also has code, and the bindings that it captured. ### When the representation is free A representation is correct if no program can see a difference from these semantics. - **A primitive** cannot change and has no identity. A copy of it and a shared pointer to it look the same. Either is correct. - **An object** can be copied instead of shared only if no program can see the copy. There are two ways to see a copy: - a write through one holder, which another holder reads later - an identity test between two holders - **A binding** must be shared if a closure captures it and a write reaches it after the capture. ## 2. The default representation 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 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. - The owner and every closure that captured the binding read and write the slot. - A scope that is entered again is a loop body. QuickJS closes each captured binding of such a scope (`close_loc k`) at every way out of it. `close_loc k` puts a new box in `k`, which holds the current value. Closures made before keep the old box. So each iteration has its own bindings. - A box's slot is never absent: JS gives a binding a value before any read of it that does not throw. A `var` starts as undefined. A `let` or `const` has no value before its declaration, where a read throws (the TDZ, section 8). - A binding that no closure captures is a C local. Transform knows this from `closure_vars`, so no analysis decides it. - A module binding has no frame. Its box is made where the fn that declares it is made. - **A function object holds a pointer to each box that it captured.** The fn that makes the closure gives each box: from its own local if the binding is its own, else from its own env. So every capture is one copy of a pointer. ## 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). ### 3.1 The lattice A type is a set of kinds, with a refinement for each kind that is in the set: | kind | refinement | |---|---| | 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 | - **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. ### 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. ### 3.3 Shapes - **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. ### 3.4 Calls - **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. ## 4. Representation The C representation of a value is a function of its type and of the value rule (section 5) only. | 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` | - **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. ## 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. - **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. **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. **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. **Default.** An object of any other shape is a pointer to a cell (`S *`). **Soundness.** No write after creation can show a copy, and no identity test can. A creation write happens before the object goes anywhere, so no copy exists yet. ## 6. Escape **Question.** Where can a cell still be reached: after its frame returns, or when the op that made it runs again? **Result.** One of three values for each value that a creating op gives: - **`escapes`** if one of these is true: - After the frame of its fn returns, the cell can be reached in a way other than as the return value itself. For example, it is stored in a field of a cell that is not `local`, stored in a box that is not `local`, or given to a callee. Every call operand counts as kept, because the analysis does not know the callee. - A value that can reach the cell is still used when the creating op runs again: the op is in a loop, and the cell reaches a var or a join slot, which can keep it until the next run. - **`returned`** if it does not escape, the return value is the only way to reach it after the return, it is the only object that a return of the fn can give, and its creating op runs at most once in each call. - **`local`** otherwise: no holder outlives the frame. A box is a cell, so the same rules apply to boxes. **Narrowing.** - **`local`:** the cell is on the stack of its frame. The value is still a pointer (`S *`) to it, so only the creating op changes. A C compiler can also split such a cell into registers if its address goes nowhere. - **`returned`:** the caller allocates the cell. The variant gets one more param: a pointer to the cell to build into. The callee builds the object there and does not allocate. In the caller, the call is the creating op of the cell. So the caller's escape result for the call's dst chooses the stack or the heap, and the decision goes up through any number of fns that make and return objects. **Default.** A cell that escapes is on the heap (`jsrt_alloc`). **Soundness.** - A stack cell lives until its frame returns, and its creating op uses the same memory again each time it runs. `local` excludes both cases. - A `returned` object is in the caller's cell from the start and never moves. So every pointer to it stays valid, including a pointer from the object to itself. **Where it is used.** Emit chooses the allocation of every cell in one function, `alloc()`. A creating op calls it, and so does a call of a variant whose result is `returned`. ## 7. Other narrowings | 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 | | 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 | ## 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. - 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. - getters and setters