Closed dckc closed 2 years ago
We should add a carefully chosen subset of the ES module export/import syntax at http://www.ecma-international.org/ecma-262/9.0/#sec-imports to the TinySES grammar. How about we allow only default import and export, which
Perhaps something like
importDecl ::= "import" defVar "from" STRING ";"
exportDecl ::= "export" "default" exportableExpr ";"
exportableExpr ::=
("function" / "async" / "class") ${() => FAIL}
/ expr;
Attn @warner
Are the things you want to export expressions? They seem more like declarations. So perhaps exportableDecl
rather than exportableExpr
? Wait... I guess I'm reading it backwards... you want to exclude export function ...
? Now I'm really lost.
What are you suggesting goes in makeAlice.js?
Are you really suggesting that export default 1;
is a declaration in the TinySES language?
Are the things you want to export expressions?
For TinySES, yes, that is what I am suggesting.
They seem more like declarations. So perhaps exportableDecl rather than exportableExpr? Wait... I guess I'm reading it backwards... you want to exclude export function ...? Now I'm really lost.
Correct, I am excluding the things excluded by the lookahead of the last clause of ExportDeclaration at http://www.ecma-international.org/ecma-262/9.0/#sec-exports , since I only what to export an expression while staying within a subset of the ES grammar.
What are you suggesting goes in makeAlice.js?
import Q from 'q';
import escrowExchange from './escrowExchange';
import cajaVM from './cajaVM';
function makeAlice(...) {...}
export default def(makeAlice);
If we wanted to write instead something like
export function makeAlice(...) {...}
which is much more idiomatic JS style, we'd have no natural way to def
(transitively tamper-proof the API surface) of the makeAlice
function before it is exposed to its clients.
Are you really suggesting that
export default 1;
is a declaration in the TinySES language?
Yes. It declares that the module exports (as its default value) the value 1.
Since TinySES includes records and record patterns, we can effectively export a set of named declarations by instead exporting, as the module's default export value, a def
ed record with those field names. IOW, rather than
export function foo(...) {...}
export const bar = 8;
import {foo, bar} from 'x.js';
we would instead write
function foo(...) {...}
const bar = 8;
export default def({foo, bar});
import mx from 'x.js';
const {foo, bar} = mx;
Admittedly, it is weird that we now need to invent the name mx
rather than putting the record pattern directly in the import declaration. That is indeed an awkward notational price of this suggestion.
Another downside of this suggestion is that it cannot express cyclic module dependencies.
TinySES modules should be statically constrained (by post-parse static checks) from doing anything imperative at top level. Outcomes are thus independent of the order modules are evaluated. Given these simplifying assumptions, default export/import has a trivial translation to commonjs modules:
import Q from 'q';
import escrowExchange from './escrowExchange';
import cajaVM from './cajaVM';
function makeAlice(...) {...}
export default def(makeAlice);
is equivalent enough to
const Q = require("q");
const escrowExchange = require("./escrowExchange");
const cajaVM = require("./cajaVM");
function makeAlice(...) {...}
module.exports = def(makeAlice);
export default def(makeAlice);
Ah. Now I see.
it cannot express cyclic module dependencies
I don't see why not. I suppose if I studied the ES specs more closely I would learn why not, but...
It looks an awful lot like what we did in Monte; while the implementation of resolving the circular dependencies hurts my head, it's similar to other knot-tying techniques with promises and seems to work well enough.
How would this knot tying work in JS? Remember that, unlike E, a JS fulfilled promise is distinct from its fulfillment.
I'm not sure, but reportedly the jspm designers figured it out.
On Sun, Aug 5, 2018, 11:21 PM Mark S. Miller notifications@github.com wrote:
How would this knot tying work in JS? Remember that, unlike E, a JS fulfilled promise is distinct from its fulfillment.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/Agoric/Jessie/issues/8#issuecomment-410584748, or mute the thread https://github.com/notifications/unsubscribe-auth/AAJNyp33M-rrNiGVdebsxYPwAIuVt9G8ks5uN8QxgaJpZM4U8c0X .
Hi, I'm needing some clarification to understand more fully what legal import
semantics is for Jessie modules.
My current approach is to parse the module, recording the expression that exists in export default EXPR
. However, evaluating EXPR has to happen sooner or later, and that means we may see ordering differences when loading modules!
Supporting a recursive implementation of import
(separate load/parse and instantiate phases) is not really any harder than the other compiler infrastructure that is needed for Jessie. For bootstrapping I'm snarfing import/export from --experimental-modules
and *.mjs source files (node.js 8.5+ supports ES Modules).
Here's what I'd consider a basic recursive module implementation for CommonJS, mapping export default EXPR
to a single module.exports
value, which is a Promise that returns EXPR evaluated in that module's environment.
// makeAlice.js below:
// Can't use const here, or else we cannot "tie the knot".
let Q, escrowExchange, cajaVM;
function makeAlice(...) {...}
module.exports = _defModule('makeAlice.js', ['q', './escrowExchange', './cajaVM'],
_imports => [Q, escrowExchange, cajaVM] = _imports,
() => def(makeAlice)));
And the import/export infrastructure can be stubbed like this. Lots of this is pseudocode:
let _loadingPromise = {};
function doLoad(mname, loader) {
// Look up the loading promise in a cache somewhere.
const [once, exportVal] = _loadingPromise[mname] || [false, loader];
if (!_loadingPromise[mname]) {
_loadingPromise[mname] = [once, exportVal];
// Update the promise cache when done exporting.
exportVal.then((val) => {
_loadingPromise[mname] = [true, val];
});
}
if (once) {
return Promise.resolve(exportVal);
}
return exportVal;
}
function _defModule(mname, imports, _importer, _exporter) {
// The instantiation semantics: when we're done loading, import, then export (instantiate).
function pimport(mod) {
// Convert an import to a Promise.
const val = require(mod);
if (val.then) {
return val;
}
return Promise.resolve(val);
}
const loading = imports.map((mod) => doLoad(mod, pimport(mod));
const instChain = Promise.all(loading).then(_importer).then(_exporter);
return doLoad(mname, instChain);
}
Implemented.
I have all but the last line of makeAlice.js working. This doesn't parse:
A comment explains that this is on purpose.
An example you sent me uses
export function ...
but I don't see that in the TineSES grammar.p.s. https://github.com/dckc/tinyses2rho/blob/master/src/test/resources/makeAlice.js 6572f13 shows the changes I made to constrain makeAlice.js to TinySES:
cancel
needs initializer