Files
ask262/engine262/src/completion.mts
T

483 lines
20 KiB
TypeScript

import { type GCMarker, surroundingAgent } from './host-defined/engine.mts';
import {
JSStringValue, Value, type Arguments,
} from './value.mts';
import {
callable,
kAsyncContext,
OutOfRange,
resume,
} from './helpers.mts';
import type { Evaluator, ValueEvaluator } from './evaluator.mts';
import {
Assert,
CreateBuiltinFunction,
PerformPromiseThen,
PromiseCapabilityRecord,
PromiseResolve,
type IteratorRecord,
} from '#self';
import { skipDebugger } from '#self';
let createNormalCompletion: <T>(init: NormalCompletionInit<T>) => NormalCompletionImpl<T>;
let createBreakCompletion: (init: BreakCompletionInit) => BreakCompletion;
let createContinueCompletion: (init: ContinueCompletionInit) => ContinueCompletion;
let createReturnCompletion: (init: ReturnCompletionInit) => ReturnCompletion;
let createThrowCompletion: (init: ThrowCompletionInit) => ThrowCompletion_;
type NormalCompletionInit<T> = Pick<NormalCompletion<T>, 'Type' | 'Value' | 'Target'>;
type BreakCompletionInit = Pick<BreakCompletion, 'Type' | 'Value' | 'Target'>;
type ContinueCompletionInit = Pick<ContinueCompletion, 'Type' | 'Value' | 'Target'>;
type ReturnCompletionInit = Pick<ReturnCompletion, 'Type' | 'Value' | 'Target'>;
type ThrowCompletionInit = Pick<ThrowCompletion, 'Type' | 'Value' | 'Target'>;
type AbruptCompletionInit =
| BreakCompletionInit
| ContinueCompletionInit
| ReturnCompletionInit
| ThrowCompletionInit;
type CompletionInit<T> =
| NormalCompletionInit<T>
| AbruptCompletionInit;
@callable((_target, _thisArg, [completionRecord]) => {
// 1. Assert: completionRecord is a Completion Record.
Assert(completionRecord instanceof Completion);
// 2. Return completionRecord as the Completion Record of this abstract operation.
return completionRecord;
})
class CompletionImpl<const T> {
declare readonly Type: 'normal' | 'break' | 'continue' | 'return' | 'throw';
readonly Value!: T | Value;
readonly Target!: JSStringValue | undefined;
constructor(init: CompletionInit<T>) {
if (new.target === CompletionImpl) {
switch (init.Type) {
case 'normal':
return createNormalCompletion(init);
case 'break':
return createBreakCompletion(init) as CompletionImpl<T>;
case 'continue':
return createContinueCompletion(init) as CompletionImpl<T>;
case 'return':
return createReturnCompletion(init) as CompletionImpl<T>;
case 'throw':
return createThrowCompletion(init) as CompletionImpl<T>;
default:
throw new OutOfRange('new Completion', init);
}
}
const { Type, Value, Target } = init;
Assert(new.target.prototype.Type === Type);
this.Value = Value as T;
this.Target = Target;
}
// NON-SPEC
mark(m: GCMarker) {
m(this.Value);
}
static {
Object.defineProperty(this, 'name', { value: 'Completion' });
}
}
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export type Completion<T> =
| NormalCompletion<T>
| AbruptCompletion<T>;
/**
* A NON-SPEC shorthand to notate "returns either a normal completion containing an ECMAScript language value or a throw completion".
*/
// export type ValueEvaluator<T extends Value = Value> = T | NormalCompletion<T> | ThrowCompletion;
export type ValueCompletion<T extends Value = Value> = T | NormalCompletion<T> | ThrowCompletion;
export { type ValueEvaluator } from './evaluator.mts';
/**
* A NON-SPEC shorthand to notate "returns either a normal completion containing ... or a throw completion".
*
* If the T is an ECMAScript language value, use ExpressionCompletion<T>.
*/
export type PlainCompletion<T> = T | NormalCompletion<T> | ThrowCompletion;
export type YieldCompletion = NormalCompletion<Value> | ThrowCompletion | ReturnCompletion;
/** https://tc39.es/ecma262/#sec-completion-ao */
export const Completion = CompletionImpl as {
/** https://tc39.es/ecma262/#sec-completion-ao */
<T extends Completion<unknown>>(completionRecord: T): T;
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
new <const T>(completion: { Type: 'normal', Value: T, Target: undefined }): NormalCompletion<T>;
new(completion: { Type: 'break', Value: void, Target: JSStringValue | undefined }): BreakCompletion;
new(completion: { Type: 'continue', Value: void, Target: JSStringValue | undefined }): ContinueCompletion;
new(completion: { Type: 'return', Value: Value, Target: undefined }): ReturnCompletion;
new(completion: { Type: 'throw', Value: Value, Target: undefined }): ThrowCompletion;
readonly prototype: CompletionImpl<unknown>;
};
@callable((_target, _thisArg, [value]) => { // eslint-disable-line arrow-body-style -- Preserve algorithm steps comments
// 1. Return Completion { [[Type]]: normal, [[Value]]: value, [[Target]]: empty }.
return new Completion({ Type: 'normal', Value: value, Target: undefined });
})
class NormalCompletionImpl<const T> extends CompletionImpl<T> {
declare readonly Type: 'normal';
declare readonly Value: T;
declare readonly Target: undefined;
private constructor(init: NormalCompletionInit<T>) { // eslint-disable-line no-useless-constructor -- Sets privacy for constructor
super(init);
}
static {
Object.defineProperty(this, 'name', { value: 'NormalCompletion' });
Object.defineProperty(this.prototype, 'Type', { value: 'normal' });
createNormalCompletion = (init) => new NormalCompletionImpl(init);
}
}
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export type NormalCompletion<T> = NormalCompletionImpl<T>;
/** https://tc39.es/ecma262/#sec-normalcompletion */
export const NormalCompletion = NormalCompletionImpl as typeof NormalCompletionImpl & {
/** https://tc39.es/ecma262/#sec-normalcompletion */
<const T>(value: T): NormalCompletion<T>;
};
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export type AbruptCompletion<T = unknown> =
| ThrowCompletion
| ReturnCompletion
| BreakCompletion
| ContinueCompletion;
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export const AbruptCompletion = (() => {
abstract class AbruptCompletion<const T> extends CompletionImpl<T | Value> {
declare readonly Type: 'break' | 'continue' | 'return' | 'throw';
declare readonly Value: T | Value;
declare readonly Target: JSStringValue | undefined;
constructor(init: AbruptCompletionInit) { // eslint-disable-line no-useless-constructor -- Sets privacy for constructor
super(init);
}
static {
Object.defineProperty(this, 'name', { value: 'AbruptCompletion' });
}
}
return AbruptCompletion;
})();
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export class BreakCompletion extends AbruptCompletion<void> {
declare readonly Type: 'break';
declare readonly Value: void;
private constructor(init: BreakCompletionInit) { // eslint-disable-line no-useless-constructor -- Sets privacy for constructor
super(init);
}
static {
Object.defineProperty(this, 'name', { value: 'BreakCompletion' });
Object.defineProperty(this.prototype, 'Type', { value: 'break' });
createBreakCompletion = (init) => new BreakCompletion(init);
}
}
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export class ContinueCompletion extends AbruptCompletion<void> {
declare readonly Type: 'continue';
declare readonly Value: void;
declare readonly Target: JSStringValue | undefined;
private constructor(init: ContinueCompletionInit) { // eslint-disable-line no-useless-constructor -- Sets privacy for constructor
super(init);
}
static {
Object.defineProperty(this, 'name', { value: 'ContinueCompletion' });
Object.defineProperty(this.prototype, 'Type', { value: 'continue' });
createContinueCompletion = (init) => new ContinueCompletion(init);
}
}
@callable((_target, _thisArg, [value]) => {
Assert(value instanceof Value);
// 1. Return Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }.
return new Completion({ Type: 'return', Value: value as Value, Target: undefined });
})
class ReturnCompletion_ extends AbruptCompletion<Value> {
declare readonly Type: 'return';
declare readonly Value: Value;
declare readonly Target: undefined;
private constructor(init: ReturnCompletionInit) { // eslint-disable-line no-useless-constructor -- Sets privacy for constructor
super(init);
}
static {
Object.defineProperty(this, 'name', { value: 'ReturnCompletion' });
Object.defineProperty(this.prototype, 'Type', { value: 'return' });
createReturnCompletion = (init) => new ReturnCompletion(init);
}
}
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export type ReturnCompletion = ReturnCompletion_;
/** https://tc39.es/ecma262/#sec-throwcompletion */
export const ReturnCompletion = ReturnCompletion_ as typeof ReturnCompletion_ & {
/** https://tc39.es/ecma262/#sec-throwcompletion */
(value: Value): ThrowCompletion;
};
const debugging = false;
@callable((_target, _thisArg, [value]) => {
Assert(value instanceof Value);
// 1. Return Completion { [[Type]]: throw, [[Value]]: value, [[Target]]: empty }.
return new Completion({ Type: 'throw', Value: value as Value, Target: undefined });
})
class ThrowCompletion_ extends AbruptCompletion<Value> {
declare readonly Type: 'throw';
declare readonly Value: Value;
declare readonly Target: undefined;
readonly stack = debugging ? new Error() : undefined;
private constructor(init: Pick<ThrowCompletion_, 'Type' | 'Value' | 'Target'>) { // eslint-disable-line no-useless-constructor -- Sets privacy for constructor
super(init);
if (debugging) {
Error.stackTraceLimit = Infinity;
}
}
static {
Object.defineProperty(this, 'name', { value: 'ThrowCompletion' });
Object.defineProperty(this.prototype, 'Type', { value: 'throw' });
createThrowCompletion = (init) => new ThrowCompletion_(init);
}
}
/** https://tc39.es/ecma262/#sec-completion-record-specification-type */
export type ThrowCompletion = ThrowCompletion_;
/** https://tc39.es/ecma262/#sec-throwcompletion */
export const ThrowCompletion = ThrowCompletion_ as typeof ThrowCompletion_ & {
/** https://tc39.es/ecma262/#sec-throwcompletion */
(value: Value): ThrowCompletion;
};
/** https://tc39.es/ecma262/#sec-updateempty */
export type UpdateEmpty<T extends Completion<unknown>, U> =
T extends NormalCompletion<infer V> ? NormalCompletion<V extends undefined ? U : V> :
T extends BreakCompletion ? BreakCompletion :
T extends ContinueCompletion ? ContinueCompletion :
T extends AbruptCompletion ? T :
T extends ReturnCompletion ? T :
never;
/** https://tc39.es/ecma262/#sec-updateempty */
export function UpdateEmpty<C extends Completion<unknown>, const T>(completionRecord: C, value: T): UpdateEmpty<C, T>;
export function UpdateEmpty<C extends Completion<unknown>, const T>(completionRecord: C, value: T) {
// 1. Assert: If completionRecord.[[Type]] is either return or throw, then completionRecord.[[Value]] is not empty.
Assert(!(completionRecord.Type === 'return' || completionRecord.Type === 'throw') || completionRecord.Value !== undefined);
// 2. If completionRecord.[[Value]] is not empty, return Completion(completionRecord).
if (completionRecord.Value !== undefined) {
return Completion(completionRecord);
}
// 3. Return Completion { [[Type]]: completionRecord.[[Type]], [[Value]]: value, [[Target]]: completionRecord.[[Target]] }.
return new CompletionImpl({ Type: completionRecord.Type, Value: value, Target: completionRecord.Target } as unknown as CompletionInit<unknown>); // NOTE: unsound cast
}
/** https://tc39.es/ecma262/#sec-returnifabrupt */
export type Q<T> =
T extends NormalCompletion<infer V> ? V :
T extends AbruptCompletion ? never :
T;
/**
* https://tc39.es/ecma262/#sec-returnifabrupt
* https://tc39.es/ecma262/#sec-returnifabrupt-shorthands ? OperationName()
*/
export function Q<const T>(_completion: T): Q<T> {
/* node:coverage ignore next */
throw new TypeError('Q requires build');
}
function Q_runtime<const T>(completion: T): Q<T> {
/* node:coverage ignore next 3 */
if (typeof completion === 'object' && completion && 'next' in completion) {
throw new TypeError('Forgot to yield* on the completion.');
}
const c = EnsureCompletion(completion);
if (c.Type === 'normal') {
return c.Value as Q<T>;
}
throw c;
}
/** https://tc39.es/ecma262/#sec-returnifabrupt-shorthands ! OperationName() */
export function X<const T>(_completion: T | Evaluator<T>): Q<T> {
/* node:coverage ignore next */
throw new TypeError('X() requires build');
}
export function unwrapCompletion<const T>(completion: T | Evaluator<T>): Q<T> {
/* node:coverage ignore next 3 */
if (typeof completion === 'object' && completion && 'next' in completion) {
completion = skipDebugger(completion);
}
const c = EnsureCompletion(completion);
if (c instanceof NormalCompletion) {
return c.Value as Q<T>;
}
/* node:coverage ignore next */
throw new Assert.Error('Unexpected AbruptCompletion.', { cause: c });
}
/** https://tc39.es/ecma262/#sec-ifabruptcloseiterator */
export function IfAbruptCloseIterator<T>(_value: T, _iteratorRecord: IteratorRecord): Q<T> {
/* node:coverage ignore next */
throw new TypeError('IfAbruptCloseIterator() requires build');
}
/** https://tc39.es/ecma262/#sec-ifabruptcloseasynciterator */
export function IfAbruptCloseAsyncIterator<T>(_value: T, _iteratorRecord: IteratorRecord): Q<T> {
/* node:coverage ignore next */
throw new TypeError('IfAbruptCloseAsyncIterator() requires build');
}
/** https://tc39.es/ecma262/#sec-ifabruptrejectpromise */
export function IfAbruptRejectPromise<T>(_value: T, _capability: PromiseCapabilityRecord): Q<T> {
/* node:coverage ignore next */
throw new TypeError('IfAbruptRejectPromise requires build');
}
/**
* This is a util for code that cannot use Q() or X() marco to emulate this behaviour.
*
* @example
* import { evalQ } from '...'
* evalQ((Q) => {
* let val = Q(operation);
* });
*/
export function evalQ<T>(callback: (q: typeof Q, x: typeof X) => Promise<T>): Promise<NormalCompletion<T> | ThrowCompletion>
export function evalQ<T>(callback: (q: typeof Q, x: typeof X) => T): NormalCompletion<T> | ThrowCompletion
export function evalQ<T>(callback: (q: typeof Q, x: typeof X) => T | Promise<T>): Promise<NormalCompletion<T> | ThrowCompletion> | NormalCompletion<T> | ThrowCompletion {
try {
const result = callback(Q_runtime, unwrapCompletion);
if (result instanceof Promise) {
return result.then(EnsureCompletion, (error) => {
if (error instanceof ThrowCompletion) {
return error;
}
throw error;
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return EnsureCompletion(result) as any;
} catch (error) {
if (error instanceof ThrowCompletion) {
return error;
}
// a real error
throw error;
}
}
export type EnsureCompletion<T> = EnsureCompletionWorker<T, T>;
// Distribute over `T`s that are `Completion`s, but don't distribute over `T`s that aren't `Completion`s
type EnsureCompletionWorker<T, _T> = T extends Completion<unknown> ? T : NormalCompletion<Exclude<_T, PlainCompletion<unknown>>>;
/** https://tc39.es/ecma262/#sec-implicit-normal-completion */
export function EnsureCompletion(val: Value): NormalCompletion<Value>;
export function EnsureCompletion<const T>(val: T): EnsureCompletion<T>;
export function EnsureCompletion<const T>(val: T) {
if (val instanceof Completion) {
return val;
}
return NormalCompletion(val);
}
export function ValueOfNormalCompletion<T>(value: NormalCompletion<T> | T) {
return value instanceof NormalCompletion ? value.Value : value;
}
export function* Await(value: Value): ValueEvaluator {
// 1. Let asyncContext be the running execution context.
const asyncContext = surroundingAgent.runningExecutionContext;
// 2. Let promise be ? PromiseResolve(%Promise%, value).
const promise = Q(yield* PromiseResolve(surroundingAgent.intrinsic('%Promise%'), value));
// 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called:
const fulfilledClosure = function* fulfilledClosure([v = Value.undefined]: Arguments) {
// a. Let prevContext be the running execution context.
const prevContext = surroundingAgent.runningExecutionContext;
// b. Suspend prevContext.
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
surroundingAgent.executionContextStack.push(asyncContext);
// d. Resume the suspended evaluation of asyncContext using NormalCompletion(value) as the result of the operation that suspended it.
yield* resume(asyncContext, { type: 'await-resume', value: NormalCompletion(v) });
// e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context.
Assert(surroundingAgent.runningExecutionContext === prevContext);
// f. Return undefined.
return Value.undefined;
};
// 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
const onFulfilled = CreateBuiltinFunction(fulfilledClosure, 1, Value(''), []);
// @ts-expect-error TODO(ts): CreateBuiltinFunction should return a specalized type FunctionObjectValue that has a kAsyncContext on it.
onFulfilled[kAsyncContext] = asyncContext;
// 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the following steps when called:
const rejectedClosure = function* rejectedClosure([reason = Value.undefined]: Arguments) {
// a. Let prevContext be the running execution context.
const prevContext = surroundingAgent.runningExecutionContext;
// b. Suspend prevContext.
// c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
surroundingAgent.executionContextStack.push(asyncContext);
// d. Resume the suspended evaluation of asyncContext using ThrowCompletion(reason) as the result of the operation that suspended it.
yield* resume(asyncContext, { type: 'await-resume', value: ThrowCompletion(reason) });
// e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context.
Assert(surroundingAgent.runningExecutionContext === prevContext);
// f. Return undefined.
return Value.undefined;
};
// 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
const onRejected = CreateBuiltinFunction(rejectedClosure, 1, Value(''), []);
// @ts-expect-error TODO(ts): CreateBuiltinFunction should return a specalized type FunctionObjectValue that has a kAsyncContext on it.
onRejected[kAsyncContext] = asyncContext;
// 7. Perform ! PerformPromiseThen(promise, onFulfilled, onRejected).
X(PerformPromiseThen(promise, onFulfilled, onRejected));
// 8. Remove asyncContext from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context.
surroundingAgent.executionContextStack.pop(asyncContext);
// 9. Set the code evaluation state of asyncContext such that when evaluation is resumed with a Completion completion, the following steps of the algorithm that invoked Await will be performed, with completion available.
const completion = yield { type: 'await' };
Assert(completion.type === 'await-resume');
// 10. Return.
return completion.value;
// 11. NOTE: This returns to the evaluation of the operation that had most previously resumed evaluation of asyncContext.
}