Closed DylanSp closed 3 years ago
May want to also restructure evaluator.ts even more; separate evaluation of a block/module from evaluation of a (potentially multi-module) program?
Proposed syntax:
module SomeModule
{
// ...
export someObj;
export someFunc;
}
import SomeModule;
module Main
{
printObj(SomeModule.someObj);
SomeModule.someFunc();
}
Do I want to require referencing by module name? Probably better than just dumping everything into one namespace.
Alternatively, require explicit import list, i.e. import someObj, someFunc from SomeModule;
What the evaluator can do is go through all files passed in, construct a Map<Identifier, Block>
mapping module names to their list of statements, then when a module is imported evaluate the module.
Re: syntax, maybe put export list after closing brace?
Unfortunately, my current thoughts lead to side-effecting imports, but I don't really see a way around that.
I would probably make it a runtime error to return from the top level of a module, except for Main.
Alternative syntax:
module SomeModule
{
// ...
}
export someObj, someFunc;
I'm thinking the only import syntax will be import someObj, someFunc from SomeModule;
, which puts someObj
and someFunc
in the namespace without qualification; I don't really want to deal with evaluating namespaces as part of identifiers.
How to handle importing the same module twice, should it be run twice? EDIT: Looking at Python for comparison, no.
Maybe construct DAG of dependencies before evaluation (in a separate pass after parsing), then do a toposort and evaluate in that order?
Change type of evaluateModule()
to (module: Module) => Either<RuntimeFailure, [Value, Map<Identifier, Value>]>
. First element of tuple is what's returned from top level of module (not a runtime error, it just gets ignored in most cases), second element is a map of exported identifiers to their values.
type ExportResult = { exportResultKind: "noSuchModule"; }
| { exportResultKind: "noSuchExport" }
| { exportResultKind: "validExport"; exportedValue: Value; };
class ExportedValues {
private values: Map<ModuleName, Option<Map<Identifier, Value>>>;
public constructor() {
this.values = new Map<ModuleName, Option<Map<Identifier, Value>>>();
}
// this gets called after parsing
public loadModuleNames(Array<ModuleName> moduleNames) {
moduleNames.forEach(moduleName => {
// insert (ModuleName, None) into values
});
}
// called when evaluating an import statement
public ExportResult getExportedValue(ModuleName moduleName, Identifier exportName) {
// if moduleName not in values, return "noSuchModule"
// if values[moduleName] is none, evaluate module
// if values[moduleName].value[exportName] is none, return "noSuchExport"
return values[moduleName].value[exportName].value;
}
private loadModule(/* params */) {
// ...
}
}
Will this have issues with circular dependencies? Should I forbid those? (Probably)
Module system is implemented, tested, and has examples; only thing outstanding is to rework full_pipeline.ts and its tests.
Module stuff, for reference when writing README later:
module Source
{
let someNum = 1;
}
export someNum;
import importedValue from ModuleName;
. If ModuleName
hasn't been evaluated previously, it's evaluated on first import, then its exported values are cached.
Look at structure of evaluator.ts; may be able to make
evaluateBlock()
,evaluateExpr()
methods onEnvironment
, make top-level driver load stdlib modules (see #35).