From abf4c6cb5d9e52d0e3b8de14cac5859a26dab549 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 17:00:10 +0000 Subject: [PATCH 1/2] Add annotations proposal implementation --- Package.swift | 5 +- README.md | 2 +- Sources/WAT/BinaryEncoding/Encoder.swift | 72 ++++- Sources/WAT/Lexer.swift | 196 +++++++++++- Sources/WAT/Parser.swift | 93 +++++- Sources/WAT/Parser/ExpressionParser.swift | 19 +- Sources/WAT/Parser/WastParser.swift | 35 +- Sources/WAT/Parser/WatParser.swift | 8 + Sources/WAT/WAT.swift | 83 ++++- Sources/WasmTools/WasmTools.swift | 6 +- Tests/WATTests/EncoderTests.swift | 372 +++++++++++++++++++++- Tests/WATTests/Spectest.swift | 5 +- Vendor/dependencies.json | 19 +- 13 files changed, 858 insertions(+), 57 deletions(-) diff --git a/Package.swift b/Package.swift index 7fa1d93e..96c79f55 100644 --- a/Package.swift +++ b/Package.swift @@ -88,10 +88,7 @@ let package = Package( .testTarget( name: "WATTests", dependencies: [ - .target( - name: "WasmTools", - condition: .when(traits: ["ComponentModel"]) - ), + "WasmTools", "WAT", ] ), diff --git a/README.md b/README.md index 7c374a44..f8f6e0b2 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Proposals are grouped by their [phase](https://github.com/WebAssembly/meetings/b | [Tail Call](https://github.com/WebAssembly/tail-call) | ✅ Implemented | [0.1.4] | | [Typed Function References](https://github.com/WebAssembly/function-references) | 🚧 Parser implemented | [0.2.0] | | [Branch Hinting](https://github.com/WebAssembly/branch-hinting) | ❌ Not implemented | | -| [Custom Annotation Syntax in the Text Format](https://github.com/WebAssembly/annotations) | ❌ Not implemented | | +| [Custom Annotation Syntax in the Text Format](https://github.com/WebAssembly/annotations) | ✅ Implemented | `main` branch | | [Exception Handling](https://github.com/WebAssembly/exception-handling) | ❌ Not implemented | | | [Extended Constant Expressions](https://github.com/WebAssembly/extended-const) | ❌ Not implemented | | | [Garbage Collection](https://github.com/WebAssembly/gc) | ❌ Not implemented | | diff --git a/Sources/WAT/BinaryEncoding/Encoder.swift b/Sources/WAT/BinaryEncoding/Encoder.swift index c1bc714d..af023ca0 100644 --- a/Sources/WAT/BinaryEncoding/Encoder.swift +++ b/Sources/WAT/BinaryEncoding/Encoder.swift @@ -29,7 +29,17 @@ package struct Encoder { output.append(contentsOf: contentEncoder.output) } - package mutating func encodeVector( + /// Overload accepting any RawRepresentable for ergonomic use with enums. + mutating func section, E: Error>(id: ID, _ sectionContent: (inout Encoder) throws(E) -> Void) throws(E) { + try section(id: id.rawValue, sectionContent) + } + + /// Append a single byte from any RawRepresentable value. + mutating func append>(_ value: T) { + output.append(value.rawValue) + } + + mutating func encodeVector( _ values: Source, encodeElement: (Source.Element, inout Encoder) throws(E) -> Void ) throws(E) { writeUnsignedLEB128(UInt32(values.count)) @@ -311,7 +321,7 @@ extension WAT.WatParser.ElementDecl { case .refNull(let type): try exprEncoder.visitRefNull(type: type) default: - throw WatParserError("unexpected instruction in element expression (\(instruction)", location: nil) + throw WatParserError("unexpected instruction in element expression \(instruction)", location: nil) } try exprEncoder.visitEnd() encoder.output.append(contentsOf: exprEncoder.encoder.output) @@ -582,22 +592,28 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> encoder.writeHeader() var codeEncoder = Encoder() - let functions = module.functionsMap.compactMap { (function: WatParser.FunctionDecl) -> ([WatParser.LocalDecl], WatParser.FunctionDecl)? in + let functions = module.functionsMap.enumerated().compactMap { (index: Int, function: WatParser.FunctionDecl) -> (Int, [WatParser.LocalDecl], WatParser.FunctionDecl)? in guard case .definition(let locals, _) = function.kind else { return nil } - return (locals, function) + return (index, locals, function) } var functionSection: [UInt32] = [] var hasDataSegmentInstruction = false var functionLabelNames: [[(Int, String)]] = [] + // Track function type indices for name section (maps function definition index to type index) + var functionTypeIndices: [Int: Int] = [:] + // Track label names per function for name section subsection 3 + var labelNamesPerFunction: [(funcIndex: Int, labels: [(index: Int, name: String)])] = [] + var definitionIndex = 0 + if !functions.isEmpty { try codeEncoder.section(id: 0x0A) { encoder throws(WatParserError) in try encoder.encodeVector( functions, encodeElement: { source, encoder throws(WatParserError) in - let (locals, function) = source + let (funcIndex, locals, function) = source var exprEncoder = ExpressionEncoder() // Encode locals var localsEntries: [(type: ValueType, count: UInt32)] = [] @@ -625,21 +641,37 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } + let cs = module.customSections + + func emitCustomSections(placement: CustomSectionDecl.Placement, encoder: inout Encoder) { + for section in cs where section.placement == placement { + encoder.section(id: 0) { encoder in + section.name.encode(to: &encoder) + encoder.output.append(contentsOf: section.content) + } + } + } + // Section 1: Type section + emitCustomSections(placement: .before(.type), encoder: &encoder) if !module.types.isEmpty { encoder.section(id: 0x01) { encoder in encoder.encodeVector(module.types, transform: \.type.signature) } } + emitCustomSections(placement: .after(.type), encoder: &encoder) // Section 2: Import section + emitCustomSections(placement: .before(.import), encoder: &encoder) if !module.imports.isEmpty { encoder.section(id: 0x02) { encoder in encoder.encodeVector(module.imports) } } + emitCustomSections(placement: .after(.import), encoder: &encoder) // Section 3: Function section + emitCustomSections(placement: .before(.func), encoder: &encoder) if !functionSection.isEmpty { encoder.section(id: 0x03) { encoder in encoder.encodeVector(functionSection) { typeIndex, encoder in @@ -647,8 +679,10 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } } + emitCustomSections(placement: .after(.func), encoder: &encoder) // Section 4: Table section + emitCustomSections(placement: .before(.table), encoder: &encoder) let tables = module.tablesMap.definitions() if !tables.isEmpty { try encoder.section(id: 0x04) { encoder throws(WatParserError) in @@ -657,16 +691,20 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } } + emitCustomSections(placement: .after(.table), encoder: &encoder) // Section 5: Memory section + emitCustomSections(placement: .before(.memory), encoder: &encoder) let memories = module.memories.definitions() if !memories.isEmpty { encoder.section(id: 0x05) { encoder in encoder.encodeVector(memories) } } + emitCustomSections(placement: .after(.memory), encoder: &encoder) // Section 6: Global section + emitCustomSections(placement: .before(.global), encoder: &encoder) let globals = module.globals.definitions() if !globals.isEmpty { try encoder.section(id: 0x06) { encoder throws(WatParserError) in @@ -675,8 +713,10 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } } + emitCustomSections(placement: .after(.global), encoder: &encoder) // Section 7: Export section + emitCustomSections(placement: .before(.export), encoder: &encoder) if !module.exports.isEmpty { encoder.section(id: 0x07) { encoder in encoder.encodeVector(module.exports) { export, encoder in @@ -684,15 +724,19 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } } + emitCustomSections(placement: .after(.export), encoder: &encoder) // Section 8: Start section + emitCustomSections(placement: .before(.start), encoder: &encoder) if let start = module.start { encoder.section(id: 0x08) { encoder in encoder.writeUnsignedLEB128(start) } } + emitCustomSections(placement: .after(.start), encoder: &encoder) // Section 9: Element section + emitCustomSections(placement: .before(.elem), encoder: &encoder) if !module.elementsMap.isEmpty { try encoder.section(id: 0x09) { encoder throws(WatParserError) in try encoder.encodeVector(module.elementsMap) { element, encoder throws(WatParserError) in @@ -700,6 +744,7 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } } + emitCustomSections(placement: .after(.elem), encoder: &encoder) // Section 12: DataCount section if !module.data.isEmpty, hasDataSegmentInstruction { @@ -709,9 +754,12 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } // Section 10: Code section + emitCustomSections(placement: .before(.code), encoder: &encoder) encoder.output.append(contentsOf: codeEncoder.output) + emitCustomSections(placement: .after(.code), encoder: &encoder) // Section 11: Data section + emitCustomSections(placement: .before(.data), encoder: &encoder) if !module.data.isEmpty { try encoder.section(id: 0x0B) { encoder throws(WatParserError) in try encoder.encodeVector(module.data) { data, encoder throws(WatParserError) in @@ -719,12 +767,20 @@ func encode(module: inout Wat, options: EncodeOptions) throws(WatParserError) -> } } } + emitCustomSections(placement: .after(.data), encoder: &encoder) + + // Unplaced custom sections go after all standard sections + emitCustomSections(placement: .unplaced, encoder: &encoder) // (Optional) Name Section if options.nameSection { - try encodeNameSection(module: &module, options: options, encoder: &encoder, functions: functions, functionLabelNames: functionLabelNames) + try encodeNameSection(module: &module, options: options, encoder: &encoder, functions: functions.map { ($0.1, $0.2) }, functionLabelNames: functionLabelNames) } + // "last" placement goes after everything including the name section + emitCustomSections(placement: .before(.last), encoder: &encoder) + emitCustomSections(placement: .after(.last), encoder: &encoder) + return encoder.output } @@ -851,9 +907,9 @@ private func encodeNameSection( encoder.section(id: 0) { encoder in encoder.encode("name") - if let moduleId = module.id { + if let moduleName = module.id { encoder.section(id: 0) { encoder in - encoder.encode(String(moduleId.dropFirst())) // Drop "$" prefix + encoder.encode(moduleName.nameValue) } } if !functionNames.isEmpty { diff --git a/Sources/WAT/Lexer.swift b/Sources/WAT/Lexer.swift index 94ede4fb..052ce742 100644 --- a/Sources/WAT/Lexer.swift +++ b/Sources/WAT/Lexer.swift @@ -5,6 +5,9 @@ enum TokenKind: Equatable { case rightParen case lineComment case blockComment + /// A recognized annotation like `(@name`. The associated value is the annotation id (e.g. "name"). + /// The lexer returns this token after consuming `(@id` but before consuming the body or closing `)`. + case annotation(String) case id case keyword case string([UInt8]) @@ -37,6 +40,14 @@ enum IntegerToken: Equatable { struct Token { let range: Range let kind: TokenKind + /// For quoted identifiers ($"..."), the decoded string bytes (not including the `$` prefix). + let quotedIdBytes: [UInt8]? + + init(range: Range, kind: TokenKind, quotedIdBytes: [UInt8]? = nil) { + self.range = range + self.kind = kind + self.quotedIdBytes = quotedIdBytes + } func text(from lexer: Lexer) -> String { String(lexer.cursor.input[range]) @@ -173,14 +184,21 @@ struct Lexer { } } + /// Temporarily stores decoded bytes from the most recently lexed quoted identifier ($"..."). + /// Set by classifyToken, consumed by rawLex. + private var pendingQuotedIdBytes: [UInt8]? = nil + /// Lex the next token without skipping comments mutating func rawLex() throws(WatParserError) -> Token? { guard let (start, initialChar) = peekNonWhitespaceChar() else { return nil } + pendingQuotedIdBytes = nil guard let kind = try classifyToken(initialChar) else { return nil } let end = cursor.nextIndex - return Token(range: start.. Location { @@ -195,6 +213,9 @@ struct Lexer { case ";": _ = cursor.next() return try lexBlockComment() + case "@": + _ = cursor.next() + return try lexAnnotation() default: return .leftParen } case ")": @@ -202,30 +223,31 @@ struct Lexer { return .rightParen case ";": _ = cursor.next() - // Lex ";; ..." line comment guard cursor.eat(";") else { throw cursor.createError("Expected ';' after ';' line comment") } - while let char = cursor.next() { - switch char { - case "\r": - if cursor.peek() == "\n" { - _ = cursor.next() - } - return .lineComment - case "\n": - return .lineComment - default: break - } - } - // source file ends with line comment + skipLineComment() return .lineComment case "\"", _ where isIdChar(initialChar): let (kind, text) = try lexReservedChars(initial: initialChar) switch kind { + case .quotedId(let bytes): + // $"..." quoted identifier form + guard !bytes.isEmpty else { + throw cursor.createError("empty identifier") + } + guard String(validating: bytes, as: UTF8.self) != nil else { + throw cursor.createError("malformed UTF-8 encoding") + } + pendingQuotedIdBytes = bytes + return .id case .idChars: if initialChar == "$" { + // id ::= '$' idchar+ — must have at least one char after '$' + guard text.count > 1 else { + throw cursor.createError("empty identifier") + } return .id } do { @@ -303,6 +325,23 @@ struct Lexer { } } + /// Skip a line comment body. The leading `;;` has already been consumed. + /// Consumes through the end of line (or EOF). + private mutating func skipLineComment() { + while let char = cursor.next() { + switch char { + case "\r": + if cursor.peek() == "\n" { + _ = cursor.next() + } + return + case "\n": + return + default: break + } + } + } + private mutating func lexBlockComment() throws(WatParserError) -> TokenKind { var level = 1 while true { @@ -328,6 +367,112 @@ struct Lexer { } } + /// Recognized annotation IDs that the parser needs to handle. + private static let recognizedAnnotations: Set = ["name", "custom"] + + /// Read an annotation ID after `(@` has been consumed. + /// Handles both idchar-form (`@id`) and string-form (`@"id"`). + private mutating func readAnnotationId() throws(WatParserError) -> String { + if let ch = cursor.peek(), ch == "\"" { + // String-form: (@"id" ...) + _ = cursor.next() + let str = try readString() + guard !str.isEmpty else { + throw cursor.createError("empty annotation id") + } + guard let id = String(validating: str, as: UTF8.self) else { + throw cursor.createError("malformed UTF-8 encoding") + } + return id + } else if let ch = cursor.peek(), isIdChar(ch) { + // idchar-form: (@id ...) + let idStart = cursor.nextIndex + _ = cursor.next() + while let next = cursor.peek(), isIdChar(next) { + _ = cursor.next() + } + return String(cursor.input[idStart.. TokenKind { + let annotationId = try readAnnotationId() + + if Self.recognizedAnnotations.contains(annotationId) { + return .annotation(annotationId) + } + + try skipAnnotationBody() + return .blockComment + } + + /// Skip the body of an unrecognized annotation, tracking paren depth. + /// Called after the annotation ID has been consumed. + /// + /// This method has its own token dispatch rather than delegating to + /// `classifyToken` because annotation bodies differ from top-level WAT + /// in three ways: + /// - A lone `;` is legal body content (not the start of a line comment). + /// - Non-ASCII and control characters must be rejected (`classifyToken` + /// returns `.unknown` instead). + /// - `(@)` is a valid parenthesized group (not a malformed annotation), + /// so `(@` must peek ahead before consuming the annotation ID. + private mutating func skipAnnotationBody() throws(WatParserError) { + var depth = 1 + while true { + guard let char = cursor.peek() else { + throw cursor.createError("unclosed annotation") + } + switch char { + case "(": + _ = cursor.next() + if cursor.peek() == ";" { + // Block comment: (; ... ;) — fully consumed, no depth change + _ = cursor.next() + _ = try lexBlockComment() + } else { + // Regular paren group or nested annotation + if cursor.peek() == "@" { + // Consume the annotation ID so body-skipping doesn't misparse it. + let charAfterAt = cursor.peek(at: 1) + if let c = charAfterAt, isIdChar(c) || c == "\"" { + _ = cursor.next() // consume @ + _ = try readAnnotationId() + } + } + depth += 1 + } + case ")": + _ = cursor.next() + depth -= 1 + if depth == 0 { + return + } + case "\"": + // String inside annotation body + _ = cursor.next() + _ = try readString() + case ";": + _ = cursor.next() + if cursor.eat(";") { + skipLineComment() + } + // A lone `;` is just a regular character in annotation body + default: + if isIllegalWATChar(char) { + throw cursor.createError("illegal character") + } + _ = cursor.next() + } + } + } + private mutating func peekNonWhitespaceChar() -> (index: Lexer.Index, byte: Unicode.Scalar)? { guard var char = cursor.peek() else { return nil } var start: Lexer.Index = cursor.nextIndex @@ -342,6 +487,17 @@ struct Lexer { return (start, char) } + /// Returns true if the scalar is not a legal WAT character outside of strings. + /// Legal: U+09 (tab), U+0A (LF), U+0D (CR), U+20..U+7E (printable ASCII). + private func isIllegalWATChar(_ char: Unicode.Scalar) -> Bool { + let value = char.value + return value <= 0x08 + || (value >= 0x0B && value <= 0x0C) + || (value >= 0x0E && value <= 0x1F) + || value == 0x7F + || value >= 0x80 + } + // https://webassembly.github.io/spec/core/text/values.html#text-idchar private func isIdChar(_ char: Unicode.Scalar) -> Bool { // NOTE: Intentionally not using Range here to keep fast enough even in debug mode @@ -359,6 +515,8 @@ struct Lexer { private enum ReservedKind { case string([UInt8]) case idChars + /// A quoted identifier: `$"..."` form with decoded string bytes + case quotedId([UInt8]) case unknown } @@ -387,7 +545,7 @@ struct Lexer { } else if numberOfIdChars == 0, strings.count == 1 { return (.string(strings[0]), text) } else if numberOfIdChars == 1, strings.count == 1, initial == "$" { - return (.idChars, text) + return (.quotedId(strings[0]), text) } return (.unknown, text) } @@ -444,6 +602,12 @@ struct Lexer { throw cursor.createError("Invalid escape sequence: \(other)") } } else { + // Validate: char must be a valid stringchar (not a control character) + // WAT spec: char ::= U+20 | ... | U+7E | + let value = char.value + if value < 0x20 || value == 0x7F { + throw cursor.createError("illegal character in string") + } append(char) } } diff --git a/Sources/WAT/Parser.swift b/Sources/WAT/Parser.swift index 027c38b4..d6854c2e 100644 --- a/Sources/WAT/Parser.swift +++ b/Sources/WAT/Parser.swift @@ -338,10 +338,96 @@ internal struct Parser { return token } + /// Consumes a `(@custom "name" [(before|after kind)] "content"*)` annotation if present. + /// Returns the parsed declaration, or nil if no `@custom` annotation is next. + mutating func takeCustomAnnotation() throws(WatParserError) -> CustomSectionDecl? { + guard let token = try peek(), case .annotation("custom") = token.kind else { + return nil + } + try consume() + + // Section name (must be a string literal) + guard let nameBytes = try takeStringBytes() else { + throw WatParserError( + "@custom annotation: missing section name", location: lexer.location() + ) + } + let name = try nameBytes.withUnsafeBufferPointer { buffer throws(WatParserError) in + guard let value = String._tryFromUTF8(buffer) else { + throw WatParserError( + "@custom annotation: malformed UTF-8 encoding", location: lexer.location() + ) + } + return value + } + + // Optional placement directive: (before|after sectionKind) + var placement: CustomSectionDecl.Placement = .unplaced + if try peek(.leftParen) != nil { + try consume() // consume ( + let kw = try expectKeyword() + guard kw == "before" || kw == "after" else { + throw WatParserError( + "@custom annotation: malformed placement", location: lexer.location() + ) + } + guard let sectionKw = try peekKeyword() else { + throw WatParserError( + "@custom annotation: malformed section kind", location: lexer.location() + ) + } + guard let sectionKind = CustomSectionDecl.SectionKind(rawValue: sectionKw) else { + throw WatParserError( + "@custom annotation: malformed section kind", location: lexer.location() + ) + } + try consume() // consume section kind keyword + try expect(.rightParen) + placement = kw == "before" ? .before(sectionKind) : .after(sectionKind) + } + + // Content strings (zero or more) + var content: [UInt8] = [] + while let bytes = try takeStringBytes() { + content.append(contentsOf: bytes) + } + + // If the next token is not ) then something unexpected is here + if try peek(.rightParen) == nil { + throw WatParserError( + "@custom annotation: unexpected token", location: lexer.location() + ) + } + try expect(.rightParen) + + return CustomSectionDecl( + name: name, placement: placement, content: content + ) + } + mutating func takeId() throws(WatParserError) -> Name? { guard let token = try peek(.id) else { return nil } try consume() - return Name(value: token.text(from: lexer), location: token.location(in: lexer)) + let value: String + if let quotedIdBytes = token.quotedIdBytes { + // Quoted identifier $"..." — normalize to plain $name form + value = "$" + String(decoding: quotedIdBytes, as: UTF8.self) + } else { + value = token.text(from: lexer) + } + return Name(value: value, location: token.location(in: lexer)) + } + + /// Consumes a `(@name "string")` annotation if present. + /// Returns the annotation's string argument, or nil if no such annotation is next. + mutating func takeNameAnnotation() throws(WatParserError) -> String? { + guard let token = try peek(), case .annotation("name") = token.kind else { + return nil + } + try consume() + let name = try expectString() + try expect(.rightParen) + return name } mutating func skipParenBlock() throws(WatParserError) { @@ -353,6 +439,11 @@ internal struct Parser { depth += 1 case .rightParen: depth -= 1 + case .annotation: + // Recognized annotations consume their opening `(` during lexing, + // so we must account for it in the depth count. The annotation's + // closing `)` will naturally decrement depth later. + depth += 1 default: break } diff --git a/Sources/WAT/Parser/ExpressionParser.swift b/Sources/WAT/Parser/ExpressionParser.swift index f072af1e..dbd23aae 100644 --- a/Sources/WAT/Parser/ExpressionParser.swift +++ b/Sources/WAT/Parser/ExpressionParser.swift @@ -5,6 +5,9 @@ struct ExpressionParser where Visitor.VisitorError typealias LocalsMap = NameMapping private struct LabelStack { private var stack: [String?] = [] + /// Collected label names for name section: (labelIndex, name) + private(set) var collectedLabels: [(index: Int, name: String)] = [] + private var nextLabelIndex: Int = 0 /// - Returns: The depth of the label of the given name in the stack. /// e.g. `(block $A (block $B (br $A)))`, then `["A"]` at `br $A` will return 1. @@ -24,6 +27,10 @@ struct ExpressionParser where Visitor.VisitorError mutating func push(_ name: Name?) { stack.append(name?.value) + if let name = name { + collectedLabels.append((index: nextLabelIndex, name: String(name.value.dropFirst()))) + } + nextLabelIndex += 1 } mutating func pop() { @@ -42,6 +49,11 @@ struct ExpressionParser where Visitor.VisitorError private(set) var collectedLabelNames: [(Int, String)] = [] private var nextLabelIndex: Int = 0 + /// Label names collected during parsing, for name section emission. + var collectedLabels: [(index: Int, name: String)] { + labelStack.collectedLabels + } + init( type: WatParser.FunctionType, locals: [WatParser.LocalDecl], @@ -350,7 +362,12 @@ struct ExpressionParser where Visitor.VisitorError } /// Parse a single instruction without consuming the surrounding parentheses and instruction keyword. - private mutating func parseTextInstruction(keyword: String, wat: inout Wat) throws(WatParserError) -> ((inout Visitor) throws(WatParserError) -> Void) { + private mutating func parseTextInstruction( + keyword: String, + wat: inout Wat + ) throws(WatParserError) -> ( + (inout Visitor) throws(WatParserError) -> Void + ) { switch keyword { case "select": // Special handling for "select", which have two variants 1. with type, 2. without type diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index 715635f8..4810e078 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -255,7 +255,7 @@ public enum WastDirective { let message = try wastParser.parser.expectString() try wastParser.parser.expect(.rightParen) return .assertInvalid(module: module, message: message) - case "assert_malformed": + case "assert_malformed", "assert_malformed_custom": try wastParser.parser.consume() let module = try wastParser.parens { wastParser throws(WatParserError) in try ModuleDirective.parse(wastParser: &wastParser) } let message = try wastParser.parser.expectString() @@ -320,11 +320,28 @@ public struct ModuleDirective { /// The location of the module in the source public let location: Location + /// The effective module name from either `@name` annotation or `$id`. + /// `@name` from the WAT source takes precedence over `$id`. + var moduleName: ModuleName? { + if case .text(let wat) = source, let watId = wat.id { + return watId + } + if let id { + return .identifier(id) + } + return nil + } + static func parse(wastParser: inout WastParser) throws(WatParserError) -> ModuleDirective { let location = wastParser.parser.lexer.location() try wastParser.parser.expectKeyword("module") let id = try wastParser.parser.takeId() - let source = try ModuleSource.parse(wastParser: &wastParser) + var source = try ModuleSource.parse(wastParser: &wastParser) + // Propagate $id into the Wat struct when @name didn't already set it + if let id, case .text(var wat) = source, wat.id == nil { + wat.id = .identifier(id.value) + source = .text(wat) + } return ModuleDirective(source: source, id: id?.value, location: location) } } @@ -346,7 +363,19 @@ public enum ModuleSource { } } - let watModule = try parseWAT(&wastParser.parser, features: wastParser.features) + // Consume @name annotation if present (before module fields) + let nameAnnot = try wastParser.parser.takeNameAnnotation() + if nameAnnot != nil, try wastParser.parser.takeNameAnnotation() != nil { + throw WatParserError("@name annotation: multiple module names", location: wastParser.parser.lexer.location()) + } + + var watModule = try parseWAT(&wastParser.parser, features: wastParser.features) + + // Store @name in the Wat struct + if let nameAnnot { + watModule.id = .annotation(nameAnnot) + } + try wastParser.parser.skipParenBlock() return .text(watModule) } diff --git a/Sources/WAT/Parser/WatParser.swift b/Sources/WAT/Parser/WatParser.swift index 34a1e65f..bb03e750 100644 --- a/Sources/WAT/Parser/WatParser.swift +++ b/Sources/WAT/Parser/WatParser.swift @@ -218,11 +218,19 @@ struct WatParser { case start(id: Parser.IndexOrId) case element(ElementDecl) case data(DataSegmentDecl) + case custom(CustomSectionDecl) } mutating func next() throws(WatParserError) -> ModuleField? { // If we have reached the end of the (module ...) block, return nil guard try !parser.isEndOfParen() else { return nil } + + // Handle @custom annotations at the module-field level + let customLocation = parser.lexer.location() + if let custom = try parser.takeCustomAnnotation() { + return ModuleField(location: customLocation, kind: .custom(custom)) + } + try parser.expect(.leftParen) let location = parser.lexer.location() let keyword = try parser.expectKeyword() diff --git a/Sources/WAT/WAT.swift b/Sources/WAT/WAT.swift index 8364191e..a0c3581d 100644 --- a/Sources/WAT/WAT.swift +++ b/Sources/WAT/WAT.swift @@ -1,5 +1,35 @@ import WasmParser +/// A `@custom` annotation declaring an inline custom section in WAT. +struct CustomSectionDecl { + /// Standard section kinds used in placement directives. + enum SectionKind: String { + case type + case `import` + case `func` + case table + case memory + case global + case export + case start + case elem + case code + case data + case last + } + + /// Where this custom section should be placed in the binary. + enum Placement: Equatable { + case unplaced + case before(SectionKind) + case after(SectionKind) + } + + let name: String + let placement: Placement + let content: [UInt8] +} + /// Options for encoding a WebAssembly module into a binary format. public struct EncodeOptions: Sendable { /// Whether to include the name section. @@ -17,7 +47,7 @@ public struct EncodeOptions: Sendable { /// Transforms a WebAssembly text format (WAT) string into a WebAssembly binary format byte array. /// /// This function supports both core modules and Component Model components (when the `ComponentModel` -/// trait is enabled). It tries to parse as a module first, then falls back to component parsing. +/// trait is enabled). It looks ahead to determine whether the input is a component or core module. /// /// - Parameter input: The WAT string to transform /// - Returns: The WebAssembly binary format byte array @@ -66,10 +96,26 @@ public func wat2wasm( #endif } +/// The origin of a module name — either from a `$id` identifier or an `@name` annotation. +public enum ModuleName: Equatable { + /// From `$id` — value includes the `$` prefix. + case identifier(String) + /// From `(@name "...")` — raw UTF-8 string, no prefix. + case annotation(String) + + /// The name for the name section (`$` prefix stripped for identifiers). + var nameValue: String { + switch self { + case .identifier(let s): String(s.dropFirst()) + case .annotation(let s): s + } + } +} + /// A WAT module representation. public struct Wat { - /// The module name from `(module $name ...)`, including the `$` prefix. - var id: String? = nil + /// The module name from `(module $name ...)` or `(module (@name "...") ...)`. + var id: ModuleName? = nil var types: TypesMap let functionsMap: NameMapping let tablesMap: NameMapping @@ -81,7 +127,7 @@ public struct Wat { let start: FunctionIndex? let imports: [Import] let exports: [Export] - let customSections = [CustomSection]() + let customSections: [CustomSectionDecl] let features: WasmFeatureSet let parser: Parser @@ -99,6 +145,7 @@ public struct Wat { start: nil, imports: [], exports: [], + customSections: [], features: features, parser: Parser("") ) @@ -142,8 +189,18 @@ public func parseWAT(_ input: String, features: WasmFeatureSet = .default) throw var wat: Wat if try parser.takeParenBlockStart("module") { let moduleId = try parser.takeId() + let nameAnnot = try parser.takeNameAnnotation() + // Reject duplicate @name + if nameAnnot != nil, try parser.takeNameAnnotation() != nil { + throw WatParserError("@name annotation: multiple module names", location: parser.lexer.location()) + } wat = try parseWAT(&parser, features: features) - wat.id = moduleId?.value + // @name takes precedence over $id for the name section + if let nameAnnot { + wat.id = .annotation(nameAnnot) + } else if let moduleId { + wat.id = .identifier(moduleId.value) + } try parser.skipParenBlock() } else { // The root (module) may be omitted @@ -217,6 +274,13 @@ public func parseWAST(_ input: String, features: WasmFeatureSet = .default) thro self.parser = ComponentWastParser(input, features: features) } + /// Returns the current parser location without consuming any tokens. + /// Useful for capturing the location before `nextDirective()` which may throw. + func currentLocation() -> Location { + (try? parser.parser.peek()?.location(in: parser.parser.lexer)) + ?? parser.parser.lexer.location() + } + /// Parses the next directive in the Component WAST script. /// /// - Returns: A tuple containing the parsed directive and its location in the script, @@ -302,6 +366,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws(WatParser var start: Parser.IndexOrId? var exportDecls: [WatParser.ExportDecl] = [] + var customSections: [CustomSectionDecl] = [] var hasNonImport = false func visitDecl(decl: WatParser.ModuleField) throws(WatParserError) { @@ -313,7 +378,10 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws(WatParser } } - func addImport(_ importNames: WatParser.ImportNames, makeDescriptor: @escaping () throws(WatParserError) -> ImportDescriptor) { + func addImport( + _ importNames: WatParser.ImportNames, + makeDescriptor: @escaping () throws(WatParserError) -> ImportDescriptor + ) { importFactories.append { return Result { () throws(WatParserError) in Import( @@ -395,6 +463,8 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws(WatParser throw WatParserError("Multiple start sections", location: location) } start = startIndex + case .custom(let decl): + customSections.append(decl) } } @@ -440,6 +510,7 @@ func parseWAT(_ parser: inout Parser, features: WasmFeatureSet) throws(WatParser start: startIndex, imports: imports, exports: exports, + customSections: customSections, features: features, parser: parser ) diff --git a/Sources/WasmTools/WasmTools.swift b/Sources/WasmTools/WasmTools.swift index 6cd8a587..b1c13106 100644 --- a/Sources/WasmTools/WasmTools.swift +++ b/Sources/WasmTools/WasmTools.swift @@ -26,9 +26,9 @@ package struct WasmToolsInputFile { package let guestPath: String package let content: [UInt8] - package init(guestPath: String, content: [UInt8]) { + package init(guestPath: String, content: some Sequence) { self.guestPath = guestPath - self.content = content + self.content = Array(content) } package init(guestPath: String, content: String) { @@ -238,7 +238,7 @@ package struct Wast2JSONCommand: Codable { package func wast2json( wasmToolsPath: String = defaultWasmToolsPath, - wastContent: [UInt8], + wastContent: some Sequence, wastFileName: String = "input.wast" ) throws -> (json: Wast2JSONOutput, wasmFiles: [String: [UInt8]]) { let inputPath = "/input/\(wastFileName)" diff --git a/Tests/WATTests/EncoderTests.swift b/Tests/WATTests/EncoderTests.swift index 8a80e578..99e9b04c 100644 --- a/Tests/WATTests/EncoderTests.swift +++ b/Tests/WATTests/EncoderTests.swift @@ -1,6 +1,9 @@ import Foundation import Testing +import WAT import WasmParser +import WasmTools +import WasmTypes @testable import WAT @@ -13,6 +16,7 @@ struct EncoderTests { "--enable-memory64", "--enable-tail-call", "--enable-threads", + "--enable-annotations", ] // MARK: - Supporting Types @@ -111,7 +115,13 @@ struct EncoderTests { } case .quote(let bytes): #expect(throws: (any Error).self, diagnostic()) { - _ = try wat2wasm(String(decoding: bytes, as: UTF8.self)) + // Validate UTF-8 encoding before attempting to parse as WAT text. + // String(decoding:as:UTF8.self) silently replaces invalid bytes, + // masking malformed UTF-8 that the spec expects to be rejected. + guard let text = String(bytes: bytes, encoding: .utf8) else { + throw WatParserError("malformed UTF-8 encoding", location: nil) + } + _ = try wat2wasm(text) recordFail() } case .binary: @@ -126,7 +136,9 @@ struct EncoderTests { moduleBinaryFiles: [(binary: URL, name: String?)], wast: URL, tempDir: String, - stats: inout CompatibilityTestStats + stats: inout CompatibilityTestStats, + encodeOptions: EncodeOptions = .default, + stripModuleNamePrefix: Bool = false ) throws { func recordFail() { stats.failed.insert(wast.lastPathComponent) @@ -146,9 +158,15 @@ struct EncoderTests { let expectedBytes = try Array(Data(contentsOf: moduleFile.binary)) do { - // Check module name + // Check module name (strip "$" prefix when comparing against WasmTools) + let actualName: String? + if stripModuleNamePrefix { + actualName = watModule.moduleName?.nameValue + } else { + actualName = watModule.id + } Self.assertEqual( - watModule.id, + actualName, moduleFile.name, description: "module name", watModule: watModule, @@ -157,7 +175,7 @@ struct EncoderTests { ) // Encode and compare module bytes - let moduleBytes = try encodeModule(watModule: watModule) + let moduleBytes = try encodeModule(watModule: watModule, options: encodeOptions) try Self.compareModuleBytes( expected: expectedBytes, actual: moduleBytes, @@ -291,14 +309,14 @@ struct EncoderTests { // MARK: - Module Encoding - private func encodeModule(watModule: ModuleDirective) throws -> [UInt8] { + private func encodeModule(watModule: ModuleDirective, options: EncodeOptions = .default) throws -> [UInt8] { switch watModule.source { - case .text(var watModule): - return try encode(module: &watModule, options: .default) + case .text(var wat): + return try encode(module: &wat, options: options) case .binary(let bytes): return bytes case .quote(let watText): - return try wat2wasm(String(decoding: watText, as: UTF8.self)) + return try WAT.wat2wasm(String(decoding: watText, as: UTF8.self), options: options) } } @@ -306,7 +324,14 @@ struct EncoderTests { #if !(os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)) @Test( - arguments: Spectest.wastFiles(include: [], exclude: []) + arguments: Spectest.wastFiles( + include: [], + exclude: [ + // Tested separately by annotationProposal() since wast2json (WABT) doesn't support the annotations proposal + "annotations.wast", "token.wast", "id.wast", + // Tested separately by dedicated tests since wast2json doesn't support assert_malformed_custom + "name_annot.wast", "custom_annot.wast", + ]) ) func spectest(wastFile: URL) throws { guard let wast2json = TestSupport.lookupExecutable("wast2json") else { @@ -368,6 +393,74 @@ struct EncoderTests { } #endif + #if !os(Android) + /// Test annotation proposal files using WasmTools as reference for binary comparison. + /// WABT's wast2json doesn't support the annotations proposal, so WasmTools is used as fallback. + @Test( + arguments: Spectest.wastFiles(include: ["annotations.wast", "token.wast", "id.wast"]) + ) + func annotationProposalSpectest(wastFile: URL) throws { + var stats = CompatibilityTestStats() + try TestSupport.withTemporaryDirectory { tempDir, shouldRetain in + let watModules: [ModuleDirective] + do { + watModules = try parseWastFile(wast: wastFile, stats: &stats) + } catch { + stats.failed.insert(wastFile.lastPathComponent) + shouldRetain = true + Self.record(wastFile: wastFile, error: error) + return + } + + let wastContent = try Data(contentsOf: wastFile) + let (json, wasmFiles) = try wast2json( + wastContent: wastContent, + wastFileName: wastFile.lastPathComponent + ) + + // Write reference wasm files to temp dir, skipping text-form modules + // (WasmTools stores "module quote" forms as raw text, not compiled Wasm) + var moduleBinaryFiles: [(binary: URL, name: String?)] = [] + for command in json.commands where command.type == "module" { + guard command.moduleType != "text" else { continue } + guard let filename = command.filename, let bytes = wasmFiles[filename] else { continue } + let binaryURL = URL(fileURLWithPath: tempDir).appendingPathComponent(filename) + try Data(bytes).write(to: binaryURL) + moduleBinaryFiles.append((binary: binaryURL, name: command.name)) + } + + // Filter out quote modules from our parsed modules to match + let binaryWatModules = watModules.filter { + switch $0.source { + case .quote: return false + default: return true + } + } + + do { + try compareModules( + watModules: binaryWatModules, + moduleBinaryFiles: moduleBinaryFiles, + wast: wastFile, + tempDir: tempDir, + stats: &stats, + encodeOptions: EncodeOptions(nameSection: true), + stripModuleNamePrefix: true + ) + } catch { + stats.failed.insert(wastFile.lastPathComponent) + shouldRetain = true + Self.record(wastFile: wastFile, error: error) + } + + if !stats.failed.isEmpty { + Issue.record("Failed test cases: \(stats.failed.sorted())") + shouldRetain = true + } + } + } + #endif + @Test func encodeNameSection() throws { let bytes = try wat2wasm( @@ -433,9 +526,8 @@ struct EncoderTests { nameBytes = section.bytes } } - let sectionBytes = try #require(Array(nameBytes ?? [])) let nameParser = NameSectionParser( - stream: StaticByteStream(bytes: sectionBytes) + stream: StaticByteStream(bytes: nameBytes ?? []) ) let parsed = try nameParser.parseAll() #expect(parsed.count == 10) @@ -477,4 +569,260 @@ struct EncoderTests { #expect(elemNames == [0: "myelem"]) #expect(dataNames == [0: "mydata"]) } + + /// Helper to extract the module name from the name section of a compiled Wasm binary. + private func extractModuleName(from bytes: [UInt8]) throws -> String? { + var parser = WasmParser.Parser(bytes: bytes) + var nameBytes: ArraySlice? + while let payload = try parser.parseNext() { + if case .customSection(let section) = payload, section.name == "name" { + nameBytes = section.bytes + } + } + guard let sectionBytes = nameBytes else { return nil } + let nameParser = NameSectionParser( + stream: StaticByteStream(bytes: Array(sectionBytes)) + ) + let parsed = try nameParser.parseAll() + for entry in parsed { + if case .moduleName(let name) = entry { + return name + } + } + return nil + } + + @Test + func encodeNameAnnotation() throws { + // @name alone + let bytes1 = try wat2wasm( + #"(module (@name "Modül"))"#, + options: EncodeOptions(nameSection: true) + ) + #expect(try extractModuleName(from: bytes1) == "Modül") + + // @name with $id — @name takes precedence + let bytes2 = try wat2wasm( + #"(module $moduel (@name "Modül"))"#, + options: EncodeOptions(nameSection: true) + ) + #expect(try extractModuleName(from: bytes2) == "Modül") + } + + @Test + func nameAnnotationMalformed() throws { + // Multiple @name annotations + #expect(throws: (any Error).self) { + _ = try wat2wasm(#"(module (@name "M1") (@name "M2"))"#) + } + + // Misplaced @name after module fields + #expect(throws: (any Error).self) { + _ = try wat2wasm(#"(module (func) (@name "M"))"#) + } + + // Misplaced @name inside a field + #expect(throws: (any Error).self) { + _ = try wat2wasm(#"(module (start $f (@name "M")) (func $f))"#) + } + } + + @Test + func nameAnnotationWast() throws { + let wastContent = """ + (module (@name "Modül")) + (module $moduel (@name "Modül")) + (assert_malformed_custom + (module quote "(module (@name \\"M1\\") (@name \\"M2\\"))") + "@name annotation: multiple module" + ) + (assert_malformed_custom + (module quote "(module (func) (@name \\"M\\"))") + "misplaced @name annotation" + ) + """ + var wast = try parseWAST(wastContent) + var moduleCount = 0 + var assertCount = 0 + while let (directive, _) = try wast.nextDirective() { + switch directive { + case .module: + moduleCount += 1 + case .assertMalformed(let module, _): + assertCount += 1 + // Verify the quoted module actually fails to parse + if case .quote(let bytes) = module.source { + if let text = String(bytes: bytes, encoding: .utf8) { + #expect(throws: (any Error).self) { + _ = try wat2wasm(text) + } + } + } + default: + break + } + } + #expect(moduleCount == 2) + #expect(assertCount == 2) + } + + #if !os(Android) + /// Test `@custom` annotation parsing and encoding. + /// wasm-tools doesn't support `assert_malformed_custom`, so we validate directly + /// rather than comparing against reference binaries. + @Test + func customAnnotationWast() throws { + let wastFile = try #require(Spectest.wastFiles(include: ["custom_annot.wast"]).first) + // parseWastFile validates all assert_malformed_custom directives internally + var stats = CompatibilityTestStats() + let watModules = try parseWastFile(wast: wastFile, stats: &stats) + #expect(stats.failed.isEmpty) + + // Verify that non-quote modules encode successfully + var encodedCount = 0 + for watModule in watModules { + if case .text(let wat) = watModule.source { + _ = try wat.encode() + encodedCount += 1 + } + } + #expect(encodedCount > 0) + } + #endif + + // MARK: - @custom Binary Output Tests + + /// Find all custom sections (id=0) in a Wasm binary, returning (name, content) pairs. + private func findCustomSections(in bytes: [UInt8]) throws -> [(name: String, content: ArraySlice)] { + var parser = WasmParser.Parser(bytes: bytes) + var sections: [(name: String, content: ArraySlice)] = [] + while let payload = try parser.parseNext() { + if case .customSection(let section) = payload { + sections.append((name: section.name, content: section.bytes)) + } + } + return sections + } + + @Test + func customAnnotationBasicEncoding() throws { + // Verify custom section appears in binary output + let bytes = try wat2wasm(#"(module (@custom "test-section" "hello"))"#) + let sections = try findCustomSections(in: bytes) + #expect(sections.count == 1) + #expect(sections[0].name == "test-section") + #expect(sections[0].content.elementsEqual("hello".utf8)) + } + + @Test + func customAnnotationEmptyContent() throws { + let bytes = try wat2wasm(#"(module (@custom "empty"))"#) + let sections = try findCustomSections(in: bytes) + #expect(sections.count == 1) + #expect(sections[0].name == "empty") + #expect(sections[0].content.isEmpty) + } + + @Test + func customAnnotationEmptyName() throws { + let bytes = try wat2wasm(#"(module (@custom "" "data"))"#) + let sections = try findCustomSections(in: bytes) + #expect(sections.count == 1) + #expect(sections[0].name == "") + #expect(sections[0].content.elementsEqual("data".utf8)) + } + + @Test + func customAnnotationMultipleStrings() throws { + // Multiple string literals should be concatenated + let bytes = try wat2wasm(#"(module (@custom "cat" "ab" "cd" "ef"))"#) + let sections = try findCustomSections(in: bytes) + #expect(sections.count == 1) + #expect(sections[0].content.elementsEqual("abcdef".utf8)) + } + + @Test + func customAnnotationOrdering() throws { + // Multiple custom sections should preserve source order + let bytes = try wat2wasm( + """ + (module + (@custom "first" "1") + (@custom "second" "2") + (@custom "third" "3") + ) + """) + let sections = try findCustomSections(in: bytes) + #expect(sections.count == 3) + #expect(sections[0].name == "first") + #expect(sections[1].name == "second") + #expect(sections[2].name == "third") + } + + @Test + func customAnnotationPlacement() throws { + // Custom sections with placement directives should appear at the right positions + // relative to standard sections in the binary. + let bytes = try wat2wasm( + """ + (module + (type (func)) + (@custom "after-type" (after type) "AT") + (@custom "before-func" (before func) "BF") + (func (type 0)) + (@custom "after-global" (after global) "AG") + (global i32 (i32.const 0)) + (@custom "unplaced" "UP") + ) + """) + let sections = try findCustomSections(in: bytes) + // Verify all custom sections are present + let names = sections.map(\.name) + #expect(names.contains("after-type")) + #expect(names.contains("before-func")) + #expect(names.contains("after-global")) + #expect(names.contains("unplaced")) + + // Verify ordering: after-type and before-func should both appear + // between type section and function section, with after-type first + let atIdx = try #require(names.firstIndex(of: "after-type")) + let bfIdx = try #require(names.firstIndex(of: "before-func")) + #expect(atIdx < bfIdx) + } + + @Test + func customAnnotationDuplicateNames() throws { + // Multiple custom sections with the same name are valid + let bytes = try wat2wasm( + """ + (module + (@custom "dup" "a") + (@custom "dup" "b") + (@custom "dup" "c") + ) + """) + let sections = try findCustomSections(in: bytes) + #expect(sections.count == 3) + #expect(sections.allSatisfy { $0.name == "dup" }) + #expect(sections[0].content.elementsEqual("a".utf8)) + #expect(sections[1].content.elementsEqual("b".utf8)) + #expect(sections[2].content.elementsEqual("c".utf8)) + } + + @Test + func customAnnotationMalformedCases() throws { + // Missing section name + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom))"#) } + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom 4))"#) } + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom bla))"#) } + + // Malformed placement + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom "x" here))"#) } + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom "x" (type)))"#) } + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom "x" (aft type)))"#) } + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (@custom "x" (before types)))"#) } + + // Misplaced inside module fields + #expect(throws: (any Error).self) { _ = try wat2wasm(#"(module (func (@custom "x")))"#) } + } } diff --git a/Tests/WATTests/Spectest.swift b/Tests/WATTests/Spectest.swift index a3de5369..038c7d00 100644 --- a/Tests/WATTests/Spectest.swift +++ b/Tests/WATTests/Spectest.swift @@ -16,7 +16,7 @@ enum Spectest { testsuitePath.appendingPathComponent(file) } - static func wastFiles(include: [String] = [], exclude: [String] = ["annotations.wast"]) -> [URL] { + static func wastFiles(include: [String] = [], exclude: [String] = ["id.wast"]) -> [URL] { #if os(Android) return [] #else @@ -25,6 +25,9 @@ enum Spectest { testsuitePath.appendingPathComponent("proposals/memory64"), testsuitePath.appendingPathComponent("proposals/tail-call"), testsuitePath.appendingPathComponent("proposals/threads"), + testsuitePath.appendingPathComponent("proposals/annotations"), + vendorDirectory.appendingPathComponent("annotations/test/custom/name"), + vendorDirectory.appendingPathComponent("annotations/test/custom/custom"), rootDirectory.appendingPathComponent("Tests/WasmKitTests/ExtraSuite"), ].flatMap { try! FileManager.default.contentsOfDirectory(at: $0, includingPropertiesForKeys: nil) diff --git a/Vendor/dependencies.json b/Vendor/dependencies.json index 487355ca..d4833f88 100644 --- a/Vendor/dependencies.json +++ b/Vendor/dependencies.json @@ -1,4 +1,15 @@ { + "spec": { + "repository": "https://github.com/WebAssembly/spec.git", + "revision": "1c9a04e7b9d6998b9a30c6edf40b42d4c2a13c7b", + "categories": ["spec"] + }, + "annotations": { + "repository": "https://github.com/WebAssembly/annotations.git", + "revision": "20a8e4f125d5d856aac392105c35055af9ed49f7", + "sparse-checkout": "test/custom", + "categories": ["default"] + }, "testsuite": { "repository": "https://github.com/WebAssembly/testsuite.git", "revision": "53da17c0936a23f68f97cde4f9346a0a374dc35f", @@ -22,7 +33,7 @@ "wasm-tools-prebuilt": { "url": "https://github.com/bytecodealliance/wasm-tools/releases/download/v1.244.0/wasm-tools-1.244.0-wasm32-wasip1.tar.gz", "sha256": "010f8a3c591f3070d06a1c13830c8a61dcbf6527a62ab289970ed3341f0008e7", - "categories": ["component-model"] + "categories": ["default"] }, "wasm-tools": { "repository": "https://github.com/bytecodealliance/wasm-tools.git", @@ -49,5 +60,11 @@ "repository": "https://github.com/eembc/coremark.git", "revision": "d5fad6bd094899101a4e5fd53af7298160ced6ab", "categories": ["benchmark"] + }, + "annotations": { + "repository": "https://github.com/WebAssembly/annotations.git", + "revision": "20a8e4f125d5d856aac392105c35055af9ed49f7", + "sparse-checkout": "test/custom", + "categories": ["default"] } } From 1b0da454570296a871a7ba1683f953fff37a4dc1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 18:51:35 +0000 Subject: [PATCH 2/2] Fix dependencies.json duplicates --- Vendor/dependencies.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Vendor/dependencies.json b/Vendor/dependencies.json index d4833f88..6c367261 100644 --- a/Vendor/dependencies.json +++ b/Vendor/dependencies.json @@ -60,11 +60,5 @@ "repository": "https://github.com/eembc/coremark.git", "revision": "d5fad6bd094899101a4e5fd53af7298160ced6ab", "categories": ["benchmark"] - }, - "annotations": { - "repository": "https://github.com/WebAssembly/annotations.git", - "revision": "20a8e4f125d5d856aac392105c35055af9ed49f7", - "sparse-checkout": "test/custom", - "categories": ["default"] } }