Skip to content

Commit 1fd899f

Browse files
committed
feat(module-source): parser pipeline support
This exposes a new function, `createModuleSourcePasses()` for use with `@endo/parser-pipeline`. Refactored code for reuse between `createModuleSourcePasses()` and `ModuleSource()`
1 parent 30884e4 commit 1fd899f

9 files changed

Lines changed: 531 additions & 99 deletions

File tree

.changeset/ripe-showers-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@endo/module-source': minor
3+
---
4+
5+
Expose `createModuleSourcePasses()` for use with `@endo/parser-pipeline`.

packages/module-source/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
export * from './src/external.types.js';
33

44
export { ModuleSource } from './src/module-source.js';
5+
6+
export { createModuleSourcePasses } from './src/visitor-passes.js';

packages/module-source/src/babel-plugin.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import * as h from './hidden.js';
44

5+
/**
6+
* @import {PluginFactory, TransformSourceParams} from './types/module-source.js'
7+
*/
8+
59
/*
610
* Collects all of the identifiers on the left-hand-side of an exported
711
* assignment expression, deeply exploring complex destructuring assignment.
@@ -40,6 +44,12 @@ const collectPatternIdentifiers = (path, pattern) => {
4044
}
4145
};
4246

47+
/**
48+
* Creates a plugin for the Babel parser that analyzes and transforms module source code.
49+
*
50+
* @param {TransformSourceParams} options
51+
* @returns {{analyzePlugin: PluginFactory, transformPlugin: PluginFactory}}
52+
*/
4353
function makeModulePlugins(options) {
4454
const {
4555
sourceType,
@@ -209,7 +219,7 @@ function makeModulePlugins(options) {
209219
} else {
210220
// Rewrite to be just name = value.
211221
soften(id);
212-
options.hoistedDecls.push([name]);
222+
options.hoistedDecls.push([name, false, undefined]);
213223
replacements.push(
214224
t.expressionStatement(
215225
t.assignmentExpression(
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Shared logic for building the SES functor source string and assembling
3+
* the frozen module record (see {@link ModuleSourceRecord}).
4+
*
5+
* @module
6+
*/
7+
8+
import * as h from './hidden.js';
9+
10+
/**
11+
* @import {ModuleSourceRecord} from './types/module-source.js'
12+
*/
13+
14+
const { keys, values, freeze: objectFreeze } = Object;
15+
16+
/**
17+
* It's {@link objectFreeze | Object.freeze} without the `Readonly`!
18+
*
19+
* @privateRemarks Remove this if we decide to make the fields of `PrecompiledModuleSource` readonly.
20+
*/
21+
const freeze = /** @type {<T>(v: T) => T} */ (objectFreeze);
22+
23+
const { stringify: js } = JSON;
24+
25+
/**
26+
* Builds the SES functor source string from generated code and the analysis
27+
* state accumulated during the analyzer and transform passes.
28+
*
29+
* Constructs the `$h_imports([...])` preamble from `sourceOptions.importSources`,
30+
* `importDecls`, and `hoistedDecls`, then wraps `scriptSource` in the SES
31+
* functor calling convention.
32+
*
33+
* @param {string} scriptSource The code produced by `@babel/generator`
34+
* after all transform passes.
35+
* @param {Record<string, any>} sourceOptions The mutable state bag populated
36+
* by `makeModulePlugins`.
37+
* @param {string} [sourceUrl] The source URL for the module.
38+
* @returns {string} The functor source string.
39+
*/
40+
export const buildFunctorSource = (scriptSource, sourceOptions, sourceUrl) => {
41+
const isrc = sourceOptions.importSources;
42+
43+
let preamble = sourceOptions.importDecls.join(',');
44+
if (preamble !== '') {
45+
preamble = `let ${preamble};`;
46+
}
47+
48+
preamble += `${h.HIDDEN_IMPORTS}([${keys(isrc)
49+
.map(
50+
src =>
51+
`[${js(src)}, [${Object.entries(isrc[src])
52+
.map(([exp, upds]) => `[${js(exp)},[${upds.join(',')}]]`)
53+
.join(',')}]]`,
54+
)
55+
.join(',')}]);`;
56+
57+
preamble += sourceOptions.hoistedDecls
58+
.map(([vname, isOnce, cvname]) => {
59+
let src = '';
60+
if (cvname) {
61+
src = `Object.defineProperty(${cvname},'name',{value:${js(vname)}});`;
62+
}
63+
const hDeclId = isOnce ? h.HIDDEN_ONCE : h.HIDDEN_LIVE;
64+
src += `${hDeclId}.${vname}(${cvname || ''});`;
65+
return src;
66+
})
67+
.join('');
68+
69+
let functorSource = `\
70+
({imports:${h.HIDDEN_IMPORTS},liveVar:${h.HIDDEN_LIVE},onceVar:${h.HIDDEN_ONCE},import:${h.HIDDEN_IMPORT},importMeta:${h.HIDDEN_META}})=>(function(){'use strict';\
71+
${preamble}\
72+
${scriptSource}
73+
})()
74+
`;
75+
76+
if (sourceUrl) {
77+
functorSource += `//# sourceURL=${sourceUrl}\n`;
78+
}
79+
80+
return functorSource;
81+
};
82+
83+
/**
84+
* Computes the derived arrays (`imports`, `exports`, `reexports`) from the
85+
* raw maps in `sourceOptions` and assembles a {@link ModuleSourceRecord}.
86+
*
87+
* @param {Record<string, any>} sourceOptions The mutable state bag populated
88+
* by `makeModulePlugins`.
89+
* @param {string} functorSource The functor source string from
90+
* {@link buildFunctorSource}.
91+
* @returns {ModuleSourceRecord}
92+
*/
93+
export const buildModuleRecord = (sourceOptions, functorSource) => {
94+
for (const entry of values(sourceOptions.liveExportMap)) {
95+
freeze(entry);
96+
}
97+
for (const entry of values(sourceOptions.fixedExportMap)) {
98+
freeze(entry);
99+
}
100+
for (const reexports of values(sourceOptions.reexportMap)) {
101+
for (const pair of reexports) {
102+
freeze(pair);
103+
}
104+
freeze(reexports);
105+
}
106+
107+
return freeze({
108+
imports: freeze([...keys(sourceOptions.imports)]),
109+
exports: freeze(
110+
[
111+
...keys(sourceOptions.liveExportMap),
112+
...keys(sourceOptions.fixedExportMap),
113+
...values(sourceOptions.reexportMap)
114+
.flat()
115+
.map(([_, exportName]) => exportName),
116+
].sort(),
117+
),
118+
reexports: freeze([...sourceOptions.exportAlls].sort()),
119+
__syncModuleProgram__: functorSource,
120+
__liveExportMap__: freeze(sourceOptions.liveExportMap),
121+
__fixedExportMap__: freeze(sourceOptions.fixedExportMap),
122+
__reexportMap__: freeze(sourceOptions.reexportMap),
123+
__needsImport__: sourceOptions.dynamicImport.present,
124+
__needsImportMeta__: sourceOptions.importMeta.present,
125+
});
126+
};

packages/module-source/src/transform-analyze.js

Lines changed: 18 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,28 @@ import { makeTransformSource } from './transform-source.js';
33
import makeModulePlugins from './babel-plugin.js';
44

55
import * as h from './hidden.js';
6+
import { createSourceOptions } from './source-options.js';
7+
import { buildFunctorSource, buildModuleRecord } from './functor.js';
68

7-
const { freeze } = Object;
8-
9-
/** @import {Options} from './module-source.js' */
9+
/** @import {ModuleSourceOptions} from './types/module-source.js' */
1010

1111
const makeCreateStaticRecord = transformSource =>
1212
/**
1313
*
1414
* @param {string} moduleSource
15-
* @param {Options} options
15+
* @param {ModuleSourceOptions} options
1616
*/
1717
function createStaticRecord(
1818
moduleSource,
1919
{ sourceUrl, sourceMapUrl, sourceMap, sourceMapHook } = {},
2020
) {
21-
// Transform the Module source code.
22-
const sourceOptions = {
21+
const sourceOptions = createSourceOptions({
2322
sourceUrl,
2423
sourceMap,
2524
sourceMapUrl,
2625
sourceMapHook,
27-
sourceType: 'module',
28-
// exportNames of variables that are only initialized and used, but
29-
// never assigned to.
30-
fixedExportMap: Object.create(null),
31-
// Record of imported module specifier names to list of importNames.
32-
// The importName '*' is that module's module namespace object.
33-
imports: Object.create(null),
34-
// List of module specifiers that we export all from.
35-
exportAlls: [],
36-
// exportNames of variables that are assigned to, or reexported and
37-
// therefore assumed live. A reexported variable might not have any
38-
// localName.
39-
reexportMap: Object.create(null),
40-
liveExportMap: Object.create(null),
41-
hoistedDecls: [],
42-
importSources: Object.create(null),
43-
importDecls: [],
44-
dynamicImport: { present: false },
45-
// enables passing import.meta usage hints up.
46-
importMeta: { present: false },
47-
};
26+
});
27+
4828
if (moduleSource.startsWith('#!')) {
4929
// Comment out the shebang lines.
5030
moduleSource = `//${moduleSource}`;
@@ -62,71 +42,23 @@ const makeCreateStaticRecord = transformSource =>
6242
);
6343
}
6444

65-
let preamble = sourceOptions.importDecls.join(',');
66-
if (preamble !== '') {
67-
preamble = `let ${preamble};`;
68-
}
69-
const js = JSON.stringify;
70-
const isrc = sourceOptions.importSources;
71-
preamble += `${h.HIDDEN_IMPORTS}([${Object.keys(isrc)
72-
.map(
73-
src =>
74-
`[${js(src)}, [${Object.entries(isrc[src])
75-
.map(([exp, upds]) => `[${js(exp)},[${upds.join(',')}]]`)
76-
.join(',')}]]`,
77-
)
78-
.join(',')}]);`;
79-
preamble += sourceOptions.hoistedDecls
80-
.map(([vname, isOnce, cvname]) => {
81-
let src = '';
82-
if (cvname) {
83-
// It's a function assigned to, so set its name property.
84-
src = `Object.defineProperty(${cvname},'name',{value:${js(vname)}});`;
85-
}
86-
const hDeclId = isOnce ? h.HIDDEN_ONCE : h.HIDDEN_LIVE;
87-
src += `${hDeclId}.${vname}(${cvname || ''});`;
88-
return src;
89-
})
90-
.join('');
91-
92-
// The outer function destructures the module calling convention's internal
93-
// variables into hidden lexical variables.
94-
// The inner function binds `this` to `undefined` and overshadows the
95-
// evaluator's `arguments` with a completely empty `arguments` object.
96-
// There is no avoiding the overshadowing of `globalThis.arguments` if it
97-
// exists in this emulation of ESM since the evaluator binds `arguments` as
98-
// well.
99-
// Relies on the evaluator to ensure these functions are strict.
100-
let functorSource = `\
101-
({imports:${h.HIDDEN_IMPORTS},liveVar:${h.HIDDEN_LIVE},onceVar:${h.HIDDEN_ONCE},import:${h.HIDDEN_IMPORT},importMeta:${h.HIDDEN_META}})=>(function(){'use strict';\
102-
${preamble}\
103-
${scriptSource}
104-
})()
105-
`;
45+
const functorSource = buildFunctorSource(
46+
scriptSource,
47+
sourceOptions,
48+
sourceUrl,
49+
);
10650

107-
if (sourceUrl) {
108-
functorSource += `//# sourceURL=${sourceUrl}\n`;
109-
}
110-
const moduleAnalysis = freeze({
111-
exportAlls: freeze(sourceOptions.exportAlls),
112-
imports: freeze(sourceOptions.imports),
113-
liveExportMap: freeze(sourceOptions.liveExportMap),
114-
fixedExportMap: freeze(sourceOptions.fixedExportMap),
115-
reexportMap: freeze(sourceOptions.reexportMap),
116-
needsImport: sourceOptions.dynamicImport.present,
117-
needsImportMeta: sourceOptions.importMeta.present,
118-
functorSource,
119-
});
120-
return moduleAnalysis;
51+
return buildModuleRecord(sourceOptions, functorSource);
12152
};
12253

123-
export const makeModuleAnalyzer = babel => {
124-
const transformSource = makeTransformSource(makeModulePlugins, babel);
54+
export const makeModuleAnalyzer = () => {
55+
const transformSource = makeTransformSource(makeModulePlugins);
12556
return makeCreateStaticRecord(transformSource);
12657
};
12758

128-
export const makeModuleTransformer = (babel, importer) => {
129-
const transformSource = makeTransformSource(makeModulePlugins, babel);
59+
// TODO: May be unused; referenced only in ses/test262
60+
export const makeModuleTransformer = (_babel, importer) => {
61+
const transformSource = makeTransformSource(makeModulePlugins);
13062
const createStaticRecord = makeCreateStaticRecord(transformSource);
13163
return {
13264
rewrite(ss) {

0 commit comments

Comments
 (0)