// Resolving `import a.b` to the modules a program is made of.
///|
/// Where a module's source comes from.
///
/// `path` is the dotted path as written -- `import collections.hashing` asks
/// for `"collections.hashing"` -- and `None` means the loader does not have
/// it. What that path denotes is entirely the loader's business: a directory
/// layout, a zip entry, a database row, a literal map.
pub(open) trait Loader {
fn load(Self, String) -> String?
}
///|
/// A loader over sources already in memory.
///
/// This is the one a test, a playground or a code generator wants, and it is
/// also the proof that the resolver does not need a filesystem: nothing else
/// in this package would change if it did.
pub struct MapLoader {
sources : Map[String, String]
}
///|
/// A loader over the given `(path, source)` pairs.
pub fn MapLoader::new(entries? : Array[(String, String)] = []) -> MapLoader {
let sources : Map[String, String] = Map([])
for e in entries {
let (k, v) = e
sources[k] = v
}
{ sources, }
}
///|
/// Add or replace one module's source.
pub fn MapLoader::add(self : MapLoader, path : String, src : String) -> Unit {
self.sources[path] = src
}
///|
pub impl Loader for MapLoader with fn load(self, path) {
self.sources.get(path)
}
///|
/// A resolution failure, as a ready-to-render report.
pub(all) suberror ResolveError {
ResolveError(@er.Report)
}
///|
/// The report inside the error.
pub fn ResolveError::report(self : Self) -> @er.Report {
let ResolveError(r) = self
r
}
///|
/// A whole program: every module it needs, in dependency order.
pub struct Program {
units : Array[@lower.Input]
sources : @er.Sources
}
///|
/// The modules, dependencies first, ready for `@lower.lower_program`.
pub fn Program::units(self : Self) -> Array[@lower.Input] {
self.units
}
///|
/// The source registry every diagnostic points into.
pub fn Program::sources(self : Self) -> @er.Sources {
self.sources
}
///|
/// The entry module's name.
pub fn Program::entry(self : Self) -> String {
if self.units.length() == 0 {
""
} else {
self.units[self.units.length() - 1].module_.name
}
}
///|
/// Resolve a program from source already in hand.
///
/// The entry module is not asked of the loader -- a caller compiling a buffer
/// has it already -- and everything it imports is.
pub fn resolve_source(
path : String,
src : String,
loader : &Loader,
) -> Program raise ResolveError {
let sources = @er.Sources::new()
let units = []
let done : Map[String, Unit] = Map([])
let visiting : Map[String, Unit] = Map([])
walk(path, Some(src), loader, sources, units, done, visiting, [])
{ units, sources, }
}
///|
/// Resolve a program by asking the loader for the entry module too.
pub fn resolve(path : String, loader : &Loader) -> Program raise ResolveError {
let sources = @er.Sources::new()
let units = []
let done : Map[String, Unit] = Map([])
let visiting : Map[String, Unit] = Map([])
walk(path, None, loader, sources, units, done, visiting, [])
{ units, sources, }
}
///|
/// Depth first, pushing each module after everything it imports.
///
/// `stack` is only for the error message: a cycle is worth naming in full,
/// because which edge to cut is the programmer's decision and they need to see
/// the ring to make it.
fn walk(
path : String,
provided : String?,
loader : &Loader,
sources : @er.Sources,
units : Array[@lower.Input],
done : Map[String, Unit],
visiting : Map[String, Unit],
stack : Array[String],
) -> Unit raise ResolveError {
if done.contains(path) {
return
}
if visiting.contains(path) {
let ring = (stack + [path]).join(" -> ")
raise ResolveError(
@er.Report::error("these modules import each other: " + ring).with_help(
"wap has no forward declarations across modules; break the cycle by moving the shared declarations into a third module",
),
)
}
let text = match provided {
Some(t) => t
None =>
match loader.load(path) {
Some(t) => t
None =>
raise ResolveError(
@er.Report::error("cannot find module `" + path + "`").with_help(
"the loader has no source for that path",
),
)
}
}
let fname = path + ".wap"
let src = sources.add(fname, text)
let parsed = @parse.parse_module(text, fname~) catch {
@parse.ParseError(r) => raise ResolveError(r)
}
let m = parsed.module_()
visiting[path] = ()
stack.push(path)
for d in m.decls {
match d {
Import(p) =>
walk(p.join("."), None, loader, sources, units, done, visiting, stack)
_ => ()
}
}
let _ = stack.pop()
visiting.remove(path)
done[path] = ()
units.push({ module_: m, source: { fname, text, src, }, })
}