rewrite in terms of two AST passes
The first pass analyzes the exports and how the symbols are used,
the second pass transforms the code.  This is much more robust.
MODIFY
REWRITE.md
REWRITE.md
+For each top level variable declaration, we make an entry
+from a variable name to the variable declaration path. That allows
+us to look up the path.scope.bindings[name] to see if constant === true.
+
+If anything from the declaration was exported, we change the declaration
+to `const`. After, the rest of the rewrite introduces either:
+
+```js
+$h_once.name(name); // only if was const or let but constant === true
+$h_live.name($c_name); // only if exported let
+let name = $c_name; // only if non-exported let
+```
+
## export { vname }
```js
const cv = 123; // const, guaranteed not modified
-let mv = 456; mv += 1; // modified
-let lv = 789; // not modified
+let [mv, lv, uv] = [456, 789]; // modified, not modified, unexported
+mv += 1;
export { cv as cv1, mv as mv1, lv as lv1 };
```
```js
const cv = 123; $h_once.cv(cv); // const, guaranteed not modified
-{ const mv = 456; $h_live.mv(mv); } mv += 1; // modified
-const lv = 789; $h_once.lv(lv); // not modified
+const [$c_mv, lv, $c_uv] = [456, 789]; \
+ $h_live.mv($c_mv); $h_once.lv(lv); let uv = $c_uv; // modified, not modified, unexported
+mv += 1;
```
## export const
@@ -21,7 +35,7 @@ export const { abc: abc2, nest: [def2] } = obj;
```js
// temporal dead zone for const decls until...
-const { abc: abc2, nest: [def2] } = obj;
+const { abc: abc2, nest: [def2] } = obj; \
$h_once.abc2(abc2); $h_once.def2(def2); // ... here.
```
@@ -33,13 +47,8 @@ export let { abc: abc2, nest: [def2] } = obj;
```js
// temporal dead zone for let decls until...
-{
- // Private scope so that the let expression can be
- // reused without interfering with the proxy traps.
- let { abc: abc2, nest: [def2] } = obj;
- $h_live.abc2(abc2); $h_live.def2(def2); // ... here.
- undefined;
-};
+const { abc: $c_abc2, nest: [$c_def2] } = obj; \
+ $h_live.abc2($c_abc2); $h_live.def2($c_def2); // ... here.
```
## export var
@@ -51,13 +60,10 @@ export var { abc: abc2, nest: [def2] } = obj;
```js
$h_live.abc2(); $h_live.def2(); // hoisted decls (no tdz)
...
-{
// Same as `let`, since our proxy traps will treat the
// identifier as a hoisted declaration.
- let { abc: abc2, nest: [def2] } = obj;
- $h_live.abc2(abc2); $h_live.def2(def2);
- undefined;
-}
+const { abc: $c_abc2, nest: [$c_def2] } = obj; \
+ $h_live.abc2($c_abc2); $h_live.def2($c_def2);
```
## export function
@@ -71,13 +77,8 @@ export function fn() {
```js
$h_live.fn(); // hoisted decl (no tdz)
...
-(() => {
- // Use an IIFE to preserve function semantics of `fn`
- // without polluting the global scope (we need the proxy
- // traps for fn)
- function fn() {
+function $c_fn() {
...
- }
- $h_live.fn(fn);
-})();
+} \
+$h_live.fn($c_fn);
```

MODIFY
src/babelPlugin.js
src/babelPlugin.js
@@ -29,8 +29,7 @@ const collectPatternIdentifiers = (path, pattern) => {
}
};
-export default options =>
- function rewriteModules({ types: t }) {
+function makeModulePlugins(options) {
const {
fixedExportMap,
imports,
@@ -38,115 +37,118 @@ export default options =>
importSources,
liveExportMap,
} = options;
+ const updaterSources = Object.create(null);
+ const topLevelIsOnce = Object.create(null);
+ const topLevelExported = Object.create(null);
+
+ const rewriteModules = pass => ({ types: t }) => {
const allowedHiddens = new WeakSet();
- const exportedVariableDecls = new WeakSet();
+ const constifiedDecls = new WeakSet();
const hiddenIdentifier = hi => {
const ident = t.identifier(hi);
allowedHiddens.add(ident);
return ident;
};
- const updaterSources = {};
- const topLevelDecls = {};
+ 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);
+ };
- const rewriteExportDecl = (path, decl) => {
- // Find all the declared identifiers.
- const replace = [];
- const vids = (decl.declarations || [decl]).reduce(
- (prior, { id: pat }) => {
- prior.push(...collectPatternIdentifiers(path, pat));
- return prior;
- },
- [],
+ const rewriteVars = (vids, isConst, needsHoisting) =>
+ vids.reduce((prior, id) => {
+ const { name } = id;
+ if (!isConst && !topLevelIsOnce[name]) {
+ if (topLevelExported[name]) {
+ // Just add $h_live.name($c_name);
+ soften(id);
+ prior.push(
+ t.expressionStatement(
+ t.callExpression(
+ t.memberExpression(hLiveId, t.identifier(name)),
+ [id],
+ ),
+ ),
);
- const vnames = vids.map(({ name }) => name);
-
- // TODO: Detect if a non-const declaration is actually fixed
- // (no other assignments to it).
- const isLive = decl.kind !== 'const';
- if (isLive) {
- // These exports may potentially change.
- vnames.forEach(vname => (liveExportMap[vname] = [vname, true]));
+ liveExportMap[name] = [name, true];
} else {
- // Fixed exports (i.e. const or non-mutated)
- vnames.forEach(vname => (fixedExportMap[vname] = [vname]));
- }
-
- // Create the export calls.
- const exportFname = hiddenIdentifier(
- isLive ? h.HIDDEN_LIVE : h.HIDDEN_ONCE,
+ // Make this variable mutable with: let name = $c_name;
+ soften(id);
+ prior.push(
+ t.variableDeclaration('let', [
+ t.variableDeclarator(t.identifier(name), id),
+ ]),
);
- const exportCalls = vids.map(id =>
+ }
+ } else if (topLevelExported[name]) {
+ if (needsHoisting) {
+ // Hoist the declaration and soften.
+ soften(id);
+ options.hoistedDecls.push(name);
+ // Rewrite to be just name = value.
+ prior.push(
t.expressionStatement(
- t.callExpression(t.memberExpression(exportFname, id), [id]),
+ t.assignmentExpression('=', t.identifier(name), id),
),
);
-
- let rewriteKind;
- if (decl.type === 'FunctionDeclaration') {
- // Hoist the name, and rewrite as an IIFE.
- options.hoistedDecls.push(...vnames);
- rewriteKind = 'function';
- } else if (decl.type !== 'VariableDeclaration') {
- throw path.buildCodeFrameError(
- `Unrecognized declaration type ${decl.type}`,
+ return prior;
+ }
+ // Just add $h_once.name(name);
+ prior.push(
+ t.expressionStatement(
+ t.callExpression(t.memberExpression(hOnceId, id), [id]),
+ ),
);
- } else if (decl.kind === 'var') {
- // Save the hoistedDecls so that we don't have a
- // temporal dead zone for them.
- options.hoistedDecls.push(...vnames);
+ fixedExportMap[name] = [name];
+ }
+ return prior;
+ }, []);
- // Use a let declaration in our rewrite.
- rewriteKind = 'let';
- } else {
- rewriteKind = decl.kind;
+ const rewriteDeclaration = path => {
+ // Find all the declared identifiers.
+ if (constifiedDecls.has(path.node)) {
+ return;
}
+ const decl = path.node;
+ const declarations =
+ decl.type === 'FunctionDeclaration' ? [decl] : decl.declarations;
+ const vids = declarations.reduce((prior, { id: pat }) => {
+ prior.push(...collectPatternIdentifiers(path, pat));
+ return prior;
+ }, []);
- switch (rewriteKind) {
- case 'const':
- // Just put the declaration and export calls next to each other.
- // This optimizes by shadowing the endowed proxy trap.
- replace.push(decl, ...exportCalls);
- break;
- case 'let':
- // Replace with a block so that the let declarations
- // don't shadow the endowed proxy trap.
- replace.push(
- t.blockStatement([
- // The let declaration,
- { ...decl, kind: rewriteKind },
- // all the export calls,
- ...exportCalls,
- ]),
+ // Create the export calls.
+ const isConst = decl.kind === 'const';
+ const replace = rewriteVars(
+ vids,
+ isConst,
+ !isConst && decl.kind !== 'let',
);
- break;
- case 'function':
- // Replace with an IIFE to preserve the function
- // semantics without shadowing the endowed proxy trap.
- replace.push(
- t.expressionStatement(
- t.callExpression(
- t.arrowFunctionExpression(
- [], // no params
- t.blockStatement([
- // The function declaration.
- decl,
- // all the export calls,
- ...exportCalls,
- ]),
- ),
- [], // no args
- ),
- ),
- );
+ if (replace.length > 0) {
+ switch (decl.type) {
+ case 'VariableDeclaration': {
+ // Need to constify the declaration.
+ const constDecl = { ...decl, kind: 'const' };
+ constifiedDecls.add(constDecl);
+ replace.unshift(constDecl);
break;
-
- default:
- throw path.buildCodeFrameError(
- `Unrecognized rewrite kind ${rewriteKind}`,
- );
}
- return replace;
+ case 'FunctionDeclaration': {
+ replace.unshift(decl);
+ break;
+ }
+ default: {
+ throw TypeError(`Unknown declaration type ${decl.type}`);
+ }
+ }
+ }
+ if (replace.length > 0) {
+ path.replaceWithMultiple(replace);
+ }
};
const visitor = {
@@ -162,6 +164,11 @@ export default options =>
`The ${h.HIDDEN_SYMBOLS[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)
@@ -171,57 +178,10 @@ export default options =>
},
};
- const moduleVisitor = {
- Program: {
- exit(_path) {
- const rewriteMap = new Map();
- [
- [fixedExportMap, hiddenIdentifier(h.HIDDEN_ONCE)],
- [liveExportMap, hiddenIdentifier(h.HIDDEN_LIVE)],
- ].forEach(([exportMap, exportId]) => {
- Object.entries(exportMap).forEach(([_exportName, [localName]]) => {
- // Find all the traversal paths for the local names.
- // console.error(`adding`, _exportName, localName, topLevelDecls);
- if (!localName) {
- return;
- }
- const path = topLevelDecls[localName];
- if (!path) {
- return;
- }
- let rewrites = rewriteMap.get(path);
- if (!rewrites) {
- // Ensure the local name will be rewritten.
- rewrites = new Map();
- rewriteMap.set(path, rewrites);
- }
- rewrites.set(localName, exportId);
- });
- });
- rewriteMap.forEach((rewrites, path) => {
- // console.log(`rewriteMap`, rewrites, path);
- const body = [path.node];
- // Rewrite the traversal paths for the export identifier.
- rewrites.forEach((exportId, vname) => {
- // console.error(`replacing`, vname, exportId);
- const callee = t.memberExpression(exportId, t.identifier(vname));
- body.push(
- t.expressionStatement(
- t.callExpression(callee, [t.identifier(vname)]),
- ),
- );
- });
- if (path.node.kind === 'const') {
- path.replaceWithMultiple(body);
- } else {
- path.replaceWith(t.blockStatement(body));
- }
- });
- },
- },
-
+ const moduleVisitor = (doAnalyze, doTransform) => ({
// 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];
@@ -276,23 +236,44 @@ export default options =>
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 callee = t.memberExpression(
hiddenIdentifier(h.HIDDEN_ONCE),
t.identifier('default'),
);
path.replaceWith(t.callExpression(callee, [path.node.declaration]));
+ }
+ },
+ 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;
+ }
+ if (doTransform) {
+ if (topLevelExported[name]) {
+ rewriteDeclaration(path);
+ }
+ }
},
VariableDeclaration(path) {
- if (
- path.parent.type !== 'Program' ||
- exportedVariableDecls.has(path.node)
- ) {
+ const ptype = path.parent.type;
+ if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') {
return;
}
@@ -301,14 +282,24 @@ export default options =>
prior.push(...collectPatternIdentifiers(path, pat));
return prior;
}, []);
- vids.forEach(vid => {
- topLevelDecls[vid.name] = path;
+ if (doAnalyze) {
+ vids.forEach(({ name }) => {
+ topLevelIsOnce[name] = path.scope.getBinding(name).constant;
});
+ }
+ if (doTransform) {
+ for (const { name } of vids) {
+ if (topLevelExported[name]) {
+ rewriteDeclaration(path);
+ break;
+ }
+ }
+ }
},
ExportNamedDeclaration(path) {
const { declaration: decl, specifiers: specs, source } = path.node;
- const replace = [];
+ if (doAnalyze) {
let myImportSources;
let myImports;
if (source) {
@@ -326,9 +317,15 @@ export default options =>
}
if (decl) {
- exportedVariableDecls.add(decl);
- replace.push(...rewriteExportDecl(path, decl));
+ const declarations =
+ decl.type === 'FunctionDeclaration' ? [decl] : decl.declarations;
+ const vids = declarations.reduce((prior, { id: pat }) => {
+ prior.push(...collectPatternIdentifiers(path, pat));
+ return prior;
+ }, []);
+ vids.forEach(({ name }) => (topLevelExported[name] = true));
}
+
specs.forEach(spec => {
const { local, exported } = spec;
@@ -352,21 +349,44 @@ export default options =>
myUpdaterSources.push(`${h.HIDDEN_LIVE}.${importFrom}`);
}
- // If it was imported directly (i.e. has a source or updaters)
- // then don't set the proxy trap.
- liveExportMap[exported.name] = [
- importFrom,
- !(source || myUpdaterSources),
- ];
+ if (source || myUpdaterSources) {
+ // Not declared, so make it a live export without proxy.
+ liveExportMap[importTo] = [importFrom, false];
+ } else {
+ topLevelExported[importFrom] = true;
+ }
});
-
- path.replaceWithMultiple(replace);
+ }
+ if (doTransform) {
+ path.replaceWithMultiple(decl ? [decl] : []);
+ }
},
- };
+ });
if (options.sourceType === 'module') {
// Add the module visitor.
- Object.assign(visitor, moduleVisitor);
+ 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 { visitor };
};
+
+ const rewriters = [rewriteModules(0)];
+ if (options.sourceType === 'module') {
+ rewriters.push(rewriteModules(1));
+ }
+ return rewriters;
+}
+
+export default makeModulePlugins;

MODIFY
src/hidden.js
src/hidden.js
export const HIDDEN_PREFIX = '$h\u200d_';
+export const HIDDEN_CONST_VAR_PREFIX = '$c\u200d_';
export const HIDDEN_A = `${HIDDEN_PREFIX}a`;
export const HIDDEN_IMPORT = `${HIDDEN_PREFIX}import`;
export const HIDDEN_IMPORT_SELF = `${HIDDEN_PREFIX}importSelf`;

MODIFY
src/index.js
src/index.js
+/* eslint-disable prefer-destructuring */
import * as h from './hidden';
-import makeModulePlugin from './babelPlugin';
+import makeModulePlugins from './babelPlugin';
const makeModuleTransformer = (babelCore, importer) => {
function transformSource(source, sourceOptions = {}) {
@@ -10,19 +11,39 @@ const makeModuleTransformer = (babelCore, importer) => {
}
// console.log(`transforming`, sourceOptions, source);
- const modulePlugin = makeModulePlugin(sourceOptions);
- const output = babelCore.transform(source, {
+ const modulePlugins = makeModulePlugins(sourceOptions);
+ const output = babelCore.transformSync(source, {
parserOpts: {
plugins: parserPlugins,
},
generatorOpts: {
retainLines: true,
},
- plugins: [modulePlugin],
+ plugins: [modulePlugins[0]],
+ ast: true,
+ code: modulePlugins.length === 1,
});
+ let { ast, code } = output;
+ for (let i = 1; i < modulePlugins.length - 1; i += 1) {
+ const middleOut = babelCore.transformFromAstSync(ast, source, {
+ plugins: [modulePlugins[i]],
+ ast: true,
+ code: false,
+ });
+ ast = middleOut.ast;
+ }
+ if (modulePlugins.length > 1) {
+ const finalOut = babelCore.transformFromAstSync(ast, source, {
+ generatorOpts: {
+ retainLines: true,
+ },
+ plugins: [modulePlugins[modulePlugins.length - 1]],
+ });
+ code = finalOut.code;
+ }
// console.log(`transformed to`, output.code);
- return output.code;
+ return code;
}
function createStaticRecord(moduleSource) {
@@ -80,6 +101,7 @@ const makeModuleTransformer = (babelCore, importer) => {
${scriptSource}
})`;
+//console.log(functorSource);
const moduleStaticRecord = {
moduleSource,
imports: sourceOptions.imports,

MODIFY
test/test-export-named.js
test/test-export-named.js
@@ -61,6 +61,18 @@ test(`export named`, async t => {
transforms,
});
+ t.deepEqual(
+ await evaluateModule(`\
+export let abc = 123;
+export let def = 456;
+export let def2 = def;
+def ++;
+export const ghi = 789;
+`),
+ { abc: 123, def: 457, def2: 456, ghi: 789 },
+ `let exports`,
+ );
+
t.deepEqual(
await evaluateModule(`\
export const abc = 123;
@@ -75,18 +87,6 @@ export const { def, nest: [, ghi, ...nestrest], ...rest } = { def: 456, nest: [
},
`const exports`,
);
-
- t.deepEqual(
- await evaluateModule(`\
-export let abc = 123;
-export let def = 456;
-export let def2 = def;
-def ++;
-export const ghi = 789;
-`),
- { abc: 123, def: 457, def2: 456, ghi: 789 },
- `let exports`,
- );
} catch (e) {
console.log('unexpected exception', e);
t.assert(false, e);

rewrite in terms of two AST passes

The first pass analyzes the exports and how the symbols are used,
the second pass transforms the code.  This is much more robust.