Modules: compile time linking (was Re: Modules feedback, proposal)

Claus Reinke claus.reinke at talk21.com
Mon Apr 2 01:03:48 PDT 2012


> I think you misunderstand the relationship between what Dave said, and

The purpose of my questions is to remove misunderstandings -
it is entirely possible that some of them are on my side!-)

>>> If you have dynamic modules, you can't use them to export any 
>>> compile-time
>>> constructs, like macros, static operator overloading, custom literals, 
>>> or
>>> static types. If you load a module at runtime, then it's too late by the
>>> time you actually have the module to use it for anything at compile 
>>> time.

This assumes that runtime loading always follows compile-time,
framing the question in such a way as to preclude alternative answers.

>>..
> This is mostly a correct characterization of the academic work on type
> Dynamic.  However, it's really missing the point that Dave was making.
> If you don't have static modules, nothing *static* can be exported
> from them.  Using a facility like you describe, static information
> such as types could be associated with the dynamic values exported by
> a dynamic module after the fact.  For example, if a module exported a
> two argument function, we could somewhere else use that operation as
> the dynamic implementation of a static overloading of +.  However, the
> static overloading itself can't be exported from the module.  Which is
> exactly what Dave said.

Narrowly speaking, yes, when importing dynamic code, you'll use static
constructs in the importing code to associate static with dynamic info.

However, consider static/lexical scoping and this JS example:

var inner = "console.log(x)";
var outer = function(varname,inner) {
    return "(function("+varname+"){ eval('"+inner+"') })(1)" };

console.log(outer("x",inner));

eval(outer("x",inner));    // 1

eval(outer("z",inner));    // ReferenceError: x is not defined

The main code dynamically evaluates the result of outer, which
dynamically evaluates inner. Yet the main code is able to establish
in outer's result a static binding to be available to inner.

This level of unsafe freedom can be detrimental to programmer
health, but it should give you the idea of what one might want to
provide in a safer form. It also shows that compile-time can follow
runtime with dynamically created/loaded code.

This nesting of eval-compiles in eval-runtimes is a bit like writing
your meta-level programs in (meta-)continuation passing style
but, at the cost of this awkward nesting, stage-n code retains
control over code *and* static environment of stage-(>n) code.
So, by phrasing the question less narrowly, we already get one
different answer.

>> This pair of constructs allows for multiple program stages, each
>> with their own static/dynamic phases. You can load a
>> module at runtime, then enter compile time for that module,
>> then enter a new runtime stage with the newly compiled and
>> linked code. This was used, eg, in orthogonally persistent
>> programming languages to dynamically store/load statically typed
>> code/modules to/from database-like storage.
>
> Again, this misunderstands the relationship between staging and
> persistence.  Persistence is about *values* -- storing and retrieving
> values of the language to a disk or database.  The type Dynamic work
> shows how to do this safely in a typed language.  Multi-state
> programming is about *programs*, not values.  Even when a value
> contains computation, as with a function or an object, the *program*
> is gone.

As for misunderstandings: programs/modules/functions can be values,
which can be persisted, and reflection/introspection allow to recover
even the source for editing. In sufficiently advanced systems, such as
those researched in the 1980/1990s, that was the basis for IDEs which
hyper-linked source code to stored objects ("hyperprogramming").

It's been a long time, groups and online material have moved or
disappeared, but I think this report is a reasonable overview of
some of the work:

    Orthogonally Persistent Object Systems (1995)
    by Malcolm Atkinson , Ronald Morrison
    http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.25.8942

An early paper describes the use of persistent procedures as
modules in PS-Algol:

    Persistent First Class Procedures are Enough (1984)
    MP Atkinson ; Ronald Morrison
    http://www.cs.st-andrews.ac.uk/files/publications/download/AM84.pdf

I did implement a module system for a functional language once,
where the introspection aspect was only available to the IDE, not
programmatically; still, one was able to step through code that
dynamically loaded modules, and the IDE would present the
dynamically loaded code for inspection and editing as if it had
just been entered.

>> Which is why I'm confused by the staging aspects of ES6 modules:
>>
>> - ES is a multi-stage language, thanks to eval and module load
>> - ES6 modules offer "static" constructs only for the first stage,
>>   after which everything seems to devolve to dynamic again?
>
> Neither of these is correct.
>
> The modules design provides for both static and dynamic elements at
> every phase.  In particular, if we have the program "A.js":
>
>    import sin from "@math";
>    console.log(sin(3));
>
> and we load that from "B.js" with:
>
>    System.loadAsync("A.js", m => m)
>
> then the runtime of "B.js" is the compile-time of "A.js", and "A.js"
> can use the static features of modules just fine.

Yes to the latter. But what are B's options for using static modules
features after loading imports from A? Since A doesn't export
anything, let me change the example - why is this not supported:

    System.loadAsync("@math", m => {
        import sin from m;
        console.log(sin(3));
    })

or this:

    module math { export function sin .. }
    module M = math;
    import sin from M;

Naively, I would expect reflecting the module instance to involve
a toDynamic, and attempting to import from a module instance
object to involve a fromDynamic. I don't want to be reduced to
late dynamic property selection and checks once I've used a loader
to obtain a module instance. But, by nature of async callbacks,
code that uses @math's exports, via m, needs to be in the loader
callback, which doesn't admit "static" module constructs.

Consider the following hierarchy of late vs early checks in selecting/
extracting components from objects:

0 property selection: each selection stands or fails on its own

1 ES6 destructuring: irrefutable matching, very late checks;
    let {f,g} = obj
    This is sugar for separate property selection from objects -
    even after destructuring, the components may not actually exist

2 ES.next pattern-matching: refutable matching, check on match;
    let !{f,g} = obj    // made-up syntax for strict matching
    If the match succeeds, the components will be available

3 structural typing: structural constraints can be propagated
    through the program source, to establish regions of code
    which share the same object structures;
    (obj::{f,g})=> {let !{f,g} = obj; ..}
    If this function is ever successfully entered, the match will
    succeed and the components will be available

4 static structural typing: there is only one region for structural
    consistency, which is the whole program;
    If the whole program is accepted, any matches or component
    selections in it will succeed

4 is much too restrictive to be useful for a dynamic environment,
but if we augment it with some form of Dynamic types, we can
get to 3, with its regions of structural consistency and early
consistency checks on entering such a region. In my view, 3 is
the most practical scenario for combining dynamic module
loading with early errors. If loader callbacks are regions of
structural consistency for the modules loaded, 3 gives us load
time checking and errors.

2 is a simple variant of 3, where consistency constraints are not
propagated by a type system, but are checked early, and in one
swoop per object. This is the simplest system that can give us
separation of concerns for dynamic modules - matching the
structure corresponds to a module API consistency check. 2
gives us link time checking and errors.

1 or 0 give more flexibility, but the late checks and errors are
not suitable for module separation - even after module loading
and linking succeed, imports can fail at runtime.

My feeling is that ES6 modules aim for a variant of 4 in their
static aspects, then fall back to 1/0 for dynamic modules. But
the sweet spots for safe dynamic modules are 2 or 3. And the
analysis hazards of JS might make 3 impractical. Still, if
import declarations were sugar for refutable object matches,
that would give a reasonable compromise, and if this can be
augmented by an analysis that allows to shift checks from
linking to loading, all the better.

Looking back to my dynamically loaded math example above,
the loader has the source code of the module to be loaded, and
the analysis results for its callback. It should be able to match
the two at load time, enabling the safe use of "static" module
features in the loader callback. If that is untractable for JS,
the import declarations should act as refutable structure
matches, providing safety at link time.

Claus
 



More information about the es-discuss mailing list