generics work

This commit is contained in:
sfja 2024-12-26 01:51:05 +01:00
parent f712b0f3a5
commit cab2c9baa3
11 changed files with 1051 additions and 44 deletions

View File

@ -125,6 +125,7 @@ export type ETypeKind =
| { type: "struct"; fields: Param[] };
export type GenericParam = {
id: number;
ident: string;
pos: Pos;
vtype?: VType;

View File

@ -2,6 +2,8 @@ import { EType, Expr, Stmt } from "./ast.ts";
import { printStackTrace, Reporter } from "./info.ts";
import { Pos } from "./token.ts";
import {
extractGenericType,
GenericArgsMap,
VType,
VTypeGenericParam,
VTypeParam,
@ -34,7 +36,8 @@ export class Checker {
if (stmt.kind.genericParams !== undefined) {
genericParams = [];
for (const etypeParam of stmt.kind.genericParams) {
genericParams.push({ ident: etypeParam.ident });
const id = genericParams.length;
genericParams.push({ id, ident: etypeParam.ident });
}
}
const params: VTypeParam[] = [];
@ -47,7 +50,13 @@ export class Checker {
param.vtype = vtype;
params.push({ ident: param.ident, vtype });
}
stmt.kind.vtype = { type: "fn", genericParams, params, returnType };
stmt.kind.vtype = {
type: "fn",
genericParams,
params,
returnType,
stmtId: stmt.id,
};
}
}
@ -336,8 +345,7 @@ export class Checker {
if (vtype.type !== "fn") {
throw new Error();
}
const { params, returnType } = vtype;
return { type: "fn", params, returnType };
return vtype;
}
case "fn_param":
return expr.kind.sym.param.vtype!;
@ -399,9 +407,10 @@ export class Checker {
}
const pos = expr.pos;
const subject = this.checkExpr(expr.kind.subject);
if (subject.type !== "fn") {
this.report("cannot call non-fn", pos);
return { type: "error" };
if (subject.type === "error") return subject;
if (subject.type === "fn") {
if (subject.genericParams !== undefined) {
throw new Error("😭😭😭");
}
const args = expr.kind.args.map((arg) => this.checkExpr(arg));
if (args.length !== subject.params.length) {
@ -415,7 +424,9 @@ export class Checker {
if (!vtypesEqual(args[i], subject.params[i].vtype)) {
this.report(
`incorrect argument ${i} '${subject.params[i].ident}'` +
`, expected ${vtypeToString(subject.params[i].vtype)}` +
`, expected ${
vtypeToString(subject.params[i].vtype)
}` +
`, got ${vtypeToString(args[i])}`,
pos,
);
@ -424,6 +435,48 @@ export class Checker {
}
return subject.returnType;
}
if (subject.type === "generic_spec" && subject.subject.type === "fn") {
const inner = subject.subject;
const params = inner.params;
const args = expr.kind.args.map((arg) => this.checkExpr(arg));
if (args.length !== params.length) {
this.report(
`incorrect number of arguments` +
`, expected ${params.length}`,
pos,
);
}
for (let i = 0; i < args.length; ++i) {
const vtypeCompatible = vtypesEqual(
args[i],
params[i].vtype,
subject.genericArgs,
);
if (!vtypeCompatible) {
this.report(
`incorrect argument ${i} '${inner.params[i].ident}'` +
`, expected ${
vtypeToString(
extractGenericType(
params[i].vtype,
subject.genericArgs,
),
)
}` +
`, got ${vtypeToString(args[i])}`,
pos,
);
break;
}
}
return extractGenericType(
subject.subject.returnType,
subject.genericArgs,
);
}
this.report("cannot call non-fn", pos);
return { type: "error" };
}
public checkPathExpr(expr: Expr): VType {
if (expr.kind.type !== "path") {
@ -445,20 +498,23 @@ export class Checker {
);
return { type: "error" };
}
const genericParams = expr.kind.etypeArgs.map((arg) =>
this.checkEType(arg)
);
if (genericParams.length !== subject.params.length) {
const args = expr.kind.etypeArgs;
if (args.length !== subject.params.length) {
this.report(
`incorrect number of arguments` +
`, expected ${subject.params.length}`,
pos,
);
}
const genericArgs: GenericArgsMap = {};
for (let i = 0; i < args.length; ++i) {
const etype = this.checkEType(args[i]);
genericArgs[subject.genericParams[i].id] = etype;
}
return {
type: "generic_spec",
subject,
genericParams,
genericArgs,
};
}
@ -491,7 +547,9 @@ export class Checker {
}
const pos = expr.pos;
const left = this.checkExpr(expr.kind.left);
if (left.type === "error") return left;
const right = this.checkExpr(expr.kind.right);
if (right.type === "error") return right;
for (const operation of simpleBinaryOperations) {
if (operation.binaryType !== expr.kind.binaryType) {
continue;
@ -520,10 +578,13 @@ export class Checker {
}
const pos = expr.pos;
const cond = this.checkExpr(expr.kind.cond);
if (cond.type === "error") return cond;
const truthy = this.checkExpr(expr.kind.truthy);
if (truthy.type === "error") return truthy;
const falsy = expr.kind.falsy
? this.checkExpr(expr.kind.falsy)
: undefined;
if (falsy?.type === "error") return falsy;
if (cond.type !== "bool") {
this.report(
`if condition should be 'bool', got '${vtypeToString(cond)}'`,
@ -626,7 +687,8 @@ export class Checker {
}
if (etype.kind.type === "sym") {
if (etype.kind.sym.type === "generic") {
return { type: "generic" };
const { id, ident } = etype.kind.sym.genericParam;
return { type: "generic", param: { id, ident } };
}
this.report(`sym type '${etype.kind.sym.type}' used as type`, pos);
return { type: "error" };

View File

@ -5,6 +5,8 @@ import { SpecialLoopDesugarer } from "./desugar/special_loop.ts";
import { Reporter } from "./info.ts";
import { Lexer } from "./lexer.ts";
import { FnNamesMap, Lowerer } from "./lowerer.ts";
import { Monomorphizer } from "./mono.ts";
import { MonoLowerer } from "./mono_lower.ts";
import { Parser } from "./parser.ts";
import { Resolver } from "./resolver.ts";
@ -45,10 +47,16 @@ export class Compiler {
Deno.exit(1);
}
const lowerer = new Lowerer(lexer.currentPos());
lowerer.lower(ast);
// lowerer.printProgram();
const { program, fnNames } = lowerer.finish();
const { monoFns, callMap } = new Monomorphizer(ast).monomorphize();
//const lowerer = new Lowerer(lexer.currentPos());
//lowerer.lower(ast);
//// lowerer.printProgram();
//const { program, fnNames } = lowerer.finish();
const lowerer = new MonoLowerer(monoFns, callMap, lexer.currentPos());
const { program, fnNames } = lowerer.lower();
lowerer.printProgram();
return { program, fnNames };
}

View File

@ -42,7 +42,7 @@ export function printStackTrace() {
}
}
try {
//throw new ReportNotAnError();
throw new ReportNotAnError();
} catch (error) {
if (!(error instanceof ReportNotAnError)) {
throw error;

View File

@ -257,6 +257,8 @@ export class Lowerer {
return this.lowerIndexExpr(expr);
case "call":
return this.lowerCallExpr(expr);
case "etype_args":
return this.lowerETypeArgsExpr(expr);
case "unary":
return this.lowerUnaryExpr(expr);
case "binary":
@ -465,6 +467,13 @@ export class Lowerer {
this.program.add(Ops.Call, expr.kind.args.length);
}
private lowerETypeArgsExpr(expr: Expr) {
if (expr.kind.type !== "etype_args") {
throw new Error();
}
throw new Error("not implemented");
}
private lowerIfExpr(expr: Expr) {
if (expr.kind.type !== "if") {
throw new Error();

262
compiler/mono.ts Normal file
View File

@ -0,0 +1,262 @@
import { Expr, Stmt } from "./ast.ts";
import { AstVisitor, visitExpr, VisitRes, visitStmts } from "./ast_visitor.ts";
import { GenericArgsMap, VType } from "./vtype.ts";
export class Monomorphizer {
private fns: MonoFnsMap = {};
private callMap: MonoCallNameGenMap = {};
private allFns: Map<number, Stmt>;
private entryFn: Stmt;
constructor(private ast: Stmt[]) {
this.allFns = new AllFnsCollector().collect(this.ast);
this.entryFn = findMain(this.allFns);
}
public monomorphize(): MonoResult {
this.monomorphizeFn(this.entryFn);
return { monoFns: this.fns, callMap: this.callMap };
}
private monomorphizeFn(
stmt: Stmt,
genericArgs?: GenericArgsMap,
): MonoFn {
const nameGen = monoFnNameGen(stmt, genericArgs);
if (nameGen in this.fns) {
return this.fns[nameGen];
}
const monoFn = { nameGen, stmt, genericArgs };
this.fns[nameGen] = monoFn;
const calls = new CallCollector().collect(stmt);
for (const call of calls) {
this.callMap[call.id] = nameGen;
if (call.kind.type !== "call") {
throw new Error();
}
if (call.kind.subject.vtype?.type === "fn") {
const fn = this.allFns.get(call.kind.subject.vtype.stmtId);
if (fn === undefined) {
throw new Error();
}
const monoFn = this.monomorphizeFn(fn);
this.callMap[call.id] = monoFn.nameGen;
continue;
}
if (call.kind.subject.vtype?.type === "generic_spec") {
const genericSpecType = call.kind.subject.vtype!;
if (genericSpecType.subject.type !== "fn") {
throw new Error();
}
const fnType = genericSpecType.subject;
const monoArgs: GenericArgsMap = {};
for (const key in genericSpecType.genericArgs) {
const vtype = genericSpecType.genericArgs[key];
if (vtype.type === "generic") {
if (genericArgs === undefined) {
throw new Error();
}
monoArgs[key] = genericArgs[vtype.param.id];
} else {
monoArgs[key] = vtype;
}
}
const fn = this.allFns.get(fnType.stmtId);
if (fn === undefined) {
throw new Error();
}
const monoFn = this.monomorphizeFn(fn, monoArgs);
this.callMap[call.id] = monoFn.nameGen;
continue;
}
throw new Error();
}
return monoFn;
}
}
export type MonoResult = {
monoFns: MonoFnsMap;
callMap: MonoCallNameGenMap;
};
export type MonoFnsMap = { [nameGen: string]: MonoFn };
export type MonoFn = {
nameGen: string;
stmt: Stmt;
genericArgs?: GenericArgsMap;
};
export type MonoCallNameGenMap = { [exprId: number]: string };
function monoFnNameGen(stmt: Stmt, genericArgs?: GenericArgsMap): string {
if (stmt.kind.type !== "fn") {
throw new Error();
}
if (stmt.kind.ident === "main") {
return "main";
}
if (genericArgs === undefined) {
return `${stmt.kind.ident}_${stmt.id}`;
}
const args = Object.values(genericArgs)
.map((arg) => vtypeNameGenPart(arg))
.join("_");
return `${stmt.kind.ident}_${stmt.id}_${args}`;
}
function vtypeNameGenPart(vtype: VType): string {
switch (vtype.type) {
case "error":
throw new Error("error in type");
case "string":
case "int":
case "bool":
case "null":
case "unknown":
return vtype.type;
case "array":
return `[${vtypeNameGenPart(vtype.inner)}]`;
case "struct": {
const fields = vtype.fields
.map((field) =>
`${field.ident}, ${vtypeNameGenPart(field.vtype)}`
)
.join(", ");
return `struct { ${fields} }`;
}
case "fn":
return `fn(${vtype.stmtId})`;
case "generic":
case "generic_spec":
throw new Error("cannot be monomorphized");
}
}
class AllFnsCollector implements AstVisitor {
private allFns = new Map<number, Stmt>();
public collect(ast: Stmt[]): Map<number, Stmt> {
visitStmts(ast, this);
return this.allFns;
}
visitFnStmt(stmt: Stmt): VisitRes {
if (stmt.kind.type !== "fn") {
throw new Error();
}
this.allFns.set(stmt.id, stmt);
}
}
function findMain(fns: Map<number, Stmt>): Stmt {
const mainId = fns.values().find((stmt) =>
stmt.kind.type === "fn" && stmt.kind.ident === "main"
);
if (mainId === undefined) {
console.error("error: cannot find function 'main'");
console.error(apology);
throw new Error("cannot find function 'main'");
}
return mainId;
}
class CallCollector implements AstVisitor {
private calls: Expr[] = [];
public collect(fn: Stmt): Expr[] {
if (fn.kind.type !== "fn") {
throw new Error();
}
visitExpr(fn.kind.body, this);
return this.calls;
}
visitFnStmt(_stmt: Stmt): VisitRes {
return "stop";
}
visitCallExpr(expr: Expr): VisitRes {
if (expr.kind.type !== "call") {
throw new Error();
}
this.calls.push(expr);
}
}
const apology = `
Hear me out. Monomorphization, meaning the process
inwich generic functions are stamped out into seperate
specialized functions is actually really hard, and I
have a really hard time right now, figuring out, how
to do it in a smart way. To really explain it, let's
imagine you have a function, you defined as a<T>().
For each call with seperate generics arguments given,
such as a::<int>() and a::<string>(), a specialized
function has to be 'stamped out', ie. created and put
into the compilation with the rest of the program. Now
to the reason as to why 'main' is needed. To do the
monomorphization, we have to do it recursively. To
explain this, imagine you have a generic function a<T>
and inside the body of a<T>, you call another generic
function such as b<T> with the same generic type. This
means that the monomorphization process of b<T> depends
on the monomorphization of a<T>. What this essentially
means, is that the monomorphization process works on
the program as a call graph, meaning a graph or tree
structure where each represents a function call to
either another function or a recursive call to the
function itself. But a problem arises from doing it
this way, which is that a call graph will need an
entrypoint. The language, as it is currently, does
not really require a 'main'-function. Or maybe it
does, but that's beside the point. The point is that
we need a main function, to be the entry point for
the call graph. The monomorphization process then
runs through the program from that entry point. This
means that each function we call, will itself be
monomorphized and added to the compilation. It also
means that functions that are not called, will also
not be added to the compilation. This essentially
eliminates uncalled/dead functions. Is this
particularly smart to do in such a high level part
of the compilation process? I don't know. It's
obvious that we can't just use every function as
an entry point in the call graph, because we're
actively added new functions. Additionally, with
generic functions, we don't know, if they're the
entry point, what generic arguments, they should
be monomorphized with. We could do monomorphization
the same way C++ does it, where all non-generic
functions before monomorphization are treated as
entry points in the call graph. But this has the
drawback that generic and non-generic functions
are treated differently, which has many underlying
drawbacks, especially pertaining to the amount of
work needed to handle both in all proceeding steps
of the compiler. Anyways, I just wanted to yap and
complain about the way generics and monomorphization
has made the compiler 100x more complicated, and
that I find it really hard to implement in a way,
that is not either too simplistic or so complicated
and advanced I'm too dumb to implement it. So if
you would be so kind as to make it clear to the
compiler, what function it should designate as
the entry point to the call graph, it will use
for monomorphization, that would be very kind of
you. The way you do this, is by added or selecting
one of your current functions and giving it the
name of 'main'. This is spelled m-a-i-n. The word
is synonemous with the words primary and principle.
The name is meant to designate the entry point into
the program, which is why the monomorphization
process uses this specific function as the entry
point into the call graph, it generates. So if you
would be so kind as to do that, that would really
make my day. In any case, keep hacking ferociously
on whatever you're working on. I have monomorphizer
to implement. See ya. -Your favorite compiler girl <3
`.replaceAll(" ", "").trim();

618
compiler/mono_lower.ts Normal file
View File

@ -0,0 +1,618 @@
import { Builtins, Ops } from "./arch.ts";
import { Assembler, Label } from "./assembler.ts";
import { Expr, Stmt } from "./ast.ts";
import { FnNamesMap } from "./lowerer.ts";
import { LocalLeaf, Locals, LocalsFnRoot } from "./lowerer_locals.ts";
import { MonoCallNameGenMap, MonoFn, MonoFnsMap } from "./mono.ts";
import { Pos } from "./token.ts";
import { vtypeToString } from "./vtype.ts";
export class MonoLowerer {
private program = Assembler.newRoot();
public constructor(
private monoFns: MonoFnsMap,
private callMap: MonoCallNameGenMap,
private lastPos: Pos,
) {}
public lower(): { program: number[]; fnNames: FnNamesMap } {
const fnLabelNameMap: FnLabelMap = {};
for (const nameGen in this.monoFns) {
fnLabelNameMap[nameGen] = nameGen;
}
this.addPrelimiary();
for (const fn of Object.values(this.monoFns)) {
const fnProgram = new MonoFnLowerer(
fn,
this.program.fork(),
this.callMap,
).lower();
this.program.join(fnProgram);
}
this.addConcluding();
const { program, locs } = this.program.assemble();
const fnNames: FnNamesMap = {};
for (const label in locs) {
if (label in fnLabelNameMap) {
fnNames[locs[label]] = fnLabelNameMap[label];
}
}
return { program, fnNames };
}
private addPrelimiary() {
this.addClearingSourceMap();
this.program.add(Ops.PushPtr, { label: "main" });
this.program.add(Ops.Call, 0);
this.program.add(Ops.PushPtr, { label: "_exit" });
this.program.add(Ops.Jump);
}
private addConcluding() {
this.program.setLabel({ label: "_exit" });
this.addSourceMap(this.lastPos);
this.program.add(Ops.Pop);
}
private addSourceMap({ index, line, col }: Pos) {
this.program.add(Ops.SourceMap, index, line, col);
}
private addClearingSourceMap() {
this.program.add(Ops.SourceMap, 0, 1, 1);
}
public printProgram() {
this.program.printProgram();
}
}
type FnLabelMap = { [nameGen: string]: string };
class MonoFnLowerer {
private locals: Locals = new LocalsFnRoot();
private returnStack: Label[] = [];
private breakStack: Label[] = [];
public constructor(
private fn: MonoFn,
private program: Assembler,
private callMap: MonoCallNameGenMap,
) {}
public lower(): Assembler {
this.lowerFnStmt(this.fn.stmt);
return this.program;
}
private lowerFnStmt(stmt: Stmt) {
if (stmt.kind.type !== "fn") {
throw new Error();
}
const label = this.fn.nameGen;
this.program.setLabel({ label });
this.addSourceMap(stmt.pos);
const outerLocals = this.locals;
const fnRoot = new LocalsFnRoot(outerLocals);
const outerProgram = this.program;
const returnLabel = this.program.makeLabel();
this.returnStack.push(returnLabel);
this.program = outerProgram.fork();
this.locals = fnRoot;
for (const { ident } of stmt.kind.params) {
this.locals.allocSym(ident);
}
if (stmt.kind.anno?.ident === "builtin") {
this.lowerFnBuiltinBody(stmt.kind.anno.values);
} else if (stmt.kind.anno?.ident === "remainder") {
this.program.add(Ops.Remainder);
} else {
this.lowerExpr(stmt.kind.body);
}
this.locals = outerLocals;
const localAmount = fnRoot.stackReserved() -
stmt.kind.params.length;
for (let i = 0; i < localAmount; ++i) {
outerProgram.add(Ops.PushNull);
}
this.returnStack.pop();
this.program.setLabel(returnLabel);
this.program.add(Ops.Return);
outerProgram.join(this.program);
this.program = outerProgram;
}
private addSourceMap({ index, line, col }: Pos) {
this.program.add(Ops.SourceMap, index, line, col);
}
private addClearingSourceMap() {
this.program.add(Ops.SourceMap, 0, 1, 1);
}
private lowerStmt(stmt: Stmt) {
switch (stmt.kind.type) {
case "error":
break;
case "break":
return this.lowerBreakStmt(stmt);
case "return":
return this.lowerReturnStmt(stmt);
case "fn":
return this.lowerFnStmt(stmt);
case "let":
return this.lowerLetStmt(stmt);
case "assign":
return this.lowerAssignStmt(stmt);
case "expr":
this.lowerExpr(stmt.kind.expr);
this.program.add(Ops.Pop);
return;
}
throw new Error(`unhandled stmt '${stmt.kind.type}'`);
}
private lowerAssignStmt(stmt: Stmt) {
if (stmt.kind.type !== "assign") {
throw new Error();
}
this.lowerExpr(stmt.kind.value);
switch (stmt.kind.subject.kind.type) {
case "field": {
this.lowerExpr(stmt.kind.subject.kind.subject);
this.program.add(Ops.PushString, stmt.kind.subject.kind.ident);
this.program.add(Ops.Builtin, Builtins.StructSet);
return;
}
case "index": {
this.lowerExpr(stmt.kind.subject.kind.subject);
this.lowerExpr(stmt.kind.subject.kind.value);
this.program.add(Ops.Builtin, Builtins.ArraySet);
return;
}
case "sym": {
this.program.add(
Ops.StoreLocal,
this.locals.symId(stmt.kind.subject.kind.sym.ident),
);
return;
}
default:
throw new Error();
}
}
private lowerReturnStmt(stmt: Stmt) {
if (stmt.kind.type !== "return") {
throw new Error();
}
if (stmt.kind.expr) {
this.lowerExpr(stmt.kind.expr);
}
this.addClearingSourceMap();
this.program.add(Ops.PushPtr, this.returnStack.at(-1)!);
this.program.add(Ops.Jump);
}
private lowerBreakStmt(stmt: Stmt) {
if (stmt.kind.type !== "break") {
throw new Error();
}
if (stmt.kind.expr) {
this.lowerExpr(stmt.kind.expr);
}
this.addClearingSourceMap();
this.program.add(Ops.PushPtr, this.breakStack.at(-1)!);
this.program.add(Ops.Jump);
}
private lowerFnBuiltinBody(annoArgs: Expr[]) {
if (annoArgs.length !== 1) {
throw new Error("invalid # of arguments to builtin annotation");
}
const anno = annoArgs[0];
if (anno.kind.type !== "ident") {
throw new Error(
`unexpected argument type '${anno.kind.type}' expected 'ident'`,
);
}
const value = anno.kind.ident;
const builtin = Object.entries(Builtins).find((entry) =>
entry[0] === value
)?.[1];
if (builtin === undefined) {
throw new Error(
`unrecognized builtin '${value}'`,
);
}
this.program.add(Ops.Builtin, builtin);
}
private lowerLetStmt(stmt: Stmt) {
if (stmt.kind.type !== "let") {
throw new Error();
}
this.lowerExpr(stmt.kind.value);
this.locals.allocSym(stmt.kind.param.ident);
this.program.add(
Ops.StoreLocal,
this.locals.symId(stmt.kind.param.ident),
);
}
private lowerExpr(expr: Expr) {
switch (expr.kind.type) {
case "error":
break;
case "sym":
return this.lowerSymExpr(expr);
case "null":
break;
case "int":
return this.lowerIntExpr(expr);
case "bool":
return this.lowerBoolExpr(expr);
case "string":
return this.lowerStringExpr(expr);
case "ident":
break;
case "group":
return void this.lowerExpr(expr.kind.expr);
case "field":
break;
case "index":
return this.lowerIndexExpr(expr);
case "call":
return this.lowerCallExpr(expr);
case "etype_args":
return this.lowerETypeArgsExpr(expr);
case "unary":
return this.lowerUnaryExpr(expr);
case "binary":
return this.lowerBinaryExpr(expr);
case "if":
return this.lowerIfExpr(expr);
case "loop":
return this.lowerLoopExpr(expr);
case "block":
return this.lowerBlockExpr(expr);
}
throw new Error(`unhandled expr '${expr.kind.type}'`);
}
private lowerIndexExpr(expr: Expr) {
if (expr.kind.type !== "index") {
throw new Error();
}
this.lowerExpr(expr.kind.subject);
this.lowerExpr(expr.kind.value);
if (expr.kind.subject.vtype?.type == "array") {
this.program.add(Ops.Builtin, Builtins.ArrayAt);
return;
}
if (expr.kind.subject.vtype?.type == "string") {
this.program.add(Ops.Builtin, Builtins.StringCharAt);
return;
}
throw new Error(`unhandled index subject type '${expr.kind.subject}'`);
}
private lowerSymExpr(expr: Expr) {
if (expr.kind.type !== "sym") {
throw new Error();
}
if (expr.kind.sym.type === "let") {
const symId = this.locals.symId(expr.kind.ident);
this.program.add(Ops.LoadLocal, symId);
return;
}
if (expr.kind.sym.type === "fn_param") {
this.program.add(
Ops.LoadLocal,
this.locals.symId(expr.kind.ident),
);
return;
}
if (expr.kind.sym.type === "fn") {
// Is this smart? Well, my presumption is
// that it isn't. The underlying problem, which
// this solutions raison d'être is to solve, is
// that the compiler, as it d'être's currently
// doesn't support checking and infering generic
// fn args all the way down to the sym. Therefore,
// when a sym is checked in a call expr, we can't
// really do anything useful. Instead the actual
// function pointer pointing to the actual
// monomorphized function is emplaced when
// lowering the call expression itself. But what
// should we do then, if the user decides to
// assign a function to a local? You might ask.
// You see, that's where the problem lies.
// My current, very thought out solution, as
// you can read below, is to push a null pointer,
// for it to then be replaced later. This will
// probably cause many hastles in the future
// for myself in particular, when trying to
// decipher the lowerer's output. So if you're
// the unlucky girl, who has tried for ages to
// decipher why a zero value is pushed and then
// later replaced, and then you finally
// stumbled upon this here implementation,
// let me first say, I'm so sorry. At the time
// of writing, I really haven't thought out
// very well, how the generic call system should
// work, and it's therefore a bit flaky, and the
// implementation kinda looks like it was
// implementated by a girl who didn't really
// understand very well what they were
// implementing at the time that they were
// implementing it. Anyway, I just wanted to
// apologize. Happy coding.
// -Your favorite compiler girl.
this.program.add(Ops.PushPtr, 0);
return;
}
throw new Error(`unhandled sym type '${expr.kind.sym.type}'`);
}
private lowerIntExpr(expr: Expr) {
if (expr.kind.type !== "int") {
throw new Error();
}
this.program.add(Ops.PushInt, expr.kind.value);
}
private lowerBoolExpr(expr: Expr) {
if (expr.kind.type !== "bool") {
throw new Error();
}
this.program.add(Ops.PushBool, expr.kind.value);
}
private lowerStringExpr(expr: Expr) {
if (expr.kind.type !== "string") {
throw new Error();
}
this.program.add(Ops.PushString, expr.kind.value);
}
private lowerUnaryExpr(expr: Expr) {
if (expr.kind.type !== "unary") {
throw new Error();
}
this.lowerExpr(expr.kind.subject);
const vtype = expr.kind.subject.vtype!;
if (vtype.type === "bool") {
switch (expr.kind.unaryType) {
case "not":
this.program.add(Ops.Not);
return;
default:
}
}
if (vtype.type === "int") {
switch (expr.kind.unaryType) {
case "-": {
this.program.add(Ops.PushInt, 0);
this.program.add(Ops.Swap);
this.program.add(Ops.Subtract);
return;
}
default:
}
}
throw new Error(
`unhandled unary` +
` '${vtypeToString(expr.vtype!)}' aka. ` +
` ${expr.kind.unaryType}` +
` '${vtypeToString(expr.kind.subject.vtype!)}'`,
);
}
private lowerBinaryExpr(expr: Expr) {
if (expr.kind.type !== "binary") {
throw new Error();
}
const vtype = expr.kind.left.vtype!;
if (vtype.type === "bool") {
if (["or", "and"].includes(expr.kind.binaryType)) {
const shortCircuitLabel = this.program.makeLabel();
this.lowerExpr(expr.kind.left);
this.program.add(Ops.Duplicate);
if (expr.kind.binaryType === "and") {
this.program.add(Ops.Not);
}
this.program.add(Ops.PushPtr, shortCircuitLabel);
this.program.add(Ops.JumpIfTrue);
this.program.add(Ops.Pop);
this.lowerExpr(expr.kind.right);
this.program.setLabel(shortCircuitLabel);
return;
}
}
this.lowerExpr(expr.kind.left);
this.lowerExpr(expr.kind.right);
if (vtype.type === "int") {
switch (expr.kind.binaryType) {
case "+":
this.program.add(Ops.Add);
return;
case "-":
this.program.add(Ops.Subtract);
return;
case "*":
this.program.add(Ops.Multiply);
return;
case "/":
this.program.add(Ops.Multiply);
return;
case "==":
this.program.add(Ops.Equal);
return;
case "!=":
this.program.add(Ops.Equal);
this.program.add(Ops.Not);
return;
case "<":
this.program.add(Ops.LessThan);
return;
case ">":
this.program.add(Ops.Swap);
this.program.add(Ops.LessThan);
return;
case "<=":
this.program.add(Ops.Swap);
this.program.add(Ops.LessThan);
this.program.add(Ops.Not);
return;
case ">=":
this.program.add(Ops.LessThan);
this.program.add(Ops.Not);
return;
default:
}
}
if (vtype.type === "bool") {
switch (expr.kind.binaryType) {
case "==":
this.program.add(Ops.And);
return;
case "!=":
this.program.add(Ops.And);
this.program.add(Ops.Not);
return;
default:
}
}
if (vtype.type === "string") {
if (expr.kind.binaryType === "+") {
this.program.add(Ops.Builtin, Builtins.StringConcat);
return;
}
if (expr.kind.binaryType === "==") {
this.program.add(Ops.Builtin, Builtins.StringEqual);
return;
}
if (expr.kind.binaryType === "!=") {
this.program.add(Ops.Builtin, Builtins.StringEqual);
this.program.add(Ops.Not);
return;
}
}
throw new Error(
`unhandled binaryType` +
` '${vtypeToString(expr.vtype!)}' aka. ` +
` '${vtypeToString(expr.kind.left.vtype!)}'` +
` ${expr.kind.binaryType}` +
` '${vtypeToString(expr.kind.left.vtype!)}'`,
);
}
private lowerCallExpr(expr: Expr) {
if (expr.kind.type !== "call") {
throw new Error();
}
for (const arg of expr.kind.args) {
this.lowerExpr(arg);
}
this.lowerExpr(expr.kind.subject);
this.program.add(Ops.Pop);
this.program.add(Ops.PushPtr, { label: this.callMap[expr.id] });
this.program.add(Ops.Call, expr.kind.args.length);
}
private lowerETypeArgsExpr(expr: Expr) {
if (expr.kind.type !== "etype_args") {
throw new Error();
}
this.lowerExpr(expr.kind.subject);
}
private lowerIfExpr(expr: Expr) {
if (expr.kind.type !== "if") {
throw new Error();
}
const falseLabel = this.program.makeLabel();
const doneLabel = this.program.makeLabel();
this.lowerExpr(expr.kind.cond);
this.program.add(Ops.Not);
this.addClearingSourceMap();
this.program.add(Ops.PushPtr, falseLabel);
this.program.add(Ops.JumpIfTrue);
this.addSourceMap(expr.kind.truthy.pos);
this.lowerExpr(expr.kind.truthy);
this.addClearingSourceMap();
this.program.add(Ops.PushPtr, doneLabel);
this.program.add(Ops.Jump);
this.program.setLabel(falseLabel);
if (expr.kind.falsy) {
this.addSourceMap(expr.kind.elsePos!);
this.lowerExpr(expr.kind.falsy);
} else {
this.program.add(Ops.PushNull);
}
this.program.setLabel(doneLabel);
}
private lowerLoopExpr(expr: Expr) {
if (expr.kind.type !== "loop") {
throw new Error();
}
const continueLabel = this.program.makeLabel();
const breakLabel = this.program.makeLabel();
this.breakStack.push(breakLabel);
this.program.setLabel(continueLabel);
this.addSourceMap(expr.kind.body.pos);
this.lowerExpr(expr.kind.body);
this.program.add(Ops.Pop);
this.addClearingSourceMap();
this.program.add(Ops.PushPtr, continueLabel);
this.program.add(Ops.Jump);
this.program.setLabel(breakLabel);
if (expr.vtype!.type === "null") {
this.program.add(Ops.PushNull);
}
this.breakStack.pop();
}
private lowerBlockExpr(expr: Expr) {
if (expr.kind.type !== "block") {
throw new Error();
}
const outerLocals = this.locals;
this.locals = new LocalLeaf(this.locals);
for (const stmt of expr.kind.stmts) {
this.addSourceMap(stmt.pos);
this.lowerStmt(stmt);
}
if (expr.kind.expr) {
this.addSourceMap(expr.kind.expr.pos);
this.lowerExpr(expr.kind.expr);
} else {
this.program.add(Ops.PushNull);
}
this.locals = outerLocals;
}
}

View File

@ -269,12 +269,16 @@ export class Parser {
return this.parseDelimitedList(this.parseETypeParam, ">", ",");
}
private veryTemporaryETypeParamIdCounter = 0;
private parseETypeParam(): Res<GenericParam> {
const pos = this.pos();
if (this.test("ident")) {
const ident = this.current().identValue!;
this.step();
return { ok: true, value: { ident, pos } };
const id = this.veryTemporaryETypeParamIdCounter;
this.veryTemporaryETypeParamIdCounter += 1;
return { ok: true, value: { id, ident, pos } };
}
this.report("expected generic parameter");
return { ok: false };

View File

@ -12,12 +12,13 @@ export type VType =
genericParams?: VTypeGenericParam[];
params: VTypeParam[];
returnType: VType;
stmtId: number;
}
| { type: "generic" }
| { type: "generic"; param: VTypeGenericParam }
| {
type: "generic_spec";
subject: VType;
genericParams: VType[];
genericArgs: GenericArgsMap;
};
export type VTypeParam = {
@ -26,21 +27,25 @@ export type VTypeParam = {
};
export type VTypeGenericParam = {
id: number;
ident: string;
};
export function vtypesEqual(a: VType, b: VType): boolean {
if (a.type !== b.type) {
return false;
}
export type GenericArgsMap = { [id: number]: VType };
export function vtypesEqual(
a: VType,
b: VType,
generics?: GenericArgsMap,
): boolean {
if (
["error", "unknown", "null", "int", "string", "bool"]
.includes(a.type)
.includes(a.type) && a.type === b.type
) {
return true;
}
if (a.type === "array" && b.type === "array") {
return vtypesEqual(a.inner, b.inner);
return vtypesEqual(a.inner, b.inner, generics);
}
if (a.type === "fn" && b.type === "fn") {
if (a.params.length !== b.params.length) {
@ -51,11 +56,41 @@ export function vtypesEqual(a: VType, b: VType): boolean {
return false;
}
}
return vtypesEqual(a.returnType, b.returnType);
return vtypesEqual(a.returnType, b.returnType, generics);
}
if (a.type === "generic" && b.type === "generic") {
return a.param.id === b.param.id;
}
if (
(a.type === "generic" || b.type === "generic") &&
generics !== undefined
) {
if (generics === undefined) {
throw new Error();
}
const generic = a.type === "generic" ? a : b;
const concrete = a.type === "generic" ? b : a;
const genericType = extractGenericType(generic, generics);
return vtypesEqual(genericType, concrete, generics);
}
return false;
}
export function extractGenericType(
generic: VType,
generics: GenericArgsMap,
): VType {
if (generic.type !== "generic") {
return generic;
}
if (!(generic.param.id in generics)) {
throw new Error("generic not found (not supposed to happen)");
}
return generics[generic.param.id];
}
export function vtypeToString(vtype: VType): string {
if (
["error", "unknown", "null", "int", "string", "bool"]

View File

@ -1,4 +1,6 @@
fn exit(status_code: int) #[builtin(Exit)] {}
fn print(msg: string) #[builtin(Print)] {}
fn println(msg: string) { print(msg + "\n") }

View File

@ -1,17 +1,23 @@
fn exit(status_code: int) #[builtin(Exit)] {}
fn print(msg: string) #[builtin(Print)] {}
fn println(msg: string) { print(msg + "\n") }
fn id<T>(v: T) -> T {
v
}
fn main() {
println("calling with int");
if id::<int>(123) != 123 {
exit(1);
}
println("calling with bool");
if id::<bool>(true) != true {
exit(1);
}
println("all tests ran successfully");
exit(0);
}