Broken site based impl

This commit is contained in:
2026-10-03 14:06:24 +00:00
parent 23a2c3bc46
commit 44498b32a9
39 changed files with 4250 additions and 1750 deletions
+93 -51
View File
@@ -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