diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b661577..a370cde 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -17,6 +17,7 @@ module.exports = {
// Some silly rules forbidding things that are not wrong:
"no-constant-condition": "off",
"no-empty": "off",
+ "@typescript-eslint/no-empty-function": "off",
// Typescript already checks problematic fallthrough.
// The eslint rule is a bit dumb and also complains about
// obvious clear fallthrough like `case "a": case "b"`.
diff --git a/src/ast.ts b/src/ast.ts
index 85c5ce2..cfba0da 100644
--- a/src/ast.ts
+++ b/src/ast.ts
@@ -1,12 +1,6 @@
import { Span } from "./error";
import { LitIntType } from "./lexer";
-export type Ast
= {
- rootItems: Item
[];
- itemsById: Map>;
- packageName: string;
-} & P["typeckResults"];
-
export type Phase = {
res: unknown;
defPath: unknown;
@@ -45,6 +39,7 @@ export type Typecked = {
ty: HasTy;
typeckResults: HasTypeckResults;
};
+
export type AnyPhase = {
res: No | HasRes;
defPath: No | HasDefPath;
@@ -52,6 +47,15 @@ export type AnyPhase = {
typeckResults: No | HasTypeckResults;
};
+export type CrateId = number;
+
+export type Crate = {
+ id: CrateId;
+ rootItems: Item
[];
+ itemsById: Map>;
+ packageName: string;
+} & P["typeckResults"];
+
export type Ident = {
name: string;
span: Span;
@@ -80,6 +84,10 @@ export type ItemKind =
| {
kind: "mod";
node: ModItem
;
+ }
+ | {
+ kind: "extern";
+ node: ExternItem;
};
export type Item
= ItemKind
& {
@@ -123,17 +131,10 @@ export type ImportDef
= {
export type ModItem
= {
name: string;
- modKind: ModItemKind
;
+ contents: Item
[];
};
-export type ModItemKind
=
- | {
- kind: "inline";
- contents: Item
[];
- }
- | {
- kind: "extern";
- };
+export type ExternItem = { name: string };
export type ExprEmpty = { kind: "empty" };
@@ -486,7 +487,7 @@ export const TY_I32: Ty = { kind: "i32" };
export const TY_NEVER: Ty = { kind: "never" };
export type TypeckResults = {
- main: Resolution;
+ main: Resolution | undefined;
};
// folders
@@ -521,7 +522,7 @@ export function mkDefaultFolder<
newItemsById: new Map(),
item(item) {
const newItem = this.itemInner(item);
- this.newItemsById.set(item.id, newItem);
+ this.newItemsById.set(newItem.id, newItem);
return newItem;
},
itemInner(_item) {
@@ -534,14 +535,15 @@ export function mkDefaultFolder<
}
export function foldAst(
- ast: Ast,
+ ast: Crate,
folder: Folder
-): Ast {
+): Crate {
if ((folder.item as any)[ITEM_DEFAULT] !== ITEM_DEFAULT) {
throw new Error("must not override `item` on folders");
}
return {
+ id: ast.id,
rootItems: ast.rootItems.map((item) => folder.item(item)),
itemsById: folder.newItemsById,
typeckResults: "typeckResults" in ast ? ast.typeckResults : undefined,
@@ -603,29 +605,18 @@ export function superFoldItem(
};
}
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,
+ contents: item.node.contents.map((item) => folder.item(item)),
},
};
}
+ case "extern": {
+ return { ...item, kind: "extern" };
+ }
}
}
diff --git a/src/error.ts b/src/error.ts
index 9b690ea..b499361 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -23,19 +23,19 @@ export class CompilerError extends Error {
}
}
-export function withErrorHandler(input: string, f: () => void): void {
+export function withErrorPrinter(input: string, filename: string, f: () => void): void {
try {
f();
} catch (e) {
if (e instanceof CompilerError) {
- renderError(input, e);
+ renderError(input, filename, e);
} else {
throw e;
}
}
}
-function renderError(input: string, e: CompilerError) {
+function renderError(input: string, filename: string, e: CompilerError) {
const lineSpans = lines(input);
const line =
e.span.start === Number.MAX_SAFE_INTEGER
@@ -49,6 +49,8 @@ function renderError(input: string, e: CompilerError) {
const lineIdx = lineSpans.indexOf(line);
const lineNo = lineIdx + 1;
console.error(`error: ${e.message}`);
+ console.error(` --> ${filename}:${lineNo}`);
+
console.error(`${lineNo} | ${spanToSnippet(input, line)}`);
const startRelLine =
diff --git a/src/index.ts b/src/index.ts
index c64164a..4aae1ea 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-import { withErrorHandler } from "./error";
+import { CompilerError, Span, withErrorPrinter } from "./error";
import { isValidIdent, tokenize } from "./lexer";
import { lower as lowerToWasm } from "./lower";
import { parse } from "./parser";
@@ -9,23 +9,23 @@ import { writeModuleWatToString } from "./wasm/wat";
import fs from "fs";
import path from "path";
import { exec } from "child_process";
-import { Ast, Built, Typecked } from "./ast";
+import { Crate, Built, Typecked } from "./ast";
+import { Ids } from "./utils";
const INPUT = `
-function main() = (
- prIntln(0);
-);
+extern mod std;
-function prIntln(x: Int) = (
- print("\n");
+function main() = (
+ std.pow(10, 2);
);
`;
function main() {
+ let filename: string;
let input: string;
let packageName: string;
if (process.argv.length > 2) {
- const filename = process.argv[2];
+ filename = process.argv[2];
if (path.extname(filename) !== ".nil") {
console.error(
`error: filename must have \`.nil\` extension: \`${filename}\``
@@ -36,6 +36,7 @@ function main() {
input = fs.readFileSync(filename, { encoding: "utf-8" });
packageName = path.basename(filename, ".nil");
} else {
+ filename = "";
input = INPUT;
packageName = "test";
}
@@ -49,14 +50,14 @@ function main() {
console.log(`package name: '${packageName}'`);
- withErrorHandler(input, () => {
+ withErrorPrinter(input, filename, () => {
const start = Date.now();
const tokens = tokenize(input);
console.log("-----TOKENS------------");
console.log(tokens);
- const ast: Ast = parse(packageName, tokens);
+ const ast: Crate = parse(packageName, tokens, 0);
console.log("-----AST---------------");
console.dir(ast.rootItems, { depth: 50 });
@@ -66,17 +67,17 @@ function main() {
console.log(printed);
console.log("-----AST resolved------");
- const resolved = resolve(ast);
+ const [resolved, crates] = resolve(ast, loadCrate);
const resolvedPrinted = printAst(resolved);
console.log(resolvedPrinted);
console.log("-----AST typecked------");
- const typecked: Ast = typeck(resolved);
+ const typecked: Crate = typeck(resolved);
const typeckPrinted = printAst(typecked);
console.log(typeckPrinted);
console.log("-----wasm--------------");
- const wasmModule = lowerToWasm(typecked);
+ const wasmModule = lowerToWasm([typecked, ...crates]);
const moduleStringColor = writeModuleWatToString(wasmModule, true);
const moduleString = writeModuleWatToString(wasmModule);
@@ -105,4 +106,46 @@ function main() {
});
}
+function loadCrate(
+ name: string,
+ span: Span,
+ crateId: Ids,
+ existingCrates: Crate[]
+): [Crate, Crate[]] {
+ // We really, really want a good algorithm for finding crates.
+ // But right now we just look for files in the CWD.
+
+ const existing = existingCrates.find((crate) => crate.packageName === name);
+ if (existing) {
+ return [existing, []];
+ }
+
+ const filename = `${name}.nil`;
+ let input;
+ try {
+ input = fs.readFileSync(filename, { encoding: "utf-8" });
+ } catch (e) {
+ throw new CompilerError(
+ `failed to load ${name}, could not fine \`${filename}\``,
+ span
+ );
+ }
+
+ try {
+ const tokens = tokenize(input);
+ const ast = parse(name, tokens, crateId.next());
+ const [resolved, crates] = resolve(ast, loadCrate);
+ const typecked = typeck(resolved);
+ return [typecked, crates];
+ } catch (e) {
+ withErrorPrinter(input, filename, () => {
+ throw e;
+ });
+ throw new CompilerError(
+ `failed to load crate ${name}: crate contains errors`,
+ span
+ );
+ }
+}
+
main();
diff --git a/src/lexer.ts b/src/lexer.ts
index 1ea29e4..902c923 100644
--- a/src/lexer.ts
+++ b/src/lexer.ts
@@ -230,10 +230,7 @@ export function tokenize(input: string): Token[] {
}
let type: LitIntType = "Int";
- 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") {
i += 4;
type = "Int";
diff --git a/src/lower.ts b/src/lower.ts
index 1398ba8..15adbd2 100644
--- a/src/lower.ts
+++ b/src/lower.ts
@@ -1,5 +1,5 @@
import {
- Ast,
+ Crate,
Expr,
ExprBlock,
FunctionDef,
@@ -39,7 +39,7 @@ export type Context = {
funcTypes: ComplexMap;
reservedHeapMemoryStart: number;
funcIndices: ComplexMap;
- ast: Ast;
+ crates: Crate[];
relocations: Relocation[];
};
@@ -89,7 +89,7 @@ function appendData(cx: Context, newData: Uint8Array): number {
}
}
-export function lower(ast: Ast): wasm.Module {
+export function lower(crates: Crate[]): wasm.Module {
const mod: wasm.Module = {
types: [],
funcs: [],
@@ -119,7 +119,7 @@ export function lower(ast: Ast): wasm.Module {
funcTypes: new ComplexMap(),
funcIndices: new ComplexMap(),
reservedHeapMemoryStart: 0,
- ast,
+ crates,
relocations: [],
};
@@ -135,15 +135,12 @@ export function lower(ast: Ast): wasm.Module {
break;
}
case "mod": {
- if (item.node.modKind.kind === "inline") {
- lowerMod(item.node.modKind.contents);
- }
+ lowerMod(item.node.contents);
}
}
});
}
-
- lowerMod(ast.rootItems);
+ crates.forEach((ast) => lowerMod(ast.rootItems));
const HEAP_ALIGN = 0x08;
cx.reservedHeapMemoryStart =
@@ -151,7 +148,7 @@ export function lower(ast: Ast): wasm.Module {
? (mod.datas[0].init.length + (HEAP_ALIGN - 1)) & ~(HEAP_ALIGN - 1)
: 0;
- addRt(cx, ast);
+ addRt(cx, crates);
// THE LINKER
const offset = cx.mod.imports.length;
@@ -863,14 +860,16 @@ function todo(msg: string): never {
}
// Make the program runnable using wasi-preview-1
-function addRt(cx: Context, ast: Ast) {
+function addRt(cx: Context, crates: Crate[]) {
const { mod } = cx;
+ const crate0 = unwrap(crates.find((crate) => crate.id === 0));
+
const mainCall: wasm.Instr = { kind: "call", func: 9999999 };
cx.relocations.push({
kind: "funccall",
instr: mainCall,
- res: ast.typeckResults.main,
+ res: unwrap(crate0.typeckResults.main),
});
const start: wasm.Func = {
diff --git a/src/parser.ts b/src/parser.ts
index a646b75..7f213c0 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -1,7 +1,7 @@
import {
ARITH_FACTOR_KINDS,
ARITH_TERM_KINDS,
- Ast,
+ Crate,
BinaryKind,
COMPARISON_KINDS,
mkDefaultFolder,
@@ -27,6 +27,7 @@ import {
superFoldItem,
Built,
Parsed,
+ ExternItem,
} from "./ast";
import { CompilerError, Span, spanMerge } from "./error";
import { BaseToken, Token, TokenIdent, TokenLitString } from "./lexer";
@@ -34,7 +35,11 @@ import { Ids } from "./utils";
type Parser = (t: Token[]) => [Token[], T];
-export function parse(packageName: string, t: Token[]): Ast {
+export function parse(
+ packageName: string,
+ t: Token[],
+ crateId: number
+): Crate {
const items: Item[] = [];
while (t.length > 0) {
@@ -43,7 +48,7 @@ export function parse(packageName: string, t: Token[]): Ast {
items.push(item);
}
- const ast = buildAst(packageName, items);
+ const ast: Crate = buildCrate(packageName, items, crateId);
validateAst(ast);
@@ -137,14 +142,13 @@ function parseItem(t: Token[]): [Token[], Item] {
let name;
[t, name] = expectNext(t, "identifier");
- const node: ModItem = {
+ const node: ExternItem = {
name: name.ident,
- modKind: { kind: "extern" },
};
[t] = expectNext(t, ";");
- return [t, { kind: "mod", node, span: name.span, id: 0 }];
+ return [t, { kind: "extern", node, span: name.span, id: 0 }];
} else if (tok.kind === "mod") {
let name;
[t, name] = expectNext(t, "identifier");
@@ -165,7 +169,7 @@ function parseItem(t: Token[]): [Token[], Item] {
const node: ModItem = {
name: name.ident,
- modKind: { kind: "inline", contents },
+ contents,
};
return [t, { kind: "mod", node, span: name.span, id: 0 }];
@@ -658,7 +662,7 @@ function unexpectedToken(token: Token, expected: string): never {
throw new CompilerError(`unexpected token, expected ${expected}`, token.span);
}
-function validateAst(ast: Ast) {
+function validateAst(ast: Crate) {
const seenItemIds = new Set();
const validator: Folder = {
@@ -719,11 +723,16 @@ function validateAst(ast: Ast) {
foldAst(ast, validator);
}
-function buildAst(packageName: string, rootItems: Item[]): Ast {
+function buildCrate(
+ packageName: string,
+ rootItems: Item[],
+ crateId: number
+): Crate {
const itemId = new Ids();
const loopId = new Ids();
- const ast: Ast = {
+ const ast: Crate = {
+ id: crateId,
rootItems,
itemsById: new Map(),
packageName,
@@ -733,7 +742,6 @@ function buildAst(packageName: string, rootItems: Item[]): Ast {
...mkDefaultFolder(),
itemInner(item: Item): Item {
const id = itemId.next();
- ast.itemsById.set(id, item);
return { ...superFoldItem(item, this), id };
},
expr(expr: Expr): Expr {
diff --git a/src/printer.ts b/src/printer.ts
index bc7b531..76ea7cf 100644
--- a/src/printer.ts
+++ b/src/printer.ts
@@ -1,6 +1,6 @@
import {
AnyPhase,
- Ast,
+ Crate,
Expr,
FunctionDef,
IdentWithRes,
@@ -15,7 +15,7 @@ import {
tyIsUnit,
} from "./ast";
-export function printAst(ast: Ast): string {
+export function printAst(ast: Crate): string {
return ast.rootItems.map(printItem).join("\n");
}
@@ -39,6 +39,9 @@ function printItem(item: Item): string {
case "mod": {
return id + printMod(item.node);
}
+ case "extern": {
+ return id + `extern mod ${item.node.name};`;
+ }
}
}
@@ -73,14 +76,7 @@ function printImportDef(def: ImportDef): string {
}
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};`;
- }
+ return `mod ${mod.name} (\n${mod.contents.map(printItem).join("\n ")});`;
}
function printExpr(expr: Expr, indent: number): string {
diff --git a/src/resolve.ts b/src/resolve.ts
index 7289d04..b46a9b6 100644
--- a/src/resolve.ts
+++ b/src/resolve.ts
@@ -1,5 +1,5 @@
import {
- Ast,
+ Crate,
BUILTINS,
Built,
BuiltinName,
@@ -16,56 +16,89 @@ import {
superFoldExpr,
superFoldItem,
superFoldType,
+ ExternItem,
+ Typecked,
} from "./ast";
-import { CompilerError, spanMerge, todo } from "./error";
-import { unwrap } from "./utils";
+import { CompilerError, Span, spanMerge } from "./error";
+import { Ids, unwrap } from "./utils";
const BUILTIN_SET = new Set(BUILTINS);
+export type CrateLoader = (
+ name: string,
+ span: Span,
+ crateId: Ids,
+ existingCrates: Crate[]
+) => [Crate, Crate[]];
+
type Context = {
- ast: Ast;
+ ast: Crate;
+ crates: Crate[];
modContentsCache: Map>;
newItemsById: Map>;
+ crateLoader: CrateLoader;
+ crateId: Ids;
};
function resolveModItem(
cx: Context,
- mod: ModItem,
- modId: ItemId,
+ mod: ModItem | ExternItem,
+ item: Item,
name: string
): ItemId | undefined {
- const cachedContents = cx.modContentsCache.get(modId);
+ const cachedContents = cx.modContentsCache.get(item.id);
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");
- }
+ let contents: Map;
+
+ if ("contents" in mod) {
+ contents = new Map(mod.contents.map((item) => [item.node.name, item.id]));
+ } else {
+ const [loadedCrate, itsDeps] = cx.crateLoader(
+ item.node.name,
+ item.span,
+ cx.crateId,
+ cx.crates
+ );
+ cx.crates.push(loadedCrate);
+ cx.crates.push(...itsDeps);
+
+ contents = new Map(
+ loadedCrate.rootItems.map((item) => [item.node.name, item.id])
+ );
}
+
+ cx.modContentsCache.set(item.id, contents);
+ return contents.get(name);
}
-export function resolve(ast: Ast): Ast {
+export function resolve(
+ ast: Crate,
+ crateLoader: CrateLoader
+): [Crate, Crate[]] {
+ const crateId = new Ids();
+ crateId.next(); // Local crate.
const cx: Context = {
ast,
+ crates: [],
modContentsCache: new Map(),
newItemsById: new Map(),
+ crateLoader,
+ crateId,
};
const rootItems = resolveModule(cx, [ast.packageName], ast.rootItems);
- return {
- itemsById: cx.newItemsById,
- rootItems,
- packageName: ast.packageName,
- };
+ return [
+ {
+ id: ast.id,
+ itemsById: cx.newItemsById,
+ rootItems,
+ packageName: ast.packageName,
+ },
+ cx.crates,
+ ];
}
function resolveModule(
@@ -129,7 +162,7 @@ function resolveModule(
const resolver: Folder = {
...mkDefaultFolder(),
- itemInner(item) {
+ itemInner(item): Item {
const defPath = [...modName, item.node.name];
switch (item.kind) {
@@ -162,20 +195,23 @@ function resolveModule(
};
}
case "mod": {
- if (item.node.modKind.kind === "inline") {
- const contents = resolveModule(
- cx,
- defPath,
- item.node.modKind.contents
- );
- return {
- ...item,
- kind: "mod",
- node: { ...item.node, modKind: { kind: "inline", contents } },
- defPath,
- };
- }
- break;
+ const contents = resolveModule(cx, defPath, item.node.contents);
+ return {
+ ...item,
+ kind: "mod",
+ node: { ...item.node, contents },
+ defPath,
+ };
+ }
+ case "extern": {
+ const node: ExternItem = {
+ ...item.node,
+ };
+ return {
+ ...item,
+ node,
+ defPath,
+ };
}
}
@@ -230,7 +266,11 @@ function resolveModule(
if (res.kind === "item") {
const module = unwrap(cx.ast.itemsById.get(res.id));
- if (module.kind === "mod") {
+ console.log("nested", module.kind, res.id, cx.ast.itemsById);
+
+ if (module.kind === "mod" || module.kind === "extern") {
+ console.log("resolve");
+
if (typeof expr.field.value === "number") {
throw new CompilerError(
"module contents cannot be indexed with a number",
@@ -241,7 +281,7 @@ function resolveModule(
const pathResItem = resolveModItem(
cx,
module.node,
- module.id,
+ module,
expr.field.value
);
if (pathResItem === undefined) {
diff --git a/src/typeck.ts b/src/typeck.ts
index 703a941..0fb2ca6 100644
--- a/src/typeck.ts
+++ b/src/typeck.ts
@@ -1,5 +1,5 @@
import {
- Ast,
+ Crate,
BuiltinName,
COMPARISON_KINDS,
mkDefaultFolder,
@@ -14,7 +14,6 @@ import {
ItemId,
LOGICAL_KINDS,
LoopId,
- ModItemKind,
Resolution,
Resolved,
Ty,
@@ -116,7 +115,7 @@ function lowerAstTyBase(
}
}
-export function typeck(ast: Ast): Ast {
+export function typeck(ast: Crate): Crate {
const itemTys = new Map();
function typeOfItem(index: ItemId, cause: Span): Ty {
const item = unwrap(ast.itemsById.get(index));
@@ -162,7 +161,13 @@ export function typeck(ast: Ast): Ast {
}
case "mod": {
throw new CompilerError(
- `module ${item.node.name} is not a type`,
+ `module ${item.node.name} cannot be used as a type or value`,
+ cause
+ );
+ }
+ case "extern": {
+ throw new CompilerError(
+ `extern declaration ${item.node.name} cannot be used as a type or value`,
cause
);
}
@@ -297,30 +302,20 @@ 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 } },
- };
- }
+ return {
+ ...item,
+ node: {
+ ...item.node,
+ contents: item.node.contents.map((item) => this.item(item)),
+ },
+ };
+ }
+ case "extern": {
+ // Nothing to check.
+ return {
+ ...item,
+ node: { ...item.node },
+ };
}
}
},
@@ -355,16 +350,19 @@ export function typeck(ast: Ast): Ast {
return false;
});
- if (!main) {
- throw new CompilerError(`\`main\` function not found`, {
- start: 0,
- end: 1,
- });
- }
+ if (ast.id === 0) {
+ // Only the final id=0 crate needs and cares about main.
+ if (!main) {
+ throw new CompilerError(`\`main\` function not found`, {
+ start: 0,
+ end: 1,
+ });
+ }
- typecked.typeckResults = {
- main: { kind: "item", id: main.id },
- };
+ typecked.typeckResults = {
+ main: { kind: "item", id: main.id },
+ };
+ }
return typecked;
}
diff --git a/src/utils.ts b/src/utils.ts
index 7392b56..826e62e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -3,7 +3,7 @@ export function encodeUtf8(s: string): Uint8Array {
}
export class Ids {
- nextId = 0;
+ private nextId = 0;
public next(): number {
return this.nextId++;
diff --git a/std.nil b/std.nil
new file mode 100644
index 0000000..90a9d41
--- /dev/null
+++ b/std.nil
@@ -0,0 +1,9 @@
+function pow(base: Int, exp: Int): Int = (
+ let acc = 1;
+ loop (
+ if exp == 0 then break;
+ acc = acc * base;
+ exp = exp - 1;
+ );
+ acc
+);