Intention of breaking change: handling re-exports in corefn

We are seeking feedback on a proposed change to the corefn JSON format. Specifically, we are considering changing the moduleExports property in the corefn JSON format from [Ident] to [(Maybe ModuleName, Ident)] in order to handle re-exports better.

If this change is accepted, we would like to include it in v0.14.0 (which should hopefully be released Soon™).

Details

At the moment, the corefn module format is defined like this:

In particular, the module exports are just a list of Ident. This means that in corefn, modules can only export local declarations. In order to sensibly include re-exports in generated code (without being forced to generate a bunch of extra local declarations), we need to provide a way of expressing “this module re-exports foo from module Bar”. The module exports would become a list of (Maybe ModuleName, Ident), where a Just indicates that the export is a re-export from the given module, and a Nothing indicates a normal export.

In terms of the actual JSON, consider the following module:

module Main (foo, module X) where

import Control.Category (identity) as X

foo :: Int
foo = 0

Right now, its corefn output looks like this:

{
  "moduleName": [
    "Main"
  ],
  "exports": [
    "foo"
  ],
  ...
}

After this change, it would look like this:

{
  "moduleName": [
    "Main"
  ],
  "exports": [
    [null, "foo"],
    [["Control", "Category"], "identity"]
  ],
  ...
}

Each export would be an array of two elements, the first being either a list of strings for the module name for a re-export, or null for a normal export, and the second being the name of the thing being exported. This is, of course, a breaking change.

Motivation

See https://github.com/purescript/purescript/issues/1888

This change will make the JavaScript output (or indeed any output, regardless of the backend in use) more useful, as right now, you can only import values from the modules they were originally defined in if you’re writing in the target language. In addition, it is the first step towards cut-off in incremental builds, allowing us to skip more work and finish compilation faster (see https://github.com/purescript/purescript/issues/3724).

An alternative option would be to generate local declarations for re-exports, in which case we could do this without breaking the format. However, that is more complicated (as it could involve lots of name mangling) and more expensive from a performance perspective. It also increases the size of the compiler output, which can be a big drawback in the case of JavaScript running in a browser.

4 Likes

I don’t have time to write up anything more detailed at the moment, but another alternative I’ve implemented is to add an explicit field moduleReExports :: Map ModuleName [Ident]. This avoids mixing the concerns of regular exports and re-exports and results in a cleaner implementation. It also makes the breakage from this change trivial to accommodate for downstream consumers (you simply ignore the new field). This change would also be purely additive, so it wouldn’t necessarily be a breaking change in the JSON format.

Ignoring the field, however, is only a temporary fix for alternative backends as the plan is to follow this up with a change to import re-exports from the module they’re imported from, not the module they are originally defined in.

3 Likes

That sounds good to me!

If the latter approach using Map has no downsides then I think I would prefer that.

Thanks to everyone who took the time to put thought and effort into this, and for explaining things so clearly.

3 Likes

@andyarvanitis, @nwolverson, @coot, @thautwarm any opinions?

The Python backend also benefits from this change, and I’m okay with both options.
Personally I’d perfer adding a moduleReExports :: Map _ _, agree with @paulyoung

no worries, I think your words clear and informative enough.

1 Like

For visibility, here’s the PR https://github.com/purescript/purescript/pull/3883

Seems ok to me, thanks.