Compare commits
No commits in common. "0983524ae6b5b12eac90a832ddd95b5b75344ecf" and "b646f46fc062f2879a23cb4f79172563a22975f4" have entirely different histories.
0983524ae6
...
b646f46fc0
@ -1,225 +0,0 @@
|
|||||||
|
|
||||||
# Tagged union pattern in Typescript
|
|
||||||
|
|
||||||
To show the pattern, let's implement a simple interpreter.
|
|
||||||
|
|
||||||
Start by defining each union variant, or *kind*, of the type:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type ExprKind =
|
|
||||||
| { tag: "Int", value: number }
|
|
||||||
| { tag: "Add", left: Expr, right: Expr }
|
|
||||||
```
|
|
||||||
|
|
||||||
Then define the type itself as a class:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
class Expr {
|
|
||||||
constructor(
|
|
||||||
public line: number,
|
|
||||||
public kind: ExprKind,
|
|
||||||
) {}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each instance of `Expr` will have a `line` (source line number), but only *IntExpr* will have `value`, and only *AddExpr* will have `left` and `right`.
|
|
||||||
|
|
||||||
To *match* each variant, a type representing an `Expr` with a specific variant:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type ExprTag = ExprKind["tag"];
|
|
||||||
|
|
||||||
type ExprWithKind<Tag extends ExprTag> = Expr & { kind: { tag: Tag } };
|
|
||||||
```
|
|
||||||
|
|
||||||
The type `ExprKind["Tag"]` is a union of each `tag` value, i.e. in this example `"Int" | "Add"`. Saying `Tag extends ExprTag` means that `Tag` (type) is either `"Int"` or `"Add"`, i.e. for `.is("Int")`, `Tag` will be `"Int"` and for `.is("Add")`, `Tag` will be `"Add"`. The *narrowed* type `ExprWithKind` is defined by saying that we want an `Expr` that specifically has the `kind.tag` value of `Tag`.
|
|
||||||
|
|
||||||
Then define a methed that with a *type predicate*:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
class Expr {
|
|
||||||
// ...
|
|
||||||
is<Tag extends ExprTag>(tag: Tag): this is ExprWithKind<Tag> {
|
|
||||||
return this.kind.tag === tag;
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The type predicate tells Typescript that it can expect the `Expr` to conform to `ExprWithKind<Tag>` if `.is` evaluates to `true`. Using this, access can be gained to the variant fields in a type safe manner.
|
|
||||||
|
|
||||||
Now, it's possible to match each variant:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
function eval(expr: Expr): number {
|
|
||||||
|
|
||||||
if (expr.is("Int")) {
|
|
||||||
return expr.kind.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expr.is("Add")) {
|
|
||||||
return eval(expr.kind.left) + eval(expr.kind.right);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`'${expr.kind.tag}' not handled`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
By having `Expr` be a class, we can also define functions like this as methods:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
class Expr {
|
|
||||||
// ...
|
|
||||||
eval(): number {
|
|
||||||
if (expr.is("Int"))
|
|
||||||
return expr.kind.value;
|
|
||||||
if (expr.is("Add"))
|
|
||||||
return expr.kind.left.eval() + expr.kind.right.eval();
|
|
||||||
throw new Error(`'${expr.kind.tag}' not handled`);
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
By having the `tag` invariant, the Typescript compiler can check that you exhaust each variant in a match. To match each variant, I use this pattern ('k-pattern'):
|
|
||||||
|
|
||||||
```ts
|
|
||||||
class Expr {
|
|
||||||
// ...
|
|
||||||
eval(): number {
|
|
||||||
const k = this.kind;
|
|
||||||
switch (k.tag) { // Typescript language servers can auto-complete all variants
|
|
||||||
case "Int":
|
|
||||||
return k.value;
|
|
||||||
case "Add":
|
|
||||||
return k.right.eval() + k.left.eval();
|
|
||||||
}
|
|
||||||
const _: never = k; // compile time exhaustiveness check
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Sometimes you need to narrow to a variant that you already know is the correct variant. For that purpose, define these helper functions:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
class Expr {
|
|
||||||
// ...
|
|
||||||
assertIs<Tag extends ExprTag>(tag: Tag): asserts this is ExprWithKind<Tag> {
|
|
||||||
if (!this.is(tag)) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
as<Tag extends ExprTag>(tag: Tag): ExprWithKind<Tag> {
|
|
||||||
this.assertIs(tag);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Using these, narrowing can be done more conveniently:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// this will always be an IntExpr
|
|
||||||
let expr: Expr = ...;
|
|
||||||
|
|
||||||
expr.kind.value; // Property 'value' does not exist ...
|
|
||||||
|
|
||||||
expr.as("Int").kind.value; // ok
|
|
||||||
|
|
||||||
expr.assertIs("Int"); // will throw if expr isn't an IntExpr
|
|
||||||
expr.kind.value; // ok
|
|
||||||
```
|
|
||||||
|
|
||||||
For some applications, it might be more conveniently with a factory-function like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type ExprKindData<Tag extends ExprTag> = Omit<ExprWithKind<Tag>, "tag">;
|
|
||||||
|
|
||||||
class Expr {
|
|
||||||
// ...
|
|
||||||
static create<Tag extends ExprTag>(
|
|
||||||
tag: Tag,
|
|
||||||
kind: ExprKindData<Tag>,
|
|
||||||
line: number,
|
|
||||||
): ExprWithKind<Tag> {
|
|
||||||
return new Expr(line, {...kind, tag} as ExprKind & { tag: Ty });
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And use it like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const myInt = Expr.create("Int", { value: 123 }, line);
|
|
||||||
```
|
|
||||||
|
|
||||||
For often-used variants, it might be more convenient with a specific type and constructor:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type IntExpr = ExprWithKind<"Int">;
|
|
||||||
const IntExpr = (line: number, value: number): IntExpr =>
|
|
||||||
Expr.create("Int", { value }, line);
|
|
||||||
```
|
|
||||||
|
|
||||||
That can be used like this:
|
|
||||||
```ts
|
|
||||||
function exprIdentity(expr: Expr): Expr { return expr; }
|
|
||||||
function intExprIdentity(expr: IntExpr): IntExpr { return expr; }
|
|
||||||
|
|
||||||
const myIntExpr = IntExpr(123, line);
|
|
||||||
myIntExpr.kind.value;
|
|
||||||
exprIdentity(myIntExpr);
|
|
||||||
intExprIdentity(myIntExpr);
|
|
||||||
|
|
||||||
const myOtherExpr = Expr.create("Add", ...);
|
|
||||||
exprIdentity(myIntExpr);
|
|
||||||
intExprIdentity(myIntExpr); // error
|
|
||||||
```
|
|
||||||
|
|
||||||
Using this pattern, it's easy to implement a visitor pattern:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface ExprVisitor {
|
|
||||||
visit(expr: Expr): void
|
|
||||||
}
|
|
||||||
|
|
||||||
class Expr {
|
|
||||||
// ...
|
|
||||||
visit(v: ExprVisitor) {
|
|
||||||
v.visit(this);
|
|
||||||
const k = this.kind;
|
|
||||||
switch (k.tag) {
|
|
||||||
case "Int":
|
|
||||||
return;
|
|
||||||
case "Add":
|
|
||||||
k.left.visit(v);
|
|
||||||
k.right.visit(v);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const _: never = k;
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The visitor interface can be used like:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const expr: Expr = parse("2 + (3 + 4)");
|
|
||||||
|
|
||||||
const intValues: number[] = [];
|
|
||||||
|
|
||||||
expr.visit({
|
|
||||||
visit(expr) {
|
|
||||||
if (expr.is("Int")) {
|
|
||||||
intValues.push(expr.kind.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
@ -61,9 +61,6 @@ export class Checker {
|
|||||||
if (sym.tag === "Fn") {
|
if (sym.tag === "Fn") {
|
||||||
return this.check(sym.stmt);
|
return this.check(sym.stmt);
|
||||||
}
|
}
|
||||||
if (sym.tag === "Bool") {
|
|
||||||
return Ty.Bool;
|
|
||||||
}
|
|
||||||
if (sym.tag === "Builtin") {
|
if (sym.tag === "Builtin") {
|
||||||
return builtins.find((s) => s.id === sym.id)!.ty;
|
return builtins.find((s) => s.id === sym.id)!.ty;
|
||||||
}
|
}
|
||||||
@ -109,8 +106,6 @@ export class Checker {
|
|||||||
return Ty.Void;
|
return Ty.Void;
|
||||||
case "int":
|
case "int":
|
||||||
return Ty.Int;
|
return Ty.Int;
|
||||||
case "bool":
|
|
||||||
return Ty.Bool;
|
|
||||||
default:
|
default:
|
||||||
this.error(node.line, `unknown type '${node.kind.ident}'`);
|
this.error(node.line, `unknown type '${node.kind.ident}'`);
|
||||||
}
|
}
|
||||||
@ -153,9 +148,9 @@ export class Checker {
|
|||||||
private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty {
|
private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty {
|
||||||
const calleeTy = this.check(node.kind.expr);
|
const calleeTy = this.check(node.kind.expr);
|
||||||
|
|
||||||
const callableTy = calleeTy.is("Fn")
|
const callableTy = calleeTy.isKind("Fn")
|
||||||
? calleeTy
|
? calleeTy
|
||||||
: calleeTy.is("FnStmt")
|
: calleeTy.isKind("FnStmt")
|
||||||
? calleeTy.kind.ty as Ty & { kind: { tag: "Fn" } }
|
? calleeTy.kind.ty as Ty & { kind: { tag: "Fn" } }
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@ -175,7 +170,7 @@ export class Checker {
|
|||||||
node.line,
|
node.line,
|
||||||
`incorrect amount of arguments. got ${args.length} expected ${params.length}`,
|
`incorrect amount of arguments. got ${args.length} expected ${params.length}`,
|
||||||
);
|
);
|
||||||
if (calleeTy.is("FnStmt")) {
|
if (calleeTy.isKind("FnStmt")) {
|
||||||
this.info(
|
this.info(
|
||||||
calleeTy.kind.stmt.line,
|
calleeTy.kind.stmt.line,
|
||||||
"function defined here",
|
"function defined here",
|
||||||
@ -191,7 +186,7 @@ export class Checker {
|
|||||||
params[i]
|
params[i]
|
||||||
}', for argument ${i}`,
|
}', for argument ${i}`,
|
||||||
);
|
);
|
||||||
if (calleeTy.is("FnStmt")) {
|
if (calleeTy.isKind("FnStmt")) {
|
||||||
this.info(
|
this.info(
|
||||||
calleeTy.kind.stmt.kind.params[i].line,
|
calleeTy.kind.stmt.kind.params[i].line,
|
||||||
`parameter '${
|
`parameter '${
|
||||||
@ -251,14 +246,4 @@ type BinaryOpPattern = {
|
|||||||
const binaryOpPatterns: BinaryOpPattern[] = [
|
const binaryOpPatterns: BinaryOpPattern[] = [
|
||||||
{ op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
{ op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
||||||
{ op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
{ op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
||||||
{ op: "Multiply", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
|
||||||
{ op: "Divide", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
|
||||||
{ op: "Remainder", left: Ty.Int, right: Ty.Int, result: Ty.Int },
|
|
||||||
|
|
||||||
{ op: "Eq", left: Ty.Int, right: Ty.Int, result: Ty.Bool },
|
|
||||||
{ op: "Ne", left: Ty.Int, right: Ty.Int, result: Ty.Bool },
|
|
||||||
{ op: "Lt", left: Ty.Int, right: Ty.Int, result: Ty.Bool },
|
|
||||||
{ op: "Gt", left: Ty.Int, right: Ty.Int, result: Ty.Bool },
|
|
||||||
{ op: "Lte", left: Ty.Int, right: Ty.Int, result: Ty.Bool },
|
|
||||||
{ op: "Gte", left: Ty.Int, right: Ty.Int, result: Ty.Bool },
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import * as ast from "../ast.ts";
|
import * as ast from "../ast.ts";
|
||||||
import { printDiagnostics } from "../diagnostics.ts";
|
import { printDiagnostics } from "../diagnostics.ts";
|
||||||
|
import { Ty } from "../ty.ts";
|
||||||
import { builtins } from "./builtins.ts";
|
import { builtins } from "./builtins.ts";
|
||||||
|
|
||||||
export class ResolveMap {
|
export class ResolveMap {
|
||||||
@ -17,7 +18,6 @@ export class ResolveMap {
|
|||||||
|
|
||||||
export type Sym =
|
export type Sym =
|
||||||
| { tag: "Error" }
|
| { tag: "Error" }
|
||||||
| { tag: "Bool"; value: boolean }
|
|
||||||
| { tag: "Builtin"; id: string }
|
| { tag: "Builtin"; id: string }
|
||||||
| { tag: "Fn"; stmt: ast.NodeWithKind<"FnStmt"> }
|
| { tag: "Fn"; stmt: ast.NodeWithKind<"FnStmt"> }
|
||||||
| {
|
| {
|
||||||
@ -79,7 +79,7 @@ export function resolve(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (k.tag === "IdentExpr") {
|
if (k.tag === "IdentExpr") {
|
||||||
const sym = syms.resolveExpr(k.ident);
|
const sym = syms.resolve(k.ident);
|
||||||
if (sym === null) {
|
if (sym === null) {
|
||||||
printDiagnostics(
|
printDiagnostics(
|
||||||
filename,
|
filename,
|
||||||
@ -126,16 +126,12 @@ class ResolverSyms {
|
|||||||
this.syms.set(ident, sym);
|
this.syms.set(ident, sym);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveExpr(ident: string): Sym | null {
|
resolve(ident: string): Sym | null {
|
||||||
if (ident === "false" || ident === "true") {
|
|
||||||
return { tag: "Bool", value: ident === "true" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.syms.has(ident)) {
|
if (this.syms.has(ident)) {
|
||||||
return this.syms.get(ident)!;
|
return this.syms.get(ident)!;
|
||||||
}
|
}
|
||||||
if (this.parent) {
|
if (this.parent) {
|
||||||
return this.parent.resolveExpr(ident);
|
return this.parent.resolve(ident);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,9 +112,6 @@ class FnLowerer {
|
|||||||
}
|
}
|
||||||
return this.pushInst(local.ty, "LocalLoad", { source: local });
|
return this.pushInst(local.ty, "LocalLoad", { source: local });
|
||||||
}
|
}
|
||||||
if (sym.tag === "Bool") {
|
|
||||||
return this.pushInst(Ty.Bool, "Bool", { value: sym.value });
|
|
||||||
}
|
|
||||||
throw new Error(`'${sym.tag}' not handled`);
|
throw new Error(`'${sym.tag}' not handled`);
|
||||||
}
|
}
|
||||||
if (expr.is("IntExpr")) {
|
if (expr.is("IntExpr")) {
|
||||||
@ -143,24 +140,17 @@ class FnLowerer {
|
|||||||
return this.pushInst(ty, "Call", { callee, args });
|
return this.pushInst(ty, "Call", { callee, args });
|
||||||
}
|
}
|
||||||
if (expr.is("BinaryExpr")) {
|
if (expr.is("BinaryExpr")) {
|
||||||
const resultTy = this.checker.check(expr);
|
const ty = this.checker.check(expr);
|
||||||
const leftTy = this.checker.check(expr.kind.left);
|
|
||||||
const rightTy = this.checker.check(expr.kind.right);
|
|
||||||
const binaryOp = binaryOpPatterns
|
const binaryOp = binaryOpPatterns
|
||||||
.find((pat) =>
|
.find((pat) =>
|
||||||
expr.kind.op === pat.op &&
|
expr.kind.op === pat.op && ty.compatibleWith(pat.ty)
|
||||||
resultTy.compatibleWith(pat.result) &&
|
|
||||||
leftTy.compatibleWith(pat.left ?? pat.result) &&
|
|
||||||
rightTy.compatibleWith(pat.right ?? pat.left ?? pat.result)
|
|
||||||
);
|
);
|
||||||
if (!binaryOp) {
|
if (!binaryOp) {
|
||||||
throw new Error(
|
throw new Error();
|
||||||
`'${expr.kind.op}' with '${resultTy.pretty()}' not handled`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const left = this.lowerExpr(expr.kind.left);
|
const left = this.lowerExpr(expr.kind.left);
|
||||||
const right = this.lowerExpr(expr.kind.right);
|
const right = this.lowerExpr(expr.kind.right);
|
||||||
return this.pushInst(resultTy, binaryOp.tag, { left, right });
|
return this.pushInst(ty, binaryOp.tag, { left, right });
|
||||||
}
|
}
|
||||||
throw new Error(`'${expr.kind.tag}' not handled`);
|
throw new Error(`'${expr.kind.tag}' not handled`);
|
||||||
}
|
}
|
||||||
@ -184,25 +174,13 @@ class FnLowerer {
|
|||||||
|
|
||||||
type BinaryOpPattern = {
|
type BinaryOpPattern = {
|
||||||
op: ast.BinaryOp;
|
op: ast.BinaryOp;
|
||||||
|
ty: Ty;
|
||||||
tag: BinaryOp;
|
tag: BinaryOp;
|
||||||
result: Ty;
|
|
||||||
left?: Ty;
|
|
||||||
right?: Ty;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const binaryOpPatterns: BinaryOpPattern[] = [
|
const binaryOpPatterns: BinaryOpPattern[] = [
|
||||||
{ op: "Add", tag: "Add", result: Ty.Int, left: Ty.Int },
|
{ op: "Add", ty: Ty.Int, tag: "Add" },
|
||||||
{ op: "Subtract", tag: "Sub", result: Ty.Int, left: Ty.Int },
|
{ op: "Subtract", ty: Ty.Int, tag: "Sub" },
|
||||||
{ op: "Multiply", tag: "Mul", result: Ty.Int, left: Ty.Int },
|
|
||||||
{ op: "Divide", tag: "Div", result: Ty.Int, left: Ty.Int },
|
|
||||||
{ op: "Remainder", tag: "Rem", result: Ty.Int },
|
|
||||||
|
|
||||||
{ op: "Eq", tag: "Eq", result: Ty.Bool, left: Ty.Int },
|
|
||||||
{ op: "Ne", tag: "Ne", result: Ty.Bool, left: Ty.Int },
|
|
||||||
{ op: "Lt", tag: "Lt", result: Ty.Bool, left: Ty.Int },
|
|
||||||
{ op: "Gt", tag: "Gt", result: Ty.Bool, left: Ty.Int },
|
|
||||||
{ op: "Lte", tag: "Lte", result: Ty.Bool, left: Ty.Int },
|
|
||||||
{ op: "Gte", tag: "Gte", result: Ty.Bool, left: Ty.Int },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export class Fn {
|
export class Fn {
|
||||||
@ -213,7 +191,7 @@ export class Fn {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
pretty(): string {
|
pretty(): string {
|
||||||
const fnTy = this.ty.is("FnStmt") && this.ty.kind.ty.is("Fn")
|
const fnTy = this.ty.isKind("FnStmt") && this.ty.kind.ty.isKind("Fn")
|
||||||
? this.ty.kind.ty
|
? this.ty.kind.ty
|
||||||
: null;
|
: null;
|
||||||
if (!fnTy) {
|
if (!fnTy) {
|
||||||
@ -289,7 +267,6 @@ export class Inst {
|
|||||||
case "Void":
|
case "Void":
|
||||||
return "";
|
return "";
|
||||||
case "Int":
|
case "Int":
|
||||||
case "Bool":
|
|
||||||
return ` ${k.value}`;
|
return ` ${k.value}`;
|
||||||
case "Fn":
|
case "Fn":
|
||||||
return ` ${k.fn.stmt.kind.ident}`;
|
return ` ${k.fn.stmt.kind.ident}`;
|
||||||
@ -335,7 +312,6 @@ export type InstKind =
|
|||||||
| { tag: "Error" }
|
| { tag: "Error" }
|
||||||
| { tag: "Void" }
|
| { tag: "Void" }
|
||||||
| { tag: "Int"; value: number }
|
| { tag: "Int"; value: number }
|
||||||
| { tag: "Bool"; value: boolean }
|
|
||||||
| { tag: "Fn"; fn: Fn }
|
| { tag: "Fn"; fn: Fn }
|
||||||
| { tag: "Param"; idx: number }
|
| { tag: "Param"; idx: number }
|
||||||
| { tag: "Call"; callee: Inst; args: Inst[] }
|
| { tag: "Call"; callee: Inst; args: Inst[] }
|
||||||
|
|||||||
@ -20,7 +20,6 @@ export class MirInterpreter {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
case "Void":
|
case "Void":
|
||||||
case "Int":
|
case "Int":
|
||||||
case "Bool":
|
|
||||||
case "Fn":
|
case "Fn":
|
||||||
regs.set(inst, new Val(k));
|
regs.set(inst, new Val(k));
|
||||||
continue;
|
continue;
|
||||||
@ -80,28 +79,14 @@ export class MirInterpreter {
|
|||||||
const rk = right.kind;
|
const rk = right.kind;
|
||||||
|
|
||||||
if (lk.tag === "Int" && rk.tag === "Int") {
|
if (lk.tag === "Int" && rk.tag === "Int") {
|
||||||
const left = lk.value;
|
|
||||||
const right = lk.value;
|
|
||||||
|
|
||||||
const value = (() => {
|
const value = (() => {
|
||||||
const Int = (value: number) =>
|
|
||||||
new Val({ tag: "Int", value });
|
|
||||||
const Bool = (value: boolean) =>
|
|
||||||
new Val({ tag: "Bool", value });
|
|
||||||
|
|
||||||
switch (k.tag) {
|
switch (k.tag) {
|
||||||
case "Eq":
|
case "Eq":
|
||||||
return Bool(left === right);
|
|
||||||
case "Ne":
|
case "Ne":
|
||||||
return Bool(left !== right);
|
|
||||||
case "Lt":
|
case "Lt":
|
||||||
return Bool(left < right);
|
|
||||||
case "Gt":
|
case "Gt":
|
||||||
return Bool(left > right);
|
|
||||||
case "Lte":
|
case "Lte":
|
||||||
return Bool(left <= right);
|
|
||||||
case "Gte":
|
case "Gte":
|
||||||
return Bool(left >= right);
|
|
||||||
case "BitOr":
|
case "BitOr":
|
||||||
case "BitXor":
|
case "BitXor":
|
||||||
case "BitAnd":
|
case "BitAnd":
|
||||||
@ -109,19 +94,17 @@ export class MirInterpreter {
|
|||||||
case "Shr":
|
case "Shr":
|
||||||
break;
|
break;
|
||||||
case "Add":
|
case "Add":
|
||||||
return Int(left + right);
|
return lk.value + rk.value;
|
||||||
case "Sub":
|
case "Sub":
|
||||||
return Int(left - right);
|
return lk.value - rk.value;
|
||||||
case "Mul":
|
case "Mul":
|
||||||
return Int(left * right);
|
|
||||||
case "Div":
|
case "Div":
|
||||||
return Int(Math.floor(left / right));
|
|
||||||
case "Rem":
|
case "Rem":
|
||||||
return Int(left % right);
|
break;
|
||||||
}
|
}
|
||||||
throw new Error(`'${k.tag}' not handled`);
|
throw new Error(`'${k.tag}' not handled`);
|
||||||
})();
|
})();
|
||||||
regs.set(inst, value);
|
regs.set(inst, new Val({ tag: "Int", value }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw new Error(`'${k.tag}' not handled`);
|
throw new Error(`'${k.tag}' not handled`);
|
||||||
@ -153,7 +136,6 @@ class Val {
|
|||||||
case "Void":
|
case "Void":
|
||||||
return "void";
|
return "void";
|
||||||
case "Int":
|
case "Int":
|
||||||
case "Bool":
|
|
||||||
return `${k.value}`;
|
return `${k.value}`;
|
||||||
case "Fn":
|
case "Fn":
|
||||||
return `<${k.fn.ty.pretty()}>`;
|
return `<${k.fn.ty.pretty()}>`;
|
||||||
@ -165,5 +147,4 @@ class Val {
|
|||||||
type ValKind =
|
type ValKind =
|
||||||
| { tag: "Void" }
|
| { tag: "Void" }
|
||||||
| { tag: "Int"; value: number }
|
| { tag: "Int"; value: number }
|
||||||
| { tag: "Bool"; value: boolean }
|
|
||||||
| { tag: "Fn"; fn: mir.Fn };
|
| { tag: "Fn"; fn: mir.Fn };
|
||||||
|
|||||||
40
src/ty.ts
40
src/ty.ts
@ -24,7 +24,6 @@ export class Ty {
|
|||||||
static Error = Ty.create("Error", {});
|
static Error = Ty.create("Error", {});
|
||||||
static Void = Ty.create("Void", {});
|
static Void = Ty.create("Void", {});
|
||||||
static Int = Ty.create("Int", {});
|
static Int = Ty.create("Int", {});
|
||||||
static Bool = Ty.create("Bool", {});
|
|
||||||
|
|
||||||
private internHash(): string {
|
private internHash(): string {
|
||||||
return JSON.stringify(this.kind);
|
return JSON.stringify(this.kind);
|
||||||
@ -35,27 +34,24 @@ export class Ty {
|
|||||||
public kind: TyKind,
|
public kind: TyKind,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
is<
|
isKind<
|
||||||
Tag extends TyKind["tag"],
|
Tag extends TyKind["tag"],
|
||||||
>(tag: Tag): this is Ty & { kind: { tag: Tag } } {
|
>(tag: Tag): this is Ty & { kind: { tag: Tag } } {
|
||||||
return this.kind.tag === tag;
|
return this.kind.tag === tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
compatibleWith(other: Ty): boolean {
|
compatibleWith(other: Ty): boolean {
|
||||||
if (this.is("Error")) {
|
if (this.isKind("Error")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.is("Void")) {
|
if (this.isKind("Void")) {
|
||||||
return other.is("Void");
|
return other.isKind("Void");
|
||||||
}
|
}
|
||||||
if (this.is("Int")) {
|
if (this.isKind("Int")) {
|
||||||
return other.is("Int");
|
return other.isKind("Int");
|
||||||
}
|
}
|
||||||
if (this.is("Bool")) {
|
if (this.isKind("Fn")) {
|
||||||
return other.is("Bool");
|
if (!other.isKind("Fn")) {
|
||||||
}
|
|
||||||
if (this.is("Fn")) {
|
|
||||||
if (!other.is("Fn")) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const i of this.kind.params.keys()) {
|
for (const i of this.kind.params.keys()) {
|
||||||
@ -68,8 +64,8 @@ export class Ty {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.is("FnStmt")) {
|
if (this.isKind("FnStmt")) {
|
||||||
if (!other.is("FnStmt")) {
|
if (!other.isKind("FnStmt")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.kind.ty.compatibleWith(other.kind.ty)) {
|
if (!this.kind.ty.compatibleWith(other.kind.ty)) {
|
||||||
@ -85,25 +81,22 @@ export class Ty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pretty(): string {
|
pretty(): string {
|
||||||
if (this.is("Error")) {
|
if (this.isKind("Error")) {
|
||||||
return "<error>";
|
return "<error>";
|
||||||
}
|
}
|
||||||
if (this.is("Void")) {
|
if (this.isKind("Void")) {
|
||||||
return "void";
|
return "void";
|
||||||
}
|
}
|
||||||
if (this.is("Int")) {
|
if (this.isKind("Int")) {
|
||||||
return "int";
|
return "int";
|
||||||
}
|
}
|
||||||
if (this.is("Bool")) {
|
if (this.isKind("Fn")) {
|
||||||
return "bool";
|
|
||||||
}
|
|
||||||
if (this.is("Fn")) {
|
|
||||||
return `fn (${
|
return `fn (${
|
||||||
this.kind.params.map((param) => param.pretty()).join(", ")
|
this.kind.params.map((param) => param.pretty()).join(", ")
|
||||||
}) -> ${this.kind.retTy.pretty()}`;
|
}) -> ${this.kind.retTy.pretty()}`;
|
||||||
}
|
}
|
||||||
if (this.is("FnStmt")) {
|
if (this.isKind("FnStmt")) {
|
||||||
if (!this.kind.ty.is("Fn")) throw new Error();
|
if (!this.kind.ty.isKind("Fn")) throw new Error();
|
||||||
return `fn ${this.kind.stmt.kind.ident}(${
|
return `fn ${this.kind.stmt.kind.ident}(${
|
||||||
this.kind.ty.kind.params.map((param) => param.pretty()).join(
|
this.kind.ty.kind.params.map((param) => param.pretty()).join(
|
||||||
", ",
|
", ",
|
||||||
@ -118,6 +111,5 @@ export type TyKind =
|
|||||||
| { tag: "Error" }
|
| { tag: "Error" }
|
||||||
| { tag: "Void" }
|
| { tag: "Void" }
|
||||||
| { tag: "Int" }
|
| { tag: "Int" }
|
||||||
| { tag: "Bool" }
|
|
||||||
| { tag: "Fn"; params: Ty[]; retTy: Ty }
|
| { tag: "Fn"; params: Ty[]; retTy: Ty }
|
||||||
| { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> };
|
| { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> };
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
fn main()
|
|
||||||
{
|
|
||||||
let my_bool = false;
|
|
||||||
let my_other_bool: bool = true;
|
|
||||||
|
|
||||||
let cond: bool = 1 == 2;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,5 @@
|
|||||||
// expect: 8
|
// expect: 8
|
||||||
// expect: 2
|
// expect: 2
|
||||||
// expect: 15
|
|
||||||
// expect: 7
|
|
||||||
// expect: 2
|
|
||||||
|
|
||||||
fn main()
|
fn main()
|
||||||
{
|
{
|
||||||
@ -11,8 +8,5 @@ fn main()
|
|||||||
|
|
||||||
print_int(a + b);
|
print_int(a + b);
|
||||||
print_int(a - b);
|
print_int(a - b);
|
||||||
print_int(a * b);
|
|
||||||
print_int(a * b / 2);
|
|
||||||
print_int(a % b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,12 +10,11 @@ TEST_SRC=$(fd '\.ethlang' $TEST_DIR)
|
|||||||
count_total=0
|
count_total=0
|
||||||
count_succeeded=0
|
count_succeeded=0
|
||||||
|
|
||||||
run_test_file() {
|
for test_file in $TEST_SRC
|
||||||
local file=$1
|
do
|
||||||
|
echo "- $(basename $test_file)"
|
||||||
echo "- $(basename $file)"
|
|
||||||
set +e
|
set +e
|
||||||
output=$(deno run -A $SRC_DIR/main.ts $file --test)
|
output=$(deno run -A $SRC_DIR/main.ts $test_file --test)
|
||||||
status=$?
|
status=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@ -27,9 +26,9 @@ run_test_file() {
|
|||||||
|
|
||||||
if [[ status -eq 0 ]]
|
if [[ status -eq 0 ]]
|
||||||
then
|
then
|
||||||
if grep -q '// expect:' $file
|
if grep -q '// expect:' $test_file
|
||||||
then
|
then
|
||||||
expected=$(grep '// expect:' $file | sed -E 's/\/\/ expect: (.*?)/\1/g')
|
expected=$(grep '// expect:' $test_file | sed -E 's/\/\/ expect: (.*?)/\1/g')
|
||||||
if [[ $output != $expected ]]
|
if [[ $output != $expected ]]
|
||||||
then
|
then
|
||||||
echo "-- failed: incorrect output --"
|
echo "-- failed: incorrect output --"
|
||||||
@ -46,25 +45,17 @@ run_test_file() {
|
|||||||
if [[ status -eq 0 ]]
|
if [[ status -eq 0 ]]
|
||||||
then
|
then
|
||||||
count_succeeded=$(($count_succeeded + 1))
|
count_succeeded=$(($count_succeeded + 1))
|
||||||
|
else
|
||||||
|
echo "failed"
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
if [[ $1 == "" ]]
|
done
|
||||||
then
|
|
||||||
for file in $TEST_SRC
|
|
||||||
do
|
|
||||||
run_test_file $file
|
|
||||||
done
|
|
||||||
else
|
|
||||||
run_test_file $1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $count_succeeded -eq $count_total ]]
|
if [[ $count_succeeded -eq $count_total ]]
|
||||||
then
|
then
|
||||||
echo "== all tests passed ($count_succeeded/$count_total passed) =="
|
echo "=== all tests passed ($count_succeeded/$count_total passed) ==="
|
||||||
else
|
else
|
||||||
echo "== tests failed ($count_succeeded/$count_total passed) =="
|
echo "=== tests failed ($count_succeeded/$count_total passed) ==="
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user