Initial commit with a substantial amount of code
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Sun, 1 Mar 2020 03:39:40 +0000 (21:39 -0600)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Sun, 1 Mar 2020 03:39:40 +0000 (21:39 -0600)
17 files changed:
Makefile [new file with mode: 0644]
clib/buffered_file.h [new file with mode: 0644]
clib/parse_helper.c [new file with mode: 0644]
clib/parse_helper.so [new file with mode: 0755]
conf.lua [new file with mode: 0644]
data/add.wasm [new file with mode: 0644]
data/main.wasm [new file with mode: 0755]
data/test.bin [new file with mode: 0644]
decompile.lua [new file with mode: 0644]
docs/todo [new file with mode: 0644]
lualib/oop.lua [new file with mode: 0755]
lualib/pprint.lua [new file with mode: 0644]
main.lua [new file with mode: 0644]
res/FiraCode-Regular.ttf [new file with mode: 0644]
testing.lua [new file with mode: 0644]
ui.lua [new file with mode: 0644]
utils.lua [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..4218884
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+clib/%.o: clib/%.c clib/*.h
+       gcc -DLUA51_TARGET -O2 -o $@ -fPIC -c $< -llua
+
+clib/%.so: clib/%.o
+       gcc -shared -o $@ $< -llua
+
+all: clib/parse_helper.so
diff --git a/clib/buffered_file.h b/clib/buffered_file.h
new file mode 100644 (file)
index 0000000..91ee50c
--- /dev/null
@@ -0,0 +1,56 @@
+#ifndef BUFFERED_FILE_H
+#define BUFFERED_FILE_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#define BUFFERED_FILE_BUFF_SIZE 4096
+typedef struct {
+       FILE* fp;
+       int buff_ptr;
+       char buff[BUFFERED_FILE_BUFF_SIZE];
+} buffered_file;
+
+buffered_file* buffered_file_open(const char* filepath) {
+       buffered_file* bf = (buffered_file*) malloc(sizeof(buffered_file));
+
+       bf->fp = fopen(filepath, "r");
+       if (bf->fp == NULL) {
+               return NULL;
+       }
+       bf->buff_ptr = BUFFERED_FILE_BUFF_SIZE;
+
+       return bf;
+}
+
+bool buffered_file_empty(buffered_file* bf) {
+       return bf->buff_ptr >= BUFFERED_FILE_BUFF_SIZE && feof(bf->fp);
+}
+
+unsigned char buffered_file_read_byte(buffered_file* bf) {
+       if (buffered_file_empty(bf)) return 0;
+
+       if (bf->buff_ptr >= BUFFERED_FILE_BUFF_SIZE) {
+               // Need to read more data
+               fread(bf->buff, BUFFERED_FILE_BUFF_SIZE, 1, bf->fp);
+               bf->buff_ptr = 0;
+       }
+
+       return bf->buff[bf->buff_ptr++];
+}
+
+unsigned char buffered_file_peek_byte(buffered_file* bf) {
+       if (buffered_file_empty(bf)) return 0;
+
+       if (bf->buff_ptr >= BUFFERED_FILE_BUFF_SIZE) {
+               // Need to read more data
+               fread(bf->buff, BUFFERED_FILE_BUFF_SIZE, 1, bf->fp);
+               bf->buff_ptr = 0;
+       }
+
+       return bf->buff[bf->buff_ptr];
+}
+
+
+#endif
diff --git a/clib/parse_helper.c b/clib/parse_helper.c
new file mode 100644 (file)
index 0000000..63cd19f
--- /dev/null
@@ -0,0 +1,115 @@
+#include <stdio.h>
+#ifdef LUA51_TARGET
+       #include <lua5.1/lua.h>
+       #include <lua5.1/lualib.h>
+       #include <lua5.1/lauxlib.h>
+#else
+       #include <lua.h>
+       #include <lualib.h>
+       #include <lauxlib.h>
+#endif
+
+#include "buffered_file.h"
+
+#define new(class, var) class* var = (class*) malloc(sizeof(class));
+#define LUA_FUNCTION(fname) int fname(lua_State *L)
+
+typedef struct {
+       buffered_file* file;
+} Parser;
+
+LUA_FUNCTION(open_file) {
+       const char *filepath = lua_tostring(L, 1);
+
+       new(Parser, parser);
+       parser->file = buffered_file_open(filepath);
+
+       lua_pushlightuserdata(L, parser);
+       return 1;
+}
+
+LUA_FUNCTION(is_file_empty) {
+       Parser* parser = (Parser*) lua_touserdata(L, 1);
+
+       lua_pushboolean(L, buffered_file_empty(parser->file) == 1);
+       return 1;
+}
+
+LUA_FUNCTION(read_byte) {
+       Parser* parser = (Parser*) lua_touserdata(L, 1);
+
+       unsigned char byte = buffered_file_read_byte(parser->file);
+
+       lua_pushinteger(L, byte);
+       return 1;
+}
+
+LUA_FUNCTION(peek_byte) {
+       Parser* parser = (Parser*) lua_touserdata(L, 1);
+
+       unsigned char byte = buffered_file_peek_byte(parser->file);
+
+       lua_pushinteger(L, byte);
+       return 1;
+}
+
+LUA_FUNCTION(parse_uleb128) {
+       Parser* parser = (Parser*) lua_touserdata(L, 1);
+
+       unsigned long long result = 0;
+       int shift = 0;
+       unsigned char byte;
+
+       while (1) {
+               byte = buffered_file_read_byte(parser->file);
+               result |= (byte & 0x7f) << shift;
+               if ((byte & 0x80) == 0)
+                       break;
+
+               shift += 7;
+       }
+       lua_pushnumber(L, result);
+       return 1;
+}
+
+LUA_FUNCTION(parse_sleb128) {
+       Parser* parser = (Parser*) lua_touserdata(L, 1);
+       int size = lua_tointeger(L, 2);
+
+       long long result = 0;
+       int shift = 0;
+
+       unsigned char byte;
+
+       do {
+               byte = buffered_file_read_byte(parser->file);
+               result |= (byte & 0x7f) << shift;
+               shift += 7;
+       } while ((byte & 0x80) != 0);
+
+       if ((shift < size) && (byte & 0x40) > 0)
+               result |= (~0ll << shift);
+
+       lua_pushnumber(L, (double) result);
+       return 1;
+}
+
+int luaopen_parse_helper(lua_State* L) {
+       const luaL_Reg functions[] = {
+               { "open_file", open_file },
+               { "read_byte", read_byte },
+               { "peek_byte", peek_byte },
+               { "is_file_empty", is_file_empty },
+
+               { "parse_uleb128", parse_uleb128 },
+               { "parse_sleb128", parse_sleb128 },
+               { NULL, NULL },
+       };
+
+#ifdef LUA51_TARGET
+       luaL_register(L, "parse_helper", functions);
+#else
+       luaL_newlib(L, functions);
+#endif
+       return 1;
+}
diff --git a/clib/parse_helper.so b/clib/parse_helper.so
new file mode 100755 (executable)
index 0000000..50ce0d0
Binary files /dev/null and b/clib/parse_helper.so differ
diff --git a/conf.lua b/conf.lua
new file mode 100644 (file)
index 0000000..3521b8e
--- /dev/null
+++ b/conf.lua
@@ -0,0 +1,6 @@
+function love.conf(t)
+       t.window.title = "TBD"
+       t.window.width = 1200
+       t.window.height = 900
+       t.window.resizable = false
+end
diff --git a/data/add.wasm b/data/add.wasm
new file mode 100644 (file)
index 0000000..67b8441
Binary files /dev/null and b/data/add.wasm differ
diff --git a/data/main.wasm b/data/main.wasm
new file mode 100755 (executable)
index 0000000..1c41dc8
Binary files /dev/null and b/data/main.wasm differ
diff --git a/data/test.bin b/data/test.bin
new file mode 100644 (file)
index 0000000..39e9efe
--- /dev/null
@@ -0,0 +1 @@
+À»x
\ No newline at end of file
diff --git a/decompile.lua b/decompile.lua
new file mode 100644 (file)
index 0000000..fee1557
--- /dev/null
@@ -0,0 +1,714 @@
+-- Recursive decent parser for the WASM v1.0 binary format
+-- Brendan Hansen 2020
+
+-- Look in clib folder for C-based libraries
+package.cpath = package.cpath .. [[;./clib/?.so]]
+
+import {
+       parse_helper = "parse_helper";
+       pprint = "lualib.pprint";
+
+       build_str = "utils:build_str";
+}
+
+VAL_TYPES = {
+       i32 = "i32",
+       i64 = "i64",
+       f32 = "f32",
+       f64 = "f64"
+}
+
+function parse_valtype(r)
+       local val = r:read_byte()
+
+       local valtypes_map = {
+               [0x7f] = VAL_TYPES.i32;
+               [0x7E] = VAL_TYPES.i64;
+               [0x7D] = VAL_TYPES.f32;
+               [0x7C] = VAL_TYPES.f64;
+       }
+
+       return valtypes_map[val]
+end
+
+function parse_vector(tp)
+       return function(r)
+               local n = r:read_uint(32)
+
+               local v = {}
+               for i=1, n do
+                       table.insert(v, tp(r))
+               end
+
+               return v
+       end
+end
+
+function parse_byte(r)
+       return r:read_byte()
+end
+
+function parse_name(r)
+       local name = parse_vector(parse_byte)(r)
+       return name
+end
+
+function parse_blocktype(r)
+       if r:peek_byte() == 0x40 then
+               r:read_byte()
+               return {}
+       else
+               return { parse_valtype(r) }
+       end
+end
+
+function parse_functype(r)
+       assert(r:read_byte() == 0x60, "functype expected 0x60 as first byte")
+
+       local params = parse_vector(parse_valtype)(r)
+       local results = parse_vector(parse_valtype)(r)
+
+       return { param_types = params, result_types = results }
+end
+
+function parse_limits(r)
+       local t = r:read_byte()
+
+       local min = r:read_uint(32)
+       local max = nil
+       if t == 0x01 then
+               max = r:read_uint(32)
+       end
+
+       return { min = min, max = max }
+end
+
+function parse_memtype(r)
+       local lims = parse_limits(r)
+
+       return lims
+end
+
+function parse_tabletype(r)
+       local elemtype = parse_elemtype(r)
+       local limits = parse_limits(r)
+
+       return { lim = limits, et = elemtype }
+end
+
+function parse_elemtype(r)
+       assert(r:read_byte() == 0x70, "elemtype should be 0x70")
+       return "funcref"
+end
+
+function parse_globaltype(r)
+       local valtype = parse_valtype(r)
+       local ismut = parse_mut(r) == 0x01
+
+       return { t = valtype, m = ismut }
+end
+
+function parse_mut(r)
+       local v = r:read_byte()
+       assert(v == 0x00 or v == 0x01, "mut should be 0x00 or 0x01")
+       return v
+end
+
+function parse_instr(r)
+       local instr = r:read_byte()
+
+       return match(instr) {
+               [0x00] = function() return { "unreachable" } end;
+               [0x01] = function() return { "nop" } end;
+               [0x02] = function()
+                       local rt = parse_blocktype(r)
+                       local instrs = {}
+
+                       while true do
+                               if r:peek_byte() == 0x0B then
+                                       r:read_byte()
+                                       break
+                               else
+                                       table.insert(instrs, parse_instr(r))
+                               end
+                       end
+
+                       return { "block", rt = rt, instrs = instrs }
+               end;
+               [0x03] = function()
+                       local rt = parse_blocktype(r)
+                       local instrs = {}
+
+                       while true do
+                               if r:peek_byte() == 0x0B then
+                                       r:read_byte()
+                                       break
+                               else
+                                       table.insert(instrs, parse_instr(r))
+                               end
+                       end
+
+                       return { "loop", rt = rt, instrs = instrs }
+               end;
+               [0x04] = function()
+                       local rt = parse_blocktype(r)
+                       local instrs = {}
+                       local else_instrs = {}
+                       local inelse = false
+
+                       while true do
+                               local peek = r:peek_byte()
+                               if peek == 0x0B then
+                                       r:read_byte()
+                                       break
+                               elseif peek == 0x05 then
+                                       r:read_byte()
+                                       inelse = true
+                               else
+                                       if not inelse then
+                                               table.insert(instrs, parse_instr(r))
+                                       else
+                                               table.insert(else_instrs, parse_instr(r))
+                                       end
+                               end
+                       end
+
+                       return { "if", rt = rt, instrs = instrs, else_instrs = else_instrs }
+               end;
+
+               [0x0C] = function() return { "br", x = parse_labelidx(r) } end;
+               [0x0D] = function() return { "br_if", x = parse_labelidx(r) } end;
+               [0x0E] = function()
+                       local labels = parse_vector(parse_labelidx)(r)
+                       local labeln = parse_labelidx(r)
+
+                       return { "br_table", labels = labels, labeln = labeln }
+               end;
+
+               [0x0F] = function() return { "return" } end;
+
+               [0x10] = function() return { "call", x = parse_funcidx(r) } end;
+               [0x11] = function()
+                       local x = parse_typeidx(r)
+                       assert(r:read_byte() ~= 0x00, "call_indirect expects 0x00 at end")
+                       return { "call_indirect", x = x }
+               end;
+
+               [0x1A] = function() return { "drop" } end;
+               [0x1B] = function() return { "select" } end;
+
+               [0x20] = function() return { "local.get", x = parse_localidx(r) } end;
+               [0x21] = function() return { "local.set", x = parse_localidx(r) } end;
+               [0x22] = function() return { "local.tee", x = parse_localidx(r) } end;
+               [0x23] = function() return { "global.get", x = parse_globalidx(r) } end;
+               [0x24] = function() return { "global.set", x = parse_globalidx(r) } end;
+
+               [0x28] = function() return { "i32.load", x = parse_memarg(r) } end;
+               [0x29] = function() return { "i64.load", x = parse_memarg(r) } end;
+               [0x2A] = function() return { "f32.load", x = parse_memarg(r) } end;
+               [0x2B] = function() return { "f64.load", x = parse_memarg(r) } end;
+               [0x2C] = function() return { "i32.load8_s", x = parse_memarg(r) } end;
+               [0x2D] = function() return { "i32.load8_u", x = parse_memarg(r) } end;
+               [0x2E] = function() return { "i32.load16_s", x = parse_memarg(r) } end;
+               [0x2F] = function() return { "i32.load16_u", x = parse_memarg(r) } end;
+               [0x30] = function() return { "i64.load8_s", x = parse_memarg(r) } end;
+               [0x31] = function() return { "i64.load8_u", x = parse_memarg(r) } end;
+               [0x32] = function() return { "i64.load16_s", x = parse_memarg(r) } end;
+               [0x33] = function() return { "i64.load16_u", x = parse_memarg(r) } end;
+               [0x34] = function() return { "i64.load32_s", x = parse_memarg(r) } end;
+               [0x35] = function() return { "i64.load32_u", x = parse_memarg(r) } end;
+               [0x36] = function() return { "i32.store", x = parse_memarg(r) } end;
+               [0x37] = function() return { "i64.store", x = parse_memarg(r) } end;
+               [0x38] = function() return { "f32.store", x = parse_memarg(r) } end;
+               [0x39] = function() return { "f64.store", x = parse_memarg(r) } end;
+               [0x3A] = function() return { "i32.store8", x = parse_memarg(r) } end;
+               [0x3B] = function() return { "i32.store16", x = parse_memarg(r) } end;
+               [0x3C] = function() return { "i64.store8", x = parse_memarg(r) } end;
+               [0x3D] = function() return { "i64.store16", x = parse_memarg(r) } end;
+               [0x3E] = function() return { "i64.store32", x = parse_memarg(r) } end;
+               [0x3F] = function()
+                       assert(r:read_byte() == 0x00, "memory.size expects 0x00")
+                       return { "memory.size" }
+               end;
+               [0x40] = function()
+                       assert(r:read_byte() == 0x00, "memory.grow expects 0x00")
+                       return { "memory.grow" }
+               end;
+
+               [0x41] = function() return { "i32.const", x = r:read_sint(32) } end;
+               [0x42] = function() return { "i64.const", x = r:read_sint(64) } end;
+               [0x43] = function() return { "f32.const", x = r:read_float(32) } end;
+               [0x44] = function() return { "f64.const", x = r:read_float(64) } end;
+
+               [0x45] = function() return { "i32.eqz" } end;
+               [0x46] = function() return { "i32.eq" } end;
+               [0x47] = function() return { "i32.ne" } end;
+               [0x48] = function() return { "i32.lt_s" } end;
+               [0x49] = function() return { "i32.lt_u" } end;
+               [0x4A] = function() return { "i32.gt_s" } end;
+               [0x4B] = function() return { "i32.gt_u" } end;
+               [0x4C] = function() return { "i32.le_s" } end;
+               [0x4D] = function() return { "i32.le_u" } end;
+               [0x4E] = function() return { "i32.ge_s" } end;
+               [0x4F] = function() return { "i32.ge_u" } end;
+               [0x50] = function() return { "i64.eqz" } end;
+               [0x51] = function() return { "i64.eq" } end;
+               [0x52] = function() return { "i64.ne" } end;
+               [0x53] = function() return { "i64.lt_s" } end;
+               [0x54] = function() return { "i64.lt_u" } end;
+               [0x55] = function() return { "i64.gt_s" } end;
+               [0x56] = function() return { "i64.gt_u" } end;
+               [0x57] = function() return { "i64.le_s" } end;
+               [0x58] = function() return { "i64.le_u" } end;
+               [0x59] = function() return { "i64.ge_s" } end;
+               [0x5A] = function() return { "i64.ge_u" } end;
+               [0x5B] = function() return { "f32.eq" } end;
+               [0x5C] = function() return { "f32.ne" } end;
+               [0x5D] = function() return { "f32.lt" } end;
+               [0x5E] = function() return { "f32.gt" } end;
+               [0x5F] = function() return { "f32.le" } end;
+               [0x60] = function() return { "f32.ge" } end;
+               [0x61] = function() return { "f64.eq" } end;
+               [0x62] = function() return { "f64.ne" } end;
+               [0x63] = function() return { "f64.lt" } end;
+               [0x64] = function() return { "f64.gt" } end;
+               [0x65] = function() return { "f64.le" } end;
+               [0x66] = function() return { "f64.ge" } end;
+               [0x67] = function() return { "i32.clz" } end;
+               [0x68] = function() return { "i32.ctz" } end;
+               [0x69] = function() return { "i32.popcnt" } end;
+               [0x6A] = function() return { "i32.add" } end;
+               [0x6B] = function() return { "i32.sub" } end;
+               [0x6C] = function() return { "i32.mul" } end;
+               [0x6D] = function() return { "i32.div_s" } end;
+               [0x6E] = function() return { "i32.div_u" } end;
+               [0x6F] = function() return { "i32.rem_s" } end;
+               [0x70] = function() return { "i32.rem_u" } end;
+               [0x71] = function() return { "i32.and" } end;
+               [0x72] = function() return { "i32.or" } end;
+               [0x73] = function() return { "i32.xor" } end;
+               [0x74] = function() return { "i32.shl" } end;
+               [0x75] = function() return { "i32.shr_s" } end;
+               [0x76] = function() return { "i32.shr_u" } end;
+               [0x77] = function() return { "i32.rotl" } end;
+               [0x78] = function() return { "i32.rotr" } end;
+               [0x79] = function() return { "i64.clz" } end;
+               [0x7A] = function() return { "i64.ctz" } end;
+               [0x7B] = function() return { "i64.popcnt" } end;
+               [0x7C] = function() return { "i64.add" } end;
+               [0x7D] = function() return { "i64.sub" } end;
+               [0x7E] = function() return { "i64.mul" } end;
+               [0x7F] = function() return { "i64.div_s" } end;
+               [0x80] = function() return { "i64.div_u" } end;
+               [0x81] = function() return { "i64.rem_s" } end;
+               [0x82] = function() return { "i64.rem_u" } end;
+               [0x83] = function() return { "i64.and" } end;
+               [0x84] = function() return { "i64.or" } end;
+               [0x85] = function() return { "i64.xor" } end;
+               [0x86] = function() return { "i64.shl" } end;
+               [0x87] = function() return { "i64.shr_s" } end;
+               [0x88] = function() return { "i64.shr_u" } end;
+               [0x89] = function() return { "i64.rotl" } end;
+               [0x8A] = function() return { "i64.rotr" } end;
+               [0x8B] = function() return { "f32.abs" } end;
+               [0x8C] = function() return { "f32.neg" } end;
+               [0x8D] = function() return { "f32.ceil" } end;
+               [0x8E] = function() return { "f32.floor" } end;
+               [0x8F] = function() return { "f32.trunc" } end;
+               [0x90] = function() return { "f32.nearest" } end;
+               [0x91] = function() return { "f32.sqrt" } end;
+               [0x92] = function() return { "f32.add" } end;
+               [0x93] = function() return { "f32.sub" } end;
+               [0x94] = function() return { "f32.mul" } end;
+               [0x95] = function() return { "f32.div" } end;
+               [0x96] = function() return { "f32.min" } end;
+               [0x97] = function() return { "f32.max" } end;
+               [0x98] = function() return { "f32.copysign" } end;
+               [0x99] = function() return { "f64.abs" } end;
+               [0x9A] = function() return { "f64.neg" } end;
+               [0x9B] = function() return { "f64.ceil" } end;
+               [0x9C] = function() return { "f64.floor" } end;
+               [0x9D] = function() return { "f64.trunc" } end;
+               [0x9E] = function() return { "f64.nearest" } end;
+               [0x9F] = function() return { "f64.sqrt" } end;
+               [0xA0] = function() return { "f64.add" } end;
+               [0xA1] = function() return { "f64.sub" } end;
+               [0xA2] = function() return { "f64.mul" } end;
+               [0xA3] = function() return { "f64.div" } end;
+               [0xA4] = function() return { "f64.min" } end;
+               [0xA5] = function() return { "f64.max" } end;
+               [0xA6] = function() return { "f64.copysign" } end;
+
+               [0xA7] = function() return { "i32.wrap_i64" } end;
+               [0xA8] = function() return { "i32.trunc_f32_s" } end;
+               [0xA9] = function() return { "i32.trunc_f32_u" } end;
+               [0xAA] = function() return { "i32.trunc_f64_s" } end;
+               [0xAB] = function() return { "i32.trunc_f64_u" } end;
+               [0xAC] = function() return { "i64.extend_i32_s" } end;
+               [0xAD] = function() return { "i64.extend_i32_u" } end;
+               [0xAE] = function() return { "i64.trunc_f32_s" } end;
+               [0xAF] = function() return { "i64.trunc_f32_u" } end;
+               [0xB0] = function() return { "i64.trunc_f64_s" } end;
+               [0xB1] = function() return { "i64.trunc_f64_u" } end;
+               [0xB2] = function() return { "f32.convert_i32_s" } end;
+               [0xB3] = function() return { "f32.convert_i32_u" } end;
+               [0xB4] = function() return { "f32.convert_i64_s" } end;
+               [0xB5] = function() return { "f32.convert_i64_u" } end;
+               [0xB6] = function() return { "f32.demote_f64" } end;
+               [0xB7] = function() return { "f64.convert_i32_s" } end;
+               [0xB8] = function() return { "f64.convert_i32_u" } end;
+               [0xB9] = function() return { "f64.convert_i64_s" } end;
+               [0xBA] = function() return { "f64.convert_i64_u" } end;
+               [0xBB] = function() return { "f64.promote_f32" } end;
+               [0xBC] = function() return { "i32.reinterpret_f32" } end;
+               [0xBD] = function() return { "i64.reinterpret_f64" } end;
+               [0xBE] = function() return { "f32.reinterpret_i32" } end;
+               [0xBF] = function() return { "f64.reinterpret_i64" } end;
+       }
+end
+
+function parse_memarg(r)
+       local a = r:read_uint(32)
+       local o = r:read_uint(32)
+
+       return { "memarg"; align = a; offset = o }
+end
+
+function parse_expr(r)
+       local instrs = {}
+       while true do
+               if r:peek_byte() == 0x0B then
+                       r:read_byte()
+                       break
+               else
+                       table.insert(instrs, parse_instr(r))
+               end
+       end
+
+       return instrs
+end
+
+function parse_typeidx(r)   return r:read_uint(32) end
+function parse_funcidx(r)   return r:read_uint(32) end
+function parse_tableidx(r)  return r:read_uint(32) end
+function parse_memidx(r)    return r:read_uint(32) end
+function parse_globalidx(r) return r:read_uint(32) end
+function parse_localidx(r)  return r:read_uint(32) end
+function parse_labelidx(r)  return r:read_uint(32) end
+
+function parse_section(r, expectN, B)
+       if r:peek_byte() ~= expectN then return end
+
+       local N = r:read_byte()
+       local size = r:read_uint(32)
+       local cont = B(r)
+
+       return { "section", contents = cont, size = size }
+end
+
+function parse_customsec(r)
+       local csect = parse_section(r, 0, parse_custom)
+       if not csect then return nil end
+
+       for i=1, csect.size do
+               -- Discard csect.size bytes
+               r:read_byte()
+       end
+       return csect
+end
+
+function parse_custom(r)
+       local name = parse_name(r)
+
+       return { "custom", name = name }
+end
+
+function parse_typesec(r)
+       return parse_section(r, 1, parse_vector(parse_functype))
+end
+
+function parse_importsec(r)
+       return parse_section(r, 2, parse_vector(parse_import))
+end
+
+function parse_import(r)
+       local mod = parse_name(r)
+       local nm = parse_name(r)
+       local d = parse_importdesc(r)
+
+       return { "import", mod = mod, name = nm, desc = d }
+end
+
+function parse_importdesc(r)
+       local t = r:read_byte()
+       if     t == 0x00 then return { "func", x = parse_typeidx(r) }
+       elseif t == 0x01 then return { "table", tt = parse_tabletype(r) }
+       elseif t == 0x02 then return { "mem", mt = parse_memtype(r) }
+       elseif t == 0x03 then return { "global", gt = parse_globaltype(r) }
+       end
+
+       error("bad importdesc")
+end
+
+function parse_funcsec(r)
+       return parse_section(r, 3, parse_vector(parse_typeidx))
+end
+
+function parse_tablesec(r)
+       return parse_section(r, 4, parse_vector(parse_table))
+end
+
+function parse_table(r)
+       local tt = parse_tabletype(r)
+       return { "table", type_ = tt }
+end
+
+function parse_memsec(r)
+       return parse_section(r, 5, parse_vector(parse_mem))
+end
+
+function parse_mem(r)
+       local mt = parse_memtype(r)
+       return { "mem", type_ = mt }
+end
+
+function parse_globalsec(r)
+       return parse_section(r, 6, parse_vector(parse_global))
+end
+
+function parse_global(r)
+       local gt = parse_globaltype(r)
+       local e = parse_expr(r)
+       return { "global", type_ = gt, init = e }
+end
+
+function parse_exportsec(r)
+       return parse_section(r, 7, parse_vector(parse_export))
+end
+
+function parse_export(r)
+       local nm = parse_name(r)
+       local d = parse_exportdesc(r)
+       return { "export", name = nm, desc = d }
+end
+
+function parse_exportdesc(r)
+       local t = r:read_byte()
+       if     t == 0x00 then return { "func", x = parse_typeidx(r) }
+       elseif t == 0x01 then return { "table", tt = parse_tableidx(r) }
+       elseif t == 0x02 then return { "mem", mt = parse_memidx(r) }
+       elseif t == 0x03 then return { "global", gt = parse_globalidx(r) }
+       end
+
+       error("bad exportdesc: ", t)
+end
+
+function parse_startsec(r)
+       return parse_section(r, 8, parse_start)
+end
+
+function parse_start(r)
+       local x = parse_funcidx(r)
+       return { "start", func = x }
+end
+
+function parse_elemsec(r)
+       return parse_section(r, 9, parse_vector(parse_elem))
+end
+
+function parse_elem(r)
+       local x = parse_tableidx(r)
+       local e = parse_expr(r)
+       local y = parse_vector(parse_funcidx)(r)
+
+       return { "elem", table = x, offset = e, init = y }
+end
+
+function parse_codesec(r)
+       return parse_section(r, 10, parse_vector(parse_code))
+end
+
+function parse_code(r)
+       local size = r:read_uint(32)
+       local code = parse_func(r)
+       return code
+end
+
+function parse_func(r)
+       local t = parse_vector(parse_locals)(r)
+       local e = parse_expr(r)
+
+       local localidx = 0
+       local locals = {}
+       for _, v in ipairs(t) do
+               for _, l in ipairs(v) do
+                       l.localidx = localidx
+                       table.insert(locals, l)
+                       localidx = localidx + 1
+               end
+       end
+
+       return { "func", locals = locals, body = e }
+end
+
+function parse_locals(r)
+       local n = r:read_uint(32)
+       local t = parse_valtype(r)
+
+       --TODO: Make a list of values with names like local0, local1, ...
+
+       local locals = {}
+       for i = 0, n - 1 do
+               table.insert(locals, {
+                       name = "local" .. i,
+                       type_ = t,
+                       localidx = i
+               })
+       end
+
+       return locals
+end
+
+function parse_datasec(r)
+       return parse_section(r, 11, parse_vector(parse_data))
+end
+
+function parse_data(r)
+       local x = parse_memidx(r)
+       local e = parse_expr(r)
+       local b = parse_vector(parse_byte)(r)
+
+       return { "data", data = x, offset = e, init = b }
+end
+
+function parse_magic(r)
+       assert(r:read_byte() == 0x00, "magic string is wrong")
+       assert(r:read_byte() == 0x61, "magic string is wrong")
+       assert(r:read_byte() == 0x73, "magic string is wrong")
+       assert(r:read_byte() == 0x6D, "magic string is wrong")
+end
+
+function parse_version(r)
+       assert(r:read_byte() == 0x01, "version is wrong")
+       assert(r:read_byte() == 0x00, "version is wrong")
+       assert(r:read_byte() == 0x00, "version is wrong")
+       assert(r:read_byte() == 0x00, "version is wrong")
+end
+
+function parse_module(r)
+       parse_magic(r)
+       parse_version(r)
+
+       local functypes = parse_typesec(r)
+       local imports   = parse_importsec(r)
+       local typeidxs  = parse_funcsec(r)
+       local tables    = parse_tablesec(r)
+       local mems              = parse_memsec(r)
+       local globals   = parse_globalsec(r)
+       local exports   = parse_exportsec(r)
+       local start     = parse_startsec(r)
+       local elems     = parse_elemsec(r)
+       local codes             = parse_codesec(r)
+       local data              = parse_datasec(r)
+
+       local funcs = {}
+       local funcidx = 0
+
+       if imports then
+               for k, v in ipairs(imports.contents) do
+                       if v.desc[1] == "func" then
+                               funcs[funcidx] = {
+                                       name = build_str(v.mod) .. "." .. build_str(v.name);
+                                       funcidx = funcidx;
+                                       type_ = functypes.contents[v.desc.x + 1];
+                               }
+                               funcidx = funcidx + 1
+                       end
+               end
+       end
+
+       if codes then
+               for i=1, #codes.contents do
+                       local locals = codes.contents[i].locals;
+                       local type_ = functypes.contents[typeidxs.contents[i] + 1];
+                       local param_types = type_.param_types;
+
+                       local new_locals = {}
+                       local new_local_idx = 0
+                       for _, p in ipairs(param_types) do
+                               table.insert(new_locals, {
+                                       name = "param" .. new_local_idx,
+                                       type_ = p;
+                                       localidx = new_local_idx
+                               })
+                               new_local_idx = new_local_idx + 1
+                       end
+                       for _, l in ipairs(locals) do
+                               l.localidx = new_local_idx
+                               table.insert(new_locals, l)
+                               new_local_idx = new_local_idx + 1
+                       end
+
+                       funcs[funcidx] = {
+                               funcidx = funcidx;
+                               name = "func" .. funcidx;
+                               type_ = functypes.contents[typeidxs.contents[i] + 1];
+                               locals = new_locals;
+                               body = codes.contents[i].body
+                       }
+                       funcidx = funcidx + 1
+               end
+       end
+
+       return {
+               "module";
+               types = functypes;
+               tables = tables;
+               mems = mems;
+               globals = globas;
+               exports = exports;
+               start = start;
+               elems = elems;
+               data = data;
+               imports = imports;
+               funcs = funcs;
+       }
+end
+
+
+-- Reader util class used for interfacing
+-- with the parse_helper c library
+class "Reader" {
+       init = function(self, filename)
+               self.wasm_file = parse_helper.open_file(filename)
+       end;
+
+       read_byte = function(self)
+               return parse_helper.read_byte(self.wasm_file)
+       end;
+
+       peek_byte = function(self)
+               return parse_helper.peek_byte(self.wasm_file)
+       end;
+
+       -- NOTE: N is unused
+       read_uint = function(self, N)
+               return parse_helper.parse_uleb128(self.wasm_file)
+       end;
+
+       read_sint = function(self, N)
+               return parse_helper.parse_sleb128(self.wasm_file, N)
+       end;
+
+       read_float = function(self, N)
+               return 0.0;
+       end;
+}
+
+function decompile(filepath)
+       local reader = Reader(filepath)
+
+       return parse_module(reader)
+end
+
+return module { decompile; VAL_TYPES = VAL_TYPES }
diff --git a/docs/todo b/docs/todo
new file mode 100644 (file)
index 0000000..f3fb67e
--- /dev/null
+++ b/docs/todo
@@ -0,0 +1,2 @@
+After experimenting with using Lua for the parser, I think it will be better to write the parser in C / C++
+and then have a function that converts all the data to a lua table that will be used for visualization / examination.
diff --git a/lualib/oop.lua b/lualib/oop.lua
new file mode 100755 (executable)
index 0000000..bf4a9e8
--- /dev/null
@@ -0,0 +1,316 @@
+--Under MIT License\r
+--Originally developed by Brendan Hansen\r
+--Copyright 2017\r
+\r
+--[[ found on http://lua-users.org/lists/lua-l/2010-06/msg00314.html ]]\r
+setfenv = setfenv or function(f, t)\r
+    f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)\r
+    local name\r
+    local up = 0\r
+    repeat\r
+        up = up + 1\r
+        name = debug.getupvalue(f, up)\r
+    until name == '_ENV' or name == nil\r
+    if name then\r
+        debug.upvaluejoin(f, up, function() return t end, 1) -- use unique upvalue, set it to f\r
+    end\r
+end\r
+\r
+getfenv = getfenv or function(fn)\r
+    local i = 1\r
+    while true do\r
+        local name, val = debug.getupvalue(fn, i)\r
+        if name == "_ENV" then\r
+            return val\r
+        elseif not name then\r
+            break\r
+        end\r
+        i = i + 1\r
+    end\r
+end\r
+\r
+local function helper(obj, super, f)\r
+    return function(...)\r
+        local a = { ... }\r
+        local data = a[#a]\r
+        local env = getfenv(2)\r
+\r
+        data.extends = super\r
+        env[obj] = f(data)\r
+    end\r
+end\r
+\r
+\r
+--[[\r
+    Module System\r
+    "imports" are defined by:\r
+    import {\r
+        NAME = "path.to.lua.file";\r
+        NAME = "path.to.lua.file:specific_import";\r
+        NAME = "path.to.lua.file:"; --this uses the default export (see below)\r
+    }\r
+\r
+    "exports" are defined by:\r
+    return module {\r
+        the_default_export_variable;\r
+        other_export=some_value;\r
+    }\r
+]]\r
+\r
+local ALL_DEPS = {}\r
+function import(reqs)\r
+    local function convertPath(req)\r
+        local r = req\r
+        if req:find(":") ~= nil then\r
+            r = req:sub(1, req:find(":") - 1)\r
+        end\r
+        r = r:gsub("%.", "/")\r
+        return r\r
+    end\r
+\r
+    local function getField(req)\r
+        if req:find(":") ~= nil then\r
+            if req:sub(-1) == ":" then\r
+                return "default"\r
+            else\r
+                return req:sub(req:find(":")+1)\r
+            end\r
+        else\r
+            return nil\r
+        end\r
+    end\r
+\r
+    local dep = {}\r
+    for name, req in pairs(reqs) do\r
+        if type(req) ~= "string" then\r
+            error "Please use strings for referencing modules"\r
+        end\r
+\r
+        local mod = require(convertPath(req))\r
+        dep[name] = mod\r
+\r
+        if type(mod) == "table" then\r
+            local field = getField(req)\r
+            if field ~= nil then\r
+                if mod[field] ~= nil then\r
+                    dep[name] = mod[field]\r
+                else\r
+                    dep[name] = nil\r
+                end\r
+            end\r
+       end\r
+    end\r
+\r
+    local newenv = setmetatable({}, {\r
+        __index = function(_, k)\r
+            local v = dep[k]\r
+            if v == nil then v = _G[k] end\r
+            return v\r
+        end;\r
+        __newindex = function(_, k, v)\r
+            dep[k] = v;\r
+        end;\r
+    })\r
+    setfenv(2, newenv)\r
+end\r
+\r
+function module(exp)\r
+    exp["default"] = exp[1]\r
+    exp[1] = nil\r
+    return exp\r
+end\r
+\r
+--[[\r
+    Data - basically tables that can only hold numbers, strings, bools, tables, or nil (no functions)\r
+]]\r
+function data(d)\r
+    for k, v in pairs(d) do\r
+        if type(v) == "function" then\r
+            d[k] = nil\r
+        end\r
+    end\r
+    return d\r
+end\r
+\r
+--[[\r
+    Factories - basically tables that can only hold functions, normally static\r
+]]\r
+function factory(f)\r
+    for k, v in pairs(f) do\r
+        if type(v) ~= "function" then\r
+            f[k] = nil\r
+        end\r
+    end\r
+    return f\r
+end\r
+\r
+--[[\r
+    Classes - factories with a initializer and a self-reference, and are allowed to hold variables\r
+\r
+    Initialize with "init"\r
+    Create new instance by calling name of class\r
+    local ClassName = class { ... }\r
+    local ClassName = Base.extend { ... }\r
+    local instance_a = ClassName(args)\r
+\r
+    Can also declare classes as\r
+    class "ClassName" { ... }\r
+    or\r
+    class "ClassName extends Base" { ... }\r
+    or\r
+    class "ClassName" ["Base"] { ... }\r
+]]\r
+function class(obj)\r
+    if type(obj) == "string" then\r
+        local sub, super = obj:match "(%w+) +extends +(%w+)"\r
+        if sub then obj = sub end\r
+\r
+        if super then\r
+            return helper(obj, super, class)\r
+        else\r
+            local o = {}\r
+            o = setmetatable(o, {\r
+                __call = helper(obj, nil, class);\r
+\r
+                __index = function(_, super)\r
+                    return helper(obj, super, class)\r
+                end;\r
+            })\r
+            return o\r
+        end\r
+    end\r
+\r
+    local cls = {}\r
+    if obj.extends ~= nil then\r
+        for k, v in pairs(obj.extends) do\r
+            cls[k] = v\r
+        end\r
+    end\r
+\r
+    for k, v in pairs(obj) do\r
+        if k ~= "extends" then\r
+            cls[k] = v\r
+        end\r
+    end\r
+\r
+    cls = setmetatable(cls, {\r
+        __call = function(_, ...)\r
+            local o = {}\r
+            local mt = {}\r
+\r
+            for k, v in pairs(cls) do\r
+                if k:sub(0, 2) == "__" then\r
+                    mt[k] = v\r
+                end\r
+            end\r
+\r
+            mt.__index = cls\r
+            o = setmetatable(o, mt)\r
+\r
+            if cls.init then\r
+                cls.init(o, ...)\r
+            end\r
+\r
+            return o\r
+        end\r
+    })\r
+\r
+    cls.extend = function(d)\r
+        d.extends = cls\r
+        return class(d)\r
+    end\r
+\r
+    return cls;\r
+end\r
+\r
+--[[\r
+    Singleton - a single instance of a class\r
+]]\r
+function singleton(obj)\r
+    return (class(obj))()\r
+end\r
+\r
+function enum(obj)\r
+    if type(obj) == "string" then\r
+        return helper(obj, nil, enum)\r
+    end\r
+\r
+    local i = 0\r
+    for k, v in pairs(obj) do\r
+        obj[k] = nil\r
+        if type(v) == "string" then\r
+            obj[v] = i\r
+            i = i + 1\r
+        end\r
+    end\r
+    return obj\r
+end\r
+\r
+function interface(fns)\r
+    if type(fns) == "string" then\r
+        local sub, super = fns:match "(%w+) +extends +(%w+)"\r
+        if sub then fns = sub end\r
+\r
+        if super then\r
+            return helper(fns, super, interface)\r
+        else\r
+            local int = {}\r
+            int = setmetatable(int, {\r
+                __call = helper(fns, nil, interface);\r
+\r
+                __index = function(_, super)\r
+                    return helper(fns, super, interface)\r
+                end;\r
+            })\r
+            return int\r
+        end\r
+    end\r
+\r
+    local int = {}\r
+    for k, v in pairs(fns) do\r
+        if type(v) == "string" then\r
+            int[v] = function(self) end\r
+        end\r
+    end\r
+\r
+    if fns.extends then\r
+        for k, v in pairs(fns.extends) do\r
+            if type(v) == "function" then\r
+                int[k] = function(self) end\r
+            end\r
+        end\r
+    end\r
+\r
+    local mt = {\r
+        __call = function(_, ...)\r
+            error "Interfaces cannot be instatiated with a constructor"\r
+        end;\r
+    }\r
+    int = setmetatable(int, mt)\r
+\r
+    return int\r
+end\r
+\r
+function match(var)\r
+    return function(tab)\r
+        local ran = false\r
+        for k, v in pairs(tab) do\r
+            if k ~= "default" and var == k then\r
+                return v()\r
+            end\r
+        end\r
+\r
+        if tab["default"] then\r
+            tab["default"]()\r
+        end\r
+    end\r
+end\r
+\r
+function with(o)\r
+    return function(tab)\r
+        for k, v in pairs(tab) do\r
+            o[k] = v\r
+        end\r
+        return o\r
+    end\r
+end\r
diff --git a/lualib/pprint.lua b/lualib/pprint.lua
new file mode 100644 (file)
index 0000000..2245484
--- /dev/null
@@ -0,0 +1,475 @@
+local pprint = { VERSION = '0.1' }
+
+local depth = 1
+
+pprint.defaults = {
+    -- If set to number N, then limit table recursion to N deep.
+    depth_limit = false,
+    -- type display trigger, hide not useful datatypes by default
+    -- custom types are treated as table
+    show_nil = true,
+    show_boolean = true,
+    show_number = true,
+    show_string = true,
+    show_table = true,
+    show_function = false,
+    show_thread = false,
+    show_userdata = false,
+    -- additional display trigger
+    show_metatable = false,     -- show metatable
+    show_all = false,           -- override other show settings and show everything
+    use_tostring = false,       -- use __tostring to print table if available
+    filter_function = nil,      -- called like callback(value[,key, parent]), return truty value to hide
+    object_cache = 'local',     -- cache blob and table to give it a id, 'local' cache per print, 'global' cache
+                                -- per process, falsy value to disable (might cause infinite loop)
+    -- format settings
+    indent_size = 2,            -- indent for each nested table level
+    level_width = 80,           -- max width per indent level
+    wrap_string = true,         -- wrap string when it's longer than level_width
+    wrap_array = false,         -- wrap every array elements
+    sort_keys = true,           -- sort table keys
+}
+
+local TYPES = {
+    ['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4, 
+    ['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8
+}
+
+-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a'
+local ESCAPE_MAP = {
+    ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r',
+    ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\',
+}
+
+-- generic utilities
+local function escape(s)
+    s = s:gsub('([%c\\])', ESCAPE_MAP)
+    local dq = s:find('"') 
+    local sq = s:find("'")
+    if dq and sq then
+        return s:gsub('"', '\\"'), '"'
+    elseif sq then
+        return s, '"'
+    else
+        return s, "'"
+    end
+end
+
+local function is_plain_key(key)
+    return type(key) == 'string' and key:match('^[%a_][%a%d_]*$')
+end
+
+local CACHE_TYPES = {
+    ['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true
+}
+
+-- cache would be populated to be like:
+-- {
+--     function = { `fun1` = 1, _cnt = 1 }, -- object id
+--     table = { `table1` = 1, `table2` = 2, _cnt = 2 },
+--     visited_tables = { `table1` = 7, `table2` = 8  }, -- visit count
+-- }
+-- use weakrefs to avoid accidentall adding refcount
+local function cache_apperance(obj, cache, option)
+    if not cache.visited_tables then
+        cache.visited_tables = setmetatable({}, {__mode = 'k'})
+    end
+    local t = type(obj)
+
+    -- TODO can't test filter_function here as we don't have the ix and key,
+    -- might cause different results?
+    -- respect show_xxx and filter_function to be consistent with print results
+    if (not TYPES[t] and not option.show_table)
+        or (TYPES[t] and not option['show_'..t]) then
+        return
+    end
+
+    if CACHE_TYPES[t] or TYPES[t] == nil then
+        if not cache[t] then
+            cache[t] = setmetatable({}, {__mode = 'k'})
+            cache[t]._cnt = 0
+        end
+        if not cache[t][obj] then
+            cache[t]._cnt = cache[t]._cnt + 1
+            cache[t][obj] = cache[t]._cnt
+        end
+    end
+    if t == 'table' or TYPES[t] == nil then
+        if cache.visited_tables[obj] == false then
+            -- already printed, no need to mark this and its children anymore
+            return
+        elseif cache.visited_tables[obj] == nil then
+            cache.visited_tables[obj] = 1
+        else
+            -- visited already, increment and continue
+            cache.visited_tables[obj] = cache.visited_tables[obj] + 1
+            return
+        end
+        for k, v in pairs(obj) do
+            cache_apperance(k, cache, option)
+            cache_apperance(v, cache, option)
+        end
+        local mt = getmetatable(obj)
+        if mt and option.show_metatable then
+            cache_apperance(mt, cache, option)
+        end
+    end
+end
+
+-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method
+local function str_natural_cmp(lhs, rhs)
+    while #lhs > 0 and #rhs > 0 do
+        local lmid, lend = lhs:find('%d+')
+        local rmid, rend = rhs:find('%d+')
+        if not (lmid and rmid) then return lhs < rhs end
+
+        local lsub = lhs:sub(1, lmid-1)
+        local rsub = rhs:sub(1, rmid-1)
+        if lsub ~= rsub then
+            return lsub < rsub
+        end
+        
+        local lnum = tonumber(lhs:sub(lmid, lend))
+        local rnum = tonumber(rhs:sub(rmid, rend))
+        if lnum ~= rnum then
+            return lnum < rnum
+        end
+
+        lhs = lhs:sub(lend+1)
+        rhs = rhs:sub(rend+1)
+    end
+    return lhs < rhs
+end
+
+local function cmp(lhs, rhs)
+    local tleft = type(lhs)
+    local tright = type(rhs)
+    if tleft == 'number' and tright == 'number' then return lhs < rhs end
+    if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end
+    if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end
+
+    -- allow custom types
+    local oleft = TYPES[tleft] or 9
+    local oright = TYPES[tright] or 9
+    return oleft < oright
+end
+
+-- setup option with default
+local function make_option(option)
+    if option == nil then
+        option = {}
+    end
+    for k, v in pairs(pprint.defaults) do
+        if option[k] == nil then
+            option[k] = v
+        end
+        if option.show_all then
+            for t, _ in pairs(TYPES) do
+                option['show_'..t] = true
+            end
+            option.show_metatable = true
+        end
+    end
+    return option
+end
+
+-- override defaults and take effects for all following calls
+function pprint.setup(option)
+    pprint.defaults = make_option(option)
+end
+
+-- format lua object into a string
+function pprint.pformat(obj, option, printer)
+    option = make_option(option)
+    local buf = {}
+    local function default_printer(s)
+        table.insert(buf, s)
+    end
+    printer = printer or default_printer
+
+    local cache
+    if option.object_cache == 'global' then
+        -- steal the cache into a local var so it's not visible from _G or anywhere
+        -- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway
+        cache = pprint._cache or {}
+        pprint._cache = nil
+    elseif option.object_cache == 'local' then
+        cache = {}
+    end
+
+    local last = '' -- used for look back and remove trailing comma
+    local status = {
+        indent = '', -- current indent
+        len = 0,     -- current line length
+    }
+
+    local wrapped_printer = function(s)
+        printer(last)
+        last = s
+    end
+
+    local function _indent(d)
+        status.indent = string.rep(' ', d + #(status.indent))
+    end
+
+    local function _n(d)
+        wrapped_printer('\n')
+        wrapped_printer(status.indent)
+        if d then
+            _indent(d)
+        end
+        status.len = 0
+        return true -- used to close bracket correctly
+    end
+
+    local function _p(s, nowrap)
+        status.len = status.len + #s
+        if not nowrap and status.len > option.level_width then
+            _n()
+            wrapped_printer(s)
+            status.len = #s
+        else
+            wrapped_printer(s)
+        end
+    end
+
+    local formatter = {}
+    local function format(v)
+        local f = formatter[type(v)]
+        f = f or formatter.table -- allow patched type()
+        if option.filter_function and option.filter_function(v, nil, nil) then
+            return ''
+        else
+            return f(v)
+        end
+    end
+
+    local function tostring_formatter(v)
+        return tostring(v)
+    end
+
+    local function number_formatter(n)
+        return n == math.huge and '[[math.huge]]' or tostring(n)
+    end
+
+    local function nop_formatter(v)
+        return ''
+    end
+
+    local function make_fixed_formatter(t, has_cache)
+        if has_cache then
+            return function (v)
+                return string.format('[[%s %d]]', t, cache[t][v])
+            end
+        else
+            return function (v)
+                return '[['..t..']]'
+            end
+        end
+    end
+
+    local function string_formatter(s, force_long_quote)
+        local s, quote = escape(s)
+        local quote_len = force_long_quote and 4 or 2
+        if quote_len + #s + status.len > option.level_width then
+            _n()
+            -- only wrap string when is longer than level_width
+            if option.wrap_string and #s + quote_len > option.level_width then
+                -- keep the quotes together
+                _p('[[')
+                while #s + status.len >= option.level_width do
+                    local seg = option.level_width - status.len
+                    _p(string.sub(s, 1, seg), true)
+                    _n()
+                    s = string.sub(s, seg+1)
+                end
+                _p(s) -- print the remaining parts
+                return ']]' 
+            end
+        end
+
+        return force_long_quote and '[['..s..']]' or quote..s..quote
+    end
+
+    local function table_formatter(t)
+        if option.use_tostring then
+            local mt = getmetatable(t)
+            if mt and mt.__tostring then
+                return string_formatter(tostring(t), true)
+            end
+        end
+
+        local print_header_ix = nil
+        local ttype = type(t)
+        if option.object_cache then
+            local cache_state = cache.visited_tables[t]
+            local tix = cache[ttype][t]
+            -- FIXME should really handle `cache_state == nil`
+            -- as user might add things through filter_function
+            if cache_state == false then
+                -- already printed, just print the the number
+                return string_formatter(string.format('%s %d', ttype, tix), true)
+            elseif cache_state > 1 then
+                -- appeared more than once, print table header with number
+                print_header_ix = tix
+                cache.visited_tables[t] = false
+            else
+                -- appeared exactly once, print like a normal table
+            end
+        end
+
+        local limit = tonumber(option.depth_limit)
+        if limit and depth > limit then
+           if print_header_ix then
+              return string.format('[[%s %d]]...', ttype, print_header_ix)
+           end
+           return string_formatter(tostring(t), true)
+        end
+
+        local tlen = #t
+        local wrapped = false
+        _p('{')
+        _indent(option.indent_size)
+        _p(string.rep(' ', option.indent_size - 1))
+        if print_header_ix then
+            _p(string.format('--[[%s %d]] ', ttype, print_header_ix))
+        end
+        for ix = 1,tlen do
+            local v = t[ix]
+            if formatter[type(v)] == nop_formatter or 
+               (option.filter_function and option.filter_function(v, ix, t)) then
+               -- pass
+            else
+                if option.wrap_array then
+                    wrapped = _n()
+                end
+                depth = depth+1
+                _p(format(v)..', ')
+                depth = depth-1
+            end
+        end
+
+        -- hashmap part of the table, in contrast to array part
+        local function is_hash_key(k)
+            if type(k) ~= 'number' then
+                return true
+            end
+
+            local numkey = math.floor(tonumber(k))
+            if numkey ~= k or numkey > tlen or numkey <= 0 then
+                return true
+            end
+        end
+
+        local function print_kv(k, v, t)
+            -- can't use option.show_x as obj may contain custom type
+            if formatter[type(v)] == nop_formatter or
+               formatter[type(k)] == nop_formatter or 
+               (option.filter_function and option.filter_function(v, k, t)) then
+                return
+            end
+            wrapped = _n()
+            if is_plain_key(k) then
+                _p(k, true)
+            else
+                _p('[')
+                -- [[]] type string in key is illegal, needs to add spaces inbetween
+                local k = format(k)
+                if string.match(k, '%[%[') then
+                    _p(' '..k..' ', true)
+                else
+                    _p(k, true)
+                end
+                _p(']')
+            end
+            _p(' = ', true)
+            depth = depth+1
+            _p(format(v), true)
+            depth = depth-1
+            _p(',', true)
+        end
+
+        if option.sort_keys then
+            local keys = {}
+            for k, _ in pairs(t) do
+                if is_hash_key(k) then
+                    table.insert(keys, k)
+                end
+            end
+            table.sort(keys, cmp)
+            for _, k in ipairs(keys) do
+                print_kv(k, t[k], t)
+            end
+        else
+            for k, v in pairs(t) do
+                if is_hash_key(k) then
+                    print_kv(k, v, t)
+                end
+            end
+        end
+
+        if option.show_metatable then
+            local mt = getmetatable(t)
+            if mt then
+                print_kv('__metatable', mt, t)
+            end
+        end
+
+        _indent(-option.indent_size)
+        -- make { } into {}
+        last = string.gsub(last, '^ +$', '')
+        -- peek last to remove trailing comma
+        last = string.gsub(last, ',%s*$', ' ')
+        if wrapped then
+            _n()
+        end
+        _p('}')
+
+        return ''
+    end
+
+    -- set formatters
+    formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter
+    formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter
+    formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge
+    formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter
+    formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter
+    formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter
+    formatter['string'] = option.show_string and string_formatter or nop_formatter
+    formatter['table'] = option.show_table and table_formatter or nop_formatter
+
+    if option.object_cache then
+        -- needs to visit the table before start printing
+        cache_apperance(obj, cache, option)
+    end
+
+    _p(format(obj))
+    printer(last) -- close the buffered one
+
+    -- put cache back if global
+    if option.object_cache == 'global' then
+        pprint._cache = cache
+    end
+
+    return table.concat(buf)
+end
+
+-- pprint all the arguments
+function pprint.pprint( ... )
+    local args = {...}
+    -- select will get an accurate count of array len, counting trailing nils
+    local len = select('#', ...)
+    for ix = 1,len do
+        pprint.pformat(args[ix], nil, io.write)
+        io.write('\n')
+    end
+end
+
+setmetatable(pprint, {
+    __call = function (_, ...)
+        pprint.pprint(...)
+    end
+})
+
+return pprint
+
diff --git a/main.lua b/main.lua
new file mode 100644 (file)
index 0000000..1dc3e36
--- /dev/null
+++ b/main.lua
@@ -0,0 +1,271 @@
+-- Required to use the import and class statements
+require "lualib.oop"
+
+import {
+       decompile = "decompile:";
+       VAL_TYPES = "decompile:VAL_TYPES";
+
+       Ui = "ui:Ui";
+       UiRegion = "ui:UiRegion";
+       ScrollingUiRegion = "ui:ScrollingUiRegion";
+       DraggableRect = "ui:DraggableRect";
+
+       render_text = "ui:render_text";
+
+       pprint = "lualib.pprint";
+}
+
+function build_func_header_text(func, mod)
+       local text = {}
+
+       table.insert(text, { text = "[" .. func.funcidx .. "] ", color = { 0, 0, 0 } })
+       table.insert(text, { text = func.name, color = { 0, 0, 1 } })
+       table.insert(text, { text = ": ", color = { 0, 0, 0 } })
+
+       local type_ = func.type_
+       for _, t in ipairs(type_.param_types) do
+               table.insert(text, { text = t .. " ", color = { 0, 0, 0 } })
+       end
+
+       if #type_.param_types == 0 then
+               table.insert(text, { text = "void ", color = { 0, 0, 0 } })
+       end
+
+       table.insert(text, { text = "-> ", color = { 0, 0, 0 } })
+
+       for _, t in ipairs(type_.result_types) do
+               table.insert(text, { text = t .. " ", color = { 0, 0, 0 } })
+       end
+
+
+       if #type_.result_types == 0 then
+               table.insert(text, { text = "void", color = { 0, 0, 0 } })
+       end
+
+       table.insert(text, { newline = true })
+
+       return text
+end
+
+local line = 1
+
+function add_line_number(textlist, newline)
+       if newline == nil then newline = true end
+       table.insert(textlist, { newline = newline })
+       table.insert(textlist, { text = ("%4d"):format(line), color = { 0.3, 0.3, 0.3 }, background = { 0.7, 0.7, 0.7 }, post_indent = true })
+       table.insert(textlist, { text = " ", color = { 0, 0, 0 }, post_indent = true })
+       line = line + 1
+end
+
+function instr_to_text(textlist, instr, func, mod)
+       if instr[1] == "block" then
+               table.insert(textlist, { text = "block", color = { 1, 0, 0 }, change_indent = 1 })
+               for _, ins in ipairs(instr.instrs) do
+                       add_line_number(textlist)
+                       instr_to_text(textlist, ins, func, mod)
+               end
+               table.insert(textlist, { change_indent = -1 })
+
+       elseif instr[1] == "loop" then
+               table.insert(textlist, { text = "loop", color = { 1, 0, 0 }, change_indent = 1 })
+               for _, ins in ipairs(instr.instrs) do
+                       add_line_number(textlist)
+                       instr_to_text(textlist, ins, func, mod)
+               end
+               table.insert(textlist, { change_indent = -1 })
+
+       elseif instr[1] == "if" then
+               table.insert(textlist, { text = "if", color = { 1, 0, 0 }, change_indent = 1 })
+               for _, ins in ipairs(instr.instrs) do
+                       add_line_number(textlist)
+                       instr_to_text(textlist, ins, func, mod)
+               end
+               table.insert(textlist, { change_indent = -1 })
+
+               if #instr.else_instrs > 0 then
+                       add_line_number(textlist)
+                       table.insert(textlist, { text = "else", color = { 1, 0, 0 }, change_indent = 1 })
+                       for _, ins in ipairs(instr.instrs) do
+                               add_line_number(textlist)
+                               instr_to_text(textlist, ins, func, mod)
+                       end
+                       table.insert(textlist, { change_indent = -1 })
+               end
+
+       elseif instr[1] == "call" then
+               table.insert(textlist, { text = instr[1] .. " ", color = { 0, 0, 1 } })
+               table.insert(textlist, { text = mod.funcs[instr.x].name, color = { 0, 0.5, 0 } })
+
+       elseif instr[1]:match("local") then
+               table.insert(textlist, { text = instr[1] .. " ", color = { 0, 0, 1 } })
+               table.insert(textlist, { text = func.locals[instr.x + 1].name, color = { 0, 0.5, 0 } })
+
+       else
+               table.insert(textlist, { text = instr[1] .. " ", color = { 0, 0, 1 } })
+               if instr.x then
+                       if type(instr.x) == "table" then
+                               if instr.x[1] == "memarg" then
+                                       table.insert(textlist, {
+                                               text = ("align=0x%03x offset=0x%08x"):format(instr.x.align, instr.x.offset);
+                                               color = { 0, 0.5, 0 }
+                                       })
+                               end
+                       else
+                               table.insert(textlist, { text = tostring(instr.x), color = { 0, 0.5, 0 } })
+                       end
+               end
+       end
+
+       return line
+end
+
+function build_func_body_text(func, mod)
+       local text = {}
+       line = 1
+
+       table.insert(text, { text = "Locals:", color = { 0, 0, 1 }, newline = true, change_indent = 2 })
+       for _, loc in ipairs(func.locals) do
+               table.insert(text, { text = loc.type_ .. " " .. loc.name, color = { 0, 0, 0 }, newline = true })
+       end
+
+       if #func.locals == 0 then
+               table.insert(text, { text = "no locals", color = { 1, 0, 0 }, newline = true })
+       end
+
+       table.insert(text, { newline = true, change_indent = -2 })
+       table.insert(text, { text = "Body:", color = { 0, 0, 1 } })
+
+       for _, instr in ipairs(func.body) do
+               add_line_number(text)
+               instr_to_text(text, instr, func, mod)
+       end
+
+       -- table.remove(text, #text)
+
+       return text
+end
+
+class "FunctionBlock" [DraggableRect] {
+       init = function(self, func, x, y)
+               DraggableRect.init(self, x, y, 400, 400)
+
+               self.header_text = build_func_header_text(func, mod)
+               self.body_text = build_func_body_text(func, mod)
+               self.scroll = 0
+       end;
+
+       onwheel = function(self, _, dy)
+               self.scroll = self.scroll - dy * 10;
+               if self.scroll < 0 then self.scroll = 0 end
+       end;
+
+       ondrag = function(self, button, x, y, dx, dy)
+               DraggableRect.ondrag(self, button, x, y, dx, dy)
+
+               if button == 2 then
+                       self.rect.w = math.max(100, x)
+                       self.rect.h = math.max(22, y)
+               end
+       end;
+
+       draw = function(self)
+               love.graphics.setColor(0, 0, 0)
+               love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h)
+               love.graphics.setColor(1, 1, 1)
+               love.graphics.rectangle("fill", 2, 2, self.rect.w - 4, self.rect.h - 4)
+               love.graphics.setColor(0.8, 0.8, 0.8)
+               love.graphics.rectangle("fill", 2, 2, self.rect.w - 4, 18)
+
+               local sx, sy = love.graphics.transformPoint(0, 0)
+               love.graphics.intersectScissor(sx, sy, self.rect.w, self.rect.h)
+               local x, y = render_text(2, 2, self.header_text)
+
+               local sx, sy = love.graphics.transformPoint(2, y - 1)
+               love.graphics.intersectScissor(sx, sy, self.rect.w - 4, self.rect.h - 22)
+               render_text(2, y + 4 - self.scroll, self.body_text)
+       end;
+}
+
+mod = nil
+ui = nil
+function love.load()
+       ui = Ui()
+
+       mod = decompile(arg[2])
+
+       ui_region = ScrollingUiRegion()
+       ui_region.rect.x = 0
+       ui_region.rect.y = 0
+       ui_region.rect.w = 1200
+       ui_region.rect.h = 900
+       ui_region.background_color = { 0.1, 0.1, 0.1 }
+       ui_region.line_color = { 0.2, 0.2, 0.2 }
+
+       -- ui_region:add_object(DraggableRect(100, 100, 100, 100))
+       -- ui_region:add_object(DraggableRect(200, 100, 100, 100))
+       -- ui_region:add_object(DraggableRect(300, 100, 100, 100))
+       -- ui_region:add_object(DraggableRect(400, 100, 300, 100))
+
+       local x = 0
+       local y = 0
+       for _, func in pairs(mod.funcs) do
+               if func.body then
+                       local block = FunctionBlock(func, x, y)
+                       block.order = 600 - y
+                       ui_region:add_object(block)
+                       y = y + 20
+               end
+       end
+
+       ui:add_region(ui_region)
+
+       -- bottom_bar_region = UiRegion()
+       -- bottom_bar_region.rect.x = 0
+       -- bottom_bar_region.rect.y = 800
+       -- bottom_bar_region.rect.w = 1200
+       -- bottom_bar_region.rect.h = 100
+       -- bottom_bar_region.layer = 1
+       -- bottom_bar_region.background_color = { 1, 0, 0 }
+
+       -- ui:add_region(bottom_bar_region)
+end
+
+function love.update(dt)
+       if love.keyboard.isDown "escape" then
+               love.event.quit()
+       end
+
+       ui:update(dt)
+end
+
+function love.mousepressed(x, y, button)
+       ui:onmousedown(button, x, y)
+end
+
+function love.mousereleased(x, y, button)
+       ui:onmouseup(button, x, y)
+end
+
+function love.mousemoved(x, y, dx, dy)
+       ui:onmousemove(x, y, dx, dy)
+end
+
+function love.wheelmoved(dx, dy)
+       ui:onmousewheel(dx, dy)
+end
+
+function love.keypressed(key)
+       ui:onkeydown(key)
+end
+
+function love.keyreleased(key)
+       ui:onkeyup(key)
+end
+
+function love.resize(w, h)
+       ui:onresize(w, h)
+end
+
+function love.draw()
+       ui:draw()
+end
diff --git a/res/FiraCode-Regular.ttf b/res/FiraCode-Regular.ttf
new file mode 100644 (file)
index 0000000..97c1159
Binary files /dev/null and b/res/FiraCode-Regular.ttf differ
diff --git a/testing.lua b/testing.lua
new file mode 100644 (file)
index 0000000..6f76fe2
--- /dev/null
@@ -0,0 +1,11 @@
+require "lualib.oop"
+
+import {
+       decompile = "decompile:";
+       build_str = "utils:build_str";
+       pprint = "lualib.pprint";
+}
+
+local mod = decompile("./data/main.wasm")
+pprint(mod)
+
diff --git a/ui.lua b/ui.lua
new file mode 100644 (file)
index 0000000..c9aa260
--- /dev/null
+++ b/ui.lua
@@ -0,0 +1,438 @@
+import {
+       Rectangle = "utils:Rectangle";
+       uuid = "utils:uuidv4";
+       revipairs = "utils:revipairs";
+}
+
+class "Ui" {
+       init = function(self)
+               self.regions = {}
+               self.active_region = nil
+       end;
+
+       add_region = function(self, region)
+               table.insert(self.regions, region)
+               table.sort(self.regions)
+       end;
+
+       onmousedown = function(self, button, x, y)
+               local region = nil
+               for _, r in revipairs(self.regions) do
+                       if r.rect:contains(x, y) then
+                               region = r
+                               break
+                       end
+               end
+
+               if region ~= nil then
+                       region:onmousedown(button, x - region.rect.x, y - region.rect.y)
+               end
+       end;
+
+       onmouseup = function(self, button, x, y)
+               local region = nil
+               for _, r in revipairs(self.regions) do
+                       if r.rect:contains(x, y) then
+                               region = r
+                               break
+                       end
+               end
+
+               if region ~= nil then
+                       region:onmouseup(button, x - region.rect.x, y - region.rect.y)
+               end
+       end;
+
+       onmousemove = function(self, x, y, dx, dy)
+               local region = nil
+               for _, r in revipairs(self.regions) do
+                       if r.rect:contains(x, y) then
+                               region = r
+                               break
+                       end
+               end
+
+               if region ~= nil then
+                       if region ~= self.active_region then
+                               if self.active_region ~= nil then
+                                       self.active_region:onmouseleave()
+                               end
+
+                               region:onmouseenter()
+                       end
+                       region:onmousemove(x - region.rect.x, y - region.rect.y, dx, dy)
+               end
+
+               self.active_region = region
+       end;
+
+       onmousewheel = function(self, dx, dy)
+               if not self.active_region then return end
+               self.active_region:onmousewheel(dx, dy)
+       end;
+
+       onkeydown = function(self, key)
+               if not self.active_region then return end
+               self.active_region:onkeydown(key)
+       end;
+
+       onkeyup = function(self, key)
+               if not self.active_region then return end
+               self.active_region:onkeyup(key)
+       end;
+
+       onresize = function(self, new_width, new_height)
+       end;
+
+       update = function(self, dt)
+               for _, region in ipairs(self.regions) do
+                       region:update(dt);
+               end
+       end;
+
+       draw = function(self)
+               for _, region in ipairs(self.regions) do
+                       love.graphics.push()
+                       love.graphics.setScissor(region.rect.x, region.rect.y, region.rect.w, region.rect.h)
+                       love.graphics.translate(region.rect.x, region.rect.y)
+                       region:draw()
+                       love.graphics.pop()
+               end
+       end;
+}
+
+class "UiRegion" {
+       init = function(self)
+               self.active = false
+
+               self.selected_object = nil
+               self.objects = {}
+
+               self.rect = Rectangle(0, 0, 0, 0)
+               self.background_color = { 0, 0, 0 }
+               self.layer = 0
+
+               -- Internals
+               self.is_mouse_down = false
+               self.fire_onclick = false
+       end;
+
+       add_object = function(self, obj)
+               table.insert(self.objects, obj)
+       end;
+
+       -- Used for sorting in the Ui
+       __lt = function(self, other)
+               return self.layer < other.layer
+       end;
+
+       _obj_rect = function(self, obj)
+               return obj.rect
+       end;
+
+       onmousedown = function(self, button, x, y)
+               self.is_mouse_down = button
+               self.fire_onclick = true
+               local new_selected = nil
+
+               for _, obj in revipairs(self.objects) do
+                       if self:_obj_rect(obj):contains(x, y) then
+                               new_selected = obj
+                               break
+                       end
+               end
+
+               if self.selected_object ~= new_selected then
+                       if self.selected_object ~= nil then
+                               self.selected_object:onunselect()
+                       end
+
+                       self.selected_object = new_selected
+
+                       if self.selected_object ~= nil then
+                               self.selected_object.order = 0
+
+                               for _, obj in ipairs(self.objects) do
+                                       if obj ~= self.selected_obj then
+                                               obj.order = obj.order + 1
+                                       end
+                               end
+
+                               table.sort(self.objects, function(a, b)
+                                       return a.order > b.order
+                               end)
+
+                               self.selected_object:onselect()
+                       end
+               end
+       end;
+
+       onmouseup = function(self, button, x, y)
+               self.is_mouse_down = false
+
+               if self.selected_object ~= nil and self.fire_onclick then
+                       local obj_rect = self:_obj_rect(self.selected_object)
+                       self.selected_object:onclick(button, x - obj_rect.x, y - obj_rect.y)
+               end
+       end;
+       onmousemove = function(self, x, y, dx, dy)
+               if self.is_mouse_down and self.selected_object ~= nil then
+                       local obj_rect = self:_obj_rect(self.selected_object)
+                       self.selected_object:ondrag(self.is_mouse_down, x - obj_rect.x, y - obj_rect.y, dx, dy)
+                       self.fire_onclick = false
+               end
+       end;
+       onmouseenter = function(self)
+               self.active = true
+       end;
+
+       onmouseleave = function(self)
+               self.active = false;
+               self.is_mouse_down = false
+               self.fire_onclick = false
+               self.selected_object = nil
+       end;
+
+       onmousewheel = function(self, dx, dy)
+               if self.selected_object ~= nil then
+                       self.selected_object:onwheel(dx, dy)
+               end
+       end;
+
+       onkeydown = function(self, key) end;
+       onkeyup = function(self, key) end;
+
+       update = function(self, dt)
+               for _, obj in ipairs(self.objects) do
+                       obj:update(dt)
+               end
+       end;
+
+       draw = function(self)
+               if self.background_color ~= nil then
+                       love.graphics.setColor(self.background_color)
+                       love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h)
+               end
+
+               for _, obj in ipairs(self.objects) do
+                       love.graphics.push()
+                       love.graphics.translate(obj.rect.x, obj.rect.y)
+                       obj:draw()
+                       love.graphics.setScissor()
+                       love.graphics.pop()
+               end
+       end;
+}
+
+class "ScrollingUiRegion" [UiRegion] {
+       init = function(self)
+               UiRegion.init(self)
+
+               self.offset = { x = 0; y = 0; }
+               self.line_color = { 0, 0, 0 }
+
+               self.zoom = 1
+       end;
+
+       _obj_rect = function(self, obj)
+               return Rectangle(
+                       obj.rect.x + self.offset.x,
+                       obj.rect.y + self.offset.y,
+                       obj.rect.w, obj.rect.h)
+       end;
+
+       onmousemove = function(self, x, y, dx, dy)
+               UiRegion.onmousemove(self, x, y, dx, dy)
+
+               if self.is_mouse_down and self.selected_object == nil then
+                       self.offset.x = self.offset.x + dx
+                       self.offset.y = self.offset.y + dy
+               end
+       end;
+
+       onmousewheel = function(self, dx, dy)
+               UiRegion.onmousewheel(self, dx, dy)
+
+               if self.selected_object == nil then
+                       self.zoom = self.zoom * (dy > 0 and 1.05 or (1 / 1.05))
+                       if self.zoom >= 1 then self.zoom = 1 end
+               end
+       end;
+
+       update = function(self, dt)
+               UiRegion.update(self, dt)
+
+               if self.active then
+                       local speed = 300
+                       if love.keyboard.isDown "left" then
+                               self.offset.x = self.offset.x + speed * dt
+                       end
+                       if love.keyboard.isDown "right" then
+                               self.offset.x = self.offset.x - speed * dt
+                       end
+                       if love.keyboard.isDown "up" then
+                               self.offset.y = self.offset.y + speed * dt
+                       end
+                       if love.keyboard.isDown "down" then
+                               self.offset.y = self.offset.y - speed * dt
+                       end
+               end
+       end;
+
+       draw = function(self)
+               love.graphics.push()
+
+               if self.background_color ~= nil then
+                       love.graphics.setColor(self.background_color)
+                       love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h)
+               end
+               local tmpbgcolor = self.background_color
+               self.background_color = nil
+
+               local xoff = self.offset.x
+               local yoff = self.offset.y
+               local spacing = 64
+
+               while xoff >= spacing do xoff = xoff - spacing end
+               while yoff >= spacing do yoff = yoff - spacing end
+               while xoff <= -spacing do xoff = xoff + spacing end
+               while yoff <= -spacing do yoff = yoff + spacing end
+
+               love.graphics.setColor(self.line_color)
+               for x = 0, 40 do
+                       love.graphics.line(x * spacing + xoff, 0, x * spacing + xoff, self.rect.h)
+               end
+               for y = 0, 40 do
+                       love.graphics.line(0, y * spacing + yoff, self.rect.w, y * spacing + yoff)
+               end
+
+               love.graphics.translate(self.offset.x, self.offset.y)
+               UiRegion.draw(self)
+               love.graphics.pop()
+
+               self.background_color = tmpbgcolor
+       end;
+}
+
+class "UiObject" {
+       init = function(self)
+               self.rect = Rectangle(0, 0, 0, 0)
+               self.uuid = uuid()
+               self.selected = false
+               self.order = math.random(1, 128)
+       end;
+
+       onclick = function(self, button, x, y) end;
+       ondrag = function(self, button, x, y, dx, dy) end;
+       onwheel = function(self, dx, dy) end;
+
+       onselect = function(self)
+               self.selected = true
+       end;
+       onunselect = function(self)
+               self.selected = false
+       end;
+
+       onkey = function(self, button) end;
+
+       update = function(dt) end;
+       draw = function() end;
+}
+
+class "DraggableRect" [UiObject] {
+       init = function(self, x, y, w, h)
+               UiObject.init(self)
+
+               self.rect = Rectangle(x, y, w, h)
+       end;
+
+       ondrag = function(self, button, x, y, dx, dy)
+               if button == 1 then
+                       self.rect.x = self.rect.x + dx
+                       self.rect.y = self.rect.y + dy
+               end
+       end;
+
+       draw = function(self)
+               if self.selected then
+                       love.graphics.setColor(0, 0, 1)
+                       love.graphics.rectangle("fill", -2, -2, self.rect.w + 4, self.rect.h + 4)
+               end
+               love.graphics.setColor(1, 1, 1)
+               love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h)
+
+               render_text(0, 0, {
+                       { text = "1 Hello ", color = { 1, 0, 0 } };
+                       { text = "world",  color = { 0, 0, 1 } };
+                       { newline = true };
+                       { text = "2 Nother line", color = { 0, 0.5, 0 } };
+                       { newline = true };
+                       { newline = true };
+                       { text = "3 nother Nother line", color = { 0, 0.5, 0 } };
+               })
+       end;
+}
+
+programming_font = false
+function render_text(x, y, text)
+       if not programming_font then
+               programming_font = love.graphics.newFont("res/FiraCode-Regular.ttf", 14)
+               love.graphics.setFont(programming_font)
+       end
+
+       local origx = x
+       local fheight = programming_font:getHeight()
+       local indent_level = 0
+       local indentation = ""
+       local fresh_newline = true
+
+       for _, txt in ipairs(text) do
+               if txt.text then
+                       local printed = txt.text
+                       if fresh_newline then
+                               if txt.post_indent then
+                                       printed = txt.text .. indentation
+                               else
+                                       printed = indentation .. txt.text
+                               end
+                               fresh_newline = false
+                       end
+
+                       if txt.background then
+                               love.graphics.setColor(txt.background)
+                               love.graphics.rectangle("fill", x, y, programming_font:getWidth(txt.text), fheight + 2);
+                       end
+
+                       love.graphics.setColor(txt.color)
+                       love.graphics.print(printed, x, y)
+
+                       x = x + programming_font:getWidth(printed)
+               end
+
+               if txt.newline then
+                       y = y + fheight + 2;
+                       x = origx
+                       fresh_newline = true
+               end
+
+               if txt.change_indent then
+                       indent_level = indent_level + txt.change_indent
+                       indentation = ""
+                       for i=1,indent_level do
+                               indentation = indentation .. "  "
+                       end
+               end
+       end
+
+       return x, y
+end
+
+return module {
+       Ui = Ui;
+       UiRegion = UiRegion;
+       ScrollingUiRegion = ScrollingUiRegion;
+       UiObject = UiObject;
+       DraggableRect = DraggableRect;
+
+       render_text = render_text;
+}
diff --git a/utils.lua b/utils.lua
new file mode 100644 (file)
index 0000000..ec4b5e0
--- /dev/null
+++ b/utils.lua
@@ -0,0 +1,101 @@
+import {}
+
+-- This is unsued in favor of the C equivalent
+function buffered_read(file, buffer_size)
+       buffer_size = buffer_size or 4 * 1024
+
+       local buffer = {}
+       local ptr = -1
+
+       return coroutine.wrap(function()
+               while true do
+                       if ptr <= 0 or ptr >= buffer_size then
+                               ptr = 0
+                               local str = file:read(buffer_size)
+                               if not str then break end
+
+                               buffer = {}
+                               for c in (str or ''):gmatch '.' do
+                                       buffer[#buffer + 1] = c
+                               end
+                       end
+
+                       ptr = ptr + 1
+                       if ptr > #buffer then break end
+                       coroutine.yield(buffer[ptr]:byte())
+               end
+               return
+       end)
+end
+
+function build_str(tbl_int)
+       if #tbl_int < 30 then
+               return string.char(unpack(tbl_int))
+       else
+               -- This is slower but may be needed for long strings
+               local str = ""
+               for _, c in ipairs(tbl_int) do
+                       str = str .. string.char(c)
+               end
+               return str
+       end
+end
+
+function uuidv4()
+       local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
+       return string.gsub(template, '[xy]', function (c)
+               local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb)
+               return string.format('%x', v)
+       end)
+end
+
+function revipairs(t)
+       local max = 1
+       while t[max] ~= nil do
+               max = max + 1
+       end
+       local function ripairs_it(t, i)
+               i = i - 1
+               local v = t[i]
+               if v ~= nil then
+                       return i, v
+               else
+                       return nil
+               end
+       end
+       return ripairs_it, t, max
+end
+
+class "Rectangle" {
+       init = function(self, x, y, w, h)
+               self.x = x
+               self.y = y
+               self.w = w
+               self.h = h
+       end;
+
+       contains = function(self, x, y)
+               return x >= self.x and x <= self.x + self.w
+                       and y >= self.y and y <= self.y + self.h
+       end;
+
+       intersects = function(self, other)
+               return self.x <= other.x + other.w
+                       and self.x + self.w >= other.x
+                       and self.y <= other.y + other.h
+                       and self.y + self.h >= other.y
+       end;
+
+       __lt = function(self, other) return self:intersects(other) end
+}
+
+return module {
+       buffered_read = buffered_read;
+       build_str = build_str;
+
+       uuidv4 = uuidv4;
+
+       revipairs = revipairs;
+
+       Rectangle = Rectangle;
+}