Skip to content

Commit

Permalink
feature: export globals, text encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
stagas committed Jul 31, 2022
1 parent 9320a68 commit a0db706
Show file tree
Hide file tree
Showing 10 changed files with 646 additions and 395 deletions.
14 changes: 10 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
declare interface Options {
export declare interface Options {
metrics: boolean
}

declare interface Context {
export declare interface GlobalContext {
globals: ({ name: string, vartype: string, type: string })[]
}

export declare interface Context {
module: ModuleBuilder
context: any
global: GlobalContext
}

declare const make: {
Expand All @@ -14,7 +18,7 @@ declare const make: {
declare type ModuleBuilder = any

declare const compile: {
(node: Node): ModuleBuilder
(node: Node): Context
}

declare type Node = any
Expand All @@ -28,6 +32,8 @@ declare const tokenize: {
}

export {
GlobalContext,
ModuleBuilder,
tokenize,
parse,
compile,
Expand Down
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import ModuleBuilder from './lib/builder.js'
import compile, { GlobalContext } from './lib/compiler.js'
import { tokenize } from './lib/lexer.js'
import parse from './lib/parser.js'
import compile from './lib/compiler.js'

export { tokenize }
export { parse }
export { compile }
export { ModuleBuilder, GlobalContext }

/**
* Compiles a WAT source string to a buffer.
Expand All @@ -20,6 +22,9 @@ export { compile }
* @param {string} code The WAT code to compile
* @param {Options} options An options object
* @param {boolean} options.metrics Enable metrics with console.time
* @param {Context} context
* @param {ModuleBuilder} context.module
* @param {GlobalContext} context.global
* @returns {Uint8Array} The buffer to be passed on to WebAssembly
*/
export default function make(code, options, context = {}) {
Expand Down
28 changes: 27 additions & 1 deletion lib/binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,34 @@
* limitations under the License.
*/

import { int, uint, bigint, f32, f64 } from './leb128.js'
// https://github.com/samthor/fast-text-encoding/blob/master/text.js
(function (l) {
function m() { } function k(a, c) { a = void 0 === a ? "utf-8" : a; c = void 0 === c ? { fatal: !1 } : c; if (-1 === r.indexOf(a.toLowerCase())) throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('" + a + "') is invalid."); if (c.fatal) throw Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported."); } function t(a) { return Buffer.from(a.buffer, a.byteOffset, a.byteLength).toString("utf-8") } function u(a) {
var c = URL.createObjectURL(new Blob([a], { type: "text/plain;charset=UTF-8" }));
try { var f = new XMLHttpRequest; f.open("GET", c, !1); f.send(); return f.responseText } catch (e) { return q(a) } finally { URL.revokeObjectURL(c) }
} function q(a) {
for (var c = 0, f = Math.min(65536, a.length + 1), e = new Uint16Array(f), h = [], d = 0; ;) {
var b = c < a.length; if (!b || d >= f - 1) { h.push(String.fromCharCode.apply(null, e.subarray(0, d))); if (!b) return h.join(""); a = a.subarray(c); d = c = 0 } b = a[c++]; if (0 === (b & 128)) e[d++] = b; else if (192 === (b & 224)) { var g = a[c++] & 63; e[d++] = (b & 31) << 6 | g } else if (224 === (b & 240)) {
g = a[c++] & 63; var n = a[c++] & 63; e[d++] =
(b & 31) << 12 | g << 6 | n
} else if (240 === (b & 248)) { g = a[c++] & 63; n = a[c++] & 63; var v = a[c++] & 63; b = (b & 7) << 18 | g << 12 | n << 6 | v; 65535 < b && (b -= 65536, e[d++] = b >>> 10 & 1023 | 55296, b = 56320 | b & 1023); e[d++] = b }
}
} if (l.TextEncoder && l.TextDecoder) return !1; var r = ["utf-8", "utf8", "unicode-1-1-utf-8"]; Object.defineProperty(m.prototype, "encoding", { value: "utf-8" }); m.prototype.encode = function (a, c) {
c = void 0 === c ? { stream: !1 } : c; if (c.stream) throw Error("Failed to encode: the 'stream' option is unsupported."); c = 0; for (var f = a.length, e = 0, h = Math.max(32,
f + (f >>> 1) + 7), d = new Uint8Array(h >>> 3 << 3); c < f;) {
var b = a.charCodeAt(c++); if (55296 <= b && 56319 >= b) { if (c < f) { var g = a.charCodeAt(c); 56320 === (g & 64512) && (++c, b = ((b & 1023) << 10) + (g & 1023) + 65536) } if (55296 <= b && 56319 >= b) continue } e + 4 > d.length && (h += 8, h *= 1 + c / a.length * 2, h = h >>> 3 << 3, g = new Uint8Array(h), g.set(d), d = g); if (0 === (b & 4294967168)) d[e++] = b; else {
if (0 === (b & 4294965248)) d[e++] = b >>> 6 & 31 | 192; else if (0 === (b & 4294901760)) d[e++] = b >>> 12 & 15 | 224, d[e++] = b >>> 6 & 63 | 128; else if (0 === (b & 4292870144)) d[e++] = b >>> 18 & 7 | 240, d[e++] = b >>> 12 &
63 | 128, d[e++] = b >>> 6 & 63 | 128; else continue; d[e++] = b & 63 | 128
}
} return d.slice ? d.slice(0, e) : d.subarray(0, e)
}; Object.defineProperty(k.prototype, "encoding", { value: "utf-8" }); Object.defineProperty(k.prototype, "fatal", { value: !1 }); Object.defineProperty(k.prototype, "ignoreBOM", { value: !1 }); var p = q; "function" === typeof Buffer && Buffer.from ? p = t : "function" === typeof Blob && "function" === typeof URL && "function" === typeof URL.createObjectURL && (p = u); k.prototype.decode = function (a, c) {
c = void 0 === c ? { stream: !1 } : c; if (c.stream) throw Error("Failed to decode: the 'stream' option is unsupported.");
a = a instanceof Uint8Array ? a : a.buffer instanceof ArrayBuffer ? new Uint8Array(a.buffer) : new Uint8Array(a); return p(a)
}; l.TextEncoder = m; l.TextDecoder = k
})("undefined" !== typeof window ? window : "undefined" !== typeof global ? global : globalThis);

import { BYTE } from './const.js'
import { bigint, f32, f64, int, uint } from './leb128.js'

export function wrap_instr (code) {
return function (args, exprs) {
Expand Down
2 changes: 2 additions & 0 deletions lib/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ export default class ModuleBuilder {
[ exp.export_name, exp.type, this.getFunc(exp.name).idx ] :
exp.type === 'memory' ?
[ exp.export_name, exp.type, this.getMemory(exp.name).idx ] :
exp.type === 'global' ?
[ exp.export_name, exp.type, this.getGlobalIndexOf(exp.name) ] :
[] // TODO: exception
))) }

Expand Down
101 changes: 69 additions & 32 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import ModuleBuilder from './builder.js'
import { uint, bigint, hex2float, nanbox32, nanbox64 } from './leb128.js'
import { INSTR, ALIGN } from './const.js'
import { ALIGN, INSTR } from './const.js'
import { bigint, hex2float, nanbox32, nanbox64, uint } from './leb128.js'

// eslint-disable-next-line no-unused-vars
const print = x => console.log(JSON.stringify(x, null, 2))

class GlobalContext {
export class GlobalContext {
globals = []
types = []
funcs = []

constructor(data) {
if (data) Object.assign(this, data)
if (data) {
Object.assign(this, data)
this.funcs.forEach(x => {
x.context = new FunctionContext(this, x.context)
})
}
}

lookup (name, instr) {
Expand All @@ -33,6 +38,41 @@ class GlobalContext {
}
}

if (!~index) throw new ReferenceError(`lookup failed at: ${instr} "${name}"`)

return uint(index)
}
}

class FunctionContext {
#global = null
locals = []
depth = []

constructor(global, data) {
this.#global = global
if (data) Object.assign(this, data)
}

lookup (name, instr) {
let index

switch (instr) {
case 'br':
case 'br_table':
case 'br_if': {
index = this.depth.lastIndexOf(name)
if (~index) index = this.depth.length - 1 - index
}
break

default: {
index = this.locals.lastIndexOf(name)
}
}

if (!~index) return this.#global.lookup(name, instr)

return uint(index)
}
}
Expand Down Expand Up @@ -95,33 +135,6 @@ export default function compile (node, moduleData, globalData) {
}
}

class FunctionContext {
locals = []
depth = []

lookup (name, instr) {
let index

switch (instr) {
case 'br':
case 'br_table':
case 'br_if': {
index = this.depth.lastIndexOf(name)
if (~index) index = this.depth.length - 1 - index
}
break

default: {
index = this.locals.lastIndexOf(name)
}
}

if (!~index) return g.lookup(name, instr)

return uint(index)
}
}

function bytes (instr, args, expr) {
if (!(instr in INSTR) || (typeof INSTR[instr] !== 'function')) {
throw new Error('Unknown instruction: ' + instr)
Expand Down Expand Up @@ -404,6 +417,12 @@ export default function compile (node, moduleData, globalData) {

g.globals.push(glob)

if (glob.type === 'export') {
const export_name = node.children.shift().params[0].param.value
m.export('global', glob.name, export_name)
glob.type = node.children[0].instr.value
}

if (glob.type === 'mut') {
glob.vartype = 'var'
glob.type = node.children[0].children[0].instr.value
Expand Down Expand Up @@ -468,7 +487,7 @@ export default function compile (node, moduleData, globalData) {
case 'func': {
const func = {
name: node.name?.value ?? g.funcs.length,
context: new FunctionContext(),
context: new FunctionContext(g),
params: [],
results: [],
locals: [],
Expand Down Expand Up @@ -508,6 +527,7 @@ export default function compile (node, moduleData, globalData) {
}
}


// function bodies are deferred evaluation
// because we need to have all their names
// in context first because they are called
Expand All @@ -532,5 +552,22 @@ export default function compile (node, moduleData, globalData) {

deferred.forEach(fn => fn())

// const GeneratorFunction = (function*(){yield undefined;}).constructor;

// // recursive function that traverses and expands all generator functions in the object
// function expand(node) {
// if (node instanceof GeneratorFunction) {
// return [...node]
// } else if (Array.isArray(node)) {
// return node.map(expand)
// } else if (node instanceof Object) {
// return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, expand(v)]))
// } else {
// return node
// }
// }

// const g_expanded = expand(g)

return { module: m, global: g }
}
2 changes: 2 additions & 0 deletions lib/leb128.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export function* int (value) {
}

export function* uint (value, pad = 0) {
if (value < 0) throw new TypeError('uint value must be positive, received: ' + value)

let byte = 0x00

do {
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
],
"scripts": {
"build": "rimraf dist && npm run build:esm && npm run build:cjs",
"build:esm": "esbuild --outdir=dist/esm --format=esm --bundle ./index.js --sourcemap && echo '{\"type\":\"module\"}' >dist/esm/package.json",
"build:cjs": "esbuild --outdir=dist/cjs --format=cjs --bundle ./index.js --sourcemap",
"build:esm": "esbuild --outdir=dist/esm --format=esm --bundle ./index.js --sourcemap=both && echo '{\"type\":\"module\"}' >dist/esm/package.json",
"build:cjs": "esbuild --outdir=dist/cjs --format=cjs --bundle ./index.js --sourcemap=both",
"test": "make test",
"prepack": "echo todo prepack",
"lint": "echo todo lint",
Expand All @@ -40,5 +40,8 @@
"short": "stagas/wat-compiler",
"repository": {
"url": "git+https://git@github.com:stagas/wat-compiler.git"
},
"dependencies": {
"fast-text-encoding": "^1.0.3"
}
}
32 changes: 31 additions & 1 deletion test/compiler.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import compile from '../lib/compiler.js'
import { tokenize } from '../lib/lexer.js'
import parse from '../lib/parser.js'
import compile from '../lib/compiler.js'
import { hexAssertEqual } from './util/hex.js'
import wat from './util/wat.js'

Expand All @@ -27,6 +27,22 @@ describe('compile', () => {
expect((await wasm(act)).answer()).to.equal(42)
}))

it('minimal function negative', () => buffers(`
(func (export "answer") (result f32)
(f32.convert_i32_s
(i32.mul
(i32.const -1)
(i32.const 1)
)
)
)
`)
.then(([exp,act]) => hexAssertEqual(exp,act))
.then(async ([exp,act]) => {
expect((await wasm(exp)).answer()).to.equal(-1)
expect((await wasm(act)).answer()).to.equal(-1)
}))

//
it('function with 1 param', () => buffers(`
(func (export "answer") (param i32) (result i32) (local.get 0))
Expand Down Expand Up @@ -186,6 +202,20 @@ describe('compile', () => {
expect((await wasm(act)).get()).to.equal(42)
}))

it('1 global var export (mut)', () => buffers(`
(global $answer (export "answer") (mut i32) (i32.const 42))
(func (export "get") (result i32)
(global.get $answer)
)
`)
.then(([exp,act]) => hexAssertEqual(exp,act))
.then(async ([exp,act]) => {
expect((await wasm(exp)).get()).to.equal(42)
expect((await wasm(act)).get()).to.equal(42)
}))

//
it('1 global var (mut) + mutate', () => buffers(`
(global $answer (mut i32) (i32.const 42))
Expand Down
Loading

0 comments on commit a0db706

Please sign in to comment.