From c14cc10877d264dec6806b1811f63d814829af48 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 17:42:38 +0800 Subject: [PATCH 01/23] docs(evolution): add proposal 0001 SymbolStore arena-based compact node storage Design proposal for a compact, arena-based storage layer coexisting with the Node class API: 12-byte flat nodes referenced by UInt32 indices, per-symbol scratch arena with intern-copy into a frozen Sendable store, and a four-phase progressive migration plan (interop, zero-materialization read path, direct-to-arena parsing, flat serialization). --- evolution/0001-symbol-store-arena.md | 139 +++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 evolution/0001-symbol-store-arena.md diff --git a/evolution/0001-symbol-store-arena.md b/evolution/0001-symbol-store-arena.md new file mode 100644 index 0000000..cb2db98 --- /dev/null +++ b/evolution/0001-symbol-store-arena.md @@ -0,0 +1,139 @@ +# 0001 - SymbolStore: Arena-Based Compact Node Storage + +- **Proposal**: 0001 +- **Author**: Mx-Iris +- **Status**: Draft +- **Date**: 2026-07-23 +- **Last Updated**: 2026-07-23 +- **Branch**: `feature/symbol-store` +- **Related**: `Documentations/SubtreeInterning.md`(前置优化:全子树 hash-consing,已合入 main `5788472`) + +## Summary + +为批量 demangle 场景引入一个与现有 `Node` 类**并存**的紧凑存储层 `SymbolStore`:所有节点平铺存放在连续缓冲(arena)中,节点间用 4 字节索引互指,每节点 12 字节、无对象头、无引用计数、无逐节点堆分配。公共 API 通过轻量值类型句柄 `NodeReference`(store 引用 + `UInt32` 索引)访问,架构对标 swift-syntax 的 SyntaxArena + 值类型句柄模式。分四个可独立交付的阶段渐进迁移,全程不破坏现有 `Node` API。 + +## Motivation + +内存基线(详见 `Documentations/SubtreeInterning.md` 的实测): + +- `Node` 是 `final class`,每实例 48 字节(16 字节对象头 + 2 字节 kind + 对齐 + 17 字节 `Payload` 枚举 = 41 字节,落入 48 字节 malloc 桶),逐节点独立 malloc + 引用计数; +- 全子树 hash-consing(proposal 前置工作)已把 49k 符号语料的解析驻留从 39.5 MB 降到 12.9 MB(存活实例 76 万 → 20.2 万); +- 对照 C++ `Demangle::Node`:24 字节/节点 + bump allocator。class 形态的剩余差距来自对象头(16B)、malloc 分桶取整(41→48)与逐节点分配/引用计数,**在保持 class 的前提下无法消除**; +- 全量 SwiftUI 场景(含 dyld cache local symbols)当前预计仍需 150–250 MB,目标降到 30–80 MB,并为「把整个 dyld cache 的解析结果持久化为符号数据库」铺路。 + +单纯把 `Node` 改为带 `[Node]` 的 struct 不可行:children 数组仍是逐节点堆分配(32 字节缓冲头 + CoW 引用计数),加上大 struct 拷贝,内存与 CPU 双双倒退。struct 的真正价值在于**精确布局 + 可平铺进连续缓冲**,因此正确形态是 arena + 索引。 + +## Detailed Design + +### 存储布局 + +```swift +@frozen struct CompactNode { // size 12, alignment 4, 无对象头 + var kindAndPayloadKind: UInt16 // bit 0-8: kind(~300 种,9 位) + // bit 9-11: payloadKind(6 种,3 位) + // bit 12-15: 保留 + var payloadA: UInt32 + var payloadB: UInt32 +} +``` + +`payloadA/payloadB` 按 `payloadKind` 解释(沿用现有 `Payload` 的互斥不变量:contents 与 children 不共存): + +| payloadKind | payloadA | payloadB | 覆盖比例(49k 语料实测) | +|---|---|---|---| +| `none` | — | — | 与 leaf 部分重叠 | +| `index` | `UInt64` 低 32 位 | 高 32 位 | 少量 | +| `text` | 字符串表 offset | 字节长度 | ~5% | +| `oneChild` | 孩子索引 | — | ~67% | +| `twoChildren` | 孩子 0 索引 | 孩子 1 索引 | ~12% | +| `manyChildren` | edges 缓冲起点 | 孩子数量 | ~16% | + +配套缓冲(每个 store 一套): + +- `nodes: ContiguousArray` — 主 arena,`append` 即 bump 分配; +- `edges: ContiguousArray` — 仅 3+ 孩子节点使用的孩子索引连续区段; +- `textBytes: ContiguousArray` — 字符串表,全部 identifier 的 UTF-8 字节连续存放并去重; +- intern 哈希表 — 把已实现的 hash-consing 从 `ObjectIdentifier` 键迁移为索引键:`(kindAndPayloadKind, payloadA, payloadB)` 经孩子索引规范化后即天然唯一,键就是 12 字节本身。 + +容量边界:`UInt32` 索引上限 42.9 亿节点、字符串表 4 GB——对单个 store(哪怕整个 dyld cache)远够;越界走 `precondition` 失败而非静默截断。 + +预算核算(49k 语料,20.2 万唯一子树):`201k × 12B ≈ 2.4 MB` 节点 + edges ~0.3 MB + 字符串表 ~0.5 MB + intern 表 ~2 MB ≈ **5–6 MB**(现状 12.9 MB)。相对 C++ 的反超点:孩子引用用 4 字节索引而非 8 字节指针。 + +### 类型与 API 面 + +```swift +/// 冻结后的不可变符号库。构建完成即 Sendable,读路径零锁。 +public final class SymbolStore: Sendable { + public func demangleAsReference(_ mangled: String) throws(DemanglingError) -> NodeReference // Phase 3 + public func reference(of node: Node) -> NodeReference // 导入现有树(intern 拷贝) +} + +/// 轻量句柄:store 引用 + 索引,16 字节值类型。 +public struct NodeReference: Hashable, Sendable { + public var kind: Node.Kind { get } + public var text: Substring? { get } // 借用字符串表,零拷贝 + public var index: UInt64? { get } + public var children: ChildrenView { get } // RandomAccessCollection + public func materialize() -> Node // 物化为现有 Node 树(互操作出口) + public func print(using options: DemangleOptions) -> String +} +``` + +- 构建期使用 `SymbolStoreBuilder`(`~Copyable`):单写者约束由编译器保证,`consuming func freeze() -> SymbolStore` 完成冻结——把现在靠 `NSLock` + 文档契约维持的「构建后不可变」升级为类型系统保证; +- `Hashable`/`==` 基于 (store identity, index):因为 store 内全量 hash-consed,索引相等 ⇔ 结构相等,比较从 O(树) 降为 O(1); +- 读路径后续用 `Span` / `UTF8Span`(Swift 6.2)暴露孩子区段与文本的借用视图,零分配零拷贝。 + +### 构建流程(两代空间) + +1. Demangler 把节点写入**每符号复用的 scratch arena**(容量保留、每符号 `removeAll(keepingCapacity: true)`)——解析中产生的临时节点零成本丢弃,等价于 C++ 每符号销毁 `NodeFactory`; +2. 解析成功后,从根出发把可达节点自底向上 intern 拷贝进持久 store——**去重与垃圾回收是同一个 pass**,键规范化逻辑与已合入的 `internTreeUnsafe` 完全同构; +3. scratch 重置,处理下一符号。 + +并行策略:每线程一个 scratch arena;持久 store 的 intern 写入初期沿用单锁(与现状一致),若成为瓶颈再演进为分片锁或每线程局部 store + 终态合并。 + +### 渐进式迁移分期 + +每个阶段独立可交付、测试全绿、`Node` API 始终不动: + +- **Phase 1 — 存储层与互操作**:`CompactNode` / `SymbolStoreBuilder` / `SymbolStore` / `NodeReference`;`reference(of:)` 导入现有 `Node` 树,`materialize()` 导出。打印/remangle 暂走物化慢路径。验收:任意树 导入→导出 与原树 `==`;导入两棵结构相等的树得到同一索引。 +- **Phase 2 — 零物化读路径**:将 `NodePrinter` / `Remangler` / `TypeDecoder` 的树访问抽象为协议(kind/text/index/children 四个只读需求),`Node` 与 `NodeReference` 双双 conform;打印与 remangle 直接从 store 读,不再物化。验收:全量 dyld cache 对齐测试在 `NodeReference` 路径下 0 失败。 +- **Phase 3 — 解析直写 arena**:`Demangler` 的节点构造抽象为存储策略(默认策略维持现有 `Node` 行为不变;store 策略直写 scratch arena),提供批量入口 `SymbolStore.demangleAsReference(_:)`。此阶段起,批量场景完全绕开 class 分配。验收:内存达标(49k 语料 ≤6 MB)、吞吐不劣于现状 1.2 倍。 +- **Phase 4(可选)— 平铺序列化**:store 的几个缓冲直接二进制序列化/反序列化(接近 memcpy 量级),支持 mmap 加载——符号数据库能力,为 RuntimeViewer 类工具缓存整个 dyld cache 的解析结果。 + +### 与现有 NodeCache 的关系 + +store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量用户迁移到 store 后,`NodeCache` 仅服务于一次性/精细操作场景,其增长压力自然消失。两者语义一致(结构相等 ⇔ 规范实例/索引相等)。 + +## Source Compatibility + +纯增量 API,无破坏性变更。`Node`、`NodeBuilder`、`NodeCache`、`demangleAsNode` 行为全部保持。Phase 2 的协议抽象对 `NodePrinter`/`Remangler` 是内部重构,公共签名不变。 + +## Performance Goals(验收标准) + +| 指标 | 目标 | +|---|---| +| 每节点存储 | ≤ 16 字节(设计值 12) | +| 49k 语料总驻留 | ≤ 6 MB(现状 12.9 MB,hash-consing 前 39.5 MB) | +| 全量 SwiftUI 场景 | ≤ 100 MB(现状预计 150–250 MB) | +| 解析吞吐 | 不劣于现状的 1.2 倍 | + +基准方法沿用既有测量工程(NodeMemBench:`class_getInstanceSize` / `malloc_size` / `phys_footprint` 三角验证),增加 store 变体对照。 + +## Alternatives Considered + +- **`ManagedBuffer` 尾分配 class 节点**(孩子内联到实例尾部,单次分配):仍保留 16 字节对象头与引用计数,加权后 ~32–48 字节,被 arena 全面支配,否决; +- **压缩 class 布局到 32 字节桶**:需手工 union 并外部化 `String`/双孩子,加权收益仅 ~1.2 倍,复杂度不成比例,否决(详见 SubtreeInterning 文档); +- **直接把 `Node` 重写为 struct 句柄**(swift-syntax 式整体替换):终态最优但破坏全部公共 API 与下游(MachOSwiftSection / RuntimeViewer),不符合渐进要求;本方案 Phase 3 完成后如需要可再评估。 + +## Future Directions + +- mmap 符号数据库格式版本化(magic + version + 缓冲布局描述); +- 分片并行 store 与终态合并; +- `InlineArray`(SE-0453)在 scratch arena 孩子暂存区的应用; +- `NodeReference` 层面的 `Node.Rewriter` 等价物(写时拷贝进新 store)。 + +## Decision Log + +| Date | Decision | Notes | +|---|---|---| +| 2026-07-23 | Created as Draft | 基于 48B class 下限与 C++ 24B 对比分析,确定 arena + 索引句柄方向;分四阶段渐进迁移,不破坏现有 `Node` API | From 50af89fb396f6fc970788d42d75e8aae2c9ae5eb Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 18:01:53 +0800 Subject: [PATCH 02/23] feat(Demangling): SymbolStore arena-based compact node storage (proposal 0001 Phase 1) Add a compact storage layer coexisting with the Node class API: - CompactNode: flat 12-byte node (9-bit kind ordinal + 3-bit payload kind packed into UInt16, two UInt32 payload words covering index/text/inline 1-2 children/edges range), referenced by UInt32 indices instead of pointers. - SymbolStoreBuilder: noncopyable append-only builder that hash-conses every inserted node (child-index keys, bottom-up like NodeCache.internTreeUnsafe); consuming freeze() produces an immutable Sendable SymbolStore, so single-writer and frozen-immutability are enforced by the type system. - NodeReference: 16-byte value handle with O(1) equality, mirroring Node accessors (kind/text/index/children) plus materialize()/print(using:) interop; builder.demangle(_:) bridges through a transient Node tree until Phase 3 parses directly into the arena. Measured on the 49k-symbol corpus: 201,876 unique nodes (exactly matching the NodeCache hash-consing count), 3.0 MB flat storage payload vs the <= 6 MB Phase 1 target, zero print mismatches in a 2000-symbol parity sample, build time on par with the Node path. --- AGENTS.md | 2 + Package.resolved | 17 +- Sources/Demangling/Store/CompactNode.swift | 103 +++++++++ Sources/Demangling/Store/NodeReference.swift | 123 +++++++++++ Sources/Demangling/Store/SymbolStore.swift | 118 ++++++++++ .../Demangling/Store/SymbolStoreBuilder.swift | 185 ++++++++++++++++ Tests/DemanglingTests/SymbolStoreTests.swift | 206 ++++++++++++++++++ evolution/0001-symbol-store-arena.md | 4 +- 8 files changed, 753 insertions(+), 5 deletions(-) create mode 100644 Sources/Demangling/Store/CompactNode.swift create mode 100644 Sources/Demangling/Store/NodeReference.swift create mode 100644 Sources/Demangling/Store/SymbolStore.swift create mode 100644 Sources/Demangling/Store/SymbolStoreBuilder.swift create mode 100644 Tests/DemanglingTests/SymbolStoreTests.swift diff --git a/AGENTS.md b/AGENTS.md index 2e2ed55..532189b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,7 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. - **`Node` as `Sequence`** (`Node+Sequence.swift`) — `Node` conforms to `Sequence` with preorder traversal as default. Also provides `.inorder()`, `.postorder()`, `.levelorder()`. Sequence extensions add `first(of:)`, `all(of:)`, `contains(_:)` by `Node.Kind`. +- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001, Phase 1). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children) with `materialize()` / `print(using:)` interop. The builder's `demangle(_:)` currently bridges through a transient `Node` tree (`internsSubtrees: false`); Phase 3 will parse directly into the arena. ### Node Identity vs Equality @@ -82,6 +83,7 @@ Sources/Demangling/ Main/TypeDecoder/ — TypeDecoder, TypeBuilder protocol Node/ — Node, Node.Children, NodeBuilder, NodeCache, Kind, Conversions, Sequence, Rewriter Node/Printer/ — NodePrinter, NodePrinterTarget protocol, NodePrintContext/State + Store/ — CompactNode, SymbolStore, SymbolStoreBuilder, NodeReference (evolution proposal 0001) Enums/ — SugarType, ManglingFlavor, DemanglingError, ManglingError, etc. Utils/ — Extensions, Common constants, Punycode Tests/DemanglingTests/ diff --git a/Package.resolved b/Package.resolved index ffe70d5..cb45934 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "version" : "0.7.1" } }, + { + "identity" : "machokit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MxIris-Reverse-Engineering/MachOKit", + "state" : { + "revision" : "9de6599aaba425632bb38a35cf9acd12a022d8f2", + "version" : "0.51.100" + } + }, { "identity" : "objectarchivekit", "kind" : "remoteSourceControl", @@ -24,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "eb50cbd14606a9161cbc5d452f18797c90ef0bab", - "version" : "1.7.0" + "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", + "version" : "1.7.1" } }, { @@ -51,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/p-x9/swift-fileio.git", "state" : { - "revision" : "d589ff3966f9f064574780f527449a946736b989", - "version" : "0.13.0" + "revision" : "e581ae04d4079c675042fa62aeabc5ba7480d3df", + "version" : "0.14.0" } }, { diff --git a/Sources/Demangling/Store/CompactNode.swift b/Sources/Demangling/Store/CompactNode.swift new file mode 100644 index 0000000..d359fae --- /dev/null +++ b/Sources/Demangling/Store/CompactNode.swift @@ -0,0 +1,103 @@ +/// Flat 12-byte node representation used by `SymbolStore`. +/// +/// Nodes live in one contiguous buffer and refer to each other by `UInt32` +/// indices instead of pointers, eliminating per-node heap allocations, object +/// headers, and reference counting. The layout mirrors the mutual-exclusivity +/// invariant of `Node.Payload`: a node carries either contents (text/index) +/// or children, never both. +@usableFromInline +struct CompactNode: Hashable, Sendable { + /// Bits 0-8: kind ordinal (see `Node.Kind.storeOrdinal`). + /// Bits 9-11: payload kind. Bits 12-15: reserved. + @usableFromInline + var kindAndPayloadKind: UInt16 + + /// First 32-bit payload word; meaning depends on `payloadKind`. + @usableFromInline + var payloadWord0: UInt32 + + /// Second 32-bit payload word; meaning depends on `payloadKind`. + @usableFromInline + var payloadWord1: UInt32 +} + +extension CompactNode { + @usableFromInline + enum PayloadKind: UInt16, Sendable { + /// No contents and no children. + case none = 0 + /// `payloadWord0` = low 32 bits, `payloadWord1` = high 32 bits of a `UInt64` index. + case index = 1 + /// `payloadWord0` = offset into the store's text bytes, `payloadWord1` = byte length. + case text = 2 + /// `payloadWord0` = child node index. + case oneChild = 3 + /// `payloadWord0` / `payloadWord1` = child node indices. + case twoChildren = 4 + /// `payloadWord0` = offset into the store's edges buffer, `payloadWord1` = child count. + case manyChildren = 5 + } + + @usableFromInline + static let payloadKindShift: UInt16 = 9 + + @usableFromInline + static let kindOrdinalMask: UInt16 = (1 << payloadKindShift) - 1 + + @usableFromInline + init(kind: Node.Kind, payloadKind: PayloadKind, payloadWord0: UInt32, payloadWord1: UInt32) { + self.kindAndPayloadKind = kind.storeOrdinal | (payloadKind.rawValue << Self.payloadKindShift) + self.payloadWord0 = payloadWord0 + self.payloadWord1 = payloadWord1 + } + + @usableFromInline + var kind: Node.Kind { + Node.Kind.kindsByStoreOrdinal[Int(kindAndPayloadKind & Self.kindOrdinalMask)] + } + + @usableFromInline + var payloadKind: PayloadKind { + PayloadKind(rawValue: kindAndPayloadKind >> Self.payloadKindShift)! + } + + @usableFromInline + var childCount: Int { + switch payloadKind { + case .none, .index, .text: return 0 + case .oneChild: return 1 + case .twoChildren: return 2 + case .manyChildren: return Int(payloadWord1) + } + } +} + +extension Node.Kind { + /// Kinds in ordinal order. The ordinal is the position in `allCases` and is + /// only stable within a single process run — it is NOT a serialization + /// format. A persisted store format (proposal 0001 Phase 4) must define its + /// own stable kind mapping. + @usableFromInline + static let kindsByStoreOrdinal: [Node.Kind] = { + let orderedKinds = Array(allCases) + precondition( + orderedKinds.count <= Int(CompactNode.kindOrdinalMask) + 1, + "Node.Kind no longer fits the 9-bit ordinal space of CompactNode" + ) + return orderedKinds + }() + + @usableFromInline + static let storeOrdinalsByKind: [Node.Kind: UInt16] = { + var ordinals = [Node.Kind: UInt16](minimumCapacity: kindsByStoreOrdinal.count) + for (ordinal, kind) in kindsByStoreOrdinal.enumerated() { + ordinals[kind] = UInt16(ordinal) + } + return ordinals + }() + + @usableFromInline + var storeOrdinal: UInt16 { + Self.storeOrdinalsByKind[self]! + } +} diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift new file mode 100644 index 0000000..f953824 --- /dev/null +++ b/Sources/Demangling/Store/NodeReference.swift @@ -0,0 +1,123 @@ +/// A lightweight handle to a node stored in a `SymbolStore`. +/// +/// Sixteen bytes as a value: a store reference plus a node index. Because +/// stores are fully hash-consed, equality is O(1) — two references are equal +/// exactly when they address the same index of the same store, which within +/// one store coincides with structural equality. +public struct NodeReference: Sendable { + public let store: SymbolStore + public let nodeIndex: SymbolStore.NodeIndex + + @usableFromInline + init(store: SymbolStore, nodeIndex: SymbolStore.NodeIndex) { + self.store = store + self.nodeIndex = nodeIndex + } + + @usableFromInline + var compactNode: CompactNode { + store.compactNode(at: nodeIndex.rawValue) + } + + // MARK: - Accessors (mirroring Node) + + public var kind: Node.Kind { + compactNode.kind + } + + /// The text contents, if this node carries text. + public var text: String? { + let compact = compactNode + guard case .text = compact.payloadKind else { return nil } + return store.text(offset: compact.payloadWord0, length: compact.payloadWord1) + } + + /// The index contents, if this node carries an index. + public var index: UInt64? { + let compact = compactNode + guard case .index = compact.payloadKind else { return nil } + return UInt64(compact.payloadWord0) | (UInt64(compact.payloadWord1) << 32) + } + + public var children: ChildrenView { + ChildrenView(store: store, compactNode: compactNode) + } + + // MARK: - Interop with Node + + /// Rebuilds a standalone `Node` tree for this subtree. + /// + /// The returned tree is freshly constructed and does not interact with the + /// global `NodeCache`; intern it explicitly if canonical instances are needed. + public func materialize() -> Node { + store.materializeNode(at: nodeIndex.rawValue) + } + + /// Prints the demangled form of this subtree. + /// + /// Phase 1 materializes and delegates to `Node.print(using:)`; proposal + /// 0001 Phase 2 replaces this with a zero-materialization printer path. + public func print(using options: DemangleOptions = .default) -> String { + materialize().print(using: options) + } +} + +// MARK: - Hashable + +extension NodeReference: Hashable { + public static func == (lhs: NodeReference, rhs: NodeReference) -> Bool { + lhs.store === rhs.store && lhs.nodeIndex == rhs.nodeIndex + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(store)) + hasher.combine(nodeIndex) + } +} + +// MARK: - ChildrenView + +extension NodeReference { + /// Random-access view of a node's children, resolved lazily from the store. + public struct ChildrenView: RandomAccessCollection, Sendable { + public typealias Element = NodeReference + public typealias Index = Int + + @usableFromInline + let store: SymbolStore + + @usableFromInline + let compactNode: CompactNode + + @usableFromInline + init(store: SymbolStore, compactNode: CompactNode) { + self.store = store + self.compactNode = compactNode + } + + public var startIndex: Int { 0 } + + public var endIndex: Int { compactNode.childCount } + + public subscript(position: Int) -> NodeReference { + let rawChildIndex: UInt32 + switch compactNode.payloadKind { + case .oneChild: + precondition(position == 0, "Child index out of range") + rawChildIndex = compactNode.payloadWord0 + case .twoChildren: + switch position { + case 0: rawChildIndex = compactNode.payloadWord0 + case 1: rawChildIndex = compactNode.payloadWord1 + default: preconditionFailure("Child index out of range") + } + case .manyChildren: + precondition(position >= 0 && position < Int(compactNode.payloadWord1), "Child index out of range") + rawChildIndex = store.edges[Int(compactNode.payloadWord0) + position] + case .none, .index, .text: + preconditionFailure("Child index out of range for a node without children") + } + return NodeReference(store: store, nodeIndex: SymbolStore.NodeIndex(rawValue: rawChildIndex)) + } + } +} diff --git a/Sources/Demangling/Store/SymbolStore.swift b/Sources/Demangling/Store/SymbolStore.swift new file mode 100644 index 0000000..d683c4a --- /dev/null +++ b/Sources/Demangling/Store/SymbolStore.swift @@ -0,0 +1,118 @@ +/// An immutable, memory-compact symbol database produced by +/// `SymbolStoreBuilder.freeze()`. +/// +/// All nodes live in flat contiguous buffers — 12 bytes per node, 4 bytes per +/// child edge beyond two, and deduplicated UTF-8 text bytes — with no per-node +/// heap allocation, object header, or reference counting. The store is fully +/// hash-consed: within one store, structurally equal subtrees have equal +/// indices, so equality checks on `NodeReference` are O(1). +/// +/// The store is deeply immutable after freezing, so it is `Sendable` and reads +/// take no locks. See evolution proposal 0001 for the overall design. +public final class SymbolStore: Sendable { + /// A stable identifier of a node within its store. + /// + /// Indices are minted by `SymbolStoreBuilder` and remain valid in the + /// frozen store. They are only meaningful for the store they came from. + public struct NodeIndex: Hashable, Sendable { + @usableFromInline + let rawValue: UInt32 + + @usableFromInline + init(rawValue: UInt32) { + self.rawValue = rawValue + } + } + + @usableFromInline + let nodes: ContiguousArray + + @usableFromInline + let edges: ContiguousArray + + @usableFromInline + let textBytes: ContiguousArray + + init( + nodes: ContiguousArray, + edges: ContiguousArray, + textBytes: ContiguousArray + ) { + self.nodes = nodes + self.edges = edges + self.textBytes = textBytes + } + + // MARK: - Statistics + + /// Number of unique nodes in the store. + public var nodeCount: Int { nodes.count } + + /// Number of child-edge slots used by nodes with three or more children. + public var edgeCount: Int { edges.count } + + /// Number of deduplicated UTF-8 text bytes. + public var textByteCount: Int { textBytes.count } + + /// Total payload bytes of the flat buffers (excluding the containers' + /// own headers and growth slack). + public var storageByteCount: Int { + nodes.count * MemoryLayout.stride + + edges.count * MemoryLayout.stride + + textBytes.count + } + + // MARK: - Access + + /// Returns a reference to the node at the given index. + public func reference(at nodeIndex: NodeIndex) -> NodeReference { + precondition(Int(nodeIndex.rawValue) < nodes.count, "NodeIndex out of range for this store") + return NodeReference(store: self, nodeIndex: nodeIndex) + } + + @usableFromInline + func compactNode(at rawIndex: UInt32) -> CompactNode { + nodes[Int(rawIndex)] + } + + @usableFromInline + func text(offset: UInt32, length: UInt32) -> String { + let start = Int(offset) + let end = start + Int(length) + return String(decoding: textBytes[start ..< end], as: UTF8.self) + } + + // MARK: - Materialization + + /// Rebuilds a `Node` tree for the subtree rooted at the given raw index. + /// + /// The returned tree is freshly constructed and does not interact with the + /// global `NodeCache`. + func materializeNode(at rawIndex: UInt32) -> Node { + let compact = compactNode(at: rawIndex) + switch compact.payloadKind { + case .none: + return Node(kind: compact.kind) + case .index: + return Node(kind: compact.kind, index: UInt64(compact.payloadWord0) | (UInt64(compact.payloadWord1) << 32)) + case .text: + return Node(kind: compact.kind, text: text(offset: compact.payloadWord0, length: compact.payloadWord1)) + case .oneChild: + return Node(kind: compact.kind, children: [materializeNode(at: compact.payloadWord0)]) + case .twoChildren: + return Node(kind: compact.kind, children: [ + materializeNode(at: compact.payloadWord0), + materializeNode(at: compact.payloadWord1), + ]) + case .manyChildren: + let edgesStart = Int(compact.payloadWord0) + let childCount = Int(compact.payloadWord1) + var children = [Node]() + children.reserveCapacity(childCount) + for edgeOffset in edgesStart ..< (edgesStart + childCount) { + children.append(materializeNode(at: edges[edgeOffset])) + } + return Node(kind: compact.kind, children: children) + } + } +} diff --git a/Sources/Demangling/Store/SymbolStoreBuilder.swift b/Sources/Demangling/Store/SymbolStoreBuilder.swift new file mode 100644 index 0000000..a34ff58 --- /dev/null +++ b/Sources/Demangling/Store/SymbolStoreBuilder.swift @@ -0,0 +1,185 @@ +/// Append-only builder that constructs a `SymbolStore`. +/// +/// The builder is noncopyable: exactly one owner may build at a time, and +/// `freeze()` consumes the builder, so "immutable after freezing" is enforced +/// by the type system rather than by locks or documentation. +/// +/// Every inserted node is hash-consed on entry: structurally equal subtrees +/// receive the same `SymbolStore.NodeIndex`. Interior-node keys use child +/// indices, which is exact because children are always interned before their +/// parent (the same bottom-up scheme as `NodeCache.internTreeUnsafe`). +public struct SymbolStoreBuilder: ~Copyable, Sendable { + private var nodes: ContiguousArray = [] + private var edges: ContiguousArray = [] + private var textBytes: ContiguousArray = [] + + /// Interning table for nodes whose 12-byte representation is already + /// canonical: leaves (text offsets are canonical because text is interned + /// first) and nodes with one or two children (child indices are canonical). + private var uniqueNodeIndices: [CompactNode: UInt32] = [:] + + /// Interning table for nodes with three or more children, whose edges + /// offset is allocation-dependent and therefore cannot serve as a key. + private var uniqueManyChildrenIndices: [ManyChildrenKey: UInt32] = [:] + + /// Interning table for text contents. + private var uniqueTextLocations: [String: TextLocation] = [:] + + private struct ManyChildrenKey: Hashable { + let kindAndPayloadKind: UInt16 + let childIndices: [UInt32] + } + + private struct TextLocation { + let offset: UInt32 + let length: UInt32 + } + + public init() {} + + // MARK: - Building + + /// Interns an existing `Node` tree, returning the canonical index of its root. + public mutating func intern(_ node: Node) -> SymbolStore.NodeIndex { + var visitedIndices = [ObjectIdentifier: UInt32]() + return SymbolStore.NodeIndex(rawValue: internRecursively(node, visitedIndices: &visitedIndices)) + } + + /// Demangles a mangled symbol and interns the resulting tree in one step. + /// + /// The intermediate `Node` tree is transient, so the global `NodeCache` + /// subtree interning is skipped — nothing accumulates outside this builder. + public mutating func demangle(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> SymbolStore.NodeIndex { + let tree = try demangleAsNode(mangled, isType: isType, internsSubtrees: false) + return intern(tree) + } + + /// Freezes the builder into an immutable, `Sendable` store. + /// + /// Consumes the builder; interning tables are dropped, only the flat + /// buffers survive. Indices minted by this builder remain valid in the + /// frozen store. + public consuming func freeze() -> SymbolStore { + SymbolStore(nodes: nodes, edges: edges, textBytes: textBytes) + } + + // MARK: - Statistics + + /// Number of unique nodes interned so far. + public var nodeCount: Int { nodes.count } + + // MARK: - Interning + + private mutating func internRecursively(_ node: Node, visitedIndices: inout [ObjectIdentifier: UInt32]) -> UInt32 { + let identifier = ObjectIdentifier(node) + if let existingIndex = visitedIndices[identifier] { + return existingIndex + } + + let children = node.children + let internedIndex: UInt32 + if children.isEmpty { + internedIndex = internLeaf(kind: node.kind, contents: node.contents) + } else { + var childIndices = [UInt32]() + childIndices.reserveCapacity(children.count) + for child in children { + childIndices.append(internRecursively(child, visitedIndices: &visitedIndices)) + } + internedIndex = internInterior(kind: node.kind, childIndices: childIndices) + } + + visitedIndices[identifier] = internedIndex + return internedIndex + } + + private mutating func internLeaf(kind: Node.Kind, contents: Node.Contents) -> UInt32 { + let compact: CompactNode + switch contents { + case .none: + compact = CompactNode(kind: kind, payloadKind: .none, payloadWord0: 0, payloadWord1: 0) + case .index(let indexValue): + compact = CompactNode( + kind: kind, + payloadKind: .index, + payloadWord0: UInt32(truncatingIfNeeded: indexValue), + payloadWord1: UInt32(truncatingIfNeeded: indexValue >> 32) + ) + case .text(let textValue): + let location = internText(textValue) + compact = CompactNode( + kind: kind, + payloadKind: .text, + payloadWord0: location.offset, + payloadWord1: location.length + ) + } + return internCanonicalCompact(compact) + } + + private mutating func internInterior(kind: Node.Kind, childIndices: [UInt32]) -> UInt32 { + switch childIndices.count { + case 1: + return internCanonicalCompact(CompactNode( + kind: kind, + payloadKind: .oneChild, + payloadWord0: childIndices[0], + payloadWord1: 0 + )) + case 2: + return internCanonicalCompact(CompactNode( + kind: kind, + payloadKind: .twoChildren, + payloadWord0: childIndices[0], + payloadWord1: childIndices[1] + )) + default: + let key = ManyChildrenKey( + kindAndPayloadKind: CompactNode(kind: kind, payloadKind: .manyChildren, payloadWord0: 0, payloadWord1: 0).kindAndPayloadKind, + childIndices: childIndices + ) + if let existingIndex = uniqueManyChildrenIndices[key] { + return existingIndex + } + precondition(edges.count + childIndices.count <= Int(UInt32.max), "SymbolStore edges buffer exceeded UInt32 index space") + let edgesOffset = UInt32(edges.count) + edges.append(contentsOf: childIndices) + let newIndex = appendNode(CompactNode( + kind: kind, + payloadKind: .manyChildren, + payloadWord0: edgesOffset, + payloadWord1: UInt32(childIndices.count) + )) + uniqueManyChildrenIndices[key] = newIndex + return newIndex + } + } + + private mutating func internCanonicalCompact(_ compact: CompactNode) -> UInt32 { + if let existingIndex = uniqueNodeIndices[compact] { + return existingIndex + } + let newIndex = appendNode(compact) + uniqueNodeIndices[compact] = newIndex + return newIndex + } + + private mutating func appendNode(_ compact: CompactNode) -> UInt32 { + precondition(nodes.count < Int(UInt32.max), "SymbolStore node buffer exceeded UInt32 index space") + let newIndex = UInt32(nodes.count) + nodes.append(compact) + return newIndex + } + + private mutating func internText(_ textValue: String) -> TextLocation { + if let existingLocation = uniqueTextLocations[textValue] { + return existingLocation + } + let utf8Bytes = Array(textValue.utf8) + precondition(textBytes.count + utf8Bytes.count <= Int(UInt32.max), "SymbolStore text buffer exceeded UInt32 offset space") + let location = TextLocation(offset: UInt32(textBytes.count), length: UInt32(utf8Bytes.count)) + textBytes.append(contentsOf: utf8Bytes) + uniqueTextLocations[textValue] = location + return location + } +} diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift new file mode 100644 index 0000000..1f7507e --- /dev/null +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -0,0 +1,206 @@ +import Foundation +import Testing +@testable import Demangling + +/// Unit tests for SymbolStore — arena-based compact node storage (proposal 0001, Phase 1). +@Suite +struct SymbolStoreTests { + + // MARK: - Kind Ordinal Mapping + + @Test func kindOrdinalRoundTripsForAllKinds() { + for kind in Node.Kind.allCases { + let compact = CompactNode(kind: kind, payloadKind: .none, payloadWord0: 0, payloadWord1: 0) + #expect(compact.kind == kind, "Kind \(kind) should round-trip through the 9-bit ordinal") + #expect(compact.payloadKind == .none) + } + } + + @Test func compactNodeIsTwelveBytes() { + #expect(MemoryLayout.size == 12) + #expect(MemoryLayout.stride == 12) + } + + // MARK: - Import / Materialize Round-Trip + + @Test func importAndMaterializeRoundTrip() { + var builder = SymbolStoreBuilder() + + let tree = Node(kind: .global, children: [ + Node(kind: .type, children: [ + Node(kind: .structure, children: [ + Node(kind: .module, text: "Swift"), + Node(kind: .identifier, text: "Int"), + ]), + ]), + Node(kind: .index, index: 42), + Node(kind: .index, index: UInt64.max), + Node(kind: .emptyList), + ]) + + let rootIndex = builder.intern(tree) + let store = builder.freeze() + let materialized = store.reference(at: rootIndex).materialize() + + #expect(materialized == tree, "Import -> materialize should reproduce a structurally equal tree") + } + + @Test func largeIndexPayloadRoundTrips() { + var builder = SymbolStoreBuilder() + let indexValues: [UInt64] = [0, 1, UInt64(UInt32.max), UInt64(UInt32.max) + 1, UInt64.max] + + var rootIndices = [SymbolStore.NodeIndex]() + for indexValue in indexValues { + rootIndices.append(builder.intern(Node(kind: .index, index: indexValue))) + } + let store = builder.freeze() + + for (rootIndex, indexValue) in zip(rootIndices, indexValues) { + #expect(store.reference(at: rootIndex).index == indexValue) + } + } + + // MARK: - Hash-Consing + + @Test func structurallyEqualTreesShareOneIndex() { + var builder = SymbolStoreBuilder() + + func makeTree() -> Node { + Node(kind: .type, children: [ + Node(kind: .structure, children: [ + Node(kind: .module, text: "Swift"), + Node(kind: .identifier, text: "Int"), + ]), + ]) + } + + let firstIndex = builder.intern(makeTree()) + let nodeCountAfterFirst = builder.nodeCount + let secondIndex = builder.intern(makeTree()) + + #expect(firstIndex == secondIndex, "Structurally equal trees should intern to the same index") + #expect(builder.nodeCount == nodeCountAfterFirst, "Re-interning an equal tree should not add nodes") + } + + @Test func manyChildrenNodesDeduplicate() { + var builder = SymbolStoreBuilder() + + func makeWideTree() -> Node { + Node(kind: .tuple, children: [ + Node(kind: .identifier, text: "a"), + Node(kind: .identifier, text: "b"), + Node(kind: .identifier, text: "c"), + Node(kind: .identifier, text: "d"), + ]) + } + + let firstIndex = builder.intern(makeWideTree()) + let secondIndex = builder.intern(makeWideTree()) + let store = builder.freeze() + + #expect(firstIndex == secondIndex, "Equal many-children trees should intern to the same index") + #expect(store.edgeCount == 4, "The edges of an equal many-children node should be stored once") + #expect(store.reference(at: firstIndex).children.count == 4) + } + + @Test func textBytesDeduplicate() { + var builder = SymbolStoreBuilder() + + _ = builder.intern(Node(kind: .identifier, text: "duplicated")) + _ = builder.intern(Node(kind: .module, text: "duplicated")) + let store = builder.freeze() + + #expect(store.textByteCount == "duplicated".utf8.count, "Equal text should be stored once across kinds") + } + + // MARK: - Reference Accessors + + @Test func referenceAccessorsMatchMaterializedTree() throws { + var builder = SymbolStoreBuilder() + let mangled = "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc" + let rootIndex = try builder.demangle(mangled) + let store = builder.freeze() + + let expectedTree = try demangleAsNode(mangled, internsSubtrees: false) + + func compare(_ reference: NodeReference, _ node: Node) { + #expect(reference.kind == node.kind) + #expect(reference.text == node.text) + #expect(reference.index == node.index) + #expect(reference.children.count == node.children.count) + for (childReference, childNode) in zip(reference.children, node.children) { + compare(childReference, childNode) + } + } + + compare(store.reference(at: rootIndex), expectedTree) + } + + // MARK: - Demangle Parity + + @Test func demangleParityWithNodePath() throws { + let mangledSymbols = [ + "$sSiD", + "$sSaySiGD", + "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s4main3FooVAA1P0B0fMq_", + "$s7SwiftUI4ViewP", + ] + + var builder = SymbolStoreBuilder() + var rootIndices = [SymbolStore.NodeIndex]() + for mangled in mangledSymbols { + rootIndices.append(try builder.demangle(mangled)) + } + let store = builder.freeze() + + for (rootIndex, mangled) in zip(rootIndices, mangledSymbols) { + let storePrinted = store.reference(at: rootIndex).print(using: .default) + let nodePrinted = try demangleAsNode(mangled, internsSubtrees: false).print(using: .default) + #expect(storePrinted == nodePrinted, "Store-backed printing should match the Node path for \(mangled)") + } + } + + @Test func sharedSubtreesAcrossSymbolsShareIndices() throws { + var builder = SymbolStoreBuilder() + let firstRootIndex = try builder.demangle("$sSiD") + let secondRootIndex = try builder.demangle("$sSaySiGD") + let store = builder.freeze() + + func findIntStructure(_ reference: NodeReference) -> NodeReference? { + if reference.kind == .structure, reference.children.contains(where: { $0.text == "Int" }) { + return reference + } + for child in reference.children { + if let found = findIntStructure(child) { + return found + } + } + return nil + } + + let firstIntStructure = findIntStructure(store.reference(at: firstRootIndex)) + let secondIntStructure = findIntStructure(store.reference(at: secondRootIndex)) + #expect(firstIntStructure != nil) + #expect(secondIntStructure != nil) + #expect(firstIntStructure == secondIntStructure, "The Swift.Int subtree should share one index across symbols") + } + + // MARK: - Reference Identity + + @Test func referenceEqualityIsStoreAndIndexBased() { + var firstBuilder = SymbolStoreBuilder() + _ = firstBuilder.intern(Node(kind: .identifier, text: "same")) + let firstStore = firstBuilder.freeze() + + var secondBuilder = SymbolStoreBuilder() + _ = secondBuilder.intern(Node(kind: .identifier, text: "same")) + let secondStore = secondBuilder.freeze() + + let firstReference = firstStore.reference(at: SymbolStore.NodeIndex(rawValue: 0)) + let secondReference = secondStore.reference(at: SymbolStore.NodeIndex(rawValue: 0)) + + #expect(firstReference != secondReference, "References into different stores should not be equal") + #expect(firstReference == firstStore.reference(at: SymbolStore.NodeIndex(rawValue: 0))) + } +} diff --git a/evolution/0001-symbol-store-arena.md b/evolution/0001-symbol-store-arena.md index cb2db98..a3e9e40 100644 --- a/evolution/0001-symbol-store-arena.md +++ b/evolution/0001-symbol-store-arena.md @@ -2,7 +2,7 @@ - **Proposal**: 0001 - **Author**: Mx-Iris -- **Status**: Draft +- **Status**: In Progress - **Date**: 2026-07-23 - **Last Updated**: 2026-07-23 - **Branch**: `feature/symbol-store` @@ -137,3 +137,5 @@ store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量 | Date | Decision | Notes | |---|---|---| | 2026-07-23 | Created as Draft | 基于 48B class 下限与 C++ 24B 对比分析,确定 arena + 索引句柄方向;分四阶段渐进迁移,不破坏现有 `Node` API | +| 2026-07-23 | Status → In Progress,Phase 1 落地 | 用户确认迭代方向,在 worktree `feature/symbol-store` 实施。`CompactNode`(实测 size/stride = 12)、`SymbolStoreBuilder`(`~Copyable` + `consuming freeze()`)、`SymbolStore`、`NodeReference` + `ChildrenView` 完成,含 `intern(_ node:)` 导入与 `materialize()` 导出互操作、桥接式 `demangle(_:)`(经 `internsSubtrees: false` 的临时 `Node` 树) | +| 2026-07-23 | Phase 1 实测达标 | 49k 语料:唯一节点 201,876(与 `NodeCache` 全树 hash-consing 计数逐一吻合,交叉验证正确性);平铺存储 3.0 MB(nodes 2.4 + edges 0.43 + text 0.26),优于 ≤6 MB 目标;打印抽样 2000 条零差异;构建 0.87s,不劣于 class 路径。**新发现**:构建期高水位 ~16 MB,由 intern 表(`[CompactNode: UInt32]` 等,~10 MB 量级)与桥接路径的临时 class 节点构成,且已冻结后 dirty pages 不随 `malloc_zone_pressure_relief` 回落;两轮连建仅 +6 MB,确认内存复用、无累积。结论:①「49k ≤6 MB」按保留存储口径已达成,进程口径需 Phase 3(直写 arena,消除临时树)+ intern 表瘦身(改为指向 nodes 缓冲的 open-addressing 索引表,去掉独立 key 存储);②桥接路径会向全局 `NodeCache` 写入叶节点(实测 11k 条),Phase 3 前的批量用户建议构建后 `NodeCache.shared.clear()` | From 2dedd3daceefb977e8f47c8f6bcd8cd10871d089 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 18:47:23 +0800 Subject: [PATCH 03/23] feat(Demangling): zero-materialization store printing (proposal 0001 Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce DemanglingNode, a read-only tree protocol conformed by both Node and NodeReference, and genericize the 2179-line printer engine over it as DemanglingPrinter. The printer is a pure read-only consumer (it never constructs nodes), so the abstraction is clean: member names match Node's API, keeping the engine body representation-agnostic. - Public NodePrinter becomes a thin facade over DemanglingPrinter — public API is unchanged. - NodeReference.print(using:) now prints straight from the SymbolStore with zero materialization instead of rebuilding a Node tree. - NodeReference.text mirrors Node.text, synthesizing the generic-parameter name for .dependentGenericParamType (the printer depends on this). - NodePrintContext.node stays a concrete Node?; the store path passes `name as? Node` (nil for NodeReference), and the String target ignores context, so rich-target semantics on the Node path are preserved. Verified byte-identical to the Node path across the full 49k-symbol corpus (SwiftUI/SwiftUICore/Foundation/stdlib/Combine) x default/simplified/ synthesizeSugar: 0 mismatches. Full dyld-cache alignment and TypeDecoder suites remain green. --- AGENTS.md | 5 +- .../Demangling/Node/Printer/NodePrinter.swift | 285 ++++++++++-------- Sources/Demangling/Store/DemanglingNode.swift | 169 +++++++++++ Sources/Demangling/Store/NodeReference.swift | 26 +- Tests/DemanglingTests/SymbolStoreTests.swift | 20 +- evolution/0001-symbol-store-arena.md | 2 + 6 files changed, 363 insertions(+), 144 deletions(-) create mode 100644 Sources/Demangling/Store/DemanglingNode.swift diff --git a/AGENTS.md b/AGENTS.md index 532189b..7489b80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,8 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. - **`Node` as `Sequence`** (`Node+Sequence.swift`) — `Node` conforms to `Sequence` with preorder traversal as default. Also provides `.inorder()`, `.postorder()`, `.levelorder()`. Sequence extensions add `first(of:)`, `all(of:)`, `contains(_:)` by `Node.Kind`. -- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001, Phase 1). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children) with `materialize()` / `print(using:)` interop. The builder's `demangle(_:)` currently bridges through a transient `Node` tree (`internsSubtrees: false`); Phase 3 will parse directly into the arena. +- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children). The builder's `demangle(_:)` currently bridges through a transient `Node` tree (`internsSubtrees: false`); Phase 3 will parse directly into the arena. +- **`DemanglingNode` / `DemanglingPrinter`** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`) — Phase 2 read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so the generic printer body is representation-agnostic). The printer engine is `DemanglingPrinter`; the public `NodePrinter` is a thin facade over `DemanglingPrinter`, and `NodeReference.print(using:)` prints straight from the store with **zero materialization** (verified byte-identical to the `Node` path across the 49k-symbol corpus × default/simplified/synthesizeSugar). When abstracting more consumers, name protocol members to match `Node`'s and note that `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil). ### Node Identity vs Equality @@ -83,7 +84,7 @@ Sources/Demangling/ Main/TypeDecoder/ — TypeDecoder, TypeBuilder protocol Node/ — Node, Node.Children, NodeBuilder, NodeCache, Kind, Conversions, Sequence, Rewriter Node/Printer/ — NodePrinter, NodePrinterTarget protocol, NodePrintContext/State - Store/ — CompactNode, SymbolStore, SymbolStoreBuilder, NodeReference (evolution proposal 0001) + Store/ — CompactNode, SymbolStore, SymbolStoreBuilder, NodeReference, DemanglingNode (evolution proposal 0001) Enums/ — SugarType, ManglingFlavor, DemanglingError, ManglingError, etc. Utils/ — Extensions, Common constants, Punycode Tests/DemanglingTests/ diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index 26a5ea0..9b60061 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -1,4 +1,7 @@ -public struct NodePrinter: Sendable { +/// Generic tree-printing engine shared by the public `NodePrinter` +/// (specialized on `Node`) and store-backed printing (specialized on +/// `NodeReference`). See evolution proposal 0001, Phase 2. +struct DemanglingPrinter: Sendable { /// Mirrors ``swift::Demangle::NodePrinter::MaxDepth`` from /// ``swift/include/swift/Demangling/Demangle.h``. Bails the print /// recursion with ``<>`` once a single root-to-leaf path @@ -13,7 +16,7 @@ public struct NodePrinter: Sendable { private var dependentMemberTypeDepth: Int = 0 private var printDepth: Int = 0 /// Memoizes the rendered fragment for each shared substitution node. - /// The demangler returns the same ``Node`` instance for every + /// The demangler returns the same ``SomeNode`` instance for every /// back-reference (``A23_`` etc.), so one mangling can produce a DAG /// that — naively walked child-by-child — expands into hundreds of /// thousands of node visits (a SwiftUI ``View.Body`` typealias was @@ -22,7 +25,7 @@ public struct NodePrinter: Sendable { /// brings cost back to the size of the unique node set instead of the /// exponential expansion. Apple's own C++ ``NodePrinter`` does not do /// this (so ``swift demangle`` itself hangs on the same input). - private var printCache: [ObjectIdentifier: Target] = [:] + private var printCache: [SomeNode.PrintCacheIdentity: Target] = [:] public init(options: DemangleOptions = .default) { self.target = .init() @@ -30,12 +33,12 @@ public struct NodePrinter: Sendable { self.options = options } - public mutating func printRoot(_ root: Node) -> Target { + public mutating func printRoot(_ root: SomeNode) -> Target { _ = printName(root) return target } - private mutating func printName(_ name: Node, asPrefixContext: Bool = false) -> Node? { + private mutating func printName(_ name: SomeNode, asPrefixContext: Bool = false) -> SomeNode? { if printDepth > Self.maxPrintDepth { target.write("<>") return nil @@ -45,7 +48,7 @@ public struct NodePrinter: Sendable { // the rendered output for the same node, so those calls bypass the // cache to stay correct. let canCache = !asPrefixContext && dependentMemberTypeDepth == 0 - if canCache, let cached = printCache[ObjectIdentifier(name)] { + if canCache, let cached = printCache[name.printCacheIdentity] { target.append(cached) return nil } @@ -59,14 +62,14 @@ public struct NodePrinter: Sendable { swap(&target, &subTarget) let result = dispatchPrintName(name, asPrefixContext: asPrefixContext) swap(&target, &subTarget) - printCache[ObjectIdentifier(name)] = subTarget + printCache[name.printCacheIdentity] = subTarget target.append(subTarget) return result } return dispatchPrintName(name, asPrefixContext: asPrefixContext) } - private mutating func dispatchPrintName(_ name: Node, asPrefixContext: Bool = false) -> Node? { + private mutating func dispatchPrintName(_ name: SomeNode, asPrefixContext: Bool = false) -> SomeNode? { switch name.kind { case .accessibleFunctionRecord: if !options.contains(.shortenThunk) { @@ -549,7 +552,7 @@ public struct NodePrinter: Sendable { return nil } - private func shouldPrintContext(_ context: Node) -> Bool { + private func shouldPrintContext(_ context: SomeNode) -> Bool { guard options.contains(.qualifyEntities) else { return false } @@ -570,7 +573,7 @@ public struct NodePrinter: Sendable { return true } - private mutating func printOptional(_ optional: Node?, prefix: String? = nil, suffix: String? = nil, asPrefixContext: Bool = false) -> Node? { + private mutating func printOptional(_ optional: SomeNode?, prefix: String? = nil, suffix: String? = nil, asPrefixContext: Bool = false) -> SomeNode? { guard let o = optional else { return nil } prefix.map { target.write($0) } let r = printName(o) @@ -578,11 +581,11 @@ public struct NodePrinter: Sendable { return r } - private mutating func printFirstChild(_ ofName: Node, prefix: String? = nil, suffix: String? = nil, asPrefixContext: Bool = false) { + private mutating func printFirstChild(_ ofName: SomeNode, prefix: String? = nil, suffix: String? = nil, asPrefixContext: Bool = false) { _ = printOptional(ofName.children.at(0), prefix: prefix, suffix: suffix) } - private mutating func printSequence(_ names: S, prefix: String? = nil, suffix: String? = nil, separator: String? = nil) where S: Sequence, S.Element == Node { + private mutating func printSequence(_ names: S, prefix: String? = nil, suffix: String? = nil, separator: String? = nil) where S: Sequence, S.Element == SomeNode { var isFirst = true prefix.map { target.write($0) } for c in names { @@ -596,15 +599,15 @@ public struct NodePrinter: Sendable { suffix.map { target.write($0) } } - private mutating func printChildren(_ ofName: Node, prefix: String? = nil, suffix: String? = nil, separator: String? = nil) { + private mutating func printChildren(_ ofName: SomeNode, prefix: String? = nil, suffix: String? = nil, separator: String? = nil) { printSequence(ofName.children, prefix: prefix, suffix: suffix, separator: separator) } - private mutating func printMacro(name: Node, asPrefixContext: Bool, label: String) -> Node? { + private mutating func printMacro(name: SomeNode, asPrefixContext: Bool, label: String) -> SomeNode? { return printEntity(name, asPrefixContext: asPrefixContext, typePrinting: .noType, hasName: true, extraName: "\(label) macro @\(name.children.at(2)?.print(using: options) ?? "") expansion #", extraIndex: (name.children.at(3)?.index ?? 0) + 1) } - private mutating func printAnonymousContext(_ name: Node) { + private mutating func printAnonymousContext(_ name: SomeNode) { if options.contains(.qualifyEntities), options.contains(.displayExtensionContexts) { _ = printOptional(name.children.at(1)) target.write(".(unknown context at " + (name.children.first?.text ?? "") + ")") @@ -616,7 +619,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printExtension(_ name: Node) { + private mutating func printExtension(_ name: SomeNode) { if options.contains(.qualifyEntities), options.contains(.displayExtensionContexts) { printFirstChild(name, prefix: "(extension in ", suffix: "):", asPrefixContext: true) } @@ -624,25 +627,25 @@ public struct NodePrinter: Sendable { _ = printOptional(!options.contains(.printForTypeName) ? name.children.at(2) : nil) } - private mutating func printSuffix(_ name: Node) { + private mutating func printSuffix(_ name: SomeNode) { if options.contains(.displayUnmangledSuffix) { target.write(" with unmangled suffix ") quotedString(name.text ?? "") } } - private mutating func printPrivateDeclName(_ name: Node) { + private mutating func printPrivateDeclName(_ name: SomeNode) { _ = printOptional(name.children.at(1), prefix: options.contains(.showPrivateDiscriminators) ? "(" : nil) target.write(options.contains(.showPrivateDiscriminators) ? "\(name.children.count > 1 ? " " : "(")in \(name.children.at(0)?.text ?? ""))" : "") } - private mutating func printModule(_ name: Node) { + private mutating func printModule(_ name: SomeNode) { if options.contains(.displayModuleNames) { - target.write(name.text ?? "", context: .context(for: name, state: .printModule)) + target.write(name.text ?? "", context: .context(for: name as? Node, state: .printModule)) } } - private mutating func printReturnType(_ name: Node) { + private mutating func printReturnType(_ name: SomeNode) { if name.children.isEmpty, let t = name.text { target.write(t) } else { @@ -650,13 +653,13 @@ public struct NodePrinter: Sendable { } } - private mutating func printRetroactiveConformance(_ name: Node) { + private mutating func printRetroactiveConformance(_ name: SomeNode) { if name.children.count == 2 { printChildren(name, prefix: "retroactive @ ") } } - private mutating func printGenericSpecializationParam(_ name: Node) { + private mutating func printGenericSpecializationParam(_ name: SomeNode) { printFirstChild(name) _ = printOptional(name.children.at(1), prefix: " with ") for slouse in name.children.slice(2, name.children.endIndex) { @@ -665,7 +668,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printFunctionSignatureSpecializationParam(_ name: Node) { + private mutating func printFunctionSignatureSpecializationParam(_ name: SomeNode) { var idx = 0 var argIdx = 0 let end = name.children.count @@ -757,7 +760,7 @@ public struct NodePrinter: Sendable { /// Prints the next non-kind/non-payload child node from a specialization param, /// applying demangling for function/global props and string leading underscore stripping. - private mutating func printNextParamChildNode(_ node: Node, argIdx: inout Int, kind: FunctionSigSpecializationParamKind?) { + private mutating func printNextParamChildNode(_ node: SomeNode, argIdx: inout Int, kind: FunctionSigSpecializationParamKind?) { while argIdx < node.children.count { let child = node.children[argIdx] argIdx += 1 @@ -795,7 +798,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printFunctionSignatureSpecializationParamKind(_ name: Node) { + private mutating func printFunctionSignatureSpecializationParamKind(_ name: SomeNode) { let raw = name.index ?? 0 var printedOptionSet = false if raw & FunctionSigSpecializationParamKind.existentialToGeneric.rawValue != 0 { @@ -832,41 +835,41 @@ public struct NodePrinter: Sendable { } } - private mutating func printLazyProtocolWitnesstableAccessor(_ name: Node) { + private mutating func printLazyProtocolWitnesstableAccessor(_ name: SomeNode) { _ = printOptional(name.children.at(0), prefix: "lazy protocol witness table accessor for type ") _ = printOptional(name.children.at(1), prefix: " and conformance ") } - private mutating func printLazyProtocolWitnesstableCacheVariable(_ name: Node) { + private mutating func printLazyProtocolWitnesstableCacheVariable(_ name: SomeNode) { _ = printOptional(name.children.at(0), prefix: "lazy protocol witness table cache variable for type ") _ = printOptional(name.children.at(1), prefix: " and conformance ") } - private mutating func printVTableThunk(_ name: Node) { + private mutating func printVTableThunk(_ name: SomeNode) { _ = printOptional(name.children.at(1), prefix: "vtable thunk for ") _ = printOptional(name.children.at(0), prefix: " dispatching to ") } - private mutating func printProtocolWitness(_ name: Node) { + private mutating func printProtocolWitness(_ name: SomeNode) { _ = printOptional(name.children.at(1), prefix: "protocol witness for ") _ = printOptional(name.children.at(0), prefix: " in conformance ") } - private mutating func printPartialApplyForwarder(_ name: Node) { + private mutating func printPartialApplyForwarder(_ name: SomeNode) { target.write("partial apply\(options.contains(.shortenPartialApply) ? "" : " forwarder")") if !name.children.isEmpty { printChildren(name, prefix: " for ") } } - private mutating func printPartialApplyObjCForwarder(_ name: Node) { + private mutating func printPartialApplyObjCForwarder(_ name: SomeNode) { target.write("partial apply\(options.contains(.shortenPartialApply) ? "" : " ObjC forwarder")") if !name.children.isEmpty { printChildren(name, prefix: " for ") } } - private mutating func printKeyPathAccessorThunkHelper(_ name: Node) { + private mutating func printKeyPathAccessorThunkHelper(_ name: SomeNode) { let prefix = switch name.kind { case .keyPathGetterThunkHelper: "getter for " case .keyPathSetterThunkHelper: "setter for " @@ -883,7 +886,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printKeyPathEqualityThunkHelper(_ name: Node) { + private mutating func printKeyPathEqualityThunkHelper(_ name: SomeNode) { target.write("key path index \(name.kind == .keyPathEqualsThunkHelper ? "equality" : "hash") operator for ") var dropLast = false if let lastChild = name.children.last, lastChild.kind == .dependentGenericSignature { @@ -897,12 +900,12 @@ public struct NodePrinter: Sendable { } } - private mutating func printFieldOffset(_ name: Node) { + private mutating func printFieldOffset(_ name: SomeNode) { printFirstChild(name) _ = printOptional(name.children.at(1), prefix: "field offset for ", asPrefixContext: true) } - private mutating func printReabstractionThunk(_ name: Node) { + private mutating func printReabstractionThunk(_ name: SomeNode) { if options.contains(.shortenThunk) { _ = printOptional(name.children.at(name.children.count - 2), prefix: "thunk for ") } else { @@ -914,30 +917,30 @@ public struct NodePrinter: Sendable { } } - private mutating func printAssociatedConformanceDescriptor(_ name: Node) { + private mutating func printAssociatedConformanceDescriptor(_ name: SomeNode) { _ = printOptional(name.children.at(0), prefix: "associated conformance descriptor for ") _ = printOptional(name.children.at(1), prefix: ".") _ = printOptional(name.children.at(2), prefix: ": ") } - private mutating func printDefaultAssociatedConformanceAccessor(_ name: Node) { + private mutating func printDefaultAssociatedConformanceAccessor(_ name: SomeNode) { _ = printOptional(name.children.at(0), prefix: "default associated conformance accessor for ") _ = printOptional(name.children.at(1), prefix: ".") _ = printOptional(name.children.at(2), prefix: ": ") } - private mutating func printAssociatedTypeMetadataAccessor(_ name: Node) { + private mutating func printAssociatedTypeMetadataAccessor(_ name: SomeNode) { _ = printOptional(name.children.at(1), prefix: "associated type metadata accessor for ") _ = printOptional(name.children.at(0), prefix: " in ") } - private mutating func printAssociatedTypeWitnessTableAccessor(_ name: Node) { + private mutating func printAssociatedTypeWitnessTableAccessor(_ name: SomeNode) { _ = printOptional(name.children.at(1), prefix: "associated type witness table accessor for ") _ = printOptional(name.children.at(2), prefix: " : ") _ = printOptional(name.children.at(0), prefix: " in ") } - private mutating func printValueWitness(_ name: Node) { + private mutating func printValueWitness(_ name: SomeNode) { // ValueWitness node structure: first child is Index node with the witness kind let witnessIndex = name.children.first?.index ?? 0 target.write(ValueWitnessKind(rawValue: witnessIndex)?.description ?? "") @@ -946,7 +949,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(1)) } - private mutating func printConcreteProtocolConformance(_ name: Node) { + private mutating func printConcreteProtocolConformance(_ name: SomeNode) { target.write("concrete protocol conformance ") if let index = name.index { target.write("#\(index) ") @@ -960,7 +963,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printMetatype(_ name: Node) { + private mutating func printMetatype(_ name: SomeNode) { if name.children.count == 2 { printFirstChild(name, suffix: " ") } @@ -972,19 +975,19 @@ public struct NodePrinter: Sendable { target.write(type.kind.isExistentialType ? ".Protocol" : ".Type") } - private mutating func printExistentialMetatype(_ name: Node) { + private mutating func printExistentialMetatype(_ name: SomeNode) { if name.children.count == 2 { printFirstChild(name, suffix: " ") } _ = printOptional(name.children.at(name.children.count == 2 ? 1 : 0), suffix: ".Type") } - private mutating func printAssociatedTypeRef(_ name: Node) { + private mutating func printAssociatedTypeRef(_ name: SomeNode) { printFirstChild(name) target.write(".\(name.children.at(1)?.text ?? "")") } - private mutating func printProtocolList(_ name: Node) { + private mutating func printProtocolList(_ name: SomeNode) { guard let typeList = name.children.first else { return } if typeList.children.isEmpty { target.write("Any") @@ -993,7 +996,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printProtocolListWithClass(_ name: Node) { + private mutating func printProtocolListWithClass(_ name: SomeNode) { guard name.children.count >= 2 else { return } _ = printOptional(name.children.at(1), suffix: " & ") if let protocolsTypeList = name.children.first?.children.first { @@ -1001,7 +1004,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printProtocolListWithAnyObject(_ name: Node) { + private mutating func printProtocolListWithAnyObject(_ name: SomeNode) { guard let prot = name.children.first, let protocolsTypeList = prot.children.first else { return } if protocolsTypeList.children.count > 0 { printChildren(protocolsTypeList, suffix: " & ", separator: " & ") @@ -1012,7 +1015,7 @@ public struct NodePrinter: Sendable { target.write("AnyObject") } - private mutating func printProtocolConformance(_ name: Node) { + private mutating func printProtocolConformance(_ name: SomeNode) { if name.children.count == 4 { _ = printOptional(name.children.at(2), prefix: "property behavior storage of ") _ = printOptional(name.children.at(0), prefix: " in ") @@ -1026,7 +1029,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printImplParameter(_ name: Node) { + private mutating func printImplParameter(_ name: SomeNode) { printFirstChild(name, suffix: " ") if name.children.count == 3 { _ = printOptional(name.children.at(1)) @@ -1037,7 +1040,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.last) } - private mutating func printDependentProtocolConformanceAssociated(_ name: Node) { + private mutating func printDependentProtocolConformanceAssociated(_ name: SomeNode) { target.write("dependent associated protocol conformance ") if let index = name.children.at(2)?.index { target.write("#\(index) ") @@ -1047,7 +1050,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(1)) } - private mutating func printDependentProtocolConformanceInherited(_ name: Node) { + private mutating func printDependentProtocolConformanceInherited(_ name: SomeNode) { target.write("dependent inherited protocol conformance ") if let index = name.children.at(2)?.index { target.write("#\(index) ") @@ -1057,7 +1060,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(1)) } - private mutating func printDependentProtocolConformanceRoot(_ name: Node) { + private mutating func printDependentProtocolConformanceRoot(_ name: SomeNode) { target.write("dependent root protocol conformance ") if let index = name.children.at(2)?.index { target.write("#\(index) ") @@ -1067,7 +1070,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(1)) } - private mutating func printDependentProtocolConformanceOpaque(_ name: Node) { + private mutating func printDependentProtocolConformanceOpaque(_ name: SomeNode) { target.write("opaque result conformance ") printFirstChild(name) target.write(" of ") @@ -1089,7 +1092,7 @@ public struct NodePrinter: Sendable { return name } - private mutating func printGenericSignature(_ name: Node) { + private mutating func printGenericSignature(_ name: SomeNode) { target.write("<") var numGenericParams = 0 for c in name.children { @@ -1125,7 +1128,7 @@ public struct NodePrinter: Sendable { return false } - let isGenericParamValue = { (depth: UInt64, index: UInt64) -> Node? in + let isGenericParamValue = { (depth: UInt64, index: UInt64) -> SomeNode? in for var child in name.children.dropFirst(numGenericParams).prefix(firstRequirement) { guard child.kind == .dependentGenericParamValueMarker else { continue } child = child.children.first ?? child @@ -1194,12 +1197,12 @@ public struct NodePrinter: Sendable { target.write(">") } - private mutating func printDependentGenericConformanceRequirement(_ name: Node) { + private mutating func printDependentGenericConformanceRequirement(_ name: SomeNode) { printFirstChild(name) _ = printOptional(name.children.at(1), prefix: ": ") } - private mutating func printDependentGenericLayoutRequirement(_ name: Node) { + private mutating func printDependentGenericLayoutRequirement(_ name: SomeNode) { guard let layout = name.children.at(1), let c = layout.text?.unicodeScalars.first else { return } printFirstChild(name, suffix: ": ") switch c { @@ -1222,18 +1225,18 @@ public struct NodePrinter: Sendable { } } - private mutating func printDependentGenericSameTypeRequirement(_ name: Node) { + private mutating func printDependentGenericSameTypeRequirement(_ name: SomeNode) { printFirstChild(name) _ = printOptional(name.children.at(1), prefix: " == ") } - private mutating func printDependentGenericType(_ name: Node) { + private mutating func printDependentGenericType(_ name: SomeNode) { guard let depType = name.children.at(1) else { return } printFirstChild(name) _ = printOptional(depType, prefix: depType.needSpaceBeforeType ? " " : "") } - private mutating func printDependentMemberType(_ name: Node) { + private mutating func printDependentMemberType(_ name: SomeNode) { dependentMemberTypeDepth += 1 defer { dependentMemberTypeDepth -= 1 } printFirstChild(name) @@ -1241,12 +1244,12 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(1)) } - private mutating func printDependentAssociatedTypeRef(_ name: Node) { + private mutating func printDependentAssociatedTypeRef(_ name: SomeNode) { _ = printOptional(name.children.at(1), suffix: ".") printFirstChild(name) } - private mutating func printSilBoxTypeWithLayout(_ name: Node) { + private mutating func printSilBoxTypeWithLayout(_ name: SomeNode) { guard let layout = name.children.first else { return } _ = printOptional(name.children.at(1), suffix: " ") _ = printName(layout) @@ -1255,7 +1258,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printSugaredOptional(_ name: Node) { + private mutating func printSugaredOptional(_ name: SomeNode) { if let type = name.children.first { let needParens = !type.isSimpleType target.write(needParens ? "(" : "") @@ -1265,12 +1268,12 @@ public struct NodePrinter: Sendable { } } - private mutating func printSugaredDictionary(_ name: Node) { + private mutating func printSugaredDictionary(_ name: SomeNode) { printFirstChild(name, prefix: "[", suffix: " : ") _ = printOptional(name.children.at(1), suffix: "]") } - private mutating func printOpaqueType(_ name: Node) { + private mutating func printOpaqueType(_ name: SomeNode) { printFirstChild(name) target.write(".") _ = printOptional(name.children.at(1)) @@ -1278,7 +1281,7 @@ public struct NodePrinter: Sendable { // printChildren(name, separator: ".") } - private mutating func printImplInvocationsSubstitutions(_ name: Node) { + private mutating func printImplInvocationsSubstitutions(_ name: SomeNode) { if let secondChild = name.children.at(0) { target.write(" for <") printChildren(secondChild, separator: ", ") @@ -1286,7 +1289,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printImplPatternSubstitutions(_ name: Node) { + private mutating func printImplPatternSubstitutions(_ name: SomeNode) { target.write("@substituted ") printFirstChild(name) if let secondChild = name.children.at(1) { @@ -1296,13 +1299,13 @@ public struct NodePrinter: Sendable { } } - private mutating func printImplDifferentiability(_ name: Node) { + private mutating func printImplDifferentiability(_ name: SomeNode) { if let text = name.text, !text.isEmpty { target.write("\(text) ") } } - private mutating func printMacroExpansionLoc(_ name: Node) { + private mutating func printMacroExpansionLoc(_ name: SomeNode) { if let module = name.children.at(0) { target.write("module ") _ = printName(module) @@ -1321,7 +1324,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printGlobalActorFunctionType(_ name: Node) { + private mutating func printGlobalActorFunctionType(_ name: SomeNode) { if let firstChild = name.children.first { target.write("@") _ = printName(firstChild) @@ -1329,7 +1332,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printGlobalVariableOnceFunction(_ name: Node) { + private mutating func printGlobalVariableOnceFunction(_ name: SomeNode) { target.write(name.kind == .globalVariableOnceToken ? "one-time initialization token for " : "one-time initialization function for ") if let firstChild = name.children.first { _ = shouldPrintContext(firstChild) @@ -1339,7 +1342,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printGlobalVariableOnceDeclList(_ name: Node) { + private mutating func printGlobalVariableOnceDeclList(_ name: SomeNode) { if name.children.count == 1 { printFirstChild(name) } else { @@ -1347,7 +1350,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printTypeThrowsAnnotation(_ name: Node) { + private mutating func printTypeThrowsAnnotation(_ name: SomeNode) { target.write(" throws(") if let child = name.children.first { _ = printName(child) @@ -1355,7 +1358,7 @@ public struct NodePrinter: Sendable { target.write(")") } - private mutating func printDifferentiableFunctionType(_ name: Node) { + private mutating func printDifferentiableFunctionType(_ name: SomeNode) { target.write("@differentiable") switch UnicodeScalar(UInt8(name.index ?? 0)) { case "f": target.write("(_forward)") @@ -1365,7 +1368,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printDifferentiabilityWitness(_ name: Node) { + private mutating func printDifferentiabilityWitness(_ name: SomeNode) { let kindNodeIndex = name.children.count - (name.children.last?.kind == .dependentGenericSignature ? 4 : 3) let kind = (name.children.at(kindNodeIndex)?.index).flatMap { Differentiability($0) } switch kind { @@ -1386,7 +1389,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(idx + 3), prefix: " with ") } - private mutating func printAsyncAwaitResumePartialFunction(_ name: Node) { + private mutating func printAsyncAwaitResumePartialFunction(_ name: SomeNode) { if options.contains(.showAsyncResumePartial) { target.write("(") _ = printName(name.children.first!) @@ -1395,7 +1398,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printAsyncSuspendResumePartialFunction(_ name: Node) { + private mutating func printAsyncSuspendResumePartialFunction(_ name: SomeNode) { if options.contains(.showAsyncResumePartial) { target.write("(") _ = printName(name.children.first!) @@ -1404,11 +1407,11 @@ public struct NodePrinter: Sendable { } } - private mutating func printExtendedExistentialTypeShape(_ name: Node) { + private mutating func printExtendedExistentialTypeShape(_ name: SomeNode) { let savedDisplayWhereClauses = options.contains(.displayWhereClauses) options.insert(.displayWhereClauses) - var genSig: Node? - var type: Node? + var genSig: SomeNode? + var type: SomeNode? if name.children.count == 2 { genSig = name.children.at(1) type = name.children.at(2) @@ -1431,7 +1434,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printSymbolicExtendedExistentialType(_ name: Node) { + private mutating func printSymbolicExtendedExistentialType(_ name: SomeNode) { guard let shape = name.children.first else { return } let isUnique = shape.kind == .uniqueExtendedExistentialTypeShapeSymbolicReference target.write("symbolic existential type (\(isUnique ? "" : "non-")unique) 0x") @@ -1446,7 +1449,7 @@ public struct NodePrinter: Sendable { target.write(">") } - private mutating func printTupleElement(_ name: Node) { + private mutating func printTupleElement(_ name: SomeNode) { if let label = name.children.first(where: { $0.kind == .tupleElementName }) { target.write("\(label.text ?? ""): ") } @@ -1457,7 +1460,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printObjCAsyncCompletionHandlerImpl(_ name: Node) { + private mutating func printObjCAsyncCompletionHandlerImpl(_ name: SomeNode) { if name.kind == .checkedObjCAsyncCompletionHandlerImpl { target.write("predefined ") } @@ -1475,7 +1478,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printImplInvocationSubstitutions(_ name: Node) { + private mutating func printImplInvocationSubstitutions(_ name: SomeNode) { if let secondChild = name.children.at(0) { target.write(" for <") printChildren(secondChild, separator: ", ") @@ -1483,9 +1486,9 @@ public struct NodePrinter: Sendable { } } - private mutating func printImplDifferentiabilityKind(_ name: Node) { + private mutating func printImplDifferentiabilityKind(_ name: SomeNode) { target.write("@differentiable") - if case .index(let value) = name.contents, let differentiability = Differentiability(value) { + if let value = name.index, let differentiability = Differentiability(value) { switch differentiability { case .normal: break case .linear: target.write("(_linear)") @@ -1495,12 +1498,12 @@ public struct NodePrinter: Sendable { } } - private mutating func printImplCoroutineKind(_ name: Node) { - guard case .text(let value) = name.contents, !value.isEmpty else { return } + private mutating func printImplCoroutineKind(_ name: SomeNode) { + guard let value = name.text, !value.isEmpty else { return } target.write("@\(value)") } - private mutating func printImplFunctionConvention(_ name: Node) { + private mutating func printImplFunctionConvention(_ name: SomeNode) { target.write("@convention(") if let second = name.children.at(1) { target.write("\(name.children.at(0)?.text ?? ""), mangledCType: \"") @@ -1512,17 +1515,17 @@ public struct NodePrinter: Sendable { target.write(")") } - private mutating func printImplParameterName(_ name: Node) { - guard case .text(let value) = name.contents, !value.isEmpty else { return } + private mutating func printImplParameterName(_ name: SomeNode) { + guard let value = name.text, !value.isEmpty else { return } target.write("\(value) ") } - private mutating func printBaseConformanceDescriptor(_ name: Node) { + private mutating func printBaseConformanceDescriptor(_ name: SomeNode) { printFirstChild(name, prefix: "base conformance descriptor for ") _ = printOptional(name.children.at(1), prefix: ": ") } - private mutating func printReabstractionThunkHelperWithSelf(_ name: Node) { + private mutating func printReabstractionThunkHelperWithSelf(_ name: SomeNode) { target.write("reabstraction thunk ") var idx = 0 if name.children.count == 4 { @@ -1534,17 +1537,17 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(idx), prefix: " self ") } - private mutating func printReabstracctionThunkHelperWithGlobalActor(_ name: Node) { + private mutating func printReabstracctionThunkHelperWithGlobalActor(_ name: SomeNode) { printFirstChild(name) _ = printOptional(name.children.at(1), prefix: " with global actor constraint ") } - private mutating func printBuildInFixedArray(_ name: Node) { + private mutating func printBuildInFixedArray(_ name: SomeNode) { _ = printOptional(name.children.first, prefix: "Builtin.FixedArray<") _ = printOptional(name.children.at(1), prefix: ", ", suffix: ">") } - private mutating func printAutoDiffFunctionOrSimpleThunk(_ name: Node) { + private mutating func printAutoDiffFunctionOrSimpleThunk(_ name: SomeNode) { var prefixEndIndex = 0 while prefixEndIndex < name.children.count, name.children[prefixEndIndex].kind != .autoDiffFunctionKind { prefixEndIndex += 1 @@ -1558,7 +1561,7 @@ public struct NodePrinter: Sendable { } _ = printOptional(funcKind) target.write(" of ") - var optionalGenSig: Node? + var optionalGenSig: SomeNode? for i in 0 ..< prefixEndIndex { if i == prefixEndIndex - 1, name.children.at(i)?.kind == .dependentGenericSignature { optionalGenSig = name.children.at(i) @@ -1576,7 +1579,7 @@ public struct NodePrinter: Sendable { _ = printOptional(options.contains(.displayWhereClauses) ? optionalGenSig : nil, prefix: " with ") } - private mutating func printAutoDiffFunctionKind(_ name: Node) { + private mutating func printAutoDiffFunctionKind(_ name: SomeNode) { guard let kind = name.index else { return } switch AutoDiffFunctionKind(kind) { case .forward: target.write("forward-mode derivative") @@ -1587,13 +1590,13 @@ public struct NodePrinter: Sendable { } } - private mutating func printAutoDiffSelfReorderingReabstractionThunk(_ name: Node) { + private mutating func printAutoDiffSelfReorderingReabstractionThunk(_ name: SomeNode) { target.write("autodiff self-reordering reabstraction thunk ") let fromType = name.children.first _ = printOptional(options.contains(.shortenThunk) ? fromType : nil, prefix: "for ") let toType = name.children.at(1) var kindIndex = 2 - var optionalGenSig: Node? + var optionalGenSig: SomeNode? if name.children.at(kindIndex)?.kind == .dependentGenericSignature { optionalGenSig = name.children.at(kindIndex) kindIndex += 1 @@ -1605,7 +1608,7 @@ public struct NodePrinter: Sendable { _ = printOptional(toType, prefix: " to ") } - private mutating func printAutoDiffSubsetParametersThunk(_ name: Node) { + private mutating func printAutoDiffSubsetParametersThunk(_ name: SomeNode) { target.write("autodiff subset parameters thunk for ") let lastIndex = name.children.count - 1 let toParamIndices = name.children.at(lastIndex) @@ -1631,7 +1634,7 @@ public struct NodePrinter: Sendable { _ = printOptional(currentIndex > 0 ? name.children.at(currentIndex) : nil, prefix: " of type ") } - private mutating func printIndexSubset(_ name: Node) { + private mutating func printIndexSubset(_ name: SomeNode) { target.write("{") var printedAnyIndex = false for (i, c) in (name.text ?? "").enumerated() { @@ -1647,12 +1650,12 @@ public struct NodePrinter: Sendable { target.write("}") } - private mutating func printBaseWitnessTableAccessor(_ name: Node) { + private mutating func printBaseWitnessTableAccessor(_ name: SomeNode) { _ = printOptional(name.children.at(1), prefix: "base witness table accessor for ") _ = printOptional(name.children.at(0), prefix: " in ") } - private mutating func printDependentGenericInverseConformanceRequirement(_ name: Node) { + private mutating func printDependentGenericInverseConformanceRequirement(_ name: SomeNode) { printFirstChild(name, suffix: ": ~") switch name.children.at(1)?.index { case 0: target.write("Swift.Copyable") @@ -1661,21 +1664,21 @@ public struct NodePrinter: Sendable { } } - private mutating func printDependentGenericSameShapeRequirement(_ name: Node) { + private mutating func printDependentGenericSameShapeRequirement(_ name: SomeNode) { _ = printOptional(name.children.at(0), suffix: ".shape == ") _ = printOptional(name.children.at(1), suffix: ".shape") } - private mutating func printConstrainedExistential(_ name: Node) { + private mutating func printConstrainedExistential(_ name: SomeNode) { printFirstChild(name, prefix: "any ") _ = printOptional(name.children.at(1), prefix: "<", suffix: ">") } - private mutating func printIdentifier(_ name: Node, asPrefixContext: Bool = false, parentKind: Node.Kind? = nil) { - target.write(name.text ?? "", context: .context(for: name, parentKind: parentKind, state: .printIdentifier)) + private mutating func printIdentifier(_ name: SomeNode, asPrefixContext: Bool = false, parentKind: Node.Kind? = nil) { + target.write(name.text ?? "", context: .context(for: name as? Node, parentKind: parentKind, state: .printIdentifier)) } - private mutating func printAbstractStorage(_ name: Node?, asPrefixContext: Bool, extraName: String) -> Node? { + private mutating func printAbstractStorage(_ name: SomeNode?, asPrefixContext: Bool, extraName: String) -> SomeNode? { guard let n = name else { return nil } switch n.kind { case .variable: return printEntity(n, asPrefixContext: asPrefixContext, typePrinting: .withColon, hasName: true, extraName: extraName) @@ -1684,7 +1687,7 @@ public struct NodePrinter: Sendable { } } - private mutating func printEntityType(name: Node, type: Node, genericFunctionTypeList: Node?) { + private mutating func printEntityType(name: SomeNode, type: SomeNode, genericFunctionTypeList: SomeNode?) { let labelList = name.children.first(where: { $0.kind == .labelList }) if labelList != nil || genericFunctionTypeList != nil { if let gftl = genericFunctionTypeList { @@ -1722,8 +1725,8 @@ public struct NodePrinter: Sendable { } } - private mutating func printEntity(_ name: Node, asPrefixContext: Bool, typePrinting: TypePrinting, hasName: Bool, extraName: String? = nil, extraIndex: UInt64? = nil, overwriteName: String? = nil) -> Node? { - var genericFunctionTypeList: Node? + private mutating func printEntity(_ name: SomeNode, asPrefixContext: Bool, typePrinting: TypePrinting, hasName: Bool, extraName: String? = nil, extraIndex: UInt64? = nil, overwriteName: String? = nil) -> SomeNode? { + var genericFunctionTypeList: SomeNode? var name = name if name.kind == .boundGenericFunction, let first = name.children.at(0), let second = name.children.at(1) { name = first @@ -1737,7 +1740,7 @@ public struct NodePrinter: Sendable { } guard let context = name.children.first else { return nil } - var postfixContext: Node? + var postfixContext: SomeNode? if shouldPrintContext(context) { if multiWordName { postfixContext = context @@ -1850,7 +1853,7 @@ public struct NodePrinter: Sendable { return postfixContext } - private mutating func printSpecializationPrefix(_ name: Node, description: String, paramPrefix: String = "") { + private mutating func printSpecializationPrefix(_ name: SomeNode, description: String, paramPrefix: String = "") { if !options.contains(.displayGenericSpecializations) { if !specializationPrefixPrinted { if name.children.first?.kind == .representationChanged { @@ -1896,7 +1899,7 @@ public struct NodePrinter: Sendable { target.write("> of ") } - private mutating func printFunctionParameters(labelList: Node?, parameterType: Node, showTypes: Bool) { + private mutating func printFunctionParameters(labelList: SomeNode?, parameterType: SomeNode, showTypes: Bool) { guard parameterType.kind == .argumentTuple else { return } guard let t = parameterType.children.first, t.kind == .type else { return } guard let parameters = t.children.first else { return } @@ -1915,17 +1918,17 @@ public struct NodePrinter: Sendable { target.write("(") for tuple in parameters.children.enumerated() { if let label = labelList?.children.at(tuple.offset) { - target.write(label.kind == .identifier ? (label.text ?? "") : "_", context: .context(for: parameterType, state: .printFunctionParameters)) + target.write(label.kind == .identifier ? (label.text ?? "") : "_", context: .context(for: parameterType as? Node, state: .printFunctionParameters)) target.write(":") if showTypes { target.write(" ") } } else if !showTypes { if let label = tuple.element.children.first(where: { $0.kind == .tupleElementName }) { - target.write(label.text ?? "", context: .context(for: parameterType, state: .printFunctionParameters)) + target.write(label.text ?? "", context: .context(for: parameterType as? Node, state: .printFunctionParameters)) target.write(":") } else { - target.write("_", context: .context(for: parameterType, state: .printFunctionParameters)) + target.write("_", context: .context(for: parameterType as? Node, state: .printFunctionParameters)) target.write(":") } } @@ -1940,7 +1943,7 @@ public struct NodePrinter: Sendable { target.write(")") } - private mutating func printConventionWithMangledCType(_ name: Node, label: String) { + private mutating func printConventionWithMangledCType(_ name: SomeNode, label: String) { target.write("@convention(\(label)") if let firstChild = name.children.first, firstChild.kind == .clangType { target.write(", mangledCType: \"") @@ -1950,7 +1953,7 @@ public struct NodePrinter: Sendable { target.write(") ") } - private mutating func printFunctionType(labelList: Node? = nil, _ name: Node) { + private mutating func printFunctionType(labelList: SomeNode? = nil, _ name: SomeNode) { switch name.kind { case .autoClosureType, .escapingAutoClosureType: target.write("@autoclosure ") @@ -1982,7 +1985,7 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(startIndex)) startIndex += 1 } - var nonIsolatedCallerNode: Node? + var nonIsolatedCallerNode: SomeNode? if name.children.at(startIndex)?.kind == .nonIsolatedCallerFunctionType { nonIsolatedCallerNode = name.children.at(startIndex) startIndex += 1 @@ -1995,7 +1998,7 @@ public struct NodePrinter: Sendable { diffKind = UnicodeScalar(UInt8(name.children.at(startIndex)?.index ?? 0)) startIndex += 1 } - var thrownErrorNode: Node? + var thrownErrorNode: SomeNode? if name.children.at(startIndex)?.kind == .throwsAnnotation || name.children.at(startIndex)?.kind == .typedThrowsAnnotation { thrownErrorNode = name.children.at(startIndex) startIndex += 1 @@ -2044,14 +2047,14 @@ public struct NodePrinter: Sendable { _ = printOptional(name.children.at(argIndex + 1)) } - private mutating func printBoundGenericNoSugar(_ name: Node) { + private mutating func printBoundGenericNoSugar(_ name: SomeNode) { guard let typeList = name.children.at(1) else { return } printFirstChild(name) guard !options.contains(.removeBoundGeneric) else { return } printChildren(typeList, prefix: "<", suffix: ">", separator: ", ") } - private func findSugar(_ name: Node) -> SugarType { + private func findSugar(_ name: SomeNode) -> SugarType { guard let firstChild = name.children.at(0) else { return .none } if name.children.count == 1, firstChild.kind == .type { return findSugar(firstChild) } @@ -2083,7 +2086,7 @@ public struct NodePrinter: Sendable { return .none } - private mutating func printBoundGeneric(_ name: Node) { + private mutating func printBoundGeneric(_ name: SomeNode) { guard name.children.count >= 2 else { return } guard name.children.count == 2, options.contains(.synthesizeSugarOnTypes), name.kind != .boundGenericClass else { printBoundGenericNoSugar(name) @@ -2117,12 +2120,12 @@ public struct NodePrinter: Sendable { } private enum PrintImplFunctionTypeState: Int { case attrs, inputs, results } - private mutating func printImplFunctionType(_ name: Node) { + private mutating func printImplFunctionType(_ name: SomeNode) { var curState: PrintImplFunctionTypeState = .attrs - var patternSubs: Node? - var invocationSubs: Node? - var sendingResult: Node? - let transitionTo = { (printer: inout NodePrinter, newState: PrintImplFunctionTypeState) in + var patternSubs: SomeNode? + var invocationSubs: SomeNode? + var sendingResult: SomeNode? + let transitionTo = { (printer: inout Self, newState: PrintImplFunctionTypeState) in while curState != newState { switch curState { case .attrs: @@ -2200,3 +2203,23 @@ public struct NodePrinter: Sendable { target.write("\"") } } + +/// Prints a `Node` tree to `Target`. +/// +/// A thin, source-compatible facade over `DemanglingPrinter`. +/// The printing logic lives in the generic engine so it can also print +/// `NodeReference` trees straight from a `SymbolStore` without materializing +/// a class tree (see `NodeReference.print(using:)`). +public struct NodePrinter: Sendable { + public static var maxPrintDepth: Int { DemanglingPrinter.maxPrintDepth } + + private var engine: DemanglingPrinter + + public init(options: DemangleOptions = .default) { + self.engine = DemanglingPrinter(options: options) + } + + public mutating func printRoot(_ root: Node) -> Target { + engine.printRoot(root) + } +} diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift new file mode 100644 index 0000000..bb3a21b --- /dev/null +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -0,0 +1,169 @@ +/// The read-only tree shape shared by `Node` (the class tree) and +/// `NodeReference` (a handle into a `SymbolStore`). +/// +/// It exists so that traversal-only consumers — first the `NodePrinter` +/// engine — can walk either representation without materializing a class +/// tree. The member names deliberately match `Node`'s existing API so that +/// the generic engine's body is identical whether specialized on `Node` or +/// `NodeReference`. See evolution proposal 0001, Phase 2. +public protocol DemanglingNode: Sendable { + associatedtype Children: DemanglingNodeChildren where Children.Element == Self + + /// A per-tree-node identity used to memoize shared subtrees while printing. + /// For `Node` this is `ObjectIdentifier`; for `NodeReference` it is the + /// store index. It must be equal exactly when two handles denote the same + /// shared node within a single traversal. + associatedtype PrintCacheIdentity: Hashable & Sendable + + var kind: Node.Kind { get } + var text: String? { get } + var index: UInt64? { get } + var hasIndex: Bool { get } + var children: Children { get } + var printCacheIdentity: PrintCacheIdentity { get } +} + +// MARK: - Derived helpers shared by the printer + +/// Generic ports of the `Node` convenience properties the printer engine uses. +/// On `Node` its own concrete members take precedence (identical behavior); the +/// generic engine resolves these when specialized on `NodeReference`. +extension DemanglingNode { + /// Prints this subtree with the given options. Mirrors `Node.print(using:)`. + public func print(using options: DemangleOptions = .default) -> String { + StackSafeExecutor.execute { + var printer = DemanglingPrinter(options: options) + return printer.printRoot(self) + } + } + + public func isIdentifier(desired: String) -> Bool { + kind == .identifier && text == desired + } + + public var isSwiftModule: Bool { + kind == .module && text == stdlibName + } + + public var isSimpleType: Bool { + switch kind { + case .associatedType, + .associatedTypeRef, + .boundGenericClass, + .boundGenericEnum, + .boundGenericFunction, + .boundGenericOtherNominalType, + .boundGenericProtocol, + .boundGenericStructure, + .boundGenericTypeAlias, + .builtinBorrow, + .builtinTypeName, + .builtinTupleType, + .builtinFixedArray, + .class, + .dependentGenericType, + .dependentMemberType, + .dependentGenericParamType, + .dynamicSelf, + .enum, + .errorType, + .existentialMetatype, + .integer, + .labelList, + .metatype, + .metatypeRepresentation, + .module, + .negativeInteger, + .otherNominalType, + .pack, + .protocol, + .protocolSymbolicReference, + .returnType, + .silBoxType, + .silBoxTypeWithLayout, + .structure, + .sugaredArray, + .sugaredDictionary, + .sugaredOptional, + .sugaredInlineArray, + .sugaredParen, + .tuple, + .tupleElementName, + .typeAlias, + .typeList, + .typeSymbolicReference: + return true + case .type: + return children.first.map(\.isSimpleType) ?? false + case .protocolList: + return children.first.map { $0.children.count <= 1 } ?? false + case .protocolListWithAnyObject: + return (children.first?.children.first).map { $0.children.count == 0 } ?? false + default: + return false + } + } + + public var needSpaceBeforeType: Bool { + switch kind { + case .type: + return children.first?.needSpaceBeforeType ?? false + case .functionType, + .noEscapeFunctionType, + .uncurriedFunctionType, + .dependentGenericType: + return false + default: + return true + } + } +} + +/// A node's children as a random-access collection, extended with the +/// safe-indexing helpers the printer relies on (`at`, `slice`). +public protocol DemanglingNodeChildren: RandomAccessCollection where Index == Int { + func at(_ index: Int) -> Element? + func slice(_ from: Int, _ to: Int) -> ArraySlice +} + +extension DemanglingNodeChildren { + @inlinable + public func at(_ index: Int) -> Element? { + (index >= startIndex && index < endIndex) ? self[index] : nil + } + + @inlinable + public func slice(_ from: Int, _ to: Int) -> ArraySlice { + let elements = Array(self) + if from > to || from > elements.endIndex || to < elements.startIndex { + return ArraySlice() + } + let lowerBound = Swift.max(from, elements.startIndex) + let upperBound = Swift.min(to, elements.endIndex) + return elements[lowerBound ..< upperBound] + } +} + +// MARK: - Node conformance + +extension Node: DemanglingNode { + @inlinable + public var printCacheIdentity: ObjectIdentifier { ObjectIdentifier(self) } +} + +extension Node.Children: DemanglingNodeChildren {} + +// MARK: - NodeReference conformance + +extension NodeReference: DemanglingNode { + @inlinable + public var hasIndex: Bool { + if case .index = compactNode.payloadKind { return true } + return false + } + + @inlinable + public var printCacheIdentity: SymbolStore.NodeIndex { nodeIndex } +} + +extension NodeReference.ChildrenView: DemanglingNodeChildren {} diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift index f953824..5754fde 100644 --- a/Sources/Demangling/Store/NodeReference.swift +++ b/Sources/Demangling/Store/NodeReference.swift @@ -26,10 +26,21 @@ public struct NodeReference: Sendable { } /// The text contents, if this node carries text. + /// + /// Mirrors `Node.text`: for `.dependentGenericParamType` (which stores + /// `[depth, index]` as children rather than text) the generic parameter + /// name is synthesized, matching what the printer expects. public var text: String? { let compact = compactNode - guard case .text = compact.payloadKind else { return nil } - return store.text(offset: compact.payloadWord0, length: compact.payloadWord1) + if case .text = compact.payloadKind { + return store.text(offset: compact.payloadWord0, length: compact.payloadWord1) + } + if compact.kind == .dependentGenericParamType { + let childrenView = children + guard let depth = childrenView.at(0)?.index, let parameterIndex = childrenView.at(1)?.index else { return nil } + return genericParameterName(depth: depth, index: parameterIndex) + } + return nil } /// The index contents, if this node carries an index. @@ -53,12 +64,13 @@ public struct NodeReference: Sendable { store.materializeNode(at: nodeIndex.rawValue) } - /// Prints the demangled form of this subtree. - /// - /// Phase 1 materializes and delegates to `Node.print(using:)`; proposal - /// 0001 Phase 2 replaces this with a zero-materialization printer path. + /// Prints the demangled form of this subtree directly from the store, + /// without materializing a `Node` tree (proposal 0001, Phase 2). public func print(using options: DemangleOptions = .default) -> String { - materialize().print(using: options) + StackSafeExecutor.execute { + var printer = DemanglingPrinter(options: options) + return printer.printRoot(self) + } } } diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index 1f7507e..e1ed419 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -139,12 +139,22 @@ struct SymbolStoreTests { // MARK: - Demangle Parity @Test func demangleParityWithNodePath() throws { + // Zero-materialization store printing must be byte-identical to the Node + // path across option sets, including generic params (dependentGenericParamType + // text synthesis) and sugared types. let mangledSymbols = [ "$sSiD", "$sSaySiGD", "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", "$s4main3FooVAA1P0B0fMq_", "$s7SwiftUI4ViewP", + "$s4main1gyxxlF", // g(A) -> A + "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", // ModifiedContent conformance + ] + let optionSets: [DemangleOptions] = [ + .default, + .simplified, + .default.union(.synthesizeSugarOnTypes), ] var builder = SymbolStoreBuilder() @@ -154,10 +164,12 @@ struct SymbolStoreTests { } let store = builder.freeze() - for (rootIndex, mangled) in zip(rootIndices, mangledSymbols) { - let storePrinted = store.reference(at: rootIndex).print(using: .default) - let nodePrinted = try demangleAsNode(mangled, internsSubtrees: false).print(using: .default) - #expect(storePrinted == nodePrinted, "Store-backed printing should match the Node path for \(mangled)") + for options in optionSets { + for (rootIndex, mangled) in zip(rootIndices, mangledSymbols) { + let storePrinted = store.reference(at: rootIndex).print(using: options) + let nodePrinted = try demangleAsNode(mangled, internsSubtrees: false).print(using: options) + #expect(storePrinted == nodePrinted, "Store-backed printing should match the Node path for \(mangled)") + } } } diff --git a/evolution/0001-symbol-store-arena.md b/evolution/0001-symbol-store-arena.md index a3e9e40..cdd9306 100644 --- a/evolution/0001-symbol-store-arena.md +++ b/evolution/0001-symbol-store-arena.md @@ -139,3 +139,5 @@ store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量 | 2026-07-23 | Created as Draft | 基于 48B class 下限与 C++ 24B 对比分析,确定 arena + 索引句柄方向;分四阶段渐进迁移,不破坏现有 `Node` API | | 2026-07-23 | Status → In Progress,Phase 1 落地 | 用户确认迭代方向,在 worktree `feature/symbol-store` 实施。`CompactNode`(实测 size/stride = 12)、`SymbolStoreBuilder`(`~Copyable` + `consuming freeze()`)、`SymbolStore`、`NodeReference` + `ChildrenView` 完成,含 `intern(_ node:)` 导入与 `materialize()` 导出互操作、桥接式 `demangle(_:)`(经 `internsSubtrees: false` 的临时 `Node` 树) | | 2026-07-23 | Phase 1 实测达标 | 49k 语料:唯一节点 201,876(与 `NodeCache` 全树 hash-consing 计数逐一吻合,交叉验证正确性);平铺存储 3.0 MB(nodes 2.4 + edges 0.43 + text 0.26),优于 ≤6 MB 目标;打印抽样 2000 条零差异;构建 0.87s,不劣于 class 路径。**新发现**:构建期高水位 ~16 MB,由 intern 表(`[CompactNode: UInt32]` 等,~10 MB 量级)与桥接路径的临时 class 节点构成,且已冻结后 dirty pages 不随 `malloc_zone_pressure_relief` 回落;两轮连建仅 +6 MB,确认内存复用、无累积。结论:①「49k ≤6 MB」按保留存储口径已达成,进程口径需 Phase 3(直写 arena,消除临时树)+ intern 表瘦身(改为指向 nodes 缓冲的 open-addressing 索引表,去掉独立 key 存储);②桥接路径会向全局 `NodeCache` 写入叶节点(实测 11k 条),Phase 3 前的批量用户建议构建后 `NodeCache.shared.clear()` | +| 2026-07-23 | Phase 2(打印)落地:零物化 store 打印 | 引入 `DemanglingNode` 只读协议(`kind`/`text`/`index`/`hasIndex`/`children` + `printCacheIdentity` 抽象缓存身份;`isSimpleType`/`needSpaceBeforeType`/`isIdentifier`/`isSwiftModule`/`print` 作为协议扩展从原语派生),`Node` 与 `NodeReference` 双双 conform。把 2179 行的 printer 引擎泛型化为 `DemanglingPrinter`(发现 printer 是纯只读消费者,全程不构造节点,泛型化干净),保留公共 `NodePrinter` 薄包装转发到 `DemanglingPrinter`——**公共 API 零破坏**。`NodeReference.print` 直接走 `DemanglingPrinter<_, NodeReference>`,不再 `materialize()`。**关键正确性点**:`NodeReference.text` 必须镜像 `Node.text` 对 `.dependentGenericParamType` 的泛型名合成(printer line 199 依赖之);`NodePrintContext.node` 是具体 `Node?`,store 路径以 `name as? Node`(NodeReference → nil)优雅降级,`String` target 无视 context 故无影响。**验证**:49k 语料 × 3 套选项(default/simplified/synthesizeSugar)store 打印与 Node 打印逐字节零差异;全量 dyld cache 对齐测试 + TypeDecoder 测试全绿 | +| 2026-07-23 | 事故与恢复 | 外部工具删除了 `.claude/worktrees/` 目录,Phase 2 的未提交改动随磁盘丢失(三个 commit 因在 git 对象库中而安全)。因 printer 改造为确定性脚本化流程(perl + 精确 Edit),已从会话记录逐字复现全部 Phase 2 变更,重建 worktree 后重跑构建/测试验证一致 | From b9252efda95a0e0c37a12d9f2bca053f9ca5774b Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 22:24:02 +0800 Subject: [PATCH 04/23] fix(Demangling): preserve subtree sharing when materializing from SymbolStore materializeNode was a naive recursive rebuild: a hash-consed subtree referenced from multiple parents materialized into duplicate Node instances, expanding the store's DAG into a tree. For symbols with heavy substitution sharing this multiplies node count and defeats the printer's per-instance memoization (printCache keys on ObjectIdentifier, so every duplicate re-renders). Add an index-keyed memo so each store index materializes once and shared subtrees stay shared (===) in the rebuilt tree. New test asserts the generic parameter subtree of $s4main1gyxxlF materializes as one shared instance and the tree equals the Node-path result. --- Sources/Demangling/Store/SymbolStore.swift | 35 ++++++++++++++------ Tests/DemanglingTests/SymbolStoreTests.swift | 20 +++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Sources/Demangling/Store/SymbolStore.swift b/Sources/Demangling/Store/SymbolStore.swift index d683c4a..fa758df 100644 --- a/Sources/Demangling/Store/SymbolStore.swift +++ b/Sources/Demangling/Store/SymbolStore.swift @@ -87,22 +87,35 @@ public final class SymbolStore: Sendable { /// Rebuilds a `Node` tree for the subtree rooted at the given raw index. /// /// The returned tree is freshly constructed and does not interact with the - /// global `NodeCache`. + /// global `NodeCache`. The store is hash-consed, so a subtree referenced + /// from multiple parents is a single index; the memo rebuilds each index + /// once and reuses the instance, preserving the store's DAG shape. + /// Expanding instead would multiply node count for symbols with heavy + /// substitution sharing and defeat the printer's per-instance memoization. func materializeNode(at rawIndex: UInt32) -> Node { + var materializedByIndex: [UInt32: Node] = [:] + return materializeNode(at: rawIndex, materializedByIndex: &materializedByIndex) + } + + private func materializeNode(at rawIndex: UInt32, materializedByIndex: inout [UInt32: Node]) -> Node { + if let shared = materializedByIndex[rawIndex] { + return shared + } let compact = compactNode(at: rawIndex) + let node: Node switch compact.payloadKind { case .none: - return Node(kind: compact.kind) + node = Node(kind: compact.kind) case .index: - return Node(kind: compact.kind, index: UInt64(compact.payloadWord0) | (UInt64(compact.payloadWord1) << 32)) + node = Node(kind: compact.kind, index: UInt64(compact.payloadWord0) | (UInt64(compact.payloadWord1) << 32)) case .text: - return Node(kind: compact.kind, text: text(offset: compact.payloadWord0, length: compact.payloadWord1)) + node = Node(kind: compact.kind, text: text(offset: compact.payloadWord0, length: compact.payloadWord1)) case .oneChild: - return Node(kind: compact.kind, children: [materializeNode(at: compact.payloadWord0)]) + node = Node(kind: compact.kind, children: [materializeNode(at: compact.payloadWord0, materializedByIndex: &materializedByIndex)]) case .twoChildren: - return Node(kind: compact.kind, children: [ - materializeNode(at: compact.payloadWord0), - materializeNode(at: compact.payloadWord1), + node = Node(kind: compact.kind, children: [ + materializeNode(at: compact.payloadWord0, materializedByIndex: &materializedByIndex), + materializeNode(at: compact.payloadWord1, materializedByIndex: &materializedByIndex), ]) case .manyChildren: let edgesStart = Int(compact.payloadWord0) @@ -110,9 +123,11 @@ public final class SymbolStore: Sendable { var children = [Node]() children.reserveCapacity(childCount) for edgeOffset in edgesStart ..< (edgesStart + childCount) { - children.append(materializeNode(at: edges[edgeOffset])) + children.append(materializeNode(at: edges[edgeOffset], materializedByIndex: &materializedByIndex)) } - return Node(kind: compact.kind, children: children) + node = Node(kind: compact.kind, children: children) } + materializedByIndex[rawIndex] = node + return node } } diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index e1ed419..48b6c8a 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -173,6 +173,26 @@ struct SymbolStoreTests { } } + @Test func materializePreservesSubtreeSharing() throws { + // g(A) -> A: the generic parameter type occurs as both argument and + // return type, so the hash-consed store holds one index for it. The + // materialized tree must keep that sharing as one Node instance rather + // than expanding the DAG into duplicates. + var builder = SymbolStoreBuilder() + let rootIndex = try builder.demangle("$s4main1gyxxlF") + let store = builder.freeze() + + let materialized = store.reference(at: rootIndex).materialize() + let nodePathTree = try demangleAsNode("$s4main1gyxxlF", internsSubtrees: false) + #expect(materialized == nodePathTree) + + let genericParameterNodes = materialized.all(of: .dependentGenericParamType) + #expect(genericParameterNodes.count >= 2, "Expected the generic parameter to occur in multiple positions") + for genericParameterNode in genericParameterNodes.dropFirst() { + #expect(genericParameterNode === genericParameterNodes[0], "Shared store subtrees should materialize as one shared instance") + } + } + @Test func sharedSubtreesAcrossSymbolsShareIndices() throws { var builder = SymbolStoreBuilder() let firstRootIndex = try builder.demangle("$sSiD") From 1d105731b8cc35a0ca7a00f4808e7c56b88b6f10 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 22:24:09 +0800 Subject: [PATCH 05/23] refactor(Demangling): single source of truth for printer-derived node helpers isSimpleType, needSpaceBeforeType, isIdentifier(desired:), and isSwiftModule existed twice: as concrete members on Node and as DemanglingNode protocol-extension members. The copies were identical today, but the generic printer engine statically dispatches to the extension (extension members are not protocol requirements), so the Node copy was dead weight inside the engine and a silent-drift hazard for anything else using it. Delete the Node copies and keep the DemanglingNode extension as the single implementation; concrete-Node callers (including downstream packages) resolve to the extension with identical behavior. Mark isIdentifier/isSwiftModule @inlinable to preserve the previous inlinability, and correct the extension doc comment that wrongly claimed Node's concrete members take precedence in the engine. Also document the materialize sharing guarantee and the single-copy rule in AGENTS.md and the proposal 0001 decision log. --- AGENTS.md | 4 +- .../Demangling/Node/Node+Conversions.swift | 82 ++----------------- Sources/Demangling/Store/DemanglingNode.swift | 10 ++- evolution/0001-symbol-store-arena.md | 1 + 4 files changed, 15 insertions(+), 82 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7489b80..a293a5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,8 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. - **`Node` as `Sequence`** (`Node+Sequence.swift`) — `Node` conforms to `Sequence` with preorder traversal as default. Also provides `.inorder()`, `.postorder()`, `.levelorder()`. Sequence extensions add `first(of:)`, `all(of:)`, `contains(_:)` by `Node.Kind`. -- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children). The builder's `demangle(_:)` currently bridges through a transient `Node` tree (`internsSubtrees: false`); Phase 3 will parse directly into the arena. -- **`DemanglingNode` / `DemanglingPrinter`** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`) — Phase 2 read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so the generic printer body is representation-agnostic). The printer engine is `DemanglingPrinter`; the public `NodePrinter` is a thin facade over `DemanglingPrinter`, and `NodeReference.print(using:)` prints straight from the store with **zero materialization** (verified byte-identical to the `Node` path across the 49k-symbol corpus × default/simplified/synthesizeSugar). When abstracting more consumers, name protocol members to match `Node`'s and note that `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil). +- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. The builder's `demangle(_:)` currently bridges through a transient `Node` tree (`internsSubtrees: false`); Phase 3 will parse directly into the arena. +- **`DemanglingNode` / `DemanglingPrinter`** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`) — Phase 2 read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so the generic printer body is representation-agnostic). The printer engine is `DemanglingPrinter`; the public `NodePrinter` is a thin facade over `DemanglingPrinter`, and `NodeReference.print(using:)` prints straight from the store with **zero materialization** (verified byte-identical to the `Node` path across the 49k-symbol corpus × default/simplified/synthesizeSugar). When abstracting more consumers, name protocol members to match `Node`'s and note that `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil). The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `isIdentifier(desired:)`, `isSwiftModule`) live **only** on the `DemanglingNode` extension — do not re-add copies on `Node`: the generic engine statically dispatches to the extension, so a parallel concrete copy would silently drift. ### Node Identity vs Equality diff --git a/Sources/Demangling/Node/Node+Conversions.swift b/Sources/Demangling/Node/Node+Conversions.swift index ec2075f..09db2dd 100644 --- a/Sources/Demangling/Node/Node+Conversions.swift +++ b/Sources/Demangling/Node/Node+Conversions.swift @@ -113,83 +113,11 @@ extension Node { } } - public var isSimpleType: Bool { - switch kind { - case .associatedType: fallthrough - case .associatedTypeRef: fallthrough - case .boundGenericClass: fallthrough - case .boundGenericEnum: fallthrough - case .boundGenericFunction: fallthrough - case .boundGenericOtherNominalType: fallthrough - case .boundGenericProtocol: fallthrough - case .boundGenericStructure: fallthrough - case .boundGenericTypeAlias: fallthrough - case .builtinBorrow: fallthrough - case .builtinTypeName: fallthrough - case .builtinTupleType: fallthrough - case .builtinFixedArray: fallthrough - case .class: fallthrough - case .dependentGenericType: fallthrough - case .dependentMemberType: fallthrough - case .dependentGenericParamType: fallthrough - case .dynamicSelf: fallthrough - case .enum: fallthrough - case .errorType: fallthrough - case .existentialMetatype: fallthrough - case .integer: fallthrough - case .labelList: fallthrough - case .metatype: fallthrough - case .metatypeRepresentation: fallthrough - case .module: fallthrough - case .negativeInteger: fallthrough - case .otherNominalType: fallthrough - case .pack: fallthrough - case .protocol: fallthrough - case .protocolSymbolicReference: fallthrough - case .returnType: fallthrough - case .silBoxType: fallthrough - case .silBoxTypeWithLayout: fallthrough - case .structure: fallthrough - case .sugaredArray: fallthrough - case .sugaredDictionary: fallthrough - case .sugaredOptional: fallthrough - case .sugaredInlineArray: fallthrough - case .sugaredParen: return true - case .tuple: fallthrough - case .tupleElementName: fallthrough - case .typeAlias: fallthrough - case .typeList: fallthrough - case .typeSymbolicReference: return true - case .type: - return children.first.map { $0.isSimpleType } ?? false - case .protocolList: - return children.first.map { $0.children.count <= 1 } ?? false - case .protocolListWithAnyObject: - return (children.first?.children.first).map { $0.children.count == 0 } ?? false - default: return false - } - } - - public var needSpaceBeforeType: Bool { - switch kind { - case .type: return children.first?.needSpaceBeforeType ?? false - case .functionType, - .noEscapeFunctionType, - .uncurriedFunctionType, - .dependentGenericType: return false - default: return true - } - } - - @inlinable - public func isIdentifier(desired: String) -> Bool { - return kind == .identifier && text == desired - } - - @inlinable - public var isSwiftModule: Bool { - return kind == .module && text == stdlibName - } + // `isSimpleType`, `needSpaceBeforeType`, `isIdentifier(desired:)`, and + // `isSwiftModule` live in the `DemanglingNode` protocol extension + // (DemanglingNode.swift) as the single implementation for both `Node` + // and `NodeReference` — a parallel copy here would drift, since the + // generic printer engine statically dispatches to the extension. } extension Node { diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift index bb3a21b..50d1626 100644 --- a/Sources/Demangling/Store/DemanglingNode.swift +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -25,9 +25,11 @@ public protocol DemanglingNode: Sendable { // MARK: - Derived helpers shared by the printer -/// Generic ports of the `Node` convenience properties the printer engine uses. -/// On `Node` its own concrete members take precedence (identical behavior); the -/// generic engine resolves these when specialized on `NodeReference`. +/// The convenience properties the printer engine derives from the protocol +/// primitives. These are the single implementation for both `Node` and +/// `NodeReference`: they are extension members (not requirements), so the +/// generic engine statically dispatches here for every conformer — keeping a +/// parallel copy on a concrete type would silently drift. extension DemanglingNode { /// Prints this subtree with the given options. Mirrors `Node.print(using:)`. public func print(using options: DemangleOptions = .default) -> String { @@ -37,10 +39,12 @@ extension DemanglingNode { } } + @inlinable public func isIdentifier(desired: String) -> Bool { kind == .identifier && text == desired } + @inlinable public var isSwiftModule: Bool { kind == .module && text == stdlibName } diff --git a/evolution/0001-symbol-store-arena.md b/evolution/0001-symbol-store-arena.md index cdd9306..af21239 100644 --- a/evolution/0001-symbol-store-arena.md +++ b/evolution/0001-symbol-store-arena.md @@ -141,3 +141,4 @@ store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量 | 2026-07-23 | Phase 1 实测达标 | 49k 语料:唯一节点 201,876(与 `NodeCache` 全树 hash-consing 计数逐一吻合,交叉验证正确性);平铺存储 3.0 MB(nodes 2.4 + edges 0.43 + text 0.26),优于 ≤6 MB 目标;打印抽样 2000 条零差异;构建 0.87s,不劣于 class 路径。**新发现**:构建期高水位 ~16 MB,由 intern 表(`[CompactNode: UInt32]` 等,~10 MB 量级)与桥接路径的临时 class 节点构成,且已冻结后 dirty pages 不随 `malloc_zone_pressure_relief` 回落;两轮连建仅 +6 MB,确认内存复用、无累积。结论:①「49k ≤6 MB」按保留存储口径已达成,进程口径需 Phase 3(直写 arena,消除临时树)+ intern 表瘦身(改为指向 nodes 缓冲的 open-addressing 索引表,去掉独立 key 存储);②桥接路径会向全局 `NodeCache` 写入叶节点(实测 11k 条),Phase 3 前的批量用户建议构建后 `NodeCache.shared.clear()` | | 2026-07-23 | Phase 2(打印)落地:零物化 store 打印 | 引入 `DemanglingNode` 只读协议(`kind`/`text`/`index`/`hasIndex`/`children` + `printCacheIdentity` 抽象缓存身份;`isSimpleType`/`needSpaceBeforeType`/`isIdentifier`/`isSwiftModule`/`print` 作为协议扩展从原语派生),`Node` 与 `NodeReference` 双双 conform。把 2179 行的 printer 引擎泛型化为 `DemanglingPrinter`(发现 printer 是纯只读消费者,全程不构造节点,泛型化干净),保留公共 `NodePrinter` 薄包装转发到 `DemanglingPrinter`——**公共 API 零破坏**。`NodeReference.print` 直接走 `DemanglingPrinter<_, NodeReference>`,不再 `materialize()`。**关键正确性点**:`NodeReference.text` 必须镜像 `Node.text` 对 `.dependentGenericParamType` 的泛型名合成(printer line 199 依赖之);`NodePrintContext.node` 是具体 `Node?`,store 路径以 `name as? Node`(NodeReference → nil)优雅降级,`String` target 无视 context 故无影响。**验证**:49k 语料 × 3 套选项(default/simplified/synthesizeSugar)store 打印与 Node 打印逐字节零差异;全量 dyld cache 对齐测试 + TypeDecoder 测试全绿 | | 2026-07-23 | 事故与恢复 | 外部工具删除了 `.claude/worktrees/` 目录,Phase 2 的未提交改动随磁盘丢失(三个 commit 因在 git 对象库中而安全)。因 printer 改造为确定性脚本化流程(perl + 精确 Edit),已从会话记录逐字复现全部 Phase 2 变更,重建 worktree 后重跑构建/测试验证一致 | +| 2026-07-23 | Phase 2 跟进:materialize 保共享 + 派生属性单一来源 | ① `materializeNode` 增加按索引 memo:store 是 hash-consed 的 DAG,同一子树索引只物化一次并复用实例——此前朴素递归会把重度替换共享的符号指数展开成树(printCache 注释中 SwiftUI `View.Body` 量级即几十万节点),且展开树上按 `ObjectIdentifier` 键的打印缓存全部脱靶;新增测试 `materializePreservesSubtreeSharing` 断言共享位置 `===`。② 删除 `Node` 上与 `DemanglingNode` 扩展重复的 `isSimpleType`/`needSpaceBeforeType`/`isIdentifier(desired:)`/`isSwiftModule`:这些是协议扩展成员(非 requirement),泛型引擎内静态派发恒走扩展版本,两份拷贝存在静默漂移风险;收敛为单一实现后对外仍是 public API(具体 `Node` 调用解析到协议扩展),行为与逐字节输出不变,全量测试绿 | From eb2795bfa2049a9b7f16287483d68377f164afe3 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 22:24:13 +0800 Subject: [PATCH 06/23] chore(deps): update package pins --- Package.resolved | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Package.resolved b/Package.resolved index cb45934..367c9cd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,17 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Mx-Iris/FrameworkToolbox", "state" : { - "revision" : "b82281eb8a6ffcb312941c3d06584182837f4ca9", - "version" : "0.7.1" - } - }, - { - "identity" : "machokit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MxIris-Reverse-Engineering/MachOKit", - "state" : { - "revision" : "9de6599aaba425632bb38a35cf9acd12a022d8f2", - "version" : "0.51.100" + "revision" : "61178c630fccf746bc2c13835dade9a7dfb79613", + "version" : "0.7.4" } }, { @@ -73,13 +64,22 @@ "version" : "0.2.2" } }, + { + "identity" : "swift-macro-toolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-macro-toolkit", + "state" : { + "revision" : "d6ed555bb83c9f21a292e9769952e8f52610a6e2", + "version" : "0.9.0" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", - "version" : "601.0.1" + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" } } ], From d9e7c66c1c20278250ac26b324558577be98200b Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 23:00:52 +0800 Subject: [PATCH 07/23] feat(Demangling): generic traversal and kind lookup over DemanglingNode Genericize the tree-traversal machinery (preorder/inorder/postorder/ levelorder), the kind-lookup helpers (first(of:)/all(of:)/contains/ filter(of:)), and the identifier extraction over DemanglingNode, and conform NodeReference to Sequence with the same preorder default as Node. Store-backed consumers can now walk and classify subtrees without materializing, which is what SymbolIndexStore-style bulk indexing needs. Single-implementation rule as before: Node's Sequence conformance now rides the generic machinery, the old Element == Node sequence extension is retargeted to Element: DemanglingNode (source-compatible for concrete-Node callers), and Node.identifier moves to the shared extension. Parity test walks three symbols on both representations and asserts identical preorder/postorder kind sequences, lookups, and identifiers. --- .../Demangling/Node/Node+Conversions.swift | 17 +- Sources/Demangling/Node/Node+Sequence.swift | 211 +-------------- .../Store/DemanglingNode+Sequence.swift | 247 ++++++++++++++++++ Tests/DemanglingTests/SymbolStoreTests.swift | 34 +++ 4 files changed, 287 insertions(+), 222 deletions(-) create mode 100644 Sources/Demangling/Store/DemanglingNode+Sequence.swift diff --git a/Sources/Demangling/Node/Node+Conversions.swift b/Sources/Demangling/Node/Node+Conversions.swift index 09db2dd..82d7efb 100644 --- a/Sources/Demangling/Node/Node+Conversions.swift +++ b/Sources/Demangling/Node/Node+Conversions.swift @@ -175,19 +175,6 @@ extension Node { return depths } - public var identifier: String? { - if let node = children.at(1), node.kind == .identifier { - return node.text - } else if let node = children.at(1), node.kind == .privateDeclName { - return node.children.at(1)?.text - } else if let node = first(of: .prefixOperator, .postfixOperator, .infixOperator) { - return node.text - } else if let node = first(of: .identifier) { - return node.text - } else if let node = first(of: .privateDeclName) { - return node.children.at(1)?.text - } else { - return nil - } - } + // `identifier` lives in DemanglingNode+Sequence.swift as the single + // implementation shared with `NodeReference`. } diff --git a/Sources/Demangling/Node/Node+Sequence.swift b/Sources/Demangling/Node/Node+Sequence.swift index d674f6e..fdaea90 100644 --- a/Sources/Demangling/Node/Node+Sequence.swift +++ b/Sources/Demangling/Node/Node+Sequence.swift @@ -1,163 +1,7 @@ -extension Node { - public func preorder() -> some Sequence { - PreorderSequence(root: self) - } - - public func inorder() -> some Sequence { - InorderSequence(root: self) - } - - public func postorder() -> some Sequence { - PostorderSequence(root: self) - } - - public func levelorder() -> some Sequence { - LevelorderSequence(root: self) - } - - private struct PreorderSequence: Sequence { - struct Iterator: IteratorProtocol { - private var stack: [Node] - - fileprivate init(root: Node) { - self.stack = [root] - } - - mutating func next() -> Node? { - guard !stack.isEmpty else { return nil } - - let current = stack.removeLast() - - // Add children in reverse order so we visit them left-to-right - for child in current.children.reversed() { - stack.append(child) - } - - return current - } - } - - private let root: Node - - fileprivate init(root: Node) { - self.root = root - } - - func makeIterator() -> Iterator { - Iterator(root: root) - } - } - - private struct InorderSequence: Sequence { - struct Iterator: IteratorProtocol { - private var stack: [Node] - private var current: Node? - - fileprivate init(root: Node) { - self.stack = [] - self.current = root - } - - mutating func next() -> Node? { - while current != nil || !stack.isEmpty { - // Go to the leftmost node - while let node = current { - stack.append(node) - current = node.children.first - } - - // Current must be nil at this point - if let node = stack.popLast() { - current = node.children.count > 1 ? node.children[1] : nil - return node - } - } - return nil - } - } - - private let root: Node - - fileprivate init(root: Node) { - self.root = root - } - - func makeIterator() -> Iterator { - Iterator(root: root) - } - } - - private struct PostorderSequence: Sequence { - struct Iterator: IteratorProtocol { - private var stack: [(node: Node, visited: Bool)] - - fileprivate init(root: Node) { - self.stack = [(root, false)] - } - - mutating func next() -> Node? { - while !stack.isEmpty { - let (node, visited) = stack.removeLast() - - if visited { - return node - } else { - // Mark as visited and push back - stack.append((node, true)) - - // Push children in reverse order - for child in node.children.reversed() { - stack.append((child, false)) - } - } - } - return nil - } - } - - private let root: Node - - fileprivate init(root: Node) { - self.root = root - } - - func makeIterator() -> Iterator { - Iterator(root: root) - } - } - - private struct LevelorderSequence: Sequence { - struct Iterator: IteratorProtocol { - private var queue: [Node] - - fileprivate init(root: Node) { - self.queue = [root] - } - - mutating func next() -> Node? { - guard !queue.isEmpty else { return nil } - - let current = queue.removeFirst() - - // Add all children to the queue - queue.append(contentsOf: current.children) - - return current - } - } - - private let root: Node - - fileprivate init(root: Node) { - self.root = root - } - - func makeIterator() -> Iterator { - Iterator(root: root) - } - } -} - +// The traversal machinery (preorder/inorder/postorder/levelorder) and the +// kind-lookup helpers (first(of:), all(of:), contains(_:), filter(of:)) live +// in DemanglingNode+Sequence.swift as single generic implementations shared +// with NodeReference — do not re-add Node-specific copies here. extension Node: Sequence { public typealias Element = Node @@ -165,50 +9,3 @@ extension Node: Sequence { preorder().makeIterator() } } - -extension Sequence where Element == Node { - @inlinable - public func first(of kind: Node.Kind) -> Node? { - first { $0.kind == kind } - } - - @inlinable - public func first(of kinds: Node.Kind...) -> Node? { - first { kinds.contains($0.kind) } - } - - @inlinable - public func contains(_ kind: Node.Kind) -> Bool { - contains { $0.kind == kind } - } - - @inlinable - public func contains(_ kinds: Node.Kind...) -> Bool { - contains { kinds.contains($0.kind) } - } - - @inlinable - public func all(of kind: Node.Kind) -> [Node] { - filter { $0.kind == kind } - } - - @inlinable - public func all(of kinds: Node.Kind...) -> [Node] { - filter { kinds.contains($0.kind) } - } - - @inlinable - public func all(of kinds: [Node.Kind]) -> [Node] { - filter { kinds.contains($0.kind) } - } - - @inlinable - public func filter(of kind: Node.Kind) -> some Sequence { - filter { $0.kind == kind } - } - - @inlinable - public func filter(of kinds: Node.Kind...) -> some Sequence { - filter { kinds.contains($0.kind) } - } -} diff --git a/Sources/Demangling/Store/DemanglingNode+Sequence.swift b/Sources/Demangling/Store/DemanglingNode+Sequence.swift new file mode 100644 index 0000000..c268f4a --- /dev/null +++ b/Sources/Demangling/Store/DemanglingNode+Sequence.swift @@ -0,0 +1,247 @@ +/// Tree traversal shared by every `DemanglingNode` representation. +/// +/// These are the single implementations behind `Node`'s and +/// `NodeReference`'s `Sequence` conformances and the kind-lookup helpers +/// (`first(of:)`, `all(of:)`, `contains(_:)`) — one copy, both +/// representations, mirroring the derived-helper rule in +/// `DemanglingNode.swift`. +extension DemanglingNode { + public func preorder() -> some Sequence { + PreorderSequence(root: self) + } + + public func inorder() -> some Sequence { + InorderSequence(root: self) + } + + public func postorder() -> some Sequence { + PostorderSequence(root: self) + } + + public func levelorder() -> some Sequence { + LevelorderSequence(root: self) + } +} + +private struct PreorderSequence: Sequence { + struct Iterator: IteratorProtocol { + private var stack: [SomeNode] + + fileprivate init(root: SomeNode) { + self.stack = [root] + } + + mutating func next() -> SomeNode? { + guard !stack.isEmpty else { return nil } + + let current = stack.removeLast() + + // Add children in reverse order so we visit them left-to-right + for child in current.children.reversed() { + stack.append(child) + } + + return current + } + } + + private let root: SomeNode + + fileprivate init(root: SomeNode) { + self.root = root + } + + func makeIterator() -> Iterator { + Iterator(root: root) + } +} + +private struct InorderSequence: Sequence { + struct Iterator: IteratorProtocol { + private var stack: [SomeNode] + private var current: SomeNode? + + fileprivate init(root: SomeNode) { + self.stack = [] + self.current = root + } + + mutating func next() -> SomeNode? { + while current != nil || !stack.isEmpty { + // Go to the leftmost node + while let node = current { + stack.append(node) + current = node.children.first + } + + // Current must be nil at this point + if let node = stack.popLast() { + current = node.children.count > 1 ? node.children[1] : nil + return node + } + } + return nil + } + } + + private let root: SomeNode + + fileprivate init(root: SomeNode) { + self.root = root + } + + func makeIterator() -> Iterator { + Iterator(root: root) + } +} + +private struct PostorderSequence: Sequence { + struct Iterator: IteratorProtocol { + private var stack: [(node: SomeNode, visited: Bool)] + + fileprivate init(root: SomeNode) { + self.stack = [(root, false)] + } + + mutating func next() -> SomeNode? { + while !stack.isEmpty { + let (node, visited) = stack.removeLast() + + if visited { + return node + } else { + // Mark as visited and push back + stack.append((node, true)) + + // Push children in reverse order + for child in node.children.reversed() { + stack.append((child, false)) + } + } + } + return nil + } + } + + private let root: SomeNode + + fileprivate init(root: SomeNode) { + self.root = root + } + + func makeIterator() -> Iterator { + Iterator(root: root) + } +} + +private struct LevelorderSequence: Sequence { + struct Iterator: IteratorProtocol { + private var queue: [SomeNode] + + fileprivate init(root: SomeNode) { + self.queue = [root] + } + + mutating func next() -> SomeNode? { + guard !queue.isEmpty else { return nil } + + let current = queue.removeFirst() + + // Add all children to the queue + queue.append(contentsOf: current.children) + + return current + } + } + + private let root: SomeNode + + fileprivate init(root: SomeNode) { + self.root = root + } + + func makeIterator() -> Iterator { + Iterator(root: root) + } +} + +// MARK: - Kind lookup over any node sequence + +extension Sequence where Element: DemanglingNode { + @inlinable + public func first(of kind: Node.Kind) -> Element? { + first { $0.kind == kind } + } + + @inlinable + public func first(of kinds: Node.Kind...) -> Element? { + first { kinds.contains($0.kind) } + } + + @inlinable + public func contains(_ kind: Node.Kind) -> Bool { + contains { $0.kind == kind } + } + + @inlinable + public func contains(_ kinds: Node.Kind...) -> Bool { + contains { kinds.contains($0.kind) } + } + + @inlinable + public func all(of kind: Node.Kind) -> [Element] { + filter { $0.kind == kind } + } + + @inlinable + public func all(of kinds: Node.Kind...) -> [Element] { + filter { kinds.contains($0.kind) } + } + + @inlinable + public func all(of kinds: [Node.Kind]) -> [Element] { + filter { kinds.contains($0.kind) } + } + + @inlinable + public func filter(of kind: Node.Kind) -> some Sequence { + filter { $0.kind == kind } + } + + @inlinable + public func filter(of kinds: Node.Kind...) -> some Sequence { + filter { kinds.contains($0.kind) } + } +} + +// MARK: - Identifier extraction + +extension DemanglingNode where Self: Sequence, Self.Element == Self { + /// The declaration identifier carried by this subtree, if any. + /// Mirrors the historical `Node.identifier` lookup order. + public var identifier: String? { + if let node = children.at(1), node.kind == .identifier { + return node.text + } else if let node = children.at(1), node.kind == .privateDeclName { + return node.children.at(1)?.text + } else if let node = first(of: .prefixOperator, .postfixOperator, .infixOperator) { + return node.text + } else if let node = first(of: .identifier) { + return node.text + } else if let node = first(of: .privateDeclName) { + return node.children.at(1)?.text + } else { + return nil + } + } +} + +// MARK: - NodeReference as Sequence (preorder, mirroring Node) + +extension NodeReference: Sequence { + public typealias Element = NodeReference + + public func makeIterator() -> some IteratorProtocol { + preorder().makeIterator() + } +} diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index 48b6c8a..df6a147 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -193,6 +193,40 @@ struct SymbolStoreTests { } } + @Test func traversalParityWithNodePath() throws { + // NodeReference's Sequence conformance and kind-lookup helpers must + // walk the store in the same order the Node path walks the class tree. + let mangledSymbols = [ + "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s4main1gyxxlF", + "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", + ] + var builder = SymbolStoreBuilder() + var rootIndices = [SymbolStore.NodeIndex]() + for mangled in mangledSymbols { + rootIndices.append(try builder.demangle(mangled)) + } + let store = builder.freeze() + + for (rootIndex, mangled) in zip(rootIndices, mangledSymbols) { + let nodePathTree = try demangleAsNode(mangled, internsSubtrees: false) + let reference = store.reference(at: rootIndex) + + let referenceKinds = reference.map(\.kind) + let nodeKinds = nodePathTree.map(\.kind) + #expect(referenceKinds == nodeKinds, "Preorder kind sequences should match for \(mangled)") + + let referencePostorderKinds = Array(reference.postorder().map(\.kind)) + let nodePostorderKinds = Array(nodePathTree.postorder().map(\.kind)) + #expect(referencePostorderKinds == nodePostorderKinds, "Postorder kind sequences should match for \(mangled)") + + #expect(reference.first(of: .identifier)?.text == nodePathTree.first(of: .identifier)?.text) + #expect(reference.all(of: .type).count == nodePathTree.all(of: .type).count) + #expect(reference.contains(.functionType) == nodePathTree.contains(.functionType)) + #expect(reference.identifier == nodePathTree.identifier, "identifier should match for \(mangled)") + } + } + @Test func sharedSubtreesAcrossSymbolsShareIndices() throws { var builder = SymbolStoreBuilder() let firstRootIndex = try builder.demangle("$sSiD") From 5e230e86efe468a68873e6be88d3a26aca281cbc Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 23:16:32 +0800 Subject: [PATCH 08/23] feat(Demangling): generic TypeDecoder engine over DemanglingNode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Genericize the type-decoding walk as TypeDecoderEngine with the public TypeDecoder as a source-compatible facade, plus a new NodeReference entry that decodes straight from a SymbolStore. The public TypeBuilder protocol is untouched: the five handoff points that give subtrees to the builder (createTypeDecl, createProtocolDecl, createSymbolicExtendedExistentialType, resolveOpaqueType, and the decodeMangledType callback) pass materializedNode — a new DemanglingNode requirement that is 'self' for Node (zero cost) and a sharing-preserving materialization for NodeReference. The builtinTypeName case bridges mangleAsString the same way until the Remangler is genericized. Also move hasChildren and subscript(throwChild:) into the DemanglingNode extension as single implementations (Node copies removed), and genericize TypeLookupError.init(node:). Parity test decodes six manglings through both representations with identical results. --- .../Main/TypeDecoder/TypeDecoder.swift | 130 ++++++++++++------ .../Main/TypeDecoder/TypeLookupError.swift | 4 +- .../Demangling/Node/Node+Conversions.swift | 21 +-- Sources/Demangling/Store/DemanglingNode.swift | 28 ++++ Tests/DemanglingTests/TypeDecoderTests.swift | 22 +++ 5 files changed, 144 insertions(+), 61 deletions(-) diff --git a/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift b/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift index 962a0fb..dc329f3 100644 --- a/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift +++ b/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift @@ -1,36 +1,40 @@ -/// Decode a mangled type to construct an abstract type using a custom builder. -/// This is a Swifty implementation that uses throws for error handling. -public final class TypeDecoder { - public typealias BuiltType = Builder.BuiltType - public typealias BuiltTypeDecl = Builder.BuiltTypeDecl - public typealias BuiltProtocolDecl = Builder.BuiltProtocolDecl - public typealias Field = Builder.BuiltSILBoxField - public typealias BuiltSubstitution = Builder.BuiltSubstitution - public typealias BuiltRequirement = Builder.BuiltRequirement - public typealias BuiltInverseRequirement = Builder.BuiltInverseRequirement - public typealias BuiltLayoutConstraint = Builder.BuiltLayoutConstraint - public typealias BuiltGenericSignature = Builder.BuiltGenericSignature - public typealias BuiltSubstitutionMap = Builder.BuiltSubstitutionMap +/// Generic type-decoding engine shared by the public `TypeDecoder` +/// (specialized on `Node`) and store-backed decoding (specialized on +/// `NodeReference`). The tree walk is representation-agnostic; the +/// `TypeBuilder` handoff points pass `materializedNode` so the public +/// `TypeBuilder` protocol keeps receiving concrete `Node` subtrees. +/// See evolution proposal 0001, Phase 2. +struct TypeDecoderEngine { + typealias BuiltType = Builder.BuiltType + typealias BuiltTypeDecl = Builder.BuiltTypeDecl + typealias BuiltProtocolDecl = Builder.BuiltProtocolDecl + typealias Field = Builder.BuiltSILBoxField + typealias BuiltSubstitution = Builder.BuiltSubstitution + typealias BuiltRequirement = Builder.BuiltRequirement + typealias BuiltInverseRequirement = Builder.BuiltInverseRequirement + typealias BuiltLayoutConstraint = Builder.BuiltLayoutConstraint + typealias BuiltGenericSignature = Builder.BuiltGenericSignature + typealias BuiltSubstitutionMap = Builder.BuiltSubstitutionMap private let builder: Builder private static var maxDepth: Int { 1024 } - public init(builder: Builder) { + init(builder: Builder) { self.builder = builder } /// Given a demangle tree, attempt to turn it into a type. - public func decodeMangledType(node: Node, forRequirement: Bool = true) throws(TypeLookupError) -> BuiltType { + func decodeMangledType(node: SomeNode, forRequirement: Bool = true) throws(TypeLookupError) -> BuiltType { try decodeMangledType(node: node, depth: 0, forRequirement: forRequirement) } } // MARK: - Main Type Decoding -extension TypeDecoder { +extension TypeDecoderEngine { private func decodeMangledType( - node: Node, + node: SomeNode, depth: Int, forRequirement: Bool = true ) throws(TypeLookupError) -> BuiltType { @@ -174,7 +178,7 @@ extension TypeDecoder { case .builtinTypeName: let mangling: String do { - mangling = try mangleAsString(node, flavor: builder.getManglingFlavor()) + mangling = try mangleAsString(node.materializedNode, flavor: builder.getManglingFlavor()) } catch { throw TypeLookupError(node: node, message: "failed to mangle node") } @@ -219,7 +223,7 @@ extension TypeDecoder { let shapeNode = node.children[0] let args = try decodeGenericArgs(node: node.children[1], depth: depth + 1) - return builder.createSymbolicExtendedExistentialType(shapeNode: shapeNode, args: args) + return builder.createSymbolicExtendedExistentialType(shapeNode: shapeNode.materializedNode, args: args) case .protocolList, .protocolListWithAnyObject, @@ -942,7 +946,7 @@ extension TypeDecoder { genericArgs.append(genericArgsBuf[start ..< end]) } - return builder.resolveOpaqueType(descriptor: descriptor, genericArgs: genericArgs, ordinal: ordinal) + return builder.resolveOpaqueType(descriptor: descriptor.materializedNode, genericArgs: genericArgs, ordinal: ordinal) case .integer: guard let index = node.index else { @@ -970,9 +974,9 @@ extension TypeDecoder { } } -extension TypeDecoder { +extension TypeDecoderEngine { private func decodeTypeSequenceElement( - node: Node, + node: SomeNode, depth: Int, resultCallback: (BuiltType) throws(TypeLookupError) -> Void ) throws(TypeLookupError) { @@ -1004,7 +1008,7 @@ extension TypeDecoder { } private func decodeImplFunctionParam( - node: Node, + node: SomeNode, depth: Int, results: inout [T] ) throws(TypeLookupError) where T.BuiltTypeParam == BuiltType { @@ -1055,7 +1059,7 @@ extension TypeDecoder { } private func decodeImplFunctionResult( - node: Node, + node: SomeNode, depth: Int, results: inout [T] ) throws(TypeLookupError) where T.BuiltTypeParam == BuiltType { @@ -1104,7 +1108,7 @@ extension TypeDecoder { results.append(result) } - private func decodeGenericArgs(node: Node, depth: Int) throws(TypeLookupError) -> [BuiltType] { + private func decodeGenericArgs(node: SomeNode, depth: Int) throws(TypeLookupError) -> [BuiltType] { guard node.kind == .typeList else { throw TypeLookupError(node: node, message: "is not TypeList") } @@ -1118,7 +1122,7 @@ extension TypeDecoder { } private func decodeMangledTypeDecl( - node: Node, + node: SomeNode, depth: Int, typeDecl: inout BuiltTypeDecl?, parent: inout BuiltType?, @@ -1138,10 +1142,12 @@ extension TypeDecoder { ) } + // The decl handoff to `TypeBuilder` requires the class representation, + // so `declNode` is materialized here (free on the `Node` path). var declNode: Node if node.kind == .typeSymbolicReference { // A symbolic reference can be directly resolved to a nominal type - declNode = node + declNode = node.materializedNode } else { guard node.children.count >= 2 else { throw TypeLookupError(node: node, message: "Number of node children (\(node.children.count)) less than required (2)") @@ -1154,7 +1160,7 @@ extension TypeDecoder { // in addition to a reference to the parent type. The // mangled name already includes the module and parent // types, if any. - declNode = node + declNode = node.materializedNode switch parentContext.kind { case .module: @@ -1176,7 +1182,7 @@ extension TypeDecoder { // Remove any generic arguments from the context node, producing a // node that references the nominal type declaration. - if let unspecNode = getUnspecialized(node) { + if let unspecNode = getUnspecialized(declNode) { declNode = unspecNode } else { throw TypeLookupError("Failed to unspecialize type") @@ -1190,7 +1196,7 @@ extension TypeDecoder { } } - private func decodeMangledProtocolType(node: Node, depth: Int) -> BuiltProtocolDecl? { + private func decodeMangledProtocolType(node: SomeNode, depth: Int) -> BuiltProtocolDecl? { guard depth <= Self.maxDepth else { return nil } @@ -1216,11 +1222,11 @@ extension TypeDecoder { } #endif - return builder.createProtocolDecl(node: node) + return builder.createProtocolDecl(node: node.materializedNode) } private func decodeMangledFunctionInputType( - node: Node, + node: SomeNode, depth: Int, params: inout [FunctionParam], hasParamFlags: inout Bool @@ -1241,7 +1247,7 @@ extension TypeDecoder { return } - func decodeParamTypeAndFlags(node: Node, param: inout FunctionParam) throws(TypeLookupError) { + func decodeParamTypeAndFlags(node: SomeNode, param: inout FunctionParam) throws(TypeLookupError) { var node = node var recurse = true while recurse { @@ -1290,7 +1296,7 @@ extension TypeDecoder { } } - func decodeParam(node: Node) throws(TypeLookupError) { + func decodeParam(node: SomeNode) throws(TypeLookupError) { guard node.kind == .tupleElement else { return } @@ -1330,7 +1336,7 @@ extension TypeDecoder { } } -extension TypeDecoder { +extension TypeDecoderEngine { private func functionConvention(for kind: Node.Kind) -> FunctionMetadataConvention { switch kind { case .objCBlock, @@ -1357,8 +1363,8 @@ extension TypeDecoder { } } -private func decodeRequirements( - node: Node, +private func decodeRequirements( + node: SomeNode, requirements: inout [BuilderType.BuiltRequirement], inverseRequirements: inout [BuilderType.BuiltInverseRequirement], builder: BuilderType @@ -1379,16 +1385,16 @@ private func decodeRequirements( } // Decode subject type - let subjectType = try builder.decodeMangledType(node: child.children[0], forRequirement: true) + let subjectType = try builder.decodeMangledType(node: child.children[0].materializedNode, forRequirement: true) switch child.kind { case .dependentGenericConformanceRequirement: - let constraintType = try builder.decodeMangledType(node: child.children[1], forRequirement: true) + let constraintType = try builder.decodeMangledType(node: child.children[1].materializedNode, forRequirement: true) let kind: RequirementKind = builder.isExistential(type: constraintType) ? .conformance : .superclass requirements.append(builder.createRequirement(kind: kind, subjectType: subjectType, constraintType: constraintType)) case .dependentGenericSameTypeRequirement: - let constraintType = try builder.decodeMangledType(node: child.children[1], forRequirement: false) + let constraintType = try builder.decodeMangledType(node: child.children[1].materializedNode, forRequirement: false) requirements.append(builder.createRequirement(kind: .sameType, subjectType: subjectType, constraintType: constraintType)) case .dependentGenericInverseConformanceRequirement: @@ -1443,7 +1449,7 @@ private func decodeRequirements( } #if canImport(ObjectiveC) -private func getObjCClassOrProtocolName(_ node: Node) -> String? { +private func getObjCClassOrProtocolName(_ node: some DemanglingNode) -> String? { guard node.kind == .class || node.kind == .protocol else { return nil } @@ -1467,3 +1473,45 @@ private func getObjCClassOrProtocolName(_ node: Node) -> String? { return nameNode.text } #endif + +// MARK: - Public facade + +/// Decode a mangled type to construct an abstract type using a custom builder. +/// This is a Swifty implementation that uses throws for error handling. +/// +/// A thin, source-compatible facade over `TypeDecoderEngine`. The decoding +/// logic lives in the generic engine so it can also decode `NodeReference` +/// trees straight from a `SymbolStore` without materializing the class tree +/// (`TypeBuilder` implementations keep receiving concrete `Node` subtrees at +/// the handoff points). +public final class TypeDecoder { + public typealias BuiltType = Builder.BuiltType + public typealias BuiltTypeDecl = Builder.BuiltTypeDecl + public typealias BuiltProtocolDecl = Builder.BuiltProtocolDecl + public typealias Field = Builder.BuiltSILBoxField + public typealias BuiltSubstitution = Builder.BuiltSubstitution + public typealias BuiltRequirement = Builder.BuiltRequirement + public typealias BuiltInverseRequirement = Builder.BuiltInverseRequirement + public typealias BuiltLayoutConstraint = Builder.BuiltLayoutConstraint + public typealias BuiltGenericSignature = Builder.BuiltGenericSignature + public typealias BuiltSubstitutionMap = Builder.BuiltSubstitutionMap + + private let builder: Builder + + public init(builder: Builder) { + self.builder = builder + } + + /// Given a demangle tree, attempt to turn it into a type. + public func decodeMangledType(node: Node, forRequirement: Bool = true) throws(TypeLookupError) -> BuiltType { + try TypeDecoderEngine(builder: builder) + .decodeMangledType(node: node, forRequirement: forRequirement) + } + + /// Store-backed variant: decodes straight from a `SymbolStore` without + /// materializing a `Node` tree (proposal 0001, Phase 2). + public func decodeMangledType(node: NodeReference, forRequirement: Bool = true) throws(TypeLookupError) -> BuiltType { + try TypeDecoderEngine(builder: builder) + .decodeMangledType(node: node, forRequirement: forRequirement) + } +} diff --git a/Sources/Demangling/Main/TypeDecoder/TypeLookupError.swift b/Sources/Demangling/Main/TypeDecoder/TypeLookupError.swift index 71b2582..ff93ee9 100644 --- a/Sources/Demangling/Main/TypeDecoder/TypeLookupError.swift +++ b/Sources/Demangling/Main/TypeDecoder/TypeLookupError.swift @@ -14,9 +14,9 @@ public struct TypeLookupError: Error, CustomStringConvertible, Sendable { } /// Initialize from a node error - public init(node: Node, message: String, file: String = #file, line: Int = #line) { + public init(node: some DemanglingNode, message: String, file: String = #file, line: Int = #line) { let nodeInfo = "Node kind \(node.kind)" - let textInfo = node.hasText ? " \"\(node.text ?? "")\"" : "" + let textInfo = node.text.map { " \"\($0)\"" } ?? "" self.message = "\(nodeInfo)\(textInfo) - \(message)" self.file = file self.line = line diff --git a/Sources/Demangling/Node/Node+Conversions.swift b/Sources/Demangling/Node/Node+Conversions.swift index 82d7efb..037ebf5 100644 --- a/Sources/Demangling/Node/Node+Conversions.swift +++ b/Sources/Demangling/Node/Node+Conversions.swift @@ -73,13 +73,9 @@ extension Node { } } - @inlinable - public var hasChildren: Bool { - switch payload { - case .none, .index, .text: return false - default: return true - } - } + // `hasChildren` and `subscript(throwChild:)` live in the `DemanglingNode` + // extension (DemanglingNode.swift) as the single implementation for both + // `Node` and `NodeReference`. @inlinable public var firstChild: Node? { @@ -138,17 +134,6 @@ extension Node { children.at(childIndex) } - @inlinable - public subscript(throwChild childIndex: Int) -> Node { - get throws(IndexOutOfBoundError) { - if let child = children.at(childIndex) { - return child - } else { - throw .default - } - } - } - public struct IndexOutOfBoundError: Error { public static let `default` = IndexOutOfBoundError() } diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift index 50d1626..88bd425 100644 --- a/Sources/Demangling/Store/DemanglingNode.swift +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -21,6 +21,12 @@ public protocol DemanglingNode: Sendable { var hasIndex: Bool { get } var children: Children { get } var printCacheIdentity: PrintCacheIdentity { get } + + /// The concrete class-tree form of this subtree, for interop boundaries + /// that still require `Node` (`TypeBuilder` handoffs, remangling until the + /// `Remangler` is genericized). `Node` returns itself; `NodeReference` + /// materializes with subtree sharing preserved. + var materializedNode: Node { get } } // MARK: - Derived helpers shared by the printer @@ -39,6 +45,22 @@ extension DemanglingNode { } } + @inlinable + public var hasChildren: Bool { + !children.isEmpty + } + + @inlinable + public subscript(throwChild childIndex: Int) -> Self { + get throws(Node.IndexOutOfBoundError) { + if let child = children.at(childIndex) { + return child + } else { + throw .default + } + } + } + @inlinable public func isIdentifier(desired: String) -> Bool { kind == .identifier && text == desired @@ -153,6 +175,9 @@ extension DemanglingNodeChildren { extension Node: DemanglingNode { @inlinable public var printCacheIdentity: ObjectIdentifier { ObjectIdentifier(self) } + + @inlinable + public var materializedNode: Node { self } } extension Node.Children: DemanglingNodeChildren {} @@ -168,6 +193,9 @@ extension NodeReference: DemanglingNode { @inlinable public var printCacheIdentity: SymbolStore.NodeIndex { nodeIndex } + + @inlinable + public var materializedNode: Node { materialize() } } extension NodeReference.ChildrenView: DemanglingNodeChildren {} diff --git a/Tests/DemanglingTests/TypeDecoderTests.swift b/Tests/DemanglingTests/TypeDecoderTests.swift index a87016d..75e7099 100644 --- a/Tests/DemanglingTests/TypeDecoderTests.swift +++ b/Tests/DemanglingTests/TypeDecoderTests.swift @@ -208,6 +208,28 @@ struct TypeDecoderTests { func existentialMetatypes(mangled: String, expected: String) throws { #expect(try Self.decodeType(mangled) == expected) } + + // MARK: - Store-Backed Decoding Parity (proposal 0001, Phase 2) + + @Test(arguments: [ + "$sBbD", + "$sBf32_Bv4_D", + "$sSiD", + "$sSaySiGD", + "$sSDySSSiGD", + "$s17lowered_metatypes5ProtoPXmT", + ]) + func storeBackedDecodingMatchesNodePath(mangled: String) throws { + let nodePathResult = try Self.decodeType(mangled) + + var builder = SymbolStoreBuilder() + let rootIndex = try builder.demangle(mangled) + let store = builder.freeze() + + let decoder = TypeDecoder(builder: StringTypeBuilder()) + let storePathResult = try decoder.decodeMangledType(node: store.reference(at: rootIndex)) + #expect(storePathResult == nodePathResult, "Store-backed decoding should match the Node path for \(mangled)") + } } // MARK: - StringTypeBuilder From 3eb55bc9a23546c29bfbad8ef2abe74da3e88b15 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 23:28:25 +0800 Subject: [PATCH 09/23] feat(Demangling): store-backed remangling via materialization bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add mangleAsString/canMangle overloads accepting any DemanglingNode (in particular NodeReference), bridging through materializedNode. Deliberately NOT a generic remangler engine: the remangling walk constructs transient helper nodes (getUnspecialized stripping in mangleAnyNominalType/mangleBoundGenericFunction, SIL box layout wrappers in mangleSILBoxTypeWithLayout) that flow back through mangle() with shared substitution state — the same NodeFactory-backed design as the C++ Remangler. A construction-free generic engine would be a redesign of a byte-exactness-critical component for no resident-memory gain: remangling's output is a fresh String and its cost is per-call transient either way. The bridge materializes once with subtree sharing preserved. Parity test remangles four symbols through both paths with identical output. --- .../Main/Remangle/RemangleInterface.swift | 27 +++++++++++++++++++ Tests/DemanglingTests/SymbolStoreTests.swift | 24 +++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/Sources/Demangling/Main/Remangle/RemangleInterface.swift b/Sources/Demangling/Main/Remangle/RemangleInterface.swift index d06eb86..58efe44 100644 --- a/Sources/Demangling/Main/Remangle/RemangleInterface.swift +++ b/Sources/Demangling/Main/Remangle/RemangleInterface.swift @@ -32,6 +32,28 @@ public func mangleAsString(_ node: Node, usePunycode: Bool = true, flavor: Mangl return try await StackSafeExecutor.executeAsync(mangleBlock) } +// MARK: - Store-Backed Remangling + +/// Remangle any `DemanglingNode` representation — in particular a +/// `NodeReference` pointing into a `SymbolStore`. +/// +/// The remangling algorithm constructs transient helper nodes while walking +/// (unspecialized nominals, SIL box layout wrappers), exactly like the C++ +/// `Remangler` does with its `NodeFactory` — it is not a read-only consumer, +/// so it runs on the class representation. This entry bridges by +/// materializing the subtree once (subtree sharing preserved); the cost is +/// transient and proportional to the subtree, and remangling's output is a +/// fresh `String` either way, so the store's resident-memory goals are +/// unaffected. +public func mangleAsString(_ node: some DemanglingNode, usePunycode: Bool = true, flavor: ManglingFlavor = .default) throws(ManglingError) -> String { + try mangleAsString(node.materializedNode, usePunycode: usePunycode, flavor: flavor) +} + +/// Asynchronous variant of the `DemanglingNode` overload. +public func mangleAsString(_ node: some DemanglingNode, usePunycode: Bool = true, flavor: ManglingFlavor = .default) async throws(ManglingError) -> String { + try await mangleAsString(node.materializedNode, usePunycode: usePunycode, flavor: flavor) +} + // MARK: - Validation Helpers /// Check if a node tree can be successfully remangled @@ -41,3 +63,8 @@ public func mangleAsString(_ node: Node, usePunycode: Bool = true, flavor: Mangl public func canMangle(_ node: Node) -> Bool { return (try? mangleAsString(node)) != nil } + +/// Check if any `DemanglingNode` representation can be successfully remangled. +public func canMangle(_ node: some DemanglingNode) -> Bool { + return (try? mangleAsString(node)) != nil +} diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index df6a147..75793bf 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -227,6 +227,30 @@ struct SymbolStoreTests { } } + @Test func remangleParityWithNodePath() throws { + // Remangling a NodeReference (bridged through materialization) must + // produce the same mangled string as the Node path. + let mangledSymbols = [ + "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s4main1gyxxlF", + "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", + "$s4main3FooVAA1P0B0fMq_", + ] + var builder = SymbolStoreBuilder() + var rootIndices = [SymbolStore.NodeIndex]() + for mangled in mangledSymbols { + rootIndices.append(try builder.demangle(mangled)) + } + let store = builder.freeze() + + for (rootIndex, mangled) in zip(rootIndices, mangledSymbols) { + let nodePathTree = try demangleAsNode(mangled, internsSubtrees: false) + let nodePathMangled = try mangleAsString(nodePathTree) + let storePathMangled = try mangleAsString(store.reference(at: rootIndex)) + #expect(storePathMangled == nodePathMangled, "Store-backed remangling should match the Node path for \(mangled)") + } + } + @Test func sharedSubtreesAcrossSymbolsShareIndices() throws { var builder = SymbolStoreBuilder() let firstRootIndex = try builder.demangle("$sSiD") From 2653728c355305f801c4c2e16d1197e5395f71e6 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 23:37:37 +0800 Subject: [PATCH 10/23] feat(Demangling): direct node construction on SymbolStoreBuilder Add intern(kind:), intern(kind:text:), intern(kind:index:), and intern(kind:children:) so index builders can construct wrapper nodes (e.g. the .type dictionary keys SymbolIndexStore-style consumers build around member contexts) straight in the arena, without a Node detour. All routes share the same hash-consing tables: a directly constructed node and an interned structurally equal Node tree collapse to one index, covered by test. --- .../Demangling/Store/SymbolStoreBuilder.swift | 36 +++++++++++++++++++ Tests/DemanglingTests/SymbolStoreTests.swift | 28 +++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/Sources/Demangling/Store/SymbolStoreBuilder.swift b/Sources/Demangling/Store/SymbolStoreBuilder.swift index a34ff58..4e7e43e 100644 --- a/Sources/Demangling/Store/SymbolStoreBuilder.swift +++ b/Sources/Demangling/Store/SymbolStoreBuilder.swift @@ -54,6 +54,42 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { return intern(tree) } + // MARK: - Direct Construction + + /// Interns a parameterless node. + public mutating func intern(kind: Node.Kind) -> SymbolStore.NodeIndex { + SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .none)) + } + + /// Interns a text-carrying leaf node. + public mutating func intern(kind: Node.Kind, text: String) -> SymbolStore.NodeIndex { + SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .text(text))) + } + + /// Interns an index-carrying leaf node. + public mutating func intern(kind: Node.Kind, index: UInt64) -> SymbolStore.NodeIndex { + SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .index(index))) + } + + /// Interns an interior node over already-interned children — e.g. a + /// `.type` wrapper around a stored subtree, the pattern index builders + /// use for dictionary keys. Children must be indices minted by this + /// builder; an empty child list interns a parameterless node. + /// + /// Hash-consing is shared with every other insertion route: constructing + /// a node directly and interning a structurally equal `Node` tree yield + /// the same index. + public mutating func intern(kind: Node.Kind, children: [SymbolStore.NodeIndex]) -> SymbolStore.NodeIndex { + let childIndices = children.map { childIndex in + precondition(Int(childIndex.rawValue) < nodes.count, "Child index does not belong to this builder") + return childIndex.rawValue + } + if childIndices.isEmpty { + return SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .none)) + } + return SymbolStore.NodeIndex(rawValue: internInterior(kind: kind, childIndices: childIndices)) + } + /// Freezes the builder into an immutable, `Sendable` store. /// /// Consumes the builder; interning tables are dropped, only the flat diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index 75793bf..c4174e5 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -227,6 +227,34 @@ struct SymbolStoreTests { } } + @Test func directConstructionSharesHashConsingWithTreeInterning() { + // Building Swift.Int by hand and interning the equivalent Node tree + // must collapse to the same index, and print identically. + var builder = SymbolStoreBuilder() + + let moduleIndex = builder.intern(kind: .module, text: "Swift") + let identifierIndex = builder.intern(kind: .identifier, text: "Int") + let structureIndex = builder.intern(kind: .structure, children: [moduleIndex, identifierIndex]) + let typeIndex = builder.intern(kind: .type, children: [structureIndex]) + + let equivalentTree = Node(kind: .type, children: [ + Node(kind: .structure, children: [ + Node(kind: .module, text: "Swift"), + Node(kind: .identifier, text: "Int"), + ]), + ]) + let internedTreeIndex = builder.intern(equivalentTree) + #expect(internedTreeIndex == typeIndex, "Direct construction and tree interning should hash-cons to one index") + + let emptyListIndex = builder.intern(kind: .emptyList) + let indexNodeIndex = builder.intern(kind: .index, index: 42) + + let store = builder.freeze() + #expect(store.reference(at: typeIndex).print(using: .default) == equivalentTree.print(using: .default)) + #expect(store.reference(at: emptyListIndex).kind == .emptyList) + #expect(store.reference(at: indexNodeIndex).index == 42) + } + @Test func remangleParityWithNodePath() throws { // Remangling a NodeReference (bridged through materialization) must // produce the same mangled string as the Node path. From a2fa44f970856214621aa71b9f0f4290d1045628 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Thu, 23 Jul 2026 23:47:29 +0800 Subject: [PATCH 11/23] feat(Demangling): expose DemanglingPrinter and StackSafeExecutor via @_spi(Internals) Deep consumers (MachOSwiftSection) need to print NodeReference trees into custom rich targets (SemanticString) and reuse the library's stack-safety wrapper when driving the engine directly. The general public surface (SymbolStore, NodeReference, DemanglingNode, traversal, print/decode/remangle entries) stays plain public; the engine-level entry points are gated behind @_spi(Internals), matching the SPI group name MachOSwiftSection already uses across its own modules. Verified from a client module: both symbols resolve under @_spi(Internals) import Demangling and are invisible without it. --- Sources/Demangling/Node/Printer/NodePrinter.swift | 10 +++++++++- Sources/Demangling/Utils/StackSafeExecutor.swift | 12 ++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index 9b60061..5f660a0 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -1,7 +1,15 @@ /// Generic tree-printing engine shared by the public `NodePrinter` /// (specialized on `Node`) and store-backed printing (specialized on /// `NodeReference`). See evolution proposal 0001, Phase 2. -struct DemanglingPrinter: Sendable { +/// +/// SPI note: exposed as `@_spi(Internals)` so deep consumers (MachOSwiftSection) +/// can print `NodeReference` trees into custom rich targets. On the store path +/// `NodePrintContext.node` and the type-reference scope hooks receive nil +/// (`name as? Node`); a rich target that keys on the node must account for +/// that until the hooks are abstracted over `DemanglingNode`. Wrap calls in +/// `StackSafeExecutor.execute` for deeply nested symbols. +@_spi(Internals) +public struct DemanglingPrinter: Sendable { /// Mirrors ``swift::Demangle::NodePrinter::MaxDepth`` from /// ``swift/include/swift/Demangling/Demangle.h``. Bails the print /// recursion with ``<>`` once a single root-to-leaf path diff --git a/Sources/Demangling/Utils/StackSafeExecutor.swift b/Sources/Demangling/Utils/StackSafeExecutor.swift index 8ce6dbe..fca147e 100644 --- a/Sources/Demangling/Utils/StackSafeExecutor.swift +++ b/Sources/Demangling/Utils/StackSafeExecutor.swift @@ -1,5 +1,8 @@ import Foundation +/// SPI note: exposed as `@_spi(Internals)` so deep consumers driving +/// `DemanglingPrinter` directly can reuse the same stack-safety wrapper the +/// library uses for its own print/remangle entry points. /// Executes blocks with automatic stack-size safety. /// /// On Darwin, non-main threads (including Swift Concurrency cooperative workers) @@ -7,7 +10,8 @@ import Foundation /// inside the demangler/remangler. This type detects insufficient remaining stack /// space and transparently re-dispatches the block to a dedicated 8MB-stack /// `Thread`. On non-Darwin platforms, the block runs directly. -enum StackSafeExecutor { +@_spi(Internals) +public enum StackSafeExecutor { #if canImport(Darwin) /// Minimum stack space (in bytes) required for safe recursive operations. private static let minimumRequiredStackSize = 2 * 1024 * 1024 // 2MB @@ -18,7 +22,7 @@ enum StackSafeExecutor { /// Executes the given block, switching to a large-stack thread if the /// current thread's remaining stack space is insufficient. - static func execute(_ block: @escaping @Sendable () -> String) -> String { + public static func execute(_ block: @escaping @Sendable () -> String) -> String { #if canImport(Darwin) if currentThreadHasSufficientStack { return block() @@ -34,7 +38,7 @@ enum StackSafeExecutor { /// Re-dispatches to a dedicated 8MB-stack `Thread` when the current thread /// is about to run out of room, and propagates typed errors across the /// thread boundary. - static func execute( + public static func execute( _ block: @escaping @Sendable () throws(Failure) -> Success ) throws(Failure) -> Success { #if canImport(Darwin) @@ -56,7 +60,7 @@ enum StackSafeExecutor { /// has enough room, the block runs inline without spawning a thread or /// suspending. Use this from async contexts when you want to avoid blocking /// a cooperative worker on an OS-level semaphore. - static func executeAsync( + public static func executeAsync( _ block: @escaping @Sendable () throws(Failure) -> Success ) async throws(Failure) -> Success { #if canImport(Darwin) From 7a6a9f41e50445897242bb789c0fbcca70178699 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Fri, 24 Jul 2026 00:16:02 +0800 Subject: [PATCH 12/23] =?UTF-8?q?feat(Demangling):=20Phase=203=20=E2=80=94?= =?UTF-8?q?=20cache-free=20bulk=20demangling=20and=20slim=20intern=20table?= =?UTF-8?q?s=20(proposal=200001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes eliminate the bulk-build overhead identified in Phase 1: 1. Demangler node-construction seam. All ~594 construction sites go through new createNode(...) instance methods; with internsLeaves: false (used by the new internal demangleAsNodeTransient entry that SymbolStoreBuilder.demangle now calls) they build plain uncached nodes — no NodeCache leaf writes, no global lock traffic, nothing retained after the transient tree drops. Default behavior of every public entry is unchanged. 2. Intern-table slimming. The builder's three dictionary tables (which duplicated keys: 12-byte compacts, child-index arrays, String texts) are replaced by open-addressing slot arrays holding 4-byte indices; keys are recovered from the flat buffers on comparison. Acceptance on the live dyld-cache SwiftUI corpus (234,232 symbols, debug build): 619,688 unique nodes in 8.75 MB flat storage (14.1 B/node vs <=16 target, 37 B/symbol), store build 25.3s vs interning Node path 28.5s (faster than baseline; budget allowed 1.2x slower), process footprint delta during build 9.9 MB ~= retained store + ~1 MB transient (the old scheme's high-water was ~16 MB at one fifth this corpus size). Acceptance test asserts per-unit storage and throughput budgets; cache-freedom is asserted by leaf-identity (no canonicalization across transient runs). --- .../Main/Demangle/DemangleInterface.swift | 14 +- .../Demangle/Demangler+NodeCreation.swift | 71 ++ .../Demangling/Main/Demangle/Demangler.swift | 1117 +++++++++-------- .../Demangling/Store/SymbolStoreBuilder.swift | 234 +++- .../SymbolStorePhase3AcceptanceTests.swift | 77 ++ Tests/DemanglingTests/SymbolStoreTests.swift | 29 + 6 files changed, 939 insertions(+), 603 deletions(-) create mode 100644 Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift create mode 100644 Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift diff --git a/Sources/Demangling/Main/Demangle/DemangleInterface.swift b/Sources/Demangling/Main/Demangle/DemangleInterface.swift index 1261168..80e1d51 100644 --- a/Sources/Demangling/Main/Demangle/DemangleInterface.swift +++ b/Sources/Demangling/Main/Demangle/DemangleInterface.swift @@ -42,8 +42,8 @@ public func demangleAsNode(_ mangled: String, isType: Bool = false, symbolicRefe /// - isType: if true, no prefix is parsed and, on completion, the first item on the parse stack is returned. /// - Returns: the successfully parsed result /// - Throws: a SwiftSymbolParseError error that contains parse position when the error occurred. -private func demangleAsNode(_ mangled: C, isType: Bool = false, symbolicReferenceResolver: DemangleSymbolicReferenceResolver? = nil, internsSubtrees: Bool = true) throws(DemanglingError) -> Node where C.Iterator.Element == UnicodeScalar, C.Index: Sendable { - var demangler = Demangler(scalars: mangled) +private func demangleAsNode(_ mangled: C, isType: Bool = false, symbolicReferenceResolver: DemangleSymbolicReferenceResolver? = nil, internsSubtrees: Bool = true, internsLeaves: Bool = true) throws(DemanglingError) -> Node where C.Iterator.Element == UnicodeScalar, C.Index: Sendable { + var demangler = Demangler(scalars: mangled, internsLeaves: internsLeaves) demangler.symbolicReferenceResolver = symbolicReferenceResolver let demangledNode: Node if isType { @@ -58,3 +58,13 @@ private func demangleAsNode(_ mangled: C, isType: Bool } return NodeCache.shared.intern(demangledNode) } + +/// Fully cache-free demangle for transient trees (proposal 0001, Phase 3): +/// neither leaves nor subtrees touch `NodeCache.shared`, so bulk demangling +/// through `SymbolStoreBuilder` leaves no trace in global state. +func demangleAsNodeTransient(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> Node { + let demangleBlock: @Sendable () throws(DemanglingError) -> Node = { + try demangleAsNode(mangled.unicodeScalars, isType: isType, internsSubtrees: false, internsLeaves: false) + } + return try StackSafeExecutor.execute(demangleBlock) +} diff --git a/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift b/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift new file mode 100644 index 0000000..2ed93c6 --- /dev/null +++ b/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift @@ -0,0 +1,71 @@ +/// The demangler's node-construction seam (proposal 0001, Phase 3). +/// +/// Every node the demangler builds goes through these instance methods. With +/// `internsLeaves` (the default) they forward to the `Node.create` factories, +/// which intern leaves in `NodeCache.shared` — the historical behavior. With +/// `internsLeaves: false` they construct plain uncached nodes: no global lock +/// traffic and nothing retained once the transient tree is dropped, which is +/// what the `SymbolStore` bridge wants. +extension Demangler { + @inline(__always) + func createNode(kind: Node.Kind, contents: Node.Contents = .none, children: [Node] = []) -> Node { + internsLeaves + ? Node.create(kind: kind, contents: contents, children: children) + : Node(kind: kind, contents: contents, children: children) + } + + @inline(__always) + func createNode(kind: Node.Kind, contents: Node.Contents = .none, inlineChildren: Node.Children) -> Node { + internsLeaves + ? Node.create(kind: kind, contents: contents, inlineChildren: inlineChildren) + : Node(kind: kind, contents: contents, children: Array(inlineChildren)) + } + + @inline(__always) + func createNode(kind: Node.Kind, child: Node) -> Node { + createNode(kind: kind, contents: .none, children: [child]) + } + + @inline(__always) + func createNode(kind: Node.Kind, text: String, child: Node) -> Node { + createNode(kind: kind, contents: .text(text), children: [child]) + } + + @inline(__always) + func createNode(kind: Node.Kind, text: String, children: [Node] = []) -> Node { + createNode(kind: kind, contents: .text(text), children: children) + } + + @inline(__always) + func createNode(kind: Node.Kind, index: UInt64, child: Node) -> Node { + createNode(kind: kind, contents: .index(index), children: [child]) + } + + @inline(__always) + func createNode(kind: Node.Kind, index: UInt64, children: [Node] = []) -> Node { + createNode(kind: kind, contents: .index(index), children: children) + } + + /// Compound: `.type` wrapping a node of `typeWithChildKind` with one child. + func createNode(typeWithChildKind: Node.Kind, childChild: Node) -> Node { + createNode(kind: .type, children: [createNode(kind: typeWithChildKind, children: [childChild])]) + } + + /// Compound: `.type` wrapping a node of `typeWithChildKind` with children. + func createNode(typeWithChildKind: Node.Kind, childChildren: [Node]) -> Node { + createNode(kind: .type, children: [createNode(kind: typeWithChildKind, children: childChildren)]) + } + + /// Compound: Swift stdlib type (`.type` > `kind` > [`.module("Swift")`, `.identifier(name)`]). + func createNode(swiftStdlibTypeKind: Node.Kind, name: String) -> Node { + createNode(kind: .type, children: [createNode(kind: swiftStdlibTypeKind, children: [ + createNode(kind: .module, text: stdlibName), + createNode(kind: .identifier, text: name), + ])]) + } + + /// Compound: Swift builtin type (`.type` > `kind(name)`). + func createNode(swiftBuiltinType: Node.Kind, name: String) -> Node { + createNode(kind: .type, children: [createNode(kind: swiftBuiltinType, text: name)]) + } +} diff --git a/Sources/Demangling/Main/Demangle/Demangler.swift b/Sources/Demangling/Main/Demangle/Demangler.swift index f419083..8a59b2b 100644 --- a/Sources/Demangling/Main/Demangle/Demangler.swift +++ b/Sources/Demangling/Main/Demangle/Demangler.swift @@ -9,8 +9,15 @@ struct Demangler: Sendable where C: Collection, C.Iterator.Element == Unicode var symbolicReferenceResolver: DemangleSymbolicReferenceResolver? - init(scalars: C) { + /// When false, node construction bypasses `NodeCache.shared` entirely — + /// no leaf interning, no global lock traffic, nothing retained after the + /// tree is dropped. Used by the `SymbolStore` bridge, whose trees are + /// transient by design (proposal 0001, Phase 3). + let internsLeaves: Bool + + init(scalars: C, internsLeaves: Bool = true) { self.scanner = ScalarScanner(scalars: scalars) + self.internsLeaves = internsLeaves } static func getManglingPrefixLength(_ scalars: C) -> Int { @@ -116,7 +123,7 @@ extension Demangler { topLevelChildren.append(suffix) } try require(topLevelChildren.count != 0) - return Node.create(kind: .global, children: topLevelChildren) + return createNode(kind: .global, children: topLevelChildren) } package mutating func demangleType() throws(DemanglingError) -> Node { @@ -127,7 +134,7 @@ extension Demangler { return result } - return Node.create(kind: .suffix, text: String(String.UnicodeScalarView(scanner.scalars))) + return createNode(kind: .suffix, text: String(String.UnicodeScalarView(scanner.scalars))) } private mutating func parseAndPushNames() throws(DemanglingError) { @@ -156,16 +163,16 @@ extension Demangler { case "a": return NodeFactory.asyncAnnotation case "A": return NodeFactory.isolatedAnyFunctionType case "b": return NodeFactory.concurrentFunctionType - case "c": return try Node.create(kind: .globalActorFunctionType, child: require(popTypeAndGetChild())) + case "c": return try createNode(kind: .globalActorFunctionType, child: require(popTypeAndGetChild())) case "C": return NodeFactory.nonIsolatedCallerFunctionType - case "i": return try Node.create(typeWithChildKind: .isolated, childChild: require(popTypeAndGetChild())) + case "i": return try createNode(typeWithChildKind: .isolated, childChild: require(popTypeAndGetChild())) case "j": return try demangleDifferentiableFunctionType() - case "k": return try Node.create(typeWithChildKind: .noDerivative, childChild: require(popTypeAndGetChild())) - case "K": return try Node.create(kind: .typedThrowsAnnotation, child: require(popTypeAndGetChild())) - case "t": return try Node.create(typeWithChildKind: .compileTimeLiteral, childChild: require(popTypeAndGetChild())) + case "k": return try createNode(typeWithChildKind: .noDerivative, childChild: require(popTypeAndGetChild())) + case "K": return try createNode(kind: .typedThrowsAnnotation, child: require(popTypeAndGetChild())) + case "t": return try createNode(typeWithChildKind: .compileTimeLiteral, childChild: require(popTypeAndGetChild())) case "T": return NodeFactory.sendingResultFunctionType - case "u": return try Node.create(typeWithChildKind: .sending, childChild: require(popTypeAndGetChild())) - case "g": return try Node.create(typeWithChildKind: .constValue, childChild: require(popTypeAndGetChild())) + case "u": return try createNode(typeWithChildKind: .sending, childChild: require(popTypeAndGetChild())) + case "g": return try createNode(typeWithChildKind: .constValue, childChild: require(popTypeAndGetChild())) default: throw failure } } @@ -193,7 +200,7 @@ extension Demangler { case "A": return try demangleMultiSubstitutions() case "B": return try demangleBuiltinType() case "C": return try demangleAnyGenericType(kind: .class) - case "D": return try Node.create(kind: .typeMangling, child: require(pop(kind: .type))) + case "D": return try createNode(kind: .typeMangling, child: require(pop(kind: .type))) case "E": return try demangleExtensionContext() case "F": return try demanglePlainFunction() case "G": return try demangleBoundGenericType() @@ -204,13 +211,13 @@ extension Demangler { case "D": return try demangleDependentProtocolConformanceRoot() case "I": return try demangleDependentProtocolConformanceInherited() case "O": return try demangleDependentProtocolConformanceOpaque() - case "P": return try Node.create(kind: .protocolConformanceRefInTypeModule, child: popProtocol()) - case "p": return try Node.create(kind: .protocolConformanceRefInProtocolModule, child: popProtocol()) - case "X": return try Node.create(kind: .packProtocolConformance, child: popAnyProtocolConformanceList()) - case "c": return try Node.create(kind: .protocolConformanceDescriptorRecord, child: popProtocolConformance()) - case "n": return try Node.create(kind: .nominalTypeDescriptorRecord, child: require(pop(kind: .type))) - case "o": return try Node.create(kind: .opaqueTypeDescriptorRecord, child: require(pop())) - case "r": return try Node.create(kind: .protocolDescriptorRecord, child: popProtocol()) + case "P": return try createNode(kind: .protocolConformanceRefInTypeModule, child: popProtocol()) + case "p": return try createNode(kind: .protocolConformanceRefInProtocolModule, child: popProtocol()) + case "X": return try createNode(kind: .packProtocolConformance, child: popAnyProtocolConformanceList()) + case "c": return try createNode(kind: .protocolConformanceDescriptorRecord, child: popProtocolConformance()) + case "n": return try createNode(kind: .nominalTypeDescriptorRecord, child: require(pop(kind: .type))) + case "o": return try createNode(kind: .opaqueTypeDescriptorRecord, child: require(pop())) + case "r": return try createNode(kind: .protocolDescriptorRecord, child: popProtocol()) case "F": return NodeFactory.accessibleFunctionRecord default: try scanner.backtrack(count: 2) @@ -220,7 +227,7 @@ extension Demangler { case "K": return NodeFactory.throwsAnnotation case "L": return try demangleLocalIdentifier() case "M": return try demangleMetatype() - case "N": return try Node.create(kind: .typeMetadata, child: require(pop(kind: .type))) + case "N": return try createNode(kind: .typeMetadata, child: require(pop(kind: .type))) case "O": return try demangleAnyGenericType(kind: .enum) case "P": return try demangleAnyGenericType(kind: .protocol) case "Q": return try demangleArchetype() @@ -231,33 +238,33 @@ extension Demangler { case "W": return try demangleWitness() case "X": return try demangleSpecialType() case "Y": return try demangleTypeAnnotation() - case "Z": return try Node.create(kind: .static, child: require(pop(where: { $0.isEntity }))) + case "Z": return try createNode(kind: .static, child: require(pop(where: { $0.isEntity }))) case "a": return try demangleAnyGenericType(kind: .typeAlias) case "c": return try require(popFunctionType(kind: .functionType)) case "d": return NodeFactory.variadicMarker case "f": return try demangleFunctionEntity() case "g": return try demangleRetroactiveConformance() - case "h": return try Node.create(typeWithChildKind: .shared, childChild: require(popTypeAndGetChild())) + case "h": return try createNode(typeWithChildKind: .shared, childChild: require(popTypeAndGetChild())) case "i": return try demangleSubscript() case "l": return try demangleGenericSignature(hasParamCounts: false) - case "m": return try Node.create(typeWithChildKind: .metatype, childChild: require(pop(kind: .type))) - case "n": return try Node.create(typeWithChildKind: .owned, childChild: popTypeAndGetChild()) + case "m": return try createNode(typeWithChildKind: .metatype, childChild: require(pop(kind: .type))) + case "n": return try createNode(typeWithChildKind: .owned, childChild: popTypeAndGetChild()) case "o": return try demangleOperatorIdentifier() case "p": return try demangleProtocolListType() - case "q": return try Node.create(kind: .type, child: demangleGenericParamIndex()) + case "q": return try createNode(kind: .type, child: demangleGenericParamIndex()) case "r": return try demangleGenericSignature(hasParamCounts: true) - case "s": return Node.create(kind: .module, contents: .text(stdlibName)) + case "s": return createNode(kind: .module, contents: .text(stdlibName)) case "t": return try popTuple() case "u": return try demangleGenericType() case "v": return try demangleVariable() case "w": return try demangleValueWitness() - case "x": return try Node.create(kind: .type, child: getDependentGenericParamType(depth: 0, index: 0)) + case "x": return try createNode(kind: .type, child: getDependentGenericParamType(depth: 0, index: 0)) case "y": return NodeFactory.emptyList - case "z": return try Node.create(typeWithChildKind: .inOut, childChild: require(popTypeAndGetChild())) + case "z": return try createNode(typeWithChildKind: .inOut, childChild: require(popTypeAndGetChild())) case "_": return NodeFactory.firstElementMarker case ".": try scanner.backtrack() - return Node.create(kind: .suffix, contents: .text(scanner.remainder())) + return createNode(kind: .suffix, contents: .text(scanner.remainder())) case "$": return try demangleIntegerType() default: try scanner.backtrack() @@ -279,7 +286,7 @@ extension Demangler { } private mutating func demangleIndexAsName() throws(DemanglingError) -> Node { - return try Node.create(kind: .number, contents: .index(demangleIndex())) + return try createNode(kind: .number, contents: .index(demangleIndex())) } private mutating func demangleMultiSubstitutions() throws(DemanglingError) -> Node { @@ -354,25 +361,25 @@ extension Demangler { } try children.append(popFunctionParams(kind: .argumentTuple)) try children.append(popFunctionParams(kind: .returnType)) - let name = Node.create(kind: kind, children: children) - return Node.create(kind: .type, child: name) + let name = createNode(kind: kind, children: children) + return createNode(kind: .type, child: name) } private mutating func popFunctionParams(kind: Node.Kind) throws(DemanglingError) -> Node { let paramsType: Node if pop(kind: .emptyList) != nil { - return Node.create(kind: kind, child: Node.create(kind: .type, child: NodeFactory.tuple)) + return createNode(kind: kind, child: createNode(kind: .type, child: NodeFactory.tuple)) } else { paramsType = try require(pop(kind: .type)) } - return Node.create(kind: kind, children: [paramsType]) + return createNode(kind: kind, children: [paramsType]) } private mutating func getLabel(params: Node, idx: Int) throws(DemanglingError) -> Node { if isOldFunctionTypeMangling { let param = try require(params.children.at(idx)) if let label = param.children.first(where: { $0.kind == .tupleElementName }) { - return Node.create(kind: .identifier, contents: .text(label.text ?? "")) + return createNode(kind: .identifier, contents: .text(label.text ?? "")) } return NodeFactory.firstElementMarker } @@ -451,7 +458,7 @@ extension Demangler { return NodeFactory.labelList } - return Node.create(kind: .labelList, children: isOldFunctionTypeMangling ? children : children.reversed()) + return createNode(kind: .labelList, children: isOldFunctionTypeMangling ? children : children.reversed()) } private mutating func popTuple() throws(DemanglingError) -> Node { @@ -465,20 +472,20 @@ extension Demangler { elementChildren.append(variadicMarker) } if let ident = pop(kind: .identifier), case .text(let text) = ident.contents { - elementChildren.append(Node.create(kind: .tupleElementName, contents: .text(text))) + elementChildren.append(createNode(kind: .tupleElementName, contents: .text(text))) } try elementChildren.append(require(pop(kind: .type))) - tupleChildren.append(Node.create(kind: .tupleElement, children: elementChildren)) + tupleChildren.append(createNode(kind: .tupleElement, children: elementChildren)) } while !firstElem tupleChildren.reverse() } - let root = Node.create(kind: .tuple, children: tupleChildren) - return Node.create(kind: .type, child: root) + let root = createNode(kind: .tuple, children: tupleChildren) + return createNode(kind: .type, child: root) } private mutating func popPack(kind: Node.Kind = .pack) throws(DemanglingError) -> Node { if pop(kind: .emptyList) != nil { - return Node.create(kind: .type, child: NodeFactory.pack) + return createNode(kind: .type, child: NodeFactory.pack) } var firstElem = false var children = [Node]() @@ -487,7 +494,7 @@ extension Demangler { try children.append(require(pop(kind: .type))) } while !firstElem children.reverse() - return Node.create(kind: .type, child: Node.create(kind: .pack, children: children)) + return createNode(kind: .type, child: createNode(kind: .pack, children: children)) } private mutating func popSilPack() throws(DemanglingError) -> Node { @@ -507,7 +514,7 @@ extension Demangler { try children.insert(require(pop(kind: .type)), at: 0) } while !firstElem } - return Node.create(kind: .typeList, children: children) + return createNode(kind: .typeList, children: children) } private mutating func popProtocol() throws(DemanglingError) -> Node { @@ -524,7 +531,7 @@ extension Demangler { let name = try require(pop { $0.isDeclName }) let context = try popContext() - return Node.create(typeWithChildKind: .protocol, childChildren: [context, name]) + return createNode(typeWithChildKind: .protocol, childChildren: [context, name]) } private mutating func popAnyProtocolConformanceList() throws(DemanglingError) -> Node { @@ -537,7 +544,7 @@ extension Demangler { } while !firstElem children.reverse() } - return Node.create(kind: .anyProtocolConformanceList, children: children) + return createNode(kind: .anyProtocolConformanceList, children: children) } private mutating func popAnyProtocolConformance() -> Node? { @@ -557,14 +564,14 @@ extension Demangler { private mutating func demangleRetroactiveProtocolConformanceRef() throws(DemanglingError) -> Node { let module = try require(popModule()) let proto = try require(popProtocol()) - return Node.create(kind: .protocolConformanceRefInOtherModule, children: [proto, module]) + return createNode(kind: .protocolConformanceRefInOtherModule, children: [proto, module]) } private mutating func demangleConcreteProtocolConformance() throws(DemanglingError) -> Node { do { let conditionalConformanceList = try require(popAnyProtocolConformanceList()) let conformanceRef = try pop(kind: .protocolConformanceRefInTypeModule) ?? pop(kind: .protocolConformanceRefInProtocolModule) ?? demangleRetroactiveProtocolConformanceRef() - return try Node.create(kind: .concreteProtocolConformance, children: [require(pop(kind: .type)), conformanceRef, conditionalConformanceList]) + return try createNode(kind: .concreteProtocolConformance, children: [require(pop(kind: .type)), conformanceRef, conditionalConformanceList]) } catch { throw error as! DemanglingError } @@ -584,27 +591,27 @@ extension Demangler { private mutating func demangleDependentProtocolConformanceRoot() throws(DemanglingError) -> Node { let index = try demangleDependentConformanceIndex() let prot = try popProtocol() - return try Node.create(kind: .dependentProtocolConformanceRoot, children: [require(pop(kind: .type)), prot, index]) + return try createNode(kind: .dependentProtocolConformanceRoot, children: [require(pop(kind: .type)), prot, index]) } private mutating func demangleDependentProtocolConformanceInherited() throws(DemanglingError) -> Node { let index = try demangleDependentConformanceIndex() let prot = try popProtocol() let nested = try require(popDependentProtocolConformance()) - return Node.create(kind: .dependentProtocolConformanceInherited, children: [nested, prot, index]) + return createNode(kind: .dependentProtocolConformanceInherited, children: [nested, prot, index]) } private mutating func popDependentAssociatedConformance() throws(DemanglingError) -> Node { let prot = try popProtocol() let dependentType = try require(pop(kind: .type)) - return Node.create(kind: .dependentAssociatedConformance, children: [dependentType, prot]) + return createNode(kind: .dependentAssociatedConformance, children: [dependentType, prot]) } private mutating func demangleDependentProtocolConformanceAssociated() throws(DemanglingError) -> Node { let index = try demangleDependentConformanceIndex() let assoc = try popDependentAssociatedConformance() let nested = try require(popDependentProtocolConformance()) - return Node.create(kind: .dependentProtocolConformanceAssociated, children: [nested, assoc, index]) + return createNode(kind: .dependentProtocolConformanceAssociated, children: [nested, assoc, index]) } private mutating func demangleDependentConformanceIndex() throws(DemanglingError) -> Node { @@ -612,13 +619,13 @@ extension Demangler { if index == 1 { return NodeFactory.unknownIndex } - return Node.create(kind: .index, contents: .index(index - 2)) + return createNode(kind: .index, contents: .index(index - 2)) } private mutating func demangleDependentProtocolConformanceOpaque() throws(DemanglingError) -> Node { let type = try require(pop(kind: .type)) let conformance = try require(popDependentProtocolConformance()) - return Node.create(kind: .dependentProtocolConformanceOpaque, children: [conformance, type]) + return createNode(kind: .dependentProtocolConformanceOpaque, children: [conformance, type]) } private mutating func popModule() -> Node? { @@ -662,9 +669,9 @@ extension Demangler { let id = try require(pop(kind: .identifier)) if let p = proto { - return Node.create(kind: .dependentAssociatedTypeRef, children: [id, p]) + return createNode(kind: .dependentAssociatedTypeRef, children: [id, p]) } else { - return Node.create(kind: .dependentAssociatedTypeRef, child: id) + return createNode(kind: .dependentAssociatedTypeRef, child: id) } } @@ -675,7 +682,7 @@ extension Demangler { firstElem = pop(kind: .firstElementMarker) != nil try assocTypePath.append(require(popAssociatedTypeName())) } while !firstElem - return Node.create(kind: .assocTypePath, children: assocTypePath.reversed()) + return createNode(kind: .assocTypePath, children: assocTypePath.reversed()) } private mutating func popProtocolConformance() throws(DemanglingError) -> Node { @@ -689,13 +696,13 @@ extension Demangler { type = pop(kind: .type) } if let gs = genSig { - type = try Node.create(typeWithChildKind: .dependentGenericType, childChildren: [gs, require(type)]) + type = try createNode(typeWithChildKind: .dependentGenericType, childChildren: [gs, require(type)]) } var children = try [require(type), proto, module] if let i = ident { children.append(i) } - return Node.create(kind: .protocolConformance, children: children) + return createNode(kind: .protocolConformance, children: children) } private mutating func getDependentGenericParamType(depth: Int, index: Int) throws(DemanglingError) -> Node { @@ -710,20 +717,20 @@ extension Demangler { name = "\(name)\(depth)" } - return Node.create(kind: .dependentGenericParamType, children: [ - Node.create(kind: .index, index: UInt64(depth)), - Node.create(kind: .index, index: UInt64(index)), + return createNode(kind: .dependentGenericParamType, children: [ + createNode(kind: .index, index: UInt64(depth)), + createNode(kind: .index, index: UInt64(index)), ]) } private mutating func demangleStandardSubstitution() throws(DemanglingError) -> Node { switch try scanner.readScalar() { - case "o": return Node.create(kind: .module, contents: .text(objcModule)) - case "C": return Node.create(kind: .module, contents: .text(cModule)) + case "o": return createNode(kind: .module, contents: .text(objcModule)) + case "C": return createNode(kind: .module, contents: .text(cModule)) case "g": - let op = try Node.create(typeWithChildKind: .boundGenericEnum, childChildren: [ - Node.create(swiftStdlibTypeKind: .enum, name: "Optional"), - Node.create(kind: .typeList, child: require(pop(kind: .type))), + let op = try createNode(typeWithChildKind: .boundGenericEnum, childChildren: [ + createNode(swiftStdlibTypeKind: .enum, name: "Optional"), + createNode(kind: .typeList, child: require(pop(kind: .type))), ]) substitutions.append(op) return op @@ -735,78 +742,78 @@ extension Demangler { let nd: Node if secondLevel { switch try scanner.readScalar() { - case "A": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Actor") - case "C": nd = Node.create(swiftStdlibTypeKind: .structure, name: "CheckedContinuation") - case "c": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeContinuation") - case "E": nd = Node.create(swiftStdlibTypeKind: .structure, name: "CancellationError") - case "e": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnownedSerialExecutor") - case "F": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Executor") - case "f": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "SerialExecutor") - case "G": nd = Node.create(swiftStdlibTypeKind: .structure, name: "TaskGroup") - case "g": nd = Node.create(swiftStdlibTypeKind: .structure, name: "ThrowingTaskGroup") - case "h": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "TaskExecutor") - case "I": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "AsyncIteratorProtocol") - case "i": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "AsyncSequence") - case "J": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnownedJob") - case "M": nd = Node.create(swiftStdlibTypeKind: .class, name: "MainActor") - case "P": nd = Node.create(swiftStdlibTypeKind: .structure, name: "TaskPriority") - case "S": nd = Node.create(swiftStdlibTypeKind: .structure, name: "AsyncStream") - case "s": nd = Node.create(swiftStdlibTypeKind: .structure, name: "AsyncThrowingStream") - case "T": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Task") - case "t": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeCurrentTask") + case "A": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Actor") + case "C": nd = createNode(swiftStdlibTypeKind: .structure, name: "CheckedContinuation") + case "c": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeContinuation") + case "E": nd = createNode(swiftStdlibTypeKind: .structure, name: "CancellationError") + case "e": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnownedSerialExecutor") + case "F": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Executor") + case "f": nd = createNode(swiftStdlibTypeKind: .protocol, name: "SerialExecutor") + case "G": nd = createNode(swiftStdlibTypeKind: .structure, name: "TaskGroup") + case "g": nd = createNode(swiftStdlibTypeKind: .structure, name: "ThrowingTaskGroup") + case "h": nd = createNode(swiftStdlibTypeKind: .protocol, name: "TaskExecutor") + case "I": nd = createNode(swiftStdlibTypeKind: .protocol, name: "AsyncIteratorProtocol") + case "i": nd = createNode(swiftStdlibTypeKind: .protocol, name: "AsyncSequence") + case "J": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnownedJob") + case "M": nd = createNode(swiftStdlibTypeKind: .class, name: "MainActor") + case "P": nd = createNode(swiftStdlibTypeKind: .structure, name: "TaskPriority") + case "S": nd = createNode(swiftStdlibTypeKind: .structure, name: "AsyncStream") + case "s": nd = createNode(swiftStdlibTypeKind: .structure, name: "AsyncThrowingStream") + case "T": nd = createNode(swiftStdlibTypeKind: .structure, name: "Task") + case "t": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeCurrentTask") default: throw failure } } else { switch try scanner.readScalar() { - case "a": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Array") - case "A": nd = Node.create(swiftStdlibTypeKind: .structure, name: "AutoreleasingUnsafeMutablePointer") - case "b": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Bool") - case "c": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnicodeScalar") - case "D": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Dictionary") - case "d": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Double") - case "f": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Float") - case "h": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Set") - case "I": nd = Node.create(swiftStdlibTypeKind: .structure, name: "DefaultIndices") - case "i": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Int") - case "J": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Character") - case "N": nd = Node.create(swiftStdlibTypeKind: .structure, name: "ClosedRange") - case "n": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Range") - case "O": nd = Node.create(swiftStdlibTypeKind: .structure, name: "ObjectIdentifier") - case "p": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeMutablePointer") - case "P": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafePointer") - case "R": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeBufferPointer") - case "r": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeMutableBufferPointer") - case "S": nd = Node.create(swiftStdlibTypeKind: .structure, name: "String") - case "s": nd = Node.create(swiftStdlibTypeKind: .structure, name: "Substring") - case "u": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UInt") - case "v": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeMutableRawPointer") - case "V": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeRawPointer") - case "W": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeRawBufferPointer") - case "w": nd = Node.create(swiftStdlibTypeKind: .structure, name: "UnsafeMutableRawBufferPointer") - case "q": nd = Node.create(swiftStdlibTypeKind: .enum, name: "Optional") - case "B": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "BinaryFloatingPoint") - case "E": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Encodable") - case "e": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Decodable") - case "F": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "FloatingPoint") - case "G": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "RandomNumberGenerator") - case "H": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Hashable") - case "j": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Numeric") - case "K": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "BidirectionalCollection") - case "k": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "RandomAccessCollection") - case "L": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Comparable") - case "l": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Collection") - case "M": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "MutableCollection") - case "m": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "RangeReplaceableCollection") - case "Q": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Equatable") - case "T": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Sequence") - case "t": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "IteratorProtocol") - case "U": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "UnsignedInteger") - case "X": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "RangeExpression") - case "x": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "Strideable") - case "Y": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "RawRepresentable") - case "y": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "StringProtocol") - case "Z": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "SignedInteger") - case "z": nd = Node.create(swiftStdlibTypeKind: .protocol, name: "BinaryInteger") + case "a": nd = createNode(swiftStdlibTypeKind: .structure, name: "Array") + case "A": nd = createNode(swiftStdlibTypeKind: .structure, name: "AutoreleasingUnsafeMutablePointer") + case "b": nd = createNode(swiftStdlibTypeKind: .structure, name: "Bool") + case "c": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnicodeScalar") + case "D": nd = createNode(swiftStdlibTypeKind: .structure, name: "Dictionary") + case "d": nd = createNode(swiftStdlibTypeKind: .structure, name: "Double") + case "f": nd = createNode(swiftStdlibTypeKind: .structure, name: "Float") + case "h": nd = createNode(swiftStdlibTypeKind: .structure, name: "Set") + case "I": nd = createNode(swiftStdlibTypeKind: .structure, name: "DefaultIndices") + case "i": nd = createNode(swiftStdlibTypeKind: .structure, name: "Int") + case "J": nd = createNode(swiftStdlibTypeKind: .structure, name: "Character") + case "N": nd = createNode(swiftStdlibTypeKind: .structure, name: "ClosedRange") + case "n": nd = createNode(swiftStdlibTypeKind: .structure, name: "Range") + case "O": nd = createNode(swiftStdlibTypeKind: .structure, name: "ObjectIdentifier") + case "p": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeMutablePointer") + case "P": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafePointer") + case "R": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeBufferPointer") + case "r": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeMutableBufferPointer") + case "S": nd = createNode(swiftStdlibTypeKind: .structure, name: "String") + case "s": nd = createNode(swiftStdlibTypeKind: .structure, name: "Substring") + case "u": nd = createNode(swiftStdlibTypeKind: .structure, name: "UInt") + case "v": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeMutableRawPointer") + case "V": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeRawPointer") + case "W": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeRawBufferPointer") + case "w": nd = createNode(swiftStdlibTypeKind: .structure, name: "UnsafeMutableRawBufferPointer") + case "q": nd = createNode(swiftStdlibTypeKind: .enum, name: "Optional") + case "B": nd = createNode(swiftStdlibTypeKind: .protocol, name: "BinaryFloatingPoint") + case "E": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Encodable") + case "e": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Decodable") + case "F": nd = createNode(swiftStdlibTypeKind: .protocol, name: "FloatingPoint") + case "G": nd = createNode(swiftStdlibTypeKind: .protocol, name: "RandomNumberGenerator") + case "H": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Hashable") + case "j": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Numeric") + case "K": nd = createNode(swiftStdlibTypeKind: .protocol, name: "BidirectionalCollection") + case "k": nd = createNode(swiftStdlibTypeKind: .protocol, name: "RandomAccessCollection") + case "L": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Comparable") + case "l": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Collection") + case "M": nd = createNode(swiftStdlibTypeKind: .protocol, name: "MutableCollection") + case "m": nd = createNode(swiftStdlibTypeKind: .protocol, name: "RangeReplaceableCollection") + case "Q": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Equatable") + case "T": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Sequence") + case "t": nd = createNode(swiftStdlibTypeKind: .protocol, name: "IteratorProtocol") + case "U": nd = createNode(swiftStdlibTypeKind: .protocol, name: "UnsignedInteger") + case "X": nd = createNode(swiftStdlibTypeKind: .protocol, name: "RangeExpression") + case "x": nd = createNode(swiftStdlibTypeKind: .protocol, name: "Strideable") + case "Y": nd = createNode(swiftStdlibTypeKind: .protocol, name: "RawRepresentable") + case "y": nd = createNode(swiftStdlibTypeKind: .protocol, name: "StringProtocol") + case "Z": nd = createNode(swiftStdlibTypeKind: .protocol, name: "SignedInteger") + case "z": nd = createNode(swiftStdlibTypeKind: .protocol, name: "BinaryInteger") default: throw failure } } @@ -886,7 +893,7 @@ extension Demangler { } } while hasWordSubs try require(!identifier.isEmpty) - let result = Node.create(kind: .identifier, contents: .text(identifier)) + let result = createNode(kind: .identifier, contents: .text(identifier)) substitutions.append(result) return result } @@ -907,9 +914,9 @@ extension Demangler { } } switch try scanner.readScalar() { - case "i": return Node.create(kind: .infixOperator, contents: .text(str)) - case "p": return Node.create(kind: .prefixOperator, contents: .text(str)) - case "P": return Node.create(kind: .postfixOperator, contents: .text(str)) + case "i": return createNode(kind: .infixOperator, contents: .text(str)) + case "p": return createNode(kind: .prefixOperator, contents: .text(str)) + case "P": return createNode(kind: .postfixOperator, contents: .text(str)) default: throw failure } } @@ -920,40 +927,40 @@ extension Demangler { case "L": let discriminator = try require(pop(kind: .identifier)) let name = try require(pop(where: { $0.isDeclName })) - return Node.create(kind: .privateDeclName, children: [discriminator, name]) + return createNode(kind: .privateDeclName, children: [discriminator, name]) case "l": let discriminator = try require(pop(kind: .identifier)) - return Node.create(kind: .privateDeclName, children: [discriminator]) + return createNode(kind: .privateDeclName, children: [discriminator]) case "a" ... "j", "A" ... "J": - return try Node.create(kind: .relatedEntityDeclName, children: [ - Node.create(kind: .identifier, contents: .text(String(c))), + return try createNode(kind: .relatedEntityDeclName, children: [ + createNode(kind: .identifier, contents: .text(String(c))), require(pop()), ]) default: try scanner.backtrack() let discriminator = try demangleIndexAsName() let name = try require(pop(where: { $0.isDeclName })) - return Node.create(kind: .localDeclName, children: [discriminator, name]) + return createNode(kind: .localDeclName, children: [discriminator, name]) } } private mutating func demangleBuiltinType() throws(DemanglingError) -> Node { let maxTypeSize: UInt64 = 4096 switch try scanner.readScalar() { - case "A": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.ImplicitActor") - case "b": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.BridgeObject") - case "B": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.UnsafeValueBuffer") - case "e": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.Executor") + case "A": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.ImplicitActor") + case "b": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.BridgeObject") + case "B": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.UnsafeValueBuffer") + case "e": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.Executor") case "f": let size = try demangleIndex() - 1 try require(size > 0 && size <= maxTypeSize) - return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.FPIEEE\(size)") + return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.FPIEEE\(size)") case "i": let size = try demangleIndex() - 1 try require(size > 0 && size <= maxTypeSize) - return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.Int\(size)") - case "I": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.IntLiteral") + return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.Int\(size)") + case "I": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.IntLiteral") case "v": let elts = try demangleIndex() - 1 try require(elts > 0 && elts <= maxTypeSize) @@ -961,25 +968,25 @@ extension Demangler { let text = try require(eltType.text) try require(eltType.kind == .builtinTypeName && text.starts(with: "Builtin.") == true) let name = text["Builtin.".endIndex...] - return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.Vec\(elts)x\(name)") + return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.Vec\(elts)x\(name)") case "V": let element = try require(pop(kind: .type)) let size = try require(pop(kind: .type)) - return Node.create(typeWithChildKind: .builtinFixedArray, childChildren: [size, element]) - case "O": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.UnknownObject") - case "o": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.NativeObject") - case "p": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.RawPointer") - case "t": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.SILToken") - case "w": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.Word") - case "c": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.RawUnsafeContinuation") - case "D": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.DefaultActorStorage") - case "d": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.NonDefaultDistributedActorStorage") - case "j": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.Job") - case "P": return Node.create(swiftBuiltinType: .builtinTypeName, name: "Builtin.PackIndex") - case "T": return Node.create(kind: .type, children: [Node.create(kind: .builtinTupleType)]) + return createNode(typeWithChildKind: .builtinFixedArray, childChildren: [size, element]) + case "O": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.UnknownObject") + case "o": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.NativeObject") + case "p": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.RawPointer") + case "t": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.SILToken") + case "w": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.Word") + case "c": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.RawUnsafeContinuation") + case "D": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.DefaultActorStorage") + case "d": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.NonDefaultDistributedActorStorage") + case "j": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.Job") + case "P": return createNode(swiftBuiltinType: .builtinTypeName, name: "Builtin.PackIndex") + case "T": return createNode(kind: .type, children: [createNode(kind: .builtinTupleType)]) case "W": let referent = try require(pop(kind: .type)) - return Node.create(typeWithChildKind: .builtinBorrow, childChildren: [referent]) + return createNode(typeWithChildKind: .builtinBorrow, childChildren: [referent]) default: throw failure } } @@ -987,7 +994,7 @@ extension Demangler { private mutating func demangleAnyGenericType(kind: Node.Kind) throws(DemanglingError) -> Node { let name = try require(pop(where: { $0.isDeclName })) let ctx = try popContext() - let type = Node.create(typeWithChildKind: kind, childChildren: [ctx, name]) + let type = createNode(typeWithChildKind: kind, childChildren: [ctx, name]) substitutions.append(type) return type } @@ -997,9 +1004,9 @@ extension Demangler { let module = try require(popModule()) let type = try popTypeAndGetAnyGeneric() if let g = genSig { - return Node.create(kind: .extension, children: [module, type, g]) + return createNode(kind: .extension, children: [module, type, g]) } else { - return Node.create(kind: .extension, children: [module, type]) + return createNode(kind: .extension, children: [module, type]) } } @@ -1034,7 +1041,7 @@ extension Demangler { if visited.children.last?.kind == .opaqueReturnTypeParent { return visited } - return visited.addingChild(Node.create(kind: .opaqueReturnTypeParent, contents: .text(getParentId()))) + return visited.addingChild(createNode(kind: .opaqueReturnTypeParent, contents: .text(getParentId()))) } switch visited.kind { @@ -1064,14 +1071,14 @@ extension Demangler { let labelList = try popFunctionParamLabels(type: type) if let g = genSig { - type = Node.create(typeWithChildKind: .dependentGenericType, childChildren: [g, type]) + type = createNode(typeWithChildKind: .dependentGenericType, childChildren: [g, type]) } let name = try require(pop(where: { $0.isDeclName })) let ctx = try popContext() let result = if let ll = labelList { - Node.create(kind: .function, children: [ctx, name, ll, type]) + createNode(kind: .function, children: [ctx, name, ll, type]) } else { - Node.create(kind: .function, children: [ctx, name, type]) + createNode(kind: .function, children: [ctx, name, type]) } let updatedType = setParentForOpaqueReturnTypeNodes(visited: type, parent: result) if updatedType !== type { @@ -1083,7 +1090,7 @@ extension Demangler { private mutating func demangleRetroactiveConformance() throws(DemanglingError) -> Node { let index = try demangleIndexAsName() let conformance = try require(popAnyProtocolConformance()) - return Node.create(kind: .retroactiveConformance, children: [index, conformance]) + return createNode(kind: .retroactiveConformance, children: [index, conformance]) } private mutating func demangleBoundGenericType() throws(DemanglingError) -> Node { @@ -1095,7 +1102,7 @@ extension Demangler { } else { boundNode } - let type = Node.create(kind: .type, child: finalBoundNode) + let type = createNode(kind: .type, child: finalBoundNode) substitutions.append(type) return type } @@ -1106,7 +1113,7 @@ extension Demangler { retroactiveConformances.append(conformance) } retroactiveConformances = retroactiveConformances.reversed() - return retroactiveConformances.isEmpty ? nil : Node.create(kind: .typeList, children: retroactiveConformances) + return retroactiveConformances.isEmpty ? nil : createNode(kind: .typeList, children: retroactiveConformances) } private mutating func demangleBoundGenerics() throws(DemanglingError) -> (typeLists: [Node], conformances: Node?) { @@ -1119,7 +1126,7 @@ extension Demangler { typeListChildren.append(t) } typeListChildren.reverse() - array.append(Node.create(kind: .typeList, children: typeListChildren)) + array.append(createNode(kind: .typeList, children: typeListChildren)) if pop(kind: .emptyList) != nil { break @@ -1134,7 +1141,7 @@ extension Demangler { private mutating func demangleBoundGenericArgs(nominal: Node, array: [Node], index: Int) throws(DemanglingError) -> Node { if nominal.kind == .typeSymbolicReference || nominal.kind == .protocolSymbolicReference { let remaining = array.reversed().flatMap { $0.children } - return Node.create(kind: .boundGenericOtherNominalType, children: [Node.create(kind: .type, child: nominal), Node.create(kind: .typeList, children: remaining)]) + return createNode(kind: .boundGenericOtherNominalType, children: [createNode(kind: .type, child: nominal), createNode(kind: .typeList, children: remaining)]) } let context = try require(nominal.children.first) @@ -1168,11 +1175,11 @@ extension Demangler { if let thirdChild = context.children.at(2) { extensionChildren.append(thirdChild) } - boundParent = Node.create(kind: .extension, children: extensionChildren) + boundParent = createNode(kind: .extension, children: extensionChildren) } else { boundParent = try demangleBoundGenericArgs(nominal: context, array: array, index: offsetIndex) } - n = Node.create(kind: nominal.kind, children: [boundParent] + nominal.children.dropFirst()) + n = createNode(kind: nominal.kind, children: [boundParent] + nominal.children.dropFirst()) } else { n = nominal } @@ -1191,12 +1198,12 @@ extension Demangler { case .typeAlias: kind = .boundGenericTypeAlias case .function, .constructor: - return Node.create(kind: .boundGenericFunction, children: [n, args]) + return createNode(kind: .boundGenericFunction, children: [n, args]) default: throw failure } - return Node.create(kind: kind, children: [Node.create(kind: .type, child: n), args]) + return createNode(kind: kind, children: [createNode(kind: .type, child: n), args]) } private mutating func demangleImplParamConvention(kind: Node.Kind) throws(DemanglingError) -> Node? { @@ -1219,7 +1226,7 @@ extension Demangler { try scanner.backtrack() return nil } - return Node.create(kind: kind, child: Node.create(kind: .implConvention, contents: .text(attr))) + return createNode(kind: kind, child: createNode(kind: .implConvention, contents: .text(attr))) } private mutating func demangleImplResultConvention(kind: Node.Kind) throws(DemanglingError) -> Node? { @@ -1238,34 +1245,34 @@ extension Demangler { try scanner.backtrack() return nil } - return Node.create(kind: kind, child: Node.create(kind: .implConvention, contents: .text(attr))) + return createNode(kind: kind, child: createNode(kind: .implConvention, contents: .text(attr))) } private mutating func demangleImplParameterSending() -> Node? { guard scanner.conditional(scalar: "T") else { return nil } - return Node.create(kind: .implParameterSending, contents: .text("sending")) + return createNode(kind: .implParameterSending, contents: .text("sending")) } private mutating func demangleImplParameterIsolated() -> Node? { guard scanner.conditional(scalar: "I") else { return nil } - return Node.create(kind: .implParameterIsolated, contents: .text("isolated")) + return createNode(kind: .implParameterIsolated, contents: .text("isolated")) } private mutating func demangleImplParameterImplicitLeading() -> Node? { guard scanner.conditional(scalar: "L") else { return nil } - return Node.create(kind: .implParameterImplicitLeading, contents: .text("sil_implicit_leading_param")) + return createNode(kind: .implParameterImplicitLeading, contents: .text("sil_implicit_leading_param")) } private mutating func demangleImplResultDifferentiability() -> Node { - return Node.create(kind: .implParameterResultDifferentiability, contents: .text(scanner.conditional(scalar: "w") ? "@noDerivative" : "")) + return createNode(kind: .implParameterResultDifferentiability, contents: .text(scanner.conditional(scalar: "w") ? "@noDerivative" : "")) } private mutating func demangleClangType() throws(DemanglingError) -> Node { let numChars = try require(demangleNatural()) let text = try scanner.readScalars(count: Int(numChars)) - return Node.create(kind: .clangType, contents: .text(text)) + return createNode(kind: .clangType, contents: .text(text)) } private mutating func demangleImplFunctionType() throws(DemanglingError) -> Node { @@ -1277,7 +1284,7 @@ extension Demangler { if let conformances { patternChildren.append(conformances) } - typeChildren.append(Node.create(kind: .implPatternSubstitutions, children: patternChildren)) + typeChildren.append(createNode(kind: .implPatternSubstitutions, children: patternChildren)) } if scanner.conditional(scalar: "I") { @@ -1286,7 +1293,7 @@ extension Demangler { if let conformances { invocationChildren.append(conformances) } - typeChildren.append(Node.create(kind: .implInvocationSubstitutions, children: invocationChildren)) + typeChildren.append(createNode(kind: .implInvocationSubstitutions, children: invocationChildren)) } var genSig = pop(kind: .dependentGenericSignature) @@ -1304,7 +1311,7 @@ extension Demangler { if let peek = scanner.peek(), let differentiability = Differentiability(rawValue: peek) { try scanner.skip() - typeChildren.append(Node.create(kind: .implDifferentiabilityKind, contents: .index(UInt64(differentiability.rawValue)))) + typeChildren.append(createNode(kind: .implDifferentiabilityKind, contents: .index(UInt64(differentiability.rawValue)))) } let cAttr: String @@ -1315,7 +1322,7 @@ extension Demangler { case "t": cAttr = "@convention(thin)" default: throw failure } - typeChildren.append(Node.create(kind: .implConvention, contents: .text(cAttr))) + typeChildren.append(createNode(kind: .implConvention, contents: .text(cAttr))) let fConv: String? var hasClangType = false @@ -1344,27 +1351,27 @@ extension Demangler { fConv = nil } if let fConv { - var convChildren = [Node.create(kind: .implFunctionConventionName, contents: .text(fConv))] + var convChildren = [createNode(kind: .implFunctionConventionName, contents: .text(fConv))] if hasClangType { try convChildren.append(demangleClangType()) } - typeChildren.append(Node.create(kind: .implFunctionConvention, children: convChildren)) + typeChildren.append(createNode(kind: .implFunctionConvention, children: convChildren)) } if scanner.conditional(scalar: "A") { - typeChildren.append(Node.create(kind: .implCoroutineKind, contents: .text("yield_once"))) + typeChildren.append(createNode(kind: .implCoroutineKind, contents: .text("yield_once"))) } else if scanner.conditional(scalar: "I") { - typeChildren.append(Node.create(kind: .implCoroutineKind, contents: .text("yield_once_2"))) + typeChildren.append(createNode(kind: .implCoroutineKind, contents: .text("yield_once_2"))) } else if scanner.conditional(scalar: "G") { - typeChildren.append(Node.create(kind: .implCoroutineKind, contents: .text("yield_many"))) + typeChildren.append(createNode(kind: .implCoroutineKind, contents: .text("yield_many"))) } if scanner.conditional(scalar: "h") { - typeChildren.append(Node.create(kind: .implFunctionAttribute, contents: .text("@Sendable"))) + typeChildren.append(createNode(kind: .implFunctionAttribute, contents: .text("@Sendable"))) } if scanner.conditional(scalar: "H") { - typeChildren.append(Node.create(kind: .implFunctionAttribute, contents: .text("@async"))) + typeChildren.append(createNode(kind: .implFunctionAttribute, contents: .text("@async"))) } if scanner.conditional(scalar: "T") { @@ -1410,73 +1417,73 @@ extension Demangler { typeChildren[idx] = try typeChildren[idx].addingChild(require(pop(kind: .type))) } - return Node.create(typeWithChildKind: .implFunctionType, childChildren: typeChildren) + return createNode(typeWithChildKind: .implFunctionType, childChildren: typeChildren) } private mutating func demangleMetatype() throws(DemanglingError) -> Node { switch try scanner.readScalar() { - case "a": return try Node.create(kind: .typeMetadataAccessFunction, child: require(pop(kind: .type))) - case "A": return try Node.create(kind: .reflectionMetadataAssocTypeDescriptor, child: popProtocolConformance()) - case "b": return try Node.create(kind: .canonicalSpecializedGenericTypeMetadataAccessFunction, child: require(pop(kind: .type))) - case "B": return try Node.create(kind: .reflectionMetadataBuiltinDescriptor, child: require(pop(kind: .type))) - case "c": return try Node.create(kind: .protocolConformanceDescriptor, child: require(popProtocolConformance())) + case "a": return try createNode(kind: .typeMetadataAccessFunction, child: require(pop(kind: .type))) + case "A": return try createNode(kind: .reflectionMetadataAssocTypeDescriptor, child: popProtocolConformance()) + case "b": return try createNode(kind: .canonicalSpecializedGenericTypeMetadataAccessFunction, child: require(pop(kind: .type))) + case "B": return try createNode(kind: .reflectionMetadataBuiltinDescriptor, child: require(pop(kind: .type))) + case "c": return try createNode(kind: .protocolConformanceDescriptor, child: require(popProtocolConformance())) case "C": let t = try require(pop(kind: .type)) try require(t.children.first?.kind.isAnyGeneric == true) - return try Node.create(kind: .reflectionMetadataSuperclassDescriptor, child: require(t.children.first)) - case "D": return try Node.create(kind: .typeMetadataDemanglingCache, child: require(pop(kind: .type))) + return try createNode(kind: .reflectionMetadataSuperclassDescriptor, child: require(t.children.first)) + case "D": return try createNode(kind: .typeMetadataDemanglingCache, child: require(pop(kind: .type))) // Apple's closed toolchain demangler accepts these metatype forms that the open-source // lib/Demangling source omits: lowercase 'd' (same node as 'D') and 'R' // (TypeMetadataMangledNameRef). Real Apple symbols depend on them — see docs/AlignmentGaps.md. - case "d": return try Node.create(kind: .typeMetadataDemanglingCache, child: require(pop(kind: .type))) - case "R": return try Node.create(kind: .typeMetadataMangledNameRef, child: require(pop(kind: .type))) - case "f": return try Node.create(kind: .fullTypeMetadata, child: require(pop(kind: .type))) - case "F": return try Node.create(kind: .reflectionMetadataFieldDescriptor, child: require(pop(kind: .type))) - case "g": return try Node.create(kind: .opaqueTypeDescriptorAccessor, child: require(pop())) - case "h": return try Node.create(kind: .opaqueTypeDescriptorAccessorImpl, child: require(pop())) - case "i": return try Node.create(kind: .typeMetadataInstantiationFunction, child: require(pop(kind: .type))) - case "I": return try Node.create(kind: .typeMetadataInstantiationCache, child: require(pop(kind: .type))) - case "j": return try Node.create(kind: .opaqueTypeDescriptorAccessorKey, child: require(pop())) - case "J": return try Node.create(kind: .noncanonicalSpecializedGenericTypeMetadataCache, child: require(pop())) - case "k": return try Node.create(kind: .opaqueTypeDescriptorAccessorVar, child: require(pop())) - case "K": return try Node.create(kind: .metadataInstantiationCache, child: require(pop())) - case "l": return try Node.create(kind: .typeMetadataSingletonInitializationCache, child: require(pop(kind: .type))) - case "L": return try Node.create(kind: .typeMetadataLazyCache, child: require(pop(kind: .type))) - case "m": return try Node.create(kind: .metaclass, child: require(pop(kind: .type))) - case "M": return try Node.create(kind: .canonicalSpecializedGenericMetaclass, child: require(pop(kind: .type))) - case "n": return try Node.create(kind: .nominalTypeDescriptor, child: require(pop(kind: .type))) - case "N": return try Node.create(kind: .noncanonicalSpecializedGenericTypeMetadata, child: require(pop(kind: .type))) - case "o": return try Node.create(kind: .classMetadataBaseOffset, child: require(pop(kind: .type))) - case "p": return try Node.create(kind: .protocolDescriptor, child: popProtocol()) - case "P": return try Node.create(kind: .genericTypeMetadataPattern, child: require(pop(kind: .type))) - case "q": return try Node.create(kind: .uniquable, child: require(pop())) - case "Q": return try Node.create(kind: .opaqueTypeDescriptor, child: require(pop())) - case "r": return try Node.create(kind: .typeMetadataCompletionFunction, child: require(pop(kind: .type))) - case "s": return try Node.create(kind: .objCResilientClassStub, child: require(pop(kind: .type))) - case "S": return try Node.create(kind: .protocolSelfConformanceDescriptor, child: popProtocol()) - case "t": return try Node.create(kind: .fullObjCResilientClassStub, child: require(pop(kind: .type))) - case "u": return try Node.create(kind: .methodLookupFunction, child: require(pop(kind: .type))) - case "U": return try Node.create(kind: .objCMetadataUpdateFunction, child: require(pop(kind: .type))) - case "V": return try Node.create(kind: .propertyDescriptor, child: require(pop { $0.isEntity })) + case "d": return try createNode(kind: .typeMetadataDemanglingCache, child: require(pop(kind: .type))) + case "R": return try createNode(kind: .typeMetadataMangledNameRef, child: require(pop(kind: .type))) + case "f": return try createNode(kind: .fullTypeMetadata, child: require(pop(kind: .type))) + case "F": return try createNode(kind: .reflectionMetadataFieldDescriptor, child: require(pop(kind: .type))) + case "g": return try createNode(kind: .opaqueTypeDescriptorAccessor, child: require(pop())) + case "h": return try createNode(kind: .opaqueTypeDescriptorAccessorImpl, child: require(pop())) + case "i": return try createNode(kind: .typeMetadataInstantiationFunction, child: require(pop(kind: .type))) + case "I": return try createNode(kind: .typeMetadataInstantiationCache, child: require(pop(kind: .type))) + case "j": return try createNode(kind: .opaqueTypeDescriptorAccessorKey, child: require(pop())) + case "J": return try createNode(kind: .noncanonicalSpecializedGenericTypeMetadataCache, child: require(pop())) + case "k": return try createNode(kind: .opaqueTypeDescriptorAccessorVar, child: require(pop())) + case "K": return try createNode(kind: .metadataInstantiationCache, child: require(pop())) + case "l": return try createNode(kind: .typeMetadataSingletonInitializationCache, child: require(pop(kind: .type))) + case "L": return try createNode(kind: .typeMetadataLazyCache, child: require(pop(kind: .type))) + case "m": return try createNode(kind: .metaclass, child: require(pop(kind: .type))) + case "M": return try createNode(kind: .canonicalSpecializedGenericMetaclass, child: require(pop(kind: .type))) + case "n": return try createNode(kind: .nominalTypeDescriptor, child: require(pop(kind: .type))) + case "N": return try createNode(kind: .noncanonicalSpecializedGenericTypeMetadata, child: require(pop(kind: .type))) + case "o": return try createNode(kind: .classMetadataBaseOffset, child: require(pop(kind: .type))) + case "p": return try createNode(kind: .protocolDescriptor, child: popProtocol()) + case "P": return try createNode(kind: .genericTypeMetadataPattern, child: require(pop(kind: .type))) + case "q": return try createNode(kind: .uniquable, child: require(pop())) + case "Q": return try createNode(kind: .opaqueTypeDescriptor, child: require(pop())) + case "r": return try createNode(kind: .typeMetadataCompletionFunction, child: require(pop(kind: .type))) + case "s": return try createNode(kind: .objCResilientClassStub, child: require(pop(kind: .type))) + case "S": return try createNode(kind: .protocolSelfConformanceDescriptor, child: popProtocol()) + case "t": return try createNode(kind: .fullObjCResilientClassStub, child: require(pop(kind: .type))) + case "u": return try createNode(kind: .methodLookupFunction, child: require(pop(kind: .type))) + case "U": return try createNode(kind: .objCMetadataUpdateFunction, child: require(pop(kind: .type))) + case "V": return try createNode(kind: .propertyDescriptor, child: require(pop { $0.isEntity })) case "X": return try demanglePrivateContextDescriptor() - case "z": return try Node.create(kind: .canonicalPrespecializedGenericTypeCachingOnceToken, child: require(pop(kind: .type))) + case "z": return try createNode(kind: .canonicalPrespecializedGenericTypeCachingOnceToken, child: require(pop(kind: .type))) default: throw failure } } private mutating func demanglePrivateContextDescriptor() throws(DemanglingError) -> Node { switch try scanner.readScalar() { - case "E": return try Node.create(kind: .extensionDescriptor, child: popContext()) - case "M": return try Node.create(kind: .moduleDescriptor, child: require(popModule())) + case "E": return try createNode(kind: .extensionDescriptor, child: popContext()) + case "M": return try createNode(kind: .moduleDescriptor, child: require(popModule())) case "Y": let discriminator = try require(pop()) let context = try popContext() - return Node.create(kind: .anonymousDescriptor, children: [context, discriminator]) - case "X": return try Node.create(kind: .anonymousDescriptor, child: popContext()) + return createNode(kind: .anonymousDescriptor, children: [context, discriminator]) + case "X": return try createNode(kind: .anonymousDescriptor, child: popContext()) case "A": let path = try require(popAssociatedTypePath()) let base = try require(pop(kind: .type)) - return Node.create(kind: .associatedTypeGenericParamRef, children: [base, path]) + return createNode(kind: .associatedTypeGenericParamRef, children: [base, path]) default: throw failure } } @@ -1486,32 +1493,32 @@ extension Demangler { case "a": let ident = try require(pop(kind: .identifier)) let arch = try popTypeAndGetChild() - let assoc = Node.create(typeWithChildKind: .associatedTypeRef, childChildren: [arch, ident]) + let assoc = createNode(typeWithChildKind: .associatedTypeRef, childChildren: [arch, ident]) substitutions.append(assoc) return assoc case "O": - return try Node.create(kind: .opaqueReturnTypeOf, child: popContext()) + return try createNode(kind: .opaqueReturnTypeOf, child: popContext()) case "o": let index = try demangleIndex() let (boundGenericArgs, retroactiveConformances) = try demangleBoundGenerics() let name = try require(pop()) var opaqueChildren: [Node] = [ name, - Node.create(kind: .index, contents: .index(index)), - Node.create(kind: .typeList, children: boundGenericArgs.reversed()), + createNode(kind: .index, contents: .index(index)), + createNode(kind: .typeList, children: boundGenericArgs.reversed()), ] if let retroactiveConformances { opaqueChildren.append(retroactiveConformances) } - let opaque = Node.create(kind: .opaqueType, children: opaqueChildren) - let opaqueType = Node.create(kind: .type, child: opaque) + let opaque = createNode(kind: .opaqueType, children: opaqueChildren) + let opaqueType = createNode(kind: .type, child: opaque) substitutions.append(opaqueType) return opaqueType case "r": - return Node.create(typeWithChildKind: .opaqueReturnType, childChildren: []) + return createNode(typeWithChildKind: .opaqueReturnType, childChildren: []) case "R": let index = try demangleIndex() - return Node.create(typeWithChildKind: .opaqueReturnType, childChildren: [Node.create(kind: .opaqueReturnTypeIndex, index: index)]) + return createNode(typeWithChildKind: .opaqueReturnType, childChildren: [createNode(kind: .opaqueReturnTypeIndex, index: index)]) case "x": let t = try demangleAssociatedTypeSimple(index: nil) substitutions.append(t) @@ -1539,11 +1546,11 @@ extension Demangler { case "p": let count = try popTypeAndGetChild() let pattern = try popTypeAndGetChild() - return Node.create(kind: .type, child: Node.create(kind: .packExpansion, children: [pattern, count])) + return createNode(kind: .type, child: createNode(kind: .packExpansion, children: [pattern, count])) case "e": let pack = try popTypeAndGetChild() let level = try demangleIndex() - return Node.create(kind: .type, child: Node.create(kind: .packElement, children: [pack, Node.create(kind: .packElementLevel, contents: .index(level))])) + return createNode(kind: .type, child: createNode(kind: .packElement, children: [pack, createNode(kind: .packElementLevel, contents: .index(level))])) case "P": return try popPack() case "S": @@ -1555,8 +1562,8 @@ extension Demangler { private mutating func demangleAssociatedTypeSimple(index: Node?) throws(DemanglingError) -> Node { do { let atName = try popAssociatedTypeName() - let gpi = try index.map { Node.create(kind: .type, child: $0) } ?? require(pop(kind: .type)) - return Node.create(typeWithChildKind: .dependentMemberType, childChildren: [gpi, atName]) + let gpi = try index.map { createNode(kind: .type, child: $0) } ?? require(pop(kind: .type)) + return createNode(typeWithChildKind: .dependentMemberType, childChildren: [gpi, atName]) } catch { throw error as! DemanglingError } @@ -1571,10 +1578,10 @@ extension Demangler { try assocTypeNames.append(popAssociatedTypeName()) } while !firstElem - var base = try index.map { Node.create(kind: .type, child: $0) } ?? require(pop(kind: .type)) + var base = try index.map { createNode(kind: .type, child: $0) } ?? require(pop(kind: .type)) while let assocType = assocTypeNames.popLast() { - let depTy = Node.create(kind: .dependentMemberType, children: [base, assocType]) - base = Node.create(kind: .type, child: depTy) + let depTy = createNode(kind: .dependentMemberType, children: [base, assocType]) + base = createNode(kind: .type, child: depTy) } return base } catch { @@ -1603,13 +1610,13 @@ extension Demangler { switch c { case "T": switch try scanner.readScalar() { - case "I": return try Node.create(kind: .silThunkIdentity, child: require(pop(where: { $0.isEntity }))) + case "I": return try createNode(kind: .silThunkIdentity, child: require(pop(where: { $0.isEntity }))) case "H": throw failure default: throw failure } - case "c": return try Node.create(kind: .curryThunk, child: require(pop(where: { $0.isEntity }))) - case "j": return try Node.create(kind: .dispatchThunk, child: require(pop(where: { $0.isEntity }))) - case "q": return try Node.create(kind: .methodDescriptor, child: require(pop(where: { $0.isEntity }))) + case "c": return try createNode(kind: .curryThunk, child: require(pop(where: { $0.isEntity }))) + case "j": return try createNode(kind: .dispatchThunk, child: require(pop(where: { $0.isEntity }))) + case "q": return try createNode(kind: .methodDescriptor, child: require(pop(where: { $0.isEntity }))) case "o": return NodeFactory.objCAttribute case "O": return NodeFactory.nonObjCAttribute case "D": return NodeFactory.dynamicAttribute @@ -1622,9 +1629,9 @@ extension Demangler { case "X": return NodeFactory.dynamicallyReplaceableFunctionVar case "x": return NodeFactory.dynamicallyReplaceableFunctionKey case "I": return NodeFactory.dynamicallyReplaceableFunctionImpl - case "Y": return try Node.create(kind: .asyncSuspendResumePartialFunction, child: demangleIndexAsName()) - case "Q": return try Node.create(kind: .asyncAwaitResumePartialFunction, child: demangleIndexAsName()) - case "C": return try Node.create(kind: .coroutineContinuationPrototype, child: require(pop(kind: .type))) + case "Y": return try createNode(kind: .asyncSuspendResumePartialFunction, child: demangleIndexAsName()) + case "Q": return try createNode(kind: .asyncAwaitResumePartialFunction, child: demangleIndexAsName()) + case "C": return try createNode(kind: .coroutineContinuationPrototype, child: require(pop(kind: .type))) case "z": fallthrough case "Z": let flagMode = try demangleIndexAsName() @@ -1635,17 +1642,17 @@ extension Demangler { if let sig { children.append(sig) } - return Node.create(kind: c == "z" ? .objCAsyncCompletionHandlerImpl : .checkedObjCAsyncCompletionHandlerImpl, children: children) + return createNode(kind: c == "z" ? .objCAsyncCompletionHandlerImpl : .checkedObjCAsyncCompletionHandlerImpl, children: children) case "V": let base = try require(pop(where: { $0.isEntity })) let derived = try require(pop(where: { $0.isEntity })) - return Node.create(kind: .vTableThunk, children: [derived, base]) + return createNode(kind: .vTableThunk, children: [derived, base]) case "W": let entity = try require(pop(where: { $0.isEntity })) let conf = try popProtocolConformance() - return Node.create(kind: .protocolWitness, children: [conf, entity]) + return createNode(kind: .protocolWitness, children: [conf, entity]) case "S": - return try Node.create(kind: .protocolSelfConformanceWitness, child: require(pop(where: { $0.isEntity }))) + return try createNode(kind: .protocolSelfConformanceWitness, child: require(pop(where: { $0.isEntity }))) case "R", "r", "y": @@ -1663,7 +1670,7 @@ extension Demangler { } try children.append(require(pop(kind: .type))) try children.append(require(pop(kind: .type))) - return Node.create(kind: kind, children: children) + return createNode(kind: kind, children: children) case "g": return try demangleGenericSpecialization(kind: .genericSpecialization) case "G": return try demangleGenericSpecialization(kind: .genericSpecializationNotReAbstracted) case "B": return try demangleGenericSpecialization(kind: .genericSpecializationInResilienceDomain) @@ -1673,7 +1680,7 @@ extension Demangler { case "P", "p": let spec = try demangleSpecAttributes(kind: c == "P" ? .genericPartialSpecializationNotReAbstracted : .genericPartialSpecialization) - let param = try Node.create(kind: .genericSpecializationParam, child: require(pop(kind: .type))) + let param = try createNode(kind: .genericSpecializationParam, child: require(pop(kind: .type))) return spec.addingChild(param) case "f": return try demangleFunctionSpecialization() case "K", @@ -1712,24 +1719,24 @@ extension Demangler { if isSerialized { resultChildren.append(NodeFactory.isSerialized) } - return Node.create(kind: nodeKind, children: resultChildren) - case "l": return try Node.create(kind: .associatedTypeDescriptor, child: require(popAssociatedTypeName())) - case "L": return try Node.create(kind: .protocolRequirementsBaseDescriptor, child: require(popProtocol())) - case "M": return try Node.create(kind: .defaultAssociatedTypeMetadataAccessor, child: require(popAssociatedTypeName())) + return createNode(kind: nodeKind, children: resultChildren) + case "l": return try createNode(kind: .associatedTypeDescriptor, child: require(popAssociatedTypeName())) + case "L": return try createNode(kind: .protocolRequirementsBaseDescriptor, child: require(popProtocol())) + case "M": return try createNode(kind: .defaultAssociatedTypeMetadataAccessor, child: require(popAssociatedTypeName())) case "n": let requirement = try popProtocol() let associatedTypePath = try popAssociatedTypePath() let protocolType = try require(pop(kind: .type)) - return Node.create(kind: .associatedConformanceDescriptor, children: [protocolType, associatedTypePath, requirement]) + return createNode(kind: .associatedConformanceDescriptor, children: [protocolType, associatedTypePath, requirement]) case "N": let requirement = try popProtocol() let associatedTypePath = try popAssociatedTypePath() let protocolType = try require(pop(kind: .type)) - return Node.create(kind: .defaultAssociatedConformanceAccessor, children: [protocolType, associatedTypePath, requirement]) + return createNode(kind: .defaultAssociatedConformanceAccessor, children: [protocolType, associatedTypePath, requirement]) case "b": let requirement = try popProtocol() let protocolType = try require(pop(kind: .type)) - return Node.create(kind: .baseConformanceDescriptor, children: [protocolType, requirement]) + return createNode(kind: .baseConformanceDescriptor, children: [protocolType, requirement]) case "H", "h": let nodeKind: Node.Kind = c == "H" ? .keyPathEqualsThunkHelper : .keyPathHashThunkHelper @@ -1755,20 +1762,20 @@ extension Demangler { if isSerialized { resultChildren.append(NodeFactory.isSerialized) } - return Node.create(kind: nodeKind, children: resultChildren) + return createNode(kind: nodeKind, children: resultChildren) case "v": let index = try demangleIndex() if scanner.conditional(scalar: "r") { - return Node.create(kind: .outlinedReadOnlyObject, contents: .index(index)) + return createNode(kind: .outlinedReadOnlyObject, contents: .index(index)) } else { - return Node.create(kind: .outlinedVariable, contents: .index(index)) + return createNode(kind: .outlinedVariable, contents: .index(index)) } - case "e": return try Node.create(kind: .outlinedBridgedMethod, contents: .text(demangleBridgedMethodParams())) + case "e": return try createNode(kind: .outlinedBridgedMethod, contents: .text(demangleBridgedMethodParams())) case "u": return NodeFactory.asyncFunctionPointer case "U": let globalActor = try require(pop(kind: .type)) let reabstraction = try require(pop()) - return Node.create(kind: .reabstractionThunkHelperWithGlobalActor, children: [reabstraction, globalActor]) + return createNode(kind: .reabstractionThunkHelperWithGlobalActor, children: [reabstraction, globalActor]) case "J": switch try scanner.readScalar() { case "S": return try demangleAutoDiffSubsetParametersThunk() @@ -1803,7 +1810,7 @@ extension Demangler { try scanner.match(scalar: "p") try children.append(require(demangleIndexSubset())) try scanner.match(scalar: "r") - return Node.create(kind: kind, children: children) + return createNode(kind: kind, children: children) } private mutating func demangleAutoDiffFunctionKind() throws(DemanglingError) -> Node { @@ -1811,7 +1818,7 @@ extension Demangler { guard let autoDiffFunctionKind = AutoDiffFunctionKind(UInt64(kind.value)) else { throw failure } - return Node.create(kind: .autoDiffFunctionKind, contents: .index(UInt64(autoDiffFunctionKind.rawValue.value))) + return createNode(kind: .autoDiffFunctionKind, contents: .index(UInt64(autoDiffFunctionKind.rawValue.value))) } private mutating func demangleAutoDiffSubsetParametersThunk() throws(DemanglingError) -> Node { @@ -1828,7 +1835,7 @@ extension Demangler { try scanner.match(scalar: "r") try children.append(require(demangleIndexSubset())) try scanner.match(scalar: "P") - return Node.create(kind: .autoDiffSubsetParametersThunk, children: children) + return createNode(kind: .autoDiffSubsetParametersThunk, children: children) } private mutating func demangleAutoDiffSelfReorderingReabstractionThunk() throws(DemanglingError) -> Node { @@ -1840,7 +1847,7 @@ extension Demangler { try children.append(require(pop(kind: .type))) children.reverse() try children.append(demangleAutoDiffFunctionKind()) - return Node.create(kind: .autoDiffSelfReorderingReabstractionThunk, children: children) + return createNode(kind: .autoDiffSelfReorderingReabstractionThunk, children: children) } private mutating func demangleDifferentiabilityWitness() throws(DemanglingError) -> Node { @@ -1857,7 +1864,7 @@ extension Demangler { case "l": .linear default: throw failure } - children.append(Node.create(kind: .index, contents: .index(UInt64(kind.rawValue.value)))) + children.append(createNode(kind: .index, contents: .index(UInt64(kind.rawValue.value)))) try children.append(require(demangleIndexSubset())) try scanner.match(scalar: "p") try children.append(require(demangleIndexSubset())) @@ -1865,7 +1872,7 @@ extension Demangler { if let optionalGenSig { children.append(optionalGenSig) } - return Node.create(kind: .differentiabilityWitness, children: children) + return createNode(kind: .differentiabilityWitness, children: children) } private mutating func demangleIndexSubset() throws(DemanglingError) -> Node { @@ -1874,7 +1881,7 @@ extension Demangler { str.unicodeScalars.append(c) } try require(!str.isEmpty) - return Node.create(kind: .indexSubset, contents: .text(str)) + return createNode(kind: .indexSubset, contents: .text(str)) } private mutating func demangleDifferentiableFunctionType() throws(DemanglingError) -> Node { @@ -1885,7 +1892,7 @@ extension Demangler { case "l": .linear default: throw failure } - return Node.create(kind: .differentiableFunctionType, contents: .index(UInt64(kind.rawValue.value))) + return createNode(kind: .differentiableFunctionType, contents: .index(UInt64(kind.rawValue.value))) } private mutating func demangleBridgedMethodParams() throws(DemanglingError) -> String { @@ -1915,7 +1922,7 @@ extension Demangler { spec = spec.addingChildren(Array(droppedArguments.children)) } let list = try popTypeList() - let paramChildren = list.children.map { Node.create(kind: .genericSpecializationParam, child: $0) } + let paramChildren = list.children.map { createNode(kind: .genericSpecializationParam, child: $0) } return spec.addingChildren(paramChildren) } @@ -1924,9 +1931,9 @@ extension Demangler { var tmpChildren: [Node] = [] while scanner.conditional(scalar: "t") { let n = try demangleNatural().map { Node.Contents.index($0 + 1) } ?? Node.Contents.index(0) - tmpChildren.append(Node.create(kind: .droppedArgument, contents: n)) + tmpChildren.append(createNode(kind: .droppedArgument, contents: n)) } - let tmp = Node.create(kind: .genericSpecialization, children: tmpChildren) + let tmp = createNode(kind: .genericSpecialization, children: tmpChildren) let kind: Node.Kind = switch try scanner.readScalar() { case "g": .genericSpecialization case "G": .genericSpecializationNotReAbstracted @@ -1942,7 +1949,7 @@ extension Demangler { // RepresentationChanged specialization has no params if specBase.children.first?.kind == .representationChanged { - return Node.create(kind: .functionSignatureSpecialization, contents: specBase.contents, children: specChildren) + return createNode(kind: .functionSignatureSpecialization, contents: specBase.contents, children: specChildren) } while !scanner.conditional(scalar: "_") { @@ -1985,7 +1992,7 @@ extension Demangler { param = param.reversingChildren(from: fixedChildrenCount) specChildren[paramIndex] = param } - return Node.create(kind: .functionSignatureSpecialization, contents: specBase.contents, children: specChildren) + return createNode(kind: .functionSignatureSpecialization, contents: specBase.contents, children: specChildren) } private mutating func demangleFuncSpecParam(kind: Node.Kind) throws(DemanglingError) -> Node { @@ -1994,32 +2001,32 @@ extension Demangler { case "n": break case "c": // Consumes an identifier and multiple type parameters. Added later. - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.closureProp.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.closureProp.rawValue))) case "C": // ClosurePropPreviousArg: consumes an index - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.closurePropPreviousArg.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.closurePropPreviousArg.rawValue))) let prevArgIndex = try require(demangleNatural()) - children.append(Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .index(prevArgIndex))) + children.append(createNode(kind: .functionSignatureSpecializationParamPayload, contents: .index(prevArgIndex))) case "p": // Multiple constant prop kinds can appear in sequence constantPropLoop: while true { switch try scanner.readScalar() { case "S": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropStruct.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropStruct.rawValue))) case "f": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFunction.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFunction.rawValue))) case "g": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropGlobal.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropGlobal.rawValue))) case "i": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropInteger.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropInteger.rawValue))) let intStr = scanner.readWhile { $0.isDigit } try require(!intStr.isEmpty) - children.append(Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(intStr))) + children.append(createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(intStr))) case "d": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFloat.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFloat.rawValue))) let floatStr = scanner.readWhile { $0.isDigit } try require(!floatStr.isEmpty) - children.append(Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(floatStr))) + children.append(createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(floatStr))) case "s": let encoding: String switch try scanner.readScalar() { @@ -2028,10 +2035,10 @@ extension Demangler { case "c": encoding = "objc" default: throw failure } - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropString.rawValue))) - children.append(Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(encoding))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropString.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(encoding))) case "k": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropKeyPath.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropKeyPath.rawValue))) default: try scanner.backtrack() break constantPropLoop @@ -2051,7 +2058,7 @@ extension Demangler { if scanner.conditional(scalar: "X") { value |= FunctionSigSpecializationParamKind.sroa.rawValue } - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) case "d": var value = FunctionSigSpecializationParamKind.dead.rawValue if scanner.conditional(scalar: "G") { @@ -2063,37 +2070,37 @@ extension Demangler { if scanner.conditional(scalar: "X") { value |= FunctionSigSpecializationParamKind.sroa.rawValue } - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) case "g": var value = FunctionSigSpecializationParamKind.ownedToGuaranteed.rawValue if scanner.conditional(scalar: "X") { value |= FunctionSigSpecializationParamKind.sroa.rawValue } - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) case "o": var value = FunctionSigSpecializationParamKind.guaranteedToOwned.rawValue if scanner.conditional(scalar: "X") { value |= FunctionSigSpecializationParamKind.sroa.rawValue } - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) case "x": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.sroa.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.sroa.rawValue))) case "i": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.boxToValue.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.boxToValue.rawValue))) case "s": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.boxToStack.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.boxToStack.rawValue))) case "r": - children.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.inOutToOut.rawValue))) + children.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.inOutToOut.rawValue))) default: throw failure } - return Node.create(kind: kind, children: children) + return createNode(kind: kind, children: children) } private mutating func addFuncSpecParamNumber(param: Node, kind: FunctionSigSpecializationParamKind) throws(DemanglingError) -> Node { - let kindChild = Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(kind.rawValue)) + let kindChild = createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(kind.rawValue)) let str = scanner.readWhile { $0.isDigit } try require(!str.isEmpty) - let payloadChild = Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(str)) + let payloadChild = createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(str)) return param.addingChildren([kindChild, payloadChild]) } @@ -2112,17 +2119,17 @@ extension Demangler { children.append(NodeFactory.asyncRemoved) } if representationChanged { - children.append(Node.create(kind: .representationChanged)) + children.append(createNode(kind: .representationChanged)) } - children.append(Node.create(kind: .specializationPassID, contents: .index(UInt64(passId)))) - return Node.create(kind: kind, contents: contents, children: children) + children.append(createNode(kind: .specializationPassID, contents: .index(UInt64(passId)))) + return createNode(kind: kind, contents: contents, children: children) } private mutating func demangleWitness() throws(DemanglingError) -> Node { let c = try scanner.readScalar() switch c { - case "C": return try Node.create(kind: .enumCase, child: require(pop(where: { $0.isEntity }))) - case "V": return try Node.create(kind: .valueWitnessTable, child: require(pop(kind: .type))) + case "C": return try createNode(kind: .enumCase, child: require(pop(where: { $0.isEntity }))) + case "V": return try createNode(kind: .valueWitnessTable, child: require(pop(kind: .type))) case "v": let directness: UInt64 switch try scanner.readScalar() { @@ -2130,63 +2137,63 @@ extension Demangler { case "i": directness = Directness.indirect.rawValue default: throw failure } - return try Node.create(kind: .fieldOffset, children: [Node.create(kind: .directness, contents: .index(directness)), require(pop(where: { $0.isEntity }))]) - case "S": return try Node.create(kind: .protocolSelfConformanceWitnessTable, child: popProtocol()) - case "P": return try Node.create(kind: .protocolWitnessTable, child: popProtocolConformance()) - case "p": return try Node.create(kind: .protocolWitnessTablePattern, child: popProtocolConformance()) - case "G": return try Node.create(kind: .genericProtocolWitnessTable, child: popProtocolConformance()) - case "I": return try Node.create(kind: .genericProtocolWitnessTableInstantiationFunction, child: popProtocolConformance()) - case "r": return try Node.create(kind: .resilientProtocolWitnessTable, child: popProtocolConformance()) + return try createNode(kind: .fieldOffset, children: [createNode(kind: .directness, contents: .index(directness)), require(pop(where: { $0.isEntity }))]) + case "S": return try createNode(kind: .protocolSelfConformanceWitnessTable, child: popProtocol()) + case "P": return try createNode(kind: .protocolWitnessTable, child: popProtocolConformance()) + case "p": return try createNode(kind: .protocolWitnessTablePattern, child: popProtocolConformance()) + case "G": return try createNode(kind: .genericProtocolWitnessTable, child: popProtocolConformance()) + case "I": return try createNode(kind: .genericProtocolWitnessTableInstantiationFunction, child: popProtocolConformance()) + case "r": return try createNode(kind: .resilientProtocolWitnessTable, child: popProtocolConformance()) case "l": let conf = try popProtocolConformance() let type = try require(pop(kind: .type)) - return Node.create(kind: .lazyProtocolWitnessTableAccessor, children: [type, conf]) + return createNode(kind: .lazyProtocolWitnessTableAccessor, children: [type, conf]) case "L": let conf = try popProtocolConformance() let type = try require(pop(kind: .type)) - return Node.create(kind: .lazyProtocolWitnessTableCacheVariable, children: [type, conf]) - case "a": return try Node.create(kind: .protocolWitnessTableAccessor, child: popProtocolConformance()) + return createNode(kind: .lazyProtocolWitnessTableCacheVariable, children: [type, conf]) + case "a": return try createNode(kind: .protocolWitnessTableAccessor, child: popProtocolConformance()) case "t": let name = try require(pop(where: { $0.isDeclName })) let conf = try popProtocolConformance() - return Node.create(kind: .associatedTypeMetadataAccessor, children: [conf, name]) + return createNode(kind: .associatedTypeMetadataAccessor, children: [conf, name]) case "T": let protoType = try require(pop(kind: .type)) let assocTypePath = try popAssocTypePath() - return try Node.create(kind: .associatedTypeWitnessTableAccessor, children: [popProtocolConformance(), assocTypePath, protoType]) + return try createNode(kind: .associatedTypeWitnessTableAccessor, children: [popProtocolConformance(), assocTypePath, protoType]) case "b": let protoTy = try require(pop(kind: .type)) let conf = try popProtocolConformance() - return Node.create(kind: .baseWitnessTableAccessor, children: [conf, protoTy]) + return createNode(kind: .baseWitnessTableAccessor, children: [conf, protoTy]) case "O": let sig = pop(kind: .dependentGenericSignature) let type = try require(pop(kind: .type)) var children: [Node] = sig.map { [type, $0] } ?? [type] switch try scanner.readScalar() { case "B": - return Node.create(kind: .outlinedInitializeWithTakeNoValueWitness, children: children) - case "C": return Node.create(kind: .outlinedInitializeWithCopyNoValueWitness, children: children) - case "D": return Node.create(kind: .outlinedAssignWithTakeNoValueWitness, children: children) - case "F": return Node.create(kind: .outlinedAssignWithCopyNoValueWitness, children: children) - case "H": return Node.create(kind: .outlinedDestroyNoValueWitness, children: children) - case "y": return Node.create(kind: .outlinedCopy, children: children) - case "e": return Node.create(kind: .outlinedConsume, children: children) - case "r": return Node.create(kind: .outlinedRetain, children: children) - case "s": return Node.create(kind: .outlinedRelease, children: children) - case "b": return Node.create(kind: .outlinedInitializeWithTake, children: children) - case "c": return Node.create(kind: .outlinedInitializeWithCopy, children: children) - case "d": return Node.create(kind: .outlinedAssignWithTake, children: children) - case "f": return Node.create(kind: .outlinedAssignWithCopy, children: children) - case "h": return Node.create(kind: .outlinedDestroy, children: children) - case "g": return Node.create(kind: .outlinedEnumGetTag, children: children) + return createNode(kind: .outlinedInitializeWithTakeNoValueWitness, children: children) + case "C": return createNode(kind: .outlinedInitializeWithCopyNoValueWitness, children: children) + case "D": return createNode(kind: .outlinedAssignWithTakeNoValueWitness, children: children) + case "F": return createNode(kind: .outlinedAssignWithCopyNoValueWitness, children: children) + case "H": return createNode(kind: .outlinedDestroyNoValueWitness, children: children) + case "y": return createNode(kind: .outlinedCopy, children: children) + case "e": return createNode(kind: .outlinedConsume, children: children) + case "r": return createNode(kind: .outlinedRetain, children: children) + case "s": return createNode(kind: .outlinedRelease, children: children) + case "b": return createNode(kind: .outlinedInitializeWithTake, children: children) + case "c": return createNode(kind: .outlinedInitializeWithCopy, children: children) + case "d": return createNode(kind: .outlinedAssignWithTake, children: children) + case "f": return createNode(kind: .outlinedAssignWithCopy, children: children) + case "h": return createNode(kind: .outlinedDestroy, children: children) + case "g": return createNode(kind: .outlinedEnumGetTag, children: children) case "i": let enumCaseIndex = try demangleIndexAsName() children.append(enumCaseIndex) - return Node.create(kind: .outlinedEnumTagStore, children: children) + return createNode(kind: .outlinedEnumTagStore, children: children) case "j": let enumCaseIndex = try demangleIndexAsName() children.append(enumCaseIndex) - return Node.create(kind: .outlinedEnumProjectDataForLoad, children: children) + return createNode(kind: .outlinedEnumProjectDataForLoad, children: children) default: throw failure } case "Z", @@ -2197,8 +2204,8 @@ extension Demangler { declChildren.append(identifier) } declChildren.reverse() - let declList = Node.create(kind: .globalVariableOnceDeclList, children: declChildren) - return try Node.create(kind: c == "Z" ? .globalVariableOnceFunction : .globalVariableOnceToken, children: [popContext(), declList]) + let declList = createNode(kind: .globalVariableOnceDeclList, children: declChildren) + return try createNode(kind: c == "Z" ? .globalVariableOnceFunction : .globalVariableOnceToken, children: [popContext(), declList]) case "J": return try demangleDifferentiabilityWitness() default: throw failure @@ -2214,7 +2221,7 @@ extension Demangler { children.append(assocType) } while !firstElem children.reverse() - return Node.create(kind: .assocTypePath, children: children) + return createNode(kind: .assocTypePath, children: children) } private mutating func popAssocTypeName() -> Node? { @@ -2234,7 +2241,7 @@ extension Demangler { if let proto { children.append(proto) } - return Node.create(kind: .dependentAssociatedTypeRef, children: children) + return createNode(kind: .dependentAssociatedTypeRef, children: children) } private mutating func demangleSpecialType() throws(DemanglingError) -> Node { @@ -2257,29 +2264,29 @@ extension Demangler { case "C": return try popFunctionType(kind: .cFunctionPointer, hasClangType: true) default: throw failure } - case "o": return try Node.create(typeWithChildKind: .unowned, childChild: require(pop(kind: .type))) - case "u": return try Node.create(typeWithChildKind: .unmanaged, childChild: require(pop(kind: .type))) - case "w": return try Node.create(typeWithChildKind: .weak, childChild: require(pop(kind: .type))) - case "b": return try Node.create(typeWithChildKind: .silBoxType, childChild: require(pop(kind: .type))) - case "D": return try Node.create(typeWithChildKind: .dynamicSelf, childChild: require(pop(kind: .type))) + case "o": return try createNode(typeWithChildKind: .unowned, childChild: require(pop(kind: .type))) + case "u": return try createNode(typeWithChildKind: .unmanaged, childChild: require(pop(kind: .type))) + case "w": return try createNode(typeWithChildKind: .weak, childChild: require(pop(kind: .type))) + case "b": return try createNode(typeWithChildKind: .silBoxType, childChild: require(pop(kind: .type))) + case "D": return try createNode(typeWithChildKind: .dynamicSelf, childChild: require(pop(kind: .type))) case "M": let mtr = try demangleMetatypeRepresentation() let type = try require(pop(kind: .type)) - return Node.create(typeWithChildKind: .metatype, childChildren: [mtr, type]) + return createNode(typeWithChildKind: .metatype, childChildren: [mtr, type]) case "m": let mtr = try demangleMetatypeRepresentation() let type = try require(pop(kind: .type)) - return Node.create(typeWithChildKind: .existentialMetatype, childChildren: [mtr, type]) + return createNode(typeWithChildKind: .existentialMetatype, childChildren: [mtr, type]) case "P": let reqs = try demangleConstrainedExistentialRequirementList() let base = try require(pop(kind: .type)) - return Node.create(typeWithChildKind: .constrainedExistential, childChildren: [base, reqs]) - case "p": return try Node.create(typeWithChildKind: .existentialMetatype, childChild: require(pop(kind: .type))) + return createNode(typeWithChildKind: .constrainedExistential, childChildren: [base, reqs]) + case "p": return try createNode(typeWithChildKind: .existentialMetatype, childChild: require(pop(kind: .type))) case "c": let superclass = try require(pop(kind: .type)) let protocols = try demangleProtocolList() - return Node.create(typeWithChildKind: .protocolListWithClass, childChildren: [protocols, superclass]) - case "l": return try Node.create(typeWithChildKind: .protocolListWithAnyObject, childChild: demangleProtocolList()) + return createNode(typeWithChildKind: .protocolListWithClass, childChildren: [protocols, superclass]) + case "l": return try createNode(typeWithChildKind: .protocolListWithAnyObject, childChild: demangleProtocolList()) case "X", "x": var signatureGenericArgs: (Node, Node)? = nil @@ -2292,44 +2299,44 @@ extension Demangler { for fieldType in fieldTypes.children { try require(fieldType.kind == .type) if fieldType.children.first?.kind == .inOut { - try layoutChildren.append(Node.create(kind: .silBoxMutableField, child: Node.create(kind: .type, child: require(fieldType.children.first?.children.first)))) + try layoutChildren.append(createNode(kind: .silBoxMutableField, child: createNode(kind: .type, child: require(fieldType.children.first?.children.first)))) } else { - layoutChildren.append(Node.create(kind: .silBoxImmutableField, child: fieldType)) + layoutChildren.append(createNode(kind: .silBoxImmutableField, child: fieldType)) } } - let layout = Node.create(kind: .silBoxLayout, children: layoutChildren) + let layout = createNode(kind: .silBoxLayout, children: layoutChildren) var boxChildren: [Node] = [layout] if let (signature, genericArgs) = signatureGenericArgs { boxChildren.append(signature) boxChildren.append(genericArgs) } - return Node.create(kind: .type, child: Node.create(kind: .silBoxTypeWithLayout, children: boxChildren)) + return createNode(kind: .type, child: createNode(kind: .silBoxTypeWithLayout, children: boxChildren)) case "Y": return try demangleAnyGenericType(kind: .otherNominalType) case "Z": let types = try popTypeList() let name = try require(pop(kind: .identifier)) let parent = try popContext() - return Node.create(kind: .anonymousContext, children: [name, parent, types]) - case "e": return Node.create(kind: .type, child: NodeFactory.errorType) + return createNode(kind: .anonymousContext, children: [name, parent, types]) + case "e": return createNode(kind: .type, child: NodeFactory.errorType) case "S": switch try scanner.readScalar() { case "q": let type = try require(pop(kind: .type)) - return Node.create(kind: .type, child: Node.create(kind: .sugaredOptional, child: type)) + return createNode(kind: .type, child: createNode(kind: .sugaredOptional, child: type)) case "a": let type = try require(pop(kind: .type)) - return Node.create(kind: .type, child: Node.create(kind: .sugaredArray, child: type)) + return createNode(kind: .type, child: createNode(kind: .sugaredArray, child: type)) case "D": let value = try require(pop(kind: .type)) let key = try require(pop(kind: .type)) - return Node.create(kind: .type, child: Node.create(kind: .sugaredDictionary, children: [key, value])) + return createNode(kind: .type, child: createNode(kind: .sugaredDictionary, children: [key, value])) case "p": let type = try require(pop(kind: .type)) - return Node.create(kind: .type, child: Node.create(kind: .sugaredParen, child: type)) + return createNode(kind: .type, child: createNode(kind: .sugaredParen, child: type)) case "A": let element = try require(pop(kind: .type)) let count = try require(pop(kind: .type)) - return Node.create(kind: .type, child: Node.create(kind: .sugaredInlineArray, children: [count, element])) + return createNode(kind: .type, child: createNode(kind: .sugaredInlineArray, children: [count, element])) default: throw failure } default: throw failure @@ -2343,12 +2350,12 @@ extension Demangler { argsChildren.append(type) } argsChildren.reverse() - let args = Node.create(kind: .typeList, children: argsChildren) + let args = createNode(kind: .typeList, children: argsChildren) let shape = try require(pop(where: { $0 == .uniqueExtendedExistentialTypeShapeSymbolicReference || $0 == .nonUniqueExtendedExistentialTypeShapeSymbolicReference })) if let retroactiveConformances { - return Node.create(typeWithChildKind: .symbolicExtendedExistentialType, childChildren: [shape, args, retroactiveConformances]) + return createNode(typeWithChildKind: .symbolicExtendedExistentialType, childChildren: [shape, args, retroactiveConformances]) } else { - return Node.create(typeWithChildKind: .symbolicExtendedExistentialType, childChildren: [shape, args]) + return createNode(typeWithChildKind: .symbolicExtendedExistentialType, childChildren: [shape, args]) } } @@ -2359,9 +2366,9 @@ extension Demangler { genSig = pop(kind: .dependentGenericSignature) } if let genSig { - return Node.create(kind: .extendedExistentialTypeShape, children: [genSig, type]) + return createNode(kind: .extendedExistentialTypeShape, children: [genSig, type]) } else { - return Node.create(kind: .extendedExistentialTypeShape, child: type) + return createNode(kind: .extendedExistentialTypeShape, child: type) } } @@ -2373,7 +2380,7 @@ extension Demangler { case "o": value = "@objc_metatype" default: throw failure } - return Node.create(kind: .metatypeRepresentation, contents: .text(value)) + return createNode(kind: .metatypeRepresentation, contents: .text(value)) } private mutating func demangleAccessor(child: Node) throws(DemanglingError) -> Node { @@ -2411,7 +2418,7 @@ extension Demangler { case "p": return child default: throw failure } - return Node.create(kind: kind, child: child) + return createNode(kind: kind, child: child) } private mutating func demangleFunctionEntity() throws(DemanglingError) -> Node { @@ -2459,7 +2466,7 @@ extension Demangler { children.append(pn) } } - return try Node.create(kind: argsAndKind.kind, children: [popContext()] + children) + return try createNode(kind: argsAndKind.kind, children: [popContext()] + children) } private mutating func demangleEntity(kind: Node.Kind) throws(DemanglingError) -> Node { @@ -2468,9 +2475,9 @@ extension Demangler { let name = try require(pop(where: { $0.isDeclName })) let context = try popContext() let result = if let labelList = labelList { - Node.create(kind: kind, children: [context, name, labelList, type]) + createNode(kind: kind, children: [context, name, labelList, type]) } else { - Node.create(kind: kind, children: [context, name, type]) + createNode(kind: kind, children: [context, name, type]) } let updatedType = setParentForOpaqueReturnTypeNodes(visited: type, parent: result) if updatedType !== type { @@ -2497,7 +2504,7 @@ extension Demangler { if let pn = privateName { children.append(pn) } - var ss = Node.create(kind: .subscript, children: children) + var ss = createNode(kind: .subscript, children: children) let updatedType = setParentForOpaqueReturnTypeNodes(visited: type, parent: ss) if updatedType !== type { let typeIndex = labelList != nil ? 2 : 1 @@ -2516,11 +2523,11 @@ extension Demangler { } while !firstElem } typeChildren.reverse() - return Node.create(kind: .protocolList, child: Node.create(kind: .typeList, children: typeChildren)) + return createNode(kind: .protocolList, child: createNode(kind: .typeList, children: typeChildren)) } private mutating func demangleProtocolListType() throws(DemanglingError) -> Node { - return try Node.create(kind: .type, child: demangleProtocolList()) + return try createNode(kind: .type, child: demangleProtocolList()) } private mutating func demangleConstrainedExistentialRequirementList() throws(DemanglingError) -> Node { @@ -2532,7 +2539,7 @@ extension Demangler { reqChildren.append(req) } while !firstElement reqChildren.reverse() - return Node.create(kind: .constrainedExistentialRequirementList, children: reqChildren) + return createNode(kind: .constrainedExistentialRequirementList, children: reqChildren) } private mutating func demangleGenericSignature(hasParamCounts: Bool) throws(DemanglingError) -> Node { @@ -2543,17 +2550,17 @@ extension Demangler { if !scanner.conditional(scalar: "z") { count = try demangleIndex() + 1 } - paramCounts.append(Node.create(kind: .dependentGenericParamCount, contents: .index(count))) + paramCounts.append(createNode(kind: .dependentGenericParamCount, contents: .index(count))) } } else { - paramCounts.append(Node.create(kind: .dependentGenericParamCount, contents: .index(1))) + paramCounts.append(createNode(kind: .dependentGenericParamCount, contents: .index(1))) } var requirements: [Node] = [] while let req = pop(where: { $0.isRequirement }) { requirements.append(req) } requirements.reverse() - return Node.create(kind: .dependentGenericSignature, children: paramCounts + requirements) + return createNode(kind: .dependentGenericSignature, children: paramCounts + requirements) } private mutating func demangleGenericRequirement() throws(DemanglingError) -> Node { @@ -2591,7 +2598,7 @@ extension Demangler { let constrType: Node switch constraintAndTypeKinds.type { - case .generic: constrType = try Node.create(kind: .type, child: demangleGenericParamIndex()) + case .generic: constrType = try createNode(kind: .type, child: demangleGenericParamIndex()) case .assoc: constrType = try demangleAssociatedTypeSimple(index: demangleGenericParamIndex()) substitutions.append(constrType) @@ -2602,13 +2609,13 @@ extension Demangler { } switch constraintAndTypeKinds.constraint { - case .valueMarker: return try Node.create(kind: .dependentGenericParamValueMarker, children: [constrType, require(pop(kind: .type))]) - case .packMarker: return Node.create(kind: .dependentGenericParamPackMarker, children: [constrType]) - case .protocol: return try Node.create(kind: .dependentGenericConformanceRequirement, children: [constrType, popProtocol()]) - case .inverse: return try Node.create(kind: .dependentGenericInverseConformanceRequirement, children: [constrType, require(inverseKind)]) - case .baseClass: return try Node.create(kind: .dependentGenericConformanceRequirement, children: [constrType, require(pop(kind: .type))]) - case .sameType: return try Node.create(kind: .dependentGenericSameTypeRequirement, children: [constrType, require(pop(kind: .type))]) - case .sameShape: return try Node.create(kind: .dependentGenericSameShapeRequirement, children: [constrType, require(pop(kind: .type))]) + case .valueMarker: return try createNode(kind: .dependentGenericParamValueMarker, children: [constrType, require(pop(kind: .type))]) + case .packMarker: return createNode(kind: .dependentGenericParamPackMarker, children: [constrType]) + case .protocol: return try createNode(kind: .dependentGenericConformanceRequirement, children: [constrType, popProtocol()]) + case .inverse: return try createNode(kind: .dependentGenericInverseConformanceRequirement, children: [constrType, require(inverseKind)]) + case .baseClass: return try createNode(kind: .dependentGenericConformanceRequirement, children: [constrType, require(pop(kind: .type))]) + case .sameType: return try createNode(kind: .dependentGenericSameTypeRequirement, children: [constrType, require(pop(kind: .type))]) + case .sameShape: return try createNode(kind: .dependentGenericSameShapeRequirement, children: [constrType, require(pop(kind: .type))]) case .layout: let c = try scanner.readScalar() var size: Node? = nil @@ -2631,7 +2638,7 @@ extension Demangler { size = try demangleIndexAsName() default: throw failure } - let name = Node.create(kind: .identifier, contents: .text(String(String.UnicodeScalarView([c])))) + let name = createNode(kind: .identifier, contents: .text(String(String.UnicodeScalarView([c])))) var layoutChildren: [Node] = [constrType, name] if let s = size { layoutChildren.append(s) @@ -2639,7 +2646,7 @@ extension Demangler { if let a = alignment { layoutChildren.append(a) } - let layoutRequirement = Node.create(kind: .dependentGenericLayoutRequirement, children: layoutChildren) + let layoutRequirement = createNode(kind: .dependentGenericLayoutRequirement, children: layoutChildren) return layoutRequirement } } @@ -2647,16 +2654,16 @@ extension Demangler { private mutating func demangleGenericType() throws(DemanglingError) -> Node { let genSig = try require(pop(kind: .dependentGenericSignature)) let type = try require(pop(kind: .type)) - return Node.create(typeWithChildKind: .dependentGenericType, childChildren: [genSig, type]) + return createNode(typeWithChildKind: .dependentGenericType, childChildren: [genSig, type]) } private mutating func demangleValueWitness() throws(DemanglingError) -> Node { let code = try scanner.readScalars(count: 2) let kind = try require(ValueWitnessKind(code: code)) // ValueWitness node should have 2 children: Index node and Type node - let indexNode = Node.create(kind: .index, contents: .index(kind.rawValue)) + let indexNode = createNode(kind: .index, contents: .index(kind.rawValue)) let typeNode = try require(pop(kind: .type)) - return Node.create(kind: .valueWitness, children: [indexNode, typeNode]) + return createNode(kind: .valueWitness, children: [indexNode, typeNode]) } } @@ -2680,11 +2687,11 @@ extension Demangler { case "X": let line = try demangleIndex() let col = try demangleIndex() - let lineNode = Node.create(kind: .index, contents: .index(line)) - let colNode = Node.create(kind: .index, contents: .index(col)) + let lineNode = createNode(kind: .index, contents: .index(line)) + let colNode = createNode(kind: .index, contents: .index(col)) let buffer = try require(pop(kind: .identifier)) let module = try require(pop(kind: .identifier)) - return Node.create(kind: .macroExpansionLoc, children: [module, buffer, lineNode, colNode]) + return createNode(kind: .macroExpansionLoc, children: [module, buffer, lineNode, colNode]) default: throw failure } @@ -2703,7 +2710,7 @@ extension Demangler { if let privateDiscriminator { children.append(privateDiscriminator) } - return Node.create(kind: kind, children: children) + return createNode(kind: kind, children: children) } catch { throw error as! DemanglingError } @@ -2711,9 +2718,9 @@ extension Demangler { private mutating func demangleIntegerType() throws(DemanglingError) -> Node { if scanner.conditional(scalar: "n") { - return try Node.create(kind: .type, children: [Node.create(kind: .negativeInteger, contents: .index(demangleIndex()))]) + return try createNode(kind: .type, children: [createNode(kind: .negativeInteger, contents: .index(demangleIndex()))]) } else { - return try Node.create(kind: .type, children: [Node.create(kind: .integer, contents: .index(demangleIndex()))]) + return try createNode(kind: .type, children: [createNode(kind: .integer, contents: .index(demangleIndex()))]) } } @@ -2722,26 +2729,26 @@ extension Demangler { if scanner.conditional(scalar: "C") { let module: Node if scanner.conditional(scalar: "s") { - module = Node.create(kind: .module, contents: .text(stdlibName)) + module = createNode(kind: .module, contents: .text(stdlibName)) } else { module = try demangleIdentifier().changeKind(.module) } - typeChild = try Node.create(kind: .class, children: [module, demangleIdentifier()]) + typeChild = try createNode(kind: .class, children: [module, demangleIdentifier()]) } else if scanner.conditional(scalar: "P") { let module: Node if scanner.conditional(scalar: "s") { - module = Node.create(kind: .module, contents: .text(stdlibName)) + module = createNode(kind: .module, contents: .text(stdlibName)) } else { module = try demangleIdentifier().changeKind(.module) } - typeChild = try Node.create(kind: .protocolList, child: Node.create(kind: .typeList, child: Node.create(kind: .type, child: Node.create(kind: .protocol, children: [module, demangleIdentifier()])))) + typeChild = try createNode(kind: .protocolList, child: createNode(kind: .typeList, child: createNode(kind: .type, child: createNode(kind: .protocol, children: [module, demangleIdentifier()])))) try scanner.match(scalar: "_") } else { throw failure } try require(scanner.isAtEnd) - let type = Node.create(kind: .type, child: typeChild) - return Node.create(kind: .global, child: Node.create(kind: .typeMangling, child: type)) + let type = createNode(kind: .type, child: typeChild) + return createNode(kind: .global, child: createNode(kind: .typeMangling, child: type)) } } @@ -3172,32 +3179,32 @@ extension Demangler { let remainder = scanner.remainder() if !remainder.isEmpty { - children.append(Node.create(kind: .suffix, contents: .text(remainder))) + children.append(createNode(kind: .suffix, contents: .text(remainder))) } - return Node.create(kind: .global, children: children) + return createNode(kind: .global, children: children) } mutating func demangleSwift3Global() throws(DemanglingError) -> Node { let c1 = try scanner.readScalar() let c2 = try scanner.readScalar() switch (c1, c2) { - case ("M", "P"): return try Node.create(kind: .genericTypeMetadataPattern, children: [demangleSwift3Type()]) - case ("M", "a"): return try Node.create(kind: .typeMetadataAccessFunction, children: [demangleSwift3Type()]) - case ("M", "L"): return try Node.create(kind: .typeMetadataLazyCache, children: [demangleSwift3Type()]) - case ("M", "m"): return try Node.create(kind: .metaclass, children: [demangleSwift3Type()]) - case ("M", "n"): return try Node.create(kind: .nominalTypeDescriptor, children: [demangleSwift3Type()]) - case ("M", "f"): return try Node.create(kind: .fullTypeMetadata, children: [demangleSwift3Type()]) - case ("M", "p"): return try Node.create(kind: .protocolDescriptor, children: [demangleSwift3ProtocolName()]) + case ("M", "P"): return try createNode(kind: .genericTypeMetadataPattern, children: [demangleSwift3Type()]) + case ("M", "a"): return try createNode(kind: .typeMetadataAccessFunction, children: [demangleSwift3Type()]) + case ("M", "L"): return try createNode(kind: .typeMetadataLazyCache, children: [demangleSwift3Type()]) + case ("M", "m"): return try createNode(kind: .metaclass, children: [demangleSwift3Type()]) + case ("M", "n"): return try createNode(kind: .nominalTypeDescriptor, children: [demangleSwift3Type()]) + case ("M", "f"): return try createNode(kind: .fullTypeMetadata, children: [demangleSwift3Type()]) + case ("M", "p"): return try createNode(kind: .protocolDescriptor, children: [demangleSwift3ProtocolName()]) case ("M", _): try scanner.backtrack() - return try Node.create(kind: .typeMetadata, children: [demangleSwift3Type()]) + return try createNode(kind: .typeMetadata, children: [demangleSwift3Type()]) case ("P", "A"): - return try Node.create(kind: scanner.conditional(scalar: "o") ? .partialApplyObjCForwarder : .partialApplyForwarder, children: scanner.conditional(string: "__T") ? [demangleSwift3Global()] : []) + return try createNode(kind: scanner.conditional(scalar: "o") ? .partialApplyObjCForwarder : .partialApplyForwarder, children: scanner.conditional(string: "__T") ? [demangleSwift3Global()] : []) case ("P", _): throw scanner.unexpectedError() case ("t", _): try scanner.backtrack() - return try Node.create(kind: .typeMangling, children: [demangleSwift3Type()]) + return try createNode(kind: .typeMangling, children: [demangleSwift3Type()]) case ("w", _): let c3 = try scanner.readScalar() let value: UInt64 @@ -3225,21 +3232,21 @@ extension Demangler { case ("u", "p"): value = ValueWitnessKind.destructiveProjectEnumData.rawValue default: throw scanner.unexpectedError() } - return try Node.create(kind: .valueWitness, children: [Node.create(kind: .index, index: value), demangleSwift3Type()]) - case ("W", "V"): return try Node.create(kind: .valueWitnessTable, children: [demangleSwift3Type()]) - case ("W", "v"): return try Node.create(kind: .fieldOffset, children: [Node.create(kind: .directness, contents: .index(scanner.readScalar() == "d" ? 0 : 1)), demangleSwift3Entity()]) - case ("W", "P"): return try Node.create(kind: .protocolWitnessTable, children: [demangleSwift3ProtocolConformance()]) - case ("W", "G"): return try Node.create(kind: .genericProtocolWitnessTable, children: [demangleSwift3ProtocolConformance()]) - case ("W", "I"): return try Node.create(kind: .genericProtocolWitnessTableInstantiationFunction, children: [demangleSwift3ProtocolConformance()]) - case ("W", "l"): return try Node.create(kind: .lazyProtocolWitnessTableAccessor, children: [demangleSwift3Type(), demangleSwift3ProtocolConformance()]) - case ("W", "L"): return try Node.create(kind: .lazyProtocolWitnessTableCacheVariable, children: [demangleSwift3Type(), demangleSwift3ProtocolConformance()]) - case ("W", "a"): return try Node.create(kind: .protocolWitnessTableAccessor, children: [demangleSwift3ProtocolConformance()]) - case ("W", "t"): return try Node.create(kind: .associatedTypeMetadataAccessor, children: [demangleSwift3ProtocolConformance(), demangleSwift3DeclName()]) - case ("W", "T"): return try Node.create(kind: .associatedTypeWitnessTableAccessor, children: [demangleSwift3ProtocolConformance(), demangleSwift3DeclName(), demangleSwift3ProtocolName()]) + return try createNode(kind: .valueWitness, children: [createNode(kind: .index, index: value), demangleSwift3Type()]) + case ("W", "V"): return try createNode(kind: .valueWitnessTable, children: [demangleSwift3Type()]) + case ("W", "v"): return try createNode(kind: .fieldOffset, children: [createNode(kind: .directness, contents: .index(scanner.readScalar() == "d" ? 0 : 1)), demangleSwift3Entity()]) + case ("W", "P"): return try createNode(kind: .protocolWitnessTable, children: [demangleSwift3ProtocolConformance()]) + case ("W", "G"): return try createNode(kind: .genericProtocolWitnessTable, children: [demangleSwift3ProtocolConformance()]) + case ("W", "I"): return try createNode(kind: .genericProtocolWitnessTableInstantiationFunction, children: [demangleSwift3ProtocolConformance()]) + case ("W", "l"): return try createNode(kind: .lazyProtocolWitnessTableAccessor, children: [demangleSwift3Type(), demangleSwift3ProtocolConformance()]) + case ("W", "L"): return try createNode(kind: .lazyProtocolWitnessTableCacheVariable, children: [demangleSwift3Type(), demangleSwift3ProtocolConformance()]) + case ("W", "a"): return try createNode(kind: .protocolWitnessTableAccessor, children: [demangleSwift3ProtocolConformance()]) + case ("W", "t"): return try createNode(kind: .associatedTypeMetadataAccessor, children: [demangleSwift3ProtocolConformance(), demangleSwift3DeclName()]) + case ("W", "T"): return try createNode(kind: .associatedTypeWitnessTableAccessor, children: [demangleSwift3ProtocolConformance(), demangleSwift3DeclName(), demangleSwift3ProtocolName()]) case ("W", _): throw scanner.unexpectedError() - case ("T", "W"): return try Node.create(kind: .protocolWitness, children: [demangleSwift3ProtocolConformance(), demangleSwift3Entity()]) + case ("T", "W"): return try createNode(kind: .protocolWitness, children: [demangleSwift3ProtocolConformance(), demangleSwift3Entity()]) case ("T", "R"): fallthrough - case ("T", "r"): return try Node.create(kind: c2 == "R" ? Node.Kind.reabstractionThunkHelper : Node.Kind.reabstractionThunk, children: scanner.conditional(scalar: "G") ? [demangleSwift3GenericSignature(), demangleSwift3Type(), demangleSwift3Type()] : [demangleSwift3Type(), demangleSwift3Type()]) + case ("T", "r"): return try createNode(kind: c2 == "R" ? Node.Kind.reabstractionThunkHelper : Node.Kind.reabstractionThunk, children: scanner.conditional(scalar: "G") ? [demangleSwift3GenericSignature(), demangleSwift3Type(), demangleSwift3Type()] : [demangleSwift3Type(), demangleSwift3Type()]) default: try scanner.backtrack(count: 2) return try demangleSwift3Entity() @@ -3252,7 +3259,7 @@ extension Demangler { if scanner.conditional(scalar: "q") { children.append(NodeFactory.isSerialized) } - try children.append(Node.create(kind: .specializationPassID, contents: .index(UInt64(scanner.readScalar().value - 48)))) + try children.append(createNode(kind: .specializationPassID, contents: .index(UInt64(scanner.readScalar().value - 48)))) switch c { case "r": fallthrough case "g": @@ -3262,9 +3269,9 @@ extension Demangler { while !scanner.conditional(scalar: "_") { try parameterChildren.append(demangleSwift3ProtocolConformance()) } - children.append(Node.create(kind: .genericSpecializationParam, children: parameterChildren)) + children.append(createNode(kind: .genericSpecializationParam, children: parameterChildren)) } - return Node.create(kind: c == "r" ? .genericSpecializationNotReAbstracted : .genericSpecialization, children: children) + return createNode(kind: c == "r" ? .genericSpecializationNotReAbstracted : .genericSpecialization, children: children) case "f": var count: UInt64 = 0 while !scanner.conditional(scalar: "_") { @@ -3274,13 +3281,13 @@ extension Demangler { case ("n", "_"): break case ("c", "p"): try paramChildren.append(contentsOf: demangleSwift3FuncSigSpecializationConstantProp()) case ("c", "l"): - paramChildren.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.closureProp.rawValue))) - try paramChildren.append(Node.create(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents)) + paramChildren.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.closureProp.rawValue))) + try paramChildren.append(createNode(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents)) while !scanner.conditional(scalar: "_") { try paramChildren.append(demangleSwift3Type()) } case ("i", "_"): fallthrough - case ("k", "_"): paramChildren.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(c == "i" ? FunctionSigSpecializationParamKind.boxToValue.rawValue : FunctionSigSpecializationParamKind.boxToStack.rawValue))) + case ("k", "_"): paramChildren.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(c == "i" ? FunctionSigSpecializationParamKind.boxToValue.rawValue : FunctionSigSpecializationParamKind.boxToStack.rawValue))) default: try scanner.backtrack(count: 2) var value: UInt64 = 0 @@ -3289,12 +3296,12 @@ extension Demangler { value |= scanner.conditional(scalar: "o") ? FunctionSigSpecializationParamKind.guaranteedToOwned.rawValue : 0 value |= scanner.conditional(scalar: "s") ? FunctionSigSpecializationParamKind.sroa.rawValue : 0 try scanner.match(scalar: "_") - paramChildren.append(Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) + paramChildren.append(createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(value))) } - children.append(Node.create(kind: .functionSignatureSpecializationParam, children: paramChildren)) + children.append(createNode(kind: .functionSignatureSpecializationParam, children: paramChildren)) count += 1 } - return Node.create(kind: .functionSignatureSpecialization, children: children) + return createNode(kind: .functionSignatureSpecialization, children: children) default: throw scanner.unexpectedError() } } @@ -3302,28 +3309,28 @@ extension Demangler { mutating func demangleSwift3FuncSigSpecializationConstantProp() throws(DemanglingError) -> [Node] { switch try (scanner.readScalar(), scanner.readScalar()) { case ("f", "r"): - let name = try Node.create(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents) + let name = try createNode(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents) try scanner.match(scalar: "_") - let kind = Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFunction.rawValue)) + let kind = createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFunction.rawValue)) return [kind, name] case ("g", _): try scanner.backtrack() - let name = try Node.create(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents) + let name = try createNode(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents) try scanner.match(scalar: "_") - let kind = Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropGlobal.rawValue)) + let kind = createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropGlobal.rawValue)) return [kind, name] case ("i", _): try scanner.backtrack() let string = try scanner.readUntil(scalar: "_") try scanner.match(scalar: "_") - let name = Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(string)) - let kind = Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropInteger.rawValue)) + let name = createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(string)) + let kind = createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropInteger.rawValue)) return [kind, name] case ("f", "l"): let string = try scanner.readUntil(scalar: "_") try scanner.match(scalar: "_") - let name = Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(string)) - let kind = Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFloat.rawValue)) + let name = createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(string)) + let kind = createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropFloat.rawValue)) return [kind, name] case ("s", "e"): var string: String @@ -3333,9 +3340,9 @@ extension Demangler { default: throw scanner.unexpectedError() } try scanner.match(scalar: "v") - let name = try Node.create(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents) - let encoding = Node.create(kind: .functionSignatureSpecializationParamPayload, contents: .text(string)) - let kind = Node.create(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropString.rawValue)) + let name = try createNode(kind: .functionSignatureSpecializationParamPayload, contents: demangleSwift3Identifier().contents) + let encoding = createNode(kind: .functionSignatureSpecializationParamPayload, contents: .text(string)) + let kind = createNode(kind: .functionSignatureSpecializationParamKind, contents: .index(FunctionSigSpecializationParamKind.constantPropString.rawValue)) try scanner.match(scalar: "_") return [kind, encoding, name] default: throw scanner.unexpectedError() @@ -3346,7 +3353,7 @@ extension Demangler { let type = try demangleSwift3Type() let prot = try demangleSwift3ProtocolName() let context = try demangleSwift3Context() - return Node.create(kind: .protocolConformance, children: [type, prot, context]) + return createNode(kind: .protocolConformance, children: [type, prot, context]) } mutating func demangleSwift3ProtocolName() throws(DemanglingError) -> Node { @@ -3359,18 +3366,18 @@ extension Demangler { default: throw scanner.unexpectedError() } } else if scanner.conditional(scalar: "s") { - let stdlib = Node.create(kind: .module, contents: .text(stdlibName)) + let stdlib = createNode(kind: .module, contents: .text(stdlibName)) name = try demangleSwift3ProtocolNameGivenContext(context: stdlib) } else { name = try demangleSwift3DeclarationName(kind: .protocol) } - return Node.create(kind: .type, children: [name]) + return createNode(kind: .type, children: [name]) } mutating func demangleSwift3ProtocolNameGivenContext(context: Node) throws(DemanglingError) -> Node { let name = try demangleSwift3DeclName() - let result = Node.create(kind: .protocol, children: [context, name]) + let result = createNode(kind: .protocol, children: [context, name]) nameStack.append(result) return result } @@ -3398,7 +3405,7 @@ extension Demangler { parentOrModule = try demangleSwift3BoundGenericArgs(nominalType: parentOrModule) guard initialNominal.children.count > 1 else { throw scanner.unexpectedError() } - nominalType = Node.create(kind: initialNominal.kind, children: [parentOrModule, initialNominal.children[1]]) + nominalType = createNode(kind: initialNominal.kind, children: [parentOrModule, initialNominal.children[1]]) } var children = [Node]() @@ -3408,12 +3415,12 @@ extension Demangler { if children.isEmpty { return nominalType } - let args = Node.create(kind: .typeList, children: children) - let unboundType = Node.create(kind: .type, children: [nominalType]) + let args = createNode(kind: .typeList, children: children) + let unboundType = createNode(kind: .type, children: [nominalType]) switch nominalType.kind { - case .class: return Node.create(kind: .boundGenericClass, children: [unboundType, args]) - case .structure: return Node.create(kind: .boundGenericStructure, children: [unboundType, args]) - case .enum: return Node.create(kind: .boundGenericEnum, children: [unboundType, args]) + case .class: return createNode(kind: .boundGenericClass, children: [unboundType, args]) + case .structure: return createNode(kind: .boundGenericStructure, children: [unboundType, args]) + case .enum: return createNode(kind: .boundGenericEnum, children: [unboundType, args]) default: throw scanner.unexpectedError() } } @@ -3463,9 +3470,9 @@ extension Demangler { case "m": (kind, hasType, name, wrapEntity) = try (.materializeForSet, true, demangleSwift3DeclName(), true) case "w": (kind, hasType, name, wrapEntity) = try (.willSet, true, demangleSwift3DeclName(), true) case "W": (kind, hasType, name, wrapEntity) = try (.didSet, true, demangleSwift3DeclName(), true) - case "U": (kind, hasType, name) = try (.explicitClosure, true, Node.create(kind: .number, contents: .index(demangleSwift3Index()))) - case "u": (kind, hasType, name) = try (.implicitClosure, true, Node.create(kind: .number, contents: .index(demangleSwift3Index()))) - case "A" where basicKind == .initializer: (kind, hasType, name) = try (.defaultArgumentInitializer, false, Node.create(kind: .number, contents: .index(demangleSwift3Index()))) + case "U": (kind, hasType, name) = try (.explicitClosure, true, createNode(kind: .number, contents: .index(demangleSwift3Index()))) + case "u": (kind, hasType, name) = try (.implicitClosure, true, createNode(kind: .number, contents: .index(demangleSwift3Index()))) + case "A" where basicKind == .initializer: (kind, hasType, name) = try (.defaultArgumentInitializer, false, createNode(kind: .number, contents: .index(demangleSwift3Index()))) case "i" where basicKind == .initializer: (kind, hasType) = (.initializer, false) case _ where basicKind == .initializer: throw scanner.unexpectedError() default: @@ -3485,7 +3492,7 @@ extension Demangler { case .some(.privateDeclName): if let n = name, let first = n.children.at(0), let second = n.children.at(1), second.text == "subscript" { isSubscript = true - name = Node.create(kind: .privateDeclName, children: [first]) + name = createNode(kind: .privateDeclName, children: [first]) } default: break } @@ -3499,8 +3506,8 @@ extension Demangler { if isSubscript, let n = name { wrappedChildren.append(n) } - let wrappedEntity = Node.create(kind: isSubscript ? .subscript : .variable, children: wrappedChildren) - entity = Node.create(kind: kind, children: [wrappedEntity]) + let wrappedEntity = createNode(kind: isSubscript ? .subscript : .variable, children: wrappedChildren) + entity = createNode(kind: kind, children: [wrappedEntity]) } else { var entityChildren: [Node] = [context] if let n = name { @@ -3509,28 +3516,28 @@ extension Demangler { if hasType { try entityChildren.append(demangleSwift3Type()) } - entity = Node.create(kind: kind, children: entityChildren) + entity = createNode(kind: kind, children: entityChildren) } - return isStatic ? Node.create(kind: .static, children: [entity]) : entity + return isStatic ? createNode(kind: .static, children: [entity]) : entity } mutating func demangleSwift3DeclarationName(kind: Node.Kind) throws(DemanglingError) -> Node { - let result = try Node.create(kind: kind, children: [demangleSwift3Context(), demangleSwift3DeclName()]) + let result = try createNode(kind: kind, children: [demangleSwift3Context(), demangleSwift3DeclName()]) nameStack.append(result) return result } mutating func demangleSwift3Context() throws(DemanglingError) -> Node { switch try scanner.readScalar() { - case "E": return try Node.create(kind: .extension, children: [demangleSwift3Module(), demangleSwift3Context()]) + case "E": return try createNode(kind: .extension, children: [demangleSwift3Module(), demangleSwift3Context()]) case "e": let module = try demangleSwift3Module() let signature = try demangleSwift3GenericSignature() let type = try demangleSwift3Context() - return Node.create(kind: .extension, children: [module, type, signature]) + return createNode(kind: .extension, children: [module, type, signature]) case "S": return try demangleSwift3SubstitutionIndex() - case "s": return Node.create(kind: .module, text: stdlibName) + case "s": return createNode(kind: .module, text: stdlibName) case "G": return try demangleSwift3BoundGenericArgs(nominalType: demangleSwift3NominalType()) case "F": fallthrough case "I": fallthrough @@ -3551,7 +3558,7 @@ extension Demangler { mutating func demangleSwift3Module() throws(DemanglingError) -> Node { switch try scanner.readScalar() { case "S": return try demangleSwift3SubstitutionIndex() - case "s": return Node.create(kind: .module, text: "Swift") + case "s": return createNode(kind: .module, text: "Swift") default: try scanner.backtrack() let module = try demangleSwift3Identifier(kind: .module) @@ -3561,13 +3568,13 @@ extension Demangler { } func swiftStdLibType(_ kind: Node.Kind, named: String) -> Node { - return Node.create(kind: kind, children: [Node.create(kind: .module, contents: .text(stdlibName)), Node.create(kind: .identifier, contents: .text(named))]) + return createNode(kind: kind, children: [createNode(kind: .module, contents: .text(stdlibName)), createNode(kind: .identifier, contents: .text(named))]) } mutating func demangleSwift3SubstitutionIndex() throws(DemanglingError) -> Node { switch try scanner.readScalar() { - case "o": return Node.create(kind: .module, contents: .text(objcModule)) - case "C": return Node.create(kind: .module, contents: .text(cModule)) + case "o": return createNode(kind: .module, contents: .text(objcModule)) + case "C": return createNode(kind: .module, contents: .text(cModule)) case "a": return swiftStdLibType(.structure, named: "Array") case "b": return swiftStdLibType(.structure, named: "Bool") case "c": return swiftStdLibType(.structure, named: "UnicodeScalar") @@ -3598,11 +3605,11 @@ extension Demangler { var children = [Node]() var c = try scanner.requirePeek() while c != "R" && c != "r" { - try children.append(Node.create(kind: .dependentGenericParamCount, contents: .index(scanner.conditional(scalar: "z") ? 0 : (demangleSwift3Index() + 1)))) + try children.append(createNode(kind: .dependentGenericParamCount, contents: .index(scanner.conditional(scalar: "z") ? 0 : (demangleSwift3Index() + 1)))) c = try scanner.requirePeek() } if children.isEmpty { - children.append(Node.create(kind: .dependentGenericParamCount, contents: .index(1))) + children.append(createNode(kind: .dependentGenericParamCount, contents: .index(1))) } if !scanner.conditional(scalar: "r") { try scanner.match(scalar: "R") @@ -3610,13 +3617,13 @@ extension Demangler { try children.append(demangleSwift3GenericRequirement()) } } - return Node.create(kind: .dependentGenericSignature, children: children) + return createNode(kind: .dependentGenericSignature, children: children) } mutating func demangleSwift3GenericRequirement() throws(DemanglingError) -> Node { let constrainedType = try demangleSwift3ConstrainedType() if scanner.conditional(scalar: "z") { - return try Node.create(kind: .dependentGenericSameTypeRequirement, children: [constrainedType, demangleSwift3Type()]) + return try createNode(kind: .dependentGenericSameTypeRequirement, children: [constrainedType, demangleSwift3Type()]) } if scanner.conditional(scalar: "l") { @@ -3647,15 +3654,15 @@ extension Demangler { size = try require(demangleNatural()) default: throw failure } - let second = Node.create(kind: kind, contents: .text(name)) + let second = createNode(kind: kind, contents: .text(name)) var reqtChildren: [Node] = [constrainedType, second] if size != UInt64.max { - reqtChildren.append(Node.create(kind: .number, contents: .index(size))) + reqtChildren.append(createNode(kind: .number, contents: .index(size))) if alignment != UInt64.max { - reqtChildren.append(Node.create(kind: .number, contents: .index(alignment))) + reqtChildren.append(createNode(kind: .number, contents: .index(alignment))) } } - return Node.create(kind: .dependentGenericLayoutRequirement, children: reqtChildren) + return createNode(kind: .dependentGenericLayoutRequirement, children: reqtChildren) } let c = try scanner.requirePeek() @@ -3672,11 +3679,11 @@ extension Demangler { case .module: typename = try demangleSwift3ProtocolNameGivenContext(context: index) default: throw scanner.unexpectedError() } - constraint = Node.create(kind: .type, children: [typename]) + constraint = createNode(kind: .type, children: [typename]) } else { constraint = try demangleSwift3ProtocolName() } - return Node.create(kind: .dependentGenericConformanceRequirement, children: [constrainedType, constraint]) + return createNode(kind: .dependentGenericConformanceRequirement, children: [constrainedType, constraint]) } mutating func demangleSwift3ConstrainedType() throws(DemanglingError) -> Node { @@ -3690,13 +3697,13 @@ extension Demangler { mutating func demangleSwift3AssociatedTypeSimple() throws(DemanglingError) -> Node { let base = try demangleSwift3GenericParamIndex() - return try demangleSwift3DependentMemberTypeName(base: Node.create(kind: .type, children: [base])) + return try demangleSwift3DependentMemberTypeName(base: createNode(kind: .type, children: [base])) } mutating func demangleSwift3AssociatedTypeCompound() throws(DemanglingError) -> Node { var base = try demangleSwift3GenericParamIndex() while !scanner.conditional(scalar: "_") { - let type = Node.create(kind: .type, children: [base]) + let type = createNode(kind: .type, children: [base]) base = try demangleSwift3DependentMemberTypeName(base: type) } return base @@ -3712,7 +3719,7 @@ extension Demangler { try scanner.backtrack() (depth, index) = try (0, demangleSwift3Index() + 1) } - return Node.create(kind: .dependentGenericParamType, children: [Node.create(kind: .index, index: depth), Node.create(kind: .index, index: index)]) + return createNode(kind: .dependentGenericParamType, children: [createNode(kind: .index, index: depth), createNode(kind: .index, index: index)]) } mutating func demangleSwift3DependentMemberTypeName(base: Node) throws(DemanglingError) -> Node { @@ -3726,20 +3733,20 @@ extension Demangler { } let identifier = try demangleSwift3Identifier() if let p = prot { - associatedType = Node.create(kind: .dependentAssociatedTypeRef, children: [identifier, p]) + associatedType = createNode(kind: .dependentAssociatedTypeRef, children: [identifier, p]) } else { - associatedType = Node.create(kind: .dependentAssociatedTypeRef, children: [identifier]) + associatedType = createNode(kind: .dependentAssociatedTypeRef, children: [identifier]) } nameStack.append(associatedType) } - return Node.create(kind: .dependentMemberType, children: [base, associatedType]) + return createNode(kind: .dependentMemberType, children: [base, associatedType]) } mutating func demangleSwift3DeclName() throws(DemanglingError) -> Node { switch try scanner.readScalar() { - case "L": return try Node.create(kind: .localDeclName, children: [Node.create(kind: .number, contents: .index(demangleSwift3Index())), demangleSwift3Identifier()]) - case "P": return try Node.create(kind: .privateDeclName, children: [demangleSwift3Identifier(), demangleSwift3Identifier()]) + case "L": return try createNode(kind: .localDeclName, children: [createNode(kind: .number, contents: .index(demangleSwift3Index())), demangleSwift3Identifier()]) + case "P": return try createNode(kind: .privateDeclName, children: [demangleSwift3Identifier(), demangleSwift3Identifier()]) default: try scanner.backtrack() return try demangleSwift3Identifier() @@ -3760,16 +3767,16 @@ extension Demangler { switch try scanner.readScalar() { case "B": switch try scanner.readScalar() { - case "b": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.BridgeObject")) - case "B": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.UnsafeValueBuffer")) + case "b": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.BridgeObject")) + case "B": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.UnsafeValueBuffer")) case "f": let size = try scanner.readInt() try scanner.match(scalar: "_") - type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.FPIEEE\(size)")) + type = createNode(kind: .builtinTypeName, contents: .text("Builtin.FPIEEE\(size)")) case "i": let size = try scanner.readInt() try scanner.match(scalar: "_") - type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.Int\(size)")) + type = createNode(kind: .builtinTypeName, contents: .text("Builtin.Int\(size)")) case "v": let elements = try scanner.readInt() try scanner.match(scalar: "B") @@ -3784,28 +3791,28 @@ extension Demangler { try scanner.match(scalar: "_") default: throw scanner.unexpectedError() } - type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.Vec\(elements)\(name)\(size)")) - case "O": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.UnknownObject")) - case "o": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.NativeObject")) - case "t": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.SILToken")) - case "p": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.RawPointer")) - case "w": type = Node.create(kind: .builtinTypeName, contents: .text("Builtin.Word")) + type = createNode(kind: .builtinTypeName, contents: .text("Builtin.Vec\(elements)\(name)\(size)")) + case "O": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.UnknownObject")) + case "o": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.NativeObject")) + case "t": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.SILToken")) + case "p": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.RawPointer")) + case "w": type = createNode(kind: .builtinTypeName, contents: .text("Builtin.Word")) default: throw scanner.unexpectedError() } case "a": type = try demangleSwift3DeclarationName(kind: .typeAlias) case "b": type = try demangleSwift3FunctionType(kind: .objCBlock) case "c": type = try demangleSwift3FunctionType(kind: .cFunctionPointer) - case "D": type = try Node.create(kind: .dynamicSelf, children: [demangleSwift3Type()]) + case "D": type = try createNode(kind: .dynamicSelf, children: [demangleSwift3Type()]) case "E": guard try scanner.readScalars(count: 2) == "RR" else { throw scanner.unexpectedError() } - type = Node.create(kind: .errorType, text: "") + type = createNode(kind: .errorType, text: "") case "F": type = try demangleSwift3FunctionType(kind: .functionType) case "f": type = try demangleSwift3FunctionType(kind: .uncurriedFunctionType) case "G": type = try demangleSwift3BoundGenericArgs(nominalType: demangleSwift3NominalType()) case "X": let c = try scanner.readScalar() switch c { - case "b": type = try Node.create(kind: .silBoxType, children: [demangleSwift3Type()]) + case "b": type = try createNode(kind: .silBoxType, children: [demangleSwift3Type()]) case "B": var signature: Node? = nil if scanner.conditional(scalar: "G") { @@ -3820,23 +3827,23 @@ extension Demangler { default: throw failure } let type = try demangleType() - layoutChildren.append(Node.create(kind: fieldKind, child: type)) + layoutChildren.append(createNode(kind: fieldKind, child: type)) } - let layout = Node.create(kind: .silBoxLayout, children: layoutChildren) + let layout = createNode(kind: .silBoxLayout, children: layoutChildren) var genericArgs: Node? = nil if signature != nil { var gaChildren: [Node] = [] while !scanner.conditional(scalar: "_") { try gaChildren.append(demangleType()) } - genericArgs = Node.create(kind: .typeList, children: gaChildren) + genericArgs = createNode(kind: .typeList, children: gaChildren) } var boxTypeChildren: [Node] = [layout] if let s = signature, let ga = genericArgs { boxTypeChildren.append(s) boxTypeChildren.append(ga) } - return Node.create(kind: .silBoxTypeWithLayout, children: boxTypeChildren) + return createNode(kind: .silBoxTypeWithLayout, children: boxTypeChildren) case "P" where scanner.conditional(scalar: "M"): fallthrough case "M": let value: String @@ -3846,20 +3853,20 @@ extension Demangler { case "o": value = "@objc_metatype" default: throw scanner.unexpectedError() } - type = try Node.create(kind: c == "P" ? .existentialMetatype : .metatype, children: [Node.create(kind: .metatypeRepresentation, contents: .text(value)), demangleSwift3Type()]) + type = try createNode(kind: c == "P" ? .existentialMetatype : .metatype, children: [createNode(kind: .metatypeRepresentation, contents: .text(value)), demangleSwift3Type()]) case "P": var children = [Node]() while !scanner.conditional(scalar: "_") { try children.append(demangleSwift3ProtocolName()) } - type = Node.create(kind: .protocolList, children: [Node.create(kind: .typeList)]) + type = createNode(kind: .protocolList, children: [createNode(kind: .typeList)]) case "f": type = try demangleSwift3FunctionType(kind: .thinFunctionType) - case "o": type = try Node.create(kind: .unowned, children: [demangleSwift3Type()]) - case "u": type = try Node.create(kind: .unmanaged, children: [demangleSwift3Type()]) - case "w": type = try Node.create(kind: .weak, children: [demangleSwift3Type()]) + case "o": type = try createNode(kind: .unowned, children: [demangleSwift3Type()]) + case "u": type = try createNode(kind: .unmanaged, children: [demangleSwift3Type()]) + case "w": type = try createNode(kind: .weak, children: [demangleSwift3Type()]) case "F": var children = [Node]() - try children.append(Node.create(kind: .implConvention, contents: .text(demangleSwift3ImplConvention(kind: .implConvention)))) + try children.append(createNode(kind: .implConvention, contents: .text(demangleSwift3ImplConvention(kind: .implConvention)))) if scanner.conditional(scalar: "C") { let name: String switch try scanner.readScalar() { @@ -3870,7 +3877,7 @@ extension Demangler { case "w": name = "@convention(witness_method)" default: throw scanner.unexpectedError() } - children.append(Node.create(kind: .implFunctionAttribute, contents: .text(name))) + children.append(createNode(kind: .implFunctionAttribute, contents: .text(name))) } if scanner.conditional(scalar: "G") { try children.append(demangleSwift3GenericSignature(isPseudo: false)) @@ -3884,24 +3891,24 @@ extension Demangler { while !scanner.conditional(scalar: "_") { try children.append(demangleSwift3ImplParameterOrResult(kind: .implResult)) } - type = Node.create(kind: .implFunctionType, children: children) + type = createNode(kind: .implFunctionType, children: children) default: throw scanner.unexpectedError() } case "K": type = try demangleSwift3FunctionType(kind: .autoClosureType) - case "M": type = try Node.create(kind: .metatype, children: [demangleSwift3Type()]) - case "P" where scanner.conditional(scalar: "M"): type = try Node.create(kind: .existentialMetatype, children: [demangleSwift3Type()]) + case "M": type = try createNode(kind: .metatype, children: [demangleSwift3Type()]) + case "P" where scanner.conditional(scalar: "M"): type = try createNode(kind: .existentialMetatype, children: [demangleSwift3Type()]) case "P": var children = [Node]() while !scanner.conditional(scalar: "_") { try children.append(demangleSwift3ProtocolName()) } - type = Node.create(kind: .protocolList, children: [Node.create(kind: .typeList, children: children)]) + type = createNode(kind: .protocolList, children: [createNode(kind: .typeList, children: children)]) case "Q": if scanner.conditional(scalar: "u") { type = NodeFactory.opaqueReturnType } else if scanner.conditional(scalar: "U") { let index = try demangleIndex() - type = Node.create(kind: .opaqueReturnType, child: Node.create(kind: .opaqueReturnTypeIndex, contents: .index(index))) + type = createNode(kind: .opaqueReturnType, child: createNode(kind: .opaqueReturnTypeIndex, contents: .index(index))) } else { type = try demangleSwift3ArchetypeType() } @@ -3912,36 +3919,36 @@ extension Demangler { } else { type = try demangleSwift3GenericParamIndex() } - case "x": type = Node.create(kind: .dependentGenericParamType, children: [Node.create(kind: .index, index: 0), Node.create(kind: .index, index: 0)]) + case "x": type = createNode(kind: .dependentGenericParamType, children: [createNode(kind: .index, index: 0), createNode(kind: .index, index: 0)]) case "w": type = try demangleSwift3AssociatedTypeSimple() case "W": type = try demangleSwift3AssociatedTypeCompound() - case "R": type = try Node.create(kind: .inOut, inlineChildren: demangleSwift3Type().children) + case "R": type = try createNode(kind: .inOut, inlineChildren: demangleSwift3Type().children) case "S": type = try demangleSwift3SubstitutionIndex() case "T": type = try demangleSwift3Tuple(variadic: false) case "t": type = try demangleSwift3Tuple(variadic: true) - case "u": type = try Node.create(kind: .dependentGenericType, children: [demangleSwift3GenericSignature(), demangleSwift3Type()]) + case "u": type = try createNode(kind: .dependentGenericType, children: [demangleSwift3GenericSignature(), demangleSwift3Type()]) case "C": type = try demangleSwift3DeclarationName(kind: .class) case "V": type = try demangleSwift3DeclarationName(kind: .structure) case "O": type = try demangleSwift3DeclarationName(kind: .enum) default: throw scanner.unexpectedError() } - return Node.create(kind: .type, children: [type]) + return createNode(kind: .type, children: [type]) } mutating func demangleSwift3ArchetypeType() throws(DemanglingError) -> Node { switch try scanner.readScalar() { case "Q": - let result = try Node.create(kind: .associatedTypeRef, children: [demangleSwift3ArchetypeType(), demangleSwift3Identifier()]) + let result = try createNode(kind: .associatedTypeRef, children: [demangleSwift3ArchetypeType(), demangleSwift3Identifier()]) nameStack.append(result) return result case "S": let index = try demangleSwift3SubstitutionIndex() - let result = try Node.create(kind: .associatedTypeRef, children: [index, demangleSwift3Identifier()]) + let result = try createNode(kind: .associatedTypeRef, children: [index, demangleSwift3Identifier()]) nameStack.append(result) return result case "s": - let root = Node.create(kind: .module, contents: .text(stdlibName)) - let result = try Node.create(kind: .associatedTypeRef, children: [root, demangleSwift3Identifier()]) + let root = createNode(kind: .module, contents: .text(stdlibName)) + let result = try createNode(kind: .associatedTypeRef, children: [root, demangleSwift3Identifier()]) nameStack.append(result) return result default: throw scanner.unexpectedError() @@ -3982,8 +3989,8 @@ extension Demangler { let convention = try demangleSwift3ImplConvention(kind: k) let type = try demangleSwift3Type() - let conventionNode = Node.create(kind: .implConvention, contents: .text(convention)) - return Node.create(kind: k, children: [conventionNode, type]) + let conventionNode = createNode(kind: .implConvention, contents: .text(convention)) + return createNode(kind: k, children: [conventionNode, type]) } mutating func demangleSwift3Tuple(variadic: Bool) throws(DemanglingError) -> Node { @@ -3995,12 +4002,12 @@ extension Demangler { try elementChildren.append(demangleSwift3Identifier(kind: .tupleElementName)) } try elementChildren.append(demangleSwift3Type()) - children.append(Node.create(kind: .tupleElement, children: elementChildren)) + children.append(createNode(kind: .tupleElement, children: elementChildren)) } if variadic, let last = children.popLast() { children.append(last.insertingChild(NodeFactory.variadicMarker, at: 0)) } - return Node.create(kind: .tuple, children: children) + return createNode(kind: .tuple, children: children) } mutating func demangleSwift3FunctionType(kind: Node.Kind) throws(DemanglingError) -> Node { @@ -4008,9 +4015,9 @@ extension Demangler { if scanner.conditional(scalar: "z") { children.append(NodeFactory.throwsAnnotation) } - try children.append(Node.create(kind: .argumentTuple, children: [demangleSwift3Type()])) - try children.append(Node.create(kind: .returnType, children: [demangleSwift3Type()])) - return Node.create(kind: kind, children: children) + try children.append(createNode(kind: .argumentTuple, children: [demangleSwift3Type()])) + try children.append(createNode(kind: .returnType, children: [demangleSwift3Type()])) + return createNode(kind: kind, children: children) } mutating func demangleSwift3Identifier(kind: Node.Kind? = nil) throws(DemanglingError) -> Node { @@ -4064,7 +4071,7 @@ extension Demangler { } } - return Node.create(kind: k, text: identifier) + return createNode(kind: k, text: identifier) } } diff --git a/Sources/Demangling/Store/SymbolStoreBuilder.swift b/Sources/Demangling/Store/SymbolStoreBuilder.swift index 4e7e43e..d2dd2cc 100644 --- a/Sources/Demangling/Store/SymbolStoreBuilder.swift +++ b/Sources/Demangling/Store/SymbolStoreBuilder.swift @@ -13,22 +13,32 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { private var edges: ContiguousArray = [] private var textBytes: ContiguousArray = [] + // Interning tables are open-addressing slot arrays that store only node + // (or text) indices — 4 bytes per slot, no separate key storage. Keys are + // recovered from the flat buffers on comparison, so the tables add ~2 MB + // for a whole-framework build instead of the ~10 MB the dictionary-keyed + // scheme cost (proposal 0001, Phase 3 intern-table slimming). + + /// Slot sentinel for an empty open-addressing slot. + private static let emptySlot: UInt32 = .max + /// Interning table for nodes whose 12-byte representation is already /// canonical: leaves (text offsets are canonical because text is interned /// first) and nodes with one or two children (child indices are canonical). - private var uniqueNodeIndices: [CompactNode: UInt32] = [:] + /// Slots hold node indices; the key is `nodes[slot]` itself. + private var compactSlots = ContiguousArray(repeating: emptySlot, count: 4096) + private var compactCount = 0 /// Interning table for nodes with three or more children, whose edges /// offset is allocation-dependent and therefore cannot serve as a key. - private var uniqueManyChildrenIndices: [ManyChildrenKey: UInt32] = [:] + /// Slots hold node indices; comparison walks the `edges` range. + private var manyChildrenSlots = ContiguousArray(repeating: emptySlot, count: 1024) + private var manyChildrenCount = 0 - /// Interning table for text contents. - private var uniqueTextLocations: [String: TextLocation] = [:] - - private struct ManyChildrenKey: Hashable { - let kindAndPayloadKind: UInt16 - let childIndices: [UInt32] - } + /// Interning table for text contents. Slots index into `uniqueTexts`; + /// comparison reads the `textBytes` range. + private var textSlots = ContiguousArray(repeating: emptySlot, count: 1024) + private var uniqueTexts: ContiguousArray = [] private struct TextLocation { let offset: UInt32 @@ -47,10 +57,11 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { /// Demangles a mangled symbol and interns the resulting tree in one step. /// - /// The intermediate `Node` tree is transient, so the global `NodeCache` - /// subtree interning is skipped — nothing accumulates outside this builder. + /// The intermediate `Node` tree is transient and fully cache-free: neither + /// leaves nor subtrees touch `NodeCache.shared`, so bulk demangling leaves + /// no trace in global state (proposal 0001, Phase 3). public mutating func demangle(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> SymbolStore.NodeIndex { - let tree = try demangleAsNode(mangled, isType: isType, internsSubtrees: false) + let tree = try demangleAsNodeTransient(mangled, isType: isType) return intern(tree) } @@ -170,36 +181,10 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { payloadWord1: childIndices[1] )) default: - let key = ManyChildrenKey( - kindAndPayloadKind: CompactNode(kind: kind, payloadKind: .manyChildren, payloadWord0: 0, payloadWord1: 0).kindAndPayloadKind, - childIndices: childIndices - ) - if let existingIndex = uniqueManyChildrenIndices[key] { - return existingIndex - } - precondition(edges.count + childIndices.count <= Int(UInt32.max), "SymbolStore edges buffer exceeded UInt32 index space") - let edgesOffset = UInt32(edges.count) - edges.append(contentsOf: childIndices) - let newIndex = appendNode(CompactNode( - kind: kind, - payloadKind: .manyChildren, - payloadWord0: edgesOffset, - payloadWord1: UInt32(childIndices.count) - )) - uniqueManyChildrenIndices[key] = newIndex - return newIndex + return internManyChildren(kind: kind, childIndices: childIndices) } } - private mutating func internCanonicalCompact(_ compact: CompactNode) -> UInt32 { - if let existingIndex = uniqueNodeIndices[compact] { - return existingIndex - } - let newIndex = appendNode(compact) - uniqueNodeIndices[compact] = newIndex - return newIndex - } - private mutating func appendNode(_ compact: CompactNode) -> UInt32 { precondition(nodes.count < Int(UInt32.max), "SymbolStore node buffer exceeded UInt32 index space") let newIndex = UInt32(nodes.count) @@ -207,15 +192,172 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { return newIndex } + // MARK: - Open-Addressing Interning Tables + + private static func mix(_ currentHash: Int, _ value: Int) -> Int { + (currentHash &* 0x9E3779B1) &+ value + } + + private static func hash(of compact: CompactNode) -> Int { + var combined = Int(compact.kindAndPayloadKind) + combined = mix(combined, Int(compact.payloadWord0)) + combined = mix(combined, Int(compact.payloadWord1)) + return mix(combined, 0) + } + + private mutating func internCanonicalCompact(_ compact: CompactNode) -> UInt32 { + if (compactCount &+ 1) &* 4 >= compactSlots.count &* 3 { + growCompactSlots() + } + let mask = compactSlots.count - 1 + var slot = Self.hash(of: compact) & mask + while true { + let existing = compactSlots[slot] + if existing == Self.emptySlot { + let newIndex = appendNode(compact) + compactSlots[slot] = newIndex + compactCount += 1 + return newIndex + } + if nodes[Int(existing)] == compact { + return existing + } + slot = (slot + 1) & mask + } + } + + private mutating func growCompactSlots() { + var grownSlots = ContiguousArray(repeating: Self.emptySlot, count: compactSlots.count * 2) + let mask = grownSlots.count - 1 + for existing in compactSlots where existing != Self.emptySlot { + var slot = Self.hash(of: nodes[Int(existing)]) & mask + while grownSlots[slot] != Self.emptySlot { + slot = (slot + 1) & mask + } + grownSlots[slot] = existing + } + compactSlots = grownSlots + } + + private static func hashOfManyChildren(kindAndPayloadKind: UInt16, childIndices: some Sequence) -> Int { + var combined = Int(kindAndPayloadKind) + for childIndex in childIndices { + combined = Self.mix(combined, Int(childIndex)) + } + return Self.mix(combined, 0) + } + + private func manyChildrenNodeMatches(_ existingIndex: UInt32, kindAndPayloadKind: UInt16, childIndices: [UInt32]) -> Bool { + let existing = nodes[Int(existingIndex)] + guard existing.kindAndPayloadKind == kindAndPayloadKind, + Int(existing.payloadWord1) == childIndices.count else { + return false + } + let edgesStart = Int(existing.payloadWord0) + return edges[edgesStart ..< edgesStart + childIndices.count].elementsEqual(childIndices) + } + + private mutating func internManyChildren(kind: Node.Kind, childIndices: [UInt32]) -> UInt32 { + if (manyChildrenCount &+ 1) &* 4 >= manyChildrenSlots.count &* 3 { + growManyChildrenSlots() + } + let kindAndPayloadKind = CompactNode(kind: kind, payloadKind: .manyChildren, payloadWord0: 0, payloadWord1: 0).kindAndPayloadKind + let mask = manyChildrenSlots.count - 1 + var slot = Self.hashOfManyChildren(kindAndPayloadKind: kindAndPayloadKind, childIndices: childIndices) & mask + while true { + let existing = manyChildrenSlots[slot] + if existing == Self.emptySlot { + precondition(edges.count + childIndices.count <= Int(UInt32.max), "SymbolStore edges buffer exceeded UInt32 index space") + let edgesOffset = UInt32(edges.count) + edges.append(contentsOf: childIndices) + let newIndex = appendNode(CompactNode( + kind: kind, + payloadKind: .manyChildren, + payloadWord0: edgesOffset, + payloadWord1: UInt32(childIndices.count) + )) + manyChildrenSlots[slot] = newIndex + manyChildrenCount += 1 + return newIndex + } + if manyChildrenNodeMatches(existing, kindAndPayloadKind: kindAndPayloadKind, childIndices: childIndices) { + return existing + } + slot = (slot + 1) & mask + } + } + + private mutating func growManyChildrenSlots() { + var grownSlots = ContiguousArray(repeating: Self.emptySlot, count: manyChildrenSlots.count * 2) + let mask = grownSlots.count - 1 + for existing in manyChildrenSlots where existing != Self.emptySlot { + let compact = nodes[Int(existing)] + let edgesStart = Int(compact.payloadWord0) + let childCount = Int(compact.payloadWord1) + var slot = Self.hashOfManyChildren( + kindAndPayloadKind: compact.kindAndPayloadKind, + childIndices: edges[edgesStart ..< edgesStart + childCount] + ) & mask + while grownSlots[slot] != Self.emptySlot { + slot = (slot + 1) & mask + } + grownSlots[slot] = existing + } + manyChildrenSlots = grownSlots + } + + private static func hashOfTextBytes(_ bytes: some Sequence) -> Int { + // FNV-1a + var combined = Int(bitPattern: 0xCBF2_9CE4_8422_2325 as UInt) + for byte in bytes { + combined = (combined ^ Int(byte)) &* 0x100_0000_01B3 + } + return combined + } + + private func textLocationMatches(_ location: TextLocation, utf8Bytes: [UInt8]) -> Bool { + guard Int(location.length) == utf8Bytes.count else { return false } + let start = Int(location.offset) + return textBytes[start ..< start + utf8Bytes.count].elementsEqual(utf8Bytes) + } + private mutating func internText(_ textValue: String) -> TextLocation { - if let existingLocation = uniqueTextLocations[textValue] { - return existingLocation + if (uniqueTexts.count &+ 1) &* 4 >= textSlots.count &* 3 { + growTextSlots() } let utf8Bytes = Array(textValue.utf8) - precondition(textBytes.count + utf8Bytes.count <= Int(UInt32.max), "SymbolStore text buffer exceeded UInt32 offset space") - let location = TextLocation(offset: UInt32(textBytes.count), length: UInt32(utf8Bytes.count)) - textBytes.append(contentsOf: utf8Bytes) - uniqueTextLocations[textValue] = location - return location + let mask = textSlots.count - 1 + var slot = Self.hashOfTextBytes(utf8Bytes) & mask + while true { + let existing = textSlots[slot] + if existing == Self.emptySlot { + precondition(textBytes.count + utf8Bytes.count <= Int(UInt32.max), "SymbolStore text buffer exceeded UInt32 offset space") + let location = TextLocation(offset: UInt32(textBytes.count), length: UInt32(utf8Bytes.count)) + textBytes.append(contentsOf: utf8Bytes) + textSlots[slot] = UInt32(uniqueTexts.count) + uniqueTexts.append(location) + return location + } + let existingLocation = uniqueTexts[Int(existing)] + if textLocationMatches(existingLocation, utf8Bytes: utf8Bytes) { + return existingLocation + } + slot = (slot + 1) & mask + } + } + + private mutating func growTextSlots() { + var grownSlots = ContiguousArray(repeating: Self.emptySlot, count: textSlots.count * 2) + let mask = grownSlots.count - 1 + for existing in textSlots where existing != Self.emptySlot { + let location = uniqueTexts[Int(existing)] + let start = Int(location.offset) + var slot = Self.hashOfTextBytes(textBytes[start ..< start + Int(location.length)]) & mask + while grownSlots[slot] != Self.emptySlot { + slot = (slot + 1) & mask + } + grownSlots[slot] = existing + } + textSlots = grownSlots } } diff --git a/Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift b/Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift new file mode 100644 index 0000000..914fbe5 --- /dev/null +++ b/Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift @@ -0,0 +1,77 @@ +import Foundation +import Testing +@testable import Demangling +@testable import DemanglingTestingSupport + +/// Acceptance measurement for proposal 0001 Phase 3 on the live dyld-cache +/// SwiftUI corpus: retained flat storage must stay under the 6 MB target and +/// the cache-free bulk build must not regress against the Node path beyond +/// the 1.2x budget (asserted generously at 2x to absorb CI noise; measured +/// numbers are recorded in the proposal's decision log). +@Suite +final class SymbolStorePhase3AcceptanceTests: DyldCacheSymbolTests, @unchecked Sendable { + private static func physicalFootprint() -> Int? { + var info = task_vm_info_data_t() + var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + let result = withUnsafeMutablePointer(to: &info) { + $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count) + } + } + guard result == KERN_SUCCESS else { return nil } + return Int(info.phys_footprint) + } + + /// Synchronous on purpose so the historical (interning) sync overload of + /// `demangleAsNode` is selected as the throughput baseline. + private static func runNodePathBaseline(_ corpus: [String]) -> Int { + var failureCount = 0 + for mangled in corpus { + do { + _ = try demangleAsNode(mangled) + } catch { + failureCount += 1 + } + } + return failureCount + } + + @Test func phase3AcceptanceOnMainImageCorpus() async throws { + let corpus = try await symbols(for: .SwiftUI).map(\.stringValue) + try #require(!corpus.isEmpty, "SwiftUI corpus unavailable on this machine") + + let footprintBefore = Self.physicalFootprint() + let storeBuildStart = ContinuousClock.now + var builder = SymbolStoreBuilder() + var storeFailureCount = 0 + for mangled in corpus { + do { + _ = try builder.demangle(mangled) + } catch { + storeFailureCount += 1 + } + } + let store = builder.freeze() + let storeBuildDuration = ContinuousClock.now - storeBuildStart + let footprintAfter = Self.physicalFootprint() + + let nodePathStart = ContinuousClock.now + let nodePathFailureCount = Self.runNodePathBaseline(corpus) + let nodePathDuration = ContinuousClock.now - nodePathStart + + let footprintDelta = (footprintAfter ?? 0) - (footprintBefore ?? 0) + print(""" + [phase3-acceptance] symbols=\(corpus.count) storeFailures=\(storeFailureCount) nodePathFailures=\(nodePathFailureCount) + [phase3-acceptance] uniqueNodes=\(store.nodeCount) storageBytes=\(store.storageByteCount) (nodes=\(store.nodeCount * 12) edges=\(store.edgeCount * 4) text=\(store.textByteCount)) + [phase3-acceptance] storeBuild=\(storeBuildDuration) nodePath=\(nodePathDuration) footprintDeltaDuringBuild=\(footprintDelta) + """) + + #expect(storeFailureCount == nodePathFailureCount, "Both paths should fail on exactly the same symbols") + // Corpus size varies by machine/OS build, so the storage target is + // asserted per unit: <=16 bytes per unique node (design value 12 plus + // edges/text amortization) and a per-symbol sanity budget. + #expect(store.storageByteCount <= store.nodeCount * 16, "Flat storage should stay within 16 bytes per unique node") + #expect(store.storageByteCount <= corpus.count * 64, "Flat storage should stay within 64 bytes per corpus symbol") + #expect(storeBuildDuration < nodePathDuration * 2, "Store build should stay within the throughput budget") + } +} diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index c4174e5..4c49516 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -227,6 +227,35 @@ struct SymbolStoreTests { } } + @Test func bridgeDemanglingLeavesNodeCacheUntouched() throws { + // Phase 3: the builder's demangle bridge must be fully cache-free. + // Cache-free demangling constructs fresh leaves every time, so two + // transient runs share no instances; the cached path interns leaves + // eagerly, so the same leaf is one canonical instance across runs. + // (Identity-based so concurrent suites interning into the global + // NodeCache cannot flake this test.) + let mangled = "$s4main27TestPhase3CacheFreeSentinelVD" + + let transientFirst = try demangleAsNodeTransient(mangled) + let transientSecond = try demangleAsNodeTransient(mangled) + let transientIdentifierFirst = try #require(transientFirst.first(of: .identifier)) + let transientIdentifierSecond = try #require(transientSecond.first(of: .identifier)) + #expect(transientIdentifierFirst.text == "TestPhase3CacheFreeSentinel") + #expect(transientIdentifierFirst !== transientIdentifierSecond, "Transient demangling should not canonicalize leaves") + + let cachedFirst = try demangleAsNode(mangled, internsSubtrees: false) + let cachedSecond = try demangleAsNode(mangled, internsSubtrees: false) + let cachedIdentifierFirst = try #require(cachedFirst.first(of: .identifier)) + let cachedIdentifierSecond = try #require(cachedSecond.first(of: .identifier)) + #expect(cachedIdentifierFirst === cachedIdentifierSecond, "Cached demangling interns leaves globally") + + // The transient tree still interns into the store correctly. + var builder = SymbolStoreBuilder() + let rootIndex = try builder.demangle(mangled) + let store = builder.freeze() + #expect(store.reference(at: rootIndex).print(using: .default) == cachedFirst.print(using: .default)) + } + @Test func directConstructionSharesHashConsingWithTreeInterning() { // Building Swift.Int by hand and interning the equivalent Node tree // must collapse to the same index, and print identically. From 8eb9f31333e016ab67f71e1ec71c27cb8a96991d Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Fri, 24 Jul 2026 00:17:25 +0800 Subject: [PATCH 13/23] perf(Demangling): zero-copy text view and allocation-free text witnesses on NodeReference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add NodeReference.textUTF8 (zero-copy ArraySlice into the store's string table) and promote isIdentifier(desired:)/isSwiftModule to DemanglingNode requirements with derived defaults, so NodeReference witnesses them by comparing string-table bytes directly — the printer's sugar-detection checks (Swift module + Optional/Array/Dictionary identifiers) no longer construct a String per call on the store path. Non-ASCII needles fall back to String comparison to preserve Unicode canonical-equivalence semantics. Parity test walks both representations and checks the witnesses and raw bytes agree. --- Sources/Demangling/Store/DemanglingNode.swift | 7 ++++ Sources/Demangling/Store/NodeReference.swift | 37 +++++++++++++++++++ Tests/DemanglingTests/SymbolStoreTests.swift | 27 ++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift index 88bd425..208e997 100644 --- a/Sources/Demangling/Store/DemanglingNode.swift +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -27,6 +27,13 @@ public protocol DemanglingNode: Sendable { /// `Remangler` is genericized). `Node` returns itself; `NodeReference` /// materializes with subtree sharing preserved. var materializedNode: Node { get } + + /// Requirements (with derived defaults) so representations can provide + /// allocation-free fast paths — `NodeReference` witnesses these with + /// byte comparisons against the store's string table instead of + /// constructing a `String` per check. + func isIdentifier(desired: String) -> Bool + var isSwiftModule: Bool { get } } // MARK: - Derived helpers shared by the printer diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift index 5754fde..ce81c04 100644 --- a/Sources/Demangling/Store/NodeReference.swift +++ b/Sources/Demangling/Store/NodeReference.swift @@ -43,6 +43,43 @@ public struct NodeReference: Sendable { return nil } + /// Zero-copy view of this node's text as UTF-8 bytes in the store's + /// string table. Only covers text physically stored in the table; + /// `.dependentGenericParamType`'s synthesized name is not included + /// (use `text` for the composed form). + public var textUTF8: ArraySlice? { + let compact = compactNode + guard case .text = compact.payloadKind else { return nil } + let start = Int(compact.payloadWord0) + return store.textBytes[start ..< start + Int(compact.payloadWord1)] + } + + /// Allocation-free witness: compares string-table bytes directly for + /// ASCII needles (every kind/sugar check the printer performs), falling + /// back to `String` comparison for non-ASCII to preserve Unicode + /// canonical-equivalence semantics. + public func isIdentifier(desired: String) -> Bool { + guard kind == .identifier else { return false } + return textMatches(desired) + } + + /// Allocation-free witness, same strategy as `isIdentifier(desired:)`. + public var isSwiftModule: Bool { + guard kind == .module else { return false } + return textMatches(stdlibName) + } + + private func textMatches(_ expected: String) -> Bool { + guard let bytes = textUTF8 else { + return text == expected + } + let expectedUTF8 = expected.utf8 + guard expectedUTF8.allSatisfy({ $0 < 0x80 }) else { + return text == expected + } + return bytes.elementsEqual(expectedUTF8) + } + /// The index contents, if this node carries an index. public var index: UInt64? { let compact = compactNode diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/SymbolStoreTests.swift index 4c49516..3b3fa1f 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/SymbolStoreTests.swift @@ -284,6 +284,33 @@ struct SymbolStoreTests { #expect(store.reference(at: indexNodeIndex).index == 42) } + @Test func byteLevelTextWitnessesMatchNodePath() throws { + // The allocation-free isIdentifier/isSwiftModule witnesses and the + // zero-copy textUTF8 view must agree with the Node path everywhere. + let mangledSymbols = [ + "$sSaySiGD", + "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + ] + var builder = SymbolStoreBuilder() + var rootIndices = [SymbolStore.NodeIndex]() + for mangled in mangledSymbols { + rootIndices.append(try builder.demangle(mangled)) + } + let store = builder.freeze() + + for (rootIndex, mangled) in zip(rootIndices, mangledSymbols) { + let nodePathTree = try demangleAsNode(mangled, internsSubtrees: false) + for (reference, node) in zip(store.reference(at: rootIndex), nodePathTree) { + #expect(reference.isSwiftModule == node.isSwiftModule) + #expect(reference.isIdentifier(desired: "Array") == node.isIdentifier(desired: "Array")) + #expect(reference.isIdentifier(desired: "Int") == node.isIdentifier(desired: "Int")) + if let bytes = reference.textUTF8 { + #expect(Array(bytes) == Array((node.text ?? "").utf8), "textUTF8 should be the exact stored bytes") + } + } + } + } + @Test func remangleParityWithNodePath() throws { // Remangling a NodeReference (bridged through materialization) must // produce the same mangled string as the Node path. From 6a38f0ba5f8ef0f53784320507cae086e4d3ab3a Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Fri, 24 Jul 2026 00:28:08 +0800 Subject: [PATCH 14/23] docs: sync AGENTS.md and proposal 0001 decision log for Phase 2 completion and Phase 3 --- AGENTS.md | 7 ++++--- evolution/0001-symbol-store-arena.md | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a293a5c..d5d4569 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,9 +48,10 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`DemangleOptions`** (`DemangleOptions.swift`) — `OptionSet` with presets: `.default`, `.simplified`, `.interface`, `.interfaceType`, etc. - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. -- **`Node` as `Sequence`** (`Node+Sequence.swift`) — `Node` conforms to `Sequence` with preorder traversal as default. Also provides `.inorder()`, `.postorder()`, `.levelorder()`. Sequence extensions add `first(of:)`, `all(of:)`, `contains(_:)` by `Node.Kind`. -- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. The builder's `demangle(_:)` currently bridges through a transient `Node` tree (`internsSubtrees: false`); Phase 3 will parse directly into the arena. -- **`DemanglingNode` / `DemanglingPrinter`** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`) — Phase 2 read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so the generic printer body is representation-agnostic). The printer engine is `DemanglingPrinter`; the public `NodePrinter` is a thin facade over `DemanglingPrinter`, and `NodeReference.print(using:)` prints straight from the store with **zero materialization** (verified byte-identical to the `Node` path across the 49k-symbol corpus × default/simplified/synthesizeSugar). When abstracting more consumers, name protocol members to match `Node`'s and note that `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil). The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `isIdentifier(desired:)`, `isSwiftModule`) live **only** on the `DemanglingNode` extension — do not re-add copies on `Node`: the generic engine statically dispatches to the extension, so a parallel concrete copy would silently drift. +- **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. +- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes) and allocation-free `isIdentifier`/`isSwiftModule` witnesses. `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. +- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless while store printing is String-target only). `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). +- **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality diff --git a/evolution/0001-symbol-store-arena.md b/evolution/0001-symbol-store-arena.md index af21239..d4c7bf7 100644 --- a/evolution/0001-symbol-store-arena.md +++ b/evolution/0001-symbol-store-arena.md @@ -4,7 +4,7 @@ - **Author**: Mx-Iris - **Status**: In Progress - **Date**: 2026-07-23 -- **Last Updated**: 2026-07-23 +- **Last Updated**: 2026-07-24 - **Branch**: `feature/symbol-store` - **Related**: `Documentations/SubtreeInterning.md`(前置优化:全子树 hash-consing,已合入 main `5788472`) @@ -142,3 +142,7 @@ store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量 | 2026-07-23 | Phase 2(打印)落地:零物化 store 打印 | 引入 `DemanglingNode` 只读协议(`kind`/`text`/`index`/`hasIndex`/`children` + `printCacheIdentity` 抽象缓存身份;`isSimpleType`/`needSpaceBeforeType`/`isIdentifier`/`isSwiftModule`/`print` 作为协议扩展从原语派生),`Node` 与 `NodeReference` 双双 conform。把 2179 行的 printer 引擎泛型化为 `DemanglingPrinter`(发现 printer 是纯只读消费者,全程不构造节点,泛型化干净),保留公共 `NodePrinter` 薄包装转发到 `DemanglingPrinter`——**公共 API 零破坏**。`NodeReference.print` 直接走 `DemanglingPrinter<_, NodeReference>`,不再 `materialize()`。**关键正确性点**:`NodeReference.text` 必须镜像 `Node.text` 对 `.dependentGenericParamType` 的泛型名合成(printer line 199 依赖之);`NodePrintContext.node` 是具体 `Node?`,store 路径以 `name as? Node`(NodeReference → nil)优雅降级,`String` target 无视 context 故无影响。**验证**:49k 语料 × 3 套选项(default/simplified/synthesizeSugar)store 打印与 Node 打印逐字节零差异;全量 dyld cache 对齐测试 + TypeDecoder 测试全绿 | | 2026-07-23 | 事故与恢复 | 外部工具删除了 `.claude/worktrees/` 目录,Phase 2 的未提交改动随磁盘丢失(三个 commit 因在 git 对象库中而安全)。因 printer 改造为确定性脚本化流程(perl + 精确 Edit),已从会话记录逐字复现全部 Phase 2 变更,重建 worktree 后重跑构建/测试验证一致 | | 2026-07-23 | Phase 2 跟进:materialize 保共享 + 派生属性单一来源 | ① `materializeNode` 增加按索引 memo:store 是 hash-consed 的 DAG,同一子树索引只物化一次并复用实例——此前朴素递归会把重度替换共享的符号指数展开成树(printCache 注释中 SwiftUI `View.Body` 量级即几十万节点),且展开树上按 `ObjectIdentifier` 键的打印缓存全部脱靶;新增测试 `materializePreservesSubtreeSharing` 断言共享位置 `===`。② 删除 `Node` 上与 `DemanglingNode` 扩展重复的 `isSimpleType`/`needSpaceBeforeType`/`isIdentifier(desired:)`/`isSwiftModule`:这些是协议扩展成员(非 requirement),泛型引擎内静态派发恒走扩展版本,两份拷贝存在静默漂移风险;收敛为单一实现后对外仍是 public API(具体 `Node` 调用解析到协议扩展),行为与逐字节输出不变,全量测试绿 | +| 2026-07-24 | 仓库形态调整 | `feature/symbol-store` 迁入独立 worktree `swift-demangling-symbol-store`(同级显式路径,避开曾被外部工具误删的 `.claude/worktrees/`),主检出切回 `main` 供 MachOSwiftSection 等路径依赖使用。合并 main 的 `7fcb0f1`(`NodePrinterTarget` type-reference scope hooks):泛型引擎以 `name as? Node` 桥接,store 路径传 nil——当前 store 打印仅 String target,无实际降级;富 target 抽象随 swift-section 迁移再设计 | +| 2026-07-24 | Phase 2 收尾:遍历 + TypeDecoder + remangle 桥 + builder 构造 API + @_spi | ① 遍历机制(preorder/inorder/postorder/levelorder、`first(of:)`/`all(of:)`/`contains`/`filter(of:)`、`identifier`)整体泛型化为单一实现,`NodeReference` conform `Sequence`(preorder 默认),parity 测试断言两种表示遍历序逐一相同。② `TypeDecoderEngine` + 公共 `TypeDecoder` facade(新增 `NodeReference` 入口);**`TypeBuilder` 协议零改动**——五个交接点经新协议 requirement `materializedNode`(`Node` 返回 self 零成本)物化小子树。③ **Remangler 决策:保持 Node 引擎**。审计确认 remangling 遍历中节点构造是承重的(`getUnspecialized` 剥泛型后回流 `mangle`、SIL box 布局 wrapper,均共享 substitution 状态,与 C++ NodeFactory 设计同构);对逐字节对齐关键组件做无构造重设计不值。`mangleAsString(some DemanglingNode)` 经 `materializedNode` 桥接(remangle 输出本就是新 String,瞬态成本,与常驻内存目标无关)。`printCacheIdentity` 改名计划放弃(未出现第二个消费者)。④ builder 新增 `intern(kind:)`/`(kind:text:)`/`(kind:index:)`/`(kind:children:)` 直接构造 API,与树 intern 共享 hash-consing(测试断言同一索引)。⑤ `DemanglingPrinter` 与 `StackSafeExecutor` 以 `@_spi(Internals)` 导出(与 MachOSwiftSection 既有 SPI 组名一致),客户端视角验证:带 SPI import 可见、不带不可见 | +| 2026-07-24 | Phase 3 落地:cache-free 批量 demangle + intern 表瘦身 | ① `Demangler` 构造 seam:全部 ~594 个构造点收敛到 `createNode(...)` 实例方法,`internsLeaves: false`(内部入口 `demangleAsNodeTransient`,builder 桥接改用之)完全绕开 `NodeCache.shared`——无叶节点泄漏、无全局锁竞争、临时树丢弃后零残留;公共入口默认行为不变。测试用叶身份断言 cache-free(并发 suite 不会 flake)。② intern 表瘦身:三张字典(键各自持有 12B compact/子索引数组/String 副本)换成 open-addressing 槽数组(4B/槽,键按需从缓冲区取回比较),FNV-1a 文本哈希 + 乘法混合节点哈希。③ **验收(本机 dyld cache SwiftUI 语料 234,232 符号,debug 构建)**:唯一节点 619,688,平铺存储 8.75 MB(14.1 B/节点 ≤16 目标,37 B/符号;nodes 7.4 + edges 0.75 + text 0.57);store 构建 25.3s vs interning Node 路径 28.5s——**快于基线**(预算允许慢 1.2×);构建期 phys_footprint 增量 9.9 MB ≈ 留存 + ~1 MB 瞬态(旧方案在 1/5 语料上高水位即 ~16 MB)。验收测试按单位口径断言(≤16 B/唯一节点、≤64 B/符号、耗时 <2× 基线)常驻于测试套件。备注:提案原稿的 `SymbolStore.demangleAsReference` 定名为 builder 侧 `demangle(_:)`——冻结后的 store 不可变,解析入口天然属于 builder | +| 2026-07-24 | Phase 3 跟进(读路径 perf):零拷贝文本 | `NodeReference.textUTF8` 暴露字符串表字节的零拷贝 `ArraySlice` 视图;`isIdentifier(desired:)`/`isSwiftModule` 升为 `DemanglingNode` requirement(带派生默认实现),`NodeReference` 以字节比较见证——printer 的 sugar 检测热路径(Swift module + Optional/Array/Dictionary)在 store 路径不再每检查构造一次 String;非 ASCII needle 回退 String 比较保持 Unicode 规范等价语义。`UTF8Span` 借用视图仍列为 Future Direction | From fcf759542c5d53b9f2d403c2dc92d09b5d809fe2 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Fri, 24 Jul 2026 11:51:46 +0800 Subject: [PATCH 15/23] refactor(Demangling): rename SymbolStore to NodeStore and add downstream migration bridges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arena types carry no symbol concept — they store demangled nodes — so SymbolStore/SymbolStoreBuilder become NodeStore/NodeStoreBuilder (proposal 0001 file renamed to 0001-node-store-arena.md; NodeFactory was considered and rejected because the name is taken by the interned-singleton namespace). Companion API for the MachOSwiftSection SymbolIndexStore migration: - @_spi(Internals) demangleAsNodeTransient — classify on the transient tree before interning (returned tree is NOT canonical) - NodeReference.structurallyEquals(_ node: Node) — zero-materialization cross-representation structural equality (frozen stores drop intern tables, so Node-keyed queries bridge through this) - NodeReference: CustomStringConvertible via materialize() (debug dump) - isKind(of:) and children.second moved up to the DemanglingNode protocol extensions; concrete Node copies removed (single-implementation rule) --- AGENTS.md | 6 +- Package.resolved | 9 ++ .../Main/Demangle/DemangleInterface.swift | 12 ++- .../Demangle/Demangler+NodeCreation.swift | 2 +- .../Demangling/Main/Demangle/Demangler.swift | 2 +- .../Main/Remangle/RemangleInterface.swift | 2 +- .../Main/TypeDecoder/TypeDecoder.swift | 4 +- Sources/Demangling/Node/Node+Children.swift | 5 - .../Demangling/Node/Node+Conversions.swift | 7 -- .../Demangling/Node/Printer/NodePrinter.swift | 2 +- Sources/Demangling/Store/CompactNode.swift | 2 +- Sources/Demangling/Store/DemanglingNode.swift | 14 ++- Sources/Demangling/Store/NodeReference.swift | 65 +++++++++++-- .../{SymbolStore.swift => NodeStore.swift} | 6 +- ...reBuilder.swift => NodeStoreBuilder.swift} | 40 ++++---- ...t => NodeStorePhase3AcceptanceTests.swift} | 4 +- ...lStoreTests.swift => NodeStoreTests.swift} | 97 +++++++++++++------ Tests/DemanglingTests/TypeDecoderTests.swift | 2 +- ...tore-arena.md => 0001-node-store-arena.md} | 20 ++-- 19 files changed, 206 insertions(+), 95 deletions(-) rename Sources/Demangling/Store/{SymbolStore.swift => NodeStore.swift} (97%) rename Sources/Demangling/Store/{SymbolStoreBuilder.swift => NodeStoreBuilder.swift} (90%) rename Tests/DemanglingTests/{SymbolStorePhase3AcceptanceTests.swift => NodeStorePhase3AcceptanceTests.swift} (96%) rename Tests/DemanglingTests/{SymbolStoreTests.swift => NodeStoreTests.swift} (82%) rename evolution/{0001-symbol-store-arena.md => 0001-node-store-arena.md} (78%) diff --git a/AGENTS.md b/AGENTS.md index d5d4569..74996ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,8 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. - **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. -- **`SymbolStore` / `SymbolStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes) and allocation-free `isIdentifier`/`isSwiftModule` witnesses. `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. -- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless while store printing is String-target only). `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). +- **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. +- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless while store printing is String-target only). `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). - **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality @@ -85,7 +85,7 @@ Sources/Demangling/ Main/TypeDecoder/ — TypeDecoder, TypeBuilder protocol Node/ — Node, Node.Children, NodeBuilder, NodeCache, Kind, Conversions, Sequence, Rewriter Node/Printer/ — NodePrinter, NodePrinterTarget protocol, NodePrintContext/State - Store/ — CompactNode, SymbolStore, SymbolStoreBuilder, NodeReference, DemanglingNode (evolution proposal 0001) + Store/ — CompactNode, NodeStore, NodeStoreBuilder, NodeReference, DemanglingNode (evolution proposal 0001) Enums/ — SugarType, ManglingFlavor, DemanglingError, ManglingError, etc. Utils/ — Extensions, Common constants, Punycode Tests/DemanglingTests/ diff --git a/Package.resolved b/Package.resolved index 367c9cd..8fa9a03 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "version" : "0.7.4" } }, + { + "identity" : "machokit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MxIris-Reverse-Engineering/MachOKit", + "state" : { + "revision" : "9de6599aaba425632bb38a35cf9acd12a022d8f2", + "version" : "0.51.100" + } + }, { "identity" : "objectarchivekit", "kind" : "remoteSourceControl", diff --git a/Sources/Demangling/Main/Demangle/DemangleInterface.swift b/Sources/Demangling/Main/Demangle/DemangleInterface.swift index 80e1d51..d3ae121 100644 --- a/Sources/Demangling/Main/Demangle/DemangleInterface.swift +++ b/Sources/Demangling/Main/Demangle/DemangleInterface.swift @@ -61,8 +61,16 @@ private func demangleAsNode(_ mangled: C, isType: Bool /// Fully cache-free demangle for transient trees (proposal 0001, Phase 3): /// neither leaves nor subtrees touch `NodeCache.shared`, so bulk demangling -/// through `SymbolStoreBuilder` leaves no trace in global state. -func demangleAsNodeTransient(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> Node { +/// through `NodeStoreBuilder` leaves no trace in global state. +/// +/// Exported via `@_spi(Internals)` for bulk-indexing consumers +/// (MachOSwiftSection's `SymbolIndexStore`) that classify each symbol on the +/// transient tree before interning it into a `NodeStoreBuilder`, so the whole +/// pipeline stays off the global cache. The returned tree is NOT canonical: +/// structurally equal nodes are distinct instances, so `===`-based sharing +/// assumptions and `NodeCache` identity guarantees do not apply. +@_spi(Internals) +public func demangleAsNodeTransient(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> Node { let demangleBlock: @Sendable () throws(DemanglingError) -> Node = { try demangleAsNode(mangled.unicodeScalars, isType: isType, internsSubtrees: false, internsLeaves: false) } diff --git a/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift b/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift index 2ed93c6..26fe9b8 100644 --- a/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift +++ b/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift @@ -5,7 +5,7 @@ /// which intern leaves in `NodeCache.shared` — the historical behavior. With /// `internsLeaves: false` they construct plain uncached nodes: no global lock /// traffic and nothing retained once the transient tree is dropped, which is -/// what the `SymbolStore` bridge wants. +/// what the `NodeStore` bridge wants. extension Demangler { @inline(__always) func createNode(kind: Node.Kind, contents: Node.Contents = .none, children: [Node] = []) -> Node { diff --git a/Sources/Demangling/Main/Demangle/Demangler.swift b/Sources/Demangling/Main/Demangle/Demangler.swift index 8a59b2b..5235c37 100644 --- a/Sources/Demangling/Main/Demangle/Demangler.swift +++ b/Sources/Demangling/Main/Demangle/Demangler.swift @@ -11,7 +11,7 @@ struct Demangler: Sendable where C: Collection, C.Iterator.Element == Unicode /// When false, node construction bypasses `NodeCache.shared` entirely — /// no leaf interning, no global lock traffic, nothing retained after the - /// tree is dropped. Used by the `SymbolStore` bridge, whose trees are + /// tree is dropped. Used by the `NodeStore` bridge, whose trees are /// transient by design (proposal 0001, Phase 3). let internsLeaves: Bool diff --git a/Sources/Demangling/Main/Remangle/RemangleInterface.swift b/Sources/Demangling/Main/Remangle/RemangleInterface.swift index 58efe44..f410bc5 100644 --- a/Sources/Demangling/Main/Remangle/RemangleInterface.swift +++ b/Sources/Demangling/Main/Remangle/RemangleInterface.swift @@ -35,7 +35,7 @@ public func mangleAsString(_ node: Node, usePunycode: Bool = true, flavor: Mangl // MARK: - Store-Backed Remangling /// Remangle any `DemanglingNode` representation — in particular a -/// `NodeReference` pointing into a `SymbolStore`. +/// `NodeReference` pointing into a `NodeStore`. /// /// The remangling algorithm constructs transient helper nodes while walking /// (unspecialized nominals, SIL box layout wrappers), exactly like the C++ diff --git a/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift b/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift index dc329f3..bb60a09 100644 --- a/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift +++ b/Sources/Demangling/Main/TypeDecoder/TypeDecoder.swift @@ -1481,7 +1481,7 @@ private func getObjCClassOrProtocolName(_ node: some DemanglingNode) -> String? /// /// A thin, source-compatible facade over `TypeDecoderEngine`. The decoding /// logic lives in the generic engine so it can also decode `NodeReference` -/// trees straight from a `SymbolStore` without materializing the class tree +/// trees straight from a `NodeStore` without materializing the class tree /// (`TypeBuilder` implementations keep receiving concrete `Node` subtrees at /// the handoff points). public final class TypeDecoder { @@ -1508,7 +1508,7 @@ public final class TypeDecoder { .decodeMangledType(node: node, forRequirement: forRequirement) } - /// Store-backed variant: decodes straight from a `SymbolStore` without + /// Store-backed variant: decodes straight from a `NodeStore` without /// materializing a `Node` tree (proposal 0001, Phase 2). public func decodeMangledType(node: NodeReference, forRequirement: Bool = true) throws(TypeLookupError) -> BuiltType { try TypeDecoderEngine(builder: builder) diff --git a/Sources/Demangling/Node/Node+Children.swift b/Sources/Demangling/Node/Node+Children.swift index da99216..9d9410d 100644 --- a/Sources/Demangling/Node/Node+Children.swift +++ b/Sources/Demangling/Node/Node+Children.swift @@ -322,11 +322,6 @@ extension Node.Children { at(index) } - @inlinable - public var second: Node? { - at(1) - } - @inlinable public func reversedFirst(_ count: Int) -> Self { var result = self diff --git a/Sources/Demangling/Node/Node+Conversions.swift b/Sources/Demangling/Node/Node+Conversions.swift index 037ebf5..9a98520 100644 --- a/Sources/Demangling/Node/Node+Conversions.swift +++ b/Sources/Demangling/Node/Node+Conversions.swift @@ -116,13 +116,6 @@ extension Node { // generic printer engine statically dispatches to the extension. } -extension Node { - @inlinable - public func isKind(of kinds: Node.Kind...) -> Bool { - return kinds.contains(kind) - } -} - extension Node { @inlinable public subscript(child childIndex: Int) -> Node { diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index 5f660a0..cc044b3 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -2216,7 +2216,7 @@ public struct DemanglingPrinter`. /// The printing logic lives in the generic engine so it can also print -/// `NodeReference` trees straight from a `SymbolStore` without materializing +/// `NodeReference` trees straight from a `NodeStore` without materializing /// a class tree (see `NodeReference.print(using:)`). public struct NodePrinter: Sendable { public static var maxPrintDepth: Int { DemanglingPrinter.maxPrintDepth } diff --git a/Sources/Demangling/Store/CompactNode.swift b/Sources/Demangling/Store/CompactNode.swift index d359fae..b3675ad 100644 --- a/Sources/Demangling/Store/CompactNode.swift +++ b/Sources/Demangling/Store/CompactNode.swift @@ -1,4 +1,4 @@ -/// Flat 12-byte node representation used by `SymbolStore`. +/// Flat 12-byte node representation used by `NodeStore`. /// /// Nodes live in one contiguous buffer and refer to each other by `UInt32` /// indices instead of pointers, eliminating per-node heap allocations, object diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift index 208e997..dbba456 100644 --- a/Sources/Demangling/Store/DemanglingNode.swift +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -1,5 +1,5 @@ /// The read-only tree shape shared by `Node` (the class tree) and -/// `NodeReference` (a handle into a `SymbolStore`). +/// `NodeReference` (a handle into a `NodeStore`). /// /// It exists so that traversal-only consumers — first the `NodePrinter` /// engine — can walk either representation without materializing a class @@ -68,6 +68,11 @@ extension DemanglingNode { } } + @inlinable + public func isKind(of kinds: Node.Kind...) -> Bool { + kinds.contains(kind) + } + @inlinable public func isIdentifier(desired: String) -> Bool { kind == .identifier && text == desired @@ -165,6 +170,11 @@ extension DemanglingNodeChildren { (index >= startIndex && index < endIndex) ? self[index] : nil } + @inlinable + public var second: Element? { + at(1) + } + @inlinable public func slice(_ from: Int, _ to: Int) -> ArraySlice { let elements = Array(self) @@ -199,7 +209,7 @@ extension NodeReference: DemanglingNode { } @inlinable - public var printCacheIdentity: SymbolStore.NodeIndex { nodeIndex } + public var printCacheIdentity: NodeStore.NodeIndex { nodeIndex } @inlinable public var materializedNode: Node { materialize() } diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift index ce81c04..4f8ff2c 100644 --- a/Sources/Demangling/Store/NodeReference.swift +++ b/Sources/Demangling/Store/NodeReference.swift @@ -1,15 +1,15 @@ -/// A lightweight handle to a node stored in a `SymbolStore`. +/// A lightweight handle to a node stored in a `NodeStore`. /// /// Sixteen bytes as a value: a store reference plus a node index. Because /// stores are fully hash-consed, equality is O(1) — two references are equal /// exactly when they address the same index of the same store, which within /// one store coincides with structural equality. public struct NodeReference: Sendable { - public let store: SymbolStore - public let nodeIndex: SymbolStore.NodeIndex + public let store: NodeStore + public let nodeIndex: NodeStore.NodeIndex @usableFromInline - init(store: SymbolStore, nodeIndex: SymbolStore.NodeIndex) { + init(store: NodeStore, nodeIndex: NodeStore.NodeIndex) { self.store = store self.nodeIndex = nodeIndex } @@ -109,6 +109,57 @@ public struct NodeReference: Sendable { return printer.printRoot(self) } } + + /// Whether this subtree is structurally equal to a `Node` tree, matching + /// the semantics of `Node.==` (kind + contents + children, recursively) + /// without materializing anything. + /// + /// This is the bridge for callers that hold an externally demangled + /// `Node` (for example from `demangleAsNode`) and need to find it among + /// `NodeReference` dictionary keys: reference-to-reference equality stays + /// O(1) via hash-consing, while reference-to-`Node` equality walks both + /// trees. Text payloads compare by string-table bytes first and fall back + /// to `String` equality so Unicode canonical equivalence matches `Node.==`. + public func structurallyEquals(_ node: Node) -> Bool { + let compact = compactNode + guard compact.kind == node.kind else { return false } + + switch node.contents { + case .none: + switch compact.payloadKind { + case .index, .text: + return false + case .none, .oneChild, .twoChildren, .manyChildren: + break + } + case .index(let indexValue): + guard index == indexValue else { return false } + case .text(let textValue): + guard case .text = compact.payloadKind else { return false } + guard let bytes = textUTF8 else { return false } + if !bytes.elementsEqual(textValue.utf8) { + guard text == textValue else { return false } + } + } + + let referenceChildren = children + let nodeChildren = node.children + guard referenceChildren.count == nodeChildren.count else { return false } + for (referenceChild, nodeChild) in zip(referenceChildren, nodeChildren) { + guard referenceChild.structurallyEquals(nodeChild) else { return false } + } + return true + } +} + +// MARK: - CustomStringConvertible + +extension NodeReference: CustomStringConvertible { + /// Debug tree dump matching `Node.description`. Bridges through + /// materialization — a debugging convenience, not a hot path. + public var description: String { + materialize().description + } } // MARK: - Hashable @@ -133,13 +184,13 @@ extension NodeReference { public typealias Index = Int @usableFromInline - let store: SymbolStore + let store: NodeStore @usableFromInline let compactNode: CompactNode @usableFromInline - init(store: SymbolStore, compactNode: CompactNode) { + init(store: NodeStore, compactNode: CompactNode) { self.store = store self.compactNode = compactNode } @@ -166,7 +217,7 @@ extension NodeReference { case .none, .index, .text: preconditionFailure("Child index out of range for a node without children") } - return NodeReference(store: store, nodeIndex: SymbolStore.NodeIndex(rawValue: rawChildIndex)) + return NodeReference(store: store, nodeIndex: NodeStore.NodeIndex(rawValue: rawChildIndex)) } } } diff --git a/Sources/Demangling/Store/SymbolStore.swift b/Sources/Demangling/Store/NodeStore.swift similarity index 97% rename from Sources/Demangling/Store/SymbolStore.swift rename to Sources/Demangling/Store/NodeStore.swift index fa758df..df10178 100644 --- a/Sources/Demangling/Store/SymbolStore.swift +++ b/Sources/Demangling/Store/NodeStore.swift @@ -1,5 +1,5 @@ /// An immutable, memory-compact symbol database produced by -/// `SymbolStoreBuilder.freeze()`. +/// `NodeStoreBuilder.freeze()`. /// /// All nodes live in flat contiguous buffers — 12 bytes per node, 4 bytes per /// child edge beyond two, and deduplicated UTF-8 text bytes — with no per-node @@ -9,10 +9,10 @@ /// /// The store is deeply immutable after freezing, so it is `Sendable` and reads /// take no locks. See evolution proposal 0001 for the overall design. -public final class SymbolStore: Sendable { +public final class NodeStore: Sendable { /// A stable identifier of a node within its store. /// - /// Indices are minted by `SymbolStoreBuilder` and remain valid in the + /// Indices are minted by `NodeStoreBuilder` and remain valid in the /// frozen store. They are only meaningful for the store they came from. public struct NodeIndex: Hashable, Sendable { @usableFromInline diff --git a/Sources/Demangling/Store/SymbolStoreBuilder.swift b/Sources/Demangling/Store/NodeStoreBuilder.swift similarity index 90% rename from Sources/Demangling/Store/SymbolStoreBuilder.swift rename to Sources/Demangling/Store/NodeStoreBuilder.swift index d2dd2cc..34585ed 100644 --- a/Sources/Demangling/Store/SymbolStoreBuilder.swift +++ b/Sources/Demangling/Store/NodeStoreBuilder.swift @@ -1,14 +1,14 @@ -/// Append-only builder that constructs a `SymbolStore`. +/// Append-only builder that constructs a `NodeStore`. /// /// The builder is noncopyable: exactly one owner may build at a time, and /// `freeze()` consumes the builder, so "immutable after freezing" is enforced /// by the type system rather than by locks or documentation. /// /// Every inserted node is hash-consed on entry: structurally equal subtrees -/// receive the same `SymbolStore.NodeIndex`. Interior-node keys use child +/// receive the same `NodeStore.NodeIndex`. Interior-node keys use child /// indices, which is exact because children are always interned before their /// parent (the same bottom-up scheme as `NodeCache.internTreeUnsafe`). -public struct SymbolStoreBuilder: ~Copyable, Sendable { +public struct NodeStoreBuilder: ~Copyable, Sendable { private var nodes: ContiguousArray = [] private var edges: ContiguousArray = [] private var textBytes: ContiguousArray = [] @@ -50,9 +50,9 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { // MARK: - Building /// Interns an existing `Node` tree, returning the canonical index of its root. - public mutating func intern(_ node: Node) -> SymbolStore.NodeIndex { + public mutating func intern(_ node: Node) -> NodeStore.NodeIndex { var visitedIndices = [ObjectIdentifier: UInt32]() - return SymbolStore.NodeIndex(rawValue: internRecursively(node, visitedIndices: &visitedIndices)) + return NodeStore.NodeIndex(rawValue: internRecursively(node, visitedIndices: &visitedIndices)) } /// Demangles a mangled symbol and interns the resulting tree in one step. @@ -60,7 +60,7 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { /// The intermediate `Node` tree is transient and fully cache-free: neither /// leaves nor subtrees touch `NodeCache.shared`, so bulk demangling leaves /// no trace in global state (proposal 0001, Phase 3). - public mutating func demangle(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> SymbolStore.NodeIndex { + public mutating func demangle(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> NodeStore.NodeIndex { let tree = try demangleAsNodeTransient(mangled, isType: isType) return intern(tree) } @@ -68,18 +68,18 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { // MARK: - Direct Construction /// Interns a parameterless node. - public mutating func intern(kind: Node.Kind) -> SymbolStore.NodeIndex { - SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .none)) + public mutating func intern(kind: Node.Kind) -> NodeStore.NodeIndex { + NodeStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .none)) } /// Interns a text-carrying leaf node. - public mutating func intern(kind: Node.Kind, text: String) -> SymbolStore.NodeIndex { - SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .text(text))) + public mutating func intern(kind: Node.Kind, text: String) -> NodeStore.NodeIndex { + NodeStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .text(text))) } /// Interns an index-carrying leaf node. - public mutating func intern(kind: Node.Kind, index: UInt64) -> SymbolStore.NodeIndex { - SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .index(index))) + public mutating func intern(kind: Node.Kind, index: UInt64) -> NodeStore.NodeIndex { + NodeStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .index(index))) } /// Interns an interior node over already-interned children — e.g. a @@ -90,15 +90,15 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { /// Hash-consing is shared with every other insertion route: constructing /// a node directly and interning a structurally equal `Node` tree yield /// the same index. - public mutating func intern(kind: Node.Kind, children: [SymbolStore.NodeIndex]) -> SymbolStore.NodeIndex { + public mutating func intern(kind: Node.Kind, children: [NodeStore.NodeIndex]) -> NodeStore.NodeIndex { let childIndices = children.map { childIndex in precondition(Int(childIndex.rawValue) < nodes.count, "Child index does not belong to this builder") return childIndex.rawValue } if childIndices.isEmpty { - return SymbolStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .none)) + return NodeStore.NodeIndex(rawValue: internLeaf(kind: kind, contents: .none)) } - return SymbolStore.NodeIndex(rawValue: internInterior(kind: kind, childIndices: childIndices)) + return NodeStore.NodeIndex(rawValue: internInterior(kind: kind, childIndices: childIndices)) } /// Freezes the builder into an immutable, `Sendable` store. @@ -106,8 +106,8 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { /// Consumes the builder; interning tables are dropped, only the flat /// buffers survive. Indices minted by this builder remain valid in the /// frozen store. - public consuming func freeze() -> SymbolStore { - SymbolStore(nodes: nodes, edges: edges, textBytes: textBytes) + public consuming func freeze() -> NodeStore { + NodeStore(nodes: nodes, edges: edges, textBytes: textBytes) } // MARK: - Statistics @@ -186,7 +186,7 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { } private mutating func appendNode(_ compact: CompactNode) -> UInt32 { - precondition(nodes.count < Int(UInt32.max), "SymbolStore node buffer exceeded UInt32 index space") + precondition(nodes.count < Int(UInt32.max), "NodeStore node buffer exceeded UInt32 index space") let newIndex = UInt32(nodes.count) nodes.append(compact) return newIndex @@ -267,7 +267,7 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { while true { let existing = manyChildrenSlots[slot] if existing == Self.emptySlot { - precondition(edges.count + childIndices.count <= Int(UInt32.max), "SymbolStore edges buffer exceeded UInt32 index space") + precondition(edges.count + childIndices.count <= Int(UInt32.max), "NodeStore edges buffer exceeded UInt32 index space") let edgesOffset = UInt32(edges.count) edges.append(contentsOf: childIndices) let newIndex = appendNode(CompactNode( @@ -331,7 +331,7 @@ public struct SymbolStoreBuilder: ~Copyable, Sendable { while true { let existing = textSlots[slot] if existing == Self.emptySlot { - precondition(textBytes.count + utf8Bytes.count <= Int(UInt32.max), "SymbolStore text buffer exceeded UInt32 offset space") + precondition(textBytes.count + utf8Bytes.count <= Int(UInt32.max), "NodeStore text buffer exceeded UInt32 offset space") let location = TextLocation(offset: UInt32(textBytes.count), length: UInt32(utf8Bytes.count)) textBytes.append(contentsOf: utf8Bytes) textSlots[slot] = UInt32(uniqueTexts.count) diff --git a/Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift b/Tests/DemanglingTests/NodeStorePhase3AcceptanceTests.swift similarity index 96% rename from Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift rename to Tests/DemanglingTests/NodeStorePhase3AcceptanceTests.swift index 914fbe5..428e0af 100644 --- a/Tests/DemanglingTests/SymbolStorePhase3AcceptanceTests.swift +++ b/Tests/DemanglingTests/NodeStorePhase3AcceptanceTests.swift @@ -9,7 +9,7 @@ import Testing /// the 1.2x budget (asserted generously at 2x to absorb CI noise; measured /// numbers are recorded in the proposal's decision log). @Suite -final class SymbolStorePhase3AcceptanceTests: DyldCacheSymbolTests, @unchecked Sendable { +final class NodeStorePhase3AcceptanceTests: DyldCacheSymbolTests, @unchecked Sendable { private static func physicalFootprint() -> Int? { var info = task_vm_info_data_t() var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) @@ -42,7 +42,7 @@ final class SymbolStorePhase3AcceptanceTests: DyldCacheSymbolTests, @unchecked S let footprintBefore = Self.physicalFootprint() let storeBuildStart = ContinuousClock.now - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() var storeFailureCount = 0 for mangled in corpus { do { diff --git a/Tests/DemanglingTests/SymbolStoreTests.swift b/Tests/DemanglingTests/NodeStoreTests.swift similarity index 82% rename from Tests/DemanglingTests/SymbolStoreTests.swift rename to Tests/DemanglingTests/NodeStoreTests.swift index 3b3fa1f..3ad3a4b 100644 --- a/Tests/DemanglingTests/SymbolStoreTests.swift +++ b/Tests/DemanglingTests/NodeStoreTests.swift @@ -1,10 +1,10 @@ import Foundation import Testing -@testable import Demangling +@_spi(Internals) @testable import Demangling -/// Unit tests for SymbolStore — arena-based compact node storage (proposal 0001, Phase 1). +/// Unit tests for NodeStore — arena-based compact node storage (proposal 0001, Phase 1). @Suite -struct SymbolStoreTests { +struct NodeStoreTests { // MARK: - Kind Ordinal Mapping @@ -24,7 +24,7 @@ struct SymbolStoreTests { // MARK: - Import / Materialize Round-Trip @Test func importAndMaterializeRoundTrip() { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let tree = Node(kind: .global, children: [ Node(kind: .type, children: [ @@ -46,10 +46,10 @@ struct SymbolStoreTests { } @Test func largeIndexPayloadRoundTrips() { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let indexValues: [UInt64] = [0, 1, UInt64(UInt32.max), UInt64(UInt32.max) + 1, UInt64.max] - var rootIndices = [SymbolStore.NodeIndex]() + var rootIndices = [NodeStore.NodeIndex]() for indexValue in indexValues { rootIndices.append(builder.intern(Node(kind: .index, index: indexValue))) } @@ -63,7 +63,7 @@ struct SymbolStoreTests { // MARK: - Hash-Consing @Test func structurallyEqualTreesShareOneIndex() { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() func makeTree() -> Node { Node(kind: .type, children: [ @@ -83,7 +83,7 @@ struct SymbolStoreTests { } @Test func manyChildrenNodesDeduplicate() { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() func makeWideTree() -> Node { Node(kind: .tuple, children: [ @@ -104,7 +104,7 @@ struct SymbolStoreTests { } @Test func textBytesDeduplicate() { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() _ = builder.intern(Node(kind: .identifier, text: "duplicated")) _ = builder.intern(Node(kind: .module, text: "duplicated")) @@ -116,7 +116,7 @@ struct SymbolStoreTests { // MARK: - Reference Accessors @Test func referenceAccessorsMatchMaterializedTree() throws { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let mangled = "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc" let rootIndex = try builder.demangle(mangled) let store = builder.freeze() @@ -157,8 +157,8 @@ struct SymbolStoreTests { .default.union(.synthesizeSugarOnTypes), ] - var builder = SymbolStoreBuilder() - var rootIndices = [SymbolStore.NodeIndex]() + var builder = NodeStoreBuilder() + var rootIndices = [NodeStore.NodeIndex]() for mangled in mangledSymbols { rootIndices.append(try builder.demangle(mangled)) } @@ -178,7 +178,7 @@ struct SymbolStoreTests { // return type, so the hash-consed store holds one index for it. The // materialized tree must keep that sharing as one Node instance rather // than expanding the DAG into duplicates. - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let rootIndex = try builder.demangle("$s4main1gyxxlF") let store = builder.freeze() @@ -201,8 +201,8 @@ struct SymbolStoreTests { "$s4main1gyxxlF", "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", ] - var builder = SymbolStoreBuilder() - var rootIndices = [SymbolStore.NodeIndex]() + var builder = NodeStoreBuilder() + var rootIndices = [NodeStore.NodeIndex]() for mangled in mangledSymbols { rootIndices.append(try builder.demangle(mangled)) } @@ -250,7 +250,7 @@ struct SymbolStoreTests { #expect(cachedIdentifierFirst === cachedIdentifierSecond, "Cached demangling interns leaves globally") // The transient tree still interns into the store correctly. - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let rootIndex = try builder.demangle(mangled) let store = builder.freeze() #expect(store.reference(at: rootIndex).print(using: .default) == cachedFirst.print(using: .default)) @@ -259,7 +259,7 @@ struct SymbolStoreTests { @Test func directConstructionSharesHashConsingWithTreeInterning() { // Building Swift.Int by hand and interning the equivalent Node tree // must collapse to the same index, and print identically. - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let moduleIndex = builder.intern(kind: .module, text: "Swift") let identifierIndex = builder.intern(kind: .identifier, text: "Int") @@ -291,8 +291,8 @@ struct SymbolStoreTests { "$sSaySiGD", "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", ] - var builder = SymbolStoreBuilder() - var rootIndices = [SymbolStore.NodeIndex]() + var builder = NodeStoreBuilder() + var rootIndices = [NodeStore.NodeIndex]() for mangled in mangledSymbols { rootIndices.append(try builder.demangle(mangled)) } @@ -320,8 +320,8 @@ struct SymbolStoreTests { "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", "$s4main3FooVAA1P0B0fMq_", ] - var builder = SymbolStoreBuilder() - var rootIndices = [SymbolStore.NodeIndex]() + var builder = NodeStoreBuilder() + var rootIndices = [NodeStore.NodeIndex]() for mangled in mangledSymbols { rootIndices.append(try builder.demangle(mangled)) } @@ -336,7 +336,7 @@ struct SymbolStoreTests { } @Test func sharedSubtreesAcrossSymbolsShareIndices() throws { - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let firstRootIndex = try builder.demangle("$sSiD") let secondRootIndex = try builder.demangle("$sSaySiGD") let store = builder.freeze() @@ -363,18 +363,61 @@ struct SymbolStoreTests { // MARK: - Reference Identity @Test func referenceEqualityIsStoreAndIndexBased() { - var firstBuilder = SymbolStoreBuilder() + var firstBuilder = NodeStoreBuilder() _ = firstBuilder.intern(Node(kind: .identifier, text: "same")) let firstStore = firstBuilder.freeze() - var secondBuilder = SymbolStoreBuilder() + var secondBuilder = NodeStoreBuilder() _ = secondBuilder.intern(Node(kind: .identifier, text: "same")) let secondStore = secondBuilder.freeze() - let firstReference = firstStore.reference(at: SymbolStore.NodeIndex(rawValue: 0)) - let secondReference = secondStore.reference(at: SymbolStore.NodeIndex(rawValue: 0)) + let firstReference = firstStore.reference(at: NodeStore.NodeIndex(rawValue: 0)) + let secondReference = secondStore.reference(at: NodeStore.NodeIndex(rawValue: 0)) #expect(firstReference != secondReference, "References into different stores should not be equal") - #expect(firstReference == firstStore.reference(at: SymbolStore.NodeIndex(rawValue: 0))) + #expect(firstReference == firstStore.reference(at: NodeStore.NodeIndex(rawValue: 0))) + } +} + +// MARK: - Cross-Representation Structural Equality + +@Suite +struct NodeReferenceStructuralEqualityTests { + private static let mangledSymbol = "$s7SwiftUI4ViewPAAE7paddingyQrAA4EdgeO3SetV_12CoreGraphics7CGFloatVSgtF" + + @Test func structurallyEqualsMatchesEquivalentNodeTree() throws { + var builder = NodeStoreBuilder() + let nodeIndex = try builder.demangle(Self.mangledSymbol) + let reference = builder.freeze().reference(at: nodeIndex) + + let equivalentTree = try demangleAsNode(Self.mangledSymbol) + #expect(reference.structurallyEquals(equivalentTree)) + + let differentTree = try demangleAsNode("$s7SwiftUI4ViewP") + #expect(!reference.structurallyEquals(differentTree)) + } + + @Test func structurallyEqualsDistinguishesTextAndIndexPayloads() { + var builder = NodeStoreBuilder() + let identifierIndex = builder.intern(kind: .identifier, text: "padding") + let store = builder.freeze() + let reference = store.reference(at: identifierIndex) + + #expect(reference.structurallyEquals(Node.create(kind: .identifier, text: "padding"))) + #expect(!reference.structurallyEquals(Node.create(kind: .identifier, text: "spacing"))) + #expect(!reference.structurallyEquals(Node.create(kind: .module, text: "padding"))) + #expect(!reference.structurallyEquals(Node.create(kind: .identifier))) + } + + @Test func structurallyEqualsWalksChildren() throws { + var builder = NodeStoreBuilder() + let childIndex = builder.intern(kind: .identifier, text: "padding") + let wrapperIndex = builder.intern(kind: .type, children: [childIndex]) + let store = builder.freeze() + let wrapperReference = store.reference(at: wrapperIndex) + + #expect(wrapperReference.structurallyEquals(Node.create(kind: .type, child: Node.create(kind: .identifier, text: "padding")))) + #expect(!wrapperReference.structurallyEquals(Node.create(kind: .type, child: Node.create(kind: .identifier, text: "spacing")))) + #expect(!wrapperReference.structurallyEquals(Node.create(kind: .identifier, text: "padding"))) } } diff --git a/Tests/DemanglingTests/TypeDecoderTests.swift b/Tests/DemanglingTests/TypeDecoderTests.swift index 75e7099..d64b7d3 100644 --- a/Tests/DemanglingTests/TypeDecoderTests.swift +++ b/Tests/DemanglingTests/TypeDecoderTests.swift @@ -222,7 +222,7 @@ struct TypeDecoderTests { func storeBackedDecodingMatchesNodePath(mangled: String) throws { let nodePathResult = try Self.decodeType(mangled) - var builder = SymbolStoreBuilder() + var builder = NodeStoreBuilder() let rootIndex = try builder.demangle(mangled) let store = builder.freeze() diff --git a/evolution/0001-symbol-store-arena.md b/evolution/0001-node-store-arena.md similarity index 78% rename from evolution/0001-symbol-store-arena.md rename to evolution/0001-node-store-arena.md index d4c7bf7..48d6321 100644 --- a/evolution/0001-symbol-store-arena.md +++ b/evolution/0001-node-store-arena.md @@ -1,16 +1,16 @@ -# 0001 - SymbolStore: Arena-Based Compact Node Storage +# 0001 - NodeStore: Arena-Based Compact Node Storage - **Proposal**: 0001 - **Author**: Mx-Iris - **Status**: In Progress - **Date**: 2026-07-23 - **Last Updated**: 2026-07-24 -- **Branch**: `feature/symbol-store` +- **Branch**: `feature/node-store` - **Related**: `Documentations/SubtreeInterning.md`(前置优化:全子树 hash-consing,已合入 main `5788472`) ## Summary -为批量 demangle 场景引入一个与现有 `Node` 类**并存**的紧凑存储层 `SymbolStore`:所有节点平铺存放在连续缓冲(arena)中,节点间用 4 字节索引互指,每节点 12 字节、无对象头、无引用计数、无逐节点堆分配。公共 API 通过轻量值类型句柄 `NodeReference`(store 引用 + `UInt32` 索引)访问,架构对标 swift-syntax 的 SyntaxArena + 值类型句柄模式。分四个可独立交付的阶段渐进迁移,全程不破坏现有 `Node` API。 +为批量 demangle 场景引入一个与现有 `Node` 类**并存**的紧凑存储层 `NodeStore`:所有节点平铺存放在连续缓冲(arena)中,节点间用 4 字节索引互指,每节点 12 字节、无对象头、无引用计数、无逐节点堆分配。公共 API 通过轻量值类型句柄 `NodeReference`(store 引用 + `UInt32` 索引)访问,架构对标 swift-syntax 的 SyntaxArena + 值类型句柄模式。分四个可独立交付的阶段渐进迁移,全程不破坏现有 `Node` API。 ## Motivation @@ -63,7 +63,7 @@ ```swift /// 冻结后的不可变符号库。构建完成即 Sendable,读路径零锁。 -public final class SymbolStore: Sendable { +public final class NodeStore: Sendable { public func demangleAsReference(_ mangled: String) throws(DemanglingError) -> NodeReference // Phase 3 public func reference(of node: Node) -> NodeReference // 导入现有树(intern 拷贝) } @@ -79,7 +79,7 @@ public struct NodeReference: Hashable, Sendable { } ``` -- 构建期使用 `SymbolStoreBuilder`(`~Copyable`):单写者约束由编译器保证,`consuming func freeze() -> SymbolStore` 完成冻结——把现在靠 `NSLock` + 文档契约维持的「构建后不可变」升级为类型系统保证; +- 构建期使用 `NodeStoreBuilder`(`~Copyable`):单写者约束由编译器保证,`consuming func freeze() -> NodeStore` 完成冻结——把现在靠 `NSLock` + 文档契约维持的「构建后不可变」升级为类型系统保证; - `Hashable`/`==` 基于 (store identity, index):因为 store 内全量 hash-consed,索引相等 ⇔ 结构相等,比较从 O(树) 降为 O(1); - 读路径后续用 `Span` / `UTF8Span`(Swift 6.2)暴露孩子区段与文本的借用视图,零分配零拷贝。 @@ -95,9 +95,9 @@ public struct NodeReference: Hashable, Sendable { 每个阶段独立可交付、测试全绿、`Node` API 始终不动: -- **Phase 1 — 存储层与互操作**:`CompactNode` / `SymbolStoreBuilder` / `SymbolStore` / `NodeReference`;`reference(of:)` 导入现有 `Node` 树,`materialize()` 导出。打印/remangle 暂走物化慢路径。验收:任意树 导入→导出 与原树 `==`;导入两棵结构相等的树得到同一索引。 +- **Phase 1 — 存储层与互操作**:`CompactNode` / `NodeStoreBuilder` / `NodeStore` / `NodeReference`;`reference(of:)` 导入现有 `Node` 树,`materialize()` 导出。打印/remangle 暂走物化慢路径。验收:任意树 导入→导出 与原树 `==`;导入两棵结构相等的树得到同一索引。 - **Phase 2 — 零物化读路径**:将 `NodePrinter` / `Remangler` / `TypeDecoder` 的树访问抽象为协议(kind/text/index/children 四个只读需求),`Node` 与 `NodeReference` 双双 conform;打印与 remangle 直接从 store 读,不再物化。验收:全量 dyld cache 对齐测试在 `NodeReference` 路径下 0 失败。 -- **Phase 3 — 解析直写 arena**:`Demangler` 的节点构造抽象为存储策略(默认策略维持现有 `Node` 行为不变;store 策略直写 scratch arena),提供批量入口 `SymbolStore.demangleAsReference(_:)`。此阶段起,批量场景完全绕开 class 分配。验收:内存达标(49k 语料 ≤6 MB)、吞吐不劣于现状 1.2 倍。 +- **Phase 3 — 解析直写 arena**:`Demangler` 的节点构造抽象为存储策略(默认策略维持现有 `Node` 行为不变;store 策略直写 scratch arena),提供批量入口 `NodeStore.demangleAsReference(_:)`。此阶段起,批量场景完全绕开 class 分配。验收:内存达标(49k 语料 ≤6 MB)、吞吐不劣于现状 1.2 倍。 - **Phase 4(可选)— 平铺序列化**:store 的几个缓冲直接二进制序列化/反序列化(接近 memcpy 量级),支持 mmap 加载——符号数据库能力,为 RuntimeViewer 类工具缓存整个 dyld cache 的解析结果。 ### 与现有 NodeCache 的关系 @@ -137,12 +137,14 @@ store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量 | Date | Decision | Notes | |---|---|---| | 2026-07-23 | Created as Draft | 基于 48B class 下限与 C++ 24B 对比分析,确定 arena + 索引句柄方向;分四阶段渐进迁移,不破坏现有 `Node` API | -| 2026-07-23 | Status → In Progress,Phase 1 落地 | 用户确认迭代方向,在 worktree `feature/symbol-store` 实施。`CompactNode`(实测 size/stride = 12)、`SymbolStoreBuilder`(`~Copyable` + `consuming freeze()`)、`SymbolStore`、`NodeReference` + `ChildrenView` 完成,含 `intern(_ node:)` 导入与 `materialize()` 导出互操作、桥接式 `demangle(_:)`(经 `internsSubtrees: false` 的临时 `Node` 树) | +| 2026-07-23 | Status → In Progress,Phase 1 落地 | 用户确认迭代方向,在 worktree `feature/symbol-store` 实施。`CompactNode`(实测 size/stride = 12)、`NodeStoreBuilder`(`~Copyable` + `consuming freeze()`)、`NodeStore`、`NodeReference` + `ChildrenView` 完成,含 `intern(_ node:)` 导入与 `materialize()` 导出互操作、桥接式 `demangle(_:)`(经 `internsSubtrees: false` 的临时 `Node` 树) | | 2026-07-23 | Phase 1 实测达标 | 49k 语料:唯一节点 201,876(与 `NodeCache` 全树 hash-consing 计数逐一吻合,交叉验证正确性);平铺存储 3.0 MB(nodes 2.4 + edges 0.43 + text 0.26),优于 ≤6 MB 目标;打印抽样 2000 条零差异;构建 0.87s,不劣于 class 路径。**新发现**:构建期高水位 ~16 MB,由 intern 表(`[CompactNode: UInt32]` 等,~10 MB 量级)与桥接路径的临时 class 节点构成,且已冻结后 dirty pages 不随 `malloc_zone_pressure_relief` 回落;两轮连建仅 +6 MB,确认内存复用、无累积。结论:①「49k ≤6 MB」按保留存储口径已达成,进程口径需 Phase 3(直写 arena,消除临时树)+ intern 表瘦身(改为指向 nodes 缓冲的 open-addressing 索引表,去掉独立 key 存储);②桥接路径会向全局 `NodeCache` 写入叶节点(实测 11k 条),Phase 3 前的批量用户建议构建后 `NodeCache.shared.clear()` | | 2026-07-23 | Phase 2(打印)落地:零物化 store 打印 | 引入 `DemanglingNode` 只读协议(`kind`/`text`/`index`/`hasIndex`/`children` + `printCacheIdentity` 抽象缓存身份;`isSimpleType`/`needSpaceBeforeType`/`isIdentifier`/`isSwiftModule`/`print` 作为协议扩展从原语派生),`Node` 与 `NodeReference` 双双 conform。把 2179 行的 printer 引擎泛型化为 `DemanglingPrinter`(发现 printer 是纯只读消费者,全程不构造节点,泛型化干净),保留公共 `NodePrinter` 薄包装转发到 `DemanglingPrinter`——**公共 API 零破坏**。`NodeReference.print` 直接走 `DemanglingPrinter<_, NodeReference>`,不再 `materialize()`。**关键正确性点**:`NodeReference.text` 必须镜像 `Node.text` 对 `.dependentGenericParamType` 的泛型名合成(printer line 199 依赖之);`NodePrintContext.node` 是具体 `Node?`,store 路径以 `name as? Node`(NodeReference → nil)优雅降级,`String` target 无视 context 故无影响。**验证**:49k 语料 × 3 套选项(default/simplified/synthesizeSugar)store 打印与 Node 打印逐字节零差异;全量 dyld cache 对齐测试 + TypeDecoder 测试全绿 | | 2026-07-23 | 事故与恢复 | 外部工具删除了 `.claude/worktrees/` 目录,Phase 2 的未提交改动随磁盘丢失(三个 commit 因在 git 对象库中而安全)。因 printer 改造为确定性脚本化流程(perl + 精确 Edit),已从会话记录逐字复现全部 Phase 2 变更,重建 worktree 后重跑构建/测试验证一致 | | 2026-07-23 | Phase 2 跟进:materialize 保共享 + 派生属性单一来源 | ① `materializeNode` 增加按索引 memo:store 是 hash-consed 的 DAG,同一子树索引只物化一次并复用实例——此前朴素递归会把重度替换共享的符号指数展开成树(printCache 注释中 SwiftUI `View.Body` 量级即几十万节点),且展开树上按 `ObjectIdentifier` 键的打印缓存全部脱靶;新增测试 `materializePreservesSubtreeSharing` 断言共享位置 `===`。② 删除 `Node` 上与 `DemanglingNode` 扩展重复的 `isSimpleType`/`needSpaceBeforeType`/`isIdentifier(desired:)`/`isSwiftModule`:这些是协议扩展成员(非 requirement),泛型引擎内静态派发恒走扩展版本,两份拷贝存在静默漂移风险;收敛为单一实现后对外仍是 public API(具体 `Node` 调用解析到协议扩展),行为与逐字节输出不变,全量测试绿 | | 2026-07-24 | 仓库形态调整 | `feature/symbol-store` 迁入独立 worktree `swift-demangling-symbol-store`(同级显式路径,避开曾被外部工具误删的 `.claude/worktrees/`),主检出切回 `main` 供 MachOSwiftSection 等路径依赖使用。合并 main 的 `7fcb0f1`(`NodePrinterTarget` type-reference scope hooks):泛型引擎以 `name as? Node` 桥接,store 路径传 nil——当前 store 打印仅 String target,无实际降级;富 target 抽象随 swift-section 迁移再设计 | | 2026-07-24 | Phase 2 收尾:遍历 + TypeDecoder + remangle 桥 + builder 构造 API + @_spi | ① 遍历机制(preorder/inorder/postorder/levelorder、`first(of:)`/`all(of:)`/`contains`/`filter(of:)`、`identifier`)整体泛型化为单一实现,`NodeReference` conform `Sequence`(preorder 默认),parity 测试断言两种表示遍历序逐一相同。② `TypeDecoderEngine` + 公共 `TypeDecoder` facade(新增 `NodeReference` 入口);**`TypeBuilder` 协议零改动**——五个交接点经新协议 requirement `materializedNode`(`Node` 返回 self 零成本)物化小子树。③ **Remangler 决策:保持 Node 引擎**。审计确认 remangling 遍历中节点构造是承重的(`getUnspecialized` 剥泛型后回流 `mangle`、SIL box 布局 wrapper,均共享 substitution 状态,与 C++ NodeFactory 设计同构);对逐字节对齐关键组件做无构造重设计不值。`mangleAsString(some DemanglingNode)` 经 `materializedNode` 桥接(remangle 输出本就是新 String,瞬态成本,与常驻内存目标无关)。`printCacheIdentity` 改名计划放弃(未出现第二个消费者)。④ builder 新增 `intern(kind:)`/`(kind:text:)`/`(kind:index:)`/`(kind:children:)` 直接构造 API,与树 intern 共享 hash-consing(测试断言同一索引)。⑤ `DemanglingPrinter` 与 `StackSafeExecutor` 以 `@_spi(Internals)` 导出(与 MachOSwiftSection 既有 SPI 组名一致),客户端视角验证:带 SPI import 可见、不带不可见 | -| 2026-07-24 | Phase 3 落地:cache-free 批量 demangle + intern 表瘦身 | ① `Demangler` 构造 seam:全部 ~594 个构造点收敛到 `createNode(...)` 实例方法,`internsLeaves: false`(内部入口 `demangleAsNodeTransient`,builder 桥接改用之)完全绕开 `NodeCache.shared`——无叶节点泄漏、无全局锁竞争、临时树丢弃后零残留;公共入口默认行为不变。测试用叶身份断言 cache-free(并发 suite 不会 flake)。② intern 表瘦身:三张字典(键各自持有 12B compact/子索引数组/String 副本)换成 open-addressing 槽数组(4B/槽,键按需从缓冲区取回比较),FNV-1a 文本哈希 + 乘法混合节点哈希。③ **验收(本机 dyld cache SwiftUI 语料 234,232 符号,debug 构建)**:唯一节点 619,688,平铺存储 8.75 MB(14.1 B/节点 ≤16 目标,37 B/符号;nodes 7.4 + edges 0.75 + text 0.57);store 构建 25.3s vs interning Node 路径 28.5s——**快于基线**(预算允许慢 1.2×);构建期 phys_footprint 增量 9.9 MB ≈ 留存 + ~1 MB 瞬态(旧方案在 1/5 语料上高水位即 ~16 MB)。验收测试按单位口径断言(≤16 B/唯一节点、≤64 B/符号、耗时 <2× 基线)常驻于测试套件。备注:提案原稿的 `SymbolStore.demangleAsReference` 定名为 builder 侧 `demangle(_:)`——冻结后的 store 不可变,解析入口天然属于 builder | +| 2026-07-24 | Phase 3 落地:cache-free 批量 demangle + intern 表瘦身 | ① `Demangler` 构造 seam:全部 ~594 个构造点收敛到 `createNode(...)` 实例方法,`internsLeaves: false`(内部入口 `demangleAsNodeTransient`,builder 桥接改用之)完全绕开 `NodeCache.shared`——无叶节点泄漏、无全局锁竞争、临时树丢弃后零残留;公共入口默认行为不变。测试用叶身份断言 cache-free(并发 suite 不会 flake)。② intern 表瘦身:三张字典(键各自持有 12B compact/子索引数组/String 副本)换成 open-addressing 槽数组(4B/槽,键按需从缓冲区取回比较),FNV-1a 文本哈希 + 乘法混合节点哈希。③ **验收(本机 dyld cache SwiftUI 语料 234,232 符号,debug 构建)**:唯一节点 619,688,平铺存储 8.75 MB(14.1 B/节点 ≤16 目标,37 B/符号;nodes 7.4 + edges 0.75 + text 0.57);store 构建 25.3s vs interning Node 路径 28.5s——**快于基线**(预算允许慢 1.2×);构建期 phys_footprint 增量 9.9 MB ≈ 留存 + ~1 MB 瞬态(旧方案在 1/5 语料上高水位即 ~16 MB)。验收测试按单位口径断言(≤16 B/唯一节点、≤64 B/符号、耗时 <2× 基线)常驻于测试套件。备注:提案原稿的 `NodeStore.demangleAsReference` 定名为 builder 侧 `demangle(_:)`——冻结后的 store 不可变,解析入口天然属于 builder | | 2026-07-24 | Phase 3 跟进(读路径 perf):零拷贝文本 | `NodeReference.textUTF8` 暴露字符串表字节的零拷贝 `ArraySlice` 视图;`isIdentifier(desired:)`/`isSwiftModule` 升为 `DemanglingNode` requirement(带派生默认实现),`NodeReference` 以字节比较见证——printer 的 sugar 检测热路径(Swift module + Optional/Array/Dictionary)在 store 路径不再每检查构造一次 String;非 ASCII needle 回退 String 比较保持 Unicode 规范等价语义。`UTF8Span` 借用视图仍列为 Future Direction | +| 2026-07-24 | 命名调整:`SymbolStore` → `NodeStore` | 库的领域概念是 `Node`(demangle 产物树节点),API 体系中并无 "Symbol" 抽象——该词仅是输入 mangled string 的口语说法。全部类型随之更名:`SymbolStore` → `NodeStore`、`SymbolStoreBuilder` → `NodeStoreBuilder`(`NodeStore.NodeIndex` 不变),与 `Node`/`NodeReference`/`CompactNode`/`NodeCache` 命名系对齐。曾考虑 `NodeFactory`(C++ 编译器中 `NodeFactory` 正是 demangle 的 slab arena 分配器,有官方先例)但放弃:本项目 `NodeFactory` 已被无参 singleton 节点集合占用(名同义异),且 C++ 版是短命裸分配器,与可 `freeze()` 的持久 hash-consing 容器语义不符——frozen 只读容器叫 Factory 会误导熟悉官方源码的读者。文件同步更名(含本提案 `0001-symbol-store-arena.md` → `0001-node-store-arena.md`);分支同步更名 `feature/symbol-store` → `feature/node-store`(本地 + origin:先核对远端 tip 一致、无关联 PR,推新名后删除旧远端分支);MachOSwiftSection 侧 `SymbolStoreMigrationPlan.md` → `NodeStoreMigrationPlan.md` 同步更新(其自有类型 `SymbolIndexStore` 不在改名范围);decision log 历史记录保留原状 | +| 2026-07-24 | 下游迁移配套:跨表示相等 + transient demangle SPI | 为 MachOSwiftSection 的 `SymbolIndexStore` → NodeStore 迁移(其 `Documentations/Internal/NodeStoreMigrationPlan.md`)新增:① `NodeReference.structurallyEquals(_ node: Node)`——零物化跨表示结构相等(语义对齐 `Node.==`:kind + contents + children 递归;text 先字节比较、Unicode 规范等价回退 String ==),服务「外部 canonical `Node` 在 `NodeReference` 字典键中查找」场景(frozen store 的 intern 表已随 `freeze()` 丢弃,不能哈希查找;name 预桶内线性结构比较足够),附 3 个单元测试;② `demangleAsNodeTransient` 以 `@_spi(Internals) public` 导出——下游批量索引在瞬态树上跑分类逻辑后 `builder.intern`,全程 cache-free(文档注明返回树非 canonical);③ `isKind(of:)`(原 `Node` 扩展)与 `children.second`(原 `Node.Children` 具体成员)上收为 `DemanglingNode`/`DemanglingNodeChildren` 协议扩展单一实现,删除具体副本;④ `NodeReference: CustomStringConvertible`(物化桥的 debug 树 dump)。迁移侧实测(SwiftUI image):构建管线换 transient+intern 后 `NodeCache` 增长归零(此前 +1.9 万叶/+56 万子树),`Storage` 释放即整镜像回收,store 本体 7 MB / 57.9 万唯一节点 | From 4f3201ab0c20f0d39d5eaa2f2b38669769a5f1b3 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Fri, 24 Jul 2026 15:30:54 +0800 Subject: [PATCH 16/23] chore: add manifest cache isolation marker for worktree builds SwiftPM caches manifest evaluations keyed by file content hash. The main checkout and this branch's worktree share identical Package.swift bytes but resolve relative local-dependency candidates to different paths, so either side could be served the other's cached evaluation (observed as local path dependencies silently degrading to remote 0.4.3, and as the main checkout's sibling paths leaking into worktree builds). A trailing comment makes the content hashes differ. --- Package.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Package.swift b/Package.swift index f12187f..0436f27 100644 --- a/Package.swift +++ b/Package.swift @@ -133,3 +133,8 @@ let package = Package( ), ], ) + +// NOTE: This branch intentionally diverges from main in this manifest so that +// SwiftPM's content-hash-keyed manifest cache cannot serve an evaluation made +// in the main checkout (where relative local-dependency candidates resolve to +// sibling checkouts) to builds running in a worktree, or vice versa. From 26db7a454bfb2238b093d56a1b6619b45cba36b6 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Fri, 24 Jul 2026 18:44:20 +0800 Subject: [PATCH 17/23] =?UTF-8?q?feat(Demangling):=20Stage=205=20upstream?= =?UTF-8?q?=20=E2=80=94=20lazy=20scope=20hook,=20transient=20construction?= =?UTF-8?q?=20SPI,=20NodeReference=20structural=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NodePrinterTarget.pushTypeReferenceScope now takes @autoclosure () -> Node?: scope-ignoring targets (String, the default) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets evaluate materializedNode and get full type-reference scope identity on the store path. Guarded by NodePrinterScopeTests (scope sequence parity across representations). - demangleAsNodeTransient accepts a symbolicReferenceResolver; new @_spi(Internals) Node.createTransient factories bypass NodeCache for resolver-built and store-feeding construction. - NodeReference gains init(interning:) (single-tree mini store), structurallyEquals(_: NodeReference) (same-store O(1), cross-store structural walk) and structuralHash(into:) for value types keying by node structure. - Replace an invalid corpus symbol (predating NodeStore; rejected by the system demangler too) with a generated real cross-module extension initializer; all corpus literals are now validated against swift-demangle. Full suite: 410 tests / 19 suites green. --- AGENTS.md | 6 +- .../Main/Demangle/DemangleInterface.swift | 4 +- Sources/Demangling/Node/Node+Init.swift | 34 +++++++ .../Demangling/Node/Printer/NodePrinter.swift | 6 +- .../Node/Printer/NodePrinterTarget.swift | 10 ++- Sources/Demangling/Store/NodeReference.swift | 83 +++++++++++++++++ Tests/DemanglingTests/NodeCacheTests.swift | 2 +- .../NodePrinterScopeTests.swift | 88 +++++++++++++++++++ Tests/DemanglingTests/NodeStoreTests.swift | 50 +++++++++-- 9 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 Tests/DemanglingTests/NodePrinterScopeTests.swift diff --git a/AGENTS.md b/AGENTS.md index 74996ea..8a0a00f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`Node`** (`Node.swift`) — Immutable tree node (reference type, `Sendable`). Uses a unified `Payload` enum that merges contents (`.text`/`.index`/`.none`) and children (`.oneChild`/`.twoChildren`/`.manyChildren`) into a single discriminated union — contents and children are mutually exclusive. Mutation methods are `fileprivate`; external code must use `NodeBuilder`. - **`Node.Children`** (`Node.Children.swift`) — Inline storage for 0–2 children without heap allocation; falls back to `ContiguousArray` for 3+. - **`NodeBuilder`** (`Node.swift`) — Thread-safe builder for constructing `Node` trees incrementally (uses `os_unfair_lock`). -- **`Node.create()`** (`Node+Init.swift`) — Public static factories that go through `NodeCache.shared` for leaf-node interning. Always use these instead of `Node.init()` when creating nodes that should be cached. +- **`Node.create()`** (`Node+Init.swift`) — Public static factories that go through `NodeCache.shared` for leaf-node interning. Always use these instead of `Node.init()` when creating nodes that should be cached. The `@_spi(Internals)` `Node.createTransient(...)` counterparts never touch the cache — use them (together with `demangleAsNodeTransient`, which also accepts a `symbolicReferenceResolver`) in pipelines that must not pin anything in global state, such as symbolic-reference resolvers and store-feeding bulk demangling. - **`NodeCache` / `NodeFactory`** (`NodeFactory.swift`) — `NodeCache` is the global interning cache with two levels: leaf nodes are interned eagerly at creation time, and whole trees are hash-consed bottom-up via `intern(_:)` / `internTreeUnsafe(_:)` (structurally equal subtrees collapse to one shared instance; interior-node keys compare children by `===`, which is safe because children are canonicalized before their parent). `demangleAsNode` runs the tree-interning pass by default (`internsSubtrees: true`), so identical symbols demangle to the identical (`===`) tree; opt out with `internsSubtrees: false`. `NodeFactory` provides pre-created singletons for common parameterless nodes (e.g., `NodeFactory.emptyList`, `.asyncAnnotation`). The `Node.init(...)` convenience initializers in `NodeFactory.swift` are **internal** and bypass the cache — they exist for `Demangler`/`Remangler` internals. - **`Node.Kind`** (`Node+Kind.swift`) — Exhaustive enum of ~300 node kinds matching the Swift compiler's `Demangle::Node::Kind`. - **`Demangler`** (`Demangler.swift`) — Generic over `Collection`. Parses mangled prefixes `_T0`, `_$S`, `_$s`, `$S`, `$s`, `$e`, `_$e`, `@__swiftmacro_`. @@ -49,8 +49,8 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. - **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. -- **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. -- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless while store printing is String-target only). `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). +- **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), `structurallyEquals(_ other: NodeReference)` (same-store O(1) via index equality, cross-store structural walk) plus `structuralHash(into:)` (structure-consistent hashing for value types that key dictionaries by node structure while storing references — `NodeReference`'s intrinsic `Hashable` is store-identity based), `NodeReference(interning:)` (interns one `Node` tree into a fresh private mini store — self-contained handles for values that outlive their source tree), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. +- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless — no rich target reads it on the store path). `NodePrinterTarget.pushTypeReferenceScope` takes its node as `@autoclosure () -> Node?`: scope-ignoring targets (`String`, the default implementation) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets (e.g. `SemanticString`) evaluate it and receive `materializedNode` — so store-backed rich printing gets full type-reference scope identity at the cost of materializing only nominal-reference subtrees. `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). - **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality diff --git a/Sources/Demangling/Main/Demangle/DemangleInterface.swift b/Sources/Demangling/Main/Demangle/DemangleInterface.swift index d3ae121..7c15ca3 100644 --- a/Sources/Demangling/Main/Demangle/DemangleInterface.swift +++ b/Sources/Demangling/Main/Demangle/DemangleInterface.swift @@ -70,9 +70,9 @@ private func demangleAsNode(_ mangled: C, isType: Bool /// structurally equal nodes are distinct instances, so `===`-based sharing /// assumptions and `NodeCache` identity guarantees do not apply. @_spi(Internals) -public func demangleAsNodeTransient(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> Node { +public func demangleAsNodeTransient(_ mangled: String, isType: Bool = false, symbolicReferenceResolver: DemangleSymbolicReferenceResolver? = nil) throws(DemanglingError) -> Node { let demangleBlock: @Sendable () throws(DemanglingError) -> Node = { - try demangleAsNode(mangled.unicodeScalars, isType: isType, internsSubtrees: false, internsLeaves: false) + try demangleAsNode(mangled.unicodeScalars, isType: isType, symbolicReferenceResolver: symbolicReferenceResolver, internsSubtrees: false, internsLeaves: false) } return try StackSafeExecutor.execute(demangleBlock) } diff --git a/Sources/Demangling/Node/Node+Init.swift b/Sources/Demangling/Node/Node+Init.swift index 3ab8bd8..ff283ee 100644 --- a/Sources/Demangling/Node/Node+Init.swift +++ b/Sources/Demangling/Node/Node+Init.swift @@ -65,6 +65,40 @@ extension Node { } } +// MARK: - Transient Factory Methods (no interning) + +/// Transient counterparts of `Node.create(...)` that never touch +/// `NodeCache.shared`: leaves are freshly allocated instead of interned, so +/// nothing gets pinned in global state for the process lifetime. +/// +/// Exported via `@_spi(Internals)` for consumers whose whole pipeline runs +/// off the global cache (transient demangling feeding a `NodeStoreBuilder`, +/// symbolic-reference resolvers building splice nodes, and similar). The +/// returned nodes are NOT canonical: structurally equal nodes are distinct +/// instances, so `===`-based sharing assumptions do not apply. +@_spi(Internals) +extension Node { + public static func createTransient(kind: Kind, contents: Contents = .none, children: [Node] = []) -> Node { + Node(kind: kind, contents: contents, children: children) + } + + public static func createTransient(kind: Kind, contents: Contents = .none, inlineChildren: Children) -> Node { + Node(kind: kind, contents: contents, inlineChildren: inlineChildren) + } + + public static func createTransient(kind: Kind, child: Node) -> Node { + Node(kind: kind, contents: .none, children: [child]) + } + + public static func createTransient(kind: Kind, text: String, children: [Node] = []) -> Node { + Node(kind: kind, contents: .text(text), children: children) + } + + public static func createTransient(kind: Kind, index: UInt64, children: [Node] = []) -> Node { + Node(kind: kind, contents: .index(index), children: children) + } +} + extension Node { @inlinable public static func create(kind: Kind, contents: Contents = .none, @ArrayBuilder childrenBuilder: () -> [Node]) -> Node { diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index cc044b3..918e87a 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -192,7 +192,11 @@ public struct DemanglingPrinter Node?) mutating func popTypeReferenceScope() } @@ -23,7 +29,7 @@ extension NodePrinterTarget { write(content) } - public mutating func pushTypeReferenceScope(_ node: Node?) {} + public mutating func pushTypeReferenceScope(_ node: @autoclosure () -> Node?) {} public mutating func popTypeReferenceScope() {} diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift index 4f8ff2c..3ff4d80 100644 --- a/Sources/Demangling/Store/NodeReference.swift +++ b/Sources/Demangling/Store/NodeReference.swift @@ -14,6 +14,21 @@ public struct NodeReference: Sendable { self.nodeIndex = nodeIndex } + /// Interns a single `Node` tree into a fresh private store and + /// references its root. + /// + /// This is the convenience path for holding one externally built tree + /// (for example a transient demangle or a synthesized `Node`) in + /// compact form: the reference keeps its private store alive, so the + /// value is self-contained and `Sendable`. For bulk interning, drive a + /// `NodeStoreBuilder` directly so trees share one arena and its + /// deduplication. + public init(interning node: Node) { + var builder = NodeStoreBuilder() + let rootNodeIndex = builder.intern(node) + self = builder.freeze().reference(at: rootNodeIndex) + } + @usableFromInline var compactNode: CompactNode { store.compactNode(at: nodeIndex.rawValue) @@ -150,6 +165,45 @@ public struct NodeReference: Sendable { } return true } + + /// Whether this subtree is structurally equal to another reference's + /// subtree, matching `Node.==` semantics across stores. + /// + /// Within one store this is O(1): hash-consing makes index equality + /// coincide with structural equality. Across two stores it walks both + /// subtrees. Text payloads compare by string-table bytes first and + /// fall back to `String` equality so Unicode canonical equivalence + /// matches `Node.==`. + public func structurallyEquals(_ other: NodeReference) -> Bool { + if store === other.store { + return nodeIndex == other.nodeIndex + } + let compact = compactNode + let otherCompact = other.compactNode + guard compact.kind == otherCompact.kind else { return false } + + switch (compact.payloadKind, otherCompact.payloadKind) { + case (.text, .text): + guard let bytes = textUTF8, let otherBytes = other.textUTF8 else { return false } + if !bytes.elementsEqual(otherBytes) { + guard text == other.text else { return false } + } + case (.index, .index): + guard index == other.index else { return false } + case (.text, _), (_, .text), (.index, _), (_, .index): + return false + default: + break + } + + let selfChildren = children + let otherChildren = other.children + guard selfChildren.count == otherChildren.count else { return false } + for (selfChild, otherChild) in zip(selfChildren, otherChildren) { + guard selfChild.structurallyEquals(otherChild) else { return false } + } + return true + } } // MARK: - CustomStringConvertible @@ -173,6 +227,35 @@ extension NodeReference: Hashable { hasher.combine(ObjectIdentifier(store)) hasher.combine(nodeIndex) } + + /// Hashes this subtree by structure (kind + contents + children, + /// recursively), consistent with `structurallyEquals(_:)` across + /// stores: structurally equal references — even from different stores — + /// produce the same hash. + /// + /// This is the building block for value types that key dictionaries by + /// a node's structure while storing a `NodeReference` (whose intrinsic + /// `Hashable` is store-identity based and would split structurally + /// equal keys from different stores). + public func structuralHash(into hasher: inout Hasher) { + let compact = compactNode + hasher.combine(compact.kind) + switch compact.payloadKind { + case .text: + hasher.combine(1) + hasher.combine(text) + case .index: + hasher.combine(2) + hasher.combine(index) + case .none, .oneChild, .twoChildren, .manyChildren: + hasher.combine(0) + } + let childrenView = children + hasher.combine(childrenView.count) + for child in childrenView { + child.structuralHash(into: &hasher) + } + } } // MARK: - ChildrenView diff --git a/Tests/DemanglingTests/NodeCacheTests.swift b/Tests/DemanglingTests/NodeCacheTests.swift index 3aa504b..c78c28f 100644 --- a/Tests/DemanglingTests/NodeCacheTests.swift +++ b/Tests/DemanglingTests/NodeCacheTests.swift @@ -272,7 +272,7 @@ struct NodeCacheDemangleTests { } @Test func interningDoesNotAffectPrintingOrRemangling() throws { - let mangled = "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc" + let mangled = "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC" let interned = try demangleAsNode(mangled) let uninterned = try demangleAsNode(mangled, internsSubtrees: false) diff --git a/Tests/DemanglingTests/NodePrinterScopeTests.swift b/Tests/DemanglingTests/NodePrinterScopeTests.swift new file mode 100644 index 0000000..f1d0116 --- /dev/null +++ b/Tests/DemanglingTests/NodePrinterScopeTests.swift @@ -0,0 +1,88 @@ +import Foundation +import Testing +@_spi(Internals) @testable import Demangling + +/// Guards the type-reference scope seam of `NodePrinterTarget`. +/// +/// The engine delivers the nominal reference node to +/// `pushTypeReferenceScope` lazily (autoclosure): rich targets that use +/// scope identity receive the same sequence whether printing a `Node` tree +/// or a store-backed `NodeReference` (which must materialize to service the +/// hook), while scope-ignoring targets (`String`, the default +/// implementation) never evaluate the closure and keep the store's +/// plain-text path materialization-free. +@Suite +struct NodePrinterScopeTests { + /// A target that mirrors `String`'s writing behavior exactly (so the + /// print flow is identical) while recording every scope event. Scope + /// identity is captured as the remangled string of the delivered node, + /// which is structural — equal across representations exactly when the + /// delivered subtrees are structurally equal. + private struct ScopeRecordingTarget: NodePrinterTarget { + enum ScopeEvent: Equatable, Sendable { + case push(identity: String?) + case pop + } + + var text: String = "" + var scopeEvents: [ScopeEvent] = [] + + init() {} + + var count: Int { text.count } + + mutating func write(_ content: String) { + text.write(content) + } + + mutating func append(_ other: ScopeRecordingTarget) { + text.append(other.text) + scopeEvents.append(contentsOf: other.scopeEvents) + } + + mutating func pushTypeReferenceScope(_ node: @autoclosure () -> Node?) { + let identity = node().flatMap { try? mangleAsString($0) } + scopeEvents.append(.push(identity: identity)) + } + + mutating func popTypeReferenceScope() { + scopeEvents.append(.pop) + } + } + + @Test func scopeIdentitySequenceMatchesAcrossRepresentations() throws { + let mangledStrings = [ + "$sSiD", + "$sSaySiGD", + "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC", + "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", + "$sSS7cStringSSSPys4Int8VG_tcfC", + ] + for mangled in mangledStrings { + var storeBuilder = NodeStoreBuilder() + let rootIndex = try storeBuilder.demangle(mangled) + let store = storeBuilder.freeze() + let reference = store.reference(at: rootIndex) + + let nodeTree = try demangleAsNode(mangled, internsSubtrees: false) + + let nodePathTarget = StackSafeExecutor.execute { () -> ScopeRecordingTarget in + var nodePathPrinter = NodePrinter(options: .default) + return nodePathPrinter.printRoot(nodeTree) + } + let referencePathTarget = StackSafeExecutor.execute { () -> ScopeRecordingTarget in + var referencePathPrinter = DemanglingPrinter(options: .default) + return referencePathPrinter.printRoot(reference) + } + + #expect(referencePathTarget.text == nodePathTarget.text, "print divergence for \(mangled)") + #expect(referencePathTarget.scopeEvents == nodePathTarget.scopeEvents, "scope divergence for \(mangled)") + + let deliveredIdentityCount = referencePathTarget.scopeEvents.count(where: { scopeEvent in + if case .push(.some) = scopeEvent { return true } + return false + }) + #expect(deliveredIdentityCount > 0, "expected at least one nominal reference scope in \(mangled)") + } + } +} diff --git a/Tests/DemanglingTests/NodeStoreTests.swift b/Tests/DemanglingTests/NodeStoreTests.swift index 3ad3a4b..4d59d0f 100644 --- a/Tests/DemanglingTests/NodeStoreTests.swift +++ b/Tests/DemanglingTests/NodeStoreTests.swift @@ -117,7 +117,7 @@ struct NodeStoreTests { @Test func referenceAccessorsMatchMaterializedTree() throws { var builder = NodeStoreBuilder() - let mangled = "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc" + let mangled = "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC" let rootIndex = try builder.demangle(mangled) let store = builder.freeze() @@ -145,7 +145,7 @@ struct NodeStoreTests { let mangledSymbols = [ "$sSiD", "$sSaySiGD", - "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC", "$s4main3FooVAA1P0B0fMq_", "$s7SwiftUI4ViewP", "$s4main1gyxxlF", // g(A) -> A @@ -197,7 +197,7 @@ struct NodeStoreTests { // NodeReference's Sequence conformance and kind-lookup helpers must // walk the store in the same order the Node path walks the class tree. let mangledSymbols = [ - "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC", "$s4main1gyxxlF", "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", ] @@ -289,7 +289,7 @@ struct NodeStoreTests { // zero-copy textUTF8 view must agree with the Node path everywhere. let mangledSymbols = [ "$sSaySiGD", - "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC", ] var builder = NodeStoreBuilder() var rootIndices = [NodeStore.NodeIndex]() @@ -315,7 +315,7 @@ struct NodeStoreTests { // Remangling a NodeReference (bridged through materialization) must // produce the same mangled string as the Node path. let mangledSymbols = [ - "$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc", + "$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC", "$s4main1gyxxlF", "$s7SwiftUI15ModifiedContentVyxq_GAA0D0AAMc", "$s4main3FooVAA1P0B0fMq_", @@ -421,3 +421,43 @@ struct NodeReferenceStructuralEqualityTests { #expect(!wrapperReference.structurallyEquals(Node.create(kind: .identifier, text: "padding"))) } } + +// MARK: - Interning convenience (Stage 5 upstream additions) + +@Suite +struct NodeReferenceInterningConvenienceTests { + @Test func interningInitializerRoundTrip() throws { + let tree = try demangleAsNodeTransient("$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC") + let reference = NodeReference(interning: tree) + #expect(reference.structurallyEquals(tree)) + #expect(reference.print(using: .default) == tree.print(using: .default)) + } + + @Test func crossStoreStructuralEquality() throws { + let mangled = "$sSaySiGD" + var firstBuilder = NodeStoreBuilder() + let firstIndex = try firstBuilder.demangle(mangled) + let firstReference = firstBuilder.freeze().reference(at: firstIndex) + + var secondBuilder = NodeStoreBuilder() + _ = try secondBuilder.demangle("$sS2iIegyd_D") + let secondIndex = try secondBuilder.demangle(mangled) + let secondReference = secondBuilder.freeze().reference(at: secondIndex) + + #expect(firstReference != secondReference) + #expect(firstReference.structurallyEquals(secondReference)) + #expect(secondReference.structurallyEquals(firstReference)) + + let differentReference = NodeReference(interning: try demangleAsNodeTransient("$sSiD")) + #expect(!firstReference.structurallyEquals(differentReference)) + } + + @Test func sameStoreStructuralEqualityIsIndexEquality() throws { + var builder = NodeStoreBuilder() + let firstIndex = try builder.demangle("$sSaySiGD") + let secondIndex = try builder.demangle("$sSaySiGD") + let store = builder.freeze() + #expect(firstIndex == secondIndex) + #expect(store.reference(at: firstIndex).structurallyEquals(store.reference(at: secondIndex))) + } +} From 4c5caf80b79d64c82058eda4913c2eb568d2a369 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 27 Jul 2026 14:51:14 +0800 Subject: [PATCH 18/23] docs(evolution): record Stage 5 upstream decisions in proposal 0001 Backfills the decision log entry for 26db7a4: why the type-reference scope hook became an @autoclosure (store printing must not pay for a materialization no scope-ignoring target reads, while rich targets get the nominal subtree instead of the previous nil), why symbolic-reference resolvers needed a transient construction SPI (user-supplied resolver closures were the one construction path Phase 3's cache-free contract did not cover), what each of the three new NodeReference structural APIs is for (and why the intrinsic store-identity Hashable cannot serve as the structural hash), and the invalid corpus symbol that swift-demangle rejects as well. --- evolution/0001-node-store-arena.md | 1 + 1 file changed, 1 insertion(+) diff --git a/evolution/0001-node-store-arena.md b/evolution/0001-node-store-arena.md index 48d6321..d069a35 100644 --- a/evolution/0001-node-store-arena.md +++ b/evolution/0001-node-store-arena.md @@ -148,3 +148,4 @@ store 自带索引级 intern,store 路径不经过全局 `NodeCache`;批量 | 2026-07-24 | Phase 3 跟进(读路径 perf):零拷贝文本 | `NodeReference.textUTF8` 暴露字符串表字节的零拷贝 `ArraySlice` 视图;`isIdentifier(desired:)`/`isSwiftModule` 升为 `DemanglingNode` requirement(带派生默认实现),`NodeReference` 以字节比较见证——printer 的 sugar 检测热路径(Swift module + Optional/Array/Dictionary)在 store 路径不再每检查构造一次 String;非 ASCII needle 回退 String 比较保持 Unicode 规范等价语义。`UTF8Span` 借用视图仍列为 Future Direction | | 2026-07-24 | 命名调整:`SymbolStore` → `NodeStore` | 库的领域概念是 `Node`(demangle 产物树节点),API 体系中并无 "Symbol" 抽象——该词仅是输入 mangled string 的口语说法。全部类型随之更名:`SymbolStore` → `NodeStore`、`SymbolStoreBuilder` → `NodeStoreBuilder`(`NodeStore.NodeIndex` 不变),与 `Node`/`NodeReference`/`CompactNode`/`NodeCache` 命名系对齐。曾考虑 `NodeFactory`(C++ 编译器中 `NodeFactory` 正是 demangle 的 slab arena 分配器,有官方先例)但放弃:本项目 `NodeFactory` 已被无参 singleton 节点集合占用(名同义异),且 C++ 版是短命裸分配器,与可 `freeze()` 的持久 hash-consing 容器语义不符——frozen 只读容器叫 Factory 会误导熟悉官方源码的读者。文件同步更名(含本提案 `0001-symbol-store-arena.md` → `0001-node-store-arena.md`);分支同步更名 `feature/symbol-store` → `feature/node-store`(本地 + origin:先核对远端 tip 一致、无关联 PR,推新名后删除旧远端分支);MachOSwiftSection 侧 `SymbolStoreMigrationPlan.md` → `NodeStoreMigrationPlan.md` 同步更新(其自有类型 `SymbolIndexStore` 不在改名范围);decision log 历史记录保留原状 | | 2026-07-24 | 下游迁移配套:跨表示相等 + transient demangle SPI | 为 MachOSwiftSection 的 `SymbolIndexStore` → NodeStore 迁移(其 `Documentations/Internal/NodeStoreMigrationPlan.md`)新增:① `NodeReference.structurallyEquals(_ node: Node)`——零物化跨表示结构相等(语义对齐 `Node.==`:kind + contents + children 递归;text 先字节比较、Unicode 规范等价回退 String ==),服务「外部 canonical `Node` 在 `NodeReference` 字典键中查找」场景(frozen store 的 intern 表已随 `freeze()` 丢弃,不能哈希查找;name 预桶内线性结构比较足够),附 3 个单元测试;② `demangleAsNodeTransient` 以 `@_spi(Internals) public` 导出——下游批量索引在瞬态树上跑分类逻辑后 `builder.intern`,全程 cache-free(文档注明返回树非 canonical);③ `isKind(of:)`(原 `Node` 扩展)与 `children.second`(原 `Node.Children` 具体成员)上收为 `DemanglingNode`/`DemanglingNodeChildren` 协议扩展单一实现,删除具体副本;④ `NodeReference: CustomStringConvertible`(物化桥的 debug 树 dump)。迁移侧实测(SwiftUI image):构建管线换 transient+intern 后 `NodeCache` 增长归零(此前 +1.9 万叶/+56 万子树),`Storage` 释放即整镜像回收,store 本体 7 MB / 57.9 万唯一节点 | +| 2026-07-24 | Stage 5 上游配套:惰性 scope hook + transient 构造 SPI + `NodeReference` 结构性 API + 语料修正(commit `26db7a4`) | 下游(MachOSwiftSection)迁移过程中暴露的四个上游缺口,一并补齐。**① `NodePrinterTarget.pushTypeReferenceScope` 的节点参数改为 `@autoclosure () -> Node?`。** 问题:该 hook 是 main 上 `7fcb0f1` 引入的富 target 分组接缝(printer 在整段限定名打印外围推入 nominal 节点),签名收的是具体 `Node?`;store 路径要服务它就必须 `materializedNode` 物化一棵子树,而 `String` 这类忽略 scope 的 target 根本不读这个节点——等于让不用的人付物化代价,与「store 纯文本打印零分配」直接冲突。此前 07-24 的权宜做法是 store 路径一律传 `nil`(见上一条「以 `name as? Node` 桥接,store 路径传 nil」),代价是富 target 走 store 时永久丢失 scope identity。改为 autoclosure 后两难消解:默认实现与 `String` 从不求值(零成本,store 纯文本路径仍无物化),富 target 求值即拿到 `name.materializedNode`——**且只物化该 nominal reference 的小子树**,不是整棵符号树。**验证**:新增 `NodePrinterScopeTests`,其 `ScopeRecordingTarget` 精确镜像 `String` 的写入行为(保证打印流程逐字节同构)并额外记录 push/pop 事件序列;scope identity 以「被递交节点的 remangle 字符串」表达——这是结构性表征,跨表示相等当且仅当递交的子树结构相等,从而绕开 `Node` 与 `NodeReference` 无法直接比较的问题。对 5 个符号断言两条路径的输出文本与 scope 事件序列逐项相同,并断言至少递交过一个非 nil identity(防止「两边都传 nil 也能通过」的空洞绿灯)。**② `demangleAsNodeTransient` 增加 `symbolicReferenceResolver` 参数,并新增 `@_spi(Internals) Node.createTransient(...)` 工厂族。** 问题:Phase 3 的 cache-free 契约此前只覆盖 demangler 自身的构造点(全部走 `createNode(...)` + `internsLeaves: false`),但 symbolic reference 解析是**用户提供的闭包**在解析中途构造并回填节点——下游只能用公共的 `Node.create(...)`,而它必然写入 `NodeCache.shared`。结果是:一旦启用 symbolic reference(MachOSwiftSection 解析 mach-o 内嵌符号引用的常规路径),批量管线的 cache 增长归零成果当场失效。补齐后 transient 入口可直接透传 resolver,resolver 内部用 `createTransient` 构造 splice 节点,全链路不碰全局 cache、不取全局锁。`createTransient` 覆盖 `create` 的五个重载形态(contents / inlineChildren / 单孩子 / text / index),文档明确标注返回节点**非 canonical**(结构相等者是不同实例,`===` 共享假设不成立)。**③ `NodeReference` 三个结构性 API。** `init(interning:)`:把单棵外部树(瞬态 demangle 结果或手工合成树)interning 进一个**私有 mini store** 并引用其根——reference 持有 store 即保活,值自足且 `Sendable`,适合「值的生命周期长于源树」的持有场景;文档同时指明批量场景仍应直接驱动 `NodeStoreBuilder`,否则每棵树一个 arena,白丢跨符号去重。`structurallyEquals(_ other: NodeReference)`:同 store 直接比索引即得答案(hash-consing 使索引相等 ⇔ 结构相等,O(1)),跨 store 才走双树遍历,text 同样字节优先、回退 `String ==` 保 Unicode 规范等价——与既有的 `structurallyEquals(_ node: Node)` 语义完全对齐。`structuralHash(into:)`:与跨 store 结构相等自洽的结构哈希(kind + contents 判别位 + 孩子数 + 递归)。**为什么不能直接用固有 `Hashable`**:`NodeReference` 的 `hash(into:)` 组合的是 `ObjectIdentifier(store)` + 索引(store 身份基底),跨 store 的结构相等键会被劈成两个桶;下游那些「按节点结构做字典键、但内部存 reference」的值类型需要的正是这个可显式调用的结构哈希构件。**④ 语料修正:`$s7SwiftUI4TextV_10FoundationE9formatterAcA20LocalizedStringStyleV_xtcSyRzlufc` 是无效符号**(早于本分支就躺在语料里,`xcrun swift-demangle` 同样原样吐回、拒绝解析——尾部 `fc` 非分配构造器与该 extension 上下文不自洽),换成生成的真实跨模块 extension initializer `$s11ExampleBase0A4TextV0A6AddonsE9formatter7subjectAcA0A5StyleV_xtcSyRzlufC`(展开为 `(extension in ExampleAddons):ExampleBase.ExampleText.init(formatter:subject:)`),涉及 `NodeCacheTests` 与 `NodeStoreTests` 共 6 处;全部语料字面量此后统一经 `swift-demangle` 校验后再入库。**全量套件:410 tests / 19 suites 全绿** | From 22a39d1d697f36a23ba7389c2ae82a98348b1869 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 27 Jul 2026 18:06:12 +0800 Subject: [PATCH 19/23] fix(Demangling): avoid Array allocation on the transient node path The `internsLeaves: false` branch of `createNode(kind:contents:inlineChildren:)` converted `Node.Children` to an `Array`, forcing a heap allocation for 0-2 children on exactly the bulk path that inline storage exists to avoid. Forward `inlineChildren` directly, matching the interning branch right above it. Also drop the manifest cache-isolation marker comment from `Package.swift`. It worked only by making this branch's manifest text differ from main's, so it becomes self-contradictory and inert the moment it merges. Use `swift package purge-cache` for worktree cache collisions instead. --- Package.swift | 5 ----- .../Demangling/Main/Demangle/Demangler+NodeCreation.swift | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index 0436f27..f12187f 100644 --- a/Package.swift +++ b/Package.swift @@ -133,8 +133,3 @@ let package = Package( ), ], ) - -// NOTE: This branch intentionally diverges from main in this manifest so that -// SwiftPM's content-hash-keyed manifest cache cannot serve an evaluation made -// in the main checkout (where relative local-dependency candidates resolve to -// sibling checkouts) to builds running in a worktree, or vice versa. diff --git a/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift b/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift index 26fe9b8..4620122 100644 --- a/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift +++ b/Sources/Demangling/Main/Demangle/Demangler+NodeCreation.swift @@ -18,7 +18,7 @@ extension Demangler { func createNode(kind: Node.Kind, contents: Node.Contents = .none, inlineChildren: Node.Children) -> Node { internsLeaves ? Node.create(kind: kind, contents: contents, inlineChildren: inlineChildren) - : Node(kind: kind, contents: contents, children: Array(inlineChildren)) + : Node(kind: kind, contents: contents, inlineChildren: inlineChildren) } @inline(__always) From ea2ec287e47f114b59b410f235055671803cbb16 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 27 Jul 2026 18:06:22 +0800 Subject: [PATCH 20/23] docs(Demangling): sync NodeStore documentation with the shipped implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `DemanglingPrinter`'s SPI comment claimed the type-reference scope hooks receive nil on the store path; only `NodePrintContext.node` does. Document what the hooks actually deliver, and that store-backed printing materializes a fresh subtree per evaluation — so rich targets must key scopes by structure (the remangled string) rather than by `ObjectIdentifier`/`===`. Mirror the same correction in AGENTS.md, which described it as "full type-reference scope identity". - Proposal 0001: mark Implemented (Phase 1-3; Phase 4 deferred) and correct the API sketches that no longer match the code — `@frozen`, `payloadA`/`payloadB`, `demangleAsReference`, `reference(of:)`, `text -> Substring?`, and the intern table layout. Rewrite the build-flow section: the scratch-arena design was never shipped; the cache-free transient `Node` tree is what landed, and the measurements justifying that trade-off are now stated inline. - README: document `NodeStore` under Features and add a bulk-demangling usage section covering build/freeze, `NodeReference`, and `Node` interop. - Add `Documentations/NodeStoreArena.md` as the topic document for the arena work (design, trade-offs, and Phase 1/3 measurements), and `Documentations/README.md` as the directory index. --- AGENTS.md | 4 +- Documentations/NodeStoreArena.md | 140 ++++++++++++++++++ Documentations/README.md | 17 +++ README.md | 37 +++++ .../Demangling/Node/Printer/NodePrinter.swift | 12 +- evolution/0001-node-store-arena.md | 60 +++++--- 6 files changed, 243 insertions(+), 27 deletions(-) create mode 100644 Documentations/NodeStoreArena.md create mode 100644 Documentations/README.md diff --git a/AGENTS.md b/AGENTS.md index 8a0a00f..c77b7d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,8 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **`TypeDecoder`** (`TypeDecoder.swift`) — Walks a `Node` tree and builds abstract types via the `TypeBuilder` protocol. - **`Node.Rewriter`** (`Node+Rewriter.swift`) — Open class for bottom-up tree rewriting. Override `visit(_:)` to transform nodes. - **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. -- **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), `structurallyEquals(_ other: NodeReference)` (same-store O(1) via index equality, cross-store structural walk) plus `structuralHash(into:)` (structure-consistent hashing for value types that key dictionaries by node structure while storing references — `NodeReference`'s intrinsic `Hashable` is store-identity based), `NodeReference(interning:)` (interns one `Node` tree into a fresh private mini store — self-contained handles for values that outlive their source tree), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. -- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless — no rich target reads it on the store path). `NodePrinterTarget.pushTypeReferenceScope` takes its node as `@autoclosure () -> Node?`: scope-ignoring targets (`String`, the default implementation) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets (e.g. `SemanticString`) evaluate it and receive `materializedNode` — so store-backed rich printing gets full type-reference scope identity at the cost of materializing only nominal-reference subtrees. `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). +- **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001; design notes and measurements in `Documentations/NodeStoreArena.md`). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), `structurallyEquals(_ other: NodeReference)` (same-store O(1) via index equality, cross-store structural walk) plus `structuralHash(into:)` (structure-consistent hashing for value types that key dictionaries by node structure while storing references — `NodeReference`'s intrinsic `Hashable` is store-identity based), `NodeReference(interning:)` (interns one `Node` tree into a fresh private mini store — self-contained handles for values that outlive their source tree), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. +- **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless — no rich target reads it on the store path). `NodePrinterTarget.pushTypeReferenceScope` takes its node as `@autoclosure () -> Node?`: scope-ignoring targets (`String`, the default implementation) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets (e.g. `SemanticString`) evaluate it and receive `materializedNode`, materializing only the nominal-reference subtree. Note the delivered node is canonical **only on the `Node` path**: store-backed printing builds a fresh non-interned subtree per evaluation, so two pushes of the same store index are not `===`. Rich targets must key scopes by structure (e.g. the remangled string, as `SemanticString` does) — never by `ObjectIdentifier`/`===`. `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). - **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality diff --git a/Documentations/NodeStoreArena.md b/Documentations/NodeStoreArena.md new file mode 100644 index 0000000..3a27530 --- /dev/null +++ b/Documentations/NodeStoreArena.md @@ -0,0 +1,140 @@ +# NodeStore:Arena 式紧凑节点存储 + +日期:2026-07-27 + +对应提案:`evolution/0001-node-store-arena.md`(Phase 1–3 已落地) +前置优化:`Documentations/SubtreeInterning.md`(全子树 hash-consing) + +## 动机 + +`SubtreeInterning.md` 结尾把「Arena + 索引式存储」列为被推迟的方向,本次即是它的兑现。 + +全子树 hash-consing 把 49k 符号语料的解析驻留从 39.5 MB 压到 12.9 MB,但剩下的开销已经无法在 class 形态下继续消除: + +- `Node` 是 `final class`,每实例 48 字节 —— 其中 16 字节是对象头,41 字节的实际内容还要向上取整到 48 字节的 malloc 桶; +- 每个节点一次独立 malloc,每次传递一次引用计数增减; +- 孩子引用是 8 字节指针。 + +对照 C++ `Demangle::Node`:24 字节/节点 + bump allocator。**差距全部来自 class 这个形态本身**,不是实现细节。 + +把 `Node` 改成带 `[Node]` 的 struct 并不能解决:children 数组仍然是逐节点堆分配(32 字节缓冲头 + CoW 引用计数),再加上大 struct 的拷贝成本,内存和 CPU 双双倒退。struct 的价值在于**精确布局 + 可平铺进连续缓冲**,所以正确形态是 arena + 索引。 + +同时,`Node` 是公共 API,下游(MachOSwiftSection、RuntimeViewer)大量依赖,不能就地重写。因此本方案是**并存**而非替换:新增一套存储层,老的 `Node` 路径行为一字不变。 + +## 范围 + +新增 `Sources/Demangling/Store/`: + +- `CompactNode.swift` — 12 字节平铺节点表示; +- `NodeStore.swift` — 冻结后的不可变存储; +- `NodeStoreBuilder.swift` — `~Copyable` 构建器,插入即 hash-consing; +- `NodeReference.swift` — 16 字节值句柄 + `ChildrenView`; +- `DemanglingNode.swift` — `Node` 与 `NodeReference` 共同遵循的只读树协议。 + +改造既有代码: + +- `Node/Printer/NodePrinter.swift` — printer 引擎泛型化为 `DemanglingPrinter`; +- `Main/TypeDecoder/TypeDecoder.swift` — 泛型化为 `TypeDecoderEngine`; +- `Main/Demangle/Demangler+NodeCreation.swift` — demangler 的节点构造 seam,支持绕开全局缓存。 + +**公共 API 零破坏**:`Node`、`NodeBuilder`、`NodeCache`、`demangleAsNode`、`NodePrinter`、`TypeDecoder`、`TypeBuilder` 的签名与行为全部保持。 + +## 关键设计 + +### 12 字节的节点 + +```swift +struct CompactNode { // size 12, alignment 4 + var kindAndPayloadKind: UInt16 // bit 0-8: kind 序号;bit 9-11: payloadKind + var payloadWord0: UInt32 + var payloadWord1: UInt32 +} +``` + +两个 payload 字的含义由 `payloadKind` 决定,沿用 `Node.Payload` 既有的互斥不变量(contents 与 children 不共存)。49k 语料实测分布:`oneChild` ~67%、`manyChildren` ~16%、`twoChildren` ~12%、`text` ~5%——**近八成节点只有零到一个孩子**,所以把两个孩子内联进 payload、只让 3+ 孩子外溢到 edges 缓冲,是很划算的选择。 + +每个 store 三块连续缓冲:`nodes`(主 arena,append 即 bump 分配)、`edges`(3+ 孩子节点的孩子索引区段)、`textBytes`(去重后的 UTF-8 字符串表)。孩子引用是 4 字节索引而非 8 字节指针——这正是相对 C++ 24 字节实现的反超点。 + +容量上限:42.9 亿节点、4 GB 字符串表。越界走 `precondition` 失败,不静默截断。 + +### 插入即 hash-consing,intern 表不留在成品里 + +`NodeStoreBuilder` 对每个插入的节点做 hash-consing:结构相等的子树收敛到同一个索引。因为孩子总是先于父节点被 intern,父节点的键可以直接用**已规范化的孩子索引**比较,无需递归结构哈希——与 `NodeCache.internTreeUnsafe` 同一套经典技巧。 + +intern 表本身是三张 open-addressing 槽数组(普通节点 / 3+ 孩子节点 / 文本),**每槽只存 4 字节索引,键按需从平铺缓冲区取回比较**,不为键单独保存一份拷贝。早期用字典(键各自持有 12 字节 compact 值、子索引数组或 `String` 副本)时,intern 表本身在全语料上要占约 10 MB 量级;改为槽数组后降到约 2 MB。 + +`consuming func freeze()` 消费 builder、丢弃全部 intern 表,只留三块缓冲。这带来两个结果: + +- 「构建完成后不可变」由类型系统保证,不再依赖锁和文档契约; +- 冻结后的 store **无法再做哈希查找**——这解释了为什么 `NodeReference` 需要 `structurallyEquals` 这类线性比较 API(见下文)。 + +### 构建流程:cache-free 临时树,而不是直写 arena + +提案原稿设想让 demangler 直接写入 arena。实际落地保留了 `Node` 作为中间的「第一代空间」: + +1. `demangleAsNodeTransient` 以 `internsLeaves: false` 解析,产出的临时 `Node` 树完全不碰 `NodeCache.shared`; +2. `builder.intern(tree)` 自底向上把可达节点 intern 进 arena —— **去重与垃圾回收是同一个 pass**; +3. 临时树失去引用即被 ARC 回收。 + +之所以没做直写:demangler 的解析逻辑(回填、substitution 复用)建立在引用语义上,改为索引式直写等于重写全部约 594 个构造点。而实测表明这一层临时成本可以忽略——234k 符号语料上构建期常驻增量仅 9.9 MB(≈ 留存 + 约 1 MB 瞬态),且构建耗时反而**快于** interning 的 `Node` 路径。 + +为此,demangler 的全部节点构造点收敛到 `createNode(...)` 这一个 seam 上。**新增构造点必须走 `createNode(...)`,不能直接调 `Node.create(...)`**,否则 cache-free 契约会被悄悄破坏。同理,用户提供的 symbolic reference resolver 需要用 `@_spi(Internals) Node.createTransient(...)` 构造回填节点——否则一启用 symbolic reference,批量管线的 cache-free 成果当场失效。 + +### 一套引擎,两种表示 + +`DemanglingNode` 是 `Node` 和 `NodeReference` 共同遵循的只读树协议(`kind`/`text`/`index`/`children` 加一个抽象的 `printCacheIdentity`)。成员名刻意与 `Node` 现有 API 一致,这样泛型引擎的函数体不论特化到哪种表示都是同一份代码。 + +据此,2000 行以上的 printer 引擎泛型化为 `DemanglingPrinter`,`TypeDecoder` 泛型化为 `TypeDecoderEngine`,公共 `NodePrinter` / `TypeDecoder` 退化为薄包装。store 路径的打印是**零物化**的:全量 dyld cache 语料 × 3 套选项,store 打印与 `Node` 打印逐字节零差异。 + +派生辅助(`isSimpleType`、`needSpaceBeforeType`、`isIdentifier(desired:)`、`isSwiftModule`、`isKind(of:)`、`children.second` 等)**只保留在协议扩展上**。它们是扩展成员而非协议要求,泛型引擎内部静态派发恒走扩展版本——在具体类型上再留一份拷贝不会被引擎调用,只会静默漂移。遍历机制(preorder/inorder/postorder/levelorder、`first(of:)`/`all(of:)`/`contains`)同理,已收敛为单一泛型实现。 + +**`Remangler` 刻意保持 `Node` 引擎**。审计确认它的遍历过程中节点构造是承重的:`getUnspecialized` 剥掉泛型后要把结果回流给 `mangle`,SIL box 布局要构造 wrapper,两者都共享 substitution 状态——这与 C++ remangler 的设计同构。对这样一个逐字节对齐关键的组件做「无构造」重设计不划算,因此 `mangleAsString(some DemanglingNode)` 经 `materializedNode` 桥接。remangle 的输出本来就是新分配的 `String`,属于瞬态成本,与常驻内存目标无关。 + +### 读路径的零分配细节 + +`NodeReference.textUTF8` 暴露字符串表字节的零拷贝 `ArraySlice` 视图。printer 的 sugar 检测热路径(判断 Swift module、Optional/Array/Dictionary)此前每次检查都要构造一个 `String`;升级为协议要求 `isIdentifier(desired:)` / `isSwiftModule` 并由 `NodeReference` 以字节比较见证后,store 路径上这些检查不再分配。非 ASCII 的比较目标回退到 `String` 比较,以保持 Unicode 规范等价语义。 + +### 跨表示的相等与哈希 + +冻结后 intern 表已丢弃,无法哈希查找,于是提供三个显式 API: + +- `structurallyEquals(_ node: Node)` —— 零物化的跨表示结构相等,语义对齐 `Node.==`;服务「手里有一棵外部 demangle 出来的 `Node`,要在 `NodeReference` 字典键中找到它」这个场景; +- `structurallyEquals(_ other: NodeReference)` —— 同 store 直接比索引即得答案(hash-consing 使索引相等 ⇔ 结构相等,O(1)),跨 store 才走双树遍历; +- `structuralHash(into:)` —— 与上述结构相等自洽的结构哈希。 + +**为什么不能直接用固有的 `Hashable`**:`NodeReference.hash(into:)` 组合的是 `ObjectIdentifier(store)` + 索引,是 store 身份基底的。跨 store 的结构相等键会被劈成两个桶。那些「按节点结构做字典键、内部却存 reference」的下游值类型,需要的正是这个可显式调用的结构哈希构件。 + +## 取舍与影响面 + +- **物化出来的树不是 canonical**。`materialize()` 重建的是一棵全新的、不进 `NodeCache` 的 `Node` 树。因此 `===` 共享假设在这条路径上**不成立**:同一个 store 索引物化两次得到两个不同实例。任何按节点身份(`ObjectIdentifier` / `===`)做关联的消费者,在 store 路径上必须改为按结构关联(例如用 remangle 后的字符串作键)。`NodePrinterTarget.pushTypeReferenceScope` 收到的节点同样受此约束。 + - 物化本身保留 DAG 共享:按索引 memo,同一子树只物化一次并复用实例。否则重度替换共享的符号会被指数展开(SwiftUI `View.Body` 量级即几十万节点),且展开树上按身份键的打印缓存会全部脱靶。 +- **索引只对签发它的 store 有效**。`NodeStore.NodeIndex` 是裸值,不带 store 反向引用。跨 store 混用索引不会被可靠拦截,只会读到无关子树。同时持有多个 builder 时需自行保证不混用。 +- **kind 序号不是序列化格式**。它取自 `Node.Kind.allCases` 中的位置,仅在单次进程运行内稳定。9 位空间共 512 槽,当前 `Node.Kind` 有 373 个 case,余量 139。本项目持续跟进 Apple 工具链的 `Demangle::Node::Kind`,超出时由 `kindsByStoreOrdinal` 的 `precondition` 拦截——注意那是**运行时**失败且位于 lazy static 中,只在首次触及 store 路径时才暴露。Phase 4 的持久化格式必须自带稳定的 kind 映射表。 +- **builder 是单写者**。`~Copyable` 保证了这一点,好处是全程无锁,代价是不能跨线程共享。多线程批量场景应每线程一个 builder,最后合并——合并 API 尚未实现。 +- **`Node` 路径完全不受影响**。默认的 `demangleAsNode` 仍然走 `NodeCache` 的叶 + 全树 interning,行为、输出、身份语义一字未变。 + +## 实测收益 + +Phase 1(49k 符号语料):唯一节点 201,876,与 `NodeCache` 全树 hash-consing 的计数逐一吻合(交叉验证了两套 intern 实现的正确性);平铺存储 3.0 MB(nodes 2.4 + edges 0.43 + text 0.26)。 + +Phase 3 验收(本机 dyld cache SwiftUI 语料 234,232 符号,debug 构建): + +| 指标 | 实测 | 目标 | +|---|---|---| +| 唯一节点 | 619,688 | — | +| 平铺存储 | 8.75 MB(nodes 7.4 + edges 0.75 + text 0.57) | — | +| 每唯一节点 | 14.1 字节 | ≤ 16 | +| 每符号 | 37 字节 | ≤ 64 | +| 构建耗时 | 25.3s(`Node` 路径 28.5s) | ≤ 1.2× 基线 | +| 构建期常驻增量 | 9.9 MB | — | + +下游(MachOSwiftSection)迁移实测:构建管线换成 transient + intern 之后,`NodeCache` 增长归零(此前每镜像 +1.9 万叶节点 / +56 万子树),store 本体 7 MB / 57.9 万唯一节点,`Storage` 释放即整镜像回收。 + +正确性:全量 dyld cache 对齐测试 0 失败;49k 语料 × 3 套打印选项,store 与 `Node` 路径逐字节零差异。 + +## 后续方向(未实施) + +- **Phase 4 — 平铺序列化**:三块缓冲直接二进制序列化 / mmap 加载(接近 memcpy 量级),把整个 dyld cache 的解析结果持久化为符号数据库。需先定义稳定的 kind 映射与格式版本号。 +- **分片并行 store 与终态合并**:配合每线程一个 builder。 +- **`Span` / `UTF8Span`(Swift 6.2)借用视图**:进一步消除 `ArraySlice` 层的开销。 +- **`NodeReference` 层的 `Node.Rewriter` 等价物**:写时拷贝进新 store。 diff --git a/Documentations/README.md b/Documentations/README.md new file mode 100644 index 0000000..e6fa272 --- /dev/null +++ b/Documentations/README.md @@ -0,0 +1,17 @@ +# 文档索引 + +本目录收录内部专题文档:每篇对应一次实质性的架构演进或一块独立的子系统。 + +## 专题 + +- [SubtreeInterning.md](SubtreeInterning.md) — 全子树 interning(hash-consing)内存优化。把 interning 从叶节点扩展到全部子树,49k 符号语料解析驻留 39.5 MB → 12.9 MB。 +- [NodeStoreArena.md](NodeStoreArena.md) — `NodeStore` arena 式紧凑存储。节点平铺进连续缓冲,每节点 12 字节、无对象头、无引用计数;printer 与 TypeDecoder 泛型化后可零物化直读。 + +两篇是承接关系:`SubtreeInterning` 把 class 形态下能做的去重做到头,`NodeStoreArena` 兑现了它结尾列为「待将来单独评估」的 arena 方向。 + +## 其他位置的文档 + +- `evolution/` — 演进提案(设计意图 + 决策日志)。`0001-node-store-arena.md` 是 `NodeStoreArena.md` 的提案原文。 +- `docs/AlignmentGaps.md` — 与上游 Swift 编译器 `Demangling` 源码的对齐缺口追踪。 +- `AGENTS.md` / `CLAUDE.md`(仓库根) — 面向编码 agent 的架构速查。 +- `README.md`(仓库根) — 面向使用者的英文说明与用法示例。 diff --git a/README.md b/README.md index b41e78b..4f5cf94 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This project is derived from [CwlDemangle](https://github.com/mattgallagher/CwlD - **Decode types** from mangled nodes via a pluggable `TypeBuilder` protocol - **Traverse & rewrite** trees with built-in iterators and `Node.Rewriter` - **Node interning (hash-consing)** via `NodeCache` — structurally equal subtrees share one instance, reducing memory ~4x for whole-binary demangling +- **Compact bulk storage** via `NodeStore` — an arena packing each node into 12 flat bytes with no object header, reference counting, or per-node allocation; printing and type decoding read straight from it without materializing a `Node` tree - Supports all mangling prefixes: `_T0`, `_$S`, `_$s`, `$S`, `$s`, `$e`, `_$e`, `@__swiftmacro_` - Swift 6 strict concurrency — all public types are `Sendable` @@ -240,6 +241,42 @@ For one-off demangling where the cache should not grow, opt out per call: let node = try demangleAsNode(symbol, internsSubtrees: false) ``` +### Bulk Demangling with NodeStore + +When demangling a whole binary and keeping every result, `NodeStore` stores nodes in a flat arena instead of as individual class instances: 12 bytes per node, no object header, no reference counting, no per-node allocation. Build with `NodeStoreBuilder`, then `freeze()` into an immutable, `Sendable` store: + +```swift +var builder = NodeStoreBuilder() +var rootIndices: [NodeStore.NodeIndex] = [] +for symbol in symbols { + rootIndices.append(try builder.demangle(symbol)) +} +let store = builder.freeze() +``` + +Nodes are addressed by `NodeReference`, a 16-byte value handle that mirrors `Node`'s accessors. Printing and type decoding read directly from the arena — no `Node` tree is materialized: + +```swift +let reference = store.reference(at: rootIndices[0]) +let readable = reference.print(using: .default) + +for child in reference.children where child.kind == .identifier { + // `textUTF8` borrows the store's string table without allocating + print(child.text ?? "") +} +``` + +The builder hash-conses on insert, so structurally equal subtrees collapse to one index and `NodeReference` equality is O(1) within a store. This path never touches `NodeCache.shared`, so bulk indexing leaves global state untouched. + +Interop with the `Node` API stays available in both directions — `builder.intern(existingNode)` imports a tree, and `reference.materialize()` rebuilds a standalone one: + +```swift +let index = builder.intern(try demangleAsNode(symbol)) +let node = store.reference(at: index).materialize() +``` + +Measured on a SwiftUI dyld-cache corpus of 234,232 symbols: 619,688 unique nodes in 8.75 MB of flat storage (14.1 bytes per unique node), built no slower than the `Node` path. + ## Acknowledgments - [CwlDemangle](https://github.com/mattgallagher/CwlDemangle) by Matt Gallagher — the original Swift translation of the demangler diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index 918e87a..b492566 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -4,10 +4,14 @@ /// /// SPI note: exposed as `@_spi(Internals)` so deep consumers (MachOSwiftSection) /// can print `NodeReference` trees into custom rich targets. On the store path -/// `NodePrintContext.node` and the type-reference scope hooks receive nil -/// (`name as? Node`); a rich target that keys on the node must account for -/// that until the hooks are abstracted over `DemanglingNode`. Wrap calls in -/// `StackSafeExecutor.execute` for deeply nested symbols. +/// `NodePrintContext.node` receives nil (`name as? Node`); a rich target that +/// keys on it must account for that until the context is abstracted over +/// `DemanglingNode`. The type-reference scope hooks do receive a node on both +/// paths, but only the `Node` path delivers a canonical instance: store-backed +/// printing materializes a fresh subtree per evaluation, so scopes must be +/// keyed by structure (e.g. the remangled string) rather than by +/// `===`/`ObjectIdentifier`. Wrap calls in `StackSafeExecutor.execute` for +/// deeply nested symbols. @_spi(Internals) public struct DemanglingPrinter: Sendable { /// Mirrors ``swift::Demangle::NodePrinter::MaxDepth`` from diff --git a/evolution/0001-node-store-arena.md b/evolution/0001-node-store-arena.md index d069a35..d388305 100644 --- a/evolution/0001-node-store-arena.md +++ b/evolution/0001-node-store-arena.md @@ -2,9 +2,9 @@ - **Proposal**: 0001 - **Author**: Mx-Iris -- **Status**: In Progress +- **Status**: Implemented(Phase 1–3 已落地;Phase 4 平铺序列化推迟) - **Date**: 2026-07-23 -- **Last Updated**: 2026-07-24 +- **Last Updated**: 2026-07-27 - **Branch**: `feature/node-store` - **Related**: `Documentations/SubtreeInterning.md`(前置优化:全子树 hash-consing,已合入 main `5788472`) @@ -28,18 +28,20 @@ ### 存储布局 ```swift -@frozen struct CompactNode { // size 12, alignment 4, 无对象头 - var kindAndPayloadKind: UInt16 // bit 0-8: kind(~300 种,9 位) +struct CompactNode { // size 12, alignment 4, 无对象头 + var kindAndPayloadKind: UInt16 // bit 0-8: kind 序号(9 位,512 槽) // bit 9-11: payloadKind(6 种,3 位) // bit 12-15: 保留 - var payloadA: UInt32 - var payloadB: UInt32 + var payloadWord0: UInt32 + var payloadWord1: UInt32 } ``` -`payloadA/payloadB` 按 `payloadKind` 解释(沿用现有 `Payload` 的互斥不变量:contents 与 children 不共存): +落地形态是 `@usableFromInline struct`(非 public、非 `@frozen`):`NodeStore` 的公共面只暴露 `NodeReference`,`CompactNode` 是内部布局细节。kind 序号取自 `Node.Kind.storeOrdinal`(`allCases` 中的位置),**仅在单次进程运行内稳定,不是序列化格式**——Phase 4 的持久化格式须自带稳定的 kind 映射表。 -| payloadKind | payloadA | payloadB | 覆盖比例(49k 语料实测) | +`payloadWord0/payloadWord1` 按 `payloadKind` 解释(沿用现有 `Payload` 的互斥不变量:contents 与 children 不共存): + +| payloadKind | payloadWord0 | payloadWord1 | 覆盖比例(49k 语料实测) | |---|---|---|---| | `none` | — | — | 与 leaf 部分重叠 | | `index` | `UInt64` 低 32 位 | 高 32 位 | 少量 | @@ -53,7 +55,7 @@ - `nodes: ContiguousArray` — 主 arena,`append` 即 bump 分配; - `edges: ContiguousArray` — 仅 3+ 孩子节点使用的孩子索引连续区段; - `textBytes: ContiguousArray` — 字符串表,全部 identifier 的 UTF-8 字节连续存放并去重; -- intern 哈希表 — 把已实现的 hash-consing 从 `ObjectIdentifier` 键迁移为索引键:`(kindAndPayloadKind, payloadA, payloadB)` 经孩子索引规范化后即天然唯一,键就是 12 字节本身。 +- intern 表 — 把已实现的 hash-consing 从 `ObjectIdentifier` 键迁移为索引键:`(kindAndPayloadKind, payloadWord0, payloadWord1)` 经孩子索引规范化后即天然唯一。落地形态是三张 open-addressing 槽数组(节点 / 多孩子节点 / 文本),每槽仅 4 字节索引,键按需从平铺缓冲区取回比较——不再为键单独存一份拷贝(Phase 3 的 intern 表瘦身,见 Decision Log)。表随 `freeze()` 丢弃,不进入冻结后的 store。 容量边界:`UInt32` 索引上限 42.9 亿节点、字符串表 4 GB——对单个 store(哪怕整个 dyld cache)远够;越界走 `precondition` 失败而非静默截断。 @@ -61,43 +63,59 @@ ### 类型与 API 面 +以下为落地签名(与实现一致): + ```swift -/// 冻结后的不可变符号库。构建完成即 Sendable,读路径零锁。 +/// 构建期的单写者。~Copyable,consuming freeze() 交出不可变 store。 +public struct NodeStoreBuilder: ~Copyable, Sendable { + public mutating func demangle(_ mangled: String, isType: Bool = false) throws(DemanglingError) -> NodeStore.NodeIndex + public mutating func intern(_ node: Node) -> NodeStore.NodeIndex // 导入现有树(intern 拷贝) + public mutating func intern(kind: Node.Kind, children: [NodeStore.NodeIndex]) -> NodeStore.NodeIndex + public consuming func freeze() -> NodeStore +} + +/// 冻结后的不可变符号库。Sendable,读路径零锁。 public final class NodeStore: Sendable { - public func demangleAsReference(_ mangled: String) throws(DemanglingError) -> NodeReference // Phase 3 - public func reference(of node: Node) -> NodeReference // 导入现有树(intern 拷贝) + public func reference(at nodeIndex: NodeIndex) -> NodeReference } /// 轻量句柄:store 引用 + 索引,16 字节值类型。 public struct NodeReference: Hashable, Sendable { public var kind: Node.Kind { get } - public var text: Substring? { get } // 借用字符串表,零拷贝 + public var text: String? { get } // 从字符串表解码 + public var textUTF8: ArraySlice? { get } // 借用字符串表字节,零拷贝 public var index: UInt64? { get } - public var children: ChildrenView { get } // RandomAccessCollection - public func materialize() -> Node // 物化为现有 Node 树(互操作出口) + public var children: ChildrenView { get } // RandomAccessCollection + public func materialize() -> Node // 物化为现有 Node 树(互操作出口) public func print(using options: DemangleOptions) -> String } ``` +解析入口最终定在 builder 而非 store(提案原稿写的是 `NodeStore.demangleAsReference`):冻结后的 store 不可变,解析必然要写入,天然属于 builder。`text` 保持 `String?` 以镜像 `Node.text` 的语义(含 `.dependentGenericParamType` 的泛型名合成,这是 printer 依赖的行为),零拷贝需求由并列的 `textUTF8` 满足。 + - 构建期使用 `NodeStoreBuilder`(`~Copyable`):单写者约束由编译器保证,`consuming func freeze() -> NodeStore` 完成冻结——把现在靠 `NSLock` + 文档契约维持的「构建后不可变」升级为类型系统保证; - `Hashable`/`==` 基于 (store identity, index):因为 store 内全量 hash-consed,索引相等 ⇔ 结构相等,比较从 O(树) 降为 O(1); - 读路径后续用 `Span` / `UTF8Span`(Swift 6.2)暴露孩子区段与文本的借用视图,零分配零拷贝。 ### 构建流程(两代空间) -1. Demangler 把节点写入**每符号复用的 scratch arena**(容量保留、每符号 `removeAll(keepingCapacity: true)`)——解析中产生的临时节点零成本丢弃,等价于 C++ 每符号销毁 `NodeFactory`; -2. 解析成功后,从根出发把可达节点自底向上 intern 拷贝进持久 store——**去重与垃圾回收是同一个 pass**,键规范化逻辑与已合入的 `internTreeUnsafe` 完全同构; -3. scratch 重置,处理下一符号。 +原稿设想的是让 Demangler 直写一块每符号复用的 scratch arena。落地形态改为「cache-free 临时 `Node` 树」作为第一代空间: + +1. `demangleAsNodeTransient` 以 `internsLeaves: false` 解析,构造出的临时 `Node` 树完全不碰 `NodeCache.shared`——无叶节点泄漏、无全局锁竞争; +2. `builder.intern(tree)` 从根出发把可达节点自底向上 intern 进持久 arena——**去重与垃圾回收是同一个 pass**,键规范化逻辑与已合入的 `internTreeUnsafe` 完全同构; +3. 临时树失去引用即被 ARC 回收,处理下一符号。 + +保留 `Node` 作为第一代空间,是因为 Demangler 的解析逻辑(回填、substitution 复用)建立在引用语义之上,改为直写索引式 arena 等于重写全部 ~594 个构造点。实测该取舍成本可忽略:234k 符号语料上构建期 phys_footprint 增量 9.9 MB ≈ 留存 + ~1 MB 瞬态,且 store 构建 25.3s 反而快于 interning `Node` 路径的 28.5s(详见 Decision Log 的 Phase 3 验收)。 -并行策略:每线程一个 scratch arena;持久 store 的 intern 写入初期沿用单锁(与现状一致),若成为瓶颈再演进为分片锁或每线程局部 store + 终态合并。 +并行策略:`NodeStoreBuilder` 是 `~Copyable` 单写者,天然无锁但也不可共享。多线程批量场景应每线程一个 builder,终态合并——合并 API 尚未实现,列为 Future Direction。 ### 渐进式迁移分期 每个阶段独立可交付、测试全绿、`Node` API 始终不动: -- **Phase 1 — 存储层与互操作**:`CompactNode` / `NodeStoreBuilder` / `NodeStore` / `NodeReference`;`reference(of:)` 导入现有 `Node` 树,`materialize()` 导出。打印/remangle 暂走物化慢路径。验收:任意树 导入→导出 与原树 `==`;导入两棵结构相等的树得到同一索引。 +- **Phase 1 — 存储层与互操作**:`CompactNode` / `NodeStoreBuilder` / `NodeStore` / `NodeReference`;`intern(_:)` 导入现有 `Node` 树,`materialize()` 导出。打印/remangle 暂走物化慢路径。验收:任意树 导入→导出 与原树 `==`;导入两棵结构相等的树得到同一索引。 - **Phase 2 — 零物化读路径**:将 `NodePrinter` / `Remangler` / `TypeDecoder` 的树访问抽象为协议(kind/text/index/children 四个只读需求),`Node` 与 `NodeReference` 双双 conform;打印与 remangle 直接从 store 读,不再物化。验收:全量 dyld cache 对齐测试在 `NodeReference` 路径下 0 失败。 -- **Phase 3 — 解析直写 arena**:`Demangler` 的节点构造抽象为存储策略(默认策略维持现有 `Node` 行为不变;store 策略直写 scratch arena),提供批量入口 `NodeStore.demangleAsReference(_:)`。此阶段起,批量场景完全绕开 class 分配。验收:内存达标(49k 语料 ≤6 MB)、吞吐不劣于现状 1.2 倍。 +- **Phase 3 — cache-free 批量解析**:`Demangler` 的节点构造收敛到 `createNode(...)` seam,`internsLeaves: false` 时完全绕开 `NodeCache.shared`;批量入口 `NodeStoreBuilder.demangle(_:)` 走「cache-free 临时 `Node` 树 → intern 进 arena → 丢弃临时树」。此阶段起批量场景不再向全局缓存写入任何东西。验收:内存达标(49k 语料 ≤6 MB)、吞吐不劣于现状 1.2 倍。 - **Phase 4(可选)— 平铺序列化**:store 的几个缓冲直接二进制序列化/反序列化(接近 memcpy 量级),支持 mmap 加载——符号数据库能力,为 RuntimeViewer 类工具缓存整个 dyld cache 的解析结果。 ### 与现有 NodeCache 的关系 From ea4101ba37d1e447a5b9d21041300b724da98aec Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 27 Jul 2026 19:12:48 +0800 Subject: [PATCH 21/23] perf(Demangling): reuse large-stack threads and print within a stack budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `StackSafeExecutor.currentThreadHasSufficientStack` requires 2MB of *remaining* stack, while a Swift Concurrency cooperative worker and a libdispatch worker each get a 512KB stack in total. The check is therefore never satisfied off the main thread: every demangle, remangle and print created — and joined — a fresh 8MB-stack `Thread`. Measured at ~50us per call, against a ~32us demangle and a ~10us print. Two independent layers: 1. `LargeStackThreadPool` reuses long-lived 8MB workers (spun up on demand, retired after a 30s idle timeout) instead of one thread per call. Covers demangle, remangle and print alike without touching any engine. A worker runs on 8MB, so nested calls take the inline branch and never re-submit — a saturated pool cannot deadlock. 2. `executeWithinStackBudget` runs the recursion inline and falls back to a worker only when it actually nears the stack end. The print path is wired in through `DemanglingPrinter.printRootWithinStackBudget`, which probes the real stack pointer at the existing `printName` convergence point — a fixed depth threshold would have to guess per-frame size, which varies by `Target`. A partial result is discarded wholesale. `Demangler` has no single recursion convergence point and `Remangler`'s is not wired up, so both take layer 1 only. 2000 iterations on a 512KB-stack thread, before -> after: print 127.5ms -> 17.5ms (7.3x, now below the main thread) demangle 162.5ms -> 74.8ms (2.2x) 6400 demangles across 32 tasks: 162.7ms -> 99.6ms Verified against MachOSwiftSection: `MachOSwiftSectionTests` + `SwiftLayoutTests` failure sets are identical before and after (157, all pre-existing on that branch); `SwiftDumpTests` / `SwiftPrintingTests` / `MachOSymbolsTests` pass except one pre-existing snapshot failure. --- AGENTS.md | 1 + Documentations/README.md | 1 + Documentations/StackSafeExecution.md | 98 ++++++++++++ .../Node/Node+CustomStringConvertible.swift | 5 +- .../Demangling/Node/Printer/NodePrinter.swift | 38 +++++ Sources/Demangling/Store/DemanglingNode.swift | 5 +- Sources/Demangling/Store/NodeReference.swift | 5 +- .../Demangling/Utils/StackSafeExecutor.swift | 143 ++++++++++++++++-- 8 files changed, 280 insertions(+), 16 deletions(-) create mode 100644 Documentations/StackSafeExecution.md diff --git a/AGENTS.md b/AGENTS.md index c77b7d8..9843f23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,7 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. - **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001; design notes and measurements in `Documentations/NodeStoreArena.md`). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), `structurallyEquals(_ other: NodeReference)` (same-store O(1) via index equality, cross-store structural walk) plus `structuralHash(into:)` (structure-consistent hashing for value types that key dictionaries by node structure while storing references — `NodeReference`'s intrinsic `Hashable` is store-identity based), `NodeReference(interning:)` (interns one `Node` tree into a fresh private mini store — self-contained handles for values that outlive their source tree), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. - **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless — no rich target reads it on the store path). `NodePrinterTarget.pushTypeReferenceScope` takes its node as `@autoclosure () -> Node?`: scope-ignoring targets (`String`, the default implementation) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets (e.g. `SemanticString`) evaluate it and receive `materializedNode`, materializing only the nominal-reference subtree. Note the delivered node is canonical **only on the `Node` path**: store-backed printing builds a fresh non-interned subtree per evaluation, so two pushes of the same store index are not `===`. Rich targets must key scopes by structure (e.g. the remangled string, as `SemanticString` does) — never by `ObjectIdentifier`/`===`. `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). +- **`StackSafeExecutor`** (`Utils/StackSafeExecutor.swift`) — the stack-safety wrapper every recursive entry point (demangle / remangle / print) goes through. `currentThreadHasSufficientStack` requires 2MB of *remaining* stack, while a Swift Concurrency cooperative worker and a libdispatch worker each get a 512KB stack **in total** — so off the main thread the large-stack branch is taken unconditionally, for every call. Two layers keep that affordable: (a) `LargeStackThreadPool` reuses long-lived 8MB-stack workers (created on demand, retired after a 30s idle timeout) instead of creating and joining a `Thread` per call — this covers demangle, remangle and print alike, with no engine changes; a worker never re-submits into the pool (it runs on 8MB, so nested calls take the inline branch), so a saturated pool cannot deadlock. (b) `executeWithinStackBudget(budgetedAttempt:unbudgetedFallback:)` runs the recursion inline on the current thread and only falls back to a worker when it actually approaches the stack end — the budgeted attempt gets a `stackFloorAddress` (thread stack base + 64KB margin) and returns `nil` to give up. Only the **print** path is wired into (b), via `DemanglingPrinter.printRootWithinStackBudget(_:stackFloorAddress:)`, which probes the real stack pointer at the existing `printName` convergence point (a fixed depth threshold would have to guess per-frame size, which varies by `Target`); a partial result is discarded wholesale, so the residue left in `target`/`printCache` never escapes. `Demangler` has no single recursion convergence point and `Remangler`'s (`mangle(_:depth:)`) is not wired up yet — both still take (a) only. Do NOT wire a new engine into (b) without a convergence point that covers *every* recursion path: incomplete coverage trades a slow-but-safe call for an overflow. Measurements and the full rationale: `Documentations/StackSafeExecution.md`. - **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality diff --git a/Documentations/README.md b/Documentations/README.md index e6fa272..e675bd0 100644 --- a/Documentations/README.md +++ b/Documentations/README.md @@ -6,6 +6,7 @@ - [SubtreeInterning.md](SubtreeInterning.md) — 全子树 interning(hash-consing)内存优化。把 interning 从叶节点扩展到全部子树,49k 符号语料解析驻留 39.5 MB → 12.9 MB。 - [NodeStoreArena.md](NodeStoreArena.md) — `NodeStore` arena 式紧凑存储。节点平铺进连续缓冲,每节点 12 字节、无对象头、无引用计数;printer 与 TypeDecoder 泛型化后可零物化直读。 +- [StackSafeExecution.md](StackSafeExecution.md) — `StackSafeExecutor` 的线程复用与乐观内联。原本在任何非主线程上每次调用都新建 8 MB 栈线程;改为常驻线程池 + 打印路径栈预算回退后,512 KB 栈线程上的打印提速 7.3 倍。 两篇是承接关系:`SubtreeInterning` 把 class 形态下能做的去重做到头,`NodeStoreArena` 兑现了它结尾列为「待将来单独评估」的 arena 方向。 diff --git a/Documentations/StackSafeExecution.md b/Documentations/StackSafeExecution.md new file mode 100644 index 0000000..2717ca6 --- /dev/null +++ b/Documentations/StackSafeExecution.md @@ -0,0 +1,98 @@ +# 栈安全执行:从「每次新建线程」到「复用 + 乐观内联」 + +## 动机 + +`StackSafeExecutor` 是本库所有递归入口(demangle / remangle / print)的栈安全包装。它的判断逻辑是: + +```swift +private static let minimumRequiredStackSize = 2 * 1024 * 1024 // 2MB + +private static var currentThreadHasSufficientStack: Bool { + let remainingStackSpace = currentAddress - Int(bitPattern: stackBase) + return remainingStackSpace >= minimumRequiredStackSize +} +``` + +判断的是**当前栈剩余空间是否 ≥ 2 MB**。实测三类线程的栈事实(Apple silicon,macOS 26): + +| 线程 | 栈总大小 | 进入时剩余 | 判断结果 | +|---|---|---|---| +| 主线程 | 8,372,224 (8 MB) | 8,358,432 | 够 → 内联执行 | +| `DispatchQueue.global()` | 536,576 (524 KB) | 536,064 | **不够 → 另起线程** | +| Swift Concurrency 协作线程 | 536,576 (524 KB) | 535,568 | **不够 → 另起线程** | + +后两类线程的**栈总量**(524 KB)就低于阈值(2 MB),因此剩余空间无论如何都不可能达标 —— 判断在这两类线程上**恒为假**。也就是说:只要不在主线程上,每一次 demangle、每一次 remangle、每一次 print 都会新建一条 8 MB 栈的 `Thread`,用信号量等它跑完,然后销毁。 + +这不是边缘情况,而是常态:`SymbolIndexStore` 的索引构建跑在 `DispatchQueue.global()` 上,逐个符号调用 `demangleAsNodeTransient`;任何 `async` 的打印管线都跑在协作线程上。实测单次线程建立/销毁成本约 50 µs: + +| 场景(2000 次,同一符号) | 主线程 | 512 KB 栈线程 | 倍率 | +|---|---|---|---| +| `demangleAsNodeTransient` | 63.9 ms | 162.5 ms | 2.54× | +| `Node.print(using:)` | 20.8 ms | 127.5 ms | **6.12×** | + +打印那一行最能说明问题:打印本体只要约 10 µs,线程开销 53 µs —— **开销是本体的 5 倍**。按一个框架 20 万个符号估算,仅线程建立就是十秒量级的纯浪费。 + +## 改动 + +分两层,互相独立、叠加生效。 + +### 第一层:常驻线程复用(`LargeStackThreadPool`) + +把「每次 `Thread(...)` + `start()` + 销毁」换成一个按需增长、空闲回收的常驻大栈线程池: + +- 提交任务时,仅当**待处理任务数超过空闲 worker 数**才新建线程;竞态下多建一条也无妨,它会在空闲超时(30 秒)后自行退出。 +- worker 拿到任务就执行,执行完回到 `NSCondition` 上等下一个,不销毁。 +- worker 自身跑在 8 MB 栈上,因此它内部的嵌套调用(例如 demangler 调用 remangler 处理 opaque return type)走的是 `currentThreadHasSufficientStack` 的内联分支,**不会**再往池子里提交,从而不存在「池子被占满导致自我死锁」的风险。 + +这一层对 demangle / remangle / print **三条路径同时生效**,且完全不触碰任何递归引擎。 + +### 第二层:乐观内联 + 栈预算回退(`executeWithinStackBudget`) + +线程池把单次成本从 ~50 µs 降到 ~8 µs(信号量往返 + 上下文切换),但这 8 µs 仍然是白付的 —— 绝大多数符号的递归深度只有几十帧,压根不需要 8 MB 栈。 + +于是新增一条乐观路径: + +```swift +StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in + // 在当前线程直接跑,递归逼近 stackFloorAddress 时返回 nil 放弃 +} unbudgetedFallback: { + // 只有放弃了才在大栈 worker 上无限制重跑 +} +``` + +`stackFloorAddress` 由当前线程的栈底加上 64 KB 安全余量算出。打印引擎在**已有的递归收敛点** `DemanglingPrinter.printName` 上做检查 —— 该函数是所有打印递归的必经之路(原本就在那里做 `maxPrintDepth` 判断),因此覆盖是完整的: + +```swift +if didExhaustStackBudget { return nil } // 已放弃:快速退栈 +if stackFloorAddress != 0 { + var stackProbe = 0 + let currentAddress = withUnsafeMutablePointer(to: &stackProbe) { UInt(bitPattern: $0) } + if currentAddress <= stackFloorAddress { + didExhaustStackBudget = true + return nil + } +} +``` + +用**实测栈指针**而不是固定深度阈值,是刻意的选择:每帧占用多少字节随 `Target` 类型(`String` 与 `SemanticString` 的帧大小不同)和优化级别变化,固定深度必然要保守到浪费;直接比地址则无需猜测,且天然适配任何栈大小的线程。 + +放弃时整个 printer 值被丢弃、在大栈上重跑,因此中途写入 `target` 和 `printCache` 的残缺片段不会外泄。 + +## 效果 + +同一台机器、同一符号、2000 次: + +| 场景 | 改动前 | 仅线程池 | 线程池 + 栈预算 | +|---|---|---|---| +| `print` @512 KB 栈线程 | 127.5 ms | 42.4 ms | **17.5 ms** | +| `demangle` @512 KB 栈线程 | 162.5 ms | 78.0 ms | 74.8 ms | +| 32 任务并发 6400 次 demangle | 162.7 ms | 101.1 ms | 99.6 ms | + +打印路径提速 **7.3 倍**,且在 512 KB 栈线程上的耗时已低于主线程 —— 跨线程成本完全消失。 + +## 影响面与限制 + +- **行为不变**:深递归仍然受保护,只是保护方式从「无条件换线程」变成「先试,不够再换」。`maxPrintDepth`(768 帧,对齐上游 C++ `NodePrinter`)与 `<>` 语义保持原样。 +- **只有打印路径接入了第二层**。`Demangler` 没有单一递归收敛点(`demangleType` / `demangleOperator` 等多个方法互递归),`Remangler` 虽有收敛点 `mangle(_:depth:)` 但暂未接入。两者目前只享受第一层的线程复用收益。若后续要给 demangler 加栈预算,前提是先为它建立一个真正覆盖全部递归路径的收敛点 —— 保护不完整反而比现状危险。 +- **非 Darwin 平台**行为不变(整套机制都在 `#if canImport(Darwin)` 内,其他平台一律直接执行)。 +- **回归验证**:MachOSwiftSection 的 `MachOSwiftSectionTests` + `SwiftLayoutTests` 全量对比,改动前后失败集合逐条一致(157 项,均为该分支既有失败);`SwiftDumpTests` / `SwiftPrintingTests` / `MachOSymbolsTests` 共 100 项中除一项既有的快照失败外全部通过。 diff --git a/Sources/Demangling/Node/Node+CustomStringConvertible.swift b/Sources/Demangling/Node/Node+CustomStringConvertible.swift index e9f3d0a..46101c4 100644 --- a/Sources/Demangling/Node/Node+CustomStringConvertible.swift +++ b/Sources/Demangling/Node/Node+CustomStringConvertible.swift @@ -14,7 +14,10 @@ extension Node: CustomStringConvertible { /// - Parameter options: an option set containing the different `DemangleOptions` from the Swift project. /// - Returns: `self` printed to a string according to the specified options. public func print(using options: DemangleOptions = .default) -> String { - StackSafeExecutor.execute { + StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in + var printer = NodePrinter(options: options) + return printer.printRootWithinStackBudget(self, stackFloorAddress: stackFloorAddress) + } unbudgetedFallback: { var printer = NodePrinter(options: options) return printer.printRoot(self) } diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index b492566..10ca573 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -27,6 +27,12 @@ public struct DemanglingPrinter Target? { + self.stackFloorAddress = stackFloorAddress + _ = printName(root) + return didExhaustStackBudget ? nil : target + } + private mutating func printName(_ name: SomeNode, asPrefixContext: Bool = false) -> SomeNode? { + // Unwind the whole recursion once any frame ran out of budget: the + // result is discarded, so continuing would only risk the overflow the + // budget exists to avoid. + if didExhaustStackBudget { + return nil + } + if stackFloorAddress != 0 { + var stackProbe = 0 + let currentAddress = withUnsafeMutablePointer(to: &stackProbe) { UInt(bitPattern: $0) } + if currentAddress <= stackFloorAddress { + didExhaustStackBudget = true + return nil + } + } if printDepth > Self.maxPrintDepth { target.write("<>") return nil @@ -2238,4 +2271,9 @@ public struct NodePrinter: Sendable { public mutating func printRoot(_ root: Node) -> Target { engine.printRoot(root) } + + /// See ``DemanglingPrinter/printRootWithinStackBudget(_:stackFloorAddress:)``. + public mutating func printRootWithinStackBudget(_ root: Node, stackFloorAddress: UInt) -> Target? { + engine.printRootWithinStackBudget(root, stackFloorAddress: stackFloorAddress) + } } diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift index dbba456..1ce27c9 100644 --- a/Sources/Demangling/Store/DemanglingNode.swift +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -46,7 +46,10 @@ public protocol DemanglingNode: Sendable { extension DemanglingNode { /// Prints this subtree with the given options. Mirrors `Node.print(using:)`. public func print(using options: DemangleOptions = .default) -> String { - StackSafeExecutor.execute { + StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in + var printer = DemanglingPrinter(options: options) + return printer.printRootWithinStackBudget(self, stackFloorAddress: stackFloorAddress) + } unbudgetedFallback: { var printer = DemanglingPrinter(options: options) return printer.printRoot(self) } diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift index 3ff4d80..c0d8d43 100644 --- a/Sources/Demangling/Store/NodeReference.swift +++ b/Sources/Demangling/Store/NodeReference.swift @@ -119,7 +119,10 @@ public struct NodeReference: Sendable { /// Prints the demangled form of this subtree directly from the store, /// without materializing a `Node` tree (proposal 0001, Phase 2). public func print(using options: DemangleOptions = .default) -> String { - StackSafeExecutor.execute { + StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in + var printer = DemanglingPrinter(options: options) + return printer.printRootWithinStackBudget(self, stackFloorAddress: stackFloorAddress) + } unbudgetedFallback: { var printer = DemanglingPrinter(options: options) return printer.printRoot(self) } diff --git a/Sources/Demangling/Utils/StackSafeExecutor.swift b/Sources/Demangling/Utils/StackSafeExecutor.swift index fca147e..ba24fb7 100644 --- a/Sources/Demangling/Utils/StackSafeExecutor.swift +++ b/Sources/Demangling/Utils/StackSafeExecutor.swift @@ -17,7 +17,11 @@ public enum StackSafeExecutor { private static let minimumRequiredStackSize = 2 * 1024 * 1024 // 2MB /// Stack size allocated for the dedicated large-stack thread. - private static let largeStackThreadSize = 8 * 1024 * 1024 // 8MB + fileprivate static let largeStackThreadSize = 8 * 1024 * 1024 // 8MB + + /// Stack space reserved below a budgeted recursion for the non-recursive + /// work that still has to run once the recursion unwinds. + private static let stackSafetyMargin = 64 * 1024 // 64KB #endif /// Executes the given block, switching to a large-stack thread if the @@ -68,16 +72,13 @@ public enum StackSafeExecutor { return try block() } let outcome: Result = await withCheckedContinuation { continuation in - let thread = Thread { + LargeStackThreadPool.shared.submit { do throws(Failure) { continuation.resume(returning: .success(try block())) } catch { continuation.resume(returning: .failure(error)) } } - thread.stackSize = largeStackThreadSize - thread.qualityOfService = .userInitiated - thread.start() } return try outcome.get() #else @@ -85,7 +86,61 @@ public enum StackSafeExecutor { #endif } + /// Runs a recursion that can bail out when it approaches the end of the + /// current thread's stack, falling back to a large-stack worker only for + /// the inputs that actually need one. + /// + /// ``execute(_:)`` has to assume the worst about every input, so on a + /// 512KB-stack thread it hands *every* call to a worker thread. Most real + /// inputs are nowhere near deep enough to need that: `printName` recursion + /// for a typical symbol is a few dozen frames. `budgetedAttempt` receives + /// the address the stack must not grow past and returns `nil` if it hit + /// that limit; only then does `unbudgetedFallback` run on a worker. + /// + /// - Parameters: + /// - budgetedAttempt: runs inline on the current thread. Must return + /// `nil` — having produced no side effects the caller depends on — + /// when the recursion reaches `stackFloorAddress`. + /// - unbudgetedFallback: re-runs the same work with no depth limit, on a + /// thread known to have room for it. + public static func executeWithinStackBudget( + budgetedAttempt: (_ stackFloorAddress: UInt) -> Success?, + unbudgetedFallback: @escaping @Sendable () -> Success + ) -> Success { + #if canImport(Darwin) + if currentThreadHasSufficientStack { + return unbudgetedFallback() + } + if let result = budgetedAttempt(stackFloorAddressForCurrentThread) { + return result + } + return executeOnLargeStackThreadReturning(unbudgetedFallback) + #else + return unbudgetedFallback() + #endif + } + #if canImport(Darwin) + private static var stackFloorAddressForCurrentThread: UInt { + let stackAddress = pthread_get_stackaddr_np(pthread_self()) + let stackSize = pthread_get_stacksize_np(pthread_self()) + let stackBase = UInt(bitPattern: stackAddress - stackSize) + return stackBase + UInt(stackSafetyMargin) + } + + private static func executeOnLargeStackThreadReturning( + _ block: @escaping @Sendable () -> Success + ) -> Success { + nonisolated(unsafe) var result: Success! + let semaphore = DispatchSemaphore(value: 0) + LargeStackThreadPool.shared.submit { + result = block() + semaphore.signal() + } + semaphore.wait() + return result + } + private static var currentThreadHasSufficientStack: Bool { let stackAddress = pthread_get_stackaddr_np(pthread_self()) let stackSize = pthread_get_stacksize_np(pthread_self()) @@ -99,13 +154,10 @@ public enum StackSafeExecutor { private static func executeOnLargeStackThread(_ block: @escaping @Sendable () -> String) -> String { nonisolated(unsafe) var result: String = "" let semaphore = DispatchSemaphore(value: 0) - let thread = Thread { + LargeStackThreadPool.shared.submit { result = block() semaphore.signal() } - thread.stackSize = largeStackThreadSize - thread.qualityOfService = .userInitiated - thread.start() semaphore.wait() return result } @@ -115,7 +167,7 @@ public enum StackSafeExecutor { ) throws(Failure) -> Success { nonisolated(unsafe) var outcome: Result! let semaphore = DispatchSemaphore(value: 0) - let thread = Thread { + LargeStackThreadPool.shared.submit { do throws(Failure) { outcome = .success(try block()) } catch { @@ -123,11 +175,76 @@ public enum StackSafeExecutor { } semaphore.signal() } - thread.stackSize = largeStackThreadSize - thread.qualityOfService = .userInitiated - thread.start() semaphore.wait() return try outcome.get() } #endif } + +#if canImport(Darwin) +/// A pool of long-lived large-stack worker threads, reused across calls. +/// +/// Every call used to create — and then join — a brand new `Thread`. Because +/// ``StackSafeExecutor/currentThreadHasSufficientStack`` demands 2MB of +/// *remaining* stack while a Swift Concurrency cooperative worker and a +/// libdispatch worker both get a 512KB stack in total, the large-stack branch +/// is taken unconditionally off the main thread: measured on this package, +/// 2000 demangles cost 64ms on the main thread (run inline) against 163ms on a +/// `DispatchQueue.global()` thread, i.e. roughly 50µs of thread setup on top of +/// a ~32µs demangle. Bulk work — demangling every symbol of a framework, or +/// printing every declaration of an interface — paid that per item. +/// +/// Threads are created on demand, reused while work keeps arriving, and retired +/// after an idle period so a burst does not leave workers resident forever. +/// +/// A worker never submits back into the pool: it runs on an 8MB stack, so +/// ``StackSafeExecutor/execute(_:)`` takes its inline branch there, and nested +/// demangle/remangle calls cannot deadlock against a saturated pool. +private final class LargeStackThreadPool: @unchecked Sendable { + static let shared = LargeStackThreadPool() + + /// How long an idle worker waits for new work before retiring. + private static let idleTimeout: TimeInterval = 30 + + private let condition = NSCondition() + private var pendingWorkItems: [@Sendable () -> Void] = [] + private var idleWorkerCount = 0 + + func submit(_ workItem: @escaping @Sendable () -> Void) { + condition.lock() + pendingWorkItems.append(workItem) + // Only spin up a worker when the queue outgrows the idle workers that + // are already parked on the condition; overshooting under a race just + // creates one extra worker, which then retires on its idle timeout. + let needsAdditionalWorker = pendingWorkItems.count > idleWorkerCount + condition.signal() + condition.unlock() + + if needsAdditionalWorker { + let thread = Thread { [self] in runWorkerLoop() } + thread.stackSize = StackSafeExecutor.largeStackThreadSize + thread.qualityOfService = .userInitiated + thread.start() + } + } + + private func runWorkerLoop() { + while true { + condition.lock() + idleWorkerCount += 1 + while pendingWorkItems.isEmpty { + if !condition.wait(until: Date(timeIntervalSinceNow: Self.idleTimeout)) { + idleWorkerCount -= 1 + condition.unlock() + return + } + } + idleWorkerCount -= 1 + let workItem = pendingWorkItems.removeFirst() + condition.unlock() + + workItem() + } + } +} +#endif From ac3058487a431cfbdea7d5d30f0021e90b27c33b Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Tue, 28 Jul 2026 10:30:30 +0800 Subject: [PATCH 22/23] perf(Demangling): remangle within a stack budget too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires `Remangler` into the same optimistic-inline path the printer already uses: run on the caller's stack, fall back to a large-stack worker only when the walk actually nears the stack end. The check goes at the existing convergence point `mangle(_:depth:)`, which already guards `maxDepth` (1024, matching upstream Remangler.cpp), so coverage is complete by construction. Remangling is `throws(ManglingError)`, hence a typed-throws overload of `executeWithinStackBudget`. The two failure modes are kept distinct: a thrown error means the tree itself is malformed and propagates immediately (retrying on a worker would only reproduce it), while budget exhaustion is signalled by returning nil and is the only thing that falls back. 2000 remangles on a 512KB-stack thread: 160.0ms -> 130.1ms, i.e. 1.24x the main thread's time down to 1.00x. The gain is smaller than the printer's 7.3x purely because of body cost — print is ~10us with ~84% thread overhead, remangle is ~65us. Also records what the call-graph analysis established about `Demangler`: its main parse loop is iterative (`parseAndPushNames` over an explicit nameStack), not recursive descent, which is why upstream has no depth limit there either. Only 21 of its 160 methods sit in any cycle, none on the main loop, so it stays on thread reuse alone. Verified: full suite passes, including the dyld-cache acceptance corpus (234232 symbols, 0 store failures, 0 node-path failures). --- AGENTS.md | 2 +- Documentations/StackSafeExecution.md | 11 ++++-- .../Main/Remangle/RemangleInterface.swift | 9 ++++- .../Demangling/Main/Remangle/Remangler.swift | 35 +++++++++++++++++++ .../Demangling/Utils/StackSafeExecutor.swift | 24 +++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9843f23..9575413 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. - **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001; design notes and measurements in `Documentations/NodeStoreArena.md`). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), `structurallyEquals(_ other: NodeReference)` (same-store O(1) via index equality, cross-store structural walk) plus `structuralHash(into:)` (structure-consistent hashing for value types that key dictionaries by node structure while storing references — `NodeReference`'s intrinsic `Hashable` is store-identity based), `NodeReference(interning:)` (interns one `Node` tree into a fresh private mini store — self-contained handles for values that outlive their source tree), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. - **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless — no rich target reads it on the store path). `NodePrinterTarget.pushTypeReferenceScope` takes its node as `@autoclosure () -> Node?`: scope-ignoring targets (`String`, the default implementation) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets (e.g. `SemanticString`) evaluate it and receive `materializedNode`, materializing only the nominal-reference subtree. Note the delivered node is canonical **only on the `Node` path**: store-backed printing builds a fresh non-interned subtree per evaluation, so two pushes of the same store index are not `===`. Rich targets must key scopes by structure (e.g. the remangled string, as `SemanticString` does) — never by `ObjectIdentifier`/`===`. `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). -- **`StackSafeExecutor`** (`Utils/StackSafeExecutor.swift`) — the stack-safety wrapper every recursive entry point (demangle / remangle / print) goes through. `currentThreadHasSufficientStack` requires 2MB of *remaining* stack, while a Swift Concurrency cooperative worker and a libdispatch worker each get a 512KB stack **in total** — so off the main thread the large-stack branch is taken unconditionally, for every call. Two layers keep that affordable: (a) `LargeStackThreadPool` reuses long-lived 8MB-stack workers (created on demand, retired after a 30s idle timeout) instead of creating and joining a `Thread` per call — this covers demangle, remangle and print alike, with no engine changes; a worker never re-submits into the pool (it runs on 8MB, so nested calls take the inline branch), so a saturated pool cannot deadlock. (b) `executeWithinStackBudget(budgetedAttempt:unbudgetedFallback:)` runs the recursion inline on the current thread and only falls back to a worker when it actually approaches the stack end — the budgeted attempt gets a `stackFloorAddress` (thread stack base + 64KB margin) and returns `nil` to give up. Only the **print** path is wired into (b), via `DemanglingPrinter.printRootWithinStackBudget(_:stackFloorAddress:)`, which probes the real stack pointer at the existing `printName` convergence point (a fixed depth threshold would have to guess per-frame size, which varies by `Target`); a partial result is discarded wholesale, so the residue left in `target`/`printCache` never escapes. `Demangler` has no single recursion convergence point and `Remangler`'s (`mangle(_:depth:)`) is not wired up yet — both still take (a) only. Do NOT wire a new engine into (b) without a convergence point that covers *every* recursion path: incomplete coverage trades a slow-but-safe call for an overflow. Measurements and the full rationale: `Documentations/StackSafeExecution.md`. +- **`StackSafeExecutor`** (`Utils/StackSafeExecutor.swift`) — the stack-safety wrapper every recursive entry point (demangle / remangle / print) goes through. `currentThreadHasSufficientStack` requires 2MB of *remaining* stack, while a Swift Concurrency cooperative worker and a libdispatch worker each get a 512KB stack **in total** — so off the main thread the large-stack branch is taken unconditionally, for every call. Two layers keep that affordable: (a) `LargeStackThreadPool` reuses long-lived 8MB-stack workers (created on demand, retired after a 30s idle timeout) instead of creating and joining a `Thread` per call — this covers demangle, remangle and print alike, with no engine changes; a worker never re-submits into the pool (it runs on 8MB, so nested calls take the inline branch), so a saturated pool cannot deadlock. (b) `executeWithinStackBudget(budgetedAttempt:unbudgetedFallback:)` runs the recursion inline on the current thread and only falls back to a worker when it actually approaches the stack end — the budgeted attempt gets a `stackFloorAddress` (thread stack base + 64KB margin) and returns `nil` to give up. The **print** and **remangle** paths are wired into (b), via `DemanglingPrinter.printRootWithinStackBudget(_:stackFloorAddress:)` and `Remangler.mangleWithinStackBudget(_:stackFloorAddress:)`; both probe the real stack pointer at their existing convergence points (`printName` / `mangle(_:depth:)`) rather than counting frames, because per-frame size varies by `Target` and optimization level. A partial result is discarded wholesale, so residue in `target`/`printCache`/`buffer` never escapes. Remangling being `throws(ManglingError)` there is a typed-throws overload of (b): a thrown error means the *tree* is bad and propagates immediately, only a `nil` return (budget exhausted) retries on a worker. `Demangler` deliberately takes (a) only, matching upstream — its main parse loop is **not recursive descent** (`parseAndPushNames()` is a `while` loop over an explicit `nameStack`), which is why upstream `Demangler.cpp` has no depth limit either. A call-graph analysis of its 160 methods found only 21 in any cycle, none on the main loop: `demangleBoundGenericArgs`, `setParentForOpaqueReturnTypeNodesImpl` ↔ `getParentId`, and a 19-method `demangleSwift3*` component (the Swift 3 mangling *is* recursive descent). Do NOT wire a new engine into (b) without a convergence point covering *every* recursion path: incomplete coverage trades a slow-but-safe call for an overflow. Measurements and the full rationale: `Documentations/StackSafeExecution.md`. - **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality diff --git a/Documentations/StackSafeExecution.md b/Documentations/StackSafeExecution.md index 2717ca6..cd6fb0c 100644 --- a/Documentations/StackSafeExecution.md +++ b/Documentations/StackSafeExecution.md @@ -78,6 +78,8 @@ if stackFloorAddress != 0 { 放弃时整个 printer 值被丢弃、在大栈上重跑,因此中途写入 `target` 和 `printCache` 的残缺片段不会外泄。 +`Remangler` 用完全相同的形状接入(`mangleWithinStackBudget(_:stackFloorAddress:)`),检查点放在它已有的收敛点 `mangle(_:depth:)` 上——那里原本就在做 `maxDepth`(1024,对齐上游 `Remangler.cpp`)判断。放弃时丢弃整个 remangler 值,残缺的 `buffer` 同样不会外泄。因为 remangle 是 `throws(ManglingError)`,`StackSafeExecutor` 相应多一个 typed-throws 重载;其中「抛错」与「预算耗尽」是两回事:抛错说明树本身有问题,直接向上传播而不去 worker 上重跑(重跑只会复现同一个错误),预算耗尽才走回退。 + ## 效果 同一台机器、同一符号、2000 次: @@ -85,14 +87,17 @@ if stackFloorAddress != 0 { | 场景 | 改动前 | 仅线程池 | 线程池 + 栈预算 | |---|---|---|---| | `print` @512 KB 栈线程 | 127.5 ms | 42.4 ms | **17.5 ms** | +| `remangle` @512 KB 栈线程 | — | 160.0 ms | **130.1 ms** | | `demangle` @512 KB 栈线程 | 162.5 ms | 78.0 ms | 74.8 ms | | 32 任务并发 6400 次 demangle | 162.7 ms | 101.1 ms | 99.6 ms | -打印路径提速 **7.3 倍**,且在 512 KB 栈线程上的耗时已低于主线程 —— 跨线程成本完全消失。 +打印路径提速 **7.3 倍**,且在 512 KB 栈线程上的耗时已低于主线程;remangle 从 1.24 倍主线程耗时回落到 1.00 倍 —— 两条路径的跨线程成本都完全消失。 + +提升幅度的差异完全由「本体耗时」决定:print 本体约 10 µs,线程开销占 84%,所以收益最大;remangle 本体约 65 µs,开销占比小,收益相应就是约 20%。 ## 影响面与限制 -- **行为不变**:深递归仍然受保护,只是保护方式从「无条件换线程」变成「先试,不够再换」。`maxPrintDepth`(768 帧,对齐上游 C++ `NodePrinter`)与 `<>` 语义保持原样。 -- **只有打印路径接入了第二层**。`Demangler` 没有单一递归收敛点(`demangleType` / `demangleOperator` 等多个方法互递归),`Remangler` 虽有收敛点 `mangle(_:depth:)` 但暂未接入。两者目前只享受第一层的线程复用收益。若后续要给 demangler 加栈预算,前提是先为它建立一个真正覆盖全部递归路径的收敛点 —— 保护不完整反而比现状危险。 +- **行为不变**:深递归仍然受保护,只是保护方式从「无条件换线程」变成「先试,不够再换」。`maxPrintDepth`(768 帧,对齐上游 C++ `NodePrinter`)、`Remangler.maxDepth`(1024,对齐上游 `Remangler.cpp`)与 `<>` / `.tooComplex` 语义都保持原样。 +- **`Demangler` 只享受第一层的线程复用**,这是刻意的,且与上游一致:它的主解析路径**根本不是递归下降**——`parseAndPushNames()` 是 `while !scanner.isAtEnd` 配合 `nameStack` 显式栈,嵌套结构靠弹栈拼装而非调用栈,所以上游 `Demangler.cpp` 同样没有任何递归深度限制(有 `depth` 的是 `Remangler` 和 `NodePrinter`,本库两者都已对齐)。对 160 个方法做过调用图分析,真正参与递归的只有 21 个,且都不在主循环上:`demangleBoundGenericArgs`(深度 = 嵌套泛型上下文层数)、`setParentForOpaqueReturnTypeNodesImpl` ↔ `getParentId`(树遍历),以及 19 个 `demangleSwift3*` 组成的互递归团(Swift 3 老 mangling 才是真正的递归下降)。前两类深度很浅;Swift 3 那条路径理论上可被构造的深嵌套老符号打爆栈,但今天 `_T` 前缀符号已基本绝迹。若将来要给它加保护,正确做法是只覆盖这 21 个方法,而不是给全部方法塞一个恒为常数的参数。 - **非 Darwin 平台**行为不变(整套机制都在 `#if canImport(Darwin)` 内,其他平台一律直接执行)。 - **回归验证**:MachOSwiftSection 的 `MachOSwiftSectionTests` + `SwiftLayoutTests` 全量对比,改动前后失败集合逐条一致(157 项,均为该分支既有失败);`SwiftDumpTests` / `SwiftPrintingTests` / `MachOSymbolsTests` 共 100 项中除一项既有的快照失败外全部通过。 diff --git a/Sources/Demangling/Main/Remangle/RemangleInterface.swift b/Sources/Demangling/Main/Remangle/RemangleInterface.swift index f410bc5..ee8204f 100644 --- a/Sources/Demangling/Main/Remangle/RemangleInterface.swift +++ b/Sources/Demangling/Main/Remangle/RemangleInterface.swift @@ -11,11 +11,18 @@ /// - flavor: The mangling flavor (default Swift, or embedded Swift) /// - Returns: The mangled string, or nil if remangling failed public func mangleAsString(_ node: Node, usePunycode: Bool = true, flavor: ManglingFlavor = .default) throws(ManglingError) -> String { + let budgetedMangleBlock: (UInt) throws(ManglingError) -> String? = { stackFloorAddress throws(ManglingError) in + var remangler = Remangler(usePunycode: usePunycode, flavor: flavor) + return try remangler.mangleWithinStackBudget(node, stackFloorAddress: stackFloorAddress) + } let mangleBlock: @Sendable () throws(ManglingError) -> String = { var remangler = Remangler(usePunycode: usePunycode, flavor: flavor) return try remangler.mangle(node) } - return try StackSafeExecutor.execute(mangleBlock) + return try StackSafeExecutor.executeWithinStackBudget( + budgetedAttempt: budgetedMangleBlock, + unbudgetedFallback: mangleBlock + ) } /// Asynchronous variant of ``mangleAsString(_:usePunycode:flavor:)``. diff --git a/Sources/Demangling/Main/Remangle/Remangler.swift b/Sources/Demangling/Main/Remangle/Remangler.swift index 6ec721d..bae2316 100644 --- a/Sources/Demangling/Main/Remangle/Remangler.swift +++ b/Sources/Demangling/Main/Remangle/Remangler.swift @@ -46,6 +46,14 @@ struct Remangler { /// Overflow storage for substitutions beyond inline capacity private var overflowSubstitutions: [SubstitutionEntry: UInt64] = [:] + /// Address the stack must not grow past, or 0 when this remangler may + /// recurse freely. See ``mangleWithinStackBudget(_:stackFloorAddress:)``. + private var stackFloorAddress: UInt = 0 + + /// Set once the walk stopped early because it reached ``stackFloorAddress``; + /// the partial ``buffer`` is then unusable. + private var didExhaustStackBudget: Bool = false + // MARK: - Initialization init(usePunycode: Bool, flavor: ManglingFlavor = .default) { @@ -349,10 +357,37 @@ struct Remangler { return buffer } + /// Remangles `node` only as long as the walk stays clear of + /// `stackFloorAddress`, returning `nil` the moment it would not. + /// + /// Mirrors ``DemanglingPrinter/printRootWithinStackBudget(_:stackFloorAddress:)``: + /// it lets a caller on a small stack run the common shallow tree inline and + /// pay for a large-stack thread only for the rare tree that needs one. + mutating func mangleWithinStackBudget(_ node: Node, stackFloorAddress: UInt) throws(ManglingError) -> String? { + self.stackFloorAddress = stackFloorAddress + clearBuffer() + try mangle(node, depth: 0) + return didExhaustStackBudget ? nil : buffer + } + // MARK: - Core Mangling /// Main entry point for mangling a single node private mutating func mangle(_ node: Node, depth: Int) throws(ManglingError) { + // Unwind the whole walk once any frame ran out of budget: the result is + // discarded, so continuing would only risk the overflow the budget + // exists to avoid. + if didExhaustStackBudget { + return + } + if stackFloorAddress != 0 { + var stackProbe = 0 + let currentAddress = withUnsafeMutablePointer(to: &stackProbe) { UInt(bitPattern: $0) } + if currentAddress <= stackFloorAddress { + didExhaustStackBudget = true + return + } + } // Check recursion depth if depth > Self.maxDepth { throw .tooComplex(node) diff --git a/Sources/Demangling/Utils/StackSafeExecutor.swift b/Sources/Demangling/Utils/StackSafeExecutor.swift index ba24fb7..3c01595 100644 --- a/Sources/Demangling/Utils/StackSafeExecutor.swift +++ b/Sources/Demangling/Utils/StackSafeExecutor.swift @@ -120,6 +120,30 @@ public enum StackSafeExecutor { #endif } + /// Throwing variant of + /// ``executeWithinStackBudget(budgetedAttempt:unbudgetedFallback:)``. + /// + /// An error thrown by `budgetedAttempt` is a genuine failure of the work + /// itself (a malformed tree), not a budget signal — budget exhaustion is + /// reported by returning `nil` — so it propagates instead of retrying on a + /// worker, which would only reproduce it. + public static func executeWithinStackBudget( + budgetedAttempt: (_ stackFloorAddress: UInt) throws(Failure) -> Success?, + unbudgetedFallback: @escaping @Sendable () throws(Failure) -> Success + ) throws(Failure) -> Success { + #if canImport(Darwin) + if currentThreadHasSufficientStack { + return try unbudgetedFallback() + } + if let result = try budgetedAttempt(stackFloorAddressForCurrentThread) { + return result + } + return try executeOnLargeStackThreadThrowing(unbudgetedFallback) + #else + return try unbudgetedFallback() + #endif + } + #if canImport(Darwin) private static var stackFloorAddressForCurrentThread: UInt { let stackAddress = pthread_get_stackaddr_np(pthread_self()) From c928554b54c9014896742a6e89b243532f8f3f17 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Tue, 28 Jul 2026 10:48:18 +0800 Subject: [PATCH 23/23] refactor(Demangling): move stack safety into the printer engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stack safety was a call-site convention: every print entry point had to wrap `printRoot` in `StackSafeExecutor` itself, and forgetting silently removed the protection. MachOSwiftSection's `printSemantic` did exactly that once (a deeply nested generic symbol could overflow where the identical NodeReference call would not), and when the stack budget landed it missed the gain the same way, still paying a thread hop per call. Two failures at the same spot means the responsibility sits in the wrong place. `DemanglingPrinter.print(_:options:)` now owns it, with `NodePrinter` forwarding. It is `static` by necessity: the fallback needs a pristine printer, and a `mutating printRoot` that already gave up mid-walk cannot re-run itself. The four in-library entry points collapse to one-line forwards, dropping the duplicated budgeted/fallback closure pairs. `printRoot` / `printRootWithinStackBudget` stay as low-level entries for callers that know they have headroom, now documented as such. `Node.description` keeps the plain wrapper — it walks the private `printNode` tree dump rather than the print engine. Audited every remaining `StackSafeExecutor` and `printRoot` call site in the library; the rest are fallback branches, facade forwards, or the demangle/async paths that intentionally take thread reuse only. --- AGENTS.md | 2 +- Documentations/StackSafeExecution.md | 24 +++++++++++++ .../Node/Node+CustomStringConvertible.swift | 8 +---- .../Demangling/Node/Printer/NodePrinter.swift | 36 +++++++++++++++++-- Sources/Demangling/Store/DemanglingNode.swift | 8 +---- Sources/Demangling/Store/NodeReference.swift | 8 +---- 6 files changed, 62 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9575413..9677f7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st - **Traversal** (`Store/DemanglingNode+Sequence.swift`, `Node+Sequence.swift`) — the traversal machinery (`preorder`/`inorder`/`postorder`/`levelorder`), the kind-lookup helpers (`first(of:)`, `all(of:)`, `contains(_:)`, `filter(of:)` on `Sequence where Element: DemanglingNode`), and `identifier` are single generic implementations shared by both representations; `Node` and `NodeReference` each conform to `Sequence` with preorder as the default. Do not re-add `Node`-specific copies. - **`NodeStore` / `NodeStoreBuilder` / `NodeReference`** (`Store/`) — Arena-based compact storage for bulk demangling (evolution proposal 0001; design notes and measurements in `Documentations/NodeStoreArena.md`). Nodes are flat 12-byte `CompactNode` values referenced by `UInt32` indices in one contiguous buffer; the `~Copyable` builder hash-conses on insert and `consuming freeze()` produces an immutable `Sendable` store. Interning tables are open-addressing slot arrays holding 4-byte indices (keys recovered from the buffers — no separate key storage). The builder's `demangle(_:)` bridge is fully cache-free (Phase 3): the transient tree is built with `internsLeaves: false`, so nothing touches `NodeCache.shared`. `intern(kind:...)` overloads construct nodes directly in the arena (wrapper `.type` nodes for index keys, etc.). `NodeReference` is a 16-byte value handle mirroring `Node` accessors (kind/text/index/children), plus `textUTF8` (zero-copy string-table bytes), allocation-free `isIdentifier`/`isSwiftModule` witnesses, `structurallyEquals(_ node: Node)` (zero-materialization cross-representation structural equality matching `Node.==` — the bridge for finding an externally demangled `Node` among `NodeReference` dictionary keys, since the frozen store drops its intern tables), `structurallyEquals(_ other: NodeReference)` (same-store O(1) via index equality, cross-store structural walk) plus `structuralHash(into:)` (structure-consistent hashing for value types that key dictionaries by node structure while storing references — `NodeReference`'s intrinsic `Hashable` is store-identity based), `NodeReference(interning:)` (interns one `Node` tree into a fresh private mini store — self-contained handles for values that outlive their source tree), and a `CustomStringConvertible` debug dump (materialization bridge). `demangleAsNodeTransient` is exported via `@_spi(Internals)` for bulk indexers that classify on the transient tree before interning it (the returned tree is NOT canonical). `materialize()` rebuilds a standalone (non-`NodeCache`) `Node` tree with an index-keyed memo, so store-level subtree sharing survives as shared instances instead of expanding the DAG. - **`DemanglingNode` / generic engines** (`Store/DemanglingNode.swift`, `Node/Printer/NodePrinter.swift`, `Main/TypeDecoder/TypeDecoder.swift`) — read-only tree protocol conformed by both `Node` and `NodeReference` (members named to match `Node`'s API so generic engine bodies are representation-agnostic). Engines: `DemanglingPrinter` behind the public `NodePrinter` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine` behind `TypeDecoder` (the public `TypeBuilder` protocol still receives concrete `Node` at the five handoff points via `materializedNode`). The `Remangler` deliberately stays a `Node` engine — its walk constructs transient helper nodes with shared substitution state (same design as the C++ remangler) — so `mangleAsString(some DemanglingNode)` bridges through `materializedNode`. The derived helpers (`isSimpleType`, `needSpaceBeforeType`, `hasChildren`, `subscript(throwChild:)`, `isIdentifier(desired:)`, `isSwiftModule`, `isKind(of:)`, and `second` on `DemanglingNodeChildren`) live **only** on the `DemanglingNode` protocol/extension — do not re-add copies on `Node` or `Node.Children`: the generic engines dispatch to the shared implementation, so a parallel concrete copy would silently drift. `NodePrintContext.node` stays a concrete `Node?` (store path passes `name as? Node` → nil; harmless — no rich target reads it on the store path). `NodePrinterTarget.pushTypeReferenceScope` takes its node as `@autoclosure () -> Node?`: scope-ignoring targets (`String`, the default implementation) never evaluate it, keeping store-backed plain-text printing allocation-free, while rich targets (e.g. `SemanticString`) evaluate it and receive `materializedNode`, materializing only the nominal-reference subtree. Note the delivered node is canonical **only on the `Node` path**: store-backed printing builds a fresh non-interned subtree per evaluation, so two pushes of the same store index are not `===`. Rich targets must key scopes by structure (e.g. the remangled string, as `SemanticString` does) — never by `ObjectIdentifier`/`===`. `DemanglingPrinter` and `StackSafeExecutor` are exported via `@_spi(Internals)` for deep consumers (MachOSwiftSection rich targets). -- **`StackSafeExecutor`** (`Utils/StackSafeExecutor.swift`) — the stack-safety wrapper every recursive entry point (demangle / remangle / print) goes through. `currentThreadHasSufficientStack` requires 2MB of *remaining* stack, while a Swift Concurrency cooperative worker and a libdispatch worker each get a 512KB stack **in total** — so off the main thread the large-stack branch is taken unconditionally, for every call. Two layers keep that affordable: (a) `LargeStackThreadPool` reuses long-lived 8MB-stack workers (created on demand, retired after a 30s idle timeout) instead of creating and joining a `Thread` per call — this covers demangle, remangle and print alike, with no engine changes; a worker never re-submits into the pool (it runs on 8MB, so nested calls take the inline branch), so a saturated pool cannot deadlock. (b) `executeWithinStackBudget(budgetedAttempt:unbudgetedFallback:)` runs the recursion inline on the current thread and only falls back to a worker when it actually approaches the stack end — the budgeted attempt gets a `stackFloorAddress` (thread stack base + 64KB margin) and returns `nil` to give up. The **print** and **remangle** paths are wired into (b), via `DemanglingPrinter.printRootWithinStackBudget(_:stackFloorAddress:)` and `Remangler.mangleWithinStackBudget(_:stackFloorAddress:)`; both probe the real stack pointer at their existing convergence points (`printName` / `mangle(_:depth:)`) rather than counting frames, because per-frame size varies by `Target` and optimization level. A partial result is discarded wholesale, so residue in `target`/`printCache`/`buffer` never escapes. Remangling being `throws(ManglingError)` there is a typed-throws overload of (b): a thrown error means the *tree* is bad and propagates immediately, only a `nil` return (budget exhausted) retries on a worker. `Demangler` deliberately takes (a) only, matching upstream — its main parse loop is **not recursive descent** (`parseAndPushNames()` is a `while` loop over an explicit `nameStack`), which is why upstream `Demangler.cpp` has no depth limit either. A call-graph analysis of its 160 methods found only 21 in any cycle, none on the main loop: `demangleBoundGenericArgs`, `setParentForOpaqueReturnTypeNodesImpl` ↔ `getParentId`, and a 19-method `demangleSwift3*` component (the Swift 3 mangling *is* recursive descent). Do NOT wire a new engine into (b) without a convergence point covering *every* recursion path: incomplete coverage trades a slow-but-safe call for an overflow. Measurements and the full rationale: `Documentations/StackSafeExecution.md`. +- **`StackSafeExecutor`** (`Utils/StackSafeExecutor.swift`) — the stack-safety wrapper every recursive entry point (demangle / remangle / print) goes through. `currentThreadHasSufficientStack` requires 2MB of *remaining* stack, while a Swift Concurrency cooperative worker and a libdispatch worker each get a 512KB stack **in total** — so off the main thread the large-stack branch is taken unconditionally, for every call. Two layers keep that affordable: (a) `LargeStackThreadPool` reuses long-lived 8MB-stack workers (created on demand, retired after a 30s idle timeout) instead of creating and joining a `Thread` per call — this covers demangle, remangle and print alike, with no engine changes; a worker never re-submits into the pool (it runs on 8MB, so nested calls take the inline branch), so a saturated pool cannot deadlock. (b) `executeWithinStackBudget(budgetedAttempt:unbudgetedFallback:)` runs the recursion inline on the current thread and only falls back to a worker when it actually approaches the stack end — the budgeted attempt gets a `stackFloorAddress` (thread stack base + 64KB margin) and returns `nil` to give up. The **print** and **remangle** paths are wired into (b); both probe the real stack pointer at their existing convergence points (`printName` / `mangle(_:depth:)`) rather than counting frames, because per-frame size varies by `Target` and optimization level. **Stack safety lives inside the engine, not at the call site**: use `DemanglingPrinter.print(_:options:)` (or the `NodePrinter` facade's), which wraps (b) itself — it is `static` because the fallback needs a pristine printer and a `mutating printRoot` that already gave up cannot re-run itself. `printRoot(_:)` / `printRootWithinStackBudget(_:stackFloorAddress:)` remain as low-level entries for callers that know they have headroom. Do NOT go back to wrapping `printRoot` in `StackSafeExecutor` at the call site: that was the old convention and MachOSwiftSection's `printSemantic` silently lost the protection by forgetting it (and then silently missed the budget gain the same way). A partial result is discarded wholesale, so residue in `target`/`printCache`/`buffer` never escapes. Remangling being `throws(ManglingError)` there is a typed-throws overload of (b): a thrown error means the *tree* is bad and propagates immediately, only a `nil` return (budget exhausted) retries on a worker. `Demangler` deliberately takes (a) only, matching upstream — its main parse loop is **not recursive descent** (`parseAndPushNames()` is a `while` loop over an explicit `nameStack`), which is why upstream `Demangler.cpp` has no depth limit either. A call-graph analysis of its 160 methods found only 21 in any cycle, none on the main loop: `demangleBoundGenericArgs`, `setParentForOpaqueReturnTypeNodesImpl` ↔ `getParentId`, and a 19-method `demangleSwift3*` component (the Swift 3 mangling *is* recursive descent). Do NOT wire a new engine into (b) without a convergence point covering *every* recursion path: incomplete coverage trades a slow-but-safe call for an overflow. Measurements and the full rationale: `Documentations/StackSafeExecution.md`. - **`Demangler` construction seam** (`Main/Demangle/Demangler+NodeCreation.swift`) — every node the demangler builds goes through `createNode(...)` instance methods; `internsLeaves: false` (used by the internal `demangleAsNodeTransient`) bypasses `NodeCache.shared` entirely. New construction sites in `Demangler` must use `createNode(...)`, never `Node.create(...)` directly. ### Node Identity vs Equality diff --git a/Documentations/StackSafeExecution.md b/Documentations/StackSafeExecution.md index cd6fb0c..3d4c46e 100644 --- a/Documentations/StackSafeExecution.md +++ b/Documentations/StackSafeExecution.md @@ -78,6 +78,30 @@ if stackFloorAddress != 0 { 放弃时整个 printer 值被丢弃、在大栈上重跑,因此中途写入 `target` 和 `printCache` 的残缺片段不会外泄。 +### 保护归位:从调用方约定改为引擎内建 + +第二层刚落地时,每个打印入口都要自己写一遍 `executeWithinStackBudget` 的两个闭包。这是**约定式**保护:调用方得记得包,漏一个就是静默失去保护。而这个坑已经踩过一次——MachOSwiftSection 的 `printSemantic` 直接驱动 `DemanglingPrinter`,历史上就没包 `StackSafeExecutor`,深嵌套泛型符号能把它打爆;这次第二层落地时它又一次没跟上,白付跨线程开销。同一个位置栽两回,说明责任放错了地方。 + +于是把保护挪进引擎,成为 `DemanglingPrinter.print(_:options:)`: + +```swift +public static func print(_ root: SomeNode, options: DemangleOptions = .default) -> Target { + StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in + var printer = DemanglingPrinter(options: options) + return printer.printRootWithinStackBudget(root, stackFloorAddress: stackFloorAddress) + } unbudgetedFallback: { + var printer = DemanglingPrinter(options: options) + return printer.printRoot(root) + } +} +``` + +它**必须是 static**:回退要用一个全新的 printer 重跑,而 `printRoot` 是 `mutating`,放弃那次已经污染了实例状态,没法自己重来。 + +`printRoot` / `printRootWithinStackBudget` 保留为低层入口,供已知有栈余量、想自己管理 printer 生命周期的调用方使用;库内四个打印入口全部塌缩成一行转发。`Node.description` 是例外,它走的是私有的 `printNode` 树 dump(debug 用途)而非打印引擎,继续用第一层。 + +### Remangler + `Remangler` 用完全相同的形状接入(`mangleWithinStackBudget(_:stackFloorAddress:)`),检查点放在它已有的收敛点 `mangle(_:depth:)` 上——那里原本就在做 `maxDepth`(1024,对齐上游 `Remangler.cpp`)判断。放弃时丢弃整个 remangler 值,残缺的 `buffer` 同样不会外泄。因为 remangle 是 `throws(ManglingError)`,`StackSafeExecutor` 相应多一个 typed-throws 重载;其中「抛错」与「预算耗尽」是两回事:抛错说明树本身有问题,直接向上传播而不去 worker 上重跑(重跑只会复现同一个错误),预算耗尽才走回退。 ## 效果 diff --git a/Sources/Demangling/Node/Node+CustomStringConvertible.swift b/Sources/Demangling/Node/Node+CustomStringConvertible.swift index 46101c4..c112ce6 100644 --- a/Sources/Demangling/Node/Node+CustomStringConvertible.swift +++ b/Sources/Demangling/Node/Node+CustomStringConvertible.swift @@ -14,13 +14,7 @@ extension Node: CustomStringConvertible { /// - Parameter options: an option set containing the different `DemangleOptions` from the Swift project. /// - Returns: `self` printed to a string according to the specified options. public func print(using options: DemangleOptions = .default) -> String { - StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in - var printer = NodePrinter(options: options) - return printer.printRootWithinStackBudget(self, stackFloorAddress: stackFloorAddress) - } unbudgetedFallback: { - var printer = NodePrinter(options: options) - return printer.printRoot(self) - } + NodePrinter.print(self, options: options) } /// Asynchronous variant of ``print(using:)``. diff --git a/Sources/Demangling/Node/Printer/NodePrinter.swift b/Sources/Demangling/Node/Printer/NodePrinter.swift index 10ca573..7020957 100644 --- a/Sources/Demangling/Node/Printer/NodePrinter.swift +++ b/Sources/Demangling/Node/Printer/NodePrinter.swift @@ -10,8 +10,8 @@ /// paths, but only the `Node` path delivers a canonical instance: store-backed /// printing materializes a fresh subtree per evaluation, so scopes must be /// keyed by structure (e.g. the remangled string) rather than by -/// `===`/`ObjectIdentifier`. Wrap calls in `StackSafeExecutor.execute` for -/// deeply nested symbols. +/// `===`/`ObjectIdentifier`. Stack safety is built into ``print(_:options:)`` — +/// use it rather than wrapping ``printRoot(_:)`` at the call site. @_spi(Internals) public struct DemanglingPrinter: Sendable { /// Mirrors ``swift::Demangle::NodePrinter::MaxDepth`` from @@ -51,6 +51,33 @@ public struct DemanglingPrinter Target { + StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in + var printer = DemanglingPrinter(options: options) + return printer.printRootWithinStackBudget(root, stackFloorAddress: stackFloorAddress) + } unbudgetedFallback: { + var printer = DemanglingPrinter(options: options) + return printer.printRoot(root) + } + } + + /// Prints `root` on the current thread with no stack guard. + /// + /// Prefer ``print(_:options:)``; reach for this only when the caller + /// already knows it has stack headroom (for example inside a large-stack + /// worker) and wants to manage the printer instance itself. public mutating func printRoot(_ root: SomeNode) -> Target { _ = printName(root) return target @@ -2268,6 +2295,11 @@ public struct NodePrinter: Sendable { self.engine = DemanglingPrinter(options: options) } + /// See ``DemanglingPrinter/print(_:options:)`` — stack-safe entry point. + public static func print(_ root: Node, options: DemangleOptions = .default) -> Target { + DemanglingPrinter.print(root, options: options) + } + public mutating func printRoot(_ root: Node) -> Target { engine.printRoot(root) } diff --git a/Sources/Demangling/Store/DemanglingNode.swift b/Sources/Demangling/Store/DemanglingNode.swift index 1ce27c9..4c57f9c 100644 --- a/Sources/Demangling/Store/DemanglingNode.swift +++ b/Sources/Demangling/Store/DemanglingNode.swift @@ -46,13 +46,7 @@ public protocol DemanglingNode: Sendable { extension DemanglingNode { /// Prints this subtree with the given options. Mirrors `Node.print(using:)`. public func print(using options: DemangleOptions = .default) -> String { - StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in - var printer = DemanglingPrinter(options: options) - return printer.printRootWithinStackBudget(self, stackFloorAddress: stackFloorAddress) - } unbudgetedFallback: { - var printer = DemanglingPrinter(options: options) - return printer.printRoot(self) - } + DemanglingPrinter.print(self, options: options) } @inlinable diff --git a/Sources/Demangling/Store/NodeReference.swift b/Sources/Demangling/Store/NodeReference.swift index c0d8d43..b23283d 100644 --- a/Sources/Demangling/Store/NodeReference.swift +++ b/Sources/Demangling/Store/NodeReference.swift @@ -119,13 +119,7 @@ public struct NodeReference: Sendable { /// Prints the demangled form of this subtree directly from the store, /// without materializing a `Node` tree (proposal 0001, Phase 2). public func print(using options: DemangleOptions = .default) -> String { - StackSafeExecutor.executeWithinStackBudget { stackFloorAddress in - var printer = DemanglingPrinter(options: options) - return printer.printRootWithinStackBudget(self, stackFloorAddress: stackFloorAddress) - } unbudgetedFallback: { - var printer = DemanglingPrinter(options: options) - return printer.printRoot(self) - } + DemanglingPrinter.print(self, options: options) } /// Whether this subtree is structurally equal to a `Node` tree, matching