This PR implements modules for the VM. (WIP)
- For each module, combine names defined in the module to create a valid environment. Reusing the same name is an
Interruption
- combine names in nested modules, and also permit outer names to flow into these internal modules. Unlike in
Rust
, in Motoko, all names in super scopes flow into subscopes.
Unlike other constructs, modules require doing a lot of steps that do not correspond to ordinary execution, but rather, correspond to phases just before or during type checking.
These steps are necessary to resolve identifiers and module projections in real Motoko programs, and involve resolving nested recursive definitions.
Each named module definition introduces a recursive scope that we construct incrementally, in a step-wise manner. When they nest, additional steps propagate the recursion between the nested levels.
Why?
We could eschew a step-based approach, and do the module-level resolution of paths in a batch style, with batch-style errors each time an identifier is used twice. However, that has two drawbacks:
- Goes against the rest of the project, where we show internal progress at every possible step and interrupt it immediately when there is an issue. A batch algorithm is not like this, and would introduce another "mode" of interacting with programs that happens before they "run step by step". That would complicate things, including the UX design, potentially.
- If we ever want to improvise with other pre-execution steps, this would introduce a place to do them. Such steps may include experimental intensional features, like
let box
; as with modules, they would step before the program evaluates.
Scoping using nested module
s
Consider this nested module
declaration:
module {
module X {
/* module Y { }; */ // will shadow Y sibling of X
public module M {
func f () : Nat {
g();h
N.g();
M.g();
/* X.N.g() */
x
};
public func g () { Y.y(); Y.Z.z() };
};
module N {
public func g () { Y.y(); /* Z.z() /* error. */ */ };
};
public let x = 5;
};
module Y {
public module Z { public func z() { Y.y(); y() } };
public func y() { X.M.g() };
};
}
Notice:
- function
X.M.g
can "see" Y.y
and Y.Z.z
.
- function
Y.Z.z
can "see" function "y" and also see the same function as Y.y
.
- function
X.N.g
can "see" Y.y
but not Z.z
(Y.Z.z()
works though)
This example could be smaller and simpler. It's something I have been using as I poke at the compiler to test my understanding of the language, and having it be about this large as been helpful.