For context, I would recommed reading the documentation first.
#Module
#Module X is analogous to ending the current file, beginning Lib\X.ahk and loading it as a module without importing it. It just defines a new module and adds it to the module list. There is no implied connection between it and any other module.
Helgef's namespace/module pull requests used syntax like Module X {}, which I have not implemented in this release. In contrast with #Module, I would expect a "module block" to:
- Add "X" to the enclosing namespace, like a class definition.
- Have access to names defined in the enclosing namespace, consistent with functions and classes. That would be all module-level names, not just exported names.
I also considered module X:, but I think a directive is more familiar.
A directive is simpler to parse without breaking backward-compatibility (than a statement beginning with an identifier, whether terminated by a colon or followed by a block). Hypothetically, a module in "v2.0-compatible mode" must allow "module" as a user-defined variable or function name, but such a module can be safely terminated with #Module since it has no other valid interpretation.
#Module might be changed to allow reopening an existing module.
It might make sense for #Module to automatically end at the end of the current file, when used inside a file being loaded as a module (including the case where #Module is used immediately before #Include). However, I'm not too sure how #Module should relate to nested modules or packages.
Import
Import can import a module, import all of a module's exports, or import specific exports from a module. I implemented syntax mainly influenced by Python and JavaScript.
Python uses import m and from m import a, b as x. Having the module name always at the start has some appeal especially when the list of names becomes long. from m import * would need special-casing to avoid line continuation, but so would import * from . (hypothetical import from current package). The choice of ordering has implications for how backward-compatibility can be handled.
JavaScript has several different forms, but they all start with import and end with "module-name";. The examples above would translate to something like import m from "module-name"; or import * as m from "module-name"; and import {a, b as x} from "module-name";. module-name would typically be a URL rather than a simple name (when an import map is not used).
I started with the intention to implement import a, b as x from m, but ended up adding braces, mainly to make it easier to parse (since the module name is needed before anything else). It also helps to show the grouping of imported names vs. module name. import {} from m is valid; it will load the module but not add any names to the current namespace.
I thought that pre-existing destructuring syntax might have been a reason for JavaScript using braces in import, but they are actually not consistent: import uses "as" where destructuring uses ":".
import m and import real_m as m work like in Python, I think. Equivalent JavaScript would probably be import m from "m"; if there is a default export, otherwise import * as m from "m";.
import {a} from m works like Python's from m import a and JavaScript's import {a} from "m"; (except for how module names work).
Aliasing the module's exports with "as alias" seems to be the same in all.
Export
Function and class definitions can now be prefixed with export or export default. This seemed to be the obvious way to mark something for export from the module. For global variables, global is replaced with export; e.g. export answer := 42.
Requiring exports to be explicit reduces the risk of conflict when a script uses import * from a and import * from b. It also reduces the risk of changes within a module breaking scripts which use the module (e.g. a user can't rely on internal functions that aren't exported).
One drawback is that a v2.0-compatible library can't have exports. One way around this is to use a separate file which #includes the v2.0 library and exports the relevant names. Another way is to use #Module and #Include at the end of the script. However, this needs further consideration with respect to whether a v2.0-compatible module can even contain exports (see Backward-Compatibility below).
Python exports everything which doesn't begin with an underscore by default, but allows __all__ to define the set of names which correspond to *. This relies on the module executing before the importing module - Python apparently doesn't allow cyclic imports.
Declarations allow exports to be found through static analysis, which means the importing module can bind to the imported module's exports at load-time, before either module is executed. This allows functions and classes to be used even if the module hasn't yet executed.
In some cases it might be preferable to export everything by default, such as for a v2.0 module which doesn't or can't use export (see Backward-Compatibility). There is no allowance for this as yet.
Lib
Import m looks for m.ahk in the usual Lib folders. I intend to add a check for an environment variable to override the Lib folder locations with a list of paths. The variable might be based on version, so v2.2 may have different libraries to v2.1, with some in common.
Module extends Any
In this release, Module is implemented as a simple dynamic interface which looks up an exported variable and gets, sets or invokes its value. This is by no means final; the goal for this release was just the core functionality. import m binds a Module to m and as such is not optimized at load time.
Normal properties can be defined in Module.Prototype, but are of course overridden by any export with the relevant name.
At one point I was thinking that a Module could be the entry point for a script to reflect on its own variables, functions and classes, but that mightn't make sense if only explicit exports are accessible.
Instead, I am thinking that Module can be an Object, with a property defined for each export. This would enable the debugger and script to enumerate the exports (properties) of a module. However, exports can't be implemented as a standard value or method property, as the module itself should not be passed as a parameter when calling. If GetOwnPropDesc is used on an export property, it might appear as a property getter with opaque implementation (nested classes are like this already) rather than as a value or constant.
Packages
Currently a module name should be a plain identifier. I may implement packages and module directories, such that import a.b may load Lib\a\b\__init.ahk, Lib\a\b.ahk or Lib\a\__init.ahk (in that order, and maybe Lib\a.ahk); and add a to the current module.
If packages are a thing, it may make sense to borrow Python's package-relative imports, like import .mod_in_same_pkg, import ..mod_in_parent_pkg or import * from . (Python: from . import *).
#Module a.b should (but doesn't yet) also create a top-level module/package a which exports b.
Placing a module in a package is just a matter of organisation and identification of modules. Lib\a\b.ahk would not implicitly have access to names defined within module a; that would be an effect of lexical nesting, as in module a { module b {} }.
I am undecided as to whether import a.b, which adds package a to the current namespace, should imply import a, which would load Lib\a\__init.ahk. I think that import a.b should not load Lib\a\c.ahk, except possibly in the case that it loads Lib\a\__init.ahk and that explicitly imports .c.
Backward-Compatibility
v2.1.0 will be required to have the capability to run v2.0 scripts without changes. The current alpha may not achieve this. My plan is for each module to have a target version for compatibility, with v2.0 being the default target version in an empty script. How exactly the v2.1 mode will be enabled is not yet decided.
Python has from __future__ import feature, which is special-cased by the compiler. I am of two minds about implementing something like this. I think that sharing code will be easier if required to conform to a target version number, but enabling individual changes may ease the process of upgrading existing scripts (or using specific new features in existing scripts without fully upgrading them).
Using v2.0 code within a v2.1 script would involve putting the v2.0 code in its own module. If export must be restricted for backward-compatibility, a v2.0 module might need different export behaviour.
Ideally, making use of new syntax should not require a thorough review of the entire script to make it compatible with the new version. New code can instead be placed within a module which is v2.1-enabled, or code can be gradually migrated between modules. If the syntax to define or load a module is to be used in a v2.0-compatible context, it must be parsed in a way that doesn't change the interpretation of valid code. For example, "Module" and "Import" can't be reserved words.
Debugger
v2.1-alpha.11 has some notable limitations for debugging (with a DBGp client):
- Querying a variable which refers to a Module does not list the module's exports, although property_get and property_set should work for directly accessing the export property.
- By design, context_get -c 1 returns global variables of the current module and names which are explicitly imported, but not names which are implicitly imported with import * or from the built-in module.