├── .github
└── workflows
│ ├── build.yml
│ └── pr.yml
├── .gitignore
├── .npmrc
├── ASYNC-LOCKING-WAITING.md
├── ATTACHING-BEHAVIOR.md
├── CHROMIUM-DEV-TRIAL.md
├── CODE-SHARING-IDEAS.md
├── README.md
├── SYNTAX.md
├── package.json
└── spec
└── index.html
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Deploy spec
2 | on:
3 | push:
4 | branches: [ main ]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-node@v4
11 | with:
12 | node-version: lts/*
13 | - run: npm install
14 | - run: npm run build
15 | - name: Publish HTML to GitHub Pages
16 | uses: peaceiris/actions-gh-pages@v3
17 | with:
18 | publish_dir: ./build
19 | keep_files: true
20 | github_token: ${{ secrets.GITHUB_TOKEN }}
21 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: Publish PR to gh-pages/pr/
2 | on:
3 | pull_request:
4 | branches: [ main ]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | if: ${{ github.event.number }}
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: actions/setup-node@v4
12 | - run: npm install
13 | - run: npm run build
14 | - name: Publish HTML to GitHub Pages
15 | uses: peaceiris/actions-gh-pages@v4
16 | with:
17 | publish_dir: ./build
18 | destination_dir: pr/${{ github.event.number }}/
19 | keep_files: true
20 | github_token: ${{ secrets.GITHUB_TOKEN }}
21 | - id: get-preview-url
22 | name: Get preview url
23 | run: echo "::set-output name=preview-url::https://tc39.es/$(basename $GITHUB_REPOSITORY)/pr/${{ github.event.number }}"
24 | shell: bash
25 | - name: Post Preview Comment
26 | uses: phulsechinmay/rewritable-pr-comment@v0.3.0
27 | with:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | COMMENT_IDENTIFIER: tc39_pr_preview_comment
30 | message: |
31 | A preview of this PR can be found at ${{ steps.get-preview-url.outputs.preview-url }}.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/ASYNC-LOCKING-WAITING.md:
--------------------------------------------------------------------------------
1 | # Asynchronous Locking and Waiting
2 |
3 | In a web browser, the main thread cannot block, with `Atomics.Mutex.lock` and `Atomics.Condition.wait` throwing instead of blocking as it would it in a worker.
4 |
5 | This document lays out the asynchronous versions of `Atomics.Mutex.lock` and `Atomics.Condition.wait` which behave fairly differently.
6 |
7 | ## `Atomics.Mutex.lockAsync`
8 |
9 | The core design of `lockAsync` mirrors that of the [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API). A callback to run under lock is passed, and is scheduled to run on the event loop when the lock is acquired.
10 |
11 | ```javascript
12 | let mutex = new Atomics.Mutex;
13 |
14 | let releasedPromise = Atomics.Mutex.lockAsync(
15 | mutex,
16 | async () => {
17 | doCriticalSectionStuff();
18 | await doAsyncCriticalSectionStuff();
19 | });
20 |
21 | // releasedPromise settles when the mutex is released.
22 | await releasedPromise;
23 | ```
24 |
25 | There are two promises:
26 |
27 | 1. A **waiting promise**, that when settled, releases the mutex. That is, this promise controls the unlock. This promise is resolved with the return value of callback to run under lock.
28 | 1. A **released promise**, that settles when the mutex is released. This is returned by `Atomics.Mutex.lockAsync` itself.
29 |
30 | For most use cases, the waiting promise is invisible, as in the above example. Since it is resolved with the return value of the callback, passing in a callback that returns a promise gives the user explicit control of when to release the lock.
31 |
32 | ```javascript
33 | let mutex = new Atomics.Mutex;
34 |
35 | let resolveWaiting;
36 | let myWaitingPromise = new Promise((resolve) => {
37 | resolveWaiting = resolve;
38 | });
39 |
40 | let resolveAcquired;
41 | let myAcquiredPromise = new Promise((resolve) => {
42 | resolveAcquired = resolve;
43 | });
44 |
45 | let releasedPromise = Atomics.Mutex.lockAsync(
46 | mutex,
47 | () => {
48 | resolveAcquired();
49 | return myWaitingPromise;
50 | });
51 |
52 | await myAcquiredPromise;
53 | doCriticalSectionStuff();
54 | resolveWaiting();
55 |
56 | await releasedPromise;
57 | ```
58 |
59 | ### Performance and starvation
60 |
61 | `lockAsync` has *drastically* different performance implications than `lock`.
62 |
63 | - Because the callback to run under lock is queued on the event loop, the critical section both runs at some arbitrary time after the lock was acquired and takes much longer to execute.
64 | - Implementations may choose to starve async waiters in favor of sync waiters.
65 |
66 | ## `Atomics.Condition.waitAsync`
67 |
68 | Actually this one is easy. `waitAsync` returns a promise that's settled when the condition is notified.
69 |
70 | ```javascript
71 | let mutex = new Atomics.Mutex;
72 | let cv = new Atomics.Condition;
73 |
74 | await Atomics.Mutex.lockAsync(mutex, async () => {
75 | // This can be called on any agent. But has *drastically* different
76 | // performance implications than sync counterparts.
77 | await Atomics.Condition.waitAsync(cv, mutex);
78 | });
79 | ```
80 |
--------------------------------------------------------------------------------
/ATTACHING-BEHAVIOR.md:
--------------------------------------------------------------------------------
1 | # Attaching Behavior to Shared Structs
2 |
3 | The shared part of shared structs refers to data. Because functions in JS are closures that are deeply tied to their creation Realm, they are not shareable in the same way data is. For this reason, methods are not allowed in shared struct declarations by default. In the fullness of time, it is desirable to add truly shareable functions to JS (see [CODE-SHARING-IDEAS.md](CODE-SHARING-IDEAS.md) for a collection of ideas).
4 |
5 | Nevertheless, it is crucial to the developer experience of shared structs that one _can_ attach behavior to them.
6 |
7 | For example, a codebase may want to incrementally adopt multithreading in an existing codebase by converting some existing `class` declarations. These existing `class` declarations are likely to have methods. Without the ability to attach methods to shared structs, the whole codebase would need to be refactored such that uses of shared structs instances use free functions instead of method calls. This is poor DX that should be solved.
8 |
9 | ## Realm-local prototypes
10 |
11 | In the short term, the crux of the idea to attach behavior to shared structs is the ability to have realm-local prototype objects, analogous to the per-realm primitive prototype wrappers (e.g. `Number.prototype`). A shared struct that declares its prototype to be realm-local will have a plain object lazily created in the current realm upon access. Since the prototype is a plain object and realm-local, it can reference any value, including ordinary functions.
12 |
13 | Pending bikeshedding, the proposed syntax is as follows.
14 |
15 | ```javascript
16 | shared struct SharedThing {
17 | // Incantation for saying SharedThing has an realm-local prototype.
18 | //
19 | // Open to bikeshedding.
20 | with nonshared prototype;
21 | }
22 | let s = new SharedThing;
23 |
24 | // Inside worker A:
25 | Object.getPrototypeOf(s).whereAmI = () => { return "local to A" };
26 |
27 | // Inside worker B:
28 | Object.getPrototypeOf(s).whereAmI = () => { return "local to B" };
29 | ```
30 |
31 | Semantically, realm-local prototypes is equivalent to a per-realm `WeakMap` keyed by some abstract "type identifier" corresponding to the shared struct (e.g. the underlying `map` in V8's object representation). This key is not exposed to user code.
32 |
33 | ## Coordinating identity continuity among workers
34 |
35 | Shared struct declarations evaluate to new constructors per evaluation, just like ordinary `class` declarations. In other words, they are nominally typed instead of structurally typed. However, this poses an identity discontinuity issue in a multi-worker setting.
36 |
37 | Consider the following:
38 |
39 | ```javascript
40 | // Inside worker A:
41 | shared struct SharedThing {
42 | with nonshared prototype;
43 |
44 | x;
45 | y;
46 | }
47 |
48 | SharedThing.prototype = {
49 | foo() { console.log("do a foo"); }
50 | };
51 | let thing1 = new SharedThing;
52 |
53 | workerB.postMessage(thing1);
54 |
55 | // Inside worker B:
56 | shared struct SharedThing {
57 | with nonshared prototype;
58 |
59 | x;
60 | y;
61 | }
62 |
63 | SharedThing.prototype = {
64 | foo() { console.log("do a foo"); }
65 | };
66 |
67 | onmessage = (thing) => {
68 | // undefined is not a function!
69 | thing.foo();
70 | };
71 | ```
72 |
73 | In the code snippet above, `thing.foo()` doesn't work because worker A and worker B have different evaluations of `SharedThing`. While the two `SharedThing`s have identical layout, they are nevertheless different "types". So, `thing` from worker A is an instance of worker A's `SharedThing`, whose realm-local prototype was never set up by worker B. Worker B only set up the realm-local prototype of its own evaluation of `SharedThing`.
74 |
75 | The proposed solution is to use an agent cluster-wide registry to correlate multiple evaluations of shared struct declarations. Shared struct declarations that are declared to be **registered** have the following semantics:
76 |
77 | - When evaluated, if the source location does not exist in the registry, insert a new entry whose key is the source location, and whose value is a description of the layout (i.e. order and names of instance fields, and whether the prototype is realm-local).
78 | - When evaluated, if the source location already exists in the registry, check if the layout exactly matches the current evaluation's layout. If not, either throw or do nothing (open design question).
79 | - Evaluation of the declaration returns a constructor function that always creates instances recognizable by the engine as the same layout. In other words, the registry deduplicates layout.
80 | - Lookup and insertion into the registry are synchronized and threadsafe.
81 | - The registry is not programatically accessible to user code.
82 |
83 | The primary purpose of the registry is to coordinate the setting up of realm-local prototypes without additional communication.
84 |
85 | Consider again the above example with registered shared structs.
86 |
87 | ```javascript
88 | // Inside worker A:
89 | shared struct SharedThing {
90 | // Incantation to perform auto-correlation.
91 | with registered;
92 | with nonshared prototype;
93 |
94 | x;
95 | y;
96 | }
97 |
98 | SharedThing.prototype = {
99 | foo() { console.log("do a foo"); }
100 | };
101 |
102 | let thing1 = new SharedThing;
103 |
104 | workerB.postMessage(thing1);
105 |
106 | // Inside worker B:
107 | shared struct SharedThing {
108 | // Incantation to perform auto-correlation.
109 | with registered;
110 | with nonshared prototype;
111 |
112 | x;
113 | y;
114 | }
115 |
116 | SharedThing.prototype = {
117 | foo() { console.log("do a foo"); }
118 | };
119 |
120 | onmessage = (thing) => {
121 | thing.foo(); // do a foo
122 | };
123 | ```
124 |
125 | Because the two shared struct declarations have the same name, `SharedThing`, and are both declared registered, the VM is able to connect the types under the hood.
126 |
127 | ### Incompatible layouts
128 |
129 | If two registered shared struct declarations share the same name, their layout must match. This means the order and name of their fields must match, and whether the prototype is realm-local must match. A non-match might throw or do nothing (open design question). If the chosen behavior is to do nothing (i.e. do not deduplicate), the console ought to warn.
130 |
131 | ```javascript
132 | {
133 | registered shared struct SharedThing {
134 | x;
135 | y;
136 | }
137 | }
138 |
139 | {
140 | registered shared struct SharedThing {
141 | y;
142 | x;
143 | } // either throws or silently do not deduplicate
144 | }
145 | ```
146 |
147 | ### Realm-local prototype "surprise"
148 |
149 | While each registered shared struct declaration continues to evaluate to a new constructor function, the layout deduplication means that a registered shared struct declaration may evaluate to a function with an already populated realm-local prototype.
150 |
151 | ```javascript
152 | {
153 | shared struct SharedThing {
154 | with nonshared prototype;
155 |
156 | x;
157 | y;
158 | }
159 |
160 | SharedThing.prototype.foo = "bar";
161 | }
162 |
163 | {
164 | shared struct SharedThing {
165 | with nonshared prototype;
166 |
167 | x;
168 | y;
169 | }
170 |
171 | console.log(SharedThing.prototype.foo); // "bar"
172 | }
173 | ```
174 |
175 | While this may be surprising, this pattern seems unlikely to occur in the wild. It is unlikely that the same registered shared struct declarations will be evaluated multiple times. Declaring something registered signals intent that it ought to be behave the same across worker boundaries.
176 |
177 | ## Communication channel
178 |
179 | Since the registry key is source location, the key is unforgeable. If on layout mismatch, the chosen behavior is to do nothing, then this is not an observable communication channel. If instead the chosen behavior is to throw, then to exploit the registry as a communication channel requires being able to modify source text and triggering re-evaluations of said source text, which seems unlikely.
180 |
181 | ## Should this be the default?
182 |
183 | It is arguable that the registered behavior ought to be the default behavior for all shared structs. Making it the default would save on modifier keyword soup and also makes the currently proposed default less surprising.
184 |
185 | ## Bundler guidance
186 |
187 | Because the source location is now meaningful, duplication of shared struct declarations in source text is no longer a semantics-preserving transformation. Bundlers and program transforms must take care during e.g., tree shaking on specific worker threads, to duplicate shared struct declarations.
188 |
189 | ## Upshot
190 |
191 | The combination of agent-local prototypes and an agent cluster-wide registry means worker threads can independently evaluate the same shared struct definitions (i.e. same source text) without additional coordination, and things more or less work as expected.
192 |
193 | ### Performance advantages
194 |
195 | Additionally, it has the performance advantage of performing deduplication of layouts at declaration time. This means more monomorphic inline caches and better sharing. Without deduplication of layout, the number of morally equivalent shared struct types scales with the number of threads, which means a multithreaded program becomes more polymorphic with more threads, which is highly undesirable.
196 |
--------------------------------------------------------------------------------
/CHROMIUM-DEV-TRIAL.md:
--------------------------------------------------------------------------------
1 | # Chromium Dev Trial
2 |
3 | An experimental implementation of JS shared memory features is gated behind Chromium as of M108 with the the `JavaScriptExperimentalSharedMemory` flag. You can enable this by passing `--enable-features=JavaScriptExperimentalSharedMemory` to a chromium build or navigating to `chrome://flags/#enable-javascript-experimental-shared-memory`.
4 |
5 | # New APIs
6 |
7 | Enabling the experimental JS shared memory features enables the following APIs, which differ in syntax from the current proposal but have largely the same semantics.
8 |
9 | ## Shared structs
10 |
11 | Shared structs can be made by first making shared struct types, and then making instances of those types. Unlike the proposal, there is no support for `struct` syntax and this must be done programmatically.
12 |
13 | Shared structs can only store JS primitives and other shared objects. They have a `null` constructor and prototype. They are sealed, i.e. new fields cannot be added, thus the need to declare a type up front.
14 |
15 | ```javascript
16 | // SharedStructType takes an array of field names.
17 | let SharedBox = new SharedStructType(["x"]);
18 |
19 | let sharedBox = new SharedBox();
20 | let sharedBox2 = new SharedBox();
21 |
22 | sharedBox.x = 42; // x is declared and rhs is primitive
23 | sharedBox.x = sharedBox2; // x is declared and rhs is shared
24 | assertThrows(() => { sharedBox.x = {}; }); // rhs is not shared
25 | ```
26 |
27 | Shared structs may be brand checked with `SharedStructType.isSharedStruct`. Note that `isSharedStruct(o)` returns `true` if `o` is _any_ shared struct.
28 |
29 | ## Shared arrays
30 |
31 | Shared arrays are fixed-length JS arrays.
32 |
33 | ```javascript
34 | // Make a 1024-long shared array.
35 | let sharedArray = new SharedArray(1024);
36 |
37 | sharedArray[0] = 42; // rhs is primitive
38 | sharedArray[0] = new SharedBox(); // rhs is shared
39 | assertThrows(() => { sharedArray[0] = {}; }); // rhs is not shared
40 | ```
41 |
42 | Shared arrays may be brand checked with `SharedArray.isSharedArray`.
43 |
44 | ## Synchronization primitives
45 |
46 | Two high-level synchronization primitives are provided: `Atomics.Mutex` and `Atomics.Condition`.
47 |
48 | Because they are shared objects, and shared objects cannot have methods, they are used using free methods on the `Atomics` namespace object.
49 |
50 | ```javascript
51 | let mutex = new Atomics.Mutex;
52 |
53 | // This would block the current thread if the lock is held
54 | // by another thread. On the web, this cannot be used on the
55 | // main thread since the main thread cannot block.
56 | Atomics.Mutex.lock(mutex, function runsUnderLock() {
57 | // Do critical section stuff
58 | });
59 |
60 | // tryLock is like lock but returns true if the lock was acquired
61 | // without blocking, and false is the lock is held by another
62 | // thread.
63 | Atomics.Mutex.tryLock(mutex, function runsUnderLock() {
64 | // Do critical section stuff
65 | });
66 |
67 | sharedBox.x = mutex; // rhs is shared
68 | ```
69 |
70 | ```javascript
71 | let cv = new Atomics.Condition;
72 |
73 | // This blocks the current thread, and cannot be used on the main
74 | // thread. The passed in mutex must be locked.
75 | Atomics.Mutex.lock(mutex, () => {
76 | Atomics.Condition.wait(cv, mutex);
77 | });
78 |
79 | // Waiters can notified with a count. A count of undefined or
80 | // +Infinity means "all waiters".
81 | let count = 1;
82 | let numWaitersWokenUp = Atomics.Condition.notify(cv, count);
83 | ```
84 |
85 | Mutexes and condition variables may be brand checked with `Atomics.Mutex.isMutex` and `Atomics.Condition.isCondition`, respectively.
86 |
87 | # Sharing across threads
88 |
89 | In a browsing context, shared objects created by the above APIs can be shared across threads if and only if SharedArrayBuffers can be shared across threads. That is, if and only if [`self.crossOriginIsolated`](https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated) is `true`, which is determined by setting [COOP and COEP headers](https://web.dev/coop-coep/).
90 |
91 | ```javascript
92 | // main.js
93 | let worker = new Worker("worker.js");
94 |
95 | // Assume sharedBox from above.
96 | assert(self.crossOriginIsolated);
97 | worker.postMessage({ sharedBox });
98 |
99 | // By default, property accesses are racy. This is a data race!
100 | sharedBox.x = "race written by main";
101 |
102 | // Lock-free accesses can be totally ordered (sequentially consistent)
103 | Atomics.store(sharedBox, "x", "sequentially consistent");
104 | ```
105 |
106 | ```javascript
107 | // worker.js
108 | onmessage = function (e) {
109 | // This is the same sharedBox as the one the main thread has, not a copy
110 | let sharedBox = e.data;
111 |
112 | // This is a data race!
113 | console.log(sharedBox.x);
114 |
115 | // This is totally ordered
116 | console.log(Atomics.load(sharedBox, "x"));
117 | };
118 | ```
119 |
--------------------------------------------------------------------------------
/CODE-SHARING-IDEAS.md:
--------------------------------------------------------------------------------
1 | # Code Sharing Ideas for Shared Structs
2 |
3 | This is a companion document collecting ideas for future proposals to share code to make shared structs more ergonomic.
4 |
5 | ## Shared function
6 |
7 | ### Introduction
8 |
9 | Intuitively, shared functions are functions that are sufficiently restricted to be shareable across agents. At a high level, this means they cannot close over local state, globals are looked up according to the caller's Realm, and have a prototype separate from `Function.prototype`.
10 |
11 | Shared functions are a new kind of exotic callable object with the following properties:
12 | - Shared functions form a separate lexical environment chain than plain functions; shared function environments are immutable
13 | - During evaluation of a shared function expression or declaration, if there is a free variable that would capture a value that is neither a primitive nor a shared struct or is a `Symbol`, throw a `ReferenceError`
14 | - During evaluation of a shared function expression or declaration, if there is a free variable with the same name as a binding in an outer plain function or as a top-level lexical binding, throw a `ReferenceError`
15 | - During execution of a shared function, free identifiers are looked up in the caller's Realm
16 | - During execution of a shared function, new objects are allocated in the caller's Realm
17 | - During execution of a shared function, they use their caller's Realm whenever a Realm is needed
18 | - Shared functions are sealed
19 | - Shared functions have as their prototype `SharedFunction.prototype`, a sealed object that is itself shared across agents; there is a single `SharedFunction.prototype` per agent cluster
20 |
21 | ### Example
22 |
23 | ```javascript
24 | // main.js
25 | shared struct SharedBox { x; }
26 | shared function mkSharedFun() {
27 | let sharedBox = new SharedBox;
28 | sharedBox.x = "main";
29 | let prim = 42;
30 | return shared function(arg) {
31 | // Console is a free variable, it'll be looked up in the caller's Realm.
32 | console.log(sharedBox.x, prim, arg);
33 | };
34 | }
35 | let worker = new Worker('worker.js');
36 | worker.postMessage({ sharedFun: mkSharedFun() });
37 | ```
38 |
39 | ```javascript
40 | // worker.js
41 | onmessage = function(e) {
42 | let sharedFun = e.data.sharedFun;
43 |
44 | // Logs "main 42 worker" using the worker's console
45 | sharedFun('worker');
46 | };
47 | ```
48 |
49 | ```javascript
50 | // Cannot accidentally close over a local binding.
51 | function outerLocal() {
52 | let local;
53 | return shared function() { console.log(local); }
54 | }
55 |
56 | // Unreachable due to SyntaxError
57 | ```
58 |
59 | ```javascript
60 | // Cannot close over a non-shared value.
61 | assertThrows(() => { (shared function outer1() {
62 | let x = {}; // Object literal syntax allocates a local object.
63 | return shared function() { x; }
64 | })(); });
65 |
66 | // Shared functions are sealed and not extensible.
67 | shared function s() { }
68 | assertThrows(() => { s.myProp = 42; });
69 |
70 | // Cannot mutate closed-over values
71 | shared function mkBad() {
72 | let x = 42;
73 | return shared function() { x++; }
74 | }
75 | assertThrows(() => { mkBad()(); });
76 | ```
77 |
78 | ### Extending shared structs
79 |
80 | With shared functions, shared structs could be extended to support methods and prototype chains. All instance methods in a `shared struct` expression would be shared functions.
81 |
82 | Shared struct constructors themselves are not shared by default. They can be made shared by prefixing `shared`. A shared constructor will also expose the `.constructor` property on its instances.
83 |
84 | A combined example is shown below.
85 |
86 | ```javascript
87 | // main.js
88 | shared struct SharedPoint {
89 | shared constructor(x, y) { this.x = x; this.y = y; }
90 | x;
91 | y;
92 | add(p) {
93 | // Warning: these are racy!
94 | this.x += p.x;
95 | this.y += p.y;
96 | }
97 | }
98 |
99 | let p = new SharedPoint(1, 1);
100 | let worker = new Worker('worker.js');
101 | worker.postMessage({ p });
102 |
103 | // Depends on timing, this could log 1 or 2.
104 | console.log(p.x);
105 | ```
106 |
107 | ```javascript
108 | // worker.js
109 | onmessage = (e) => {
110 | let p = e.data.p;
111 | let SharedPoint = p.constructor;
112 | console.log(p.add(new SharedPointer(1, 1)));
113 | };
114 | ```
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JavaScript Structs: Fixed Layout Objects and Some Synchronization Primitives
2 |
3 | Stage: 2
4 |
5 | Author: Shu-yu Guo (@syg)
6 |
7 | Champion: Shu-yu Guo (@syg), Ron Buckton (@rbuckton)
8 |
9 | ## Introduction
10 |
11 | This proposal introduces four (4) logical features:
12 |
13 | - [**Structs**](#structs), or unshared structs, which are fixed-layout objects. They behave like `class` instances, with more restrictions that are beneficial for optimizations and analysis.
14 | - [**Shared Structs**](#shared-structs), which are further restricted structs that can be shared and accessed in parallel by multiple agents. They enable shared memory multithreading.
15 | - [**Mutex and Condition**](#mutex-and-condition), which are higher level abstractions for synchronizing access to shared memory.
16 |
17 | This proposal is intended to be minimal, but still useful by itself, without follow-up proposals.
18 |
19 | The motivations are:
20 | - Enable new, high-performance applications to be written in JavaScript and on the web by unlocking shared memory multithreading.
21 | - Give developers an alternative to `class`es that favors a higher performance ceiling and statically analyzability over flexbility.
22 |
23 | Like other shared memory features in JavaScript, it is high in expressive power and high in difficulty to use correctly. This proposal is both intended as an incremental step towards higher-level, easier-to-use (e.g. data-race free by construction) parallelism abstractions as well as an escape hatch for expert programmers who need the expressivity.
24 |
25 | The two design principles that this proposal follows for shared memory are:
26 | 1. Syntax that looks atomic ought to be atomic. (For example, the dot operator on shared structs should only access an existing field and does not tear.)
27 | 1. There are no references from shared objects to non-shared objects. The shared and non-shared heaps are conceptually separate, with direct references only going one way.
28 |
29 | ### Structs
30 |
31 | Unshared structs are a refinement on JavaScript `class`es. They are declarative, like classes, but layout of struct instances are fixed.
32 |
33 | Structs have the following properties:
34 | - They are created with [integrity level sealed](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal). In other words, they have a fixed layout. Specifically, they cannot be extended with new properties. The value in their [[Prototype]] cannot change. Every syntactically declared field is writable, enumerable, and non-configurable.
35 | - They have "one-shot initialization". Once a struct instance is accessible to JavaScript code, all their fields, including those of their superclasses, are already initialized to `undefined`.
36 | - Struct `constructor` methods have a usable `this` value, which is the one-shot initialize instance, on entry. As a result, return override is not expressible. `super()` is still allowed, but not required.
37 | - They can only extend other structs.
38 | - The struct constructor itself is also sealed.
39 | - Struct methods are non-generic. Their `this` value must be an instance of the struct or of a subclass.
40 |
41 | Struct declarations use the `struct` keyword, and share their syntax with `class` declarations.
42 |
43 | Some examples:
44 |
45 | ```javascript
46 | struct Box {
47 | constructor(x) { this.x = x; }
48 | x;
49 | }
50 |
51 | let box = new Box(0);
52 | box.x = 42; // x is declared
53 | assertThrows(() => { box.y = 8.8; }); // structs are sealed
54 | assertThrows(() => { box.__proto__ = {}; }); // structs are sealed
55 | ```
56 |
57 | ```javascript
58 | struct Point extends Box { // allowed because Box is also a struct
59 | constructor(x, y) {
60 | this.y = y; // the this value is immediately usable
61 | super(x); // calls the super constructor
62 | return {}; // the return value is discarded, no return override
63 | }
64 |
65 | distance(other) {
66 | return Math.sqrt((other.x - this.x) ** 2 +
67 | (other.y - this.y) ** 2);
68 | }
69 |
70 | y;
71 | }
72 |
73 | let p = new Point(1, 2);
74 | let fake = { x: 4, y: 5 };
75 | // methods are non-generic
76 | assertThrows(() => Point.prototype.distance.call(fake, p));
77 | p.distance(fake); // allowed, the receiver is a Point
78 | ```
79 |
80 | ### Shared Structs
81 |
82 | Shared structs are structs with even more restricted behavior, so as to make them amenable to be shared between different agents. Their fields can be accessed in parallel by multiple agents.
83 |
84 | Shared structs, in addition to the properties listed for structs above, have the following additional properties:
85 | - They can only extend other shared structs.
86 | - They have a `null` prototype.
87 | - They cannot contain instance methods.
88 | - Their instances can be communicated to other agents without copying.
89 | - Their fields can only reference primitives or other shared structs. That is, they cannot point to unshared values.
90 | - They cannot be frozen, because that would change their shape, which must be immutable to be amenable for sharing.
91 |
92 | ```javascript
93 | // main.js
94 | shared struct SharedBox {
95 | x;
96 | }
97 |
98 | let sharedBox = new SharedBox();
99 | let sharedBox2 = new SharedBox();
100 |
101 | sharedBox.x = 42; // x is declared and rhs is primitive
102 | sharedBox.x = sharedBox2; // x is declared and rhs is shared
103 | assertThrows(() => { sharedBox.x = {}; }) // rhs is not a shared struct
104 |
105 | // can programmatically test if a value can be shared
106 | assert(Reflect.canBeShared(sharedBox2));
107 | assert(!Reflect.canBeShared({}));
108 |
109 | let worker = new Worker('worker.js');
110 | worker.postMessage({ sharedBox });
111 |
112 | sharedBox.x = "main"; // x is declared and rhs is primitive
113 | console.log(sharedBox.x);
114 | ```
115 |
116 | ```javascript
117 | // worker.js
118 | onmessage = function(e) {
119 | let sharedBox = e.data.sharedBox;
120 | sharedBox.x = "worker"; // x is declared and rhs is primitive
121 | console.log(sharedBox.x);
122 | };
123 | ```
124 |
125 | The above program is permitted to print out any interleaving:
126 | - main main
127 | - main worker
128 | - worker worker
129 | - worker main
130 |
131 | #### Shared Arrays
132 |
133 | Shared Arrays are a fixed-length arrays that may be shared across agents. They are a special case of shared structs. There is no special syntax for Shared Arrays. They have a read-only `length` property.
134 |
135 | ```javascript
136 | let sharedArray = new SharedArray(100);
137 | assert(sharedArray.length === 100);
138 | for (i = 0; i < sharedArray.length; i++) {
139 | // like shared structs, all elements are initialized to undefined
140 | assert(sharedArray[i] === undefined);
141 | }
142 |
143 | let worker = new Worker('worker_array.js');
144 | worker.postMessage({ sharedArray });
145 |
146 | sharedArray[0] = "main";
147 | console.log(sharedArray[0]);
148 | ```
149 |
150 | ```javascript
151 | // worker_array.js
152 | onmessage = function(e) {
153 | let sharedArray = e.data.sharedArray;
154 | sharedArray[0] = "worker";
155 | console.log(sharedArray[0]);
156 | };
157 | ```
158 |
159 | Like the shared struct example, the above program is also permitted to print out any interleaving:
160 | - main main
161 | - main worker
162 | - worker worker
163 | - worker main
164 |
165 | #### Memory Model
166 |
167 | By default, field accesses on shared structs are unordered. Sequentially consistent accesses are performed via the following new overloads on existing `Atomics` static methods.
168 |
169 | The following pseudocode describes the new overloads.
170 |
171 | ```javascript
172 | class Atomics {
173 | // ... existing stuff
174 |
175 | // Performs a seq-cst load on struct[fieldName].
176 | static load(struct, fieldName);
177 |
178 | // Performs a seq-cst store of value in struct[fieldName].
179 | static store(struct, fieldName, value);
180 |
181 | // Performs a seq-cst exchange on struct[fieldName] with newValue.
182 | //
183 | // newValue must be a primitive or a shared struct.
184 | //
185 | // Returns the old value.
186 | static exchange(struct, fieldName, newValue);
187 |
188 | // Performs a seq-cst compare-exchange on struct[fieldName].
189 | //
190 | // If the existing value in struct[fieldName] is expected, replace it with
191 | // replacement.
192 | //
193 | // replacement must be a primitive or a shared struct.
194 | //
195 | // Returns the value in struct[fieldName] before the replacement,
196 | // regardless of whether the value was exchanged.
197 | static compareExchange(struct, fieldName, expected, replacement);
198 | }
199 | ```
200 |
201 | Shared struct field accesses can never tear, regardless of the memory order. That is, a read of a shared struct field sees exactly one write of a shared struct field, never a mix of multiple writes.
202 |
203 | ### Mutex and Condition
204 |
205 | Higher-level synchronization primitives are needed to aid in writing threadsafe code. This proposal adds `Atomics.Mutex` and `Atomics.Condition`.
206 |
207 | #### Atomics.Mutex
208 |
209 | `Atomics.Mutex` is a non-recursive mutex. The mutex itself is a shared struct with no fields. It is used via static methods on `Atomics.Mutex`. (If [per-Realm prototypes](#attaching-methods-to-shared-structs) become part of this proposal, those static methods will become prototype methods.)
210 |
211 | The following pseudocode describes the API.
212 |
213 | ```javascript
214 | shared struct Atomics.Mutex {
215 | // Creates a new mutex.
216 | constructor();
217 |
218 | // Attempt to acquire the lock on mutex.
219 | //
220 | // If the mutex is already locked, this blocks the agent until the lock is
221 | // acquired.
222 | //
223 | // If unlockToken is not undefined, it must be an empty UnlockToken.
224 | //
225 | // If unlockToken is undefined, returns a new UnlockToken.
226 | // Otherwise return the unlockToken that was passed in.
227 | static lock(mutex: Mutex,
228 | unlockToken: UnlockToken|undefined = undefined)
229 | : UnlockedToken;
230 |
231 | // Attempt to acquire the lock on mutex.
232 | //
233 | // If timeout is not Infinity, only block for timeout milliseconds. If
234 | // the operation timed out without acquiring the lock, returns
235 | // null.
236 | //
237 | // If timeout is 0, returns immediately without blocking if the lock
238 | // cannot be acquired.
239 | //
240 | // unlockToken behaves as it does in the lock method.
241 | static lockIfAvailable(mutex: Mutex,
242 | timeout: Number,
243 | unlockToken: UnlockToken|undefined = undefined)
244 | : UnlockToken|null;
245 | }
246 | ```
247 |
248 | A mutex can only be unlocked via `UnlockToken`s, which are unlock capabilities. These tokens are ordinary objects, not shared structs. For high-performance applications, an application can allocate an empty `UnlockToken` and reuse it. This API is inspired by Rust and aims to minimize misuse (e.g. double unlocks).
249 |
250 | ```javascript
251 | class Atomics.Mutex.UnlockToken {
252 | // Creates an empty UnlockToken.
253 | constructor();
254 |
255 | // Returns true if this token is non-empty.
256 | get locked(): bool;
257 |
258 | // If this token is non-empty, unlock the underlying mutex and returns true.
259 | // Otherwise returns false.
260 | unlock(): bool;
261 |
262 | // Like unlock, but returns undefined instead of bool.
263 | [Symbol.dispose](): undefined;
264 | }
265 | ```
266 |
267 | For example,
268 |
269 | ```javascript
270 | shared struct MicrosoftSharePoint {
271 | x;
272 | y;
273 | mutex;
274 | }
275 |
276 | let point = new MicrosoftSharePoint();
277 | point.mutex = new Atomics.Mutex();
278 |
279 | let worker = new Worker('worker_mutex.js');
280 | worker.postMessage({ point });
281 |
282 | // assume this agent can block
283 | {
284 | using lock = Atomics.Mutex.lock(point.mutex);
285 | point.x = "main";
286 | point.y = "main";
287 | }
288 |
289 | {
290 | using lock = Atomics.Mutex.lock(point.mutex);
291 | console.log(point.x, point.y);
292 | }
293 | ```
294 |
295 | ```javascript
296 | // worker_mutex.js
297 | onmessage = function(e) {
298 | let point = e.data.point;
299 | {
300 | using lock = Atomics.Mutex.lock(point.mutex);
301 | point.x = "worker";
302 | point.y = "worker";
303 | }
304 | };
305 | ```
306 |
307 | The above program prints one of the following:
308 | - main main
309 | - worker worker
310 |
311 | That is, because point the `x` and `y` fields are accessed under lock, no agent can observe a mix of main and worker values.
312 |
313 | #### Atomics.Condition
314 |
315 | `Atomics.Condition` is a condition variable. It is a shared struct with no fields. It is used via static methods on `Atomics.Condition`. (If [per-Realm prototypes](#attaching-methods-to-shared-structs) become part of this proposal, those static methods will become prototype methods.)
316 |
317 | The following pseudocode describes the API.
318 |
319 | ```javascript
320 | shared struct Atomics.Condition {
321 | // Creates a new condition variable.
322 | constructor();
323 |
324 | // Atomically unlocks the unlockToken and blocks the current agent until cv
325 | // is notified.
326 | //
327 | // unlockToken must not be empty.
328 | //
329 | // When the agent is resumed, the lock underlying mutex in unlockToken is
330 | // reacquired, blocking if necessary.
331 | //
332 | // Returns undefined.
333 | static wait(cv: Condition,
334 | unlockToken: UnlockToken): undefined
335 |
336 | // Atomically unlocks the unlockToken and blocks the current agent until cv
337 | // is notified or timed out.
338 | //
339 | // unlockToken must not be empty.
340 | //
341 | // timeout is in milliseconds.
342 | //
343 | // If predicate is not undefined, this method returns after predicate returns
344 | // true, or if timed out. If timed out, the final return value of the
345 | // predicate is returned. Whenever the predicate is executing, the lock on the
346 | // underlying mutex of unlockToken is acquired.
347 | //
348 | // If predicate is undefined, returns true if the wait was notified, and false
349 | // if timed out.
350 | static waitFor(cv: Condition,
351 | unlockToken: UnlockToken,
352 | timeout: Number,
353 | predicate: Callable|undefined): bool
354 |
355 | // Notifies count waiters waiting on the condition variable cv.
356 | //
357 | // Returns the number of waiters that were notified.
358 | static notify(cv: Condition,
359 | count: Number = Infinity)
360 | : Number;
361 | }
362 | ```
363 |
364 | ## Open Questions
365 |
366 | ### Attaching methods to shared structs
367 |
368 | Because functions are deeply unshareable, shared structs currently do not have methods. However, this is a severe ergonomic pain point. It is also counter to encapsulation, which may have real harm in encouraging more thread-unsafe code. The current direction being explored to enable methods, which is undergoing discussion, is to give shared structs the following additional features:
369 | 1. A per-Realm prototype object, which is an ordinary object and thus can contain methods. This corresponds to making the [[Prototype]] internal field on shared structs thread-local storage.
370 | 1. A correlation mechanism to correlate evaluations of the same "logical" shared struct declaration across different agents.
371 |
372 | This is an involved topic and has its own document. See [ATTACHING-BEHAVIOR.md](ATTACHING-BEHAVIOR.md).
373 |
374 | ## Future Work
375 |
376 | ### Asynchronous locking and waiting
377 |
378 | Asynchronous locking is planned upcoming work but is out of scope of this proposal. See [ASYNC-LOCKING-WAITING.md](ASYNC-LOCKING-WAITING.md) for `lockAsync` and `waitAsync`.
379 |
380 | ## WasmGC interoperability
381 |
382 | The [WasmGC proposal](https://github.com/WebAssembly/gc/blob/master/proposals/gc/Overview.md) adds fixed layout, garbage-collected objects to Wasm. While the details of the type system of these objects are yet to be nailed down, interoperability with JavaScript is a requirement.
383 |
384 | WasmGC objects have opaque storage and are not aliased by linear memory, so they cannot be exposed as all Wasm memory is exposed today via `ArrayBuffer`s. We propose structs to be the reflection of WasmGC objects in JS.
385 |
386 | WasmGC objects exposed to JS should behave the same as structs, modulo extra type checking that WasmGC require that JS structs do not. JS structs is also a good foundation for reflecting into Wasm as WasmGC objects, but that is currently left as future work as it may need a typed field extensions to be worthwhile.
387 |
388 | Further, WasmGC itself will eventually have multithreading. It behooves us to maintain a single memory model between JavaScript and Wasm as we have today, even with higher-level object abstractions.
389 |
390 | ## Out-of-Scope
391 |
392 | ### Value semantics, immutability, and operator overloading
393 |
394 | This proposal does not intend to explore the space of objects with value semantics, including immutability and operator overloading. Structs have identity like other objects and are designed to be used like other objects. Value semantics is a sufficient departure that it may be better solved with other proposals that focus on that space.
395 |
396 | ### Sophisticated type systems
397 |
398 | This proposal does not intend to explore sophisticated type and runtime guard systems. It is even more minimal than the closest spiritual ancestor, the [Typed Objects proposal](https://github.com/tschneidereit/proposal-typed-objects/blob/main/explainer.md), in that we do not propose integral types for sized fields. (Typed and sized fields are reserved for future work.)
399 |
400 | ### Binary data overlay views
401 |
402 | This proposal does not intend to explore the space of overlaying structured views on binary data in an `ArrayBuffer`. This is a requirement arising from the desire for WasmGC integration, and WasmGC objects are similarly opaque.
403 |
404 | Structured overlays are fundamentally about aliasing memory, which we feel is both a different problem domain, has significant performance downsides, and sufficiently solvable today in userland. For example, see [buffer-backed objects](https://github.com/GoogleChromeLabs/buffer-backed-object).
405 |
406 | Notably, structured overlays in JavaScript essentially involves allocating unshared wrappers *per agent*. If an application has shared state with a complex structure, such as a large object graph, recreating that structure via a set of wrappers per agent negates the memory use benefits of *shared* memory. Structured overlays would work for specific application architectures where the structure of the shared state itself is simple, like a byte buffer.
407 |
408 | ## Implementation guidance
409 |
410 | ### Immutable shapes
411 |
412 | Structs are declared with fixed layout up front. Engines should make an immutable shape for such objects. Optimizers can optimize field accesses without worrying about deopts.
413 |
414 | ### Shared structs: make sure fields are pointer-width and aligned
415 |
416 | Shared structs should store fields such that underlying architectures can perform atomic stores and loads. This usually eans the fields should be at least pointer-width and aligned.
417 |
418 | ### Shared structs: strings will be difficult
419 |
420 | Except for strings, sharing primitives in the engine is usually trivial, especially for NaN-boxing implementations.
421 |
422 | Strings in production engines have in-place mutation to transition representation in order to optimize for different use ases (e.g. ropes, slices, canonicalized, etc). Sharing strings will likely be the most challenging part of the mplementation.
423 |
424 | It is possible to support sharing strings by copying-on-sharing, but may be too slow. If possible, lockfree mplementations of in-place mutations above is ideal.
425 |
426 | ### Synchronization primitives: they must be moving GC-safe
427 |
428 | Production engines use moving garbage collectors, such as generational collectors and compacting collectors. If JS ynchronization primitives are implemented under the hood as OS-level synchronization primitives, those primitives most ikely depend on an unchanging address in memory and are _not_ moving GC-safe.
429 |
430 | Engines can choose to pin these objects and make them immovable.
431 |
432 | Engines can also choose to implement synchronization primitives entirely in userspace. For example, WebKit's `ParkingLot`](https://webkit.org/blog/6161/locking-in-webkit/) is a userspace implementation of Linux futexes. This may have other benefits, such as improved and tuneable performance.
433 |
--------------------------------------------------------------------------------
/SYNTAX.md:
--------------------------------------------------------------------------------
1 | # Structs
2 |
3 | A `struct` declaration shares many similarities with a `class` declaration, with respect to syntax, and can have
4 | elements such as constructors, fields, methods, getters, and setters:
5 | ```
6 | struct S {
7 | foo;
8 |
9 | constructor() { }
10 |
11 | bar() { }
12 |
13 | get baz() { }
14 | set baz(value) { }
15 | }
16 | ```
17 |
18 | Methods and accessors are attached to a realm-local prototype reachable via the `struct`'s constructor. Unlike a
19 | `class`, the fields of a `struct` have a fixed layout. You cannot `delete` fields from a `struct` instance, and
20 | cannot attach new ones.
21 |
22 | ## Static Elements
23 |
24 | In addition, fields, methods, getters, and setters can also be declared `static`, and you may also use `static {}`
25 | blocks:
26 |
27 | ```
28 | struct S {
29 | static foo = 1;
30 |
31 | static {
32 | }
33 |
34 | static bar() {}
35 | static get baz() { }
36 | static set baz(value) { }
37 | }
38 | ```
39 |
40 | As with a `class`, any `static` elements are attached to the constructor.
41 |
42 | ## Computed Property Names
43 |
44 | Members of a `struct` may have computed property names, and the names of methods, getters, setters, and _static_ fields
45 | can be either strings or symbols. However, it is unclear as to whether symbols will be allowed for the names of
46 | fixed-layout fields.
47 |
48 | ```
49 | const sym = Symbol();
50 |
51 | struct S {
52 | [sym] = 1; // probably not ok?
53 |
54 | [Symbol.iterator]() { } // ok
55 | }
56 | ```
57 |
58 | ## Private Names
59 |
60 | Ideally, a `struct` may also have private-named elements, just like a `class`. Private-named fields would also be
61 | part of the `struct`'s fixed field layout.
62 |
63 | ```
64 | struct S {
65 | #bar;
66 | get bar() { return this.#bar; }
67 | set bar(value) { this.#bar = value; }
68 | }
69 | ```
70 |
71 | ## Subclassing
72 |
73 | A `struct` can inherit from another `struct` but cannot inherit from a regular function or `class` due to the nature of
74 | how field layouts are established.
75 |
76 | ```
77 | struct Sup {
78 | x;
79 | constructor(x) {
80 | this.x = x;
81 | }
82 | }
83 | struct Sub extends Sup {
84 | y;
85 | constructor(x, y) {
86 | super(x);
87 | this.y = y;
88 | }
89 | }
90 | ```
91 |
92 | Unlike with `class`, however, returning a different object from the superclass constructor does not change `this`,
93 | and *all* fields of the constructed instance are installed on the `this` value when it is allocated so as to maintain
94 | the fixed field layout inherent in `struct` values.
95 |
96 | ## `null` Prototypes
97 |
98 | A `struct` can inherit from `null` instead of another `struct`, which produces a data-only `struct` with no prototypal
99 | behavior. As such, a `struct` that extends `null` may not have non-static methods, getters, or setters.
100 |
101 | ```
102 | struct S extends null {
103 | foo;
104 | bar;
105 | }
106 | ```
107 |
108 | # Shared Structs
109 |
110 | Shared structs borrow the same syntax as non-shared struct, and are indicated with a leading `shared` keyword:
111 |
112 | ```
113 | shared struct S {
114 | foo;
115 |
116 | constructor() { }
117 |
118 | bar() { }
119 |
120 | get baz() { }
121 | set baz(value) { }
122 | }
123 | ```
124 |
125 | As with non-shared structs, methods, getters, and setters are attached to a realm-local prototype. The constructor
126 | function produced for a shared struct is also local to the realm, but is capable of producing shared struct instances.
127 |
128 | When a shared struct is passed to a `Worker`, it is given a different realm-local prototype in that `Worker`'s realm.
129 | If that `Worker` happens to load the same `shared struct` declaration from the same path/position, the prototype from
130 | that declaration is used, and that declaration's constructor can be used to create new instances of a `shared struct`
131 | that, for all intents and purposes, is essentially the same as an instance from the main thread.
132 |
133 | ## Private Names
134 |
135 | Ideally, a `shared struct` may also have private-named elements, just like a `class`. Private-named fields would also be
136 | part of the `shared struct`'s fixed field layout.
137 |
138 | ```
139 | shared struct S {
140 | #bar;
141 | get bar() { return this.#bar; }
142 | set bar(value) { this.#bar = value; }
143 | }
144 | ```
145 |
146 | However, this may not guarantee quite the same level of "hard privacy" that a private name on a class might have,
147 | depending on what final mechanisms we might choose for correlating `shared struct` declarations. If malfeasant code is
148 | able to construct a `Worker` that can forge an alternative definition for `S`, they would be able to craft replacement
149 | methods that can directly access shared private state. For that reason, it would be recommended to avoid granting
150 | untrusted code the ability to load an alternative definition for the `shared struct`, possibly through the use of CSP.
151 |
152 | # Future Syntax Support: Decorators
153 |
154 | Struct syntax can be further extended in the future for additional consistency with `class`, such as through the use
155 | of decorators. Decorators have the potential to be useful for a number of scenarios, such as supplying runtime
156 | annotations for WASM-GC types to fixed layout fields, or reusing existing decorators for methods and fields (including
157 | auto-`accessor` fields):
158 |
159 | ```
160 | shared struct S {
161 | @wasm_type("i8") x;
162 | @wasm_type("i32") y;
163 |
164 | @logged method() {}
165 | }
166 | ```
167 |
168 | # Future Syntax Support: References
169 |
170 | While not yet proposed for Stage 1, some form of the [`ref` proposal](https://github.com/rbuckton/proposal-refs) would
171 | also benefit `shared struct` declarations that utilize private-named fields as a mechanism to perform atomic updates:
172 |
173 | ```
174 | shared struct S {
175 | #count = 0;
176 |
177 | get value() { return this.#count; }
178 |
179 | increment() {
180 | return Atomics.add(ref this.#count, 1);
181 | }
182 |
183 | decrement() {
184 | return Atomics.sub(ref this.#count, 1);
185 |
186 | }
187 | }
188 | ```
189 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "proposal-structs",
4 | "description": "JavaScript Structs (build)",
5 | "license": "Fair",
6 | "scripts": {
7 | "build": "npm run build-loose -- --strict",
8 | "build-loose": "node -e 'fs.mkdirSync(\"build\", { recursive: true })' && ecmarkup --load-biblio @tc39/ecma262-biblio --verbose spec/index.html build/index.html --assets-dir build/ --lint-spec",
9 | "format": "emu-format --write 'spec/*.html'"
10 | },
11 | "homepage": "https://github.com/syg/proposal-structs",
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/syg/proposal-structs.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/tc39/proposal-structs/issues"
18 | },
19 | "devDependencies": {
20 | "@tc39/ecma262-biblio": "^2.1.2407",
21 | "ecmarkup": "^19.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/spec/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | title: JavaScript Structs
5 | stage: 2
6 | contributors: Shu-yu Guo
7 | markEffects: true
8 |
9 |
10 |
11 | Structs, Shared Structs, and Synchronization Primitives
12 | We extend the JS language with fixed-layout objects both for unshared and shared uses and high-level synchronization primitive APIs. This spec draft is organized by logical feature.
13 |
14 |
15 |
16 | Structs
17 | Structs are fixed-layout objects. They are constructed with the integrity level ~sealed~, and have all declared fields initialized before the instance is made available to user code. They may only extend other structs. Their instance methods are non-generic, and throw *TypeError* exceptions when the *this* value is not either an instance of the struct declaration within which the method was declared, or a subclass of that struct declaration.
18 |
19 |
20 | Syntax
21 |
22 |
23 |
24 | StructDeclaration[Yield, Await, Default] :
25 | `struct` [no LineTerminator here] BindingIdentifier[?Yield, ?Await] StructTail[?Yield, ?Await]
26 | [+Default] `struct` [no LineTerminator here] StructTail[?Yield, ?Await]
27 |
28 | StructTail[Yield, Await] :
29 | ClassHeritage[?Yield, ?Await]? `{` StructBody[?Yield, ?Await]? `}`
30 |
31 | StructBody[Yield, Await] :
32 | ClassElementList[?Yield, ?Await]
33 |
34 |
35 | A struct definition is always strict mode code.
36 |
37 |
38 |
39 | Static Semantics: Early Errors
40 | StructBody : ClassElementList
41 |
42 | -
43 | It is a SyntaxError if PrototypePropertyNameList of |ClassElementList| contains more than one occurrence of *"constructor"*.
44 |
45 | -
46 | It is a Syntax Error if the PrivateBoundIdentifiers of |ClassElementList| contains any duplicate entries, unless the name is used once for a getter and once for a setter and in no other entries, and the getter and setter are either both static or both non-static.
47 |
48 |
49 |
50 |
51 |
52 |
53 | DefineStructField (
54 | _receiver_: an Object,
55 | _fieldRecord_: a ClassFieldDefinition Record,
56 | ): ~unused~
57 |
58 |
60 |
61 | 1. Let _fieldName_ be _fieldRecord_.[[Name]].
62 | 1. If _fieldName_ is a Private Name, then
63 | 1. Perform ! PrivateFieldAdd(_receiver_, _fieldName_, *undefined*).
64 | 1. Else,
65 | 1. Assert: _fieldName_ is a property key.
66 | 1. Perform ! DefinePropertyOrThrow(_receiver_, _fieldName_, PropertyDescriptor { [[Value]]: *undefined*, [[Writable]]: *true*, [[Enumerable]]: *true*, [[Configurable]]: *false* }).
67 | 1. Return ~unused~.
68 |
69 |
70 |
71 |
72 |
73 | InitializeStructInstanceFieldsAndBrand (
74 | _receiver_: an Object,
75 | _constructor_: an ECMAScript function object,
76 | ): ~unused~
77 |
78 |
80 |
81 | 1. If _constructor_.[[ConstructorKind]] is ~derived~, then
82 | 1. Let _parent_ be ! _constructor_.[[GetPrototypeOf]]().
83 | 1. Perform InitializeStructInstanceFieldsAndBrand(_receiver_, _parent_).
84 | 1. If _constructor_ has a [[StructBrand]] internal slot, then
85 | 1. Prepend _constructor_.[[StructBrand]] to _receiver_.[[StructBrands]].
86 | 1. NOTE: Shared Struct constructors do not have a [[StructBrand]] internal slot because per-Realm prototypes is currently an open design question and are not included in this draft. Without per-Realm prototypes, Shared Structs cannot have methods, and there are no users of Shared Struct brands.
87 | 1. Let _methods_ be the value of _constructor_.[[PrivateMethods]].
88 | 1. For each PrivateElement _method_ of _methods_, do
89 | 1. Perform ! PrivateMethodOrAccessorAdd(_receiver_, _method_).
90 | 1. Let _fields_ be the value of _constructor_.[[Fields]].
91 | 1. For each element _fieldRecord_ of _fields_, do
92 | 1. Perform DefineStructField(_receiver_, _fieldRecord_).
93 | 1. Return ~unused~.
94 |
95 |
96 |
97 |
98 |
99 | RunFieldInitializer (
100 | _receiver_: an Object,
101 | _fieldRecord_: a ClassFieldDefinition Record,
102 | ): either a normal completion containing ~unused~ or a throw completion
103 |
104 |
106 |
107 | 1. Let _fieldName_ be _fieldRecord_.[[Name]].
108 | 1. Let _initializer_ be _fieldRecord_.[[Initializer]].
109 | 1. If _initializer_ is not ~empty~, then
110 | 1. Let _initValue_ be ? Call(_initializer_, _receiver_).
111 | 1. If _fieldName_ is a Private Name, then
112 | 1. Perform ? PrivateSet(_receiver_, _fieldName_, _initValue_).
113 | 1. Else,
114 | 1. Assert: _fieldName_ is a property key.
115 | 1. Perform ? DefinePropertyOrThrow(_receiver_, _fieldName_, PropertyDescriptor { [[Value]]: _initValue_, [[Writable]]: *true*, [[Enumerable]]: *true*, [[Configurable]]: *false* }).
116 | 1. Return ~unused~.
117 |
118 |
119 |
120 |
121 |
122 | RunStructInstanceFieldInitializers (
123 | _receiver_: an Object,
124 | _constructor_: an ECMAScript function object,
125 | ): either a normal completion containing ~unused~ or a throw completion
126 |
127 |
129 |
130 | 1. If _constructor_.[[ConstructorKind]] is ~derived~, then
131 | 1. Let _parent_ be ! _constructor_.[[GetPrototypeOf]]().
132 | 1. Perform ? RunStructInstanceFieldInitializers(_receiver_, _parent_).
133 | 1. Let _fields_ be the value of _constructor_.[[Fields]].
134 | 1. For each element _fieldRecord_ of _fields_, do
135 | 1. Perform ? RunFieldInitializer(_receiver_, _fieldRecord_).
136 | 1. Return ~unused~.
137 |
138 |
139 |
140 |
141 |
142 | Runtime Semantics: StructDefinitionEvaluation (
143 | _structBinding_: a String or *undefined*,
144 | _structName_: a property key,
145 | ): either a normal completion containing a function object or an abrupt completion
146 |
147 |
149 | StructTail : ClassHeritage? `{` StructBody? `}`
150 |
151 | 1. Let _env_ be the LexicalEnvironment of the running execution context.
152 | 1. Let _structEnv_ be NewDeclarativeEnvironment(_env_).
153 | 1. If _structBinding_ is not *undefined*, then
154 | 1. Perform ! _structEnv_.CreateImmutableBinding(_structBinding_, *true*).
155 | 1. Let _outerPrivateEnvironment_ be the running execution context's PrivateEnvironment.
156 | 1. Let _classPrivateEnvironment_ be NewPrivateEnvironment(_outerPrivateEnvironment_).
157 | 1. If |StructBody| is present, then
158 | 1. For each String _dn_ of the PrivateBoundIdentifiers of |StructBody|, do
159 | 1. If _classPrivateEnvironment_.[[Names]] contains a Private Name _pn_ such that _pn_.[[Description]] is _dn_, then
160 | 1. Assert: This is only possible for getter/setter pairs.
161 | 1. Else,
162 | 1. Let _name_ be a new Private Name whose [[Description]] is _dn_.
163 | 1. Append _name_ to _classPrivateEnvironment_.[[Names]].
164 | 1. If |ClassHeritage| is not present, then
165 | 1. Let _protoParent_ be %Object.prototype%.
166 | 1. Let _constructorParent_ be %Function.prototype%.
167 | 1. Else,
168 | 1. Set the running execution context's LexicalEnvironment to _structEnv_.
169 | 1. NOTE: The running execution context's PrivateEnvironment is _outerPrivateEnvironment_ when evaluating |ClassHeritage|.
170 | 1. Let _superclassRef_ be Completion(Evaluation of |ClassHeritage|).
171 | 1. Set the running execution context's LexicalEnvironment to _env_.
172 | 1. Let _superclass_ be ? GetValue(? _superclassRef_).
173 | 1. If _superclass_ is *null*, then
174 | 1. Let _protoParent_ be *null*.
175 | 1. Let _constructorParent_ be %Function.prototype%.
176 | 1. Else if _superclass_ does not have a [[IsStructConstructor]] internal slot, then
177 | 1. Throw a *TypeError* exception.
178 | 1. Else,
179 | 1. Let _protoParent_ be ? Get(_superclass_, *"prototype"*).
180 | 1. If _protoParent_ is not an Object and _protoParent_ is not *null*, throw a *TypeError* exception.
181 | 1. Let _constructorParent_ be _superclass_.
182 | 1. Let _proto_ be OrdinaryObjectCreate(_protoParent_, « [[StructBrand]] »).
183 | 1. Let _structSerial_ be the value of GlobalStructSerial.
184 | 1. Set _proto_.[[StructBrand]] to _structSerial_.
185 | 1. Set GlobalStructSerial to GlobalStructSerial + 1.
186 | 1. NOTE: GlobalStructSerial is a monotonically increasing integer that is globally available. It is shared by all realms. Prior to the evaluation of any ECMAScript code, it is initialized to 0.
187 | 1. NOTE: Structs have one-shot construction, with the user-defined "constructor" method performing post-construction initialization. By the time ECMAScript code has access to a struct instance, it already has all of its declared fields as own properties.
188 | 1. Set the running execution context's LexicalEnvironment to _structEnv_.
189 | 1. Set the running execution context's PrivateEnvironment to _classPrivateEnvironment_.
190 | 1. If |StructBody| is not present, let _initializerParseNode_ be ~empty~.
191 | 1. Else, let _initializerParseNode_ be ConstructorMethod of |StructBody|.
192 | 1. If _initializerParseNode_ is ~empty~, then
193 | 1. Let _initializer_ be ~empty~.
194 | 1. Else,
195 | 1. Let _initializerInfo_ be ? DefineMethod of _initializerParseNode_ with arguments _proto_ and _constructorParent_.
196 | 1. Let _initializer_ be _initializerInfo_.[[Closure]].
197 | 1. Perform SetFunctionName(_initializer_, _structName_).
198 | 1. Let _constructor_ be a new Abstract Closure with no parameters that captures _initializer_ and _structSerial_ and performs the following steps when called:
199 | 1. Let _args_ be the List of arguments that was passed to this function by [[Call]] or [[Construct]].
200 | 1. Let _F_ be the active function object.
201 | 1. If NewTarget is not _F_, throw a *TypeError* exception.
202 | 1. Let _result_ be ? OrdinaryCreateFromConstructor(NewTarget, *"%Object.prototype%"*, « [[StructBrands]] »).
203 | 1. Set _result_.[[StructBrands]] to « _structSerial_ ».
204 | 1. Perform InitializeStructInstanceFieldsAndBrand(_result_, _F_).
205 | 1. Perform ! _result_.[[PreventExtensions]]().
206 | 1. Assert: ! TestIntegrityLevel(_result_, ~sealed~) is *true*.
207 | 1. Perform ? RunStructInstanceFieldInitializers(_result_, _F_).
208 | 1. If _initializer_ is not ~empty~, then
209 | 1. Perform ? Call(_initializer_, _result_).
210 | 1. Return _result_.
211 | 1. Let _F_ be CreateBuiltinFunction(_constructor_, 0, structName, « [[ConstructorKind]], [[SourceText]], [[StructBrand]], [[StructInitializer]], [[IsStructConstructor]] », the current Realm Record, _constructorParent_).
212 | 1. Perform MakeConstructor(_F_, *false*, _proto_).
213 | 1. If |ClassHeritage| is present, set _F_.[[ConstructorKind]] to ~derived~.
214 | 1. Set _F_.[[StructInitializer]] to _initializer_.
215 | 1. Set _F_.[[StructBrand]] to _structSerial_.
216 | 1. Set _F_.[[IsStructConstructor]] to *true*.
217 | 1. If |StructBody| is not present, let _elements_ be a new empty List.
218 | 1. Else, let _elements_ be NonConstructorElements of |StructBody|.
219 | 1. Let _instancePrivateMethods_ be a new empty List.
220 | 1. Let _staticPrivateMethods_ be a new empty List.
221 | 1. Let _instanceFields_ be a new empty List.
222 | 1. Let _staticElements_ be a new empty List.
223 | 1. For each |ClassElement| _e_ of _elements_, do
224 | 1. If IsStatic of _e_ is *false*, then
225 | 1. Let _element_ be Completion(ClassElementEvaluation of _e_ with argument _proto_).
226 | 1. Else,
227 | 1. Let _element_ be Completion(ClassElementEvaluation of _e_ with argument _F_).
228 | 1. If _element_ is an abrupt completion, then
229 | 1. Set the running execution context's LexicalEnvironment to _env_.
230 | 1. Set the running execution context's PrivateEnvironment to _outerPrivateEnvironment_.
231 | 1. Return ? _element_.
232 | 1. Set _element_ to ! _element_.
233 | 1. If _element_ is a PrivateElement, then
234 | 1. Assert: _element_.[[Kind]] is either ~method~ or ~accessor~.
235 | 1. If IsStatic of _e_ is *false*, let _container_ be _instancePrivateMethods_.
236 | 1. Else, let _container_ be _staticPrivateMethods_.
237 | 1. If _container_ contains a PrivateElement _pe_ such that _pe_.[[Key]] is _element_.[[Key]], then
238 | 1. Assert: _element_.[[Kind]] and _pe_.[[Kind]] are both ~accessor~.
239 | 1. If _element_.[[Get]] is *undefined*, then
240 | 1. Let _combined_ be PrivateElement { [[Key]]: _element_.[[Key]], [[Kind]]: ~accessor~, [[Get]]: _pe_.[[Get]], [[Set]]: _element_.[[Set]] }.
241 | 1. Else,
242 | 1. Let _combined_ be PrivateElement { [[Key]]: _element_.[[Key]], [[Kind]]: ~accessor~, [[Get]]: _element_.[[Get]], [[Set]]: _pe_.[[Set]] }.
243 | 1. Replace _pe_ in _container_ with _combined_.
244 | 1. Else,
245 | 1. Append _element_ to _container_.
246 | 1. Else if _element_ is a ClassFieldDefinition Record, then
247 | 1. If IsStatic of _e_ is *false*, append _element_ to _instanceFields_.
248 | 1. Else, append _element_ to _staticElements_.
249 | 1. Else if _element_ is a ClassStaticBlockDefinition Record, then
250 | 1. Append _element_ to _staticElements_.
251 | 1. Set the running execution context's LexicalEnvironment to _env_.
252 | 1. If _structBinding_ is not *undefined*, then
253 | 1. Perform ! _structEnv_.InitializeBinding(_structBinding_, _F_).
254 | 1. Set _F_.[[PrivateMethods]] to _instancePrivateMethods_.
255 | 1. Set _F_.[[Fields]] to _instanceFields_.
256 | 1. For each PrivateElement _method_ of _staticPrivateMethods_, do
257 | 1. Perform ! PrivateMethodOrAccessorAdd(_F_, _method_).
258 | 1. For each element _elementRecord_ of _staticElements_, do
259 | 1. If _elementRecord_ is a ClassFieldDefinition Record, then
260 | 1. Let _result_ be Completion(DefineField(_F_, _elementRecord_)).
261 | 1. Else,
262 | 1. Assert: _elementRecord_ is a ClassStaticBlockDefinition Record.
263 | 1. Let _result_ be Completion(Call(_elementRecord_.[[BodyFunction]], _F_)).
264 | 1. If _result_ is an abrupt completion, then
265 | 1. Set the running execution context's PrivateEnvironment to _outerPrivateEnvironment_.
266 | 1. Return ? _result_.
267 | 1. Set the running execution context's PrivateEnvironment to _outerPrivateEnvironment_.
268 | 1. Perform ! SetIntegrityLevel(_proto_, ~sealed~).
269 | 1. Return _F_.
270 |
271 |
272 |
273 |
274 | Runtime Semantics: BindingStructDeclarationEvaluation ( ): either a normal completion containing a function object or an abrupt completion
275 |
277 | StructDeclaration : `struct` BindingIdentifier StructTail
278 |
279 | 1. Let _structName_ be the StringValue of |BindingIdentifier|.
280 | 1. Let _value_ be ? StructDefinitionEvaluation of |StructTail| with arguments _structName_ and _structName_.
281 | 1. Set _value_.[[SourceText]] to the source text matched by |StructDeclaration|.
282 | 1. Let _env_ be the running execution context's LexicalEnvironment.
283 | 1. Perform ? InitializeBoundName(_structName_, _value_, _env_).
284 | 1. Return _value_.
285 |
286 | StructDeclaration : `struct` StructTail
287 |
288 | 1. Let _value_ be ? StructDefinitionEvaluation of |StructTail| with arguments *undefined* and *"default"*.
289 | 1. Set _value_.[[SourceText]] to the source text matched by |StructDeclaration|.
290 | 1. Return _value_.
291 |
292 |
293 |
294 |
295 | Runtime Semantics: Evaluation
296 | StructDeclaration : `struct` BindingIdentifier StructTail
297 |
298 | 1. Perform ? BindingStructDeclarationEvaluation of this |StructDeclaration|.
299 | 1. Return ~empty~.
300 |
301 |
302 |
303 |
304 |
305 |
306 | Struct Method Exotic Objects
307 |
308 |
309 | A struct method exotic object is an exotic object that wraps another method. A struct method exotic object is callable (it has a [[Call]] internal method). Calling a struct method exotic object checks if the *this* value is a struct instance constructed by the same struct declaration that defined the method, then in calls its wrapped method.
310 |
311 | An object is a struct method exotic object if its [[Call]] internal method uses the following implementation, and its other essential internal methods use the definitions found in . These methods are installed in StructMethodCreate.
312 |
313 | Struct method exotic objects do not have the internal slots of ECMAScript function objects listed in . Instead they have the internal slots listed in , in addition to [[Prototype]] and [[Extensible]].
314 |
315 |
316 |
317 |
318 |
319 | Internal Slot
320 | |
321 |
322 | Type
323 | |
324 |
325 | Description
326 | |
327 |
328 |
329 |
330 |
331 | [[BoundTargetMethod]]
332 | |
333 |
334 | a callable Object
335 | |
336 |
337 | The wrapped method object.
338 | |
339 |
340 |
341 |
342 |
343 |
344 |
345 | [[Call]] (
346 | _thisArgument_: an ECMAScript language value,
347 | _argumentsList_: a List of ECMAScript language values,
348 | ): either a normal completion containing an ECMAScript language value or a throw completion
349 |
350 |
354 |
355 | 1. Let _target_ be _F_.[[BoundTargetMethod]].
356 | 1. Let _homeObject_ be _target_.[[HomeObject]].
357 | 1. Assert: _homeObject_ is not *undefined*.
358 | 1. Assert: _homeObject_ has a [[StructBrand]] internal slot.
359 | 1. If _thisArgument_ is not an Object, throw a *TypeError* exception.
360 | 1. If _thisArgument_ does not have a [[StructBrands]] internal slot, throw a *TypeError* exception.
361 | 1. If _thisArgument_.[[StructBrands]] does not contain _homeObject_.[[StructBrand]], throw a *TypeError* exception.
362 | 1. Return ? Call(_target_, _thisArgument_, _argumentsList_).
363 |
364 |
365 |
366 |
367 |
368 | StructMethodCreate (
369 | _targetMethod_: a function object,
370 | ): either a normal completion containing a function object or a throw completion
371 |
372 |
376 |
377 | 1. Let _proto_ be ? _targetMethod_.[[GetPrototypeOf]]().
378 | 1. Let _internalSlotsList_ be the list-concatenation of « [[Prototype]], [[Extensible]] » and the internal slots listed in .
379 | 1. Let _obj_ be MakeBasicObject(_internalSlotsList_).
380 | 1. Set _obj_.[[Prototype]] to _proto_.
381 | 1. Set _obj_.[[Call]] as described in .
382 | 1. Set _obj_.[[BoundTargetMethod]] to _targetMethod_.
383 | 1. Return _obj_.
384 |
385 |
386 |
387 |
388 |
389 |
390 | Changes to ECMAScript Language: Expressions
391 |
392 | Runtime Semantics: Evaluation
393 | SuperCall : `super` Arguments
394 |
395 | 1. Let _newTarget_ be GetNewTarget().
396 | 1. Assert: _newTarget_ is an Object.
397 | 1. Let _func_ be GetSuperConstructor().
398 | 1. Let _argList_ be ? ArgumentListEvaluation of |Arguments|.
399 | 1. If IsConstructor(_func_) is *false*, throw a *TypeError* exception.
400 | 1. If _func_ has a [[StructInitializer]] internal slot, then
401 | 1. If _func_.[[StructInitializer]] is not ~empty~, then
402 | 1. Let _envRec_ be GetThisEnvironment().
403 | 1. Let _thisValue_ be _envRec_.GetThisBinding().
404 | 1. Return ? Call(_func_.[[StructInitializer]], _thisValue_).
405 | 1. Else,
406 | 1. Return *undefined*.
407 | 1. Let _result_ be ? Construct(_func_, _argList_, _newTarget_).
408 | 1. Let _thisER_ be GetThisEnvironment().
409 | 1. Perform ? _thisER_.BindThisValue(_result_).
410 | 1. Let _F_ be _thisER_.[[FunctionObject]].
411 | 1. Assert: _F_ is an ECMAScript function object.
412 | 1. Perform ? InitializeInstanceElements(_result_, _F_).
413 | 1. Return _result_.
414 |
415 |
416 |
417 |
418 |
419 | Changes to ECMAScript Language: Functions and Classes
420 |
421 |
422 | Runtime Semantics: DefineMethod (
423 | _object_: an Object,
424 | optional _functionPrototype_: an Object,
425 | ): either a normal completion containing a Record with fields [[Key]] (a property key) and [[Closure]] (an ECMAScript function object) or an abrupt completion
426 |
427 |
429 | MethodDefinition : ClassElementName `(` UniqueFormalParameters `)` `{` FunctionBody `}`
430 |
431 | 1. Let _propKey_ be ? Evaluation of |ClassElementName|.
432 | 1. Let _env_ be the running execution context's LexicalEnvironment.
433 | 1. Let _privateEnv_ be the running execution context's PrivateEnvironment.
434 | 1. If _functionPrototype_ is present, then
435 | 1. Let _prototype_ be _functionPrototype_.
436 | 1. Else,
437 | 1. Let _prototype_ be %Function.prototype%.
438 | 1. Let _sourceText_ be the source text matched by |MethodDefinition|.
439 | 1. Let _closure_ be OrdinaryFunctionCreate(_prototype_, _sourceText_, |UniqueFormalParameters|, |FunctionBody|, ~non-lexical-this~, _env_, _privateEnv_).
440 | 1. Perform MakeMethod(_closure_, _object_).
441 | 1. If _object_ has a [[StructBrand]] internal slot, then
442 | 1. NOTE: Struct instance methods' home object have a [[StructBrand]] internal slot.
443 | 1. Set _closure_ to ? StructMethodCreate(_closure_).
444 | 1. Return the Record { [[Key]]: _propKey_, [[Closure]]: _closure_ }.
445 |
446 |
447 |
448 |
449 |
450 | Changes to Modules
451 |
452 |
453 | ExportDeclaration :
454 | `export` ExportFromClause FromClause `;`
455 | `export` NamedExports `;`
456 | `export` VariableStatement[~Yield, +Await]
457 | `export` Declaration[~Yield, +Await]
458 | `export` `default` HoistableDeclaration[~Yield, +Await, +Default]
459 | `export` `default` ClassDeclaration[~Yield, +Await, +Default]
460 | `export` `default` StructDeclaration[~Yield, +Await, +Default]
461 | `export` `default` [lookahead ∉ { `function`, `async` [no LineTerminator here] `function`, `class` }] AssignmentExpression[+In, ~Yield, +Await] `;`
462 |
463 |
464 |
465 | Runtime Semantics: Evaluation
466 |
467 | ExportDeclaration : `export` `default` StructDeclaration
468 |
469 | 1. Let _value_ be ? BindingStructDeclarationEvaluation of |StructDeclaration|.
470 | 1. Let _structName_ be the sole element of the BoundNames of |StructDeclaration|.
471 | 1. If _structName_ is *"\*default\*"*, then
472 | 1. Let _env_ be the running execution context's LexicalEnvironment.
473 | 1. Perform ? InitializeBoundName(*"\*default\*"*, _value_, _env_).
474 | 1. Return ~empty~.
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 | Shared Structs
483 |
484 |
485 | Shared Struct Exotic Objects
486 |
487 | Shared Structs are fixed-layout exotic objects that can be shared across agents and be accessed in parallel from multiple agents. They are like structs with more restricted behaviour so as to be possible to be shared across agents. They cannot contain methods or private fields. Their fields can only hold primitives or other shared values. Accessing their fields is unordered by default and is governed by the memory model. Such accesses can be made sequentially consistent by using newly overloaded Atomics methods.
488 | An object is a Shared Struct if its [[GetOwnProperty]], [[DefineOwnProperty]], [[HasProperty]], [[Get]], [[Set]], and [[Delete]] internal methods use the definitions in this section, and its other essential internal methods use the definitions found in .
489 |
490 |
491 | Critical Section for Shared Struct Creation
492 |
493 |
494 |
495 | EnterSharedStructCreationCriticalSection (
496 | ): ~unused~
497 |
498 |
500 |
501 | 1. Assert: The surrounding agent is not in the critical section for Shared Struct creation.
502 | 1. Wait until no agent is in the critical section for Shared Struct creation, then enter the critical section for Shared Struct creation (without allowing any other agent to enter).
503 | 1. Return ~unused~.
504 |
505 |
506 |
507 |
508 |
509 | LeaveSharedStructCreationCriticalSection (
510 | ): ~unused~
511 |
512 |
514 |
515 | 1. Assert: The surrounding agent is in the critical section for Shared Struct creation.
516 | 1. Leave the critical section for Shared Struct creation.
517 | 1. Return ~unused~.
518 |
519 |
520 |
521 |
522 | This critical section is a specification semantic prescription of the memory model to prohibit the nondeterministic read in ReadSharedStructField from manifesting Shared Struct values that are partially initialized.
523 | This critical section does not provide any ordering guarantees.
524 | In implementations, this critical section is not needed. Implementations must not allow out-of-thin-air reads.
525 |
526 |
527 |
528 |
529 |
530 | SharedStructCreate (
531 | _initializer_: an Abstract Closure with one parameter,
532 | optional _internalSlotsList_: a List of internal slot names,
533 | ) : a Shared Struct
534 |
535 |
537 |
538 | 1. If _internalSlotsList_ is not present, set _internalSlotsList_ to a new empty List.
539 | 1. Perform EnterSharedStructCreationCriticalSection().
540 | 1. Let _result_ be OrdinaryObjectCreate(*null*, _internalSlotsList_).
541 | 1. Set _result_.[[GetOwnProperty]] as specified in .
542 | 1. Set _result_.[[DefineOwnProperty]] as specified in .
543 | 1. Set _result_.[[HasProperty]] as specified in .
544 | 1. Set _result_.[[Get]] as specified in .
545 | 1. Set _result_.[[Set]] as specified in .
546 | 1. Set _result_.[[Delete]] as specified in .
547 | 1. Perform _initializer_(_result_).
548 | 1. Perform ! _result_.[[PreventExtensions]]().
549 | 1. Perform LeaveSharedStructCreationCriticalSection().
550 | 1. Assert: ! TestIntegrityLevel(_result_, ~sealed~) is *true*.
551 | 1. Return _result_.
552 |
553 |
554 |
555 |
556 |
557 | ReadSharedStructField (
558 | _struct_: a Shared Struct,
559 | _field_: a property key,
560 | _order_: ~seq-cst~ or ~unordered~,
561 | ): an ECMAScript language value
562 |
563 |
565 |
566 | 1. Let _execution_ be the [[CandidateExecution]] field of the surrounding agent's Agent Record.
567 | 1. Let _eventsRecord_ be the Agent Events Record of _execution_.[[EventsRecords]] whose [[AgentSignifier]] is AgentSignifier().
568 | 1. Let _storage_ be SharedStructStorage { [[Struct]]: _struct_, [[Field]]: _field_ }.
569 | 1. Perform EnterSharedStructCreationCriticalSection().
570 | 1. Let _rawLanguageValue_ be a nondeterministically chosen ECMAScript language value such that CanBeSharedAcrossAgents(_rawLanguageValue_) is *true*.
571 | 1. Perform LeaveSharedStructCreationCriticalSection().
572 | 1. NOTE: In implementations, _rawLanguageValue_ is the result of a non-atomic or atomic read instruction on the underlying hardware. The nondeterminism is a semantic prescription of the memory model to describe observable behaviour of hardware with weak consistency.
573 | 1. Let _readEvent_ be ReadSharedMemory { [[Order]]: _order_, [[NoTear]]: *true*, [[Storage]]: _storage_ } to _eventsRecord_.[[EventList]].
574 | 1. Append _readEvent_ to _eventsRecord_.[[EventList]].
575 | 1. NOTE: Shared struct field accesses can never tear.
576 | 1. Append Chosen Value Record { [[Event]]: _readEvent_, [[ChosenValue]]: _rawLanguageValue_ } to _execution_.[[ChosenValues]].
577 | 1. Return _rawLanguageValue_.
578 |
579 |
580 |
581 |
582 |
583 | WriteSharedStructField (
584 | _struct_: a Shared Struct,
585 | _field_: a property key,
586 | _value_: an ECMAScript language value,
587 | _order_: ~seq-cst~, ~unordered~, or ~init~,
588 | ): ~unused~
589 |
590 |
592 |
593 | 1. Assert: CanBeSharedAcrossAgents(_value_) is *true*.
594 | 1. Let _execution_ be the [[CandidateExecution]] field of the surrounding agent's Agent Record.
595 | 1. Let _eventsRecord_ be the Agent Events Record of _execution_.[[EventsRecords]] whose [[AgentSignifier]] is AgentSignifier().
596 | 1. Let _storage_ be SharedStructStorage { [[Struct]]: _struct_, [[Field]]: _field_ }.
597 | 1. Append WriteSharedMemory { [[Order]]: _order_, [[NoTear]]: *true*, [[Storage]]: _storage_, [[Payload]]: _value_ } to _eventsRecord_.[[EventList]].
598 | 1. NOTE: Shared struct field accesses can never tear.
599 | 1. Return ~unused~.
600 |
601 |
602 |
603 |
604 |
605 | [[GetOwnProperty]] (
606 | _P_: a property key,
607 | ): a normal completion containing either a Property Descriptor or *undefined*
608 |
609 |
613 |
614 | 1. If _O_ does not have an own property with key _P_, return *undefined*.
615 | 1. Let _D_ be a newly created Property Descriptor with no fields.
616 | 1. Let _X_ be _O_'s own property whose key is _P_.
617 | 1. Assert: _X_ is a data property.
618 | 1. Set _D_.[[Value]] to ReadSharedStructField(_O_, _P_, ~unordered~).
619 | 1. Set _D_.[[Writable]] to *false*.
620 | 1. Set _D_.[[Enumerable]] to *true*.
621 | 1. Set _D_.[[Configurable]] to *false*.
622 | 1. Return _D_.
623 |
624 |
625 |
626 |
627 |
628 | [[DefineOwnProperty]] (
629 | _P_: a property key,
630 | _Desc_: a Property Descriptor,
631 | ): either a normal completion containing a Boolean or a throw completion
632 |
633 |
637 |
638 | 1. Assert: ! TestIntegrityLevel(_O_, ~sealed~) is *true*.
639 | 1. Let _current_ be ! _O_.[[GetOwnProperty]](_P_).
640 | 1. If _current_ is *undefined*, return *false*.
641 | 1. Assert: IsDataDescriptor(_current_) is *true*.
642 | 1. Assert: _current_.[[Enumerable]] is *true*.
643 | 1. Assert: _current_.[[Configurable]] is *false*.
644 | 1. Assert: _current_.[[Writable]] is *true*.
645 | 1. If _Desc_ has a [[Enumerable]] field and _Desc_.[[Enumerable]] is *false*, return *false*.
646 | 1. If _Desc_ has a [[Configurable]] field and _Desc_.[[Configurable]] is *true*, return *false*.
647 | 1. If _Desc_ has a [[Writable]] field and _Desc_.[[Writable]] is *false*, return *false*.
648 | 1. If _Desc_ has a [[Value]] field, then
649 | 1. If CanBeSharedAcrossAgents(_Desc_.[[Value]]) is *false*, throw a *TypeError* exception.
650 | 1. Perform WriteSharedStructField(_O_, _P_, _Desc_.[[Value]], ~unordered~).
651 | 1. Return *true*.
652 |
653 |
654 |
655 |
656 |
657 | [[HasProperty]] (
658 | _P_: a property key,
659 | ): a normal completion containing a Boolean
660 |
661 |
665 |
666 | 1. If _O_ does not have an own property with key _P_, return *false*.
667 | 1. NOTE: [[GetOwnPropertyDescriptor]] is not used to avoid an unnecessary ReadSharedMemory event.
668 | 1. Return *true*.
669 |
670 |
671 |
672 |
673 |
674 | [[Get]] (
675 | _P_: a property key,
676 | _Receiver_: an ECMAScript language value,
677 | ): either a normal completion containing *undefined* or a throw completion
678 |
679 |
683 |
684 | 1. If _O_ does not have an own property with key _P_, return *undefined*.
685 | 1. Let _ownDesc_ be ! _O_.[[GetOwnProperty]](_P_).
686 | 1. Assert: _ownDesc_ is not *undefined*.
687 | 1. Return _ownDesc.[[Value]].
688 |
689 |
690 |
691 |
692 |
693 | [[Set]] (
694 | _P_: a property key,
695 | _V_: an ECMAScript language value,
696 | _Receiver_: an ECMAScript language value,
697 | ): either a normal completion containing a Boolean or a throw completion
698 |
699 |
703 |
704 | 1. If _O_ does not have an own property with key _P_, return *false*.
705 | 1. NOTE: [[GetOwnPropertyDescriptor]] is not used to avoid an unnecessary ReadSharedMemory event.
706 | 1. Let _desc_ be PropertyDescriptor { [[Value]]: _V_, [[Writable]]: *true*, [[Enumerable]]: *true*, [[Configurable]]: *false* }.
707 | 1. Return ? _O_.[[DefineOwnProperty]](_P_, _desc_).
708 |
709 |
710 |
711 |
712 |
713 | [[Delete]] (
714 | _P_: a property key,
715 | ): a normal completion containing *false*
716 |
717 |
721 |
722 | 1. Return *false*.
723 |
724 |
725 |
726 |
727 |
728 |
729 | Syntax
730 |
731 |
732 |
733 | StructDeclaration[Yield, Await, Default] :
734 | `shared` [no LineTerminator here] `struct` [no LineTerminator here] BindingIdentifier[?Yield, ?Await] SharedStructTail[?Yield, ?Await]
735 | [+Default] `shared` [no LineTerminator here] `struct` [no LineTerminator here] SharedStructTail[?Yield, ?Await]
736 |
737 | SharedStructTail[Yield, Await] :
738 | ClassHeritage[?Yield, ?Await]? `{` SharedStructBody[?Yield, ?Await]? `}`
739 |
740 | SharedStructBody[Yield, Await] :
741 | ClassElementList[?Yield, ?Await]
742 |
743 |
744 | A shared struct definition is always strict mode code.
745 |
746 |
747 |
748 | Static Semantics: ContainsInstancePrivateIdentifier ( ) : a Boolean
749 |
751 | ClassElementList : ClassElement
752 |
753 | 1. Return ContainsInstancePrivateIdentifier of |ClassElement|.
754 |
755 |
756 | ClassElementList : ClassElementList ClassElement
757 |
758 | 1. If ContainsInstancePrivateIdentifier of |ClassElementList|, return *true*.
759 | 1. Return ContainsInstancePrivateIdentifier of |ClassElement|.
760 |
761 |
762 | ClassElement : FieldDefinition `;`
763 |
764 | 1. Return |FieldDefinition| Contains |PrivateIdentifier|.
765 |
766 |
767 | ClassElement : MethodDefinition
768 |
769 | 1. Return |MethodDefinition| Contains |PrivateIdentifier|.
770 |
771 |
772 |
773 | ClassElement :
774 | `static` FieldDefinition `;`
775 | `static` MethodDefinition
776 | ClassStaticBlock
777 | `;`
778 |
779 |
780 | 1. Return *false*.
781 |
782 |
783 |
784 |
785 | Static Semantics: ContainsInstanceMethod ( ) : a Boolean
786 |
788 | ClassElementList : ClassElement
789 |
790 | 1. Return ContainsInstanceMethod of |ClassElement|.
791 |
792 |
793 | ClassElementList : ClassElementList ClassElement
794 |
795 | 1. If ContainsInstanceMethod of |ClassElementList|, return *true*.
796 | 1. Return ContainsInstanceMethod of |ClassElement|.
797 |
798 |
799 | ClassElement : MethodDefinition
800 |
801 | 1. If ClassElementKind of |ClassElement| is ~constructor-method~, return *false*.
802 | 1. Return *true*.
803 |
804 |
805 |
806 | ClassElement :
807 | FieldDefinition `;`
808 | `static` FieldDefinition `;`
809 | `static` MethodDefinition
810 | ClassStaticBlock
811 | `;`
812 |
813 |
814 | 1. Return *false*.
815 |
816 |
817 |
818 |
819 | Static Semantics: Early Errors
820 | SharedStructBody : ClassElementList
821 |
822 | -
823 | It is a SyntaxError if PrototypePropertyNameList of |ClassElementList| contains more than one occurrence of *"constructor"*.
824 |
825 | -
826 | It is a Syntax Error if ContainsInstancePrivateIdentifier of |ClassElementList| is *true*.
827 |
828 | -
829 | It is a SyntaxError if ContainsInstanceMethod of |ClassElementList| is *true*.
830 |
831 |
832 |
833 | EDITOR'S NOTE: Per-Realm prototypes, which is currently an open design question and not included in this draft, will allow methods.
834 |
835 |
836 |
837 |
838 |
839 | CanBeSharedAcrossAgents (
840 | _val_: an ECMAScript language value
841 | ): a Boolean
842 |
843 |
849 |
850 | 1. If _val_ is *undefined*, return *true*.
851 | 1. If _val_ is *null*, return *true*.
852 | 1. If _val_ is a String, return *true*.
853 | 1. If _val_ is a Boolean, return *true*.
854 | 1. If _val_ is a Number, return *true*.
855 | 1. If _val_ is a BigInt, return *true*.
856 | 1. If _val_ is a Symbol, return *true*.
857 | 1. Assert: _val_ is an Object.
858 | 1. If _val_ is a Shared Struct exotic object, return *true*.
859 | 1. Return *false*.
860 |
861 |
862 |
863 |
864 |
865 | DefineSharedStructField (
866 | _receiver_: a Shared Struct,
867 | _fieldRecord_: a ClassFieldDefinition Record,
868 | ): ~unused~
869 |
870 |
872 |
873 | 1. Assert: The surrounding agent is in the critical section for Shared Struct creation.
874 | 1. Let _fieldName_ be _fieldRecord_.[[Name]].
875 | 1. Assert: _fieldName_ is a property key.
876 | 1. Create an own data property named _fieldName_ of object _receiver_ whose [[Value]] is *undefined*, [[Writable]] is *true*, [[Enumerable]] is *true*, and [[Configurable]] is *false*.
877 | 1. Perform WriteSharedStructField(_receiver_, _fieldName_, *undefined*, ~init~).
878 | 1. Return ~unused~.
879 |
880 |
881 |
882 |
883 |
884 | Runtime Semantics: SharedStructDefinitionEvaluation (
885 | _structBinding_: a String or *undefined*,
886 | _structName_: a property key,
887 | ): either a normal completion containing a function object or an abrupt completion
888 |
889 |
891 | SharedStructTail : ClassHeritage? `{` SharedStructBody? `}`
892 |
893 | 1. Let _env_ be the LexicalEnvironment of the running execution context.
894 | 1. Let _structEnv_ be NewDeclarativeEnvironment(_env_).
895 | 1. If _structBinding_ is not *undefined*, then
896 | 1. Perform ! _structEnv_.CreateImmutableBinding(_structBinding_, *true*).
897 | 1. Let _outerPrivateEnvironment_ be the running execution context's PrivateEnvironment.
898 | 1. Let _classPrivateEnvironment_ be NewPrivateEnvironment(_outerPrivateEnvironment_).
899 | 1. If |SharedStructBody| is present, then
900 | 1. For each String _dn_ of the PrivateBoundIdentifiers of |SharedStructBody|, do
901 | 1. If _classPrivateEnvironment_.[[Names]] contains a Private Name _pn_ such that _pn_.[[Description]] is _dn_, then
902 | 1. Assert: This is only possible for getter/setter pairs.
903 | 1. Else,
904 | 1. Let _name_ be a new Private Name whose [[Description]] is _dn_.
905 | 1. Append _name_ to _classPrivateEnvironment_.[[Names]].
906 | 1. If |ClassHeritage| is not present, then
907 | 1. Let _constructorParent_ be %Function.prototype%.
908 | 1. Else,
909 | 1. Set the running execution context's LexicalEnvironment to _structEnv_.
910 | 1. Let _superclassRef_ be Completion(Evaluation of |ClassHeritage|).
911 | 1. Set the running execution context's LexicalEnvironment to _env_.
912 | 1. Let _superclass_ be ? GetValue(? _superclassRef_).
913 | 1. If _superclass_ is *null*, then
914 | 1. Let _constructorParent_ be %Function.prototype%.
915 | 1. Else if _superclass_ does not have a [[IsSharedStructConstructor]] internal slot, then
916 | 1. Throw a *TypeError* exception.
917 | 1. Else,
918 | 1. Let _constructorParent_ be _superclass_.
919 | 1. Let _proto_ be *null*.
920 | 1. NOTE: Per-Realm prototypes, which is currently an open design question and not included in this draft, will allow prototypes.
921 | 1. NOTE: Shared Structs have one-shot construction, with the user-defined "constructor" method performing post-construction initialization. By the time ECMAScript code has access to a Shared Struct instance, it already has all of its declared fields as own properties.
922 | 1. Set the running execution context's LexicalEnvironment to _structEnv_.
923 | 1. Set the running execution context's PrivateEnvironment to _classPrivateEnvironment_.
924 | 1. If |SharedStructBody| is not present, let _initializerParseNode_ be ~empty~.
925 | 1. Else, let _initializerParseNode_ be ConstructorMethod of |SharedStructBody|.
926 | 1. If _initializerParseNode_ is ~empty~, then
927 | 1. Let _initializer_ be ~empty~.
928 | 1. Else,
929 | 1. Let _initializerInfo_ be ? DefineMethod of _initializerParseNode_ with arguments _proto_ and _constructorParent_.
930 | 1. Let _initializer_ be _initializerInfo_.[[Closure]].
931 | 1. Perform SetFunctionName(_initializer_, _structName_).
932 | 1. Let _constructor_ be a new Abstract Closure with no parameters that captures _initializer_ and performs the following steps when called:
933 | 1. Let _args_ be the List of arguments that was passed to this function by [[Call]] or [[Construct]].
934 | 1. Let _F_ be the active function object.
935 | 1. If NewTarget is not _F_, throw a *TypeError* exception.
936 | 1. Let _createInitializer_ be a new Abstract Closure with parameters (_newSharedStruct_) that captures _F_ and performs the following steps when called:
937 | 1. Perform InitializeStructInstanceFieldsAndBrand(_newSharedStruct_, _F_).
938 | 1. Return ~unused~.
939 | 1. Let _result_ be SharedStructCreate(_createInitializer_).
940 | 1. Perform ? RunStructInstanceFieldInitializers(_result_, _F_).
941 | 1. If _initializer_ is not ~empty~, then
942 | 1. Perform ? Call(_initializer_, _result_).
943 | 1. Return _result_.
944 | 1. Let _F_ be CreateBuiltinFunction(_constructor_, 0, structName, « [[ConstructorKind]], [[SourceText]], [[StructInitializer]], [[IsSharedStructConstructor]] », the current Realm Record, _constructorParent_).
945 | 1. Perform MakeConstructor(_F_, *false*, _proto_).
946 | 1. If |ClassHeritage| is present, set _F_.[[ConstructorKind]] to ~derived~.
947 | 1. Set _F_.[[StructInitializer]] to _initializer_.
948 | 1. Set _F_.[[IsSharedStructConstructor]] to *true*.
949 | 1. If |StructBody| is not present, let _elements_ be a new empty List.
950 | 1. Else, let _elements_ be NonConstructorElements of |SharedStructBody|.
951 | 1. Let _staticPrivateMethods_ be a new empty List.
952 | 1. Let _instanceFields_ be a new empty List.
953 | 1. Let _staticElements_ be a new empty List.
954 | 1. For each |ClassElement| _e_ of _elements_, do
955 | 1. If IsStatic of _e_ is *false*, then
956 | 1. Let _element_ be Completion(ClassElementEvaluation of _e_ with argument _proto_).
957 | 1. Else,
958 | 1. Let _element_ be Completion(ClassElementEvaluation of _e_ with argument _F_).
959 | 1. If _element_ is an abrupt completion, then
960 | 1. Set the running execution context's LexicalEnvironment to _env_.
961 | 1. Set the running execution context's PrivateEnvironment to _outerPrivateEnvironment_.
962 | 1. Return ? _element_.
963 | 1. Set _element_ to ! _element_.
964 | 1. If _element_ is a PrivateElement, then
965 | 1. Assert: _element_.[[Kind]] is either ~method~ or ~accessor~.
966 | 1. Assert: IsStatic of _e_ is *true*.
967 | 1. Let _container_ be _staticPrivateMethods_.
968 | 1. If _container_ contains a PrivateElement _pe_ such that _pe_.[[Key]] is _element_.[[Key]], then
969 | 1. Assert: _element_.[[Kind]] and _pe_.[[Kind]] are both ~accessor~.
970 | 1. If _element_.[[Get]] is *undefined*, then
971 | 1. Let _combined_ be PrivateElement { [[Key]]: _element_.[[Key]], [[Kind]]: ~accessor~, [[Get]]: _pe_.[[Get]], [[Set]]: _element_.[[Set]] }.
972 | 1. Else,
973 | 1. Let _combined_ be PrivateElement { [[Key]]: _element_.[[Key]], [[Kind]]: ~accessor~, [[Get]]: _element_.[[Get]], [[Set]]: _pe_.[[Set]] }.
974 | 1. Replace _pe_ in _container_ with _combined_.
975 | 1. Else,
976 | 1. Append _element_ to _container_.
977 | 1. Else if _element_ is a ClassFieldDefinition Record, then
978 | 1. If IsStatic of _e_ is *false*, append _element_ to _instanceFields_.
979 | 1. Else, append _element_ to _staticElements_.
980 | 1. Else if _element_ is a ClassStaticBlockDefinition Record, then
981 | 1. Append _element_ to _staticElements_.
982 | 1. Set the running execution context's LexicalEnvironment to _env_.
983 | 1. If _structBinding_ is not *undefined*, then
984 | 1. Perform ! _structEnv_.InitializeBinding(_structBinding_, _F_).
985 | 1. Set _F_.[[Fields]] to _instanceFields_.
986 | 1. For each PrivateElement _method_ of _staticPrivateMethods_, do
987 | 1. Perform ! PrivateMethodOrAccessorAdd(_F_, _method_).
988 | 1. For each element _elementRecord_ of _staticElements_, do
989 | 1. If _elementRecord_ is a ClassFieldDefinition Record, then
990 | 1. Let _result_ be Completion(DefineField(_F_, _elementRecord_)).
991 | 1. Else,
992 | 1. Assert: _elementRecord_ is a ClassStaticBlockDefinition Record.
993 | 1. Let _result_ be Completion(Call(_elementRecord_.[[BodyFunction]], _F_)).
994 | 1. If _result_ is an abrupt completion, then
995 | 1. Set the running execution context's PrivateEnvironment to _outerPrivateEnvironment_.
996 | 1. Return ? _result_.
997 | 1. Set the running execution context's PrivateEnvironment to _outerPrivateEnvironment_.
998 | 1. Perform ! SetIntegrityLevel(_F_, ~sealed~).
999 | 1. Return _F_.
1000 |
1001 |
1002 |
1003 |
1004 | Runtime Semantics: BindingStructDeclarationEvaluation
1005 | StructDeclaration : `shared` `struct` BindingIdentifier SharedStructTail
1006 |
1007 | 1. Let _structName_ be the StringValue of |BindingIdentifier|.
1008 | 1. Let _value_ be ? SharedStructDefinitionEvaluation of |StructTail| with arguments _structName_ and _structName_.
1009 | 1. Set _value_.[[SourceText]] to the source text matched by |StructDeclaration|.
1010 | 1. Let _env_ be the running execution context's LexicalEnvironment.
1011 | 1. Perform ? InitializeBoundName(_structName_, _value_, _env_).
1012 | 1. Return _value_.
1013 |
1014 | StructDeclaration : `shared` `struct` SharedStructTail
1015 |
1016 | 1. Let _value_ be ? SharedStructDefinitionEvaluation of |SharedStructTail| with arguments *undefined* and *"default"*.
1017 | 1. Set _value_.[[SourceText]] to the source text matched by |StructDeclaration|.
1018 | 1. Return _value_.
1019 |
1020 |
1021 |
1022 |
1023 | Runtime Semantics: Evaluation
1024 | StructDeclaration : `shared` `struct` BindingIdentifier SharedStructTail
1025 |
1026 | 1. Perform ? BindingStructDeclarationEvaluation of this |StructDeclaration|.
1027 | 1. Return ~empty~.
1028 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 | Changes to the Atomics Object
1035 |
1036 |
1037 |
1038 |
1039 | AtomicCompareExchangeInSharedStruct (
1040 | _struct_: a Shared Struct,
1041 | _field_: a property key,
1042 | _expectedValue_: an ECMAScript language value,
1043 | _replacementValue_: an ECMAScript language value
1044 | ): an ECMAScript language value
1045 |
1046 |
1048 |
1049 | 1. Assert: CanBeSharedAcrossAgents(_replacementValue_) is *true*.
1050 | 1. Let _execution_ be the [[CandidateExecution]] field of the surrounding agent's Agent Record.
1051 | 1. Let _eventsRecord_ be the Agent Events Record of _execution_.[[EventsRecords]] whose [[AgentSignifier]] is AgentSignifier().
1052 | 1. Perform EnterSharedStructCreationCriticalSection().
1053 | 1. Let _rawLanguageValue_ be a nondeterministically chosen ECMAScript language value such that CanBeSharedAcrossAgents(_rawLanguageValue_) is *true*.
1054 | 1. Perform LeaveSharedStructCreationCriticalSection().
1055 | 1. NOTE: In implementations, _rawLanguageValue_ is the result of a non-atomic or atomic read instruction on the underlying hardware. The nondeterminism is a semantic prescription of the memory model to describe observable behaviour of hardware with weak consistency.
1056 | 1. NOTE: The comparison of the expected value and the read value is performed outside of the read-modify-write modification function to avoid needlessly strong synchronization when the expected value is not equal to the read value.
1057 | 1. Let _storage_ be SharedStructStorage { [[Struct]]: _struct_, [[Field]]: _field_ }.
1058 | 1. If SameValue(_expectedValue_, _replacementValue_) is *true*, then
1059 | 1. Let _second_ be a new read-modify-write modification function with parameters (_oldValue_, _newValue_) that captures nothing and performs the following steps atomically when called:
1060 | 1. Return _newValue_.
1061 | 1. Let _event_ be ReadModifyWriteSharedMemory { [[Order]]: ~seq-cst~, [[NoTear]]: *true*, [[Storage]]: _storage_, [[Payload]]: _replacementValue_, [[ModifyOp]]: _second_ }.
1062 | 1. Else,
1063 | 1. Let _event_ be ReadSharedMemory { [[Order]]: ~seq-cst~, [[NoTear]]: *true*, [[Storage]]: _storage_ }.
1064 | 1. Append _event_ to _eventsRecord_.[[EventList]].
1065 | 1. Append Chosen Value Record { [[Event]]: _event_, [[ChosenValue]]: _rawLanguageValue_ } to _execution_.[[ChosenValues]].
1066 | 1. Return _rawLanguageValue_.
1067 |
1068 |
1069 |
1070 |
1071 |
1072 | AtomicReadModifyWriteInSharedStruct (
1073 | _struct_: a Shared Struct
1074 | _field_: an ECMAScript language value,
1075 | _value_: an ECMAScript language value,
1076 | _op_: a read-modify-write modification function,
1077 | ): either a normal completion containing an ECMAScript language value, or a throw completion
1078 |
1079 |
1081 |
1082 | 1. If _field_ is not a property key, throw a *TypeError* exception.
1083 | 1. If CanBeSharedAcrossAgents(_value_) is *false*, throw a *TypeError* exception.
1084 | 1. If _struct_ does not have an own property with key _field_, throw a *RangeError* exception.
1085 | 1. Let _execution_ be the [[CandidateExecution]] field of the surrounding agent's Agent Record.
1086 | 1. Let _eventsRecord_ be the Agent Events Record of _execution_.[[EventsRecords]] whose [[AgentSignifier]] is AgentSignifier().
1087 | 1. Perform EnterSharedStructCreationCriticalSection().
1088 | 1. Let _rawLanguageValue_ be a nondeterministically chosen ECMAScript language value such that CanBeSharedAcrossAgents(_rawLanguageValue_) is *true*.
1089 | 1. Perform LeaveSharedStructCreationCriticalSection().
1090 | 1. NOTE: In implementations, _rawLanguageValue_ is the result of a non-atomic or atomic read instruction on the underlying hardware. The nondeterminism is a semantic prescription of the memory model to describe observable behaviour of hardware with weak consistency.
1091 | 1. Let _storage_ be SharedStructStorage { [[Struct]]: _struct_, [[Field]]: _field_ }.
1092 | 1. Let _rmwEvent_ be ReadModifyWriteSharedMemory { [[Order]]: ~seq-cst~, [[NoTear]]: *true*, [[Storage]]: _storage_, [[Payload]]: _rawLanguageValue_, [[ModifyOp]]: _op_ }.
1093 | 1. Append _rmwEvent_ to _eventsRecord_.[[EventList]].
1094 | 1. Append Chosen Value Record { [[Event]]: _rmwEvent_, [[ChosenValue]]: _rawLanguageValue_ } to _execution_.[[ChosenValues]].
1095 | 1. Return _rawLanguageValue_.
1096 |
1097 |
1098 |
1099 |
1100 |
1101 | Atomics.compareExchange ( _typedArray__typedArrayOrStruct_, _index__indexOrField_, _expectedValue_, _replacementValue_ )
1102 | This function performs the following steps when called:
1103 |
1104 | 1. If _typedArrayOrStruct_ is a Shared Struct, then
1105 | 1. If _indexOrField_ is not a property key, throw a *TypeError* exception.
1106 | 1. If CanBeSharedAcrossAgents(_replacementValue_) is *false*, throw a *TypeError* exception.
1107 | 1. If _typedArrayOrStruct_ does not have an own property with key _indexOrField_, throw a *RangeError* exception.
1108 | 1. Return AtomicCompareExchangeInSharedStruct(_typedArrayOrStruct_, _indexOrField_, _expectedValue_, _replacementValue_).
1109 | 1. Let _typedArray_ be _typedArrayOrStruct_.
1110 | 1. Let _index_ be _indexOrField_.
1111 | 1. Let _byteIndexInBuffer_ be ? ValidateAtomicAccessOnIntegerTypedArray(_typedArray_, _index_).
1112 | 1. Let _buffer_ be _typedArray_.[[ViewedArrayBuffer]].
1113 | 1. Let _block_ be _buffer_.[[ArrayBufferData]].
1114 | 1. If _typedArray_.[[ContentType]] is ~bigint~, then
1115 | 1. Let _expected_ be ? ToBigInt(_expectedValue_).
1116 | 1. Let _replacement_ be ? ToBigInt(_replacementValue_).
1117 | 1. Else,
1118 | 1. Let _expected_ be 𝔽(? ToIntegerOrInfinity(_expectedValue_)).
1119 | 1. Let _replacement_ be 𝔽(? ToIntegerOrInfinity(_replacementValue_)).
1120 | 1. Perform ? RevalidateAtomicAccess(_typedArray_, _byteIndexInBuffer_).
1121 | 1. Let _elementType_ be TypedArrayElementType(_typedArray_).
1122 | 1. Let _elementSize_ be TypedArrayElementSize(_typedArray_).
1123 | 1. Let _isLittleEndian_ be the value of the [[LittleEndian]] field of the surrounding agent's Agent Record.
1124 | 1. Let _expectedBytes_ be NumericToRawBytes(_elementType_, _expected_, _isLittleEndian_).
1125 | 1. Let _replacementBytes_ be NumericToRawBytes(_elementType_, _replacement_, _isLittleEndian_).
1126 | 1. If IsSharedArrayBuffer(_buffer_) is *true*, then
1127 | 1. Let _rawBytesRead_ be AtomicCompareExchangeInSharedBlock(_block_, _byteIndexInBuffer_, _elementSize_, _expectedBytes_, _replacementBytes_).
1128 | 1. Else,
1129 | 1. Let _rawBytesRead_ be a List of length _elementSize_ whose elements are the sequence of _elementSize_ bytes starting with _block_[_byteIndexInBuffer_].
1130 | 1. If ByteListEqual(_rawBytesRead_, _expectedBytes_) is *true*, then
1131 | 1. Store the individual bytes of _replacementBytes_ into _block_, starting at _block_[_byteIndexInBuffer_].
1132 | 1. Return RawBytesToNumeric(_elementType_, _rawBytesRead_, _isLittleEndian_).
1133 |
1134 |
1135 |
1136 |
1137 | Atomics.exchange ( _typedArray__typedArrayOrStruct_, _index__indexOrField_, _value_ )
1138 | This function performs the following steps when called:
1139 |
1140 | 1. Let _second_ be a new read-modify-write modification function with parameters (_oldBytes__oldValue_, _newBytes__newValue_) that captures nothing and performs the following steps atomically when called:
1141 | 1. Return _newBytes__newValue_.
1142 | 1. If _typedArrayOrStruct is a Shared Struct, then
1143 | 1. Return ? AtomicReadModifyWriteInSharedStruct(_typedArrayOrStruct_, _indexOrField_, _value_, _second_).
1144 | 1. Let _typedArray_ be _typedArrayOrStruct_.
1145 | 1. Let _index_ be _indexOrField_.
1146 | 1. Return ? AtomicReadModifyWrite(_typedArray_, _index_, _value_, _second_).
1147 |
1148 |
1149 |
1150 |
1151 | Atomics.load ( _typedArray__typedArrayOrStruct_, _index__indexOrField_ )
1152 | This function performs the following steps when called:
1153 |
1154 | 1. If _typedArrayOrStruct_ is a Shared Struct, then
1155 | 1. If _indexOrField_ is not a property key, throw a *TypeError* exception.
1156 | 1. If _typedArrayOrStruct_ does not have an own property with key _indexOrField_, throw a *RangeError* exception.
1157 | 1. Return ReadSharedStructField(_typedArrayOrStruct_, _indexOrField_, ~seq-cst~).
1158 | 1. Let _byteIndexInBuffer_ be ? ValidateAtomicAccessOnIntegerTypedArray(_typedArray_, _index_).
1159 | 1. Perform ? RevalidateAtomicAccess(_typedArray_, _byteIndexInBuffer_).
1160 | 1. Let _buffer_ be _typedArray_.[[ViewedArrayBuffer]].
1161 | 1. Let _elementType_ be TypedArrayElementType(_typedArray_).
1162 | 1. Return GetValueFromBuffer(_buffer_, _byteIndexInBuffer_, _elementType_, *true*, ~seq-cst~).
1163 |
1164 |
1165 |
1166 |
1167 | Atomics.store ( _typedArray__typedArrayOrStruct_, _index__indexOrField_, _value_ )
1168 | This function performs the following steps when called:
1169 |
1170 | 1. If _typedArrayOrStruct_ is a Shared Struct, then
1171 | 1. If _indexOrField_ is not a property key, throw a *TypeError* exception.
1172 | 1. If CanBeSharedAcrossAgents(_value_) is *false*, throw a *TypeError* exception.
1173 | 1. If _typedArrayOrStruct_ does not have an own property with key _indexOrField_, throw a *RangeError* exception.
1174 | 1. Perform WriteSharedStructField(_typedArrayOrStruct_, _indexOrField_, _value_, ~seq-cst~).
1175 | 1. Return _value_.
1176 | 1. Let _byteIndexInBuffer_ be ? ValidateAtomicAccessOnIntegerTypedArray(_typedArray_, _index_).
1177 | 1. If _typedArray_.[[ContentType]] is ~bigint~, let _v_ be ? ToBigInt(_value_).
1178 | 1. Otherwise, let _v_ be 𝔽(? ToIntegerOrInfinity(_value_)).
1179 | 1. Perform ? RevalidateAtomicAccess(_typedArray_, _byteIndexInBuffer_).
1180 | 1. Let _buffer_ be _typedArray_.[[ViewedArrayBuffer]].
1181 | 1. Let _elementType_ be TypedArrayElementType(_typedArray_).
1182 | 1. Perform SetValueInBuffer(_buffer_, _byteIndexInBuffer_, _elementType_, _v_, *true*, ~seq-cst~).
1183 | 1. Return _v_.
1184 |
1185 |
1186 |
1187 |
1188 |
1189 | Changes to the Reflect Object
1190 |
1191 |
1192 | Reflect.canBeShared ( _val_ )
1193 |
1194 |
1195 | 1. Return CanBeSharedAcrossAgents(_val_).
1196 |
1197 |
1198 |
1199 |
1200 |
1201 |
1202 | Changes to the Memory Model
1203 |
1204 | Memory Model Fundamentals
1205 | Shared memory accesses (reads and writes) are divided into two groups, atomic accesses and data accesses, defined below. Atomic accesses are sequentially consistent, i.e., there is a strict total ordering of events agreed upon by all agents in an agent cluster. Non-atomic accesses do not have a strict total ordering agreed upon by all agents, i.e., unordered.
1206 |
1207 | No orderings weaker than sequentially consistent and stronger than unordered, such as release-acquire, are supported.
1208 |
1209 |
1210 | A Shared Memory Storage Record is either a SharedBlockStorage or SharedStructStorage Record.
1211 |
1212 |
1213 |
1214 |
1215 |
1216 | Field Name |
1217 | Value |
1218 | Meaning |
1219 |
1220 |
1221 | [[Block]] |
1222 | a Shared Data Block |
1223 | The block the event operates on. |
1224 |
1225 |
1226 | [[ByteIndex]] |
1227 | a non-negative integer |
1228 | The byte address of the access in [[Block]]. |
1229 |
1230 |
1231 | [[ElementSize]] |
1232 | a non-negative integer |
1233 | The size of the access. |
1234 |
1235 |
1236 |
1237 |
1238 |
1239 |
1240 |
1241 |
1242 |
1243 | Field Name |
1244 | Value |
1245 | Meaning |
1246 |
1247 |
1248 | [[Struct]] |
1249 | a Shared Struct |
1250 | The shared struct the event operates on. |
1251 |
1252 |
1253 | [[Field]] |
1254 | a property key |
1255 | The field that is accessed in [[Struct]]. |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 |
1262 | A Shared Data Block event is either a ReadSharedMemory, WriteSharedMemory, or ReadModifyWriteSharedMemory Record.
1263 |
1264 |
1265 |
1266 |
1267 |
1268 | Field Name |
1269 | Value |
1270 | Meaning |
1271 |
1272 |
1273 |
1274 | [[Order]] |
1275 | ~seq-cst~ or ~unordered~ |
1276 | The weakest ordering guaranteed by the memory model for the event. |
1277 |
1278 |
1279 | [[NoTear]] |
1280 | a Boolean |
1281 | Whether this event is allowed to read from multiple write events with equal range as this event. |
1282 |
1283 |
1284 | [[Block]] |
1285 | a Shared Data Block |
1286 | The block the event operates on. |
1287 |
1288 |
1289 | [[ByteIndex]] |
1290 | a non-negative integer |
1291 | The byte address of the read in [[Block]]. |
1292 |
1293 |
1294 | [[ElementSize]] |
1295 | a non-negative integer |
1296 | The size of the read. |
1297 |
1298 |
1299 | [[Storage]] |
1300 | a Shared Memory Storage Record |
1301 | The storage of memory that is read. |
1302 |
1303 |
1304 |
1305 |
1306 |
1307 |
1308 |
1309 |
1310 | Field Name |
1311 | Value |
1312 | Meaning |
1313 |
1314 |
1315 |
1316 | [[Order]] |
1317 | ~seq-cst~, ~unordered~, or ~init~ |
1318 | The weakest ordering guaranteed by the memory model for the event. |
1319 |
1320 |
1321 | [[NoTear]] |
1322 | a Boolean |
1323 | Whether this event is allowed to be read from multiple read events with equal range as this event. |
1324 |
1325 |
1326 | [[Block]] |
1327 | a Shared Data Block |
1328 | The block the event operates on. |
1329 |
1330 |
1331 | [[ByteIndex]] |
1332 | a non-negative integer |
1333 | The byte address of the write in [[Block]]. |
1334 |
1335 |
1336 | [[ElementSize]] |
1337 | a non-negative integer |
1338 | The size of the write. |
1339 |
1340 |
1341 | [[Storage]] |
1342 | a Shared Memory Storage Record |
1343 | The storage of memory that is written. |
1344 |
1345 |
1346 | [[Payload]] |
1347 | a List of byte values |
1348 | The List of byte values to be read by other events. |
1349 |
1350 |
1351 |
1352 |
1353 |
1354 |
1355 |
1356 |
1357 | Field Name |
1358 | Value |
1359 | Meaning |
1360 |
1361 |
1362 |
1363 | [[Order]] |
1364 | ~seq-cst~ |
1365 | Read-modify-write events are always sequentially consistent. |
1366 |
1367 |
1368 | [[NoTear]] |
1369 | *true* |
1370 | Read-modify-write events cannot tear. |
1371 |
1372 |
1373 | [[Block]] |
1374 | a Shared Data Block |
1375 | The block the event operates on. |
1376 |
1377 |
1378 | [[ByteIndex]] |
1379 | a non-negative integer |
1380 | The byte address of the read-modify-write in [[Block]]. |
1381 |
1382 |
1383 | [[ElementSize]] |
1384 | a non-negative integer |
1385 | The size of the read-modify-write. |
1386 |
1387 |
1388 | [[Storage]] |
1389 | a Shared Memory Storage Record |
1390 | The storage of memory of the read-modify-write. |
1391 |
1392 |
1393 | [[Payload]] |
1394 | a List of byte values |
1395 | The List of byte values to be passed to [[ModifyOp]]. |
1396 |
1397 |
1398 | [[ModifyOp]] |
1399 | a read-modify-write modification function |
1400 | An abstract closure that returns a modified List of byte values from a read List of byte values and [[Payload]]. |
1401 |
1402 |
1403 |
1404 |
1405 | These events are introduced by abstract operations or by methods on the Atomics object.
1406 | Some operations may also introduce Synchronize events. A Synchronize event has no fields, and exists purely to directly constrain the permitted orderings of other events.
1407 | In addition to Shared Data Block and Synchronize events, there are host-specific events.
1408 | If the [[Storage]] field of a ReadSharedMemory, WriteSharedMemory, or ReadModifyWriteSharedMemory event is a SharedBlockStorage, then Llet theits range of a ReadSharedMemory, WriteSharedMemory, or ReadModifyWriteSharedMemory event be the Set of contiguous integers from its [[Storage]].[[ByteIndex]] to [[Storage]].[[ByteIndex]] + [[Storage]].[[ElementSize]] - 1. Two events' ranges are equal when the events have a SharedBlockStorage in their [[Storage]] field, have the same [[Storage]].[[Block]], and the ranges are element-wise equal. Two events' ranges are overlapping when the events have the same [[Storage]].[[Block]], the ranges are not equal and their intersection is non-empty. Two events' ranges are disjoint when the events do not both have a SharedBlockStorage in their [[Storage]] field, do not have the same [[Storage]].[[Block]], or their ranges are neither equal nor overlapping.
1409 | If the [[Storage]] field of a ReadSharedMemory, WriteSharedMemory, or ReadModifyWriteSharedMemory event is a SharedStructStorage, then let its range be the value of the [[Storage]] field. Two events' ranges are equal when the events have a SharedStructStorage in their [[Storage]] field, have the same [[Storage]].[[Struct]] and the same [[Storage]].[[Field]]. Two events' ranges that both have a SharedStructStorage in their [[Storage]] field are never overlapping. Two events' ranges are disjoint when the events do not both have a SharedStructStorage in their [[Storage]] Field, or do not have the same [[Storage]].[[Struct]] or the same [[Storage]].[[Field]].
1410 |
1411 |
1412 | For brevity, the refactoring of the memory model relations to use SharedStructStorage and the modified definition of event ranges is omitted.
1413 |
1414 |
1415 |
1416 | Shared Array Object
1417 | Shared Arrays are a special case of Shared Structs with array indexed properties and an immutable *"length"* own property. Since they are Shared Structs, their layout, i.e. their length, is fixed at creation time.
1418 |
1419 |
1420 |
1421 | The SharedArray Constructor
1422 | The SharedArray constructor:
1423 |
1424 | - is %SharedArray%.
1425 | - is the initial value of the *"SharedArray"* property of the global object.
1426 | - creates and initializes a new Shared Array when called as a constructor.
1427 | - is not intended to be called as a function and will throw an exception when called in that manner.
1428 | - is a function whose behaviour differs based upon the number and types of its arguments.
1429 |
1430 |
1431 |
1432 |
1433 | SharedArrayCreate (
1434 | _length_: an non-negative integer,
1435 | ): a Shared Array
1436 |
1437 |
1441 |
1442 | 1. Assert: _length_ ≤ 232 - 1.
1443 | 1. Let _createInitializer_ be a new Abstract Closure with parameters (_newSharedArray_) that captures _length_ and performs the following steps when called:
1444 | 1. Let _k_ be 0.
1445 | 1. Repeat, while _k_ < _length_,
1446 | 1. Let _Pk_ be ! ToString(𝔽(_k_)).
1447 | 1. Create an own data property named _Pk_ of object _newSharedArray_ whose [[Value]] is *undefined*, [[Writable]] is *true*, [[Enumerable]] is *true*, and [[Configurable]] is *false*.
1448 | 1. Perform WriteSharedStructField(_newSharedArray_, _Pk_, *undefined*, ~init~).
1449 | 1. Create an own data property named *"length"* of object _newSharedArray_ whose [[Value]] is 𝔽(_length_), [[Writable]] is *false*, [[Enumerable]] is *false*, and [[Configurable]] is *false*.
1450 | 1. Perform WriteSharedStructField(_newSharedArray_, *"length"*, 𝔽(_length_), ~init~).
1451 | 1. Return ~unused~.
1452 | 1. Return SharedStructCreate(_createInitializer_).
1453 |
1454 |
1455 |
1456 |
1457 | SharedArray ( ..._values_ )
1458 | This function performs the following steps when called:
1459 |
1460 | 1. If NewTarget is *undefined*, throw a *TypeError* exception.
1461 | 1. EDITOR'S NOTE: Per-Realm prototypes, which is currently an open design question and not included in this draft, will give Shared Arrays a per-Realm prototype with built-in methods.
1462 | 1. Let _numberOfArgs_ be the number of elements in _values_.
1463 | 1. If _numberOfArgs_ = 0, then
1464 | 1. Return SharedArrayCreate(0).
1465 | 1. Else if _numberOfArgs_ = 1, then
1466 | 1. Let _len_ be _values_[0].
1467 | 1. If _len_ is not an integral Number, throw a *TypeError* exception.
1468 | 1. If _len_ < 0, throw a *RangeError* exception.
1469 | 1. Let _lenReal_ be ℝ(_len_).
1470 | 1. If _lenReal_ > 232 - 1, throw a *RangeError* exception.
1471 | 1. Return SharedArrayCreate(_lenReal_).
1472 | 1. Else,
1473 | 1. Assert: _numberOfArgs_ ≥ 2.
1474 | 1. Let _array_ be SharedArrayCreate(_numberOfArgs_).
1475 | 1. Let _k_ be 0.
1476 | 1. Repeat, while _k_ < _numberOfArgs_,
1477 | 1. Let _Pk_ be ! ToString(𝔽(_k_)).
1478 | 1. Let _itemK_ be _values_[_k_].
1479 | 1. Perform ! Set(_array_, _Pk_, _itemK_, *true*).
1480 | 1. Set _k_ to _k_ + 1.
1481 | 1. Return _array_.
1482 |
1483 |
1484 |
1485 |
1486 |
1487 |
1488 |
1489 | Synchronization Primitives
1490 | Mutexes and condition variables are provided as higher level abstractions, as an easier to use alternative to user-built abstractions on top of Atomics.wait and Atomics.notify. They are Shared Structs with no fields.
1491 |
1492 |
1493 |
1494 | Abstract Operations for Mutex Objects
1495 |
1496 |
1497 |
1498 | UnlockTokenCreateIfNeeded (
1499 | _token_: an Object or *undefined*,
1500 | _mutex_: an Object,
1501 | ) : an Object
1502 |
1503 |
1505 |
1506 | 1. Assert: _mutex_ has a [[MutexWaiterList]] internal slot.
1507 | 1. If _token_ is *undefined*, then
1508 | 1. Set _token_ to OrdinaryObjectCreate(%Atomics.Mutex.UnlockToken.prototype%, « [[LockedMutex]] »).
1509 | 1. Else,
1510 | 1. Assert: _token_ has a [[LockedMutex]] internal slot.
1511 | 1. Assert: _token_.[[LockedMutex]] is ~empty~.
1512 | 1. Set _token_.[[LockedMutex]] to _mutex_.
1513 | 1. Return _token_.
1514 |
1515 |
1516 |
1517 |
1518 |
1519 | LockMutex (
1520 | _mutex_: an Object,
1521 | _tMillis_: a mathematical value,
1522 | ) : ~acquired~, ~deadlock~, or ~timed-out~
1523 |
1524 |
1526 |
1527 | 1. Assert: _mutex_ has a [[MutexWaiterList]] internal slot.
1528 | 1. Assert: If _tMillis_ is not 0, AgentCanSuspend() is *true*.
1529 | 1. Let _thisAgent_ be AgentSignifier().
1530 | 1. Let _WL_ be _mutex_.[[MutexWaiterList]].
1531 | 1. Perform EnterCriticalSection(_WL_).
1532 | 1. If _mutex_.[[IsLockedBy]] is ~empty~, then
1533 | 1. Set _mutex_.[[IsLockedBy]] to _thisAgent_.
1534 | 1. Let _result_ be ~acquired~.
1535 | 1. Else if _mutex_.[[IsLockedBy]] is _thisAgent_, then
1536 | 1. Let _result_ be ~deadlock~.
1537 | 1. Else,
1538 | 1. If _tMillis_ is 0, return ~timed-out~.
1539 | 1. Let _now_ be the time value (UTC) identifying the current time.
1540 | 1. Let _additionalTimeout_ be an implementation-defined non-negative mathematical value.
1541 | 1. Let _timeoutTime_ be ℝ(_now_) + _tMillis_ + _additionalTimeout_.
1542 | 1. NOTE: When _tMillis_ is +∞, _timeoutTime_ is also +∞.
1543 | 1. Let _done_ be *false*.
1544 | 1. Repeat, while _done_ is *false*,
1545 | 1. Let _waiterRecord_ be a new Waiter Record { [[AgentSignifier]]: _thisAgent_, [[PromiseCapability]]: ~blocking~, [[TimeoutTime]]: _timeoutTime_, [[Result]]: *"ok"* }.
1546 | 1. Perform AddWaiter(_WL_, _waiterRecord_).
1547 | 1. Perform SuspendThisAgent(_WL_, _waiterRecord_).
1548 | 1. If _mutex_.[[IsLockedBy]] is ~empty~, then
1549 | 1. Set _mutex_.[[IsLockedBy]] to _thisAgent_.
1550 | 1. Set _waiterRecord_.[[Result]] to *"ok"*.
1551 | 1. Set _done_ to *true*.
1552 | 1. Else if _waiterRecord_.[[Result]] is *"timed-out"*, then
1553 | 1. Set _done_ to *true*.
1554 | 1. If _waiterRecord_.[[Result]] is *"ok"*, then
1555 | 1. Let _result_ be ~acquired~.
1556 | 1. Else,
1557 | 1. Assert: _waiterRecord_.[[Result]] is *"timed-out"*.
1558 | 1. Let _result_ be ~timed-out~.
1559 | 1. Perform LeaveCriticalSection(_WL_).
1560 | 1. Return _result_.
1561 |
1562 |
1563 |
1564 |
1565 |
1566 | UnlockMutex (
1567 | _mutex_: an Object,
1568 | ) : ~unused~
1569 |
1570 |
1572 |
1573 | 1. Assert: _mutex_ has a [[MutexWaiterList]] internal slot.
1574 | 1. Let _WL_ be _mutex_.[[MutexWaiterList]].
1575 | 1. Perform EnterCriticalSection(_WL_).
1576 | 1. Assert: _mutex_.[[IsLockedBy]] is AgentSignifier().
1577 | 1. Set _mutex_.[[IsLockedBy]] to ~empty~.
1578 | 1. Let _S_ be RemoveWaiters(_WL_, 1).
1579 | 1. For each element _W_ of _S_, do
1580 | 1. Perform NotifyWaiter(_WL_, _W_).
1581 | 1. Perform LeaveCriticalSection(_WL_).
1582 | 1. Return ~unused~.
1583 |
1584 |
1585 |
1586 |
1587 |
1588 | The Mutex Constructor
1589 | The Mutex constructor:
1590 |
1591 | - is %Atomics.Mutex%.
1592 | - is the initial value of the *"Mutex"* property of the %Atomics% object.
1593 | - creates and initializes a new Mutex when called as constructor.
1594 | - is not intended to be called as a function and will throw an exception when called in that manner.
1595 |
1596 |
1597 |
1598 | Atomics.Mutex ( )
1599 | This function performs the following steps when called:
1600 |
1601 | 1. If NewTarget is *undefined*, throw a *TypeError* exception.
1602 | 1. Let _createInitializer_ be a new Abstract Closure with parameters (_newMutex_) that captures nothing and performs the following steps when called:
1603 | 1. Set _newMutex_.[[MutexWaiterList]] to a new WaiterList Record.
1604 | 1. Set _newMutex_.[[IsLockedBy]] to ~empty~.
1605 | 1. Return ~unused~.
1606 | 1. Return SharedStructCreate(_createInitializer_, « [[MutexWaiterList]], [[IsLockedBy]] »).
1607 |
1608 |
1609 |
1610 |
1611 |
1612 | Properties of the Mutex Constructor
1613 | The Mutex constructor:
1614 |
1615 | - has a [[Prototype]] internal slot whose value is %Function.prototype%.
1616 | - has the following properties:
1617 |
1618 |
1619 |
1620 | Per-Realm prototypes, which is currently an open design question and not included in this draft, will give Mutexes a per-Realm prototype with built-in methods instead of static methods.
1621 |
1622 |
1623 |
1624 | Atomics.Mutex.UnlockToken ( )
1625 | See .
1626 |
1627 |
1628 |
1629 | Atomics.Mutex.lock ( _mutex_ [ , _unlockToken_ ] )
1630 | This function puts the surrounding agent in a wait queue and suspends it until the mutex is unlocked.
1631 | It performs the following steps when called:
1632 |
1633 | 1. Perform ? RequireInternalSlot(_mutex_, [[MutexWaiterList]]).
1634 | 1. If _unlockToken_ not *undefined*, then
1635 | 1. Perform ? RequireInternalSlot(_unlockToken_, [[LockedMutex]]).
1636 | 1. If _unlockToken_.[[LockedMutex]] is not ~empty~, throw a *TypeError* exception.
1637 | 1. If AgentCanSuspend() is *false*, throw a *TypeError* exception.
1638 | 1. Let _result_ be LockMutex(_mutex_, +∞).
1639 | 1. If _result_ is ~deadlock~, throw a *TypeError* exception.
1640 | 1. Assert: _result_ is ~acquired~.
1641 | 1. Return UnlockTokenCreateIfNeeded(_unlockToken_, _mutex_).
1642 |
1643 |
1644 |
1645 |
1646 | Atomics.Mutex.lockIfAvailable ( _mutex_, _timeout_ [ , _unlockToken_ ] )
1647 | This function puts the surrounding agent in a wait queue and suspends it until the mutex is unlocked, or until the wait times out. If _timeout_ is 0, this function can be called in agents that cannot suspend.
1648 | It performs the following steps when called:
1649 |
1650 | 1. Perform ? RequireInternalSlot(_mutex_, [[MutexWaiterList]]).
1651 | 1. If _unlockToken_ not *undefined*, then
1652 | 1. Perform ? RequireInternalSlot(_unlockToken_, [[LockedMutex]]).
1653 | 1. If _unlockToken_.[[LockedMutex]] is not ~empty~, throw a *TypeError* exception.
1654 | 1. If _timeout_ is not a Number, throw a *TypeError* exception.
1655 | 1. If _timeout_ is either *NaN* or *+∞*𝔽, let _tMillis_ be +∞; else if _timeout_ is *-∞*𝔽, let _tMillis_ be 0; else let _tMillis_ be max(ℝ(_timeout_), 0).
1656 | 1. If _tMillis_ is not 0 and AgentCanSuspend() is *false*, throw a *TypeError* exception.
1657 | 1. Let _result_ be LockMutex(_mutex_, _tMillis_).
1658 | 1. If _result_ is ~deadlock~, then
1659 | 1. Throw *TypeError* exception.
1660 | 1. Else if _result_ is ~acquired~, then
1661 | 1. Return UnlockTokenCreateIfNeeded(_unlockToken_, _mutex_).
1662 | 1. Else,
1663 | 1. Assert: _result_ is ~timed-out~.
1664 | 1. Return *null*.
1665 | 1. NOTE: The return value of the timed-out case is an open design question. Specifically, whether the return value ought to throw when attempted to be used with the `using` syntax.
1666 |
1667 |
1668 |
1669 |
1670 |
1671 | UnlockToken Objects
1672 | An UnlockToken is the unlock capability returned when a Mutex's lock is acquired. It can be reused. An uninitialized UnlockToken can be created by using the Atomics.Mutex.UnlockToken constructor.
1673 |
1674 |
1675 | The UnlockToken Constructor
1676 | The UnlockToken constructor:
1677 |
1678 | - is %Atomics.Mutex.UnlockToken%.
1679 | - is the initial value of the *"UnlockToken"* property of the %Atomics.Mutex% object.
1680 | - creates and initializes a new UnlockToken when called as a constructor.
1681 | - is not intended to be called as a function and will throw an exception when called in that manner.
1682 |
1683 |
1684 |
1685 | Atomics.Mutex.UnlockToken ( )
1686 | This function performs the following steps when called:
1687 |
1688 | 1. If NewTarget is *undefined*, throw a *TypeError* exception.
1689 | 1. Let _token_ be OrdinaryObjectCreate(%Atomics.Mutex.UnlockToken.prototype%, « [[LockedMutex]] »).
1690 | 1. Set _token_.[[LockedMutex]] to ~empty~.
1691 | 1. Return _token_.
1692 |
1693 |
1694 |
1695 |
1696 |
1697 | Properties of the UnlockToken Constructor
1698 | The UnlockToken constructor:
1699 |
1700 | - has a [[Prototype]] internal slot whose value is %Function.prototype%.
1701 | - has the following properties:
1702 |
1703 |
1704 |
1705 | Atomics.Mutex.UnlockToken.prototype
1706 | The initial value of `Atomics.Mutex.UnlockToken.prototype` is the UnlockToken prototype object.
1707 | This property has the attributes { [[Writable]]: *false*, [[Enumerable]]: *false*, [[Configurable]]: *false* }.
1708 |
1709 |
1710 |
1711 |
1712 | Properties of the UnlockToken Prototype Object
1713 | The UnlockToken prototype object:
1714 |
1715 | - is %Atomics.Mutex.UnlockToken.prototype%.
1716 | - has a [[Prototype]] internal slot whose value is %Object.prototype%.
1717 | - is an ordinary object.
1718 | - does not have a [[LockedMutex]] internal slot.
1719 | - has the following properties:
1720 |
1721 |
1722 |
1723 | get Atomics.Mutex.UnlockToken.prototype.locked
1724 | `Atomics.Mutex.UnlockToken.locked` is an accessor property whose set accessor is *undefined*. Its get accessor function performs the following steps when called:
1725 |
1726 | 1. Let _token_ be the *this* value.
1727 | 1. Perform ? RequireInternalSlot(_token_, [[LockedMutex]]).
1728 | 1. If _token_.[[LockedMutex]] is ~empty~, return *false*; else return *true*.
1729 |
1730 |
1731 |
1732 |
1733 | Atomics.Mutex.UnlockToken.prototype.unlock ( )
1734 | This function performs the following steps when called:
1735 |
1736 | 1. Let _token_ be the *this* value.
1737 | 1. Perform ? RequireInternalSlot(_token_, [[LockedMutex]]).
1738 | 1. Let _mutex_ be _token_.[[LockedMutex]].
1739 | 1. If _mutex_ is not ~empty~, then
1740 | 1. Set _token_.[[LockedMutex]] to ~empty~.
1741 | 1. Perform UnlockMutex(_mutex_).
1742 | 1. Return *true*.
1743 | 1. Return *false*.
1744 |
1745 |
1746 |
1747 |
1748 | Atomics.Mutex.UnlockToken.prototype [ %Symbol.dispose% ] ( )
1749 | This function performs the following steps when called:
1750 |
1751 | 1. Let _token_ be the *this* value.
1752 | 1. Perform ? RequireInternalSlot(_token_, [[LockedMutex]]).
1753 | 1. Let _mutex_ be _token_.[[LockedMutex]].
1754 | 1. If _mutex_ is not ~empty~, then
1755 | 1. Set _token_.[[LockedMutex]] to ~empty~.
1756 | 1. Perform UnlockMutex(_mutex_).
1757 | 1. Return *undefined*.
1758 |
1759 |
1760 |
1761 |
1762 |
1763 |
1764 | The Condition Constructor
1765 | The Condition constructor:
1766 |
1767 | - is %Atomics.Condition%.
1768 | - is the initial value of the *"Condition"* property of the %Atomics% object.
1769 | - creates and initializes a new Condition when called as constructor.
1770 | - is not intended to be called as a function and will throw an exception when called in that manner.
1771 |
1772 |
1773 |
1774 | Atomics.Condition ( )
1775 | This function performs the following steps when called:
1776 |
1777 | 1. If NewTarget is *undefined*, throw a *TypeError* exception.
1778 | 1. Let _createInitializer_ be a new Abstract Closure with parameters (_newCV_) that captures nothing and performs the following steps when called:
1779 | 1. Set _newCV_.[[ConditionWaiterList]] to a new WaiterList Record.
1780 | 1. Return ~unused~.
1781 | 1. Return SharedStructCreate(_createInitializer_, « [[ConditionWaiterList]] »).
1782 |
1783 |
1784 |
1785 |
1786 |
1787 | Properties of the Condition Constructor
1788 | The Condition constructor:
1789 |
1790 | - has a [[Prototype]] internal slot whose value is %Function.prototype%.
1791 | - has the following properties:
1792 |
1793 |
1794 |
1795 | Per-Realm prototypes, which is currently an open design question and not included in this draft, will give Conditions a per-Realm prototype with built-in methods instead of static methods.
1796 |
1797 |
1798 |
1799 | Atomics.Condition.wait ( _cv_, _mutexUnlockToken_ )
1800 | This function atomically unlocks _mutexUnlockToken_ and puts the surrounding agent in a wait queue and suspends it until the condition variable is notified.
1801 | It performs the following steps when called:
1802 |
1803 | 1. Perform ? RequireInternalSlot(_cv_, [[ConditionWaiterList]]).
1804 | 1. Perform ? RequireInternalSlot(_mutexUnlockToken_, [[LockedMutex]]).
1805 | 1. Let _mutex_ be _mutexUnlockToken_.[[LockedMutex]].
1806 | 1. If _mutex_ is ~empty~, throw a *TypeError* exception.
1807 | 1. If AgentCanSuspend() is *false*, throw a *TypeError* exception.
1808 | 1. Let _thisAgent_ be AgentSignifier().
1809 | 1. Let _WL_ be _cv_.[[ConditionWaiterList]].
1810 | 1. Perform EnterCriticalSection(_WL_).
1811 | 1. Let _waiterRecord_ be a new Waiter Record { [[AgentSignifier]]: _thisAgent_, [[PromiseCapability]]: ~blocking~, [[TimeoutTime]]: +∞, [[Result]]: *"ok"* }.
1812 | 1. Perform AddWaiter(_WL_, _waiterRecord_).
1813 | 1. Perform UnlockMutex(_mutex_).
1814 | 1. Perform SuspendThisAgent(_WL_, _waiterRecord_).
1815 | 1. Perform LeaveCriticalSection(_WL_).
1816 | 1. Let _lockResult_ be LockMutex(_mutex_, +∞).
1817 | 1. Assert: _lockResult_ is ~acquired~.
1818 | 1. Assert: _waiterRecord_.[[Result]] is *"ok"*.
1819 | 1. Return *undefined*.
1820 |
1821 |
1822 |
1823 |
1824 | Atomics.Condition.waitFor ( _cv_, _mutexUnlockToken_, _timeout_ [ , _predicate_ ] )
1825 | If _predicate_ is *undefined*, this function atomically unlocks _mutexUnlockToken_ and puts the surrounding agent in a wait queue and suspends it until the condition variable is notified or until the wait times out, returning *true* for the former and *false* for the latter.
1826 | If a _predicate_ is passed and calling it returns *false*, this function atomically unlocks _mutexUnlockToken_ and puts the surrounding agent in a wait queue and suspends it until _predicate_ returns *true*, or until the wait times out. Whenever _predicate_ is executing, the lock on the underlying mutex of _mutexUnlockToken_ is acquired. Returns return value of the final call to _predicate_.
1827 | It performs the following steps when called:
1828 |
1829 | 1. Perform ? RequireInternalSlot(_cv_, [[ConditionWaiterList]]).
1830 | 1. Perform ? RequireInternalSlot(_mutexUnlockToken_, [[LockedMutex]]).
1831 | 1. Let _mutex_ be _mutexUnlockToken_.[[LockedMutex]].
1832 | 1. If _mutex_ is ~empty~, throw a *TypeError* exception.
1833 | 1. If _timeout_ is not a Number, throw a *TypeError* exception.
1834 | 1. If _timeout_ is either *NaN* or *+∞*𝔽, let _tMillis_ be +∞; else if _timeout_ is *-∞*𝔽, let _tMillis_ be 0; else let _tMillis_ be max(ℝ(_timeout_), 0).
1835 | 1. If _predicate_ is not *undefined* and IsCallable(_predicate_) is *false*, throw a *TypeError* exception.
1836 | 1. If AgentCanSuspend() is *false*, throw a *TypeError* exception.
1837 | 1. Let _timeBeforeWaitLoop_ be the time value (UTC) identifying the current time.
1838 | 1. Let _additionalTimeout_ be an implementation-defined non-negative mathematical value.
1839 | 1. Let _timeoutTime_ be ℝ(_timeBeforeWaitLoop_) + _tMillis_ + _additionalTimeout_.
1840 | 1. NOTE: When _tMillis_ is +∞, _timeoutTime_ is also +∞.
1841 | 1. Let _thisAgent_ be AgentSignifier().
1842 | 1. Let _WL_ be _cv_.[[ConditionWaiterList]].
1843 | 1. Let _satisfied_ be *false*.
1844 | 1. Repeat,
1845 | 1. If _predicate_ is not *undefined*, then
1846 | 1. Set _satisfied_ to ToBoolean(? Call(_predicate_, *undefined*)).
1847 | 1. If _satisfied_ is *true*, return *true*.
1848 | 1. Let _now_ be the time value (UTC) identifying the current time.
1849 | 1. If _now_ ≥ _timeoutTime_, return *false*.
1850 | 1. Perform EnterCriticalSection(_WL_).
1851 | 1. Let _waiterRecord_ be a new Waiter Record { [[AgentSignifier]]: _thisAgent_, [[PromiseCapability]]: ~blocking~, [[TimeoutTime]]: _timeoutTime_, [[Result]]: *"ok"* }.
1852 | 1. Perform AddWaiter(_WL_, _waiterRecord_).
1853 | 1. Perform UnlockMutex(_mutex_).
1854 | 1. Perform SuspendThisAgent(_WL_, _waiterRecord_).
1855 | 1. Perform LeaveCriticalSection(_WL_).
1856 | 1. Let _lockResult_ be LockMutex(_mutex_, +∞).
1857 | 1. Assert: _lockResult_ is ~acquired~.
1858 | 1. If _waiterRecord_.[[Result]] is *"ok"*, then
1859 | 1. Set _satisfied_ to *true*.
1860 | 1. Else,
1861 | 1. Assert: _waiterRecord_.[[Result]] is *"timed-out"*.
1862 | 1. Set _satisfied_ to *false*.
1863 |
1864 |
1865 |
1866 |
1867 | Atomics.Condition.notify ( _cv_ [ , _count_ ] )
1868 | It performs the following steps when called:
1869 |
1870 | 1. Perform ? RequireInternalSlot(_cv_, [[ConditionWaiterList]]).
1871 | 1. If _count_ is *undefined*, set _count_ to *+∞*𝔽.
1872 | 1. If _count_ is not an integral Number or is not *+∞*𝔽, throw a *TypeError* exception.
1873 | 1. Let _WL_ be _cv_.[[ConditionWaiterList]].
1874 | 1. Perform EnterCriticalSection(_WL_).
1875 | 1. Let _S_ be RemoveWaiters(_WL_, ℝ(_count_)).
1876 | 1. For each element _W_ of _S_, do
1877 | 1. Perform NotifyWaiter(_WL_, _W_).
1878 | 1. Perform LeaveCriticalSection(_WL_).
1879 | 1. Let _n_ be the number of elements in _S_.
1880 | 1. Return 𝔽(_n_).
1881 |
1882 |
1883 |
1884 |
1885 |
1886 |
--------------------------------------------------------------------------------