diff --git a/src/WasmDis.ts b/src/WasmDis.ts index f005346..d58d317 100644 --- a/src/WasmDis.ts +++ b/src/WasmDis.ts @@ -31,6 +31,7 @@ import { Int64, ITableType, IMemoryType, + IEventType, IGlobalType, IResizableLimits, IDataSegmentBody, @@ -44,6 +45,7 @@ import { NameType, IFunctionNameEntry, ILocalNameEntry, + IEventNameEntry, ITypeNameEntry, ITableNameEntry, IMemoryNameEntry, @@ -258,6 +260,7 @@ export interface IExportMetadata { getGlobalExportNames(index: number): string[]; getMemoryExportNames(index: number): string[]; getTableExportNames(index: number): string[]; + getEventExportNames(index: number): string[]; } export interface INameResolver { @@ -266,6 +269,7 @@ export interface INameResolver { getMemoryName(index: number, isRef: boolean): string; getGlobalName(index: number, isRef: boolean): string; getElementName(index: number, isRef: boolean): string; + getEventName(index: number, isRef: boolean): string; getFunctionName(index: number, isImport: boolean, isRef: boolean): string; getVariableName(funcIndex: number, index: number, isRef: boolean): string; getFieldName(typeIndex: number, index: number, isRef: boolean): string; @@ -287,6 +291,9 @@ export class DefaultNameResolver implements INameResolver { public getElementName(index: number, isRef: boolean): string { return `$elem${index}`; } + public getEventName(index: number, isRef: boolean): string { + return `$event${index}`; + } public getFunctionName( index: number, isImport: boolean, @@ -320,17 +327,20 @@ class DevToolsExportMetadata implements IExportMetadata { private readonly _globalExportNames: string[][]; private readonly _memoryExportNames: string[][]; private readonly _tableExportNames: string[][]; + private readonly _eventExportNames: string[][]; constructor( functionExportNames: string[][], globalExportNames: string[][], memoryExportNames: string[][], - tableExportNames: string[][] + tableExportNames: string[][], + eventExportNames: string[][] ) { this._functionExportNames = functionExportNames; this._globalExportNames = globalExportNames; this._memoryExportNames = memoryExportNames; this._tableExportNames = tableExportNames; + this._eventExportNames = eventExportNames; } public getFunctionExportNames(index: number) { @@ -348,6 +358,10 @@ class DevToolsExportMetadata implements IExportMetadata { public getTableExportNames(index: number) { return this._tableExportNames[index] ?? EMPTY_STRING_ARRAY; } + + public getEventExportNames(index: number) { + return this._eventExportNames[index] ?? EMPTY_STRING_ARRAY; + } } export class NumericNameResolver implements INameResolver { @@ -366,6 +380,9 @@ export class NumericNameResolver implements INameResolver { public getElementName(index: number, isRef: boolean): string { return isRef ? "" + index : `(;${index};)`; } + public getEventName(index: number, isRef: boolean): string { + return isRef ? "" + index : `(;${index};)`; + } public getFunctionName( index: number, isImport: boolean, @@ -421,6 +438,7 @@ export class WasmDisassembler { private _importCount: number; private _globalCount: number; private _memoryCount: number; + private _eventCount: number; private _tableCount: number; private _elementCount: number; private _expression: Array; @@ -469,6 +487,7 @@ export class WasmDisassembler { this._importCount = 0; this._globalCount = 0; this._memoryCount = 0; + this._eventCount = 0; this._tableCount = 0; this._elementCount = 0; this._expression = []; @@ -675,11 +694,12 @@ export class WasmDisassembler { this.appendBuffer(")"); } } - private useLabel(depth: number): string { + // extraDepthOffset is used by "delegate" instructions. + private useLabel(depth: number, extraDepthOffset = 0): string { if (!this._backrefLabels) { return "" + depth; } - var i = this._backrefLabels.length - depth - 1; + var i = this._backrefLabels.length - depth - 1 - extraDepthOffset; if (i < 0) { return "" + depth; } @@ -704,6 +724,7 @@ export class WasmDisassembler { case OperatorCode.block: case OperatorCode.loop: case OperatorCode.if: + case OperatorCode.try: if (this._labelMode !== LabelMode.Depth) { const backrefLabel = { line: this._lines.length, @@ -751,6 +772,22 @@ export class WasmDisassembler { this.appendBuffer(this.useLabel(operator.brTable[i])); } break; + case OperatorCode.rethrow: + this.appendBuffer(" "); + this.appendBuffer(this.useLabel(operator.relativeDepth)); + break; + case OperatorCode.delegate: + this.appendBuffer(" "); + this.appendBuffer(this.useLabel(operator.relativeDepth, 1)); + break; + case OperatorCode.catch: + case OperatorCode.throw: + var eventName = this._nameResolver.getEventName( + operator.eventIndex, + true + ); + this.appendBuffer(` ${eventName}`); + break; case OperatorCode.ref_null: this.appendBuffer(" "); this.appendBuffer(this.typeIndexToString(operator.refType)); @@ -1139,6 +1176,7 @@ export class WasmDisassembler { case SectionCode.Data: case SectionCode.Table: case SectionCode.Element: + case SectionCode.Event: this._currentSectionId = sectionInfo.id; break; // reading known section; default: @@ -1165,6 +1203,22 @@ export class WasmDisassembler { this.appendBuffer(")"); this.newLine(); break; + case BinaryReaderState.EVENT_SECTION_ENTRY: + var eventInfo = reader.result; + var eventIndex = this._eventCount++; + var eventName = this._nameResolver.getEventName(eventIndex, false); + this.appendBuffer(` (event ${eventName}`); + if (this._exportMetadata !== null) { + for (const exportName of this._exportMetadata.getEventExportNames( + eventIndex + )) { + this.appendBuffer(` (export "${exportName}")`); + } + } + this.printFuncType(eventInfo.typeIndex); + this.appendBuffer(")"); + this.newLine(); + break; case BinaryReaderState.TABLE_SECTION_ENTRY: var tableInfo = reader.result; var tableIndex = this._tableCount++; @@ -1222,6 +1276,13 @@ export class WasmDisassembler { ); this.appendBuffer(`(global ${globalName})`); break; + case ExternalKind.Event: + var eventName = this._nameResolver.getEventName( + exportInfo.index, + true + ); + this.appendBuffer(`(event ${eventName})`); + break; default: throw new Error(`Unsupported export ${exportInfo.kind}`); } @@ -1321,6 +1382,27 @@ export class WasmDisassembler { )} ${this.typeToString(tableImportInfo.elementType)})` ); break; + case ExternalKind.Event: + var eventImportInfo = importInfo.type; + var eventIndex = this._eventCount++; + var eventName = this._nameResolver.getEventName( + eventIndex, + false + ); + this.appendBuffer(` (event ${eventName}`); + if (this._exportMetadata !== null) { + for (const exportName of this._exportMetadata.getEventExportNames( + eventIndex + )) { + this.appendBuffer(` (export "${exportName}")`); + } + } + this.appendBuffer(` (import `); + this.printImportSource(importInfo); + this.appendBuffer(")"); + this.printFuncType(eventImportInfo.typeIndex); + this.appendBuffer(")"); + break; default: throw new Error(`NYI other import types: ${importInfo.kind}`); } @@ -1534,6 +1616,10 @@ export class WasmDisassembler { switch (operator.code) { case OperatorCode.end: case OperatorCode.else: + case OperatorCode.catch: + case OperatorCode.catch_all: + case OperatorCode.unwind: + case OperatorCode.delegate: this.decreaseIndent(); break; } @@ -1545,6 +1631,10 @@ export class WasmDisassembler { case OperatorCode.block: case OperatorCode.loop: case OperatorCode.else: + case OperatorCode.try: + case OperatorCode.catch: + case OperatorCode.catch_all: + case OperatorCode.unwind: this.increaseIndent(); break; } @@ -1567,6 +1657,7 @@ const UNKNOWN_FUNCTION_PREFIX = "unknown"; class NameSectionNameResolver extends DefaultNameResolver { protected readonly _functionNames: string[]; protected readonly _localNames: string[][]; + protected readonly _eventNames: string[]; protected readonly _typeNames: string[]; protected readonly _tableNames: string[]; protected readonly _memoryNames: string[]; @@ -1576,6 +1667,7 @@ class NameSectionNameResolver extends DefaultNameResolver { constructor( functionNames: string[], localNames: string[][], + eventNames: string[], typeNames: string[], tableNames: string[], memoryNames: string[], @@ -1585,6 +1677,7 @@ class NameSectionNameResolver extends DefaultNameResolver { super(); this._functionNames = functionNames; this._localNames = localNames; + this._eventNames = eventNames; this._typeNames = typeNames; this._tableNames = tableNames; this._memoryNames = memoryNames; @@ -1616,6 +1709,12 @@ class NameSectionNameResolver extends DefaultNameResolver { return isRef ? `$${name}` : `$${name} (;${index};)`; } + public getEventName(index: number, isRef: boolean): string { + const name = this._eventNames[index]; + if (!name) return super.getEventName(index, isRef); + return isRef ? `$${name}` : `$${name} (;${index};)`; + } + public getFunctionName( index: number, isImport: boolean, @@ -1655,6 +1754,7 @@ export class NameSectionReader { private _functionImportsCount = 0; private _functionNames: string[] = null; private _functionLocalNames: string[][] = null; + private _eventNames: string[] = null; private _typeNames: string[] = null; private _tableNames: string[] = null; private _memoryNames: string[] = null; @@ -1683,6 +1783,7 @@ export class NameSectionReader { this._functionImportsCount = 0; this._functionNames = []; this._functionLocalNames = []; + this._eventNames = []; this._typeNames = []; this._tableNames = []; this._memoryNames = []; @@ -1733,6 +1834,12 @@ export class NameSectionReader { }); }); this._hasNames = true; + } else if (nameInfo.type === NameType.Event) { + const { names } = nameInfo; + names.forEach(({ index, name }) => { + this._eventNames[index] = bytesToString(name); + }); + this._hasNames = true; } else if (nameInfo.type === NameType.Type) { const { names } = nameInfo; names.forEach(({ index, name }) => { @@ -1807,6 +1914,7 @@ export class NameSectionReader { return new NameSectionNameResolver( functionNames, this._functionLocalNames, + this._eventNames, this._typeNames, this._tableNames, this._memoryNames, @@ -1820,6 +1928,7 @@ export class DevToolsNameResolver extends NameSectionNameResolver { constructor( functionNames: string[], localNames: string[][], + eventNames: string[], typeNames: string[], tableNames: string[], memoryNames: string[], @@ -1829,6 +1938,7 @@ export class DevToolsNameResolver extends NameSectionNameResolver { super( functionNames, localNames, + eventNames, typeNames, tableNames, memoryNames, @@ -1854,9 +1964,11 @@ export class DevToolsNameGenerator { private _memoryImportsCount = 0; private _tableImportsCount = 0; private _globalImportsCount = 0; + private _eventImportsCount = 0; private _functionNames: string[] = null; private _functionLocalNames: string[][] = null; + private _eventNames: string[] = null; private _memoryNames: string[] = null; private _typeNames: string[] = null; private _tableNames: string[] = null; @@ -1867,6 +1979,7 @@ export class DevToolsNameGenerator { private _globalExportNames: string[][] = null; private _memoryExportNames: string[][] = null; private _tableExportNames: string[][] = null; + private _eventExportNames: string[][] = null; private _addExportName(exportNames: string[][], index: number, name: string) { const names = exportNames[index]; @@ -1913,6 +2026,7 @@ export class DevToolsNameGenerator { this._memoryImportsCount = 0; this._tableImportsCount = 0; this._globalImportsCount = 0; + this._eventImportsCount = 0; this._functionNames = []; this._functionLocalNames = []; @@ -1925,6 +2039,7 @@ export class DevToolsNameGenerator { this._globalExportNames = []; this._memoryExportNames = []; this._tableExportNames = []; + this._eventExportNames = []; break; case BinaryReaderState.END_SECTION: break; @@ -1984,6 +2099,13 @@ export class DevToolsNameGenerator { false ); break; + case ExternalKind.Event: + this._setName( + this._eventNames, + this._eventImportsCount++, + importName, + false + ); default: throw new Error(`Unsupported export ${importInfo.kind}`); } @@ -2008,6 +2130,11 @@ export class DevToolsNameGenerator { localNames[index] = bytesToString(name); }); }); + } else if (nameInfo.type === NameType.Event) { + const { names } = nameInfo; + names.forEach(({ index, name }) => { + this._setName(this._eventNames, index, bytesToString(name), true); + }); } else if (nameInfo.type === NameType.Type) { const { names } = nameInfo; names.forEach(({ index, name }) => { @@ -2104,6 +2231,19 @@ export class DevToolsNameGenerator { false ); break; + case ExternalKind.Event: + this._addExportName( + this._eventExportNames, + exportInfo.index, + exportName + ); + this._setName( + this._eventNames, + exportInfo.index, + exportName, + false + ); + break; default: throw new Error(`Unsupported export ${exportInfo.kind}`); } @@ -2119,7 +2259,8 @@ export class DevToolsNameGenerator { this._functionExportNames, this._globalExportNames, this._memoryExportNames, - this._tableExportNames + this._tableExportNames, + this._eventExportNames ); } @@ -2127,6 +2268,7 @@ export class DevToolsNameGenerator { return new DevToolsNameResolver( this._functionNames, this._functionLocalNames, + this._eventNames, this._typeNames, this._tableNames, this._memoryNames, diff --git a/src/WasmParser.ts b/src/WasmParser.ts index 1429c7c..fdad7b3 100644 --- a/src/WasmParser.ts +++ b/src/WasmParser.ts @@ -31,6 +31,7 @@ export const enum SectionCode { Element = 9, // Elements section Code = 10, // Function bodies (code) Data = 11, // Data segments + Event = 13, // Events } export const enum OperatorCode { unreachable = 0x00, @@ -39,6 +40,11 @@ export const enum OperatorCode { loop = 0x03, if = 0x04, else = 0x05, + try = 0x06, + catch = 0x07, + throw = 0x08, + rethrow = 0x09, + unwind = 0x0a, end = 0x0b, br = 0x0c, br_if = 0x0d, @@ -51,6 +57,8 @@ export const enum OperatorCode { call_ref = 0x14, return_call_ref = 0x15, let = 0x17, + delegate = 0x18, + catch_all = 0x19, drop = 0x1a, select = 0x1b, local_get = 0x20, @@ -598,11 +606,11 @@ export const OperatorCodeNames = [ "loop", "if", "else", - undefined, - undefined, - undefined, - undefined, - undefined, + "try", + "catch", + "throw", + "rethrow", + "unwind", "end", "br", "br_if", @@ -616,8 +624,8 @@ export const OperatorCodeNames = [ "return_call_ref", undefined, "let", - undefined, - undefined, + "delegate", + "catch_all", "drop", "select", undefined, @@ -1254,6 +1262,7 @@ export const enum ExternalKind { Table = 1, Memory = 2, Global = 3, + Event = 4, } export const enum TypeKind { unspecified = 0, @@ -1326,6 +1335,7 @@ export const enum NameType { Module = 0, Function = 1, Local = 2, + Event = 3, Type = 4, Table = 5, Memory = 6, @@ -1355,6 +1365,7 @@ export const enum BinaryReaderState { ELEMENT_SECTION_ENTRY = 20, LINKING_SECTION_ENTRY = 21, START_SECTION_ENTRY = 22, + EVENT_SECTION_ENTRY = 23, BEGIN_INIT_EXPRESSION_BODY = 25, INIT_EXPRESSION_OPERATOR = 26, @@ -1481,6 +1492,10 @@ export interface IGlobalType { contentType: Type; mutability: number; } +export interface IEventType { + attribute: number; + typeIndex: number; +} export interface IGlobalVariable { type: IGlobalType; } @@ -1498,7 +1513,11 @@ export interface IDataSegment { export interface IDataSegmentBody { data: Uint8Array; } -export type ImportEntryType = ITableType | IMemoryType | IGlobalType; +export type ImportEntryType = + | ITableType + | IMemoryType + | IGlobalType + | IEventType; export interface IImportEntry { module: Uint8Array; field: Uint8Array; @@ -1531,6 +1550,9 @@ export interface ILocalName { export interface ILocalNameEntry extends INameEntry { funcs: ILocalName[]; } +export interface IEventNameEntry extends INameEntry { + names: INaming[]; +} export interface ITypeNameEntry extends INameEntry { names: INaming[]; } @@ -1603,6 +1625,7 @@ export interface IOperatorInformation { refType?: number; // "HeapType" format, a.k.a. s33 brDepth?: number; brTable?: Array; + relativeDepth?: number; funcIndex?: number; typeIndex?: number; tableIndex?: number; @@ -1610,6 +1633,7 @@ export interface IOperatorInformation { fieldIndex?: number; globalIndex?: number; segmentIndex?: number; + eventIndex?: number; destinationIndex?: number; memoryAddress?: IMemoryAddress; literal?: number | Int64 | Uint8Array; @@ -1979,6 +2003,14 @@ export class BinaryReader { var mutability = this.readVarUint1(); return { contentType: contentType, mutability: mutability }; } + private readEventType(): IEventType { + var attribute = this.readVarUint32() >>> 0; + var typeIndex = this.readVarUint32() >>> 0; + return { + attribute: attribute, + typeIndex: typeIndex, + }; + } private readTypeEntry() { if (this._sectionEntriesLeft === 0) { this.skipSection(); @@ -2012,7 +2044,7 @@ export class BinaryReader { var field = this.readStringBytes(); var kind = this.readUint8(); var funcTypeIndex: number; - var type: ITableType | IMemoryType | IGlobalType; + var type: ITableType | IMemoryType | IGlobalType | IEventType; switch (kind) { case ExternalKind.Function: funcTypeIndex = this.readVarUint32() >>> 0; @@ -2026,6 +2058,9 @@ export class BinaryReader { case ExternalKind.Global: type = this.readGlobalType(); break; + case ExternalKind.Event: + type = this.readEventType(); + break; } this.result = { module: module, @@ -2081,6 +2116,16 @@ export class BinaryReader { this._sectionEntriesLeft--; return true; } + private readEventEntry(): boolean { + if (this._sectionEntriesLeft === 0) { + this.skipSection(); + return this.read(); + } + this.state = BinaryReaderState.EVENT_SECTION_ENTRY; + this.result = this.readEventType(); + this._sectionEntriesLeft--; + return true; + } private readGlobalEntry(): boolean { if (this._sectionEntriesLeft === 0) { this.skipSection(); @@ -2267,6 +2312,7 @@ export class BinaryReader { | IModuleNameEntry | IFunctionNameEntry | ILocalNameEntry + | IEventNameEntry | ITypeNameEntry | ITableNameEntry | IMemoryNameEntry @@ -2280,6 +2326,7 @@ export class BinaryReader { }; break; case NameType.Function: + case NameType.Event: case NameType.Type: case NameType.Table: case NameType.Memory: @@ -3023,11 +3070,13 @@ export class BinaryReader { refType, brDepth, brTable, + relativeDepth, funcIndex, typeIndex, tableIndex, localIndex, globalIndex, + eventIndex, memoryAddress, literal, reserved; @@ -3059,6 +3108,7 @@ export class BinaryReader { case OperatorCode.block: case OperatorCode.loop: case OperatorCode.if: + case OperatorCode.try: blockType = this.readBlockType(); break; case OperatorCode.br: @@ -3083,6 +3133,14 @@ export class BinaryReader { brTable.push(this.readVarUint32() >>> 0); } break; + case OperatorCode.rethrow: + case OperatorCode.delegate: + relativeDepth = this.readVarUint32() >>> 0; + break; + case OperatorCode.catch: + case OperatorCode.throw: + eventIndex = this.readVarInt32(); + break; case OperatorCode.ref_null: refType = this.readHeapType(); break; @@ -3185,8 +3243,10 @@ export class BinaryReader { case OperatorCode.unreachable: case OperatorCode.nop: case OperatorCode.else: + case OperatorCode.unwind: case OperatorCode.end: case OperatorCode.return: + case OperatorCode.catch_all: case OperatorCode.drop: case OperatorCode.select: case OperatorCode.i32_eqz: @@ -3335,12 +3395,14 @@ export class BinaryReader { refType, brDepth, brTable, + relativeDepth, tableIndex, funcIndex, typeIndex, localIndex, globalIndex, fieldIndex: undefined, + eventIndex, memoryAddress, literal, segmentIndex: undefined, @@ -3494,6 +3556,10 @@ export class BinaryReader { if (!this.hasVarIntBytes()) return false; this._sectionEntriesLeft = this.readVarUint32() >>> 0; return this.readDataEntry(); + case SectionCode.Event: + if (!this.hasVarIntBytes()) return false; + this._sectionEntriesLeft = this.readVarUint32() >>> 0; + return this.readEventEntry(); case SectionCode.Custom: var customSectionName = bytesToString(currentSection.name); if (customSectionName === "name") { @@ -3582,6 +3648,8 @@ export class BinaryReader { return this.readTableEntry(); case BinaryReaderState.MEMORY_SECTION_ENTRY: return this.readMemoryEntry(); + case BinaryReaderState.EVENT_SECTION_ENTRY: + return this.readEventEntry(); case BinaryReaderState.GLOBAL_SECTION_ENTRY: case BinaryReaderState.END_GLOBAL_SECTION_ENTRY: return this.readGlobalEntry(); diff --git a/test/WasmDis.test.ts b/test/WasmDis.test.ts index 36610ed..1d21237 100644 --- a/test/WasmDis.test.ts +++ b/test/WasmDis.test.ts @@ -1444,3 +1444,83 @@ describe("GC proposal support", () => { expect(result.lines).toEqual(expectedLines); }); }); + +describe("Exception handling support", () => { + test("Exception handling disassembly", async () => { + const { parseWat } = await wabtPromise; + const { buffer } = parseWat( + `test.wat`, + `(module + (type (func)) + (type (func (param i32))) + (import "m" "ex" (event (type 0))) + (event (type 1)) + (export "ex" (event 0)) + (func (param i32) (result i32) + try (result i32) + throw 0 + catch 0 + i32.const 0 + catch 1 + catch_all + rethrow 0 + end + try + throw 0 + delegate 0 + try + try + throw 0 + delegate 0 + catch 0 + end + try + throw 0 + unwind + nop + end + ) + )`, + { exceptions: true } + ).toBinary({ write_debug_names: true }); + const reader = new BinaryReader(); + reader.setData(buffer.buffer, 0, buffer.byteLength); + + const expectedLines = [ + "(module", + ' (event $event0 (import "m" "ex"))', + " (event $event1 (param i32))", + ' (export "ex" (event $event0))', + " (func $func0 (param $var0 i32) (result i32)", + " try $label0 (result i32)", + " throw $event0", + " catch $event0", + " i32.const 0", + " catch $event1", + " catch_all", + " rethrow $label0", + " end $label0", + " try", + " throw $event0", + " delegate 0", + " try $label1", + " try", + " throw $event0", + " delegate $label1", + " catch $event0", + " end", + " try", + " throw $event0", + " unwind", + " nop", + " end", + " )", + ")", + ]; + + const dis = new WasmDisassembler(); + dis.disassembleChunk(reader); + const result = dis.getResult(); + expect(result.lines).toEqual(expectedLines); + }); +});