From aad89d0158e1b994b98c94c1070be972504ad79f Mon Sep 17 00:00:00 2001 From: Zbyszek Tenerowicz Date: Thu, 26 May 2022 10:47:47 +0200 Subject: [PATCH] fix(static-module-record): babelPlugin visitor to not skip declarations in exports + benchmark setup (#1188) * babelPlugin fix proposal and benchmarking setup * remove debris Co-authored-by: Michael FIG * more cleanup --- packages/static-module-record/package.json | 1 + .../static-module-record/src/babelPlugin.js | 56 +- .../src/transform-analyze.js | 47 +- .../src/transformSource.js | 39 ++ .../test/benchmark-babel-plugin.js | 53 ++ .../test/fixtures/exportheavy.js | 55 ++ .../test/fixtures/large.js | 651 ++++++++++++++++++ .../test/fixtures/small.js | 44 ++ .../test/test-static-module-record.js | 14 +- yarn.lock | 20 + 10 files changed, 897 insertions(+), 83 deletions(-) create mode 100644 packages/static-module-record/src/transformSource.js create mode 100644 packages/static-module-record/test/benchmark-babel-plugin.js create mode 100644 packages/static-module-record/test/fixtures/exportheavy.js create mode 100644 packages/static-module-record/test/fixtures/large.js create mode 100644 packages/static-module-record/test/fixtures/small.js diff --git a/packages/static-module-record/package.json b/packages/static-module-record/package.json index ab8c0fc918..bfa8fa2a08 100644 --- a/packages/static-module-record/package.json +++ b/packages/static-module-record/package.json @@ -47,6 +47,7 @@ "@endo/ses-ava": "^0.2.25", "ava": "^3.12.1", "babel-eslint": "^10.0.3", + "benchmark": "^2.1.4", "c8": "^7.7.3", "eslint": "^7.32.0", "eslint-config-airbnb-base": "^14.0.0", diff --git a/packages/static-module-record/src/babelPlugin.js b/packages/static-module-record/src/babelPlugin.js index 94d6ca68b9..1ece31f27f 100644 --- a/packages/static-module-record/src/babelPlugin.js +++ b/packages/static-module-record/src/babelPlugin.js @@ -86,10 +86,6 @@ function makeModulePlugins(options) { return node; }; - const prependReplacements = (replacements, node) => { - replacements.unshift(node); - }; - const allowedHiddens = new WeakSet(); const rewrittenDecls = new WeakSet(); const hiddenIdentifier = hi => { @@ -241,7 +237,7 @@ function makeModulePlugins(options) { return replacements; }; - const rewriteDeclaration = path => { + const rewriteExportDeclaration = path => { // Find all the declared identifiers. if (rewrittenDecls.has(path.node)) { return; @@ -254,7 +250,7 @@ function makeModulePlugins(options) { // Create the export calls. const isConst = decl.kind === 'const'; - const replacements = rewriteVars( + const additions = rewriteVars( vids, isConst, decl.type === 'FunctionDeclaration' @@ -262,23 +258,11 @@ function makeModulePlugins(options) { : !isConst && decl.kind !== 'let', ); - if (replacements.length > 0) { - switch (decl.type) { - case 'VariableDeclaration': { - // We rewrote the declaration. - rewrittenDecls.add(decl); - prependReplacements(replacements, decl); - break; - } - case 'FunctionDeclaration': { - prependReplacements(replacements, decl); - break; - } - default: { - throw TypeError(`Unknown declaration type ${decl.type}`); - } + if (additions.length > 0) { + if (decl.type === 'VariableDeclaration') { + rewrittenDecls.add(decl); } - path.replaceWithMultiple(replacements); + path.insertAfter(additions); } }; @@ -309,23 +293,22 @@ function makeModulePlugins(options) { }, }; - const moduleVisitor = (doAnalyze, doTransform) => ({ + const importMetaVisitor = { MetaProperty(path) { if ( path.node.meta && path.node.meta.name === 'import' && path.node.property.name === 'meta' ) { - if (doAnalyze) { - importMeta.uttered = true; - } - if (doTransform) { - path.replaceWithMultiple([ - replace(path.node, hiddenIdentifier(h.HIDDEN_META)), - ]); - } + importMeta.uttered = true; + path.replaceWithMultiple([ + replace(path.node, hiddenIdentifier(h.HIDDEN_META)), + ]); } }, + }; + + const moduleVisitor = (doAnalyze, doTransform) => ({ // We handle all the import and export productions. ImportDeclaration(path) { if (doAnalyze) { @@ -473,7 +456,7 @@ function makeModulePlugins(options) { } if (doTransform) { if (topLevelExported[name]) { - rewriteDeclaration(path); + rewriteExportDeclaration(path); markExport(name); } } @@ -496,7 +479,7 @@ function makeModulePlugins(options) { if (doTransform) { for (const { name } of vids) { if (topLevelExported[name]) { - rewriteDeclaration(path); + rewriteExportDeclaration(path); break; } } @@ -614,7 +597,12 @@ function makeModulePlugins(options) { }, }; case 1: - return { visitor: moduleVisitor(false, true) }; + return { + visitor: { + ...moduleVisitor(false, true), + ...importMetaVisitor, + }, + }; default: throw TypeError(`Unrecognized module pass ${pass}`); } diff --git a/packages/static-module-record/src/transform-analyze.js b/packages/static-module-record/src/transform-analyze.js index cc66b89eb7..8daaf3cd78 100644 --- a/packages/static-module-record/src/transform-analyze.js +++ b/packages/static-module-record/src/transform-analyze.js @@ -1,49 +1,10 @@ -import * as babelParser from '@babel/parser'; -import babelGenerate from '@agoric/babel-generator'; -import babelTraverse from '@babel/traverse'; -import babelTypes from '@babel/types'; +import { makeTransformSource } from './transformSource.js'; +import makeModulePlugins from './babelPlugin.js'; import * as h from './hidden.js'; -import makeModulePlugins from './babelPlugin.js'; const { freeze } = Object; -const parseBabel = babelParser.default - ? babelParser.default.parse - : babelParser.parse || babelParser; - -const visitorFromPlugin = plugin => plugin({ types: babelTypes }).visitor; - -const traverseBabel = babelTraverse.default || babelTraverse; -const generateBabel = babelGenerate.default || babelGenerate; - -const makeTransformSource = (babel = null) => { - if (babel !== null) { - throw new Error( - `transform-analyze.js no longer allows injecting babel; use \`null\``, - ); - } - - const transformSource = (code, sourceOptions = {}) => { - // console.log(`transforming`, sourceOptions, code); - const { analyzePlugin, transformPlugin } = makeModulePlugins(sourceOptions); - - const ast = parseBabel(code, { sourceType: sourceOptions.sourceType }); - - traverseBabel(ast, visitorFromPlugin(analyzePlugin)); - traverseBabel(ast, visitorFromPlugin(transformPlugin)); - - const { code: transformedCode } = generateBabel(ast, { - retainLines: true, - compact: true, - verbatim: true, - }); - return transformedCode; - }; - - return transformSource; -}; - const makeCreateStaticRecord = transformSource => function createStaticRecord(moduleSource, url) { // Transform the Module source code. @@ -136,13 +97,13 @@ const makeCreateStaticRecord = transformSource => }; export const makeModuleAnalyzer = babel => { - const transformSource = makeTransformSource(babel); + const transformSource = makeTransformSource(makeModulePlugins, babel); const createStaticRecord = makeCreateStaticRecord(transformSource); return ({ string, url }) => createStaticRecord(string, url); }; export const makeModuleTransformer = (babel, importer) => { - const transformSource = makeTransformSource(babel); + const transformSource = makeTransformSource(makeModulePlugins, babel); const createStaticRecord = makeCreateStaticRecord(transformSource); return { rewrite(ss) { diff --git a/packages/static-module-record/src/transformSource.js b/packages/static-module-record/src/transformSource.js new file mode 100644 index 0000000000..cba7f8e413 --- /dev/null +++ b/packages/static-module-record/src/transformSource.js @@ -0,0 +1,39 @@ +import * as babelParser from '@babel/parser'; +import babelGenerate from '@agoric/babel-generator'; +import babelTraverse from '@babel/traverse'; +import babelTypes from '@babel/types'; + +const parseBabel = babelParser.default + ? babelParser.default.parse + : babelParser.parse || babelParser; + +const visitorFromPlugin = plugin => plugin({ types: babelTypes }).visitor; + +const traverseBabel = babelTraverse.default || babelTraverse; +const generateBabel = babelGenerate.default || babelGenerate; + +export const makeTransformSource = (makeModulePlugins, babel = null) => { + if (babel !== null) { + throw new Error( + `transform-analyze.js no longer allows injecting babel; use \`null\``, + ); + } + + const transformSource = (code, sourceOptions = {}) => { + const { analyzePlugin, transformPlugin } = makeModulePlugins(sourceOptions); + + const ast = parseBabel(code, { sourceType: sourceOptions.sourceType }); + + traverseBabel(ast, visitorFromPlugin(analyzePlugin)); + traverseBabel(ast, visitorFromPlugin(transformPlugin)); + + const { code: transformedCode } = generateBabel(ast, { + retainLines: true, + compact: true, + verbatim: true, + }); + return transformedCode; + }; + + return transformSource; +}; diff --git a/packages/static-module-record/test/benchmark-babel-plugin.js b/packages/static-module-record/test/benchmark-babel-plugin.js new file mode 100644 index 0000000000..f0d3d0091a --- /dev/null +++ b/packages/static-module-record/test/benchmark-babel-plugin.js @@ -0,0 +1,53 @@ +import Benchmark from 'benchmark'; +import fs from 'fs'; +import url from 'url'; +import { makeTransformSource } from '../src/transformSource.js'; +import makeModulePlugins from '../src/babelPlugin.js'; + +const suite = new Benchmark.Suite(); + +const resolveLocal = path => url.fileURLToPath(new URL(path, import.meta.url)); +const cases = [ + { + name: 'small', + fixture: fs.readFileSync(resolveLocal('./fixtures/small.js'), 'utf8'), + }, + { + name: 'large', + fixture: fs.readFileSync(resolveLocal('./fixtures/large.js'), 'utf8'), + }, + { + name: 'exportheavy', + fixture: fs.readFileSync(resolveLocal('./fixtures/exportheavy.js'), 'utf8'), + }, +]; + +const transformSource = makeTransformSource(makeModulePlugins); +const freshOptions = () => { + return { + sourceType: 'module', + fixedExportMap: Object.create(null), + imports: Object.create(null), + exportAlls: [], + liveExportMap: Object.create(null), + hoistedDecls: [], + importSources: Object.create(null), + importDecls: [], + importMeta: { uttered: false }, + }; +}; + +cases.map(testCase => + suite.add(testCase.name, () => { + transformSource(testCase.fixture, freshOptions()); + }), +); + +suite + .on('cycle', event => { + console.log(String(event.target)); + }) + .on('error', event => { + console.log(String(event.target), event.target.error); + }) + .run(); diff --git a/packages/static-module-record/test/fixtures/exportheavy.js b/packages/static-module-record/test/fixtures/exportheavy.js new file mode 100644 index 0000000000..fc65ec49a9 --- /dev/null +++ b/packages/static-module-record/test/fixtures/exportheavy.js @@ -0,0 +1,55 @@ +/* eslint-disable */ +console.error("This is a code sample for trying out babel transforms, it's not meant to be run"); + +export { mapIterable, filterIterable } from './src/helpers/iter-helpers.js'; +export { + PASS_STYLE, + isObject, + assertChecker, + getTag, + hasOwnPropertyOf, +} from './src/helpers/passStyle-helpers.js'; + +export { getErrorConstructor, toPassableError } from './src/helpers/error.js'; +export { getInterfaceOf } from './src/helpers/remotable.js'; + +export { + nameForPassableSymbol, + passableSymbolForName, +} from './src/helpers/symbol.js'; + +export { passStyleOf, assertPassable } from './src/passStyleOf.js'; + +export { deeplyFulfilled } from './src/deeplyFulfilled.js'; + +export { makeTagged } from './src/makeTagged.js'; +export { Remotable, Far, ToFarFunction } from './src/make-far.js'; + +export { QCLASS, makeMarshal } from './src/marshal.js'; +export { stringify, parse } from './src/marshal-stringify.js'; +// Works, but not yet used +// export { decodeToJustin } from './src/marshal-justin.js'; + +export { + assertRecord, + assertCopyArray, + assertRemotable, + isRemotable, + isRecord, + isCopyArray, +} from './src/typeGuards.js'; + +// eslint-disable-next-line import/export +export * from './src/types.js'; + + +const { details: X } = assert; + +// This is a pathological minimum, but exercised by the unit test. +export const MIN_DATA_BUFFER_LENGTH = 1; + +// Calculate how big the transfer buffer needs to be. +export const TRANSFER_OVERHEAD_LENGTH = + BigUint64Array.BYTES_PER_ELEMENT + Int32Array.BYTES_PER_ELEMENT; +export const MIN_TRANSFER_BUFFER_LENGTH = + MIN_DATA_BUFFER_LENGTH + TRANSFER_OVERHEAD_LENGTH; \ No newline at end of file diff --git a/packages/static-module-record/test/fixtures/large.js b/packages/static-module-record/test/fixtures/large.js new file mode 100644 index 0000000000..17946914a8 --- /dev/null +++ b/packages/static-module-record/test/fixtures/large.js @@ -0,0 +1,651 @@ +/* eslint-disable */ +console.error("This is a code sample for trying out babel transforms, it's not meant to be run"); + +import * as h from './hidden.js'; + +/* + * Collects all of the identifiers on the left-hand-side of an exported + * assignment expression, deeply exploring complex destructuring assignment. + * In an export assignment, every one of these identifiers is an exported name. + * + * ``` + * export const pattern = ...; + * export let pattern = ...; + * export var pattern = ...; + * ``` + */ +const collectPatternIdentifiers = (path, pattern) => { + switch (pattern.type) { + case 'Identifier': + return [pattern]; + case 'RestElement': + return collectPatternIdentifiers(path, pattern.argument); + case 'ObjectProperty': + return collectPatternIdentifiers(path, pattern.value); + case 'ObjectPattern': + return pattern.properties.flatMap(prop => + collectPatternIdentifiers(path, prop), + ); + case 'ArrayPattern': + return pattern.elements.flatMap(pat => { + if (pat === null) return []; + // Non-elided pattern. + return collectPatternIdentifiers(path, pat); + }); + default: + throw path.buildCodeFrameError( + `Pattern type ${pattern.type} is not recognized`, + ); + } +}; + +function makeModulePlugins(options) { + const { + sourceType, + exportAlls, + fixedExportMap, + imports, + importDecls, + importSources, + liveExportMap, + importMeta, + } = options; + + if (sourceType !== 'module') { + throw new Error(`Module sourceType must be 'module'`); + } + + const updaterSources = Object.create(null); + /** + * Indicates that a name is declared at the top level and is never + * reassigned. + * All of these declarations are discovered in the analysis pass by visiting + * every function, class, and declaration. + * + * @type {Record} + */ + const topLevelIsOnce = Object.create(null); + /** + * Indicates that a local name is declared at the top level and exported, and + * lists all of the corresponding exported names that should be updated if it + * changes. + * All of these declarations are discovered in the analysis pass by visiting + * every export declaration. + * + * @type {Record} + */ + const topLevelExported = Object.create(null); + + const rewriteModules = pass => ({ types: t }) => { + const replace = ( + src, + node = t.expressionStatement(t.identifier('null')), + ) => { + node.loc = src.loc; + node.comments = [...(src.leadingComments || [])]; + t.inheritsComments(node, src); + return node; + }; + + const prependReplacements = (replacements, node) => { + replacements.unshift(node); + }; + + const allowedHiddens = new WeakSet(); + const rewrittenDecls = new WeakSet(); + const hiddenIdentifier = hi => { + const ident = t.identifier(hi); + allowedHiddens.add(ident); + return ident; + }; + const hOnceId = hiddenIdentifier(h.HIDDEN_ONCE); + const hLiveId = hiddenIdentifier(h.HIDDEN_LIVE); + const soften = id => { + // Remap the name to $c_name. + const { name } = id; + id.name = `${h.HIDDEN_CONST_VAR_PREFIX}${name}`; + allowedHiddens.add(id); + }; + + /** + * Adds an exported name to the module static record private metadata, + * indicating that it is updated live as opposted to a constant + * or variable that is only initialized once and never reassigned. + * + * Any top-level exported `function`, `let`, or `var` declaration is a + * "live" binding unless it's initialized and never reassigned anywhere + * in the module. + * As are any other top-level exported `var` declarations because they + * require hoisting. + * + * This method gets called in the transform phase. + * The returned hidden variable name may be used to transform + * a declaration, particularly for an export class statement. + * + * @param {string} name - the local name of the exported variable. + */ + const markLiveExport = name => { + topLevelExported[name].forEach(importTo => { + liveExportMap[importTo] = [name, true]; + }); + return hLiveId; + }; + + /** + * Adds an exported name to the module static record private metadata, + * indicating that it is updated fixed, either because it is a constant + * or because it is initialized and never reassigned. + * + * This method gets called in the transform phase. + * The returned hidden variable name may be used to transform + * a declaration, particularly for an export class statement. + * + * @param {string} name - the local name of the exported variable. + */ + const markFixedExport = name => { + topLevelExported[name].forEach(importTo => { + fixedExportMap[importTo] = [name]; + }); + return hOnceId; + }; + + /** + * Adds an exported name to the module static record private metadata, + * indicating whether it is fixed or live depending on whether + * there are any assignments to the bound variable except for + * its declaration. + * + * This function gets called in the cases where whether the export is + * live or fixed depends only on whether the export gets assigned + * anywhere outside its declaration: exported function declarations and + * exported variables initialized to function declarations. + * + * This method gets called in the transform phase. + * The returned hidden variable name may be used to transform + * a declaration, particularly for an export class statement. + * + * @param {string} name - the local name of the exported variable. + */ + const markExport = name => { + if (topLevelIsOnce[name]) { + return markFixedExport(name); + } else { + return markLiveExport(name); + } + }; + + const rewriteVars = (vids, isConst, needsHoisting) => { + const replacements = []; + for (const id of vids) { + const { name } = id; + if (!isConst && !topLevelIsOnce[name]) { + if (topLevelExported[name]) { + // Just add $h_live.name($c_name); + soften(id); + replacements.push( + t.expressionStatement( + t.callExpression( + t.memberExpression(hLiveId, t.identifier(name)), + [t.identifier(id.name)], + ), + ), + ); + markLiveExport(name); + } else { + // Make this variable mutable with: let name = $c_name; + soften(id); + replacements.push( + t.variableDeclaration('let', [ + t.variableDeclarator(t.identifier(name), t.identifier(id.name)), + ]), + ); + markLiveExport(name); + } + } else if (topLevelExported[name]) { + if (needsHoisting) { + // Hoist the declaration and soften. + if (needsHoisting === 'function') { + if (!topLevelIsOnce[name]) { + soften(id); + } + options.hoistedDecls.push([name, topLevelIsOnce[name], id.name]); + markExport(name); + } else { + // Rewrite to be just name = value. + soften(id); + options.hoistedDecls.push([name]); + replacements.push( + t.expressionStatement( + t.assignmentExpression( + '=', + t.identifier(name), + t.identifier(id.name), + ), + ), + ); + markLiveExport(name); + } + } else { + // Just add $h_once.name(name); + replacements.push( + t.expressionStatement( + t.callExpression( + t.memberExpression(hOnceId, t.identifier(id.name)), + [t.identifier(id.name)], + ), + ), + ); + markFixedExport(name); + } + } + } + return replacements; + }; + + const rewriteExportDeclaration = path => { + // Find all the declared identifiers. + if (rewrittenDecls.has(path.node)) { + return; + } + const decl = path.node; + const declarations = decl.declarations || [decl]; + const vids = declarations.flatMap(({ id }) => + collectPatternIdentifiers(path, id), + ); + + // Create the export calls. + const isConst = decl.kind === 'const'; + const replacements = rewriteVars( + vids, + isConst, + decl.type === 'FunctionDeclaration' + ? 'function' + : !isConst && decl.kind !== 'let', + ); + + if (replacements.length > 0) { + switch (decl.type) { + case 'VariableDeclaration': { + // We rewrote the declaration. + rewrittenDecls.add(decl); + prependReplacements(replacements, decl); + break; + } + case 'FunctionDeclaration': { + prependReplacements(replacements, decl); + break; + } + default: { + throw TypeError(`Unknown declaration type ${decl.type}`); + } + } + console.error(decl.declarations[0]) + + path.traverse({ + MetaProperty(pathWithin) { + if ( + pathWithin.node.meta && + pathWithin.node.meta.name === 'import' && + pathWithin.node.property.name === 'meta' + ) { + console.error('this works') + + pathWithin.replaceWithMultiple([ + replace(pathWithin.node, hiddenIdentifier(h.HIDDEN_META)), + ]); + + } + }, + }); + console.error(decl.declarations[0]) + + path.replaceWithMultiple(replacements); + } + }; + + const visitor = { + Identifier(path) { + if (options.allowHidden || allowedHiddens.has(path.node)) { + return; + } + // Ensure the parse doesn't already include our required hidden identifiers. + // console.log(`have identifier`, path.node); + const i = h.HIDDEN_IDENTIFIERS.indexOf(path.node.name); + if (i >= 0) { + throw path.buildCodeFrameError( + `The ${h.HIDDEN_IDENTIFIERS[i]} identifier is reserved`, + ); + } + if (path.node.name.startsWith(h.HIDDEN_CONST_VAR_PREFIX)) { + throw path.buildCodeFrameError( + `The ${path.node.name} constant variable is reserved`, + ); + } + }, + CallExpression(path) { + // import(FOO) -> $h_import(FOO) + if (path.node.callee.type === 'Import') { + path.node.callee = hiddenIdentifier(h.HIDDEN_IMPORT); + } + }, + }; + + const moduleVisitor = (doAnalyze, doTransform) => ({ + MetaProperty(path) { + if ( + path.node.meta && + path.node.meta.name === 'import' && + path.node.property.name === 'meta' + ) { + if (doAnalyze) { + importMeta.uttered = true; + } + if (doTransform) { + console.error('at least I tried') + path.replaceWithMultiple([ + replace(path.node, hiddenIdentifier(h.HIDDEN_META)), + ]); + } + } + }, + // We handle all the import and export productions. + ImportDeclaration(path) { + if (doAnalyze) { + const specs = path.node.specifiers; + const specifier = path.node.source.value; + let myImportSources = importSources[specifier]; + if (!myImportSources) { + myImportSources = Object.create(null); + importSources[specifier] = myImportSources; + } + let myImports = imports[specifier]; + if (!myImports) { + myImports = []; + imports[specifier] = myImports; + } + if (!specs) { + return; + } + specs.forEach(spec => { + const importTo = spec.local.name; + importDecls.push(importTo); + let importFrom; + switch (spec.type) { + // import importTo from 'module'; + case 'ImportDefaultSpecifier': + importFrom = 'default'; + break; + // import * as importTo from 'module'; + case 'ImportNamespaceSpecifier': + importFrom = '*'; + break; + // import { importFrom as importTo } from 'module'; + case 'ImportSpecifier': + importFrom = spec.imported.name; + break; + default: + throw path.buildCodeFrameError( + `Unrecognized import specifier type ${spec.type}`, + ); + } + if (myImports && myImports.indexOf(importFrom) < 0) { + myImports.push(importFrom); + } + + if (myImportSources) { + let myUpdaterSources = myImportSources[importFrom]; + if (!myUpdaterSources) { + myUpdaterSources = []; + myImportSources[importFrom] = myUpdaterSources; + } + + myUpdaterSources.push( + `${h.HIDDEN_A} => (${importTo} = ${h.HIDDEN_A})`, + ); + updaterSources[importTo] = myUpdaterSources; + } + }); + } + if (doTransform) { + // Nullify the import declaration. + path.replaceWithMultiple([]); + } + }, + ExportDefaultDeclaration(path) { + // export default FOO -> $h_once.default(FOO) + if (doAnalyze) { + fixedExportMap.default = ['default']; + } + if (doTransform) { + const id = t.identifier('default'); + const cid = t.identifier('default'); + soften(cid); + const callee = t.memberExpression( + hiddenIdentifier(h.HIDDEN_ONCE), + id, + ); + let expr = path.node.declaration; + const decl = path.node.declaration; + if (expr.type === 'ClassDeclaration') { + expr = t.classExpression(expr.id, expr.superClass, expr.body); + } else if (expr.type === 'FunctionDeclaration') { + expr = t.functionExpression( + expr.id, + expr.params, + expr.body, + expr.generator, + expr.async, + ); + } + + if (decl.id) { + // Just keep the same declaration and mark it as the default. + path.replaceWithMultiple([ + replace(path.node, decl), + t.expressionStatement(t.callExpression(callee, [decl.id])), + ]); + return; + } + + // const {default: $c_default} = {default: (XXX)}; $h_once.default($c_default); + path.replaceWithMultiple([ + replace( + path.node, + t.variableDeclaration('const', [ + t.variableDeclarator( + t.objectPattern([t.objectProperty(id, cid)]), + t.objectExpression([t.objectProperty(id, expr)]), + ), + ]), + ), + t.expressionStatement(t.callExpression(callee, [cid])), + ]); + } + }, + ClassDeclaration(path) { + const ptype = path.parent.type; + if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') { + return; + } + + const { name } = path.node.id; + if (doAnalyze) { + topLevelIsOnce[name] = path.scope.getBinding(name).constant; + } + if (doTransform) { + if (topLevelExported[name]) { + const callee = t.memberExpression(markExport(name), path.node.id); + path.replaceWithMultiple([ + path.node, + t.expressionStatement(t.callExpression(callee, [path.node.id])), + ]); + } + } + }, + FunctionDeclaration(path) { + const ptype = path.parent.type; + if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') { + return; + } + + const { name } = path.node.id; + if (doAnalyze) { + topLevelIsOnce[name] = path.scope.getBinding(name).constant; + // console.error('have function', name, 'is', topLevelIsOnce[name]); + } + if (doTransform) { + if (topLevelExported[name]) { + rewriteExportDeclaration(path); + markExport(name); + } + } + }, + VariableDeclaration(path) { + const ptype = path.parent.type; + if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') { + return; + } + + // We may need to rewrite this topLevelDecl later. + const vids = path.node.declarations.flatMap(({ id }) => + collectPatternIdentifiers(path, id), + ); + if (doAnalyze) { + vids.forEach(({ name }) => { + topLevelIsOnce[name] = path.scope.getBinding(name).constant; + }); + } + if (doTransform) { + for (const { name } of vids) { + if (topLevelExported[name]) { + rewriteExportDeclaration(path); + break; + } + } + } + }, + ExportAllDeclaration(path) { + const { source } = path.node; + if (doAnalyze) { + const specifier = source.value; + let myImportSources = importSources[specifier]; + if (!myImportSources) { + myImportSources = Object.create(null); + importSources[specifier] = myImportSources; + } + let myImports = imports[specifier]; + if (!myImports) { + // Ensure that the specifier is imported. + myImports = []; + imports[specifier] = myImports; + } + exportAlls.push(specifier); + } + if (doTransform) { + path.replaceWithMultiple([]); + } + }, + ExportNamedDeclaration(path) { + const { declaration: decl, specifiers: specs, source } = path.node; + + if (doAnalyze) { + let myImportSources; + let myImports; + if (source) { + const specifier = source.value; + myImportSources = importSources[specifier]; + if (!myImportSources) { + myImportSources = Object.create(null); + importSources[specifier] = myImportSources; + } + myImports = imports[specifier]; + if (!myImports) { + myImports = []; + imports[specifier] = myImports; + } + } + + if (decl) { + const declarations = decl.declarations || [decl]; + const vids = declarations.flatMap(({ id }) => + collectPatternIdentifiers(path, id), + ); + vids.forEach(({ name }) => { + let tle = topLevelExported[name]; + if (!tle) { + tle = []; + topLevelExported[name] = tle; + } + tle.push(name); + }); + } + + specs.forEach(spec => { + const { local, exported } = spec; + const importFrom = + spec.type === 'ExportNamespaceSpecifier' ? '*' : local.name; + + // If local.name is reexported we omit it. + const importTo = exported.name; + let myUpdaterSources = updaterSources[importFrom]; + if (myImportSources) { + myUpdaterSources = myImportSources[importFrom]; + if (!myUpdaterSources) { + myUpdaterSources = []; + myImportSources[importFrom] = myUpdaterSources; + } + updaterSources[importTo] = myUpdaterSources; + myImports.push(importFrom); + } + + if (myUpdaterSources) { + // If there are updaters, we must have a local + // name, so update it with this export. + const ident = topLevelIsOnce[importFrom] + ? h.HIDDEN_ONCE + : h.HIDDEN_LIVE; + myUpdaterSources.push(`${ident}[${JSON.stringify(importFrom)}]`); + } + + if (source || myUpdaterSources) { + // Not declared, so make it a live export without proxy. + liveExportMap[importTo] = [importFrom, false]; + } else { + let tle = topLevelExported[importFrom]; + if (!tle) { + tle = []; + topLevelExported[importFrom] = tle; + } + tle.push(importTo); + } + }); + } + if (doTransform) { + path.replaceWithMultiple(decl ? [replace(path.node, decl)] : []); + } + }, + }); + + // Add the module visitor. + switch (pass) { + case 0: + return { + visitor: { + ...visitor, + ...moduleVisitor(true, false), + }, + }; + case 1: + return { visitor: moduleVisitor(false, true) }; + default: + throw TypeError(`Unrecognized module pass ${pass}`); + } + }; + + return { + analyzePlugin: rewriteModules(0), + transformPlugin: rewriteModules(1), + }; +} + +export default makeModulePlugins; diff --git a/packages/static-module-record/test/fixtures/small.js b/packages/static-module-record/test/fixtures/small.js new file mode 100644 index 0000000000..c390bced60 --- /dev/null +++ b/packages/static-module-record/test/fixtures/small.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +console.error("This is a code sample for trying out babel transforms, it's not meant to be run"); +import * as babelParser from '@babel/parser'; +import babelGenerate from '@agoric/babel-generator'; +import babelTraverse from '@babel/traverse'; +import babelTypes from '@babel/types'; + +import makeModulePlugins from './babelPlugin.js'; + +const parseBabel = babelParser.default + ? babelParser.default.parse + : babelParser.parse || babelParser; + +const visitorFromPlugin = plugin => plugin({ types: babelTypes }).visitor; + +const traverseBabel = babelTraverse.default || babelTraverse; +const generateBabel = babelGenerate.default || babelGenerate; + +export const makeTransformSource = (babel = null) => { + if (babel !== null) { + throw new Error( + `transform-analyze.js no longer allows injecting babel; use \`null\``, + ); + } + + const transformSource = (code, sourceOptions = {}) => { + // console.log(`transforming`, sourceOptions, code); + const { analyzePlugin, transformPlugin } = makeModulePlugins(sourceOptions); + + const ast = parseBabel(code, { sourceType: sourceOptions.sourceType }); + + traverseBabel(ast, visitorFromPlugin(analyzePlugin)); + traverseBabel(ast, visitorFromPlugin(transformPlugin)); + + const { code: transformedCode } = generateBabel(ast, { + retainLines: true, + compact: true, + verbatim: true, + }); + return transformedCode; + }; + + return transformSource; +}; diff --git a/packages/static-module-record/test/test-static-module-record.js b/packages/static-module-record/test/test-static-module-record.js index f729b6119c..4c9fe48ad2 100644 --- a/packages/static-module-record/test/test-static-module-record.js +++ b/packages/static-module-record/test/test-static-module-record.js @@ -64,7 +64,7 @@ test('export default', t => { function initialize(t, source, options = {}) { const { endowments, imports = new Map() } = options; const record = new StaticModuleRecord(source); - // t.log(record.__syncModuleProgram__); + t.log(record.__syncModuleProgram__); const liveUpdaters = {}; const onceUpdaters = {}; const namespace = {}; @@ -105,7 +105,6 @@ function initialize(t, source, options = {}) { }; }, ); - const functor = compartment.evaluate(record.__syncModuleProgram__); /** @type {Map>} */ @@ -560,13 +559,16 @@ test('import for side-effect', t => { test('import meta', t => { t.notThrows(() => initialize(t, `const a = import.meta.url`)); }); -test.failing('import meta in export', t => { +test('import meta in export', t => { let namespace = {}; t.notThrows(() => { - namespace = initialize(t, `export const a = 'ok ' + import.meta.url`) - .namespace; + namespace = initialize( + t, + `export const a = 'ok ' + import.meta.url; + const unrelated = {b:import.meta.url};`, + ).namespace; }); - t.is(namespace.result, 'ok file://meta.url'); + t.is(namespace.a, 'ok file://meta.url'); }); test('import meta member uttered', t => { const record = new StaticModuleRecord(`const a = import.meta.url`); diff --git a/yarn.lock b/yarn.lock index c3080b9c5c..c390735bab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3019,6 +3019,14 @@ before-after-hook@^2.0.0, before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.1.tgz#73540563558687586b52ed217dad6a802ab1549c" integrity sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw== +benchmark@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ== + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -9636,6 +9644,18 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= + dependencies: + find-up "^2.1.0" + +platform@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + plur@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/plur/-/plur-4.0.0.tgz#729aedb08f452645fe8c58ef115bf16b0a73ef84"