Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c14cc10
docs(evolution): add proposal 0001 SymbolStore arena-based compact no…
Mx-Iris Jul 23, 2026
50af89f
feat(Demangling): SymbolStore arena-based compact node storage (propo…
Mx-Iris Jul 23, 2026
2dedd3d
feat(Demangling): zero-materialization store printing (proposal 0001 …
Mx-Iris Jul 23, 2026
b9252ef
fix(Demangling): preserve subtree sharing when materializing from Sym…
Mx-Iris Jul 23, 2026
1d10573
refactor(Demangling): single source of truth for printer-derived node…
Mx-Iris Jul 23, 2026
eb2795b
chore(deps): update package pins
Mx-Iris Jul 23, 2026
d9e7c66
feat(Demangling): generic traversal and kind lookup over DemanglingNode
Mx-Iris Jul 23, 2026
5e230e8
feat(Demangling): generic TypeDecoder engine over DemanglingNode
Mx-Iris Jul 23, 2026
3eb55bc
feat(Demangling): store-backed remangling via materialization bridge
Mx-Iris Jul 23, 2026
2653728
feat(Demangling): direct node construction on SymbolStoreBuilder
Mx-Iris Jul 23, 2026
a2fa44f
feat(Demangling): expose DemanglingPrinter and StackSafeExecutor via …
Mx-Iris Jul 23, 2026
7a6a9f4
feat(Demangling): Phase 3 — cache-free bulk demangling and slim inter…
Mx-Iris Jul 23, 2026
8eb9f31
perf(Demangling): zero-copy text view and allocation-free text witnes…
Mx-Iris Jul 23, 2026
6a38f0b
docs: sync AGENTS.md and proposal 0001 decision log for Phase 2 compl…
Mx-Iris Jul 23, 2026
fcf7595
refactor(Demangling): rename SymbolStore to NodeStore and add downstr…
Mx-Iris Jul 24, 2026
4f3201a
chore: add manifest cache isolation marker for worktree builds
Mx-Iris Jul 24, 2026
26db7a4
feat(Demangling): Stage 5 upstream — lazy scope hook, transient const…
Mx-Iris Jul 24, 2026
4c5caf8
docs(evolution): record Stage 5 upstream decisions in proposal 0001
Mx-Iris Jul 27, 2026
22a39d1
fix(Demangling): avoid Array allocation on the transient node path
Mx-Iris Jul 27, 2026
ea2ec28
docs(Demangling): sync NodeStore documentation with the shipped imple…
Mx-Iris Jul 27, 2026
73fb660
Merge pull request #3 from MxIris-Reverse-Engineering/feature/node-store
Mx-Iris Jul 27, 2026
ea4101b
perf(Demangling): reuse large-stack threads and print within a stack …
Mx-Iris Jul 27, 2026
ac30584
perf(Demangling): remangle within a stack budget too
Mx-Iris Jul 28, 2026
c928554
refactor(Demangling): move stack safety into the printer engine
Mx-Iris Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnicodeScalar>`. Parses mangled prefixes `_T0`, `_$S`, `_$s`, `$S`, `$s`, `$e`, `_$e`, `@__swiftmacro_`.
Expand All @@ -48,7 +48,11 @@ mangled string → Demangler → Node tree → NodePrinter → human-readable st
- **`DemangleOptions`** (`DemangleOptions.swift`) — `OptionSet` with presets: `.default`, `.simplified`, `.interface`, `.interfaceType`, etc.
- **`TypeDecoder<Builder>`** (`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`.
- **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<Target, SomeNode>` behind the public `NodePrinter<Target>` facade (store printing is **zero materialization**, byte-identical to the `Node` path across the full dyld-cache corpus), and `TypeDecoderEngine<Builder, SomeNode>` behind `TypeDecoder<Builder>` (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); 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<Target>` 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

Expand Down Expand Up @@ -82,6 +86,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, NodeStore, NodeStoreBuilder, NodeReference, DemanglingNode (evolution proposal 0001)
Enums/ — SugarType, ManglingFlavor, DemanglingError, ManglingError, etc.
Utils/ — Extensions, Common constants, Punycode
Tests/DemanglingTests/
Expand Down
Loading