import { Pkg, BUILTINS, Built, BuiltinName, Expr, Folder, Ident, Item, ItemId, LocalInfo, ItemMod, Resolution, Resolved, mkDefaultFolder, superFoldExpr, superFoldItem, superFoldType, ItemExtern, } from "./ast"; import { GlobalContext } from "./context"; import { CompilerError, ErrorEmitted, Span } from "./error"; import { ComplexMap } from "./utils"; const BUILTIN_SET = new Set(BUILTINS); type Context = { ast: Pkg; gcx: GlobalContext; modContentsCache: ComplexMap>; newItemsById: ComplexMap>; }; function loadPkg(cx: Context, name: string, span: Span): Map { const loadedPkg = cx.gcx.pkgLoader(cx.gcx, name, span); const contents = new Map( loadedPkg.rootItems.map((item) => [item.name, item.id]), ); return contents; } function resolveModItem( cx: Context, mod: ItemMod | ItemExtern, name: string, ): ItemId | undefined { const cachedContents = cx.modContentsCache.get(mod.id); if (cachedContents) { return cachedContents.get(name); } let contents: Map; if ("contents" in mod) { contents = new Map(mod.contents.map((item) => [item.name, item.id])); } else { contents = loadPkg(cx, mod.name, mod.span); } cx.modContentsCache.set(mod.id, contents); return contents.get(name); } export function resolve(gcx: GlobalContext, ast: Pkg): Pkg { const cx: Context = { ast, gcx, modContentsCache: new ComplexMap(), newItemsById: new ComplexMap(), }; const rootItems = resolveModule(cx, [ast.packageName], ast.rootItems); return { id: ast.id, itemsById: cx.newItemsById, rootItems, packageName: ast.packageName, rootFile: ast.rootFile, fatalError: ast.fatalError, }; } function resolveModule( cx: Context, modName: string[], contents: Item[], ): Item[] { const items = new Map(); contents.forEach((item) => { const existing = items.get(item.name); if (existing !== undefined) { cx.gcx.error.emitError( new CompilerError( `item \`${item.name}\` has already been declared`, item.span, ), ); } else { items.set(item.name, item.id); } }); const scopes: string[] = []; let tyParamScopes: string[] = []; const popScope = (expected: string) => { const popped = scopes.pop(); if (popped !== expected) { throw new Error( `Scopes corrupted, wanted to pop ${expected} but popped ${popped}`, ); } }; const resolveIdent = (ident: Ident): Resolution => { const lastIdx = scopes.length - 1; for (let i = lastIdx; i >= 0; i--) { const candidate = scopes[i]; if (candidate === ident.name) { const index = lastIdx - i; return { kind: "local", index, }; } } for (let i = tyParamScopes.length - 1; i >= 0; i--) { const candidate = tyParamScopes[i]; if (candidate === ident.name) { return { kind: "tyParam", index: i, name: ident.name, }; } } const item = items.get(ident.name); if (item !== undefined) { return { kind: "item", id: item, }; } // All loaded pkgs are in scope. for (const pkg of [cx.ast, ...cx.gcx.finalizedPkgs]) { if (ident.name === pkg.packageName) { return { kind: "item", id: ItemId.pkgRoot(pkg.id), }; } } if (BUILTIN_SET.has(ident.name)) { return { kind: "builtin", name: ident.name as BuiltinName }; } return { kind: "error", err: cx.gcx.error.emitError( new CompilerError(`cannot find ${ident.name}`, ident.span), ), }; }; const blockLocals: LocalInfo[][] = []; const resolver: Folder = { ...mkDefaultFolder(), itemInner(item): Item { const defPath = [...modName, item.name]; switch (item.kind) { case "function": { const params = item.params.map(({ ident, type }) => ({ ident, type: this.type(type), })); const returnType = item.returnType && this.type(item.returnType); item.params.forEach(({ ident: name }) => scopes.push(name.name)); const body = this.expr(item.body); const revParams = item.params.slice(); revParams.reverse(); revParams.forEach(({ ident: name }) => popScope(name.name)); return { kind: "function", span: item.span, name: item.name, params, returnType, body, id: item.id, defPath, }; } case "mod": { const contents = resolveModule(cx, defPath, item.contents); return { ...item, kind: "mod", contents, defPath, }; } case "extern": { // Eagerly resolve the pkg. // Note that because you can reference extern pkgs before the item, // we still need the loadPkg in the field access code above. loadPkg(cx, item.name, item.span); return { ...item, defPath, }; } case "type": { tyParamScopes = item.genericParams.map(({ name }) => name); const type = { ...superFoldItem(item, this) }; return type; } } return { ...superFoldItem(item, this), defPath }; }, expr(expr): Expr { switch (expr.kind) { case "block": { const prevScopeLength = scopes.length; blockLocals.push([]); const exprs = expr.exprs.map>((inner) => this.expr(inner), ); scopes.length = prevScopeLength; const locals = blockLocals.pop(); return { kind: "block", exprs, locals, span: expr.span, }; } case "let": { const rhs = this.expr(expr.rhs); const type = expr.type && this.type(expr.type); scopes.push(expr.name.name); const local = { name: expr.name.name, span: expr.name.span }; blockLocals[blockLocals.length - 1].push(local); return { ...expr, name: expr.name, local, type, rhs, }; } case "fieldAccess": { // We convert field accesses to paths if the lhs refers to a module. const lhs = this.expr(expr.lhs); if (lhs.kind === "ident" || lhs.kind === "path") { const res = lhs.kind === "ident" ? resolveIdent(lhs.value) : lhs.value.res; const segments = lhs.kind === "ident" ? [lhs.value.name] : lhs.segments; if (res.kind === "item") { const module = cx.gcx.findItem(res.id, cx.ast); if (module.kind === "mod" || module.kind === "extern") { let pathRes: Resolution; if (typeof expr.field.value === "number") { const err: ErrorEmitted = cx.gcx.error.emitError( new CompilerError( "module contents cannot be indexed with a number", expr.field.span, ), ); pathRes = { kind: "error", err }; } else { const pathResItem = resolveModItem( cx, module, expr.field.value, ); if (pathResItem === undefined) { const err: ErrorEmitted = cx.gcx.error.emitError( new CompilerError( `module ${module.name} has no item ${expr.field.value}`, expr.field.span, ), ); pathRes = { kind: "error", err }; } else { pathRes = { kind: "item", id: pathResItem }; } } const span = lhs.span.merge(expr.field.span); return { kind: "path", segments: [...segments, String(expr.field.value)], value: { res: pathRes, span }, span, }; } } } return superFoldExpr(expr, this); } default: { return superFoldExpr(expr, this); } } }, ident(ident) { const res = resolveIdent(ident); return { name: ident.name, span: ident.span, res }; }, type(type) { return superFoldType(type, this); }, newItemsById: cx.newItemsById, }; return contents.map((item) => resolver.item(item)); }