generate more code

This commit is contained in:
nora 2023-07-26 20:50:48 +02:00
parent ccd8008731
commit 42bc96dbce
7 changed files with 306 additions and 51 deletions

View file

@ -1,4 +1,4 @@
import { Ast, FunctionDef, Item, Ty, TyFn, varUnreachable } from "./ast";
import { Ast, Expr, FunctionDef, Item, Ty, TyFn, varUnreachable } from "./ast";
import * as wasm from "./wasm/defs";
type StringifiedForMap<T> = string;
@ -39,13 +39,7 @@ export function lower(ast: Ast): wasm.Module {
ast.forEach((item) => {
switch (item.kind) {
case "function": {
const fcx: FuncContext = {
cx,
item,
func: item.node,
};
lowerFunc(fcx);
lowerFunc(cx, item, item.node);
}
}
});
@ -57,6 +51,8 @@ type FuncContext = {
cx: Context;
item: Item;
func: FunctionDef;
wasm: wasm.Func;
varLocations: VarLocation[];
};
type Abi = { params: ArgAbi[]; ret: RetAbi };
@ -64,22 +60,210 @@ type Abi = { params: ArgAbi[]; ret: RetAbi };
type ArgAbi = { kind: "scalar"; type: wasm.ValType } | { kind: "zst" };
type RetAbi = { kind: "scalar"; type: wasm.ValType } | { kind: "zst" };
function lowerFunc(fcx: FuncContext) {
const abi = computeAbi(fcx.func.ty!);
const wasmType = wasmTypeForAbi(abi);
const type = internFuncType(fcx.cx, wasmType);
type VarLocation = { kind: "local"; idx: number } | { kind: "zst" };
function lowerFunc(cx: Context, item: Item, func: FunctionDef) {
const abi = computeAbi(func.ty!);
const { type: wasmType, paramLocations } = wasmTypeForAbi(abi);
const type = internFuncType(cx, wasmType);
const wasmFunc: wasm.Func = {
_name: func.name,
type,
locals: [],
body: [],
};
const fcx: FuncContext = {
cx,
item,
func,
wasm: wasmFunc,
varLocations: paramLocations,
};
lowerExpr(fcx, wasmFunc.body, fcx.func.body);
const idx = fcx.cx.mod.funcs.length;
fcx.cx.mod.funcs.push(wasmFunc);
fcx.cx.funcIndices.set(fcx.item.id, idx);
}
/*
Expression lowering.
- the result of an expression evaluation is stored on the top of the stack
*/
function lowerExpr(fcx: FuncContext, instrs: wasm.Instr[], expr: Expr) {
const ty = expr.ty!;
switch (expr.kind) {
case "empty":
// A ZST, do nothing.
return;
case "let":
// Let, that's complicated.
todo("let");
case "block":
if (expr.exprs.length === 1) {
lowerExpr(fcx, instrs, expr.exprs[0]);
}
break;
case "literal":
switch (expr.value.kind) {
case "str":
todo("strings");
case "int":
instrs.push({ kind: "i64.const", imm: expr.value.value });
}
break;
case "ident":
const res = expr.value.res!;
switch (res.kind) {
case "local": {
const location =
fcx.varLocations[fcx.varLocations.length - 1 - res.index];
loadVariable(instrs, location);
break;
}
case "item":
todo("item ident res");
case "builtin":
switch (res.name) {
case "false":
instrs.push({ kind: "i32.const", imm: 0 });
break;
case "true":
instrs.push({ kind: "i32.const", imm: 1 });
break;
case "print":
todo("print function");
default: {
throw new Error(`${res.name}#B is not a value`);
}
}
}
break;
case "binary":
// By evaluating the LHS first, the RHS is on top, which
// is correct as it's popped first. Evaluating the LHS first
// is correct for the source language too so great, no swapping.
lowerExpr(fcx, instrs, expr.lhs);
lowerExpr(fcx, instrs, expr.rhs);
if (expr.lhs.ty!.kind === "int" && expr.rhs.ty!.kind === "int") {
let kind: wasm.Instr["kind"];
switch (expr.binaryKind) {
case "+":
kind = "i64.add";
break;
case "-":
kind = "i64.sub";
break;
case "*":
kind = "i64.mul";
break;
case "/":
kind = "i64.div_u";
break;
case "&":
kind = "i64.and";
break;
case "|":
kind = "i64.or";
break;
case "<":
kind = "i64.lt_u";
break;
case ">":
kind = "i64.gt_u";
break;
case "==":
kind = "i64.eq";
break;
case "<=":
kind = "i64.le_u";
break;
case ">=":
kind = "i64.ge_u";
break;
case "!=":
kind = "i64.ne";
break;
}
instrs.push({ kind });
} else if (expr.lhs.ty!.kind === "bool" && expr.rhs.ty!.kind === "bool") {
let kind: wasm.Instr["kind"];
switch (expr.binaryKind) {
case "&":
kind = "i32.and";
break;
case "|":
kind = "i32.or";
break;
case "==":
kind = "i32.eq";
break;
case "!=":
kind = "i32.ne";
break;
case "<":
case ">":
case "<=":
case ">=":
case "+":
case "-":
case "*":
case "/":
throw new Error(`Invalid bool binary expr: ${expr.binaryKind}`);
}
instrs.push({ kind });
} else {
todo("non int/bool binary expr");
}
break;
case "unary":
lowerExpr(fcx, instrs, expr.rhs);
switch (expr.unaryKind) {
case "!":
if (ty.kind === "bool") {
// `xor RHS, 1` flips the lowermost bit.
instrs.push({ kind: "i64.const", imm: 1 });
instrs.push({ kind: "i64.xor" });
} else if (ty.kind === "int") {
// `xor RHS, -1` flips all bits.
todo("Thanks to JS, we cannot represent -1 i64 yet");
}
break;
case "-":
todo("negation");
}
break;
case "call":
todo("call");
case "if":
todo("ifs");
}
}
function loadVariable(instrs: wasm.Instr[], loc: VarLocation) {
switch (loc.kind) {
case "local": {
instrs.push({ kind: "local.get", imm: loc.idx });
break;
}
case "zst":
// Load the ZST:
// ...
// 🪄 poof, the ZST is on the stack now.
break;
}
}
function computeAbi(ty: TyFn): Abi {
const scalar = (type: wasm.ValType): ArgAbi & RetAbi =>
({ kind: "scalar", type } as const);
@ -141,18 +325,24 @@ function computeAbi(ty: TyFn): Abi {
return { params, ret };
}
function wasmTypeForAbi(abi: Abi): wasm.FuncType {
const params = abi.params
.map((arg) => {
switch (arg.kind) {
case "scalar":
return arg.type;
case "zst":
return undefined;
}
})
.filter(exists);
function wasmTypeForAbi(abi: Abi): {
type: wasm.FuncType;
paramLocations: VarLocation[];
} {
const params: wasm.ValType[] = [];
const paramLocations: VarLocation[] = [];
abi.params.forEach((arg) => {
switch (arg.kind) {
case "scalar":
paramLocations.push({ kind: "local", idx: params.length });
params.push(arg.type);
break;
case "zst":
paramLocations.push({ kind: "zst" });
return undefined;
}
});
let returns: wasm.ValType[];
switch (abi.ret.kind) {
case "scalar":
@ -163,7 +353,31 @@ function wasmTypeForAbi(abi: Abi): wasm.FuncType {
break;
}
return { params, returns };
return { type: { params, returns }, paramLocations };
}
function wasmTypeForBody(ty: Ty): wasm.ValType | undefined {
switch (ty.kind) {
case "string":
todo("string types");
case "int":
return "i64";
case "bool":
return "i32";
case "list":
todo("list types");
case "tuple":
if (ty.elems.length === 0) {
return undefined;
} else if (ty.elems.length === 1) {
return wasmTypeForBody(ty.elems[0]);
}
todo("complex tuples");
case "fn":
todo("fn types");
case "var":
varUnreachable();
}
}
function todo(msg: string): never {