Files
ask262/.opencode/plans/1775540058831-quick-planet.md
T

14 KiB

model
model
accounts/fireworks/routers/kimi-k2p5-turbo

Plan: ask262Debug Module for engine262

Overview

Runtime execution tracker that records which ECMAScript spec sections are entered during execution. The Babel transform auto-injects ask262Debug.mark() at the start of every function body with URL comments — developer writes nothing. All metadata (sectionId, file, line) resolved at build time.

Primary Use Case: AI analysis of execution flow mapped to ECMAScript spec sections for understanding JavaScript engine behavior.

Files to Change

File Action
engine262/src/ask262-debug.mts CREATE — the module with deduplication logic
engine262/src/index.mts MODIFY — add named re-export
engine262/tsconfig.base.json MODIFY — add #self/ask262Debug path mapping
engine262/package.json MODIFY — add #self/ask262Debug import
engine262/scripts/rollup.config.mts MODIFY — add resolve-self plugin to main build, extend for subpaths
engine262/scripts/transform.mts MODIFY — auto-inject mark() and import
engine262/scripts/verify-ask262-debug.mts CREATE — post-build verification script

1. scripts/transform.mts — Enhance the Babel Plugin

1a. Extend State Interface

type NeededNames = 'Completion' | 'AbruptCompletion' | 'Assert' | 'Call' | 
                   'IteratorClose' | 'AsyncIteratorClose' | 'Value' | 
                   'skipDebugger' | 'ask262Debug';  // NEW

interface State extends PluginPass {
  needed: Partial<Record<NeededNames, boolean>>;
  fileRelativePath?: string;  // NEW: captured at Program enter
}

1b. Refactor addSectionFromComments

function addSectionFromComments(
  path: NodePath<t.FunctionDeclaration> | NodePath<t.VariableDeclaration> | 
        NodePath<t.ExportNamedDeclaration> | NodePath<t.ClassMethod> | NodePath<t.ObjectMethod>,
  state: State,
  getName: () => string
) {
  if (!path.node.leadingComments) return;

  const sectionIds: string[] = [];
  let url = '';
  
  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 (!url) {
          const section = line.split(' ').find(l => l.includes('#sec'));
          if (section) {
            url = section.includes('https') ? section : `https://tc39.es/ecma262/${section}`;
          }
        }
      }
    }
  }
  
  if (sectionIds.length === 0) return;

  const name = getName();
  
  // 1. Keep existing .section (backward compat) - uses first URL
  if (url) {
    path.insertAfter(template.ast(`${name}.section = '${url}';`));
  }

  // 2. Determine if we can inject mark()
  let body: t.BlockStatement | null = null;
  
  if (path.isFunctionDeclaration()) {
    body = path.node.body;
  } else if (path.isClassMethod() || path.isObjectMethod()) {
    body = path.node.body;
  } else if (path.isVariableDeclaration()) {
    const init = (path.get('declarations.0') as NodePath<t.VariableDeclarator>).get('init');
    if (init.isFunctionExpression()) {
      body = init.node.body;
    } else if (init.isArrowFunctionExpression()) {
      body = init.node.body.type === 'BlockStatement' ? init.node.body : null;
    }
  } else if (path.isExportNamedDeclaration()) {
    const declaration = path.node.declaration;
    if (declaration?.type === 'FunctionDeclaration') {
      body = declaration.body;
    }
  }

  // 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 markCall = template.statement(
      `ask262Debug.mark(${JSON.stringify(sectionIds)}, ${JSON.stringify(state.fileRelativePath)}, ${line});`
    )();
    body.body.unshift(markCall);
    state.needed.ask262Debug = true;
  }
}

1c. Add Visitors for All Function-like Nodes

