From ebe36c37377a24bcaeb58a18a2f7e6e88450aff3 Mon Sep 17 00:00:00 2001 From: bendtherules Date: Wed, 8 Apr 2026 13:55:13 +0530 Subject: [PATCH] build: ask262Debug --- .opencode/package-lock.json | 117 +++++++++++++++ .opencode/package.json | 2 +- engine262/scripts/transform.mts | 169 +++++++++++++++++----- engine262/scripts/verify-ask262-debug.mts | 50 +++++++ engine262/src/ask262-debug.mts | 94 ++++++++++++ engine262/src/index.mts | 1 + 6 files changed, 395 insertions(+), 38 deletions(-) create mode 100644 .opencode/package-lock.json create mode 100644 engine262/scripts/verify-ask262-debug.mts create mode 100644 engine262/src/ask262-debug.mts diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json new file mode 100644 index 0000000..899a1ea --- /dev/null +++ b/.opencode/package-lock.json @@ -0,0 +1,117 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "1.3.17" + } + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.17.tgz", + "integrity": "sha512-N5lckFtYvEu2R8K1um//MIOTHsJHniF2kHoPIWPCrxKG5Jpismt1ISGzIiU3aKI2ht/9VgcqKPC5oZFLdmpxPw==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.3.17", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.96", + "@opentui/solid": ">=0.1.96" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.17.tgz", + "integrity": "sha512-2+MGgu7wynqTBwxezR01VAGhILXlpcHDY/pF7SWB87WOgLt3kD55HjKHNj6PWxyY8n575AZolR95VUC3gtwfmA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/.opencode/package.json b/.opencode/package.json index b9c3f07..5ffa908 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -3,4 +3,4 @@ "dependencies": { "@opencode-ai/plugin": "1.3.17" } -} +} \ No newline at end of file diff --git a/engine262/scripts/transform.mts b/engine262/scripts/transform.mts index 1a10371..0c3176f 100644 --- a/engine262/scripts/transform.mts +++ b/engine262/scripts/transform.mts @@ -26,10 +26,11 @@ function getEnclosingConditionalExpression(path: NodePath) { return null; } -type NeededNames = 'Completion' | 'AbruptCompletion' | 'Assert' | 'Call' | 'IteratorClose' | 'AsyncIteratorClose' | 'Value' | 'skipDebugger'; +type NeededNames = 'Completion' | 'AbruptCompletion' | 'Assert' | 'Call' | 'IteratorClose' | 'AsyncIteratorClose' | 'Value' | 'skipDebugger' | 'ask262Debug'; interface State extends PluginPass { needed: Partial>; + fileRelativePath?: string; } interface Macro> { @@ -98,38 +99,72 @@ export default ({ types: t, template }: typeof import('@babel/core')): PluginObj `); } - function addSectionFromComments(path: NodePath | NodePath | NodePath) { - if (path.node.leadingComments) { - for (const c of path.node.leadingComments) { - let name: string; - switch (path.type) { - case 'FunctionDeclaration': - name = path.node.id!.name; - break; - case 'ExportNamedDeclaration': - name = (path.node.declaration as t.FunctionDeclaration).id!.name; - break; - case 'VariableDeclaration': - name = (path.node.declarations[0].id as t.Identifier).name; - break; - default: - throw (path as NodePath).buildCodeFrameError('Internal error: Unsupported path to addSectionFromComments'); - } - const lines = c.value.split('\n'); - for (const line of lines) { - if (/#sec/.test(line)) { - const section = line.split(' ').find((l) => l.includes('#sec'))!; - const url = section.includes('https') ? section : `https://tc39.es/ecma262/${section}`; - const result = path.insertAfter(withSource(c, template.ast(`${name}.section = '${url}';`))); - if (path.node.trailingComments) { - result[result.length - 1].node.trailingComments = path.node.trailingComments; - path.node.trailingComments = null; + function createImportAsk262Debug() { + return template.ast(` + import { ask262Debug } from "#self"; + `); + } + + function addSectionFromComments( + path: NodePath | NodePath | NodePath | NodePath | NodePath, + state: State, + getName: () => string, + getBody: () => t.BlockStatement | null, + insertSection: boolean, + ) { + if (!path.node.leadingComments) return; + + const sectionIds: string[] = []; + let url = ''; + let firstComment: t.Comment | null = null; + + for (const c of path.node.leadingComments) { + for (const line of c.value.split('\n')) { + const matches = line.match(/#(sec-[a-zA-Z0-9._%-]+)/g); + if (matches) { + sectionIds.push(...matches.map((m) => m.substring(1))); + if (!firstComment) { + firstComment = c; + } + if (!url) { + const section = line.split(' ').find((l) => l.includes('#sec')); + // Only capture external URLs (skip local/fragment references) + if (section?.startsWith('https://')) { + url = section; } - return; } } } } + + if (sectionIds.length === 0) return; + + const name = getName(); + + // 1. Keep existing .section (backward compat) - uses first URL + // Only applies when caller explicitly allows it + if (name && url && firstComment && insertSection) { + const result = path.insertAfter(withSource(firstComment, template.ast(`${name}.section = '${url}';`))); + if (path.node.trailingComments) { + result[result.length - 1].node.trailingComments = path.node.trailingComments; + path.node.trailingComments = null; + } + } + + // 2. Get function body via callback (node-type check done by caller) + const body = getBody(); + + // 3. Inject mark() at start of function body (only for block bodies) + if (body?.type === 'BlockStatement') { + const line = path.node.loc?.start.line ?? 0; + const sectionIdsStr = JSON.stringify(sectionIds); + const filePathStr = JSON.stringify(state.fileRelativePath); + const markCall = template.statement( + `ask262Debug.mark(${sectionIdsStr}, ${filePathStr}, ${line});`, + )(); + body.body.unshift(markCall); + state.needed.ask262Debug = true; + } } @@ -254,8 +289,11 @@ export default ({ types: t, template }: typeof import('@babel/core')): PluginObj return { visitor: { Program: { - enter(_path, state) { + enter(path, state) { state.needed = {}; + // Capture relative file path from state.filename (relative to src/) + const absolutePath = state.filename || ''; + state.fileRelativePath = absolutePath.replace(/.*\/src\//, '') || 'unknown.mts'; }, exit(path, state) { if (state.needed.skipDebugger) { @@ -282,6 +320,9 @@ export default ({ types: t, template }: typeof import('@babel/core')): PluginObj if (state.needed.Value) { path.unshiftContainer('body', createImportValue()); } + if (state.needed.ask262Debug) { + path.unshiftContainer('body', createImportAsk262Debug()); + } }, }, CallExpression(path, state) { @@ -438,17 +479,71 @@ export default ({ types: t, template }: typeof import('@babel/core')): PluginObj } } }, - FunctionDeclaration(path) { - addSectionFromComments(path); + FunctionDeclaration(path, state) { + addSectionFromComments( + path, + state, + () => path.node.id!.name, + () => path.node.body, + true, + ); }, - VariableDeclaration(path) { - if (path.get('declarations.0.init').isArrowFunctionExpression() || path.get('declarations.0.init').isFunctionExpression()) { - addSectionFromComments(path); + VariableDeclaration(path, state) { + const init = path.get('declarations.0.init'); + if (init.isFunctionExpression()) { + const id = path.node.declarations[0].id as t.Identifier; + addSectionFromComments( + path, + state, + () => id.name, + () => init.node.body, + true, + ); + } else if (init.isArrowFunctionExpression()) { + const id = path.node.declarations[0].id as t.Identifier; + addSectionFromComments( + path, + state, + () => id.name, + () => (init.node.body.type === 'BlockStatement' ? init.node.body : null), + true, + ); } }, - ExportNamedDeclaration(path) { - if (path.get('declaration').isFunctionDeclaration()) { - addSectionFromComments(path); + ExportNamedDeclaration(path, state) { + const declaration = path.node.declaration; + if (declaration?.type === 'FunctionDeclaration') { + addSectionFromComments( + path, + state, + () => declaration.id!.name, + () => declaration.body, + true, + ); + } + }, + ClassMethod(path, state) { + const key = path.node.key; + if (key.type === 'Identifier') { + addSectionFromComments( + path, + state, + () => key.name, + () => path.node.body, + false, + ); + } + }, + ObjectMethod(path, state) { + const key = path.node.key; + if (key.type === 'Identifier') { + addSectionFromComments( + path, + state, + () => key.name, + () => path.node.body, + false, + ); } }, }, diff --git a/engine262/scripts/verify-ask262-debug.mts b/engine262/scripts/verify-ask262-debug.mts new file mode 100644 index 0000000..e2f23c4 --- /dev/null +++ b/engine262/scripts/verify-ask262-debug.mts @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/** + * Verification script for ask262Debug module. + * Tests that the module is properly exported and captures spec section marks. + */ +import { + ask262Debug, Agent, ManagedRealm, setSurroundingAgent, +} from '#self'; + +console.log('=== ask262Debug Verification ===\n'); + +// 1. Check export exists +console.log('1. Checking export...'); +if (!ask262Debug) { + console.error('FAIL: ask262Debug not exported'); + process.exit(1); +} +console.log(' ✓ ask262Debug exported\n'); + +// 2. Run code that hits known spec sections +console.log('2. Executing test code...'); +const agent = new Agent(); +setSurroundingAgent(agent); +const realm = new ManagedRealm(); + +realm.evaluateScript(` + // Array.prototype.every - should hit sec-array.prototype.every + [1, 2, 3].every(x => x > 0); + + // Proxy creation - should hit sec-proxycreate or similar + new Proxy({}, {}); +`); + +// 3. Verify marks were captured +console.log('3. Checking marks...'); +const marks = ask262Debug.marks; +console.log(` Captured ${marks.length} unique marks\n`); + +if (marks.length === 0) { + console.error('FAIL: No marks captured'); + process.exit(1); +} + +// 4. Show sample marks +console.log('4. Sample marks:'); +marks.slice(0, 5).forEach((m, i) => { + console.log(` [${i}] ${m.sectionIds.join(', ')} @ ${m.fileRelativePath}:${m.lineNumber}${m.important ? ' [important]' : ''}`); +}); + +console.log('=== All Checks Passed ==='); diff --git a/engine262/src/ask262-debug.mts b/engine262/src/ask262-debug.mts new file mode 100644 index 0000000..a5ecc3b --- /dev/null +++ b/engine262/src/ask262-debug.mts @@ -0,0 +1,94 @@ +/** + * Runtime execution tracker for ECMAScript spec section analysis. + * Records which spec sections are entered during execution with deduplication. + * Used for AI analysis of execution flow mapped to ECMAScript spec sections. + */ + +/** + * Represents a mark entry captured during execution. + */ +export interface MarkData { + /** Array of ECMAScript spec section IDs (e.g., ["sec-array.prototype.every"]) */ + readonly sectionIds: string[]; + /** Relative file path from engine262/src/ directory */ + readonly fileRelativePath: string; + /** Line number in source file (1-indexed) */ + readonly lineNumber: number; + /** Whether this mark was captured during an "important" execution phase */ + readonly important: boolean; +} + +/** + * Runtime execution tracker that deduplicates marks by (sectionIds, file, line). + * Provides chronological trace of spec section entry points during execution. + */ +class Ask262Debug { + /** Collected mark entries with deduplication */ + marks: MarkData[] = []; + + /** Whether to mark new entries as important */ + private _important = false; + + /** Map from deduplication key to index in marks array */ + private _markIndex = new Map(); + + /** + * Creates a deduplication key from section IDs, file path, and line number. + * @param sectionIds - Array of spec section IDs + * @param file - Relative file path + * @param line - Line number + * @returns String key for deduplication + */ + private _makeKey(sectionIds: string[], file: string, line: number): string { + return `${sectionIds.join(',')}|${file}|${line}`; + } + + /** + * Records a mark for spec section entry during execution. + * Deduplicates based on (sectionIds, file, line) combination. + * If duplicate found, merges important flags (OR logic). + * @param sectionIds - Array of ECMAScript spec section IDs + * @param file - Relative file path from engine262/src/ + * @param line - Line number in source file (1-indexed) + */ + mark(sectionIds: string[], file: string, line: number) { + const key = this._makeKey(sectionIds, file, line); + const existingIndex = this._markIndex.get(key); + + if (existingIndex !== undefined) { + // Merge important flag using OR logic + const existing = this.marks[existingIndex]; + if (this._important && !existing.important) { + (this.marks[existingIndex] as MarkData & { important: boolean }).important = true; + } + return; + } + + const newMark: MarkData = { + sectionIds: [...sectionIds], // defensive copy + fileRelativePath: file, + lineNumber: line, + important: this._important, + }; + + this._markIndex.set(key, this.marks.length); + this.marks.push(newMark); + } + + /** + * Marks subsequent mark() calls as important. + * Used to annotate interesting execution phases. + */ + startImportant() { + this._important = true; + } + + /** + * Stops marking subsequent mark() calls as important. + */ + stopImportant() { + this._important = false; + } +} + +export const ask262Debug = new Ask262Debug(); diff --git a/engine262/src/index.mts b/engine262/src/index.mts index ea9d9b2..339ef4b 100644 --- a/engine262/src/index.mts +++ b/engine262/src/index.mts @@ -12,6 +12,7 @@ export { type ErrorType, type Formattable, Throw } from './host-defined/error-me export * from './evaluator.mts'; export { captureStack } from './helpers.mts'; +export { ask262Debug, type MarkData } from './ask262-debug.mts'; export { gc, runJobQueue, type ManagedRealmHostDefined, ManagedRealm, } from './api.mts';