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 implements Map { #map = new Map(); 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) => void, thisArg?: JSStringMap) { 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 implements Map { #map = new Map(); 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) => void, thisArg?: PropertyKeyMap) { 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(); constructor(value?: Iterable) { 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) => 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; * values() { for (const value of this.#set) { yield Value(value); } return undefined; } declare [Symbol.iterator]: () => SetIterator; 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(iterator: Evaluator, 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 += ''; } 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 += ''; } 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() || '', scriptId: source, url: this.getSpecifier() || '', }; } } 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 || '', scriptId: this.scriptId, url: this.url || '', }; } } 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 { 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 \n${nativeStack.split('\n').slice(6).join('\n')}`; } return errorString; } export function callable( onCalled = (target: Class, _thisArg: unknown, args: unknown[]) => Reflect.construct(target as new (...args: unknown[]) => unknown, args), ) { const handler: ProxyHandler = Object.freeze({ __proto__: null, apply: onCalled, }); return function decorator(classValue: Class, _classContext: ClassDecoratorContext unknown)>) { return new Proxy(classValue, handler); }; } export type Mutable = { -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__(_value: unknown): asserts _value is T { }