diff --git a/src/benchmark.cts b/src/benchmark.cts index d78836b8..5ef5e9e1 100644 --- a/src/benchmark.cts +++ b/src/benchmark.cts @@ -28,7 +28,7 @@ export default class Benchmark { // console.log('wrote temp trace to disk and start stream code generation') const code = await new Generator().generateReplayFromStream(fss.createReadStream(diskSave)) // console.log('stream code generation finished. Now stream js string to file') - code.toWriteStream(fss.createWriteStream(path.join(binPath, 'replay.js'))) + await code.toWriteStream(fss.createWriteStream(path.join(binPath, 'replay.js'))) // const jsString = new Generator().generateReplay(trace).toString() // console.log('js code generation finished. Now dump wasm to file') // await fs.writeFile(path.join(binPath, 'replay.js'), jsString) diff --git a/src/replay-generator.cts b/src/replay-generator.cts index f0cb584f..28dba247 100644 --- a/src/replay-generator.cts +++ b/src/replay-generator.cts @@ -13,10 +13,11 @@ type TableGrow = { type: "TableGrow", idx: number, amount: number } & ImpExp type GlobalSet = { type: "GlobalSet", value: number, bigInt: boolean } & ImpExp type Event = Call | Store | MemGrow | TableSet | TableGrow | GlobalSet type Import = { module: string, name: string } -type Function = Import & { body: { results: number[], events: Event[] }[] } -type Memory = Import & { pages: number, maxPages: number } +type BodyPart = { results: number[], events: Event[] } +type Function = Import & { body: BodyPart[] } +type Memory = Import & WebAssembly.MemoryDescriptor type Table = Import & WebAssembly.TableDescriptor -type Global = Import & { valtype: string, value: number } +type Global = Import & WebAssembly.GlobalDescriptor & { initial: number } type State = { callStack: Function[], lastFunc?: Function } export default class Generator { @@ -64,17 +65,11 @@ export default class Generator { this.pushEvent({ type: 'Call', name: event.name, params: event.params }) break case "ImportCall": - if (this.code.funcImports[event.idx] === undefined) { - console.log('body') - } this.code.funcImports[event.idx].body.push({ results: [], events: [] }) this.state.callStack.push(this.code.funcImports[event.idx]) break case "ImportReturn": this.state.lastFunc = this.state.callStack.pop() - if (this.state.lastFunc === undefined) { - console.log('yo') - } this.state.lastFunc.body.slice(-1)[0].results = event.results break case "Load": @@ -118,8 +113,8 @@ export default class Generator { this.code.memImports[event.idx] = { module: event.module, name: event.name, - pages: event.pages, - maxPages: event.maxPages + initial: event.initial, + maximum: event.maximum } break case 'GlobalGet': @@ -146,8 +141,9 @@ export default class Generator { this.code.globalImports[event.idx] = { module: event.module, name: event.name, - valtype: event.valtype, value: event.value, + initial: event.initial, + mutable: event.mutable } break case 'ImportFunc': @@ -220,20 +216,20 @@ class Code { // Init memories for (let memidx in this.memImports) { let mem = this.memImports[memidx] - jsString += `const ${mem.name} = new WebAssembly.Memory({ initial: ${mem.pages}, maximum: ${mem.maxPages} })\n` + jsString += `const ${mem.name} = new WebAssembly.Memory({ initial: ${mem.initial}, maximum: ${mem.maximum} })\n` jsString += `${writeImport(mem.module, mem.name)}${mem.name}\n` } // Init globals for (let globalIdx in this.globalImports) { let global = this.globalImports[globalIdx] - jsString += `const ${global.name} = new WebAssembly.Global({ value: '${global.valtype}', mutable: true}, ${global.value})\n` - jsString += `${global.name}.value = ${global.value}\n` + jsString += `const ${global.name} = new WebAssembly.Global({ value: '${global.value}', mutable: ${global.mutable}}, ${global.initial})\n` + // jsString += `${global.name}.value = ${global.initial}\n` jsString += `${writeImport(global.module, global.name)}${global.name}\n` } // Init tables for (let tableidx in this.tableImports) { let table = this.tableImports[tableidx] - jsString += `const ${table.name} = new WebAssembly.Table({ initial: ${table.initial}, element: '${table.element}'})\n` + jsString += `const ${table.name} = new WebAssembly.Table({ initial: ${table.initial}, maximum: ${table.maximum}, element: '${table.element}'})\n` jsString += `${writeImport(table.module, table.name)}${table.name}\n` } // Init entity states @@ -304,7 +300,7 @@ class Code { return jsString } - toWriteStream(stream: WriteStream) { + async toWriteStream(stream: WriteStream) { stream.write(`import fs from 'fs'\n`) stream.write(`import path from 'path'\n`) stream.write(`export default async function replay(wasmBinary) {\n`) @@ -318,20 +314,20 @@ class Code { // Init memories for (let memidx in this.memImports) { let mem = this.memImports[memidx] - stream.write(`const ${mem.name} = new WebAssembly.Memory({ initial: ${mem.pages}, maximum: ${mem.maxPages} })\n`) + stream.write(`const ${mem.name} = new WebAssembly.Memory({ initial: ${mem.initial}, maximum: ${mem.maximum} })\n`) stream.write(`${writeImport(mem.module, mem.name)}${mem.name}\n`) } // Init globals for (let globalIdx in this.globalImports) { let global = this.globalImports[globalIdx] - stream.write(`const ${global.name} = new WebAssembly.Global({ value: '${global.valtype}', mutable: true}, ${global.value})\n`) - stream.write(`${global.name}.value = ${global.value}\n`) + stream.write(`const ${global.name} = new WebAssembly.Global({ value: '${global.value}', mutable: ${global.mutable}}, ${global.initial})\n`) + // stream.write(`${global.name}.value = ${global.initial}\n`) stream.write(`${writeImport(global.module, global.name)}${global.name}\n`) } // Init tables for (let tableidx in this.tableImports) { let table = this.tableImports[tableidx] - stream.write(`const ${table.name} = new WebAssembly.Table({ initial: ${table.initial}, element: '${table.element}'})\n`) + stream.write(`const ${table.name} = new WebAssembly.Table({ initial: ${table.initial}, maximum: ${table.maximum}, element: '${table.element}'})\n`) stream.write(`${writeImport(table.module, table.name)}${table.name}\n`) } // Init entity states @@ -358,39 +354,9 @@ class Code { stream.write(`${writeImport(func.module, func.name)}() => {\n`) stream.write(`${writeFuncGlobal(funcidx)}++\n`) stream.write(`switch (${writeFuncGlobal(funcidx)}) {\n`) - func.body.forEach((b, i) => { - if (b.events.length !== 0 || b.results.length !== 0) { - stream.write(`case ${i}:\n`) - } - if (b.events.length !== 0) { - for (let event of b.events) { - switch (event.type) { - case 'Call': - stream.write(`instance.exports.${event.name}(${writeParamsString(event.params)})\n`) - break - case 'Store': - stream.write(this.storeEvent(event)) - break - case 'MemGrow': - stream.write(this.memGrowEvent(event)) - break - case 'TableSet': - stream.write(this.tableSetEvent(event)) - break - case 'TableGrow': - stream.write(this.tableGrowEvent(event)) - break - case 'GlobalSet': - stream.write(this.globalSet(event)) - break - default: unreachable(event) - } - } - } - if (b.results.length !== 0) { - stream.write(`return ${b.results[0]} \n`) - } - }) + for (let i = 0; i < func.body.length; i++) { + await this.writeBody(stream, func.body[i], i) + } stream.write('}\n') stream.write('}\n') } @@ -408,6 +374,46 @@ class Code { stream.close() } + private async writeBody(stream: WriteStream, b: BodyPart, i: number) { + if (b.events.length !== 0 || b.results.length !== 0) { + await this.write(stream, `case ${i}:\n`) + } + if (b.events.length !== 0) { + for (let event of b.events) { + switch (event.type) { + case 'Call': + stream.write(`instance.exports.${event.name}(${writeParamsString(event.params)})\n`) + break + case 'Store': + stream.write(this.storeEvent(event)) + break + case 'MemGrow': + stream.write(this.memGrowEvent(event)) + break + case 'TableSet': + stream.write(this.tableSetEvent(event)) + break + case 'TableGrow': + stream.write(this.tableGrowEvent(event)) + break + case 'GlobalSet': + stream.write(this.globalSet(event)) + break + default: unreachable(event) + } + } + } + if (b.results.length !== 0) { + await this.write(stream, `return ${b.results[0]} \n`) + } + } + + private async write(stream: WriteStream, s: string) { + if (stream.write(s) === false) { + await new Promise((resolve) => stream.once('drain', resolve)) + } + } + private storeEvent(event: Store) { let jsString = '' event.data.forEach((byte, j) => { diff --git a/src/tracer.cts b/src/tracer.cts index 6a920c28..36d5a2b7 100644 --- a/src/tracer.cts +++ b/src/tracer.cts @@ -71,13 +71,13 @@ export class Trace { trace.push({type: 'ImportReturn', idx: e[1], name: e[2], results: e[3]}) break case 'IM': - trace.push({type: 'ImportMemory', idx: e[1], module: e[2], name: e[3], pages: e[4], maxPages: e[5]}) + trace.push({type: 'ImportMemory', idx: e[1], module: e[2], name: e[3], initial: e[4], maximum: e[5]}) break case 'IT': - trace.push({type: 'ImportTable', idx: e[1], module: e[2], name: e[3], initial: e[4], element: e[5]}) + trace.push({type: 'ImportTable', idx: e[1], module: e[2], name: e[3], initial: e[4], maximum: e[5], element: e[6]}) break case 'IG': - trace.push({type: 'ImportGlobal', idx: e[1], module: e[2], name: e[3], valtype: e[4], value: e[5]}) + trace.push({type: 'ImportGlobal', idx: e[1], module: e[2], name: e[3], value: e[4], mutable: e[5] === 1, initial: e[6]}) break case 'IF': trace.push({type: 'ImportFunc', idx: e[1], module: e[2], name: e[3]}) @@ -101,7 +101,9 @@ export class Trace { } }) } else { - eventString += value + if (value !== undefined && value !== null) { + eventString += value + } } if (i < event.length - 1) { eventString += ';' @@ -137,7 +139,7 @@ export class Trace { components[2], components[3], parseInt(components[4]), - parseInt(components[5]) + components[5] === '' ? undefined : parseInt(components[5]) ] case "EC": return [ @@ -199,12 +201,13 @@ export class Trace { ] case 'IG': return [ - components[0], - parseInt(components[1]), - components[2], - components[3], + components[0], + parseInt(components[1]), + components[2], + components[3], components[4] as ValType, - parseInt(components[5]), + parseInt(components[5]) as 0 | 1, + parseInt(components[6]), ] case 'IF': return [ @@ -215,12 +218,13 @@ export class Trace { ] case 'IT': return [ - components[0], + components[0], parseInt(components[1]), components[2], components[3], parseInt(components[4]), - components[5] as 'anyfunc' + components[5] === '' ? undefined : parseInt(components[5]), + components[6] as 'anyfunc' ] default: throw new Error(`${components[0]}: Not a valid trace event. The whole event: ${event}.`) @@ -253,8 +257,8 @@ export class Trace { idx: parseInt(components[1]), module: components[2], name: components[3], - pages: parseInt(components[4]), - maxPages: parseInt(components[5]) + initial: parseInt(components[4]), + maximum: components[5] === '' ? undefined : parseInt(components[5]) } case "EC": return { @@ -320,8 +324,9 @@ export class Trace { idx: parseInt(components[1]), module: components[2], name: components[3], - valtype: components[4] as ValType, - value: parseInt(components[5]), + initial: parseInt(components[6]), + value: components[4] as ValType, + mutable: parseInt(components[5]) === 1 } case 'IF': return { @@ -337,7 +342,8 @@ export class Trace { module: components[2], name: components[3], initial: parseInt(components[4]), - element: components[5] as 'anyfunc' + maximum: components[5] === '' ? undefined : parseInt(components[5]), + element: components[6] as 'anyfunc' } default: throw new Error(`${components[0]}: Not a valid trace event. The whole event: ${event}.`) @@ -725,29 +731,26 @@ export default class Analysis implements AnalysisI { }) this.Wasabi.module.info.memories.forEach((m, idx) => { if (m.import !== null) { - const memory = this.Wasabi.module.memories[idx] - const pages = memory.buffer.byteLength / this.MEM_PAGE_SIZE - const maxPages = memory.buffer.byteLength / (64 * 1024) - this.trace.push(['IM', idx, m.import[0], m.import[1], pages, maxPages ]) + this.trace.push(['IM', idx, m.import[0], m.import[1], m.initial, m.maximum ]) } }) // Init Tables this.Wasabi.module.tables.forEach((t, i) => { - this.shadowTables.push(new WebAssembly.Table({ initial: this.Wasabi.module.tables[i].length, element: 'anyfunc' })) + this.shadowTables.push(new WebAssembly.Table({ initial: this.Wasabi.module.tables[i].length, element: 'anyfunc' })) // want to replace anyfunc through t.refType but it holds the wrong string ('funcref') for (let y = 0; y < this.Wasabi.module.tables[i].length; y++) { this.shadowTables[i].set(y, t.get(y)) } }) this.Wasabi.module.info.tables.forEach((t, idx) => { if (t.import !== null) { - this.trace.push(['IT', idx, t.import![0], t.import![1], this.Wasabi.module.tables[idx].length , 'anyfunc']) + this.trace.push(['IT', idx, t.import![0], t.import![1], t.initial, t.maximum, 'anyfunc']) // want to replace anyfunc through t.refType but it holds the wrong string ('funcref') } }) // Init Globals this.shadowGlobals = this.Wasabi.module.globals.map(g => g.value) this.Wasabi.module.info.globals.forEach((g, idx) => { if (g.import !== null) { - this.trace.push([ 'IG', idx, g.import[0], g.import[1], g.valType, this.Wasabi.module.globals[idx].value]) + this.trace.push([ 'IG', idx, g.import[0], g.import[1], g.valType, g.mutability === 'Mut' ? 1 : 0, this.Wasabi.module.globals[idx].value]) } }) // Init Functions diff --git a/tests/node/glob-imp-const/index.wat b/tests/node/glob-imp-const/index.wat new file mode 100644 index 00000000..8b22a162 --- /dev/null +++ b/tests/node/glob-imp-const/index.wat @@ -0,0 +1,7 @@ +(module + (import "env" "global" (global $global i32)) + (func $main (export "main") + global.get $global + drop + ) +) diff --git a/tests/node/glob-imp-const/test.js b/tests/node/glob-imp-const/test.js new file mode 100644 index 00000000..d3d242ab --- /dev/null +++ b/tests/node/glob-imp-const/test.js @@ -0,0 +1,12 @@ +export default async function test(wasmBinary) { + let instance + const global = new WebAssembly.Global({ value: "i32", mutable: false }, 4); + let imports = { + env: { + global: global + } + } + let wasm = await WebAssembly.instantiate(wasmBinary, imports) + instance = wasm.instance + instance.exports.main() +} \ No newline at end of file diff --git a/tests/run-tests.cts b/tests/run-tests.cts index 82082c3d..5a202cec 100644 --- a/tests/run-tests.cts +++ b/tests/run-tests.cts @@ -1,5 +1,5 @@ import fs from 'fs/promises' -import { existsSync as exists } from 'fs' +import fss from 'fs' import path from 'path' import cp from 'child_process' import express from 'express' @@ -50,10 +50,10 @@ async function runNodeTest(name: string): Promise { // 1. Instrument with Wasabi !!Please use the newest version const indexRsPath = path.join(testPath, 'index.rs') const indexCPath = path.join(testPath, 'index.c') - if (exists(indexRsPath)) { + if (fss.exists(indexRsPath)) { cp.execSync(`rustc --crate-type cdylib ${indexRsPath} --target wasm32-unknown-unknown --crate-type cdylib -o ${wasmPath}`, { stdio: 'ignore' }) cp.execSync(`wasm2wat ${wasmPath} -o ${watPath}`) - } else if (exists(indexCPath)) { + } else if (fss.exists(indexCPath)) { // TODO } else { cp.execSync(`wat2wasm ${watPath} -o ${wasmPath}`); @@ -89,7 +89,7 @@ async function runNodeTest(name: string): Promise { } let replayCode try { - replayCode = new Generator().generateReplay(trace).toString() + replayCode = await new Generator().generateReplay(trace).toWriteStream(fss.) } catch (e: any) { return { testPath, success: false, reason: e.stack } } @@ -144,11 +144,9 @@ async function runNodeTests(names: string[]) { 'mem-exp-copy-no-host-mod', 'mem-exp-fill-no-host-mod', 'mem-exp-host-mod-load-vec', - 'table-imp-init-max', 'table-exp-host-mod', 'table-exp-host-grow', 'funky-kart', - 'mem-imp-host-grow' ] names = names.filter((n) => !filter.includes(n)) // names = ["mem-imp-host-grow"] diff --git a/trace.d.cts b/trace.d.cts index 4c66397d..87893b3f 100644 --- a/trace.d.cts +++ b/trace.d.cts @@ -39,17 +39,17 @@ export type ConsiseImportCall = ['IC', number, string] // export type ImportReturn = [type, idx, name, results] export type ConsiseImportReturn = ['IR', number, string, number[]] -// export type ImportMemory = { type: 'IM', idx: number, pages: number, maxPages: number } & Import -// export type ImportMemory = [type, idx, module, name, pages, maxPages] -export type ConsiseImportMemory = ['IM', number, string, string, number, number] +// export type ImportMemory = { type: 'IM', idx: number, initial: number, maximum: number } & Import +// export type ImportMemory = [type, idx, module, name, initial, maxiumum] +export type ConsiseImportMemory = ['IM', number, string, string, number | undefined, number] // export type ImportTable = { type: 'IT', idx: number } & WebAssembly.TableDescriptor & Import -// export type ImportTable = [type, idx, module, name, size, tablekind] -export type ConsiseImportTable = ['IT', number, string, string, number, WebAssembly.TableKind] +// export type ImportTable = [type, idx, module, name, initial, maximum, element] +export type ConsiseImportTable = ['IT', number, string, string, number, number | undefined, WebAssembly.TableKind] // export type ImportGlobal = { type: 'IG', idx: number, valtype: ValType, value: number } & Import -// export type ImportGlobal = [type, idx, module, name, valtype, value] -export type ConsiseImportGlobal = ['IG', number, string, string, ValType, number] +// export type ImportGlobal = [type, idx, module, name, value, mutable, initial] +export type ConsiseImportGlobal = ['IG', number, string, string, ValType, 0 | 1, number] // export type ImportFunc = { type: 'IF', idx: number } & Import // export type ImportGlobal = [type, idx, module, name] @@ -79,16 +79,17 @@ export type ImportCall = { type: "ImportCall", idx: number, name: string } export type ImportReturn = { type: "ImportReturn", idx: number, name: string, results: number[] } -export type ImportMemory = { type: 'ImportMemory', idx: number, pages: number, maxPages: number } & Import +export type ImportMemory = { type: 'ImportMemory', idx: number } & WebAssembly.MemoryDescriptor & Import export type ImportTable = { type: 'ImportTable', idx: number } & WebAssembly.TableDescriptor & Import -export type ImportGlobal = { type: 'ImportGlobal', idx: number, valtype: ValType, value: number } & Import +export type ImportGlobal = { type: 'ImportGlobal', idx: number, initial: number } & WebAssembly.GlobalDescriptor & Import export type ImportFunc = { type: 'ImportFunc', idx: number } & Import -export type ValType = 'i32' | 'i64' | 'f32' | 'f64' | 'anyfunc' | 'funcref' | 'externref' +export type ValType = keyof WebAssembly.ValueTypeMap +// export type ValType = 'i32' | 'i64' | 'f32' | 'f64' | 'anyfunc' | 'funcref' | 'externref' export type RefType = 'funcref' | 'externref' export type Import = { module: string, name: string } \ No newline at end of file diff --git a/wasabi b/wasabi index adede458..02f4210f 160000 --- a/wasabi +++ b/wasabi @@ -1 +1 @@ -Subproject commit adede4588ebed6d40af15b29307855696c228111 +Subproject commit 02f4210ffc1daa5efd08b5501c70e99c8d91ebb7 diff --git a/wasabi.d.cts b/wasabi.d.cts index 266d6d73..c88ce98e 100644 --- a/wasabi.d.cts +++ b/wasabi.d.cts @@ -1,5 +1,5 @@ export type Location = { func: number, instr: number } -export type ValType = 'i32' | 'i64' | 'f32' | 'f64' | 'anyfunc' | 'funcref' | 'externref' +export type ValType = keyof WebAssembly.ValueTypeMap export type GlobalOp = 'global.set' | 'global.get' export type LocalOp = 'local.get' | 'local.set' | 'local.tee' export type StoreOp = 'i32.store' | 'i32.store8' | 'i32.store16' | 'i64.store' | 'i64.store8' | 'i64.store16' | 'i64.store32' | 'f32.store' | 'f64.store' @@ -12,6 +12,8 @@ export type BlockType = 'function' | 'block' | 'loop' | 'if' | 'else' export type UnaryOp = string // todo export type BinaryOp = string // todo export type ImpExp = { import: string[] | null, export: string } +export type Mutability = 'Mut' | 'Const' +export type Limits = { initial: number, maximum: number | null } export type Wasabi = { HOOK_NAMES: [ @@ -52,9 +54,9 @@ export type Wasabi = { locals: string, instrCount: number })[], - memories: ImpExp[], - tables: (ImpExp & { ref_type: any })[], - globals: (ImpExp & { valType: ValType })[], + memories: (ImpExp & Limits)[], + tables: (ImpExp & Limits & { refType: any })[], + globals: (ImpExp & { valType: ValType, mutability: Mutability })[], start: any, tableExportNames: string[], memoryExportNames: string[],