diff --git a/.prettierignore b/.prettierignore index c3a237c..4cd37e2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ /target -/ui-tests/target \ No newline at end of file +/ui-harness/target \ No newline at end of file diff --git a/src/ast.ts b/src/ast.ts index 1937c4d..e32ca04 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,4 +1,4 @@ -import { LoadedFile, Span } from "./error"; +import { ErrorEmitted, LoadedFile, Span, unreachable } from "./error"; import { LitIntType } from "./lexer"; import { ComplexMap } from "./utils"; @@ -58,6 +58,7 @@ export type Crate

= { itemsById: ComplexMap>; packageName: string; rootFile: LoadedFile; + fatalError: ErrorEmitted | undefined; } & P["typeckResults"]; export type DepCrate = Crate; @@ -103,7 +104,8 @@ export type ItemKind

= | ItemKindImport

| ItemKindMod

| ItemKindExtern - | ItemKindGlobal

; + | ItemKindGlobal

+ | ItemKindError; type ItemVariant = Variant & Item

; @@ -113,6 +115,7 @@ export type ItemImport

= ItemVariant, P>; export type ItemMod

= ItemVariant, P>; export type ItemExtern

= ItemVariant; export type ItemGlobal

= ItemVariant, P>; +export type ItemError

= ItemVariant; export type Item

= ItemKind

