mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-19 00:31:06 +00:00
478 lines
16 KiB
TypeScript
478 lines
16 KiB
TypeScript
import { ObjectValue, Value, type PropertyKeyValue } from './value.mts';
|
|
import {
|
|
surroundingAgent,
|
|
ScriptEvaluation,
|
|
type Markable,
|
|
} from './host-defined/engine.mts';
|
|
import { HostEnqueueFinalizationRegistryCleanupJob } from './execution-context/WeakReference.mts';
|
|
import { AgentSignifier } from './execution-context/Agent.mts';
|
|
import { ExecutionContext } from './execution-context/ExecutionContext.mts';
|
|
import {
|
|
X,
|
|
ThrowCompletion,
|
|
AbruptCompletion,
|
|
type PlainCompletion,
|
|
type ValueCompletion,
|
|
NormalCompletion,
|
|
Q,
|
|
} from './completion.mts';
|
|
import {
|
|
ParseScript,
|
|
ParseModule,
|
|
ParseJSONModule,
|
|
ScriptRecord,
|
|
type ParseScriptHostDefined,
|
|
} from './parse.mts';
|
|
import {
|
|
AbstractModuleRecord, ModuleRecord, SourceTextModuleRecord, type ModuleRecordHostDefined, type ModuleRecordHostDefinedPublic,
|
|
} from './modules.mts';
|
|
import { isWeakRef, type WeakRefObject } from './intrinsics/WeakRef.mts';
|
|
import { isFinalizationRegistryObject, type FinalizationRegistryObject } from './intrinsics/FinalizationRegistry.mts';
|
|
import { isWeakMapObject, type WeakMapObject } from './intrinsics/WeakMap.mts';
|
|
import { isWeakSetObject, type WeakSetObject } from './intrinsics/WeakSet.mts';
|
|
import type { PromiseObject } from './intrinsics/Promise.mts';
|
|
import type { ParseNode } from './parser/ParseNode.mts';
|
|
import {
|
|
ClearKeptObjects,
|
|
CreateIntrinsics,
|
|
SetDefaultGlobalBindings,
|
|
OrdinaryObjectCreate,
|
|
Assert,
|
|
} from '#self';
|
|
import {
|
|
Realm,
|
|
EnsureCompletion, GetModuleNamespace, GlobalEnvironmentRecord, type Intrinsics,
|
|
type ValueEvaluator,
|
|
} from '#self';
|
|
|
|
/** https://tc39.es/ecma262/#sec-weakref-execution */
|
|
export function gc() {
|
|
// At any time, if a set of objects S is not live, an ECMAScript implementation may perform the following steps atomically:
|
|
// 1. For each obj of S, do
|
|
// a. For each WeakRef ref such that ref.[[WeakRefTarget]] is obj,
|
|
// i. Set ref.[[WeakRefTarget]] to empty.
|
|
// b. For each FinalizationRegistry fg such that fg.[[Cells]] contains cell, and cell.[[WeakRefTarget]] is obj,
|
|
// i. Set cell.[[WeakRefTarget]] to empty.
|
|
// ii. Optionally, perform ! HostEnqueueFinalizationRegistryCleanupJob(fg).
|
|
// c. For each WeakMap map such that map.WeakMapData contains a record r such that r.Key is obj,
|
|
// i. Set r.[[Key]] to empty.
|
|
// ii. Set r.[[Value]] to empty.
|
|
// d. For each WeakSet set such that set.[[WeakSetData]] contains obj,
|
|
// i. Replace the element of set whose value is obj with an element whose value is empty.
|
|
|
|
const marked = new Set<unknown>();
|
|
const weakrefs = new Set<WeakRefObject>();
|
|
const fgs = new Set<FinalizationRegistryObject>();
|
|
const weakmaps = new Set<WeakMapObject>();
|
|
const weaksets = new Set<WeakSetObject>();
|
|
const ephemeronQueue: WeakMapObject['WeakMapData'][number][] = [];
|
|
|
|
const markCb = (O: unknown) => {
|
|
if (typeof O !== 'object' || O === null) {
|
|
return;
|
|
}
|
|
|
|
if (marked.has(O)) {
|
|
return;
|
|
}
|
|
marked.add(O);
|
|
|
|
if (isWeakRef(O)) {
|
|
weakrefs.add(O);
|
|
markCb(O.properties);
|
|
markCb(O.Prototype);
|
|
} else if (isFinalizationRegistryObject(O)) {
|
|
fgs.add(O);
|
|
markCb(O.properties);
|
|
markCb(O.Prototype);
|
|
O.Cells.forEach((cell) => {
|
|
markCb(cell.HeldValue);
|
|
});
|
|
} else if (isWeakMapObject(O)) {
|
|
weakmaps.add(O);
|
|
markCb(O.properties);
|
|
markCb(O.Prototype);
|
|
O.WeakMapData.forEach((r) => {
|
|
ephemeronQueue.push(r);
|
|
});
|
|
} else if (isWeakSetObject(O)) {
|
|
weaksets.add(O);
|
|
markCb(O.properties);
|
|
markCb(O.Prototype);
|
|
} else if ('mark' in O) {
|
|
(O as Markable).mark(markCb);
|
|
}
|
|
};
|
|
|
|
markCb(surroundingAgent);
|
|
|
|
while (ephemeronQueue.length > 0) {
|
|
const item = ephemeronQueue.shift()!;
|
|
if (marked.has(item.Key)) {
|
|
markCb(item.Value);
|
|
}
|
|
}
|
|
|
|
weakrefs.forEach((ref) => {
|
|
if (!marked.has(ref.WeakRefTarget)) {
|
|
ref.WeakRefTarget = undefined;
|
|
}
|
|
});
|
|
|
|
fgs.forEach((fg) => {
|
|
let dirty = false;
|
|
fg.Cells.forEach((cell) => {
|
|
if (!marked.has(cell.WeakRefTarget)) {
|
|
cell.WeakRefTarget = undefined;
|
|
dirty = true;
|
|
}
|
|
});
|
|
if (dirty) {
|
|
X(HostEnqueueFinalizationRegistryCleanupJob(fg));
|
|
}
|
|
});
|
|
|
|
weakmaps.forEach((map) => {
|
|
map.WeakMapData.forEach((r) => {
|
|
if (!marked.has(r.Key)) {
|
|
r.Key = undefined;
|
|
r.Value = undefined;
|
|
}
|
|
});
|
|
});
|
|
|
|
weaksets.forEach((set) => {
|
|
set.WeakSetData.forEach((obj, i) => {
|
|
if (!marked.has(obj)) {
|
|
set.WeakSetData[i] = undefined;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/** https://tc39.es/ecma262/#sec-jobs */
|
|
export function runJobQueue() {
|
|
if (surroundingAgent.executionContextStack.some((e) => e.ScriptOrModule !== Value.null)) {
|
|
return;
|
|
}
|
|
|
|
// At some future point in time, when there is no running execution context
|
|
// and the execution context stack is empty, the implementation must:
|
|
while (surroundingAgent.jobQueue.length > 0) { // eslint-disable-line no-constant-condition
|
|
const {
|
|
job: abstractClosure,
|
|
callerRealm,
|
|
callerScriptOrModule,
|
|
} = surroundingAgent.jobQueue.shift()!;
|
|
|
|
// 1. Perform any implementation-defined preparation steps.
|
|
const newContext = new ExecutionContext();
|
|
surroundingAgent.executionContextStack.push(newContext);
|
|
newContext.Function = Value.null;
|
|
newContext.Realm = callerRealm;
|
|
newContext.ScriptOrModule = callerScriptOrModule;
|
|
// 2. Call the abstract closure.
|
|
X(abstractClosure());
|
|
// 3. Perform any host-defined cleanup steps, after which the execution context stack must be empty.
|
|
ClearKeptObjects();
|
|
gc();
|
|
surroundingAgent.executionContextStack.pop(newContext);
|
|
}
|
|
}
|
|
|
|
export interface ManagedRealmHostDefined {
|
|
promiseRejectionTracker?(promise: PromiseObject, operation: 'reject' | 'handle'): void;
|
|
getImportMetaProperties?(module: ModuleRecordHostDefinedPublic): readonly { readonly Key: PropertyKeyValue, readonly Value: Value }[];
|
|
finalizeImportMeta?(meta: ObjectValue, module: ModuleRecordHostDefinedPublic): PlainCompletion<void>;
|
|
resolverCache?: Map<string, AbstractModuleRecord>;
|
|
|
|
randomSeed?(): string;
|
|
attachingInspector?: unknown;
|
|
attachingInspectorReportError?(realm: Realm, error: Value): void;
|
|
/**
|
|
* See https://tc39.es/ecma262/#sec-HostLoadImportedModule
|
|
* In case of
|
|
* <button type="button" onclick="import('./foo.mjs')">Click me</button>
|
|
* and
|
|
* new ShadowRealm().importValue('./foo.mjs', 'default')
|
|
* a Realm instead of a ModuleRecord or ScriptRecord is passed as the referrer.
|
|
*/
|
|
specifier?: string | undefined;
|
|
/** The name displayed in the inspector. */
|
|
name?: string | undefined;
|
|
}
|
|
export class ManagedRealm extends Realm {
|
|
override TemplateMap: { Site: ParseNode.TemplateLiteral; Array: ObjectValue; }[];
|
|
|
|
override AgentSignifier: unknown;
|
|
|
|
override Intrinsics: Intrinsics;
|
|
|
|
override randomState: BigUint64Array<ArrayBufferLike> | undefined;
|
|
|
|
override GlobalObject: ObjectValue;
|
|
|
|
override GlobalEnv: GlobalEnvironmentRecord;
|
|
|
|
override HostDefined: ManagedRealmHostDefined;
|
|
|
|
topContext: ExecutionContext;
|
|
|
|
active = false;
|
|
|
|
/** https://tc39.es/ecma262/#sec-initializehostdefinedrealm */
|
|
constructor(HostDefined: ManagedRealmHostDefined = {}, customizations?: (record: Realm) => [global: ObjectValue | undefined, thisValue: ObjectValue | undefined]) {
|
|
super();
|
|
this.Intrinsics = CreateIntrinsics(this);
|
|
this.AgentSignifier = AgentSignifier();
|
|
this.TemplateMap = [];
|
|
let [global, thisValue] = customizations?.(this) || [];
|
|
if (!global) {
|
|
global = OrdinaryObjectCreate(this.Intrinsics['%Object.prototype%']);
|
|
} else {
|
|
Assert(global instanceof ObjectValue);
|
|
}
|
|
if (!thisValue) {
|
|
thisValue = global;
|
|
} else {
|
|
Assert(thisValue instanceof ObjectValue);
|
|
}
|
|
this.GlobalObject = global;
|
|
this.GlobalEnv = new GlobalEnvironmentRecord(global, thisValue);
|
|
SetDefaultGlobalBindings(this);
|
|
const newContext = new ExecutionContext();
|
|
newContext.Function = Value.null;
|
|
newContext.Realm = this;
|
|
newContext.ScriptOrModule = Value.null;
|
|
this.HostDefined = HostDefined;
|
|
this.topContext = newContext;
|
|
|
|
surroundingAgent.hostDefinedOptions.onRealmCreated?.(this);
|
|
}
|
|
|
|
scope(inspectorPreview?: boolean): Disposable | null;
|
|
|
|
scope<T>(cb: () => T, inspectorPreview?: boolean): T
|
|
|
|
scope<T>(arg0?: (() => T) | boolean, arg2?: boolean): T | Disposable | null {
|
|
if (typeof arg0 !== 'function') {
|
|
const inspectorPreview = arg0;
|
|
if (this.active) {
|
|
return null;
|
|
}
|
|
this.active = true;
|
|
surroundingAgent.executionContextStack.push(this.topContext);
|
|
using _ = inspectorPreview ? surroundingAgent.debugger_scopePreview() : null;
|
|
return {
|
|
[Symbol.dispose]: () => {
|
|
surroundingAgent.executionContextStack.pop(this.topContext);
|
|
this.active = false;
|
|
},
|
|
};
|
|
} else {
|
|
const callback = arg0;
|
|
if (this.active) {
|
|
return arg0();
|
|
}
|
|
this.active = true;
|
|
surroundingAgent.executionContextStack.push(this.topContext);
|
|
const result = arg2 ? surroundingAgent.debugger_scopePreview(callback) : callback();
|
|
surroundingAgent.executionContextStack.pop(this.topContext);
|
|
this.active = false;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
compileScript(sourceText: string, hostDefined?: ParseScriptHostDefined): PlainCompletion<ScriptRecord> {
|
|
return this.scope(() => {
|
|
const s = ParseScript(sourceText, this, hostDefined);
|
|
if (Array.isArray(s)) {
|
|
return ThrowCompletion(s[0]);
|
|
}
|
|
return NormalCompletion(s);
|
|
});
|
|
}
|
|
|
|
compileModule(sourceText: string, hostDefined?: ModuleRecordHostDefined) {
|
|
return this.scope(() => {
|
|
const s = ParseModule(sourceText, this, {
|
|
SourceTextModuleRecord: ManagedSourceTextModuleRecord,
|
|
...hostDefined,
|
|
});
|
|
if (Array.isArray(s)) {
|
|
return ThrowCompletion(s[0]);
|
|
}
|
|
return NormalCompletion(s);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Call surroundingAgent.resumeEvaluate() to continue evaluation.
|
|
*
|
|
* This function will synchronously return a completion if this is a nested evaluation and debugger cannot be triggered.
|
|
*/
|
|
evaluate(sourceText: ScriptRecord | ModuleRecord | ValueEvaluator, callback: (completion: NormalCompletion<Value> | ThrowCompletion) => void) {
|
|
if (!sourceText) {
|
|
throw new TypeError('sourceText is null or undefined');
|
|
}
|
|
let result: ValueCompletion | undefined;
|
|
|
|
if (sourceText instanceof ModuleRecord) {
|
|
const old = this.active;
|
|
this.active = true;
|
|
surroundingAgent.executionContextStack.push(this.topContext);
|
|
|
|
const loadModuleCompletion = sourceText.LoadRequestedModules();
|
|
const link = ((): PlainCompletion<void> => {
|
|
if (loadModuleCompletion.PromiseState === 'rejected') {
|
|
Q(ThrowCompletion(loadModuleCompletion.PromiseResult!));
|
|
} else if (loadModuleCompletion.PromiseState === 'pending') {
|
|
throw new Error('Internal error: .LoadRequestedModules() returned a pending promise');
|
|
}
|
|
Q(sourceText.Link());
|
|
})();
|
|
if (link instanceof ThrowCompletion) {
|
|
callback(link);
|
|
return link;
|
|
}
|
|
surroundingAgent.evaluate(sourceText.Evaluate(), (completion) => {
|
|
if (completion instanceof NormalCompletion && completion.Value.PromiseState === 'fulfilled') {
|
|
result = GetModuleNamespace(sourceText, 'evaluation');
|
|
} else {
|
|
result = completion;
|
|
}
|
|
this.active = old;
|
|
surroundingAgent.executionContextStack.pop(this.topContext);
|
|
callback(EnsureCompletion(result));
|
|
});
|
|
return result;
|
|
} else if (sourceText instanceof ScriptRecord) {
|
|
const old = this.active;
|
|
this.active = true;
|
|
surroundingAgent.executionContextStack.push(this.topContext);
|
|
|
|
surroundingAgent.evaluate(ScriptEvaluation(sourceText), (completion) => {
|
|
this.active = old;
|
|
surroundingAgent.executionContextStack.pop(this.topContext);
|
|
result = completion;
|
|
callback(completion);
|
|
});
|
|
return result;
|
|
} else {
|
|
// this path only called by the inspector
|
|
Assert(!!surroundingAgent.hostDefinedOptions.onDebugger);
|
|
let emptyExecutionStack = false;
|
|
if (!surroundingAgent.runningExecutionContext) {
|
|
emptyExecutionStack = true;
|
|
this.active = true;
|
|
surroundingAgent.executionContextStack.push(this.topContext);
|
|
}
|
|
surroundingAgent.evaluate(sourceText, (completion) => {
|
|
result = completion;
|
|
if (emptyExecutionStack) {
|
|
this.active = false;
|
|
surroundingAgent.executionContextStack.pop(this.topContext);
|
|
}
|
|
callback(completion);
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
|
|
evaluateScript(sourceText: string | ScriptRecord, { specifier, doNotTrackScriptId }: { specifier?: string, doNotTrackScriptId?: boolean } = {}): ValueCompletion {
|
|
if (sourceText === undefined || sourceText === null) {
|
|
throw new TypeError('sourceText must be a string or a ScriptRecord');
|
|
}
|
|
if (typeof sourceText === 'string') {
|
|
sourceText = Q(this.compileScript(sourceText, { specifier, doNotTrackScriptId }));
|
|
}
|
|
|
|
let completion;
|
|
completion = this.evaluate(sourceText, (c) => {
|
|
completion = c;
|
|
});
|
|
if (!completion) {
|
|
surroundingAgent.resumeEvaluate({
|
|
noBreakpoint: true,
|
|
});
|
|
}
|
|
if (!completion) {
|
|
throw new Assert.Error('Expect evaluation completes synchronously');
|
|
}
|
|
if (!(completion instanceof AbruptCompletion)) {
|
|
runJobQueue();
|
|
}
|
|
|
|
return completion;
|
|
}
|
|
|
|
evaluateModule(sourceText: string, specifier: string): PlainCompletion<SourceTextModuleRecord>
|
|
|
|
evaluateModule<T extends ModuleRecord>(sourceText: T, specifier: string): PlainCompletion<T>
|
|
|
|
evaluateModule(sourceText: string | ModuleRecord, specifier: string): PlainCompletion<ModuleRecord> {
|
|
if (sourceText === undefined || sourceText === null) {
|
|
throw new TypeError('sourceText must be a string or a ModuleRecord');
|
|
}
|
|
if (typeof sourceText === 'string') {
|
|
sourceText = Q(this.compileModule(sourceText, { specifier }));
|
|
}
|
|
|
|
let completion;
|
|
completion = this.evaluate(sourceText, (c) => {
|
|
completion = c;
|
|
if (!(completion instanceof AbruptCompletion)) {
|
|
runJobQueue();
|
|
}
|
|
});
|
|
if (!completion) {
|
|
surroundingAgent.resumeEvaluate({
|
|
noBreakpoint: true,
|
|
});
|
|
}
|
|
|
|
return sourceText;
|
|
}
|
|
|
|
/**
|
|
* @deprecated use compileModule
|
|
*/
|
|
createSourceTextModule(specifier: string, sourceText: string): PlainCompletion<SourceTextModuleRecord> {
|
|
if (typeof specifier !== 'string') {
|
|
throw new TypeError('specifier must be a string');
|
|
}
|
|
if (typeof sourceText !== 'string') {
|
|
throw new TypeError('sourceText must be a string');
|
|
}
|
|
const module = this.scope(() => ParseModule(sourceText, this, {
|
|
specifier,
|
|
SourceTextModuleRecord: ManagedSourceTextModuleRecord,
|
|
}));
|
|
if (Array.isArray(module)) {
|
|
return ThrowCompletion(module[0]);
|
|
}
|
|
return module;
|
|
}
|
|
|
|
createJSONModule(specifier: string, sourceText: string) {
|
|
if (typeof specifier !== 'string') {
|
|
throw new TypeError('specifier must be a string');
|
|
}
|
|
if (typeof sourceText !== 'string') {
|
|
throw new TypeError('sourceText must be a string');
|
|
}
|
|
const module = this.scope(() => ParseJSONModule(Value(sourceText), this, {
|
|
specifier,
|
|
}));
|
|
return module;
|
|
}
|
|
}
|
|
|
|
class ManagedSourceTextModuleRecord extends SourceTextModuleRecord {
|
|
override* Evaluate() {
|
|
const r = yield* super.Evaluate();
|
|
runJobQueue();
|
|
return r;
|
|
}
|
|
}
|