return {
  visitor: {
    Program: {
      enter(path, state) {
        state.needed = {};
        // Capture relative file path from state.filename
        const absolutePath = state.filename || '';
        state.fileRelativePath = absolutePath.replace(/.*\/(src\/)/, '$1').replace(/\.mts$/, '.mts') || 'unknown.mts';
      },
      exit(path, state) {
        // Existing imports...
        if (state.needed.ask262Debug) {
          path.unshiftContainer('body', template.ast(`import ask262Debug from '#self/ask262Debug';`));
        }
      },
    },
    FunctionDeclaration(path, state) {
      addSectionFromComments(path, state, () => path.node.id!.name);
    },
    VariableDeclaration(path, state) {
      const init = path.get('declarations.0.init');
      if (init.isFunctionExpression() || init.isArrowFunctionExpression()) {
        const id = path.node.declarations[0].id as t.Identifier;
        addSectionFromComments(path, state, () => id.name);
      }
    },
    ExportNamedDeclaration(path, state) {
      const declaration = path.node.declaration;
      if (declaration?.type === 'FunctionDeclaration') {
        addSectionFromComments(path, state, () => declaration.id!.name);
      }
    },
    ClassMethod(path, state) {
      const key = path.node.key;
      if (key.type === 'Identifier') {
        addSectionFromComments(path, state, () => key.name);
      }
    },
    ObjectMethod(path, state) {
      const key = path.node.key;
      if (key.type === 'Identifier') {
        addSectionFromComments(path, state, () => key.name);
      }
    },
    // ... rest of existing visitors (CallExpression, ThrowStatement, etc.) unchanged
  },
};

1d. Arrow Function Special Handling

The updated addSectionFromComments handles this automatically:

  • Arrow with block body () => { ... }: .section + mark() injected
  • Arrow with expression body () => expr: .section only, no mark()

2. engine262/package.json — Add Subpath Import

{
  "imports": {
    "#self": "./lib/engine262.mjs",
    "#self/ask262Debug": "./src/ask262-debug.mts"
  }
}

3. engine262/tsconfig.base.json — Add Path Mapping

{
  "compilerOptions": {
    "paths": {
      "#self": ["./lib/engine262.mjs"],
      "#self/ask262Debug": ["./src/ask262-debug.mts"]
    }
  }
}

4. engine262/scripts/rollup.config.mts — Add resolve-self to Main Build

4a. Extend Existing resolve-self Plugin

// Inspector build (existing plugin, modified)
{
  name: 'resolve-self',
  resolveId(source, _importer, _options) {
    if (source === '#self') {
      return { id: './engine262.mjs' };
    }
    if (source.startsWith('#self/')) {
      // For inspector build, resolve subpaths to bundled engine262
      return { id: './engine262.mjs' }; // All #self/* resolve to main bundle
    }
    return undefined;
  },
}

4b. Add to Main Engine262 Build

{
  input: './src/index.mts',
  plugins: [
    importUnicodeLib(),
    (json.default || json)({ compact: true }),
    (commonjs.default || commonjs)(),
    nodeResolve({ exportConditions: ['rollup'], extensions: ['.mts'] }),
    {
      name: 'resolve-self',
      resolveId(source) {
        if (source === '#self') {
          return { id: '#self' }; // Mark as internal, bundled
        }
        if (source === '#self/ask262Debug') {
          return { id: './src/ask262-debug.mts' }; // Will be bundled
        }
        return undefined;
      },
    },
    babel({
      ...babelOptions,
      plugins: [
        './scripts/transform.mts',
        ['@babel/plugin-proposal-decorators', { 'version': '2023-11' }],
      ],
    }),
    // ... dts plugin
  ],
  // ... output config
}

5. engine262/src/index.mts — Add Named Re-export

// Add to existing exports
export { default as ask262Debug, type MarkData } from './ask262-debug.mts';

6. engine262/src/ask262-debug.mts — The Module

Deduplicated data collection. No stack parsing, no node: imports, no decorators.

export interface MarkData {
  readonly sectionIds: string[];
  readonly fileRelativePath: string;
  readonly lineNumber: number;
  readonly important: boolean;
}

interface MarkKey {
  readonly sectionIds: string;
  readonly file: string;
  readonly line: number;
}