& { span: Span; @@ -178,6 +181,11 @@ export type ItemKindGlobal

= { ty?: Ty; }; +export type ItemKindError = { + kind: "error"; + err: ErrorEmitted; +}; + export type ExprEmpty = { kind: "empty" }; export type ExprLet

= { @@ -294,11 +302,16 @@ export type StructLiteralField

= { fieldIdx?: number; }; -export type TupleLiteral

= { +export type ExprTupleLiteral

= { kind: "tupleLiteral"; fields: Expr

[]; }; +export type ExprError = { + kind: "error"; + err: ErrorEmitted; +}; + export type ExprKind

= | ExprEmpty | ExprLet

@@ -315,7 +328,8 @@ export type ExprKind

= | ExprLoop

| ExprBreak | ExprStructLiteral

- | TupleLiteral

; + | ExprTupleLiteral

+ | ExprError; export type Expr

= ExprKind

& { span: Span; @@ -382,7 +396,7 @@ const BINARY_KIND_PREC_CLASS = new Map([ export function binaryExprPrecedenceClass(k: BinaryKind): number { const cls = BINARY_KIND_PREC_CLASS.get(k); if (cls === undefined) { - throw new Error(`Invalid binary kind: '${k}'`); + unreachable(`Invalid binary kind: '${k}'`); } return cls; } @@ -407,7 +421,8 @@ export type TypeKind

= kind: "rawptr"; inner: Type

; } - | { kind: "never" }; + | { kind: "never" } + | { kind: "error"; err: ErrorEmitted }; export type Type

= TypeKind

& { span: Span; @@ -437,7 +452,8 @@ export type Resolution = | { kind: "builtin"; name: BuiltinName; - }; + } + | { kind: "error"; err: ErrorEmitted }; export const BUILTINS = [ "print", @@ -527,6 +543,11 @@ export type TyNever = { kind: "never"; }; +export type TyError = { + kind: "error"; + err: ErrorEmitted; +}; + export type Ty = | TyString | TyInt @@ -538,7 +559,8 @@ export type Ty = | TyVar | TyStruct | TyRawPtr - | TyNever; + | TyNever + | TyError; export function tyIsUnit(ty: Ty): ty is TyUnit { return ty.kind === "tuple" && ty.elems.length === 0; @@ -591,7 +613,7 @@ export function mkDefaultFolder< return newItem; }, itemInner(_item) { - throw new Error("unimplemented"); + unreachable("unimplemented"); }, }; (folder.item as any)[ITEM_DEFAULT] = ITEM_DEFAULT; @@ -604,7 +626,7 @@ export function foldAst( folder: Folder, ): Crate { if ((folder.item as any)[ITEM_DEFAULT] !== ITEM_DEFAULT) { - throw new Error("must not override `item` on folders"); + unreachable("must not override `item` on folders"); } return { @@ -614,6 +636,7 @@ export function foldAst( typeckResults: "typeckResults" in ast ? ast.typeckResults : undefined, packageName: ast.packageName, rootFile: ast.rootFile, + fatalError: ast.fatalError, }; } @@ -701,6 +724,9 @@ export function superFoldItem( init: folder.expr(item.init), }; } + case "error": { + return { ...item }; + } } } @@ -818,6 +844,9 @@ export function superFoldExpr( fields: expr.fields.map(folder.expr.bind(folder)), }; } + case "error": { + return { ...expr }; + } } } @@ -857,9 +886,11 @@ export function superFoldType( case "never": { return { ...type, kind: "never" }; } + case "error": + return { ...type }; } } export function varUnreachable(): never { - throw new Error("Type variables must not occur after type checking"); + unreachable("Type variables must not occur after type checking"); } diff --git a/src/codegen.ts b/src/codegen.ts index 15af43f..83c6f0c 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -210,6 +210,8 @@ export function lower(gcx: GlobalContext): wasm.Module { case "extern": case "type": break; + case "error": + unreachable("codegen should never see errors"); default: { const _: never = item; } @@ -233,7 +235,7 @@ export function lower(gcx: GlobalContext): wasm.Module { case "funccall": { const idx = cx.funcIndices.get(rel.res); if (idx === undefined) { - throw new Error( + unreachable( `no function found for relocation '${JSON.stringify(rel.res)}'`, ); } @@ -243,7 +245,7 @@ export function lower(gcx: GlobalContext): wasm.Module { case "globalref": { const idx = cx.globalIndices.get(rel.res); if (idx === undefined) { - throw new Error( + unreachable( `no global found for relocation '${JSON.stringify(rel.res)}'`, ); } @@ -299,11 +301,11 @@ function lowerGlobal(cx: Context, def: ItemGlobal) { valtype = "i64"; break; default: - throw new Error(`invalid global ty: ${printTy(def.init.ty)}`); + unreachable(`invalid global ty: ${printTy(def.init.ty)}`); } if (def.init.kind !== "literal" || def.init.value.kind !== "int") { - throw new Error(`invalid global init: ${JSON.stringify(def)}`); + unreachable(`invalid global init: ${JSON.stringify(def)}`); } const init: wasm.Instr = { @@ -464,7 +466,7 @@ function tryLowerLValue( case "item": { const item = fcx.cx.gcx.findItem(res.id); if (item.kind !== "global") { - throw new Error("cannot store to non-global item"); + unreachable("cannot store to non-global item"); } return { @@ -473,9 +475,11 @@ function tryLowerLValue( }; } case "builtin": { - throw new Error("cannot store to builtin"); + unreachable("cannot store to builtin"); } } + + break; } case "fieldAccess": { // Field access lvalues (or rather, lvalues in general) are made of two important parts: @@ -666,7 +670,7 @@ function lowerExpr( case "print": todo("print function"); default: { - throw new Error(`${res.name}#B is not a value`); + unreachable(`${res.name}#B is not a value`); } } } @@ -758,7 +762,7 @@ function lowerExpr( case "*": case "/": case "%": - throw new Error(`Invalid bool binary expr: ${expr.binaryKind}`); + unreachable(`Invalid bool binary expr: ${expr.binaryKind}`); } instrs.push({ kind }); @@ -785,7 +789,7 @@ function lowerExpr( instrs.push({ kind: "i32.const", imm: -1n }); instrs.push({ kind: "i32.xor" }); } else { - throw new Error("invalid type for !"); + unreachable("invalid type for !"); } break; case "-": @@ -801,7 +805,7 @@ function lowerExpr( const { res } = expr.lhs.value; if (res.kind === "builtin") { const assertArgs = (n: number) => { - if (expr.args.length !== n) throw new Error("nope"); + if (expr.args.length !== n) unreachable("nope"); }; switch (res.name) { case "trap": { @@ -951,7 +955,7 @@ function lowerExpr( }); break; default: { - throw new Error( + unreachable( `unsupported struct content type: ${fieldPart.type}`, ); } @@ -961,7 +965,7 @@ function lowerExpr( break; } default: - throw new Error("invalid field access lhs"); + unreachable("invalid field access lhs"); } break; @@ -1042,7 +1046,7 @@ function lowerExpr( const allocate: wasm.Instr = { kind: "call", func: DUMMY_IDX }; const allocateItemId = fcx.cx.knownDefPaths.get(ALLOCATE_ITEM); if (!allocateItemId) { - throw new Error("std.rt.allocateItem not found"); + unreachable("std.rt.allocateItem not found"); } fcx.cx.relocations.push({ kind: "funccall", @@ -1074,6 +1078,8 @@ function lowerExpr( expr.fields.forEach((field) => lowerExpr(fcx, instrs, field)); break; } + case "error": + unreachable("codegen should never see errors"); default: { const _: never = expr; } @@ -1247,6 +1253,8 @@ function argRetAbi(param: Ty): ArgRetAbi { return []; case "var": varUnreachable(); + case "error": + unreachable("codegen should not see errors"); } } @@ -1302,6 +1310,8 @@ function wasmTypeForBody(ty: Ty): wasm.ValType[] { return []; case "var": varUnreachable(); + case "error": + unreachable("codegen should not see errors"); } } @@ -1316,7 +1326,7 @@ function sizeOfValtype(type: wasm.ValType): number { case "v128": case "funcref": case "externref": - throw new Error("types not emitted"); + unreachable("types not emitted"); } } @@ -1490,7 +1500,7 @@ function subRefcount( ) { const deallocateItemId = fcx.cx.knownDefPaths.get(DEALLOCATE_ITEM); if (!deallocateItemId) { - throw new Error("std.rt.deallocateItem not found"); + unreachable("std.rt.deallocateItem not found"); } const layout: wasm.ValType[] = kind === "string" ? ["i32", "i32"] : ["i32"]; @@ -1556,7 +1566,7 @@ function subRefcount( } function todo(msg: string): never { - throw new Error(`TODO: ${msg}`); + unreachable(`TODO: ${msg}`); } // Make the program runnable using wasi-preview-1 diff --git a/src/context.ts b/src/context.ts index 3b861ec..b6cba67 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,5 @@ import { Crate, DepCrate, Final, Item, ItemId, Phase } from "./ast"; -import { Span } from "./error"; +import { ErrorHandler, Span } from "./error"; import { Ids, unwrap } from "./utils"; import fs from "fs"; import path from "path"; @@ -20,6 +20,7 @@ export type CrateLoader = ( * dependencies (which also use the same context) do not care about that. */ export class GlobalContext { + public error: ErrorHandler = new ErrorHandler(); public finalizedCrates: Crate[] = []; public crateId: Ids = new Ids(); @@ -38,6 +39,17 @@ export class GlobalContext { allCrates.find((crate) => crate.id === id.crateId), ); + if (crate.fatalError) { + return { + kind: "error", + defPath: [], + err: crate.fatalError, + id: new ItemId(crate.id, 0), + name: "", + span: Span.startOfFile(crate.rootFile), + }; + } + if (id.itemIdx === 0) { const contents: Item

[] | Item[] = crate.rootItems; // Typescript does not seem to be able to understand this here. diff --git a/src/error.ts b/src/error.ts index b2a839c..35fee9b 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,3 +1,5 @@ +import chalk from "chalk"; + export type LoadedFile = { path?: string; content: string; @@ -33,34 +35,44 @@ export class Span { public static DUMMY: Span = new Span(0, 0, { content: "" }); } -export class CompilerError extends Error { +export type Emitter = (string: string) => void; + +export class ErrorHandler { + private errors: CompilerError[] = []; + + constructor( + private emitter = (msg: string) => globalThis.console.error(msg), + ) {} + + public emit(err: CompilerError): ErrorEmitted { + renderError(this.emitter, err); + this.errors.push(err); + return ERROR_EMITTED; + } + + public hasErrors(): boolean { + return this.errors.length > 0; + } +} + +const ERROR_EMITTED = Symbol(); +export type ErrorEmitted = typeof ERROR_EMITTED; + +export class CompilerError { msg: string; span: Span; constructor(msg: string, span: Span) { - super(msg); this.msg = msg; this.span = span; } } -export function withErrorPrinter( - f: () => R, - afterError: (e: CompilerError) => R, -): R { - try { - return f(); - } catch (e) { - if (e instanceof CompilerError) { - renderError(e); - return afterError(e); - } else { - throw e; - } - } -} +// Shadow console. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const console = {}; -function renderError(e: CompilerError) { +function renderError(emitter: Emitter, e: CompilerError) { const { span } = e; const { content } = span.file; @@ -76,10 +88,10 @@ function renderError(e: CompilerError) { } const lineIdx = lineSpans.indexOf(line); const lineNo = lineIdx + 1; - console.error(`error: ${e.message}`); - console.error(` --> ${span.file.path ?? ""}:${lineNo}`); + emitter(chalk.red(`error: ${e.msg}`)); + emitter(` --> ${span.file.path ?? ""}:${lineNo}`); - console.error(`${lineNo} | ${spanToSnippet(content, line)}`); + emitter(`${lineNo} | ${spanToSnippet(content, line)}`); const startRelLine = span.start === Number.MAX_SAFE_INTEGER ? 0 : span.start - line.start; @@ -88,7 +100,7 @@ function renderError(e: CompilerError) { ? 1 : min(span.end, line.end) - span.start; - console.error( + emitter( `${" ".repeat(String(lineNo).length)} ${" ".repeat( startRelLine, )}${"^".repeat(spanLength)}`, diff --git a/src/index.ts b/src/index.ts index 41d04ae..a15122c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { LoadedFile, Span, withErrorPrinter } from "./error"; +import { LoadedFile, Span } from "./error"; import { isValidIdent, tokenize } from "./lexer"; import { lower as lowerToWasm } from "./codegen"; import { ParseState, parse } from "./parser"; @@ -16,9 +16,9 @@ const INPUT = ` type A = struct { a: Int }; function main() = ( - let a = A { a: 0 }; - rawr(___transmute(a)); - std.printInt(a.a); + let a: Int = ""; + let b: Int = ""; + c; ); function rawr(a: *A) = ( @@ -42,91 +42,95 @@ function main() { const gcx = new GlobalContext(opts, loadCrate); const mainCrate = gcx.crateId.next(); - withErrorPrinter( - () => { - const start = Date.now(); + const start = Date.now(); - if (packageName !== "std") { - gcx.crateLoader(gcx, "std", Span.startOfFile(file)); + if (packageName !== "std") { + gcx.crateLoader(gcx, "std", Span.startOfFile(file)); + } + + const tokens = tokenize(gcx.error, file); + // We treat lexer errors as fatal. + if (!tokens.ok) { + process.exit(1); + } + if (debug.has("tokens")) { + console.log("-----TOKENS------------"); + console.log(tokens); + } + + const parseState: ParseState = { tokens: tokens.tokens, gcx, file }; + + const ast: Crate = parse(packageName, parseState, mainCrate); + if (debug.has("ast")) { + console.log("-----AST---------------"); + + console.dir(ast.rootItems, { depth: 50 }); + + console.log("-----AST pretty--------"); + const printed = printAst(ast); + console.log(printed); + } + + if (debug.has("resolved")) { + console.log("-----AST resolved------"); + } + const resolved = resolve(gcx, ast); + if (debug.has("resolved")) { + const resolvedPrinted = printAst(resolved); + console.log(resolvedPrinted); + } + + if (debug.has("typecked")) { + console.log("-----AST typecked------"); + } + const typecked: Crate = typeck(gcx, resolved); + if (debug.has("typecked")) { + const typeckPrinted = printAst(typecked); + console.log(typeckPrinted); + } + + if (debug.has("wat")) { + console.log("-----wasm--------------"); + } + + // Codegen should never handle errornous code. + if (gcx.error.hasErrors()) { + process.exit(1); + } + + gcx.finalizedCrates.push(typecked); + const wasmModule = lowerToWasm(gcx); + const moduleStringColor = writeModuleWatToString(wasmModule, true); + const moduleString = writeModuleWatToString(wasmModule); + + if (debug.has("wat")) { + console.log(moduleStringColor); + } + + if (!opts.noOutput) { + fs.writeFileSync("out.wat", moduleString); + } + + if (debug.has("wasm-validate")) { + console.log("--validate wasm-tools--"); + + exec("wasm-tools validate out.wat", (error, stdout, stderr) => { + if (error && error.code === 1) { + console.log(stderr); + } else if (error) { + console.error(`failed to spawn wasm-tools: ${error.message}`); + } else { + if (stderr) { + console.log(stderr); + } + if (stdout) { + console.log(stdout); + } } - const tokens = tokenize(file); - if (debug.has("tokens")) { - console.log("-----TOKENS------------"); - console.log(tokens); - } - - const parseState: ParseState = { tokens, file }; - - const ast: Crate = parse(packageName, parseState, mainCrate); - if (debug.has("ast")) { - console.log("-----AST---------------"); - - console.dir(ast.rootItems, { depth: 50 }); - - console.log("-----AST pretty--------"); - const printed = printAst(ast); - console.log(printed); - } - - if (debug.has("resolved")) { - console.log("-----AST resolved------"); - } - const resolved = resolve(gcx, ast); - if (debug.has("resolved")) { - const resolvedPrinted = printAst(resolved); - console.log(resolvedPrinted); - } - - if (debug.has("typecked")) { - console.log("-----AST typecked------"); - } - const typecked: Crate = typeck(gcx, resolved); - if (debug.has("typecked")) { - const typeckPrinted = printAst(typecked); - console.log(typeckPrinted); - } - - if (debug.has("wat")) { - console.log("-----wasm--------------"); - } - - gcx.finalizedCrates.push(typecked); - const wasmModule = lowerToWasm(gcx); - const moduleStringColor = writeModuleWatToString(wasmModule, true); - const moduleString = writeModuleWatToString(wasmModule); - - if (debug.has("wat")) { - console.log(moduleStringColor); - } - - if (!opts.noOutput) { - fs.writeFileSync("out.wat", moduleString); - } - - if (debug.has("wasm-validate")) { - console.log("--validate wasm-tools--"); - - exec("wasm-tools validate out.wat", (error, stdout, stderr) => { - if (error && error.code === 1) { - console.log(stderr); - } else if (error) { - console.error(`failed to spawn wasm-tools: ${error.message}`); - } else { - if (stderr) { - console.log(stderr); - } - if (stdout) { - console.log(stdout); - } - } - - console.log(`finished in ${Date.now() - start}ms`); - }); - } - }, - () => process.exit(1), - ); + console.log(`finished in ${Date.now() - start}ms`); + }); + } } main(); diff --git a/src/lexer.test.ts b/src/lexer.test.ts index 390d164..98a66fd 100644 --- a/src/lexer.test.ts +++ b/src/lexer.test.ts @@ -1,17 +1,20 @@ +import { ErrorHandler, unreachable } from "./error"; import { tokenize } from "./lexer"; it("should tokenize an emtpy function", () => { const input = `function hello() = ;`; - const tokens = tokenize({ content: input }); + const tokens = tokenize(new ErrorHandler(), { content: input }); + if (!tokens.ok) unreachable("lexer error"); - expect(tokens).toMatchSnapshot(); + expect(tokens.tokens).toMatchSnapshot(); }); it("should tokenize hello world", () => { const input = `print("hello world")`; - const tokens = tokenize({ content: input }); + const tokens = tokenize(new ErrorHandler(), { content: input }); + if (!tokens.ok) unreachable("lexer error"); - expect(tokens).toMatchSnapshot(); + expect(tokens.tokens).toMatchSnapshot(); }); diff --git a/src/lexer.ts b/src/lexer.ts index e4785ea..276f98c 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -1,4 +1,10 @@ -import { CompilerError, LoadedFile, Span } from "./error"; +import { + CompilerError, + ErrorEmitted, + ErrorHandler, + LoadedFile, + Span, +} from "./error"; export type DatalessToken = | "function" @@ -86,7 +92,40 @@ const SINGLE_PUNCT: string[] = [ "%", ]; -export function tokenize(file: LoadedFile): Token[] { +class LexerError extends Error { + constructor(public inner: CompilerError) { + super("lexer error"); + } +} + +export type LexerResult = + | { + ok: true; + tokens: Token[]; + } + | { + ok: false; + + err: ErrorEmitted; + }; + +export function tokenize(handler: ErrorHandler, file: LoadedFile): LexerResult { + try { + return { ok: true, tokens: tokenizeInner(file) }; + } catch (e) { + if (e instanceof LexerError) { + const err: ErrorEmitted = handler.emit(e.inner); + return { ok: false, err }; + } else { + throw e; + } + } +} + +function tokenizeInner(file: LoadedFile): Token[] { + const err = (msg: string, span: Span) => + new LexerError(new CompilerError(msg, span)); + const { content: input } = file; const tokens: Token[] = []; let i = 0; @@ -109,7 +148,7 @@ export function tokenize(file: LoadedFile): Token[] { while (input[i] !== "*" && input[i + 1] !== "/") { i++; if (input[i] === undefined) { - throw new CompilerError("unterminated block comment", span); + throw err("unterminated block comment", span); } } i++; @@ -205,7 +244,7 @@ export function tokenize(file: LoadedFile): Token[] { result.push("\x19"); break; default: - throw new CompilerError( + throw err( `invalid escape character: ${input[i]}`, new Span(span.end - 1, span.end, file), ); @@ -215,7 +254,7 @@ export function tokenize(file: LoadedFile): Token[] { result.push(next); if (next === undefined) { - throw new CompilerError(`Unterminated string literal`, span); + throw err(`Unterminated string literal`, span); } } const value = result.join(""); @@ -263,7 +302,7 @@ export function tokenize(file: LoadedFile): Token[] { } else if (isWhitespace(next)) { // ignore } else { - throw new CompilerError(`invalid character: \`${next}\``, span); + throw err(`invalid character: \`${next}\``, span); } } } diff --git a/src/loader.ts b/src/loader.ts index e7ff7d2..3ebdbb0 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,27 +1,41 @@ -import { DepCrate } from "./ast"; +import { CrateId, DepCrate } from "./ast"; import { CrateLoader, GlobalContext } from "./context"; -import { CompilerError, LoadedFile, Span, withErrorPrinter } from "./error"; +import { CompilerError, ErrorEmitted, LoadedFile, Span } from "./error"; import fs from "fs"; import path from "path"; import { tokenize } from "./lexer"; import { ParseState, parse } from "./parser"; import { resolve } from "./resolve"; import { typeck } from "./typeck"; +import { ComplexMap } from "./utils"; + +export type LoadResult = + | { + ok: true; + value: T; + } + | { + ok: false; + err: CompilerError; + }; export function loadModuleFile( relativeTo: string, moduleName: string, span: Span, -): LoadedFile { +): LoadResult { let searchDir: string; if (relativeTo.endsWith(".mod.nil")) { // x/uwu.mod.nil searches in x/ searchDir = path.dirname(relativeTo); } else if (relativeTo.endsWith(".nil")) { - throw new CompilerError( - `.nil files cannot have submodules. use .mod.nil in a subdirectory`, - span, - ); + return { + ok: false, + err: new CompilerError( + `.nil files cannot have submodules. use .mod.nil in a subdirectory`, + span, + ), + }; } else { searchDir = relativeTo; } @@ -41,13 +55,34 @@ export function loadModuleFile( }); if (content === undefined || filePath === undefined) { - throw new CompilerError( - `failed to load ${moduleName}, could not find ${options.join(" or ")}`, - span, - ); + return { + ok: false, + err: new CompilerError( + `failed to load ${moduleName}, could not find ${options.join(" or ")}`, + span, + ), + }; } - return { content, path: filePath }; + return { ok: true, value: { content, path: filePath } }; +} + +function dummyErrorCrate( + id: CrateId, + packageName: string, + emitted: ErrorEmitted, +): DepCrate { + return { + id, + packageName, + rootItems: [], + itemsById: new ComplexMap(), + rootFile: { content: "" }, + fatalError: emitted, + typeckResults: { + main: undefined, + }, + }; } export const loadCrate: CrateLoader = ( @@ -65,27 +100,27 @@ export const loadCrate: CrateLoader = ( return existing; } - return withErrorPrinter( - (): DepCrate => { - const file = loadModuleFile(".", name, span); + const crateId = gcx.crateId.next(); - const crateId = gcx.crateId.next(); + const file = loadModuleFile(".", name, span); + if (!file.ok) { + return dummyErrorCrate(crateId, name, gcx.error.emit(file.err)); + } - const tokens = tokenize(file); - const parseState: ParseState = { tokens, file }; - const ast = parse(name, parseState, crateId); - const resolved = resolve(gcx, ast); + const tokens = tokenize(gcx.error, file.value); + if (!tokens.ok) { + return dummyErrorCrate(crateId, name, tokens.err); + } + const parseState: ParseState = { + tokens: tokens.tokens, + file: file.value, + gcx, + }; + const ast = parse(name, parseState, crateId); + const resolved = resolve(gcx, ast); - const typecked = typeck(gcx, resolved); + const typecked = typeck(gcx, resolved); - gcx.finalizedCrates.push(typecked); - return typecked; - }, - () => { - throw new CompilerError( - `failed to load crate ${name}: crate contains errors`, - span, - ); - }, - ); + gcx.finalizedCrates.push(typecked); + return typecked; }; diff --git a/src/parser.ts b/src/parser.ts index cf3a310..17ec24b 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -28,7 +28,8 @@ import { StructLiteralField, TypeDefKind, } from "./ast"; -import { CompilerError, LoadedFile, Span } from "./error"; +import { GlobalContext } from "./context"; +import { CompilerError, ErrorEmitted, LoadedFile, Span } from "./error"; import { BaseToken, Token, @@ -39,21 +40,48 @@ import { import { loadModuleFile } from "./loader"; import { ComplexMap, ComplexSet, Ids } from "./utils"; -export type ParseState = { tokens: Token[]; file: LoadedFile }; +export type ParseState = { + tokens: Token[]; + file: LoadedFile; + gcx: GlobalContext; +}; type State = ParseState; type Parser = (t: State) => [State, T]; +class FatalParseError extends Error { + constructor(public inner: ErrorEmitted) { + super("fatal parser error"); + } +} + export function parse( packageName: string, t: State, crateId: number, ): Crate { - const [, items] = parseItems(t); + let items: Item[]; + let fatalError: ErrorEmitted | undefined = undefined; + try { + [, items] = parseItems(t); + } catch (e) { + if (e instanceof FatalParseError) { + items = []; + fatalError = e.inner; + } else { + throw e; + } + } - const ast: Crate = buildCrate(packageName, items, crateId, t.file); + const ast: Crate = buildCrate( + packageName, + items, + crateId, + t.file, + fatalError, + ); - validateAst(ast); + validateAst(ast, t.gcx); return ast; } @@ -200,15 +228,32 @@ function parseItem(t: State): [State, Item] { [t] = expectNext(t, ")"); } else { if (name.span.file.path === undefined) { - throw new CompilerError( - `no known source file for statement, cannot load file relative to it`, - name.span, + t.gcx.error.emit( + new CompilerError( + `no known source file for statement, cannot load file relative to it`, + name.span, + ), ); - } - const file = loadModuleFile(name.span.file.path, name.ident, name.span); - const tokens = tokenize(file); - [, contents] = parseItems({ file, tokens }); + contents = []; + } else { + const file = loadModuleFile(name.span.file.path, name.ident, name.span); + + if (!file.ok) { + t.gcx.error.emit(file.err); + contents = []; + } else { + const tokens = tokenize(t.gcx.error, file.value); + if (!tokens.ok) { + throw new FatalParseError(tokens.err); + } + [, contents] = parseItems({ + file: file.value, + tokens: tokens.tokens, + gcx: t.gcx, + }); + } + } } [t] = expectNext(t, ";"); @@ -244,7 +289,7 @@ function parseItem(t: State): [State, Item] { }; return [t, global]; } else { - unexpectedToken(tok, "item"); + unexpectedToken(t, tok, "item"); } } @@ -413,7 +458,7 @@ function parseExprCall(t: State): [State, Expr] { } else if (access.kind === "lit_int") { value = access.value; } else { - unexpectedToken(access, "identifier or integer"); + unexpectedToken(t, access, "identifier or integer"); } lhs = { @@ -467,7 +512,7 @@ function parseExprAtom(startT: State): [State, Expr] { return [t, { kind: "tupleLiteral", span, fields: [expr, ...rest] }]; } - unexpectedToken(peek, "`,`, `;` or `)`"); + unexpectedToken(t, peek, "`,`, `;` or `)`"); } if (tok.kind === "lit_string") { @@ -657,9 +702,13 @@ function parseType(t: State): [State, Type] { return [t, { kind: "rawptr", inner, span }]; } default: { - throw new CompilerError( - `unexpected token: \`${tok.kind}\`, expected type`, - span, + throw new FatalParseError( + t.gcx.error.emit( + new CompilerError( + `unexpected token: \`${tok.kind}\`, expected type`, + span, + ), + ), ); } } @@ -688,7 +737,7 @@ function parseCommaSeparatedList( // No comma? Fine, you don't like trailing commas. // But this better be the end. if (peekKind(t) !== terminator) { - unexpectedToken(next(t)[1], `, or ${terminator}`); + unexpectedToken(t, next(t)[1], `, or ${terminator}`); } break; } @@ -720,15 +769,23 @@ function expectNext( let tok; [t, tok] = maybeNextT(t); if (!tok) { - throw new CompilerError( - `expected \`${kind}\`, found end of file`, - Span.eof(t.file), + throw new FatalParseError( + t.gcx.error.emit( + new CompilerError( + `expected \`${kind}\`, found end of file`, + Span.eof(t.file), + ), + ), ); } if (tok.kind !== kind) { - throw new CompilerError( - `expected \`${kind}\`, found \`${tok.kind}\``, - tok.span, + throw new FatalParseError( + t.gcx.error.emit( + new CompilerError( + `expected \`${kind}\`, found \`${tok.kind}\``, + tok.span, + ), + ), ); } return [t, tok as unknown as T & Token]; @@ -737,7 +794,11 @@ function expectNext( function next(t: State): [State, Token] { const [rest, next] = maybeNextT(t); if (!next) { - throw new CompilerError("unexpected end of file", Span.eof(t.file)); + throw new FatalParseError( + t.gcx.error.emit( + new CompilerError("unexpected end of file", Span.eof(t.file)), + ), + ); } return [rest, next]; } @@ -749,11 +810,15 @@ function maybeNextT(t: State): [State, Token | undefined] { return [{ ...t, tokens: rest }, next]; } -function unexpectedToken(token: Token, expected: string): never { - throw new CompilerError(`unexpected token, expected ${expected}`, token.span); +function unexpectedToken(t: ParseState, token: Token, expected: string): never { + throw new FatalParseError( + t.gcx.error.emit( + new CompilerError(`unexpected token, expected ${expected}`, token.span), + ), + ); } -function validateAst(ast: Crate) { +function validateAst(ast: Crate, gcx: GlobalContext) { const seenItemIds = new ComplexSet(); const validator: Folder = { @@ -781,7 +846,10 @@ function validateAst(ast: Crate) { }); return expr; } else if (expr.kind === "let") { - throw new CompilerError("let is only allowed in blocks", expr.span); + gcx.error.emit( + new CompilerError("let is only allowed in blocks", expr.span), + ); + return superFoldExpr(expr, this); } else if (expr.kind === "binary") { const checkPrecedence = (inner: Expr, side: string) => { if (inner.kind === "binary") { @@ -789,9 +857,11 @@ function validateAst(ast: Crate) { const innerClass = binaryExprPrecedenceClass(inner.binaryKind); if (ourClass !== innerClass) { - throw new CompilerError( - `mixing operators without parentheses is not allowed. ${side} is ${inner.binaryKind}, which is different from ${expr.binaryKind}`, - expr.span, + gcx.error.emit( + new CompilerError( + `mixing operators without parentheses is not allowed. ${side} is ${inner.binaryKind}, which is different from ${expr.binaryKind}`, + expr.span, + ), ); } } @@ -821,6 +891,7 @@ function buildCrate( rootItems: Item[], crateId: number, rootFile: LoadedFile, + fatalError: ErrorEmitted | undefined, ): Crate { const itemId = new Ids(); itemId.next(); // crate root ID @@ -832,6 +903,7 @@ function buildCrate( itemsById: new ComplexMap(), packageName, rootFile, + fatalError, }; const assigner: Folder = { diff --git a/src/printer.ts b/src/printer.ts index e665601..c9b1904 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -51,6 +51,8 @@ function printItem(item: Item): string { )};` ); } + case "error": + return ""; } } @@ -203,6 +205,8 @@ function printExpr(expr: Expr, indent: number): string { .map((expr) => printExpr(expr, indent)) .join(", ")})`; } + case "error": + return ""; } } @@ -218,6 +222,8 @@ function printType(type: Type): string { return `*${printType(type.inner)}`; case "never": return "!"; + case "error": + return ""; } } @@ -230,6 +236,9 @@ function printRes(res: Resolution): string { case "builtin": { return `#B`; } + case "error": { + return "#E"; + } } } @@ -274,6 +283,8 @@ export function printTy(ty: Ty): string { case "never": { return "!"; } + case "error": + return ""; } } diff --git a/src/resolve.ts b/src/resolve.ts index 36d1150..175cadf 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -19,7 +19,7 @@ import { ItemExtern, } from "./ast"; import { GlobalContext } from "./context"; -import { CompilerError, Span } from "./error"; +import { CompilerError, ErrorEmitted, Span } from "./error"; import { ComplexMap } from "./utils"; const BUILTIN_SET = new Set(BUILTINS); @@ -81,6 +81,7 @@ export function resolve( rootItems, packageName: ast.packageName, rootFile: ast.rootFile, + fatalError: ast.fatalError, }; } @@ -94,12 +95,15 @@ function resolveModule( contents.forEach((item) => { const existing = items.get(item.name); if (existing !== undefined) { - throw new CompilerError( - `item \`${item.name}\` has already been declared`, - item.span, + cx.gcx.error.emit( + new CompilerError( + `item \`${item.name}\` has already been declared`, + item.span, + ), ); + } else { + items.set(item.name, item.id); } - items.set(item.name, item.id); }); const scopes: string[] = []; @@ -148,7 +152,12 @@ function resolveModule( return { kind: "builtin", name: ident.name as BuiltinName }; } - throw new CompilerError(`cannot find ${ident.name}`, ident.span); + return { + kind: "error", + err: cx.gcx.error.emit( + new CompilerError(`cannot find ${ident.name}`, ident.span), + ), + }; }; const blockLocals: LocalInfo[][] = []; @@ -260,30 +269,39 @@ function resolveModule( 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") { - throw new CompilerError( - "module contents cannot be indexed with a number", - expr.field.span, + const err: ErrorEmitted = cx.gcx.error.emit( + new CompilerError( + "module contents cannot be indexed with a number", + expr.field.span, + ), ); - } - - const pathResItem = resolveModItem( - cx, - module, - expr.field.value, - ); - if (pathResItem === undefined) { - throw new CompilerError( - `module ${module.name} has no item ${expr.field.value}`, - expr.field.span, + pathRes = { kind: "error", err }; + } else { + const pathResItem = resolveModItem( + cx, + module, + expr.field.value, ); - } - const pathRes: Resolution = { kind: "item", id: pathResItem }; + if (pathResItem === undefined) { + const err: ErrorEmitted = cx.gcx.error.emit( + 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, expr.field.value], + segments: [...segments, String(expr.field.value)], value: { res: pathRes, span }, span, }; diff --git a/src/typeck.test.ts b/src/typeck.test.ts index 02f0c17..086f844 100644 --- a/src/typeck.test.ts +++ b/src/typeck.test.ts @@ -1,11 +1,13 @@ import { TY_INT, TY_STRING, TY_UNIT } from "./ast"; -import { Span } from "./error"; +import { Emitter, ErrorHandler, Span } from "./error"; import { InferContext } from "./typeck"; const SPAN: Span = Span.startOfFile({ content: "" }); +const dummyEmitter: Emitter = () => {}; + it("should infer types across assignments", () => { - const infcx = new InferContext(); + const infcx = new InferContext(new ErrorHandler(dummyEmitter)); const a = infcx.newVar(); const b = infcx.newVar(); @@ -26,7 +28,9 @@ it("should infer types across assignments", () => { }); it("should conflict assignments to resolvable type vars", () => { - const infcx = new InferContext(); + let errorLines = 0; + const emitter = () => (errorLines += 1); + const infcx = new InferContext(new ErrorHandler(emitter)); const a = infcx.newVar(); const b = infcx.newVar(); @@ -34,11 +38,15 @@ it("should conflict assignments to resolvable type vars", () => { infcx.assign(a, b, SPAN); infcx.assign(b, TY_INT, SPAN); - expect(() => infcx.assign(a, TY_STRING, SPAN)).toThrow(); + expect(errorLines).toEqual(0); + + infcx.assign(a, TY_STRING, SPAN); + + expect(errorLines).toBeGreaterThan(0); }); it("should not cycle", () => { - const infcx = new InferContext(); + const infcx = new InferContext(new ErrorHandler(dummyEmitter)); const a = infcx.newVar(); const b = infcx.newVar(); diff --git a/src/typeck.ts b/src/typeck.ts index 3df3a9d..ca5cc03 100644 --- a/src/typeck.ts +++ b/src/typeck.ts @@ -32,7 +32,13 @@ import { ExprCall, } from "./ast"; import { GlobalContext } from "./context"; -import { CompilerError, Span, unreachable } from "./error"; +import { + CompilerError, + ErrorEmitted, + ErrorHandler, + Span, + unreachable, +} from "./error"; import { printTy } from "./printer"; import { ComplexMap } from "./utils"; @@ -52,7 +58,22 @@ function mkTyFn(params: Ty[], returnTy: Ty): Ty { return { kind: "fn", params, returnTy }; } -function builtinAsTy(name: string, span: Span): Ty { +function tyError(cx: TypeckCtx, err: CompilerError): Ty { + return { + kind: "error", + err: emitError(cx, err), + }; +} + +function tyErrorFrom(prev: { err: ErrorEmitted }): Ty { + return { kind: "error", err: prev.err }; +} + +function emitError(cx: TypeckCtx, err: CompilerError): ErrorEmitted { + return cx.gcx.error.emit(err); +} + +function builtinAsTy(cx: TypeckCtx, name: string, span: Span): Ty { switch (name) { case "String": { return TY_STRING; @@ -67,12 +88,12 @@ function builtinAsTy(name: string, span: Span): Ty { return TY_BOOL; } default: { - throw new CompilerError(`\`${name}\` is not a type`, span); + return tyError(cx, new CompilerError(`\`${name}\` is not a type`, span)); } } } -function typeOfBuiltinValue(name: BuiltinName, span: Span): Ty { +function typeOfBuiltinValue(cx: TypeckCtx, name: BuiltinName, span: Span): Ty { switch (name) { case "false": case "true": @@ -96,7 +117,10 @@ function typeOfBuiltinValue(name: BuiltinName, span: Span): Ty { case "__i32_extend_to_i64_u": return mkTyFn([TY_I32], TY_INT); default: { - throw new CompilerError(`\`${name}\` cannot be used as a value`, span); + return tyError( + cx, + new CompilerError(`\`${name}\` cannot be used as a value`, span), + ); } } } @@ -115,7 +139,10 @@ function lowerAstTy(cx: TypeckCtx, type: Type): Ty { return typeOfItem(cx, res.id, type.span); } case "builtin": { - return builtinAsTy(res.name, ident.span); + return builtinAsTy(cx, res.name, ident.span); + } + case "error": { + return tyErrorFrom(res); } } } @@ -134,9 +161,9 @@ function lowerAstTy(cx: TypeckCtx, type: Type): Ty { case "rawptr": { const inner = lowerAstTy(cx, type.inner); if (inner.kind !== "struct") { - throw new CompilerError( - "raw pointers must point to structs", - type.span, + return tyError( + cx, + new CompilerError("raw pointers must point to structs", type.span), ); } @@ -145,6 +172,9 @@ function lowerAstTy(cx: TypeckCtx, type: Type): Ty { case "never": { return TY_NEVER; } + case "error": { + return tyErrorFrom(type); + } } } @@ -161,15 +191,21 @@ function typeOfItem(cx: TypeckCtx, itemId: ItemId, cause: Span): Ty { case "global": return item.ty!; case "mod": { - throw new CompilerError( - `module ${item.name} cannot be used as a type or value`, - cause, + return tyError( + cx, + new CompilerError( + `module ${item.name} cannot be used as a type or value`, + cause, + ), ); } case "extern": { - throw new CompilerError( - `extern declaration ${item.name} cannot be used as a type or value`, - cause, + return tyError( + cx, + new CompilerError( + `extern declaration ${item.name} cannot be used as a type or value`, + cause, + ), ); } } @@ -181,9 +217,12 @@ function typeOfItem(cx: TypeckCtx, itemId: ItemId, cause: Span): Ty { return cachedTy; } if (cachedTy === null) { - throw new CompilerError( - `cycle computing type of #G${itemId.toString()}`, - item.span, + return tyError( + cx, + new CompilerError( + `cycle computing type of #G${itemId.toString()}`, + item.span, + ), ); } cx.itemTys.set(itemId, null); @@ -230,21 +269,30 @@ function typeOfItem(cx: TypeckCtx, itemId: ItemId, cause: Span): Ty { break; } case "mod": { - throw new CompilerError( - `module ${item.name} cannot be used as a type or value`, - cause, + return tyError( + cx, + new CompilerError( + `module ${item.name} cannot be used as a type or value`, + cause, + ), ); } case "extern": { - throw new CompilerError( - `extern declaration ${item.name} cannot be used as a type or value`, - cause, + return tyError( + cx, + new CompilerError( + `extern declaration ${item.name} cannot be used as a type or value`, + cause, + ), ); } case "global": { ty = lowerAstTy(cx, item.type); break; } + case "error": { + return tyErrorFrom(item); + } } cx.itemTys.set(item.id, ty); @@ -286,9 +334,12 @@ export function typeck( case "i32": break; default: { - throw new CompilerError( - `import parameters must be I32 or Int`, - item.params[i].span, + emitError( + cx, + new CompilerError( + `import parameters must be I32 or Int`, + item.params[i].span, + ), ); } } @@ -300,9 +351,12 @@ export function typeck( case "i32": break; default: { - throw new CompilerError( - `import return must be I32, Int or ()`, - item.returnType!.span, + emitError( + cx, + new CompilerError( + `import return must be I32, Int or ()`, + item.returnType!.span, + ), ); } } @@ -323,12 +377,16 @@ export function typeck( const fieldNames = new Set(); item.type.fields.forEach(({ name }) => { if (fieldNames.has(name)) { - throw new CompilerError( - `type ${item.name} has a duplicate field: ${name.name}`, - name.span, + emitError( + cx, + new CompilerError( + `type ${item.name} has a duplicate field: ${name.name}`, + name.span, + ), ); + } else { + fieldNames.add(name); } - fieldNames.add(name); }); return { @@ -363,23 +421,32 @@ export function typeck( const ty = typeOfItem(cx, item.id, item.span); const { init } = item; + let initChecked: Expr; if (init.kind !== "literal" || init.value.kind !== "int") { - throw new CompilerError( - "globals must be initialized with an integer literal", - init.span, + const err: ErrorEmitted = emitError( + cx, + new CompilerError( + "globals must be initialized with an integer literal", + init.span, + ), ); + initChecked = exprError(err, init.span); + } else { + const initTy = init.value.type === "I32" ? TY_I32 : TY_INT; + const infcx = new InferContext(cx.gcx.error); + infcx.assign(ty, initTy, init.span); + initChecked = { ...init, ty }; } - const initTy = init.value.type === "I32" ? TY_I32 : TY_INT; - const infcx = new InferContext(); - infcx.assign(ty, initTy, init.span); - return { ...item, ty, - init: { ...init, ty }, + init: initChecked, }; } + case "error": { + return { ...item }; + } } }, expr(_expr) { @@ -398,9 +465,12 @@ export function typeck( const main = typecked.rootItems.find((item) => { if (item.kind === "function" && item.name === "main") { if (!tyIsUnit(item.ty!.returnTy)) { - throw new CompilerError( - `\`main\` has an invalid signature. main takes no arguments and returns nothing`, - item.span, + emitError( + cx, + new CompilerError( + `\`main\` has an invalid signature. main takes no arguments and returns nothing`, + item.span, + ), ); } @@ -412,15 +482,19 @@ export function typeck( if (ast.id === 0) { // Only the final id=0 crate needs and cares about main. if (!main) { - throw new CompilerError( - `\`main\` function not found`, - Span.startOfFile(ast.rootFile), + emitError( + cx, + new CompilerError( + `\`main\` function not found`, + Span.startOfFile(ast.rootFile), + ), ); } - typecked.typeckResults = { - main: { kind: "item", id: main.id }, - }; + typecked.typeckResults = { main: undefined }; + if (main) { + typecked.typeckResults.main = { kind: "item", id: main.id }; + } } return typecked; @@ -442,6 +516,8 @@ type TyVarRes = export class InferContext { tyVars: TyVarRes[] = []; + constructor(public error: ErrorHandler) {} + public newVar(): Ty { const index = this.tyVars.length; this.tyVars.push({ kind: "unknown" }); @@ -524,7 +600,13 @@ export class InferContext { return; } + if (lhs.kind === "error" || rhs.kind === "error") { + // This Is Fine 🐢πŸ”₯. + return; + } + if (rhs.kind === "never") { + // not sure whether this is entirely correct wrt inference.. it will work out, probably. return; } @@ -585,9 +667,11 @@ export class InferContext { } } - throw new CompilerError( - `cannot assign ${printTy(rhs)} to ${printTy(lhs)}`, - span, + this.error.emit( + new CompilerError( + `cannot assign ${printTy(rhs)} to ${printTy(lhs)}`, + span, + ), ); } } @@ -612,17 +696,28 @@ function typeOfValue(fcx: FuncCtx, res: Resolution, span: Span): Ty { return typeOfItem(fcx.cx, res.id, span); } case "builtin": - return typeOfBuiltinValue(res.name, span); + return typeOfBuiltinValue(fcx.cx, res.name, span); + case "error": + return tyErrorFrom(res); } } +function exprError(err: ErrorEmitted, span: Span): Expr { + return { + kind: "error", + err, + span, + ty: tyErrorFrom({ err }), + }; +} + export function checkBody( cx: TypeckCtx, ast: Crate, body: Expr, fnTy: TyFn, ): Expr { - const infcx = new InferContext(); + const infcx = new InferContext(cx.gcx.error); const fcx: FuncCtx = { cx, @@ -683,26 +778,32 @@ export function checkBody( case "item": { const item = cx.gcx.findItem(res.id, ast); if (item.kind !== "global") { - throw new CompilerError("cannot assign to item", expr.span); + emitError( + fcx.cx, + new CompilerError("cannot assign to item", expr.span), + ); } break; } case "builtin": - throw new CompilerError( - "cannot assign to builtins", - expr.span, + emitError( + fcx.cx, + new CompilerError("cannot assign to builtins", expr.span), ); } break; } case "fieldAccess": { - checkLValue(lhs); + checkLValue(cx, lhs); break; } default: { - throw new CompilerError( - "invalid left-hand side of assignment", - lhs.span, + emitError( + fcx.cx, + new CompilerError( + "invalid left-hand side of assignment", + lhs.span, + ), ); } } @@ -764,7 +865,7 @@ export function checkBody( case "unary": { const rhs = this.expr(expr.rhs); rhs.ty = infcx.resolveIfPossible(rhs.ty); - return checkUnary(expr, rhs); + return checkUnary(fcx, expr, rhs); } case "call": { return checkCall(fcx, expr); @@ -775,7 +876,7 @@ export function checkBody( const { field } = expr; let ty: Ty; - let fieldIdx: number; + let fieldIdx: number | undefined; switch (lhs.ty.kind) { case "tuple": { const { elems } = lhs.ty; @@ -784,15 +885,21 @@ export function checkBody( ty = elems[field.value]; fieldIdx = field.value; } else { - throw new CompilerError( - `tuple with ${elems.length} elements cannot be indexed with ${field.value}`, - field.span, + ty = tyError( + fcx.cx, + new CompilerError( + `tuple with ${elems.length} elements cannot be indexed with ${field.value}`, + field.span, + ), ); } } else { - throw new CompilerError( - "tuple fields must be accessed with numbers", - field.span, + ty = tyError( + fcx.cx, + new CompilerError( + "tuple fields must be accessed with numbers", + field.span, + ), ); } break; @@ -804,30 +911,40 @@ export function checkBody( if (typeof field.value === "string") { const idx = fields.findIndex(([name]) => name === field.value); if (idx === -1) { - throw new CompilerError( - `field \`${field.value}\` does not exist on ${printTy( - lhs.ty, - )}`, - field.span, + ty = tyError( + fcx.cx, + new CompilerError( + `field \`${field.value}\` does not exist on ${printTy( + lhs.ty, + )}`, + field.span, + ), ); + break; } ty = fields[idx][1]; fieldIdx = idx; } else { - throw new CompilerError( - "struct fields must be accessed with their name", - field.span, + ty = tyError( + fcx.cx, + new CompilerError( + "struct fields must be accessed with their name", + field.span, + ), ); } break; } default: { - throw new CompilerError( - `cannot access field \`${field.value}\` on type \`${printTy( - lhs.ty, - )}\``, - expr.span, + ty = tyError( + fcx.cx, + new CompilerError( + `cannot access field \`${field.value}\` on type \`${printTy( + lhs.ty, + )}\``, + expr.span, + ), ); } } @@ -881,7 +998,11 @@ export function checkBody( case "break": { const loopStateLength = fcx.loopState.length; if (loopStateLength === 0) { - throw new CompilerError("break outside loop", expr.span); + const err: ErrorEmitted = emitError( + fcx.cx, + new CompilerError("break outside loop", expr.span), + ); + return exprError(err, expr.span); } const target = fcx.loopState[loopStateLength - 1].loopId; fcx.loopState[loopStateLength - 1].hasBreak = true; @@ -900,10 +1021,14 @@ export function checkBody( const structTy = typeOfValue(fcx, expr.name.res, expr.name.span); if (structTy.kind !== "struct") { - throw new CompilerError( - `struct literal is only allowed for struct types`, - expr.span, + const err: ErrorEmitted = emitError( + fcx.cx, + new CompilerError( + `struct literal is only allowed for struct types`, + expr.span, + ), ); + return exprError(err, expr.span); } const assignedFields = new Set(); @@ -913,9 +1038,12 @@ export function checkBody( (def) => def[0] === name.name, ); if (fieldIdx == -1) { - throw new CompilerError( - `field ${name.name} doesn't exist on type ${expr.name.name}`, - name.span, + emitError( + fcx.cx, + new CompilerError( + `field ${name.name} doesn't exist on type ${expr.name.name}`, + name.span, + ), ); } const fieldTy = structTy.fields[fieldIdx]; @@ -931,9 +1059,12 @@ export function checkBody( } }); if (missing.length > 0) { - throw new CompilerError( - `missing fields in literal: ${missing.join(", ")}`, - expr.span, + emitError( + fcx.cx, + new CompilerError( + `missing fields in literal: ${missing.join(", ")}`, + expr.span, + ), ); } @@ -949,6 +1080,9 @@ export function checkBody( return { ...expr, fields, ty }; } + case "error": { + return { ...expr, ty: tyErrorFrom(expr) }; + } } }, itemInner(_item) { @@ -968,23 +1102,23 @@ export function checkBody( infcx.assign(fnTy.returnTy, checked.ty, body.span); - const resolved = resolveBody(infcx, checked); + const resolved = resolveBody(fcx, checked); return resolved; } -function checkLValue(expr: Expr) { +function checkLValue(cx: TypeckCtx, expr: Expr) { switch (expr.kind) { case "ident": case "path": break; case "fieldAccess": - checkLValue(expr.lhs); + checkLValue(cx, expr.lhs); break; default: - throw new CompilerError( - "invalid left-hand side of assignment", - expr.span, + emitError( + cx, + new CompilerError("invalid left-hand side of assignment", expr.span), ); } } @@ -1035,15 +1169,20 @@ function checkBinary( } } - throw new CompilerError( - `invalid types for binary operation: ${printTy(lhs.ty)} ${ - expr.binaryKind - } ${printTy(rhs.ty)}`, - expr.span, + const ty = tyError( + fcx.cx, + new CompilerError( + `invalid types for binary operation: ${printTy(lhs.ty)} ${ + expr.binaryKind + } ${printTy(rhs.ty)}`, + expr.span, + ), ); + return { ...expr, lhs, rhs, ty }; } function checkUnary( + fcx: FuncCtx, expr: Expr & ExprUnary, rhs: Expr, ): Expr { @@ -1060,10 +1199,14 @@ function checkUnary( // Negating an unsigned integer is a bad idea. } - throw new CompilerError( - `invalid types for unary operation: ${expr.unaryKind} ${printTy(rhs.ty)}`, - expr.span, + const ty = tyError( + fcx.cx, + new CompilerError( + `invalid types for unary operation: ${expr.unaryKind} ${printTy(rhs.ty)}`, + expr.span, + ), ); + return { ...expr, rhs, ty }; } function checkCall( @@ -1089,21 +1232,30 @@ function checkCall( const lhs = fcx.checkExpr(expr.lhs); lhs.ty = fcx.infcx.resolveIfPossible(lhs.ty); + + // check args before checking the lhs. + const args = expr.args.map((arg) => fcx.checkExpr(arg)); + const lhsTy = lhs.ty; if (lhsTy.kind !== "fn") { - throw new CompilerError( - `expression of type ${printTy(lhsTy)} is not callable`, - lhs.span, + const ty = tyError( + fcx.cx, + new CompilerError( + `expression of type ${printTy(lhsTy)} is not callable`, + lhs.span, + ), ); + return { ...expr, lhs, args, ty }; } - const args = expr.args.map((arg) => fcx.checkExpr(arg)); - lhsTy.params.forEach((param, i) => { if (args.length <= i) { - throw new CompilerError( - `missing argument of type ${printTy(param)}`, - expr.span, + emitError( + fcx.cx, + new CompilerError( + `missing argument of type ${printTy(param)}`, + expr.span, + ), ); } @@ -1111,24 +1263,24 @@ function checkCall( }); if (args.length > lhsTy.params.length) { - throw new CompilerError( - `too many arguments passed, expected ${lhsTy.params.length}, found ${args.length}`, - expr.span, + emitError( + fcx.cx, + new CompilerError( + `too many arguments passed, expected ${lhsTy.params.length}, found ${args.length}`, + expr.span, + ), ); } return { ...expr, lhs, args, ty: lhsTy.returnTy }; } -function resolveBody( - infcx: InferContext, - checked: Expr, -): Expr { +function resolveBody(fcx: FuncCtx, checked: Expr): Expr { const resolveTy = (ty: Ty, span: Span) => { - const resTy = infcx.resolveIfPossible(ty); + const resTy = fcx.infcx.resolveIfPossible(ty); // TODO: When doing deep resolution, we need to check for _any_ vars. if (resTy.kind === "var") { - throw new CompilerError("cannot infer type", span); + return tyError(fcx.cx, new CompilerError("cannot infer type", span)); } return resTy; }; diff --git a/test.nil b/test.nil index dc80e74..9cf782f 100644 --- a/test.nil +++ b/test.nil @@ -1,6 +1,11 @@ type A = struct { a: Int }; function main() = ( - let a = A { a: 0 }; + let a: Int = ""; + let b: Int = ""; + c; +); + +function rawr(a: *A) = ( a.a = 1; ); \ No newline at end of file diff --git a/ui-tests/basic_recovery.nil b/ui-tests/basic_recovery.nil new file mode 100644 index 0000000..e6953c0 --- /dev/null +++ b/ui-tests/basic_recovery.nil @@ -0,0 +1,5 @@ +function main() = ( + let a: Int = ""; + let b: Int = ""; + c; +); diff --git a/ui-tests/basic_recovery.stderr b/ui-tests/basic_recovery.stderr new file mode 100644 index 0000000..e933ad1 --- /dev/null +++ b/ui-tests/basic_recovery.stderr @@ -0,0 +1,12 @@ +error: cannot find c + --> $DIR/basic_recovery.nil:4 +4 | c; + ^ +error: cannot assign String to Int + --> $DIR/basic_recovery.nil:2 +2 | let a: Int = ""; + ^ +error: cannot assign String to Int + --> $DIR/basic_recovery.nil:3 +3 | let b: Int = ""; + ^ diff --git a/ui-tests/mismatched_parens.stderr b/ui-tests/mismatched_parens.stderr index d914e7a..2919441 100644 --- a/ui-tests/mismatched_parens.stderr +++ b/ui-tests/mismatched_parens.stderr @@ -2,3 +2,7 @@ error: unexpected end of file --> $DIR/mismatched_parens.nil:2 2 | ^ +error: `main` function not found + --> $DIR/mismatched_parens.nil:1 +1 | function main() = ( + ^