mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
build: ask262Debug
This commit is contained in:
+132
-37
@@ -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<Record<NeededNames, boolean>>;
|
||||
fileRelativePath?: string;
|
||||
}
|
||||
|
||||
interface Macro<R extends PublicReplacements = Record<string, Node | null>> {
|
||||
@@ -98,38 +99,72 @@ export default ({ types: t, template }: typeof import('@babel/core')): PluginObj
|
||||
`);
|
||||
}
|
||||
|
||||
function addSectionFromComments(path: NodePath<t.FunctionDeclaration> | NodePath<t.VariableDeclaration> | NodePath<t.ExportNamedDeclaration>) {
|
||||
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<t.FunctionDeclaration> | NodePath<t.VariableDeclaration> | NodePath<t.ExportNamedDeclaration> | NodePath<t.ClassMethod> | NodePath<t.ObjectMethod>,
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 ===');
|
||||
@@ -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<string, number>();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user