diff --git a/package-lock.json b/package-lock.json index fd4e5f1..6689d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "tscript", "version": "1.0.0", "license": "ISC", + "dependencies": { + "chalk": "^4.0.0" + }, "devDependencies": { "@types/jest": "^29.5.3", "jest": "^29.6.1", @@ -1183,7 +1186,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1442,7 +1444,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1518,7 +1519,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1529,8 +1529,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -1931,7 +1930,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3530,7 +3528,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, diff --git a/package.json b/package.json index fda1012..5f6c3db 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,8 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.1.6" + }, + "dependencies": { + "chalk": "^4.0.0" } } diff --git a/src/ast.ts b/src/ast.ts index fa6d04a..cc4e940 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -8,6 +8,8 @@ export type Identifier = { res?: Resolution; }; +export type ItemId = number; + export type ItemKind = { kind: "function"; node: FunctionDef; @@ -15,7 +17,7 @@ export type ItemKind = { export type Item = ItemKind & { span: Span; - id: number; + id: ItemId; }; export type FunctionDef = { diff --git a/src/index.ts b/src/index.ts index f4edec4..ed6a716 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,29 +1,29 @@ import { withErrorHandler } from "./error"; import { tokenize } from "./lexer"; +import { lower as lowerToWasm } from "./lower"; import { parse } from "./parser"; import { printAst } from "./printer"; import { resolve } from "./resolve"; import { typeck } from "./typeck"; +import { writeModuleWatToString } from "./wasm/wat"; const input = ` -function main() = ( - let a = 0 in 0; -); +function main(i: Int): Int = 0; `; function main() { withErrorHandler(input, () => { const tokens = tokenize(input); - console.log("-----TOKENS---"); + console.log("-----TOKENS------------"); console.log(tokens); const ast = parse(tokens); - console.log("-----AST------"); + console.log("-----AST---------------"); console.dir(ast, { depth: 50 }); const printed = printAst(ast); - console.log("-----AST pretty------"); + console.log("-----AST pretty--------"); console.log(printed); const resolved = resolve(ast); @@ -34,7 +34,11 @@ function main() { console.log("-----AST typecked------"); const typecked = typeck(resolved); - console.dir(typecked, { depth: 10 }); + + console.log("-----wasm--------------"); + const wasmModule = lowerToWasm(typecked); + const moduleString = writeModuleWatToString(wasmModule); + console.log(moduleString); }); } diff --git a/src/lower.ts b/src/lower.ts new file mode 100644 index 0000000..3979d14 --- /dev/null +++ b/src/lower.ts @@ -0,0 +1,175 @@ +import { Ast, FunctionDef, Item, Ty, TyFn, varUnreachable } from "./ast"; +import * as wasm from "./wasm/defs"; + +type StringifiedForMap = string; + +type Context = { + mod: wasm.Module; + funcTypes: Map, wasm.TypeIdx>; + funcIndices: Map; +}; + +function internFuncType(cx: Context, type: wasm.FuncType): wasm.TypeIdx { + const s = JSON.stringify(type); + const existing = cx.funcTypes.get(s); + if (existing !== undefined) { + return existing; + } + const idx = cx.mod.types.length; + cx.mod.types.push(type); + cx.funcTypes.set(s, idx); + return idx; +} + +export function lower(ast: Ast): wasm.Module { + const mod: wasm.Module = { + types: [], + funcs: [], + tables: [], + mems: [], + globals: [], + elems: [], + datas: [], + imports: [], + exports: [], + }; + + const cx: Context = { mod, funcTypes: new Map(), funcIndices: new Map() }; + + ast.forEach((item) => { + switch (item.kind) { + case "function": { + const fcx: FuncContext = { + cx, + item, + func: item.node, + }; + + lowerFunc(fcx); + } + } + }); + + return mod; +} + +type FuncContext = { + cx: Context; + item: Item; + func: FunctionDef; +}; + +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); + + const wasmFunc: wasm.Func = { + type, + locals: [], + body: [], + }; + + const idx = fcx.cx.mod.funcs.length; + fcx.cx.mod.funcs.push(wasmFunc); + fcx.cx.funcIndices.set(fcx.item.id, idx); +} + +function computeAbi(ty: TyFn): Abi { + const scalar = (type: wasm.ValType): ArgAbi & RetAbi => + ({ kind: "scalar", type } as const); + const zst: ArgAbi & RetAbi = { kind: "zst" }; + + function paramAbi(param: Ty): ArgAbi { + switch (param.kind) { + case "string": + todo("string abi"); + case "fn": + todo("fn abi"); + case "int": + return scalar("i64"); + case "bool": + return scalar("i32"); + case "list": + todo("list abi"); + case "tuple": + if (param.elems.length === 0) { + return zst; + } else if (param.elems.length === 1) { + return paramAbi(param.elems[0]); + } + todo("complex tuple abi"); + case "var": + varUnreachable(); + } + } + + const params = ty.params.map(paramAbi); + + let ret: RetAbi; + switch (ty.returnTy.kind) { + case "string": + todo("string abi"); + case "fn": + todo("fn abi"); + case "int": + ret = scalar("i64"); + break; + case "bool": + ret = scalar("i32"); + break; + case "list": + todo("list abi"); + case "tuple": + if (ty.returnTy.elems.length === 0) { + ret = zst; + break; + } else if (ty.returnTy.elems.length === 1) { + ret = paramAbi(ty.returnTy.elems[0]); + break; + } + todo("complex tuple abi"); + case "var": + varUnreachable(); + } + + 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); + + let returns: wasm.ValType[]; + switch (abi.ret.kind) { + case "scalar": + returns = [abi.ret.type]; + break; + case "zst": + returns = []; + break; + } + + return { params, returns }; +} + +function todo(msg: string): never { + throw new Error(`TODO: ${msg}`); +} + +function exists(val: T | undefined): val is T { + return val !== undefined; +} diff --git a/src/typeck.ts b/src/typeck.ts index 51e0e93..be9f972 100644 --- a/src/typeck.ts +++ b/src/typeck.ts @@ -10,6 +10,7 @@ import { foldAst, Folder, Identifier, + ItemId, LOGICAL_KINDS, Resolution, Ty, @@ -29,7 +30,7 @@ function builtinAsTy(name: string, span: Span): Ty { return TY_STRING; } case "Int": { - return TY_BOOL; + return TY_INT; } case "Bool": { return TY_BOOL; @@ -92,7 +93,7 @@ function lowerAstTyBase( export function typeck(ast: Ast): Ast { const itemTys = new Map(); - function typeOfItem(index: number): Ty { + function typeOfItem(index: ItemId): Ty { const ty = itemTys.get(index); if (ty) { return ty; @@ -146,8 +147,7 @@ export function typeck(ast: Ast): Ast { const returnType = item.node.returnType && { ...item.node.returnType, ty: fnTy.returnTy, - }; - + }; return { kind: "function", node: { @@ -158,10 +158,10 @@ export function typeck(ast: Ast): Ast { })), body, returnType, + ty: fnTy, }, span: item.span, id: item.id, - ty: fnTy, }; } } diff --git a/src/wasm/defs.ts b/src/wasm/defs.ts index 1465112..806e26b 100644 --- a/src/wasm/defs.ts +++ b/src/wasm/defs.ts @@ -3,7 +3,6 @@ // Base types. -export type Vec = T[]; export type u32 = number; export type u64 = number; export type f32 = number; @@ -21,7 +20,7 @@ export type Reftype = "funcref" | "externref"; export type ValType = Numtype | Vectype | Reftype; -export type ResultType = Vec; +export type ResultType = ValType[]; export type FuncType = { params: ResultType; @@ -279,16 +278,16 @@ export type Expr = Instr[]; // Modules export type Module = { - types: Vec; - funcs: Vec; - tables: Vec; - mems: Vec; - globals: Vec; - elems: Vec; - datas: Vec; + types: FuncType[]; + funcs: Func[]; + tables: Table[]; + mems: Mem[]; + globals: Global[]; + elems: Elem[]; + datas: Data[]; start?: Start; - imports: Vec; - exports: Vec; + imports: Import[]; + exports: Export[]; _name?: string; }; @@ -304,7 +303,7 @@ export type LabelIdx = u32; export type Func = { type: TypeIdx; - locals: Vec; + locals: ValType[]; body: Expr; _name?: string; }; diff --git a/src/wasm/wat.test.ts b/src/wasm/wat.test.ts index f46d308..a2cd690 100644 --- a/src/wasm/wat.test.ts +++ b/src/wasm/wat.test.ts @@ -79,7 +79,7 @@ const EXAMPLE_MODULE: Module = { }; it("should print a Wasm module with the correct formatting", () => { - const wat = writeModuleWatToString(EXAMPLE_MODULE); + const wat = writeModuleWatToString(EXAMPLE_MODULE, false); expect(wat).toMatchInlineSnapshot(` "(module $example diff --git a/src/wasm/wat.ts b/src/wasm/wat.ts index 19da86a..533c42d 100644 --- a/src/wasm/wat.ts +++ b/src/wasm/wat.ts @@ -1,6 +1,7 @@ // This module converts the Wasm definitions to the WAT // WebAssembly text format for easier debugging and inspection. +import chalk from "chalk"; import { Blocktype, Data, @@ -23,17 +24,21 @@ import { ValType, } from "./defs"; +const identity = (s: string) => s; + class Formatter { print: (chunk: string) => void; indentation: number; wordsInSexpr: number[]; freshLinebreak: boolean; + color: boolean; - constructor(print: (chunk: string) => void) { + constructor(print: (chunk: string) => void, color = true) { this.print = print; this.indentation = 0; this.wordsInSexpr = []; this.freshLinebreak = false; + this.color = color; } linebreak() { @@ -47,6 +52,11 @@ class Formatter { } breakDedent() { this.indentation--; + if (this.indentation < 0) { + throw new Error( + "Cannot dedent from 0 indents, there are more dedents than indents" + ); + } this.linebreak(); } @@ -56,14 +66,26 @@ class Formatter { this.endSexpr(); } - word(word: string | number) { + keyword(word: string) { + this.word(word, chalk.blue); + } + + type(word: string | number) { + this.word(word, chalk.green); + } + + word(word: string | number, color: (s: string) => string = identity) { const last = this.wordsInSexpr.length - 1; if (this.wordsInSexpr[last] > 0 && !this.freshLinebreak) { // The first word hugs the left parenthesis. this.print(" "); } this.freshLinebreak = false; - this.print(String(word)); + if (this.color) { + this.print(color(String(word))); + } else { + this.print(String(word)); + } this.wordsInSexpr[last]++; } @@ -78,10 +100,10 @@ class Formatter { } } -export function writeModuleWatToString(module: Module): string { +export function writeModuleWatToString(module: Module, color = true): string { const parts: string[] = []; const writer = (s: string) => parts.push(s); - printModule(module, new Formatter(writer)); + printModule(module, new Formatter(writer, color)); return parts.join(""); } @@ -108,19 +130,19 @@ function printId(id: string | undefined, f: Formatter) { // types function printValType(type: ValType, f: Formatter) { - f.word(type); + f.type(type); } function printFuncType(type: FuncType, f: Formatter) { f.sexpr(() => { - f.word("func"); + f.keyword("func"); f.sexpr(() => { - f.word("param"); - type.params.forEach(f.word.bind(f)); + f.keyword("param"); + type.params.forEach((param) => printValType(param, f)); }); f.sexpr(() => { - f.word("result"); - type.returns.forEach(f.word.bind(f)); + f.keyword("result"); + type.returns.forEach((type) => printValType(type, f)); }); }); } @@ -132,7 +154,7 @@ function printLimits(limits: Limits, f: Formatter) { function printTableType(type: TableType, f: Formatter) { printLimits(type.limits, f); - f.word(type.reftype); + printValType(type.reftype, f); } function printGlobalType(type: GlobalType, f: Formatter) { @@ -140,7 +162,7 @@ function printGlobalType(type: GlobalType, f: Formatter) { printValType(type.type, f); } else { f.sexpr(() => { - f.word("mut"); + f.keyword("mut"); printValType(type.type, f); }); } @@ -150,9 +172,9 @@ function printGlobalType(type: GlobalType, f: Formatter) { function printBlockType(type: Blocktype, f: Formatter) { f.sexpr(() => { - f.word("type"); + f.keyword("type"); if (type.kind === "typeidx") { - f.word(type.idx); + f.type(type.idx); } else if (type.type !== undefined) { printValType(type.type, f); } @@ -375,7 +397,7 @@ function printInstr(instr: Instr, f: Formatter) { break; case "br_table": f.word(instr.kind); - instr.labels.forEach(f.word.bind(f)); + instr.labels.forEach((label) => f.word(label)); f.word(instr.label); break; case "call": @@ -454,14 +476,14 @@ function printInstr(instr: Instr, f: Formatter) { function printType(type: FuncType, f: Formatter) { f.sexpr(() => { - f.word("type"); + f.keyword("type"); printFuncType(type, f); }); } function printImport(import_: Import, f: Formatter) { f.sexpr(() => { - f.word("import"); + f.keyword("import"); printString(import_.module, f); printString(import_.name, f); @@ -471,8 +493,8 @@ function printImport(import_: Import, f: Formatter) { switch (desc.kind) { case "func": f.sexpr(() => { - f.word("type"); - f.word(desc.type); + f.keyword("type"); + f.type(desc.type); }); break; case "table": @@ -491,33 +513,35 @@ function printImport(import_: Import, f: Formatter) { function printFunction(func: Func, f: Formatter) { f.sexpr(() => { - f.word("func"); + f.keyword("func"); printId(func._name, f); f.sexpr(() => { - f.word("type"); - f.word(func.type); + f.keyword("type"); + f.type(func.type); }); - f.breakIndent(); + if (func.locals.length > 0 || func.body.length > 0) { + f.breakIndent(); + } if (func.locals.length > 0) { f.sexpr(() => { - f.word("local"); + f.keyword("local"); func.locals.forEach((local) => printValType(local, f)); }); + f.linebreak(); + } + if (func.body.length > 0) { + printInstrBlock(func.body, f); } - - f.linebreak(); - - printInstrBlock(func.body, f); }); } function printTable(table: Table, f: Formatter) { f.sexpr(() => { - f.word("table"); + f.keyword("table"); printId(table._name, f); printTableType(table.type, f); }); @@ -525,7 +549,7 @@ function printTable(table: Table, f: Formatter) { function printMem(mem: Mem, f: Formatter) { f.sexpr(() => { - f.word("memory"); + f.keyword("memory"); printId(mem._name, f); printLimits(mem.type, f); @@ -534,7 +558,7 @@ function printMem(mem: Mem, f: Formatter) { function printGlobal(global: Global, f: Formatter) { f.sexpr(() => { - f.word("global"); + f.keyword("global"); printId(global._name, f); printGlobalType(global.type, f); @@ -548,7 +572,7 @@ function printExport(export_: Export, f: Formatter) { const desc = export_.desc; f.sexpr(() => { - f.word("export"); + f.keyword("export"); printString(export_.name, f); f.sexpr(() => { @@ -560,7 +584,7 @@ function printExport(export_: Export, f: Formatter) { function printStart(start: Start, f: Formatter) { f.sexpr(() => { - f.word("start"); + f.keyword("start"); f.word(start.func); }); } @@ -573,14 +597,14 @@ function printData(data: Data, f: Formatter) { let mode = data.mode; f.sexpr(() => { - f.word("data"); + f.keyword("data"); printId(data._name, f); if (mode.kind === "active") { const active: DatamodeActive = mode; if (active.memory !== 0) { f.sexpr(() => { - f.word("memory"); + f.keyword("memory"); f.word(active.memory); }); } @@ -589,7 +613,7 @@ function printData(data: Data, f: Formatter) { if (active.offset.length === 1) { printInstr(active.offset[0], f); } else { - f.word("offset"); + f.keyword("offset"); f.linebreak(); printInstrBlock(active.offset, f); } @@ -602,7 +626,7 @@ function printData(data: Data, f: Formatter) { function printModule(module: Module, f: Formatter) { f.sexpr(() => { - f.word("module"); + f.keyword("module"); printId(module._name, f); f.breakIndent();