Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix early call to return/throw on generator #186

Merged
merged 3 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions docs/generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Here's the body of the `__generator` helper:

```js
__generator = function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t;
return { next: verb(0), "throw": verb(1), "return": verb(2) };
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
Expand Down Expand Up @@ -98,7 +98,7 @@ arguments, and their purpose:
| 7 (endfinally) | | Exits a finally block, resuming any previous operation (such as a break, return, throw, etc.) |

# State
The `_`, `f`, `y`, and `t` variables make up the persistent state of the `__generator` function. Each variable
The `_`, `f`, `y`, `t`, and `g` variables make up the persistent state of the `__generator` function. Each variable
has a specific purpose, as described in the following sections:

## The `_` variable
Expand Down Expand Up @@ -148,6 +148,12 @@ The `t` variable is a temporary variable that stores one of the following values

> NOTE: None of the above cases overlap.

## The `g` variable
The `g` variable is a temporary variable that holds onto the generator object for the purpose of attaching a
`Symbol.iterator` method (if its available), and holds onto that value until the generator is started, allowing

rbuckton marked this conversation as resolved.
Show resolved Hide resolved
it to also act as the [`suspendedStart`](https://tc39.es/ecma262/#table-internal-slots-of-generator-instances) state.

# Protected Regions
A **Protected Region** is a region within the `body` function that indicates a
`try..catch..finally` statement. It consists of a 4-tuple that contains 4 labels:
Expand All @@ -164,13 +170,18 @@ The final step of the `__generator` helper is the allocation of an object that i
`Generator` protocol, to be used by the `__awaiter` helper:

```ts
return { next: verb(0), "throw": verb(1), "return": verb(2) };
return g = { next: verb(0), "throw": verb(1), "return": verb(2) },
typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }),
g;
function verb(n) { return function (v) { return step([n, v]); }; }
```

This object translates calls to `next`, `throw`, and `return` to the appropriate Opcodes and
invokes the `step` orchestration function to continue execution. The `throw` and `return` method
names are quoted to better support ES3.
names are quoted to better support ES3. In addition, a `Symbol.iterator` method is added to the
generator if the global `Symbol` constructor is available. Once we return, the object reference in
the `g` variable isn't used by the main [orchestration method](#orchestration), so we can use its
truthiness as a mechanism to determine whether the generator is in the `suspendedStart` state.

# Orchestration
The `step` function is the main orechestration mechanism for the `__generator` helper. It
Expand All @@ -181,9 +192,9 @@ Here's a closer look at the `step` function:
```ts
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
Expand Down Expand Up @@ -219,13 +230,25 @@ The main body of the `step` function consists of a `while` loop which continues
instructions until the generator exits or is suspended:

```ts
while (_) try ...
while (g && (g = 0, op[0] && (_ = 0)), _) try ...
```

During the first call to `next()`, `return()`, or `throw()`, the generator will be in the `suspendedStart` state. This
is indicated by the `g` variable being "truthy" since it still holds a reference to the generator object. If `g` is
"truthy", then we reset it and check whether the first instruction sent to the generator (`op[0]`) is either a
`return` (`op[0] === 1`) or `throw` (`op[0] === 2`) Opcode, indicating an abrupt completion.

If the first instruction is abrupt, we can emulate [GeneratorResumeAbrupt](https://tc39.es/ecma262/#sec-generatorresumeabrupt)
by setting the state variable (`_`) to a falsy value, which will skip the loop body and
[complete the generator](#handling-a-completed-generator).

If this is _not_ the first instruction sent to the generator, or if the first instruction was `next()`, we will proceed
to evaluate the loop body.

When the generator has run to completion, the `_` state variable will be cleared, forcing the loop
to exit.

## Evaluating the generator body.
## Evaluating the generator body
```ts
try {
...
Expand Down Expand Up @@ -272,8 +295,8 @@ reduce the overall footprint of the helper.
The first two statements of the `try..finally` statement handle delegation for `yield*`:

```ts
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
```

If the `y` variable is set, and `y` has a `next`, `throw`, or `return` method (depending on the
Expand All @@ -282,7 +305,7 @@ current operation), we invoke this method and store the return value (an Iterato
If `t` indicates it is a yielded value (e.g. `t.done === false`), we return `t` to the caller.
If `t` indicates it is a returned value (e.g. `t.done === true`), we mark the operation with the
`next` Opcode, and the returned value.
If `y` did not have the appropriate method, or `t` was a returned value, we reset `y` to a falsey
If `y` did not have the appropriate method, or `t` was a returned value, we reset `y` to a falsy
value and continue processing the operation.

## Handling operations
Expand Down Expand Up @@ -402,7 +425,7 @@ if (!(t = ...) && (op[0] === 6 || op[0] === 2)) {
```

If we encounter an Opcode 6 ("catch") or Opcode 2 ("return"), and we are not in a protected region,
then this operation completes the generator by setting the `_` variable to a falsey value. The
then this operation completes the generator by setting the `_` variable to a falsy value. The
`continue` statement resumes execution at the top of the `while` statement, which will exit the loop
so that we continue execution at the statement following the loop.

Expand Down Expand Up @@ -463,7 +486,7 @@ current **protected region** from the stack and spin the `while` statement to ev
operation again in the next **protected region** or at the function boundary.

## Handling a completed generator
Once the generator has completed, the `_` state variable will be falsey. As a result, the `while`
Once the generator has completed, the `_` state variable will be falsy. As a result, the `while`
loop will terminate and hand control off to the final statement of the orchestration function,
which deals with how a completed generator is evaluated:

Expand Down
2 changes: 1 addition & 1 deletion tslib.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function __generator(thisArg, body) {
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
Expand Down
2 changes: 1 addition & 1 deletion tslib.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ var __createBinding;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
Expand Down