From 42bc96dbcecbc48d92dcbf84554bdec72bf798c0 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Wed, 26 Jul 2023 20:50:48 +0200 Subject: [PATCH] generate more code --- .gitignore | 3 +- src/ast.ts | 20 +++- src/index.ts | 33 +++++- src/lower.ts | 262 +++++++++++++++++++++++++++++++++++++++++++----- src/resolve.ts | 21 ++-- src/typeck.ts | 16 +-- src/wasm/wat.ts | 2 +- 7 files changed, 306 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index d2c1855..7390d18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules /target *.tsbuildinfo -/*-example.wat \ No newline at end of file +/*-example.wat +/out.wat diff --git a/src/ast.ts b/src/ast.ts index cc4e940..a53ee68 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -155,8 +155,8 @@ const BINARY_KIND_PREC_CLASS = new Map([ export function binaryExprPrecedenceClass(k: BinaryKind): number { const cls = BINARY_KIND_PREC_CLASS.get(k); - if (!cls) { - throw new Error(`Invalid binary kind: ${k}`); + if (cls === undefined) { + throw new Error(`Invalid binary kind: '${k}'`); } return cls; } @@ -209,15 +209,27 @@ export type Resolution = } | { kind: "builtin"; - name: string; + name: BuiltinName; }; +export const BUILTINS = [ + "print", + "String", + "Int", + "Bool", + "true", + "false", +] as const; + +export type BuiltinName = (typeof BUILTINS)[number]; + export type TyString = { kind: "string"; }; export type TyInt = { kind: "int"; + signed: boolean; }; export type TyBool = { @@ -259,7 +271,7 @@ export function tyIsUnit(ty: Ty): ty is TyUnit { export const TY_UNIT: Ty = { kind: "tuple", elems: [] }; export const TY_STRING: Ty = { kind: "string" }; export const TY_BOOL: Ty = { kind: "bool" }; -export const TY_INT: Ty = { kind: "int" }; +export const TY_INT: Ty = { kind: "int", signed: false }; // folders diff --git a/src/index.ts b/src/index.ts index ed6a716..72c8b1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,13 +6,17 @@ import { printAst } from "./printer"; import { resolve } from "./resolve"; import { typeck } from "./typeck"; import { writeModuleWatToString } from "./wasm/wat"; +import fs from "fs"; +import { exec } from "child_process"; const input = ` -function main(i: Int): Int = 0; +function main(i: Int, j: Int): Bool = false == (i == 0); `; function main() { withErrorHandler(input, () => { + const start = Date.now(); + const tokens = tokenize(input); console.log("-----TOKENS------------"); console.log(tokens); @@ -34,11 +38,34 @@ function main() { console.log("-----AST typecked------"); const typecked = typeck(resolved); - + console.log("-----wasm--------------"); const wasmModule = lowerToWasm(typecked); + const moduleStringColor = writeModuleWatToString(wasmModule, true); const moduleString = writeModuleWatToString(wasmModule); - console.log(moduleString); + + console.log(moduleStringColor); + + fs.writeFileSync("out.wat", moduleString); + + console.log("--validate wasm-tools--"); + + exec("wasm-tools validate out.wat", (error, stdout, stderr) => { + if (error && error.code === 1) { + console.log(stderr); + } else if (error) { + console.error(`failed to spawn wasm-tools: ${error}`); + } else { + if (stderr) { + console.log(stderr); + } + if (stdout) { + console.log(stdout); + } + } + + console.log(`finished in ${Date.now() - start}ms`); + }); }); } diff --git a/src/lower.ts b/src/lower.ts index 3979d14..b138c83 100644 --- a/src/lower.ts +++ b/src/lower.ts @@ -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 = 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 { diff --git a/src/resolve.ts b/src/resolve.ts index 34da31d..8fbf00a 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -1,5 +1,7 @@ import { Ast, + BUILTINS, + BuiltinName, DEFAULT_FOLDER, Folder, Identifier, @@ -10,14 +12,7 @@ import { } from "./ast"; import { CompilerError } from "./error"; -const BUILTINS = new Set([ - "print", - "String", - "Int", - "Bool", - "true", - "false", -]); +const BUILTIN_SET = new Set(BUILTINS); export function resolve(ast: Ast): Ast { const items = new Map(); @@ -40,7 +35,7 @@ export function resolve(ast: Ast): Ast { const popped = scopes.pop(); if (popped !== expected) { throw new Error( - `Scopes corrupted, wanted to pop ${name} but popped ${popped}` + `Scopes corrupted, wanted to pop ${expected} but popped ${popped}` ); } }; @@ -66,8 +61,8 @@ export function resolve(ast: Ast): Ast { }; } - if (BUILTINS.has(ident.name)) { - return { kind: "builtin", name: ident.name }; + if (BUILTIN_SET.has(ident.name)) { + return { kind: "builtin", name: ident.name as BuiltinName }; } throw new CompilerError(`cannot find ${ident.name}`, ident.span); @@ -88,7 +83,9 @@ export function resolve(ast: Ast): Ast { item.node.params.forEach(({ name }) => scopes.push(name)); const body = superFoldExpr(item.node.body, this); - item.node.params.forEach(({ name }) => popScope(name)); + const revParams = item.node.params.slice(); + revParams.reverse(); + revParams.forEach(({ name }) => popScope(name)); return { kind: "function", diff --git a/src/typeck.ts b/src/typeck.ts index be9f972..1a131bb 100644 --- a/src/typeck.ts +++ b/src/typeck.ts @@ -147,7 +147,7 @@ export function typeck(ast: Ast): Ast { const returnType = item.node.returnType && { ...item.node.returnType, ty: fnTy.returnTy, - }; + }; return { kind: "function", node: { @@ -506,11 +506,11 @@ function checkBinary(expr: Expr & ExprBinary): Expr { let lhsTy = expr.lhs.ty!; let rhsTy = expr.rhs.ty!; - if (lhsTy.kind === "int" && rhsTy.kind === "int") { - return { ...expr, ty: TY_INT }; - } - if (COMPARISON_KINDS.includes(expr.binaryKind)) { + if (lhsTy.kind === "int" && rhsTy.kind === "int") { + return { ...expr, ty: TY_BOOL }; + } + if (lhsTy.kind === "string" && rhsTy.kind === "string") { return { ...expr, ty: TY_BOOL }; } @@ -522,6 +522,10 @@ function checkBinary(expr: Expr & ExprBinary): Expr { } } + if (lhsTy.kind === "int" && rhsTy.kind === "int") { + return { ...expr, ty: TY_INT }; + } + if (LOGICAL_KINDS.includes(expr.binaryKind)) { if (lhsTy.kind === "bool" && rhsTy.kind === "bool") { return { ...expr, ty: TY_BOOL }; @@ -547,7 +551,7 @@ function checkUnary(expr: Expr & ExprUnary): Expr { } if (expr.unaryKind === "-" && rhsTy.kind == "int") { - return { ...expr, ty: rhsTy }; + // Negating an unsigned integer is a bad idea. } throw new CompilerError( diff --git a/src/wasm/wat.ts b/src/wasm/wat.ts index 533c42d..41a5a7c 100644 --- a/src/wasm/wat.ts +++ b/src/wasm/wat.ts @@ -100,7 +100,7 @@ class Formatter { } } -export function writeModuleWatToString(module: Module, color = true): string { +export function writeModuleWatToString(module: Module, color = false): string { const parts: string[] = []; const writer = (s: string) => parts.push(s); printModule(module, new Formatter(writer, color));