diff --git a/src/ast.ts b/src/ast.ts index 0c5a07c..ba6d629 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,7 +1,11 @@ import { Span } from "./error"; import { LitIntType } from "./lexer"; -export type Ast = { items: Item[]; typeckResults?: TypeckResults }; +export type Ast = { + rootItems: Item[]; + typeckResults?: TypeckResults; + itemsById: Map; +}; export type Identifier = { name: string; @@ -23,6 +27,10 @@ export type ItemKind = | { kind: "import"; node: ImportDef; + } + | { + kind: "mod"; + node: ModItem; }; export type Item = ItemKind & { @@ -64,6 +72,20 @@ export type ImportDef = { ty?: TyFn; }; +export type ModItem = { + name: string; + modKind: ModItemKind; +}; + +export type ModItemKind = + | { + kind: "inline"; + contents: Item[]; + } + | { + kind: "extern"; + }; + export type ExprEmpty = { kind: "empty" }; export type ExprLet = { @@ -101,6 +123,20 @@ export type ExprIdent = { value: Identifier; }; +/** + * `a.b.c` in source code where `a` and `b` are modules. + * This expression is not parsed, but fieldAccess gets converted + * to path expressions in resolve. + */ +export type ExprPath = { + kind: "path"; + segments: string[]; + /** + * Since this only exists after resolve, we always have a res. + */ + res: Resolution; +}; + export type ExprBinary = { kind: "binary"; binaryKind: BinaryKind; @@ -168,6 +204,7 @@ export type ExprKind = | ExprBlock | ExprLiteral | ExprIdent + | ExprPath | ExprBinary | ExprUnary | ExprCall @@ -291,12 +328,7 @@ export type Resolution = } | { kind: "item"; - /** - * Items are numbered in the order they appear in. - * Right now we only have one scope of items (global) - * so this is enough. - */ - index: number; + id: ItemId; } | { kind: "builtin"; @@ -438,7 +470,8 @@ export const DEFAULT_FOLDER: Folder = { export function foldAst(ast: Ast, folder: Folder): Ast { return { - items: ast.items.map((item) => folder.item(item)), + rootItems: ast.rootItems.map((item) => folder.item(item)), + itemsById: ast.itemsById, typeckResults: ast.typeckResults, }; } @@ -493,6 +526,30 @@ export function superFoldItem(item: Item, folder: Folder): Item { }, }; } + case "mod": { + let kind: ModItemKind; + const { modKind: itemKind } = item.node; + switch (itemKind.kind) { + case "inline": + kind = { + kind: "inline", + contents: itemKind.contents.map((item) => folder.item(item)), + }; + break; + case "extern": + kind = { kind: "extern" }; + break; + } + + return { + ...item, + kind: "mod", + node: { + name: item.node.name, + modKind: kind, + }, + }; + } } } @@ -532,6 +589,9 @@ export function superFoldExpr(expr: Expr, folder: Folder): Expr { case "ident": { return { kind: "ident", value: folder.ident(expr.value), span }; } + case "path": { + return { ...expr, kind: "path" }; + } case "binary": { return { ...expr, diff --git a/src/index.ts b/src/index.ts index 2870578..8b5dadd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,12 +11,13 @@ import { exec } from "child_process"; const INPUT = ` function main() = ( - printTuples(("hello, ", "world\\n")); + owo.uwu.meow(); ); -function printTuples(a: (String, String)) = ( - print(a.0); - print(a.1); +mod owo ( + mod uwu ( + function meow() =; + ); ); `; @@ -38,14 +39,14 @@ function main() { const ast = parse(tokens); console.log("-----AST---------------"); - console.dir(ast, { depth: 50 }); + console.dir(ast.rootItems, { depth: 50 }); - const printed = printAst(ast); console.log("-----AST pretty--------"); + const printed = printAst(ast); console.log(printed); - const resolved = resolve(ast); console.log("-----AST resolved------"); + const resolved = resolve(ast); const resolvedPrinted = printAst(resolved); console.log(resolvedPrinted); diff --git a/src/lexer.ts b/src/lexer.ts index 40513b1..a51d7ad 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -10,6 +10,8 @@ export type DatalessToken = | "loop" | "break" | "import" + | "extern" + | "mod" | "(" | ")" | "{" @@ -90,13 +92,24 @@ export function tokenize(input: string): Token[] { const next = input[i]; const span: Span = { start: i, end: i + 1 }; - if (next === "/" && input[i + 1] === "/") { + if (next === "/" && input[i + 1] === "/") { while (input[i] !== "\n") { i++; } continue; - } + } + + if (next === "/" && input[i + 1] === "*") { + i++; + i++; + while (input[i] !== "*" && input[i + 1] !== "/") { + i++; + if (input[i] === undefined) { + throw new CompilerError("unterminated block comment", span); + } + } + } if (SINGLE_PUNCT.includes(next)) { tokens.push({ kind: next as DatalessToken, span }); @@ -218,14 +231,14 @@ export function tokenize(input: string): Token[] { } let type: LitIntType = "Int"; - console.log(input[i + 2]); + console.log(input[i + 2]); if (input[i + 1] === "_" && isIdentStart(input[i + 2])) { - console.log("yes", input.slice(i+2, i+5)); - - if (input.slice(i+2, i+5) === "Int") { + console.log("yes", input.slice(i + 2, i + 5)); + + if (input.slice(i + 2, i + 5) === "Int") { i += 4; type = "Int"; - } else if (input.slice(i+2, i+5) === "I32") { + } else if (input.slice(i + 2, i + 5) === "I32") { i += 4; type = "I32"; } @@ -292,6 +305,8 @@ const KEYOWRDS: DatalessToken[] = [ "loop", "break", "import", + "extern", + "mod", ]; const KEYWORD_SET = new Set(KEYOWRDS); diff --git a/src/lower.ts b/src/lower.ts index 950a36c..f3ebceb 100644 --- a/src/lower.ts +++ b/src/lower.ts @@ -128,7 +128,7 @@ export function lower(ast: Ast): wasm.Module { relocations: [], }; - ast.items.forEach((item) => { + ast.rootItems.forEach((item) => { switch (item.kind) { case "function": { lowerFunc(cx, item, item.node); @@ -195,7 +195,7 @@ function lowerImport(cx: Context, item: Item, def: ImportDef) { setMap( cx.funcIndices, - { kind: "item", index: item.id }, + { kind: "item", id: item.id }, { kind: "import", idx } ); } @@ -246,7 +246,7 @@ function lowerFunc(cx: Context, item: Item, func: FunctionDef) { fcx.cx.mod.funcs.push(wasmFunc); setMap( fcx.cx.funcIndices, - { kind: "item", index: fcx.item.id }, + { kind: "item", id: fcx.item.id }, { kind: "func", idx } ); } @@ -382,6 +382,9 @@ function lowerExpr(fcx: FuncContext, instrs: wasm.Instr[], expr: Expr) { break; } + case "path": { + todo("path"); + } case "binary": { // By evaluating the LHS first, the RHS is on top, which // is correct as it's popped first. Evaluating the LHS first diff --git a/src/parser.ts b/src/parser.ts index f42d643..9115141 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -16,6 +16,7 @@ import { ImportDef, Item, LOGICAL_KINDS, + ModItem, Type, TypeDef, UNARY_KINDS, @@ -23,6 +24,7 @@ import { binaryExprPrecedenceClass, foldAst, superFoldExpr, + superFoldItem, } from "./ast"; import { CompilerError, Span, spanMerge } from "./error"; import { BaseToken, Token, TokenIdent, TokenLitString } from "./lexer"; @@ -39,7 +41,7 @@ export function parse(t: Token[]): Ast { items.push(item); } - const ast = assignIds({ items: items }); + const ast = assignIds(items); validateAst(ast); @@ -128,6 +130,43 @@ function parseItem(t: Token[]): [Token[], Item] { }; return [t, { kind: "import", node: def, span: tok.span, id: 0 }]; + } else if (tok.kind === "extern") { + [t] = expectNext(t, "mod"); + let name; + [t, name] = expectNext(t, "identifier"); + + const node: ModItem = { + name: name.ident, + modKind: { kind: "extern" }, + }; + + [t] = expectNext(t, ";"); + + return [t, { kind: "mod", node, span: name.span, id: 0 }]; + } else if (tok.kind === "mod") { + let name; + [t, name] = expectNext(t, "identifier"); + + [t] = expectNext(t, "("); + + const contents: Item[] = []; + + while (next(t)[1].kind !== ")") { + let item; + [t, item] = parseItem(t); + + contents.push(item); + } + + [t] = expectNext(t, ")"); + [t] = expectNext(t, ";"); + + const node: ModItem = { + name: name.ident, + modKind: { kind: "inline", contents }, + }; + + return [t, { kind: "mod", node, span: name.span, id: 0 }]; } else { unexpectedToken(tok, "item"); } @@ -615,8 +654,17 @@ function unexpectedToken(token: Token, expected: string): never { } function validateAst(ast: Ast) { + const seenItemIds = new Set(); + const validator: Folder = { ...DEFAULT_FOLDER, + item(item: Item): Item { + if (seenItemIds.has(item.id)) { + throw new Error(`duplicate item id: ${item.id} for ${item.node.name}`); + } + seenItemIds.add(item.id); + return superFoldItem(item, this); + }, expr(expr: Expr): Expr { if (expr.kind === "block") { expr.exprs.forEach((inner) => { @@ -660,13 +708,22 @@ function validateAst(ast: Ast) { foldAst(ast, validator); } -function assignIds(ast: Ast): Ast { - let loopId = new Ids(); +function assignIds(rootItems: Item[]): Ast { + const itemId = new Ids(); + const loopId = new Ids(); - const astItems = { items: ast.items.map((item, i) => ({ ...item, id: i })) }; + const ast: Ast = { + rootItems, + itemsById: new Map(), + }; const assigner: Folder = { ...DEFAULT_FOLDER, + item(item: Item): Item { + const id = itemId.next(); + ast.itemsById.set(id, item); + return { ...superFoldItem(item, this), id }; + }, expr(expr: Expr): Expr { if (expr.kind === "loop") { return { @@ -678,5 +735,5 @@ function assignIds(ast: Ast): Ast { }, }; - return foldAst(astItems, assigner); + return foldAst(ast, assigner); } diff --git a/src/printer.ts b/src/printer.ts index 2036022..adc2205 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -5,6 +5,7 @@ import { Identifier, ImportDef, Item, + ModItem, Resolution, StringLiteral, Ty, @@ -14,7 +15,7 @@ import { } from "./ast"; export function printAst(ast: Ast): string { - return ast.items.map(printItem).join("\n"); + return ast.rootItems.map(printItem).join("\n"); } function printStringLiteral(lit: StringLiteral): string { @@ -22,15 +23,20 @@ function printStringLiteral(lit: StringLiteral): string { } function printItem(item: Item): string { + const id = `/*${item.id}*/ `; + switch (item.kind) { case "function": { - return printFunction(item.node); + return id + printFunction(item.node); } case "type": { - return printTypeDef(item.node); + return id + printTypeDef(item.node); } case "import": { - return printImportDef(item.node); + return id + printImportDef(item.node); + } + case "mod": { + return id +printMod(item.node); } } } @@ -65,6 +71,17 @@ function printImportDef(def: ImportDef): string { )}(${args})${ret};`; } +function printMod(mod: ModItem): string { + switch (mod.modKind.kind) { + case "inline": + return `mod ${mod.name} (\n${mod.modKind.contents + .map(printItem) + .join("\n ")});`; + case "extern": + return `extern mod ${mod.name};`; + } +} + function printExpr(expr: Expr, indent: number): string { switch (expr.kind) { case "empty": { @@ -117,6 +134,9 @@ function printExpr(expr: Expr, indent: number): string { case "ident": { return printIdent(expr.value); } + case "path": { + return `<${expr.segments.join(".")}>${printRes(expr.res)}`; + } case "binary": { return `${printExpr(expr.lhs, indent)} ${expr.binaryKind} ${printExpr( expr.rhs, @@ -185,18 +205,19 @@ function printType(type: Type): string { } } -function printIdent(ident: Identifier): string { - const printRes = (res: Resolution): string => { - switch (res.kind) { - case "local": - return `#${res.index}`; - case "item": - return `#G${res.index}`; - case "builtin": { - return `#B`; - } +function printRes(res: Resolution): string { + switch (res.kind) { + case "local": + return `#${res.index}`; + case "item": + return `#G${res.id}`; + case "builtin": { + return `#B`; } - }; + } +} + +function printIdent(ident: Identifier): string { const res = ident.res ? printRes(ident.res) : ""; return `${ident.name}${res}`; } diff --git a/src/resolve.ts b/src/resolve.ts index a5b4423..c244f98 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -6,21 +6,60 @@ import { Expr, Folder, Identifier, + Item, + ItemId, LocalInfo, + ModItem, Resolution, - foldAst, superFoldExpr, superFoldItem, } from "./ast"; -import { CompilerError } from "./error"; +import { CompilerError, spanMerge, todo } from "./error"; +import { unwrap } from "./utils"; const BUILTIN_SET = new Set(BUILTINS); +type Context = { + ast: Ast; + modContentsCache: Map>; +}; + +function resolveModItem( + cx: Context, + mod: ModItem, + modId: ItemId, + name: string +): ItemId | undefined { + const cachedContents = cx.modContentsCache.get(modId); + if (cachedContents) { + return cachedContents.get(name); + } + + switch (mod.modKind.kind) { + case "inline": { + const contents = new Map( + mod.modKind.contents.map((item) => [item.node.name, item.id]) + ); + cx.modContentsCache.set(modId, contents); + return contents.get(name); + } + case "extern": { + todo("extern mod items"); + } + } +} + export function resolve(ast: Ast): Ast { + const cx: Context = { ast, modContentsCache: new Map() }; + + const rootItems = resolveModule(cx, ast.rootItems); + return { ...ast, rootItems }; +} + +function resolveModule(cx: Context, contents: Item[]): Item[] { const items = new Map(); - for (let i = 0; i < ast.items.length; i++) { - const item = ast.items[i]; + contents.forEach((item) => { const existing = items.get(item.node.name); if (existing !== undefined) { throw new CompilerError( @@ -28,8 +67,8 @@ export function resolve(ast: Ast): Ast { item.span ); } - items.set(item.node.name, i); - } + items.set(item.node.name, item.id); + }); const scopes: string[] = []; @@ -59,7 +98,7 @@ export function resolve(ast: Ast): Ast { if (item !== undefined) { return { kind: "item", - index: item, + id: item, }; } @@ -103,43 +142,99 @@ export function resolve(ast: Ast): Ast { id: item.id, }; } + case "mod": { + if (item.node.modKind.kind === "inline") { + const contents = resolveModule(cx, item.node.modKind.contents); + return { + ...item, + kind: "mod", + node: { ...item.node, modKind: { kind: "inline", contents } }, + }; + } + break; + } } return superFoldItem(item, this); }, expr(expr) { - if (expr.kind === "block") { - const prevScopeLength = scopes.length; - blockLocals.push([]); + switch (expr.kind) { + case "block": { + const prevScopeLength = scopes.length; + blockLocals.push([]); - const exprs = expr.exprs.map((inner) => this.expr(inner)); + const exprs = expr.exprs.map((inner) => this.expr(inner)); - scopes.length = prevScopeLength; - const locals = blockLocals.pop(); + scopes.length = prevScopeLength; + const locals = blockLocals.pop(); - return { - kind: "block", - exprs, - locals, - span: expr.span, - }; - } else if (expr.kind === "let") { - let rhs = this.expr(expr.rhs); - let type = expr.type && this.type(expr.type); + return { + kind: "block", + exprs, + locals, + span: expr.span, + }; + } + case "let": { + let rhs = this.expr(expr.rhs); + let 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); + 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, - }; - } else { - return superFoldExpr(expr, this); + return { + ...expr, + name: expr.name, + local, + type, + rhs, + }; + } + case "fieldAccess": { + if (expr.lhs.kind === "ident") { + // If the lhs is a module we need to convert this into a path. + const res = resolveIdent(expr.lhs.value); + if (res.kind === "item") { + const module = unwrap(cx.ast.itemsById.get(res.id)); + if (module.kind === "mod") { + if (typeof expr.field.value === "number") { + throw new CompilerError( + "module contents cannot be indexed with a number", + expr.field.span + ); + } + + const pathResItem = resolveModItem( + cx, + module.node, + module.id, + expr.field.value + ); + if (pathResItem === undefined) { + throw new CompilerError( + `module ${module.node.name} has no item ${expr.field.value}`, + expr.field.span + ); + } + + const pathRes: Resolution = { kind: "item", id: pathResItem }; + + return { + kind: "path", + segments: [expr.lhs.value.name, expr.field.value], + res: pathRes, + span: spanMerge(expr.lhs.span, expr.field.span), + }; + } + } + } + + return superFoldExpr(expr, this); + } + default: { + return superFoldExpr(expr, this); + } } }, ident(ident) { @@ -148,7 +243,5 @@ export function resolve(ast: Ast): Ast { }, }; - const resolved = foldAst(ast, resolver); - - return resolved; + return contents.map((item) => resolver.item(item)); } diff --git a/src/typeck.ts b/src/typeck.ts index 6e6df80..05ee403 100644 --- a/src/typeck.ts +++ b/src/typeck.ts @@ -14,6 +14,7 @@ import { ItemId, LOGICAL_KINDS, LoopId, + ModItemKind, Resolution, Ty, TY_BOOL, @@ -29,6 +30,7 @@ import { } from "./ast"; import { CompilerError, Span } from "./error"; import { printTy } from "./printer"; +import { unwrap } from "./utils"; function mkTyFn(params: Ty[], returnTy: Ty): Ty { return { kind: "fn", params, returnTy }; @@ -81,10 +83,11 @@ function typeOfBuiltinValue(name: BuiltinName, span: Span): Ty { } } +// TODO: Cleanup, maybe get the ident switch into this function because typeOfItem is unused. function lowerAstTyBase( type: Type, lowerIdentTy: (ident: Identifier) => Ty, - typeOfItem: (index: number) => Ty + typeOfItem: (index: number, cause: Span) => Ty ): Ty { switch (type.kind) { case "ident": { @@ -112,8 +115,8 @@ function lowerAstTyBase( export function typeck(ast: Ast): Ast { const itemTys = new Map(); - function typeOfItem(index: ItemId): Ty { - const item = ast.items[index]; + function typeOfItem(index: ItemId, cause: Span): Ty { + const item = unwrap(ast.itemsById.get(index)); const ty = itemTys.get(index); if (ty) { @@ -154,6 +157,12 @@ export function typeck(ast: Ast): Ast { ty.fields = fields; return ty; } + case "mod": { + throw new CompilerError( + `module ${item.node.name} is not a type`, + cause + ); + } } } @@ -167,7 +176,7 @@ export function typeck(ast: Ast): Ast { throw new Error("Item type cannot refer to local variable"); } case "item": { - return typeOfItem(res.index); + return typeOfItem(res.id, type.span); } case "builtin": { return builtinAsTy(res.name, ident.span); @@ -183,7 +192,7 @@ export function typeck(ast: Ast): Ast { item(item) { switch (item.kind) { case "function": { - const fnTy = typeOfItem(item.id) as TyFn; + const fnTy = typeOfItem(item.id, item.span) as TyFn; const body = checkBody(item.node.body, fnTy, typeOfItem); const returnType = item.node.returnType && { @@ -205,7 +214,7 @@ export function typeck(ast: Ast): Ast { }; } case "import": { - const fnTy = typeOfItem(item.id) as TyFn; + const fnTy = typeOfItem(item.id, item.span) as TyFn; fnTy.params.forEach((param, i) => { switch (param.kind) { @@ -268,7 +277,7 @@ export function typeck(ast: Ast): Ast { fieldNames.add(name); }); - const ty = typeOfItem(item.id) as TyStruct; + const ty = typeOfItem(item.id, item.span) as TyStruct; return { ...item, @@ -284,13 +293,38 @@ export function typeck(ast: Ast): Ast { }, }; } + case "mod": { + switch (item.node.modKind.kind) { + case "inline": + const modKind: ModItemKind = { + kind: "inline", + contents: item.node.modKind.contents.map((item) => + this.item(item) + ), + }; + + return { + ...item, + node: { + ...item.node, + modKind, + }, + }; + case "extern": + // Nothing to check. + return { + ...item, + node: { ...item.node, modKind: { ...item.node.modKind } }, + }; + } + } } }, }; const typecked = foldAst(ast, checker); - const main = typecked.items.find((item) => { + const main = typecked.rootItems.find((item) => { if (item.kind === "function" && item.node.name === "main") { const func = item.node; if (func.returnType !== undefined) { @@ -316,7 +350,7 @@ export function typeck(ast: Ast): Ast { } typecked.typeckResults = { - main: { kind: "item", index: main.id }, + main: { kind: "item", id: main.id }, }; return typecked; @@ -483,7 +517,7 @@ export class InferContext { export function checkBody( body: Expr, fnTy: TyFn, - typeOfItem: (index: number) => Ty + typeOfItem: (index: number, cause: Span) => Ty ): Expr { const localTys = [...fnTy.params]; const loopState: { hasBreak: boolean; loopId: LoopId }[] = []; @@ -497,7 +531,7 @@ export function checkBody( return localTys[idx]; } case "item": { - return typeOfItem(res.index); + return typeOfItem(res.id, span); } case "builtin": return typeOfBuiltinValue(res.name, span); @@ -515,7 +549,7 @@ export function checkBody( return localTys[idx]; } case "item": { - return typeOfItem(res.index); + return typeOfItem(res.id, type.span); } case "builtin": return builtinAsTy(res.name, ident.span); @@ -629,6 +663,10 @@ export function checkBody( return { ...expr, ty }; } + case "path": { + const ty = typeOf(expr.res, expr.span); + return { ...expr, ty }; + } case "binary": { const lhs = this.expr(expr.lhs); const rhs = this.expr(expr.rhs); @@ -731,7 +769,7 @@ export function checkBody( } default: { throw new CompilerError( - "only tuples and structs have fields", + `cannot access field \`${field.value}\` on type \`${printTy(lhs.ty)}\``, expr.span ); } diff --git a/test.nil b/test.nil index 67b19cf..5cd32fe 100644 --- a/test.nil +++ b/test.nil @@ -1,3 +1,9 @@ +extern mod std; + +mod uwu; + +mod owo {} + function main() = ( prIntln(0); prIntln(1);