├── README.md ├── classsyntax.md ├── core.md ├── ootypes.md ├── overloading.md └── valuetypes.md /README.md: -------------------------------------------------------------------------------- 1 | # WARNING: Outdated! Typed Objects Explainer 2 | 3 | This set of explainer documents is for the old, inactive Typed Objects proposal. The new proposal can be found here: 4 | https://github.com/tschneidereit/proposal-typed-objects 5 | -------------------------------------------------------------------------------- /classsyntax.md: -------------------------------------------------------------------------------- 1 | # Class syntax 2 | 3 | This is just a sketch, but the idea is to extend class type syntax to 4 | integrate with typed objects. 5 | 6 | *WARNING:* This is *very* preliminary and not fully thought through. 7 | 8 | ## Field types 9 | 10 | Allow users to declare field types. If any field types exist, this 11 | class syntax will be desugared into a typed object definition. 12 | 13 | ```js 14 | class PointType { 15 | r: uint8; 16 | g: uint8; 17 | b: uint8; 18 | a: uint8; 19 | 20 | constructor(r, g, b, a) { 21 | this.r = r; ... 22 | } 23 | } 24 | ```` 25 | 26 | desugars into something like: 27 | 28 | ``` 29 | var _PointType = new StructType({r: uint8, g: uint8, b: uint8, a: uint8}); 30 | class PointType(_PointType) { 31 | constructor(r, g, b, a) { 32 | this.r = r; ... 33 | } 34 | } 35 | ``` 36 | 37 | ## Sealed classes 38 | 39 | It'd also be nice to be able to have classes whose prototypes are 40 | sealed after they are constructed. This gives better optimization 41 | opportunities. It is orthogonal to the syntax above. 42 | 43 | ``` 44 | sealed class Foo { ... }` 45 | ``` 46 | 47 | desugars into something which freezes `Foo.prototype` after 48 | construction. 49 | 50 | ## Open questions 51 | 52 | - How to integrate class syntax with forward references. 53 | - Can we integrate with module loading somehow to avoid the need for 54 | forward references when using declarative syntax? 55 | -------------------------------------------------------------------------------- /core.md: -------------------------------------------------------------------------------- 1 | # Explainer for Type Objects proposal 2 | 3 | ## Outline 4 | 5 | The explainer proceeds as follows: 6 | 7 | 8 | 9 | - [Explainer for Type Objects proposal](#explainer-for-type-objects-proposal) 10 | - [Outline](#outline) 11 | - [Type definitions](#type-definitions) 12 | - [Primitive type definitions](#primitive-type-definitions) 13 | - [Struct type definitions](#struct-type-definitions) 14 | - [Options](#options) 15 | - [Option: defaults](#option-defaults) 16 | - [Example: Standard structs](#example-standard-structs) 17 | - [Example: Nested structs](#example-nested-structs) 18 | - [Struct arrays](#struct-arrays) 19 | - [Alignment and Padding](#alignment-and-padding) 20 | - [Instantiation](#instantiation) 21 | - [Instantiating struct types](#instantiating-struct-types) 22 | - [Default Values](#default-values) 23 | - [Creating struct arrays](#creating-struct-arrays) 24 | - [Reading fields](#reading-fields) 25 | - [Assigning fields](#assigning-fields) 26 | - [No Dynamic Properties](#no-dynamic-properties) 27 | - [Backing buffers](#backing-buffers) 28 | - [Canonicalization of typed objects and equality](#canonicalization-of-typed-objects-and-equality) 29 | - [Prototypes](#prototypes) 30 | - [Shared Base Constructors](#shared-base-constructors) 31 | 32 | 33 | 34 | ## Type definitions 35 | 36 | The central part of the typed objects specification are *type 37 | definition objects*, generally called *type definitions* for 38 | short. Type definitions describe the layout of a value in memory. 39 | 40 | ### Primitive type definitions 41 | 42 | The system comes predefined with type definitions for all the 43 | primitive types: 44 | 45 | uint8 int8 float32 any 46 | uint16 int16 float64 string 47 | uint32 int32 object 48 | 49 | These primitive type definitions represent the various kinds of 50 | existing JS values. For example, the type `float64` describes a JS 51 | number, and `string` defines a JS string. The type `object` indicates 52 | a pointer to a JS object. Finally, `any` can be any kind of value 53 | (`undefined`, number, string, pointer to object, etc). 54 | 55 | Primitive type definitions can be called, in which case they act as a 56 | kind of cast or coercion. For numeric types, these coercions will 57 | first convert the value to a number (as is common with JS) and then 58 | coerce the value into the specified size: 59 | 60 | ```js 61 | int8(128) // returns 127 62 | int8("128") // returns 127 63 | int8(2.2) // returns 2 64 | int8({valueOf() {return "2.2"}}) // returns 2 65 | int8({}) // returns 0, because Number({}) results in NaN, which is replaced with the default value 0. 66 | ``` 67 | 68 | If you're familiar with C, these coercions are basically equivalent to 69 | C casts. 70 | 71 | In some cases, coercions can throw. For example, in the case of 72 | `object`, the value being coerced must be an object or `null`: 73 | 74 | ```js 75 | object("foo") // throws 76 | object({}) // returns the object {} 77 | object(null) // returns null 78 | ``` 79 | 80 | Finally, in the case of `any`, the coercion is a no-op, because any 81 | kind of value is acceptable: 82 | 83 | ```js 84 | any(x) === x 85 | ``` 86 | 87 | In this base spec, the set of primitive type definitions cannot be 88 | extended. The [value types](valuetypes.md) extension describes 89 | the mechanisms needed to support that. 90 | 91 | ### Struct type definitions 92 | 93 | Struct types are defined using the `StructType` constructor: 94 | 95 | ```js 96 | function StructType(structure, [options]) 97 | ``` 98 | 99 | The `structure` argument must recursively consist of fields whose values are type 100 | definitions: either *primitive* or *struct type definitions*. 101 | 102 | #### Options 103 | 104 | The optional `options` parameter can influence certain aspects of a struct's semantics. 105 | Options are specified using fields on an object passed as the `options` parameter. 106 | 107 | ##### Option: defaults 108 | 109 | If the `options` object contains a `defaults` field, the value of that field is used as a 110 | source of default values for fields of the specified type. See the [section on default 111 | values](#default-values) below for details. 112 | 113 | #### Example: Standard structs 114 | 115 | ```js 116 | const PointStruct = new StructType({x: float64, y: float64}); 117 | ``` 118 | 119 | This defines a new type definition `PointStruct` that consists of two 120 | floats. These will be laid out in memory consecutively, just as a C 121 | struct would: 122 | 123 | +============+ --+ PointStruct 124 | | x: float64 | | 125 | | y: float64 | | 126 | +============+ --+ 127 | 128 | #### Example: Nested structs 129 | 130 | Struct types can embed other struct types as named fields: 131 | 132 | ```js 133 | const LineStruct = new StructType({from: PointStruct, to: PointStruct}); 134 | ``` 135 | 136 | The result is a structure that contains two points embedded (again, 137 | just as you would get in C): 138 | 139 | +==================+ --+ LineStruct 140 | | from: x: float64 | | --+ PointStruct 141 | | y: float64 | | 142 | | to: x: float64 | | --+ PointStruct 143 | | y: float64 | | 144 | +==================+ --+ 145 | 146 | The fact that the `from` and `to` fields are *embedded within* the 147 | line is very important. It is also different from the way normal 148 | JavaScript objects work. Typically, embedding one JavaScript object 149 | within another is done using pointers and indirection. So, for 150 | example, if you make a JavaScript object using an expression like the 151 | following: 152 | 153 | ```js 154 | let line = { from: { x: 3, y: 5 }, to: { x: 7, y: 8 } }; 155 | ``` 156 | 157 | you will create three objects, which means you have a memory 158 | layout roughly like this: 159 | 160 | line -------> +======+ 161 | | from | ------------> +======+ 162 | | to | ---->+======+ | x: 3 | 163 | +======+ | x: 7 | | y: 5 | 164 | | y: 8 | +======+ 165 | +======+ 166 | 167 | The typed objects approach of embedding types within one another by 168 | default can save a significant amount of memory, particularly if you 169 | have a large number of lines embedded in an array. It also 170 | improves cache behavior since the data is contiguous in memory. 171 | 172 | ### Struct arrays 173 | 174 | Each struct type has an accompanying `array` method which 175 | can be used to create fixed-sized typed arrays of elements of the struct's type. 176 | Just as for the existing typed arrays such as `Uint8Array`, instances of these arrays 177 | all share the same nominal type and `prototype`, regardless of the length. 178 | 179 | ```js 180 | const PointStruct = new StructType({x: float64, y: float64}); 181 | let points = new PointStruct.Array(10); 182 | ``` 183 | 184 | For the full set of overloads of the `array` method see the [section on 185 | creating struct arrays](#creating-struct-arrays) below. 186 | 187 | ## Alignment and Padding 188 | 189 | The memory layout of struct types isn't observable by content, so an 190 | implementation is free to change the order of fields to minimize required 191 | padding or satisfy alignment requirements. 192 | 193 | ## Instantiation 194 | 195 | ### Instantiating struct types 196 | 197 | You can create an instance of a struct type using the `new` 198 | operator: 199 | 200 | ```js 201 | const PointStruct = new StructType({x: float64, y: float64}); 202 | const LineStruct = new StructType({from: PointStruct, to: PointStruct}); 203 | let line = new LineStruct(); 204 | console.log(line.from.x); // logs 0 205 | ``` 206 | 207 | The resulting object is called a *typed object*: it will have the 208 | fields specified in `LineStruct`. Each field will be initialized to an 209 | appropriate default value based on its type (e.g., numbers are 210 | initialized to 0, fields of type `any` are initialized to `undefined`, 211 | and so on). Fields with structural type (like `from` and `to` in this 212 | case) are recursively initialized. 213 | 214 | When creating a new typed object, you can also supply a "source 215 | object". This object will be used to extract the initial values for 216 | each field: 217 | 218 | ```js 219 | let line1 = new LineStruct({from: {x: 1, y: 2}, 220 | to: {x: 3, y: 4}}); 221 | console.log(line1.from.x); // logs 1 222 | 223 | let line2 = new LineStruct(line1); 224 | console.log(line2.from.x); // also logs 1 225 | ``` 226 | 227 | As the example shows, the example object can be either a normal JS 228 | object or another typed object. The only requirement is that it have 229 | fields of the appropriate type. Essentially, writing: 230 | 231 | ```js 232 | let line1 = new LineStruct(example); 233 | ``` 234 | 235 | is exactly equivalent to writing: 236 | 237 | ```js 238 | let line1 = new LineStruct(); 239 | line1.from.x = example.from.x; 240 | line1.from.y = example.from.y; 241 | line1.from.x = example.to.x; 242 | line1.from.y = example.to.y; 243 | ``` 244 | 245 | *Note*: this equivalence doesn't necessarily hold. Depending on the type definition missing fields 246 | in `example` might be interpreted differently during initialization vs. assignment. See the 247 | sections on default values and assignment below. 248 | 249 | #### Default Values 250 | 251 | Using the `defaults` field on the `StructType` constructor's `options` parameter, 252 | default values for members of the struct type can be overridden. As for the normal 253 | default values, overridden defaults are applied in place of missing fields in the source 254 | object. That means that it's possible to provide defaults for primitives and embedded 255 | struct types alike. In case an embedded struct isn't completely defined by the source 256 | object, only the missing fields are set to the overridden default values - again just as 257 | for builtin defaults. 258 | 259 | ```js 260 | const PointStruct = new StructType({x: float64, y: float64}); 261 | let defaults = { 262 | topLeft: new PointStruct({x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY}), 263 | bottomRight: {x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY} 264 | }; 265 | const RectangleStruct = new StructType({topLeft: PointStruct, bottomRight: PointStruct}, 266 | {defaults: defaults}); 267 | // Instantiate from a source object with one partially and one entirely missing field: 268 | let rect1 = new RectangleStruct({topLeft: {x: 10}}); 269 | rect1.topLeft.x === 10; 270 | rect1.topLeft.y === Number.NEGATIVE_INFINITY; 271 | rect1.bottomRight.x === Number.POSITIVE_INFINITY; 272 | rect1.bottomRight.y === Number.POSITIVE_INFINITY; 273 | ``` 274 | 275 | It's an error to provide default values with incorrect types for members with primitive 276 | type. For members with struct type, only the structure of the default value (and the 277 | types of any fields that provide default values for members with primitive type contained 278 | in the struct type) is relevant, not its type. 279 | 280 | As with builtin default values, overridden default values only apply during 281 | instantiation, not during assignment. That is, it's still an error to assign an 282 | incomplete example object to a member that is an embedded struct. 283 | 284 | ### Creating struct arrays 285 | 286 | For each struct type definition, a fixed-sized typed array of instances of 287 | that type can be created using the type's `.Array` constructor. 288 | 289 | In difference to structs, struct arrays aren't canonicalized. The reasons for this 290 | are threefold: 291 | 292 | 1. Since struct arrays can't be embedded into structs, there aren't any questions 293 | around the semantics of accessing them as struct members. 294 | 2. The canonicalization would have to include the length, increasing overhead and 295 | making it more difficult to reason about them: two struct arrays that're otherwise 296 | identical wouldn't be canonicalized if their length differs, so an object's 297 | identity would depend on subtle factors. 298 | 3. Not canonicalizing typed object arrays means they behave very similarly to the 299 | existing typed arrays. Even if completely merging typed object arrays and typed 300 | arrays on the spec level isn't feasible, the desire to reduce overall language 301 | complexity still calls for making them behave as similar as possible. 302 | 303 | Just as the `Array` constructor, typed object array constructors support two 304 | different overloads: 305 | 306 | ```js 307 | const PointStruct = new StructType({x: float64, y: float64}); 308 | 309 | // Creates an instance of length 10, with all entries initialized to default values. 310 | let points = new PointStruct.Array(10); 311 | 312 | // Creates a copy of `points`. 313 | let pointsCopy = new PointStruct.Array(points); 314 | 315 | // Creates an instance by iterating over the array-like or iterable source and 316 | // creating instances of `PointStruct` for all encountered items. 317 | let coercedPoints = new PointStruct.Array([new PointStruct(1, 2), { x: 1, y: 2 }]); 318 | ``` 319 | 320 | ## Reading fields 321 | 322 | When you access a field `f` of a typed object, the result depends on 323 | the type with which `f` was declared. If `f` was declared with *struct type*, 324 | then the result is a new *typed object pointer* that points into the same 325 | backing buffer as before. Therefore, this fragment of code: 326 | 327 | ```js 328 | let line1 = new LineStruct({from: {x: 1, y: 2}, 329 | to: {x: 3, y: 4}}); 330 | let toPoint = line1.to; 331 | ``` 332 | 333 | yields the following result: 334 | 335 | line1 ----> +===========+ 336 | | buffer | --+-> +============+ underlying buffer 337 | | offset: 0 | | | from: x: 1 | 338 | +===========+ | | y: 2 | 339 | | | to: x: 3 | 340 | toPoint --> +============+ | | y: 4 | 341 | | buffer | -+ +============+ 342 | | offset: 16 | 343 | +============+ 344 | 345 | You can see that the object `toPoint` references the same buffer as 346 | before, but with a different offset. The offset is now 16, because it 347 | points at the `to` point (and each point consists of two `float64`s, 348 | which are 8 bytes each). 349 | 350 | Accessing a field of primitive type does not return a typed object, in 351 | contrast to fields of struct type. Instead, it simply copies the value out 352 | from the array buffer and returns that. Therefore, `toPoint.x` yields 353 | the value `3`, not a pointer into the buffer. 354 | 355 | Structs are non-extensible, and trying to access non-existent properties on them 356 | throws a `TypeError`. 357 | 358 | ## Assigning fields 359 | 360 | When you assign to a field, the backing store is modified accordingly. 361 | As long as the right hand side has the required structure, the process is precisely the same as when 362 | providing an initial value for a typed object. This means that you can write things like: 363 | 364 | ```js 365 | let line = new LineStruct(); 366 | line.to = {x: 22, y: 44}; 367 | console.log(line.from.x); // prints 0 368 | console.log(line.to.x); // prints 22 369 | ``` 370 | 371 | When assigning to a field that has a struct type, the assigned value must be an object 372 | (or a typed object) with the right structure: it must recursively contain at least all 373 | properties the target field's type has; otherwise, a `TypeError` is thrown: 374 | 375 | ```js 376 | let line = new LineStruct(); 377 | line.to = {x: 22, y: 44}; // Ok. 378 | line.to = {x: 22, y: 44, z: 88}; // Ok. 379 | line.to = {x: 22}; // Throws. 380 | ``` 381 | 382 | The rationale for this behavior is that both alternatives - leaving absent fields 383 | unchanged or resetting them to their default values - are very likely to cover up 384 | subtle bugs. This is especially true when gradually converting an existing code base 385 | to using typed objects. On the other hand, ignoring additional fields on the source object doesn't 386 | have the same issues: all fields on the target instance are set to predictable values. 387 | 388 | If a field has primitive type, then when it is assigned, the value is 389 | transformed in the same way that it would be if you invoked the type 390 | definition to "cast" the value. Hence all of the following ways to 391 | assign the fields `line.to.x` and `line.to.y` (which are declared with 392 | type `float64`) are equivalent: 393 | 394 | ```js 395 | line.to.x = 22; 396 | line.to.y = 44; 397 | 398 | line.to.x = float64(22); 399 | line.to.y = float64(44); 400 | 401 | line.to = {x: 22, y: 44}; 402 | 403 | line.to = {x: float64(22), y: float64(44)}; 404 | ``` 405 | 406 | ## No Dynamic Properties 407 | 408 | Trying to assign to a non-existent field on a struct throws a `TypeError` instead of 409 | adding a dynamic property to the instance. Essentially all struct type instances behave 410 | as though `Object.preventExtensions()` had been invoked on them. 411 | 412 | *Rationale*: The canonicalization rules described 413 | [below](#canonicalization-of-typed-objects-and-equality) mean that structs don't have a way 414 | to add dynamic properties to them: they would have to be associated with the starting 415 | offset of the struct in the underlying buffer because the struct itself is just a fat pointer 416 | to that location. Only for structs that are not embedded in other structs would it be 417 | possible to add them to the struct itself, but supporting dynamic properties on some structs 418 | but not others would be surprising. 419 | 420 | ## Backing buffers 421 | 422 | Conceptually at least, every typed object is actually a *view* onto an 423 | `ArrayBuffer` backing buffer, just as is the case for typed arrays. Say you 424 | create a line like: 425 | 426 | ```js 427 | let line1 = new LineStruct({from: {x: 1, y: 2}, 428 | to: {x: 3, y: 4}}); 429 | ``` 430 | 431 | The result will be two objects as shown: 432 | 433 | line1 ----> +===========+ 434 | | buffer | ----> +============+ buffer 435 | | offset: 0 | | from: x: 1 | 436 | +===========+ | y: 2 | 437 | | to: x: 3 | 438 | | y: 4 | 439 | +============+ 440 | 441 | As you can see from the diagram, the typed object `line1` doesn't 442 | actually store any data itself. Instead, it is simply a pointer into a 443 | backing store that contains the data itself. 444 | 445 | *Efficiency note:* The spec has been designed so that, most of the 446 | time, engines only have to create the backing buffer object and not 447 | the pointer object `line1`. Instead of creating an object to store the 448 | buffer and offset, the engine can usually just store the buffer and 449 | offset directly as synthetic local variables. 450 | 451 | ## Canonicalization of typed objects and equality 452 | 453 | In a prior section, we said that accessing a field of a typed object 454 | will return a new typed object that shares the same backing buffer if the 455 | field has a struct type. Based on this, you might wonder what happens if you 456 | access the same field twice in a row: 457 | 458 | ```js 459 | let line = new LineStruct({from: {x: 1, y: 2}, 460 | to: {x: 3, y: 4}}); 461 | let toPoint1 = line.to; 462 | let toPoint2 = line.to; 463 | ``` 464 | 465 | The answer is that each time you access the same field, you get back 466 | the same pointer (at least conceptually): 467 | 468 | line --------> +===========+ 469 | | buffer | --+-> +============+ ArrayBuffer 470 | | offset: 0 | | | from: x: 1 | 471 | +===========+ | | y: 2 | 472 | | | to: x: 3 | 473 | toPoint1 -+--> +============+ | | y: 4 | 474 | | | buffer | -+ +============+ 475 | | | offset: 16 | 476 | | +============+ 477 | | 478 | toPoint2 -+ 479 | 480 | In fact, the drawing is somewhat incomplete. The model is that all the 481 | typed objects that point into a buffer are cached on the buffer 482 | itself, and hence there should be reverse arrays leading out from the 483 | `ArrayBuffer` to the typed objects. 484 | 485 | *Efficiency note:* In practice, engines are not expected (nor 486 | required) to actually have such a cache, though it may be useful to 487 | handle corner cases like placing a typed object into a weak 488 | map. Nonetheless, modeling the behavior as if the cache existed is 489 | useful because it tells us what should happen when a typed object is 490 | placed into a weakmap. 491 | 492 | ## Prototypes 493 | 494 | Typed objects introduce several inheritance hierarchies. In addition to the 495 | prototype chains of struct type instances, there are those for the struct 496 | types themselves and for struct type arrays. 497 | 498 | In general, just like with other objects, the `[[Prototype]]` of all involved 499 | instances is set to their constructor's `prototype`: 500 | 501 | For struct type definitions, that means the `[[Prototype]]` is set to 502 | `StructType.prototype`. 503 | 504 | For instances of a struct type `PointStruct`, the `[[Prototype]]` is set to 505 | `PointStruct.prototype`. 506 | 507 | Finally, for instances of a struct type array `PointStruct.Array`, the 508 | `[[Prototype]]` is set to `PointStruct.Array.prototype`. 509 | 510 | ### Shared Base Constructors 511 | 512 | Analogously to typed arrays, which all inherit from 513 | [`%TypedArray%`](https://tc39.github.io/ecma262/#sec-%typedarray%-intrinsic-object), 514 | Struct types and their instances inherit from shared base constructors: 515 | 516 | The `[[Prototype]]` of `StructType.prototype` is `%Type%.prototype`, where 517 | `%Type%` is an intrinsic that's not directly exposed. 518 | 519 | The `[[Prototype]]` of `PointStruct.prototype` is `%Struct%.prototype`, where 520 | `%Struct%` is an intrinsic that's not directly exposed. 521 | 522 | The `[[Prototype]]` of `PointStruct.Array.prototype` is `%Struct%.Array.prototype`, 523 | where, again, `%Struct%` is an intrinsic that's not directly exposed. 524 | 525 | All `[[Prototype]]`s in these hierarchies are set immutably. 526 | 527 | In code: 528 | 529 | ```js 530 | const PointStruct = new StructType({x: float64, y: float64}); 531 | const LineStruct = new StructType({from: Point, to: Point}); 532 | 533 | let point = new PointStruct(); 534 | let points1 = new PointStruct.Array(2); 535 | let points2 = new PointStruct.Array(5); 536 | let line = new LineStruct(); 537 | 538 | // These all yield `true`: 539 | point.__proto__ === PointStruct.prototype; 540 | line.__proto__ === LineStruct.prototype; 541 | line.from.__proto__ === PointStruct.prototype; 542 | 543 | points1.__proto__ === PointStruct.Array.prototype; 544 | points2.__proto__ === points1.__proto__; 545 | 546 | // Pretending %Struct% is directly exposed: 547 | PointStruct.prototype.__proto__ === %Struct%.prototype; 548 | PointStruct.Array.prototype.__proto__ === %Struct%.Array.prototype; 549 | ``` 550 | -------------------------------------------------------------------------------- /ootypes.md: -------------------------------------------------------------------------------- 1 | # Extensions to describe object-oriented languages 2 | 3 | This is just a sketch rather than a detailed writeup. The ideas are 4 | preliminary. 5 | 6 | ## Typed object references 7 | 8 | Currently there is no way to embed a *reference* to another typed 9 | object except by using the rather imprecise type `object`. Sometimes 10 | it'd be nice to say not only that this is a reference to an object, 11 | but rather a reference to an object of a particular type: 12 | 13 | ```js 14 | var RoomType = new StructType(...); 15 | 16 | var PlayerType = new StructType({ 17 | ..., 18 | room: object, // but we know it's a Room! 19 | ... 20 | }); 21 | ``` 22 | 23 | To support this, each non-value type definition (structs and arrays, 24 | basically) is extended with a property `refType`. This lets you encode 25 | the example above as follows: 26 | 27 | ```js 28 | var RoomType = new StructType(...); 29 | 30 | var PlayerType = new StructType({ 31 | ... 32 | room: RoomType.refType, 33 | ... 34 | }); 35 | ``` 36 | 37 | A `refType` is a primitive type. It will throw if asked to coerce a 38 | value that is not a typed object instance of the appropriate type. It 39 | understands subtyping (see below). It does not care about opacity and 40 | will accept either a transparent or opaque typed object of the 41 | appropriate type. 42 | 43 | The rules above mean that ref types can also be used as a type assertion: 44 | 45 | ```js 46 | // throws if `shouldBePlayer` is not an instance of PlayerType 47 | player = PlayerType.refType(shouldBePlayer); 48 | ``` 49 | 50 | ## Cyclic type definitions 51 | 52 | The above system doesn't work if you want to have cyclic type definitions: 53 | 54 | ```js 55 | var RoomType = new StructType({ 56 | ..., 57 | players: PlayerType.arrayType.refType, 58 | }); 59 | 60 | var PlayerType = new StructType({ 61 | ... 62 | room: RoomType.refType, 63 | ... 64 | }); 65 | ``` 66 | 67 | After all, which type do you want to define first? 68 | 69 | To address this, we permit *forward* declarations of a struct type. 70 | 71 | ```js 72 | var RoomType = new StructType(); // no details yet 73 | var PlayerType = new StructType(); 74 | 75 | RoomType.define({ 76 | ..., 77 | players: PlayerType.arrayType.refType, 78 | }); 79 | 80 | PlayerType.define({ 81 | ... 82 | room: RoomType.refType, 83 | ... 84 | }); 85 | ``` 86 | 87 | In this way, the existing constructor forms are just shorthand for 88 | invoking `define` immediately. It is an error to try and instantiate a 89 | type that has not been defined. 90 | 91 | ## Subtyping 92 | 93 | Extend `StructType` with an `extends` method to define a subtype. 94 | Subtypes inherit all fields. 95 | 96 | ```js 97 | var MobileType = new StructType({ 98 | ... 99 | room: RoomType.refType, 100 | ... 101 | }); 102 | 103 | var MonsterType = MobileType.extend({ 104 | ... 105 | }); 106 | 107 | var PlayerType = MobileType.extend({ 108 | ... 109 | }); 110 | ``` 111 | 112 | References to a supertype are assigned from a subtype: 113 | 114 | ```js 115 | var player = new PlayerType(); 116 | var t = MobileType.refType(player); // successful, returns player 117 | ``` 118 | -------------------------------------------------------------------------------- /overloading.md: -------------------------------------------------------------------------------- 1 | # Operator overloading 2 | 3 | To make value types usable, it should be possible to override the 4 | operators that are used with them. The system I sketch out here is not 5 | specific to value types, however. It is based on Brendan's ideas for 6 | multidispatch. 7 | 8 | *WARNING:* This is *very* preliminary and *not even close* to thought 9 | through. 10 | 11 | The idea is to employ multidispatch. The correct function to apply is 12 | based on the prototype of both the left and right hand sides (note 13 | that the prototype of value operands is obtained via the registry 14 | described in the [value type](valuetypes.md) proposal). There is some 15 | form of operator registry that lets you add pairs of prototypes and, 16 | when an operator is applied, the most specific one is chosen. 17 | Ideally, the existing coercion rules would be subsumed under this 18 | registry. 19 | 20 | We might wish to guarantee that adding prototype pairs to the registry 21 | never disturbs existing pairs (that is, the registry grows 22 | monotonically). Alternatively, maybe allow users to freeze prototype 23 | pairs. 24 | 25 | We do not attempt to guarantee algebraic equivalence guarantees and 26 | the like (IEEE floating point already removes most of them anyhow). 27 | 28 | - What operators can be overloaded? 29 | - Not `===`, but yes `==` 30 | - `+`, `-`, etc 31 | - Not `.`, but what about `[]`? 32 | 33 | -------------------------------------------------------------------------------- /valuetypes.md: -------------------------------------------------------------------------------- 1 | # Explainer for value objects 2 | 3 | **Note:** The material in this spec is under discussion. 4 | 5 | ## Motivation 6 | 7 | JavaScript today includes a distinction between value types (number, 8 | string, etc) and object types. Value types have a number of useful 9 | properties: 10 | 11 | - they have no identity and compare by value; 12 | - they have no associated mutable state; 13 | - they can be sent across realms; 14 | - etc. 15 | 16 | Unfortunately, the set of value types is correct closed and defined by 17 | the spec. It would be nice if it were user extensible. 18 | 19 | There are a number of goals: 20 | 21 | - The system should be compatible with the existing value types like 22 | string, symbol, and number. Ideally it should be a generalization of 23 | how those value types work rather than its own different thing. 24 | - The engine should be able to freely optimize the representation of 25 | these new value types. For example, it should be possible to 26 | duplicate them invisibly to the user and/or store a copy on the 27 | stack and so forth. 28 | - ... 29 | 30 | ## The ValueType meta type definition 31 | 32 | We introduce the meta type definition `ValueType`. `ValueType` can be 33 | used to define a new value type definition. It takes the same 34 | arguments as `StructType`, with the exception of a mandatory symbol as 35 | the first argument. Unlike `StructType`, `ValueType` is a regular 36 | function, not a constructor, and hence you do not use the `new` 37 | keyword with it (I'll explain why in the section on prototypes below). 38 | 39 | ```js 40 | var ColorType = ValueType(Symbol("Color"), 41 | {r: uint8, g: uint8, b: uint8, a: uint8}); 42 | ``` 43 | 44 | The type of every field in a value type must itself be a value type or 45 | else an exception is thrown. 46 | 47 | If you wish to create a value array, that can be done by using a variation 48 | on the `ValueType` constructor: 49 | 50 | ```js 51 | var ColorArrayType = ValueType(Symbol("Color"), uint8, 4); 52 | ``` 53 | 54 | Passing a type and number is equivalent to creating a value type with 55 | fields named `0...n-1` and all with the same type. 56 | 57 | *NOTE:* 58 | [Issue #1](https://github.com/nikomatsakis/typed-objects-explainer/issues/1) 59 | proposes to extend this constructor form for fixed-length arrays to 60 | non-value types as well. 61 | 62 | ## Creating a value type instance 63 | 64 | Instances of a value type are created by invoking the function with 65 | a single argument. This argument will be coerced to the value in the same 66 | was as with a normal type definition. Hence: 67 | 68 | ```js 69 | var color = ColorType({r: 22, g: 44, b: 66, a: 88}); 70 | color.b == 66 71 | ``` 72 | 73 | ## Assigning to fields of a value type 74 | 75 | Values are deeply immutable. Therefore, attempting to assign to a 76 | field of a value throws an error: 77 | 78 | ```js 79 | var color = ColorType({r: 22, g: 44, b: 66, a: 88}); 80 | color.b = 66 // throws 81 | ``` 82 | 83 | ## Prototypes 84 | 85 | Instances of value types are *not* objects. Therefore, like a number 86 | or a string, they have no associated prototype. Nonetheless, we would 87 | like to be able to attach prototypes to them. To achieve this, we 88 | generalize the wrapper mechanism that is used with the existing value 89 | types like string etc. 90 | 91 | The idea is that there is a *per-realm* (not global!) registry of 92 | value types defined in the engine. This registry is pre-populated with 93 | the engine-defined types like strings and numbers (which map to the 94 | existing constructors `String`, `Number`, etc). 95 | 96 | When you apply the `.` operator to a value, what happens is that the 97 | value's type is looked up in the per-realm registry. If an entry `C` 98 | exists, then a wrapper object is constructed whose prototype is 99 | `C.prototype` and execution proceeds as normal. 100 | 101 | When you invoke the `ValueType` function to define a new type, you are 102 | in fact submitting a new type to be added to the registry, if it does 103 | not already exist. If an equivalent entry already exists, it is 104 | returned to you. (More details on this process are in the next 105 | section.) 106 | 107 | All this machinery together basically means you can attach methods to 108 | value types as normal and everything works: 109 | 110 | ```js 111 | var ColorType = ValueType(Symbol("Color"), 112 | {r: uint8, g: uint8, b: uint8, a: uint8}); 113 | 114 | ColorType.prototype.average = function() { 115 | (this.r + this.g + this.b) / 3 116 | }; 117 | 118 | var color = ColorType({r: 22, g: 44, b: 66, a: 88}); 119 | color.average() // yields 44 120 | ``` 121 | 122 | ## The registry and structural equivalence 123 | 124 | I said above that when you invoke the `ValueType` constructor, you are 125 | in fact submitting a new value type to be registered. If a 126 | *structurally equivalent* value type already exists, then that object 127 | will be returned to you. This, by the way, is why we do not use the 128 | `new` keyword when creating a value type -- it is not *necessarily* a 129 | new object that is returned. 130 | 131 | But when are two types *structurally equivalent*? The answer is 132 | fairly straightforward: 133 | 134 | - Every type definition is structurally equivalent to itself. 135 | - Two user-defined value-type definitions are structurally equivalent 136 | if: 137 | - they both have the same associated symbol 138 | - they both have the same field names defined in the same order 139 | - the types of all their fields are structurally equivalent to one another 140 | 141 | Let's go through some examples. First, if you register the same type 142 | definition twice, you get back the same object each time: 143 | 144 | ```js 145 | // Registering the same type twice yields the same object 146 | // the second time: 147 | var ColorSymbol = Symbol("Color"); 148 | var ColorType1 = ValueType(ColorSymbol, 149 | {r: uint8, g: uint8, b: uint8, a: uint8}); 150 | var ColorType2 = ValueType(ColorSymbol, 151 | {r: uint8, g: uint8, b: uint8, a: uint8}); 152 | ColorType1 === ColorType2 153 | ``` 154 | 155 | Note that the symbol is very important. If I try to register two value 156 | types that have the same field names and field types, but different 157 | symbols, then they are *not* considered equivalent (here, assume that 158 | the omitted text `...` is the same both times): 159 | 160 | ```js 161 | // Different symbols: 162 | var ColorType1 = ValueType(Symbol("Color"), ...); 163 | var ColorType2 = ValueType(Symbol("Color"), ...); 164 | ColorType1 !== ColorType2 165 | ``` 166 | 167 | Similarly, if the symbols are the same but the field names are not, 168 | the types are not equivalent: 169 | 170 | ```js 171 | var ColorSymbol = Symbol("Color"); 172 | var ColorType1 = ValueType(ColorSymbol, 173 | {r: uint8, g: uint8, b: uint8, a: uint8}); 174 | var ColorType2 = ValueType(ColorSymbol, 175 | {y: uint16, u: uint16, v: uint16}); 176 | ColorType1 !== ColorType2 177 | ``` 178 | 179 | ### Design discussion 180 | 181 | This setup is designed to permit multiple libraries to be combined 182 | into a single JS app without interfering with one another. By default, 183 | all libaries will create their own symbols and hence their own 184 | distinct sets of value types. This means you can freely add methods to 185 | value types that you define without fear of stomping on somebody 186 | else's value type. 187 | 188 | On the other hand, if libraries wish to interoperate, they can do so 189 | via the symbol registry. Similarly, value types that should be 190 | equivalent across realms can be achieved using the global symbol 191 | registry. 192 | 193 | Put another way, ES6 introduced symbols in order to give libraries a 194 | flexible means of declaring when they wish to use the same name to 195 | refer to the same thing versus using the same name in different ways. 196 | Value types build on that. 197 | 198 | ## Comparing values 199 | 200 | Two values are consider `===` if their types are structurally 201 | equivalent (as defined above) and their values are (recursively) 202 | `===`. As a simple case of this definition, if I instantiate two 203 | instances of the same value type with the same values, they are `===`: 204 | 205 | ```js 206 | var color = ColorType({r: 22, g: 44, b: 66, a: 88}); 207 | color === ColorType({r: 22, g: 44, b: 66, a: 88}); 208 | color !== ColorType({r: 88, g: 66, b: 44, a: 22}); 209 | ``` 210 | 211 | In fact, If I define `ColorType` twice, but using the same `Symbol`, 212 | instances are *still* equivalent (here, assuming that the stuff I 213 | omitted with `...` is the same in both cases): 214 | 215 | ```js 216 | var symbol = Symbol("Color"); 217 | var ColorType1 = ValueType(symbol, {...}); // ... == rgba 218 | var ColorType2 = ValueType(symbol, {...}); 219 | ColorType1({r:22, ...}) === ColorType2({r: 22, ...}) // ... == same values 220 | ``` 221 | 222 | However, If I define `ColorType` twice, but using different `Symbol` 223 | values, then instances are *not* the same (again, assume that the stuff 224 | I omitted with `...` is the same in both cases): 225 | 226 | ```js 227 | var ColorType1 = ValueType(Symbol(...), {...}); // ... == rgba 228 | var ColorType2 = ValueType(Symbol(...), {...}); 229 | ColorType1({r:22, ...}) !== ColorType2({r: 22, ...}) // ... == same values 230 | ``` 231 | 232 | Note that I have only defined `===` here. It is not clear to me at 233 | this moment what the behavior of `==` ought to be: it might be useful 234 | for `==` to ignore the symbol and only consider field names and types 235 | when deciding whether two types are equivalent. Alternatively, if we 236 | introduce overloadable operators as [described here](overloading.md), 237 | then this could potentially be done in user libraries. 238 | 239 | ## The typeof operator 240 | 241 | The `typeof` operator applied to a value type yields its associated 242 | symbol. This changes `typeof` so that it does not always return a 243 | string. If this is deemed too controversial, then `typeof` could 244 | return the stringification of the associated symbol, but that seems 245 | suboptimal to me because in that case `(typeof v === typeof w)` has no 246 | real meaning (it also permits "forgery" of the return values 247 | `"string"` and so forth). On the other hand, that makes user-defined 248 | value types somewhat inconsistent with the existing value types like 249 | string. 250 | 251 | --------------------------------------------------------------------------------