endojs / Jessie

Tiny subset of JavaScript for ocap-safe universal mobile code
Apache License 2.0
281 stars 16 forks source link

Module exports? #8

Closed dckc closed 2 years ago

dckc commented 6 years ago

I have all but the last line of makeAlice.js working. This doesn't parse:

module.exports = makeAlice;

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:

erights commented 6 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

dckc commented 6 years ago

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?

erights commented 6 years ago

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 defed 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.

erights commented 6 years ago

Another downside of this suggestion is that it cannot express cyclic module dependencies.

erights commented 6 years ago

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);
dckc commented 6 years ago

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.

erights commented 6 years ago

How would this knot tying work in JS? Remember that, unlike E, a JS fulfilled promise is distinct from its fulfillment.

dckc commented 6 years ago

I'm not sure, but reportedly the jspm designers figured it out.

https://blog.angular-university.io/introduction-to-es6-modularity-the-jspm-package-manager-and-the-systemjs-loader/

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 .

michaelfig commented 5 years ago

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);
}
michaelfig commented 2 years ago

Implemented.