mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 21:31:46 +00:00
632 lines
16 KiB
TypeScript
632 lines
16 KiB
TypeScript
import type { Protocol } from 'devtools-protocol';
|
|
import {
|
|
DynamicParsedCodeRecord, type GCMarker, surroundingAgent,
|
|
} from './host-defined/engine.mts';
|
|
import { ExecutionContext } from './execution-context/ExecutionContext.mts';
|
|
import {
|
|
Value, JSStringValue, ObjectValue, UndefinedValue, NullValue, type PropertyKeyValue,
|
|
SymbolValue,
|
|
} from './value.mts';
|
|
import { Q } from './completion.mts';
|
|
import type { ParseNode } from './parser/ParseNode.mts';
|
|
import type {
|
|
Evaluator, EvaluatorNextType, ValueEvaluator, YieldEvaluator,
|
|
} from './evaluator.mts';
|
|
import type { ErrorObject } from './intrinsics/Error.mts';
|
|
import {
|
|
Call,
|
|
isFunctionObject,
|
|
isBuiltinFunctionObject,
|
|
isECMAScriptFunctionObject,
|
|
} from '#self';
|
|
|
|
export const kInternal = Symbol('kInternal');
|
|
|
|
export class JSStringMap<V> implements Map<JSStringValue, V> {
|
|
#map = new Map<string, V>();
|
|
|
|
clear() {
|
|
this.#map.clear();
|
|
}
|
|
|
|
delete(key: JSStringValue | string) {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
return this.#map.delete(key);
|
|
}
|
|
|
|
forEach(callbackfn: (value: V, key: JSStringValue, map: Map<JSStringValue, V>) => void, thisArg?: JSStringMap<V>) {
|
|
this.#map.forEach((value, key) => Reflect.apply(callbackfn, thisArg, [value, typeof key === 'string' ? Value(key) : key, this]));
|
|
}
|
|
|
|
get(key: JSStringValue | string) {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
return this.#map.get(key);
|
|
}
|
|
|
|
has(key: JSStringValue | string) {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
return this.#map.has(key);
|
|
}
|
|
|
|
set(key: JSStringValue | string, value: V): this {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
this.#map.set(key, value);
|
|
return this;
|
|
}
|
|
|
|
get size() {
|
|
return this.#map.size;
|
|
}
|
|
|
|
* entries() {
|
|
for (const [key, value] of this.#map.entries()) {
|
|
yield [Value(key), value] as [JSStringValue, V];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
* keys() {
|
|
for (const key of this.#map.keys()) {
|
|
yield Value(key);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
values() {
|
|
return this.#map.values();
|
|
}
|
|
|
|
declare [Symbol.iterator]: () => MapIterator<[JSStringValue, V]>;
|
|
|
|
declare [Symbol.toStringTag]: string;
|
|
|
|
static {
|
|
JSStringMap.prototype[Symbol.toStringTag] = 'JSStringMap';
|
|
JSStringMap.prototype[Symbol.iterator] = JSStringMap.prototype.entries;
|
|
}
|
|
|
|
mark(m: GCMarker) {
|
|
for (const [k, v] of this.#map.entries()) {
|
|
m(k);
|
|
m(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class PropertyKeyMap<V> implements Map<PropertyKeyValue, V> {
|
|
#map = new Map<string | SymbolValue, V>();
|
|
|
|
clear() {
|
|
this.#map.clear();
|
|
}
|
|
|
|
delete(key: PropertyKeyValue | string) {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
return this.#map.delete(key);
|
|
}
|
|
|
|
forEach(callbackfn: (value: V, key: PropertyKeyValue, map: Map<PropertyKeyValue, V>) => void, thisArg?: PropertyKeyMap<V>) {
|
|
this.#map.forEach((value, key) => Reflect.apply(callbackfn, thisArg, [value, typeof key === 'string' ? Value(key) : key, this]));
|
|
}
|
|
|
|
get(key: PropertyKeyValue | string) {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
return this.#map.get(key);
|
|
}
|
|
|
|
has(key: PropertyKeyValue | string) {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
return this.#map.has(key);
|
|
}
|
|
|
|
set(key: PropertyKeyValue | string, value: V): this {
|
|
if (key instanceof JSStringValue) {
|
|
key = key.stringValue();
|
|
}
|
|
this.#map.set(key, value);
|
|
return this;
|
|
}
|
|
|
|
get size() {
|
|
return this.#map.size;
|
|
}
|
|
|
|
* entries() {
|
|
for (const [key, value] of this.#map.entries()) {
|
|
if (typeof key === 'string') {
|
|
yield [Value(key), value] as [JSStringValue, V];
|
|
} else {
|
|
yield [key, value] as [SymbolValue, V];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
* keys() {
|
|
for (const key of this.#map.keys()) {
|
|
if (typeof key === 'string') {
|
|
yield Value(key);
|
|
} else {
|
|
yield key;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
* values() {
|
|
for (const value of this.#map.values()) {
|
|
yield value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
declare [Symbol.iterator]: () => MapIterator<[PropertyKeyValue, V]>;
|
|
|
|
declare [Symbol.toStringTag]: string;
|
|
|
|
static {
|
|
PropertyKeyMap.prototype[Symbol.toStringTag] = 'PropertyKeyMap';
|
|
PropertyKeyMap.prototype[Symbol.iterator] = PropertyKeyMap.prototype.entries;
|
|
}
|
|
|
|
mark(m: GCMarker) {
|
|
for (const [k, v] of this.#map.entries()) {
|
|
m(k);
|
|
m(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class JSStringSet {
|
|
#set = new Set<string>();
|
|
|
|
constructor(value?: Iterable<JSStringValue | string>) {
|
|
if (value) {
|
|
for (const item of value) {
|
|
this.add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
add(value: JSStringValue | string): this {
|
|
this.#set.add(typeof value === 'string' ? value : value.stringValue());
|
|
return this;
|
|
}
|
|
|
|
clear(): void {
|
|
this.#set.clear();
|
|
}
|
|
|
|
delete(value: JSStringValue | string): boolean {
|
|
return this.#set.delete(typeof value === 'string' ? value : value.stringValue());
|
|
}
|
|
|
|
forEach(callbackfn: (value: JSStringValue, value2: JSStringValue, set: Set<JSStringValue>) => void, thisArg?: JSStringSet): void {
|
|
for (const value of this.#set) {
|
|
Reflect.apply(callbackfn, thisArg, [Value(value), Value(value), this]);
|
|
}
|
|
}
|
|
|
|
has(value: JSStringValue | NullValue | string): boolean {
|
|
if (value instanceof NullValue) {
|
|
return false;
|
|
}
|
|
return this.#set.has(typeof value === 'string' ? value : value.stringValue());
|
|
}
|
|
|
|
get size() {
|
|
return this.#set.size;
|
|
}
|
|
|
|
* entries(): SetIterator<[JSStringValue, JSStringValue]> {
|
|
for (const value of this.#set) {
|
|
yield [Value(value), Value(value)];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
declare keys: () => SetIterator<JSStringValue>;
|
|
|
|
* values() {
|
|
for (const value of this.#set) {
|
|
yield Value(value);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
declare [Symbol.iterator]: () => SetIterator<JSStringValue>;
|
|
|
|
declare [Symbol.toStringTag]: string;
|
|
|
|
static {
|
|
JSStringSet.prototype[Symbol.toStringTag] = 'JSStringSet';
|
|
JSStringSet.prototype[Symbol.iterator] = JSStringSet.prototype.values;
|
|
JSStringSet.prototype.keys = JSStringSet.prototype.values;
|
|
}
|
|
|
|
mark(_m: GCMarker) { }
|
|
}
|
|
|
|
export class OutOfRange extends RangeError {
|
|
/* node:coverage disable */
|
|
declare cause: unknown;
|
|
|
|
detail: unknown;
|
|
|
|
constructor(fn: string, detail: unknown) {
|
|
super(`${fn}() argument out of range`, { cause: detail });
|
|
this.detail = detail;
|
|
}
|
|
}
|
|
/* node:coverage enable */
|
|
|
|
export function skipDebugger<T>(iterator: Evaluator<T>, maxSteps = Infinity): T {
|
|
let steps = 0;
|
|
while (true) {
|
|
const { done, value } = iterator.next({ type: 'debugger-resume', value: undefined });
|
|
if (done) {
|
|
return value;
|
|
}
|
|
/* node:coverage ignore next 4 */
|
|
steps += 1;
|
|
if (steps > maxSteps) {
|
|
throw new RangeError('Max steps exceeded');
|
|
}
|
|
}
|
|
}
|
|
|
|
export function* resume(context: ExecutionContext, completion: EvaluatorNextType): YieldEvaluator {
|
|
let result;
|
|
while (true) {
|
|
result = context.codeEvaluationState!.next(completion);
|
|
if (result.done) {
|
|
return result.value;
|
|
}
|
|
const { value } = result;
|
|
if (value.type === 'debugger' || value.type === 'potential-debugger') {
|
|
completion = yield value;
|
|
} else if (value.type === 'await' || value.type === 'async-generator-yield') {
|
|
return Value.undefined;
|
|
} else if (value.type === 'yield') {
|
|
return value.value;
|
|
} else {
|
|
unreachable(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class CallSite {
|
|
context: ExecutionContext;
|
|
|
|
lastNode: ParseNode | null = null;
|
|
|
|
nextNode: ParseNode | null = null;
|
|
|
|
lastCallNode: ParseNode.CallExpression | null = null;
|
|
|
|
inheritedLastCallNode: ParseNode.CallExpression | null = null;
|
|
|
|
constructCall = false;
|
|
|
|
constructor(context: ExecutionContext) {
|
|
this.context = context;
|
|
}
|
|
|
|
clone(context = this.context) {
|
|
const c = new CallSite(context);
|
|
c.lastNode = this.lastNode;
|
|
c.lastCallNode = this.lastCallNode;
|
|
c.inheritedLastCallNode = this.inheritedLastCallNode;
|
|
c.constructCall = this.constructCall;
|
|
return c;
|
|
}
|
|
|
|
isTopLevel() {
|
|
return this.context.Function === Value.null;
|
|
}
|
|
|
|
isConstructCall() {
|
|
return this.constructCall;
|
|
}
|
|
|
|
isAsync() {
|
|
if (!(this.context.Function instanceof NullValue) && isECMAScriptFunctionObject(this.context.Function) && this.context.Function.ECMAScriptCode) {
|
|
const code = this.context.Function.ECMAScriptCode;
|
|
return code.type === 'AsyncBody' || code.type === 'AsyncGeneratorBody';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isNative() {
|
|
return isBuiltinFunctionObject(this.context.Function);
|
|
}
|
|
|
|
getFunctionName(): string | null {
|
|
if (isFunctionObject(this.context.Function)) {
|
|
const name = this.context.Function.properties.get('name');
|
|
if (name && name.Value && name.Value instanceof JSStringValue) {
|
|
return name.Value.stringValue();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getSpecifier() {
|
|
if (this.context.HostDefined?.scriptId && surroundingAgent.parsedSources.get(this.context.HostDefined.scriptId) instanceof DynamicParsedCodeRecord) {
|
|
return null;
|
|
}
|
|
if (!(this.context.ScriptOrModule instanceof NullValue)) {
|
|
return this.context.ScriptOrModule.HostDefined.specifier;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getScriptId() {
|
|
const context = this.context.HostDefined?.scriptId;
|
|
if (context) {
|
|
return context;
|
|
}
|
|
if (!(this.context.ScriptOrModule instanceof NullValue)) {
|
|
return this.context.ScriptOrModule.HostDefined.scriptId;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
setLocation(node: ParseNode) {
|
|
this.lastNode = node;
|
|
}
|
|
|
|
setNextLocation(node: ParseNode) {
|
|
this.nextNode = node;
|
|
}
|
|
|
|
setCallLocation(node: ParseNode.CallExpression | null) {
|
|
this.lastCallNode = node;
|
|
}
|
|
|
|
get lineNumber() {
|
|
if (this.lastNode) {
|
|
return this.lastNode.location.start.line;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get columnNumber() {
|
|
if (this.lastNode) {
|
|
return this.lastNode.location.start.column;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
loc() {
|
|
if (this.isNative()) {
|
|
return 'native';
|
|
}
|
|
let out = '';
|
|
const specifier = this.getSpecifier();
|
|
if (specifier) {
|
|
out += specifier;
|
|
} else {
|
|
out += '<anonymous>';
|
|
}
|
|
if (this.lineNumber !== null) {
|
|
out += `:${this.lineNumber}`;
|
|
if (this.columnNumber !== null) {
|
|
out += `:${this.columnNumber}`;
|
|
}
|
|
}
|
|
return out.trim();
|
|
}
|
|
|
|
toString() {
|
|
const isAsync = this.isAsync();
|
|
const functionName = this.getFunctionName();
|
|
const isConstructCall = this.isConstructCall();
|
|
const isMethodCall = !isConstructCall && !this.isTopLevel();
|
|
|
|
let visualFunctionName;
|
|
if (this.inheritedLastCallNode?.CallExpression.type === 'IdentifierReference') {
|
|
visualFunctionName = this.inheritedLastCallNode.CallExpression.name;
|
|
}
|
|
if (visualFunctionName === functionName) {
|
|
visualFunctionName = undefined;
|
|
}
|
|
|
|
let string = isAsync ? 'async ' : '';
|
|
|
|
if (isConstructCall) {
|
|
string += 'new ';
|
|
}
|
|
|
|
if (isMethodCall || isConstructCall) {
|
|
if (functionName) {
|
|
string += functionName;
|
|
} else {
|
|
string += '<anonymous>';
|
|
}
|
|
if (visualFunctionName) {
|
|
string += ` (as ${visualFunctionName})`;
|
|
}
|
|
} else if (functionName) {
|
|
string += functionName;
|
|
if (visualFunctionName) {
|
|
string += ` (as ${visualFunctionName})`;
|
|
}
|
|
} else {
|
|
return `${string}${this.loc()}`;
|
|
}
|
|
|
|
return `${string} (${this.loc()})`;
|
|
}
|
|
|
|
toCallFrame(): Protocol.Runtime.CallFrame | undefined {
|
|
const source = this.getScriptId();
|
|
if (source === undefined || source === null) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
columnNumber: (this.columnNumber || 1) - 1,
|
|
lineNumber: (this.lineNumber || 1) - 1,
|
|
functionName: this.getFunctionName() || '<anonymous>',
|
|
scriptId: source,
|
|
url: this.getSpecifier() || '<anonymous>',
|
|
};
|
|
}
|
|
}
|
|
|
|
export class CallFrame {
|
|
columnNumber: number | undefined;
|
|
|
|
lineNumber: number | undefined;
|
|
|
|
functionName: string | undefined;
|
|
|
|
scriptId: string | undefined;
|
|
|
|
url: string | undefined;
|
|
|
|
toCallFrame(): Protocol.Runtime.CallFrame | undefined {
|
|
if (!this.scriptId) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
columnNumber: (this.columnNumber || 1) - 1,
|
|
lineNumber: (this.lineNumber || 1) - 1,
|
|
functionName: this.functionName || '<anonymous>',
|
|
scriptId: this.scriptId,
|
|
url: this.url || '<anonymous>',
|
|
};
|
|
}
|
|
}
|
|
|
|
export const kAsyncContext = Symbol('kAsyncContext');
|
|
|
|
function captureAsyncStack(stack: CallSite[]) {
|
|
let promise = stack[0].context.promiseCapability!.Promise;
|
|
for (let i = 0; i < 10; i += 1) {
|
|
if (promise.PromiseFulfillReactions!.length !== 1) {
|
|
return;
|
|
}
|
|
const [reaction] = promise.PromiseFulfillReactions!;
|
|
if (reaction.Handler && reaction.Handler.Callback[kAsyncContext]) {
|
|
const asyncContext = reaction.Handler.Callback[kAsyncContext];
|
|
stack.push(asyncContext.callSite.clone());
|
|
if ('PromiseState' in asyncContext.promiseCapability!.Promise) {
|
|
promise = asyncContext.promiseCapability!.Promise;
|
|
} else {
|
|
return;
|
|
}
|
|
} else if (!(reaction.Capability instanceof UndefinedValue)) {
|
|
if ('PromiseState' in reaction.Capability.Promise) {
|
|
promise = reaction.Capability.Promise;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getHostDefinedErrorStack(O: Value): (CallSite | CallFrame)[] | undefined {
|
|
if (O instanceof ObjectValue && 'HostDefinedErrorStack' in O && isArray((O as ErrorObject).HostDefinedErrorStack)) {
|
|
return (O as ErrorObject).HostDefinedErrorStack as (CallSite | CallFrame)[];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getCurrentStack(excludeGlobalStack = true) {
|
|
const stack: CallSite[] = [];
|
|
for (let i = surroundingAgent.executionContextStack.length - (excludeGlobalStack ? 2 : 1); i >= 0; i -= 1) {
|
|
const e = surroundingAgent.executionContextStack[i];
|
|
if (e.VariableEnvironment === undefined && e.Function === Value.null) {
|
|
break;
|
|
}
|
|
const clone = e.callSite.clone();
|
|
const parent = stack[stack.length - 1];
|
|
if (parent && !parent.context.poppedForTailCall) {
|
|
parent.inheritedLastCallNode = clone.lastCallNode;
|
|
}
|
|
stack.push(clone);
|
|
if (e.callSite.isAsync()) {
|
|
i -= 1; // skip original execution context which has no useful information.
|
|
}
|
|
}
|
|
|
|
if (stack.length > 0 && stack[0].context.promiseCapability) {
|
|
captureAsyncStack(stack);
|
|
}
|
|
return stack;
|
|
}
|
|
|
|
export function captureStack() {
|
|
const stack = getCurrentStack();
|
|
|
|
let nativeStack: string | undefined;
|
|
if (surroundingAgent.hostDefinedOptions.errorStackAttachNativeStack) {
|
|
const origStackTraceLimit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = 12;
|
|
try {
|
|
nativeStack = new Error().stack;
|
|
} finally {
|
|
Error.stackTraceLimit = origStackTraceLimit;
|
|
}
|
|
}
|
|
|
|
return {
|
|
stack,
|
|
nativeStack,
|
|
};
|
|
}
|
|
|
|
export function* callSiteToErrorString(O: ErrorObject, stack: readonly CallSite[], nativeStack?: string): ValueEvaluator<JSStringValue> {
|
|
const errorString = (Q(yield* Call(surroundingAgent.intrinsic('%Error.prototype.toString%'), O)) as JSStringValue).stringValue();
|
|
const errorStack = callSiteToErrorStack(stack, nativeStack);
|
|
return Value(errorString + errorStack);
|
|
}
|
|
|
|
export function callSiteToErrorStack(stack: readonly CallSite[], nativeStack: string | undefined) {
|
|
let errorString = '';
|
|
stack.forEach((s) => {
|
|
errorString = `${errorString}\n at ${s.toString()}`;
|
|
});
|
|
if (typeof nativeStack === 'string') {
|
|
errorString = `${errorString}\n <NATIVE>\n${nativeStack.split('\n').slice(6).join('\n')}`;
|
|
}
|
|
return errorString;
|
|
}
|
|
|
|
export function callable<Class extends object>(
|
|
onCalled = (target: Class, _thisArg: unknown, args: unknown[]) => Reflect.construct(target as new (...args: unknown[]) => unknown, args),
|
|
) {
|
|
const handler: ProxyHandler<Class> = Object.freeze({
|
|
__proto__: null,
|
|
apply: onCalled,
|
|
});
|
|
return function decorator(classValue: Class, _classContext: ClassDecoratorContext<Class & (new (...args: readonly unknown[]) => unknown)>) {
|
|
return new Proxy(classValue, handler);
|
|
};
|
|
}
|
|
|
|
export type Mutable<T> = {
|
|
-readonly [P in keyof T]: T[P];
|
|
}
|
|
|
|
export const isArray: (arg: unknown) => arg is readonly unknown[] = Array.isArray;
|
|
export function unreachable(_: never): never {
|
|
throw new Error('Unreachable');
|
|
}
|
|
export function __ts_cast__<T>(_value: unknown): asserts _value is T { }
|