mirror of
https://git.sfja.dk/Mikkel/slige.git
synced 2025-01-18 19:36:32 +00:00
add typechecker
This commit is contained in:
parent
aa888c9368
commit
d4ea73de1d
229
compiler/Checker.ts
Normal file
229
compiler/Checker.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import { EType, Expr } from "./ast.ts";
|
||||||
|
import { Pos } from "./Token.ts";
|
||||||
|
import { VType, VTypeParam, vtypesEqual, vtypeToString } from "./vtypes.ts";
|
||||||
|
|
||||||
|
export class Checker {
|
||||||
|
public report(msg: string, pos: Pos) {
|
||||||
|
console.error(`${msg} at ${pos.line}:${pos.col}`);
|
||||||
|
}
|
||||||
|
public checkEType(etype: EType): VType {
|
||||||
|
const pos = etype.pos;
|
||||||
|
if (etype.kind.type === "ident") {
|
||||||
|
if (etype.kind.value === "null") {
|
||||||
|
return { type: "null" };
|
||||||
|
}
|
||||||
|
if (etype.kind.value === "int") {
|
||||||
|
return { type: "int" };
|
||||||
|
}
|
||||||
|
if (etype.kind.value === "bool") {
|
||||||
|
return { type: "bool" };
|
||||||
|
}
|
||||||
|
if (etype.kind.value === "string") {
|
||||||
|
return { type: "string" };
|
||||||
|
}
|
||||||
|
this.report(`undefined type '${etype.kind.value}'`, pos);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
if (etype.kind.type === "array") {
|
||||||
|
const inner = this.checkEType(etype.kind.inner);
|
||||||
|
return { type: "array", inner };
|
||||||
|
}
|
||||||
|
if (etype.kind.type === "struct") {
|
||||||
|
const noTypeTest = etype.kind.fields.reduce(
|
||||||
|
(acc, param) => [acc[0] || !param.etype, param.ident],
|
||||||
|
[false, ""],
|
||||||
|
);
|
||||||
|
if (noTypeTest[0]) {
|
||||||
|
this.report(
|
||||||
|
`field '${noTypeTest[1]}' declared without type`,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
const declaredTwiceTest = etype.kind.fields.reduce<
|
||||||
|
[boolean, string[], string]
|
||||||
|
>(
|
||||||
|
(acc, curr) => {
|
||||||
|
if (acc[0]) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (acc[1].includes(curr.ident)) {
|
||||||
|
return [true, acc[1], curr.ident];
|
||||||
|
}
|
||||||
|
return [false, [...acc[1], curr.ident], ""];
|
||||||
|
},
|
||||||
|
[false, [], ""],
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
declaredTwiceTest[0]
|
||||||
|
) {
|
||||||
|
this.report(`field ${declaredTwiceTest[2]} defined twice`, pos);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
const fields = etype.kind.fields.map((param): VTypeParam => ({
|
||||||
|
ident: param.ident,
|
||||||
|
vtype: this.checkEType(param.etype!),
|
||||||
|
}));
|
||||||
|
return { type: "struct", fields };
|
||||||
|
}
|
||||||
|
throw new Error(`unknown explicit type ${etype.kind.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkExpr(expr: Expr): VType {
|
||||||
|
const pos = expr.pos;
|
||||||
|
const vtype = ((): VType => {
|
||||||
|
switch (expr.kind.type) {
|
||||||
|
case "error":
|
||||||
|
throw new Error("error in AST");
|
||||||
|
case "ident":
|
||||||
|
throw new Error("ident expr in AST");
|
||||||
|
case "sym":
|
||||||
|
return this.checkSymExpr(expr);
|
||||||
|
case "null":
|
||||||
|
return { type: "null" };
|
||||||
|
case "int":
|
||||||
|
return { type: "int" };
|
||||||
|
case "bool":
|
||||||
|
return { type: "bool" };
|
||||||
|
case "string":
|
||||||
|
return { type: "string" };
|
||||||
|
case "binary":
|
||||||
|
return this.checkBinaryExpr(expr);
|
||||||
|
case "group":
|
||||||
|
return this.checkExpr(expr.kind.expr);
|
||||||
|
case "field": {
|
||||||
|
const subject = this.checkExpr(expr.kind.subject);
|
||||||
|
if (subject.type !== "struct") {
|
||||||
|
this.report("cannot use field on non-struct", pos);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
const value = expr.kind.value;
|
||||||
|
const found = subject.fields.find((param) =>
|
||||||
|
param.ident === value
|
||||||
|
);
|
||||||
|
if (!found) {
|
||||||
|
this.report(
|
||||||
|
`no field named '${expr.kind.value}' on struct`,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
return found.vtype;
|
||||||
|
}
|
||||||
|
case "index": {
|
||||||
|
const subject = this.checkExpr(expr.kind.subject);
|
||||||
|
if (subject.type !== "array") {
|
||||||
|
this.report("cannot index on non-array", pos);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
return subject.inner;
|
||||||
|
}
|
||||||
|
case "call": {
|
||||||
|
const subject = this.checkExpr(expr.kind.subject);
|
||||||
|
if (subject.type !== "fn") {
|
||||||
|
this.report("cannot call non-fn", pos);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
const args = expr.kind.args.map((arg) =>
|
||||||
|
this.checkExpr(arg)
|
||||||
|
);
|
||||||
|
if (args.length !== subject.params.length) {
|
||||||
|
this.report(
|
||||||
|
`incorrect number of arguments` +
|
||||||
|
`, expected ${subject.params.length}`,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; ++i) {
|
||||||
|
if (!vtypesEqual(args[i], subject.params[i].vtype)) {
|
||||||
|
this.report(
|
||||||
|
`incorrect argument ${i} '${
|
||||||
|
subject.params[i].ident
|
||||||
|
}'` +
|
||||||
|
`, expected ${
|
||||||
|
vtypeToString(subject.params[i].vtype)
|
||||||
|
}` +
|
||||||
|
`, got ${vtypeToString(args[i])}`,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subject.returnType;
|
||||||
|
}
|
||||||
|
case "unary":
|
||||||
|
case "if":
|
||||||
|
case "loop":
|
||||||
|
case "block":
|
||||||
|
}
|
||||||
|
throw new Error(`unhandled type ${expr.kind.type}`);
|
||||||
|
})();
|
||||||
|
expr.vtype = vtype;
|
||||||
|
throw new Error(`unknown expression ${expr.kind.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkSymExpr(expr: Expr): VType {
|
||||||
|
const pos = expr.pos;
|
||||||
|
if (expr.kind.type !== "sym") {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkBinaryExpr(expr: Expr): VType {
|
||||||
|
const pos = expr.pos;
|
||||||
|
if (expr.kind.type !== "binary") {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
const left = this.checkExpr(expr.kind.left);
|
||||||
|
const right = this.checkExpr(expr.kind.right);
|
||||||
|
for (const operation of simpleBinaryOperations) {
|
||||||
|
if (operation.binaryType !== expr.kind.binaryType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!vtypesEqual(operation.operand, left)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!vtypesEqual(left, right)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return operation.result ?? operation.operand;
|
||||||
|
}
|
||||||
|
this.report(
|
||||||
|
`cannot apply binary operation '${expr.kind.binaryType}' ` +
|
||||||
|
`on types '${vtypeToString(left)}' and '${
|
||||||
|
vtypeToString(right)
|
||||||
|
}'`,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
return { type: "error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleBinaryOperations: {
|
||||||
|
binaryType: string;
|
||||||
|
operand: VType;
|
||||||
|
result?: VType;
|
||||||
|
}[] = [
|
||||||
|
// arithmetic
|
||||||
|
{ binaryType: "+", operand: { type: "int" } },
|
||||||
|
{ binaryType: "-", operand: { type: "int" } },
|
||||||
|
{ binaryType: "*", operand: { type: "int" } },
|
||||||
|
{ binaryType: "/", operand: { type: "int" } },
|
||||||
|
// logical
|
||||||
|
{ binaryType: "and", operand: { type: "bool" } },
|
||||||
|
{ binaryType: "or", operand: { type: "bool" } },
|
||||||
|
// equality
|
||||||
|
{ binaryType: "==", operand: { type: "null" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "==", operand: { type: "int" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "==", operand: { type: "string" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "==", operand: { type: "bool" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "!=", operand: { type: "null" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "!=", operand: { type: "int" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "!=", operand: { type: "string" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "!=", operand: { type: "bool" }, result: { type: "bool" } },
|
||||||
|
// comparison
|
||||||
|
{ binaryType: "<", operand: { type: "int" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: ">", operand: { type: "int" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: "<=", operand: { type: "int" }, result: { type: "bool" } },
|
||||||
|
{ binaryType: ">=", operand: { type: "int" }, result: { type: "bool" } },
|
||||||
|
];
|
@ -5,16 +5,17 @@ export class Lexer {
|
|||||||
private line = 1;
|
private line = 1;
|
||||||
private col = 1;
|
private col = 1;
|
||||||
|
|
||||||
|
|
||||||
public constructor(private text: string) {}
|
public constructor(private text: string) {}
|
||||||
|
|
||||||
public next(): Token | null {
|
public next(): Token | null {
|
||||||
if (this.done())
|
if (this.done()) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
const pos = this.pos();
|
const pos = this.pos();
|
||||||
if (this.test(/[ \t\n\r]/)) {
|
if (this.test(/[ \t\n\r]/)) {
|
||||||
while (!this.done() && this.test(/[ \t\n\r]/))
|
while (!this.done() && this.test(/[ \t\n\r]/)) {
|
||||||
this.step();
|
this.step();
|
||||||
|
}
|
||||||
return this.next();
|
return this.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +40,8 @@ export class Lexer {
|
|||||||
return this.token("if", pos);
|
return this.token("if", pos);
|
||||||
case "else":
|
case "else":
|
||||||
return this.token("else", pos);
|
return this.token("else", pos);
|
||||||
|
case "struct":
|
||||||
|
return this.token("struct", pos);
|
||||||
default:
|
default:
|
||||||
return { ...this.token("ident", pos), identValue: value };
|
return { ...this.token("ident", pos), identValue: value };
|
||||||
}
|
}
|
||||||
@ -53,25 +56,26 @@ export class Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.test("0")) {
|
if (this.test("0")) {
|
||||||
this.step()
|
this.step();
|
||||||
if (!this.done() && this.test(/[0-9]/)) {
|
if (!this.done() && this.test(/[0-9]/)) {
|
||||||
console.error(
|
console.error(
|
||||||
`Lexer: invalid number`
|
`Lexer: invalid number` +
|
||||||
+ ` at ${pos.line}:${pos.col}`,
|
` at ${pos.line}:${pos.col}`,
|
||||||
);
|
);
|
||||||
return this.token("error", pos);
|
return this.token("error", pos);
|
||||||
}
|
}
|
||||||
return { ...this.token("int", pos), intValue: 0 };
|
return { ...this.token("int", pos), intValue: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.test("\"")) {
|
if (this.test('"')) {
|
||||||
this.step();
|
this.step();
|
||||||
let value = "";
|
let value = "";
|
||||||
while (!this.done() && !this.test("\"")) {
|
while (!this.done() && !this.test('"')) {
|
||||||
if (this.test("\\")) {
|
if (this.test("\\")) {
|
||||||
this.step();
|
this.step();
|
||||||
if (this.done())
|
if (this.done()) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
value += {
|
value += {
|
||||||
"n": "\n",
|
"n": "\n",
|
||||||
"t": "\t",
|
"t": "\t",
|
||||||
@ -82,10 +86,10 @@ export class Lexer {
|
|||||||
}
|
}
|
||||||
this.step();
|
this.step();
|
||||||
}
|
}
|
||||||
if (this.done() || !this.test("\"")) {
|
if (this.done() || !this.test('"')) {
|
||||||
console.error(
|
console.error(
|
||||||
`Lexer: unclosed/malformed string`
|
`Lexer: unclosed/malformed string` +
|
||||||
+ ` at ${pos.line}:${pos.col}`,
|
` at ${pos.line}:${pos.col}`,
|
||||||
);
|
);
|
||||||
return this.token("error", pos);
|
return this.token("error", pos);
|
||||||
}
|
}
|
||||||
@ -113,7 +117,7 @@ export class Lexer {
|
|||||||
return this.token("->", pos);
|
return this.token("->", pos);
|
||||||
}
|
}
|
||||||
if (this.test("=")) {
|
if (this.test("=")) {
|
||||||
this.step()
|
this.step();
|
||||||
return this.token("-=", pos);
|
return this.token("-=", pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,16 +129,21 @@ export class Lexer {
|
|||||||
this.step();
|
this.step();
|
||||||
return this.token("+=", pos);
|
return this.token("+=", pos);
|
||||||
}
|
}
|
||||||
|
if (first === "-" && !this.done() && this.test(">")) {
|
||||||
|
this.step();
|
||||||
|
return this.token("->", pos);
|
||||||
|
}
|
||||||
return this.token(first, pos);
|
return this.token(first, pos);
|
||||||
}
|
}
|
||||||
if (this.test("/")) {
|
if (this.test("/")) {
|
||||||
this.step()
|
|
||||||
if (this.test("/")) {
|
|
||||||
while (!this.done() && !this.test("\n"))
|
|
||||||
this.step();
|
this.step();
|
||||||
return this.next()
|
if (this.test("/")) {
|
||||||
|
while (!this.done() && !this.test("\n")) {
|
||||||
|
this.step();
|
||||||
}
|
}
|
||||||
return this.token("/", pos)
|
return this.next();
|
||||||
|
}
|
||||||
|
return this.token("/", pos);
|
||||||
}
|
}
|
||||||
if (this.test("false")) {
|
if (this.test("false")) {
|
||||||
this.step();
|
this.step();
|
||||||
@ -180,20 +189,29 @@ export class Lexer {
|
|||||||
this.step();
|
this.step();
|
||||||
return this.token("return", pos);
|
return this.token("return", pos);
|
||||||
}
|
}
|
||||||
console.error(`Lexer: illegal character '${this.current()}' at ${pos.line}:${pos.col}`);
|
console.error(
|
||||||
|
`Lexer: illegal character '${this.current()}' at ${pos.line}:${pos.col}`,
|
||||||
|
);
|
||||||
this.step();
|
this.step();
|
||||||
return this.next();
|
return this.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
private done(): boolean { return this.index >= this.text.length; }
|
private done(): boolean {
|
||||||
|
return this.index >= this.text.length;
|
||||||
|
}
|
||||||
|
|
||||||
private current(): string { return this.text[this.index]; }
|
private current(): string {
|
||||||
|
return this.text[this.index];
|
||||||
|
}
|
||||||
|
|
||||||
public currentPos(): Pos { return this.pos(); }
|
public currentPos(): Pos {
|
||||||
|
return this.pos();
|
||||||
|
}
|
||||||
|
|
||||||
private step() {
|
private step() {
|
||||||
if (this.done())
|
if (this.done()) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
if (this.current() === "\n") {
|
if (this.current() === "\n") {
|
||||||
this.line += 1;
|
this.line += 1;
|
||||||
this.col = 1;
|
this.col = 1;
|
||||||
@ -207,8 +225,8 @@ export class Lexer {
|
|||||||
return {
|
return {
|
||||||
index: this.index,
|
index: this.index,
|
||||||
line: this.line,
|
line: this.line,
|
||||||
col: this.col
|
col: this.col,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private token(type: string, pos: Pos): Token {
|
private token(type: string, pos: Pos): Token {
|
||||||
@ -217,13 +235,10 @@ export class Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private test(pattern: RegExp | string): boolean {
|
private test(pattern: RegExp | string): boolean {
|
||||||
if (typeof pattern === "string")
|
if (typeof pattern === "string") {
|
||||||
return this.current() === pattern;
|
return this.current() === pattern;
|
||||||
else
|
} else {
|
||||||
return pattern.test(this.current());
|
return pattern.test(this.current());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
import { Expr, ExprKind, Param, Stmt, StmtKind, BinaryType} from "./ast.ts";
|
import {
|
||||||
|
BinaryType,
|
||||||
|
EType,
|
||||||
|
ETypeKind,
|
||||||
|
Expr,
|
||||||
|
ExprKind,
|
||||||
|
Param,
|
||||||
|
Stmt,
|
||||||
|
StmtKind,
|
||||||
|
} from "./ast.ts";
|
||||||
import { Lexer } from "./Lexer.ts";
|
import { Lexer } from "./Lexer.ts";
|
||||||
import { Pos, Token } from "./Token.ts";
|
import { Pos, Token } from "./Token.ts";
|
||||||
|
|
||||||
@ -10,12 +19,19 @@ export class Parser {
|
|||||||
this.currentToken = lexer.next();
|
this.currentToken = lexer.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
private step() { this.currentToken = this.lexer.next() }
|
private step() {
|
||||||
public done(): boolean { return this.currentToken == null; }
|
this.currentToken = this.lexer.next();
|
||||||
private current(): Token { return this.currentToken!; }
|
}
|
||||||
|
public done(): boolean {
|
||||||
|
return this.currentToken == null;
|
||||||
|
}
|
||||||
|
private current(): Token {
|
||||||
|
return this.currentToken!;
|
||||||
|
}
|
||||||
private pos(): Pos {
|
private pos(): Pos {
|
||||||
if (this.done())
|
if (this.done()) {
|
||||||
return this.lexer.currentPos();
|
return this.lexer.currentPos();
|
||||||
|
}
|
||||||
return this.current().pos;
|
return this.current().pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,13 +41,18 @@ export class Parser {
|
|||||||
|
|
||||||
private report(msg: string, pos = this.pos()) {
|
private report(msg: string, pos = this.pos()) {
|
||||||
console.log(`Parser: ${msg} at ${pos.line}:${pos.col}`);
|
console.log(`Parser: ${msg} at ${pos.line}:${pos.col}`);
|
||||||
class ReportNotAnError extends Error { constructor() { super("ReportNotAnError"); } }
|
class ReportNotAnError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("ReportNotAnError");
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
throw new ReportNotAnError();
|
throw new ReportNotAnError();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof ReportNotAnError))
|
if (!(error instanceof ReportNotAnError)) {
|
||||||
throw error;
|
throw error;
|
||||||
console.log(error)
|
}
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,33 +68,47 @@ export class Parser {
|
|||||||
return { kind, pos, id };
|
return { kind, pos, id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private etype(kind: ETypeKind, pos: Pos): EType {
|
||||||
|
const id = this.nextNodeId;
|
||||||
|
this.nextNodeId += 1;
|
||||||
|
return { kind, pos, id };
|
||||||
|
}
|
||||||
|
|
||||||
private parseMultiLineBlockExpr(): Expr {
|
private parseMultiLineBlockExpr(): Expr {
|
||||||
const pos = this.pos();
|
const pos = this.pos();
|
||||||
if (this.test("{"))
|
if (this.test("{")) {
|
||||||
return this.parseBlock();
|
return this.parseBlock();
|
||||||
if (this.test("if"))
|
}
|
||||||
|
if (this.test("if")) {
|
||||||
return this.parseIf();
|
return this.parseIf();
|
||||||
if (this.test("loop"))
|
}
|
||||||
|
if (this.test("loop")) {
|
||||||
return this.parseLoop();
|
return this.parseLoop();
|
||||||
|
}
|
||||||
this.report("expected expr");
|
this.report("expected expr");
|
||||||
return this.expr({ type: "error" }, pos);
|
return this.expr({ type: "error" }, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSingleLineBlockStmt(): Stmt {
|
private parseSingleLineBlockStmt(): Stmt {
|
||||||
const pos = this.pos();
|
const pos = this.pos();
|
||||||
if (this.test("let"))
|
if (this.test("let")) {
|
||||||
return this.parseLet();
|
return this.parseLet();
|
||||||
if (this.test("return"))
|
}
|
||||||
|
if (this.test("return")) {
|
||||||
return this.parseReturn();
|
return this.parseReturn();
|
||||||
if (this.test("break"))
|
}
|
||||||
|
if (this.test("break")) {
|
||||||
return this.parseBreak();
|
return this.parseBreak();
|
||||||
|
}
|
||||||
this.report("expected stmt");
|
this.report("expected stmt");
|
||||||
return this.stmt({ type: "error" }, pos);
|
return this.stmt({ type: "error" }, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private eatSemicolon() {
|
private eatSemicolon() {
|
||||||
if (!this.test(";")) {
|
if (!this.test(";")) {
|
||||||
this.report(`expected ';', got '${this.currentToken?.type ?? "eof"}'`);
|
this.report(
|
||||||
|
`expected ';', got '${this.currentToken?.type ?? "eof"}'`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.step();
|
this.step();
|
||||||
@ -91,11 +126,12 @@ export class Parser {
|
|||||||
if (this.test("}")) {
|
if (this.test("}")) {
|
||||||
this.step();
|
this.step();
|
||||||
return this.expr({ type: "block", stmts }, pos);
|
return this.expr({ type: "block", stmts }, pos);
|
||||||
} else if (this.test("return") || this.test("break") || this.test("let")) {
|
} else if (
|
||||||
|
this.test("return") || this.test("break") || this.test("let")
|
||||||
|
) {
|
||||||
stmts.push(this.parseSingleLineBlockStmt());
|
stmts.push(this.parseSingleLineBlockStmt());
|
||||||
this.eatSemicolon();
|
this.eatSemicolon();
|
||||||
}
|
} else if (this.test("fn")) {
|
||||||
else if (this.test("fn")) {
|
|
||||||
stmts.push(this.parseSingleLineBlockStmt());
|
stmts.push(this.parseSingleLineBlockStmt());
|
||||||
stmts.push(this.parseFn());
|
stmts.push(this.parseFn());
|
||||||
} else if (this.test("{") || this.test("if") || this.test("loop")) {
|
} else if (this.test("{") || this.test("if") || this.test("loop")) {
|
||||||
@ -111,7 +147,12 @@ export class Parser {
|
|||||||
this.step();
|
this.step();
|
||||||
const value = this.parseExpr();
|
const value = this.parseExpr();
|
||||||
this.eatSemicolon();
|
this.eatSemicolon();
|
||||||
stmts.push(this.stmt({ type: "assign", subject: expr, value }, pos));
|
stmts.push(
|
||||||
|
this.stmt(
|
||||||
|
{ type: "assign", subject: expr, value },
|
||||||
|
pos,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (this.test(";")) {
|
} else if (this.test(";")) {
|
||||||
this.step();
|
this.step();
|
||||||
stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
|
stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
|
||||||
@ -133,7 +174,9 @@ export class Parser {
|
|||||||
while (!this.done()) {
|
while (!this.done()) {
|
||||||
if (this.test("fn")) {
|
if (this.test("fn")) {
|
||||||
stmts.push(this.parseFn());
|
stmts.push(this.parseFn());
|
||||||
} else if (this.test("let") || this.test("return") || this.test("break")) {
|
} else if (
|
||||||
|
this.test("let") || this.test("return") || this.test("break")
|
||||||
|
) {
|
||||||
stmts.push(this.parseSingleLineBlockStmt());
|
stmts.push(this.parseSingleLineBlockStmt());
|
||||||
this.eatSemicolon();
|
this.eatSemicolon();
|
||||||
} else if (this.test("{") || this.test("if") || this.test("loop")) {
|
} else if (this.test("{") || this.test("if") || this.test("loop")) {
|
||||||
@ -161,13 +204,21 @@ export class Parser {
|
|||||||
return this.stmt({ type: "error" }, pos);
|
return this.stmt({ type: "error" }, pos);
|
||||||
}
|
}
|
||||||
const params = this.parseFnParams();
|
const params = this.parseFnParams();
|
||||||
|
let returnType: EType | null = null;
|
||||||
|
if (this.test("->")) {
|
||||||
|
this.step();
|
||||||
|
returnType = this.parseEType();
|
||||||
|
}
|
||||||
if (!this.test("{")) {
|
if (!this.test("{")) {
|
||||||
this.report("expected block");
|
this.report("expected block");
|
||||||
return this.stmt({ type: "error" }, pos);
|
return this.stmt({ type: "error" }, pos);
|
||||||
}
|
}
|
||||||
const body = this.parseBlock();
|
const body = this.parseBlock();
|
||||||
|
if (returnType === null) {
|
||||||
return this.stmt({ type: "fn", ident, params, body }, pos);
|
return this.stmt({ type: "fn", ident, params, body }, pos);
|
||||||
}
|
}
|
||||||
|
return this.stmt({ type: "fn", ident, params, returnType, body }, pos);
|
||||||
|
}
|
||||||
|
|
||||||
public parseFnParams(): Param[] {
|
public parseFnParams(): Param[] {
|
||||||
this.step();
|
this.step();
|
||||||
@ -175,18 +226,21 @@ export class Parser {
|
|||||||
this.step();
|
this.step();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let params: Param[] = [];
|
const params: Param[] = [];
|
||||||
const paramResult = this.parseParam();
|
const paramResult = this.parseParam();
|
||||||
if (!paramResult.ok)
|
if (!paramResult.ok) {
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
params.push(paramResult.value);
|
params.push(paramResult.value);
|
||||||
while (this.test(",")) {
|
while (this.test(",")) {
|
||||||
this.step();
|
this.step();
|
||||||
if (this.test(")"))
|
if (this.test(")")) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
const paramResult = this.parseParam();
|
const paramResult = this.parseParam();
|
||||||
if (!paramResult.ok)
|
if (!paramResult.ok) {
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
params.push(paramResult.value);
|
params.push(paramResult.value);
|
||||||
}
|
}
|
||||||
if (!this.test(")")) {
|
if (!this.test(")")) {
|
||||||
@ -197,11 +251,15 @@ export class Parser {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
public parseParam(): { ok: true, value: Param } | { ok: false } {
|
public parseParam(): { ok: true; value: Param } | { ok: false } {
|
||||||
const pos = this.pos();
|
const pos = this.pos();
|
||||||
if (this.test("ident")) {
|
if (this.test("ident")) {
|
||||||
const ident = this.current().identValue!;
|
const ident = this.current().identValue!;
|
||||||
this.step();
|
this.step();
|
||||||
|
if (this.test(":")) {
|
||||||
|
const etype = this.parseEType();
|
||||||
|
return { ok: true, value: { ident, etype, pos } };
|
||||||
|
}
|
||||||
return { ok: true, value: { ident, pos } };
|
return { ok: true, value: { ident, pos } };
|
||||||
}
|
}
|
||||||
this.report("expected param");
|
this.report("expected param");
|
||||||
@ -212,8 +270,9 @@ export class Parser {
|
|||||||
const pos = this.pos();
|
const pos = this.pos();
|
||||||
this.step();
|
this.step();
|
||||||
const paramResult = this.parseParam();
|
const paramResult = this.parseParam();
|
||||||
if (!paramResult.ok)
|
if (!paramResult.ok) {
|
||||||
return this.stmt({ type: "error" }, pos);
|
return this.stmt({ type: "error" }, pos);
|
||||||
|
}
|
||||||
const param = paramResult.value;
|
const param = paramResult.value;
|
||||||
if (!this.test("=")) {
|
if (!this.test("=")) {
|
||||||
this.report("expected '='");
|
this.report("expected '='");
|
||||||
@ -297,10 +356,25 @@ export class Parser {
|
|||||||
const subject = this.parsePrefix();
|
const subject = this.parsePrefix();
|
||||||
return this.expr({ type: "unary", unaryType: "not", subject }, pos);
|
return this.expr({ type: "unary", unaryType: "not", subject }, pos);
|
||||||
}
|
}
|
||||||
for (const binaryType of ["+", "*", "==", "-", "/", "!=", "<", ">", "<=", ">=", "or", "and"]) {
|
for (
|
||||||
const subject = this.parseBinary(binaryType as BinaryType, pos)
|
const binaryType of [
|
||||||
|
"+",
|
||||||
|
"*",
|
||||||
|
"==",
|
||||||
|
"-",
|
||||||
|
"/",
|
||||||
|
"!=",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
"<=",
|
||||||
|
">=",
|
||||||
|
"or",
|
||||||
|
"and",
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
const subject = this.parseBinary(binaryType as BinaryType, pos);
|
||||||
if (subject !== null) {
|
if (subject !== null) {
|
||||||
return subject
|
return subject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.parsePostfix();
|
return this.parsePostfix();
|
||||||
@ -313,7 +387,7 @@ export class Parser {
|
|||||||
const right = this.parsePrefix();
|
const right = this.parsePrefix();
|
||||||
return this.expr({ type: "binary", binaryType, left, right }, pos);
|
return this.expr({ type: "binary", binaryType, left, right }, pos);
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public parsePostfix(): Expr {
|
public parsePostfix(): Expr {
|
||||||
@ -349,8 +423,9 @@ export class Parser {
|
|||||||
args.push(this.parseExpr());
|
args.push(this.parseExpr());
|
||||||
while (this.test(",")) {
|
while (this.test(",")) {
|
||||||
this.step();
|
this.step();
|
||||||
if (this.test(")"))
|
if (this.test(")")) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
args.push(this.parseExpr());
|
args.push(this.parseExpr());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -406,16 +481,78 @@ export class Parser {
|
|||||||
this.step();
|
this.step();
|
||||||
return this.expr({ type: "group", expr }, pos);
|
return this.expr({ type: "group", expr }, pos);
|
||||||
}
|
}
|
||||||
if (this.test("{"))
|
if (this.test("{")) {
|
||||||
return this.parseBlock();
|
return this.parseBlock();
|
||||||
if (this.test("if"))
|
}
|
||||||
|
if (this.test("if")) {
|
||||||
return this.parseIf();
|
return this.parseIf();
|
||||||
if (this.test("loop"))
|
}
|
||||||
|
if (this.test("loop")) {
|
||||||
return this.parseLoop();
|
return this.parseLoop();
|
||||||
|
}
|
||||||
|
|
||||||
this.report("expected expr", pos);
|
this.report("expected expr", pos);
|
||||||
this.step();
|
this.step();
|
||||||
return this.expr({ type: "error" }, pos);
|
return this.expr({ type: "error" }, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public parseEType(): EType {
|
||||||
|
const pos = this.pos();
|
||||||
|
if (this.test("ident")) {
|
||||||
|
const ident = this.current().identValue!;
|
||||||
|
return this.etype({ type: "ident", value: ident }, pos);
|
||||||
|
}
|
||||||
|
if (this.test("[")) {
|
||||||
|
this.step();
|
||||||
|
const inner = this.parseEType();
|
||||||
|
if (!this.test("]")) {
|
||||||
|
this.report("expected ']'", pos);
|
||||||
|
return this.etype({ type: "error" }, pos);
|
||||||
|
}
|
||||||
|
this.step();
|
||||||
|
return this.etype({ type: "array", inner }, pos);
|
||||||
|
}
|
||||||
|
if (this.test("struct")) {
|
||||||
|
this.step();
|
||||||
|
if (!this.test("{")) {
|
||||||
|
this.report("expected '{'");
|
||||||
|
return this.etype({ type: "error" }, pos);
|
||||||
|
}
|
||||||
|
const fields = this.parseETypeStructFields();
|
||||||
|
return this.etype({ type: "struct", fields }, pos);
|
||||||
|
}
|
||||||
|
this.report("expected type");
|
||||||
|
return this.etype({ type: "error" }, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseETypeStructFields(): Param[] {
|
||||||
|
this.step();
|
||||||
|
if (this.test("}")) {
|
||||||
|
this.step();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const params: Param[] = [];
|
||||||
|
const paramResult = this.parseParam();
|
||||||
|
if (!paramResult.ok) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
params.push(paramResult.value);
|
||||||
|
while (this.test(",")) {
|
||||||
|
this.step();
|
||||||
|
if (this.test("}")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const paramResult = this.parseParam();
|
||||||
|
if (!paramResult.ok) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
params.push(paramResult.value);
|
||||||
|
}
|
||||||
|
if (!this.test("}")) {
|
||||||
|
this.report("expected '}'");
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
this.step();
|
||||||
|
return params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
112
compiler/ast.ts
112
compiler/ast.ts
@ -1,59 +1,97 @@
|
|||||||
import { Pos } from "./Token.ts";
|
import { Pos } from "./Token.ts";
|
||||||
|
import { VType } from "./vtypes.ts";
|
||||||
|
|
||||||
export type UnaryType = "not";
|
export type UnaryType = "not";
|
||||||
export type BinaryType = "+" | "*" | "==" | "-" | "/" | "!=" | "<" | ">" | "<=" | ">=" | "or" | "and";
|
export type BinaryType =
|
||||||
|
| "+"
|
||||||
|
| "*"
|
||||||
|
| "=="
|
||||||
|
| "-"
|
||||||
|
| "/"
|
||||||
|
| "!="
|
||||||
|
| "<"
|
||||||
|
| ">"
|
||||||
|
| "<="
|
||||||
|
| ">="
|
||||||
|
| "or"
|
||||||
|
| "and";
|
||||||
|
|
||||||
export type Param = {
|
export type Param = {
|
||||||
ident: string,
|
ident: string;
|
||||||
pos: Pos,
|
etype?: EType;
|
||||||
|
pos: Pos;
|
||||||
|
vtype?: VType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Stmt = {
|
export type Stmt = {
|
||||||
kind: StmtKind,
|
kind: StmtKind;
|
||||||
pos: Pos,
|
pos: Pos;
|
||||||
id: number,
|
vtype?: VType;
|
||||||
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StmtKind =
|
export type StmtKind =
|
||||||
| { type: "error" }
|
| { type: "error" }
|
||||||
| { type: "break", expr?: Expr }
|
| { type: "break"; expr?: Expr }
|
||||||
| { type: "return", expr?: Expr }
|
| { type: "return"; expr?: Expr }
|
||||||
| { type: "fn", ident: string, params: Param[], body: Expr }
|
| {
|
||||||
| { type: "let", param: Param, value: Expr }
|
type: "fn";
|
||||||
| { type: "assign", subject: Expr, value: Expr }
|
ident: string;
|
||||||
| { type: "expr", expr: Expr }
|
params: Param[];
|
||||||
;
|
returnType?: EType;
|
||||||
|
body: Expr;
|
||||||
|
}
|
||||||
|
| { type: "let"; param: Param; value: Expr }
|
||||||
|
| { type: "assign"; subject: Expr; value: Expr }
|
||||||
|
| { type: "expr"; expr: Expr };
|
||||||
|
|
||||||
export type Expr = {
|
export type Expr = {
|
||||||
kind: ExprKind,
|
kind: ExprKind;
|
||||||
pos: Pos,
|
pos: Pos;
|
||||||
id: number,
|
vtype?: VType;
|
||||||
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExprKind =
|
export type ExprKind =
|
||||||
| { type: "error" }
|
| { type: "error" }
|
||||||
| { type: "int", value: number }
|
| { type: "int"; value: number }
|
||||||
| { type: "string", value: string }
|
| { type: "string"; value: string }
|
||||||
| { type: "ident", value: string }
|
| { type: "ident"; value: string }
|
||||||
| { type: "group", expr: Expr }
|
| { type: "group"; expr: Expr }
|
||||||
| { type: "field", subject: Expr, value: string }
|
| { type: "field"; subject: Expr; value: string }
|
||||||
| { type: "index", subject: Expr, value: Expr }
|
| { type: "index"; subject: Expr; value: Expr }
|
||||||
| { type: "call", subject: Expr, args: Expr[] }
|
| { type: "call"; subject: Expr; args: Expr[] }
|
||||||
| { type: "unary", unaryType: UnaryType, subject: Expr }
|
| { type: "unary"; unaryType: UnaryType; subject: Expr }
|
||||||
| { type: "binary", binaryType: BinaryType, left: Expr, right: Expr }
|
| { type: "binary"; binaryType: BinaryType; left: Expr; right: Expr }
|
||||||
| { type: "if", cond: Expr, truthy: Expr, falsy?: Expr }
|
| { type: "if"; cond: Expr; truthy: Expr; falsy?: Expr }
|
||||||
| { type: "bool", value: boolean}
|
| { type: "bool"; value: boolean }
|
||||||
| { type: "null" }
|
| { type: "null" }
|
||||||
| { type: "loop", body: Expr }
|
| { type: "loop"; body: Expr }
|
||||||
| { type: "block", stmts: Stmt[], expr?: Expr }
|
| { type: "block"; stmts: Stmt[]; expr?: Expr }
|
||||||
| { type: "sym", ident: string, defType: "let" | "fn" | "fn_param" | "builtin", stmt?: Stmt, param?: Param }
|
| {
|
||||||
;
|
type: "sym";
|
||||||
|
ident: string;
|
||||||
|
defType: "let" | "fn" | "fn_param" | "builtin";
|
||||||
|
stmt?: Stmt;
|
||||||
|
param?: Param;
|
||||||
|
};
|
||||||
|
|
||||||
export type Sym = {
|
export type Sym = {
|
||||||
ident: string,
|
ident: string;
|
||||||
type: "let" | "fn" | "fn_param" | "builtin",
|
type: "let" | "fn" | "fn_param" | "builtin";
|
||||||
pos?: Pos,
|
pos?: Pos;
|
||||||
stmt?: Stmt,
|
stmt?: Stmt;
|
||||||
param?: Param,
|
param?: Param;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type EType = {
|
||||||
|
kind: ETypeKind;
|
||||||
|
pos: Pos;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ETypeKind =
|
||||||
|
| { type: "error" }
|
||||||
|
| { type: "ident"; value: string }
|
||||||
|
| { type: "array"; inner: EType }
|
||||||
|
| { type: "struct"; fields: Param[] };
|
||||||
|
63
compiler/vtypes.ts
Normal file
63
compiler/vtypes.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
export type VType =
|
||||||
|
| { type: "" }
|
||||||
|
| { type: "error" }
|
||||||
|
| { type: "unknown" }
|
||||||
|
| { type: "null" }
|
||||||
|
| { type: "int" }
|
||||||
|
| { type: "string" }
|
||||||
|
| { type: "bool" }
|
||||||
|
| { type: "array"; inner: VType }
|
||||||
|
| { type: "struct"; fields: VTypeParam[] }
|
||||||
|
| { type: "fn"; params: VTypeParam[]; returnType: VType };
|
||||||
|
|
||||||
|
export type VTypeParam = {
|
||||||
|
ident: string;
|
||||||
|
vtype: VType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function vtypesEqual(a: VType, b: VType): boolean {
|
||||||
|
if (a.type !== b.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
["error", "unknown", "null", "int", "string", "bool", "struct"]
|
||||||
|
.includes(a.type)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (a.type === "array" && b.type === "array") {
|
||||||
|
return vtypesEqual(a.inner, b.inner);
|
||||||
|
}
|
||||||
|
if (a.type === "fn" && b.type === "fn") {
|
||||||
|
if (a.params.length !== b.params.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < a.params.length; ++i) {
|
||||||
|
if (!vtypesEqual(a.params[i].vtype, b.params[i].vtype)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vtypesEqual(a.returnType, b.returnType);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vtypeToString(vtype: VType): string {
|
||||||
|
if (
|
||||||
|
["error", "unknown", "null", "int", "string", "bool", "struct"]
|
||||||
|
.includes(vtype.type)
|
||||||
|
) {
|
||||||
|
return vtype.type;
|
||||||
|
}
|
||||||
|
if (vtype.type === "array") {
|
||||||
|
return `[${vtypeToString(vtype.inner)}]`;
|
||||||
|
}
|
||||||
|
if (vtype.type === "fn") {
|
||||||
|
const paramString = vtype.params.map((param) =>
|
||||||
|
`${param.ident}: ${vtypeToString(param.vtype)}`
|
||||||
|
)
|
||||||
|
.join(", ");
|
||||||
|
return `fn (${paramString}) -> ${vtypeToString(vtype.returnType)}`;
|
||||||
|
}
|
||||||
|
throw new Error(`unhandled vtype '${vtype.type}'`);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user