This commit is contained in:
nora 2023-07-25 22:13:36 +02:00
parent 87b8b8eb28
commit 8b424c0add
5 changed files with 501 additions and 202 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/node_modules /node_modules
/target /target
*.tsbuildinfo *.tsbuildinfo
/*-example.wat

View file

@ -1,15 +1,10 @@
{ {
description = "Example JavaScript development environment for Zero to Nix"; description = "Programming language written in TypeScript compiling to Wasm";
# Flake inputs
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs"; # also valid: "nixpkgs" nixpkgs.url = "github:NixOS/nixpkgs";
}; };
# Flake outputs
outputs = { self, nixpkgs }: outputs = { self, nixpkgs }:
let let
# Systems supported
allSystems = [ allSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux "x86_64-linux" # 64-bit Intel/AMD Linux
"aarch64-linux" # 64-bit ARM Linux "aarch64-linux" # 64-bit ARM Linux
@ -23,12 +18,12 @@
}); });
in in
{ {
# Development environment output
devShells = forAllSystems ({ pkgs }: { devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell { default = pkgs.mkShell {
# The Nix packages provided in the environment
packages = with pkgs; [ packages = with pkgs; [
nodejs-18_x # Node.js 18, plus npm, npx, and corepack nodejs-18_x # Node.js 18, plus npm, npx, and corepack
wasmtime
wasm-tools
]; ];
}; };
}); });

View file

@ -19,11 +19,11 @@ export type Vectype = "v128";
export type Reftype = "funcref" | "externref"; export type Reftype = "funcref" | "externref";
export type Valtype = Numtype | Vectype | Reftype; export type ValType = Numtype | Vectype | Reftype;
export type ResultType = Vec<Valtype>; export type ResultType = Vec<ValType>;
export type Functype = { export type FuncType = {
params: ResultType; params: ResultType;
returns: ResultType; returns: ResultType;
}; };
@ -42,7 +42,7 @@ export type TableType = {
export type GlobalType = { export type GlobalType = {
mut: Mut; mut: Mut;
type: Valtype; type: ValType;
}; };
export type Mut = "const" | "var"; export type Mut = "const" | "var";
@ -50,7 +50,7 @@ export type Mut = "const" | "var";
export type Externtype = export type Externtype =
| { | {
kind: "func"; kind: "func";
type: Functype; type: FuncType;
} }
| { | {
kind: "table"; kind: "table";
@ -152,7 +152,7 @@ export type ReferenceInstr =
export type ParametricInstr = export type ParametricInstr =
| { kind: "drop" } | { kind: "drop" }
| { kind: "select"; type?: Valtype[] }; | { kind: "select"; type?: ValType[] };
// . variable // . variable
@ -223,7 +223,7 @@ export type MemoryInstr =
export type Blocktype = export type Blocktype =
| { kind: "typeidx"; idx: TypeIdx } | { kind: "typeidx"; idx: TypeIdx }
| { kind: "valtype"; type?: Valtype }; | { kind: "valtype"; type?: ValType };
export type ControlInstr = export type ControlInstr =
| { | {
@ -232,7 +232,7 @@ export type ControlInstr =
| { | {
kind: "block" | "loop"; kind: "block" | "loop";
type: Blocktype; type: Blocktype;
instr: Instr[]; instrs: Instr[];
} }
| { | {
kind: "if"; kind: "if";
@ -279,7 +279,7 @@ export type Expr = Instr[];
// Modules // Modules
export type Module = { export type Module = {
types: Vec<Functype>; types: Vec<FuncType>;
funcs: Vec<Func>; funcs: Vec<Func>;
tables: Vec<Table>; tables: Vec<Table>;
mems: Vec<Mem>; mems: Vec<Mem>;
@ -304,7 +304,7 @@ export type LabelIdx = u32;
export type Func = { export type Func = {
type: TypeIdx; type: TypeIdx;
locals: Vec<Valtype>; locals: Vec<ValType>;
body: Expr; body: Expr;
_name?: string; _name?: string;
}; };
@ -333,9 +333,8 @@ export type Data = {
_name?: string; _name?: string;
}; };
export type Datamode = export type DatamodeActive = { kind: "active"; memory: MemIdx; offset: Expr };
| { kind: "passive" } export type Datamode = { kind: "passive" } | DatamodeActive;
| { kind: "active"; memory: MemIdx; offset: Expr };
export type Start = { export type Start = {
func: FuncIdx; func: FuncIdx;
@ -352,7 +351,7 @@ export type ExportDesc =
idx: FuncIdx; idx: FuncIdx;
} }
| { kind: "table"; idx: TableIdx } | { kind: "table"; idx: TableIdx }
| { kind: "mem"; idx: MemIdx } | { kind: "memory"; idx: MemIdx }
| { kind: "global"; idx: GlobalIdx }; | { kind: "global"; idx: GlobalIdx };
export type Import = { export type Import = {

109
src/wasm/wat.test.ts Normal file
View file

@ -0,0 +1,109 @@
import { Module } from "./defs";
import { writeModuleWatToString } from "./wat";
const EXAMPLE_MODULE: Module = {
_name: "example",
types: [
{ params: ["i32"], returns: ["i32"] },
{ params: [], returns: ["i32"] },
],
imports: [
{
module: "left-pad",
name: "padLeft",
desc: {
kind: "func",
type: 0,
},
},
],
funcs: [
{
_name: "addOne",
type: 0,
locals: ["i32", "i32"],
body: [
{ kind: "local.set", imm: 0 },
{
kind: "block",
type: { kind: "typeidx", idx: 1 },
instrs: [{ kind: "local.get", imm: 0 }],
},
{ kind: "i32.const", imm: 1 },
{ kind: "i32.add" },
],
},
],
tables: [
{
type: { reftype: "funcref", limits: { min: 10, max: 20 } },
_name: "cool-table",
},
],
mems: [
{
type: { min: 100, max: 1000 },
_name: "the_memory",
},
],
globals: [
{
type: { mut: "const", type: "i32" },
init: [{ kind: "i32.const", imm: 0 }],
_name: "globalling",
},
],
elems: [],
exports: [
{
name: "addOne",
desc: { kind: "func", idx: 0 },
},
],
datas: [
{
mode: { kind: "passive" },
init: new Uint8Array(),
_name: "meow",
},
{
mode: {
kind: "active",
memory: 0,
offset: [{ kind: "i32.const", imm: 0 }],
},
init: new Uint8Array(),
_name: "very-active-data",
},
],
};
it("should print a Wasm module with the correct formatting", () => {
const wat = writeModuleWatToString(EXAMPLE_MODULE);
expect(wat).toMatchInlineSnapshot(`
"(module $example
(type (func (param i32) (result i32)))
(type (func (param) (result i32)))
(import "left-pad" "padLeft" (func (type 0)))
(func $addOne (type 0)
(local i32 i32)
local.set 0
block (type 1)
local.get 0
end
i32.const 1
i32.add
)
(table $cool-table 10 20 funcref)
(memory $the_memory 100 1000)
(global $globalling i32
i32.const 0
)
(export "addOne" (func 0))
(data $meow "")
(data $very-active-data (i32.const 0) "")
)
"
`);
});

View file

@ -4,10 +4,11 @@
import { import {
Blocktype, Blocktype,
Data, Data,
DatamodeActive,
Elem, Elem,
Export, Export,
Func, Func,
Functype as FuncType, FuncType,
Global, Global,
GlobalType, GlobalType,
Import, Import,
@ -18,125 +19,198 @@ import {
Module, Module,
Start, Start,
Table, Table,
TableType as TableType, TableType,
Valtype as ValType, ValType,
} from "./defs"; } from "./defs";
const INLINE_SYM = Symbol.for("inline"); class Formatter {
const INLINE_OWN_LINE = Symbol.for("inline_own_line"); print: (chunk: string) => void;
indentation: number;
wordsInSexpr: number[];
freshLinebreak: boolean;
type Sexpr = string | number | Sexpr[] | { inline: Symbol; items: Sexpr[] }; constructor(print: (chunk: string) => void) {
this.print = print;
this.indentation = 0;
this.wordsInSexpr = [];
this.freshLinebreak = false;
}
export function writeModuleWat(module: Module) { linebreak() {
const sexprs = sexprModule(module); this.print("\n");
console.dir(sexprs, { depth: 100 }); this.print(" ".repeat(this.indentation));
console.log(printSexpr(sexprs)); this.freshLinebreak = true;
} }
breakIndent() {
this.indentation++;
this.linebreak();
}
breakDedent() {
this.indentation--;
this.linebreak();
}
function printSexpr(sexpr: Sexpr): string { sexpr(f: () => void) {
if (typeof sexpr === "string") { this.startSexpr();
return sexpr; f();
} else if (typeof sexpr === "number") { this.endSexpr();
return String(sexpr); }
} else if (typeof sexpr === "object" && "inline" in sexpr) {
return sexpr.items.map(printSexpr).join(" "); word(word: string | number) {
} else { const last = this.wordsInSexpr.length - 1;
const all = sexpr.map(printSexpr).join(" "); if (this.wordsInSexpr[last] > 0 && !this.freshLinebreak) {
return `(${all})`; // The first word hugs the left parenthesis.
this.print(" ");
}
this.freshLinebreak = false;
this.print(String(word));
this.wordsInSexpr[last]++;
}
startSexpr() {
this.word("(");
this.wordsInSexpr.push(0);
}
endSexpr() {
this.print(")");
this.freshLinebreak = false;
this.wordsInSexpr.pop();
} }
} }
function inline(items: Sexpr[]): Sexpr { export function writeModuleWatToString(module: Module): string {
return { inline: INLINE_SYM, items }; const parts: string[] = [];
const writer = (s: string) => parts.push(s);
printModule(module, new Formatter(writer));
return parts.join("");
} }
function inlineOwnLine(items: Sexpr[]): Sexpr { export function writeModuleWat(module: Module, f: Formatter) {
return { inline: INLINE_OWN_LINE, items }; printModule(module, f);
} }
// base // base
function sexprString(s: string): Sexpr { function printString(s: string, f: Formatter) {
// TODO: escaping // TODO: escaping
return `"$${s}"`; f.word(`"${s}"`);
} }
function sexprBinaryString(buf: Uint8Array): Sexpr { function printBinaryString(buf: Uint8Array, f: Formatter) {
todo(); f.word(`"${buf.toString()}"`);
} }
function optArr<T>(elem?: T): T[] { function printId(id: string | undefined, f: Formatter) {
return elem !== undefined ? [elem] : []; if (id) {
f.word(`$${id}`);
}
} }
// types // types
function sexprValtype(type: ValType): Sexpr { function printValType(type: ValType, f: Formatter) {
return type; f.word(type);
} }
function sexprFuncType(type: FuncType): Sexpr { function printFuncType(type: FuncType, f: Formatter) {
return ["func", ["param", ...type.params], ["result", ...type.returns]]; f.sexpr(() => {
f.word("func");
f.sexpr(() => {
f.word("param");
type.params.forEach(f.word.bind(f));
});
f.sexpr(() => {
f.word("result");
type.returns.forEach(f.word.bind(f));
});
});
} }
function sexprLimits(limits: Limits): Sexpr { function printLimits(limits: Limits, f: Formatter) {
return inline([limits.min, limits.max]); f.word(limits.min);
f.word(limits.max);
} }
function sexprTableType(type: TableType): Sexpr { function printTableType(type: TableType, f: Formatter) {
todo(); printLimits(type.limits, f);
f.word(type.reftype);
} }
function sexprGlobalType(type: GlobalType): Sexpr { function printGlobalType(type: GlobalType, f: Formatter) {
return type.mut === "const" if (type.mut === "const") {
? sexprValtype(type.type) printValType(type.type, f);
: ["mut", sexprValtype(type.type)]; } else {
f.sexpr(() => {
f.word("mut");
printValType(type.type, f);
});
}
} }
// instrs // instrs
function sexprBlockType(type: Blocktype): Sexpr { function printBlockType(type: Blocktype, f: Formatter) {
return type.kind === "typeidx" f.sexpr(() => {
? type.idx f.word("type");
: type.type if (type.kind === "typeidx") {
? sexprValtype(type.type) f.word(type.idx);
: inline([]); } else if (type.type !== undefined) {
printValType(type.type, f);
}
});
} }
function sexprMemarg(arg: MemArg): Sexpr { function printMemarg(arg: MemArg, f: Formatter) {
const align = arg.align !== undefined ? `align=${arg.align}` : ""; if (arg.align !== undefined) {
const offset = arg.offset /*0->false*/ ? `offset=${arg.offset}` : ""; f.word(`align=${arg.align}`);
return inline([offset, align]); }
if (arg.offset /*0->false*/) {
`offset=${arg.offset}`;
}
} }
function sexprInstr(instr: Instr): Sexpr { /**
* Print a list of instructions with one extra indentation.
* Start: indented start of first instr
* End: start of next line
*/
function printInstrBlock(instrs: Instr[], f: Formatter) {
instrs.forEach((nested, i) => {
printInstr(nested, f);
if (i !== instrs.length - 1) {
f.linebreak();
}
});
f.breakDedent();
}
function printInstr(instr: Instr, f: Formatter) {
switch (instr.kind) { switch (instr.kind) {
case "block": case "block":
case "loop": case "loop":
return inlineOwnLine([ f.word(instr.kind);
instr.kind, printBlockType(instr.type, f);
sexprBlockType(instr.type), f.breakIndent();
...instr.instr.map(sexprInstr), printInstrBlock(instr.instrs, f);
"end", f.word("end");
]); break;
case "if": case "if":
if (instr.else.length === 0) { if (instr.else.length === 0) {
return inlineOwnLine([ f.word(instr.kind);
instr.kind, printBlockType(instr.type, f);
sexprBlockType(instr.type), f.breakIndent();
...instr.then.map(sexprInstr), printInstrBlock(instr.then, f);
"end", f.word("end");
]);
} else { } else {
return inlineOwnLine([ f.word(instr.kind);
instr.kind, printBlockType(instr.type, f);
sexprBlockType(instr.type), f.breakIndent();
...instr.then.map(sexprInstr), printInstrBlock(instr.then, f);
"else", f.word("else");
...instr.else.map(sexprInstr), f.breakIndent();
"end", printInstrBlock(instr.else, f);
]); f.word("end");
} }
break;
case "unreachable": case "unreachable":
case "nop": case "nop":
case "return": case "return":
@ -292,16 +366,27 @@ function sexprInstr(instr: Instr): Sexpr {
case "i64.extend8_s": case "i64.extend8_s":
case "i64.extend16_s": case "i64.extend16_s":
case "i64.extend32_s": case "i64.extend32_s":
return instr.kind; f.word(instr.kind);
break;
case "br": case "br":
case "br_if": case "br_if":
return inlineOwnLine([instr.kind, instr.label]); f.word(instr.kind);
f.word(instr.label);
break;
case "br_table": case "br_table":
return inlineOwnLine([instr.kind, ...instr.labels, instr.label]); f.word(instr.kind);
instr.labels.forEach(f.word.bind(f));
f.word(instr.label);
break;
case "call": case "call":
return inlineOwnLine([instr.kind, instr.func]); f.word(instr.kind);
f.word(instr.func);
break;
case "call_indirect": case "call_indirect":
return inlineOwnLine([instr.kind, instr.table, instr.type]); f.word(instr.kind);
f.word(instr.table);
f.word(instr.type);
break;
case "i32.const": case "i32.const":
case "i64.const": case "i64.const":
case "f32.const": case "f32.const":
@ -321,15 +406,19 @@ function sexprInstr(instr: Instr): Sexpr {
case "elem.drop": case "elem.drop":
case "memory.init": case "memory.init":
case "data.drop": case "data.drop":
return inlineOwnLine([instr.kind, instr.imm]); f.word(instr.kind);
f.word(instr.imm);
break;
case "select": case "select":
return inlineOwnLine([ f.word(instr.kind);
instr.kind, instr.type?.forEach((type) => printValType(type, f));
...optArr(instr.type?.map(sexprValtype)), break;
]);
case "table.copy": case "table.copy":
case "table.init": case "table.init":
return inlineOwnLine([instr.kind, instr.imm1, instr.imm2]); f.word(instr.kind);
f.word(instr.imm1);
f.word(instr.imm2);
break;
case "i32.load": case "i32.load":
case "i64.load": case "i64.load":
case "f32.load": case "f32.load":
@ -355,124 +444,230 @@ function sexprInstr(instr: Instr): Sexpr {
case "i64.store32": case "i64.store32":
case "v128.load": case "v128.load":
case "v128.store": case "v128.store":
return inlineOwnLine([instr.kind, sexprMemarg(instr.imm)]); f.word(instr.kind);
printMemarg(instr.imm, f);
break;
} }
} }
// modules // modules
function sexprType(type: FuncType): Sexpr { function printType(type: FuncType, f: Formatter) {
return ["type", sexprFuncType(type)]; f.sexpr(() => {
f.word("type");
printFuncType(type, f);
});
} }
function sexprImport(import_: Import): Sexpr { function printImport(import_: Import, f: Formatter) {
const desc = import_.desc; f.sexpr(() => {
let importDesc: Sexpr; f.word("import");
switch (desc.kind) { printString(import_.module, f);
case "func": printString(import_.name, f);
importDesc = ["type", desc.type];
break;
case "table":
importDesc = sexprTableType(desc.type);
break;
case "memory":
importDesc = sexprLimits(desc.type);
break;
case "global":
importDesc = sexprGlobalType(desc.type);
break;
}
return [ const desc = import_.desc;
"import", f.sexpr(() => {
sexprString(import_.module), f.word(desc.kind);
sexprString(import_.name), switch (desc.kind) {
[desc.kind, importDesc], case "func":
]; f.sexpr(() => {
f.word("type");
f.word(desc.type);
});
break;
case "table":
printTableType(desc.type, f);
break;
case "memory":
printLimits(desc.type, f);
break;
case "global":
printGlobalType(desc.type, f);
break;
}
});
});
} }
function sexprFunction(func: Func): Sexpr { function printFunction(func: Func, f: Formatter) {
return [ f.sexpr(() => {
"func", f.word("func");
...optArr(func._name), printId(func._name, f);
["type", func.type],
...func.locals.map((local) => ["local", sexprValtype(local)]), f.sexpr(() => {
...func.body.map((instr) => sexprInstr(instr)), f.word("type");
]; f.word(func.type);
});
f.breakIndent();
if (func.locals.length > 0) {
f.sexpr(() => {
f.word("local");
func.locals.forEach((local) => printValType(local, f));
});
}
f.linebreak();
printInstrBlock(func.body, f);
});
} }
function sexprTable(table: Table): Sexpr { function printTable(table: Table, f: Formatter) {
return ["table", ...optArr(table._name), sexprTableType(table.type)]; f.sexpr(() => {
f.word("table");
printId(table._name, f);
printTableType(table.type, f);
});
} }
function sexprMem(mem: Mem): Sexpr { function printMem(mem: Mem, f: Formatter) {
return ["memory", ...optArr(mem._name), sexprLimits(mem.type)]; f.sexpr(() => {
f.word("memory");
printId(mem._name, f);
printLimits(mem.type, f);
});
} }
function sexprGlobal(global: Global): Sexpr { function printGlobal(global: Global, f: Formatter) {
return [ f.sexpr(() => {
"global", f.word("global");
...optArr(global._name), printId(global._name, f);
sexprGlobalType(global.type),
...global.init.map(sexprInstr), printGlobalType(global.type, f);
];
f.breakIndent();
printInstrBlock(global.init, f);
});
} }
function sexprExport(export_: Export): Sexpr {
function printExport(export_: Export, f: Formatter) {
const desc = export_.desc; const desc = export_.desc;
let exportDesc;
switch (desc.kind) { f.sexpr(() => {
case "func": f.word("export");
exportDesc = ["func", desc.idx]; printString(export_.name, f);
break;
case "table": f.sexpr(() => {
exportDesc = ["table", desc.idx]; f.word(desc.kind);
break; f.word(desc.idx);
case "mem": });
exportDesc = ["memory", desc.idx]; });
break;
case "global":
exportDesc = ["global", desc.idx];
break;
}
return ["export", export_.name, exportDesc];
} }
function sexprStart(start: Start): Sexpr { function printStart(start: Start, f: Formatter) {
return ["start", start.func]; f.sexpr(() => {
f.word("start");
f.word(start.func);
});
} }
function sexprElem(_elem: Elem): Sexpr {
function printElem(_elem: Elem, f: Formatter) {
todo(); todo();
} }
function sexprData(data: Data): Sexpr {
function printData(data: Data, f: Formatter) {
let mode = data.mode; let mode = data.mode;
if (mode.kind === "passive") {
return ["data", ...optArr(data._name), sexprBinaryString(data.init)]; f.sexpr(() => {
} else { f.word("data");
return [ printId(data._name, f);
"data",
...optArr(data._name), if (mode.kind === "active") {
...optArr(mode.memory === 0 ? undefined : ["memory", mode.memory]), const active: DatamodeActive = mode;
["offset", ...mode.offset.map(sexprInstr)], if (active.memory !== 0) {
sexprBinaryString(data.init), f.sexpr(() => {
]; f.word("memory");
} f.word(active.memory);
});
}
f.sexpr(() => {
if (active.offset.length === 1) {
printInstr(active.offset[0], f);
} else {
f.word("offset");
f.linebreak();
printInstrBlock(active.offset, f);
}
});
}
printBinaryString(data.init, f);
});
} }
function sexprModule(module: Module): Sexpr { function printModule(module: Module, f: Formatter) {
return [ f.sexpr(() => {
"module", f.word("module");
...optArr(module._name), printId(module._name, f);
...module.types.map(sexprType), f.breakIndent();
...module.imports.map(sexprImport),
...module.funcs.map(sexprFunction), let hasAnything = false;
...module.tables.map(sexprTable), const breakIfAny = () => {
...module.mems.map(sexprMem), if (hasAnything) {
...module.globals.map(sexprGlobal), f.linebreak();
...module.exports.map(sexprExport), }
...optArr(module.start && sexprStart(module.start)), hasAnything = true;
...module.elems.map(sexprElem), };
...module.datas.map(sexprData),
]; module.types.forEach((type) => {
breakIfAny();
printType(type, f);
});
module.imports.forEach((import_) => {
breakIfAny();
printImport(import_, f);
});
module.funcs.forEach((func) => {
breakIfAny();
printFunction(func, f);
});
module.tables.forEach((table) => {
breakIfAny();
printTable(table, f);
});
module.mems.forEach((mem) => {
breakIfAny();
printMem(mem, f);
});
module.globals.forEach((global) => {
breakIfAny();
printGlobal(global, f);
});
module.exports.forEach((export_) => {
breakIfAny();
printExport(export_, f);
});
if (module.start) {
breakIfAny();
printStart(module.start, f);
}
module.elems.forEach((elem) => {
breakIfAny();
printElem(elem, f);
});
module.datas.forEach((data) => {
breakIfAny();
printData(data, f);
});
f.breakDedent();
});
f.linebreak();
} }
function todo(): never { function todo(): never {