protoverse

A metaverse protocol
git clone git://jb55.com/protoverse
Log | Files | Refs | README | LICENSE

commit b688f8167ef5e24dc94e24b781bf68833f749328
parent e482eb80dfa279758ac403286b60bdad98526911
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 21 Nov 2020 03:00:38 -0800

Initial wasm parser

Diffstat:
M.gitignore | 3+++
MMakefile | 17+++++++++++++++--
Mdefault.nix | 2+-
Msrc/cursor.c | 21++++++++++++++++++---
Msrc/cursor.h | 1+
Msrc/io.c | 18++++++++++++++++++
Msrc/io.h | 1+
Asrc/parser.c | 33+++++++++++++++++++++++++++++++++
Asrc/parser.h | 12++++++++++++
Msrc/protoverse.c | 19++++++++++++++++++-
Asrc/wasm.c | 490+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/wasm.h | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awasm/func.wat | 5+++++
Awasm/hello-2.wat | 6++++++
Awasm/hello.wat | 1+
Awasm/module.wat | 1+
16 files changed, 790 insertions(+), 7 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,5 +1,8 @@ *.o +.build-result /protoverse /libprotoverse.a /TAGS /test +*.wasm +/tags diff --git a/Makefile b/Makefile @@ -1,7 +1,17 @@ CFLAGS = -Wno-error=unused-function -O1 -g -std=c89 -Wall -Wextra -Werror -Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes -Wmissing-declarations -Wdeclaration-after-statement -OBJS = src/io.o src/parse.o src/cursor.o src/describe.o src/serve.o src/client.o src/net.o src/varint.o src/util.o +OBJS = src/io.o \ + src/parse.o \ + src/cursor.o \ + src/describe.o \ + src/serve.o \ + src/client.o \ + src/net.o \ + src/varint.o \ + src/util.o \ + src/parser.o \ + src/wasm.o all: protoverse libprotoverse.a @@ -9,6 +19,9 @@ all: protoverse libprotoverse.a @echo "cc $<" @$(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $< +%.wasm: %.wat + wat2wasm $^ + protoverse: src/protoverse.c $(OBJS) @echo "ld $@" @$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@ @@ -17,7 +30,7 @@ libprotoverse.a: $(OBJS) ar rcs $@ $^ clean: - rm -f protoverse test $(OBJS) + rm -f protoverse test $(OBJS) libprotoverse.a test: src/test.c $(OBJS) $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@ diff --git a/default.nix b/default.nix @@ -2,5 +2,5 @@ with pkgs; stdenv.mkDerivation { name = "protoverse"; - nativeBuildInputs = [ gdb ]; + nativeBuildInputs = [ gdb wabt ]; } diff --git a/src/cursor.c b/src/cursor.c @@ -6,6 +6,21 @@ #include <stdio.h> #include <string.h> +void *cursor_alloc(struct cursor *mem, unsigned long size) +{ + void *ret; + + if (mem->p + size > mem->end) { + return NULL; + } + + ret = mem->p; + memset(ret, 0, size); + mem->p += size; + + return ret; +} + void copy_cursor(struct cursor *src, struct cursor *dest) { dest->start = src->start; @@ -40,7 +55,7 @@ int pull_byte(struct cursor *cursor, u8 *c) int push_byte(struct cursor *cursor, u8 c) { - if (cursor->p + 1 >= cursor->end) { + if (cursor->p + 1 > cursor->end) { return 0; } @@ -57,7 +72,7 @@ int pull_data_into_cursor(struct cursor *cursor, { int ok; - if (dest->p + len >= dest->end) { + if (dest->p + len > dest->end) { printf("not enough room in dest buffer\n"); return 0; } @@ -73,7 +88,7 @@ int pull_data_into_cursor(struct cursor *cursor, int pull_data(struct cursor *cursor, u8 *data, int len) { - if (cursor->p + len >= cursor->end) { + if (cursor->p + len > cursor->end) { return 0; } diff --git a/src/cursor.h b/src/cursor.h @@ -9,6 +9,7 @@ struct cursor { }; +void *cursor_alloc(struct cursor *mem, unsigned long size); void copy_cursor(struct cursor *src, struct cursor *dest); int cursor_index(struct cursor *cursor, int elem_size); void make_cursor(unsigned char *start, unsigned char *end, struct cursor *cursor); diff --git a/src/io.c b/src/io.c @@ -2,6 +2,10 @@ #include "io.h" #include <string.h> +#include <sys/stat.h> +#include <sys/mman.h> +#include <unistd.h> +#include <fcntl.h> int read_fd(FILE *fd, unsigned char *buf, int buflen, int *written) { @@ -20,6 +24,20 @@ int read_fd(FILE *fd, unsigned char *buf, int buflen, int *written) return 1; } +int map_file(const char *filename, unsigned char **p, size_t *flen) +{ + struct stat st; + int des; + stat(filename, &st); + *flen = st.st_size; + + des = open(filename, O_RDONLY); + + *p = mmap(NULL, *flen, PROT_READ, MAP_PRIVATE, des, 0); + close(des); + + return *p != MAP_FAILED; +} int read_file(const char *filename, unsigned char *buf, int buflen, int *written) { diff --git a/src/io.h b/src/io.h @@ -7,6 +7,7 @@ int read_fd(FILE *fd, unsigned char *buf, int buflen, int *written); int read_file(const char *filename, unsigned char *buf, int buflen, int *written); int read_file_or_stdin(const char *filename, unsigned char *buf, int buflen, int *written); +int map_file(const char *filename, unsigned char **p, size_t *flen); #endif /* PROTOVERSE_IO_H */ diff --git a/src/parser.c b/src/parser.c @@ -0,0 +1,33 @@ + +#include "parser.h" +#include <stdio.h> + + +int consume_bytes(struct cursor *cursor, const unsigned char *match, int len) +{ + int i; + + if (cursor->p + len > cursor->end) { + fprintf(stderr, "consume_bytes overflow\n"); + return 0; + } + + for (i = 0; i < len; i++) { + if (cursor->p[i] != match[i]) + return 0; + } + + cursor->p += len; + + return 1; +} + +int consume_byte(struct cursor *cursor, unsigned char match) +{ + return consume_bytes(cursor, &match, 1); +} + +int consume_u32(struct cursor *cursor, unsigned int match) +{ + return consume_bytes(cursor, (unsigned char*)&match, sizeof(match)); +} diff --git a/src/parser.h b/src/parser.h @@ -0,0 +1,12 @@ + +#ifndef CURSOR_PARSER +#define CURSOR_PARSER + +#include "cursor.h" + +int consume_bytes(struct cursor *cur, const unsigned char *match, int len); +int consume_byte(struct cursor *cur, const unsigned char match); +int consume_u32(struct cursor *cur, unsigned int match); + +#endif /* CURSOR_PARSER */ + diff --git a/src/protoverse.c b/src/protoverse.c @@ -4,10 +4,12 @@ #include "describe.h" #include "serve.h" #include "client.h" +#include "wasm.h" #include <assert.h> #include <stdlib.h> #include <string.h> +#include <sys/mman.h> #define streq(a, b) strcmp(a,b) == 0 @@ -70,6 +72,7 @@ static int usage(void) printf(" parse file.space\n"); printf(" serve file.space\n"); printf(" client\n"); + printf(" run code.wasm\n"); return 1; } @@ -78,12 +81,14 @@ static int usage(void) int main(int argc, const char *argv[]) { - const char *space; + const char *space, *code_file; const char *cmd; + unsigned char *wasm_data; struct parser parser; struct protoverse_server server; u16 root; int ok; + size_t len; if (argc < 2) return usage(); @@ -115,6 +120,18 @@ int main(int argc, const char *argv[]) protoverse_serve(&server); } else if (streq(cmd, "client")) { protoverse_connect("127.0.0.1", 1988); + } else if (streq(cmd, "run")) { + if (argc != 3) + return usage(); + code_file = argv[2]; + if (!map_file(code_file, &wasm_data, &len)) { + perror("mmap"); + return 1; + } + if (!run_wasm(wasm_data, len)) { + return 2; + } + munmap(wasm_data, len); } return 0; diff --git a/src/wasm.c b/src/wasm.c @@ -0,0 +1,490 @@ + +#include "wasm.h" +#include "parser.h" + +#include <stdarg.h> +#include <stdio.h> +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +#define note_error(p, fmt, ...) note_error_(p, "%s: " fmt, __FUNCTION__, ##__VA_ARGS__) + +#define ERR_STACK_SIZE 16 + +struct parse_error { + int pos; + char *msg; + struct parse_error *next; +}; + +struct wasm_parser { + struct module module; + struct cursor cur; + struct cursor mem; + struct parse_error *errors; +}; + +#ifdef DEBUG +static void log_dbg_(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +#define log_dbg(...) log_dbg_(__VA_ARGS__) +#else +#define log_dbg(...) +#endif + + +static void note_error_(struct wasm_parser *p, const char *fmt, ...) +{ + static char buf[512]; + struct parse_error err; + struct parse_error *perr, *new_err; + + va_list ap; + va_start(ap, fmt); + vsprintf(buf, fmt, ap); + va_end(ap); + + perr = NULL; + err.msg = (char*)p->mem.p; + err.pos = p->cur.p - p->cur.start; + err.next = NULL; + + if (!push_str(&p->mem, buf)) { + fprintf(stderr, "arena OOM when recording parse error, "); + fprintf(stderr, "mem->p at %ld, remaining %ld, strlen %ld\n", + p->mem.p - p->mem.start, + p->mem.end - p->mem.p, + strlen(buf)); + return; + } + + new_err = (struct parse_error *)p->mem.p; + + if (!push_data(&p->mem, (unsigned char*)&err, sizeof(err))) { + fprintf(stderr, "arena OOM when pushing data, "); + fprintf(stderr, "mem->p at %ld, remaining %ld, data size %ld\n", + p->mem.p - p->mem.start, + p->mem.end - p->mem.p, + sizeof(err)); + return; + } + + for (perr = p->errors; perr != NULL;) { + if (perr == NULL || perr->next == NULL) + break; + perr = perr->next; + } + + if (p->errors == NULL) { + p->errors = new_err; + } else { + perr->next = new_err; + } +} + +static void print_parse_backtrace(struct wasm_parser *p) +{ + struct parse_error *err; + for (err = p->errors; err != NULL; err = err->next) { + fprintf(stderr, "%08x:%s\n", err->pos, err->msg); + } +} + +static const char *valtype_name(enum valtype valtype) +{ + switch (valtype) { + case i32: return "i32"; + case i64: return "i64"; + case f32: return "f32"; + case f64: return "f64"; + } + + return "unk"; +} + +static void print_functype(struct functype *ft) +{ + int i; + + for (i = 0; i < ft->params.num_valtypes; i++) { + printf("%s ", valtype_name(ft->params.valtypes[i])); + } + printf("-> "); + for (i = 0; i < ft->result.num_valtypes; i++) { + printf("%s ", valtype_name(ft->result.valtypes[i])); + } + printf("\n"); +} + +static void print_module(struct module *module) +{ + int i; + printf("%d functypes:\n", module->type_section.num_functypes); + for (i = 0; i < module->type_section.num_functypes; i++) { + print_functype(&module->type_section.functypes[i]); + } +} + +/* I DONT NEED THIS (yet?) */ +/* +static int leb128_write(struct cursor *write, unsigned int val) +{ + unsigned char byte; + while (1) { + byte = value & 0x7F; + value >>= 7; + if (value == 0) { + if (!push_byte(write, byte)) + return 0; + return 1; + } else { + if (!push_byte(write, byte | 0x80)) + return 0; + } + } +} +*/ + +#define BYTE_AT(type, i, shift) (((type)(p[i]) & 0x7f) << (shift)) +#define LEB128_1(type) (BYTE_AT(type, 0, 0)) +#define LEB128_2(type) (BYTE_AT(type, 1, 7) | LEB128_1(type)) +#define LEB128_3(type) (BYTE_AT(type, 2, 14) | LEB128_2(type)) +#define LEB128_4(type) (BYTE_AT(type, 3, 21) | LEB128_3(type)) +#define LEB128_5(type) (BYTE_AT(type, 4, 28) | LEB128_4(type)) + +static int leb128_read(struct cursor *read, unsigned int *val) +{ + unsigned char p[5]; + unsigned char *start; + + start = read->p; + *val = 0; + + if (pull_byte(read, &p[0]) && (p[0] & 0x80) == 0) { + *val = LEB128_1(unsigned int); + return 1; + } else if (pull_byte(read, &p[1]) && (p[1] & 0x80) == 0) { + *val = LEB128_2(unsigned int); + return 2; + } else if (pull_byte(read, &p[2]) && (p[2] & 0x80) == 0) { + *val = LEB128_3(unsigned int); + return 3; + } else if (pull_byte(read, &p[3]) && (p[3] & 0x80) == 0) { + *val = LEB128_4(unsigned int); + return 4; + } else if (pull_byte(read, &p[4]) && (p[4] & 0x80) == 0) { + if (!(p[4] & 0xF0)) { + *val = LEB128_5(unsigned int); + return 5; + } + } + + /* reset if we're missing */ + read->p = start; + return 0; +} + +static int parse_section_tag(struct cursor *cur, enum section_tag *section) +{ + unsigned char byte; + unsigned char *start; + assert(section); + + start = cur->p; + + if (!pull_byte(cur, &byte)) { + return 0; + } + + if (byte >= num_sections) { + cur->p = start; + return 0; + } + + *section = (enum section_tag)byte; + return 1; +} + +static int parse_valtype(struct wasm_parser *p, unsigned char *valtype) +{ + unsigned char *start; + + start = p->cur.p; + + if (!pull_byte(&p->cur, valtype)) { + note_error(p, "valtype tag oob"); + return 0; + } + + switch ((enum valtype)*valtype) { + case i32: + case i64: + case f32: + case f64: + return 1; + } + + p->cur.p = start; + note_error(p, "%c is not a valid valtype tag", *valtype); + return 0; +} + +static int parse_result_type(struct wasm_parser *p, struct resulttype *rt) +{ + int i, elems; + unsigned char valtype; + unsigned char *start; + + rt->num_valtypes = 0; + rt->valtypes = 0; + start = p->mem.p; + + if (!leb128_read(&p->cur, (unsigned int*)&elems)) { + note_error(p, "vec len"); + return 0; + } + + for (i = 0; i < elems; i++) + { + if (!parse_valtype(p, &valtype)) { + note_error(p, "valtype #%d", i); + p->mem.p = start; + return 0; + } + + if (!push_byte(&p->mem, valtype)) { + note_error(p, "valtype push data OOM #%d", i); + p->mem.p = start; + return 0; + } + } + + rt->num_valtypes = elems; + rt->valtypes = start; + + return 1; +} + + +static int parse_func_type(struct wasm_parser *p, struct functype *func) +{ + if (!consume_byte(&p->cur, FUNC_TYPE_TAG)) { + note_error(p, "type tag"); + return 0; + } + + if (!parse_result_type(p, &func->params)) { + note_error(p, "params"); + return 0; + } + + if (!parse_result_type(p, &func->result)) { + note_error(p, "result"); + return 0; + } + + return 1; +} + + +/* type section is just a vector of function types */ +static int parse_type_section(struct wasm_parser *p, struct typesec *typesec) +{ + unsigned int elems, i; + struct functype *functypes; + + typesec->num_functypes = 0; + typesec->functypes = NULL; + + if (!leb128_read(&p->cur, &elems)) { + fprintf(stderr, "what\n"); + note_error(p, "functypes vec len"); + return 0; + } + + functypes = cursor_alloc(&p->mem, elems * sizeof(struct functype)); + + if (!functypes) { + /* can't use note_error because we're oom */ + fprintf(stderr, "could not allocate memory for type section\n"); + return 0; + } + + for (i = 0; i < elems; i++) { + if (!parse_func_type(p, &functypes[i])) { + note_error(p, "functype #%d", i); + return 0; + } + } + + typesec->functypes = functypes; + typesec->num_functypes = elems; + + return 1; +} + +static int parse_section_by_tag(struct wasm_parser *p, + enum section_tag tag, unsigned int size) +{ + (void)size; + switch (tag) { + case section_custom: + note_error(p, "section_custom parse not implemented"); + return 0; + case section_type: + if (!parse_type_section(p, &p->module.type_section)) { + fprintf(stderr,"what\n"); + note_error(p, "type section"); + return 0; + } + return 1; + case section_import: + note_error(p, "section_import parse not implemented"); + return 0; + case section_function: + note_error(p, "section_function parse not implemented"); + return 0; + case section_table: + note_error(p, "section_table parse not implemented"); + return 0; + case section_memory: + note_error(p, "section_memory parse not implemented"); + return 0; + case section_global: + note_error(p, "section_global parse not implemented"); + return 0; + case section_export: + note_error(p, "section_export parse not implemented"); + return 0; + case section_start: + note_error(p, "section_start parse not implemented"); + return 0; + case section_element: + note_error(p, "section_element parse not implemented"); + return 0; + case section_code: + note_error(p, "section_code parse not implemented"); + return 0; + case section_data: + note_error(p, "section_data parse not implemented"); + return 0; + default: + note_error(p, "invalid section tag"); + return 0; + } + + return 1; +} + +static const char *section_name(enum section_tag tag) +{ + switch (tag) { + case section_custom: + return "custom"; + case section_type: + return "type"; + case section_import: + return "import"; + case section_function: + return "function"; + case section_table: + return "table"; + case section_memory: + return "memory"; + case section_global: + return "global"; + case section_export: + return "export"; + case section_start: + return "start"; + case section_element: + return "element"; + case section_code: + return "code"; + case section_data: + return "data"; + default: + return "invalid"; + } + +} + +static int parse_section(struct wasm_parser *p) +{ + enum section_tag tag; + struct section; + unsigned int bytes; + + if (!parse_section_tag(&p->cur, &tag)) { + note_error(p, "section tag"); + return 0; + } + + if (!leb128_read(&p->cur, &bytes)) { + note_error(p, "section len"); + return 0; + } + + if (!parse_section_by_tag(p, tag, bytes)) { + note_error(p, "%s (%d bytes)", section_name(tag), bytes); + return 0; + } + + return 1; +} + +static int parse_wasm(struct wasm_parser *p) +{ + if (!consume_bytes(&p->cur, WASM_MAGIC, sizeof(WASM_MAGIC))) { + note_error(p, "magic"); + goto fail; + } + + if (!consume_u32(&p->cur, WASM_VERSION)) { + note_error(p, "version"); + goto fail; + } + + while (1) { + if (!parse_section(p)) { + note_error(p, "section"); + goto fail; + } + } + + return 1; + +fail: + printf("parse failure backtrace:\n"); + print_parse_backtrace(p); + printf("\npartially parsed module:\n"); + print_module(&p->module); + return 0; +} + +int run_wasm(unsigned char *wasm, unsigned long len) +{ + struct wasm_parser p; + + void *mem; + int ok, arena_size; + + arena_size = len * 16; + memset(&p, 0, sizeof(p)); + mem = malloc(arena_size); + assert(mem); + + make_cursor(wasm, wasm + len, &p.cur); + make_cursor(mem, mem + arena_size, &p.mem); + + ok = parse_wasm(&p); + free(mem); + return ok; +} diff --git a/src/wasm.h b/src/wasm.h @@ -0,0 +1,167 @@ + +#ifndef WASM +#define WASM + +static const unsigned char WASM_MAGIC[] = {0,'a','s','m'}; +#define WASM_VERSION 0x01 +#define MAX_U32_LEB128_BYTES 5 +#define MAX_U64_LEB128_BYTES 10 + +#define FUNC_TYPE_TAG 0x60 + +struct resulttype { + unsigned char *valtypes; /* enum valtype */ + int num_valtypes; +}; + +struct functype { + struct resulttype params; + struct resulttype result; +}; + +struct typesec { + struct functype *functypes; + int num_functypes; +}; + +struct module { + struct typesec type_section; +}; + +enum valtype { + i32 = 0x7F, + i64 = 0x7E, + f32 = 0x7D, + f64 = 0x7C, +}; + +enum limits { + limit_min = 0x00, + limit_min_max = 0x01, +}; + +enum section_tag { + section_custom, + section_type, + section_import, + section_function, + section_table, + section_memory, + section_global, + section_export, + section_start, + section_element, + section_code, + section_data, + num_sections, +}; + +struct section { + enum section_tag tag; +}; + +enum instr { + /* control instructions */ + i_unreachable = 0x00, + i_nop = 0x01, + i_block = 0x02, + i_loop = 0x03, + i_if = 0x04, + i_else = 0x05, + i_end = 0x0B, + i_br = 0x0C, + i_br_if = 0x0D, + i_br_table = 0x0E, + i_return = 0x0F, + i_call = 0x10, + i_call_indirect = 0x11, + + /* parametric instructions */ + i_drop = 0x1A, + i_select = 0x1B, + + /* variable instructions */ + i_local_get = 0x20, + i_local_set = 0x21, + i_local_tee = 0x22, + i_global_get = 0x23, + i_global_set = 0x24, + + /* memory instructions */ + i_i32_load = 0x28, + i_i64_load = 0x29, + i_f32_load = 0x2A, + i_f64_load = 0x2B, + i_i32_load8_s = 0x2C, + i_i32_load8_u = 0x2D, + i_i32_load16_s = 0x2E, + i_i32_load16_u = 0x2F, + i_i64_load8_s = 0x30, + i_i64_load8_u = 0x31, + i_i64_load16_s = 0x32, + i_i64_load16_u = 0x33, + i_i64_load32_s = 0x34, + i_i64_load32_u = 0x35, + i_i32_store = 0x36, + i_i64_store = 0x37, + i_f32_store = 0x38, + i_f64_store = 0x39, + i_i32_store8 = 0x3A, + i_i32_store16 = 0x3B, + i_i64_store8 = 0x3C, + i_i64_store16 = 0x3D, + i_i64_store32 = 0x3E, + i_memory_size = 0x3F, + i_memory_grow = 0x40, + + /* numeric instructions */ + i_i32_const = 0x41, + i_i64_const = 0x42, + i_f32_const = 0x43, + i_f64_const = 0x44, + + i_i32_eqz = 0x45, + i_i32_eq = 0x46, + i_i32_ne = 0x47, + i_i32_lt_s = 0x48, + i_i32_lt_u = 0x49, + i_i32_gt_s = 0x4A, + i_i32_gt_u = 0x4B, + i_i32_le_s = 0x4C, + i_i32_le_u = 0x4D, + i_i32_ge_s = 0x4E, + i_i32_ge_u = 0x4F, + + i_i64_eqz = 0x50, + i_i64_eq = 0x51, + i_i64_ne = 0x52, + i_i64_lt_s = 0x53, + i_i64_lt_u = 0x54, + i_i64_gt_s = 0x55, + i_i64_gt_u = 0x56, + i_i64_le_s = 0x57, + i_i64_le_u = 0x58, + i_i64_ge_s = 0x59, + i_i64_ge_u = 0x5A, + + i_f32_eq = 0x5B, + i_f32_ne = 0x5C, + i_f32_lt = 0x5D, + i_f32_gt = 0x5E, + i_f32_le = 0x5F, + i_f32_ge = 0x60, + + i_f64_eq = 0x61, + i_f64_ne = 0x62, + i_f64_lt = 0x63, + i_f64_gt = 0x64, + i_f64_le = 0x65, + i_f64_ge = 0x66, + + i_i32_clz = 0x67, + /* TODO: more instrs */ +}; + +int run_wasm(unsigned char *wasm, unsigned long len); + +#endif /* WASM */ diff --git a/wasm/func.wat b/wasm/func.wat @@ -0,0 +1,5 @@ +(module + (func $add (param $a i32) (param $b i32) (result i32) + local.get $a + local.get $b + i32.add)) diff --git a/wasm/hello-2.wat b/wasm/hello-2.wat @@ -0,0 +1,6 @@ +(module + (func $add (param $lhs i32) (param $rhs i32) (result i32) + local.get $lhs + local.get $rhs + i32.add) + (export "add" (func $add))) diff --git a/wasm/hello.wat b/wasm/hello.wat @@ -0,0 +1 @@ +(module) diff --git a/wasm/module.wat b/wasm/module.wat @@ -0,0 +1 @@ +(module)