From 7ff06cc702b2aacbe475bb506982eeba47e4cf32 Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Tue, 31 Dec 2024 18:51:58 +0000 Subject: [PATCH 01/18] GC: Run function-references & gc proposal tests. Setup test suite in preparation for adding support for these proposals. --- Tests/WasmKitTests/Spectest/Spectest.swift | 86 ++++++++++++--------- Tests/WasmKitTests/SpectestTests.swift | 88 ++++++++++++++++++++++ 2 files changed, 137 insertions(+), 37 deletions(-) diff --git a/Tests/WasmKitTests/Spectest/Spectest.swift b/Tests/WasmKitTests/Spectest/Spectest.swift index f42cd9ae..710804d9 100644 --- a/Tests/WasmKitTests/Spectest/Spectest.swift +++ b/Tests/WasmKitTests/Spectest/Spectest.swift @@ -7,6 +7,41 @@ private func loadStringArrayFromEnvironment(_ key: String) -> [String] { ProcessInfo.processInfo.environment[key]?.split(separator: ",").map(String.init) ?? [] } +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 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 +51,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 +75,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 +87,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 +146,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 +180,5 @@ public func spectest( result.dump() - return result.failed == 0 + return result } diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 054d50dc..3048ec0f 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -17,6 +17,13 @@ final class SpectestTests: XCTestCase { ] } + static var gcPath: [String] { + [ + Self.testsuite.appendingPathComponent("proposals/function-references").path, + Self.testsuite.appendingPathComponent("proposals/gc").path + ] + } + /// Run all the tests in the spectest suite. func testRunAll() async throws { let defaultConfig = EngineConfiguration() @@ -43,4 +50,85 @@ final class SpectestTests: XCTestCase { ) XCTAssertTrue(ok) } + + /// Run the function-references & 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 + ) + + let failed = result.failedCases.map { + URL(filePath: $0).pathComponents.suffix(2).joined(separator: "/") + }.sorted() + + XCTAssertEqual(result.passed, 1975) + XCTAssertEqual(failed, [ + "function-references/br_on_non_null.wast", + "function-references/br_on_null.wast", + "function-references/br_table.wast", + "function-references/call_ref.wast", + "function-references/elem.wast", + "function-references/func.wast", + "function-references/linking.wast", + "function-references/local_init.wast", + "function-references/ref.wast", + "function-references/ref_as_non_null.wast", + "function-references/ref_is_null.wast", + "function-references/ref_null.wast", + "function-references/return_call.wast", + "function-references/return_call_indirect.wast", + "function-references/return_call_ref.wast", + "function-references/select.wast", + "function-references/table-sub.wast", + "function-references/table.wast", + "function-references/type-equivalence.wast", + "function-references/unreached-valid.wast", + "gc/array.wast", + "gc/array_copy.wast", + "gc/array_fill.wast", + "gc/array_init_data.wast", + "gc/array_init_elem.wast", + "gc/br_if.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/local_tee.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.wast", + "gc/return_call_indirect.wast", + "gc/return_call_ref.wast", + "gc/select.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-valid.wast" + ]) + } } From d502b706fa72074a50e295f0df6eda0853a9400d Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Fri, 3 Jan 2025 17:50:12 +0000 Subject: [PATCH 02/18] Function Reference: Add explicit nonNull references & call_ref instruction --- README.md | 2 + Sources/WAT/BinaryInstructionEncoder.swift | 4 + Sources/WAT/Encoder.swift | 2 + Sources/WAT/ParseTextInstruction.swift | 3 + Sources/WAT/Parser/ExpressionParser.swift | 8 +- Sources/WAT/Parser/WastParser.swift | 2 + Sources/WAT/Parser/WatParser.swift | 36 ++++- .../WasmKit/Execution/ConstEvaluation.swift | 4 +- Sources/WasmKit/Execution/Instances.swift | 4 + .../WasmKit/Execution/Instructions/Misc.swift | 4 +- Sources/WasmKit/Execution/UntypedValue.swift | 6 + Sources/WasmKit/Validator.swift | 4 + .../WasmParser/BinaryInstructionDecoder.swift | 5 + Sources/WasmParser/InstructionVisitor.swift | 6 + Sources/WasmParser/WasmParser.swift | 5 + Sources/WasmTypes/WasmTypes.swift | 7 + Tests/WasmKitTests/Spectest/Spectest.swift | 4 + Tests/WasmKitTests/SpectestTests.swift | 139 ++++++++---------- Utilities/Instructions.json | 3 +- 19 files changed, 165 insertions(+), 83 deletions(-) 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..f3c44b32 100644 --- a/Sources/WAT/BinaryInstructionEncoder.swift +++ b/Sources/WAT/BinaryInstructionEncoder.swift @@ -392,4 +392,8 @@ extension BinaryInstructionEncoder { try encodeInstruction([0xFC, 0x10]) try encodeImmediates(table: table) } + mutating func visitCallRef(functionIndex: UInt32) throws { + try encodeInstruction([0x14]) + try encodeImmediates(functionIndex: functionIndex) + } } diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 0f0829cc..3514b715 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -129,7 +129,9 @@ extension ReferenceType: WasmEncodable { func encode(to encoder: inout Encoder) { switch self { case .funcRef: encoder.output.append(0x70) + case .funcRefNonNull: encoder.output.append(0x71) case .externRef: encoder.output.append(0x6F) + case .externRefNonNull: encoder.output.append(0x6E) // Is this correct } } } diff --git a/Sources/WAT/ParseTextInstruction.swift b/Sources/WAT/ParseTextInstruction.swift index fb57a2e3..f75f6ee6 100644 --- a/Sources/WAT/ParseTextInstruction.swift +++ b/Sources/WAT/ParseTextInstruction.swift @@ -332,6 +332,9 @@ func parseTextInstruction(keyword: String, expressionPars case "i64.trunc_sat_f32_u": return { return try $0.visitConversion(.i64TruncSatF32U) } case "i64.trunc_sat_f64_s": return { return try $0.visitConversion(.i64TruncSatF64S) } case "i64.trunc_sat_f64_u": return { return try $0.visitConversion(.i64TruncSatF64U) } + case "call_ref": + let (functionIndex) = try expressionParser.visitCallRef(wat: &wat) + return { return try $0.visitCallRef(functionIndex: functionIndex) } default: return nil } } diff --git a/Sources/WAT/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index b832e1a4..1bcf65a9 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -362,7 +362,9 @@ struct ExpressionParser { } private mutating func refKind() throws -> ReferenceType { - if try parser.takeKeyword("func") { + if try parser.take(.id) { + return .funcRef // not sure about this. + } else if try parser.takeKeyword("func") { return .funcRef } else if try parser.takeKeyword("extern") { return .externRef @@ -439,6 +441,10 @@ 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 visitCallIndirect(wat: inout Wat) throws -> (typeIndex: UInt32, tableIndex: UInt32) { let tableIndex: UInt32 if let tableId = try parser.takeIndexOrId() { diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index cfc69fb1..00faacdd 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -67,7 +67,9 @@ struct WastParser { let value: Reference switch type { case .externRef: value = .extern(nil) + case .externRefNonNull: value = .function(nil) // non null case .funcRef: value = .function(nil) + case .funcRefNonNull: value = .function(nil) // non null } addValue(.ref(value)) } diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index 530494a4..6f0e14ac 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -606,6 +606,34 @@ struct WatParser { } mutating func valueType() throws -> ValueType { + if try parser.peek(.leftParen) != nil { + return try _referenceValueType() + } else { + return try _valueType() + } + } + + // must consume right paren + mutating func _referenceValueType() throws -> ValueType { + var isNullable = false + _ = try parser.takeParenBlockStart("ref") + if try parser.peekKeyword() == "null" { + _ = try parser.takeKeyword("null") + isNullable = true + } + + if try parser.takeId() != nil { + _ = try parser.take(.rightParen) + return .ref(refType(keyword: "func", isNullable: isNullable)!) + } + + let keyword = try parser.expectKeyword() + _ = try parser.take(.rightParen) + if let refType = refType(keyword: keyword, isNullable: isNullable) { return .ref(refType) } + throw WatParserError("unexpected value type \(keyword)", location: parser.lexer.location()) + } + + mutating func _valueType() throws -> ValueType { let keyword = try parser.expectKeyword() switch keyword { case "i32": return .i32 @@ -613,22 +641,24 @@ struct WatParser { case "f32": return .f32 case "f64": return .f64 default: - if let refType = refType(keyword: keyword) { return .ref(refType) } + if let refType = refType(keyword: keyword, isNullable: true) { return .ref(refType) } throw WatParserError("unexpected value type \(keyword)", location: parser.lexer.location()) } } - mutating func refType(keyword: String) -> ReferenceType? { + mutating func refType(keyword: String, isNullable: Bool) -> ReferenceType? { switch keyword { case "funcref": return .funcRef case "externref": return .externRef + case "func": return isNullable ? .funcRef : .funcRefNonNull + case "extern": return isNullable ? .externRef : .externRefNonNull default: return nil } } mutating func refType() throws -> ReferenceType { let keyword = try parser.expectKeyword() - guard let refType = refType(keyword: keyword) else { + guard let refType = refType(keyword: keyword, isNullable: true) else { throw WatParserError("unexpected ref type \(keyword)", location: parser.lexer.location()) } return refType diff --git a/Sources/WasmKit/Execution/ConstEvaluation.swift b/Sources/WasmKit/Execution/ConstEvaluation.swift index 3f155e6b..293f1f26 100644 --- a/Sources/WasmKit/Execution/ConstEvaluation.swift +++ b/Sources/WasmKit/Execution/ConstEvaluation.swift @@ -62,8 +62,8 @@ extension ConstExpression { return try context.globalValue(globalIndex) case .refNull(let type): switch type { - case .externRef: return .ref(.extern(nil)) - case .funcRef: return .ref(.function(nil)) + case .externRef, .externRefNonNull: return .ref(.extern(nil)) + case .funcRef, .funcRefNonNull: return .ref(.function(nil)) } case .refFunc(let functionIndex): return try .ref(context.functionRef(functionIndex)) diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 27a2452a..67c8365d 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -277,8 +277,12 @@ struct TableEntity /* : ~Copyable */ { switch tableType.elementType { case .funcRef: emptyElement = .function(nil) + case .funcRefNonNull: + emptyElement = .function(nil) // shouldn't be null case .externRef: emptyElement = .extern(nil) + case .externRefNonNull: + emptyElement = .extern(nil) // shouldn't be null } let numberOfElements = Int(tableType.limits.min) diff --git a/Sources/WasmKit/Execution/Instructions/Misc.swift b/Sources/WasmKit/Execution/Instructions/Misc.swift index 31386545..85ad36c6 100644 --- a/Sources/WasmKit/Execution/Instructions/Misc.swift +++ b/Sources/WasmKit/Execution/Instructions/Misc.swift @@ -22,9 +22,9 @@ extension Execution { mutating func refNull(sp: Sp, immediate: Instruction.RefNullOperand) { let value: Value switch immediate.type { - case .externRef: + case .externRef, .externRefNonNull: value = .ref(.extern(nil)) - case .funcRef: + case .funcRef, .funcRefNonNull: value = .ref(.function(nil)) } sp[immediate.result] = UntypedValue(value) diff --git a/Sources/WasmKit/Execution/UntypedValue.swift b/Sources/WasmKit/Execution/UntypedValue.swift index 57e80114..06296165 100644 --- a/Sources/WasmKit/Execution/UntypedValue.swift +++ b/Sources/WasmKit/Execution/UntypedValue.swift @@ -117,8 +117,14 @@ struct UntypedValue: Equatable, Hashable { switch type { case .funcRef: return .function(decodeOptionalInt()) + case .funcRefNonNull: + // can skip optional check + return .function(decodeOptionalInt()) case .externRef: return .extern(decodeOptionalInt()) + case .externRefNonNull: + // can skip optional check + return .extern(decodeOptionalInt()) } } diff --git a/Sources/WasmKit/Validator.swift b/Sources/WasmKit/Validator.swift index a313901f..2f212a6f 100644 --- a/Sources/WasmKit/Validator.swift +++ b/Sources/WasmKit/Validator.swift @@ -333,9 +333,13 @@ struct ModuleValidator { extension WasmTypes.Reference { /// Checks if the reference type matches the expected type. func checkType(_ type: WasmTypes.ReferenceType) throws { + + // Should we validate nonNull variants have associated values present? switch (self, type) { case (.function, .funcRef): return + case (.function, .funcRefNonNull): return case (.extern, .externRef): return + case (.extern, .externRefNonNull): return default: throw ValidationError(.expectTypeButGot(expected: "\(type)", got: "\(self)")) } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index a89443c2..b61be947 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -86,6 +86,8 @@ protocol BinaryInstructionDecoder { @inlinable mutating func visitTableGrow() throws -> UInt32 /// Decode `table.size` immediates @inlinable mutating func visitTableSize() throws -> UInt32 + /// Decode `call_ref` immediates + @inlinable mutating func visitCallRef() throws -> UInt32 } @inlinable func parseBinaryInstruction(visitor: inout V, decoder: inout D) throws -> Bool { @@ -132,6 +134,9 @@ func parseBinaryInstruction( case 0x13: let (typeIndex, tableIndex) = try decoder.visitReturnCallIndirect() try visitor.visitReturnCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex) + case 0x14: + let (functionIndex) = try decoder.visitCallRef() + try visitor.visitCallRef(functionIndex: functionIndex) case 0x1A: try visitor.visitDrop() case 0x1B: diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 230ba132..09860e26 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -226,6 +226,7 @@ public enum Instruction: Equatable { case `tableSet`(table: UInt32) case `tableGrow`(table: UInt32) case `tableSize`(table: UInt32) + case `callRef`(functionIndex: UInt32) } /// A visitor that visits all instructions by a single visit method. @@ -287,6 +288,7 @@ extension AnyInstructionVisitor { public mutating func visitTableSet(table: UInt32) throws { return try self.visit(.tableSet(table: table)) } public mutating func visitTableGrow(table: UInt32) throws { return try self.visit(.tableGrow(table: table)) } public mutating func visitTableSize(table: UInt32) throws { return try self.visit(.tableSize(table: table)) } + public mutating func visitCallRef(functionIndex: UInt32) throws { return try self.visit(.callRef(functionIndex: functionIndex)) } } /// A visitor for WebAssembly instructions. @@ -398,6 +400,8 @@ public protocol InstructionVisitor { mutating func visitTableGrow(table: UInt32) throws /// Visiting `table.size` instruction. mutating func visitTableSize(table: UInt32) throws + /// Visiting `call_ref` instruction. + mutating func visitCallRef(functionIndex: UInt32) throws } extension InstructionVisitor { @@ -456,6 +460,7 @@ extension InstructionVisitor { case let .tableSet(table): return try visitTableSet(table: table) case let .tableGrow(table): return try visitTableGrow(table: table) case let .tableSize(table): return try visitTableSize(table: table) + case let .callRef(functionIndex): return try visitCallRef(functionIndex: functionIndex) } } } @@ -514,5 +519,6 @@ extension InstructionVisitor { public mutating func visitTableSet(table: UInt32) throws {} public mutating func visitTableGrow(table: UInt32) throws {} public mutating func visitTableSize(table: UInt32) throws {} + public mutating func visitCallRef(functionIndex: UInt32) throws {} } diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 2f8374b2..b07b349b 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -444,6 +444,7 @@ extension Parser { case 0x7C: return .f64 case 0x7B: return .f64 case 0x70: return .ref(.funcRef) + case 0x71: return .ref(.funcRefNonNull) case 0x6F: return .ref(.externRef) default: throw StreamError.unexpected(b, index: offset, expected: Set(0x7C...0x7F)) @@ -621,6 +622,10 @@ 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 + try parseUnsigned() + } @inlinable mutating func visitCallIndirect() throws -> (typeIndex: UInt32, tableIndex: UInt32) { let typeIndex: TypeIndex = try parseUnsigned() diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index e3e1e589..ebad0634 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -18,8 +18,15 @@ public struct FunctionType: Equatable, Hashable { public enum ReferenceType: UInt8, Equatable, Hashable { /// A nullable reference type to a function. case funcRef + + /// A non-nullable reference type to a function + case funcRefNonNull + /// A nullable external reference type. case externRef + + /// A non-nullable external reference type. + case externRefNonNull } public enum ValueType: Equatable, Hashable { diff --git a/Tests/WasmKitTests/Spectest/Spectest.swift b/Tests/WasmKitTests/Spectest/Spectest.swift index 710804d9..685088f9 100644 --- a/Tests/WasmKitTests/Spectest/Spectest.swift +++ b/Tests/WasmKitTests/Spectest/Spectest.swift @@ -30,6 +30,10 @@ public struct SpectestResult { "\(Int(Double(numerator) / Double(denominator) * 100))%" } + func sortedFailedCases() -> [String] { + failedCases.map { URL(filePath: $0).pathComponents.suffix(2).joined(separator: "/") }.sorted() + } + func dump() { print( "\(passed)/\(total) (\(percentage(passed, total)) passing, \(percentage(skipped, total)) skipped, \(percentage(failed, total)) failed)" diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 3048ec0f..78b5cb2a 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -17,12 +17,8 @@ final class SpectestTests: XCTestCase { ] } - static var gcPath: [String] { - [ - Self.testsuite.appendingPathComponent("proposals/function-references").path, - Self.testsuite.appendingPathComponent("proposals/gc").path - ] - } + 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 { @@ -51,7 +47,21 @@ final class SpectestTests: XCTestCase { XCTAssertTrue(ok) } - /// Run the function-references & garbage collection proposal tests + 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, 8) + XCTAssertEqual(result.failed, 26) + } + + /// 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() @@ -63,72 +73,53 @@ final class SpectestTests: XCTestCase { configuration: defaultConfig ) - let failed = result.failedCases.map { - URL(filePath: $0).pathComponents.suffix(2).joined(separator: "/") - }.sorted() - - XCTAssertEqual(result.passed, 1975) - XCTAssertEqual(failed, [ - "function-references/br_on_non_null.wast", - "function-references/br_on_null.wast", - "function-references/br_table.wast", - "function-references/call_ref.wast", - "function-references/elem.wast", - "function-references/func.wast", - "function-references/linking.wast", - "function-references/local_init.wast", - "function-references/ref.wast", - "function-references/ref_as_non_null.wast", - "function-references/ref_is_null.wast", - "function-references/ref_null.wast", - "function-references/return_call.wast", - "function-references/return_call_indirect.wast", - "function-references/return_call_ref.wast", - "function-references/select.wast", - "function-references/table-sub.wast", - "function-references/table.wast", - "function-references/type-equivalence.wast", - "function-references/unreached-valid.wast", - "gc/array.wast", - "gc/array_copy.wast", - "gc/array_fill.wast", - "gc/array_init_data.wast", - "gc/array_init_elem.wast", - "gc/br_if.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/local_tee.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.wast", - "gc/return_call_indirect.wast", - "gc/return_call_ref.wast", - "gc/select.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-valid.wast" - ]) + XCTAssertEqual(result.passed, 1316) + XCTAssertEqual(result.failed, 218) + 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_if.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/local_tee.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.wast", + "gc/return_call_indirect.wast", + "gc/return_call_ref.wast", + "gc/select.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/Utilities/Instructions.json b/Utilities/Instructions.json index 953a4073..d890d6ae 100644 --- a/Utilities/Instructions.json +++ b/Utilities/Instructions.json @@ -201,5 +201,6 @@ ["saturatingFloatToInt", "i64.trunc_sat_f32_s" , ["0xFC", "0x04"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f32_u" , ["0xFC", "0x05"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f64_s" , ["0xFC", "0x06"], [] , "conversion"], - ["saturatingFloatToInt", "i64.trunc_sat_f64_u" , ["0xFC", "0x07"], [] , "conversion"] + ["saturatingFloatToInt", "i64.trunc_sat_f64_u" , ["0xFC", "0x07"], [] , "conversion"], + ["referenceTypes" , "call_ref" , ["0x14"] , [["functionIndex", "UInt32"]] , null ] ] From f1e14da4caae38a7cda21653623160786aba5a85 Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 11:40:51 +0000 Subject: [PATCH 03/18] Migrate to Heap Type with some failing tests --- Sources/WAT/Encoder.swift | 6 ++-- Sources/WAT/Parser/WastParser.swift | 4 +-- Sources/WAT/Parser/WatParser.swift | 4 +-- .../WasmKit/Execution/ConstEvaluation.swift | 6 ++-- Sources/WasmKit/Execution/Instances.swift | 6 +--- .../Instructions/InstructionSupport.swift | 4 +-- .../WasmKit/Execution/Instructions/Misc.swift | 6 ++-- Sources/WasmKit/Execution/UntypedValue.swift | 8 ++--- Sources/WasmKit/Execution/Value.swift | 2 +- Sources/WasmKit/Validator.swift | 2 -- Sources/WasmParser/WasmParser.swift | 2 +- Sources/WasmTypes/WasmTypes.swift | 30 +++++++++++++------ Tests/WasmKitTests/SpectestTests.swift | 4 +-- 13 files changed, 43 insertions(+), 41 deletions(-) diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 3514b715..bd0ec74a 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -127,11 +127,9 @@ extension ValueType: WasmEncodable { extension ReferenceType: WasmEncodable { func encode(to encoder: inout Encoder) { - switch self { - case .funcRef: encoder.output.append(0x70) - case .funcRefNonNull: encoder.output.append(0x71) + switch self.heapType { + case .funcRef: encoder.output.append(isNullable ? 0x70 : 0x71) case .externRef: encoder.output.append(0x6F) - case .externRefNonNull: encoder.output.append(0x6E) // Is this correct } } } diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index 00faacdd..1734f80f 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -63,13 +63,11 @@ struct WastParser { mutating func visitRefFunc(functionIndex: UInt32) throws { addValue(.ref(.function(FunctionAddress(functionIndex)))) } - mutating func visitRefNull(type: ReferenceType) throws { + mutating func visitRefNull(type: HeapType) throws { let value: Reference switch type { case .externRef: value = .extern(nil) - case .externRefNonNull: value = .function(nil) // non null case .funcRef: value = .function(nil) - case .funcRefNonNull: value = .function(nil) // non null } addValue(.ref(value)) } diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index 6f0e14ac..a5b5cab1 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -650,8 +650,8 @@ struct WatParser { switch keyword { case "funcref": return .funcRef case "externref": return .externRef - case "func": return isNullable ? .funcRef : .funcRefNonNull - case "extern": return isNullable ? .externRef : .externRefNonNull + case "func": return ReferenceType(isNullable: isNullable, heapType: .funcRef) + case "extern": return ReferenceType(isNullable: isNullable, heapType: .funcRef) default: return nil } } diff --git a/Sources/WasmKit/Execution/ConstEvaluation.swift b/Sources/WasmKit/Execution/ConstEvaluation.swift index 293f1f26..5bd7cc80 100644 --- a/Sources/WasmKit/Execution/ConstEvaluation.swift +++ b/Sources/WasmKit/Execution/ConstEvaluation.swift @@ -62,8 +62,10 @@ extension ConstExpression { return try context.globalValue(globalIndex) case .refNull(let type): switch type { - case .externRef, .externRefNonNull: return .ref(.extern(nil)) - case .funcRef, .funcRefNonNull: return .ref(.function(nil)) + 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/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 67c8365d..7054337d 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -274,15 +274,11 @@ struct TableEntity /* : ~Copyable */ { init(_ tableType: TableType, resourceLimiter: any ResourceLimiter) throws { let emptyElement: Reference - switch tableType.elementType { + switch tableType.elementType.heapType { case .funcRef: emptyElement = .function(nil) - case .funcRefNonNull: - emptyElement = .function(nil) // shouldn't be null case .externRef: emptyElement = .extern(nil) - case .externRefNonNull: - emptyElement = .extern(nil) // shouldn't be null } let numberOfElements = Int(tableType.limits.min) diff --git a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift index 2888bee8..909742db 100644 --- a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift +++ b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift @@ -105,11 +105,11 @@ extension Int32: InstructionImmediate { extension Instruction.RefNullOperand { init(result: VReg, type: ReferenceType) { - self.init(result: result, rawType: type.rawValue) + self.init(result: result, rawType: 0) // need to figure out rawType here } var type: ReferenceType { - ReferenceType(rawValue: rawType).unsafelyUnwrapped + ReferenceType(isNullable: true, heapType: .funcRef) // is this still a valid conversion? } } diff --git a/Sources/WasmKit/Execution/Instructions/Misc.swift b/Sources/WasmKit/Execution/Instructions/Misc.swift index 85ad36c6..a3d101d6 100644 --- a/Sources/WasmKit/Execution/Instructions/Misc.swift +++ b/Sources/WasmKit/Execution/Instructions/Misc.swift @@ -22,10 +22,12 @@ extension Execution { mutating func refNull(sp: Sp, immediate: Instruction.RefNullOperand) { let value: Value switch immediate.type { - case .externRef, .externRefNonNull: + case .externRef: value = .ref(.extern(nil)) - case .funcRef, .funcRefNonNull: + case .funcRef: value = .ref(.function(nil)) + default: + fatalError() } sp[immediate.result] = UntypedValue(value) } diff --git a/Sources/WasmKit/Execution/UntypedValue.swift b/Sources/WasmKit/Execution/UntypedValue.swift index 06296165..0390628e 100644 --- a/Sources/WasmKit/Execution/UntypedValue.swift +++ b/Sources/WasmKit/Execution/UntypedValue.swift @@ -117,14 +117,10 @@ struct UntypedValue: Equatable, Hashable { switch type { case .funcRef: return .function(decodeOptionalInt()) - case .funcRefNonNull: - // can skip optional check - return .function(decodeOptionalInt()) case .externRef: return .extern(decodeOptionalInt()) - case .externRefNonNull: - // can skip optional check - return .extern(decodeOptionalInt()) + default: + fatalError() } } 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/Validator.swift b/Sources/WasmKit/Validator.swift index 2f212a6f..f35c3749 100644 --- a/Sources/WasmKit/Validator.swift +++ b/Sources/WasmKit/Validator.swift @@ -337,9 +337,7 @@ extension WasmTypes.Reference { // Should we validate nonNull variants have associated values present? switch (self, type) { case (.function, .funcRef): return - case (.function, .funcRefNonNull): return case (.extern, .externRef): return - case (.extern, .externRefNonNull): return default: throw ValidationError(.expectTypeButGot(expected: "\(type)", got: "\(self)")) } diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index b07b349b..9bd49a29 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -444,7 +444,7 @@ extension Parser { case 0x7C: return .f64 case 0x7B: return .f64 case 0x70: return .ref(.funcRef) - case 0x71: return .ref(.funcRefNonNull) + case 0x71: return .ref(.funcRef) case 0x6F: return .ref(.externRef) default: throw StreamError.unexpected(b, index: offset, expected: Set(0x7C...0x7F)) diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index ebad0634..82a31d1d 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -14,19 +14,31 @@ public struct FunctionType: Equatable, Hashable { public let results: [ValueType] } +public enum HeapType: 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 +} + /// Reference types -public enum ReferenceType: UInt8, Equatable, Hashable { - /// A nullable reference type to a function. - case funcRef +public struct ReferenceType: Equatable, Hashable { + public var isNullable: Bool + public var heapType: HeapType - /// A non-nullable reference type to a function - case funcRefNonNull + public static var funcRef: ReferenceType { + ReferenceType(isNullable: true, heapType: .funcRef) + } - /// A nullable external reference type. - case externRef + public static var externRef: ReferenceType { + ReferenceType(isNullable: true, heapType: .externRef) + } - /// A non-nullable external reference type. - case externRefNonNull + public init(isNullable: Bool, heapType: HeapType) { + self.isNullable = isNullable + self.heapType = heapType + } } public enum ValueType: Equatable, Hashable { diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 78b5cb2a..9f1cbb54 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -73,8 +73,8 @@ final class SpectestTests: XCTestCase { configuration: defaultConfig ) - XCTAssertEqual(result.passed, 1316) - XCTAssertEqual(result.failed, 218) + XCTAssertEqual(result.passed, 1313) + XCTAssertEqual(result.failed, 221) XCTAssertEqual( result.sortedFailedCases(), [ From 494e87822d35438da1f7e8b2e067cf83cb188ec0 Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 18:51:08 +0000 Subject: [PATCH 04/18] Fix for breaking ref.null --- Sources/WAT/Parser/WastParser.swift | 4 ++-- .../WasmKit/Execution/Instructions/InstructionSupport.swift | 4 ++-- Sources/WasmTypes/WasmTypes.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index 1734f80f..91df6d0b 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -63,9 +63,9 @@ struct WastParser { mutating func visitRefFunc(functionIndex: UInt32) throws { addValue(.ref(.function(FunctionAddress(functionIndex)))) } - mutating func visitRefNull(type: HeapType) throws { + mutating func visitRefNull(type: ReferenceType) throws { let value: Reference - switch type { + switch type.heapType { case .externRef: value = .extern(nil) case .funcRef: value = .function(nil) } diff --git a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift index 909742db..4f2c1b5c 100644 --- a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift +++ b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift @@ -105,11 +105,11 @@ extension Int32: InstructionImmediate { extension Instruction.RefNullOperand { init(result: VReg, type: ReferenceType) { - self.init(result: result, rawType: 0) // need to figure out rawType here + self.init(result: result, rawType: type.heapType.rawValue) // need to figure out rawType here } var type: ReferenceType { - ReferenceType(isNullable: true, heapType: .funcRef) // is this still a valid conversion? + ReferenceType(isNullable: true, heapType: HeapType(rawValue: rawType)!) // is this still a valid conversion? } } diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index 82a31d1d..7459122f 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -14,7 +14,7 @@ public struct FunctionType: Equatable, Hashable { public let results: [ValueType] } -public enum HeapType: Equatable, Hashable { +public enum HeapType: UInt8, Equatable, Hashable { /// A reference to any kind of function. case funcRef // -> to be renamed func From d2cdfbdc584b6d9eb6ef8b9b6f8de80480f23ff9 Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 18:54:59 +0000 Subject: [PATCH 05/18] Update test counts --- Tests/WasmKitTests/SpectestTests.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 9f1cbb54..a8ac4d0e 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -73,8 +73,8 @@ final class SpectestTests: XCTestCase { configuration: defaultConfig ) - XCTAssertEqual(result.passed, 1313) - XCTAssertEqual(result.failed, 221) + XCTAssertEqual(result.passed, 1400) + XCTAssertEqual(result.failed, 134) XCTAssertEqual( result.sortedFailedCases(), [ @@ -106,8 +106,6 @@ final class SpectestTests: XCTestCase { "gc/ref_is_null.wast", "gc/ref_null.wast", "gc/ref_test.wast", - "gc/return_call.wast", - "gc/return_call_indirect.wast", "gc/return_call_ref.wast", "gc/select.wast", "gc/struct.wast", From 264a526bc832c1fa6dd29596e85a10ab914205ce Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 19:00:22 +0000 Subject: [PATCH 06/18] Tidy up --- Sources/WasmKit/Execution/Instructions/Misc.swift | 4 +--- Sources/WasmKit/Execution/UntypedValue.swift | 4 +--- Sources/WasmKit/Validator.swift | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Sources/WasmKit/Execution/Instructions/Misc.swift b/Sources/WasmKit/Execution/Instructions/Misc.swift index a3d101d6..c3af975b 100644 --- a/Sources/WasmKit/Execution/Instructions/Misc.swift +++ b/Sources/WasmKit/Execution/Instructions/Misc.swift @@ -21,13 +21,11 @@ extension Execution { extension Execution { mutating func refNull(sp: Sp, immediate: Instruction.RefNullOperand) { let value: Value - switch immediate.type { + switch immediate.type.heapType { case .externRef: value = .ref(.extern(nil)) case .funcRef: value = .ref(.function(nil)) - default: - fatalError() } sp[immediate.result] = UntypedValue(value) } diff --git a/Sources/WasmKit/Execution/UntypedValue.swift b/Sources/WasmKit/Execution/UntypedValue.swift index 0390628e..1c6b0c32 100644 --- a/Sources/WasmKit/Execution/UntypedValue.swift +++ b/Sources/WasmKit/Execution/UntypedValue.swift @@ -114,13 +114,11 @@ struct UntypedValue: Equatable, Hashable { guard storage & Self.isNullMaskPattern == 0 else { return nil } return Int(storage) } - switch type { + switch type.heapType { case .funcRef: return .function(decodeOptionalInt()) case .externRef: return .extern(decodeOptionalInt()) - default: - fatalError() } } diff --git a/Sources/WasmKit/Validator.swift b/Sources/WasmKit/Validator.swift index f35c3749..a313901f 100644 --- a/Sources/WasmKit/Validator.swift +++ b/Sources/WasmKit/Validator.swift @@ -333,8 +333,6 @@ struct ModuleValidator { extension WasmTypes.Reference { /// Checks if the reference type matches the expected type. func checkType(_ type: WasmTypes.ReferenceType) throws { - - // Should we validate nonNull variants have associated values present? switch (self, type) { case (.function, .funcRef): return case (.extern, .externRef): return From eeda13208d25a82af1435f16005b8c6a419a8da4 Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 19:11:33 +0000 Subject: [PATCH 07/18] Add annotation --- Sources/WasmParser/WasmParser.swift | 1 + Tests/WasmKitTests/Spectest/Spectest.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 9bd49a29..6ffe0c36 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -624,6 +624,7 @@ extension Parser: BinaryInstructionDecoder { @inlinable mutating func visitCall() throws -> UInt32 { try parseUnsigned() } @inlinable mutating func visitCallRef() throws -> UInt32 { // TODO reference types checks + // traps on nil try parseUnsigned() } diff --git a/Tests/WasmKitTests/Spectest/Spectest.swift b/Tests/WasmKitTests/Spectest/Spectest.swift index 685088f9..7d5771e0 100644 --- a/Tests/WasmKitTests/Spectest/Spectest.swift +++ b/Tests/WasmKitTests/Spectest/Spectest.swift @@ -7,6 +7,7 @@ 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 From 09396e143fe619f5dccd346c35664ec4fb023905 Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 19:16:19 +0000 Subject: [PATCH 08/18] Fix for linux --- Tests/WasmKitTests/Spectest/Spectest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/WasmKitTests/Spectest/Spectest.swift b/Tests/WasmKitTests/Spectest/Spectest.swift index 7d5771e0..35c19e61 100644 --- a/Tests/WasmKitTests/Spectest/Spectest.swift +++ b/Tests/WasmKitTests/Spectest/Spectest.swift @@ -32,7 +32,7 @@ public struct SpectestResult { } func sortedFailedCases() -> [String] { - failedCases.map { URL(filePath: $0).pathComponents.suffix(2).joined(separator: "/") }.sorted() + failedCases.map { URL(fileURLWithPath: $0).pathComponents.suffix(2).joined(separator: "/") }.sorted() } func dump() { From 93721a38e647b4bbeb558d9ba81cb11c3b40d2fd Mon Sep 17 00:00:00 2001 From: Iain Smith Date: Sat, 4 Jan 2025 19:37:26 +0000 Subject: [PATCH 09/18] Add remaining instructions --- Sources/WAT/BinaryInstructionEncoder.swift | 13 ++++++++++ Sources/WAT/ParseTextInstruction.swift | 10 ++++++++ Sources/WAT/Parser/ExpressionParser.swift | 11 ++++++++- .../Instructions/InstructionSupport.swift | 4 ++-- .../WasmParser/BinaryInstructionDecoder.swift | 17 +++++++++++++ Sources/WasmParser/InstructionVisitor.swift | 24 +++++++++++++++++++ Sources/WasmParser/WasmParser.swift | 10 ++++++++ Sources/WasmTypes/WasmTypes.swift | 4 ++-- Tests/WasmKitTests/SpectestTests.swift | 4 ++-- Utilities/Instructions.json | 6 ++++- 10 files changed, 95 insertions(+), 8 deletions(-) diff --git a/Sources/WAT/BinaryInstructionEncoder.swift b/Sources/WAT/BinaryInstructionEncoder.swift index f3c44b32..e3516985 100644 --- a/Sources/WAT/BinaryInstructionEncoder.swift +++ b/Sources/WAT/BinaryInstructionEncoder.swift @@ -396,4 +396,17 @@ extension BinaryInstructionEncoder { try encodeInstruction([0x14]) try encodeImmediates(functionIndex: functionIndex) } + mutating func visitReturnCallRef(functionIndex: UInt32) throws { + try encodeInstruction([0x15]) + try encodeImmediates(functionIndex: functionIndex) + } + mutating func visitAsNonNull() throws { try encodeInstruction([0xD4]) } + mutating func visitBrOnNull(functionIndex: UInt32) throws { + try encodeInstruction([0xD5]) + try encodeImmediates(functionIndex: functionIndex) + } + mutating func visitBrOnNonNull(functionIndex: UInt32) throws { + try encodeInstruction([0xD6]) + try encodeImmediates(functionIndex: functionIndex) + } } diff --git a/Sources/WAT/ParseTextInstruction.swift b/Sources/WAT/ParseTextInstruction.swift index f75f6ee6..ad49ab97 100644 --- a/Sources/WAT/ParseTextInstruction.swift +++ b/Sources/WAT/ParseTextInstruction.swift @@ -335,6 +335,16 @@ func parseTextInstruction(keyword: String, expressionPars case "call_ref": let (functionIndex) = try expressionParser.visitCallRef(wat: &wat) return { return try $0.visitCallRef(functionIndex: functionIndex) } + case "return_call_ref": + let (functionIndex) = try expressionParser.visitReturnCallRef(wat: &wat) + return { return try $0.visitReturnCallRef(functionIndex: functionIndex) } + case "as_non_null": return { return try $0.visitAsNonNull() } + case "br_on_null": + let (functionIndex) = try expressionParser.visitBrOnNull(wat: &wat) + return { return try $0.visitBrOnNull(functionIndex: functionIndex) } + case "br_on_non_null": + let (functionIndex) = try expressionParser.visitBrOnNonNull(wat: &wat) + return { return try $0.visitBrOnNonNull(functionIndex: functionIndex) } default: return nil } } diff --git a/Sources/WAT/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index 1bcf65a9..0f70b463 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -363,7 +363,7 @@ struct ExpressionParser { private mutating func refKind() throws -> ReferenceType { if try parser.take(.id) { - return .funcRef // not sure about this. + return .funcRef // not sure about this. } else if try parser.takeKeyword("func") { return .funcRef } else if try parser.takeKeyword("extern") { @@ -445,6 +445,15 @@ extension ExpressionParser { let use = try parser.expectIndexOrId() return UInt32(try wat.types.resolve(use: use).index) } + mutating func visitReturnCallRef(wat: inout Wat) throws -> UInt32 { + return 0 + } + mutating func visitBrOnNull(wat: inout Wat) throws -> UInt32 { + return 0 + } + mutating func visitBrOnNonNull(wat: inout Wat) throws -> UInt32 { + return 0 + } mutating func visitCallIndirect(wat: inout Wat) throws -> (typeIndex: UInt32, tableIndex: UInt32) { let tableIndex: UInt32 if let tableId = try parser.takeIndexOrId() { diff --git a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift index 4f2c1b5c..71396724 100644 --- a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift +++ b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift @@ -105,11 +105,11 @@ extension Int32: InstructionImmediate { extension Instruction.RefNullOperand { init(result: VReg, type: ReferenceType) { - self.init(result: result, rawType: type.heapType.rawValue) // need to figure out rawType here + self.init(result: result, rawType: type.heapType.rawValue) // need to figure out rawType here } var type: ReferenceType { - ReferenceType(isNullable: true, heapType: HeapType(rawValue: rawType)!) // is this still a valid conversion? + ReferenceType(isNullable: true, heapType: HeapType(rawValue: rawType)!) // is this still a valid conversion? } } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index b61be947..a27c0200 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -88,6 +88,12 @@ protocol BinaryInstructionDecoder { @inlinable mutating func visitTableSize() throws -> UInt32 /// Decode `call_ref` immediates @inlinable mutating func visitCallRef() throws -> UInt32 + /// Decode `return_call_ref` immediates + @inlinable mutating func visitReturnCallRef() 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 } @inlinable func parseBinaryInstruction(visitor: inout V, decoder: inout D) throws -> Bool { @@ -137,6 +143,9 @@ func parseBinaryInstruction( case 0x14: let (functionIndex) = try decoder.visitCallRef() try visitor.visitCallRef(functionIndex: functionIndex) + case 0x15: + let (functionIndex) = try decoder.visitReturnCallRef() + try visitor.visitReturnCallRef(functionIndex: functionIndex) case 0x1A: try visitor.visitDrop() case 0x1B: @@ -516,6 +525,14 @@ func parseBinaryInstruction( case 0xD2: let (functionIndex) = try decoder.visitRefFunc() try visitor.visitRefFunc(functionIndex: functionIndex) + case 0xD4: + try visitor.visitAsNonNull() + case 0xD5: + let (functionIndex) = try decoder.visitBrOnNull() + try visitor.visitBrOnNull(functionIndex: functionIndex) + case 0xD6: + let (functionIndex) = try decoder.visitBrOnNonNull() + try visitor.visitBrOnNonNull(functionIndex: functionIndex) case 0xFC: let opcode1 = try decoder.claimNextByte() diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 09860e26..be961ca2 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -227,6 +227,10 @@ public enum Instruction: Equatable { case `tableGrow`(table: UInt32) case `tableSize`(table: UInt32) case `callRef`(functionIndex: UInt32) + case `returnCallRef`(functionIndex: UInt32) + case `asNonNull` + case `brOnNull`(functionIndex: UInt32) + case `brOnNonNull`(functionIndex: UInt32) } /// A visitor that visits all instructions by a single visit method. @@ -289,6 +293,10 @@ extension AnyInstructionVisitor { public mutating func visitTableGrow(table: UInt32) throws { return try self.visit(.tableGrow(table: table)) } public mutating func visitTableSize(table: UInt32) throws { return try self.visit(.tableSize(table: table)) } public mutating func visitCallRef(functionIndex: UInt32) throws { return try self.visit(.callRef(functionIndex: functionIndex)) } + public mutating func visitReturnCallRef(functionIndex: UInt32) throws { return try self.visit(.returnCallRef(functionIndex: functionIndex)) } + public mutating func visitAsNonNull() throws { return try self.visit(.asNonNull) } + public mutating func visitBrOnNull(functionIndex: UInt32) throws { return try self.visit(.brOnNull(functionIndex: functionIndex)) } + public mutating func visitBrOnNonNull(functionIndex: UInt32) throws { return try self.visit(.brOnNonNull(functionIndex: functionIndex)) } } /// A visitor for WebAssembly instructions. @@ -402,6 +410,14 @@ public protocol InstructionVisitor { mutating func visitTableSize(table: UInt32) throws /// Visiting `call_ref` instruction. mutating func visitCallRef(functionIndex: UInt32) throws + /// Visiting `return_call_ref` instruction. + mutating func visitReturnCallRef(functionIndex: UInt32) throws + /// Visiting `as_non_null` instruction. + mutating func visitAsNonNull() throws + /// Visiting `br_on_null` instruction. + mutating func visitBrOnNull(functionIndex: UInt32) throws + /// Visiting `br_on_non_null` instruction. + mutating func visitBrOnNonNull(functionIndex: UInt32) throws } extension InstructionVisitor { @@ -461,6 +477,10 @@ extension InstructionVisitor { case let .tableGrow(table): return try visitTableGrow(table: table) case let .tableSize(table): return try visitTableSize(table: table) case let .callRef(functionIndex): return try visitCallRef(functionIndex: functionIndex) + case let .returnCallRef(functionIndex): return try visitReturnCallRef(functionIndex: functionIndex) + case .asNonNull: return try visitAsNonNull() + case let .brOnNull(functionIndex): return try visitBrOnNull(functionIndex: functionIndex) + case let .brOnNonNull(functionIndex): return try visitBrOnNonNull(functionIndex: functionIndex) } } } @@ -520,5 +540,9 @@ extension InstructionVisitor { public mutating func visitTableGrow(table: UInt32) throws {} public mutating func visitTableSize(table: UInt32) throws {} public mutating func visitCallRef(functionIndex: UInt32) throws {} + public mutating func visitReturnCallRef(functionIndex: UInt32) throws {} + public mutating func visitAsNonNull() throws {} + public mutating func visitBrOnNull(functionIndex: UInt32) throws {} + public mutating func visitBrOnNonNull(functionIndex: UInt32) throws {} } diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 6ffe0c36..90015155 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -648,6 +648,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 { @@ -692,6 +696,12 @@ extension Parser: BinaryInstructionDecoder { } return refType } + @inlinable mutating func visitBrOnNull() throws -> UInt32 { + return 0 + } + @inlinable mutating func visitBrOnNonNull() throws -> UInt32 { + return 0 + } @inlinable mutating func visitRefFunc() throws -> UInt32 { try parseUnsigned() } @inlinable mutating func visitMemoryInit() throws -> UInt32 { diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index 7459122f..886028b4 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -16,10 +16,10 @@ public struct FunctionType: Equatable, Hashable { public enum HeapType: UInt8, Equatable, Hashable { /// A reference to any kind of function. - case funcRef // -> to be renamed func + case funcRef // -> to be renamed func /// An external host data. - case externRef // -> to be renamed extern + case externRef // -> to be renamed extern } /// Reference types diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index a8ac4d0e..678af0e0 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -51,7 +51,7 @@ final class SpectestTests: XCTestCase { 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. + 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 @@ -116,7 +116,7 @@ final class SpectestTests: XCTestCase { "gc/type-rec.wast", "gc/type-subtyping.wast", "gc/unreached-invalid.wast", - "gc/unreached-valid.wast" + "gc/unreached-valid.wast", ] ) } diff --git a/Utilities/Instructions.json b/Utilities/Instructions.json index d890d6ae..92a1ef5c 100644 --- a/Utilities/Instructions.json +++ b/Utilities/Instructions.json @@ -202,5 +202,9 @@ ["saturatingFloatToInt", "i64.trunc_sat_f32_u" , ["0xFC", "0x05"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f64_s" , ["0xFC", "0x06"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f64_u" , ["0xFC", "0x07"], [] , "conversion"], - ["referenceTypes" , "call_ref" , ["0x14"] , [["functionIndex", "UInt32"]] , null ] + ["referenceTypes" , "call_ref" , ["0x14"] , [["functionIndex", "UInt32"]] , null ], + ["referenceTypes" , "return_call_ref" , ["0x15"] , [["functionIndex", "UInt32"]] , null ], + ["referenceTypes" , "as_non_null" , ["0xd4"] , [] , null ], + ["referenceTypes" , "br_on_null" , ["0xd5"] , [["functionIndex", "UInt32"]] , null ], + ["referenceTypes" , "br_on_non_null" , ["0xd6"] , [["functionIndex", "UInt32"]] , null ] ] From b141c7d8d4ff0fb443ee639b895be47cc847e920 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 5 Jan 2025 14:06:24 +0900 Subject: [PATCH 10/18] Fix instruction immediate labels --- Sources/WAT/BinaryInstructionEncoder.swift | 17 ++++---- Sources/WAT/Encoder.swift | 1 + Sources/WAT/ParseTextInstruction.swift | 16 ++++---- .../WasmParser/BinaryInstructionDecoder.swift | 16 ++++---- Sources/WasmParser/InstructionVisitor.swift | 40 +++++++++---------- Utilities/Instructions.json | 10 ++--- 6 files changed, 51 insertions(+), 49 deletions(-) diff --git a/Sources/WAT/BinaryInstructionEncoder.swift b/Sources/WAT/BinaryInstructionEncoder.swift index e3516985..2dab9d08 100644 --- a/Sources/WAT/BinaryInstructionEncoder.swift +++ b/Sources/WAT/BinaryInstructionEncoder.swift @@ -25,6 +25,7 @@ protocol BinaryInstructionEncoder: InstructionVisitor { mutating func encodeImmediates(targets: BrTable) throws mutating func encodeImmediates(type: ReferenceType) 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 @@ -392,21 +393,21 @@ extension BinaryInstructionEncoder { try encodeInstruction([0xFC, 0x10]) try encodeImmediates(table: table) } - mutating func visitCallRef(functionIndex: UInt32) throws { + mutating func visitCallRef(typeIndex: UInt32) throws { try encodeInstruction([0x14]) - try encodeImmediates(functionIndex: functionIndex) + try encodeImmediates(typeIndex: typeIndex) } - mutating func visitReturnCallRef(functionIndex: UInt32) throws { + mutating func visitReturnCallRef(typeIndex: UInt32) throws { try encodeInstruction([0x15]) - try encodeImmediates(functionIndex: functionIndex) + try encodeImmediates(typeIndex: typeIndex) } mutating func visitAsNonNull() throws { try encodeInstruction([0xD4]) } - mutating func visitBrOnNull(functionIndex: UInt32) throws { + mutating func visitBrOnNull(relativeDepth: UInt32) throws { try encodeInstruction([0xD5]) - try encodeImmediates(functionIndex: functionIndex) + try encodeImmediates(relativeDepth: relativeDepth) } - mutating func visitBrOnNonNull(functionIndex: UInt32) throws { + mutating func visitBrOnNonNull(relativeDepth: UInt32) throws { try encodeInstruction([0xD6]) - try encodeImmediates(functionIndex: functionIndex) + try encodeImmediates(relativeDepth: relativeDepth) } } diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index bd0ec74a..75ba43c7 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -496,6 +496,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) diff --git a/Sources/WAT/ParseTextInstruction.swift b/Sources/WAT/ParseTextInstruction.swift index ad49ab97..00fb16b8 100644 --- a/Sources/WAT/ParseTextInstruction.swift +++ b/Sources/WAT/ParseTextInstruction.swift @@ -333,18 +333,18 @@ func parseTextInstruction(keyword: String, expressionPars case "i64.trunc_sat_f64_s": return { return try $0.visitConversion(.i64TruncSatF64S) } case "i64.trunc_sat_f64_u": return { return try $0.visitConversion(.i64TruncSatF64U) } case "call_ref": - let (functionIndex) = try expressionParser.visitCallRef(wat: &wat) - return { return try $0.visitCallRef(functionIndex: functionIndex) } + let (typeIndex) = try expressionParser.visitCallRef(wat: &wat) + return { return try $0.visitCallRef(typeIndex: typeIndex) } case "return_call_ref": - let (functionIndex) = try expressionParser.visitReturnCallRef(wat: &wat) - return { return try $0.visitReturnCallRef(functionIndex: functionIndex) } + let (typeIndex) = try expressionParser.visitReturnCallRef(wat: &wat) + return { return try $0.visitReturnCallRef(typeIndex: typeIndex) } case "as_non_null": return { return try $0.visitAsNonNull() } case "br_on_null": - let (functionIndex) = try expressionParser.visitBrOnNull(wat: &wat) - return { return try $0.visitBrOnNull(functionIndex: functionIndex) } + let (relativeDepth) = try expressionParser.visitBrOnNull(wat: &wat) + return { return try $0.visitBrOnNull(relativeDepth: relativeDepth) } case "br_on_non_null": - let (functionIndex) = try expressionParser.visitBrOnNonNull(wat: &wat) - return { return try $0.visitBrOnNonNull(functionIndex: functionIndex) } + let (relativeDepth) = try expressionParser.visitBrOnNonNull(wat: &wat) + return { return try $0.visitBrOnNonNull(relativeDepth: relativeDepth) } default: return nil } } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index a27c0200..0cb80682 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -141,11 +141,11 @@ func parseBinaryInstruction( let (typeIndex, tableIndex) = try decoder.visitReturnCallIndirect() try visitor.visitReturnCallIndirect(typeIndex: typeIndex, tableIndex: tableIndex) case 0x14: - let (functionIndex) = try decoder.visitCallRef() - try visitor.visitCallRef(functionIndex: functionIndex) + let (typeIndex) = try decoder.visitCallRef() + try visitor.visitCallRef(typeIndex: typeIndex) case 0x15: - let (functionIndex) = try decoder.visitReturnCallRef() - try visitor.visitReturnCallRef(functionIndex: functionIndex) + let (typeIndex) = try decoder.visitReturnCallRef() + try visitor.visitReturnCallRef(typeIndex: typeIndex) case 0x1A: try visitor.visitDrop() case 0x1B: @@ -528,11 +528,11 @@ func parseBinaryInstruction( case 0xD4: try visitor.visitAsNonNull() case 0xD5: - let (functionIndex) = try decoder.visitBrOnNull() - try visitor.visitBrOnNull(functionIndex: functionIndex) + let (relativeDepth) = try decoder.visitBrOnNull() + try visitor.visitBrOnNull(relativeDepth: relativeDepth) case 0xD6: - let (functionIndex) = try decoder.visitBrOnNonNull() - try visitor.visitBrOnNonNull(functionIndex: functionIndex) + 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 be961ca2..fabacc95 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -226,11 +226,11 @@ public enum Instruction: Equatable { case `tableSet`(table: UInt32) case `tableGrow`(table: UInt32) case `tableSize`(table: UInt32) - case `callRef`(functionIndex: UInt32) - case `returnCallRef`(functionIndex: UInt32) + case `callRef`(typeIndex: UInt32) + case `returnCallRef`(typeIndex: UInt32) case `asNonNull` - case `brOnNull`(functionIndex: UInt32) - case `brOnNonNull`(functionIndex: UInt32) + case `brOnNull`(relativeDepth: UInt32) + case `brOnNonNull`(relativeDepth: UInt32) } /// A visitor that visits all instructions by a single visit method. @@ -292,11 +292,11 @@ extension AnyInstructionVisitor { public mutating func visitTableSet(table: UInt32) throws { return try self.visit(.tableSet(table: table)) } public mutating func visitTableGrow(table: UInt32) throws { return try self.visit(.tableGrow(table: table)) } public mutating func visitTableSize(table: UInt32) throws { return try self.visit(.tableSize(table: table)) } - public mutating func visitCallRef(functionIndex: UInt32) throws { return try self.visit(.callRef(functionIndex: functionIndex)) } - public mutating func visitReturnCallRef(functionIndex: UInt32) throws { return try self.visit(.returnCallRef(functionIndex: functionIndex)) } + 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 visitAsNonNull() throws { return try self.visit(.asNonNull) } - public mutating func visitBrOnNull(functionIndex: UInt32) throws { return try self.visit(.brOnNull(functionIndex: functionIndex)) } - public mutating func visitBrOnNonNull(functionIndex: UInt32) throws { return try self.visit(.brOnNonNull(functionIndex: functionIndex)) } + 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)) } } /// A visitor for WebAssembly instructions. @@ -409,15 +409,15 @@ public protocol InstructionVisitor { /// Visiting `table.size` instruction. mutating func visitTableSize(table: UInt32) throws /// Visiting `call_ref` instruction. - mutating func visitCallRef(functionIndex: UInt32) throws + mutating func visitCallRef(typeIndex: UInt32) throws /// Visiting `return_call_ref` instruction. - mutating func visitReturnCallRef(functionIndex: UInt32) throws + mutating func visitReturnCallRef(typeIndex: UInt32) throws /// Visiting `as_non_null` instruction. mutating func visitAsNonNull() throws /// Visiting `br_on_null` instruction. - mutating func visitBrOnNull(functionIndex: UInt32) throws + mutating func visitBrOnNull(relativeDepth: UInt32) throws /// Visiting `br_on_non_null` instruction. - mutating func visitBrOnNonNull(functionIndex: UInt32) throws + mutating func visitBrOnNonNull(relativeDepth: UInt32) throws } extension InstructionVisitor { @@ -476,11 +476,11 @@ extension InstructionVisitor { case let .tableSet(table): return try visitTableSet(table: table) case let .tableGrow(table): return try visitTableGrow(table: table) case let .tableSize(table): return try visitTableSize(table: table) - case let .callRef(functionIndex): return try visitCallRef(functionIndex: functionIndex) - case let .returnCallRef(functionIndex): return try visitReturnCallRef(functionIndex: functionIndex) + case let .callRef(typeIndex): return try visitCallRef(typeIndex: typeIndex) + case let .returnCallRef(typeIndex): return try visitReturnCallRef(typeIndex: typeIndex) case .asNonNull: return try visitAsNonNull() - case let .brOnNull(functionIndex): return try visitBrOnNull(functionIndex: functionIndex) - case let .brOnNonNull(functionIndex): return try visitBrOnNonNull(functionIndex: functionIndex) + case let .brOnNull(relativeDepth): return try visitBrOnNull(relativeDepth: relativeDepth) + case let .brOnNonNull(relativeDepth): return try visitBrOnNonNull(relativeDepth: relativeDepth) } } } @@ -539,10 +539,10 @@ extension InstructionVisitor { public mutating func visitTableSet(table: UInt32) throws {} public mutating func visitTableGrow(table: UInt32) throws {} public mutating func visitTableSize(table: UInt32) throws {} - public mutating func visitCallRef(functionIndex: UInt32) throws {} - public mutating func visitReturnCallRef(functionIndex: UInt32) throws {} + public mutating func visitCallRef(typeIndex: UInt32) throws {} + public mutating func visitReturnCallRef(typeIndex: UInt32) throws {} public mutating func visitAsNonNull() throws {} - public mutating func visitBrOnNull(functionIndex: UInt32) throws {} - public mutating func visitBrOnNonNull(functionIndex: UInt32) throws {} + public mutating func visitBrOnNull(relativeDepth: UInt32) throws {} + public mutating func visitBrOnNonNull(relativeDepth: UInt32) throws {} } diff --git a/Utilities/Instructions.json b/Utilities/Instructions.json index 92a1ef5c..b07ed42b 100644 --- a/Utilities/Instructions.json +++ b/Utilities/Instructions.json @@ -202,9 +202,9 @@ ["saturatingFloatToInt", "i64.trunc_sat_f32_u" , ["0xFC", "0x05"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f64_s" , ["0xFC", "0x06"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f64_u" , ["0xFC", "0x07"], [] , "conversion"], - ["referenceTypes" , "call_ref" , ["0x14"] , [["functionIndex", "UInt32"]] , null ], - ["referenceTypes" , "return_call_ref" , ["0x15"] , [["functionIndex", "UInt32"]] , null ], - ["referenceTypes" , "as_non_null" , ["0xd4"] , [] , null ], - ["referenceTypes" , "br_on_null" , ["0xd5"] , [["functionIndex", "UInt32"]] , null ], - ["referenceTypes" , "br_on_non_null" , ["0xd6"] , [["functionIndex", "UInt32"]] , null ] + ["functionReferences" , "call_ref" , ["0x14"] , [["typeIndex", "UInt32"]] , null ], + ["functionReferences" , "return_call_ref" , ["0x15"] , [["typeIndex", "UInt32"]] , null ], + ["functionReferences" , "as_non_null" , ["0xd4"] , [] , null ], + ["functionReferences" , "br_on_null" , ["0xd5"] , [["relativeDepth", "UInt32"]] , null ], + ["functionReferences" , "br_on_non_null" , ["0xd6"] , [["relativeDepth", "UInt32"]] , null ] ] From 9966354c8e44d7d8a9b595b9acb199c2783185f3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 5 Jan 2025 17:13:50 +0900 Subject: [PATCH 11/18] Fix encoding of heap types and leave concrete heap types unimplemented --- Sources/WAT/BinaryInstructionEncoder.swift | 4 +- Sources/WAT/Encoder.swift | 27 +++++++-- Sources/WAT/Parser.swift | 2 + Sources/WAT/Parser/ExpressionParser.swift | 16 +++--- Sources/WAT/Parser/WastParser.swift | 38 +++++++------ Sources/WasmKit/Execution/Errors.swift | 3 + Sources/WasmKit/Execution/Instances.swift | 6 +- .../Instructions/InstructionSupport.swift | 8 +-- .../WasmKit/Execution/Instructions/Misc.swift | 2 +- Sources/WasmKit/Execution/UntypedValue.swift | 6 +- Sources/WasmKit/Translator.swift | 8 ++- Sources/WasmKit/Validator.swift | 8 ++- .../WasmParser/BinaryInstructionDecoder.swift | 2 +- Sources/WasmParser/InstructionVisitor.swift | 8 +-- Sources/WasmParser/WasmParser.swift | 52 +++++++++++++---- Sources/WasmTypes/WasmTypes.swift | 15 ++++- Tests/WasmKitTests/Spectest/TestCase.swift | 57 ++++++++++++------- Utilities/Instructions.json | 2 +- 18 files changed, 181 insertions(+), 83 deletions(-) diff --git a/Sources/WAT/BinaryInstructionEncoder.swift b/Sources/WAT/BinaryInstructionEncoder.swift index 2dab9d08..8f8f3594 100644 --- a/Sources/WAT/BinaryInstructionEncoder.swift +++ b/Sources/WAT/BinaryInstructionEncoder.swift @@ -23,7 +23,7 @@ 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 @@ -172,7 +172,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) } diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 75ba43c7..f5df8e1b 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 self.heapType { - case .funcRef: encoder.output.append(isNullable ? 0x70 : 0x71) - case .externRef: encoder.output.append(0x6F) + 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 .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)) } } } @@ -511,7 +528,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) } 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 0f70b463..48458963 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -361,15 +361,17 @@ struct ExpressionParser { return UInt32(index) } - private mutating func refKind() throws -> ReferenceType { - if try parser.take(.id) { - return .funcRef // not sure about this. - } else if try parser.takeKeyword("func") { + /// 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 (decl, 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 { @@ -513,8 +515,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 91df6d0b..77476457 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.heapType { - 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) {} @@ -137,16 +131,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 @@ -155,7 +159,7 @@ public struct WastInvoke { public enum WastExpectValue { /// A concrete value that is expected to be returned. - case value(Value) + case value(WastConstValue) /// A value that is expected to be a canonical NaN. /// Corresponds to `f32.const nan:canonical` in WAST. case f32CanonicalNaN 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 7054337d..6fb024fd 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -275,10 +275,12 @@ struct TableEntity /* : ~Copyable */ { init(_ tableType: TableType, resourceLimiter: any ResourceLimiter) throws { let emptyElement: Reference switch tableType.elementType.heapType { - case .funcRef: + 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 71396724..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.heapType.rawValue) // need to figure out rawType here + init(result: VReg, type: AbstractHeapType) { + self.init(result: result, rawType: type.rawValue) // need to figure out rawType here } - var type: ReferenceType { - ReferenceType(isNullable: true, heapType: HeapType(rawValue: rawType)!) // is this still a valid conversion? + var type: AbstractHeapType { + AbstractHeapType(rawValue: rawType).unsafelyUnwrapped } } diff --git a/Sources/WasmKit/Execution/Instructions/Misc.swift b/Sources/WasmKit/Execution/Instructions/Misc.swift index c3af975b..31386545 100644 --- a/Sources/WasmKit/Execution/Instructions/Misc.swift +++ b/Sources/WasmKit/Execution/Instructions/Misc.swift @@ -21,7 +21,7 @@ extension Execution { extension Execution { mutating func refNull(sp: Sp, immediate: Instruction.RefNullOperand) { let value: Value - switch immediate.type.heapType { + switch immediate.type { case .externRef: value = .ref(.extern(nil)) case .funcRef: diff --git a/Sources/WasmKit/Execution/UntypedValue.swift b/Sources/WasmKit/Execution/UntypedValue.swift index 1c6b0c32..ba498fa3 100644 --- a/Sources/WasmKit/Execution/UntypedValue.swift +++ b/Sources/WasmKit/Execution/UntypedValue.swift @@ -115,10 +115,12 @@ struct UntypedValue: Equatable, Hashable { return Int(storage) } switch type.heapType { - case .funcRef: + 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/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 0cb80682..fbddbaac 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -59,7 +59,7 @@ 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 `memory.init` immediates diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index fabacc95..6a06418e 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -205,7 +205,7 @@ 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 `i32Eqz` @@ -271,7 +271,7 @@ 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 visitI32Eqz() throws { return try self.visit(.i32Eqz) } @@ -367,7 +367,7 @@ 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. @@ -518,7 +518,7 @@ 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 visitI32Eqz() throws {} diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 90015155..c0e109b7 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,11 +448,42 @@ extension Parser { case 0x7D: return .f32 case 0x7C: return .f64 case 0x7B: return .f64 - case 0x70: return .ref(.funcRef) - case 0x71: 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.consumeAny() + switch b { + case 0x6F: return .externRef + case 0x70: return .funcRef + default: + let rawIndex = try stream.parseVarSigned33() + guard let index = TypeIndex(exactly: rawIndex) else { + throw makeError(.invalidFunctionType(rawIndex)) + } + return .concrete(typeIndex: index) } } @@ -689,12 +725,8 @@ 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 diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index 886028b4..9bfe3fe9 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -14,7 +14,7 @@ public struct FunctionType: Equatable, Hashable { public let results: [ValueType] } -public enum HeapType: UInt8, Equatable, Hashable { +public enum AbstractHeapType: UInt8, Equatable, Hashable { /// A reference to any kind of function. case funcRef // -> to be renamed func @@ -22,6 +22,19 @@ public enum HeapType: UInt8, Equatable, Hashable { 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 struct ReferenceType: Equatable, Hashable { public var isNullable: Bool diff --git a/Tests/WasmKitTests/Spectest/TestCase.swift b/Tests/WasmKitTests/Spectest/TestCase.swift index 2fb1809e..90951a87 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,37 +396,36 @@ 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)): + case let (.i32(lhs), .value(.i32(rhs))): return lhs == rhs - case let (.i64(lhs), .i64(rhs)): + case let (.i64(lhs), .value(.i64(rhs))): return lhs == rhs - case let (.f32(lhs), .f32(rhs)): + case let (.f32(lhs), .value(.f32(rhs))): let lhs = Float32(bitPattern: lhs) let rhs = Float32(bitPattern: rhs) return lhs.isNaN && rhs.isNaN || lhs == rhs - case let (.f64(lhs), .f64(rhs)): + case let (.f64(lhs), .value(.f64(rhs))): 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))): + 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?)), .value(.refExtern(rhs))): return lhs == rhs - case let (.ref(.function(lhs)), .ref(.function(rhs))): + case let (.ref(.function(lhs?)), .value(.refFunc(rhs))): return lhs == rhs + case (.ref(.extern(nil)), .value(.refNull(.abstract(.externRef)))), + (.ref(.function(nil)), .value(.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/Utilities/Instructions.json b/Utilities/Instructions.json index b07ed42b..945e8b6e 100644 --- a/Utilities/Instructions.json +++ b/Utilities/Instructions.json @@ -51,7 +51,7 @@ ["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 ], ["mvp" , "i32.eqz" , ["0x45"] , [] , null ], From 5049e3f86ba01cca461f2683c855d937ca38221e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 5 Jan 2025 19:22:51 +0900 Subject: [PATCH 12/18] Fix WAT parsing around heap type and skip concrete heap type parsing We need to change the core `ValueType` modeling to support the concrete heap type, which can reference another type by index, but it would be a non-trivial change. So, for now, we skip parsing the concrete heap type and only support the abstract heap type for now. --- Sources/WAT/Parser/WatParser.swift | 90 +++++++++++--------------- Tests/WasmKitTests/SpectestTests.swift | 4 +- 2 files changed, 40 insertions(+), 54 deletions(-) diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index a5b5cab1..fee73429 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -238,7 +238,7 @@ struct WatParser { var inlineElement: ElementDecl? let isMemory64 = try expectAddressSpaceType() - if let refType = try maybeRefType() { + if let refType = try takeRefType() { guard try parser.takeParenBlockStart("elem") else { throw WatParserError("expected elem", location: parser.lexer.location()) } @@ -373,7 +373,7 @@ struct WatParser { // | funcidx* (iff the tableuse is omitted) let indices: ElementDecl.Indices let type: ReferenceType - if let refType = try maybeRefType() { + if let refType = try takeRefType() { indices = .elementExprList(parser.lexer) type = refType } else if try parser.takeKeyword("func") || table == nil { @@ -606,70 +606,56 @@ struct WatParser { } mutating func valueType() throws -> ValueType { - if try parser.peek(.leftParen) != nil { - return try _referenceValueType() + if try parser.takeKeyword("i32") { + return .i32 + } else if try parser.takeKeyword("i64") { + return .i64 + } else if try parser.takeKeyword("f32") { + return .f32 + } else if try parser.takeKeyword("f64") { + return .f64 + } else if let refType = try takeRefType() { + return .ref(refType) } else { - return try _valueType() - } - } - - // must consume right paren - mutating func _referenceValueType() throws -> ValueType { - var isNullable = false - _ = try parser.takeParenBlockStart("ref") - if try parser.peekKeyword() == "null" { - _ = try parser.takeKeyword("null") - isNullable = true - } - - if try parser.takeId() != nil { - _ = try parser.take(.rightParen) - return .ref(refType(keyword: "func", isNullable: isNullable)!) - } - - let keyword = try parser.expectKeyword() - _ = try parser.take(.rightParen) - if let refType = refType(keyword: keyword, isNullable: isNullable) { return .ref(refType) } - throw WatParserError("unexpected value type \(keyword)", location: parser.lexer.location()) - } - - 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, isNullable: true) { return .ref(refType) } - throw WatParserError("unexpected value type \(keyword)", location: parser.lexer.location()) - } - } - - mutating func refType(keyword: String, isNullable: Bool) -> ReferenceType? { - switch keyword { - case "funcref": return .funcRef - case "externref": return .externRef - case "func": return ReferenceType(isNullable: isNullable, heapType: .funcRef) - case "extern": return ReferenceType(isNullable: isNullable, heapType: .funcRef) - default: return nil + 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, isNullable: true) else { - throw WatParserError("unexpected ref type \(keyword)", location: parser.lexer.location()) + 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 -> ReferenceType? { + // Check abbreviations first + // https://webassembly.github.io/function-references/core/text/types.html#abbreviations if try parser.takeKeyword("funcref") { return .funcRef } else if try parser.takeKeyword("externref") { return .externRef + } else if try parser.takeParenBlockStart("ref") { + let isNullable = try parser.takeKeyword("null") + let heapType = try heapType() + try parser.expect(.rightParen) + return ReferenceType(isNullable: isNullable, heapType: heapType) } return nil } + + /// > Note: + /// + mutating func heapType() throws -> HeapType { + if try parser.takeKeyword("func") { + return .abstract(.funcRef) + } else if try parser.takeKeyword("extern") { + return .abstract(.externRef) + } else if try parser.takeIndexOrId() != nil { + // TODO: Implement (ref $t) + throw WatParserError("concrete heap type is not supported yet", location: parser.lexer.location()) + } + throw WatParserError("expected heap type", location: parser.lexer.location()) + } } diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 678af0e0..3314b5bc 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -73,8 +73,8 @@ final class SpectestTests: XCTestCase { configuration: defaultConfig ) - XCTAssertEqual(result.passed, 1400) - XCTAssertEqual(result.failed, 134) + XCTAssertEqual(result.passed, 1228) + XCTAssertEqual(result.failed, 49) XCTAssertEqual( result.sortedFailedCases(), [ From e1743042df0e5b1510050b8ffc273b1413520730 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 5 Jan 2025 22:44:18 +0900 Subject: [PATCH 13/18] WAT: Add concrete type support --- Sources/WAT/Encoder.swift | 21 ++-- Sources/WAT/NameMapping.swift | 34 ++++-- Sources/WAT/Parser/ExpressionParser.swift | 23 ++-- Sources/WAT/Parser/WatParser.swift | 142 ++++++++++++++-------- Sources/WAT/WAT.swift | 34 ++++-- Sources/WasmParser/WasmParser.swift | 10 +- 6 files changed, 175 insertions(+), 89 deletions(-) diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index f5df8e1b..68ab8f59 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -211,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? @@ -250,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 @@ -269,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)) @@ -341,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") @@ -575,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 @@ -622,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/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index 48458963..f5f6f8bf 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 } @@ -261,8 +266,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 +345,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) } diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index fee73429..4cb68f77 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -35,9 +35,27 @@ 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 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 +71,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 +111,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 +126,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 +157,7 @@ struct WatParser { var id: Name? var mode: Mode - var type: ReferenceType + var type: UnresolvedType var indices: Indices } @@ -141,7 +170,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,7 +263,7 @@ 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() @@ -262,14 +291,16 @@ 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) } @@ -372,13 +403,13 @@ struct WatParser { // elemlist ::= reftype elemexpr* | 'func' funcidx* // | funcidx* (iff the tableuse is omitted) let indices: ElementDecl.Indices - let type: ReferenceType + 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()) } @@ -496,11 +527,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 +539,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 +558,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 +569,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 +585,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 +635,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,23 +646,23 @@ struct WatParser { return results } - mutating func valueType() throws -> ValueType { + mutating func valueType() throws -> UnresolvedType { if try parser.takeKeyword("i32") { - return .i32 + return UnresolvedType(.i32) } else if try parser.takeKeyword("i64") { - return .i64 + return UnresolvedType(.i64) } else if try parser.takeKeyword("f32") { - return .f32 + return UnresolvedType(.f32) } else if try parser.takeKeyword("f64") { - return .f64 + return UnresolvedType(.f64) } else if let refType = try takeRefType() { - return .ref(refType) + return refType.map { .ref($0) } } else { throw WatParserError("expected value type", location: parser.lexer.location()) } } - mutating func refType() throws -> ReferenceType { + mutating func refType() throws -> UnresolvedType { guard let refType = try takeRefType() else { throw WatParserError("expected reference type", location: parser.lexer.location()) } @@ -629,32 +670,35 @@ struct WatParser { } /// Parse a reference type tokens if the head tokens seems like so. - mutating func takeRefType() throws -> ReferenceType? { + 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 ReferenceType(isNullable: isNullable, heapType: heapType) + return heapType.map { + ReferenceType(isNullable: isNullable, heapType: $0) + } } return nil } /// > Note: /// - mutating func heapType() throws -> HeapType { + mutating func heapType() throws -> UnresolvedType { if try parser.takeKeyword("func") { - return .abstract(.funcRef) + return UnresolvedType(.abstract(.funcRef)) } else if try parser.takeKeyword("extern") { - return .abstract(.externRef) - } else if try parser.takeIndexOrId() != nil { - // TODO: Implement (ref $t) - throw WatParserError("concrete heap type is not supported yet", location: parser.lexer.location()) + 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..c9a3174d 100644 --- a/Sources/WAT/WAT.swift +++ b/Sources/WAT/WAT.swift @@ -176,8 +176,25 @@ 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 +236,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { } switch decl.kind { - case let .type(decl): - try typesMap.add(decl) + case let .type(decl): break case let .function(decl): try checkImportOrder(decl.importNames) let index = try functionsMap.add(decl) @@ -244,7 +260,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 +282,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 +298,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 +330,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/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index c0e109b7..d8b84c89 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -474,10 +474,14 @@ extension Parser { /// @usableFromInline func parseHeapType() throws -> HeapType { - let b = try stream.consumeAny() + let b = try stream.peek() switch b { - case 0x6F: return .externRef - case 0x70: return .funcRef + 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 { From ed04d10860f56bd0593cb2418d4c809639b90c2c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 6 Jan 2025 01:18:15 +0900 Subject: [PATCH 14/18] WAT: Enable smoke encoding test for the proposal --- Sources/WAT/Parser/ExpressionParser.swift | 11 ++++ Sources/WAT/Parser/WastParser.swift | 30 ++++++++- Sources/WAT/Parser/WatParser.swift | 39 ++++++++++-- Sources/WasmParser/WasmTypes.swift | 12 ++-- Tests/WATTests/EncoderTests.swift | 74 ++++++++++++++++------ Tests/WATTests/Spectest.swift | 20 ++++-- Tests/WasmKitTests/Spectest/TestCase.swift | 20 +++--- 7 files changed, 154 insertions(+), 52 deletions(-) diff --git a/Sources/WAT/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index f5f6f8bf..dbd44fe3 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -162,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 } diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index 77476457..5bef2b32 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -82,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() { @@ -158,8 +170,20 @@ public struct WastInvoke { } public enum WastExpectValue { - /// A concrete value that is expected to be returned. - case value(WastConstValue) + 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 4cb68f77..14c42f8b 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -46,6 +46,13 @@ struct WatParser { 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)) }) } @@ -267,6 +274,17 @@ struct WatParser { var inlineElement: ElementDecl? let isMemory64 = try expectAddressSpaceType() + // 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()) @@ -274,12 +292,7 @@ struct WatParser { 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) @@ -302,7 +315,19 @@ struct WatParser { ) } } 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( 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/Tests/WATTests/EncoderTests.swift b/Tests/WATTests/EncoderTests.swift index d5adb8ca..1cdc64ce 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,38 @@ class EncoderTests: XCTestCase { #endif } + 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: [] + ) { + 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 + } + _ = wat = wat + // TODO: Enable smoke check for encoding + // _ = try wat.encode() + case .assertMalformed(let module, let message): + checkMalformed(wast: wastFile, module: module, message: message, recordFail: {}) + default: + break + } + } + } + } + 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/TestCase.swift b/Tests/WasmKitTests/Spectest/TestCase.swift index 90951a87..b49703ee 100644 --- a/Tests/WasmKitTests/Spectest/TestCase.swift +++ b/Tests/WasmKitTests/Spectest/TestCase.swift @@ -401,15 +401,15 @@ extension WastRunContext { extension Value { func isTestEquivalent(to value: WastExpectValue) -> Bool { switch (self, value) { - case let (.i32(lhs), .value(.i32(rhs))): + case let (.i32(lhs), .i32(rhs)): return lhs == rhs - case let (.i64(lhs), .value(.i64(rhs))): + case let (.i64(lhs), .i64(rhs)): return lhs == rhs - case let (.f32(lhs), .value(.f32(rhs))): + case let (.f32(lhs), .f32(rhs)): let lhs = Float32(bitPattern: lhs) let rhs = Float32(bitPattern: rhs) return lhs.isNaN && rhs.isNaN || lhs == rhs - case let (.f64(lhs), .value(.f64(rhs))): + case let (.f64(lhs), .f64(rhs)): let lhs = Float64(bitPattern: lhs) let rhs = Float64(bitPattern: rhs) return lhs.isNaN && rhs.isNaN || lhs == rhs @@ -419,12 +419,12 @@ extension Value { case let (.f32(lhs), .f32ArithmeticNaN), let (.f32(lhs), .f32CanonicalNaN): return Float32(bitPattern: lhs).isNaN - case let (.ref(.extern(lhs?)), .value(.refExtern(rhs))): - return lhs == rhs - case let (.ref(.function(lhs?)), .value(.refFunc(rhs))): - return lhs == rhs - case (.ref(.extern(nil)), .value(.refNull(.abstract(.externRef)))), - (.ref(.function(nil)), .value(.refNull(.abstract(.funcRef)))): + 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 From d7a52e11113b7b6b93177dba8be3fb6ba5564bad Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 6 Jan 2025 01:22:01 +0900 Subject: [PATCH 15/18] Test: Update spectest results for reference proposal --- Tests/WasmKitTests/SpectestTests.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 3314b5bc..06c5d2a7 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -57,8 +57,8 @@ final class SpectestTests: XCTestCase { configuration: defaultConfig ) - XCTAssertEqual(result.passed, 8) - XCTAssertEqual(result.failed, 26) + XCTAssertEqual(result.passed, 7) + XCTAssertEqual(result.failed, 27) } /// Run the garbage collection proposal tests @@ -73,8 +73,8 @@ final class SpectestTests: XCTestCase { configuration: defaultConfig ) - XCTAssertEqual(result.passed, 1228) - XCTAssertEqual(result.failed, 49) + XCTAssertEqual(result.passed, 1552) + XCTAssertEqual(result.failed, 368) XCTAssertEqual( result.sortedFailedCases(), [ @@ -83,7 +83,6 @@ final class SpectestTests: XCTestCase { "gc/array_fill.wast", "gc/array_init_data.wast", "gc/array_init_elem.wast", - "gc/br_if.wast", "gc/br_on_cast.wast", "gc/br_on_cast_fail.wast", "gc/br_on_non_null.wast", @@ -98,7 +97,6 @@ final class SpectestTests: XCTestCase { "gc/i31.wast", "gc/linking.wast", "gc/local_init.wast", - "gc/local_tee.wast", "gc/ref.wast", "gc/ref_as_non_null.wast", "gc/ref_cast.wast", @@ -107,7 +105,6 @@ final class SpectestTests: XCTestCase { "gc/ref_null.wast", "gc/ref_test.wast", "gc/return_call_ref.wast", - "gc/select.wast", "gc/struct.wast", "gc/table-sub.wast", "gc/table.wast", From b5c08d85972f023ce9760a7bcf8286dd11c50834 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 6 Jan 2025 01:22:47 +0900 Subject: [PATCH 16/18] format.py --- Sources/WAT/WAT.swift | 7 ++++--- Tests/WATTests/EncoderTests.swift | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/WAT/WAT.swift b/Sources/WAT/WAT.swift index c9a3174d..a5fa3b70 100644 --- a/Sources/WAT/WAT.swift +++ b/Sources/WAT/WAT.swift @@ -188,9 +188,10 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { try unresolvedTypesMapping.add(decl) } for decl in unresolvedTypesMapping { - try typesMap.add(TypesMap.NamedResolvedType( - id: decl.id, type: decl.type.resolve(unresolvedTypesMapping) - )) + try typesMap.add( + TypesMap.NamedResolvedType( + id: decl.id, type: decl.type.resolve(unresolvedTypesMapping) + )) } } diff --git a/Tests/WATTests/EncoderTests.swift b/Tests/WATTests/EncoderTests.swift index 1cdc64ce..861777f0 100644 --- a/Tests/WATTests/EncoderTests.swift +++ b/Tests/WATTests/EncoderTests.swift @@ -166,8 +166,8 @@ class EncoderTests: XCTestCase { continue } _ = wat = wat - // TODO: Enable smoke check for encoding - // _ = try wat.encode() + // TODO: Enable smoke check for encoding + // _ = try wat.encode() case .assertMalformed(let module, let message): checkMalformed(wast: wastFile, module: module, message: message, recordFail: {}) default: From b4fcf716fb0a4ece07edfff070b8f176340efbe7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 10 Jan 2025 17:48:29 +0900 Subject: [PATCH 17/18] WAT: Implement remaining instruction parsing --- Sources/WAT/BinaryInstructionEncoder.swift | 34 +++++------ Sources/WAT/ParseTextInstruction.swift | 26 ++++---- Sources/WAT/Parser/ExpressionParser.swift | 8 +-- Sources/WAT/Parser/WatParser.swift | 12 +++- Sources/WAT/WAT.swift | 2 +- .../WasmParser/BinaryInstructionDecoder.swift | 18 +++--- Sources/WasmParser/InstructionVisitor.swift | 60 +++++++++---------- Tests/WATTests/EncoderTests.swift | 4 +- Utilities/Instructions.json | 12 ++-- 9 files changed, 90 insertions(+), 86 deletions(-) diff --git a/Sources/WAT/BinaryInstructionEncoder.swift b/Sources/WAT/BinaryInstructionEncoder.swift index 8f8f3594..6591c7df 100644 --- a/Sources/WAT/BinaryInstructionEncoder.swift +++ b/Sources/WAT/BinaryInstructionEncoder.swift @@ -83,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 { @@ -181,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] @@ -393,21 +410,4 @@ extension BinaryInstructionEncoder { try encodeInstruction([0xFC, 0x10]) try encodeImmediates(table: table) } - 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 visitAsNonNull() 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) - } } diff --git a/Sources/WAT/ParseTextInstruction.swift b/Sources/WAT/ParseTextInstruction.swift index 00fb16b8..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) } @@ -332,19 +345,6 @@ func parseTextInstruction(keyword: String, expressionPars case "i64.trunc_sat_f32_u": return { return try $0.visitConversion(.i64TruncSatF32U) } case "i64.trunc_sat_f64_s": return { return try $0.visitConversion(.i64TruncSatF64S) } case "i64.trunc_sat_f64_u": return { return try $0.visitConversion(.i64TruncSatF64U) } - 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 "as_non_null": return { return try $0.visitAsNonNull() } - 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) } default: return nil } } diff --git a/Sources/WAT/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index dbd44fe3..be608bd4 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -388,7 +388,7 @@ struct ExpressionParser { } else if try parser.takeKeyword("extern") { return .externRef } else if let id = try parser.takeIndexOrId() { - let (decl, index) = try wat.types.resolve(use: id) + let (_, index) = try wat.types.resolve(use: id) return .concrete(typeIndex: UInt32(index)) } throw WatParserError("expected \"func\", \"extern\" or type index", location: parser.lexer.location()) @@ -468,13 +468,13 @@ extension ExpressionParser { return UInt32(try wat.types.resolve(use: use).index) } mutating func visitReturnCallRef(wat: inout Wat) throws -> UInt32 { - return 0 + return try visitCallRef(wat: &wat) } mutating func visitBrOnNull(wat: inout Wat) throws -> UInt32 { - return 0 + return try labelIndex() } mutating func visitBrOnNonNull(wat: inout Wat) throws -> UInt32 { - return 0 + return try labelIndex() } mutating func visitCallIndirect(wat: inout Wat) throws -> (typeIndex: UInt32, tableIndex: UInt32) { let tableIndex: UInt32 diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index 14c42f8b..7f3859e0 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -409,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 ( @@ -462,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") { @@ -513,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() diff --git a/Sources/WAT/WAT.swift b/Sources/WAT/WAT.swift index a5fa3b70..ada38bbf 100644 --- a/Sources/WAT/WAT.swift +++ b/Sources/WAT/WAT.swift @@ -237,7 +237,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws -> Wat { } switch decl.kind { - case let .type(decl): break + case .type: break case let .function(decl): try checkImportOrder(decl.importNames) let index = try functionsMap.add(decl) diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index fbddbaac..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 @@ -62,6 +66,10 @@ protocol BinaryInstructionDecoder { @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 @@ -86,14 +94,6 @@ protocol BinaryInstructionDecoder { @inlinable mutating func visitTableGrow() throws -> UInt32 /// Decode `table.size` immediates @inlinable mutating func visitTableSize() throws -> UInt32 - /// Decode `call_ref` immediates - @inlinable mutating func visitCallRef() throws -> UInt32 - /// Decode `return_call_ref` immediates - @inlinable mutating func visitReturnCallRef() 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 } @inlinable func parseBinaryInstruction(visitor: inout V, decoder: inout D) throws -> Bool { @@ -526,7 +526,7 @@ func parseBinaryInstruction( let (functionIndex) = try decoder.visitRefFunc() try visitor.visitRefFunc(functionIndex: functionIndex) case 0xD4: - try visitor.visitAsNonNull() + try visitor.visitRefAsNonNull() case 0xD5: let (relativeDepth) = try decoder.visitBrOnNull() try visitor.visitBrOnNull(relativeDepth: relativeDepth) diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 6a06418e..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) @@ -208,6 +210,9 @@ public enum Instruction: Equatable { 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` @@ -226,11 +231,6 @@ public enum Instruction: Equatable { case `tableSet`(table: UInt32) case `tableGrow`(table: UInt32) case `tableSize`(table: UInt32) - case `callRef`(typeIndex: UInt32) - case `returnCallRef`(typeIndex: UInt32) - case `asNonNull` - case `brOnNull`(relativeDepth: UInt32) - case `brOnNonNull`(relativeDepth: UInt32) } /// A visitor that visits all instructions by a single visit method. @@ -255,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)) } @@ -274,6 +276,9 @@ extension AnyInstructionVisitor { 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) } @@ -292,11 +297,6 @@ extension AnyInstructionVisitor { public mutating func visitTableSet(table: UInt32) throws { return try self.visit(.tableSet(table: table)) } public mutating func visitTableGrow(table: UInt32) throws { return try self.visit(.tableGrow(table: table)) } public mutating func visitTableSize(table: UInt32) throws { return try self.visit(.tableSize(table: table)) } - 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 visitAsNonNull() throws { return try self.visit(.asNonNull) } - 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)) } } /// A visitor for WebAssembly instructions. @@ -334,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. @@ -372,6 +376,12 @@ public protocol InstructionVisitor { 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. @@ -408,16 +418,6 @@ public protocol InstructionVisitor { mutating func visitTableGrow(table: UInt32) throws /// Visiting `table.size` instruction. mutating func visitTableSize(table: UInt32) throws - /// Visiting `call_ref` instruction. - mutating func visitCallRef(typeIndex: UInt32) throws - /// Visiting `return_call_ref` instruction. - mutating func visitReturnCallRef(typeIndex: UInt32) throws - /// Visiting `as_non_null` instruction. - mutating func visitAsNonNull() throws - /// Visiting `br_on_null` instruction. - mutating func visitBrOnNull(relativeDepth: UInt32) throws - /// Visiting `br_on_non_null` instruction. - mutating func visitBrOnNonNull(relativeDepth: UInt32) throws } extension InstructionVisitor { @@ -439,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) @@ -458,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() @@ -476,11 +481,6 @@ extension InstructionVisitor { case let .tableSet(table): return try visitTableSet(table: table) case let .tableGrow(table): return try visitTableGrow(table: table) case let .tableSize(table): return try visitTableSize(table: table) - case let .callRef(typeIndex): return try visitCallRef(typeIndex: typeIndex) - case let .returnCallRef(typeIndex): return try visitReturnCallRef(typeIndex: typeIndex) - case .asNonNull: return try visitAsNonNull() - case let .brOnNull(relativeDepth): return try visitBrOnNull(relativeDepth: relativeDepth) - case let .brOnNonNull(relativeDepth): return try visitBrOnNonNull(relativeDepth: relativeDepth) } } } @@ -502,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 {} @@ -521,6 +523,9 @@ extension InstructionVisitor { 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 {} @@ -539,10 +544,5 @@ extension InstructionVisitor { public mutating func visitTableSet(table: UInt32) throws {} public mutating func visitTableGrow(table: UInt32) throws {} public mutating func visitTableSize(table: UInt32) throws {} - public mutating func visitCallRef(typeIndex: UInt32) throws {} - public mutating func visitReturnCallRef(typeIndex: UInt32) throws {} - public mutating func visitAsNonNull() throws {} - public mutating func visitBrOnNull(relativeDepth: UInt32) throws {} - public mutating func visitBrOnNonNull(relativeDepth: UInt32) throws {} } diff --git a/Tests/WATTests/EncoderTests.swift b/Tests/WATTests/EncoderTests.swift index 861777f0..e99f3d27 100644 --- a/Tests/WATTests/EncoderTests.swift +++ b/Tests/WATTests/EncoderTests.swift @@ -165,9 +165,7 @@ class EncoderTests: XCTestCase { guard case var .text(wat) = directive.source else { continue } - _ = wat = wat - // TODO: Enable smoke check for encoding - // _ = try wat.encode() + _ = try wat.encode() case .assertMalformed(let module, let message): checkMalformed(wast: wastFile, module: module, message: message, recordFail: {}) default: diff --git a/Utilities/Instructions.json b/Utilities/Instructions.json index 945e8b6e..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 ], @@ -54,6 +56,9 @@ ["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" ], @@ -201,10 +206,5 @@ ["saturatingFloatToInt", "i64.trunc_sat_f32_s" , ["0xFC", "0x04"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f32_u" , ["0xFC", "0x05"], [] , "conversion"], ["saturatingFloatToInt", "i64.trunc_sat_f64_s" , ["0xFC", "0x06"], [] , "conversion"], - ["saturatingFloatToInt", "i64.trunc_sat_f64_u" , ["0xFC", "0x07"], [] , "conversion"], - ["functionReferences" , "call_ref" , ["0x14"] , [["typeIndex", "UInt32"]] , null ], - ["functionReferences" , "return_call_ref" , ["0x15"] , [["typeIndex", "UInt32"]] , null ], - ["functionReferences" , "as_non_null" , ["0xd4"] , [] , null ], - ["functionReferences" , "br_on_null" , ["0xd5"] , [["relativeDepth", "UInt32"]] , null ], - ["functionReferences" , "br_on_non_null" , ["0xd6"] , [["relativeDepth", "UInt32"]] , null ] + ["saturatingFloatToInt", "i64.trunc_sat_f64_u" , ["0xFC", "0x07"], [] , "conversion"] ] From 8c91e3d4186756444f1105023e5129d5db655b28 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 10 Jan 2025 18:23:04 +0900 Subject: [PATCH 18/18] WasmParser: Test function-references --- Package.swift | 2 +- Tests/WATTests/EncoderTests.swift | 40 +++++++------ Tests/WasmParserTests/ParserTests.swift | 76 +++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 Tests/WasmParserTests/ParserTests.swift 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/Tests/WATTests/EncoderTests.swift b/Tests/WATTests/EncoderTests.swift index e99f3d27..0f52f6df 100644 --- a/Tests/WATTests/EncoderTests.swift +++ b/Tests/WATTests/EncoderTests.swift @@ -145,6 +145,27 @@ 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 @@ -154,24 +175,7 @@ class EncoderTests: XCTestCase { Spectest.testsuitePath.appendingPathComponent("proposals/function-references") ], include: [], exclude: [] ) { - 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 - } - } + try smokeCheck(wastFile: wastFile) } } 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) + } + } +}