class Ask262Debug {
  marks: MarkData[] = [];
  private _importantStack = false;
  private _markIndex = new Map<string, number>(); // key -> index in marks

  private _makeKey(sectionIds: string[], file: string, line: number): string {
    return `${sectionIds.join(',')}|${file}|${line}`;
  }

  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
      const existing = this.marks[existingIndex];
      if (this._importantStack && !existing.important) {
        (this.marks[existingIndex] as any).important = true;
      }
      return;
    }
    
    const newMark: MarkData = {
      sectionIds: [...sectionIds], // defensive copy
      fileRelativePath: file,
      lineNumber: line,
      important: this._importantStack,
    };
    
    this._markIndex.set(key, this.marks.length);
    this.marks.push(newMark);
  }

  startImportant() {
    this._importantStack = true;
  }

  stopImportant() {
    this._importantStack = false;
  }
}

const ask262Debug = new Ask262Debug();
export default ask262Debug;

Key Behaviors

Scenario Behavior
Duplicate (sectionIds, file, line) Keep one entry, merge important flags (OR logic)
Generator resumption Mark on every .next() call (may create duplicates if same location)
Nested functions Each entry adds its mark (chronological trace)

7. engine262/scripts/verify-ask262-debug.mts — Verification Script

#!/usr/bin/env node
import engine262 from '../lib/engine262.mjs';

console.log('=== ask262Debug Verification ===\n');

// 1. Check export exists
console.log('1. Checking export...');
if (!engine262.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 engine262.Agent();
const realm = new engine262.ManagedRealm({ agent });

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 = engine262.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('\n=== All Checks Passed ===');

Key Design Decisions Summary

Decision Choice Rationale
Use case AI execution flow analysis Map runtime to spec sections
Data structure Deduplicated array Chronological trace with unique entries only
Deduplication key (sectionIds, file, line) Same location = same semantic entry
Duplicate handling Merge important flags (OR) If any call was important, mark it important
Module name #self/ask262Debug Follows existing #self pattern
Export style Named export from engine262 import { ask262Debug } from '#self'
Bundle strategy Included in engine262.mjs Single artifact, no external dependency
All metadata From transform (build time) No runtime stack parsing, no node: imports
mark() injection Auto-injected at start of block body Developer writes nothing
Arrow functions Block body: .section + mark(), Expression: .section only Can't inject statement in expression
Multi-section Array of section IDs per function Functions can implement multiple spec sections
.section Kept (backward compat) Used by test262-intrinsics
Generators Mark on every .next() Simpler, deduplication handles repeats
Nested functions Mark all Chronological trace for AI analysis
important User-driven startImportant()/stopImportant() Manual annotation of interesting sections
Function types FunctionDeclaration + ClassMethod + ObjectMethod + VariableDeclaration Covers all URL comment patterns
Class/Object method keys Identifier names only Simple, covers 99% of cases
File paths Relative to engine262/src/, .mts extension Maps to source, not compiled output
Line numbers 1-indexed from Babel AST Matches source file line numbers

Verification Steps

  1. Type checking: cd engine262 && npx tsc -b .
  2. Linting: cd engine262 && npm run lint
  3. Build: cd engine262 && npm run build:engine
  4. Verification: cd engine262 && node scripts/verify-ask262-debug.mts
  5. Manual inspection: Check that transformed files have ask262Debug.mark() at start of functions with URL comments
  6. Multi-section test: Find a function with multiple URL comments (e.g., ArrayProto_every) and verify all section IDs are in the array

Expected Output from Verification

=== ask262Debug Verification ===

1. Checking export...
   ✓ ask262Debug exported

2. Executing test code...
3. Checking marks...
   Captured 47 unique marks

4. Sample marks:
   [0] sec-array.prototype.every @ intrinsics/ArrayPrototypeShared.mts:173
   [1] sec-proxycreate @ abstract-ops/proxy-objects.mts:540
   ...

=== All Checks Passed ===