-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement module imports #53
Comments
Just a quick thought: at the point when we have module imports, and the I just realized that whereas an identifier
There's no leeway here. Why? Becuase we've already left that compilation unit, and we won't learn anything more about it. As far as I can see, |
I don't think I realized it at the time, but... if we have the concept of "internal modules" anyway, the cleanest way by far to provide
Though perhaps the names should be re-thought to be more suggestive:
Or something. |
This won't be enough. Take the use case of loading ranges as a module.
The
Perhaps we can all survive having to write But with the operators, it's a non-starter to have them not import, because not importing them means not installing them as operators in the current parser. And then what good are they? (Same deal with macros we'd want to export, of all three kinds.) I can see the problem here quite clearly, but I don't yet have a good solution to it. There's plenty of prior art, and maybe we can lean on some of that. For example, Python (and Haskell) for this
It's nice in that it's very explicit — the importing compunit itself contains enough information for its parser to know which symbols will be imported — but I have a feeling that'd also be annoying. In particular, I want all those four range infix operators, always, I don't want to have to go and update the import list when I use a new one. Ditto the Since there's nothing that gets imported via the Maybe we should do what lizmat++ suggested once, and have two separate keywords? One (Hm, I can certainly argue this one both ways. On the one hand, it's cool that On the module end of things, I was toying with the idea of an So if we don't go with the Still pondering all the forces involved. |
I added the "needs-more-design" label to this issue. In my view, the next step is to identify the main use cases for modules (such as the |
Coming back to this issue, I think this is where we should start. Possibly we can have a I think we should avoid having an explicit exporting mechanism, at least not having one proves untenable. Instead, a module implicitly exports everything in its compunit top level. (If we ever decide to go with an explicit exporting mechanism, an We need to remember to handle the case of colliding names through imports. Though strictly, this is just a special case of colliding declarations. The A module should consider its top-level names to be its "published API", and a change to that might affect its importing modules adversely. This problem is what versioning would normally solve; I don't see that we need to do that, because 007 is not meant for actual real-world production use, just to explore things around macros. |
There's a contradiction here that I can't quite pinpoint. I think these three wishes are mutually incompatible in a 2-out-of-3 sense:
Though it strikes me as I write this that fixing this contradiction might be as simple as loosening up the first requirement to Come to think of it, I'm guessing a similar thing would need to happen with enums. |
Aye; coming back to this issue, it now seems clear that each imported module needs to have as a type its own anonymous The pattern here is a little bit similar to anonymous subclasses in Java. |
I think I've arrived at a good design that covers all the bases. First of, we're going to make the keyword Actual syntax/design in the next comment. |
Ok, so. First off, in this design, exports are explicit. I would do it through an There are three
|
Immediate afterthought: since That is, Just to be clear, the string form will still be allowed, and might be useful in some rare cases when the module is not a valid identifier. I suggest the idiomatic way to write it be with the identipath, but we drop down to the string when necessary. Notice that the identipath exists in another "namespace", so for example this
resolves the name |
I initially meant to include a fourth form in the design:
Note that this form is the only one that contains nothing between the At the last moment as I was writing down the other forms, I changed my mind. For three reasons:
Much, much later edit: And of course, you could always write it using the second form:
Which, to be honest, also has clarity going for it: it explicitly say "do the import of the module, but bind no names" — from which the obvious conclusion needs to be that we're importing for some other reason than names. |
Oh, and for a brief while before I rejected the fourth form, I made up the rule that forms 1..3 would require there to be at least one export from the module, whereas form 4 would require there to be none. But then the above arguments won out, and I dropped the fourth form. |
One more thought for now: let's call the thing I realized in this comment "early module binding". Early module binding is necessary with the first form of import: we need to know right away what names we're importing into the current lexical scope, so that subsequent code can know which names are taken already. However, with the third form, we're only really declaring a module With a late-bound module we might write When might this happen? When we have mutual imports:
I said in the OP that this should be impossible by design (and I still think it should be). But I'd just like to point out that it seems like it could be made to work, and this would in theory be a way for modules to cyclically depend on each other. The whole thing reminds me of this p6l thread (Edit: link updated/rescued out of link rot), which feels like it's from a completely different geological era. Here's TimToady's take on it in that thread:
Yep. I think type recursion should be possible in some rare cases, but nowadays I would recommend breaking things apart using a pattern like this:
Boom, circularity gone. I guess I think it's important enough for modules to know what's "up" and what's "down" to not want to mess with import circularity. |
Leaving circularity aside, it's perfectly admissible for the same modules to be imported several times during the same compilation process, as we traverse the compunits and the import relations between them. I guess this is the same as saying that the import tree is actually a DAG. But it's actually some kind of multi-DAG, since the same compunit can even import the same module multiple times, in different lexical scopes. (Note to self: write a test for that.) In the end,
Step 2 only needs to be done once per module and compilation process. If we have some kind of |
Does it make sense to compile but not run a module that's imported? I'm asking because I just realized that something like
would be problematic, because the So, yes, "export the static things" seems a way forward. And maybe even "compile the imported module, but do not run it". How do the other languages do this? Quick investigation:
I also tried putting an So I think the way to do it is this:
|
Pawel Murias points out that we can arrange things so that all modules are compiled first, and only later do we run all the compunits. This is a good point, and I think it aligns better with users' expectations. |
One thing I realized is that all the forms of In the case of the first form, the answer is over in the loaded module, and so we need to parse it first before we finish building the Qnode for that import statement. Also, in an IDE setting, the question "where was this declared?" on an imported name can mean two different things — either the |
I think a "where was this declared" depends: in the |
@vendethiel Yes, something like that is what I'd expect too. Note that the first form (
|
Exports should be immutable bindings, like in ES6. That post is great, I need to read it again. If I'm reading it right, the fact that exports are immutable bindings will even make cyclic imports feasible. (I'm fine with that, but I prefer to be conservative in the short run and think about allowing cycles when we're comfortable with the base functionality.) |
One thing (it seems to me) we will lose if we allow cyclic bindings is the ability to tell at parse-time what type object is in a name like |
I was reading a TC39 proposal today, and it seems to have changed my mind about whether In the above discussion, I have But then I read that "export default from" proposal, and I remembered/realized that From what I can see, we'd have five forms of export statement:
The remaining three mirror what
Whereas the three I think |
Both export form 2 and export form 4 support the same type of |
Just throwing this blog post about Python imports into this issue, as it seems to have relevant information about a lot of things that we might want to consider when implementing 007 modules/imports. |
Just doing some drive-by commenting here, pointing out that macro hygiene will have a cross-cutting impact on modules and imports. Namely, if module A calls a macro in module B, which generates code containing a name that B imported from module C, then (invoking hygiene) module A just gained a "hidden import" of module C. (Conceptually, a "macro-expanded AST" for module A might contain an explicit import; the point is that it's "hidden", as in, it doesn't need to be explicitly imported in A.) Since Alma is a language primarily with macros and only secondarily with modules, this aspect of modules is pretty front-and-center. The paper Extending the Scope of Syntactic Abstraction addresses this quite foundationally, by defining modules on top of Scheme macros. My fingers are itching a little bit to try defining the things in that paper in a small prototype Scheme. (Edit: This slide deck by Flatt references the paper, calling the technique "splicing scope".) |
This Mozilla blog post contains a good run-down of this process in the case of ES modules, explaining how there are three phases:
It also explains the "live bindings" thing, and how the instantiation phase wires things up without evaluating them, which allows for reference/dependency cycles. |
Why modules and module imports? Because eventually we'll want to play around with macros that affect the parsing context that imported them.
I have no reasonable basis for choosing either (Perl)
use
or (Python)import
, so let's go withuse
because it's short. (But I will keep referring to them as "imports".)There are two sorts of import. The form that takes an identifier loads something from 007 itself.
The form that takes a string literal loads something from a path relative to to loading script or module.
In either case, a symbol gets installed in the lexical scope corresponding to the loaded module. (It's a compile-time error for the file name sans
.007
extension to not be a valid identifier.)Importing the module causes its 007 file to run. (Though in the case of internal 007 modules, this may be faked.) The symbol that gets installed is an object, but let's give it the object type
Module
. Its properties correspond to the variables defined in the topmost scope at the end of running the module.A
use
counts as a variable declaration. Therefore it's a compile-time error to refer to an import before importing it, or to refer to an outer variablex
and then importingx
on top of it. Aside from this,use
statements can occur anywhere. The import logic happens atBEGIN
time.It's fine for a module to import other modules. Paths keep being relative to the thing that does the importing. At any given time, we're pushed a number of compunits on a conceptual stack, waiting for the thing they imported to finish loading. It's an immediate compile-time error to try to import something that's already on that stack.
In the fullness of time, a module being loaded is meant to be able to influence its loading context more than just installing a single symbol into it. The exact mechanism for this I leave unspecified for now — but it could be something as simple as there always being a
loader
object available in a loaded module. Similarly, the parser of the loaded module could perhaps be accessed through aparser
object.The text was updated successfully, but these errors were encountered: