mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
Merge commit '6e92d2345e8231a44556ce7d416451f5f3e6c398' as 'engine262'
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
opendir, readFile, stat, writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeModules = path.resolve(path.resolve(import.meta.dirname, '..'), 'node_modules');
|
||||
const unicodeDir = path.resolve(nodeModules, '@unicode', 'unicode-16.0.0');
|
||||
|
||||
async function writeUnicodePropertyMapping() {
|
||||
async function* scan(d: string): AsyncGenerator<string, void, void> {
|
||||
for await (const dirent of await opendir(d)) {
|
||||
if (dirent.isDirectory()) {
|
||||
const p = path.join(d, dirent.name);
|
||||
const test = path.join(p, 'code-points.js');
|
||||
try {
|
||||
await stat(test);
|
||||
yield p;
|
||||
} catch {
|
||||
yield* scan(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Range = readonly [from: number, to: number]
|
||||
|
||||
const data: Record<string, readonly Range[]> = {};
|
||||
|
||||
for await (const item of scan(unicodeDir)) {
|
||||
const category = path.relative(unicodeDir, item).replace(/\\/g, '/');
|
||||
const { default: cps } = await import(`@unicode/unicode-16.0.0/${category}/code-points.js`);
|
||||
if (!Array.isArray(cps)) {
|
||||
continue;
|
||||
}
|
||||
if (!category.startsWith('General_Category/') && !category.startsWith('Script/') && !category.startsWith('Script_Extensions/') && !category.startsWith('Binary_Property/')) {
|
||||
continue;
|
||||
}
|
||||
const ranges: Range[] = [];
|
||||
let from = 0;
|
||||
let to = 0;
|
||||
cps.forEach((cp, i) => {
|
||||
if (i === 0) {
|
||||
from = cp;
|
||||
to = cp;
|
||||
} else {
|
||||
if (to + 1 === cp) {
|
||||
to += 1;
|
||||
} else {
|
||||
ranges.push([from, to]);
|
||||
from = cp;
|
||||
to = cp;
|
||||
}
|
||||
}
|
||||
});
|
||||
ranges.push([from, to]);
|
||||
data[category] = ranges;
|
||||
}
|
||||
await writeFile(path.resolve(path.resolve(import.meta.dirname, '..'), 'src/unicode/CodePointProperties.json'), JSON.stringify(data));
|
||||
}
|
||||
|
||||
async function writeUnicodeStringsMapping() {
|
||||
const data: Record<string, string> = {};
|
||||
for (const cat of [
|
||||
'Basic_Emoji',
|
||||
'Emoji_Keycap_Sequence',
|
||||
'RGI_Emoji_Modifier_Sequence',
|
||||
'RGI_Emoji_Flag_Sequence',
|
||||
'RGI_Emoji_Tag_Sequence',
|
||||
'RGI_Emoji_ZWJ_Sequence',
|
||||
'RGI_Emoji',
|
||||
]) {
|
||||
const file = path.resolve(unicodeDir, 'Sequence_Property', cat, 'index.js');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { default: strings } = await import(pathToFileURL(file).href);
|
||||
data[cat] = strings.join(',');
|
||||
}
|
||||
await writeFile(path.resolve(path.resolve(import.meta.dirname, '..'), 'src', 'unicode/SequenceProperties.json'), JSON.stringify(data));
|
||||
}
|
||||
|
||||
async function writeUnicodePropertyAliasMapping() {
|
||||
const file = readFile(new URL('./Unicode/PropertyValueAliases.txt', import.meta.url), 'utf-8');
|
||||
const lines = (await file).split('\n').filter((line) => line.length > 0 && !line.startsWith('#'));
|
||||
|
||||
const gc: Record<string, string> = {};
|
||||
const sc: Record<string, string> = {};
|
||||
const scx: Record<string, string> = {};
|
||||
// gc ; M ; Mark ; Combining_Mark
|
||||
// where Mark is the official name, I guess?
|
||||
for (const line of lines) {
|
||||
const [cat, alias, formalName, ...moreAlias] = line
|
||||
.split('#')[0]
|
||||
.split(';')
|
||||
.map((s) => s.trim());
|
||||
if (cat === 'gc') {
|
||||
gc[alias] = formalName;
|
||||
gc[formalName] = formalName;
|
||||
moreAlias.forEach((name) => {
|
||||
gc[name] = formalName;
|
||||
});
|
||||
} else if (cat === 'sc') {
|
||||
sc[alias] = formalName;
|
||||
sc[formalName] = formalName;
|
||||
moreAlias.forEach((name) => {
|
||||
sc[name] = formalName;
|
||||
});
|
||||
} else if (cat === 'scx') {
|
||||
scx[alias] = formalName;
|
||||
scx[formalName] = formalName;
|
||||
moreAlias.forEach((name) => {
|
||||
scx[name] = formalName;
|
||||
});
|
||||
}
|
||||
}
|
||||
await writeFile(
|
||||
new URL('../src/unicode/PropertyValueAliases.json', import.meta.url),
|
||||
JSON.stringify(
|
||||
{
|
||||
description:
|
||||
'Unicode Property Value Aliases, generated from https://unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt',
|
||||
General_Category: gc,
|
||||
Script: sc,
|
||||
Script_Extensions: scx,
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
writeUnicodePropertyMapping(),
|
||||
writeUnicodeStringsMapping(),
|
||||
writeUnicodePropertyAliasMapping(),
|
||||
]);
|
||||
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable no-console */
|
||||
import { opendir, readFile, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
createSourceFile, isCallExpression, isIdentifier, isPropertyAccessExpression, isStringLiteral, ScriptTarget,
|
||||
} from 'typescript';
|
||||
|
||||
async function* readdir(dir: string): AsyncGenerator<string> {
|
||||
for await (const dirent of await opendir(dir)) {
|
||||
const p = join(dir, dirent.name);
|
||||
if (dirent.isDirectory()) {
|
||||
yield* readdir(p);
|
||||
} else {
|
||||
yield p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const list = ['EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'Error', 'AggregateError'];
|
||||
const messages = new Set<string>();
|
||||
const promises: Promise<void>[] = [];
|
||||
for await (const filePath of readdir(join(import.meta.dirname, '../src/'))) {
|
||||
if (!filePath.endsWith('.mts')) {
|
||||
continue;
|
||||
}
|
||||
promises.push(readFile(filePath, 'utf8').then((content) => {
|
||||
const sourceFile = createSourceFile(filePath, content, {
|
||||
languageVersion: ScriptTarget.ESNext,
|
||||
});
|
||||
sourceFile.forEachChild(function visitor(node) {
|
||||
if (
|
||||
isCallExpression(node)
|
||||
&& isPropertyAccessExpression(node.expression)
|
||||
&& isIdentifier(node.expression.expression)
|
||||
&& node.expression.expression.escapedText === 'Throw'
|
||||
&& isIdentifier(node.expression.name)
|
||||
&& list.includes(node.expression.name.escapedText as string)
|
||||
&& node.arguments.length >= 1
|
||||
) {
|
||||
if (!isStringLiteral(node.arguments[0])) {
|
||||
console.warn(`Non-literal error message in ${filePath}`);
|
||||
} else {
|
||||
messages.add(node.arguments[0].text);
|
||||
}
|
||||
}
|
||||
node.forEachChild(visitor);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const sortedMessages = Array.from(messages).sort();
|
||||
|
||||
const old = await readFile(join(import.meta.dirname, '../src/host-defined/error-messages.mts'), 'utf8');
|
||||
|
||||
const autoGenStart = '// auto-generate start';
|
||||
const autoGenEnd = '// auto-generate end';
|
||||
|
||||
const beforeAutoGen = old.slice(0, old.indexOf(autoGenStart) + autoGenStart.length);
|
||||
const afterAutoGen = old.slice(old.indexOf(autoGenEnd));
|
||||
|
||||
const messagesByParameterCount: string[][] = [];
|
||||
sortedMessages.forEach((m) => {
|
||||
// const paramCount = (m.match(/\$\d+/g) || []).length;
|
||||
// const params = Array.from({ length: paramCount }, (_, i) => `$${i + 1}: Formattable`).join(', ');
|
||||
// return ` (m: '${m}'${params ? `, ${params}` : ''}): ThrowCompletion;`;
|
||||
if (m.includes('$3')) {
|
||||
messagesByParameterCount[3] ??= [];
|
||||
messagesByParameterCount[3].push(m);
|
||||
} else if (m.includes('$2')) {
|
||||
messagesByParameterCount[2] ??= [];
|
||||
messagesByParameterCount[2].push(m);
|
||||
} else if (m.includes('$1')) {
|
||||
messagesByParameterCount[1] ??= [];
|
||||
messagesByParameterCount[1].push(m);
|
||||
} else {
|
||||
messagesByParameterCount[0] ??= [];
|
||||
messagesByParameterCount[0].push(m);
|
||||
}
|
||||
});
|
||||
|
||||
const generatedLines: string[] = [];
|
||||
messagesByParameterCount.forEach((group, index) => {
|
||||
const args: string[] = [group.sort().map((m) => (m.includes("'") ? `"${m}"` : `'${m}'`)).join('\n | '), ...Array(index).fill('Formattable').map((t, i) => `$${i + 1}: ${t}`)];
|
||||
args[0] += '\n ';
|
||||
generatedLines.push(` (m:\n${args.join(', ')}): ThrowCompletion;`);
|
||||
});
|
||||
const generated = generatedLines.join('\n');
|
||||
|
||||
const newFileContent = `${beforeAutoGen}
|
||||
${generated}
|
||||
${afterAutoGen}`;
|
||||
|
||||
if (newFileContent !== old) {
|
||||
console.log('Updating error-messages.mts');
|
||||
await writeFile(
|
||||
join(import.meta.dirname, '../src/host-defined/error-messages.mts'),
|
||||
newFileContent,
|
||||
);
|
||||
} else {
|
||||
console.log('error-messages.mts is up to date');
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { babel, type RollupBabelInputPluginOptions } from '@rollup/plugin-babel';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import { defineConfig, type Plugin } from 'rollup';
|
||||
import packageJson from '../package.json' with { type: 'json' };
|
||||
|
||||
const commitHash = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
||||
|
||||
const banner = `/*!
|
||||
* engine262 ${packageJson.version} ${commitHash}
|
||||
*
|
||||
* ${readFileSync('./LICENSE', 'utf8').trim().split('\n').join('\n * ')}
|
||||
*/
|
||||
`;
|
||||
|
||||
const babelOptions: RollupBabelInputPluginOptions = {
|
||||
babelHelpers: 'bundled',
|
||||
exclude: 'node_modules/**',
|
||||
generatorOpts: {
|
||||
importAttributesKeyword: 'with',
|
||||
},
|
||||
presets: [[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
// this includes at least 1 LTS for Node.js
|
||||
targets: ['last 2 node versions'],
|
||||
spec: true,
|
||||
bugfixes: true,
|
||||
},
|
||||
], [
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
allowDeclareFields: true,
|
||||
},
|
||||
]],
|
||||
extensions: ['.mts'],
|
||||
};
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: 'lib-src/inspector/index.mts',
|
||||
plugins: [
|
||||
babel(babelOptions),
|
||||
{
|
||||
name: 'resolve-self',
|
||||
resolveId(source, _importer, _options) {
|
||||
if (source === '#self') {
|
||||
return { id: './engine262.mjs' };
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dts',
|
||||
buildStart() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'inspector.d.ts',
|
||||
source: 'export * from "../lib/inspector/index.d.mts";',
|
||||
});
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'inspector.d.mts',
|
||||
source: 'export * from "../lib/inspector/index.d.mts";',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
external: ['./engine262.mjs'],
|
||||
output: [
|
||||
{
|
||||
file: 'lib/inspector.js',
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
name: `${packageJson.name}/inspector`,
|
||||
banner,
|
||||
globals: { './engine262.mjs': '@engine262/engine262' },
|
||||
},
|
||||
{
|
||||
file: 'lib/inspector.mjs',
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
banner,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './src/index.mts',
|
||||
plugins: [
|
||||
importUnicodeLib(),
|
||||
(json.default || json)({ compact: true }),
|
||||
(commonjs.default || commonjs)(),
|
||||
nodeResolve({ exportConditions: ['rollup'], extensions: ['.mts'] }),
|
||||
babel({
|
||||
...babelOptions,
|
||||
plugins: [
|
||||
'./scripts/transform.mts',
|
||||
['@babel/plugin-proposal-decorators', {
|
||||
'version': '2023-11',
|
||||
}],
|
||||
],
|
||||
}),
|
||||
{
|
||||
name: 'dts',
|
||||
buildStart() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'engine262.d.ts',
|
||||
source: 'export * from "../declaration/index.d.mjs";',
|
||||
});
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'engine262.d.mts',
|
||||
source: 'export * from "../declaration/index.d.mjs";',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
output: [
|
||||
{
|
||||
file: 'lib/engine262.js',
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
name: packageJson.name,
|
||||
banner,
|
||||
},
|
||||
{
|
||||
file: 'lib/engine262.mjs',
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
banner,
|
||||
},
|
||||
],
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' || warning.code === 'SOURCEMAP_BROKEN') {
|
||||
// Squelch.
|
||||
return;
|
||||
}
|
||||
process.exitCode = 1;
|
||||
warn(warning);
|
||||
},
|
||||
}]);
|
||||
|
||||
/**
|
||||
* Special handle of the following modules so we don't need to import the whole zlib polyfill.
|
||||
*/
|
||||
function importUnicodeLib(): Plugin {
|
||||
const canImport = ['@unicode/unicode-16.0.0/Case_Folding/C/symbols.js', '@unicode/unicode-16.0.0/Case_Folding/S/symbols.js'];
|
||||
return {
|
||||
name: '@unicode lib import',
|
||||
async transform(code, id) {
|
||||
if (!id.includes('node_modules/@unicode')) {
|
||||
return { code, map: this.getCombinedSourcemap() };
|
||||
}
|
||||
if (canImport.some((i) => id.endsWith(i))) {
|
||||
const module = createRequire(import.meta.url)(id) as Map<string, string>;
|
||||
const codePointsInArray = Array.from(module.entries()).map(([str, str2]) => {
|
||||
const it1 = str[Symbol.iterator]();
|
||||
const it2 = str2[Symbol.iterator]();
|
||||
it1.next();
|
||||
it2.next();
|
||||
if (!it1.next().done || !it2.next().done) {
|
||||
throw new Error(`TODO: handle something strange: ${str} ${str2}`);
|
||||
}
|
||||
return [str.codePointAt(0), str2.codePointAt(0)];
|
||||
});
|
||||
const str = JSON.stringify(JSON.stringify(codePointsInArray));
|
||||
return `export default new Map(JSON.parse(${str}).map(([cp1, cp2]) => [String.fromCodePoint(cp1), String.fromCodePoint(cp2)]));`;
|
||||
}
|
||||
return code;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import json from '../package.json' with { type: 'json' };
|
||||
|
||||
const jsonPath = new URL('../package.json', import.meta.url);
|
||||
|
||||
process.stdout.write('Checking package.json for git revision...\n');
|
||||
|
||||
if (!json.version.includes('-')) {
|
||||
process.stdout.write('Inserting git revision into package.json...\n');
|
||||
|
||||
const hash = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
||||
json.version = `${json.version}-${hash}`;
|
||||
fs.writeFileSync(jsonPath, `${JSON.stringify(json, null, 2)}\n`);
|
||||
}
|
||||
|
||||
process.stdout.write('Done!\n');
|
||||
@@ -0,0 +1,456 @@
|
||||
import {
|
||||
type NodePath,
|
||||
traverse,
|
||||
type Node,
|
||||
type PluginObj, type PluginPass,
|
||||
type types as t,
|
||||
} from '@babel/core';
|
||||
import type { PublicReplacements } from '@babel/template';
|
||||
|
||||
function __ts_cast__<T>(_value: unknown): asserts _value is T { }
|
||||
|
||||
function findParentStatementPath(path: NodePath): NodePath<t.Statement> | null {
|
||||
while (path && !path.isStatement()) {
|
||||
path = path.parentPath!;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function getEnclosingConditionalExpression(path: NodePath) {
|
||||
while (path && !path.isStatement()) {
|
||||
if (path.isConditionalExpression()) {
|
||||
return path;
|
||||
}
|
||||
path = path.parentPath!;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type NeededNames = 'Completion' | 'AbruptCompletion' | 'Assert' | 'Call' | 'IteratorClose' | 'AsyncIteratorClose' | 'Value' | 'skipDebugger';
|
||||
|
||||
interface State extends PluginPass {
|
||||
needed: Partial<Record<NeededNames, boolean>>;
|
||||
}
|
||||
|
||||
interface Macro<R extends PublicReplacements = Record<string, Node | null>> {
|
||||
template(sourceLocation: Node, replacements: Readonly<R>): t.Statement | t.Statement[];
|
||||
readonly imports: readonly NeededNames[];
|
||||
readonly allowAnyExpression?: boolean;
|
||||
}
|
||||
|
||||
interface Macros {
|
||||
[m: string]: Macro;
|
||||
Q: Macro<{ value: t.Identifier, checkYieldStar: t.Statement | null }>;
|
||||
X: Macro<{ value: t.Identifier, checkYieldStar: t.Statement | null, source: t.StringLiteral }>;
|
||||
ReturnIfAbrupt: Macro<{ value: t.Identifier, checkYieldStar: t.Statement | null }>;
|
||||
IfAbruptCloseIterator: Macro<{ value: t.Identifier, iteratorRecord: t.Identifier }>;
|
||||
IfAbruptCloseAsyncIterator: Macro<{ value: t.Identifier, iteratorRecord: t.Identifier }>;
|
||||
IfAbruptRejectPromise: Macro<{ value: t.Identifier, capability: t.Identifier }>;
|
||||
}
|
||||
|
||||
export default ({ types: t, template }: typeof import('@babel/core')): PluginObj<State> => {
|
||||
const parseOptions = { preserveComments: true };
|
||||
function createImportCompletion() {
|
||||
return template.ast(`
|
||||
import { Completion } from "#self";
|
||||
`);
|
||||
}
|
||||
|
||||
function createImportSkipDebugger() {
|
||||
return template.ast(`
|
||||
import { skipDebugger } from "#self";
|
||||
`);
|
||||
}
|
||||
|
||||
function createImportAbruptCompletion() {
|
||||
return template.ast(`
|
||||
import { AbruptCompletion } from "#self";
|
||||
`);
|
||||
}
|
||||
|
||||
function createImportAssert() {
|
||||
return template.ast(`
|
||||
import { Assert } from "#self";
|
||||
`);
|
||||
}
|
||||
|
||||
function createImportCall() {
|
||||
return template.ast(`
|
||||
import { Call } from "#self";
|
||||
`);
|
||||
}
|
||||
|
||||
function createImportIteratorClose() {
|
||||
return template.statement.ast`
|
||||
import { IteratorClose } from "#self";
|
||||
`;
|
||||
}
|
||||
|
||||
function createImportAsyncIteratorClose() {
|
||||
return template.statement.ast`
|
||||
import { AsyncIteratorClose } from "#self";
|
||||
`;
|
||||
}
|
||||
|
||||
function createImportValue() {
|
||||
return template.ast(`
|
||||
import { Value } from "#self";
|
||||
`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const maybeSkipDebugger = (value: t.Identifier, callee: Node) => withSource(callee, template.statement(`
|
||||
/* node:coverage ignore next */ if (%%value%% && typeof %%value%% === 'object' && 'next' in %%value%%) %%value%% = skipDebugger(%%value%%);
|
||||
`, { preserveComments: true })({ value }))[0];
|
||||
|
||||
type NodeWithLocation = Pick<Node, 'start' | 'end' | 'loc'>;
|
||||
|
||||
function setSource(source: NodeWithLocation, n: t.Node) {
|
||||
if (n.loc) {
|
||||
return;
|
||||
}
|
||||
n.start = source.start;
|
||||
n.end = source.end;
|
||||
n.loc = source.loc;
|
||||
n.leadingComments?.forEach((comment) => {
|
||||
comment.start = source.start || undefined;
|
||||
comment.end = source.end || undefined;
|
||||
comment.loc = source.loc || undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function withSource(source: NodeWithLocation, node: t.Statement | t.Statement[]): t.Statement[] {
|
||||
if (!Array.isArray(node)) {
|
||||
node = [node];
|
||||
}
|
||||
for (const n of node) {
|
||||
setSource(source, n);
|
||||
traverse(n, {
|
||||
noScope: true,
|
||||
enter(path) {
|
||||
setSource(source, path.node);
|
||||
},
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
const MACROS: Macros = {
|
||||
Q: {
|
||||
template: (source, code) => withSource(source, template(`
|
||||
/* ReturnIfAbrupt */
|
||||
%%checkYieldStar%%
|
||||
/* node:coverage ignore next */ if (%%value%% instanceof AbruptCompletion) return %%value%%;
|
||||
/* node:coverage ignore next */ if (%%value%% instanceof Completion) %%value%% = %%value%%.Value;
|
||||
`, parseOptions)(code)),
|
||||
imports: ['AbruptCompletion', 'Completion', 'Assert'],
|
||||
allowAnyExpression: true,
|
||||
},
|
||||
X: {
|
||||
template: (source, code) => withSource(source, template(`
|
||||
/* X */
|
||||
%%checkYieldStar%%
|
||||
/* node:coverage ignore next */ if (%%value%% instanceof AbruptCompletion) throw new Assert.Error(%%source%%, { cause: %%value%% });
|
||||
/* node:coverage ignore next */ if (%%value%% instanceof Completion) %%value%% = %%value%%.Value;
|
||||
`, parseOptions)(code)),
|
||||
imports: ['Assert', 'Completion', 'AbruptCompletion', 'skipDebugger'],
|
||||
allowAnyExpression: true,
|
||||
},
|
||||
IfAbruptCloseIterator: {
|
||||
template: (source, code) => withSource(source, template(`
|
||||
/* IfAbruptCloseIterator */
|
||||
/* node:coverage ignore next */
|
||||
if (%%value%% instanceof AbruptCompletion) return skipDebugger(IteratorClose(%%iteratorRecord%%, %%value%%));
|
||||
/* node:coverage ignore next */
|
||||
if (%%value%% instanceof Completion) %%value%% = %%value%%.Value;
|
||||
`, parseOptions)(code)),
|
||||
imports: ['IteratorClose', 'AbruptCompletion', 'Completion', 'skipDebugger'],
|
||||
},
|
||||
IfAbruptCloseAsyncIterator: {
|
||||
template: (source, code) => withSource(source, template(`
|
||||
/* IfAbruptCloseAsyncIterator */
|
||||
/* node:coverage ignore next */
|
||||
if (%%value%% instanceof AbruptCompletion) return yield* AsyncIteratorClose(%%iteratorRecord%%, %%value%%);
|
||||
/* node:coverage ignore next */
|
||||
if (%%value%% instanceof Completion) %%value%% = %%value%%.Value;
|
||||
`, parseOptions)(code)),
|
||||
imports: ['Assert', 'AsyncIteratorClose', 'AbruptCompletion', 'Completion', 'skipDebugger'],
|
||||
},
|
||||
IfAbruptRejectPromise: {
|
||||
template: (source, code) => withSource(source, template(`
|
||||
/* IfAbruptRejectPromise */
|
||||
/* node:coverage disable */
|
||||
if (%%value%% instanceof AbruptCompletion) {
|
||||
const callRejectCompletion = skipDebugger(Call(%%capability%%.Reject, Value.undefined, [%%value%%.Value]));
|
||||
if (callRejectCompletion instanceof AbruptCompletion) return callRejectCompletion;
|
||||
return %%capability%%.Promise;
|
||||
}
|
||||
if (%%value%% instanceof Completion) %%value%% = %%value%%.Value;
|
||||
/* node:coverage enable */
|
||||
`, parseOptions)(code)),
|
||||
imports: ['Call', 'Value', 'AbruptCompletion', 'Completion', 'skipDebugger'],
|
||||
},
|
||||
ReturnIfAbrupt: null!,
|
||||
};
|
||||
__ts_cast__<Macros>(MACROS);
|
||||
MACROS.ReturnIfAbrupt = MACROS.Q;
|
||||
const MACRO_NAMES = Object.keys(MACROS);
|
||||
|
||||
// For frequently used Record-like classes, inline them to get a better debug experience.
|
||||
const Completions = {
|
||||
NormalCompletion: (source: Node, code: PublicReplacements) => withSource(source, template('({ __proto__: NormalCompletion.prototype, Value: %%value%% })', parseOptions)(code))[0],
|
||||
ThrowCompletion: (source: Node, code: PublicReplacements) => withSource(source, template('({ __proto__: ThrowCompletion.prototype, Value: %%value%% })', parseOptions)(code))[0],
|
||||
};
|
||||
const Structs = [
|
||||
'AsyncGeneratorRequestRecord',
|
||||
'ClassElementDefinitionRecord',
|
||||
'ClassFieldDefinitionRecord',
|
||||
'ClassStaticBlockDefinitionRecord',
|
||||
'PrivateElementRecord',
|
||||
];
|
||||
|
||||
function tryRemove(path: NodePath<t.CallExpression>) {
|
||||
try {
|
||||
path.remove();
|
||||
} catch (e) {
|
||||
throw path.get('arguments.0').buildCodeFrameError(`Macros error: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(_path, state) {
|
||||
state.needed = {};
|
||||
},
|
||||
exit(path, state) {
|
||||
if (state.needed.skipDebugger) {
|
||||
path.unshiftContainer('body', createImportSkipDebugger());
|
||||
}
|
||||
if (state.needed.Completion) {
|
||||
path.unshiftContainer('body', createImportCompletion());
|
||||
}
|
||||
if (state.needed.AbruptCompletion) {
|
||||
path.unshiftContainer('body', createImportAbruptCompletion());
|
||||
}
|
||||
if (state.needed.Assert) {
|
||||
path.unshiftContainer('body', createImportAssert());
|
||||
}
|
||||
if (state.needed.Call) {
|
||||
path.unshiftContainer('body', createImportCall());
|
||||
}
|
||||
if (state.needed.IteratorClose) {
|
||||
path.unshiftContainer('body', createImportIteratorClose());
|
||||
}
|
||||
if (state.needed.AsyncIteratorClose) {
|
||||
path.unshiftContainer('body', createImportAsyncIteratorClose());
|
||||
}
|
||||
if (state.needed.Value) {
|
||||
path.unshiftContainer('body', createImportValue());
|
||||
}
|
||||
},
|
||||
},
|
||||
CallExpression(path, state) {
|
||||
const callee = path.node.callee;
|
||||
if (!t.isIdentifier(callee)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (callee.name && callee.name in Completions) {
|
||||
const template = Completions[callee.name as keyof typeof Completions];
|
||||
path.replaceWith(template(callee, { value: path.node.arguments[0] }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Structs.includes(callee.name) && path.node.arguments.length === 1) {
|
||||
const arg0 = path.node.arguments[0];
|
||||
if (t.isObjectExpression(arg0)) {
|
||||
path.replaceWith(t.objectExpression([
|
||||
t.objectProperty(t.identifier('__proto__'), t.memberExpression(t.identifier(callee.name), t.identifier('prototype'))),
|
||||
...arg0.properties,
|
||||
]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const macroName = callee.name;
|
||||
if (MACRO_NAMES.includes(macroName)) {
|
||||
const enclosingConditional = getEnclosingConditionalExpression(path);
|
||||
if (enclosingConditional !== null) {
|
||||
if (enclosingConditional.parentPath.isVariableDeclarator()) {
|
||||
const declaration = enclosingConditional.parentPath.parentPath;
|
||||
const id = enclosingConditional.parentPath.get('id');
|
||||
declaration.replaceWithMultiple(template.ast(`
|
||||
let ${id};
|
||||
if (${enclosingConditional.get('test')}) {
|
||||
${id} = ${enclosingConditional.get('consequent')}
|
||||
} else {
|
||||
${id} = ${enclosingConditional.get('alternate')}
|
||||
}
|
||||
`));
|
||||
return;
|
||||
} else {
|
||||
throw path.buildCodeFrameError('Macros may not be used within conditional expressions');
|
||||
}
|
||||
}
|
||||
|
||||
const macro = MACROS[macroName];
|
||||
const [argument] = path.node.arguments;
|
||||
|
||||
if (macro === MACROS.Q && (path.parentPath.isReturnStatement() || path.parentPath.isArrowFunctionExpression())) {
|
||||
path.replaceWith(path.node.arguments[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isArrowFunctionExpression()) {
|
||||
throw path.buildCodeFrameError('Macros may not be the sole expression of an arrow function');
|
||||
}
|
||||
|
||||
const statementPath = findParentStatementPath(path);
|
||||
if (!statementPath) {
|
||||
throw path.buildCodeFrameError('Internal error: no parent statement found');
|
||||
}
|
||||
|
||||
macro.imports.forEach((i) => {
|
||||
state.needed[i] = path.scope.getBinding(i) === undefined;
|
||||
});
|
||||
|
||||
if (macro === MACROS.Q && t.isIdentifier(argument)) {
|
||||
const binding = path.scope.getBinding(argument.name)!;
|
||||
(binding.path.parent as t.VariableDeclaration).kind = 'let';
|
||||
statementPath.insertBefore(withSource(callee, template(`
|
||||
/* ReturnIfAbrupt */
|
||||
/* node:coverage ignore next */ if (%%value%% && typeof %%value%% === 'object' && 'next' in %%value%%) throw new Assert.Error('Forgot to yield* on the completion.');
|
||||
/* node:coverage ignore next */ if (%%value%% instanceof AbruptCompletion) return %%value%%;
|
||||
/* node:coverage ignore next */ if (%%value%% instanceof Completion) %%value%% = %%value%%.Value;
|
||||
`, parseOptions)({ value: argument })));
|
||||
path.replaceWith(argument);
|
||||
} else {
|
||||
if (macro === MACROS.IfAbruptRejectPromise) {
|
||||
const [, capability] = path.node.arguments;
|
||||
if (!t.isIdentifier(argument)) {
|
||||
throw path.get('arguments.0').buildCodeFrameError('First argument to IfAbruptRejectPromise should be an identifier');
|
||||
}
|
||||
if (!t.isIdentifier(capability)) {
|
||||
throw path.get('arguments.1').buildCodeFrameError('Second argument to IfAbruptRejectPromise should be an identifier');
|
||||
}
|
||||
const binding = path.scope.getBinding(argument.name)!;
|
||||
(binding.path.parent as t.VariableDeclaration).kind = 'let';
|
||||
statementPath.insertBefore(macro.template(callee, { value: argument, capability }));
|
||||
tryRemove(path);
|
||||
} else if (macro === MACROS.IfAbruptCloseIterator || macro === MACROS.IfAbruptCloseAsyncIterator) {
|
||||
if (!t.isIdentifier(argument)) {
|
||||
throw path.get('arguments.0').buildCodeFrameError('First argument to IfAbruptCloseIterator should be an identifier');
|
||||
}
|
||||
const iteratorRecord = path.get('arguments.1');
|
||||
if (!iteratorRecord.isIdentifier()) {
|
||||
throw iteratorRecord.buildCodeFrameError('Second argument to IfAbruptCloseIterator should be an identifier');
|
||||
}
|
||||
const binding = path.scope.getBinding(argument.name)!;
|
||||
(binding.path.parent as t.VariableDeclaration).kind = 'let';
|
||||
statementPath.insertBefore(
|
||||
macro.template(callee, {
|
||||
value: argument,
|
||||
iteratorRecord: iteratorRecord.node,
|
||||
}),
|
||||
);
|
||||
tryRemove(path);
|
||||
} else {
|
||||
let id;
|
||||
if (!macro.allowAnyExpression) {
|
||||
if (!t.isIdentifier(argument)) {
|
||||
throw path.get('arguments.0').buildCodeFrameError(`First argument to ${macroName} should be an identifier`);
|
||||
}
|
||||
id = argument;
|
||||
} else {
|
||||
id = statementPath.scope.generateUidIdentifier();
|
||||
statementPath.insertBefore(withSource(callee, template(`
|
||||
/* ${macroName !== 'Q' ? macroName : 'ReturnIfAbrupt'} */
|
||||
let %%id%% = %%argument%%;
|
||||
`, parseOptions)({ id, argument })));
|
||||
}
|
||||
|
||||
const replacement: { value: typeof id, checkYieldStar: t.Statement | null, source?: t.StringLiteral } = {
|
||||
checkYieldStar: null,
|
||||
value: id,
|
||||
};
|
||||
if (macro === MACROS.X) {
|
||||
replacement.source = t.stringLiteral(`! ${path.get('arguments.0').getSource()} returned an abrupt completion`);
|
||||
if (!t.isYieldExpression(argument, { delegate: true })) {
|
||||
replacement.checkYieldStar = maybeSkipDebugger(id, callee);
|
||||
}
|
||||
}
|
||||
statementPath.insertBefore(macro.template(callee, replacement));
|
||||
path.replaceWith(id);
|
||||
}
|
||||
}
|
||||
} else if (macroName === 'Assert') {
|
||||
if (!path.node.arguments[1]) {
|
||||
path.node.arguments.push(t.stringLiteral(path.get('arguments.0').getSource()));
|
||||
}
|
||||
}
|
||||
},
|
||||
ThrowStatement(path) {
|
||||
const arg = path.get('argument');
|
||||
if (arg.isNewExpression()) {
|
||||
const callee = arg.get('callee');
|
||||
if (callee.isIdentifier() && callee.node.name === 'OutOfRange') {
|
||||
path.addComment('leading', ' node:coverage ignore next ', false);
|
||||
|
||||
const { parentPath } = path;
|
||||
if (parentPath.isSwitchCase() && parentPath.node.consequent[0] === path.node) {
|
||||
parentPath.addComment('leading', ' node:coverage ignore next ', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
FunctionDeclaration(path) {
|
||||
addSectionFromComments(path);
|
||||
},
|
||||
VariableDeclaration(path) {
|
||||
if (path.get('declarations.0.init').isArrowFunctionExpression() || path.get('declarations.0.init').isFunctionExpression()) {
|
||||
addSectionFromComments(path);
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration(path) {
|
||||
if (path.get('declaration').isFunctionDeclaration()) {
|
||||
addSectionFromComments(path);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"include": ["."],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user