split up front
This commit is contained in:
parent
bdee8b5bed
commit
b646f46fc0
42
src/diagnostics.ts
Normal file
42
src/diagnostics.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
export function printDiagnostics(
|
||||||
|
filename: string,
|
||||||
|
line: number,
|
||||||
|
severity: "error" | "info",
|
||||||
|
message: string,
|
||||||
|
text?: string,
|
||||||
|
) {
|
||||||
|
const severityColor = ({
|
||||||
|
"error": "red",
|
||||||
|
"info": "blue",
|
||||||
|
} as { [Key in typeof severity]: string })[severity];
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`%c${severity}%c: ${message}\n %c--> ${filename}:${line}%c`,
|
||||||
|
`color: ${severityColor}; font-weight: bold;`,
|
||||||
|
"color: lightwhite; font-weight: bold;",
|
||||||
|
"color: gray;",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newlines = text
|
||||||
|
.split("")
|
||||||
|
.map((ch, idx) => ch === "\n" ? idx : null)
|
||||||
|
.filter((v) => v !== null);
|
||||||
|
const lineText = text.slice(newlines[line - 2] + 1, newlines[line - 1]);
|
||||||
|
const lineNumberText = line.toString();
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`${" ".repeat(lineNumberText.length)}%c|\n` +
|
||||||
|
`${lineNumberText}|%c${lineText}\n` +
|
||||||
|
`${" ".repeat(lineNumberText.length)}%c|` +
|
||||||
|
`%c${"~".repeat(lineText.length)}%c`,
|
||||||
|
"color: cyan;",
|
||||||
|
"color: lightwhite;",
|
||||||
|
"color: cyan;",
|
||||||
|
`color: ${severityColor};`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
756
src/front.ts
756
src/front.ts
@ -1,756 +0,0 @@
|
|||||||
import * as ast from "./ast.ts";
|
|
||||||
import { Ty } from "./ty.ts";
|
|
||||||
|
|
||||||
export class Checker {
|
|
||||||
private nodeTys = new Map<number, Ty>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private filename: string,
|
|
||||||
private text: string,
|
|
||||||
private file: ast.Node,
|
|
||||||
private resols: ResolveMap,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
check(node: ast.Node): Ty {
|
|
||||||
if (this.nodeTys.has(node.id)) {
|
|
||||||
return this.nodeTys.get(node.id)!;
|
|
||||||
}
|
|
||||||
const ty = this.checkNode(node);
|
|
||||||
this.nodeTys.set(node.id, ty);
|
|
||||||
return ty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkNode(node: ast.Node): Ty {
|
|
||||||
const k = node.kind;
|
|
||||||
|
|
||||||
if (node.is("FnStmt")) {
|
|
||||||
return this.checkFnStmt(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is("Param")) {
|
|
||||||
const sym = this.resols.get(node);
|
|
||||||
|
|
||||||
if (sym.tag === "Let") {
|
|
||||||
const exprTy = this.check(sym.stmt.kind.expr);
|
|
||||||
if (node.kind.ty) {
|
|
||||||
const explicitTy = this.check(node.kind.ty);
|
|
||||||
this.assertCompatible(
|
|
||||||
exprTy,
|
|
||||||
explicitTy,
|
|
||||||
sym.stmt.kind.expr.line,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return exprTy;
|
|
||||||
}
|
|
||||||
if (sym.tag === "FnParam") {
|
|
||||||
if (!node.kind.ty) {
|
|
||||||
this.error(node.line, `parameter must have a type`);
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
return this.check(node.kind.ty);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`'${sym.tag}' not handled`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is("IdentExpr")) {
|
|
||||||
const sym = this.resols.get(node);
|
|
||||||
if (sym.tag === "Fn") {
|
|
||||||
return this.check(sym.stmt);
|
|
||||||
}
|
|
||||||
if (sym.tag === "Builtin") {
|
|
||||||
return builtins.find((s) => s.id === sym.id)!.ty;
|
|
||||||
}
|
|
||||||
if (sym.tag === "FnParam") {
|
|
||||||
return this.check(sym.param);
|
|
||||||
}
|
|
||||||
if (sym.tag === "Let") {
|
|
||||||
return this.check(sym.param);
|
|
||||||
}
|
|
||||||
throw new Error(`'${sym.tag}' not handled`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is("IntExpr")) {
|
|
||||||
return Ty.Int;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is("CallExpr")) {
|
|
||||||
return this.checkCall(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is("BinaryExpr")) {
|
|
||||||
const left = this.check(node.kind.left);
|
|
||||||
const right = this.check(node.kind.right);
|
|
||||||
const binaryOp = binaryOpPatterns
|
|
||||||
.find((pat) =>
|
|
||||||
pat.op === node.kind.op &&
|
|
||||||
left.compatibleWith(pat.left) &&
|
|
||||||
right.compatibleWith(pat.right)
|
|
||||||
);
|
|
||||||
if (!binaryOp) {
|
|
||||||
this.error(
|
|
||||||
node.line,
|
|
||||||
`operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`,
|
|
||||||
);
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
return binaryOp.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is("IdentTy")) {
|
|
||||||
switch (node.kind.ident) {
|
|
||||||
case "void":
|
|
||||||
return Ty.Void;
|
|
||||||
case "int":
|
|
||||||
return Ty.Int;
|
|
||||||
default:
|
|
||||||
this.error(node.line, `unknown type '${node.kind.ident}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`'${k.tag}' not unhandled`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkFnStmt(stmt: ast.NodeWithKind<"FnStmt">): Ty {
|
|
||||||
const k = stmt.kind;
|
|
||||||
|
|
||||||
const params = k.params.map((param) => this.check(param));
|
|
||||||
const retTy = k.retTy ? this.check(k.retTy) : Ty.Void;
|
|
||||||
|
|
||||||
k.body.visit({
|
|
||||||
visit: (node) => {
|
|
||||||
if (node.is("ReturnStmt")) {
|
|
||||||
const ty = node.kind.expr
|
|
||||||
? this.check(node.kind.expr)
|
|
||||||
: Ty.Void;
|
|
||||||
if (!ty.compatibleWith(retTy)) {
|
|
||||||
this.error(
|
|
||||||
node.line,
|
|
||||||
`type '${ty.pretty()}' not compatible with return type '${retTy.pretty()}'`,
|
|
||||||
);
|
|
||||||
this.info(
|
|
||||||
stmt.kind.retTy?.line ?? stmt.line,
|
|
||||||
`return type '${retTy}' defined here`,
|
|
||||||
);
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ty = Ty.create("Fn", { params, retTy });
|
|
||||||
return Ty.create("FnStmt", { stmt, ty });
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty {
|
|
||||||
const calleeTy = this.check(node.kind.expr);
|
|
||||||
|
|
||||||
const callableTy = calleeTy.isKind("Fn")
|
|
||||||
? calleeTy
|
|
||||||
: calleeTy.isKind("FnStmt")
|
|
||||||
? calleeTy.kind.ty as Ty & { kind: { tag: "Fn" } }
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!callableTy) {
|
|
||||||
this.error(
|
|
||||||
node.line,
|
|
||||||
`type '${calleeTy.pretty()}' not callable`,
|
|
||||||
);
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
const args = node.kind.args
|
|
||||||
.map((arg) => this.check(arg));
|
|
||||||
const params = callableTy.kind.params;
|
|
||||||
if (args.length !== params.length) {
|
|
||||||
this.error(
|
|
||||||
node.line,
|
|
||||||
`incorrect amount of arguments. got ${args.length} expected ${params.length}`,
|
|
||||||
);
|
|
||||||
if (calleeTy.isKind("FnStmt")) {
|
|
||||||
this.info(
|
|
||||||
calleeTy.kind.stmt.line,
|
|
||||||
"function defined here",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
for (const i of args.keys()) {
|
|
||||||
if (!args[i].compatibleWith(params[i])) {
|
|
||||||
this.error(
|
|
||||||
node.kind.args[i].line,
|
|
||||||
`type '${args[i].pretty()}' not compatible with type '${
|
|
||||||
params[i]
|
|
||||||
}', for argument ${i}`,
|
|
||||||
);
|
|
||||||
if (calleeTy.isKind("FnStmt")) {
|
|
||||||
this.info(
|
|
||||||
calleeTy.kind.stmt.kind.params[i].line,
|
|
||||||
`parameter '${
|
|
||||||
calleeTy.kind.stmt.kind.params[i]
|
|
||||||
.as("Param").kind.ident
|
|
||||||
}' defined here`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return callableTy.kind.retTy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private assertCompatible(left: Ty, right: Ty, line: number): void {
|
|
||||||
if (!left.compatibleWith(right)) {
|
|
||||||
this.error(
|
|
||||||
line,
|
|
||||||
`type '${left.pretty()}' not compatible with type '${right.pretty()}'`,
|
|
||||||
);
|
|
||||||
this.fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private error(line: number, message: string) {
|
|
||||||
printDiagnostics(
|
|
||||||
this.filename,
|
|
||||||
line,
|
|
||||||
"error",
|
|
||||||
message,
|
|
||||||
this.text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private info(line: number, message: string) {
|
|
||||||
printDiagnostics(
|
|
||||||
this.filename,
|
|
||||||
line,
|
|
||||||
"info",
|
|
||||||
message,
|
|
||||||
this.text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fail(): never {
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BinaryOpPattern = {
|
|
||||||
op: ast.BinaryOp;
|
|
||||||
left: Ty;
|
|
||||||
right: Ty;
|
|
||||||
result: Ty;
|
|
||||||
};
|
|
||||||
|
|
||||||
const binaryOpPatterns: BinaryOpPattern[] = [
|
|
||||||
{ op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
|
||||||
{ op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
|
||||||
];
|
|
||||||
|
|
||||||
export type Sym =
|
|
||||||
| { tag: "Error" }
|
|
||||||
| { tag: "Builtin"; id: string }
|
|
||||||
| { tag: "Fn"; stmt: ast.NodeWithKind<"FnStmt"> }
|
|
||||||
| {
|
|
||||||
tag: "FnParam";
|
|
||||||
stmt: ast.NodeWithKind<"FnStmt">;
|
|
||||||
param: ast.NodeWithKind<"Param">;
|
|
||||||
idx: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
tag: "Let";
|
|
||||||
stmt: ast.NodeWithKind<"LetStmt">;
|
|
||||||
param: ast.NodeWithKind<"Param">;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ResolveMap {
|
|
||||||
constructor(
|
|
||||||
private resols: Map<number, Sym>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get(node: ast.Node): Sym {
|
|
||||||
if (!this.resols.has(node.id)) {
|
|
||||||
throw new Error(`'${node.kind.tag}' not resolved`);
|
|
||||||
}
|
|
||||||
return this.resols.get(node.id)!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ResolverSyms {
|
|
||||||
static root(): ResolverSyms {
|
|
||||||
return new ResolverSyms(
|
|
||||||
new Map(
|
|
||||||
builtins.map<[string, Sym]>((sym) => [
|
|
||||||
sym.id,
|
|
||||||
{ tag: "Builtin", id: sym.id },
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
static forkFrom(parent: ResolverSyms): ResolverSyms {
|
|
||||||
return new ResolverSyms(
|
|
||||||
new Map(),
|
|
||||||
parent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
private syms = new Map<string, Sym>(),
|
|
||||||
public parent: ResolverSyms | null,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
define(ident: string, sym: Sym) {
|
|
||||||
this.syms.set(ident, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(ident: string): Sym | null {
|
|
||||||
if (this.syms.has(ident)) {
|
|
||||||
return this.syms.get(ident)!;
|
|
||||||
}
|
|
||||||
if (this.parent) {
|
|
||||||
return this.parent.resolve(ident);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolve(
|
|
||||||
filename: string,
|
|
||||||
text: string,
|
|
||||||
file: ast.Node,
|
|
||||||
): ResolveMap {
|
|
||||||
let syms = ResolverSyms.root();
|
|
||||||
const resols = new Map<number, Sym>();
|
|
||||||
|
|
||||||
file.visit({
|
|
||||||
visit(node) {
|
|
||||||
const k = node.kind;
|
|
||||||
|
|
||||||
if (k.tag === "File" || k.tag === "Block") {
|
|
||||||
syms = ResolverSyms.forkFrom(syms);
|
|
||||||
for (const stmt of k.stmts) {
|
|
||||||
if (stmt.is("FnStmt")) {
|
|
||||||
syms.define(stmt.kind.ident, { tag: "Fn", stmt });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.visitBelow(this);
|
|
||||||
syms = syms.parent!;
|
|
||||||
return "break";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (k.tag === "FnStmt") {
|
|
||||||
ast.assertNodeWithKind(node, "FnStmt");
|
|
||||||
syms = ResolverSyms.forkFrom(syms);
|
|
||||||
for (const [idx, param] of k.params.entries()) {
|
|
||||||
ast.assertNodeWithKind(param, "Param");
|
|
||||||
const sym: Sym = { tag: "FnParam", stmt: node, param, idx };
|
|
||||||
syms.define(param.kind.ident, sym);
|
|
||||||
resols.set(param.id, sym);
|
|
||||||
}
|
|
||||||
node.visitBelow(this);
|
|
||||||
syms = syms.parent!;
|
|
||||||
return "break";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (k.tag === "LetStmt") {
|
|
||||||
const stmt = node as ast.NodeWithKind<"LetStmt">;
|
|
||||||
const param = k.param as ast.NodeWithKind<"Param">;
|
|
||||||
const sym: Sym = { tag: "Let", stmt, param };
|
|
||||||
syms.define(param.kind.ident, sym);
|
|
||||||
resols.set(param.id, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (k.tag === "IdentExpr") {
|
|
||||||
const sym = syms.resolve(k.ident);
|
|
||||||
if (sym === null) {
|
|
||||||
printDiagnostics(
|
|
||||||
filename,
|
|
||||||
node.line,
|
|
||||||
"error",
|
|
||||||
`undefined symbol '${k.ident}'`,
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
resols.set(node.id, sym);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return new ResolveMap(resols);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Builtin = {
|
|
||||||
id: string;
|
|
||||||
ty: Ty;
|
|
||||||
};
|
|
||||||
|
|
||||||
const builtins: Builtin[] = [
|
|
||||||
{
|
|
||||||
id: "print_int",
|
|
||||||
ty: Ty.create("Fn", {
|
|
||||||
params: [Ty.Int],
|
|
||||||
retTy: Ty.Void,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "__add",
|
|
||||||
ty: Ty.create("Fn", {
|
|
||||||
params: [Ty.Int, Ty.Int],
|
|
||||||
retTy: Ty.Int,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function parse(
|
|
||||||
filename: string,
|
|
||||||
text: string,
|
|
||||||
): ast.Node {
|
|
||||||
return new Parser(filename, text).parseFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Parser {
|
|
||||||
private toks: Tok[];
|
|
||||||
private idx = 0;
|
|
||||||
private currentLine = 1;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private filename: string,
|
|
||||||
private text: string,
|
|
||||||
) {
|
|
||||||
this.toks = tokenize(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseFile(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
const stmts: ast.Node[] = [];
|
|
||||||
while (!this.done) {
|
|
||||||
stmts.push(this.parseStmt());
|
|
||||||
}
|
|
||||||
return ast.Node.create(loc, "File", { stmts });
|
|
||||||
}
|
|
||||||
|
|
||||||
parseBlock(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
this.mustEat("{");
|
|
||||||
const stmts: ast.Node[] = [];
|
|
||||||
while (!this.done && !this.test("}")) {
|
|
||||||
stmts.push(this.parseStmt());
|
|
||||||
}
|
|
||||||
this.mustEat("}");
|
|
||||||
return ast.Node.create(loc, "Block", { stmts });
|
|
||||||
}
|
|
||||||
|
|
||||||
parseStmt(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
if (this.test("fn")) {
|
|
||||||
return this.parseFnStmt();
|
|
||||||
} else if (this.test("return")) {
|
|
||||||
return this.parseReturnStmt();
|
|
||||||
} else if (this.test("let")) {
|
|
||||||
return this.parseLetStmt();
|
|
||||||
} else {
|
|
||||||
const place = this.parseExpr();
|
|
||||||
if (this.eat("=")) {
|
|
||||||
const expr = this.parseExpr();
|
|
||||||
this.mustEat(";");
|
|
||||||
return ast.Node.create(loc, "AssignStmt", { place, expr });
|
|
||||||
}
|
|
||||||
this.mustEat(";");
|
|
||||||
return ast.Node.create(loc, "ExprStmt", { expr: place });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseFnStmt(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
this.step();
|
|
||||||
const ident = this.mustEat("ident").value;
|
|
||||||
this.mustEat("(");
|
|
||||||
const params: ast.Node[] = [];
|
|
||||||
if (!this.test(")")) {
|
|
||||||
params.push(this.parseParam());
|
|
||||||
while (this.eat(",")) {
|
|
||||||
if (this.test(")")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
params.push(this.parseParam());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.mustEat(")");
|
|
||||||
let retTy: ast.Node | null = null;
|
|
||||||
if (this.eat("->")) {
|
|
||||||
retTy = this.parseTy();
|
|
||||||
}
|
|
||||||
const body = this.parseBlock();
|
|
||||||
return ast.Node.create(loc, "FnStmt", { ident, params, retTy, body });
|
|
||||||
}
|
|
||||||
|
|
||||||
parseReturnStmt(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
this.step();
|
|
||||||
let expr: ast.Node | null = null;
|
|
||||||
if (!this.test(";")) {
|
|
||||||
expr = this.parseExpr();
|
|
||||||
}
|
|
||||||
this.mustEat(";");
|
|
||||||
return ast.Node.create(loc, "ReturnStmt", { expr });
|
|
||||||
}
|
|
||||||
|
|
||||||
parseLetStmt(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
this.step();
|
|
||||||
const param = this.parseParam();
|
|
||||||
this.mustEat("=");
|
|
||||||
const expr = this.parseExpr();
|
|
||||||
this.mustEat(";");
|
|
||||||
return ast.Node.create(loc, "LetStmt", { param, expr });
|
|
||||||
}
|
|
||||||
|
|
||||||
parseParam(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
const ident = this.mustEat("ident").value;
|
|
||||||
let ty: ast.Node | null = null;
|
|
||||||
if (this.eat(":")) {
|
|
||||||
ty = this.parseTy();
|
|
||||||
}
|
|
||||||
return ast.Node.create(loc, "Param", { ident, ty });
|
|
||||||
}
|
|
||||||
|
|
||||||
parseExpr(): ast.Node {
|
|
||||||
return this.parseBinary();
|
|
||||||
}
|
|
||||||
|
|
||||||
parseBinary(prec = 7): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
if (prec == 0) {
|
|
||||||
return this.parsePrefixE();
|
|
||||||
}
|
|
||||||
const ops: [Tok["type"], ast.BinaryOp, number][] = [
|
|
||||||
["or", "Or", 9],
|
|
||||||
["and", "And", 8],
|
|
||||||
["==", "Eq", 7],
|
|
||||||
["!=", "Ne", 7],
|
|
||||||
["<", "Lt", 7],
|
|
||||||
[">", "Gt", 7],
|
|
||||||
["<=", "Lte", 7],
|
|
||||||
[">=", "Gte", 7],
|
|
||||||
["|", "BitOr", 6],
|
|
||||||
["^", "BitXor", 5],
|
|
||||||
["&", "BitAnd", 4],
|
|
||||||
["<<", "Shl", 3],
|
|
||||||
[">>", "Shr", 3],
|
|
||||||
["+", "Add", 2],
|
|
||||||
["-", "Subtract", 2],
|
|
||||||
["*", "Multiply", 1],
|
|
||||||
["/", "Divide", 1],
|
|
||||||
["%", "Remainder", 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
let left = this.parseBinary(prec - 1);
|
|
||||||
|
|
||||||
let should_continue = true;
|
|
||||||
while (should_continue) {
|
|
||||||
should_continue = false;
|
|
||||||
for (const [tok, op, p] of ops) {
|
|
||||||
if (prec >= p && this.eat(tok)) {
|
|
||||||
const right = this.parseBinary(prec - 1);
|
|
||||||
left = ast.Node.create(
|
|
||||||
loc,
|
|
||||||
"BinaryExpr",
|
|
||||||
{ op, left, right, tok },
|
|
||||||
);
|
|
||||||
should_continue = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
|
|
||||||
parsePrefixE() {
|
|
||||||
return this.parsePostfix();
|
|
||||||
}
|
|
||||||
|
|
||||||
parsePostfix(): ast.Node {
|
|
||||||
let expr = this.parseOperand();
|
|
||||||
while (true) {
|
|
||||||
const loc = this.loc();
|
|
||||||
if (this.eat("(")) {
|
|
||||||
const args: ast.Node[] = [];
|
|
||||||
if (!this.test(")")) {
|
|
||||||
args.push(this.parseExpr());
|
|
||||||
while (this.eat(",")) {
|
|
||||||
if (this.done || this.test(")")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
args.push(this.parseExpr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.mustEat(")");
|
|
||||||
expr = ast.Node.create(loc, "CallExpr", { expr, args });
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseOperand(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
if (this.test("ident")) {
|
|
||||||
const ident = this.current.value;
|
|
||||||
this.step();
|
|
||||||
return ast.Node.create(loc, "IdentExpr", { ident });
|
|
||||||
} else if (this.test("int")) {
|
|
||||||
const value = Number(this.current.value);
|
|
||||||
this.step();
|
|
||||||
return ast.Node.create(loc, "IntExpr", { value });
|
|
||||||
} else {
|
|
||||||
this.mustEat("<expression>");
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseTy(): ast.Node {
|
|
||||||
const loc = this.loc();
|
|
||||||
if (this.test("ident")) {
|
|
||||||
const ident = this.current.value;
|
|
||||||
this.step();
|
|
||||||
return ast.Node.create(loc, "IdentTy", { ident });
|
|
||||||
} else {
|
|
||||||
this.mustEat("<type>");
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mustEat(type: string, loc: number = this.loc()): Tok {
|
|
||||||
const tok = this.current;
|
|
||||||
if (tok.type !== type) {
|
|
||||||
this.error(
|
|
||||||
`expected '${type}', got '${
|
|
||||||
this.done ? "eof" : this.current.type
|
|
||||||
}'`,
|
|
||||||
loc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.step();
|
|
||||||
return tok;
|
|
||||||
}
|
|
||||||
|
|
||||||
private error(message: string, loc: number): never {
|
|
||||||
printDiagnostics(this.filename, loc, "error", message, this.text);
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private eat(type: string): boolean {
|
|
||||||
if (this.test(type)) {
|
|
||||||
this.step();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private step() {
|
|
||||||
this.idx += 1;
|
|
||||||
if (!this.done) {
|
|
||||||
this.currentLine = this.current.line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private test(type: string): boolean {
|
|
||||||
return !this.done && this.current.type == type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private loc(): number {
|
|
||||||
return this.currentLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get current(): Tok {
|
|
||||||
return this.toks[this.idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
private get done(): boolean {
|
|
||||||
return this.idx >= this.toks.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Tok = { type: string; value: string; line: number };
|
|
||||||
|
|
||||||
const keywordPattern =
|
|
||||||
/^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/;
|
|
||||||
const operatorPattern =
|
|
||||||
/((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g;
|
|
||||||
|
|
||||||
export function tokenize(text: string): Tok[] {
|
|
||||||
return text
|
|
||||||
.replace(/\/\/[^\n]*/g, "")
|
|
||||||
.replace(operatorPattern, " $1 ")
|
|
||||||
.split(/[ \t\r]/)
|
|
||||||
.filter((value) => value !== "")
|
|
||||||
.reduce<[[string, number][], number]>(
|
|
||||||
([toks, line], value) => {
|
|
||||||
if (value === "\n") {
|
|
||||||
return [toks, line + 1];
|
|
||||||
} else {
|
|
||||||
return [[...toks, [value, line]], line];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[[], 1],
|
|
||||||
)[0]
|
|
||||||
.map<Tok>(([value, line]) => ({ type: value, value, line }))
|
|
||||||
.map((tok) =>
|
|
||||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tok.value)
|
|
||||||
? {
|
|
||||||
...tok,
|
|
||||||
type: keywordPattern.test(tok.value) ? tok.value : "ident",
|
|
||||||
}
|
|
||||||
: tok
|
|
||||||
)
|
|
||||||
.map((tok) =>
|
|
||||||
/^0|(?:[1-9][0-9]*)$/.test(tok.value)
|
|
||||||
? { ...tok, type: "int" }
|
|
||||||
: tok
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function printDiagnostics(
|
|
||||||
filename: string,
|
|
||||||
line: number,
|
|
||||||
severity: "error" | "info",
|
|
||||||
message: string,
|
|
||||||
text?: string,
|
|
||||||
) {
|
|
||||||
const severityColor = ({
|
|
||||||
"error": "red",
|
|
||||||
"info": "blue",
|
|
||||||
} as { [Key in typeof severity]: string })[severity];
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
`%c${severity}%c: ${message}\n %c--> ${filename}:${line}%c`,
|
|
||||||
`color: ${severityColor}; font-weight: bold;`,
|
|
||||||
"color: lightwhite; font-weight: bold;",
|
|
||||||
"color: gray;",
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
if (!text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newlines = text
|
|
||||||
.split("")
|
|
||||||
.map((ch, idx) => ch === "\n" ? idx : null)
|
|
||||||
.filter((v) => v !== null);
|
|
||||||
const lineText = text.slice(newlines[line - 2] + 1, newlines[line - 1]);
|
|
||||||
const lineNumberText = line.toString();
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
`${" ".repeat(lineNumberText.length)}%c|\n` +
|
|
||||||
`${lineNumberText}|%c${lineText}\n` +
|
|
||||||
`${" ".repeat(lineNumberText.length)}%c|` +
|
|
||||||
`%c${"~".repeat(lineText.length)}%c`,
|
|
||||||
"color: cyan;",
|
|
||||||
"color: lightwhite;",
|
|
||||||
"color: cyan;",
|
|
||||||
`color: ${severityColor};`,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
23
src/front/builtins.ts
Normal file
23
src/front/builtins.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Ty } from "../ty.ts";
|
||||||
|
|
||||||
|
export type Builtin = {
|
||||||
|
id: string;
|
||||||
|
ty: Ty;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const builtins: Builtin[] = [
|
||||||
|
{
|
||||||
|
id: "print_int",
|
||||||
|
ty: Ty.create("Fn", {
|
||||||
|
params: [Ty.Int],
|
||||||
|
retTy: Ty.Void,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "__add",
|
||||||
|
ty: Ty.create("Fn", {
|
||||||
|
params: [Ty.Int, Ty.Int],
|
||||||
|
retTy: Ty.Int,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
249
src/front/check.ts
Normal file
249
src/front/check.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import * as ast from "../ast.ts";
|
||||||
|
import { printDiagnostics } from "../diagnostics.ts";
|
||||||
|
import { Ty } from "../ty.ts";
|
||||||
|
import { builtins } from "./builtins.ts";
|
||||||
|
import { ResolveMap } from "./resolve.ts";
|
||||||
|
|
||||||
|
export class Checker {
|
||||||
|
private nodeTys = new Map<number, Ty>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private filename: string,
|
||||||
|
private text: string,
|
||||||
|
private file: ast.Node,
|
||||||
|
private resols: ResolveMap,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
check(node: ast.Node): Ty {
|
||||||
|
if (this.nodeTys.has(node.id)) {
|
||||||
|
return this.nodeTys.get(node.id)!;
|
||||||
|
}
|
||||||
|
const ty = this.checkNode(node);
|
||||||
|
this.nodeTys.set(node.id, ty);
|
||||||
|
return ty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkNode(node: ast.Node): Ty {
|
||||||
|
const k = node.kind;
|
||||||
|
|
||||||
|
if (node.is("FnStmt")) {
|
||||||
|
return this.checkFnStmt(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.is("Param")) {
|
||||||
|
const sym = this.resols.get(node);
|
||||||
|
|
||||||
|
if (sym.tag === "Let") {
|
||||||
|
const exprTy = this.check(sym.stmt.kind.expr);
|
||||||
|
if (node.kind.ty) {
|
||||||
|
const explicitTy = this.check(node.kind.ty);
|
||||||
|
this.assertCompatible(
|
||||||
|
exprTy,
|
||||||
|
explicitTy,
|
||||||
|
sym.stmt.kind.expr.line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return exprTy;
|
||||||
|
}
|
||||||
|
if (sym.tag === "FnParam") {
|
||||||
|
if (!node.kind.ty) {
|
||||||
|
this.error(node.line, `parameter must have a type`);
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
return this.check(node.kind.ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`'${sym.tag}' not handled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.is("IdentExpr")) {
|
||||||
|
const sym = this.resols.get(node);
|
||||||
|
if (sym.tag === "Fn") {
|
||||||
|
return this.check(sym.stmt);
|
||||||
|
}
|
||||||
|
if (sym.tag === "Builtin") {
|
||||||
|
return builtins.find((s) => s.id === sym.id)!.ty;
|
||||||
|
}
|
||||||
|
if (sym.tag === "FnParam") {
|
||||||
|
return this.check(sym.param);
|
||||||
|
}
|
||||||
|
if (sym.tag === "Let") {
|
||||||
|
return this.check(sym.param);
|
||||||
|
}
|
||||||
|
throw new Error(`'${sym.tag}' not handled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.is("IntExpr")) {
|
||||||
|
return Ty.Int;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.is("CallExpr")) {
|
||||||
|
return this.checkCall(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.is("BinaryExpr")) {
|
||||||
|
const left = this.check(node.kind.left);
|
||||||
|
const right = this.check(node.kind.right);
|
||||||
|
const binaryOp = binaryOpPatterns
|
||||||
|
.find((pat) =>
|
||||||
|
pat.op === node.kind.op &&
|
||||||
|
left.compatibleWith(pat.left) &&
|
||||||
|
right.compatibleWith(pat.right)
|
||||||
|
);
|
||||||
|
if (!binaryOp) {
|
||||||
|
this.error(
|
||||||
|
node.line,
|
||||||
|
`operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`,
|
||||||
|
);
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
return binaryOp.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.is("IdentTy")) {
|
||||||
|
switch (node.kind.ident) {
|
||||||
|
case "void":
|
||||||
|
return Ty.Void;
|
||||||
|
case "int":
|
||||||
|
return Ty.Int;
|
||||||
|
default:
|
||||||
|
this.error(node.line, `unknown type '${node.kind.ident}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`'${k.tag}' not unhandled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkFnStmt(stmt: ast.NodeWithKind<"FnStmt">): Ty {
|
||||||
|
const k = stmt.kind;
|
||||||
|
|
||||||
|
const params = k.params.map((param) => this.check(param));
|
||||||
|
const retTy = k.retTy ? this.check(k.retTy) : Ty.Void;
|
||||||
|
|
||||||
|
k.body.visit({
|
||||||
|
visit: (node) => {
|
||||||
|
if (node.is("ReturnStmt")) {
|
||||||
|
const ty = node.kind.expr
|
||||||
|
? this.check(node.kind.expr)
|
||||||
|
: Ty.Void;
|
||||||
|
if (!ty.compatibleWith(retTy)) {
|
||||||
|
this.error(
|
||||||
|
node.line,
|
||||||
|
`type '${ty.pretty()}' not compatible with return type '${retTy.pretty()}'`,
|
||||||
|
);
|
||||||
|
this.info(
|
||||||
|
stmt.kind.retTy?.line ?? stmt.line,
|
||||||
|
`return type '${retTy}' defined here`,
|
||||||
|
);
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ty = Ty.create("Fn", { params, retTy });
|
||||||
|
return Ty.create("FnStmt", { stmt, ty });
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty {
|
||||||
|
const calleeTy = this.check(node.kind.expr);
|
||||||
|
|
||||||
|
const callableTy = calleeTy.isKind("Fn")
|
||||||
|
? calleeTy
|
||||||
|
: calleeTy.isKind("FnStmt")
|
||||||
|
? calleeTy.kind.ty as Ty & { kind: { tag: "Fn" } }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!callableTy) {
|
||||||
|
this.error(
|
||||||
|
node.line,
|
||||||
|
`type '${calleeTy.pretty()}' not callable`,
|
||||||
|
);
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = node.kind.args
|
||||||
|
.map((arg) => this.check(arg));
|
||||||
|
const params = callableTy.kind.params;
|
||||||
|
if (args.length !== params.length) {
|
||||||
|
this.error(
|
||||||
|
node.line,
|
||||||
|
`incorrect amount of arguments. got ${args.length} expected ${params.length}`,
|
||||||
|
);
|
||||||
|
if (calleeTy.isKind("FnStmt")) {
|
||||||
|
this.info(
|
||||||
|
calleeTy.kind.stmt.line,
|
||||||
|
"function defined here",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
for (const i of args.keys()) {
|
||||||
|
if (!args[i].compatibleWith(params[i])) {
|
||||||
|
this.error(
|
||||||
|
node.kind.args[i].line,
|
||||||
|
`type '${args[i].pretty()}' not compatible with type '${
|
||||||
|
params[i]
|
||||||
|
}', for argument ${i}`,
|
||||||
|
);
|
||||||
|
if (calleeTy.isKind("FnStmt")) {
|
||||||
|
this.info(
|
||||||
|
calleeTy.kind.stmt.kind.params[i].line,
|
||||||
|
`parameter '${
|
||||||
|
calleeTy.kind.stmt.kind.params[i]
|
||||||
|
.as("Param").kind.ident
|
||||||
|
}' defined here`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callableTy.kind.retTy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertCompatible(left: Ty, right: Ty, line: number): void {
|
||||||
|
if (!left.compatibleWith(right)) {
|
||||||
|
this.error(
|
||||||
|
line,
|
||||||
|
`type '${left.pretty()}' not compatible with type '${right.pretty()}'`,
|
||||||
|
);
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private error(line: number, message: string) {
|
||||||
|
printDiagnostics(
|
||||||
|
this.filename,
|
||||||
|
line,
|
||||||
|
"error",
|
||||||
|
message,
|
||||||
|
this.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private info(line: number, message: string) {
|
||||||
|
printDiagnostics(
|
||||||
|
this.filename,
|
||||||
|
line,
|
||||||
|
"info",
|
||||||
|
message,
|
||||||
|
this.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fail(): never {
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinaryOpPattern = {
|
||||||
|
op: ast.BinaryOp;
|
||||||
|
left: Ty;
|
||||||
|
right: Ty;
|
||||||
|
result: Ty;
|
||||||
|
};
|
||||||
|
|
||||||
|
const binaryOpPatterns: BinaryOpPattern[] = [
|
||||||
|
{ op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
||||||
|
{ op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
||||||
|
];
|
||||||
3
src/front/mod.ts
Normal file
3
src/front/mod.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./parse.ts";
|
||||||
|
export * from "./resolve.ts";
|
||||||
|
export * from "./check.ts";
|
||||||
313
src/front/parse.ts
Normal file
313
src/front/parse.ts
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import * as ast from "../ast.ts";
|
||||||
|
import { printDiagnostics } from "../diagnostics.ts";
|
||||||
|
|
||||||
|
export function parse(
|
||||||
|
filename: string,
|
||||||
|
text: string,
|
||||||
|
): ast.Node {
|
||||||
|
return new Parser(filename, text).parseFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Parser {
|
||||||
|
private toks: Tok[];
|
||||||
|
private idx = 0;
|
||||||
|
private currentLine = 1;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private filename: string,
|
||||||
|
private text: string,
|
||||||
|
) {
|
||||||
|
this.toks = tokenize(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFile(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
const stmts: ast.Node[] = [];
|
||||||
|
while (!this.done) {
|
||||||
|
stmts.push(this.parseStmt());
|
||||||
|
}
|
||||||
|
return ast.Node.create(loc, "File", { stmts });
|
||||||
|
}
|
||||||
|
|
||||||
|
parseBlock(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
this.mustEat("{");
|
||||||
|
const stmts: ast.Node[] = [];
|
||||||
|
while (!this.done && !this.test("}")) {
|
||||||
|
stmts.push(this.parseStmt());
|
||||||
|
}
|
||||||
|
this.mustEat("}");
|
||||||
|
return ast.Node.create(loc, "Block", { stmts });
|
||||||
|
}
|
||||||
|
|
||||||
|
parseStmt(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
if (this.test("fn")) {
|
||||||
|
return this.parseFnStmt();
|
||||||
|
} else if (this.test("return")) {
|
||||||
|
return this.parseReturnStmt();
|
||||||
|
} else if (this.test("let")) {
|
||||||
|
return this.parseLetStmt();
|
||||||
|
} else {
|
||||||
|
const place = this.parseExpr();
|
||||||
|
if (this.eat("=")) {
|
||||||
|
const expr = this.parseExpr();
|
||||||
|
this.mustEat(";");
|
||||||
|
return ast.Node.create(loc, "AssignStmt", { place, expr });
|
||||||
|
}
|
||||||
|
this.mustEat(";");
|
||||||
|
return ast.Node.create(loc, "ExprStmt", { expr: place });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFnStmt(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
this.step();
|
||||||
|
const ident = this.mustEat("ident").value;
|
||||||
|
this.mustEat("(");
|
||||||
|
const params: ast.Node[] = [];
|
||||||
|
if (!this.test(")")) {
|
||||||
|
params.push(this.parseParam());
|
||||||
|
while (this.eat(",")) {
|
||||||
|
if (this.test(")")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
params.push(this.parseParam());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mustEat(")");
|
||||||
|
let retTy: ast.Node | null = null;
|
||||||
|
if (this.eat("->")) {
|
||||||
|
retTy = this.parseTy();
|
||||||
|
}
|
||||||
|
const body = this.parseBlock();
|
||||||
|
return ast.Node.create(loc, "FnStmt", { ident, params, retTy, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
parseReturnStmt(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
this.step();
|
||||||
|
let expr: ast.Node | null = null;
|
||||||
|
if (!this.test(";")) {
|
||||||
|
expr = this.parseExpr();
|
||||||
|
}
|
||||||
|
this.mustEat(";");
|
||||||
|
return ast.Node.create(loc, "ReturnStmt", { expr });
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLetStmt(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
this.step();
|
||||||
|
const param = this.parseParam();
|
||||||
|
this.mustEat("=");
|
||||||
|
const expr = this.parseExpr();
|
||||||
|
this.mustEat(";");
|
||||||
|
return ast.Node.create(loc, "LetStmt", { param, expr });
|
||||||
|
}
|
||||||
|
|
||||||
|
parseParam(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
const ident = this.mustEat("ident").value;
|
||||||
|
let ty: ast.Node | null = null;
|
||||||
|
if (this.eat(":")) {
|
||||||
|
ty = this.parseTy();
|
||||||
|
}
|
||||||
|
return ast.Node.create(loc, "Param", { ident, ty });
|
||||||
|
}
|
||||||
|
|
||||||
|
parseExpr(): ast.Node {
|
||||||
|
return this.parseBinary();
|
||||||
|
}
|
||||||
|
|
||||||
|
parseBinary(prec = 7): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
if (prec == 0) {
|
||||||
|
return this.parsePrefixE();
|
||||||
|
}
|
||||||
|
const ops: [Tok["type"], ast.BinaryOp, number][] = [
|
||||||
|
["or", "Or", 9],
|
||||||
|
["and", "And", 8],
|
||||||
|
["==", "Eq", 7],
|
||||||
|
["!=", "Ne", 7],
|
||||||
|
["<", "Lt", 7],
|
||||||
|
[">", "Gt", 7],
|
||||||
|
["<=", "Lte", 7],
|
||||||
|
[">=", "Gte", 7],
|
||||||
|
["|", "BitOr", 6],
|
||||||
|
["^", "BitXor", 5],
|
||||||
|
["&", "BitAnd", 4],
|
||||||
|
["<<", "Shl", 3],
|
||||||
|
[">>", "Shr", 3],
|
||||||
|
["+", "Add", 2],
|
||||||
|
["-", "Subtract", 2],
|
||||||
|
["*", "Multiply", 1],
|
||||||
|
["/", "Divide", 1],
|
||||||
|
["%", "Remainder", 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
let left = this.parseBinary(prec - 1);
|
||||||
|
|
||||||
|
let should_continue = true;
|
||||||
|
while (should_continue) {
|
||||||
|
should_continue = false;
|
||||||
|
for (const [tok, op, p] of ops) {
|
||||||
|
if (prec >= p && this.eat(tok)) {
|
||||||
|
const right = this.parseBinary(prec - 1);
|
||||||
|
left = ast.Node.create(
|
||||||
|
loc,
|
||||||
|
"BinaryExpr",
|
||||||
|
{ op, left, right, tok },
|
||||||
|
);
|
||||||
|
should_continue = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePrefixE() {
|
||||||
|
return this.parsePostfix();
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePostfix(): ast.Node {
|
||||||
|
let expr = this.parseOperand();
|
||||||
|
while (true) {
|
||||||
|
const loc = this.loc();
|
||||||
|
if (this.eat("(")) {
|
||||||
|
const args: ast.Node[] = [];
|
||||||
|
if (!this.test(")")) {
|
||||||
|
args.push(this.parseExpr());
|
||||||
|
while (this.eat(",")) {
|
||||||
|
if (this.done || this.test(")")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
args.push(this.parseExpr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mustEat(")");
|
||||||
|
expr = ast.Node.create(loc, "CallExpr", { expr, args });
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseOperand(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
if (this.test("ident")) {
|
||||||
|
const ident = this.current.value;
|
||||||
|
this.step();
|
||||||
|
return ast.Node.create(loc, "IdentExpr", { ident });
|
||||||
|
} else if (this.test("int")) {
|
||||||
|
const value = Number(this.current.value);
|
||||||
|
this.step();
|
||||||
|
return ast.Node.create(loc, "IntExpr", { value });
|
||||||
|
} else {
|
||||||
|
this.mustEat("<expression>");
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTy(): ast.Node {
|
||||||
|
const loc = this.loc();
|
||||||
|
if (this.test("ident")) {
|
||||||
|
const ident = this.current.value;
|
||||||
|
this.step();
|
||||||
|
return ast.Node.create(loc, "IdentTy", { ident });
|
||||||
|
} else {
|
||||||
|
this.mustEat("<type>");
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mustEat(type: string, loc: number = this.loc()): Tok {
|
||||||
|
const tok = this.current;
|
||||||
|
if (tok.type !== type) {
|
||||||
|
this.error(
|
||||||
|
`expected '${type}', got '${
|
||||||
|
this.done ? "eof" : this.current.type
|
||||||
|
}'`,
|
||||||
|
loc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.step();
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private error(message: string, loc: number): never {
|
||||||
|
printDiagnostics(this.filename, loc, "error", message, this.text);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private eat(type: string): boolean {
|
||||||
|
if (this.test(type)) {
|
||||||
|
this.step();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private step() {
|
||||||
|
this.idx += 1;
|
||||||
|
if (!this.done) {
|
||||||
|
this.currentLine = this.current.line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private test(type: string): boolean {
|
||||||
|
return !this.done && this.current.type == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loc(): number {
|
||||||
|
return this.currentLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get current(): Tok {
|
||||||
|
return this.toks[this.idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
private get done(): boolean {
|
||||||
|
return this.idx >= this.toks.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Tok = { type: string; value: string; line: number };
|
||||||
|
|
||||||
|
const keywordPattern =
|
||||||
|
/^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/;
|
||||||
|
const operatorPattern =
|
||||||
|
/((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g;
|
||||||
|
|
||||||
|
export function tokenize(text: string): Tok[] {
|
||||||
|
return text
|
||||||
|
.replace(/\/\/[^\n]*/g, "")
|
||||||
|
.replace(operatorPattern, " $1 ")
|
||||||
|
.split(/[ \t\r]/)
|
||||||
|
.filter((value) => value !== "")
|
||||||
|
.reduce<[[string, number][], number]>(
|
||||||
|
([toks, line], value) => {
|
||||||
|
if (value === "\n") {
|
||||||
|
return [toks, line + 1];
|
||||||
|
} else {
|
||||||
|
return [[...toks, [value, line]], line];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[[], 1],
|
||||||
|
)[0]
|
||||||
|
.map<Tok>(([value, line]) => ({ type: value, value, line }))
|
||||||
|
.map((tok) =>
|
||||||
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tok.value)
|
||||||
|
? {
|
||||||
|
...tok,
|
||||||
|
type: keywordPattern.test(tok.value) ? tok.value : "ident",
|
||||||
|
}
|
||||||
|
: tok
|
||||||
|
)
|
||||||
|
.map((tok) =>
|
||||||
|
/^0|(?:[1-9][0-9]*)$/.test(tok.value)
|
||||||
|
? { ...tok, type: "int" }
|
||||||
|
: tok
|
||||||
|
);
|
||||||
|
}
|
||||||
138
src/front/resolve.ts
Normal file
138
src/front/resolve.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import * as ast from "../ast.ts";
|
||||||
|
import { printDiagnostics } from "../diagnostics.ts";
|
||||||
|
import { Ty } from "../ty.ts";
|
||||||
|
import { builtins } from "./builtins.ts";
|
||||||
|
|
||||||
|
export class ResolveMap {
|
||||||
|
constructor(
|
||||||
|
private resols: Map<number, Sym>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get(node: ast.Node): Sym {
|
||||||
|
if (!this.resols.has(node.id)) {
|
||||||
|
throw new Error(`'${node.kind.tag}' not resolved`);
|
||||||
|
}
|
||||||
|
return this.resols.get(node.id)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Sym =
|
||||||
|
| { tag: "Error" }
|
||||||
|
| { tag: "Builtin"; id: string }
|
||||||
|
| { tag: "Fn"; stmt: ast.NodeWithKind<"FnStmt"> }
|
||||||
|
| {
|
||||||
|
tag: "FnParam";
|
||||||
|
stmt: ast.NodeWithKind<"FnStmt">;
|
||||||
|
param: ast.NodeWithKind<"Param">;
|
||||||
|
idx: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
tag: "Let";
|
||||||
|
stmt: ast.NodeWithKind<"LetStmt">;
|
||||||
|
param: ast.NodeWithKind<"Param">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolve(
|
||||||
|
filename: string,
|
||||||
|
text: string,
|
||||||
|
file: ast.Node,
|
||||||
|
): ResolveMap {
|
||||||
|
let syms = ResolverSyms.root();
|
||||||
|
const resols = new Map<number, Sym>();
|
||||||
|
|
||||||
|
file.visit({
|
||||||
|
visit(node) {
|
||||||
|
const k = node.kind;
|
||||||
|
|
||||||
|
if (k.tag === "File" || k.tag === "Block") {
|
||||||
|
syms = ResolverSyms.forkFrom(syms);
|
||||||
|
for (const stmt of k.stmts) {
|
||||||
|
if (stmt.is("FnStmt")) {
|
||||||
|
syms.define(stmt.kind.ident, { tag: "Fn", stmt });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.visitBelow(this);
|
||||||
|
syms = syms.parent!;
|
||||||
|
return "break";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k.tag === "FnStmt") {
|
||||||
|
ast.assertNodeWithKind(node, "FnStmt");
|
||||||
|
syms = ResolverSyms.forkFrom(syms);
|
||||||
|
for (const [idx, param] of k.params.entries()) {
|
||||||
|
ast.assertNodeWithKind(param, "Param");
|
||||||
|
const sym: Sym = { tag: "FnParam", stmt: node, param, idx };
|
||||||
|
syms.define(param.kind.ident, sym);
|
||||||
|
resols.set(param.id, sym);
|
||||||
|
}
|
||||||
|
node.visitBelow(this);
|
||||||
|
syms = syms.parent!;
|
||||||
|
return "break";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k.tag === "LetStmt") {
|
||||||
|
const stmt = node as ast.NodeWithKind<"LetStmt">;
|
||||||
|
const param = k.param as ast.NodeWithKind<"Param">;
|
||||||
|
const sym: Sym = { tag: "Let", stmt, param };
|
||||||
|
syms.define(param.kind.ident, sym);
|
||||||
|
resols.set(param.id, sym);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k.tag === "IdentExpr") {
|
||||||
|
const sym = syms.resolve(k.ident);
|
||||||
|
if (sym === null) {
|
||||||
|
printDiagnostics(
|
||||||
|
filename,
|
||||||
|
node.line,
|
||||||
|
"error",
|
||||||
|
`undefined symbol '${k.ident}'`,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
resols.set(node.id, sym);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ResolveMap(resols);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResolverSyms {
|
||||||
|
static root(): ResolverSyms {
|
||||||
|
return new ResolverSyms(
|
||||||
|
new Map(
|
||||||
|
builtins.map<[string, Sym]>((sym) => [
|
||||||
|
sym.id,
|
||||||
|
{ tag: "Builtin", id: sym.id },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static forkFrom(parent: ResolverSyms): ResolverSyms {
|
||||||
|
return new ResolverSyms(
|
||||||
|
new Map(),
|
||||||
|
parent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
private syms = new Map<string, Sym>(),
|
||||||
|
public parent: ResolverSyms | null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
define(ident: string, sym: Sym) {
|
||||||
|
this.syms.set(ident, sym);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(ident: string): Sym | null {
|
||||||
|
if (this.syms.has(ident)) {
|
||||||
|
return this.syms.get(ident)!;
|
||||||
|
}
|
||||||
|
if (this.parent) {
|
||||||
|
return this.parent.resolve(ident);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import * as ast from "./ast.ts";
|
import * as ast from "./ast.ts";
|
||||||
import * as front from "./front.ts";
|
import * as front from "./front/mod.ts";
|
||||||
import * as middle from "./middle.ts";
|
import * as middle from "./middle.ts";
|
||||||
import { MirInterpreter } from "./mir_interpreter.ts";
|
import { MirInterpreter } from "./mir_interpreter.ts";
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as ast from "./ast.ts";
|
import * as ast from "./ast.ts";
|
||||||
import { Checker, ResolveMap } from "./front.ts";
|
import { Checker, ResolveMap } from "./front/mod.ts";
|
||||||
import { Ty } from "./ty.ts";
|
import { Ty } from "./ty.ts";
|
||||||
|
|
||||||
export class MiddleLowerer {
|
export class MiddleLowerer {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user