diff --git a/Package.swift b/Package.swift index b7cdf734..f4b211ba 100644 --- a/Package.swift +++ b/Package.swift @@ -70,7 +70,7 @@ let package = Package( ], exclude: ["CMakeLists.txt"] ), - .testTarget(name: "WasmParserTests", dependencies: ["WasmParser"]), + .testTarget(name: "WasmParserTests", dependencies: ["WasmParser", "WAT"]), .target(name: "WasmTypes", exclude: ["CMakeLists.txt"]), diff --git a/README.md b/README.md index 5768d2ae..da60ca21 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ and should work on the following platforms: | | [Memory64](https://github.com/WebAssembly/memory64/blob/main/proposals/memory64/Overview.md) | ✅ Implemented | | | [Tail call](https://github.com/WebAssembly/tail-call/blob/master/proposals/tail-call/Overview.md) | ✅ Implemented | | | [Threads and atomics](https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md) | 🚧 Parser implemented | +| | [Typed Function References](https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md) | 📋 Todo | +| | [Garbage Collection](https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md) | 📋 Todo | | WASI | WASI Preview 1 | ✅ Implemented | diff --git a/Sources/WAT/BinaryInstructionEncoder.swift b/Sources/WAT/BinaryInstructionEncoder.swift index c9bfe7c2..6591c7df 100644 --- a/Sources/WAT/BinaryInstructionEncoder.swift +++ b/Sources/WAT/BinaryInstructionEncoder.swift @@ -23,8 +23,9 @@ protocol BinaryInstructionEncoder: InstructionVisitor { mutating func encodeImmediates(relativeDepth: UInt32) throws mutating func encodeImmediates(table: UInt32) throws mutating func encodeImmediates(targets: BrTable) throws - mutating func encodeImmediates(type: ReferenceType) throws + mutating func encodeImmediates(type: HeapType) throws mutating func encodeImmediates(type: ValueType) throws + mutating func encodeImmediates(typeIndex: UInt32) throws mutating func encodeImmediates(value: IEEE754.Float32) throws mutating func encodeImmediates(value: IEEE754.Float64) throws mutating func encodeImmediates(value: Int32) throws @@ -82,6 +83,14 @@ extension BinaryInstructionEncoder { try encodeInstruction([0x13]) try encodeImmediates(typeIndex: typeIndex, tableIndex: tableIndex) } + mutating func visitCallRef(typeIndex: UInt32) throws { + try encodeInstruction([0x14]) + try encodeImmediates(typeIndex: typeIndex) + } + mutating func visitReturnCallRef(typeIndex: UInt32) throws { + try encodeInstruction([0x15]) + try encodeImmediates(typeIndex: typeIndex) + } mutating func visitDrop() throws { try encodeInstruction([0x1A]) } mutating func visitSelect() throws { try encodeInstruction([0x1B]) } mutating func visitTypedSelect(type: ValueType) throws { @@ -171,7 +180,7 @@ extension BinaryInstructionEncoder { try encodeInstruction([0x44]) try encodeImmediates(value: value) } - mutating func visitRefNull(type: ReferenceType) throws { + mutating func visitRefNull(type: HeapType) throws { try encodeInstruction([0xD0]) try encodeImmediates(type: type) } @@ -180,6 +189,15 @@ extension BinaryInstructionEncoder { try encodeInstruction([0xD2]) try encodeImmediates(functionIndex: functionIndex) } + mutating func visitRefAsNonNull() throws { try encodeInstruction([0xD4]) } + mutating func visitBrOnNull(relativeDepth: UInt32) throws { + try encodeInstruction([0xD5]) + try encodeImmediates(relativeDepth: relativeDepth) + } + mutating func visitBrOnNonNull(relativeDepth: UInt32) throws { + try encodeInstruction([0xD6]) + try encodeImmediates(relativeDepth: relativeDepth) + } mutating func visitI32Eqz() throws { try encodeInstruction([0x45]) } mutating func visitCmp(_ cmp: Instruction.Cmp) throws { let opcode: [UInt8] diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 0f0829cc..68ab8f59 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -120,16 +120,33 @@ extension ValueType: WasmEncodable { case .f32: encoder.output.append(0x7D) case .f64: encoder.output.append(0x7C) case .v128: encoder.output.append(0x7B) - case .ref(let refType): refType.encode(to: &encoder) + case .ref(let refType): encoder.encode(refType) } } } extension ReferenceType: WasmEncodable { + func encode(to encoder: inout Encoder) { + switch (isNullable, heapType) { + // Use short form when available + case (true, .externRef): encoder.output.append(0x6F) + case (true, .funcRef): encoder.output.append(0x70) + default: + encoder.output.append(isNullable ? 0x63 : 0x64) + encoder.encode(heapType) + } + } +} + +extension HeapType: WasmEncodable { func encode(to encoder: inout Encoder) { switch self { - case .funcRef: encoder.output.append(0x70) - case .externRef: encoder.output.append(0x6F) + case .abstract(.externRef): encoder.output.append(0x6F) + case .abstract(.funcRef): encoder.output.append(0x70) + case .concrete(let typeIndex): + // Note that the typeIndex is decoded as s33, + // so we need to encode it as signed. + encoder.writeSignedLEB128(Int64(typeIndex)) } } } @@ -194,15 +211,16 @@ struct ElementExprCollector: AnyInstructionVisitor { extension WAT.WatParser.ElementDecl { func encode(to encoder: inout Encoder, wat: inout Wat) throws { - func isMemory64(tableIndex: Int) -> Bool { + func isMemory64(tableIndex: Int) throws -> Bool { guard tableIndex < wat.tablesMap.count else { return false } - return wat.tablesMap[tableIndex].type.limits.isMemory64 + return try wat.tablesMap[tableIndex].type.resolve(wat.types).limits.isMemory64 } var flags: UInt32 = 0 var tableIndex: UInt32? = nil var isPassive = false var hasTableIndex = false + let type = try type.resolve(wat.types) switch self.mode { case .active(let table, _): let index: Int? @@ -233,7 +251,7 @@ extension WAT.WatParser.ElementDecl { try collector.parse(indices: indices, wat: &wat) var useExpression: Bool { // if all instructions are ref.func, use function indices representation - return !collector.isAllRefFunc || self.type != .funcRef + return !collector.isAllRefFunc || type != .funcRef } if useExpression { // use expression @@ -252,7 +270,7 @@ extension WAT.WatParser.ElementDecl { try encoder.writeInstruction(lexer: &lexer, wat: &wat) case .synthesized(let offset): var exprEncoder = ExpressionEncoder() - if isMemory64(tableIndex: Int(tableIndex ?? 0)) { + if try isMemory64(tableIndex: Int(tableIndex ?? 0)) { try exprEncoder.visitI64Const(value: Int64(offset)) } else { try exprEncoder.visitI32Const(value: Int32(offset)) @@ -324,6 +342,7 @@ extension Export: WasmEncodable { extension WatParser.GlobalDecl { func encode(to encoder: inout Encoder, wat: inout Wat) throws { + let type = try self.type.resolve(wat.types) encoder.encode(type) guard case var .definition(expr) = kind else { fatalError("imported global declaration should not be encoded here") @@ -496,6 +515,7 @@ struct ExpressionEncoder: BinaryInstructionEncoder { mutating func encodeImmediates(functionIndex: UInt32) throws { encodeUnsigned(functionIndex) } mutating func encodeImmediates(globalIndex: UInt32) throws { encodeUnsigned(globalIndex) } mutating func encodeImmediates(localIndex: UInt32) throws { encodeUnsigned(localIndex) } + mutating func encodeImmediates(typeIndex: UInt32) throws { encodeUnsigned(typeIndex) } mutating func encodeImmediates(memarg: WasmParser.MemArg) throws { encodeUnsigned(UInt(memarg.align)) encodeUnsigned(memarg.offset) @@ -510,7 +530,7 @@ struct ExpressionEncoder: BinaryInstructionEncoder { encodeUnsigned(targets.defaultIndex) } mutating func encodeImmediates(type: WasmTypes.ValueType) throws { encoder.encode(type) } - mutating func encodeImmediates(type: WasmTypes.ReferenceType) throws { encoder.encode(type) } + mutating func encodeImmediates(type: WasmTypes.HeapType) throws { encoder.encode(type) } mutating func encodeImmediates(value: Int32) throws { encodeSigned(value) } mutating func encodeImmediates(value: Int64) throws { encodeSigned(value) } mutating func encodeImmediates(value: WasmParser.IEEE754.Float32) throws { encodeFixedWidth(value.bitPattern) } @@ -557,10 +577,11 @@ func encode(module: inout Wat, options: EncodeOptions) throws -> [UInt8] { // Encode locals var localsEntries: [(type: ValueType, count: UInt32)] = [] for local in locals { - if localsEntries.last?.type == local.type { + let type = try local.type.resolve(module.types) + if localsEntries.last?.type == type { localsEntries[localsEntries.count - 1].count += 1 } else { - localsEntries.append((type: local.type, count: 1)) + localsEntries.append((type: type, count: 1)) } } exprEncoder.encoder.encodeVector(localsEntries) { local, encoder in @@ -604,9 +625,9 @@ func encode(module: inout Wat, options: EncodeOptions) throws -> [UInt8] { // Section 4: Table section let tables = module.tablesMap.definitions() if !tables.isEmpty { - encoder.section(id: 0x04) { encoder in - encoder.encodeVector(tables) { table, encoder in - table.type.encode(to: &encoder) + try encoder.section(id: 0x04) { encoder in + try encoder.encodeVector(tables) { table, encoder in + try table.type.resolve(module.types).encode(to: &encoder) } } } diff --git a/Sources/WAT/NameMapping.swift b/Sources/WAT/NameMapping.swift index ab7f019b..c525ef2b 100644 --- a/Sources/WAT/NameMapping.swift +++ b/Sources/WAT/NameMapping.swift @@ -21,8 +21,12 @@ protocol ImportableModuleFieldDecl { var importNames: WatParser.ImportNames? { get } } +protocol NameToIndexResolver { + func resolveIndex(use: Parser.IndexOrId) throws -> Int +} + /// A map of module field declarations indexed by their name -struct NameMapping { +struct NameMapping: NameToIndexResolver { private var decls: [Decl] = [] private var nameToIndex: [String: Int] = [:] @@ -94,15 +98,21 @@ extension NameMapping where Decl: ImportableModuleFieldDecl { } } +typealias TypesNameMapping = NameMapping + /// A map of unique function types indexed by their name or type signature struct TypesMap { - private var nameMapping = NameMapping() + struct NamedResolvedType: NamedModuleFieldDecl { + let id: Name? + let type: WatParser.FunctionType + } + private(set) var nameMapping = NameMapping() /// Tracks the earliest index for each function type private var indices: [FunctionType: Int] = [:] /// Adds a new function type to the mapping @discardableResult - mutating func add(_ decl: WatParser.FunctionTypeDecl) throws -> Int { + mutating func add(_ decl: NamedResolvedType) throws -> Int { try nameMapping.add(decl) // Normalize the function type signature without parameter names if let existing = indices[decl.type.signature] { @@ -120,7 +130,7 @@ struct TypesMap { return existing } return try add( - WatParser.FunctionTypeDecl( + NamedResolvedType( id: nil, type: WatParser.FunctionType(signature: signature, parameterNames: []) ) @@ -170,10 +180,10 @@ struct TypesMap { mutating func resolveBlockType(use: WatParser.TypeUse) throws -> BlockType { switch (use.index, use.inline) { case let (indexOrId?, inline): - let (type, index) = try resolveAndCheck(use: indexOrId, inline: inline) + let (type, index) = try resolveAndCheck(use: indexOrId, inline: inline?.resolve(nameMapping)) return try resolveBlockType(signature: type.signature, resolveSignatureIndex: { _ in index }) case (nil, let inline?): - return try resolveBlockType(signature: inline.signature) + return try resolveBlockType(signature: inline.resolve(nameMapping).signature) case (nil, nil): return .empty } } @@ -183,7 +193,7 @@ struct TypesMap { case let (indexOrId?, _): return try nameMapping.resolveIndex(use: indexOrId) case (nil, let inline): - let inline = inline?.signature ?? WasmTypes.FunctionType(parameters: [], results: []) + let inline = try inline?.resolve(nameMapping).signature ?? WasmTypes.FunctionType(parameters: [], results: []) return try addAnonymousSignature(inline) } } @@ -209,16 +219,16 @@ struct TypesMap { mutating func resolve(use: WatParser.TypeUse) throws -> (type: WatParser.FunctionType, index: Int) { switch (use.index, use.inline) { case let (indexOrId?, inline): - return try resolveAndCheck(use: indexOrId, inline: inline) + return try resolveAndCheck(use: indexOrId, inline: inline?.resolve(nameMapping)) case (nil, let inline): // If no index and no inline type, then it's a function type with no parameters or results - let inline = inline ?? WatParser.FunctionType(signature: WasmTypes.FunctionType(parameters: [], results: []), parameterNames: []) + let inline = try inline?.resolve(nameMapping) ?? WatParser.FunctionType(signature: WasmTypes.FunctionType(parameters: [], results: []), parameterNames: []) // Check if the inline type already exists if let index = indices[inline.signature] { return (inline, index) } // Add inline type to the index space if it doesn't already exist - let index = try add(WatParser.FunctionTypeDecl(id: nil, type: inline)) + let index = try add(NamedResolvedType(id: nil, type: inline)) return (inline, index) } } @@ -233,11 +243,11 @@ extension TypesMap: Collection { nameMapping.index(after: i) } - subscript(position: Int) -> WatParser.FunctionTypeDecl { + subscript(position: Int) -> NamedResolvedType { return nameMapping[position] } - func makeIterator() -> NameMapping.Iterator { + func makeIterator() -> NameMapping.Iterator { return nameMapping.makeIterator() } } diff --git a/Sources/WAT/ParseTextInstruction.swift b/Sources/WAT/ParseTextInstruction.swift index fb57a2e3..12dd108c 100644 --- a/Sources/WAT/ParseTextInstruction.swift +++ b/Sources/WAT/ParseTextInstruction.swift @@ -49,6 +49,12 @@ func parseTextInstruction(keyword: String, expressionPars case "return_call_indirect": let (typeIndex, tableIndex) = try expressionParser.visitReturnCallIndirect(wat: &wat) return { return try $0.visitReturnCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex) } + case "call_ref": + let (typeIndex) = try expressionParser.visitCallRef(wat: &wat) + return { return try $0.visitCallRef(typeIndex: typeIndex) } + case "return_call_ref": + let (typeIndex) = try expressionParser.visitReturnCallRef(wat: &wat) + return { return try $0.visitReturnCallRef(typeIndex: typeIndex) } case "drop": return { return try $0.visitDrop() } case "select": return { return try $0.visitSelect() } case "local.get": @@ -160,6 +166,13 @@ func parseTextInstruction(keyword: String, expressionPars case "ref.func": let (functionIndex) = try expressionParser.visitRefFunc(wat: &wat) return { return try $0.visitRefFunc(functionIndex: functionIndex) } + case "ref.as_non_null": return { return try $0.visitRefAsNonNull() } + case "br_on_null": + let (relativeDepth) = try expressionParser.visitBrOnNull(wat: &wat) + return { return try $0.visitBrOnNull(relativeDepth: relativeDepth) } + case "br_on_non_null": + let (relativeDepth) = try expressionParser.visitBrOnNonNull(wat: &wat) + return { return try $0.visitBrOnNonNull(relativeDepth: relativeDepth) } case "i32.eqz": return { return try $0.visitI32Eqz() } case "i32.eq": return { return try $0.visitCmp(.i32Eq) } case "i32.ne": return { return try $0.visitCmp(.i32Ne) } diff --git a/Sources/WAT/Parser.swift b/Sources/WAT/Parser.swift index a016413d..c241d268 100644 --- a/Sources/WAT/Parser.swift +++ b/Sources/WAT/Parser.swift @@ -111,6 +111,8 @@ internal struct Parser { return String(decoding: bytes, as: UTF8.self) } + /// Parse a `u32` raw index or a symbolic `id` identifier. + /// https://webassembly.github.io/function-references/core/text/modules.html#indices mutating func takeIndexOrId() throws -> IndexOrId? { let location = lexer.location() if let index: UInt32 = try takeUnsignedInt() { diff --git a/Sources/WAT/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index b832e1a4..be608bd4 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -2,7 +2,7 @@ import WasmParser import WasmTypes struct ExpressionParser { - typealias LocalsMap = NameMapping + typealias LocalsMap = NameMapping private struct LabelStack { private var stack: [String?] = [] @@ -43,10 +43,11 @@ struct ExpressionParser { type: WatParser.FunctionType, locals: [WatParser.LocalDecl], lexer: Lexer, - features: WasmFeatureSet + features: WasmFeatureSet, + typeMap: TypesNameMapping ) throws { self.parser = Parser(lexer) - self.locals = try Self.computeLocals(type: type, locals: locals) + self.locals = try Self.computeLocals(type: type, locals: locals, typeMap: typeMap) self.features = features } @@ -56,13 +57,17 @@ struct ExpressionParser { self.features = features } - static func computeLocals(type: WatParser.FunctionType, locals: [WatParser.LocalDecl]) throws -> LocalsMap { + static func computeLocals( + type: WatParser.FunctionType, + locals: [WatParser.LocalDecl], + typeMap: TypesNameMapping + ) throws -> LocalsMap { var localsMap = LocalsMap() for (name, type) in zip(type.parameterNames, type.signature.parameters) { - try localsMap.add(WatParser.LocalDecl(id: name, type: type)) + try localsMap.add(.init(id: name, type: type)) } for local in locals { - try localsMap.add(local) + try localsMap.add(local.resolve(typeMap)) } return localsMap } @@ -157,6 +162,17 @@ struct ExpressionParser { { return value } + + // WAST predication allows omitting some concrete specifiers + if try parser.takeParenBlockStart("ref.null"), try parser.isEndOfParen() { + return .refNull(nil) + } + if try parser.takeParenBlockStart("ref.func"), try parser.isEndOfParen() { + return .refFunc(functionIndex: nil) + } + if try parser.takeParenBlockStart("ref.extern"), try parser.isEndOfParen() { + return .refFunc(functionIndex: nil) + } parser = initialParser return nil } @@ -261,8 +277,10 @@ struct ExpressionParser { case "select": // Special handling for "select", which have two variants 1. with type, 2. without type let results = try withWatParser({ try $0.results() }) + let types = wat.types return { visitor in if let type = results.first { + let type = try type.resolve(types) return try visitor.visitTypedSelect(type: type) } else { return try visitor.visitSelect() @@ -338,7 +356,9 @@ struct ExpressionParser { } private mutating func blockType(wat: inout Wat) throws -> BlockType { - let results = try withWatParser({ try $0.results() }) + let results = try withWatParser { + try $0.results().map { try $0.resolve(wat.types) } + } if !results.isEmpty { return try wat.types.resolveBlockType(results: results) } @@ -361,13 +381,17 @@ struct ExpressionParser { return UInt32(index) } - private mutating func refKind() throws -> ReferenceType { + /// https://webassembly.github.io/function-references/core/text/types.html#text-heaptype + private mutating func heapType(wat: inout Wat) throws -> HeapType { if try parser.takeKeyword("func") { return .funcRef } else if try parser.takeKeyword("extern") { return .externRef + } else if let id = try parser.takeIndexOrId() { + let (_, index) = try wat.types.resolve(use: id) + return .concrete(typeIndex: UInt32(index)) } - throw WatParserError("expected \"func\" or \"extern\"", location: parser.lexer.location()) + throw WatParserError("expected \"func\", \"extern\" or type index", location: parser.lexer.location()) } private mutating func memArg(defaultAlign: UInt32) throws -> MemArg { @@ -439,6 +463,19 @@ extension ExpressionParser { let use = try parser.expectIndexOrId() return UInt32(try wat.functionsMap.resolve(use: use).index) } + mutating func visitCallRef(wat: inout Wat) throws -> UInt32 { + let use = try parser.expectIndexOrId() + return UInt32(try wat.types.resolve(use: use).index) + } + mutating func visitReturnCallRef(wat: inout Wat) throws -> UInt32 { + return try visitCallRef(wat: &wat) + } + mutating func visitBrOnNull(wat: inout Wat) throws -> UInt32 { + return try labelIndex() + } + mutating func visitBrOnNonNull(wat: inout Wat) throws -> UInt32 { + return try labelIndex() + } mutating func visitCallIndirect(wat: inout Wat) throws -> (typeIndex: UInt32, tableIndex: UInt32) { let tableIndex: UInt32 if let tableId = try parser.takeIndexOrId() { @@ -498,8 +535,8 @@ extension ExpressionParser { mutating func visitF64Const(wat: inout Wat) throws -> IEEE754.Float64 { return try parser.expectFloat64() } - mutating func visitRefNull(wat: inout Wat) throws -> ReferenceType { - return try refKind() + mutating func visitRefNull(wat: inout Wat) throws -> HeapType { + return try heapType(wat: &wat) } mutating func visitRefFunc(wat: inout Wat) throws -> UInt32 { return try functionIndex(wat: &wat) diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index cfc69fb1..5bef2b32 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -54,31 +54,25 @@ struct WastParser { } struct ConstExpressionCollector: WastConstInstructionVisitor { - let addValue: (Value) -> Void + let addValue: (WastConstValue) -> Void mutating func visitI32Const(value: Int32) throws { addValue(.i32(UInt32(bitPattern: value))) } mutating func visitI64Const(value: Int64) throws { addValue(.i64(UInt64(bitPattern: value))) } mutating func visitF32Const(value: IEEE754.Float32) throws { addValue(.f32(value.bitPattern)) } mutating func visitF64Const(value: IEEE754.Float64) throws { addValue(.f64(value.bitPattern)) } mutating func visitRefFunc(functionIndex: UInt32) throws { - addValue(.ref(.function(FunctionAddress(functionIndex)))) + addValue(.refFunc(functionIndex: functionIndex)) } - mutating func visitRefNull(type: ReferenceType) throws { - let value: Reference - switch type { - case .externRef: value = .extern(nil) - case .funcRef: value = .function(nil) - } - addValue(.ref(value)) + mutating func visitRefNull(type: HeapType) throws { + addValue(.refNull(type)) } - - mutating func visitRefExtern(value: UInt32) throws { - addValue(.ref(.extern(ExternAddress(value)))) + func visitRefExtern(value: UInt32) throws { + addValue(.refExtern(value: value)) } } - mutating func constExpression() throws -> [Value] { - var values: [Value] = [] + mutating func argumentValues() throws -> [WastConstValue] { + var values: [WastConstValue] = [] var collector = ConstExpressionCollector(addValue: { values.append($0) }) var exprParser = ExpressionParser(lexer: parser.lexer, features: features) while try exprParser.parseWastConstInstruction(visitor: &collector) {} @@ -88,7 +82,19 @@ struct WastParser { mutating func expectationValues() throws -> [WastExpectValue] { var values: [WastExpectValue] = [] - var collector = ConstExpressionCollector(addValue: { values.append(.value($0)) }) + var collector = ConstExpressionCollector(addValue: { + let value: WastExpectValue + switch $0 { + case .i32(let v): value = .i32(v) + case .i64(let v): value = .i64(v) + case .f32(let v): value = .f32(v) + case .f64(let v): value = .f64(v) + case .refNull(let heapTy): value = .refNull(heapTy) + case .refFunc(let index): value = .refFunc(functionIndex: index) + case .refExtern(let v): value = .refExtern(value: v) + } + values.append(value) + }) var exprParser = ExpressionParser(lexer: parser.lexer, features: features) while true { if let expectValue = try exprParser.parseWastExpectValue() { @@ -137,16 +143,26 @@ public enum WastExecute { } } +public enum WastConstValue { + case i32(UInt32) + case i64(UInt64) + case f32(UInt32) + case f64(UInt64) + case refNull(HeapType) + case refFunc(functionIndex: UInt32) + case refExtern(value: UInt32) +} + public struct WastInvoke { public let module: String? public let name: String - public let args: [Value] + public let args: [WastConstValue] static func parse(wastParser: inout WastParser) throws -> WastInvoke { try wastParser.parser.expectKeyword("invoke") let module = try wastParser.parser.takeId() let name = try wastParser.parser.expectString() - let args = try wastParser.constExpression() + let args = try wastParser.argumentValues() try wastParser.parser.expect(.rightParen) let invoke = WastInvoke(module: module?.value, name: name, args: args) return invoke @@ -154,8 +170,20 @@ public struct WastInvoke { } public enum WastExpectValue { - /// A concrete value that is expected to be returned. - case value(Value) + case i32(UInt32) + case i64(UInt64) + case f32(UInt32) + case f64(UInt64) + + /// A value that is expected to be a null reference, + /// optionally with a specific type. + case refNull(HeapType?) + /// A value that is expected to be a non-null reference + /// to a function, optionally with a specific index. + case refFunc(functionIndex: UInt32?) + /// A value that is expected to be a non-null reference + /// to an extern, optionally with a specific value. + case refExtern(value: UInt32?) /// A value that is expected to be a canonical NaN. /// Corresponds to `f32.const nan:canonical` in WAST. case f32CanonicalNaN diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index 530494a4..7f3859e0 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -35,9 +35,34 @@ struct WatParser { case global } - struct Parameter: Equatable { - let id: String? - let type: ValueType + struct UnresolvedType { + private let make: (any NameToIndexResolver) throws -> T + + init(make: @escaping (any NameToIndexResolver) throws -> T) { + self.make = make + } + + init(_ value: T) { + self.make = { _ in value } + } + + func project(_ keyPath: KeyPath) -> UnresolvedType { + return UnresolvedType { + let parent = try make($0) + return parent[keyPath: keyPath] + } + } + + func map(_ transform: @escaping (T) -> U) -> UnresolvedType { + return UnresolvedType(make: { try transform(resolve($0)) }) + } + + func resolve(_ typeMap: TypesMap) throws -> T { + return try resolve(typeMap.nameMapping) + } + func resolve(_ resolver: any NameToIndexResolver) throws -> T { + return try make(resolver) + } } struct FunctionType { @@ -53,12 +78,20 @@ struct WatParser { /// The index of the type in the type section specified by `(type ...)`. let index: Parser.IndexOrId? /// The inline type specified by `(param ...) (result ...)`. - let inline: FunctionType? + let inline: UnresolvedType? /// The source location of the type use. let location: Location } struct LocalDecl: NamedModuleFieldDecl { + var id: Name? + var type: UnresolvedType + + func resolve(_ typeMap: TypesNameMapping) throws -> ResolvedLocalDecl { + try ResolvedLocalDecl(id: id, type: type.resolve(typeMap)) + } + } + struct ResolvedLocalDecl: NamedModuleFieldDecl { var id: Name? var type: ValueType } @@ -85,7 +118,10 @@ struct WatParser { fatalError("Imported functions cannot be parsed") } let (type, typeIndex) = try wat.types.resolve(use: typeUse) - var parser = try ExpressionParser(type: type, locals: locals, lexer: body, features: features) + var parser = try ExpressionParser( + type: type, locals: locals, lexer: body, + features: features, typeMap: wat.types.nameMapping + ) try parser.parse(visitor: &visitor, wat: &wat) // Check if the parser has reached the end of the function body guard try parser.parser.isEndOfParen() else { @@ -97,13 +133,13 @@ struct WatParser { struct FunctionTypeDecl: NamedModuleFieldDecl { let id: Name? - let type: FunctionType + let type: UnresolvedType } struct TableDecl: NamedModuleFieldDecl, ImportableModuleFieldDecl { var id: Name? var exports: [String] - var type: TableType + var type: UnresolvedType var importNames: ImportNames? var inlineElement: ElementDecl? } @@ -128,7 +164,7 @@ struct WatParser { var id: Name? var mode: Mode - var type: ReferenceType + var type: UnresolvedType var indices: Indices } @@ -141,7 +177,7 @@ struct WatParser { struct GlobalDecl: NamedModuleFieldDecl, ImportableModuleFieldDecl { var id: Name? var exports: [String] - var type: GlobalType + var type: UnresolvedType var kind: GlobalKind var importNames: WatParser.ImportNames? { @@ -234,23 +270,29 @@ struct WatParser { let id = try parser.takeId() let exports = try inlineExports() let importNames = try inlineImport() - let type: TableType + let type: UnresolvedType var inlineElement: ElementDecl? let isMemory64 = try expectAddressSpaceType() - if let refType = try maybeRefType() { + // elemexpr ::= '(' 'item' expr ')' | '(' instr ')' + func parseExprList() throws -> (UInt64, ElementDecl.Indices) { + var numberOfItems: UInt64 = 0 + let indices: ElementDecl.Indices = .elementExprList(parser.lexer) + while try parser.take(.leftParen) { + numberOfItems += 1 + try parser.skipParenBlock() + } + return (numberOfItems, indices) + } + + if let refType = try takeRefType() { guard try parser.takeParenBlockStart("elem") else { throw WatParserError("expected elem", location: parser.lexer.location()) } var numberOfItems: UInt64 = 0 let indices: ElementDecl.Indices if try parser.peek(.leftParen) != nil { - // elemexpr ::= '(' 'item' expr ')' | '(' instr ')' - indices = .elementExprList(parser.lexer) - while try parser.take(.leftParen) { - numberOfItems += 1 - try parser.skipParenBlock() - } + (numberOfItems, indices) = try parseExprList() } else { // Consume function indices indices = .functionList(parser.lexer) @@ -262,16 +304,30 @@ struct WatParser { mode: .inline, type: refType, indices: indices ) try parser.expect(.rightParen) - type = TableType( - elementType: refType, - limits: Limits( - min: numberOfItems, - max: numberOfItems, - isMemory64: isMemory64 + type = refType.map { + TableType( + elementType: $0, + limits: Limits( + min: numberOfItems, + max: numberOfItems, + isMemory64: isMemory64 + ) ) - ) + } } else { - type = try tableType(isMemory64: isMemory64) + var tableType = try tableType(isMemory64: isMemory64) + if try parser.peek(.leftParen) != nil { + let (numberOfItems, indices) = try parseExprList() + inlineElement = ElementDecl( + mode: .inline, type: tableType.project(\.elementType), indices: indices + ) + tableType = tableType.map { + var value = $0 + value.limits.min = numberOfItems + return value + } + } + type = tableType } kind = .table( TableDecl( @@ -353,12 +409,13 @@ struct WatParser { if try parser.takeKeyword("declare") { mode = .declarative } else { - table = try tableUse() + table = try takeTableUse() if try parser.takeParenBlockStart("offset") { mode = .active(table: table, offset: .expression(parser.lexer)) try parser.skipParenBlock() } else { - if try parser.peek(.leftParen) != nil { + // Need to distinguish '(' instr ')' and reftype without parsing instruction + if try parser.peek(.leftParen) != nil, try fork({ try $0.takeRefType() == nil }) { // abbreviated offset instruction mode = .active(table: table, offset: .singleInstruction(parser.lexer)) try parser.consume() // consume ( @@ -372,13 +429,13 @@ struct WatParser { // elemlist ::= reftype elemexpr* | 'func' funcidx* // | funcidx* (iff the tableuse is omitted) let indices: ElementDecl.Indices - let type: ReferenceType - if let refType = try maybeRefType() { + let type: UnresolvedType + if let refType = try takeRefType() { indices = .elementExprList(parser.lexer) type = refType } else if try parser.takeKeyword("func") || table == nil { indices = .functionList(parser.lexer) - type = .funcRef + type = UnresolvedType(.funcRef) } else { throw WatParserError("expected element list", location: parser.lexer.location()) } @@ -406,6 +463,11 @@ struct WatParser { return ModuleField(location: location, kind: kind) } + private func fork(_ body: (inout WatParser) throws -> R) rethrows -> R { + var subParser = WatParser(parser: parser) + return try body(&subParser) + } + mutating func locals() throws -> [LocalDecl] { var decls: [LocalDecl] = [] while try parser.takeParenBlockStart("local") { @@ -457,7 +519,7 @@ struct WatParser { return TypeUse(index: index, inline: inline, location: location) } - mutating func tableUse() throws -> Parser.IndexOrId? { + mutating func takeTableUse() throws -> Parser.IndexOrId? { var index: Parser.IndexOrId? if try parser.takeParenBlockStart("table") { index = try parser.expectIndexOrId() @@ -496,11 +558,11 @@ struct WatParser { return isMemory64 } - mutating func tableType() throws -> TableType { + mutating func tableType() throws -> UnresolvedType { return try tableType(isMemory64: expectAddressSpaceType()) } - mutating func tableType(isMemory64: Bool) throws -> TableType { + mutating func tableType(isMemory64: Bool) throws -> UnresolvedType { let limits: Limits if isMemory64 { limits = try limit64() @@ -508,7 +570,7 @@ struct WatParser { limits = try limit32() } let elementType = try refType() - return TableType(elementType: elementType, limits: limits) + return elementType.map { TableType(elementType: $0, limits: limits) } } mutating func memoryType() throws -> MemoryType { @@ -527,7 +589,7 @@ struct WatParser { } /// globaltype ::= t:valtype | '(' 'mut' t:valtype ')' - mutating func globalType() throws -> GlobalType { + mutating func globalType() throws -> UnresolvedType { let mutability: Mutability if try parser.takeParenBlockStart("mut") { mutability = .variable @@ -538,7 +600,7 @@ struct WatParser { if mutability == .variable { try parser.expect(.rightParen) } - return GlobalType(mutability: mutability, valueType: valueType) + return valueType.map { GlobalType(mutability: mutability, valueType: $0) } } mutating func limit32() throws -> Limits { @@ -554,26 +616,36 @@ struct WatParser { } /// functype ::= '(' 'func' t1*:vec(param) t2*:vec(result) ')' => [t1*] -> [t2*] - mutating func funcType() throws -> FunctionType { + mutating func funcType() throws -> UnresolvedType { try parser.expect(.leftParen) try parser.expectKeyword("func") let (params, names) = try params(mayHaveName: true) let results = try results() try parser.expect(.rightParen) - return FunctionType(signature: WasmTypes.FunctionType(parameters: params, results: results), parameterNames: names) + return UnresolvedType { typeMap in + let params = try params.map { try $0.resolve(typeMap) } + let results = try results.map { try $0.resolve(typeMap) } + let signature = WasmTypes.FunctionType(parameters: params, results: results) + return FunctionType(signature: signature, parameterNames: names) + } } - mutating func optionalFunctionType(mayHaveName: Bool) throws -> FunctionType? { + mutating func optionalFunctionType(mayHaveName: Bool) throws -> UnresolvedType? { let (params, names) = try params(mayHaveName: mayHaveName) let results = try results() if results.isEmpty, params.isEmpty { return nil } - return FunctionType(signature: WasmTypes.FunctionType(parameters: params, results: results), parameterNames: names) + return UnresolvedType { typeMap in + let params = try params.map { try $0.resolve(typeMap) } + let results = try results.map { try $0.resolve(typeMap) } + let signature = WasmTypes.FunctionType(parameters: params, results: results) + return FunctionType(signature: signature, parameterNames: names) + } } - mutating func params(mayHaveName: Bool) throws -> ([ValueType], [Name?]) { - var types: [ValueType] = [] + mutating func params(mayHaveName: Bool) throws -> ([UnresolvedType], [Name?]) { + var types: [UnresolvedType] = [] var names: [Name?] = [] while try parser.takeParenBlockStart("param") { if mayHaveName { @@ -594,8 +666,8 @@ struct WatParser { return (types, names) } - mutating func results() throws -> [ValueType] { - var results: [ValueType] = [] + mutating func results() throws -> [UnresolvedType] { + var results: [UnresolvedType] = [] while try parser.takeParenBlockStart("result") { while try !parser.take(.rightParen) { let valueType = try valueType() @@ -605,41 +677,60 @@ struct WatParser { return results } - mutating func valueType() throws -> ValueType { - let keyword = try parser.expectKeyword() - switch keyword { - case "i32": return .i32 - case "i64": return .i64 - case "f32": return .f32 - case "f64": return .f64 - default: - if let refType = refType(keyword: keyword) { return .ref(refType) } - throw WatParserError("unexpected value type \(keyword)", location: parser.lexer.location()) - } - } - - mutating func refType(keyword: String) -> ReferenceType? { - switch keyword { - case "funcref": return .funcRef - case "externref": return .externRef - default: return nil + mutating func valueType() throws -> UnresolvedType { + if try parser.takeKeyword("i32") { + return UnresolvedType(.i32) + } else if try parser.takeKeyword("i64") { + return UnresolvedType(.i64) + } else if try parser.takeKeyword("f32") { + return UnresolvedType(.f32) + } else if try parser.takeKeyword("f64") { + return UnresolvedType(.f64) + } else if let refType = try takeRefType() { + return refType.map { .ref($0) } + } else { + throw WatParserError("expected value type", location: parser.lexer.location()) } } - mutating func refType() throws -> ReferenceType { - let keyword = try parser.expectKeyword() - guard let refType = refType(keyword: keyword) else { - throw WatParserError("unexpected ref type \(keyword)", location: parser.lexer.location()) + mutating func refType() throws -> UnresolvedType { + guard let refType = try takeRefType() else { + throw WatParserError("expected reference type", location: parser.lexer.location()) } return refType } - mutating func maybeRefType() throws -> ReferenceType? { + /// Parse a reference type tokens if the head tokens seems like so. + mutating func takeRefType() throws -> UnresolvedType? { + // Check abbreviations first + // https://webassembly.github.io/function-references/core/text/types.html#abbreviations if try parser.takeKeyword("funcref") { - return .funcRef + return UnresolvedType(.funcRef) } else if try parser.takeKeyword("externref") { - return .externRef + return UnresolvedType(.externRef) + } else if try parser.takeParenBlockStart("ref") { + let isNullable = try parser.takeKeyword("null") + let heapType = try heapType() + try parser.expect(.rightParen) + return heapType.map { + ReferenceType(isNullable: isNullable, heapType: $0) + } } return nil } + + /// > Note: + /// + mutating func heapType() throws -> UnresolvedType { + if try parser.takeKeyword("func") { + return UnresolvedType(.abstract(.funcRef)) + } else if try parser.takeKeyword("extern") { + return UnresolvedType(.abstract(.externRef)) + } else if let id = try parser.takeIndexOrId() { + return UnresolvedType(make: { + try .concrete(typeIndex: UInt32($0.resolveIndex(use: id))) + }) + } + throw WatParserError("expected heap type", location: parser.lexer.location()) + } } diff --git a/Sources/WAT/WAT.swift b/Sources/WAT/WAT.swift index 707f7161..ada38bbf 100644 --- a/Sources/WAT/WAT.swift +++ b/Sources/WAT/WAT.swift @@ -176,8 +176,26 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { let initialParser = parser - var importFactories: [() throws -> Import] = [] var typesMap = TypesMap() + + do { + var unresolvedTypesMapping = NameMapping() + // 1. Collect module type decls and resolve symbolic references inside + // their definitions. + var watParser = WatParser(parser: initialParser) + while let decl = try watParser.next() { + guard case let .type(decl) = decl.kind else { continue } + try unresolvedTypesMapping.add(decl) + } + for decl in unresolvedTypesMapping { + try typesMap.add( + TypesMap.NamedResolvedType( + id: decl.id, type: decl.type.resolve(unresolvedTypesMapping) + )) + } + } + + var importFactories: [() throws -> Import] = [] var functionsMap = NameMapping() var tablesMap = NameMapping() var memoriesMap = NameMapping() @@ -219,8 +237,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { } switch decl.kind { - case let .type(decl): - try typesMap.add(decl) + case .type: break case let .function(decl): try checkImportOrder(decl.importNames) let index = try functionsMap.add(decl) @@ -244,7 +261,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { try elementSegmentsMap.add(inlineElement) } if let importNames = decl.importNames { - addImport(importNames) { .table(decl.type) } + addImport(importNames) { try .table(decl.type.resolve(typesMap)) } } case let .memory(decl): try checkImportOrder(decl.importNames) @@ -266,7 +283,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { switch decl.kind { case .definition: break case .imported(let importNames): - addImport(importNames) { .global(decl.type) } + addImport(importNames) { try .global(decl.type.resolve(typesMap)) } } case let .element(decl): try elementSegmentsMap.add(decl) @@ -282,13 +299,13 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { } } - // 1. Collect module decls and create name -> index mapping + // 2. Collect module decls and create name -> index mapping var watParser = WatParser(parser: initialParser) while let decl = try watParser.next() { try visitDecl(decl: decl) } - // 2. Resolve a part of module items that reference other module items. + // 3. Resolve a part of module items that reference other module items. // Remaining items like $id references like (call $func) are resolved during encoding. let exports: [Export] = try exportDecls.compactMap { let descriptor: ExportDescriptor @@ -314,8 +331,8 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { types: typesMap, functionsMap: functionsMap, tablesMap: tablesMap, - tables: tablesMap.map { - Table(type: $0.type) + tables: try tablesMap.map { + try Table(type: $0.type.resolve(typesMap)) }, memories: memoriesMap, globals: globalsMap, diff --git a/Sources/WasmKit/Execution/ConstEvaluation.swift b/Sources/WasmKit/Execution/ConstEvaluation.swift index 3f155e6b..5bd7cc80 100644 --- a/Sources/WasmKit/Execution/ConstEvaluation.swift +++ b/Sources/WasmKit/Execution/ConstEvaluation.swift @@ -64,6 +64,8 @@ extension ConstExpression { switch type { case .externRef: return .ref(.extern(nil)) case .funcRef: return .ref(.function(nil)) + default: + throw ValidationError(.illegalConstExpressionInstruction(constInst)) } case .refFunc(let functionIndex): return try .ref(context.functionRef(functionIndex)) diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 884901b7..bf1c2c45 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -139,6 +139,9 @@ extension TrapReason.Message { static func exportedFunctionNotFound(name: String, instance: Instance) -> Self { Self("exported function \(name) not found in instance \(instance)") } + static func unimplemented(feature: String) -> Self { + Self("\(feature) is not implemented yet") + } } struct ImportError: Error { diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 27a2452a..6fb024fd 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -274,11 +274,13 @@ struct TableEntity /* : ~Copyable */ { init(_ tableType: TableType, resourceLimiter: any ResourceLimiter) throws { let emptyElement: Reference - switch tableType.elementType { - case .funcRef: + switch tableType.elementType.heapType { + case .abstract(.funcRef): emptyElement = .function(nil) - case .externRef: + case .abstract(.externRef): emptyElement = .extern(nil) + case .concrete: + throw Trap(.unimplemented(feature: "heap type other than `func` and `extern`")) } let numberOfElements = Int(tableType.limits.min) diff --git a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift index 2888bee8..72b9802a 100644 --- a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift +++ b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift @@ -104,12 +104,12 @@ extension Int32: InstructionImmediate { // MARK: - Immediate type extensions extension Instruction.RefNullOperand { - init(result: VReg, type: ReferenceType) { - self.init(result: result, rawType: type.rawValue) + init(result: VReg, type: AbstractHeapType) { + self.init(result: result, rawType: type.rawValue) // need to figure out rawType here } - var type: ReferenceType { - ReferenceType(rawValue: rawType).unsafelyUnwrapped + var type: AbstractHeapType { + AbstractHeapType(rawValue: rawType).unsafelyUnwrapped } } diff --git a/Sources/WasmKit/Execution/UntypedValue.swift b/Sources/WasmKit/Execution/UntypedValue.swift index 57e80114..ba498fa3 100644 --- a/Sources/WasmKit/Execution/UntypedValue.swift +++ b/Sources/WasmKit/Execution/UntypedValue.swift @@ -114,11 +114,13 @@ struct UntypedValue: Equatable, Hashable { guard storage & Self.isNullMaskPattern == 0 else { return nil } return Int(storage) } - switch type { - case .funcRef: + switch type.heapType { + case .abstract(.funcRef): return .function(decodeOptionalInt()) - case .externRef: + case .abstract(.externRef): return .extern(decodeOptionalInt()) + case .concrete: + fatalError("heap type other than `func` and `extern` is not implemented yet") } } diff --git a/Sources/WasmKit/Execution/Value.swift b/Sources/WasmKit/Execution/Value.swift index aec5637d..9c6a4fcb 100644 --- a/Sources/WasmKit/Execution/Value.swift +++ b/Sources/WasmKit/Execution/Value.swift @@ -1,4 +1,4 @@ -import enum WasmTypes.ReferenceType +import struct WasmTypes.ReferenceType import enum WasmTypes.ValueType /// > Note: diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 55db6715..bd197b5f 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -1879,8 +1879,12 @@ struct InstructionTranslator: InstructionVisitor { mutating func visitI64Const(value: Int64) -> Output { visitConst(.i64, .i64(UInt64(bitPattern: value))) } mutating func visitF32Const(value: IEEE754.Float32) -> Output { visitConst(.f32, .f32(value.bitPattern)) } mutating func visitF64Const(value: IEEE754.Float64) -> Output { visitConst(.f64, .f64(value.bitPattern)) } - mutating func visitRefNull(type: WasmTypes.ReferenceType) -> Output { - pushEmit(.ref(type), { .refNull(Instruction.RefNullOperand(result: $0, type: type)) }) + mutating func visitRefNull(type: HeapType) throws { + guard case let .abstract(abstractType) = type else { + throw TranslationError("concrete heap type is not implemented yet") + } + let typeToPush = ReferenceType(isNullable: true, heapType: type) + pushEmit(.ref(typeToPush), { .refNull(Instruction.RefNullOperand(result: $0, type: abstractType)) }) } mutating func visitRefIsNull() throws -> Output { let value = try valueStack.popRef() diff --git a/Sources/WasmKit/Validator.swift b/Sources/WasmKit/Validator.swift index a313901f..dd14aa32 100644 --- a/Sources/WasmKit/Validator.swift +++ b/Sources/WasmKit/Validator.swift @@ -333,9 +333,11 @@ struct ModuleValidator { extension WasmTypes.Reference { /// Checks if the reference type matches the expected type. func checkType(_ type: WasmTypes.ReferenceType) throws { - switch (self, type) { - case (.function, .funcRef): return - case (.extern, .externRef): return + switch (self, type.heapType, type.isNullable) { + case (.function(_?), .funcRef, _): return + case (.function(nil), .funcRef, true): return + case (.extern(_?), .externRef, _): return + case (.extern(nil), .externRef, true): return default: throw ValidationError(.expectTypeButGot(expected: "\(type)", got: "\(self)")) } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index a89443c2..e1b59c7b 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -30,6 +30,10 @@ protocol BinaryInstructionDecoder { @inlinable mutating func visitReturnCall() throws -> UInt32 /// Decode `return_call_indirect` immediates @inlinable mutating func visitReturnCallIndirect() throws -> (typeIndex: UInt32, tableIndex: UInt32) + /// Decode `call_ref` immediates + @inlinable mutating func visitCallRef() throws -> UInt32 + /// Decode `return_call_ref` immediates + @inlinable mutating func visitReturnCallRef() throws -> UInt32 /// Decode `typedSelect` immediates @inlinable mutating func visitTypedSelect() throws -> ValueType /// Decode `local.get` immediates @@ -59,9 +63,13 @@ protocol BinaryInstructionDecoder { /// Decode `f64.const` immediates @inlinable mutating func visitF64Const() throws -> IEEE754.Float64 /// Decode `ref.null` immediates - @inlinable mutating func visitRefNull() throws -> ReferenceType + @inlinable mutating func visitRefNull() throws -> HeapType /// Decode `ref.func` immediates @inlinable mutating func visitRefFunc() throws -> UInt32 + /// Decode `br_on_null` immediates + @inlinable mutating func visitBrOnNull() throws -> UInt32 + /// Decode `br_on_non_null` immediates + @inlinable mutating func visitBrOnNonNull() throws -> UInt32 /// Decode `memory.init` immediates @inlinable mutating func visitMemoryInit() throws -> UInt32 /// Decode `data.drop` immediates @@ -132,6 +140,12 @@ func parseBinaryInstruction( case 0x13: let (typeIndex, tableIndex) = try decoder.visitReturnCallIndirect() try visitor.visitReturnCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex) + case 0x14: + let (typeIndex) = try decoder.visitCallRef() + try visitor.visitCallRef(typeIndex: typeIndex) + case 0x15: + let (typeIndex) = try decoder.visitReturnCallRef() + try visitor.visitReturnCallRef(typeIndex: typeIndex) case 0x1A: try visitor.visitDrop() case 0x1B: @@ -511,6 +525,14 @@ func parseBinaryInstruction( case 0xD2: let (functionIndex) = try decoder.visitRefFunc() try visitor.visitRefFunc(functionIndex: functionIndex) + case 0xD4: + try visitor.visitRefAsNonNull() + case 0xD5: + let (relativeDepth) = try decoder.visitBrOnNull() + try visitor.visitBrOnNull(relativeDepth: relativeDepth) + case 0xD6: + let (relativeDepth) = try decoder.visitBrOnNonNull() + try visitor.visitBrOnNonNull(relativeDepth: relativeDepth) case 0xFC: let opcode1 = try decoder.claimNextByte() diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 230ba132..c553498b 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -189,6 +189,8 @@ public enum Instruction: Equatable { case `callIndirect`(typeIndex: UInt32, tableIndex: UInt32) case `returnCall`(functionIndex: UInt32) case `returnCallIndirect`(typeIndex: UInt32, tableIndex: UInt32) + case `callRef`(typeIndex: UInt32) + case `returnCallRef`(typeIndex: UInt32) case `drop` case `select` case `typedSelect`(type: ValueType) @@ -205,9 +207,12 @@ public enum Instruction: Equatable { case `i64Const`(value: Int64) case `f32Const`(value: IEEE754.Float32) case `f64Const`(value: IEEE754.Float64) - case `refNull`(type: ReferenceType) + case `refNull`(type: HeapType) case `refIsNull` case `refFunc`(functionIndex: UInt32) + case `refAsNonNull` + case `brOnNull`(relativeDepth: UInt32) + case `brOnNonNull`(relativeDepth: UInt32) case `i32Eqz` case `cmp`(Instruction.Cmp) case `i64Eqz` @@ -250,6 +255,8 @@ extension AnyInstructionVisitor { public mutating func visitCallIndirect(typeIndex: UInt32, tableIndex: UInt32) throws { return try self.visit(.callIndirect(typeIndex: typeIndex, tableIndex: tableIndex)) } public mutating func visitReturnCall(functionIndex: UInt32) throws { return try self.visit(.returnCall(functionIndex: functionIndex)) } public mutating func visitReturnCallIndirect(typeIndex: UInt32, tableIndex: UInt32) throws { return try self.visit(.returnCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex)) } + public mutating func visitCallRef(typeIndex: UInt32) throws { return try self.visit(.callRef(typeIndex: typeIndex)) } + public mutating func visitReturnCallRef(typeIndex: UInt32) throws { return try self.visit(.returnCallRef(typeIndex: typeIndex)) } public mutating func visitDrop() throws { return try self.visit(.drop) } public mutating func visitSelect() throws { return try self.visit(.select) } public mutating func visitTypedSelect(type: ValueType) throws { return try self.visit(.typedSelect(type: type)) } @@ -266,9 +273,12 @@ extension AnyInstructionVisitor { public mutating func visitI64Const(value: Int64) throws { return try self.visit(.i64Const(value: value)) } public mutating func visitF32Const(value: IEEE754.Float32) throws { return try self.visit(.f32Const(value: value)) } public mutating func visitF64Const(value: IEEE754.Float64) throws { return try self.visit(.f64Const(value: value)) } - public mutating func visitRefNull(type: ReferenceType) throws { return try self.visit(.refNull(type: type)) } + public mutating func visitRefNull(type: HeapType) throws { return try self.visit(.refNull(type: type)) } public mutating func visitRefIsNull() throws { return try self.visit(.refIsNull) } public mutating func visitRefFunc(functionIndex: UInt32) throws { return try self.visit(.refFunc(functionIndex: functionIndex)) } + public mutating func visitRefAsNonNull() throws { return try self.visit(.refAsNonNull) } + public mutating func visitBrOnNull(relativeDepth: UInt32) throws { return try self.visit(.brOnNull(relativeDepth: relativeDepth)) } + public mutating func visitBrOnNonNull(relativeDepth: UInt32) throws { return try self.visit(.brOnNonNull(relativeDepth: relativeDepth)) } public mutating func visitI32Eqz() throws { return try self.visit(.i32Eqz) } public mutating func visitCmp(_ cmp: Instruction.Cmp) throws { return try self.visit(.cmp(cmp)) } public mutating func visitI64Eqz() throws { return try self.visit(.i64Eqz) } @@ -324,6 +334,10 @@ public protocol InstructionVisitor { mutating func visitReturnCall(functionIndex: UInt32) throws /// Visiting `return_call_indirect` instruction. mutating func visitReturnCallIndirect(typeIndex: UInt32, tableIndex: UInt32) throws + /// Visiting `call_ref` instruction. + mutating func visitCallRef(typeIndex: UInt32) throws + /// Visiting `return_call_ref` instruction. + mutating func visitReturnCallRef(typeIndex: UInt32) throws /// Visiting `drop` instruction. mutating func visitDrop() throws /// Visiting `select` instruction. @@ -357,11 +371,17 @@ public protocol InstructionVisitor { /// Visiting `f64.const` instruction. mutating func visitF64Const(value: IEEE754.Float64) throws /// Visiting `ref.null` instruction. - mutating func visitRefNull(type: ReferenceType) throws + mutating func visitRefNull(type: HeapType) throws /// Visiting `ref.is_null` instruction. mutating func visitRefIsNull() throws /// Visiting `ref.func` instruction. mutating func visitRefFunc(functionIndex: UInt32) throws + /// Visiting `ref.as_non_null` instruction. + mutating func visitRefAsNonNull() throws + /// Visiting `br_on_null` instruction. + mutating func visitBrOnNull(relativeDepth: UInt32) throws + /// Visiting `br_on_non_null` instruction. + mutating func visitBrOnNonNull(relativeDepth: UInt32) throws /// Visiting `i32.eqz` instruction. mutating func visitI32Eqz() throws /// Visiting `cmp` category instruction. @@ -419,6 +439,8 @@ extension InstructionVisitor { case let .callIndirect(typeIndex, tableIndex): return try visitCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex) case let .returnCall(functionIndex): return try visitReturnCall(functionIndex: functionIndex) case let .returnCallIndirect(typeIndex, tableIndex): return try visitReturnCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex) + case let .callRef(typeIndex): return try visitCallRef(typeIndex: typeIndex) + case let .returnCallRef(typeIndex): return try visitReturnCallRef(typeIndex: typeIndex) case .drop: return try visitDrop() case .select: return try visitSelect() case let .typedSelect(type): return try visitTypedSelect(type: type) @@ -438,6 +460,9 @@ extension InstructionVisitor { case let .refNull(type): return try visitRefNull(type: type) case .refIsNull: return try visitRefIsNull() case let .refFunc(functionIndex): return try visitRefFunc(functionIndex: functionIndex) + case .refAsNonNull: return try visitRefAsNonNull() + case let .brOnNull(relativeDepth): return try visitBrOnNull(relativeDepth: relativeDepth) + case let .brOnNonNull(relativeDepth): return try visitBrOnNonNull(relativeDepth: relativeDepth) case .i32Eqz: return try visitI32Eqz() case let .cmp(cmp): return try visitCmp(cmp) case .i64Eqz: return try visitI64Eqz() @@ -477,6 +502,8 @@ extension InstructionVisitor { public mutating func visitCallIndirect(typeIndex: UInt32, tableIndex: UInt32) throws {} public mutating func visitReturnCall(functionIndex: UInt32) throws {} public mutating func visitReturnCallIndirect(typeIndex: UInt32, tableIndex: UInt32) throws {} + public mutating func visitCallRef(typeIndex: UInt32) throws {} + public mutating func visitReturnCallRef(typeIndex: UInt32) throws {} public mutating func visitDrop() throws {} public mutating func visitSelect() throws {} public mutating func visitTypedSelect(type: ValueType) throws {} @@ -493,9 +520,12 @@ extension InstructionVisitor { public mutating func visitI64Const(value: Int64) throws {} public mutating func visitF32Const(value: IEEE754.Float32) throws {} public mutating func visitF64Const(value: IEEE754.Float64) throws {} - public mutating func visitRefNull(type: ReferenceType) throws {} + public mutating func visitRefNull(type: HeapType) throws {} public mutating func visitRefIsNull() throws {} public mutating func visitRefFunc(functionIndex: UInt32) throws {} + public mutating func visitRefAsNonNull() throws {} + public mutating func visitBrOnNull(relativeDepth: UInt32) throws {} + public mutating func visitBrOnNonNull(relativeDepth: UInt32) throws {} public mutating func visitI32Eqz() throws {} public mutating func visitCmp(_ cmp: Instruction.Cmp) throws {} public mutating func visitI64Eqz() throws {} diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 2f8374b2..d8b84c89 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -256,6 +256,11 @@ extension WasmParserError.Message { Self("malformed section id: \(id)") } + @usableFromInline + static func malformedValueType(_ byte: UInt8) -> Self { + Self("malformed value type: \(byte)") + } + @usableFromInline static func zeroExpected(actual: UInt8) -> Self { Self("Zero expected but got \(actual)") } @@ -443,10 +448,46 @@ extension Parser { case 0x7D: return .f32 case 0x7C: return .f64 case 0x7B: return .f64 - case 0x70: return .ref(.funcRef) - case 0x6F: return .ref(.externRef) default: - throw StreamError.unexpected(b, index: offset, expected: Set(0x7C...0x7F)) + guard let refType = try parseReferenceType(byte: b) else { + throw makeError(.malformedValueType(b)) + } + return .ref(refType) + } + } + + /// - Returns: `nil` if the given `byte` discriminator is malformed + /// > Note: + /// + @usableFromInline + func parseReferenceType(byte: UInt8) throws -> ReferenceType? { + switch byte { + case 0x63: return try ReferenceType(isNullable: true, heapType: parseHeapType()) + case 0x64: return try ReferenceType(isNullable: false, heapType: parseHeapType()) + case 0x6F: return .externRef + case 0x70: return .funcRef + default: return nil // invalid discriminator + } + } + + /// > Note: + /// + @usableFromInline + func parseHeapType() throws -> HeapType { + let b = try stream.peek() + switch b { + case 0x6F: + _ = try stream.consumeAny() + return .externRef + case 0x70: + _ = try stream.consumeAny() + return .funcRef + default: + let rawIndex = try stream.parseVarSigned33() + guard let index = TypeIndex(exactly: rawIndex) else { + throw makeError(.invalidFunctionType(rawIndex)) + } + return .concrete(typeIndex: index) } } @@ -621,6 +662,11 @@ extension Parser: BinaryInstructionDecoder { return BrTable(labelIndices: labelIndices, defaultIndex: labelIndex) } @inlinable mutating func visitCall() throws -> UInt32 { try parseUnsigned() } + @inlinable mutating func visitCallRef() throws -> UInt32 { + // TODO reference types checks + // traps on nil + try parseUnsigned() + } @inlinable mutating func visitCallIndirect() throws -> (typeIndex: UInt32, tableIndex: UInt32) { let typeIndex: TypeIndex = try parseUnsigned() @@ -642,6 +688,10 @@ extension Parser: BinaryInstructionDecoder { return (typeIndex, tableIndex) } + @inlinable mutating func visitReturnCallRef() throws -> UInt32 { + return 0 + } + @inlinable mutating func visitTypedSelect() throws -> WasmTypes.ValueType { let results = try parseVector { try parseValueType() } guard results.count == 1 else { @@ -679,12 +729,14 @@ extension Parser: BinaryInstructionDecoder { let n = try parseDouble() return IEEE754.Float64(bitPattern: n) } - @inlinable mutating func visitRefNull() throws -> WasmTypes.ReferenceType { - let type = try parseValueType() - guard case let .ref(refType) = type else { - throw makeError(.expectedRefType(actual: type)) - } - return refType + @inlinable mutating func visitRefNull() throws -> WasmTypes.HeapType { + return try parseHeapType() + } + @inlinable mutating func visitBrOnNull() throws -> UInt32 { + return 0 + } + @inlinable mutating func visitBrOnNonNull() throws -> UInt32 { + return 0 } @inlinable mutating func visitRefFunc() throws -> UInt32 { try parseUnsigned() } diff --git a/Sources/WasmParser/WasmTypes.swift b/Sources/WasmParser/WasmTypes.swift index e58ae783..3f34a71b 100644 --- a/Sources/WasmParser/WasmTypes.swift +++ b/Sources/WasmParser/WasmTypes.swift @@ -49,10 +49,10 @@ public enum BlockType: Equatable { /// > Note: /// public struct Limits: Equatable { - public let min: UInt64 - public let max: UInt64? - public let isMemory64: Bool - public let shared: Bool + public var min: UInt64 + public var max: UInt64? + public var isMemory64: Bool + public var shared: Bool public init(min: UInt64, max: UInt64? = nil, isMemory64: Bool = false, shared: Bool = false) { self.min = min @@ -69,8 +69,8 @@ public typealias MemoryType = Limits /// > Note: /// public struct TableType: Equatable { - public let elementType: ReferenceType - public let limits: Limits + public var elementType: ReferenceType + public var limits: Limits public init(elementType: ReferenceType, limits: Limits) { self.elementType = elementType diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index e3e1e589..9bfe3fe9 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -14,12 +14,44 @@ public struct FunctionType: Equatable, Hashable { public let results: [ValueType] } +public enum AbstractHeapType: UInt8, Equatable, Hashable { + /// A reference to any kind of function. + case funcRef // -> to be renamed func + + /// An external host data. + case externRef // -> to be renamed extern +} + +public enum HeapType: Equatable, Hashable { + case abstract(AbstractHeapType) + case concrete(typeIndex: UInt32) + + public static var funcRef: HeapType { + return .abstract(.funcRef) + } + + public static var externRef: HeapType { + return .abstract(.externRef) + } +} + /// Reference types -public enum ReferenceType: UInt8, Equatable, Hashable { - /// A nullable reference type to a function. - case funcRef - /// A nullable external reference type. - case externRef +public struct ReferenceType: Equatable, Hashable { + public var isNullable: Bool + public var heapType: HeapType + + public static var funcRef: ReferenceType { + ReferenceType(isNullable: true, heapType: .funcRef) + } + + public static var externRef: ReferenceType { + ReferenceType(isNullable: true, heapType: .externRef) + } + + public init(isNullable: Bool, heapType: HeapType) { + self.isNullable = isNullable + self.heapType = heapType + } } public enum ValueType: Equatable, Hashable { diff --git a/Tests/WATTests/EncoderTests.swift b/Tests/WATTests/EncoderTests.swift index d5adb8ca..0f52f6df 100644 --- a/Tests/WATTests/EncoderTests.swift +++ b/Tests/WATTests/EncoderTests.swift @@ -11,6 +11,28 @@ class EncoderTests: XCTestCase { var failed: Set = [] } + private func checkMalformed(wast: URL, module: ModuleDirective, message: String, recordFail: () -> Void) { + let diagnostic = { + let (line, column) = module.location.computeLineAndColumn() + return "\(wast.path):\(line):\(column) should be malformed: \(message)" + } + switch module.source { + case .text(var wat): + XCTAssertThrowsError( + try { + _ = try wat.encode() + recordFail() + }(), diagnostic()) + case .quote(let bytes): + XCTAssertThrowsError( + try { + _ = try wat2wasm(String(decoding: bytes, as: UTF8.self)) + recordFail() + }(), diagnostic()) + case .binary: break + } + } + func checkWabtCompatibility( wast: URL, json: URL, stats parentStats: inout CompatibilityTestStats ) throws { @@ -35,25 +57,7 @@ class EncoderTests: XCTestCase { case .module(let moduleDirective): watModules.append(moduleDirective) case .assertMalformed(let module, let message): - let diagnostic = { - let (line, column) = module.location.computeLineAndColumn() - return "\(wast.path):\(line):\(column) should be malformed: \(message)" - } - switch module.source { - case .text(var wat): - XCTAssertThrowsError( - try { - _ = try wat.encode() - recordFail() - }(), diagnostic()) - case .quote(let bytes): - XCTAssertThrowsError( - try { - _ = try wat2wasm(String(decoding: bytes, as: UTF8.self)) - recordFail() - }(), diagnostic()) - case .binary: break - } + checkMalformed(wast: wast, module: module, message: message, recordFail: recordFail) default: break } } @@ -141,6 +145,40 @@ class EncoderTests: XCTestCase { #endif } + func smokeCheck(wastFile: URL) throws { + print("Checking \(wastFile.path)") + var parser = WastParser( + try String(contentsOf: wastFile), + features: Spectest.deriveFeatureSet(wast: wastFile) + ) + while let directive = try parser.nextDirective() { + switch directive { + case .module(let directive): + guard case var .text(wat) = directive.source else { + continue + } + _ = try wat.encode() + case .assertMalformed(let module, let message): + checkMalformed(wast: wastFile, module: module, message: message, recordFail: {}) + default: + break + } + } + } + + func testFunctionReferencesProposal() throws { + // NOTE: Perform smoke check for function-references proposal here without + // bit-to-bit compatibility check with wabt as wabt does not support + // function-references proposal yet. + for wastFile in Spectest.wastFiles( + path: [ + Spectest.testsuitePath.appendingPathComponent("proposals/function-references") + ], include: [], exclude: [] + ) { + try smokeCheck(wastFile: wastFile) + } + } + func testEncodeNameSection() throws { let bytes = try wat2wasm( """ diff --git a/Tests/WATTests/Spectest.swift b/Tests/WATTests/Spectest.swift index bc743ecf..1f113574 100644 --- a/Tests/WATTests/Spectest.swift +++ b/Tests/WATTests/Spectest.swift @@ -16,13 +16,19 @@ enum Spectest { testsuitePath.appendingPathComponent(file) } - static func wastFiles(include: [String] = [], exclude: [String] = ["annotations.wast"]) -> AnyIterator { - var allFiles = [ - testsuitePath, - testsuitePath.appendingPathComponent("proposals/memory64"), - testsuitePath.appendingPathComponent("proposals/tail-call"), - rootDirectory.appendingPathComponent("Tests/WasmKitTests/ExtraSuite"), - ].flatMap { + static let defaultSuites = [ + testsuitePath, + testsuitePath.appendingPathComponent("proposals/memory64"), + testsuitePath.appendingPathComponent("proposals/tail-call"), + rootDirectory.appendingPathComponent("Tests/WasmKitTests/ExtraSuite"), + ] + + static func wastFiles( + path: [URL] = defaultSuites, + include: [String] = [], + exclude: [String] = ["annotations.wast"] + ) -> AnyIterator { + var allFiles = path.flatMap { try! FileManager.default.contentsOfDirectory(at: $0, includingPropertiesForKeys: nil) }.makeIterator() diff --git a/Tests/WasmKitTests/Spectest/Spectest.swift b/Tests/WasmKitTests/Spectest/Spectest.swift index f42cd9ae..35c19e61 100644 --- a/Tests/WasmKitTests/Spectest/Spectest.swift +++ b/Tests/WasmKitTests/Spectest/Spectest.swift @@ -7,6 +7,46 @@ private func loadStringArrayFromEnvironment(_ key: String) -> [String] { ProcessInfo.processInfo.environment[key]?.split(separator: ",").map(String.init) ?? [] } +@available(macOS 11, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public struct SpectestResult { + var passed = 0 + var skipped = 0 + var failed = 0 + var total: Int { passed + skipped + failed } + var failedCases: Set = [] + + mutating func append(_ testCase: TestCase, _ result: Result) { + switch result { + case .passed: + passed += 1 + case .skipped: + skipped += 1 + case .failed: + failed += 1 + failedCases.insert(testCase.path) + } + } + + func percentage(_ numerator: Int, _ denominator: Int) -> String { + "\(Int(Double(numerator) / Double(denominator) * 100))%" + } + + func sortedFailedCases() -> [String] { + failedCases.map { URL(fileURLWithPath: $0).pathComponents.suffix(2).joined(separator: "/") }.sorted() + } + + func dump() { + print( + "\(passed)/\(total) (\(percentage(passed, total)) passing, \(percentage(skipped, total)) skipped, \(percentage(failed, total)) failed)" + ) + guard !failedCases.isEmpty else { return } + print("Failed cases:") + for testCase in failedCases.sorted() { + print(" \(testCase)") + } + } +} + @available(macOS 11, iOS 14.0, watchOS 7.0, tvOS 14.0, *) public func spectest( path: [String], @@ -16,6 +56,18 @@ public func spectest( parallel: Bool = true, configuration: EngineConfiguration = .init() ) async throws -> Bool { + try await spectestResult(path: path, include: include, exclude: exclude, verbose: verbose, parallel: parallel, configuration: configuration).failed == 0 +} + +@available(macOS 11, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func spectestResult( + path: [String], + include: [String]? = nil, + exclude: [String]? = nil, + verbose: Bool = false, + parallel: Bool = true, + configuration: EngineConfiguration = .init() +) async throws -> SpectestResult { let printVerbose = verbose @Sendable func log(_ message: String, verbose: Bool = false) { if !verbose || printVerbose { @@ -28,10 +80,6 @@ public func spectest( fputs("\(path):\(line): " + message + "\n", stderr) } } - func percentage(_ numerator: Int, _ denominator: Int) -> String { - "\(Int(Double(numerator) / Double(denominator) * 100))%" - } - let include = include ?? loadStringArrayFromEnvironment("WASMKIT_SPECTEST_INCLUDE") let exclude = exclude ?? loadStringArrayFromEnvironment("WASMKIT_SPECTEST_EXCLUDE") @@ -44,7 +92,7 @@ public func spectest( guard !testCases.isEmpty else { log("No test found") - return true + return SpectestResult() } // https://github.com/WebAssembly/spec/tree/8a352708cffeb71206ca49a0f743bdc57269fb1a/interpreter#spectest-host-module @@ -103,37 +151,6 @@ public func spectest( return testCaseResults } - struct SpectestResult { - var passed = 0 - var skipped = 0 - var failed = 0 - var total: Int { passed + skipped + failed } - var failedCases: Set = [] - - mutating func append(_ testCase: TestCase, _ result: Result) { - switch result { - case .passed: - passed += 1 - case .skipped: - skipped += 1 - case .failed: - failed += 1 - failedCases.insert(testCase.path) - } - } - - func dump() { - print( - "\(passed)/\(total) (\(percentage(passed, total)) passing, \(percentage(skipped, total)) skipped, \(percentage(failed, total)) failed)" - ) - guard !failedCases.isEmpty else { return } - print("Failed cases:") - for testCase in failedCases.sorted() { - print(" \(testCase)") - } - } - } - let result: SpectestResult if parallel { @@ -168,5 +185,5 @@ public func spectest( result.dump() - return result.failed == 0 + return result } diff --git a/Tests/WasmKitTests/Spectest/TestCase.swift b/Tests/WasmKitTests/Spectest/TestCase.swift index 2fb1809e..b49703ee 100644 --- a/Tests/WasmKitTests/Spectest/TestCase.swift +++ b/Tests/WasmKitTests/Spectest/TestCase.swift @@ -268,8 +268,7 @@ extension WastRunContext { } return .passed - case .assertReturn(let execute, let results): - let expected = parseValues(args: results) + case .assertReturn(let execute, let expected): let actual = try wastExecute(execute: execute) guard actual.isTestEquivalent(to: expected) else { return .failed("invoke result mismatch: expected: \(expected), actual: \(actual)") @@ -346,7 +345,24 @@ extension WastRunContext { guard let function = instance.exportedFunction(name: call.name) else { throw SpectestError("function \(call.name) not exported") } - return try function.invoke(call.args) + let args = try call.args.map { arg -> Value in + switch arg { + case .i32(let value): return .i32(value) + case .i64(let value): return .i64(value) + case .f32(let value): return .f32(value) + case .f64(let value): return .f64(value) + case .refNull(let heapType): + switch heapType { + case .abstract(.funcRef): return .ref(.function(nil)) + case .abstract(.externRef): return .ref(.extern(nil)) + case .concrete: + throw SpectestError("concrete ref.null is not supported yet") + } + case .refExtern(let value): return .ref(.extern(Int(value))) + case .refFunc(let value): return .ref(.function(Int(value))) + } + } + return try function.invoke(args) } private func deriveFeatureSet(rootPath: FilePath) -> WasmFeatureSet { @@ -380,20 +396,10 @@ extension WastRunContext { let module = try parseWasm(bytes: binary, features: deriveFeatureSet(rootPath: rootPath)) return module } - - private func parseValues(args: [WastExpectValue]) -> [WasmKit.Value] { - return args.compactMap { - switch $0 { - case .value(let value): return value - case .f32CanonicalNaN, .f32ArithmeticNaN: return .f32(Float.nan.bitPattern) - case .f64CanonicalNaN, .f64ArithmeticNaN: return .f64(Double.nan.bitPattern) - } - } - } } extension Value { - func isTestEquivalent(to value: Self) -> Bool { + func isTestEquivalent(to value: WastExpectValue) -> Bool { switch (self, value) { case let (.i32(lhs), .i32(rhs)): return lhs == rhs @@ -407,10 +413,19 @@ extension Value { let lhs = Float64(bitPattern: lhs) let rhs = Float64(bitPattern: rhs) return lhs.isNaN && rhs.isNaN || lhs == rhs - case let (.ref(.extern(lhs)), .ref(.extern(rhs))): - return lhs == rhs - case let (.ref(.function(lhs)), .ref(.function(rhs))): - return lhs == rhs + case let (.f64(lhs), .f64ArithmeticNaN), + let (.f64(lhs), .f64CanonicalNaN): + return Float64(bitPattern: lhs).isNaN + case let (.f32(lhs), .f32ArithmeticNaN), + let (.f32(lhs), .f32CanonicalNaN): + return Float32(bitPattern: lhs).isNaN + case let (.ref(.extern(lhs?)), .refExtern(rhs)): + return rhs.map { lhs == $0 } ?? true + case let (.ref(.function(lhs?)), .refFunc(rhs)): + return rhs.map { lhs == $0 } ?? true + case (.ref(.extern(nil)), .refNull(.abstract(.externRef))), + (.ref(.function(nil)), .refNull(.abstract(.funcRef))): + return true default: return false } @@ -418,7 +433,7 @@ extension Value { } extension Array where Element == Value { - func isTestEquivalent(to arrayOfValues: Self) -> Bool { + func isTestEquivalent(to arrayOfValues: [WastExpectValue]) -> Bool { guard count == arrayOfValues.count else { return false } diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 054d50dc..06c5d2a7 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -17,6 +17,9 @@ final class SpectestTests: XCTestCase { ] } + static var functionReferences: [String] { [Self.testsuite.appendingPathComponent("proposals/function-references").path] } + static var gcPath: [String] { [Self.testsuite.appendingPathComponent("proposals/gc").path] } + /// Run all the tests in the spectest suite. func testRunAll() async throws { let defaultConfig = EngineConfiguration() @@ -43,4 +46,75 @@ final class SpectestTests: XCTestCase { ) XCTAssertTrue(ok) } + + func testFunctionReferencesProposals() async throws { + let defaultConfig = EngineConfiguration() + let result = try await spectestResult( + path: Self.functionReferences, + include: ["function-references/call_ref.wast"], // focusing on call_ref for now, but will update to run all function-references tests. + exclude: [], + parallel: false, + configuration: defaultConfig + ) + + XCTAssertEqual(result.passed, 7) + XCTAssertEqual(result.failed, 27) + } + + /// Run the garbage collection proposal tests + /// As we add support, we can increase the passed count and delete entries from the failed array. + func testFunctionReferencesAndGarbageCollectionProposals() async throws { + let defaultConfig = EngineConfiguration() + let result = try await spectestResult( + path: Self.gcPath, + include: [], + exclude: [], + parallel: true, + configuration: defaultConfig + ) + + XCTAssertEqual(result.passed, 1552) + XCTAssertEqual(result.failed, 368) + XCTAssertEqual( + result.sortedFailedCases(), + [ + "gc/array.wast", + "gc/array_copy.wast", + "gc/array_fill.wast", + "gc/array_init_data.wast", + "gc/array_init_elem.wast", + "gc/br_on_cast.wast", + "gc/br_on_cast_fail.wast", + "gc/br_on_non_null.wast", + "gc/br_on_null.wast", + "gc/br_table.wast", + "gc/call_ref.wast", + "gc/data.wast", + "gc/elem.wast", + "gc/extern.wast", + "gc/func.wast", + "gc/global.wast", + "gc/i31.wast", + "gc/linking.wast", + "gc/local_init.wast", + "gc/ref.wast", + "gc/ref_as_non_null.wast", + "gc/ref_cast.wast", + "gc/ref_eq.wast", + "gc/ref_is_null.wast", + "gc/ref_null.wast", + "gc/ref_test.wast", + "gc/return_call_ref.wast", + "gc/struct.wast", + "gc/table-sub.wast", + "gc/table.wast", + "gc/type-canon.wast", + "gc/type-equivalence.wast", + "gc/type-rec.wast", + "gc/type-subtyping.wast", + "gc/unreached-invalid.wast", + "gc/unreached-valid.wast", + ] + ) + } } diff --git a/Tests/WasmParserTests/ParserTests.swift b/Tests/WasmParserTests/ParserTests.swift new file mode 100644 index 00000000..ebd6dd90 --- /dev/null +++ b/Tests/WasmParserTests/ParserTests.swift @@ -0,0 +1,76 @@ +import XCTest +import WasmParser +import WAT + +final class ParserTests: XCTestCase { + func parseAll(bytes: [UInt8]) throws { + var parser = Parser(bytes: bytes) + struct NopVisitor: InstructionVisitor {} + while let payload = try parser.parseNext() { + switch payload { + case .codeSection(let section): + for code in section { + var visitor = NopVisitor() + try code.parseExpression(visitor: &visitor) + } + default: break + } + } + } + func smokeCheck(wastFile: URL) throws { + print("Checking \(wastFile.path)") + var parser = try parseWAST(String(contentsOf: wastFile)) + while let (directive, location) = try parser.nextDirective() { + switch directive { + case .module(let directive): + guard case var .text(wat) = directive.source else { + continue + } + let diagnostic = { + let (line, column) = location.computeLineAndColumn() + return "\(wastFile.path):\(line):\(column) should be parsed" + } + let bytes = try wat.encode() + XCTAssertNoThrow(try parseAll(bytes: bytes), diagnostic()) + case .assertMalformed(let module, let message): + guard case let .binary(bytes) = module.source else { + continue + } + let diagnostic = { + let (line, column) = module.location.computeLineAndColumn() + return "\(wastFile.path):\(line):\(column) should be malformed: \(message)" + } + XCTAssertThrowsError(try parseAll(bytes: bytes), diagnostic()) + default: + break + } + } + } + + static let rootDirectory = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // WATTests + .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // Root + static let vendorDirectory: URL = + rootDirectory + .appendingPathComponent("Vendor") + + static var testsuitePath: URL { Self.vendorDirectory.appendingPathComponent("testsuite") } + + static func wastFiles(path: [String]) -> [URL] { + path.flatMap { + try! FileManager.default.contentsOfDirectory( + at: Self.testsuitePath.appendingPathComponent($0), + includingPropertiesForKeys: nil + ) + } + .filter { $0.pathExtension == "wast" } + } + + func testFunctionReferencesProposal() throws { + for wastFile in Self.wastFiles(path: ["proposals/function-references"]) { + guard wastFile.pathExtension == "wast" else { continue } + try smokeCheck(wastFile: wastFile) + } + } +} diff --git a/Utilities/Instructions.json b/Utilities/Instructions.json index 953a4073..142f5730 100644 --- a/Utilities/Instructions.json +++ b/Utilities/Instructions.json @@ -14,6 +14,8 @@ ["mvp" , "call_indirect" , ["0x11"] , [["typeIndex", "UInt32"], ["tableIndex", "UInt32"]], null ], ["tailCall" , "return_call" , ["0x12"] , [["functionIndex", "UInt32"]] , null ], ["tailCall" , "return_call_indirect" , ["0x13"] , [["typeIndex", "UInt32"], ["tableIndex", "UInt32"]], null ], + ["functionReferences" , "call_ref" , ["0x14"] , [["typeIndex", "UInt32"]] , null ], + ["functionReferences" , "return_call_ref" , ["0x15"] , [["typeIndex", "UInt32"]] , null ], ["mvp" , "drop" , ["0x1A"] , [] , null ], ["mvp" , "select" , ["0x1B"] , [] , null ], ["referenceTypes" , {"enumCase": "typedSelect"}, ["0x1C"] , [["type", "ValueType"]] , null ], @@ -51,9 +53,12 @@ ["mvp" , "i64.const" , ["0x42"] , [["value", "Int64"]] , null ], ["mvp" , "f32.const" , ["0x43"] , [["value", "IEEE754.Float32"]] , null ], ["mvp" , "f64.const" , ["0x44"] , [["value", "IEEE754.Float64"]] , null ], - ["referenceTypes" , "ref.null" , ["0xD0"] , [["type", "ReferenceType"]] , null ], + ["referenceTypes" , "ref.null" , ["0xD0"] , [["type", "HeapType"]] , null ], ["referenceTypes" , "ref.is_null" , ["0xD1"] , [] , null ], ["referenceTypes" , "ref.func" , ["0xD2"] , [["functionIndex", "UInt32"]] , null ], + ["functionReferences" , "ref.as_non_null" , ["0xD4"] , [] , null ], + ["functionReferences" , "br_on_null" , ["0xD5"] , [["relativeDepth", "UInt32"]] , null ], + ["functionReferences" , "br_on_non_null" , ["0xD6"] , [["relativeDepth", "UInt32"]] , null ], ["mvp" , "i32.eqz" , ["0x45"] , [] , null ], ["mvp" , "i32.eq" , ["0x46"] , [] , "cmp" ], ["mvp" , "i32.ne" , ["0x47"] , [] , "cmp" ],