split up front

This commit is contained in:
sfja 2026-03-11 09:54:21 +01:00
parent bdee8b5bed
commit b646f46fc0
9 changed files with 770 additions and 758 deletions

42
src/diagnostics.ts Normal file
View 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};`,
"",
);
}

View File

@ -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
View 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
View 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
View File

@ -0,0 +1,3 @@
export * from "./parse.ts";
export * from "./resolve.ts";
export * from "./check.ts";

313
src/front/parse.ts Normal file
View 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
View 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;
}
}

View File

@ -1,5 +1,5 @@
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 { MirInterpreter } from "./mir_interpreter.ts";

View File

@ -1,5 +1,5 @@
import * as ast from "./ast.ts";
import { Checker, ResolveMap } from "./front.ts";
import { Checker, ResolveMap } from "./front/mod.ts";
import { Ty } from "./ty.ts";
export class MiddleLowerer {