diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de38781b..13014293 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,15 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.88.0 + - name: Install Rust compatibility toolchains + run: | + mapfile -t toolchains < <( + sed -e 's/[[:space:]]*#.*$//' \ + -e '/^[[:space:]]*$/d' \ + e2e-tests/rust-compat-toolchains.txt + ) + rustup toolchain install --profile minimal "${toolchains[@]}" + - name: Cache cargo registry uses: actions/cache@v4 with: @@ -133,6 +142,7 @@ jobs: - name: Run workspace tests run: cargo test --all-features env: + GHOSTSCOPE_REQUIRE_RUST_COMPAT_TOOLCHAINS: 1 RUST_BACKTRACE: 1 e2e: @@ -156,6 +166,15 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.88.0 + - name: Install Rust e2e compatibility toolchains + run: | + mapfile -t toolchains < <( + sed -e 's/[[:space:]]*#.*$//' \ + -e '/^[[:space:]]*$/d' \ + e2e-tests/rust-compat-toolchains.txt + ) + rustup toolchain install --profile minimal "${toolchains[@]}" + - name: Cache cargo registry uses: actions/cache@v4 with: @@ -185,7 +204,7 @@ jobs: uses: actions/cache@v4 with: path: e2e-tests/.fixture-cache/fixture-binaries.tar - key: ${{ runner.os }}-${{ github.job }}-fixtures-${{ env.FIXTURE_COMPILER_CACHE_REV }}-${{ hashFiles('e2e-tests/cache-fixtures.sh', 'e2e-tests/compile-fixtures.sh', 'e2e-tests/tests/fixtures/**/*.c', 'e2e-tests/tests/fixtures/**/*.h', 'e2e-tests/tests/fixtures/**/*.cpp', 'e2e-tests/tests/fixtures/**/*.rs', 'e2e-tests/tests/fixtures/**/Makefile', 'e2e-tests/tests/fixtures/**/Cargo.toml', 'e2e-tests/tests/fixtures/**/Cargo.lock') }} + key: ${{ runner.os }}-${{ github.job }}-fixtures-${{ env.FIXTURE_COMPILER_CACHE_REV }}-${{ hashFiles('e2e-tests/cache-fixtures.sh', 'e2e-tests/compile-fixtures.sh', 'e2e-tests/compile-rust-compat-fixtures.sh', 'e2e-tests/rust-compat-toolchains.txt', 'e2e-tests/tests/fixtures/**/*.c', 'e2e-tests/tests/fixtures/**/*.h', 'e2e-tests/tests/fixtures/**/*.cpp', 'e2e-tests/tests/fixtures/**/*.rs', 'e2e-tests/tests/fixtures/**/Makefile', 'e2e-tests/tests/fixtures/**/Cargo.toml', 'e2e-tests/tests/fixtures/**/Cargo.lock') }} - name: Restore fixture archive if: steps.fixture-cache.outputs.cache-hit == 'true' @@ -200,13 +219,22 @@ jobs: "$FIXTURE_COMPILER_IMAGE" \ bash ./compile-fixtures.sh sudo chown -R "$(id -u):$(id -g)" "${GITHUB_WORKSPACE}/e2e-tests/tests/fixtures" - bash ./e2e-tests/cache-fixtures.sh pack + + - name: Compile Rust target-compiler matrix fixtures + if: steps.fixture-cache.outputs.cache-hit != 'true' + run: bash ./e2e-tests/compile-rust-compat-fixtures.sh + + - name: Pack fixture archive + if: steps.fixture-cache.outputs.cache-hit != 'true' + run: bash ./e2e-tests/cache-fixtures.sh pack - name: Build GhostScope CLI and dwarf-tool run: cargo build -p ghostscope -p dwarf-tool --all-features - name: Run e2e env: + GHOSTSCOPE_REQUIRE_RUST_135_E2E: 1 + GHOSTSCOPE_REQUIRE_RUST_E2E_TOOLCHAINS: 1 GHOSTSCOPE_PRESERVE_PRECOMPILED_FIXTURES: 1 RUST_BACKTRACE: 1 run: sudo -E "$(which cargo)" test -p ghostscope-e2e-tests --tests --all-features -- --nocapture diff --git a/.gitignore b/.gitignore index 962dec4e..58a77692 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ test_* # Keep fixture compiler entrypoint tracked !e2e-tests/compile-fixtures.sh !e2e-tests/cache-fixtures.sh +!e2e-tests/compile-rust-compat-fixtures.sh # Keep other C files in test fixtures but exclude random C files in root /*.c e2e-tests/tests/fixtures/complex_types_program/complex_types_program @@ -69,6 +70,7 @@ e2e-tests/tests/fixtures/member_pointer_program/member_pointer_program e2e-tests/tests/fixtures/member_pointer_program/member_pointer_program_o* e2e-tests/tests/fixtures/partitioned_ranges_program/partitioned_ranges_program e2e-tests/tests/fixtures/rust_global_program/target/ +e2e-tests/tests/fixtures/rust_compat_program/bin/ e2e-tests/tests/fixtures/scalar_types_program/scalar_types_program e2e-tests/tests/fixtures/sample_program/sample_lib.o e2e-tests/tests/fixtures/sample_program/sample_program diff --git a/docs/design-contract.md b/docs/design-contract.md index 655488ce..9def4f89 100644 --- a/docs/design-contract.md +++ b/docs/design-contract.md @@ -23,9 +23,11 @@ does not permit known unsupported paths to return plausible but unproven data. traced. Embedded DWARF is read from that module. For separate debug files, available `.gnu_debuglink` CRC and Build ID evidence is checked strictly by default. -- C has the strongest end-to-end coverage. C++ and Rust currently use a more - limited, DWARF-layout-oriented model. Language coverage is documented in - [limitations](limitations.md). +- C has the strongest end-to-end coverage. C++ remains primarily + DWARF-layout-oriented, while Rust adds targeted standard-library semantic + adapters selected from validated target DWARF. See + [Source Language Support](scripting.md#source-language-support) for current + coverage and [limitations](limitations.md) for its boundaries. - The kernel must provide the required eBPF, uprobe, and event transport facilities, and GhostScope must have permission to use them. diff --git a/docs/limitations.md b/docs/limitations.md index f3198688..835636a1 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -22,9 +22,27 @@ Official builds and correctness testing currently support **Linux x86_64 (AMD64) ## Soft Limitations ### 1. Language Support -Primary focus is on **C language**, which currently has the best end-to-end support. **C++** and **Rust** are supported in a more limited, DWARF-layout-oriented way: GhostScope can automatically demangle function names and global/static symbols, but most language-specific features are not modeled yet. In practice, simple C-like layouts work best, while advanced Rust and C++ features still require substantial future work. - -For interpreted languages (Lua, Python, Ruby, etc.), only the interpreter itself can be traced (since interpreters are typically implemented in compiled languages). Tracing script code is technically feasible but requires substantial development time. JIT language support is an even more distant goal. +Primary focus is on **C**, which has the best end-to-end coverage. Inline C +structs, arrays, unions, enums, typedefs, and qualifiers are formatted +recursively from DWARF. Pointer members are shown as addresses unless the DSL +or a read plan explicitly follows them; GhostScope does not automatically walk +arbitrary C pointer graphs or linked data structures. Recursive inline +formatting has a maximum depth of 32. + +**Rust** has targeted semantic support for common standard-library value +families. It is selected from the concrete target DWARF rather than from a +promised Rust ABI. Nested semantic adapters do not yet compose automatically. +See [Rust Value Presentation](scripting.md#rust-value-presentation) for the +current type list, formatting behavior, and nesting boundary. + +**C++** remains primarily DWARF-layout-oriented. Simple C-like layouts work +best; broader standard-library and language-specific semantics are not modeled +yet. + +For interpreted languages (Lua, Python, Ruby, etc.), only the interpreter +itself can be traced because interpreters are typically implemented in compiled +languages. Tracing script code is technically feasible but requires substantial +development time. JIT language support is an even more distant goal. ### 2. User-Memory Reads via `bpf_probe_read_user` In traditional non-sleepable probe paths, helpers such as `bpf_probe_read_user` cannot resolve user-space page faults, so reads from a target virtual address may still fail if the page is not resident or otherwise faults on access. diff --git a/docs/scripting.md b/docs/scripting.md index e21ab79f..8a154dbb 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -120,13 +120,123 @@ GhostScope's script syntax is source-language agnostic, but real-world support d | --- | --- | --- | | C | Best | This is the primary target. Plain locals, globals, x86_64 executable static thread-local variables, pointers, arrays, structs, enums, and C strings map most directly to the current DWARF readers and script operators. | | C++ | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Beyond name resolution, most C++-specific language features are not modeled yet, so the best results come from simple, C-like layouts and scalar fields. | -| Rust | Limited | Automatic demangling is supported for function names in `trace ...` patterns and for global/static variable lookup. Numeric tuple fields such as `value.0` are supported for tuples and tuple structs when Rust DWARF type identity is available. Most other Rust-specific features are not modeled yet. | +| Rust | Targeted | See [Rust value details](#rust-value-presentation). | Practical guidance: - Prefer C targets when you need the highest success rate for complex DWARF expressions. - GhostScope recognizes `DW_OP_form_tls_address`, which is used for both static and dynamic TLS. Runtime address resolution currently supports only x86_64 executable static TLS; dynamic/shared-library TLS is not modeled yet. -- C++ remains primarily DWARF-layout aware. Rust tuple fields are the first language-aware projection, but broader Rust semantics are not modeled yet. -- In C++ and Rust, start from demangled function/global names, then probe simple fields first. If name lookup is ambiguous, fall back to line- or address-based trace patterns. +- C++ remains primarily DWARF-layout aware. +- Rust support is determined from each target type's language, identity, and + validated DWARF layout. A rustc release parsed from `DW_AT_producer` is only + advisory metadata; it never establishes a private layout by itself. +- If name lookup is ambiguous, fall back to line- or address-based trace + patterns. + +### Rust Value Presentation + +Rust value support does not add Rust-specific syntax to the DSL. After the DSL +resolves an expression, the DWARF layer may select a bounded semantic read plan +for its root type. If the concrete type identity or layout does not validate, +GhostScope keeps the native DWARF presentation instead of guessing an ABI. + +GhostScope's own Rust 1.88 build toolchain does not constrain the rustc used to +build a traced target. CI compiles representative target binaries with the +releases in the [compatibility matrix](../e2e-tests/rust-compat-toolchains.txt). +That matrix records audited DWARF layouts rather than version gates: coverage is +adapter-specific, so not every value family is validated on every listed +release, and unmatched older layouts fall back to native DWARF presentation. + +The currently supported value families are: + +- UTF-8 strings: `&str`, `&mut str`, `String`, and `Box`; +- platform byte strings: `OsString`, `PathBuf`, and supported `&Path` layouts; +- sequences: `Vec`, `&[T]`, `&mut [T]`, and `VecDeque`; +- transparent and state wrappers: `NonZero*`, `Cell`, and `RefCell`; +- borrow guards: `Ref` and `RefMut`; +- reference counting: `Rc` and `Arc`; +- collections: `HashMap`, `HashSet`, `BTreeMap`, and `BTreeSet`; +- enums: fieldless, unit, tuple, and struct variants, including DWARF default + branches used by niche-optimized types such as `Option`; and +- tuple and tuple-struct projections such as `value.0`. + +`Rc` and `Arc` are displayed as their target address plus public +strong and weak counts. GhostScope does not read the dynamically sized string +contents through these owners. + +Rust enum formatting follows the target DWARF rather than a rustc ABI table. +For payload enums, GhostScope reads `DW_TAG_variant_part`, follows its +`DW_AT_discr` member, and handles exact values, discriminant ranges, and default +branches before recursively formatting the active inline payload. Fieldless +enums use `DW_TAG_enumeration_type` and `DW_AT_enum_class`. The DSL does not yet +provide Rust pattern matching or direct active-variant payload projection, and +semantic adapters nested inside an enum payload still use native DWARF +formatting. + +Semantic values use the existing format placeholders: + +```ghostscope +trace do_stuff { + print "text={} ascii={:s} bytes={:x}", text, text, text; + print "items={}", values; +} +``` + +- `{}` uses the selected semantic presentation. +- When a semantic value supports no-length `{:s}` or `{:x}`, the formatter uses + its logical captured payload rather than exposing internal protocol metadata. +- Length forms such as `{:s.16}`, `{:x.*}`, and `{:s.n$}` retain their generic + explicit memory-read behavior. +- `ebpf.mem_dump_cap` bounds each indirect argument and defaults to 256 bytes. + A semantic value whose logical length exceeds the captured prefix is marked + ``. + +For example, with `mem_dump_cap = 3`, an `&str` containing +`"hello from rust"` is rendered as: + +```text +"hel" +``` + +The value can also be read explicitly when its target DWARF exposes known +members: + +```ghostscope +trace do_stuff { + print "manual={:s.*}", text.length, text.data_ptr; + print "manual_hex={:x.*}", text.length, text.data_ptr; +} +``` + +This explicit form depends on the target's physical member paths. The semantic +form validates and hides those paths. + +#### Nested Rust Values + +Inline DWARF structure formatting is recursive, with a maximum depth of 32. +This also applies to inline Rust values after an outer adapter captures them: + +```text +Outer { inner: Pair } inline Pair uses recursive DWARF formatting +Vec Vec captures elements; Pair formats structurally +Rc Rc projects Pair and counters; Pair formats inline +Cell<(i32, u16)> Cell projects and formats the inline tuple value +``` + +Semantic adapter selection currently occurs once for the root expression type. +Nested semantic containers are not recursively recaptured: + +```text +Rc> inner Vec elements are not captured recursively +Cell inner String bytes are not captured recursively +Vec String elements retain physical DWARF formatting +HashMap> keys and values do not run nested adapters +``` + +Known nested fields remain accessible explicitly through the DSL. The +limitation applies to automatic whole-value presentation. Recursive semantic +capture requires bounded depth, byte, element, and dereference budgets; cycle +detection; deterministic event reservation; and per-child read-error and +truncation status. ## Variables @@ -854,7 +964,8 @@ trace foo { 4. Limited string operations (CString equality and built‑ins only) 5. Integer-only arithmetic/bitwise operators; floating-point arithmetic is not supported 6. No dynamic memory allocation in eBPF -7. Uneven source-language coverage: C works best; C++ and Rust currently rely mostly on automatic demangling plus DWARF-layout-based access, with most language-specific features unsupported +7. Source-language coverage remains type- and DWARF-layout-specific; see + [Source Language Support](#source-language-support) ## Best Practices diff --git a/docs/zh/design-contract.md b/docs/zh/design-contract.md index 25dc4452..e94203c0 100644 --- a/docs/zh/design-contract.md +++ b/docs/zh/design-contract.md @@ -10,7 +10,10 @@ GhostScope 把运行中程序的机器状态转换为源码感知的观测结果 - 官方发布产物和正确性测试面向 Linux x86_64。运行时构建会拒绝其他构建目标,trace setup 也会拒绝不是 64 位小端 x86_64 ELF 的目标文件。 - 源码级值依赖与目标模块对应的 DWARF。内嵌 DWARF 直接来自目标模块;独立调试文件默认严格检查可用的 `.gnu_debuglink` CRC 和 Build ID 证据。 -- C 语言的端到端覆盖最完整。C++ 和 Rust 目前主要采用能力更有限的“按 DWARF 布局访问”模型。具体语言覆盖见[使用限制](limitations.md)。 +- C 语言的端到端覆盖最完整。C++ 仍主要按 DWARF 布局访问;Rust 则在 + 验证目标 DWARF 后,为部分标准库类型提供语义适配。当前覆盖范围见 + [源语言支持现状](scripting.md#源语言支持现状),能力边界见 + [使用限制](limitations.md)。 - 内核必须提供所需的 eBPF、uprobe 和事件传输能力,GhostScope 也必须具备使用这些设施的权限。 `--allow-loose-debug-match` 会显式退出独立调试文件的严格匹配。启用后 GhostScope 会对不匹配发出警告,但用户同时也削弱了源码语义解释所依赖的证据。如果一个显式指定的调试文件既没有可用 CRC,也没有可比较的 Build ID,系统同样无法证明它与目标模块一致;相应 warning 表示这成为用户提供的信任假设,不属于严格证据范围。 diff --git a/docs/zh/limitations.md b/docs/zh/limitations.md index f8386511..38f9466d 100644 --- a/docs/zh/limitations.md +++ b/docs/zh/limitations.md @@ -19,7 +19,19 @@ GhostScope 的脚本和编译器能力面只用于观测,不提供主动修改 ## 软性限制 ### 1. 语言支持 -目前主要支持 **C 语言**,对 C 的端到端支持最完善。**C++** 和 **Rust** 目前属于更偏“按 DWARF 布局访问”的有限支持:GhostScope 支持对函数名以及全局/静态符号做自动 demangle,但大多数语言特性还没有建模。实际使用中,越接近 C 风格的数据布局越容易成功;Rust 和 C++ 的高级特性仍需要后续大量补强。 +目前以 **C 语言** 为主要目标,对 C 的端到端支持最完善。内联 C +结构体、数组、联合体、枚举、typedef 和限定类型会根据 DWARF 递归格式化。 +除非 DSL 或读取计划显式跟随指针,否则指针成员只显示地址;GhostScope +不会自动遍历任意 C 指针图或链式数据结构。内联递归格式化的最大深度为 +32。 + +**Rust** 已针对常见标准库类型提供定向语义支持。这些适配器根据具体目标 +的 DWARF 选择,而不是假设一个稳定的 Rust ABI。嵌套语义适配器目前不会 +自动递归组合。当前类型列表、格式化行为和嵌套边界见 +[Rust 值展示](scripting.md#rust-值展示)。 + +**C++** 仍主要按 DWARF 布局访问。简单、接近 C 的数据布局效果最好; +更广泛的标准库和语言特定语义尚未建模。 对于解释型语言(Lua、Python、Ruby 等),目前只能追踪解释器本身(因为解释器通常用编译型语言实现)。追踪脚本代码在技术上可行,但需要大量开发时间,JIT 语言支持计划就更远了。 diff --git a/docs/zh/scripting.md b/docs/zh/scripting.md index 34ab3b26..9627abc4 100644 --- a/docs/zh/scripting.md +++ b/docs/zh/scripting.md @@ -114,13 +114,112 @@ GhostScope 的脚本语法本身与源语言无关,但实际效果取决于被 | --- | --- | --- | | C | 最好 | 这是当前的主要优化目标。局部变量、全局变量、x86_64 可执行文件中的 static 线程局部变量、指针、数组、结构体、枚举和 C 字符串这类 C 风格布局,与现有 DWARF 读取和脚本运算最匹配。 | | C++ | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。除名字解析外,C++ 语言特性目前大多还没有建模,实际更接近“按 DWARF 布局访问”,适合简单、接近 C 的数据布局和标量字段。 | -| Rust | 有限 | `trace ...` 里的函数名匹配,以及全局/静态变量查找,支持自动 demangle(符号名自动反混淆)。当 Rust DWARF 类型身份可用时,tuple 和 tuple struct 支持 `value.0` 这类数字成员访问;其他大多数 Rust 语言特性仍未建模。 | +| Rust | 定向支持 | 见 [Rust 值详情](#rust-值展示)。 | 实践建议: - 需要较高成功率的复杂 DWARF 表达式时,优先选择 C 目标。 - GhostScope 能识别 `DW_OP_form_tls_address`,这个操作既会用于 static TLS,也会用于 dynamic TLS。运行时地址解析目前只支持 x86_64 可执行文件 static TLS;dynamic/shared-library TLS 尚未建模。 -- C++ 目前仍主要按 DWARF 布局访问。Rust tuple 成员是第一个语言语义投影,但更广泛的 Rust 语义仍未建模。 -- 在 C++ 和 Rust 上,先从 demangle 后的函数名/全局名入手,再逐步验证简单字段;若名字解析有歧义,优先退回到按源码行号或地址下探。 +- C++ 目前仍主要按 DWARF 布局访问。 +- Rust 支持根据每个目标类型的源语言、类型身份和经过验证的 DWARF 布局 + 决定。从 `DW_AT_producer` 解析出的 rustc 版本只作为提示性元数据; + 它本身不能证明任何私有布局。 +- 若名字解析有歧义,优先退回到按源码行号或地址下探。 + +### Rust 值展示 + +Rust 值支持不会给 DSL 增加 Rust 专用语法。DSL 解析表达式后,DWARF 层 +可以为根类型选择一个有界的语义读取计划。如果具体类型的身份或布局未通过 +验证,GhostScope 会保留原生 DWARF 展示,而不会猜测 Rust ABI。 + +GhostScope 自身使用 Rust 1.88 构建,但这不会限制被追踪目标使用的 rustc +版本。CI 会使用[兼容性矩阵](../../e2e-tests/rust-compat-toolchains.txt)中的 +版本构建有代表性的目标二进制。该矩阵记录的是已审计的 DWARF 布局,而不是 +版本 gate:覆盖范围取决于具体 adapter,并非每个类型族都在每个列出的版本 +上验证过;不匹配的旧布局会退回原生 DWARF 展示。 + +目前支持以下类型族: + +- UTF-8 字符串:`&str`、`&mut str`、`String` 和 `Box`; +- 平台原生字节串:`OsString`、`PathBuf`,以及布局验证通过的 `&Path`; +- 序列:`Vec`、`&[T]`、`&mut [T]` 和 `VecDeque`; +- 透明包装和状态包装:`NonZero*`、`Cell` 和 `RefCell`; +- 借用守卫:`Ref` 和 `RefMut`; +- 引用计数:`Rc` 和 `Arc`; +- 集合:`HashMap`、`HashSet`、`BTreeMap` 和 `BTreeSet`; +- 枚举:无字段、unit、tuple 和 struct variant,包括 `Option` + 等 niche 优化类型在 DWARF 中使用的默认分支; +- tuple 和 tuple struct 投影,例如 `value.0`。 + +`Rc` 和 `Arc` 会显示目标地址以及对外可见的 strong/weak 计数。 +GhostScope 不会通过这些所有者读取动态大小字符串的内容。 + +Rust enum 的格式化依据目标程序的 DWARF,而不是 rustc ABI 表。对于带 +payload 的 enum,GhostScope 解析 `DW_TAG_variant_part`,跟随其 +`DW_AT_discr` 成员,并处理精确判别值、判别值范围和默认分支,随后递归 +格式化当前激活的内联 payload。无字段 enum 使用 +`DW_TAG_enumeration_type` 和 `DW_AT_enum_class`。DSL 目前还不提供 Rust +模式匹配或直接投影当前 variant payload 的语法;enum payload 内嵌套的 +语义适配器仍按原生 DWARF 格式展示。 + +语义值沿用现有格式占位符: + +```ghostscope +trace do_stuff { + print "text={} ascii={:s} bytes={:x}", text, text, text; + print "items={}", values; +} +``` + +- `{}` 使用选中的语义展示。 +- 当语义值支持无长度的 `{:s}` 或 `{:x}` 时,格式化器使用逻辑采集结果, + 不会暴露内部协议元数据。 +- `{:s.16}`、`{:x.*}` 和 `{:s.n$}` 等带长度形式仍保留通用的显式内存 + 读取语义。 +- `ebpf.mem_dump_cap` 限制每个间接参数,默认值为 256 字节。若语义值的 + 逻辑长度超过已采集的前缀,输出会标记 ``。 + +例如,当 `mem_dump_cap = 3` 时,内容为 `"hello from rust"` 的 `&str` +会显示为: + +```text +"hel" +``` + +如果目标 DWARF 暴露了已知成员,也可以显式读取: + +```ghostscope +trace do_stuff { + print "manual={:s.*}", text.length, text.data_ptr; + print "manual_hex={:x.*}", text.length, text.data_ptr; +} +``` + +这种显式写法依赖目标的物理成员路径;语义形式会先验证并隐藏这些路径。 + +#### 嵌套 Rust 值 + +内联 DWARF 结构格式化会递归执行,最大深度为 32。外层适配器采集 Rust +值后,其中的内联值也遵循这一规则: + +```text +Outer { inner: Pair } 内联 Pair 使用递归 DWARF 格式化 +Vec Vec 采集元素,Pair 按结构格式化 +Rc Rc 投影 Pair 和计数,Pair 内联格式化 +Cell<(i32, u16)> Cell 投影并格式化内联 tuple 值 +``` + +语义适配器目前只根据根表达式类型选择一次,不会再次递归采集嵌套语义容器: + +```text +Rc> 不会递归采集内部 Vec 的元素 +Cell 不会递归采集内部 String 的字节 +Vec String 元素保留物理 DWARF 展示 +HashMap> key 和 value 不会运行嵌套适配器 +``` + +已知的嵌套字段仍可在 DSL 中显式访问。该限制只影响整个根值的自动语义展示。 +要支持递归语义采集,需要限制深度、字节数、元素数和解引用次数,并提供循环 +检测、确定性的事件空间预留,以及每个子值独立的读取错误和截断状态。 ## 变量 @@ -893,7 +992,8 @@ trace foo { 4. **字符串操作有限**:支持 C 字符串等值(==/!=)与内置函数 `strncmp`/`starts_with`;不支持字符串连接或一般性字符串处理 5. **整数运算限定**:支持整数算术与位运算;不支持浮点运算 6. **无动态内存**:不能分配内存 -7. **源语言支持不均衡**:C 支持最好;C++ 和 Rust 目前主要依赖自动 demangle 和基于 DWARF 布局的访问,大多数语言特性尚未支持 +7. **源语言支持与具体类型和 DWARF 布局有关**:详见 + [源语言支持现状](#源语言支持现状) ## 最佳实践 diff --git a/e2e-tests/cache-fixtures.sh b/e2e-tests/cache-fixtures.sh index d5bbd440..cc3a9577 100644 --- a/e2e-tests/cache-fixtures.sh +++ b/e2e-tests/cache-fixtures.sh @@ -33,6 +33,19 @@ fixture_cache_paths() { if [[ -f "$rust_binary" ]]; then printf '%s\n' "$rust_binary" fi + + # The target-compiler matrix stores one standalone Rust binary below + # bin/. These are built with host rustup toolchains after + # the fixed fixture-compiler image has produced the regular fixtures. + local rust_compat_dir="e2e-tests/tests/fixtures/rust_compat_program/bin" + if [[ -d "$rust_compat_dir" ]]; then + find "$rust_compat_dir" \ + -mindepth 2 \ + -maxdepth 2 \ + -type f \ + -perm -111 \ + -print + fi ) | LC_ALL=C sort -u } diff --git a/e2e-tests/compile-rust-compat-fixtures.sh b/e2e-tests/compile-rust-compat-fixtures.sh new file mode 100755 index 00000000..de557d73 --- /dev/null +++ b/e2e-tests/compile-rust-compat-fixtures.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MATRIX_FILE="$ROOT_DIR/rust-compat-toolchains.txt" +FIXTURE_DIR="$ROOT_DIR/tests/fixtures/rust_compat_program" +SOURCE="$FIXTURE_DIR/main.rs" + +configured_toolchains() { + if [[ -n "${GHOSTSCOPE_RUST_COMPAT_TOOLCHAINS:-}" ]]; then + printf '%s\n' "$GHOSTSCOPE_RUST_COMPAT_TOOLCHAINS" \ + | tr ',' '\n' \ + | sed '/^[[:space:]]*$/d' + return + fi + + sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$MATRIX_FILE" +} + +while IFS= read -r toolchain; do + toolchain="$(printf '%s' "$toolchain" | xargs)" + safe_toolchain="$(printf '%s' "$toolchain" | sed 's/[^A-Za-z0-9]/_/g')" + output_dir="$FIXTURE_DIR/bin/$safe_toolchain" + binary="$output_dir/rust_compat_program" + + printf '==> compiling Rust compatibility fixture with %s\n' "$toolchain" + mkdir -p "$output_dir" + rustup run "$toolchain" rustc \ + --edition=2018 \ + -g \ + -C opt-level=0 \ + "$SOURCE" \ + -o "$binary" +done < <(configured_toolchains) diff --git a/e2e-tests/rust-compat-toolchains.txt b/e2e-tests/rust-compat-toolchains.txt new file mode 100644 index 00000000..91116d64 --- /dev/null +++ b/e2e-tests/rust-compat-toolchains.txt @@ -0,0 +1,10 @@ +# Rust releases whose target DWARF layouts GhostScope checks explicitly. +# Keep this list small: each entry adds both a DWARF planning check and a +# standard host e2e run against a binary produced by that exact toolchain. +1.35.0 +1.49.0 +# Last release before Rust 1.82 moved RawVec pointers under `inner`. +1.81.0 +1.88.0 +1.93.0 +nightly-2026-05-30 diff --git a/e2e-tests/tests/common/mod.rs b/e2e-tests/tests/common/mod.rs index 03c73aaa..c7bcc040 100644 --- a/e2e-tests/tests/common/mod.rs +++ b/e2e-tests/tests/common/mod.rs @@ -110,7 +110,8 @@ struct RegisteredFixture { // Keep CI fixture-cache discovery in e2e-tests/cache-fixtures.sh aligned with // this registry and the fixture build outputs. Most C/C++ fixtures are cached // by executable outputs directly under tests/fixtures/*_program; rust_global is -// the explicit target/debug exception. +// the explicit target/debug exception. The standalone Rust target-compiler +// matrix is built and cached separately by compile-rust-compat-fixtures.sh. const REGISTERED_FIXTURES: &[RegisteredFixture] = &[ RegisteredFixture { name: "sample_program", @@ -1267,6 +1268,7 @@ lazy_static! { // Re-export the shared runner for convenience in tests pub mod runner; +pub mod rust_toolchain; pub mod sandbox; pub mod targets; pub mod termination; diff --git a/e2e-tests/tests/common/rust_toolchain.rs b/e2e-tests/tests/common/rust_toolchain.rs new file mode 100644 index 00000000..1503a379 --- /dev/null +++ b/e2e-tests/tests/common/rust_toolchain.rs @@ -0,0 +1,160 @@ +#![allow(dead_code)] + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use ghostscope_dwarf::RustcVersion; +use tempfile::{Builder, TempDir}; + +const DEFAULT_TOOLCHAINS: &str = include_str!("../../rust-compat-toolchains.txt"); +const TOOLCHAINS_ENV: &str = "GHOSTSCOPE_RUST_COMPAT_TOOLCHAINS"; + +pub fn fixture_tempdir() -> anyhow::Result { + // Container-backed targets can only access host paths covered by the + // repository bind mount, so keep runtime-compiled fixtures under it. + Ok(Builder::new() + .prefix(".ghostscope-rust-e2e-") + .tempdir_in(env!("CARGO_MANIFEST_DIR"))?) +} + +pub fn compile_standalone_fixture( + rustc: &Path, + toolchain: &str, + source: &Path, + binary: &Path, +) -> anyhow::Result<()> { + compile_fixture(rustc, toolchain, source, binary, true) +} + +pub fn compile_compact_standalone_fixture( + rustc: &Path, + toolchain: &str, + source: &Path, + binary: &Path, +) -> anyhow::Result<()> { + compile_fixture(rustc, toolchain, source, binary, false) +} + +fn compile_fixture( + rustc: &Path, + toolchain: &str, + source: &Path, + binary: &Path, + link_dead_code: bool, +) -> anyhow::Result<()> { + let mut command = Command::new(rustc); + command + .args(["--edition=2018", "-g"]) + .arg("-C") + .arg("opt-level=0"); + if link_dead_code { + command.arg("-C").arg("link-dead-code"); + } + let output = command.arg(source).arg("-o").arg(binary).output()?; + anyhow::ensure!( + output.status.success(), + "rustc {toolchain} failed for {}:\n{}", + source.display(), + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) +} + +pub fn rustc_for_toolchain(toolchain: &str) -> Option { + if let Ok(output) = Command::new("rustup") + .args(["which", "--toolchain", toolchain, "rustc"]) + .output() + { + if output.status.success() { + let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()); + if path.is_file() { + return Some(path); + } + } + } + + let mut rustup_homes = Vec::new(); + if let Some(home) = std::env::var_os("RUSTUP_HOME") { + rustup_homes.push(PathBuf::from(home)); + } + if let Some(home) = std::env::var_os("HOME") { + rustup_homes.push(PathBuf::from(home).join(".rustup")); + } + if let Some(cargo_home) = std::env::var_os("CARGO_HOME") { + if let Some(home) = Path::new(&cargo_home).parent() { + rustup_homes.push(home.join(".rustup")); + } + } + if let Some(user) = std::env::var_os("SUDO_USER") { + let user = user.to_string_lossy(); + if !user.is_empty() && !user.contains('/') { + rustup_homes.push(PathBuf::from("/home").join(user.as_ref()).join(".rustup")); + } + } + + rustup_homes.into_iter().find_map(|rustup_home| { + std::fs::read_dir(rustup_home.join("toolchains")) + .ok()? + .filter_map(Result::ok) + .find_map(|entry| { + let name = entry.file_name(); + let name = name.to_string_lossy(); + let rustc = entry.path().join("bin/rustc"); + (name == toolchain || name.starts_with(&format!("{toolchain}-"))) + .then_some(rustc) + .filter(|path| path.is_file()) + }) + }) +} + +pub fn configured_toolchains() -> Vec { + std::env::var(TOOLCHAINS_ENV) + .ok() + .map(|value| parse_toolchains(&value, ',')) + .filter(|toolchains| !toolchains.is_empty()) + .unwrap_or_else(|| parse_toolchains(DEFAULT_TOOLCHAINS, '\n')) +} + +pub fn rustc_version(rustc: &Path, toolchain: &str) -> anyhow::Result { + let output = Command::new(rustc).arg("--version").output()?; + anyhow::ensure!( + output.status.success(), + "rustc {toolchain} --version failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout)?; + stdout + .split_ascii_whitespace() + .nth(1) + .and_then(RustcVersion::parse) + .ok_or_else(|| anyhow::anyhow!("unrecognized rustc version output: {stdout:?}")) +} + +pub fn toolchain_id(toolchain: &str) -> String { + toolchain + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() { + character + } else { + '_' + } + }) + .collect() +} + +pub fn precompiled_compat_fixture(toolchain: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_compat_program/bin") + .join(toolchain_id(toolchain)) + .join("rust_compat_program") +} + +fn parse_toolchains(value: &str, separator: char) -> Vec { + value + .split(separator) + .map(|line| line.split('#').next().unwrap_or_default().trim()) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect() +} diff --git a/e2e-tests/tests/fixtures/rust_compat_program/main.rs b/e2e-tests/tests/fixtures/rust_compat_program/main.rs new file mode 100644 index 00000000..70b40ba2 --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_compat_program/main.rs @@ -0,0 +1,238 @@ +use std::collections::{BTreeMap, HashMap}; +use std::num::NonZeroI32; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread; +use std::time::Duration; + +static RESULT: AtomicUsize = AtomicUsize::new(0); + +pub enum MatrixEnum { + Unit, + Tuple(i16, u16), + Struct { value: u16, flag: u16 }, +} + +pub enum MatrixInner { + Pair(i16, u16), +} + +pub enum MatrixOuter { + Empty, + Wrapped(MatrixInner), +} + +#[repr(C)] +pub enum MatrixReprC { + Unit, + Tuple(i16, u16), + Struct { left: i16, right: u16 }, +} + +pub enum MatrixFieldless { + First, + Second, +} + +pub enum MatrixSingle { + Only(i32), +} + +#[repr(i8)] +pub enum MatrixSigned { + Negative = -1, + Positive = 1, +} + +#[repr(u64)] +pub enum MatrixUnsigned { + Low = 1, + High = 0x8000_0000_0000_0000, +} + +// This fixture intentionally uses only APIs available in Rust 1.35. The +// parameters represent the layout families most likely to change between +// target compilers; the complete adapter matrix lives in ghostscope-dwarf. +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_values( + string: String, + vector: Vec, + btree_map: BTreeMap, + hash_map: HashMap, +) -> usize { + string.len() + vector.len() + btree_map.len() + hash_map.len() +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_rc(rc: Rc) -> usize { + *rc as usize +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_dst(rc: Rc, arc: Arc) -> usize { + rc.len() + arc.len() +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_mut_str(value: &mut str) -> usize { + value.len() +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_enums( + unit: &MatrixEnum, + tuple: &MatrixEnum, + struct_value: &MatrixEnum, + fieldless: &MatrixFieldless, + some: &Option, + none: &Option, +) -> usize { + let unit = match *unit { + MatrixEnum::Unit => 1, + _ => 0, + }; + let tuple = match *tuple { + MatrixEnum::Tuple(left, right) => left as usize + right as usize, + _ => 0, + }; + let struct_value = match *struct_value { + MatrixEnum::Struct { value, flag } => value as usize + flag as usize, + _ => 0, + }; + let fieldless = match *fieldless { + MatrixFieldless::First => 0, + MatrixFieldless::Second => 1, + }; + unit + tuple + struct_value + fieldless + some.map_or(0, NonZeroI32::get) as usize + + none.map_or(0, NonZeroI32::get) as usize +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_enum_edges( + single: &MatrixSingle, + signed: &MatrixSigned, + unsigned: &MatrixUnsigned, +) -> usize { + let single = match *single { + MatrixSingle::Only(value) => value as usize, + }; + let signed = match *signed { + MatrixSigned::Negative => 1, + MatrixSigned::Positive => 0, + }; + let unsigned = match *unsigned { + MatrixUnsigned::Low => 0, + MatrixUnsigned::High => 1, + }; + single + signed + unsigned +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_nested(value: &MatrixOuter) -> usize { + match value { + MatrixOuter::Empty => 0, + MatrixOuter::Wrapped(MatrixInner::Pair(left, right)) => { + *left as usize + *right as usize + } + } +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_pointer_niche( + some: &Option<&i32>, + none: &Option<&i32>, +) -> usize { + let some = match *some { + Some(value) => *value as usize, + None => 0, + }; + let none = match *none { + Some(value) => *value as usize, + None => 0, + }; + some + none +} + +#[no_mangle] +#[inline(never)] +pub fn observe_matrix_repr_c( + unit: &MatrixReprC, + tuple: &MatrixReprC, + struct_value: &MatrixReprC, +) -> usize { + let unit = match unit { + MatrixReprC::Unit => 1, + _ => 0, + }; + let tuple = match tuple { + MatrixReprC::Tuple(left, right) => *left as usize + *right as usize, + _ => 0, + }; + let struct_value = match struct_value { + MatrixReprC::Struct { left, right } => *left as usize + *right as usize, + _ => 0, + }; + unit + tuple + struct_value +} + +fn main() { + loop { + let mut btree_map = BTreeMap::new(); + btree_map.insert(-7, 13); + + let mut hash_map = HashMap::new(); + hash_map.insert(29, 17); + + let mut result = observe_matrix_values( + String::from("matrix = string"), + vec![10, -20], + btree_map, + hash_map, + ); + result += observe_matrix_rc(Rc::new(11)); + result += observe_matrix_dst(Rc::from("matrix-rc"), Arc::from("matrix-arc")); + let mut mutable_text = String::from("matrix mutable"); + result += observe_matrix_mut_str(mutable_text.as_mut_str()); + result += observe_matrix_enums( + &MatrixEnum::Unit, + &MatrixEnum::Tuple(31, 37), + &MatrixEnum::Struct { + value: 47, + flag: 53, + }, + &MatrixFieldless::Second, + &NonZeroI32::new(59), + &None, + ); + result += observe_matrix_enum_edges( + &MatrixSingle::Only(71), + &MatrixSigned::Negative, + &MatrixUnsigned::High, + ); + result += observe_matrix_nested(&MatrixOuter::Wrapped(MatrixInner::Pair(7, 9))); + + let pointed = 67; + let pointer_some = Some(&pointed); + let pointer_none: Option<&i32> = None; + result += observe_matrix_pointer_niche(&pointer_some, &pointer_none); + result += observe_matrix_repr_c( + &MatrixReprC::Unit, + &MatrixReprC::Tuple(73, 79), + &MatrixReprC::Struct { + left: 83, + right: 89, + }, + ); + RESULT.store(result, Ordering::Relaxed); + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/fixtures/rust_global_program/src/main.rs b/e2e-tests/tests/fixtures/rust_global_program/src/main.rs index de9ca807..c908babb 100644 --- a/e2e-tests/tests/fixtures/rust_global_program/src/main.rs +++ b/e2e-tests/tests/fixtures/rust_global_program/src/main.rs @@ -2,14 +2,170 @@ #![allow(static_mut_refs)] use std::{ + cell::{Cell, RefCell}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, + ffi::OsString, marker::{PhantomData, PhantomPinned}, - num::NonZeroU32, + num::{NonZeroI32, NonZeroU128, NonZeroU32}, + rc::Rc, + sync::Arc, thread, time::Duration, }; +#[cfg(unix)] +use std::os::unix::ffi::OsStringExt; pub static mut G_COUNTER: i32 = 0; pub static G_MESSAGE: &str = "hello from rust"; +pub static G_EMPTY_MESSAGE: &str = ""; +pub static G_NUL_MESSAGE: &str = "left\0right"; +pub static mut G_OWNED_MESSAGE: String = String::new(); +pub static mut G_EMPTY_OWNED: String = String::new(); +pub static mut G_NUL_OWNED: String = String::new(); +pub static mut G_SEPARATOR_OWNED: String = String::new(); +pub static mut G_VEC_U8: Vec = Vec::new(); +pub static mut G_VEC_I32: Vec = Vec::new(); +pub static mut G_EMPTY_VEC: Vec = Vec::new(); +pub static mut G_VEC_UNIT: Vec<()> = Vec::new(); +pub static mut G_VEC_DEQUE_I32: VecDeque = VecDeque::new(); +pub static mut G_VEC_DEQUE_UNIT: VecDeque<()> = VecDeque::new(); +pub static mut G_SLICE_I32: &[i32] = &[]; +pub static mut G_MUT_SLICE_U16: &mut [u16] = &mut []; +pub static mut G_EMPTY_SLICE: &[i32] = &[]; +pub static G_NONZERO_U32: NonZeroU32 = NonZeroU32::new(7).unwrap(); +pub static G_NONZERO_I32: NonZeroI32 = NonZeroI32::new(-9).unwrap(); +pub static G_NONZERO_U128: NonZeroU128 = + NonZeroU128::new(340_282_366_920_938_463_463_374_607_431_768_211_454).unwrap(); +pub static mut G_CELL_U32: Cell = Cell::new(41); +pub static mut G_CELL_PAIR: Cell<(i32, u16)> = Cell::new((-4, 12)); +pub static mut G_CELL_UNIT: Cell<()> = Cell::new(()); +pub static mut G_REF_CELL_IDLE: RefCell = RefCell::new(17); +pub static mut G_REF_CELL_SHARED: RefCell = RefCell::new(23); +pub static mut G_REF_CELL_MUT: RefCell = RefCell::new(31); +pub static mut G_REF_CELL_PAIR: RefCell<(i32, u16)> = RefCell::new((-6, 14)); +pub static mut G_REF_CELL_UNIT: RefCell<()> = RefCell::new(()); + +pub mod user_types { + pub struct String { + pub vec: std::vec::Vec, + pub marker: u32, + } + + #[repr(C)] + pub struct Vec { + pub buf: Buffer, + pub len: usize, + } + + #[repr(C)] + pub struct Buffer { + pub inner: BufferInner, + } + + #[repr(C)] + pub struct BufferInner { + pub padding: usize, + pub ptr: Unique, + } + + #[repr(C)] + pub struct Unique { + pub pointer: NonNull, + } + + #[repr(C)] + pub struct NonNull { + pub pointer: *mut T, + } + + pub struct NonZero(pub NonZeroInner); + + pub struct NonZeroInner(pub T); + + pub struct Cell(pub UnsafeCell); + + pub struct UnsafeCell(pub T); + + pub struct RefCell { + pub value: UnsafeCell, + pub borrow: Cell, + } + + pub struct BorrowRef { + pub borrow: *const Cell, + } + + pub struct BorrowRefMut { + pub borrow: *const Cell, + } + + pub struct Ref { + pub value: NonNull, + pub borrow: BorrowRef, + } + + pub struct RefMut { + pub value: NonNull, + pub borrow: BorrowRefMut, + pub marker: std::marker::PhantomData, + } + + pub struct Rc { + pub value: T, + } + + pub struct Arc { + pub value: T, + } + + pub struct HashMap { + pub key: K, + pub value: V, + } + + pub struct HashSet { + pub value: T, + } + + pub struct BTreeMap { + pub key: K, + pub value: V, + } + + pub struct BTreeSet { + pub value: T, + } +} + +pub static mut G_USER_STRING: user_types::String = user_types::String { + vec: Vec::new(), + marker: 41, +}; + +pub static mut G_USER_VEC: user_types::Vec = user_types::Vec { + buf: user_types::Buffer { + inner: user_types::BufferInner { + padding: 0, + ptr: user_types::Unique { + pointer: user_types::NonNull { + pointer: std::ptr::null_mut(), + }, + }, + }, + }, + len: 0, +}; + +pub static G_USER_NONZERO: user_types::NonZero = + user_types::NonZero(user_types::NonZeroInner(11)); + +pub static mut G_USER_CELL: user_types::Cell = + user_types::Cell(user_types::UnsafeCell(13)); + +pub static mut G_USER_REF_CELL: user_types::RefCell = user_types::RefCell { + value: user_types::UnsafeCell(19), + borrow: user_types::Cell(user_types::UnsafeCell(0)), +}; #[repr(C)] pub struct Config { @@ -107,11 +263,214 @@ pub static mut GLOBAL_ENUM: GlobalState = GlobalState::Idle; pub static mut GLOBAL_ENUM_BITS: i32 = 0; pub mod math { + use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + rc::Rc, + sync::Arc, + }; + #[inline(never)] pub fn do_stuff(x: i32) -> i32 { // Simple function for DWARF function indexing x + 1 } + + #[inline(never)] + pub fn observe_boxed_str(value: Box, empty: Box) -> usize { + value.len() + empty.len() + } + + #[inline(never)] + pub fn observe_os_string( + value: std::ffi::OsString, + invalid: std::ffi::OsString, + empty: std::ffi::OsString, + ) -> usize { + value.len() + invalid.len() + empty.len() + } + + #[inline(never)] + pub fn observe_vec_deque( + wrapped: std::collections::VecDeque, + contiguous: std::collections::VecDeque, + empty: std::collections::VecDeque, + ) -> usize { + wrapped.len() + contiguous.len() + empty.len() + } + + #[inline(never)] + pub fn observe_nonzero(value: std::num::NonZeroU32) -> u32 { + value.get() + } + + #[inline(never)] + pub fn observe_ref_cell_states( + idle: &std::cell::RefCell, + shared: &std::cell::RefCell, + mutable: &std::cell::RefCell, + pair: &std::cell::RefCell<(i32, u16)>, + unit: &std::cell::RefCell<()>, + owned: std::cell::RefCell, + ) -> usize { + std::hint::black_box(idle.as_ptr()); + std::hint::black_box(shared.as_ptr()); + std::hint::black_box(mutable.as_ptr()); + std::hint::black_box(pair.as_ptr()); + std::hint::black_box(unit.as_ptr()); + std::hint::black_box(owned.into_inner()); + 1 + } + + #[inline(never)] + pub fn observe_ref_guards( + shared: &std::cell::Ref<'_, i32>, + mutable: &std::cell::RefMut<'_, i32>, + pair: &std::cell::Ref<'_, (i32, u16)>, + unit: &std::cell::Ref<'_, ()>, + ) -> i64 { + let shared = std::hint::black_box(**shared as i64); + let mutable = std::hint::black_box(**mutable as i64); + let pair = std::hint::black_box((**pair).0 as i64); + std::hint::black_box(**unit); + shared + mutable + pair + } + + #[inline(never)] + pub fn observe_user_ref(value: &crate::user_types::Ref) -> usize { + std::hint::black_box(value.value.pointer); + std::hint::black_box(value.borrow.borrow); + 1 + } + + #[inline(never)] + pub fn observe_rc_arc( + rc: Rc<(i32, u16)>, + arc: Arc<(i32, u16)>, + rc_unit: Rc<()>, + arc_unit: Arc<()>, + ) -> i64 { + let rc_value = std::hint::black_box(rc.0 as i64); + let arc_value = std::hint::black_box(arc.0 as i64); + std::hint::black_box(Rc::strong_count(&rc)); + std::hint::black_box(Rc::weak_count(&rc)); + std::hint::black_box(Arc::strong_count(&arc)); + std::hint::black_box(Arc::weak_count(&arc)); + std::hint::black_box((rc_unit, arc_unit)); + rc_value + arc_value + } + + #[inline(never)] + pub fn observe_user_rc_arc( + rc: &crate::user_types::Rc, + arc: &crate::user_types::Arc, + ) -> i64 { + std::hint::black_box((rc.value, arc.value)); + rc.value as i64 + arc.value as i64 + } + + #[inline(never)] + pub fn observe_hash_collections( + map: HashMap, + set: HashSet, + empty_map: HashMap, + empty_set: HashSet, + unit_map: HashMap<(), ()>, + unit_set: HashSet<()>, + ) -> usize { + std::hint::black_box(( + &map, + &set, + &empty_map, + &empty_set, + &unit_map, + &unit_set, + )); + map.len() + + set.len() + + empty_map.len() + + empty_set.len() + + unit_map.len() + + unit_set.len() + } + + #[inline(never)] + pub fn observe_user_hash_collections( + map: &crate::user_types::HashMap, + set: &crate::user_types::HashSet, + ) -> i64 { + std::hint::black_box((map.key, map.value, set.value)); + map.key as i64 + map.value as i64 + set.value as i64 + } + + #[inline(never)] + pub fn observe_btree_collections( + map: BTreeMap, + set: BTreeSet, + empty_map: BTreeMap, + empty_set: BTreeSet, + unit_map: BTreeMap<(), ()>, + unit_set: BTreeSet<()>, + ) -> usize { + std::hint::black_box(( + &map, + &set, + &empty_map, + &empty_set, + &unit_map, + &unit_set, + )); + map.len() + + set.len() + + empty_map.len() + + empty_set.len() + + unit_map.len() + + unit_set.len() + } + + #[inline(never)] + pub fn observe_deep_btree_collections( + map: BTreeMap, + set: BTreeSet, + ) -> usize { + std::hint::black_box((&map, &set)); + map.len() + set.len() + } + + #[inline(never)] + pub fn observe_internal_btree_collections( + map: BTreeMap, + set: BTreeSet, + ) -> usize { + std::hint::black_box((&map, &set)); + map.len() + set.len() + } + + #[inline(never)] + pub fn observe_user_btree_collections( + map: &crate::user_types::BTreeMap, + set: &crate::user_types::BTreeSet, + ) -> i64 { + std::hint::black_box((map.key, map.value, set.value)); + map.key as i64 + map.value as i64 + set.value as i64 + } +} + +fn wrapped_vec_deque() -> VecDeque { + let mut deque = VecDeque::with_capacity(4); + let filler_len = deque.capacity() - 2; + + for _ in 0..filler_len { + deque.push_back(-1); + } + deque.push_back(10); + deque.push_back(20); + for _ in 0..filler_len { + deque.pop_front(); + } + deque.push_back(30); + deque.push_back(40); + + deque } fn touch_globals() -> i32 { @@ -167,9 +526,40 @@ fn touch_globals() -> i32 { GlobalState::TupleState { left, right } => left + if right { 1 } else { 0 }, }; GLOBAL_ENUM_BITS = enum_contrib; + G_CELL_UNIT.get(); let total = CONFIG.a as i64 + G_MESSAGE.len() as i64 + + G_EMPTY_MESSAGE.len() as i64 + + G_NUL_MESSAGE.len() as i64 + + G_OWNED_MESSAGE.len() as i64 + + G_EMPTY_OWNED.len() as i64 + + G_NUL_OWNED.len() as i64 + + G_SEPARATOR_OWNED.len() as i64 + + G_USER_STRING.vec.len() as i64 + + G_USER_STRING.marker as i64 + + G_VEC_U8.len() as i64 + + G_VEC_I32.len() as i64 + + G_EMPTY_VEC.len() as i64 + + G_VEC_UNIT.len() as i64 + + G_VEC_DEQUE_I32.len() as i64 + + G_VEC_DEQUE_UNIT.len() as i64 + + G_SLICE_I32.len() as i64 + + G_MUT_SLICE_U16.len() as i64 + + G_EMPTY_SLICE.len() as i64 + + G_NONZERO_U32.get() as i64 + + G_NONZERO_I32.get() as i64 + + G_NONZERO_U128.get() as i64 + + G_CELL_U32.get() as i64 + + G_CELL_PAIR.get().0 as i64 + + *G_REF_CELL_IDLE.get_mut() as i64 + + *G_REF_CELL_SHARED.get_mut() as i64 + + *G_REF_CELL_MUT.get_mut() as i64 + + G_REF_CELL_PAIR.get_mut().0 as i64 + + G_USER_NONZERO.0.0 as i64 + + G_USER_CELL.0.0 as i64 + + G_USER_REF_CELL.value.0 as i64 + + G_USER_VEC.len as i64 + GLOBAL_PAIRS[0].0 as i64 + union_value as i64 + pinned_value as i64 @@ -181,9 +571,125 @@ fn touch_globals() -> i32 { } fn main() { + // SAFETY: The fixture initializes these globals before its tracing loop and + // does not mutate them afterward. + unsafe { + G_OWNED_MESSAGE = String::from("owned from rust"); + G_NUL_OWNED = String::from("owned\0value"); + G_SEPARATOR_OWNED = String::from("left = right"); + G_USER_STRING.vec = b"user bytes".to_vec(); + G_VEC_U8 = vec![1, 2, 3, 255]; + G_VEC_I32 = vec![10, -20, 30, 40]; + G_VEC_UNIT = vec![(); 3]; + G_VEC_DEQUE_I32 = VecDeque::from([10, 20, 30, 40]); + G_VEC_DEQUE_UNIT = VecDeque::from([(), (), ()]); + G_SLICE_I32 = Box::leak(vec![7, -8, 9].into_boxed_slice()); + G_MUT_SLICE_U16 = Box::leak(vec![1000, 2000, 65535].into_boxed_slice()); + } + let mut acc: i64 = 0; for _ in 0..50000 { acc += math::do_stuff(3) as i64; + acc += math::observe_boxed_str("boxed from rust".into(), "".into()) as i64; + acc += math::observe_os_string( + OsString::from("os from rust"), + OsString::from_vec(vec![b'o', b's', 0xff, b'x']), + OsString::new(), + ) as i64; + acc += math::observe_vec_deque( + wrapped_vec_deque(), + VecDeque::from([7_u16, 8, 9]), + VecDeque::new(), + ) as i64; + acc += math::observe_nonzero(NonZeroU32::new(23).unwrap()) as i64; + // SAFETY: The fixture is single-threaded. The guards deliberately stay + // live across the probe so its borrow-state presentation is observable. + unsafe { + let shared_one = G_REF_CELL_SHARED.borrow(); + let shared_two = G_REF_CELL_SHARED.borrow(); + let mutable = G_REF_CELL_MUT.borrow_mut(); + acc += math::observe_ref_cell_states( + &G_REF_CELL_IDLE, + &G_REF_CELL_SHARED, + &G_REF_CELL_MUT, + &G_REF_CELL_PAIR, + &G_REF_CELL_UNIT, + RefCell::new(-12_i16), + ) as i64; + let pair = G_REF_CELL_PAIR.borrow(); + let unit = G_REF_CELL_UNIT.borrow(); + acc += math::observe_ref_guards(&shared_one, &mutable, &pair, &unit); + acc += pair.0 as i64; + acc += *shared_one as i64 + *shared_two as i64 + *mutable as i64; + } + let user_ref = user_types::Ref { + value: user_types::NonNull { + pointer: std::ptr::null_mut(), + }, + borrow: user_types::BorrowRef { + borrow: std::ptr::null(), + }, + }; + acc += math::observe_user_ref(&user_ref) as i64; + let rc = Rc::new((-7_i32, 13_u16)); + let rc_peer = Rc::clone(&rc); + let rc_weak = Rc::downgrade(&rc); + let arc = Arc::new((29_i32, 17_u16)); + let arc_peer = Arc::clone(&arc); + let arc_weak = Arc::downgrade(&arc); + acc += math::observe_rc_arc( + Rc::clone(&rc), + Arc::clone(&arc), + Rc::new(()), + Arc::new(()), + ); + std::hint::black_box((rc_peer, rc_weak, arc_peer, arc_weak)); + let user_rc = user_types::Rc { value: 37_i32 }; + let user_arc = user_types::Arc { value: 41_i32 }; + acc += math::observe_user_rc_arc(&user_rc, &user_arc); + let map = HashMap::from([(-7_i32, 13_u16), (29_i32, 17_u16)]); + let set = HashSet::from([-9_i32, 5_i32]); + acc += math::observe_hash_collections( + map, + set, + HashMap::new(), + HashSet::new(), + HashMap::from([((), ())]), + HashSet::from([()]), + ) as i64; + let user_map = user_types::HashMap { + key: 43_i32, + value: 47_u16, + }; + let user_set = user_types::HashSet { value: 53_i32 }; + acc += math::observe_user_hash_collections(&user_map, &user_set); + acc += math::observe_btree_collections( + BTreeMap::from([(-7_i32, 13_u16), (29_i32, 17_u16)]), + BTreeSet::from([-9_i32, 5_i32]), + BTreeMap::new(), + BTreeSet::new(), + BTreeMap::from([((), ())]), + BTreeSet::from([()]), + ) as i64; + let internal_map = (0_i32..20) + .map(|key| (key, (key * 3 + 1) as u16)) + .collect(); + let internal_set = (0_i32..20).collect(); + acc += math::observe_internal_btree_collections(internal_map, internal_set) as i64; + let deep_map = (0_i32..160) + .map(|key| (key, (key * 3 + 1) as u16)) + .collect(); + let deep_set = (0_i32..160).collect(); + acc += math::observe_deep_btree_collections(deep_map, deep_set) as i64; + let user_btree_map = user_types::BTreeMap { + key: 59_i32, + value: 61_u16, + }; + let user_btree_set = user_types::BTreeSet { value: 67_i32 }; + acc += math::observe_user_btree_collections( + &user_btree_map, + &user_btree_set, + ); acc += touch_globals() as i64; thread::sleep(Duration::from_millis(1000)); } diff --git a/e2e-tests/tests/fixtures/rust_legacy_btree_program/main.rs b/e2e-tests/tests/fixtures/rust_legacy_btree_program/main.rs new file mode 100644 index 00000000..c8e64a2c --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_legacy_btree_program/main.rs @@ -0,0 +1,45 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::thread; +use std::time::Duration; + +static mut RESULT: usize = 0; + +#[no_mangle] +#[inline(never)] +pub fn observe_legacy_btree_nodes(map: BTreeMap, set: BTreeSet) -> usize { + map.len() + set.len() +} + +#[no_mangle] +#[inline(never)] +pub fn observe_legacy_btree_edge_cases( + empty_map: BTreeMap, + empty_set: BTreeSet, + unit_map: BTreeMap<(), ()>, + unit_set: BTreeSet<()>, +) -> usize { + empty_map.len() + empty_set.len() + unit_map.len() + unit_set.len() +} + +fn main() { + loop { + let mut map = BTreeMap::new(); + let mut set = BTreeSet::new(); + for key in 0_i32..20 { + map.insert(key, (key * 3 + 1) as u16); + set.insert(key); + } + + let mut unit_map = BTreeMap::new(); + unit_map.insert((), ()); + let mut unit_set = BTreeSet::new(); + unit_set.insert(()); + + let result = observe_legacy_btree_nodes(map, set) + + observe_legacy_btree_edge_cases(BTreeMap::new(), BTreeSet::new(), unit_map, unit_set); + unsafe { + std::ptr::write_volatile(&mut RESULT, result); + } + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/fixtures/rust_legacy_hash_program/main.rs b/e2e-tests/tests/fixtures/rust_legacy_hash_program/main.rs new file mode 100644 index 00000000..c300cbb0 --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_legacy_hash_program/main.rs @@ -0,0 +1,53 @@ +use std::collections::{HashMap, HashSet}; +use std::thread; +use std::time::Duration; + +static mut RESULT: usize = 0; + +#[no_mangle] +#[inline(never)] +pub fn observe_legacy_hash_collections( + map: HashMap, + set: HashSet, + empty_map: HashMap, + empty_set: HashSet, + unit_map: HashMap<(), ()>, + unit_set: HashSet<()>, +) -> usize { + map.len() + + set.len() + + empty_map.len() + + empty_set.len() + + unit_map.len() + + unit_set.len() +} + +fn main() { + loop { + let mut map = HashMap::new(); + map.insert(-7, 13); + map.insert(29, 17); + + let mut set = HashSet::new(); + set.insert(-9); + set.insert(5); + + let mut unit_map = HashMap::new(); + unit_map.insert((), ()); + let mut unit_set = HashSet::new(); + unit_set.insert(()); + + let result = observe_legacy_hash_collections( + map, + set, + HashMap::new(), + HashSet::new(), + unit_map, + unit_set, + ); + unsafe { + std::ptr::write_volatile(&mut RESULT, result); + } + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/fixtures/rust_legacy_sequence_program/main.rs b/e2e-tests/tests/fixtures/rust_legacy_sequence_program/main.rs new file mode 100644 index 00000000..b580e05a --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_legacy_sequence_program/main.rs @@ -0,0 +1,83 @@ +use std::collections::VecDeque; +use std::thread; +use std::time::Duration; + +static SLICE_VALUES: [i32; 3] = [-3, 5, 8]; +static EMPTY_SLICE: [i32; 0] = []; +static mut RESULT: usize = 0; + +fn wrapped_vec_deque() -> VecDeque { + let mut values = VecDeque::with_capacity(8); + let advance = values.capacity() - 2; + for _ in 0..advance { + values.push_back(-1); + } + for _ in 0..advance { + let _ = values.pop_front(); + } + for value in 3_i32..=8 { + values.push_back(value); + } + assert!(!values.as_slices().1.is_empty()); + values +} + +#[no_mangle] +#[inline(never)] +pub fn observe_legacy_sequences( + slice: &[i32], + vector: Vec, + wrapped: VecDeque, + contiguous: VecDeque, + empty_slice: &[i32], + empty_vector: Vec, + empty_deque: VecDeque, + unit_vector: Vec<()>, + unit_deque: VecDeque<()>, +) -> usize { + slice.len() + + vector.len() + + wrapped.len() + + contiguous.len() + + empty_slice.len() + + empty_vector.len() + + empty_deque.len() + + unit_vector.len() + + unit_deque.len() +} + +fn main() { + loop { + // eBPF user-memory reads are non-faulting. Touch the static slice before + // the probe fires so this test exercises slice capture, not page residency. + unsafe { + std::ptr::read_volatile(SLICE_VALUES.as_ptr()); + } + + let mut contiguous = VecDeque::new(); + contiguous.push_back(7); + contiguous.push_back(8); + contiguous.push_back(9); + + let mut unit_deque = VecDeque::new(); + unit_deque.push_back(()); + unit_deque.push_back(()); + unit_deque.push_back(()); + + let result = observe_legacy_sequences( + &SLICE_VALUES, + vec![10, -20, 30], + wrapped_vec_deque(), + contiguous, + &EMPTY_SLICE, + Vec::new(), + VecDeque::new(), + vec![(); 3], + unit_deque, + ); + unsafe { + std::ptr::write_volatile(&mut RESULT, result); + } + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/fixtures/rust_legacy_string_program/main.rs b/e2e-tests/tests/fixtures/rust_legacy_string_program/main.rs new file mode 100644 index 00000000..2969ea19 --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_legacy_string_program/main.rs @@ -0,0 +1,50 @@ +use std::ffi::OsString; +use std::os::unix::ffi::OsStringExt; +use std::thread; +use std::time::Duration; + +static mut RESULT: usize = 0; + +#[no_mangle] +#[inline(never)] +pub fn observe_legacy_strings( + owned: String, + valid_os: OsString, + invalid_os: OsString, + text: &str, + boxed: Box, + empty_owned: String, + empty_os: OsString, + empty_text: &str, + empty_boxed: Box, +) -> usize { + owned.len() + + valid_os.len() + + invalid_os.len() + + text.len() + + boxed.len() + + empty_owned.len() + + empty_os.len() + + empty_text.len() + + empty_boxed.len() +} + +fn main() { + loop { + let result = observe_legacy_strings( + String::from("legacy = string"), + OsString::from("os from 1.35"), + OsString::from_vec(vec![b'o', b's', 0xff, b'x']), + "legacy\0str", + String::from("boxed from 1.35").into_boxed_str(), + String::new(), + OsString::new(), + "", + String::new().into_boxed_str(), + ); + unsafe { + std::ptr::write_volatile(&mut RESULT, result); + } + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/fixtures/rust_path_program/main.rs b/e2e-tests/tests/fixtures/rust_path_program/main.rs new file mode 100644 index 00000000..322e5392 --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_path_program/main.rs @@ -0,0 +1,47 @@ +use std::ffi::OsString; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::Duration; + +static mut RESULT: usize = 0; + +#[no_mangle] +#[inline(never)] +pub fn observe_paths( + borrowed: &Path, + owned: PathBuf, + empty: &Path, + long: &Path, +) -> usize { + std::hint::black_box(&owned); + borrowed.as_os_str().as_bytes().len() + + owned.as_os_str().as_bytes().len() + + empty.as_os_str().as_bytes().len() + + long.as_os_str().as_bytes().len() +} + +fn main() { + let borrowed = PathBuf::from("borrowed/path"); + let empty = PathBuf::new(); + let long = PathBuf::from("abcdefghijklmnopqrstuvwxyz"); + loop { + let invalid = PathBuf::from(OsString::from_vec(b"bad/\xff/path".to_vec())); + let owned_result = observe_paths( + borrowed.as_path(), + PathBuf::from("owned/path"), + empty.as_path(), + long.as_path(), + ); + let invalid_result = observe_paths( + borrowed.as_path(), + invalid, + empty.as_path(), + long.as_path(), + ); + unsafe { + std::ptr::write_volatile(&mut RESULT, owned_result + invalid_result); + } + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/fixtures/rust_rc_str_program/main.rs b/e2e-tests/tests/fixtures/rust_rc_str_program/main.rs new file mode 100644 index 00000000..9036dd7e --- /dev/null +++ b/e2e-tests/tests/fixtures/rust_rc_str_program/main.rs @@ -0,0 +1,34 @@ +use std::rc::Rc; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +static mut RESULT: usize = 0; + +#[no_mangle] +#[inline(never)] +pub fn observe_reference_counted_str(value: Rc, shared: Arc) -> usize { + std::hint::black_box((Rc::strong_count(&value), Rc::weak_count(&value))); + std::hint::black_box((Arc::strong_count(&shared), Arc::weak_count(&shared))); + value.len() + shared.len() +} + +fn main() { + loop { + let value: Rc = Rc::from("rust = rc"); + let peer = Rc::clone(&value); + let weak = Rc::downgrade(&value); + let shared: Arc = Arc::from("rust = arc"); + let shared_peer = Arc::clone(&shared); + let shared_weak = Arc::downgrade(&shared); + let result = observe_reference_counted_str( + Rc::clone(&value), + Arc::clone(&shared), + ); + std::hint::black_box((&peer, &weak, &shared_peer, &shared_weak)); + unsafe { + std::ptr::write_volatile(&mut RESULT, result); + } + thread::sleep(Duration::from_millis(25)); + } +} diff --git a/e2e-tests/tests/rust_legacy_btree_execution.rs b/e2e-tests/tests/rust_legacy_btree_execution.rs new file mode 100644 index 00000000..0f269e41 --- /dev/null +++ b/e2e-tests/tests/rust_legacy_btree_execution.rs @@ -0,0 +1,136 @@ +//! Runtime coverage for Rust 1.35's direct-root B-Tree layout. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{compile_standalone_fixture, fixture_tempdir, rustc_for_toolchain}, +}; + +const TOOLCHAIN: &str = "1.35.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_135_E2E"; + +fn compile_fixture(rustc: &Path, output_dir: &Path) -> anyhow::Result { + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_legacy_btree_program/main.rs"); + let binary = output_dir.join("rust_legacy_btree_program"); + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + Ok(binary) +} + +#[tokio::test] +async fn test_rust_135_btree_map_and_btree_set_values() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path())?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + + let node_script = r#" +trace observe_legacy_btree_nodes { + print "R135_BTREE_MAP:{}", map; + print "R135_BTREE_SET:{}", set; +} +"#; + let node_result = common::runner::GhostscopeRunner::new() + .with_script(node_script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 450 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await; + + let edge_script = r#" +trace observe_legacy_btree_edge_cases { + print "R135_BTREE_EMPTY_MAP:{}", empty_map; + print "R135_BTREE_EMPTY_SET:{}", empty_set; + print "R135_BTREE_UNIT_MAP:{}", unit_map; + print "R135_BTREE_UNIT_SET:{}", unit_set; +} +"#; + let edge_result = common::runner::GhostscopeRunner::new() + .with_script(edge_script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 90 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await; + target.terminate().await?; + + let (exit_code, node_stdout, node_stderr) = node_result?; + assert_eq!(exit_code, 0, "stderr={node_stderr} stdout={node_stdout}"); + let expected_map = (0_i32..20) + .map(|key| format!("{key}: {}", key * 3 + 1)) + .collect::>() + .join(", "); + let expected_set = (0_i32..20) + .map(|value| value.to_string()) + .collect::>() + .join(", "); + assert!( + node_stdout.contains(&format!( + "R135_BTREE_MAP:BTreeMap(size=20) {{{expected_map}}}" + )), + "missing complete Rust 1.35 BTreeMap: {node_stdout}" + ); + assert!( + node_stdout.contains(&format!( + "R135_BTREE_SET:BTreeSet(size=20) {{{expected_set}}}" + )), + "missing complete Rust 1.35 BTreeSet: {node_stdout}" + ); + + let (exit_code, edge_stdout, edge_stderr) = edge_result?; + assert_eq!(exit_code, 0, "stderr={edge_stderr} stdout={edge_stdout}"); + for expected in [ + "R135_BTREE_EMPTY_MAP:BTreeMap(size=0) {}", + "R135_BTREE_EMPTY_SET:BTreeSet(size=0) {}", + "R135_BTREE_UNIT_MAP:BTreeMap(size=1) {(): ()}", + "R135_BTREE_UNIT_SET:BTreeSet(size=1) {()}", + ] { + assert!( + edge_stdout.contains(expected), + "missing '{expected}': {edge_stdout}" + ); + } + for stdout in [&node_stdout, &edge_stdout] { + assert!( + !stdout.contains("ExprError"), + "unexpected ExprError: {stdout}" + ); + assert!(!stdout.contains(""), + "truncated payload: {stdout}" + ); + } + + Ok(()) +} diff --git a/e2e-tests/tests/rust_legacy_hash_execution.rs b/e2e-tests/tests/rust_legacy_hash_execution.rs new file mode 100644 index 00000000..277f4ea8 --- /dev/null +++ b/e2e-tests/tests/rust_legacy_hash_execution.rs @@ -0,0 +1,110 @@ +//! Runtime coverage for Rust 1.35's pre-hashbrown HashMap layout. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{compile_standalone_fixture, fixture_tempdir, rustc_for_toolchain}, +}; + +const TOOLCHAIN: &str = "1.35.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_135_E2E"; + +fn compile_fixture(rustc: &Path, output_dir: &Path) -> anyhow::Result { + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_legacy_hash_program/main.rs"); + let binary = output_dir.join("rust_legacy_hash_program"); + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + Ok(binary) +} + +#[tokio::test] +async fn test_rust_135_hash_map_and_hash_set_values() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path())?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + + let script = r#" +trace observe_legacy_hash_collections { + print "R135_MAP:{}", map; + print "R135_SET:{}", set; + print "R135_EMPTY_MAP:{}", empty_map; + print "R135_EMPTY_SET:{}", empty_set; + print "R135_UNIT_MAP:{}", unit_map; + print "R135_UNIT_SET:{}", unit_set; +} +"#; + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + // Rust 1.35 starts at 32 buckets and uses a randomized hash seed. Read + // the complete table so the asserted entries cannot fall outside a + // truncated prefix. + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 2048 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let map_line = stdout + .lines() + .find(|line| line.contains("R135_MAP:")) + .ok_or_else(|| anyhow::anyhow!("missing Rust 1.35 HashMap output: {stdout}"))?; + assert!(map_line.contains("HashMap(size=2)"), "{map_line}"); + assert!(map_line.contains("-7: 13"), "{map_line}"); + assert!(map_line.contains("29: 17"), "{map_line}"); + + let set_line = stdout + .lines() + .find(|line| line.contains("R135_SET:")) + .ok_or_else(|| anyhow::anyhow!("missing Rust 1.35 HashSet output: {stdout}"))?; + assert!(set_line.contains("HashSet(size=2)"), "{set_line}"); + assert!( + set_line.contains("{-9, 5}") || set_line.contains("{5, -9}"), + "{set_line}" + ); + + for expected in [ + "R135_EMPTY_MAP:HashMap(size=0) {}", + "R135_EMPTY_SET:HashSet(size=0) {}", + "R135_UNIT_MAP:HashMap(size=1) {(): ()}", + "R135_UNIT_SET:HashSet(size=1) {()}", + ] { + assert!(stdout.contains(expected), "missing '{expected}': {stdout}"); + } + assert!( + !stdout.contains("ExprError"), + "unexpected ExprError: {stdout}" + ); + assert!(!stdout.contains(""), + "truncated payload: {stdout}" + ); + + Ok(()) +} diff --git a/e2e-tests/tests/rust_legacy_sequence_execution.rs b/e2e-tests/tests/rust_legacy_sequence_execution.rs new file mode 100644 index 00000000..d2f305b4 --- /dev/null +++ b/e2e-tests/tests/rust_legacy_sequence_execution.rs @@ -0,0 +1,92 @@ +//! Runtime coverage for Rust 1.35 sequence and ring-buffer layouts. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{compile_standalone_fixture, fixture_tempdir, rustc_for_toolchain}, +}; + +const TOOLCHAIN: &str = "1.35.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_135_E2E"; + +fn compile_fixture(rustc: &Path, output_dir: &Path) -> anyhow::Result { + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_legacy_sequence_program/main.rs"); + let binary = output_dir.join("rust_legacy_sequence_program"); + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + Ok(binary) +} + +#[tokio::test] +async fn test_rust_135_sequence_family_values() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path())?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + + let script = r#" +trace observe_legacy_sequences { + print "R135_SLICE:{}:{}", slice, empty_slice; + print "R135_VEC:{}:{}:{}", vector, empty_vector, unit_vector; + print "R135_DEQUE:{}:{}:{}:{}", wrapped, contiguous, empty_deque, unit_deque; + print "R135_DEQUE_RAW:{:x}", wrapped; +} +"#; + let result = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 256 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await; + target.terminate().await?; + + let (exit_code, stdout, stderr) = result?; + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + for expected in [ + "R135_SLICE:[-3, 5, 8]:[]", + "R135_VEC:[10, -20, 30]:[]:[(), (), ()]", + "R135_DEQUE:[3, 4, 5, 6, 7, 8]:[7, 8, 9]:[]:[(), (), ()]", + concat!( + "R135_DEQUE_RAW:", + "03 00 00 00 04 00 00 00 05 00 00 00 ", + "06 00 00 00 07 00 00 00 08 00 00 00" + ), + ] { + assert!(stdout.contains(expected), "missing '{expected}': {stdout}"); + } + assert!( + !stdout.contains("ExprError"), + "unexpected ExprError: {stdout}" + ); + assert!(!stdout.contains(""), + "truncated payload: {stdout}" + ); + + Ok(()) +} diff --git a/e2e-tests/tests/rust_legacy_string_execution.rs b/e2e-tests/tests/rust_legacy_string_execution.rs new file mode 100644 index 00000000..ad9998ed --- /dev/null +++ b/e2e-tests/tests/rust_legacy_string_execution.rs @@ -0,0 +1,95 @@ +//! Runtime coverage for Rust 1.35 string-family layouts. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{compile_standalone_fixture, fixture_tempdir, rustc_for_toolchain}, +}; + +const TOOLCHAIN: &str = "1.35.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_135_E2E"; + +fn compile_fixture(rustc: &Path, output_dir: &Path) -> anyhow::Result { + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_legacy_string_program/main.rs"); + let binary = output_dir.join("rust_legacy_string_program"); + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + Ok(binary) +} + +#[tokio::test] +async fn test_rust_135_string_family_values() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path())?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + + let script = r#" +trace observe_legacy_strings { + print "R135_STRING:{}", owned; + print "R135_STRING_RAW:{:s}:{:x}", owned, owned; + print "R135_OS:{}:{}", valid_os, invalid_os; + print "R135_STR:{}", text; + print "R135_BOX_STR:{}", boxed; + print "R135_EMPTY:{}:{}:{}:{}", empty_owned, empty_os, empty_text, empty_boxed; +} +"#; + let result = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 64 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await; + target.terminate().await?; + + let (exit_code, stdout, stderr) = result?; + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + for expected in [ + r#"R135_STRING:"legacy = string""#, + concat!( + "R135_STRING_RAW:legacy = string:", + "6c 65 67 61 63 79 20 3d 20 73 74 72 69 6e 67" + ), + r#"R135_OS:"os from 1.35":"os\xffx""#, + r#"R135_STR:"legacy\0str""#, + r#"R135_BOX_STR:"boxed from 1.35""#, + r#"R135_EMPTY:"":"":"":"""#, + ] { + assert!(stdout.contains(expected), "missing '{expected}': {stdout}"); + } + assert!( + !stdout.contains("ExprError"), + "unexpected ExprError: {stdout}" + ); + assert!(!stdout.contains(""), + "truncated payload: {stdout}" + ); + + Ok(()) +} diff --git a/e2e-tests/tests/rust_path_execution.rs b/e2e-tests/tests/rust_path_execution.rs new file mode 100644 index 00000000..775822de --- /dev/null +++ b/e2e-tests/tests/rust_path_execution.rs @@ -0,0 +1,95 @@ +//! Runtime coverage for DWARF-described Rust `PathBuf` and `&Path` values. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{compile_standalone_fixture, fixture_tempdir, rustc_for_toolchain}, +}; + +const TOOLCHAIN: &str = "1.88.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_188_E2E"; + +fn compile_fixture(rustc: &Path, output_dir: &Path) -> anyhow::Result { + let source = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rust_path_program/main.rs"); + let binary = output_dir.join("rust_path_program"); + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + Ok(binary) +} + +#[tokio::test] +async fn test_rust_188_path_values() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path())?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + + let script = r#" +trace observe_paths { + print "R188_PATH:{}:{}:{}:{}", borrowed, owned, empty, long; + print "R188_PATH_RAW:{:s}:{:x}", borrowed, owned; + print "R188_PATH_LONG_RAW:{:s}:{:x}", long, long; +} +"#; + let result = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 16 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await; + target.terminate().await?; + + let (exit_code, stdout, stderr) = result?; + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + for expected in [ + concat!( + "R188_PATH:\"borrowed/path\":\"owned/path\":\"\":", + "\"abcdefghijklmnop\" " + ), + concat!( + "R188_PATH:\"borrowed/path\":\"bad/\\xff/path\":\"\":", + "\"abcdefghijklmnop\" " + ), + concat!( + "R188_PATH_RAW:borrowed/path:", + "62 61 64 2f ff 2f 70 61 74 68" + ), + concat!( + "R188_PATH_LONG_RAW:abcdefghijklmnop :", + "61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 " + ), + ] { + assert!(stdout.contains(expected), "missing '{expected}': {stdout}"); + } + assert!( + !stdout.contains("ExprError"), + "unexpected ExprError: {stdout}" + ); + assert!(!stdout.contains("` and `Arc` values. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{compile_standalone_fixture, fixture_tempdir, rustc_for_toolchain}, +}; +use regex::Regex; + +const TOOLCHAIN: &str = "1.88.0"; +const REQUIRE_TOOLCHAIN_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_188_E2E"; + +fn compile_fixture(rustc: &Path, output_dir: &Path) -> anyhow::Result { + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_rc_str_program/main.rs"); + let binary = output_dir.join("rust_rc_str_program"); + compile_standalone_fixture(rustc, TOOLCHAIN, &source, &binary)?; + Ok(binary) +} + +#[tokio::test] +async fn test_rust_188_reference_counted_str_pointers() -> anyhow::Result<()> { + init(); + + let Some(rustc) = rustc_for_toolchain(TOOLCHAIN) else { + anyhow::ensure!( + std::env::var_os(REQUIRE_TOOLCHAIN_ENV).is_none(), + "required Rust toolchain {TOOLCHAIN} is not installed" + ); + eprintln!("skipping unavailable Rust toolchain {TOOLCHAIN}"); + return Ok(()); + }; + + let temp_dir = fixture_tempdir()?; + let binary = compile_fixture(&rustc, temp_dir.path())?; + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + + let script = r#" +trace observe_reference_counted_str { + print "R188_RC_ARC_STR:{}:{}", value, shared; +} +"#; + let result = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 16 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await; + target.terminate().await?; + + let (exit_code, stdout, stderr) = result?; + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let output = Regex::new(concat!( + r"R188_RC_ARC_STR:", + r"Rc\(strong=3, weak=1\) \{ ptr: 0x[0-9a-f]+ \([^)]*\), ", + r"strong: 3, weak: 1 \}:", + r"Arc\(strong=3, weak=1\) \{ ptr: 0x[0-9a-f]+ \([^)]*\), ", + r"strong: 3, weak: 1 \}", + ))?; + assert!(output.is_match(&stdout), "missing pointer output: {stdout}"); + assert!( + !stdout.contains("ExprError"), + "unexpected ExprError: {stdout}" + ); + assert!(!stdout.contains(" anyhow Some(ghostscope_dwarf::SourceLanguage::Rust) ); + let (_, message_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_MESSAGE", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_MESSAGE read plan"))?; + let message_type = analyzer + .resolved_type_for_plan(&message_plan)? + .ok_or_else(|| anyhow::anyhow!("expected resolved G_MESSAGE type"))?; + let value_plan = analyzer + .value_read_plan(&message_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected Rust &str value read plan"))?; + assert_eq!( + value_plan.presentation, + ghostscope_dwarf::ValuePresentation::Utf8String + ); + let ghostscope_dwarf::ValueCapturePlan::IndirectBytes { data, length } = value_plan.capture + else { + anyhow::bail!("expected indirect byte capture for Rust &str") + }; + assert_eq!( + data.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { offset: 0 } + ); + assert_eq!( + length.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { offset: 8 } + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_string_value_plan_uses_type_namespace() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + + let (_, std_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_OWNED_MESSAGE", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_OWNED_MESSAGE read plan"))?; + let std_type = analyzer + .resolved_type_for_plan(&std_plan)? + .ok_or_else(|| anyhow::anyhow!("expected standard String type"))?; + let std_value_plan = analyzer + .value_read_plan(&std_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected standard String value plan"))?; + assert_eq!( + std_value_plan.presentation, + ghostscope_dwarf::ValuePresentation::Utf8String + ); + + let (_, user_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_USER_STRING", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_USER_STRING read plan"))?; + let user_type = analyzer + .resolved_type_for_plan(&user_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user String type"))?; + assert!( + analyzer + .value_read_plan(&user_type, Some(&binary_path))? + .is_none(), + "user-defined String must retain DWARF presentation" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_vec_value_plan_uses_dwarf_type_parameter_and_namespace() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let (_, std_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_VEC_I32", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_VEC_I32 read plan"))?; + let std_type = analyzer + .resolved_type_for_plan(&std_plan)? + .ok_or_else(|| anyhow::anyhow!("expected standard Vec type"))?; + let value_plan = analyzer + .value_read_plan(&std_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected standard Vec value plan"))?; + + match &value_plan.presentation { + ghostscope_dwarf::ValuePresentation::Sequence { + element_type, + element_stride, + } => { + assert_eq!(*element_stride, 4); + assert!(matches!( + element_type.as_ref(), + ghostscope_dwarf::TypeInfo::BaseType { + name, + size: 4, + encoding, + } if name == "i32" + && *encoding == ghostscope_dwarf::constants::DW_ATE_signed.0 as u16 + )); + } + presentation => anyhow::bail!("unexpected Vec presentation: {presentation:?}"), + } + let ghostscope_dwarf::ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + } = value_plan.capture + else { + anyhow::bail!("expected indirect sequence capture for Vec") + }; + assert_eq!(element_stride, 4); + assert!(matches!( + data.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { .. } + )); + assert!(matches!( + length.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { .. } + )); + + let (_, user_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_USER_VEC", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_USER_VEC read plan"))?; + let user_type = analyzer + .resolved_type_for_plan(&user_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user Vec type"))?; + assert!( + analyzer + .value_read_plan(&user_type, Some(&binary_path))? + .is_none(), + "user-defined Vec must retain DWARF presentation" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_vec_deque_value_plan_uses_dwarf_ring_layout() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let (_, read_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_VEC_DEQUE_I32", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_VEC_DEQUE_I32 read plan"))?; + let resolved_type = analyzer + .resolved_type_for_plan(&read_plan)? + .ok_or_else(|| anyhow::anyhow!("expected standard VecDeque type"))?; + let value_plan = analyzer + .value_read_plan(&resolved_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected standard VecDeque value plan"))?; + + match &value_plan.presentation { + ghostscope_dwarf::ValuePresentation::Sequence { + element_type, + element_stride, + } => { + assert_eq!(*element_stride, 4); + assert!(matches!( + element_type.as_ref(), + ghostscope_dwarf::TypeInfo::BaseType { + name, + size: 4, + encoding, + } if name == "i32" + && *encoding == ghostscope_dwarf::constants::DW_ATE_signed.0 as u16 + )); + } + presentation => anyhow::bail!("unexpected VecDeque presentation: {presentation:?}"), + } + + let ghostscope_dwarf::ValueCapturePlan::IndirectRingSequence { + data, + start, + length, + capacity, + element_stride, + } = value_plan.capture + else { + anyhow::bail!("expected indirect ring capture for VecDeque") + }; + assert_eq!(element_stride, 4); + let length = match *length { + ghostscope_dwarf::RingSequenceLength::Explicit(length) => length, + ghostscope_dwarf::RingSequenceLength::End(_) => { + anyhow::bail!("current VecDeque should expose an explicit length") + } + }; + assert!(matches!( + data.resolved_type.summary, + ghostscope_dwarf::TypeInfo::PointerType { size: 8, .. } + )); + for projection in [&start, &length, &capacity] { + assert!(matches!( + projection.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { .. } + )); + assert!(matches!( + projection.resolved_type.summary, + ghostscope_dwarf::TypeInfo::BaseType { size: 8, .. } + )); + } + + let (_, zst_read_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_VEC_DEQUE_UNIT", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_VEC_DEQUE_UNIT read plan"))?; + let zst_type = analyzer + .resolved_type_for_plan(&zst_read_plan)? + .ok_or_else(|| anyhow::anyhow!("expected VecDeque<()> type"))?; + let zst_value_plan = analyzer + .value_read_plan(&zst_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected VecDeque<()> value plan"))?; + assert!(matches!( + zst_value_plan.presentation, + ghostscope_dwarf::ValuePresentation::Sequence { + element_stride: 0, + .. + } + )); + assert!(matches!( + zst_value_plan.capture, + ghostscope_dwarf::ValueCapturePlan::IndirectSequence { + element_stride: 0, + .. + } + )); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_slice_value_plan_uses_dwarf_pointer_target() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let (_, read_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_SLICE_I32", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_SLICE_I32 read plan"))?; + let resolved_type = analyzer + .resolved_type_for_plan(&read_plan)? + .ok_or_else(|| anyhow::anyhow!("expected slice type"))?; + let value_plan = analyzer + .value_read_plan(&resolved_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected slice value plan"))?; + + match &value_plan.presentation { + ghostscope_dwarf::ValuePresentation::Sequence { + element_type, + element_stride, + } => { + assert_eq!(*element_stride, 4); + assert!(matches!( + element_type.as_ref(), + ghostscope_dwarf::TypeInfo::BaseType { + name, + size: 4, + encoding, + } if name == "i32" + && *encoding == ghostscope_dwarf::constants::DW_ATE_signed.0 as u16 + )); + } + presentation => anyhow::bail!("unexpected slice presentation: {presentation:?}"), + } + let ghostscope_dwarf::ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + } = value_plan.capture + else { + anyhow::bail!("expected indirect sequence capture for &[i32]") + }; + assert_eq!(element_stride, 4); + assert!(matches!( + data.resolved_type.summary, + ghostscope_dwarf::TypeInfo::PointerType { size: 8, .. } + )); + assert!(matches!( + length.resolved_type.summary, + ghostscope_dwarf::TypeInfo::BaseType { size: 8, .. } + )); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_nonzero_value_plan_projects_dwarf_scalar() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let (_, read_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_NONZERO_U32", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_NONZERO_U32 read plan"))?; + let resolved_type = analyzer + .resolved_type_for_plan(&read_plan)? + .ok_or_else(|| anyhow::anyhow!("expected NonZeroU32 type"))?; + let value_plan = analyzer + .value_read_plan(&resolved_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected NonZeroU32 value plan"))?; + + assert_eq!( + value_plan.presentation, + ghostscope_dwarf::ValuePresentation::Dwarf + ); + let ghostscope_dwarf::ValueCapturePlan::ProjectedValue { value } = value_plan.capture else { + anyhow::bail!("expected projected value capture for NonZeroU32") + }; + assert_eq!( + value.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { offset: 0 } + ); + assert!(matches!( + value.resolved_type.summary, + ghostscope_dwarf::TypeInfo::BaseType { + ref name, + size: 4, + encoding, + } if name == "u32" + && encoding == ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16 + )); + + let parameter_plan = analyzer + .lookup_function_addresses("observe_nonzero") + .into_iter() + .find_map(|address| { + let context = analyzer.resolve_pc(&address).ok()?; + analyzer.plan_variable_by_name(&context, "value").ok()? + }) + .ok_or_else(|| anyhow::anyhow!("expected observe_nonzero value plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected NonZeroU32 parameter type"))?; + assert!(matches!( + analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .map(|plan| plan.capture), + Some(ghostscope_dwarf::ValueCapturePlan::ProjectedValue { .. }) + )); + + let (_, user_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_USER_NONZERO", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_USER_NONZERO read plan"))?; + let user_type = analyzer + .resolved_type_for_plan(&user_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user NonZero type"))?; + assert!( + analyzer + .value_read_plan(&user_type, Some(&binary_path))? + .is_none(), + "user-defined NonZero must retain DWARF presentation" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_cell_value_plan_projects_dwarf_value() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let (_, read_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_CELL_U32", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_CELL_U32 read plan"))?; + let resolved_type = analyzer + .resolved_type_for_plan(&read_plan)? + .ok_or_else(|| anyhow::anyhow!("expected Cell type"))?; + let value_plan = analyzer + .value_read_plan(&resolved_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected Cell value plan"))?; + + assert_eq!( + value_plan.presentation, + ghostscope_dwarf::ValuePresentation::SingleField { + type_name: "Cell".to_string(), + field_name: "value".to_string(), + } + ); + let ghostscope_dwarf::ValueCapturePlan::ProjectedValue { value } = value_plan.capture else { + anyhow::bail!("expected projected value capture for Cell") + }; + assert_eq!( + value.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { offset: 0 } + ); + assert!(matches!( + value.resolved_type.summary, + ghostscope_dwarf::TypeInfo::BaseType { + ref name, + size: 4, + encoding, + } if name == "u32" + && encoding == ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16 + )); + + let (_, pair_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_CELL_PAIR", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_CELL_PAIR read plan"))?; + let pair_type = analyzer + .resolved_type_for_plan(&pair_plan)? + .ok_or_else(|| anyhow::anyhow!("expected Cell<(i32, u16)> type"))?; + let pair_value = analyzer + .value_read_plan(&pair_type, Some(&binary_path))? + .and_then(|plan| match plan.capture { + ghostscope_dwarf::ValueCapturePlan::ProjectedValue { value } => Some(value), + _ => None, + }) + .ok_or_else(|| anyhow::anyhow!("expected projected pair capture"))?; + assert!(matches!( + pair_value.resolved_type.summary, + ghostscope_dwarf::TypeInfo::StructType { ref members, .. } + if members.len() == 2 + )); + + let (_, unit_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_CELL_UNIT", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_CELL_UNIT read plan"))?; + let unit_type = analyzer + .resolved_type_for_plan(&unit_plan)? + .ok_or_else(|| anyhow::anyhow!("expected Cell<()> type"))?; + let unit_value = analyzer + .value_read_plan(&unit_type, Some(&binary_path))? + .and_then(|plan| match plan.capture { + ghostscope_dwarf::ValueCapturePlan::ProjectedValue { value } => Some(value), + _ => None, + }) + .ok_or_else(|| anyhow::anyhow!("expected projected unit capture"))?; + assert!(matches!( + unit_value.resolved_type.summary, + ghostscope_dwarf::TypeInfo::BaseType { + ref name, + size: 0, + .. + } if name == "()" + )); + + let (_, user_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_USER_CELL", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_USER_CELL read plan"))?; + let user_type = analyzer + .resolved_type_for_plan(&user_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user Cell type"))?; + assert!( + analyzer + .value_read_plan(&user_type, Some(&binary_path))? + .is_none(), + "user-defined Cell must retain DWARF presentation" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_ref_cell_value_plan_builds_dwarf_inline_view() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let (_, read_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_REF_CELL_IDLE", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_REF_CELL_IDLE read plan"))?; + let resolved_type = analyzer + .resolved_type_for_plan(&read_plan)? + .ok_or_else(|| anyhow::anyhow!("expected RefCell type"))?; + let root_size = resolved_type.summary.size(); + let value_plan = analyzer + .value_read_plan(&resolved_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected RefCell value plan"))?; + + assert_eq!( + value_plan.presentation, + ghostscope_dwarf::ValuePresentation::SignedStateStruct { + state_field: "borrow".to_string(), + non_negative_label: "borrow".to_string(), + negative_label: "borrow_mut".to_string(), + } + ); + let ghostscope_dwarf::ValueCapturePlan::InlineView { output_type } = value_plan.capture else { + anyhow::bail!("expected inline semantic view for RefCell") + }; + let ghostscope_dwarf::TypeInfo::StructType { + name, + size, + members, + } = output_type + else { + anyhow::bail!("expected RefCell struct view") + }; + assert_eq!(name, "RefCell"); + assert_eq!(size, root_size); + let [value, borrow] = members.as_slice() else { + anyhow::bail!("expected value and borrow fields") + }; + assert_eq!(value.name, "value"); + assert_eq!(borrow.name, "borrow"); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&value.member_type), + ghostscope_dwarf::TypeInfo::BaseType { + name, + size: 4, + encoding, + } if name == "i32" + && *encoding == ghostscope_dwarf::constants::DW_ATE_signed.0 as u16 + )); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&borrow.member_type), + ghostscope_dwarf::TypeInfo::BaseType { size, encoding, .. } + if matches!(*size, 4 | 8) + && *encoding == ghostscope_dwarf::constants::DW_ATE_signed.0 as u16 + )); + let value_end = value.offset + value.member_type.size(); + let borrow_end = borrow.offset + borrow.member_type.size(); + assert!(value_end <= root_size); + assert!(borrow_end <= root_size); + assert!(value_end <= borrow.offset || borrow_end <= value.offset); + + let (_, unit_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_REF_CELL_UNIT", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_REF_CELL_UNIT read plan"))?; + let unit_type = analyzer + .resolved_type_for_plan(&unit_plan)? + .ok_or_else(|| anyhow::anyhow!("expected RefCell<()> type"))?; + let unit_view = analyzer + .value_read_plan(&unit_type, Some(&binary_path))? + .and_then(|plan| match plan.capture { + ghostscope_dwarf::ValueCapturePlan::InlineView { output_type } => Some(output_type), + _ => None, + }) + .ok_or_else(|| anyhow::anyhow!("expected RefCell<()> inline view"))?; + let ghostscope_dwarf::TypeInfo::StructType { members, .. } = unit_view else { + anyhow::bail!("expected RefCell<()> struct view") + }; + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&members[0].member_type), + ghostscope_dwarf::TypeInfo::BaseType { + name, + size: 0, + .. + } if name == "()" + )); + + let (_, user_plan) = analyzer + .plan_global_access_read_plan( + &binary_path, + "G_USER_REF_CELL", + &ghostscope_dwarf::VariableAccessPath::default(), + )? + .ok_or_else(|| anyhow::anyhow!("expected G_USER_REF_CELL read plan"))?; + let user_type = analyzer + .resolved_type_for_plan(&user_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user RefCell type"))?; + assert!( + analyzer + .value_read_plan(&user_type, Some(&binary_path))? + .is_none(), + "user-defined RefCell must retain DWARF presentation" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_ref_value_plan_builds_dwarf_projected_view() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let ref_context = analyzer + .lookup_function_addresses("observe_ref_guards") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected observe_ref_guards context"))?; + + for (parameter, expected_value) in [("shared", "i32"), ("mutable", "i32")] { + let parameter_plan = analyzer + .plan_variable_by_name(&ref_context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let guard_type = analyzer + .project_resolved_type( + ¶meter_type, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(&binary_path), + )? + .resolved_type; + let value_plan = analyzer + .value_read_plan(&guard_type, Some(&binary_path))? + .ok_or_else(|| { + anyhow::anyhow!("expected {parameter} semantic plan for {guard_type:#?}") + })?; + assert_eq!( + value_plan.presentation, + ghostscope_dwarf::ValuePresentation::SignedStateStruct { + state_field: "borrow".to_string(), + non_negative_label: "borrow".to_string(), + negative_label: "borrow_mut".to_string(), + } + ); + let ghostscope_dwarf::ValueCapturePlan::ProjectedView { + output_type, + fields, + } = value_plan.capture + else { + anyhow::bail!("expected projected semantic view for {parameter}") + }; + let ghostscope_dwarf::TypeInfo::StructType { + name, + size, + members, + } = output_type + else { + anyhow::bail!("expected projected Ref struct") + }; + assert_eq!(name, "Ref"); + let [value_member, borrow_member] = members.as_slice() else { + anyhow::bail!("expected projected value and borrow members") + }; + assert_eq!(value_member.name, "*value"); + assert_eq!(borrow_member.name, "borrow"); + assert_eq!(value_member.offset, 0); + assert_eq!(borrow_member.offset, value_member.member_type.size()); + assert_eq!( + size, + borrow_member.offset + borrow_member.member_type.size() + ); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&value_member.member_type), + ghostscope_dwarf::TypeInfo::BaseType { name, size: 4, .. } + if name == expected_value + )); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&borrow_member.member_type), + ghostscope_dwarf::TypeInfo::BaseType { size, encoding, .. } + if matches!(*size, 4 | 8) + && *encoding == ghostscope_dwarf::constants::DW_ATE_signed.0 as u16 + )); + let [value_field, borrow_field] = fields.as_slice() else { + anyhow::bail!("expected projected value and borrow reads") + }; + assert_eq!(value_field.output_offset, value_member.offset); + assert_eq!(borrow_field.output_offset, borrow_member.offset); + assert!(matches!( + value_field.value.steps.last(), + Some(ghostscope_dwarf::ProjectedValueStep::Dereference { + pointer_size: 4 | 8 + }) + )); + assert!(borrow_field.value.steps.iter().any(|step| matches!( + step, + ghostscope_dwarf::ProjectedValueStep::Dereference { + pointer_size: 4 | 8 + } + ))); + } + + let unit_plan = analyzer + .plan_variable_by_name(&ref_context, "unit")? + .ok_or_else(|| anyhow::anyhow!("expected unit parameter plan"))?; + let unit_pointer = analyzer + .resolved_type_for_plan(&unit_plan)? + .ok_or_else(|| anyhow::anyhow!("expected unit parameter type"))?; + let unit_type = analyzer + .project_resolved_type( + &unit_pointer, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(&binary_path), + )? + .resolved_type; + let unit_view = analyzer + .value_read_plan(&unit_type, Some(&binary_path))? + .and_then(|plan| match plan.capture { + ghostscope_dwarf::ValueCapturePlan::ProjectedView { output_type, .. } => { + Some(output_type) + } + _ => None, + }) + .ok_or_else(|| anyhow::anyhow!("expected unit Ref projected view"))?; + let ghostscope_dwarf::TypeInfo::StructType { members, .. } = unit_view else { + anyhow::bail!("expected unit Ref struct") + }; + assert_eq!(members[0].member_type.size(), 0); + assert_eq!(members[1].offset, 0); + + let user_context = analyzer + .lookup_function_addresses("observe_user_ref") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected observe_user_ref context"))?; + let user_plan = analyzer + .plan_variable_by_name(&user_context, "value")? + .ok_or_else(|| anyhow::anyhow!("expected user Ref parameter plan"))?; + let user_pointer = analyzer + .resolved_type_for_plan(&user_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user Ref parameter type"))?; + let user_type = analyzer + .project_resolved_type( + &user_pointer, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(&binary_path), + )? + .resolved_type; + assert!( + analyzer + .value_read_plan(&user_type, Some(&binary_path))? + .is_none(), + "user-defined Ref must retain DWARF presentation" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_rc_arc_value_plans_follow_dwarf_projected_views() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let context = analyzer + .lookup_function_addresses("observe_rc_arc") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected observe_rc_arc context"))?; + + for (parameter, expected_name) in [("rc", "Rc"), ("arc", "Arc")] { + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let value_plan = analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .ok_or_else(|| { + anyhow::anyhow!("expected {parameter} semantic plan for {parameter_type:#?}") + })?; + assert_eq!( + value_plan.presentation, + ghostscope_dwarf::ValuePresentation::ReferenceCountedStruct { + strong_field: "strong".to_string(), + weak_field: "weak".to_string(), + implicit_weak: 1, + } + ); + let ghostscope_dwarf::ValueCapturePlan::ProjectedView { + output_type, + fields, + } = value_plan.capture + else { + anyhow::bail!("expected projected semantic view for {parameter}") + }; + let ghostscope_dwarf::TypeInfo::StructType { + name, + size, + members, + } = output_type + else { + anyhow::bail!("expected projected {expected_name} struct") + }; + assert_eq!(name, expected_name); + let [value_member, strong_member, weak_member] = members.as_slice() else { + anyhow::bail!("expected value and reference-count members") + }; + assert_eq!(value_member.name, "value"); + assert_eq!(strong_member.name, "strong"); + assert_eq!(weak_member.name, "weak"); + assert_eq!(value_member.offset, 0); + assert_eq!(strong_member.offset, value_member.member_type.size()); + assert_eq!( + weak_member.offset, + strong_member.offset + strong_member.member_type.size() + ); + assert_eq!(size, weak_member.offset + weak_member.member_type.size()); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&value_member.member_type), + ghostscope_dwarf::TypeInfo::StructType { name, size: 8, .. } + if name == "(i32, u16)" + )); + for member in [strong_member, weak_member] { + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&member.member_type), + ghostscope_dwarf::TypeInfo::BaseType { size, encoding, .. } + if matches!(*size, 4 | 8) + && *encoding + == ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16 + )); + } + assert_eq!(fields.len(), 3); + for field in &fields { + assert!(field.value.steps.iter().any(|step| matches!( + step, + ghostscope_dwarf::ProjectedValueStep::Dereference { + pointer_size: 4 | 8 + } + ))); + } + } + + for parameter in ["rc_unit", "arc_unit"] { + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let output_type = analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .and_then(|plan| match plan.capture { + ghostscope_dwarf::ValueCapturePlan::ProjectedView { output_type, .. } => { + Some(output_type) + } + _ => None, + }) + .ok_or_else(|| anyhow::anyhow!("expected {parameter} projected view"))?; + let ghostscope_dwarf::TypeInfo::StructType { members, .. } = output_type else { + anyhow::bail!("expected {parameter} struct view") + }; + assert_eq!(members[0].member_type.size(), 0); + assert_eq!(members[1].offset, 0); + } + + let user_context = analyzer + .lookup_function_addresses("observe_user_rc_arc") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected observe_user_rc_arc context"))?; + for parameter in ["rc", "arc"] { + let parameter_plan = analyzer + .plan_variable_by_name(&user_context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected user {parameter} plan"))?; + let parameter_pointer = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user {parameter} type"))?; + let parameter_type = analyzer + .project_resolved_type( + ¶meter_pointer, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(&binary_path), + )? + .resolved_type; + assert!( + analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .is_none(), + "user-defined {parameter} must retain DWARF presentation" + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_rust_hash_collection_plans_follow_dwarf_raw_table_layout() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let context = analyzer + .lookup_function_addresses("observe_hash_collections") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected observe_hash_collections context"))?; + + for (parameter, is_map) in [("map", true), ("set", false)] { + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let value_plan = analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} semantic plan"))?; + let ghostscope_dwarf::ValuePresentation::HashTable { + entry_stride, + bucket_order, + occupancy, + entry, + } = value_plan.presentation + else { + anyhow::bail!("expected {parameter} hash-table presentation") + }; + assert_eq!( + bucket_order, + ghostscope_dwarf::HashTableBucketOrder::Reverse + ); + assert_eq!( + occupancy, + ghostscope_dwarf::HashTableOccupancy::ControlByteHighBitClear + ); + match entry { + ghostscope_dwarf::HashTableEntryPresentation::Map { key, value } => { + assert!(is_map); + assert_eq!(entry_stride, 8); + assert_eq!(key.offset, 0); + assert_eq!(key.field_type.type_name(), "i32"); + assert_eq!(value.offset, 4); + assert_eq!(value.field_type.type_name(), "u16"); + } + ghostscope_dwarf::HashTableEntryPresentation::Set { value } => { + assert!(!is_map); + assert_eq!(entry_stride, 4); + assert_eq!(value.offset, 0); + assert_eq!(value.field_type.type_name(), "i32"); + } + } + let ghostscope_dwarf::ValueCapturePlan::IndirectHashTable { + control, + length, + bucket_mask, + entry_stride: capture_stride, + occupancy: capture_occupancy, + buckets, + bucket_order: capture_order, + } = value_plan.capture + else { + anyhow::bail!("expected {parameter} bounded hash-table capture") + }; + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&control.resolved_type.summary), + ghostscope_dwarf::TypeInfo::PointerType { size: 4 | 8, .. } + )); + assert!(matches!( + buckets, + ghostscope_dwarf::HashTableBucketSource::ReverseFromControl + )); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&length.resolved_type.summary), + ghostscope_dwarf::TypeInfo::BaseType { size: 4 | 8, .. } + )); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&bucket_mask.resolved_type.summary), + ghostscope_dwarf::TypeInfo::BaseType { size: 4 | 8, .. } + )); + assert_eq!(capture_stride, entry_stride); + assert_eq!(capture_occupancy, occupancy); + assert_eq!(capture_order, bucket_order); + } + + for (parameter, expected_map) in [("unit_map", true), ("unit_set", false)] { + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let presentation = analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .map(|plan| plan.presentation) + .ok_or_else(|| anyhow::anyhow!("expected {parameter} semantic plan"))?; + let ghostscope_dwarf::ValuePresentation::HashTable { + entry_stride, + entry, + .. + } = presentation + else { + anyhow::bail!("expected {parameter} hash-table presentation") + }; + assert_eq!(entry_stride, 0); + assert_eq!( + matches!( + entry, + ghostscope_dwarf::HashTableEntryPresentation::Map { .. } + ), + expected_map + ); + } + + let user_context = analyzer + .lookup_function_addresses("observe_user_hash_collections") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected user hash collection context"))?; + for parameter in ["map", "set"] { + let parameter_plan = analyzer + .plan_variable_by_name(&user_context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected user {parameter} plan"))?; + let parameter_pointer = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user {parameter} type"))?; + let parameter_type = analyzer + .project_resolved_type( + ¶meter_pointer, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(&binary_path), + )? + .resolved_type; + assert!( + analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .is_none(), + "user-defined {parameter} must retain DWARF presentation" + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_rust_btree_collection_plans_follow_dwarf_node_layout() -> anyhow::Result<()> { + init(); + + let binary_path = FIXTURES.get_test_binary("rust_global_program")?; + let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path).await?; + let context = analyzer + .lookup_function_addresses("observe_btree_collections") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected observe_btree_collections context"))?; + + for (parameter, is_map) in [("map", true), ("set", false)] { + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let value_plan = analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} semantic plan"))?; + let ghostscope_dwarf::ValuePresentation::BTree { + node_capacity, + entry, + } = value_plan.presentation + else { + anyhow::bail!("expected {parameter} B-Tree presentation") + }; + assert_eq!(node_capacity, 11); + match entry { + ghostscope_dwarf::BTreeEntryPresentation::Map { key, value } => { + assert!(is_map); + assert_eq!(key.slot_stride, 4); + assert_eq!(key.value_offset, 0); + assert_eq!(key.field_type.type_name(), "i32"); + assert_eq!(value.slot_stride, 2); + assert_eq!(value.value_offset, 0); + assert_eq!(value.field_type.type_name(), "u16"); + } + ghostscope_dwarf::BTreeEntryPresentation::Set { value } => { + assert!(!is_map); + assert_eq!(value.slot_stride, 4); + assert_eq!(value.value_offset, 0); + assert_eq!(value.field_type.type_name(), "i32"); + } + } + + let ghostscope_dwarf::ValueCapturePlan::IndirectBTree { + root_pointer, + root_height, + length, + node_length, + keys, + values, + edges, + node_capacity: capture_capacity, + } = value_plan.capture + else { + anyhow::bail!("expected {parameter} bounded B-Tree capture") + }; + assert!(matches!( + root_pointer.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { .. } + )); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&root_pointer.resolved_type.summary), + ghostscope_dwarf::TypeInfo::PointerType { size: 8, .. } + )); + for scalar in [&root_height, &length] { + assert!(matches!( + scalar.layout, + ghostscope_dwarf::TypeProjectionLayout::Member { .. } + )); + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&scalar.resolved_type.summary), + ghostscope_dwarf::TypeInfo::BaseType { size: 8, .. } + )); + } + assert!(matches!( + ghostscope_dwarf::strip_type_aliases(&node_length.resolved_type.summary), + ghostscope_dwarf::TypeInfo::BaseType { size: 2, .. } + )); + assert_eq!(capture_capacity, node_capacity); + assert_eq!(keys.slot_stride, 4); + assert_eq!(values.is_some(), is_map); + if let Some(values) = values { + assert_eq!(values.slot_stride, 2); + } + assert_eq!(edges.edge_count, node_capacity + 1); + assert_eq!(edges.slot_stride, 8); + assert_eq!(edges.pointer_size, 8); + assert!(edges.pointer_offset < edges.slot_stride); + } + + for (parameter, expected_map) in [("unit_map", true), ("unit_set", false)] { + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter plan"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected {parameter} parameter type"))?; + let presentation = analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .map(|plan| plan.presentation) + .ok_or_else(|| anyhow::anyhow!("expected {parameter} semantic plan"))?; + let ghostscope_dwarf::ValuePresentation::BTree { entry, .. } = presentation else { + anyhow::bail!("expected {parameter} B-Tree presentation") + }; + let (is_map, field_strides) = match entry { + ghostscope_dwarf::BTreeEntryPresentation::Map { key, value } => { + (true, vec![key.slot_stride, value.slot_stride]) + } + ghostscope_dwarf::BTreeEntryPresentation::Set { value } => { + (false, vec![value.slot_stride]) + } + }; + assert_eq!(is_map, expected_map); + assert!(field_strides.into_iter().all(|stride| stride == 0)); + } + + let user_context = analyzer + .lookup_function_addresses("observe_user_btree_collections") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("expected user B-Tree collection context"))?; + for parameter in ["map", "set"] { + let parameter_plan = analyzer + .plan_variable_by_name(&user_context, parameter)? + .ok_or_else(|| anyhow::anyhow!("expected user {parameter} plan"))?; + let parameter_pointer = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("expected user {parameter} type"))?; + let parameter_type = analyzer + .project_resolved_type( + ¶meter_pointer, + &ghostscope_dwarf::VariableAccessSegment::Dereference, + Some(&binary_path), + )? + .resolved_type; + assert!( + analyzer + .value_read_plan(¶meter_type, Some(&binary_path))? + .is_none(), + "user-defined {parameter} must retain DWARF presentation" + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_str_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RSTR:{}:{}:{}", G_MESSAGE, G_EMPTY_MESSAGE, G_NUL_MESSAGE; + print "RSTR_RAW:{:s}:{:x}", G_MESSAGE, G_MESSAGE; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains(r#"RSTR:"hello from rust":"":"left\0right""#)), + "Expected Rust str output: {stdout}" + ); + let expected_raw = concat!( + "RSTR_RAW:hello from rust:", + "68 65 6c 6c 6f 20 66 72 6f 6d 20 72 75 73 74" + ); + assert!( + stdout.lines().any(|line| line.contains(expected_raw)), + "Expected raw Rust str output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_string_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RSTRING:{}:{}:{}", G_OWNED_MESSAGE, G_EMPTY_OWNED, G_NUL_OWNED; + print "RSTRING_RAW:{:s}:{:x}", G_OWNED_MESSAGE, G_OWNED_MESSAGE; + print "RSTRING_SEPARATOR:{}", G_SEPARATOR_OWNED; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| { line.contains(r#"RSTRING:"owned from rust":"":"owned\0value""#) }), + "Expected Rust String output: {stdout}" + ); + let expected_raw = concat!( + "RSTRING_RAW:owned from rust:", + "6f 77 6e 65 64 20 66 72 6f 6d 20 72 75 73 74" + ); + assert!( + stdout.lines().any(|line| line.contains(expected_raw)), + "Expected raw Rust String output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains(r#"RSTRING_SEPARATOR:"left = right""#)), + "Expected Rust String separator output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_vec_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RVEC:{}:{}:{}:{}", G_VEC_U8, G_VEC_I32, G_EMPTY_VEC, G_VEC_UNIT; + print "RVEC_RAW:{:x}", G_VEC_U8; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| { line.contains("RVEC:[1, 2, 3, 255]:[10, -20, 30, 40]:[]:[(), (), ()]") }), + "Expected Rust Vec output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("RVEC_RAW:01 02 03 ff")), + "Expected raw Rust Vec output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_vec_deque_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_vec_deque { + print "RDEQUE:{}:{}:{}", wrapped, contiguous, empty; +} +trace do_stuff { + print "RDEQUE_ZST:{}", G_VEC_DEQUE_UNIT; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("RDEQUE:[10, 20, 30, 40]:[7, 8, 9]:[]")), + "Expected Rust VecDeque output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("RDEQUE_ZST:[(), (), ()]")), + "Expected Rust VecDeque ZST output: stderr={stderr} stdout={stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_nonzero_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RNONZERO:{}:{}:{}", G_NONZERO_U32, G_NONZERO_I32, G_NONZERO_U128; +} +trace observe_nonzero { + print "RNONZERO_ARG:{}", value; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let expected_global = concat!("RNONZERO:7:-9:", "340282366920938463463374607431768211454"); + assert!( + stdout.lines().any(|line| line.contains(expected_global)), + "Expected Rust NonZero global output: {stdout}" + ); + assert!( + stdout.lines().any(|line| line.contains("RNONZERO_ARG:23")), + "Expected Rust NonZero argument output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_cell_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RCELL:{}:{}:{}", G_CELL_U32, G_CELL_PAIR, G_CELL_UNIT; + print "RCELL_RAW:{:x}", G_CELL_U32; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("RCELL:Cell { value: 41 }:Cell { value:")), + "Expected Rust Cell wrapper output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + line.contains("RCELL:") && line.contains("__0: -4") && line.contains("__1: 12") + }), + "Expected Rust Cell aggregate output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains(":Cell { value: () }")), + "Expected Rust Cell unit output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("RCELL_RAW:29 00 00 00")), + "Expected raw Rust Cell payload: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_ref_cell_values_and_borrow_states() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_ref_cell_states { + print "RREFCELL:{}:{}:{}:{}:{}", G_REF_CELL_IDLE, + G_REF_CELL_SHARED, G_REF_CELL_MUT, G_REF_CELL_PAIR, G_REF_CELL_UNIT; + print "RREFCELL_RAW:{:x}", G_REF_CELL_SHARED; + print "RREFCELL_ARG:{}", owned; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("RefCell(borrow=0) { value: 17, borrow: 0 }")), + "Expected idle Rust RefCell output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("RefCell(borrow=2) { value: 23, borrow: 2 }")), + "Expected shared Rust RefCell output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| { line.contains("RefCell(borrow_mut=1) { value: 31, borrow: -1 }") }), + "Expected mutably borrowed Rust RefCell output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + line.contains("RREFCELL:") && line.contains("__0: -6") && line.contains("__1: 14") + }), + "Expected aggregate Rust RefCell output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("RefCell(borrow=0) { value: (), borrow: 0 }")), + "Expected zero-sized Rust RefCell output: {stdout}" + ); + let raw_line = stdout + .lines() + .find(|line| line.contains("RREFCELL_RAW:")) + .ok_or_else(|| anyhow::anyhow!("missing raw RefCell output: {stdout}"))?; + let (_, raw_payload) = raw_line + .split_once("RREFCELL_RAW:") + .ok_or_else(|| anyhow::anyhow!("invalid raw RefCell output: {raw_line}"))?; + assert!( + raw_payload.contains("00"), + "raw payload was empty: {raw_line}" + ); + assert!( + !raw_payload.contains("RefCell"), + "raw specifier used semantic formatting: {raw_line}" + ); + assert!( + stdout.lines().any(|line| { + line.contains( + "RREFCELL_ARG:RefCell(borrow=0) \ + { value: -12, borrow: 0 }", + ) + }), + "Expected Rust RefCell argument output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_ref_and_ref_mut_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_ref_guards { + print "RREF:{}:{}:{}:{}", *shared, *mutable, *pair, *unit; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("Ref(borrow=2) { *value: 23, borrow: 2 }")), + "Expected shared Rust Ref output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| { line.contains("Ref(borrow_mut=1) { *value: 31, borrow: -1 }") }), + "Expected Rust RefMut output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + line.contains("Ref(borrow=1)") && line.contains("__0: -6") && line.contains("__1: 14") + }), + "Expected aggregate Rust Ref output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("Ref(borrow=1) { *value: (), borrow: 1 }")), + "Expected zero-sized Rust Ref output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_rc_and_arc_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_rc_arc { + print "RRCARC:{}:{}:{}:{}", rc, arc, rc_unit, arc_unit; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout.lines().any(|line| { + line.contains("Rc(strong=3, weak=1)") + && line.contains("__0: -7") + && line.contains("__1: 13") + && line.contains("strong: 3") + && line.contains("weak: 1") + }), + "Expected Rust Rc output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + line.contains("Arc(strong=3, weak=1)") + && line.contains("__0: 29") + && line.contains("__1: 17") + && line.contains("strong: 3") + && line.contains("weak: 1") + }), + "Expected Rust Arc output: {stdout}" + ); + assert!( + stdout + .lines() + .any(|line| line.contains("Rc(strong=1, weak=0) { value: (), strong: 1, weak: 0 }")), + "Expected zero-sized Rust Rc output: {stdout}" + ); + assert!( + stdout.lines().any(|line| { + line.contains("Arc(strong=1, weak=0) { value: (), strong: 1, weak: 0 }") + }), + "Expected zero-sized Rust Arc output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_hash_map_and_hash_set_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_hash_collections { + print "RHASH_MAP:{}", map; + print "RHASH_SET:{}", set; + print "RHASH_EMPTY_MAP:{}", empty_map; + print "RHASH_EMPTY_SET:{}", empty_set; + print "RHASH_UNIT_MAP:{}", unit_map; + print "RHASH_UNIT_SET:{}", unit_set; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let map_line = stdout + .lines() + .find(|line| line.contains("RHASH_MAP:")) + .ok_or_else(|| anyhow::anyhow!("missing Rust HashMap output: {stdout}"))?; + assert!(map_line.contains("HashMap(size=2)"), "{map_line}"); + assert!(map_line.contains("-7: 13"), "{map_line}"); + assert!(map_line.contains("29: 17"), "{map_line}"); + let set_line = stdout + .lines() + .find(|line| line.contains("RHASH_SET:")) + .ok_or_else(|| anyhow::anyhow!("missing Rust HashSet output: {stdout}"))?; + assert!(set_line.contains("HashSet(size=2)"), "{set_line}"); + assert!(set_line.contains("-9"), "{set_line}"); + assert!(set_line.contains('5'), "{set_line}"); + assert!( + stdout.contains("RHASH_EMPTY_MAP:HashMap(size=0) {}"), + "Expected empty Rust HashMap output: {stdout}" + ); + assert!( + stdout.contains("RHASH_EMPTY_SET:HashSet(size=0) {}"), + "Expected empty Rust HashSet output: {stdout}" + ); + assert!( + stdout.contains("RHASH_UNIT_MAP:HashMap(size=1) {(): ()}"), + "Expected ZST Rust HashMap output: {stdout}" + ); + assert!( + stdout.contains("RHASH_UNIT_SET:HashSet(size=1) {()}"), + "Expected ZST Rust HashSet output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_hash_map_respects_bucket_aligned_cap() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_hash_collections { + print "RHASH_CAP:{}", map; +} +"#; + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 9 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let line = stdout + .lines() + .find(|line| line.contains("RHASH_CAP:")) + .ok_or_else(|| anyhow::anyhow!("missing capped Rust HashMap output: {stdout}"))?; + assert!(line.contains("HashMap(size=2)"), "{line}"); + assert!(line.contains(""), "{line}"); + assert!(!line.contains(" anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_btree_collections { + print "RBTREE_MAP:{}", map; + print "RBTREE_SET:{}", set; + print "RBTREE_EMPTY_MAP:{}", empty_map; + print "RBTREE_EMPTY_SET:{}", empty_set; + print "RBTREE_UNIT_MAP:{}", unit_map; + print "RBTREE_UNIT_SET:{}", unit_set; + print "RBTREE_RAW:{:x}", map; +} +"#; + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 90 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout.contains("RBTREE_MAP:BTreeMap(size=2) {-7: 13, 29: 17}"), + "Expected Rust BTreeMap output: {stdout}" + ); + assert!( + stdout.contains("RBTREE_SET:BTreeSet(size=2) {-9, 5}"), + "Expected Rust BTreeSet output: {stdout}" + ); + assert!( + stdout.contains("RBTREE_EMPTY_MAP:BTreeMap(size=0) {}"), + "Expected empty Rust BTreeMap output: {stdout}" + ); + assert!( + stdout.contains("RBTREE_EMPTY_SET:BTreeSet(size=0) {}"), + "Expected empty Rust BTreeSet output: {stdout}" + ); + assert!( + stdout.contains("RBTREE_UNIT_MAP:BTreeMap(size=1) {(): ()}"), + "Expected ZST Rust BTreeMap output: {stdout}" + ); + assert!( + stdout.contains("RBTREE_UNIT_SET:BTreeSet(size=1) {()}"), + "Expected ZST Rust BTreeSet output: {stdout}" + ); + assert!( + stdout.contains("RBTREE_RAW:f9 ff ff ff 0d 00 1d 00 00 00 11 00"), + "Expected logical raw BTreeMap bytes: {stdout}" + ); + assert!(!stdout.contains(" anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_internal_btree_collections { + print "RBTREE_INTERNAL_MAP:{}", map; + print "RBTREE_INTERNAL_SET:{}", set; +} +"#; + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 450 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let expected_map = (0_i32..20) + .map(|key| format!("{key}: {}", key * 3 + 1)) + .collect::>() + .join(", "); + let expected_set = (0_i32..20) + .map(|value| value.to_string()) + .collect::>() + .join(", "); + assert!( + stdout.contains(&format!( + "RBTREE_INTERNAL_MAP:BTreeMap(size=20) {{{expected_map}}}" + )), + "Expected complete internal Rust BTreeMap output: {stdout}" + ); + assert!( + stdout.contains(&format!( + "RBTREE_INTERNAL_SET:BTreeSet(size=20) {{{expected_set}}}" + )), + "Expected complete internal Rust BTreeSet output: {stdout}" + ); + assert!( + !stdout.contains(""), + "Unexpected truncation: {stdout}" + ); + assert!(!stdout.contains(" anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_deep_btree_collections { + print "RBTREE_DEEP_MAP:{}", map; + print "RBTREE_DEEP_SET:{}", set; +} +"#; + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + for marker in ["RBTREE_DEEP_MAP:BTreeMap", "RBTREE_DEEP_SET:BTreeSet"] { + let line = stdout + .lines() + .find(|line| line.contains(marker)) + .ok_or_else(|| anyhow::anyhow!("missing {marker} output: {stdout}"))?; + assert!(line.contains("size=160"), "{line}"); + assert!(line.contains(""), "{line}"); + assert!(!line.contains(" anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_vec_deque { + print "RDEQUE_CAP:{}", wrapped; +} +"#; + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 9 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("RDEQUE_CAP:[10, 20] ")), + "Expected capped Rust VecDeque output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_vec_respects_element_aligned_cap() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RVEC_CAP:{}", G_VEC_I32; +} +"#; + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 9 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("RVEC_CAP:[10, -20] ")), + "Expected element-aligned capped Rust Vec output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_slice_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RSLICE:{}:{}:{}", G_SLICE_I32, G_MUT_SLICE_U16, G_EMPTY_SLICE; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| { line.contains("RSLICE:[7, -8, 9]:[1000, 2000, 65535]:[]") }), + "Expected Rust slice output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_box_str_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_boxed_str { + print "RBOX:{}:{}", value, empty; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("RBOX:\"boxed from rust\":\"\"")), + "Expected Rust Box output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_os_string_values() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace observe_os_string { + print "ROS:{}:{}:{}", value, invalid, empty; +} +"#; + + let (exit_code, stdout, stderr) = + run_ghostscope_with_script_for_target(script, 9, &target).await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + assert!( + stdout + .lines() + .any(|line| line.contains("ROS:\"os from rust\":\"os\\xffx\":\"\"")), + "Expected Rust OsString output: {stdout}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_rust_script_print_str_respects_mem_dump_cap() -> anyhow::Result<()> { + init(); + + let target = spawn_rust_global_program().await?; + let script = r#" +trace do_stuff { + print "RSTR_CAP:{}", G_MESSAGE; + print "RSTR_CAP_RAW:{:s}:{:x}", G_MESSAGE, G_MESSAGE; +} +"#; + + let (exit_code, stdout, stderr) = common::runner::GhostscopeRunner::new() + .with_script(script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 3 +"#, + ) + .attach_to(&target) + .timeout_secs(9) + .enable_sysmon_for_target(false) + .run() + .await?; + target.terminate().await?; + + assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}"); + let cap_line = stdout + .lines() + .find(|line| line.contains("RSTR_CAP:")) + .ok_or_else(|| anyhow::anyhow!("Expected capped Rust str output: {stdout}"))?; + assert!( + cap_line.contains(r#"RSTR_CAP:"hel" "#), + "Unexpected capped Rust str output: {cap_line}" + ); + assert!( + !cap_line.contains("hell"), + "Rust str read exceeded mem_dump_cap: {cap_line}" + ); + let cap_raw_line = stdout + .lines() + .find(|line| line.contains("RSTR_CAP_RAW:")) + .ok_or_else(|| anyhow::anyhow!("Expected raw capped Rust str output: {stdout}"))?; + assert!( + cap_raw_line.contains("RSTR_CAP_RAW:hel :68 65 6c "), + "Unexpected raw capped Rust str output: {cap_raw_line}" + ); + assert!( + !stdout.contains("ExprError"), + "Unexpected ExprError: {stdout}" + ); + Ok(()) } diff --git a/e2e-tests/tests/rust_version_matrix_execution.rs b/e2e-tests/tests/rust_version_matrix_execution.rs new file mode 100644 index 00000000..2d42dee4 --- /dev/null +++ b/e2e-tests/tests/rust_version_matrix_execution.rs @@ -0,0 +1,318 @@ +//! Runtime smoke coverage for Rust values across pinned target compilers. + +mod common; + +use std::path::{Path, PathBuf}; + +use common::{ + init, + rust_toolchain::{ + compile_compact_standalone_fixture, configured_toolchains, fixture_tempdir, + precompiled_compat_fixture, rustc_for_toolchain, rustc_version, toolchain_id, + }, +}; +use ghostscope_dwarf::{DwarfAnalyzer, RustcVersion, SourceLanguage}; +use regex::Regex; + +const REQUIRE_TOOLCHAINS_ENV: &str = "GHOSTSCOPE_REQUIRE_RUST_E2E_TOOLCHAINS"; +const PRESERVE_FIXTURES_ENV: &str = "GHOSTSCOPE_PRESERVE_PRECOMPILED_FIXTURES"; + +const COMMON_SCRIPT: &str = r#" +trace observe_matrix_values { + print "RUST_MATRIX_VALUES:{}:{}:{}:{}", string, vector, btree_map, + hash_map; +} + +trace observe_matrix_dst { + print "RUST_MATRIX_DST:{}:{}", rc, arc; +} + +trace observe_matrix_mut_str { + print "RUST_MATRIX_MUT_STR:{}", value; +} + +trace observe_matrix_enums { + print "RUST_MATRIX_ENUMS:{}:{}:{}:{}:{}:{}", *unit, *tuple, + *struct_value, *fieldless, *some, *none; +} + +trace observe_matrix_enum_edges { + print "RUST_MATRIX_ENUM_EDGES:{}:{}:{}", *single, *signed, *unsigned; +} + +trace observe_matrix_nested { + print "RUST_MATRIX_NESTED:{}", *value; +} + +trace observe_matrix_pointer_niche { + print "RUST_MATRIX_POINTER_NICHE:{}:{}", *some, *none; +} + +trace observe_matrix_repr_c { + print "RUST_MATRIX_REPR_C:{}:{}:{}", *unit, *tuple, *struct_value; +} +"#; + +const WRAPPER_SCRIPT: &str = r#" +trace observe_matrix_rc { + print "RUST_MATRIX_RC:{}", rc; +} +"#; + +fn compile_fixture(rustc: &Path, toolchain: &str, output_dir: &Path) -> anyhow::Result { + std::fs::create_dir_all(output_dir)?; + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/rust_compat_program/main.rs"); + let binary = output_dir.join("rust_compat_program"); + compile_compact_standalone_fixture(rustc, toolchain, &source, &binary)?; + Ok(binary) +} + +fn fixture_binary(rustc: &Path, toolchain: &str, temp_dir: &Path) -> anyhow::Result { + let precompiled = precompiled_compat_fixture(toolchain); + if std::env::var_os(PRESERVE_FIXTURES_ENV).is_some() && precompiled.is_file() { + return Ok(precompiled); + } + + compile_fixture(rustc, toolchain, &temp_dir.join(toolchain_id(toolchain))) +} + +async fn assert_target_rustc_version( + binary: &Path, + toolchain: &str, + expected: RustcVersion, +) -> anyhow::Result<()> { + let analyzer = DwarfAnalyzer::from_exec_path(binary).await?; + let context = analyzer + .lookup_function_addresses("observe_matrix_values") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing function observe_matrix_values"))?; + let metadata = analyzer + .compilation_unit_metadata_for_context(&context)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing compilation-unit metadata"))?; + anyhow::ensure!( + metadata.language == SourceLanguage::Rust, + "{toolchain}: expected Rust, got {:?}", + metadata.language + ); + anyhow::ensure!( + metadata.rustc_version() == Some(expected), + "{toolchain}: target producer {:?} did not report rustc {expected}", + metadata.producer + ); + Ok(()) +} + +fn assert_common_output(toolchain: &str, stdout: &str) -> anyhow::Result<()> { + let values = marker_line(stdout, "RUST_MATRIX_VALUES", toolchain)?; + for expected in [ + r#""matrix = string""#, + "[10, -20]", + "BTreeMap(size=1) {-7: 13}", + "HashMap(size=1) {29: 17}", + ] { + anyhow::ensure!( + values.contains(expected), + "{toolchain}: missing {expected:?} in {values}" + ); + } + + let dst = marker_line(stdout, "RUST_MATRIX_DST", toolchain)?; + let expected = Regex::new(concat!( + r"Rc\(strong=1, weak=0\) \{ ptr: 0x[0-9a-f]+ \([^)]*\), ", + r"strong: 1, weak: 0 \}:", + r"Arc\(strong=1, weak=0\) \{ ptr: 0x[0-9a-f]+ \([^)]*\), ", + r"strong: 1, weak: 0 \}", + ))?; + anyhow::ensure!( + expected.is_match(dst), + "{toolchain}: unexpected Rc/Arc output: {dst}" + ); + + let mutable_text = marker_line(stdout, "RUST_MATRIX_MUT_STR", toolchain)?; + anyhow::ensure!( + mutable_text.contains(r#""matrix mutable""#), + "{toolchain}: unexpected &mut str output: {mutable_text}" + ); + Ok(()) +} + +fn assert_wrapper_output(toolchain: &str, stdout: &str) -> anyhow::Result<()> { + let rc = marker_line(stdout, "RUST_MATRIX_RC", toolchain)?; + anyhow::ensure!( + rc.contains("Rc(strong=1, weak=0) { value: 11, strong: 1, weak: 0 }"), + "{toolchain}: unexpected Rc output: {rc}" + ); + Ok(()) +} + +fn assert_enum_output(toolchain: &str, stdout: &str) -> anyhow::Result<()> { + let enums = marker_line(stdout, "RUST_MATRIX_ENUMS", toolchain)?; + for expected in [ + "MatrixEnum::Unit", + "MatrixEnum::Tuple(31, 37)", + "MatrixEnum::Struct { value: 47, flag: 53 }", + "MatrixFieldless::Second", + "Option::Some(", + ">::None", + "59", + ] { + anyhow::ensure!( + enums.contains(expected), + "{toolchain}: missing {expected:?} in {enums}" + ); + } + + let edges = marker_line(stdout, "RUST_MATRIX_ENUM_EDGES", toolchain)?; + for expected in [ + "MatrixSingle::Only(71)", + "MatrixSigned::Negative", + "MatrixUnsigned::High", + ] { + anyhow::ensure!( + edges.contains(expected), + "{toolchain}: missing {expected:?} in {edges}" + ); + } + + let nested = marker_line(stdout, "RUST_MATRIX_NESTED", toolchain)?; + anyhow::ensure!( + nested.contains("MatrixOuter::Wrapped(MatrixInner::Pair(7, 9))"), + "{toolchain}: unexpected nested enum output: {nested}" + ); + + let pointer_niche = marker_line(stdout, "RUST_MATRIX_POINTER_NICHE", toolchain)?; + let pointer_niche_pattern = + Regex::new(concat!(r"::Some\(0x[0-9a-f]+ \(i32\*\)\)", r".*::None",))?; + anyhow::ensure!( + pointer_niche_pattern.is_match(pointer_niche), + "{toolchain}: unexpected pointer niche output: {pointer_niche}" + ); + + let repr_c = marker_line(stdout, "RUST_MATRIX_REPR_C", toolchain)?; + for expected in [ + "MatrixReprC::Unit", + "MatrixReprC::Tuple(73, 79)", + "MatrixReprC::Struct { left: 83, right: 89 }", + ] { + anyhow::ensure!( + repr_c.contains(expected), + "{toolchain}: missing {expected:?} in {repr_c}" + ); + } + Ok(()) +} + +fn marker_line<'a>(stdout: &'a str, marker: &str, toolchain: &str) -> anyhow::Result<&'a str> { + stdout + .lines() + .find(|line| line.contains(marker)) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing {marker}: {stdout}")) +} + +async fn run_toolchain(toolchain: String, require_all: bool) -> anyhow::Result { + eprintln!("testing Rust e2e toolchain {toolchain}"); + let Some(rustc) = rustc_for_toolchain(&toolchain) else { + anyhow::ensure!( + !require_all, + "required Rust toolchain {toolchain} is not installed" + ); + eprintln!("skipping unavailable Rust e2e toolchain {toolchain}"); + return Ok(false); + }; + + let temp_dir = fixture_tempdir()?; + let expected_version = rustc_version(&rustc, &toolchain)?; + let binary = fixture_binary(&rustc, &toolchain, temp_dir.path())?; + assert_target_rustc_version(&binary, &toolchain, expected_version).await?; + + // Keep runtime coverage to representative high-risk layout families. + // The DWARF compatibility test checks every adapter audited for each + // pinned compiler, while the complete Rust 1.88 e2e suite checks all + // output. + // Wrapper adapters have a 1.49 floor until their older concrete DWARF + // shapes receive the same rust-gdb-based audit. + let mut script = COMMON_SCRIPT.to_string(); + let include_wrappers = expected_version >= RustcVersion::new(1, 49, 0); + if include_wrappers { + script.push_str(WRAPPER_SCRIPT); + } + + let target = common::targets::TargetLauncher::binary(&binary) + .current_dir(temp_dir.path()) + .spawn() + .await?; + tokio::time::sleep(std::time::Duration::from_millis(750)).await; + let result = common::runner::GhostscopeRunner::new() + .with_script(&script) + .with_config_content( + r#" +[ebpf] +mem_dump_cap = 512 +"#, + ) + .attach_to(&target) + .timeout_secs(5) + .enable_sysmon_for_target(false) + .run() + .await; + target.terminate().await?; + + let (exit_code, stdout, stderr) = result?; + anyhow::ensure!( + exit_code == 0, + "{toolchain}: exit={exit_code} stderr={stderr} stdout={stdout}" + ); + assert_common_output(&toolchain, &stdout)?; + assert_enum_output(&toolchain, &stdout)?; + if include_wrappers { + assert_wrapper_output(&toolchain, &stdout)?; + } + anyhow::ensure!( + !stdout.contains("ExprError"), + "{toolchain}: unexpected ExprError: {stdout}" + ); + anyhow::ensure!( + !stdout.contains(""), + "{toolchain}: truncated payload: {stdout}" + ); + eprintln!("completed Rust e2e toolchain {toolchain}"); + Ok(true) +} + +#[tokio::test] +async fn test_rust_target_compiler_runtime_matrix() -> anyhow::Result<()> { + init(); + + let require_all = std::env::var_os(REQUIRE_TOOLCHAINS_ENV).is_some(); + let mut tested = 0usize; + let mut failures = Vec::new(); + + // Each case starts a complete GhostScope process, including DWARF analysis + // and eBPF compilation. Running all pinned compilers concurrently multiplies + // peak memory inside same-sandbox containers without increasing coverage. + for toolchain in configured_toolchains() { + match run_toolchain(toolchain, require_all).await { + Ok(true) => tested += 1, + Ok(false) => {} + Err(error) => failures.push(format!("{error:#}")), + } + } + + anyhow::ensure!( + failures.is_empty(), + "Rust target-compiler matrix failures:\n{}", + failures.join("\n") + ); + + if tested == 0 { + eprintln!("no Rust e2e toolchains were available; skipping test"); + } + Ok(()) +} diff --git a/ghostscope-compiler/src/ebpf/codegen/args.rs b/ghostscope-compiler/src/ebpf/codegen/args.rs index 80f922fc..ed9e9481 100644 --- a/ghostscope-compiler/src/ebpf/codegen/args.rs +++ b/ghostscope-compiler/src/ebpf/codegen/args.rs @@ -1,13 +1,639 @@ use super::*; +fn metadata_access_size( + projection: &ghostscope_dwarf::TypeProjection, + role: &str, +) -> Result { + let size = projection.resolved_type.summary.size(); + exact_memory_access_size(size, &format!("indirect value {role} member")) +} + +fn exact_memory_access_size(size: u64, role: &str) -> Result { + // Keep this exact; `from_size` falls back to U64 for unknown widths. + match size { + 1 => Ok(ghostscope_dwarf::MemoryAccessSize::U8), + 2 => Ok(ghostscope_dwarf::MemoryAccessSize::U16), + 4 => Ok(ghostscope_dwarf::MemoryAccessSize::U32), + 8 => Ok(ghostscope_dwarf::MemoryAccessSize::U64), + _ => Err(CodeGenError::DwarfError(format!( + "{role} has unsupported DWARF size {size}" + ))), + } +} + +fn metadata_member( + projection: &ghostscope_dwarf::TypeProjection, + role: &str, +) -> Result<(u64, ghostscope_dwarf::MemoryAccessSize)> { + let access_size = metadata_access_size(projection, role)?; + let offset = projected_member_offset(projection, role)?; + Ok((offset, access_size)) +} + +fn projected_member_offset( + projection: &ghostscope_dwarf::TypeProjection, + role: &str, +) -> Result { + match &projection.layout { + ghostscope_dwarf::TypeProjectionLayout::Member { offset } => Ok(*offset), + layout => Err(CodeGenError::DwarfError(format!( + "semantic value {role} projection must be a member, got {layout:?}" + ))), + } +} + +fn is_known_zero_sized_type(type_info: &ghostscope_dwarf::TypeInfo) -> bool { + match type_info { + ghostscope_dwarf::TypeInfo::BaseType { name, size: 0, .. } if name == "()" => true, + ghostscope_dwarf::TypeInfo::StructType { size: 0, .. } + | ghostscope_dwarf::TypeInfo::UnionType { size: 0, .. } + | ghostscope_dwarf::TypeInfo::VariantType { size: 0, .. } + | ghostscope_dwarf::TypeInfo::ArrayType { + total_size: Some(0), + .. + } => true, + ghostscope_dwarf::TypeInfo::TypedefType { + underlying_type, .. + } + | ghostscope_dwarf::TypeInfo::QualifiedType { + underlying_type, .. + } => is_known_zero_sized_type(underlying_type), + _ => false, + } +} + +fn inline_view_data_len( + physical_type: &ghostscope_dwarf::TypeInfo, + output_type: &ghostscope_dwarf::TypeInfo, +) -> Result { + let physical_size = usize::try_from(physical_type.size()).map_err(|_| { + CodeGenError::DwarfError("inline semantic root size does not fit this host".to_string()) + })?; + let output_size = usize::try_from(output_type.size()).map_err(|_| { + CodeGenError::DwarfError("inline semantic view size does not fit this host".to_string()) + })?; + if output_size != physical_size { + return Err(CodeGenError::DwarfError(format!( + "inline semantic view size {output_size} does not match DWARF root size {physical_size}" + ))); + } + if output_size == 0 && !is_known_zero_sized_type(output_type) { + return Err(CodeGenError::DwarfError( + "inline semantic view has an unknown zero-byte layout".to_string(), + )); + } + Ok(output_size) +} + +fn projected_view_source( + output_type: &ghostscope_dwarf::TypeInfo, + fields: &[ghostscope_dwarf::ProjectedViewField], +) -> Result<(usize, Vec)> { + let ghostscope_dwarf::TypeInfo::StructType { size, members, .. } = output_type else { + return Err(CodeGenError::DwarfError( + "projected semantic view must be a struct".to_string(), + )); + }; + if members.len() != fields.len() { + return Err(CodeGenError::DwarfError( + "projected semantic fields do not match the output type".to_string(), + )); + } + + let data_len = usize::try_from(*size).map_err(|_| { + CodeGenError::DwarfError("projected semantic view size does not fit this host".to_string()) + })?; + if data_len > u16::MAX as usize { + return Err(CodeGenError::DwarfError(format!( + "projected semantic view size {data_len} exceeds the protocol limit" + ))); + } + let mut sources = Vec::with_capacity(fields.len()); + let mut ranges = Vec::with_capacity(fields.len()); + for (member, field) in members.iter().zip(fields) { + let type_matches = match field.capture { + ghostscope_dwarf::ProjectedViewFieldCapture::Value => { + member.member_type == field.value.resolved_type.summary + } + ghostscope_dwarf::ProjectedViewFieldCapture::Address => { + let pointer_size = field.value.steps.iter().rev().find_map(|step| match step { + ghostscope_dwarf::ProjectedValueStep::Dereference { pointer_size } => { + Some(*pointer_size) + } + ghostscope_dwarf::ProjectedValueStep::Member { .. } => None, + }); + matches!( + &member.member_type, + ghostscope_dwarf::TypeInfo::PointerType { target_type, size } + if target_type.as_ref() == &field.value.resolved_type.summary + && pointer_size == Some(*size) + ) + } + }; + if member.offset != field.output_offset || !type_matches { + return Err(CodeGenError::DwarfError(format!( + "projected semantic field '{}' does not match its output member", + member.name + ))); + } + let output_offset = usize::try_from(field.output_offset).map_err(|_| { + CodeGenError::DwarfError(format!( + "projected semantic field '{}' offset does not fit this host", + member.name + )) + })?; + let value_len = usize::try_from(member.member_type.size()).map_err(|_| { + CodeGenError::DwarfError(format!( + "projected semantic field '{}' size does not fit this host", + member.name + )) + })?; + let end = output_offset.checked_add(value_len).ok_or_else(|| { + CodeGenError::DwarfError(format!( + "projected semantic field '{}' end overflow", + member.name + )) + })?; + if end > data_len || (value_len == 0 && !is_known_zero_sized_type(&member.member_type)) { + return Err(CodeGenError::DwarfError(format!( + "projected semantic field '{}' exceeds its output layout", + member.name + ))); + } + if value_len > 0 + && ranges + .iter() + .any(|(start, range_end)| output_offset < *range_end && *start < end) + { + return Err(CodeGenError::DwarfError(format!( + "projected semantic field '{}' overlaps another output member", + member.name + ))); + } + ranges.push((output_offset, end)); + + let mut steps = Vec::with_capacity(field.value.steps.len()); + for step in &field.value.steps { + steps.push(match step { + ghostscope_dwarf::ProjectedValueStep::Member { offset } => { + ProjectedViewStep::Member { offset: *offset } + } + ghostscope_dwarf::ProjectedValueStep::Dereference { pointer_size } => { + ProjectedViewStep::Dereference { + pointer_size: exact_memory_access_size( + *pointer_size, + "projected semantic pointer", + )?, + } + } + }); + } + sources.push(ProjectedViewFieldSource { + output_offset, + value_len, + steps, + capture: field.capture, + }); + } + + Ok((data_len, sources)) +} + +fn sequence_capture_limits(cap: usize, element_stride: u64) -> Result<(usize, usize, usize)> { + let stride = usize::try_from(element_stride).map_err(|_| { + CodeGenError::DwarfError(format!( + "sequence element DWARF size {element_stride} does not fit this host" + )) + })?; + let (max_elements, max_len) = if stride == 0 { + // A byte cap cannot bound a ZST payload, so use the configured value + // as its logical element-count cap. + (cap, 0) + } else { + let max_elements = cap / stride; + (max_elements, max_elements * stride) + }; + let data_len = ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE.saturating_add(max_len); + Ok((max_elements, max_len, data_len)) +} + +fn hash_table_capture_limits( + cap: usize, + entry_stride: u64, + occupancy: ghostscope_dwarf::HashTableOccupancy, +) -> Result<(usize, usize, usize)> { + let stride = usize::try_from(entry_stride).map_err(|_| { + CodeGenError::DwarfError(format!( + "hash-table entry DWARF size {entry_stride} does not fit this host" + )) + })?; + let occupancy_width = occupancy + .byte_width() + .and_then(|width| usize::try_from(width).ok()) + .ok_or_else(|| { + CodeGenError::DwarfError("invalid hash-table occupancy width".to_string()) + })?; + let bytes_per_bucket = stride.checked_add(occupancy_width).ok_or_else(|| { + CodeGenError::DwarfError("hash-table bucket capture size overflow".to_string()) + })?; + let max_buckets = cap / bytes_per_bucket; + let max_len = max_buckets + .checked_mul(bytes_per_bucket) + .ok_or_else(|| CodeGenError::DwarfError("hash-table payload size overflow".to_string()))?; + let data_len = ghostscope_protocol::HASH_TABLE_HEADER_SIZE.saturating_add(max_len); + Ok((max_buckets, max_len, data_len)) +} + +fn btree_capture_limits( + cap: usize, + node_capacity: u64, + key_stride: u64, + value_stride: Option, +) -> Result<(usize, usize, usize)> { + // Nodes are emitted as fixed control-flow blocks so kernels without + // bounded-loop support can verify the program. Cap the unrolled code size. + const MAX_CAPTURE_NODES: usize = 16; + + let capacity = usize::try_from(node_capacity).map_err(|_| { + CodeGenError::DwarfError(format!( + "B-Tree node capacity {node_capacity} does not fit this host" + )) + })?; + let key_stride = usize::try_from(key_stride).map_err(|_| { + CodeGenError::DwarfError("B-Tree key slot stride does not fit this host".to_string()) + })?; + let value_stride = value_stride + .map(|stride| { + usize::try_from(stride).map_err(|_| { + CodeGenError::DwarfError( + "B-Tree value slot stride does not fit this host".to_string(), + ) + }) + }) + .transpose()? + .unwrap_or(0); + let slot_bytes = key_stride + .checked_add(value_stride) + .and_then(|stride| stride.checked_mul(capacity)) + .and_then(|bytes| bytes.checked_add(ghostscope_protocol::BTREE_NODE_HEADER_SIZE)) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree node payload overflow".to_string()))?; + if slot_bytes == 0 { + return Err(CodeGenError::DwarfError( + "B-Tree node payload has no metadata".to_string(), + )); + } + let max_nodes = (cap / slot_bytes).min(MAX_CAPTURE_NODES); + let max_len = max_nodes + .checked_mul(slot_bytes) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree payload size overflow".to_string()))?; + let data_len = ghostscope_protocol::BTREE_HEADER_SIZE + .checked_add(max_len) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree payload size overflow".to_string()))?; + Ok((max_nodes, max_len, data_len)) +} + impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { pub(super) const UNKNOWN_CHAR_ARRAY_READ_FALLBACK: usize = 256; + + fn semantic_value_read_plan( + &self, + resolved_type: &ghostscope_dwarf::ResolvedType, + type_module_path: Option<&std::path::Path>, + ) -> Result> { + let Some(analyzer) = self.process_analyzer else { + return Ok(None); + }; + analyzer + .value_read_plan(resolved_type, type_module_path) + .map_err(|error| CodeGenError::DwarfError(error.to_string())) + } + + fn complex_arg_from_value_read_plan( + &mut self, + display_name: String, + dwarf_type: ghostscope_dwarf::TypeInfo, + descriptor: RuntimeAddress<'ctx>, + plan: ghostscope_dwarf::ValueReadPlan, + ) -> Result> { + let cap = self.compile_options.mem_dump_cap as usize; + let ghostscope_dwarf::ValueReadPlan { + presentation, + capture, + } = plan; + let mut output_type = dwarf_type; + let (data_len, source) = match capture { + ghostscope_dwarf::ValueCapturePlan::ProjectedValue { value } => { + let offset = projected_member_offset(&value, "projected value")?; + output_type = value.resolved_type.summary; + let data_len = Self::compute_read_size_for_type(&output_type); + if data_len == 0 { + if is_known_zero_sized_type(&output_type) { + (0, ComplexArgSource::ImmediateBytes { bytes: Vec::new() }) + } else { + return Err(CodeGenError::TypeSizeNotAvailable(display_name)); + } + } else { + let address = if offset == 0 { + descriptor + } else { + let offset_value = self.context.i64_type().const_int(offset, false); + let address = self + .builder + .build_int_add( + descriptor.value, + offset_value, + "semantic_projected_value_address", + ) + .map_err(|error| CodeGenError::Builder(error.to_string()))?; + descriptor.with_value(address) + }; + ( + data_len, + ComplexArgSource::MemDump { + address, + len: data_len, + }, + ) + } + } + ghostscope_dwarf::ValueCapturePlan::InlineView { + output_type: view_type, + } => { + let data_len = inline_view_data_len(&output_type, &view_type)?; + output_type = view_type; + if data_len == 0 { + (0, ComplexArgSource::ImmediateBytes { bytes: Vec::new() }) + } else { + ( + data_len, + ComplexArgSource::MemDump { + address: descriptor, + len: data_len, + }, + ) + } + } + ghostscope_dwarf::ValueCapturePlan::ProjectedView { + output_type: view_type, + fields, + } => { + let (data_len, fields) = projected_view_source(&view_type, &fields)?; + output_type = view_type; + ( + data_len, + ComplexArgSource::ProjectedView { descriptor, fields }, + ) + } + ghostscope_dwarf::ValueCapturePlan::IndirectBytes { data, length } => { + let (data_offset, data_access_size) = metadata_member(&data, "data")?; + let (length_offset, length_access_size) = metadata_member(&length, "length")?; + let data_len = + ghostscope_protocol::INDIRECT_BYTES_LENGTH_PREFIX_SIZE.saturating_add(cap); + ( + data_len, + ComplexArgSource::IndirectBytes { + descriptor, + data_offset, + data_access_size, + length_offset, + length_access_size, + max_len: cap, + }, + ) + } + ghostscope_dwarf::ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + } => { + let (data_offset, data_access_size) = metadata_member(&data, "data")?; + let (length_offset, length_access_size) = metadata_member(&length, "length")?; + let (max_elements, max_len, data_len) = + sequence_capture_limits(cap, element_stride)?; + ( + data_len, + ComplexArgSource::IndirectSequence { + descriptor, + data_offset, + data_access_size, + length_offset, + length_access_size, + element_stride, + max_elements, + max_len, + }, + ) + } + ghostscope_dwarf::ValueCapturePlan::IndirectRingSequence { + data, + start, + length, + capacity, + element_stride, + } => { + let (data_offset, data_access_size) = metadata_member(&data, "data")?; + let (start_offset, start_access_size) = metadata_member(&start, "start")?; + let length = match *length { + ghostscope_dwarf::RingSequenceLength::Explicit(length) => { + let (offset, access_size) = metadata_member(&length, "length")?; + RingSequenceLengthSource::Explicit { + offset, + access_size, + } + } + ghostscope_dwarf::RingSequenceLength::End(end) => { + let (offset, access_size) = metadata_member(&end, "end")?; + RingSequenceLengthSource::End { + offset, + access_size, + } + } + }; + let (capacity_offset, capacity_access_size) = + metadata_member(&capacity, "capacity")?; + let (max_elements, max_len, data_len) = + sequence_capture_limits(cap, element_stride)?; + ( + data_len, + ComplexArgSource::IndirectRingSequence { + descriptor, + data_offset, + data_access_size, + start_offset, + start_access_size, + length, + capacity_offset, + capacity_access_size, + element_stride, + max_elements, + max_len, + }, + ) + } + ghostscope_dwarf::ValueCapturePlan::IndirectHashTable { + control, + length, + bucket_mask, + entry_stride, + occupancy, + buckets, + bucket_order, + } => { + let (control_offset, control_access_size) = + metadata_member(&control, "hash-table control")?; + let buckets = match buckets { + ghostscope_dwarf::HashTableBucketSource::Forward { data } => { + let (data_offset, data_access_size) = + metadata_member(&data, "hash-table data")?; + HashTableBucketSource::Forward { + data_offset, + data_access_size, + } + } + ghostscope_dwarf::HashTableBucketSource::ReverseFromControl => { + HashTableBucketSource::ReverseFromControl + } + ghostscope_dwarf::HashTableBucketSource::LegacyAfterControl { + entry_alignment, + pointer_tag_mask, + } => HashTableBucketSource::LegacyAfterControl { + entry_alignment, + pointer_tag_mask, + }, + }; + let (length_offset, length_access_size) = + metadata_member(&length, "hash-table length")?; + let (bucket_mask_offset, bucket_mask_access_size) = + metadata_member(&bucket_mask, "hash-table bucket mask")?; + let layout_matches = matches!( + (bucket_order, buckets), + ( + ghostscope_dwarf::HashTableBucketOrder::Forward, + HashTableBucketSource::Forward { .. } + | HashTableBucketSource::LegacyAfterControl { .. } + ) | ( + ghostscope_dwarf::HashTableBucketOrder::Reverse, + HashTableBucketSource::ReverseFromControl + ) + ); + if !layout_matches { + return Err(CodeGenError::DwarfError( + "hash-table bucket order does not match its data source".to_string(), + )); + } + let (max_buckets, max_len, data_len) = + hash_table_capture_limits(cap, entry_stride, occupancy)?; + ( + data_len, + ComplexArgSource::IndirectHashTable { + descriptor, + control_offset, + control_access_size, + length_offset, + length_access_size, + bucket_mask_offset, + bucket_mask_access_size, + entry_stride, + occupancy, + buckets, + bucket_order, + max_buckets, + max_len, + }, + ) + } + ghostscope_dwarf::ValueCapturePlan::IndirectBTree { + root_pointer, + root_height, + length, + node_length, + keys, + values, + edges, + node_capacity, + } => { + let (root_pointer_offset, root_pointer_access_size) = + metadata_member(&root_pointer, "B-Tree root pointer")?; + let (root_height_offset, root_height_access_size) = + metadata_member(&root_height, "B-Tree root height")?; + let (length_offset, length_access_size) = + metadata_member(&length, "B-Tree length")?; + let (node_length_offset, node_length_access_size) = + metadata_member(&node_length, "B-Tree node length")?; + let pointer_access_size = + exact_memory_access_size(edges.pointer_size, "B-Tree edge pointer")?; + let values_source = values.map(|values| BTreeArraySource { + offset: values.offset, + slot_stride: values.slot_stride, + }); + let (max_nodes, max_len, data_len) = btree_capture_limits( + cap, + node_capacity, + keys.slot_stride, + values_source.map(|values| values.slot_stride), + )?; + ( + data_len, + ComplexArgSource::IndirectBTree { + descriptor, + root_pointer_offset, + root_pointer_access_size, + root_height_offset, + root_height_access_size, + length_offset, + length_access_size, + node_length_offset, + node_length_access_size, + keys: BTreeArraySource { + offset: keys.offset, + slot_stride: keys.slot_stride, + }, + values: values_source, + edges: BTreeEdgesSource { + offset_from_leaf: edges.offset_from_leaf, + slot_stride: edges.slot_stride, + pointer_offset: edges.pointer_offset, + pointer_access_size, + edge_count: edges.edge_count, + }, + node_capacity, + max_nodes, + max_len, + }, + ) + } + }; + + Ok(ComplexArg { + var_name_index: self.trace_context.add_variable_name(display_name)?, + type_index: self + .trace_context + .add_type_with_presentation(output_type, presentation)?, + access_path: Vec::new(), + data_len, + source, + }) + } + pub(super) fn complex_arg_from_dwarf_read_plan( &mut self, plan: ghostscope_dwarf::VariableReadPlan, display_name: Option, ) -> Result> { let pc_address = self.get_compile_time_context()?.pc_address; + let semantic_plan = if let Some(analyzer) = self.process_analyzer { + let resolved_type = analyzer + .resolved_type_for_plan(&plan) + .map_err(|error| CodeGenError::DwarfError(error.to_string()))?; + match resolved_type { + Some(resolved_type) => { + self.semantic_value_read_plan(&resolved_type, plan.module_path.as_deref())? + } + None => None, + } + } else { + None + }; let materialized = self.variable_read_plan_to_materialization(plan, pc_address)?; let display_name = display_name.unwrap_or_else(|| materialized.name.clone()); @@ -39,12 +665,22 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { "Expression has no DWARF type information".to_string(), ) })?; + let module_hint = + Self::module_path_for_offsets(materialized.module_path.as_deref()); + if let Some(semantic_plan) = semantic_plan { + let descriptor = + self.resolve_planned_address(address, None, module_hint.as_deref())?; + return self.complex_arg_from_value_read_plan( + display_name, + dwarf_type, + descriptor, + semantic_plan, + ); + } let data_len = Self::compute_read_size_for_type(&dwarf_type); if data_len == 0 { return Err(CodeGenError::TypeSizeNotAvailable(display_name)); } - let module_hint = - Self::module_path_for_offsets(materialized.module_path.as_deref()); Ok(ComplexArg { var_name_index: self.trace_context.add_variable_name(display_name)?, type_index: self.trace_context.add_type(dwarf_type.clone())?, @@ -58,13 +694,59 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { }) } ghostscope_dwarf::VariableMaterialization::DirectValue { .. } => { - let value = - self.variable_materialization_to_llvm_value(&materialized, pc_address, None)?; let dwarf_type = materialized.dwarf_type.clone().ok_or_else(|| { CodeGenError::DwarfError( "Expression has no DWARF type information".to_string(), ) })?; + if let Some(ghostscope_dwarf::ValueReadPlan { + presentation, + capture: + ghostscope_dwarf::ValueCapturePlan::ProjectedValue { value: projected }, + }) = semantic_plan.as_ref() + { + let projected_type = &projected.resolved_type.summary; + if Self::compute_read_size_for_type(projected_type) == 0 + && is_known_zero_sized_type(projected_type) + { + return Ok(ComplexArg { + var_name_index: self + .trace_context + .add_variable_name(display_name)?, + type_index: self.trace_context.add_type_with_presentation( + projected_type.clone(), + presentation.clone(), + )?, + access_path: Vec::new(), + data_len: 0, + source: ComplexArgSource::ImmediateBytes { bytes: Vec::new() }, + }); + } + } + if let Some(ghostscope_dwarf::ValueReadPlan { + presentation, + capture: + ghostscope_dwarf::ValueCapturePlan::InlineView { output_type }, + }) = semantic_plan.as_ref() + { + let data_len = inline_view_data_len(&dwarf_type, output_type)?; + if data_len == 0 { + return Ok(ComplexArg { + var_name_index: self + .trace_context + .add_variable_name(display_name)?, + type_index: self.trace_context.add_type_with_presentation( + output_type.clone(), + presentation.clone(), + )?, + access_path: Vec::new(), + data_len: 0, + source: ComplexArgSource::ImmediateBytes { bytes: Vec::new() }, + }); + } + } + let value = + self.variable_materialization_to_llvm_value(&materialized, pc_address, None)?; let value = match value { BasicValueEnum::IntValue(value) => value, BasicValueEnum::PointerValue(value) => self @@ -78,6 +760,93 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { ))) } }; + if let Some(semantic_plan) = semantic_plan { + match semantic_plan.capture { + ghostscope_dwarf::ValueCapturePlan::ProjectedValue { + value: projected, + } => { + let offset = + projected_member_offset(&projected, "projected value")?; + let projected_type = projected.resolved_type.summary; + let data_len = Self::compute_read_size_for_type(&projected_type); + let container_len = Self::compute_read_size_for_type(&dwarf_type); + let projected_end = usize::try_from(offset) + .ok() + .and_then(|offset| offset.checked_add(data_len)); + if data_len == 0 + || data_len > 8 + || projected_end + .is_none_or(|end| end > container_len || end > 8) + { + return Err(CodeGenError::DwarfError(format!( + "direct semantic projection for '{}' does not fit one eBPF register", + materialized.name + ))); + } + let value = if offset == 0 { + value + } else { + let shift = value.get_type().const_int(offset * 8, false); + self.builder + .build_right_shift( + value, + shift, + false, + "semantic_projected_direct_value", + ) + .map_err(|error| { + CodeGenError::Builder(error.to_string()) + })? + }; + return Ok(ComplexArg { + var_name_index: self + .trace_context + .add_variable_name(display_name)?, + type_index: self.trace_context.add_type_with_presentation( + projected_type, + semantic_plan.presentation, + )?, + access_path: Vec::new(), + data_len, + source: ComplexArgSource::ComputedInt { + value, + byte_len: data_len, + }, + }); + } + ghostscope_dwarf::ValueCapturePlan::InlineView { output_type } => { + let data_len = inline_view_data_len(&dwarf_type, &output_type)?; + if data_len == 0 || data_len > 8 { + return Err(CodeGenError::DwarfError(format!( + "direct semantic view for '{}' does not fit one eBPF register", + materialized.name + ))); + } + return Ok(ComplexArg { + var_name_index: self + .trace_context + .add_variable_name(display_name)?, + type_index: self.trace_context.add_type_with_presentation( + output_type, + semantic_plan.presentation, + )?, + access_path: Vec::new(), + data_len, + source: ComplexArgSource::ComputedInt { + value, + byte_len: data_len, + }, + }); + } + ghostscope_dwarf::ValueCapturePlan::ProjectedView { .. } => { + return Err(CodeGenError::DwarfError(format!( + "direct semantic view for '{}' requires an address-backed root", + materialized.name + ))); + } + _ => {} + } + } let data_len = Self::compute_read_size_for_type(&dwarf_type).clamp(1, 8); Ok(ComplexArg { var_name_index: self.trace_context.add_variable_name(display_name)?, @@ -101,6 +870,17 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { expr: &crate::script::ast::Expr, lvalue: crate::ebpf::expression::DynamicLvalue<'ctx>, ) -> Result> { + if let Some(plan) = self.semantic_value_read_plan( + &lvalue.type_info.resolved_type, + lvalue.type_info.type_module_path.as_deref(), + )? { + return self.complex_arg_from_value_read_plan( + self.expr_to_name(expr), + lvalue.type_info.resolved_type.summary, + lvalue.address, + plan, + ); + } let dwarf_type = lvalue.type_info.resolved_type.summary; let data_len = Self::compute_read_size_for_type(&dwarf_type); if data_len == 0 { @@ -695,7 +1475,14 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { self.generate_print_complex_format_instruction(fmt_idx, &[arg])?; Ok(1) } - ComplexArgSource::MemDump { .. } | ComplexArgSource::MemDumpDynamic { .. } => { + ComplexArgSource::MemDump { .. } + | ComplexArgSource::MemDumpDynamic { .. } + | ComplexArgSource::IndirectBytes { .. } + | ComplexArgSource::IndirectSequence { .. } + | ComplexArgSource::IndirectRingSequence { .. } + | ComplexArgSource::IndirectHashTable { .. } + | ComplexArgSource::IndirectBTree { .. } + | ComplexArgSource::ProjectedView { .. } => { // Use ComplexFormat with "{}"; generate_print_complex_format_instruction handles MemDump let fmt_idx = self.trace_context.add_string("{}".to_string())?; self.generate_print_complex_format_instruction(fmt_idx, &[arg])?; @@ -785,6 +1572,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { | ghostscope_dwarf::TypeInfo::ArrayType { .. } | ghostscope_dwarf::TypeInfo::StructType { .. } | ghostscope_dwarf::TypeInfo::UnionType { .. } + | ghostscope_dwarf::TypeInfo::VariantType { .. } ) } @@ -1020,6 +1808,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { | ghostscope_dwarf::TypeInfo::ArrayType { .. } | ghostscope_dwarf::TypeInfo::StructType { .. } | ghostscope_dwarf::TypeInfo::UnionType { .. } + | ghostscope_dwarf::TypeInfo::VariantType { .. } ) }), // Constant offset on top of an alias-eligible expression @@ -1051,3 +1840,323 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { // removed old helpers (pure lvalue/binary_op detection) — unified resolver handles shapes } + +#[cfg(test)] +mod semantic_value_tests { + use super::*; + use ghostscope_dwarf::{ + MemoryAccessSize, ProjectedValueRead, ProjectedValueStep, ProjectedViewField, ResolvedType, + StructMember, TypeIdentity, TypeInfo, TypeProjection, TypeProjectionLayout, + }; + + fn projection(size: u64) -> TypeProjection { + let encoding = ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16; + TypeProjection { + layout: TypeProjectionLayout::Member { offset: 0 }, + resolved_type: ResolvedType::new( + TypeInfo::BaseType { + name: "metadata".to_string(), + size, + encoding, + }, + TypeIdentity::Unknown, + None, + ), + } + } + + #[test] + fn maps_indirect_metadata_size_from_projected_dwarf_type() { + assert_eq!( + metadata_access_size(&projection(4), "data").unwrap(), + MemoryAccessSize::U32 + ); + assert_eq!( + metadata_access_size(&projection(8), "length").unwrap(), + MemoryAccessSize::U64 + ); + } + + #[test] + fn rejects_unsupported_indirect_metadata_size() { + let projected = projection(3); + let error = metadata_access_size(&projected, "data").unwrap_err(); + + assert!(error.to_string().contains("unsupported DWARF size 3")); + } + + #[test] + fn btree_capture_limits_reserve_complete_fixed_node_records() { + let header = ghostscope_protocol::BTREE_HEADER_SIZE; + assert_eq!( + btree_capture_limits(108, 2, 4, Some(2)).unwrap(), + (3, 108, header + 108) + ); + assert_eq!( + btree_capture_limits(107, 2, 4, Some(2)).unwrap(), + (2, 72, header + 72) + ); + + let zst_record_size = ghostscope_protocol::BTREE_NODE_HEADER_SIZE; + assert_eq!( + btree_capture_limits(zst_record_size * 20, 11, 0, Some(0)).unwrap(), + (16, zst_record_size * 16, header + zst_record_size * 16,) + ); + } + + #[test] + fn reads_projected_value_offset_without_assuming_field_names() { + let mut projected = projection(4); + projected.layout = TypeProjectionLayout::Member { offset: 12 }; + + assert_eq!( + projected_member_offset(&projected, "projected value").unwrap(), + 12 + ); + + projected.layout = TypeProjectionLayout::Dereference; + let error = projected_member_offset(&projected, "projected value").unwrap_err(); + assert!(error.to_string().contains("must be a member")); + } + + #[test] + fn distinguishes_known_zero_sized_types_from_unknown_layouts() { + let unit = TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16, + }; + let unknown = TypeInfo::UnknownType { + name: "T".to_string(), + }; + + assert!(is_known_zero_sized_type(&unit)); + assert!(!is_known_zero_sized_type(&unknown)); + } + + #[test] + fn inline_view_requires_the_exact_dwarf_root_size() { + let physical = TypeInfo::StructType { + name: "Physical".to_string(), + size: 16, + members: Vec::new(), + }; + let view = TypeInfo::StructType { + name: "Semantic".to_string(), + size: 16, + members: Vec::new(), + }; + assert_eq!(inline_view_data_len(&physical, &view).unwrap(), 16); + + let undersized = TypeInfo::StructType { + name: "Semantic".to_string(), + size: 8, + members: Vec::new(), + }; + let error = inline_view_data_len(&physical, &undersized).unwrap_err(); + assert!(error + .to_string() + .contains("does not match DWARF root size 16")); + } + + fn projected_field( + output_offset: u64, + summary: TypeInfo, + steps: Vec, + ) -> ProjectedViewField { + ProjectedViewField { + output_offset, + value: ProjectedValueRead { + steps, + resolved_type: ResolvedType::new(summary, TypeIdentity::Unknown, None), + }, + capture: ghostscope_dwarf::ProjectedViewFieldCapture::Value, + } + } + + fn output_member(name: &str, member_type: TypeInfo, offset: u64) -> StructMember { + StructMember { + name: name.to_string(), + member_type, + offset, + bit_offset: None, + bit_size: None, + } + } + + #[test] + fn projected_view_uses_exact_output_and_pointer_layouts() { + let value_type = TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: ghostscope_dwarf::constants::DW_ATE_signed.0 as u16, + }; + let borrow_type = TypeInfo::BaseType { + name: "isize".to_string(), + size: 8, + encoding: ghostscope_dwarf::constants::DW_ATE_signed.0 as u16, + }; + let output_type = TypeInfo::StructType { + name: "Ref".to_string(), + size: 12, + members: vec![ + output_member("*value", value_type.clone(), 0), + output_member("borrow", borrow_type.clone(), 4), + ], + }; + let fields = vec![ + projected_field( + 0, + value_type, + vec![ + ProjectedValueStep::Member { offset: 8 }, + ProjectedValueStep::Dereference { pointer_size: 8 }, + ], + ), + projected_field( + 4, + borrow_type, + vec![ProjectedValueStep::Dereference { pointer_size: 8 }], + ), + ]; + + let (data_len, sources) = projected_view_source(&output_type, &fields).unwrap(); + assert_eq!(data_len, 12); + assert_eq!(sources.len(), 2); + assert_eq!(sources[0].output_offset, 0); + assert_eq!(sources[0].value_len, 4); + assert!(matches!( + sources[0].steps.as_slice(), + [ + ProjectedViewStep::Member { offset: 8 }, + ProjectedViewStep::Dereference { + pointer_size: MemoryAccessSize::U64 + } + ] + )); + } + + #[test] + fn projected_view_accepts_a_dwarf_sized_address_field() { + let target = TypeInfo::ArrayType { + element_type: Box::new(TypeInfo::BaseType { + name: "u8".to_string(), + size: 1, + encoding: ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16, + }), + element_count: None, + total_size: None, + }; + let pointer = TypeInfo::PointerType { + target_type: Box::new(target.clone()), + size: 8, + }; + let output_type = TypeInfo::StructType { + name: "Rc".to_string(), + size: 8, + members: vec![output_member("ptr", pointer, 0)], + }; + let fields = vec![ProjectedViewField { + output_offset: 0, + value: ProjectedValueRead { + steps: vec![ + ProjectedValueStep::Dereference { pointer_size: 8 }, + ProjectedValueStep::Member { offset: 16 }, + ], + resolved_type: ResolvedType::new(target, TypeIdentity::Unknown, None), + }, + capture: ghostscope_dwarf::ProjectedViewFieldCapture::Address, + }]; + + let (data_len, sources) = projected_view_source(&output_type, &fields).unwrap(); + assert_eq!(data_len, 8); + assert_eq!(sources[0].value_len, 8); + assert_eq!( + sources[0].capture, + ghostscope_dwarf::ProjectedViewFieldCapture::Address + ); + } + + #[test] + fn projected_view_accepts_packed_zero_sized_fields() { + let unit = TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: ghostscope_dwarf::constants::DW_ATE_unsigned.0 as u16, + }; + let borrow = TypeInfo::BaseType { + name: "isize".to_string(), + size: 8, + encoding: ghostscope_dwarf::constants::DW_ATE_signed.0 as u16, + }; + let output_type = TypeInfo::StructType { + name: "Ref".to_string(), + size: 8, + members: vec![ + output_member("*value", unit.clone(), 0), + output_member("borrow", borrow.clone(), 0), + ], + }; + let fields = vec![ + projected_field( + 0, + unit, + vec![ProjectedValueStep::Dereference { pointer_size: 8 }], + ), + projected_field( + 0, + borrow, + vec![ProjectedValueStep::Dereference { pointer_size: 8 }], + ), + ]; + + let (data_len, sources) = projected_view_source(&output_type, &fields).unwrap(); + assert_eq!(data_len, 8); + assert_eq!(sources[0].value_len, 0); + assert_eq!(sources[1].output_offset, 0); + } + + #[test] + fn projected_view_rejects_invalid_protocol_and_field_layouts() { + let oversized = TypeInfo::StructType { + name: "Oversized".to_string(), + size: u16::MAX as u64 + 1, + members: Vec::new(), + }; + let error = projected_view_source(&oversized, &[]).unwrap_err(); + assert!(error.to_string().contains("exceeds the protocol limit")); + + let value_type = TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: ghostscope_dwarf::constants::DW_ATE_signed.0 as u16, + }; + let overlapping = TypeInfo::StructType { + name: "Overlap".to_string(), + size: 6, + members: vec![ + output_member("left", value_type.clone(), 0), + output_member("right", value_type.clone(), 2), + ], + }; + let fields = vec![ + projected_field(0, value_type.clone(), Vec::new()), + projected_field(2, value_type.clone(), Vec::new()), + ]; + let error = projected_view_source(&overlapping, &fields).unwrap_err(); + assert!(error.to_string().contains("overlaps another output member")); + + let output = TypeInfo::StructType { + name: "Pointer".to_string(), + size: 4, + members: vec![output_member("value", value_type.clone(), 0)], + }; + let fields = vec![projected_field( + 0, + value_type, + vec![ProjectedValueStep::Dereference { pointer_size: 3 }], + )]; + let error = projected_view_source(&output, &fields).unwrap_err(); + assert!(error.to_string().contains("unsupported DWARF size 3")); + } +} diff --git a/ghostscope-compiler/src/ebpf/codegen/backtrace/payload.rs b/ghostscope-compiler/src/ebpf/codegen/backtrace/payload.rs index 0b060f43..e5916edf 100644 --- a/ghostscope-compiler/src/ebpf/codegen/backtrace/payload.rs +++ b/ghostscope-compiler/src/ebpf/codegen/backtrace/payload.rs @@ -341,29 +341,4 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { .map_err(|e| CodeGenError::LLVMError(e.to_string())) } } - - pub(super) fn build_entry_alloca(&self, ty: T, name: &str) -> Result> - where - T: inkwell::types::BasicType<'ctx>, - { - let current_block = self.builder.get_insert_block().ok_or_else(|| { - CodeGenError::LLVMError("no current block for bt stack allocation".to_string()) - })?; - let current_fn = self.current_function("allocate bt scratch")?; - let entry_block = current_fn.get_first_basic_block().ok_or_else(|| { - CodeGenError::LLVMError("no entry block for bt stack allocation".to_string()) - })?; - - if let Some(first_instruction) = entry_block.get_first_instruction() { - self.builder.position_before(&first_instruction); - } else { - self.builder.position_at_end(entry_block); - } - let alloca = self - .builder - .build_alloca(ty, name) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - self.builder.position_at_end(current_block); - Ok(alloca) - } } diff --git a/ghostscope-compiler/src/ebpf/codegen/format.rs b/ghostscope-compiler/src/ebpf/codegen/format.rs index 4aa198cc..6a111198 100644 --- a/ghostscope-compiler/src/ebpf/codegen/format.rs +++ b/ghostscope-compiler/src/ebpf/codegen/format.rs @@ -19,6 +19,97 @@ struct ComplexFormatArgPointers<'ctx> { var_data_ptr: PointerValue<'ctx>, } +#[derive(Clone, Copy)] +enum RingCaptureLengthKind { + Explicit, + End, +} + +#[derive(Clone, Copy)] +struct RingCaptureConfig { + start_offset: u64, + start_access_size: ghostscope_dwarf::MemoryAccessSize, + capacity_offset: u64, + capacity_access_size: ghostscope_dwarf::MemoryAccessSize, + length_kind: RingCaptureLengthKind, +} + +#[derive(Clone, Copy)] +enum IndirectCaptureShape { + Bytes, + Sequence { + element_stride: u64, + max_elements: usize, + ring: Option, + }, +} + +impl IndirectCaptureShape { + fn prefix_len(self) -> usize { + match self { + Self::Bytes => ghostscope_protocol::INDIRECT_BYTES_LENGTH_PREFIX_SIZE, + Self::Sequence { .. } => ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE, + } + } + + fn reservation_factor(self) -> usize { + match self { + Self::Sequence { ring: Some(_), .. } => 2, + Self::Bytes | Self::Sequence { ring: None, .. } => 1, + } + } +} + +#[derive(Clone, Copy)] +struct IndirectCaptureConfig { + data_offset: u64, + data_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + max_len: usize, + shape: IndirectCaptureShape, +} + +#[derive(Clone, Copy)] +struct HashTableCaptureConfig { + control_offset: u64, + control_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + bucket_mask_offset: u64, + bucket_mask_access_size: ghostscope_dwarf::MemoryAccessSize, + entry_stride: u64, + occupancy: ghostscope_dwarf::HashTableOccupancy, + buckets: HashTableBucketSource, + bucket_order: ghostscope_dwarf::HashTableBucketOrder, + max_buckets: usize, +} + +#[derive(Clone, Copy)] +struct BTreeCaptureConfig { + root_pointer_offset: u64, + root_pointer_access_size: ghostscope_dwarf::MemoryAccessSize, + root_height_offset: u64, + root_height_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + node_length_offset: u64, + node_length_access_size: ghostscope_dwarf::MemoryAccessSize, + keys: BTreeArraySource, + values: Option, + edges: BTreeEdgesSource, + node_capacity: u64, + max_nodes: usize, +} + +struct BTreeBulkRead<'ctx, 'name> { + destination_offset: usize, + source_address: IntValue<'ctx>, + length: IntValue<'ctx>, + max_len: usize, + name: &'name str, +} + fn complex_format_arg_header_len(arg: &ComplexArg<'_>) -> usize { PRINT_COMPLEX_FORMAT_ARG_FIXED_HEADER_LEN + arg.access_path.len() } @@ -35,10 +126,30 @@ fn complex_format_static_payload_len(arg: &ComplexArg<'_>) -> Option { ComplexArgSource::MemDump { len, .. } => { Some(std::cmp::max(*len, VARIABLE_READ_ERROR_PAYLOAD_LEN)) } + ComplexArgSource::ProjectedView { .. } => { + Some(std::cmp::max(arg.data_len, VARIABLE_READ_ERROR_PAYLOAD_LEN)) + } ComplexArgSource::MemDumpDynamic { .. } => None, + ComplexArgSource::IndirectBytes { .. } => None, + ComplexArgSource::IndirectSequence { .. } => None, + ComplexArgSource::IndirectRingSequence { .. } => None, + ComplexArgSource::IndirectHashTable { .. } => None, + ComplexArgSource::IndirectBTree { .. } => None, } } +fn indirect_capture_capacity( + reserved_len: usize, + max_len: usize, + shape: IndirectCaptureShape, +) -> usize { + reserved_len + .saturating_sub(shape.prefix_len()) + .checked_div(shape.reservation_factor()) + .unwrap_or(0) + .min(max_len) +} + fn plan_complex_format_layout( max_trace_event_size: usize, bytes_reserved_so_far: usize, @@ -64,6 +175,26 @@ fn plan_complex_format_layout( static_payload_total += payload_len; } else if let ComplexArgSource::MemDumpDynamic { max_len, .. } = &arg.source { dynamic_max_lens.push(*max_len); + } else if let ComplexArgSource::IndirectBytes { max_len, .. } = &arg.source { + dynamic_max_lens.push( + ghostscope_protocol::INDIRECT_BYTES_LENGTH_PREFIX_SIZE.saturating_add(*max_len), + ); + } else if let ComplexArgSource::IndirectSequence { max_len, .. } = &arg.source { + dynamic_max_lens + .push(ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE.saturating_add(*max_len)); + } else if let ComplexArgSource::IndirectRingSequence { max_len, .. } = &arg.source { + // The verifier cannot relate a second helper's destination offset to + // its length. An unused payload-sized tail gives both independent + // bounds enough map headroom; user space ignores the padding. + dynamic_max_lens.push( + ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE + .saturating_add(max_len.saturating_mul(2)), + ); + } else if let ComplexArgSource::IndirectHashTable { max_len, .. } = &arg.source { + dynamic_max_lens + .push(ghostscope_protocol::HASH_TABLE_HEADER_SIZE.saturating_add(*max_len)); + } else if let ComplexArgSource::IndirectBTree { max_len, .. } = &arg.source { + dynamic_max_lens.push(ghostscope_protocol::BTREE_HEADER_SIZE.saturating_add(*max_len)); } arg_payload_plans.push((header_len, static_payload_len)); @@ -1044,6 +1175,17 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { var_data_ptr: PointerValue<'ctx>, address: &RuntimeAddress<'ctx>, len: usize, + ) -> Result<()> { + self.emit_complex_format_memdump_at(status_ptr, var_data_ptr, var_data_ptr, address, len) + } + + fn emit_complex_format_memdump_at( + &mut self, + status_ptr: PointerValue<'ctx>, + payload_ptr: PointerValue<'ctx>, + dst_ptr: PointerValue<'ctx>, + address: &RuntimeAddress<'ctx>, + len: usize, ) -> Result<()> { // Branchy emitters must leave the builder at their continuation block so // the caller can append the next formatted argument. @@ -1053,7 +1195,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { let dst_ptr = self .builder - .build_pointer_cast(var_data_ptr, ptr_ty, "md_dst_ptr") + .build_pointer_cast(dst_ptr, ptr_ty, "md_dst_ptr") .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; let base_src_ptr = self .builder @@ -1147,12 +1289,12 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { .const_int(VariableStatus::ReadError as u64, false), ) .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - // SAFETY: var_data_ptr points at the read-error payload. + // SAFETY: payload_ptr points at the read-error payload. let errno_ptr_i8 = unsafe { self.builder .build_gep( self.context.i8_type(), - var_data_ptr, + payload_ptr, &[self .context .i32_type() @@ -1178,7 +1320,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { self.builder .build_gep( self.context.i8_type(), - var_data_ptr, + payload_ptr, &[self .context .i32_type() @@ -1457,189 +1599,3107 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { Ok(()) } - fn emit_complex_format_runtime_read( + fn emit_complex_format_read_error_payload( &mut self, - status_ptr: PointerValue<'ctx>, var_data_ptr: PointerValue<'ctx>, - address: &ghostscope_dwarf::PlannedAddress, - dwarf_type: &ghostscope_dwarf::TypeInfo, - module_for_offsets: Option<&str>, - data_len: usize, + reserved_len: usize, + helper_result: IntValue<'ctx>, + address: IntValue<'ctx>, ) -> Result<()> { - // Branchy emitters must leave the builder at their continuation block so - // the caller can append the next formatted argument. - let ptr_type = self.context.ptr_type(AddressSpace::default()); let i32_type = self.context.i32_type(); - let i64_type = self.context.i64_type(); - let dst_ptr = self - .builder - .build_bit_cast(var_data_ptr, ptr_type, "dst_ptr") - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - let size_val = i32_type.const_int(data_len as u64, false); - let src_addr = - self.resolve_planned_address(address, Some(status_ptr), module_for_offsets)?; - let offsets_found = src_addr.offsets_found; - let current_fn = self.current_function("compile complex variable read")?; - let cont2_block = self.context.append_basic_block(current_fn, "after_read"); - let skip_block = self.context.append_basic_block(current_fn, "offsets_skip"); - let found_block = self.context.append_basic_block(current_fn, "offsets_found"); - self.builder - .build_conditional_branch(offsets_found, found_block, skip_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - - self.builder.position_at_end(skip_block); - self.mark_any_fail()?; - self.builder - .build_unconditional_branch(cont2_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + let ptr_type = self.context.ptr_type(AddressSpace::default()); + let errno_end = VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET + std::mem::size_of::(); + if reserved_len >= errno_end { + // SAFETY: the reserved payload includes the errno field. + let errno_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type + .const_int(VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET as u64, false)], + "indirect_errno_ptr_i8", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let errno_ptr = self + .builder + .build_pointer_cast(errno_ptr_i8, ptr_type, "indirect_errno_ptr") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let errno = self.build_errno_i32(helper_result, "indirect_errno_i32")?; + self.builder + .build_store(errno_ptr, errno) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } - self.builder.position_at_end(found_block); - let src_ptr = self - .builder - .build_int_to_ptr(src_addr.value, ptr_type, "src_ptr") - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - let zero64 = i64_type.const_zero(); - let is_null = self - .builder - .build_int_compare(inkwell::IntPredicate::EQ, src_addr.value, zero64, "is_null") - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - let null_block = self.context.append_basic_block(current_fn, "null_deref"); - let read_block = self.context.append_basic_block(current_fn, "read_user"); - self.builder - .build_conditional_branch(is_null, null_block, read_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + if reserved_len >= VARIABLE_READ_ERROR_PAYLOAD_LEN { + // SAFETY: the reserved payload includes the address field. + let addr_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type + .const_int(VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET as u64, false)], + "indirect_addr_ptr_i8", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let addr_ptr = self + .builder + .build_pointer_cast(addr_ptr_i8, ptr_type, "indirect_addr_ptr") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(addr_ptr, address) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } - self.builder.position_at_end(null_block); - self.builder - .build_store( - status_ptr, - self.context - .i8_type() - .const_int(VariableStatus::NullDeref as u64, false), - ) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - self.mark_any_fail()?; - self.builder - .build_unconditional_branch(cont2_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + Ok(()) + } - self.builder.position_at_end(read_block); - let ret = self - .create_bpf_helper_call( - BPF_FUNC_probe_read_user as u64, - &[dst_ptr, size_val.into(), src_ptr.into()], - i32_type.into(), - "probe_read_user", - )? - .into_int_value(); - let is_err = self + fn select_indirect_metadata_failure( + &self, + read: &crate::ebpf::helper_functions::MemoryReadDiagnostics<'ctx>, + address: IntValue<'ctx>, + fallback_result: IntValue<'ctx>, + fallback_address: IntValue<'ctx>, + name: &str, + ) -> Result<(IntValue<'ctx>, IntValue<'ctx>)> { + let offsets_available = self .builder - .build_int_compare( - inkwell::IntPredicate::SLT, - ret, - i32_type.const_zero(), - "ret_lt_zero", - ) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - let err_block = self.context.append_basic_block(current_fn, "read_err"); - let ok_block = self.context.append_basic_block(current_fn, "read_ok"); - self.builder - .build_conditional_branch(is_err, err_block, ok_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - - self.builder.position_at_end(err_block); - self.builder - .build_store( - status_ptr, - self.context - .i8_type() - .const_int(VariableStatus::ReadError as u64, false), + .build_not(read.not_found, &format!("{name}_offsets_available")) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let helper_failed = self + .builder + .build_and( + read.combined_fail, + offsets_available, + &format!("{name}_helper_failed"), ) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - // SAFETY: var_data_ptr points at the read-error payload. - let errno_ptr_i8 = unsafe { - self.builder - .build_gep( - self.context.i8_type(), - var_data_ptr, - &[i32_type.const_int(VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET as u64, false)], - "errno_ptr_i8", - ) - .map_err(|e| CodeGenError::LLVMError(format!("Failed to get errno gep: {e}")))? - }; - let i32_ptr = self + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let result = self .builder - .build_pointer_cast( - errno_ptr_i8, - self.context.ptr_type(AddressSpace::default()), - "errno_ptr", + .build_select::, _>( + helper_failed, + read.helper_result.into(), + fallback_result.into(), + &format!("{name}_error_result"), ) - .map_err(|e| CodeGenError::LLVMError(format!("Failed to cast errno ptr: {e}")))?; - self.builder - .build_store(i32_ptr, ret) - .map_err(|e| CodeGenError::LLVMError(format!("Failed to store errno: {e}")))?; - // SAFETY: read-error payload reserves enough bytes for the addr field. - let addr_ptr_i8 = unsafe { - self.builder - .build_gep( - self.context.i8_type(), - var_data_ptr, - &[i32_type.const_int(VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET as u64, false)], - "addr_ptr_i8", - ) - .map_err(|e| CodeGenError::LLVMError(format!("Failed to get addr gep: {e}")))? - }; - let addr_ptr = self + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let address = self .builder - .build_pointer_cast( - addr_ptr_i8, - self.context.ptr_type(AddressSpace::default()), - "addr_ptr", + .build_select::, _>( + helper_failed, + address.into(), + fallback_address.into(), + &format!("{name}_error_address"), ) - .map_err(|e| CodeGenError::LLVMError(format!("Failed to cast addr ptr: {e}")))?; - self.builder - .build_store(addr_ptr, src_addr.value) - .map_err(|e| CodeGenError::LLVMError(format!("Failed to store addr: {e}")))?; - self.mark_any_fail()?; - self.builder - .build_unconditional_branch(cont2_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - - self.builder.position_at_end(ok_block); - if data_len < dwarf_type.size() as usize { - self.builder - .build_store( - status_ptr, - self.context - .i8_type() - .const_int(VariableStatus::Truncated as u64, false), - ) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - self.mark_any_success()?; - self.mark_any_fail()?; - } else { - self.mark_any_success()?; - } - self.builder - .build_unconditional_branch(cont2_block) - .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; - - self.builder.position_at_end(cont2_block); - Ok(()) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + Ok((result, address)) } - fn emit_complex_format_arg_source( + fn emit_complex_format_projected_view( &mut self, - arg: &ComplexArg<'ctx>, - arg_ptrs: ComplexFormatArgPointers<'ctx>, + status_ptr: PointerValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + descriptor: &RuntimeAddress<'ctx>, + fields: &[ProjectedViewFieldSource], reserved_len: usize, ) -> Result<()> { - let status_ptr = arg_ptrs.status_ptr; - let var_data_ptr = arg_ptrs.var_data_ptr; + let function = self.current_function("compile projected semantic view")?; + let finish_block = self + .context + .append_basic_block(function, "projected_view_finish"); - match &arg.source { - ComplexArgSource::ImmediateBytes { bytes, .. } => { - self.emit_complex_format_immediate_bytes(var_data_ptr, bytes) + if fields.is_empty() { + self.builder + .build_unconditional_branch(finish_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(finish_block); + return Ok(()); + } + + for (field_index, field) in fields.iter().enumerate() { + let mut address = *descriptor; + for (step_index, step) in field.steps.iter().enumerate() { + match step { + ProjectedViewStep::Member { offset } => { + if *offset != 0 { + let value = self + .builder + .build_int_add( + address.value, + self.context.i64_type().const_int(*offset, false), + &format!("projected_view_{field_index}_{step_index}_member"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + address = address.with_value(value); + } + } + ProjectedViewStep::Dereference { pointer_size } => { + let read = self.generate_memory_read_with_diagnostics( + address, + *pointer_size, + Some(status_ptr), + &format!("projected_view_{field_index}_{step_index}_pointer"), + )?; + let ok = self + .builder + .build_not( + read.combined_fail, + &format!("projected_view_{field_index}_{step_index}_ok"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let ok_block = self.context.append_basic_block( + function, + &format!("projected_view_{field_index}_{step_index}_pointer_ok"), + ); + let error_block = self.context.append_basic_block( + function, + &format!("projected_view_{field_index}_{step_index}_pointer_error"), + ); + self.builder + .build_conditional_branch(ok, ok_block, error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(error_block); + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + read.helper_result, + address.value, + )?; + self.builder + .build_unconditional_branch(finish_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(ok_block); + address = + RuntimeAddress::available(read.value.into_int_value(), self.context); + } + } + } + + if field.value_len > 0 { + // SAFETY: output_offset and value_len were validated against + // the statically reserved projected-view payload. + let field_ptr = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[self + .context + .i32_type() + .const_int(field.output_offset as u64, false)], + &format!("projected_view_{field_index}_output"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + match field.capture { + ghostscope_dwarf::ProjectedViewFieldCapture::Value => { + self.emit_complex_format_memdump_at( + status_ptr, + var_data_ptr, + field_ptr, + &address, + field.value_len, + )?; + } + ghostscope_dwarf::ProjectedViewFieldCapture::Address => { + self.emit_complex_format_computed_int( + field_ptr, + address.value, + field.value_len, + )?; + } + } + } + + if field_index + 1 == fields.len() { + self.builder + .build_unconditional_branch(finish_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + continue; + } + if field.value_len == 0 { + continue; + } + + let status = self + .builder + .build_load( + self.context.i8_type(), + status_ptr, + &format!("projected_view_{field_index}_status"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + status, + self.context.i8_type().const_zero(), + &format!("projected_view_{field_index}_read_ok"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let next_block = self.context.append_basic_block( + function, + &format!("projected_view_{}_next", field_index + 1), + ); + self.builder + .build_conditional_branch(ok, next_block, finish_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(next_block); + } + + self.builder.position_at_end(finish_block); + Ok(()) + } + + fn clamp_probe_read_length( + &mut self, + length: IntValue<'ctx>, + max_len: usize, + name: &str, + ) -> Result> { + let max_len = u32::try_from(max_len).map_err(|_| { + CodeGenError::DwarfError( + "dynamic capture exceeds the eBPF helper length width".to_string(), + ) + })?; + let i32_type = self.context.i32_type(); + if length.get_type() != i32_type { + return Err(CodeGenError::LLVMError(format!( + "probe read length must be i32, got {} bits", + length.get_type().get_bit_width() + ))); + } + + if max_len == u32::MAX { + return Ok(length); + } + if max_len == 0 { + return Ok(i32_type.const_zero()); + } + + // The eBPF verifier does not reliably preserve an i64 upper bound + // through an ALU32 truncation. Repeat the semantic clamp in i32 first. + let limit = i32_type.const_int(max_len as u64, false); + let exceeds_limit = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + length, + limit, + &format!("{name}_exceeds_limit"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let semantic_bound = self + .builder + .build_select( + exceeds_limit, + limit, + length, + &format!("{name}_semantic_bound"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + + // LLVM can lower min(length, limit) by comparing a temporary register + // and then copying the original, unbounded register into the helper + // argument. Older kernel verifiers do not propagate the temporary's + // range to that sibling copy. Hide the semantic range from generic + // optimization, then establish it again with verifier-visible ALU32 + // operations. The mask is an identity for every value at or below the + // semantic limit; the second clamp narrows a non-all-ones mask exactly. + let passthrough_type = i32_type.fn_type(&[i32_type.into(), i32_type.into()], false); + let passthrough_name = "llvm.bpf.passthrough.i32.i32"; + let passthrough = self + .module + .get_function(passthrough_name) + .unwrap_or_else(|| { + self.module + .add_function(passthrough_name, passthrough_type, None) + }); + let sequence = self.next_bpf_passthrough_sequence; + self.next_bpf_passthrough_sequence = sequence.checked_add(1).ok_or_else(|| { + CodeGenError::LLVMError("BPF passthrough sequence exhausted".to_string()) + })?; + let opaque_bound = self + .builder + .build_call( + passthrough, + &[ + i32_type.const_int(sequence as u64, false).into(), + semantic_bound.into(), + ], + &format!("{name}_opaque_bound"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .try_as_basic_value() + .left() + .ok_or_else(|| CodeGenError::LLVMError("BPF passthrough returned void".to_string()))? + .into_int_value(); + let verifier_mask = u32::MAX >> max_len.leading_zeros(); + let masked_bound = self + .builder + .build_and( + opaque_bound, + i32_type.const_int(verifier_mask as u64, false), + &format!("{name}_verifier_masked"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + if verifier_mask == max_len { + return Ok(masked_bound); + } + + let masked_exceeds_limit = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + masked_bound, + limit, + &format!("{name}_masked_exceeds_limit"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_select( + masked_exceeds_limit, + limit, + masked_bound, + &format!("{name}_bounded"), + ) + .map(|value| value.into_int_value()) + .map_err(|error| CodeGenError::LLVMError(error.to_string())) + } + + fn emit_complex_format_indirect( + &mut self, + status_ptr: PointerValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + descriptor: &RuntimeAddress<'ctx>, + reserved_len: usize, + capture: IndirectCaptureConfig, + ) -> Result<()> { + let prefix_len = capture.shape.prefix_len(); + if reserved_len < prefix_len { + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + return Ok(()); + } + + let i64_type = self.context.i64_type(); + let i32_type = self.context.i32_type(); + let ptr_type = self.context.ptr_type(AddressSpace::default()); + let ring_config = match capture.shape { + IndirectCaptureShape::Sequence { ring, .. } => ring, + IndirectCaptureShape::Bytes => None, + }; + let data_member = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(capture.data_offset, false), + "indirect_data_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length_member = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(capture.length_offset, false), + "indirect_length_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let data_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(data_member), + capture.data_access_size, + Some(status_ptr), + "indirect_data_metadata", + )?; + let data_address = data_read.value.into_int_value(); + let length_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(length_member), + capture.length_access_size, + Some(status_ptr), + "indirect_length_metadata", + )?; + let length_value = length_read.value.into_int_value(); + let ring_reads = if let Some(ring) = ring_config { + let start_member = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(ring.start_offset, false), + "indirect_ring_start_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let start_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(start_member), + ring.start_access_size, + Some(status_ptr), + "indirect_ring_start_metadata", + )?; + let capacity_member = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(ring.capacity_offset, false), + "indirect_ring_capacity_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let capacity_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(capacity_member), + ring.capacity_access_size, + Some(status_ptr), + "indirect_ring_capacity_metadata", + )?; + Some(( + ring, + start_member, + start_read, + capacity_member, + capacity_read, + )) + } else { + None + }; + + let mut original_len = length_value; + let mut ring_start = None; + let mut ring_capacity = None; + let mut ring_metadata_valid = None; + if let Some((ring, _, start_read, _, capacity_read)) = &ring_reads { + let start = start_read.value.into_int_value(); + let capacity = capacity_read.value.into_int_value(); + if matches!(ring.length_kind, RingCaptureLengthKind::End) { + let direct_distance = self + .builder + .build_int_sub(length_value, start, "indirect_ring_direct_distance") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let wrapped_prefix = self + .builder + .build_int_sub(capacity, start, "indirect_ring_wrapped_prefix") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let wrapped_distance = self + .builder + .build_int_add( + wrapped_prefix, + length_value, + "indirect_ring_wrapped_distance", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let no_wrap = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGE, + length_value, + start, + "indirect_ring_no_wrap", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + original_len = self + .builder + .build_select( + no_wrap, + direct_distance, + wrapped_distance, + "indirect_ring_distance", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + } + + let capacity_nonzero = self + .builder + .build_int_compare( + inkwell::IntPredicate::NE, + capacity, + i64_type.const_zero(), + "indirect_ring_capacity_nonzero", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let start_in_bounds = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULT, + start, + capacity, + "indirect_ring_start_in_bounds", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length_valid = match ring.length_kind { + RingCaptureLengthKind::Explicit => self + .builder + .build_int_compare( + inkwell::IntPredicate::ULE, + original_len, + capacity, + "indirect_ring_length_in_bounds", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?, + RingCaptureLengthKind::End => self + .builder + .build_int_compare( + inkwell::IntPredicate::ULT, + length_value, + capacity, + "indirect_ring_end_in_bounds", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?, + }; + let indices_valid = self + .builder + .build_and( + capacity_nonzero, + start_in_bounds, + "indirect_ring_indices_valid", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + ring_metadata_valid = Some( + self.builder + .build_and(indices_valid, length_valid, "indirect_ring_metadata_valid") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?, + ); + ring_start = Some(start); + ring_capacity = Some(capacity); + } + + let current_status = self + .builder + .build_load( + self.context.i8_type(), + status_ptr, + "indirect_metadata_status", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let metadata_ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + current_status, + self.context.i8_type().const_zero(), + "indirect_metadata_ok", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let function = self.current_function("compile indirect value capture")?; + let metadata_ok_block = self + .context + .append_basic_block(function, "indirect_metadata_ok"); + let metadata_error_block = self + .context + .append_basic_block(function, "indirect_metadata_error"); + let continue_block = self + .context + .append_basic_block(function, "indirect_continue"); + self.builder + .build_conditional_branch(metadata_ok, metadata_ok_block, metadata_error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(metadata_error_block); + let metadata_read_error = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + current_status, + self.context + .i8_type() + .const_int(VariableStatus::ReadError as u64, false), + "indirect_metadata_read_error", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let metadata_payload_block = self + .context + .append_basic_block(function, "indirect_metadata_error_payload"); + self.builder + .build_conditional_branch(metadata_read_error, metadata_payload_block, continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(metadata_payload_block); + let (mut metadata_helper_result, mut metadata_error_address) = ring_reads + .as_ref() + .map(|(_, _, _, capacity_member, capacity_read)| { + (capacity_read.helper_result, *capacity_member) + }) + .unwrap_or((length_read.helper_result, length_member)); + if let Some((_, start_member, start_read, _, _)) = &ring_reads { + (metadata_helper_result, metadata_error_address) = self + .select_indirect_metadata_failure( + start_read, + *start_member, + metadata_helper_result, + metadata_error_address, + "indirect_ring_start", + )?; + } + (metadata_helper_result, metadata_error_address) = self.select_indirect_metadata_failure( + &length_read, + length_member, + metadata_helper_result, + metadata_error_address, + "indirect_length", + )?; + (metadata_helper_result, metadata_error_address) = self.select_indirect_metadata_failure( + &data_read, + data_member, + metadata_helper_result, + metadata_error_address, + "indirect_data", + )?; + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + metadata_helper_result, + metadata_error_address, + )?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(metadata_ok_block); + let length_prefix_ptr = self + .builder + .build_pointer_cast(var_data_ptr, ptr_type, "indirect_length_prefix") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(length_prefix_ptr, original_len) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + if matches!(capture.shape, IndirectCaptureShape::Sequence { .. }) { + // SAFETY: sequence payloads reserve the complete two-u64 header. + let captured_count_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type.const_int( + ghostscope_protocol::INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET as u64, + false, + )], + "indirect_captured_count_ptr_i8", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let captured_count_ptr = self + .builder + .build_pointer_cast( + captured_count_ptr_i8, + ptr_type, + "indirect_captured_count_ptr", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(captured_count_ptr, i64_type.const_zero()) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } + let is_empty = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + original_len, + i64_type.const_zero(), + "indirect_is_empty", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let empty_block = self.context.append_basic_block(function, "indirect_empty"); + let nonempty_block = self + .context + .append_basic_block(function, "indirect_nonempty"); + self.builder + .build_conditional_branch(is_empty, empty_block, nonempty_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(empty_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::ZeroLength as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(nonempty_block); + if let Some(metadata_valid) = ring_metadata_valid { + let ring_valid_block = self + .context + .append_basic_block(function, "indirect_ring_valid"); + let ring_invalid_block = self + .context + .append_basic_block(function, "indirect_ring_invalid"); + self.builder + .build_conditional_branch(metadata_valid, ring_valid_block, ring_invalid_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(ring_invalid_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::AccessError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(ring_valid_block); + } + let capture_capacity = + indirect_capture_capacity(reserved_len, capture.max_len, capture.shape); + let (unit_size, max_units) = match capture.shape { + IndirectCaptureShape::Bytes => (1usize, capture_capacity), + IndirectCaptureShape::Sequence { + element_stride, + max_elements, + .. + } => { + let stride = usize::try_from(element_stride).map_err(|_| { + CodeGenError::DwarfError(format!( + "sequence element DWARF size {element_stride} does not fit this host" + )) + })?; + let payload_elements = if stride == 0 { + max_elements + } else { + capture_capacity / stride + }; + (stride, max_elements.min(payload_elements)) + } + }; + let capture_limit = i64_type.const_int(max_units as u64, false); + let is_truncated = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + original_len, + capture_limit, + "indirect_is_truncated", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let captured_units = self + .builder + .build_select( + is_truncated, + capture_limit, + original_len, + "indirect_captured_units", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + + if matches!(capture.shape, IndirectCaptureShape::Sequence { .. }) { + // SAFETY: sequence payloads reserve the complete two-u64 header. + let captured_count_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type.const_int( + ghostscope_protocol::INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET as u64, + false, + )], + "indirect_captured_count_ptr_i8_nonempty", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let captured_count_ptr = self + .builder + .build_pointer_cast( + captured_count_ptr_i8, + ptr_type, + "indirect_captured_count_ptr_nonempty", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(captured_count_ptr, captured_units) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } + + if max_units == 0 || unit_size == 0 { + let truncated_block = self + .context + .append_basic_block(function, "indirect_no_read_truncated"); + let complete_block = self + .context + .append_basic_block(function, "indirect_no_read_complete"); + self.builder + .build_conditional_branch(is_truncated, truncated_block, complete_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(truncated_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(complete_block); + self.mark_any_success()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(continue_block); + return Ok(()); + } + + let is_null = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + data_address, + i64_type.const_zero(), + "indirect_data_is_null", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let null_block = self.context.append_basic_block(function, "indirect_null"); + let read_block = self.context.append_basic_block(function, "indirect_read"); + self.builder + .build_conditional_branch(is_null, null_block, read_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(null_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::NullDeref as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(read_block); + // SAFETY: reserved_len includes the fixed length prefix. + let byte_payload_ptr = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type.const_int(prefix_len as u64, false)], + "indirect_byte_payload", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let destination = self + .builder + .build_pointer_cast(byte_payload_ptr, ptr_type, "indirect_destination") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let read_outcome; + if let (Some(start), Some(capacity)) = (ring_start, ring_capacity) { + let available_before_wrap = self + .builder + .build_int_sub(capacity, start, "indirect_ring_available_before_wrap") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let wraps = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + captured_units, + available_before_wrap, + "indirect_ring_wraps", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_units = self + .builder + .build_select( + wraps, + available_before_wrap, + captured_units, + "indirect_ring_first_units", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let stride = i64_type.const_int(unit_size as u64, false); + let start_offset = self + .builder + .build_int_mul(start, stride, "indirect_ring_start_offset") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_address = self + .builder + .build_int_add(data_address, start_offset, "indirect_ring_first_address") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_len = self + .builder + .build_int_mul(first_units, stride, "indirect_ring_first_len") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_len_i32 = self + .builder + .build_int_truncate(first_len, i32_type, "indirect_ring_first_len_i32") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_len_i32 = self.clamp_probe_read_length( + first_len_i32, + capture_capacity, + "indirect_ring_first_len", + )?; + let first_payload_len = self + .builder + .build_int_z_extend(first_len_i32, i64_type, "indirect_ring_first_payload_len") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_source = self + .builder + .build_int_to_ptr(first_address, ptr_type, "indirect_ring_first_source") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + // SAFETY: first_payload_len is the verifier-bounded helper length, + // so it cannot exceed the reserved sequence payload. + let second_payload_ptr = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + byte_payload_ptr, + &[first_payload_len], + "indirect_ring_second_payload", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let second_destination = self + .builder + .build_pointer_cast( + second_payload_ptr, + ptr_type, + "indirect_ring_second_destination", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let first_result = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[ + destination.into(), + first_len_i32.into(), + first_source.into(), + ], + i64_type.into(), + "probe_read_user_indirect_ring_first", + )? + .into_int_value(); + + let second_read_block = self + .context + .append_basic_block(function, "indirect_ring_second_read"); + let no_second_read_block = self + .context + .append_basic_block(function, "indirect_ring_no_second_read"); + let ring_read_complete_block = self + .context + .append_basic_block(function, "indirect_ring_read_complete"); + self.builder + .build_conditional_branch(wraps, second_read_block, no_second_read_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(no_second_read_block); + self.builder + .build_unconditional_branch(ring_read_complete_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(second_read_block); + let unbounded_second_units = self + .builder + .build_int_sub( + captured_units, + available_before_wrap, + "indirect_ring_unbounded_second_units", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let second_units_exceed_limit = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + unbounded_second_units, + capture_limit, + "indirect_ring_second_units_exceed_limit", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let second_units = self + .builder + .build_select( + second_units_exceed_limit, + capture_limit, + unbounded_second_units, + "indirect_ring_second_units", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let second_len = self + .builder + .build_int_mul(second_units, stride, "indirect_ring_second_len") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let second_len_limit = i64_type.const_int(capture_capacity as u64, false); + let second_len_exceeds_limit = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + second_len, + second_len_limit, + "indirect_ring_second_len_exceeds_limit", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let bounded_second_len = self + .builder + .build_select( + second_len_exceeds_limit, + second_len_limit, + second_len, + "indirect_ring_bounded_second_len", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let second_len_i32 = self + .builder + .build_int_truncate(bounded_second_len, i32_type, "indirect_ring_second_len_i32") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let second_len_i32 = self.clamp_probe_read_length( + second_len_i32, + capture_capacity, + "indirect_ring_second_len_i32", + )?; + let second_source = self + .builder + .build_int_to_ptr(data_address, ptr_type, "indirect_ring_second_source") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let second_result = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[ + second_destination.into(), + second_len_i32.into(), + second_source.into(), + ], + i64_type.into(), + "probe_read_user_indirect_ring_second", + )? + .into_int_value(); + self.builder + .build_unconditional_branch(ring_read_complete_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(ring_read_complete_block); + let second_result_phi = self + .builder + .build_phi(i64_type, "indirect_ring_second_result") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + second_result_phi.add_incoming(&[ + (&i64_type.const_zero(), no_second_read_block), + (&second_result, second_read_block), + ]); + let second_result = second_result_phi.as_basic_value().into_int_value(); + let first_failed = self + .builder + .build_int_compare( + inkwell::IntPredicate::NE, + first_result, + i64_type.const_zero(), + "indirect_ring_first_failed", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let read_result = self + .builder + .build_select( + first_failed, + first_result, + second_result, + "indirect_ring_read_result", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let read_error_address = self + .builder + .build_select( + first_failed, + first_address, + data_address, + "indirect_ring_read_error_address", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let read_ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + read_result, + i64_type.const_zero(), + "indirect_ring_read_ok", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + read_outcome = (read_result, read_error_address, read_ok); + } else { + let read_len = self + .builder + .build_int_mul( + captured_units, + i64_type.const_int(unit_size as u64, false), + "indirect_read_len_bytes", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let read_len = self + .builder + .build_int_truncate(read_len, i32_type, "indirect_read_len_i32") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let read_len = + self.clamp_probe_read_length(read_len, capture_capacity, "indirect_read_len")?; + let source = self + .builder + .build_int_to_ptr(data_address, ptr_type, "indirect_source") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let read_result = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[destination.into(), read_len.into(), source.into()], + i64_type.into(), + "probe_read_user_indirect", + )? + .into_int_value(); + let read_ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + read_result, + i64_type.const_zero(), + "indirect_read_ok", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + read_outcome = (read_result, data_address, read_ok); + } + let (read_result, read_error_address, read_ok) = read_outcome; + let read_ok_block = self + .context + .append_basic_block(function, "indirect_read_ok"); + let read_error_block = self + .context + .append_basic_block(function, "indirect_read_error"); + self.builder + .build_conditional_branch(read_ok, read_ok_block, read_error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(read_error_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::ReadError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + read_result, + read_error_address, + )?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(read_ok_block); + let truncated_block = self + .context + .append_basic_block(function, "indirect_truncated"); + let complete_block = self + .context + .append_basic_block(function, "indirect_complete"); + self.builder + .build_conditional_branch(is_truncated, truncated_block, complete_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(truncated_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(complete_block); + self.mark_any_success()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(continue_block); + Ok(()) + } + + fn store_complex_payload_u64( + &self, + data_ptr: PointerValue<'ctx>, + offset: usize, + value: IntValue<'ctx>, + name: &str, + ) -> Result<()> { + // SAFETY: callers validate that the fixed header field is reserved. + let field_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + data_ptr, + &[self.context.i32_type().const_int(offset as u64, false)], + &format!("{name}_ptr_i8"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let field_ptr = self + .builder + .build_pointer_cast( + field_ptr_i8, + self.context.ptr_type(AddressSpace::default()), + &format!("{name}_ptr"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(field_ptr, value) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + Ok(()) + } + + fn emit_complex_format_hash_table( + &mut self, + status_ptr: PointerValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + descriptor: &RuntimeAddress<'ctx>, + reserved_len: usize, + capture: HashTableCaptureConfig, + ) -> Result<()> { + let header_len = ghostscope_protocol::HASH_TABLE_HEADER_SIZE; + if reserved_len < header_len { + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + return Ok(()); + } + + let stride = usize::try_from(capture.entry_stride).map_err(|_| { + CodeGenError::DwarfError(format!( + "hash-table entry DWARF size {} does not fit this host", + capture.entry_stride + )) + })?; + let occupancy_width = capture + .occupancy + .byte_width() + .and_then(|width| usize::try_from(width).ok()) + .ok_or_else(|| { + CodeGenError::DwarfError("invalid hash-table occupancy width".to_string()) + })?; + let layout_matches = matches!( + (capture.bucket_order, capture.occupancy, capture.buckets), + ( + ghostscope_dwarf::HashTableBucketOrder::Forward, + ghostscope_dwarf::HashTableOccupancy::ControlByteHighBitClear, + HashTableBucketSource::Forward { .. } + ) | ( + ghostscope_dwarf::HashTableBucketOrder::Reverse, + ghostscope_dwarf::HashTableOccupancy::ControlByteHighBitClear, + HashTableBucketSource::ReverseFromControl + ) | ( + ghostscope_dwarf::HashTableBucketOrder::Forward, + ghostscope_dwarf::HashTableOccupancy::NonZeroWord { .. }, + HashTableBucketSource::LegacyAfterControl { .. } + ) + ); + if !layout_matches { + return Err(CodeGenError::DwarfError( + "hash-table occupancy and bucket source do not match".to_string(), + )); + } + if let HashTableBucketSource::LegacyAfterControl { + entry_alignment, + pointer_tag_mask, + } = capture.buckets + { + let valid_alignment = entry_alignment.is_power_of_two() + && (capture.entry_stride == 0 || entry_alignment <= capture.entry_stride); + let valid_tag = occupancy_width.is_power_of_two() + && pointer_tag_mask & !(occupancy_width as u64 - 1) == 0; + if !valid_alignment + || !valid_tag + || occupancy_width != capture.control_access_size.bytes() + { + return Err(CodeGenError::DwarfError( + "invalid legacy hash-table storage layout".to_string(), + )); + } + } + let bytes_per_bucket = stride.checked_add(occupancy_width).ok_or_else(|| { + CodeGenError::DwarfError("hash-table bucket capture size overflow".to_string()) + })?; + let reservation_buckets = reserved_len + .saturating_sub(header_len) + .checked_div(bytes_per_bucket) + .unwrap_or(0); + let max_buckets = capture.max_buckets.min(reservation_buckets); + let max_control_bytes = max_buckets.checked_mul(occupancy_width).ok_or_else(|| { + CodeGenError::DwarfError("hash-table control payload overflow".to_string()) + })?; + let bucket_payload_offset = header_len.checked_add(max_control_bytes).ok_or_else(|| { + CodeGenError::DwarfError("hash-table bucket offset overflow".to_string()) + })?; + let max_bucket_bytes = max_buckets.checked_mul(stride).ok_or_else(|| { + CodeGenError::DwarfError("hash-table bucket payload overflow".to_string()) + })?; + if max_control_bytes > u32::MAX as usize || max_bucket_bytes > u32::MAX as usize { + return Err(CodeGenError::DwarfError( + "hash-table capture exceeds the eBPF helper length width".to_string(), + )); + } + + let i8_type = self.context.i8_type(); + let i32_type = self.context.i32_type(); + let i64_type = self.context.i64_type(); + let ptr_type = self.context.ptr_type(AddressSpace::default()); + let member_address = |offset: u64, name: &str| { + self.builder + .build_int_add(descriptor.value, i64_type.const_int(offset, false), name) + .map_err(|error| CodeGenError::LLVMError(error.to_string())) + }; + let control_member = member_address(capture.control_offset, "hash_table_control_member")?; + let length_member = member_address(capture.length_offset, "hash_table_length_member")?; + let bucket_mask_member = + member_address(capture.bucket_mask_offset, "hash_table_bucket_mask_member")?; + let data_member = match capture.buckets { + HashTableBucketSource::Forward { + data_offset, + data_access_size, + } => Some(( + member_address(data_offset, "hash_table_data_member")?, + data_access_size, + )), + HashTableBucketSource::ReverseFromControl + | HashTableBucketSource::LegacyAfterControl { .. } => None, + }; + let control_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(control_member), + capture.control_access_size, + Some(status_ptr), + "hash_table_control_metadata", + )?; + let length_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(length_member), + capture.length_access_size, + Some(status_ptr), + "hash_table_length_metadata", + )?; + let bucket_mask_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(bucket_mask_member), + capture.bucket_mask_access_size, + Some(status_ptr), + "hash_table_bucket_mask_metadata", + )?; + let data_read = if let Some((address, access_size)) = data_member { + let read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(address), + access_size, + Some(status_ptr), + "hash_table_data_metadata", + )?; + Some((address, read)) + } else { + None + }; + + let current_status = self + .builder + .build_load(i8_type, status_ptr, "hash_table_metadata_status") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let metadata_ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + current_status, + i8_type.const_zero(), + "hash_table_metadata_ok", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let function = self.current_function("compile hash-table value capture")?; + let metadata_ok_block = self + .context + .append_basic_block(function, "hash_table_metadata_ok"); + let metadata_error_block = self + .context + .append_basic_block(function, "hash_table_metadata_error"); + let continue_block = self + .context + .append_basic_block(function, "hash_table_continue"); + self.builder + .build_conditional_branch(metadata_ok, metadata_ok_block, metadata_error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(metadata_error_block); + let metadata_read_error = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + current_status, + i8_type.const_int(VariableStatus::ReadError as u64, false), + "hash_table_metadata_read_error", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let metadata_payload_block = self + .context + .append_basic_block(function, "hash_table_metadata_error_payload"); + self.builder + .build_conditional_branch(metadata_read_error, metadata_payload_block, continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(metadata_payload_block); + let (mut helper_result, mut error_address) = + (bucket_mask_read.helper_result, bucket_mask_member); + if let Some((data_member, data_read)) = &data_read { + (helper_result, error_address) = self.select_indirect_metadata_failure( + data_read, + *data_member, + helper_result, + error_address, + "hash_table_data", + )?; + } + (helper_result, error_address) = self.select_indirect_metadata_failure( + &bucket_mask_read, + bucket_mask_member, + helper_result, + error_address, + "hash_table_bucket_mask", + )?; + (helper_result, error_address) = self.select_indirect_metadata_failure( + &length_read, + length_member, + helper_result, + error_address, + "hash_table_length", + )?; + (helper_result, error_address) = self.select_indirect_metadata_failure( + &control_read, + control_member, + helper_result, + error_address, + "hash_table_control", + )?; + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + helper_result, + error_address, + )?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(metadata_ok_block); + let raw_control_address = control_read.value.into_int_value(); + let control_address = match capture.buckets { + HashTableBucketSource::LegacyAfterControl { + pointer_tag_mask, .. + } => self + .builder + .build_and( + raw_control_address, + i64_type.const_int(!pointer_tag_mask, false), + "hash_table_legacy_control_address", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?, + HashTableBucketSource::Forward { .. } | HashTableBucketSource::ReverseFromControl => { + raw_control_address + } + }; + let original_count = length_read.value.into_int_value(); + let bucket_mask = bucket_mask_read.value.into_int_value(); + let capacity = self + .builder + .build_int_add( + bucket_mask, + i64_type.const_int(1, false), + "hash_table_capacity", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let capacity_nonzero = self + .builder + .build_int_compare( + inkwell::IntPredicate::NE, + capacity, + i64_type.const_zero(), + "hash_table_capacity_nonzero", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let count_zero = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + original_count, + i64_type.const_zero(), + "hash_table_count_zero", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let capacity_valid = self + .builder + .build_or(capacity_nonzero, count_zero, "hash_table_capacity_valid") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length_valid = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULE, + original_count, + capacity, + "hash_table_length_valid", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let mut metadata_valid = self + .builder + .build_and(capacity_valid, length_valid, "hash_table_metadata_valid") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + if let HashTableBucketSource::LegacyAfterControl { + entry_alignment, .. + } = capture.buckets + { + let alignment_padding = entry_alignment - 1; + let max_capacity = (u64::MAX - alignment_padding) / occupancy_width as u64; + let capacity_fits = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULE, + capacity, + i64_type.const_int(max_capacity, false), + "hash_table_legacy_capacity_fits", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + metadata_valid = self + .builder + .build_and( + metadata_valid, + capacity_fits, + "hash_table_legacy_metadata_valid", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } + let valid_block = self + .context + .append_basic_block(function, "hash_table_metadata_valid"); + let invalid_block = self + .context + .append_basic_block(function, "hash_table_metadata_invalid"); + self.builder + .build_conditional_branch(metadata_valid, valid_block, invalid_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(invalid_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::AccessError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(valid_block); + self.store_complex_payload_u64(var_data_ptr, 0, original_count, "hash_table_item_count")?; + self.store_complex_payload_u64( + var_data_ptr, + ghostscope_protocol::HASH_TABLE_CAPACITY_OFFSET, + capacity, + "hash_table_capacity_header", + )?; + self.store_complex_payload_u64( + var_data_ptr, + ghostscope_protocol::HASH_TABLE_CAPTURED_BUCKETS_OFFSET, + i64_type.const_zero(), + "hash_table_captured_buckets_empty", + )?; + self.store_complex_payload_u64( + var_data_ptr, + ghostscope_protocol::HASH_TABLE_BUCKET_DATA_OFFSET, + i64_type.const_int(bucket_payload_offset as u64, false), + "hash_table_bucket_payload_offset", + )?; + let is_empty = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + original_count, + i64_type.const_zero(), + "hash_table_is_empty", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let empty_block = self + .context + .append_basic_block(function, "hash_table_empty"); + let nonempty_block = self + .context + .append_basic_block(function, "hash_table_nonempty"); + self.builder + .build_conditional_branch(is_empty, empty_block, nonempty_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(empty_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::ZeroLength as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(nonempty_block); + if max_buckets == 0 { + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(continue_block); + return Ok(()); + } + + let capture_limit = i64_type.const_int(max_buckets as u64, false); + let capacity_exceeds_limit = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + capacity, + capture_limit, + "hash_table_capacity_exceeds_limit", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let captured_buckets = self + .builder + .build_select( + capacity_exceeds_limit, + capture_limit, + capacity, + "hash_table_captured_buckets", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + self.store_complex_payload_u64( + var_data_ptr, + ghostscope_protocol::HASH_TABLE_CAPTURED_BUCKETS_OFFSET, + captured_buckets, + "hash_table_captured_buckets_header", + )?; + + let data_address = data_read + .as_ref() + .map(|(_, read)| read.value.into_int_value()); + let control_null = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + control_address, + i64_type.const_zero(), + "hash_table_control_null", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let pointer_null = if stride > 0 { + if let Some(data_address) = data_address { + let data_null = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + data_address, + i64_type.const_zero(), + "hash_table_data_null", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_or(control_null, data_null, "hash_table_pointer_null") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + } else { + control_null + } + } else { + control_null + }; + let null_block = self + .context + .append_basic_block(function, "hash_table_null_pointer"); + let control_read_block = self + .context + .append_basic_block(function, "hash_table_read_controls"); + self.builder + .build_conditional_branch(pointer_null, null_block, control_read_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(null_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::NullDeref as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(control_read_block); + // SAFETY: the hash-table header and maximum occupancy bytes are + // included in the reservation validated above. + let control_destination_i8 = unsafe { + self.builder + .build_gep( + i8_type, + var_data_ptr, + &[i32_type.const_int(header_len as u64, false)], + "hash_table_control_destination_i8", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let control_destination = self + .builder + .build_pointer_cast( + control_destination_i8, + ptr_type, + "hash_table_control_destination", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let control_length_i64 = self + .builder + .build_int_mul( + captured_buckets, + i64_type.const_int(occupancy_width as u64, false), + "hash_table_control_length_i64", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let control_length = self + .builder + .build_int_truncate(control_length_i64, i32_type, "hash_table_control_length") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let control_length = self.clamp_probe_read_length( + control_length, + max_control_bytes, + "hash_table_control_length", + )?; + let control_source = self + .builder + .build_int_to_ptr(control_address, ptr_type, "hash_table_control_source") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let control_result = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[ + control_destination.into(), + control_length.into(), + control_source.into(), + ], + i64_type.into(), + "probe_read_user_hash_table_controls", + )? + .into_int_value(); + let control_ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + control_result, + i64_type.const_zero(), + "hash_table_control_read_ok", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let control_ok_block = self + .context + .append_basic_block(function, "hash_table_control_read_ok"); + let control_error_block = self + .context + .append_basic_block(function, "hash_table_control_read_error"); + self.builder + .build_conditional_branch(control_ok, control_ok_block, control_error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(control_error_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::ReadError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + control_result, + control_address, + )?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(control_ok_block); + let finish_read_block = if stride == 0 { + control_ok_block + } else { + // SAFETY: bucket_payload_offset and max_bucket_bytes were derived + // from the same reservation, so this fixed destination is in bounds. + let bucket_destination_i8 = unsafe { + self.builder + .build_gep( + i8_type, + var_data_ptr, + &[i32_type.const_int(bucket_payload_offset as u64, false)], + "hash_table_bucket_destination_i8", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + let bucket_destination = self + .builder + .build_pointer_cast( + bucket_destination_i8, + ptr_type, + "hash_table_bucket_destination", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let bucket_length_i64 = self + .builder + .build_int_mul( + captured_buckets, + i64_type.const_int(stride as u64, false), + "hash_table_bucket_length_i64", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let bucket_length = self + .builder + .build_int_truncate(bucket_length_i64, i32_type, "hash_table_bucket_length") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let bucket_length = self.clamp_probe_read_length( + bucket_length, + max_bucket_bytes, + "hash_table_bucket_length", + )?; + let bucket_source_address = match capture.buckets { + HashTableBucketSource::Forward { .. } => data_address.ok_or_else(|| { + CodeGenError::DwarfError( + "forward hash-table capture is missing a data pointer".to_string(), + ) + })?, + HashTableBucketSource::ReverseFromControl => self + .builder + .build_int_sub( + control_address, + bucket_length_i64, + "hash_table_reverse_bucket_source", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?, + HashTableBucketSource::LegacyAfterControl { + entry_alignment, .. + } => { + let hash_words_len = self + .builder + .build_int_mul( + capacity, + i64_type.const_int(occupancy_width as u64, false), + "hash_table_legacy_hash_words_length", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let padded_hash_words_len = self + .builder + .build_int_add( + hash_words_len, + i64_type.const_int(entry_alignment - 1, false), + "hash_table_legacy_hash_words_padded_length", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let aligned_hash_words_len = self + .builder + .build_and( + padded_hash_words_len, + i64_type.const_int(!(entry_alignment - 1), false), + "hash_table_legacy_hash_words_aligned_length", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_int_add( + control_address, + aligned_hash_words_len, + "hash_table_legacy_bucket_source", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + } + }; + let bucket_source = self + .builder + .build_int_to_ptr(bucket_source_address, ptr_type, "hash_table_bucket_source") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let bucket_result = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[ + bucket_destination.into(), + bucket_length.into(), + bucket_source.into(), + ], + i64_type.into(), + "probe_read_user_hash_table_buckets", + )? + .into_int_value(); + let bucket_ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + bucket_result, + i64_type.const_zero(), + "hash_table_bucket_read_ok", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let bucket_ok_block = self + .context + .append_basic_block(function, "hash_table_bucket_read_ok"); + let bucket_error_block = self + .context + .append_basic_block(function, "hash_table_bucket_read_error"); + self.builder + .build_conditional_branch(bucket_ok, bucket_ok_block, bucket_error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(bucket_error_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::ReadError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + bucket_result, + bucket_source_address, + )?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + bucket_ok_block + }; + + self.builder.position_at_end(finish_read_block); + let truncated_block = self + .context + .append_basic_block(function, "hash_table_truncated"); + let complete_block = self + .context + .append_basic_block(function, "hash_table_complete"); + self.builder + .build_conditional_branch(capacity_exceeds_limit, truncated_block, complete_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(truncated_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(complete_block); + self.mark_any_success()?; + self.builder + .build_unconditional_branch(continue_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(continue_block); + Ok(()) + } + + fn btree_payload_u64_ptr( + &self, + data_ptr: PointerValue<'ctx>, + offset: usize, + name: &str, + ) -> Result> { + // SAFETY: every caller derives the fixed offset from the validated + // B-Tree reservation and record layout. + let field_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + data_ptr, + &[self.context.i32_type().const_int(offset as u64, false)], + &format!("{name}_i8"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + }; + self.builder + .build_pointer_cast( + field_ptr_i8, + self.context.ptr_type(AddressSpace::default()), + name, + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string())) + } + + fn continue_after_btree_scalar_read( + &mut self, + read: &crate::ebpf::helper_functions::MemoryReadDiagnostics<'ctx>, + address: IntValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + reserved_len: usize, + abort_block: inkwell::basic_block::BasicBlock<'ctx>, + name: &str, + ) -> Result<()> { + let function = self.current_function("compile B-Tree scalar read")?; + let ok_block = self + .context + .append_basic_block(function, &format!("{name}_ok")); + let error_block = self + .context + .append_basic_block(function, &format!("{name}_error")); + self.builder + .build_conditional_branch(read.combined_fail, error_block, ok_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(error_block); + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + read.helper_result, + address, + )?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(ok_block); + Ok(()) + } + + fn emit_btree_bulk_read( + &mut self, + status_ptr: PointerValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + reserved_len: usize, + abort_block: inkwell::basic_block::BasicBlock<'ctx>, + read: BTreeBulkRead<'ctx, '_>, + ) -> Result<()> { + let BTreeBulkRead { + destination_offset, + source_address, + length, + max_len, + name, + } = read; + let i32_type = self.context.i32_type(); + let i64_type = self.context.i64_type(); + let ptr_type = self.context.ptr_type(AddressSpace::default()); + let destination = self.btree_payload_u64_ptr( + var_data_ptr, + destination_offset, + &format!("{name}_destination"), + )?; + let source = self + .builder + .build_int_to_ptr(source_address, ptr_type, &format!("{name}_source")) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length = self + .builder + .build_int_truncate(length, i32_type, &format!("{name}_length")) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length = self.clamp_probe_read_length(length, max_len, &format!("{name}_length"))?; + let result = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[destination.into(), length.into(), source.into()], + i64_type.into(), + &format!("probe_read_user_{name}"), + )? + .into_int_value(); + let ok = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + result, + i64_type.const_zero(), + &format!("{name}_ok"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let function = self.current_function("compile B-Tree bulk read")?; + let ok_block = self + .context + .append_basic_block(function, &format!("{name}_read_ok")); + let error_block = self + .context + .append_basic_block(function, &format!("{name}_read_error")); + self.builder + .build_conditional_branch(ok, ok_block, error_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(error_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::ReadError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.emit_complex_format_read_error_payload( + var_data_ptr, + reserved_len, + result, + source_address, + )?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(ok_block); + Ok(()) + } + + fn emit_complex_format_btree( + &mut self, + status_ptr: PointerValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + descriptor: &RuntimeAddress<'ctx>, + reserved_len: usize, + capture: BTreeCaptureConfig, + ) -> Result<()> { + let header_len = ghostscope_protocol::BTREE_HEADER_SIZE; + if reserved_len < header_len { + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + return Ok(()); + } + + let capacity = usize::try_from(capture.node_capacity).map_err(|_| { + CodeGenError::DwarfError("B-Tree capacity does not fit this host".to_string()) + })?; + let key_stride = usize::try_from(capture.keys.slot_stride).map_err(|_| { + CodeGenError::DwarfError("B-Tree key stride does not fit this host".to_string()) + })?; + let value_stride = capture + .values + .map(|values| { + usize::try_from(values.slot_stride).map_err(|_| { + CodeGenError::DwarfError( + "B-Tree value stride does not fit this host".to_string(), + ) + }) + }) + .transpose()? + .unwrap_or(0); + let key_bytes = capacity + .checked_mul(key_stride) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree key payload overflow".to_string()))?; + let value_bytes = capacity + .checked_mul(value_stride) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree value payload overflow".to_string()))?; + let values_offset = ghostscope_protocol::BTREE_NODE_HEADER_SIZE + .checked_add(key_bytes) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree value offset overflow".to_string()))?; + let record_size = values_offset + .checked_add(value_bytes) + .ok_or_else(|| CodeGenError::DwarfError("B-Tree record size overflow".to_string()))?; + let reservation_nodes = reserved_len + .saturating_sub(header_len) + .checked_div(record_size) + .unwrap_or(0); + let node_slots = capture.max_nodes.min(reservation_nodes); + if capture.edges.edge_count != capture.node_capacity.saturating_add(1) + || capture.edges.pointer_access_size.bytes() != capture.root_pointer_access_size.bytes() + { + return Err(CodeGenError::DwarfError( + "B-Tree edge layout does not match its DWARF node capacity".to_string(), + )); + } + let edge_count = usize::try_from(capture.edges.edge_count).map_err(|_| { + CodeGenError::DwarfError("B-Tree edge count does not fit this host".to_string()) + })?; + + let i8_type = self.context.i8_type(); + let i64_type = self.context.i64_type(); + let function = self.current_function("compile B-Tree value capture")?; + let abort_block = self.context.append_basic_block(function, "btree_abort"); + let invalid_block = self.context.append_basic_block(function, "btree_invalid"); + let finish_block = self.context.append_basic_block(function, "btree_finish"); + let truncated_ptr = self.build_entry_alloca(i8_type, "btree_frontier_truncated")?; + self.builder + .build_store(truncated_ptr, i8_type.const_zero()) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + let length_address = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(capture.length_offset, false), + "btree_length_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let root_pointer_address = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(capture.root_pointer_offset, false), + "btree_root_pointer_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let root_height_address = self + .builder + .build_int_add( + descriptor.value, + i64_type.const_int(capture.root_height_offset, false), + "btree_root_height_member", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(length_address), + capture.length_access_size, + Some(status_ptr), + "btree_length_metadata", + )?; + self.continue_after_btree_scalar_read( + &length_read, + length_address, + var_data_ptr, + reserved_len, + abort_block, + "btree_length", + )?; + let original_count = length_read.value.into_int_value(); + self.store_complex_payload_u64(var_data_ptr, 0, original_count, "btree_item_count")?; + self.store_complex_payload_u64( + var_data_ptr, + ghostscope_protocol::BTREE_NODE_SLOT_COUNT_OFFSET, + i64_type.const_int(node_slots as u64, false), + "btree_node_slots", + )?; + self.store_complex_payload_u64( + var_data_ptr, + ghostscope_protocol::BTREE_CAPTURED_ITEM_COUNT_OFFSET, + i64_type.const_zero(), + "btree_captured_items_empty", + )?; + for slot in 0..node_slots { + let record_offset = header_len + .checked_add(slot.checked_mul(record_size).ok_or_else(|| { + CodeGenError::DwarfError("B-Tree record offset overflow".to_string()) + })?) + .ok_or_else(|| { + CodeGenError::DwarfError("B-Tree record offset overflow".to_string()) + })?; + self.store_complex_payload_u64( + var_data_ptr, + record_offset, + i64_type.const_zero(), + &format!("btree_node_{slot}_empty"), + )?; + } + + let is_empty = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + original_count, + i64_type.const_zero(), + "btree_is_empty", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let empty_block = self.context.append_basic_block(function, "btree_empty"); + let nonempty_block = self.context.append_basic_block(function, "btree_nonempty"); + self.builder + .build_conditional_branch(is_empty, empty_block, nonempty_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(empty_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::ZeroLength as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(nonempty_block); + if node_slots == 0 { + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } else { + let root_pointer_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(root_pointer_address), + capture.root_pointer_access_size, + Some(status_ptr), + "btree_root_pointer_metadata", + )?; + self.continue_after_btree_scalar_read( + &root_pointer_read, + root_pointer_address, + var_data_ptr, + reserved_len, + abort_block, + "btree_root_pointer", + )?; + let root_pointer = root_pointer_read.value.into_int_value(); + let root_height_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(root_height_address), + capture.root_height_access_size, + Some(status_ptr), + "btree_root_height_metadata", + )?; + self.continue_after_btree_scalar_read( + &root_height_read, + root_height_address, + var_data_ptr, + reserved_len, + abort_block, + "btree_root_height", + )?; + let root_height = root_height_read.value.into_int_value(); + let root_nonnull = self + .builder + .build_int_compare( + inkwell::IntPredicate::NE, + root_pointer, + i64_type.const_zero(), + "btree_root_nonnull", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let height_valid = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULT, + root_height, + i64_type + .const_int((capture.root_pointer_access_size.bytes() * 8) as u64, false), + "btree_root_height_valid", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let root_valid = self + .builder + .build_and(root_nonnull, height_valid, "btree_root_valid") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let root_valid_block = self + .context + .append_basic_block(function, "btree_root_valid"); + self.builder + .build_conditional_branch(root_valid, root_valid_block, invalid_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(root_valid_block); + self.store_complex_payload_u64( + var_data_ptr, + header_len, + root_pointer, + "btree_root_node", + )?; + self.store_complex_payload_u64( + var_data_ptr, + header_len + ghostscope_protocol::BTREE_NODE_HEIGHT_OFFSET, + root_height, + "btree_root_node_height", + )?; + + for slot in 0..node_slots { + let record_offset = header_len + slot * record_size; + let node_ptr = self.btree_payload_u64_ptr( + var_data_ptr, + record_offset, + &format!("btree_node_{slot}_address_ptr"), + )?; + let node_address = self + .builder + .build_load(i64_type, node_ptr, &format!("btree_node_{slot}_address")) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let present = self + .builder + .build_int_compare( + inkwell::IntPredicate::NE, + node_address, + i64_type.const_zero(), + &format!("btree_node_{slot}_present"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let capture_block = self + .context + .append_basic_block(function, &format!("btree_node_{slot}_capture")); + let next_block = self + .context + .append_basic_block(function, &format!("btree_node_{slot}_next")); + self.builder + .build_conditional_branch(present, capture_block, next_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(capture_block); + + let height_ptr = self.btree_payload_u64_ptr( + var_data_ptr, + record_offset + ghostscope_protocol::BTREE_NODE_HEIGHT_OFFSET, + &format!("btree_node_{slot}_height_ptr"), + )?; + let height = self + .builder + .build_load(i64_type, height_ptr, &format!("btree_node_{slot}_height")) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let length_address = self + .builder + .build_int_add( + node_address, + i64_type.const_int(capture.node_length_offset, false), + &format!("btree_node_{slot}_length_address"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let node_length_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(length_address), + capture.node_length_access_size, + Some(status_ptr), + &format!("btree_node_{slot}_length"), + )?; + self.continue_after_btree_scalar_read( + &node_length_read, + length_address, + var_data_ptr, + reserved_len, + abort_block, + &format!("btree_node_{slot}_length"), + )?; + let node_length = node_length_read.value.into_int_value(); + let length_valid = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULE, + node_length, + i64_type.const_int(capture.node_capacity, false), + &format!("btree_node_{slot}_length_valid"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let node_valid_block = self + .context + .append_basic_block(function, &format!("btree_node_{slot}_valid")); + self.builder + .build_conditional_branch(length_valid, node_valid_block, invalid_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(node_valid_block); + self.store_complex_payload_u64( + var_data_ptr, + record_offset + ghostscope_protocol::BTREE_NODE_LENGTH_OFFSET, + node_length, + &format!("btree_node_{slot}_stored_length"), + )?; + let captured_ptr = self.btree_payload_u64_ptr( + var_data_ptr, + ghostscope_protocol::BTREE_CAPTURED_ITEM_COUNT_OFFSET, + &format!("btree_node_{slot}_captured_ptr"), + )?; + let captured_items = self + .builder + .build_load( + i64_type, + captured_ptr, + &format!("btree_node_{slot}_captured_items"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let captured_items = self + .builder + .build_int_add( + captured_items, + node_length, + &format!("btree_node_{slot}_captured_items_next"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(captured_ptr, captured_items) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + if key_stride > 0 { + let source = self + .builder + .build_int_add( + node_address, + i64_type.const_int(capture.keys.offset, false), + &format!("btree_node_{slot}_keys_source"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length = self + .builder + .build_int_mul( + node_length, + i64_type.const_int(key_stride as u64, false), + &format!("btree_node_{slot}_keys_length"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.emit_btree_bulk_read( + status_ptr, + var_data_ptr, + reserved_len, + abort_block, + BTreeBulkRead { + destination_offset: record_offset + + ghostscope_protocol::BTREE_NODE_HEADER_SIZE, + source_address: source, + length, + max_len: key_bytes, + name: &format!("btree_node_{slot}_keys"), + }, + )?; + } + if let Some(values) = capture.values.filter(|_| value_stride > 0) { + let source = self + .builder + .build_int_add( + node_address, + i64_type.const_int(values.offset, false), + &format!("btree_node_{slot}_values_source"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let length = self + .builder + .build_int_mul( + node_length, + i64_type.const_int(value_stride as u64, false), + &format!("btree_node_{slot}_values_length"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.emit_btree_bulk_read( + status_ptr, + var_data_ptr, + reserved_len, + abort_block, + BTreeBulkRead { + destination_offset: record_offset + values_offset, + source_address: source, + length, + max_len: value_bytes, + name: &format!("btree_node_{slot}_values"), + }, + )?; + } + + let child_base = slot + .checked_mul(edge_count) + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + CodeGenError::DwarfError("B-Tree child slot overflow".to_string()) + })?; + let available_edges = node_slots.saturating_sub(child_base).min(edge_count); + for edge in 0..available_edges { + let internal = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + height, + i64_type.const_zero(), + &format!("btree_node_{slot}_edge_{edge}_internal"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let initialized = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULE, + i64_type.const_int(edge as u64, false), + node_length, + &format!("btree_node_{slot}_edge_{edge}_initialized"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let should_read = self + .builder + .build_and( + internal, + initialized, + &format!("btree_node_{slot}_edge_{edge}_read"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let read_block = self.context.append_basic_block( + function, + &format!("btree_node_{slot}_edge_{edge}_read"), + ); + let edge_next = self.context.append_basic_block( + function, + &format!("btree_node_{slot}_edge_{edge}_next"), + ); + self.builder + .build_conditional_branch(should_read, read_block, edge_next) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(read_block); + let edge_offset = capture + .edges + .offset_from_leaf + .checked_add( + (edge as u64) + .checked_mul(capture.edges.slot_stride) + .ok_or_else(|| { + CodeGenError::DwarfError( + "B-Tree edge offset overflow".to_string(), + ) + })?, + ) + .and_then(|offset| offset.checked_add(capture.edges.pointer_offset)) + .ok_or_else(|| { + CodeGenError::DwarfError("B-Tree edge offset overflow".to_string()) + })?; + let edge_address = self + .builder + .build_int_add( + node_address, + i64_type.const_int(edge_offset, false), + &format!("btree_node_{slot}_edge_{edge}_address"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let edge_read = self.generate_memory_read_with_diagnostics( + descriptor.with_value(edge_address), + capture.edges.pointer_access_size, + Some(status_ptr), + &format!("btree_node_{slot}_edge_{edge}"), + )?; + self.continue_after_btree_scalar_read( + &edge_read, + edge_address, + var_data_ptr, + reserved_len, + abort_block, + &format!("btree_node_{slot}_edge_{edge}"), + )?; + let child_address = edge_read.value.into_int_value(); + let child_nonnull = self + .builder + .build_int_compare( + inkwell::IntPredicate::NE, + child_address, + i64_type.const_zero(), + &format!("btree_node_{slot}_edge_{edge}_nonnull"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let child_valid = self.context.append_basic_block( + function, + &format!("btree_node_{slot}_edge_{edge}_valid"), + ); + self.builder + .build_conditional_branch(child_nonnull, child_valid, invalid_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(child_valid); + let child_slot = child_base + edge; + let child_offset = header_len + child_slot * record_size; + self.store_complex_payload_u64( + var_data_ptr, + child_offset, + child_address, + &format!("btree_node_{slot}_edge_{edge}_child"), + )?; + let child_height = self + .builder + .build_int_sub( + height, + i64_type.const_int(1, false), + &format!("btree_node_{slot}_edge_{edge}_child_height"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.store_complex_payload_u64( + var_data_ptr, + child_offset + ghostscope_protocol::BTREE_NODE_HEIGHT_OFFSET, + child_height, + &format!("btree_node_{slot}_edge_{edge}_stored_height"), + )?; + self.builder + .build_unconditional_branch(edge_next) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(edge_next); + } + + let is_internal = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGT, + height, + i64_type.const_zero(), + &format!("btree_node_{slot}_frontier_internal"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let misses_edge = self + .builder + .build_int_compare( + inkwell::IntPredicate::UGE, + node_length, + i64_type.const_int(available_edges as u64, false), + &format!("btree_node_{slot}_frontier_missing"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let frontier_truncated = self + .builder + .build_and( + is_internal, + misses_edge, + &format!("btree_node_{slot}_frontier_truncated"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let old_truncated = self + .builder + .build_load( + i8_type, + truncated_ptr, + &format!("btree_node_{slot}_old_truncated"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let new_truncated = self + .builder + .build_select( + frontier_truncated, + i8_type.const_int(1, false), + old_truncated, + &format!("btree_node_{slot}_new_truncated"), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_store(truncated_ptr, new_truncated) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder + .build_unconditional_branch(next_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(next_block); + } + self.builder + .build_unconditional_branch(finish_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + } + + self.builder.position_at_end(finish_block); + let captured_ptr = self.btree_payload_u64_ptr( + var_data_ptr, + ghostscope_protocol::BTREE_CAPTURED_ITEM_COUNT_OFFSET, + "btree_final_captured_ptr", + )?; + let captured_items = self + .builder + .build_load(i64_type, captured_ptr, "btree_final_captured_items") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let captured_valid = self + .builder + .build_int_compare( + inkwell::IntPredicate::ULE, + captured_items, + original_count, + "btree_captured_count_valid", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let count_complete = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + captured_items, + original_count, + "btree_count_complete", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let frontier_truncated = self + .builder + .build_load(i8_type, truncated_ptr, "btree_frontier_truncated_final") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))? + .into_int_value(); + let frontier_complete = self + .builder + .build_int_compare( + inkwell::IntPredicate::EQ, + frontier_truncated, + i8_type.const_zero(), + "btree_frontier_complete", + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let complete = self + .builder + .build_and(count_complete, frontier_complete, "btree_complete") + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + let count_valid_block = self + .context + .append_basic_block(function, "btree_captured_count_valid"); + self.builder + .build_conditional_branch(captured_valid, count_valid_block, invalid_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(count_valid_block); + let complete_block = self.context.append_basic_block(function, "btree_complete"); + let truncated_block = self.context.append_basic_block(function, "btree_truncated"); + self.builder + .build_conditional_branch(complete, complete_block, truncated_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(complete_block); + self.mark_any_success()?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(truncated_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(invalid_block); + self.builder + .build_store( + status_ptr, + i8_type.const_int(VariableStatus::AccessError as u64, false), + ) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(abort_block) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + + self.builder.position_at_end(abort_block); + Ok(()) + } + + fn emit_complex_format_runtime_read( + &mut self, + status_ptr: PointerValue<'ctx>, + var_data_ptr: PointerValue<'ctx>, + address: &ghostscope_dwarf::PlannedAddress, + dwarf_type: &ghostscope_dwarf::TypeInfo, + module_for_offsets: Option<&str>, + data_len: usize, + ) -> Result<()> { + // Branchy emitters must leave the builder at their continuation block so + // the caller can append the next formatted argument. + let ptr_type = self.context.ptr_type(AddressSpace::default()); + let i32_type = self.context.i32_type(); + let i64_type = self.context.i64_type(); + let dst_ptr = self + .builder + .build_bit_cast(var_data_ptr, ptr_type, "dst_ptr") + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + let size_val = i32_type.const_int(data_len as u64, false); + let src_addr = + self.resolve_planned_address(address, Some(status_ptr), module_for_offsets)?; + let offsets_found = src_addr.offsets_found; + let current_fn = self.current_function("compile complex variable read")?; + let cont2_block = self.context.append_basic_block(current_fn, "after_read"); + let skip_block = self.context.append_basic_block(current_fn, "offsets_skip"); + let found_block = self.context.append_basic_block(current_fn, "offsets_found"); + self.builder + .build_conditional_branch(offsets_found, found_block, skip_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(skip_block); + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(cont2_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(found_block); + let src_ptr = self + .builder + .build_int_to_ptr(src_addr.value, ptr_type, "src_ptr") + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + let zero64 = i64_type.const_zero(); + let is_null = self + .builder + .build_int_compare(inkwell::IntPredicate::EQ, src_addr.value, zero64, "is_null") + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + let null_block = self.context.append_basic_block(current_fn, "null_deref"); + let read_block = self.context.append_basic_block(current_fn, "read_user"); + self.builder + .build_conditional_branch(is_null, null_block, read_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(null_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::NullDeref as u64, false), + ) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(cont2_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(read_block); + let ret = self + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[dst_ptr, size_val.into(), src_ptr.into()], + i32_type.into(), + "probe_read_user", + )? + .into_int_value(); + let is_err = self + .builder + .build_int_compare( + inkwell::IntPredicate::SLT, + ret, + i32_type.const_zero(), + "ret_lt_zero", + ) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + let err_block = self.context.append_basic_block(current_fn, "read_err"); + let ok_block = self.context.append_basic_block(current_fn, "read_ok"); + self.builder + .build_conditional_branch(is_err, err_block, ok_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(err_block); + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::ReadError as u64, false), + ) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + // SAFETY: var_data_ptr points at the read-error payload. + let errno_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type.const_int(VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET as u64, false)], + "errno_ptr_i8", + ) + .map_err(|e| CodeGenError::LLVMError(format!("Failed to get errno gep: {e}")))? + }; + let i32_ptr = self + .builder + .build_pointer_cast( + errno_ptr_i8, + self.context.ptr_type(AddressSpace::default()), + "errno_ptr", + ) + .map_err(|e| CodeGenError::LLVMError(format!("Failed to cast errno ptr: {e}")))?; + self.builder + .build_store(i32_ptr, ret) + .map_err(|e| CodeGenError::LLVMError(format!("Failed to store errno: {e}")))?; + // SAFETY: read-error payload reserves enough bytes for the addr field. + let addr_ptr_i8 = unsafe { + self.builder + .build_gep( + self.context.i8_type(), + var_data_ptr, + &[i32_type.const_int(VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET as u64, false)], + "addr_ptr_i8", + ) + .map_err(|e| CodeGenError::LLVMError(format!("Failed to get addr gep: {e}")))? + }; + let addr_ptr = self + .builder + .build_pointer_cast( + addr_ptr_i8, + self.context.ptr_type(AddressSpace::default()), + "addr_ptr", + ) + .map_err(|e| CodeGenError::LLVMError(format!("Failed to cast addr ptr: {e}")))?; + self.builder + .build_store(addr_ptr, src_addr.value) + .map_err(|e| CodeGenError::LLVMError(format!("Failed to store addr: {e}")))?; + self.mark_any_fail()?; + self.builder + .build_unconditional_branch(cont2_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(ok_block); + if data_len < dwarf_type.size() as usize { + self.builder + .build_store( + status_ptr, + self.context + .i8_type() + .const_int(VariableStatus::Truncated as u64, false), + ) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + self.mark_any_success()?; + self.mark_any_fail()?; + } else { + self.mark_any_success()?; + } + self.builder + .build_unconditional_branch(cont2_block) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + + self.builder.position_at_end(cont2_block); + Ok(()) + } + + fn emit_complex_format_arg_source( + &mut self, + arg: &ComplexArg<'ctx>, + arg_ptrs: ComplexFormatArgPointers<'ctx>, + reserved_len: usize, + ) -> Result<()> { + let status_ptr = arg_ptrs.status_ptr; + let var_data_ptr = arg_ptrs.var_data_ptr; + + match &arg.source { + ComplexArgSource::ImmediateBytes { bytes, .. } => { + self.emit_complex_format_immediate_bytes(var_data_ptr, bytes) } ComplexArgSource::MemDump { address, len } => { self.emit_complex_format_memdump(status_ptr, var_data_ptr, address, *len) @@ -1655,6 +4715,180 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { *len_value, reserved_len, ), + ComplexArgSource::IndirectBytes { + descriptor, + data_offset, + data_access_size, + length_offset, + length_access_size, + max_len, + } => self.emit_complex_format_indirect( + status_ptr, + var_data_ptr, + descriptor, + reserved_len, + IndirectCaptureConfig { + data_offset: *data_offset, + data_access_size: *data_access_size, + length_offset: *length_offset, + length_access_size: *length_access_size, + max_len: *max_len, + shape: IndirectCaptureShape::Bytes, + }, + ), + ComplexArgSource::IndirectSequence { + descriptor, + data_offset, + data_access_size, + length_offset, + length_access_size, + element_stride, + max_elements, + max_len, + } => self.emit_complex_format_indirect( + status_ptr, + var_data_ptr, + descriptor, + reserved_len, + IndirectCaptureConfig { + data_offset: *data_offset, + data_access_size: *data_access_size, + length_offset: *length_offset, + length_access_size: *length_access_size, + max_len: *max_len, + shape: IndirectCaptureShape::Sequence { + element_stride: *element_stride, + max_elements: *max_elements, + ring: None, + }, + }, + ), + ComplexArgSource::IndirectRingSequence { + descriptor, + data_offset, + data_access_size, + start_offset, + start_access_size, + length, + capacity_offset, + capacity_access_size, + element_stride, + max_elements, + max_len, + } => { + let (length_offset, length_access_size, length_kind) = match length { + RingSequenceLengthSource::Explicit { + offset, + access_size, + } => (*offset, *access_size, RingCaptureLengthKind::Explicit), + RingSequenceLengthSource::End { + offset, + access_size, + } => (*offset, *access_size, RingCaptureLengthKind::End), + }; + self.emit_complex_format_indirect( + status_ptr, + var_data_ptr, + descriptor, + reserved_len, + IndirectCaptureConfig { + data_offset: *data_offset, + data_access_size: *data_access_size, + length_offset, + length_access_size, + max_len: *max_len, + shape: IndirectCaptureShape::Sequence { + element_stride: *element_stride, + max_elements: *max_elements, + ring: Some(RingCaptureConfig { + start_offset: *start_offset, + start_access_size: *start_access_size, + capacity_offset: *capacity_offset, + capacity_access_size: *capacity_access_size, + length_kind, + }), + }, + }, + ) + } + ComplexArgSource::IndirectHashTable { + descriptor, + control_offset, + control_access_size, + length_offset, + length_access_size, + bucket_mask_offset, + bucket_mask_access_size, + entry_stride, + occupancy, + buckets, + bucket_order, + max_buckets, + max_len: _, + } => self.emit_complex_format_hash_table( + status_ptr, + var_data_ptr, + descriptor, + reserved_len, + HashTableCaptureConfig { + control_offset: *control_offset, + control_access_size: *control_access_size, + length_offset: *length_offset, + length_access_size: *length_access_size, + bucket_mask_offset: *bucket_mask_offset, + bucket_mask_access_size: *bucket_mask_access_size, + entry_stride: *entry_stride, + occupancy: *occupancy, + buckets: *buckets, + bucket_order: *bucket_order, + max_buckets: *max_buckets, + }, + ), + ComplexArgSource::IndirectBTree { + descriptor, + root_pointer_offset, + root_pointer_access_size, + root_height_offset, + root_height_access_size, + length_offset, + length_access_size, + node_length_offset, + node_length_access_size, + keys, + values, + edges, + node_capacity, + max_nodes, + max_len: _, + } => self.emit_complex_format_btree( + status_ptr, + var_data_ptr, + descriptor, + reserved_len, + BTreeCaptureConfig { + root_pointer_offset: *root_pointer_offset, + root_pointer_access_size: *root_pointer_access_size, + root_height_offset: *root_height_offset, + root_height_access_size: *root_height_access_size, + length_offset: *length_offset, + length_access_size: *length_access_size, + node_length_offset: *node_length_offset, + node_length_access_size: *node_length_access_size, + keys: *keys, + values: *values, + edges: *edges, + node_capacity: *node_capacity, + max_nodes: *max_nodes, + }, + ), + ComplexArgSource::ProjectedView { descriptor, fields } => self + .emit_complex_format_projected_view( + status_ptr, + var_data_ptr, + descriptor, + fields, + reserved_len, + ), ComplexArgSource::ComputedInt { value, byte_len } => { self.emit_complex_format_computed_int(var_data_ptr, *value, *byte_len) } @@ -1729,6 +4963,9 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { mod complex_format_layout_tests { use super::*; use inkwell::context::Context; + use inkwell::targets::{CodeModel, FileType, RelocMode}; + use inkwell::targets::{Target, TargetTriple}; + use inkwell::OptimizationLevel; fn immediate_arg<'ctx>(bytes: &[u8], access_path: Vec) -> ComplexArg<'ctx> { ComplexArg { @@ -1756,6 +4993,348 @@ mod complex_format_layout_tests { } } + fn indirect_bytes_arg<'ctx>(context: &'ctx Context, max_len: usize) -> ComplexArg<'ctx> { + ComplexArg { + var_name_index: 0, + type_index: 0, + access_path: Vec::new(), + data_len: ghostscope_protocol::INDIRECT_BYTES_LENGTH_PREFIX_SIZE + max_len, + source: ComplexArgSource::IndirectBytes { + descriptor: RuntimeAddress::available(context.i64_type().const_zero(), context), + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + max_len, + }, + } + } + + fn indirect_sequence_arg<'ctx>( + context: &'ctx Context, + element_stride: u64, + max_elements: usize, + max_len: usize, + ) -> ComplexArg<'ctx> { + ComplexArg { + var_name_index: 0, + type_index: 0, + access_path: Vec::new(), + data_len: ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE + max_len, + source: ComplexArgSource::IndirectSequence { + descriptor: RuntimeAddress::available(context.i64_type().const_zero(), context), + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + element_stride, + max_elements, + max_len, + }, + } + } + + fn indirect_ring_sequence_arg<'ctx>( + context: &'ctx Context, + element_stride: u64, + max_elements: usize, + max_len: usize, + ) -> ComplexArg<'ctx> { + ComplexArg { + var_name_index: 0, + type_index: 0, + access_path: Vec::new(), + data_len: ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE + max_len, + source: ComplexArgSource::IndirectRingSequence { + descriptor: RuntimeAddress::available(context.i64_type().const_zero(), context), + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + start_offset: 8, + start_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length: RingSequenceLengthSource::Explicit { + offset: 16, + access_size: ghostscope_dwarf::MemoryAccessSize::U64, + }, + capacity_offset: 24, + capacity_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + element_stride, + max_elements, + max_len, + }, + } + } + + fn indirect_hash_table_arg<'ctx>( + context: &'ctx Context, + entry_stride: u64, + max_buckets: usize, + max_len: usize, + ) -> ComplexArg<'ctx> { + ComplexArg { + var_name_index: 0, + type_index: 0, + access_path: Vec::new(), + data_len: ghostscope_protocol::HASH_TABLE_HEADER_SIZE + max_len, + source: ComplexArgSource::IndirectHashTable { + descriptor: RuntimeAddress::available(context.i64_type().const_zero(), context), + control_offset: 0, + control_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + bucket_mask_offset: 16, + bucket_mask_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + entry_stride, + occupancy: ghostscope_dwarf::HashTableOccupancy::ControlByteHighBitClear, + buckets: HashTableBucketSource::ReverseFromControl, + bucket_order: ghostscope_dwarf::HashTableBucketOrder::Reverse, + max_buckets, + max_len, + }, + } + } + + fn indirect_emitter_ir(capture: IndirectCaptureConfig) -> String { + let context = Context::create(); + let options = crate::CompileOptions::default(); + let ebpf = EbpfContext::new(&context, "indirect", Some(0), &options); + let mut ebpf = ebpf.expect("create eBPF context"); + let function_type = context.i64_type().fn_type(&[], false); + let function = ebpf + .module + .add_function("emit_indirect", function_type, None); + let entry = context.append_basic_block(function, "entry"); + ebpf.builder.position_at_end(entry); + let status_ptr = ebpf + .builder + .build_alloca(context.i8_type(), "status") + .expect("allocate status"); + let reserved_len = VARIABLE_READ_ERROR_PAYLOAD_LEN.max( + capture.shape.prefix_len().saturating_add( + capture + .max_len + .saturating_mul(capture.shape.reservation_factor()), + ), + ); + let data_ptr = ebpf + .builder + .build_alloca(context.i8_type().array_type(reserved_len as u32), "payload") + .expect("allocate payload"); + ebpf.builder + .build_store(status_ptr, context.i8_type().const_zero()) + .expect("initialize status"); + let descriptor_value = context.i64_type().const_int(0x1000, false); + let descriptor = RuntimeAddress::available(descriptor_value, &context); + + ebpf.emit_complex_format_indirect(status_ptr, data_ptr, &descriptor, reserved_len, capture) + .expect("emit indirect capture"); + ebpf.builder + .build_return(Some(&context.i64_type().const_zero())) + .expect("return from test function"); + ebpf.module.verify().expect("verify generated LLVM IR"); + + ebpf.module.print_to_string().to_string() + } + + fn clamped_probe_read_assembly(max_len: usize) -> String { + let context = Context::create(); + let options = crate::CompileOptions::default(); + let mut ebpf = EbpfContext::new(&context, "probe_read_bound", Some(0), &options) + .expect("create eBPF context"); + let i32_type = context.i32_type(); + let i64_type = context.i64_type(); + let ptr_type = context.ptr_type(AddressSpace::default()); + let function_type = i64_type.fn_type(&[i32_type.into()], false); + let function = ebpf + .module + .add_function("probe_read_bound", function_type, None); + let entry = context.append_basic_block(function, "entry"); + ebpf.builder.position_at_end(entry); + let length = function + .get_nth_param(0) + .expect("length argument") + .into_int_value(); + let length = ebpf + .clamp_probe_read_length(length, max_len, "test_probe_read_len") + .expect("clamp probe read length"); + let result = ebpf + .create_bpf_helper_call( + BPF_FUNC_probe_read_user as u64, + &[ + ptr_type.const_null().into(), + length.into(), + ptr_type.const_null().into(), + ], + i64_type.into(), + "test_probe_read", + ) + .expect("emit probe read") + .into_int_value(); + ebpf.builder + .build_return(Some(&result)) + .expect("return probe read result"); + ebpf.module.verify().expect("verify generated LLVM IR"); + + Target::initialize_bpf(&Default::default()); + let triple = TargetTriple::create("bpf-pc-linux"); + let target = Target::from_triple(&triple).expect("get BPF target"); + let target_machine = target + .create_target_machine( + &triple, + "generic", + "+alu32", + OptimizationLevel::Default, + RelocMode::PIC, + CodeModel::Small, + ) + .expect("create BPF target machine"); + let assembly = target_machine + .write_to_memory_buffer(&ebpf.module, FileType::Assembly) + .expect("emit BPF assembly"); + String::from_utf8(assembly.as_slice().to_vec()).expect("BPF assembly is UTF-8") + } + + fn hash_table_emitter_ir(capture: HashTableCaptureConfig, max_len: usize) -> String { + let context = Context::create(); + let options = crate::CompileOptions::default(); + let ebpf = EbpfContext::new(&context, "hash_table", Some(0), &options); + let mut ebpf = ebpf.expect("create eBPF context"); + let function_type = context.i64_type().fn_type(&[], false); + let function = ebpf + .module + .add_function("emit_hash_table", function_type, None); + let entry = context.append_basic_block(function, "entry"); + ebpf.builder.position_at_end(entry); + let status_ptr = ebpf + .builder + .build_alloca(context.i8_type(), "status") + .expect("allocate status"); + let reserved_len = ghostscope_protocol::HASH_TABLE_HEADER_SIZE + max_len; + let data_ptr = ebpf + .builder + .build_alloca(context.i8_type().array_type(reserved_len as u32), "payload") + .expect("allocate payload"); + ebpf.builder + .build_store(status_ptr, context.i8_type().const_zero()) + .expect("initialize status"); + let descriptor = + RuntimeAddress::available(context.i64_type().const_int(0x1000, false), &context); + + ebpf.emit_complex_format_hash_table( + status_ptr, + data_ptr, + &descriptor, + reserved_len, + capture, + ) + .expect("emit hash-table capture"); + ebpf.builder + .build_return(Some(&context.i64_type().const_zero())) + .expect("return from test function"); + ebpf.module.verify().expect("verify generated LLVM IR"); + + ebpf.module.print_to_string().to_string() + } + + fn btree_emitter_ir(capture: BTreeCaptureConfig, max_len: usize) -> String { + let context = Context::create(); + let options = crate::CompileOptions::default(); + let ebpf = EbpfContext::new(&context, "btree", Some(0), &options); + let mut ebpf = ebpf.expect("create eBPF context"); + let function_type = context.i64_type().fn_type(&[], false); + let function = ebpf.module.add_function("emit_btree", function_type, None); + let entry = context.append_basic_block(function, "entry"); + ebpf.builder.position_at_end(entry); + let status_ptr = ebpf + .builder + .build_alloca(context.i8_type(), "status") + .expect("allocate status"); + let reserved_len = ghostscope_protocol::BTREE_HEADER_SIZE + max_len; + let data_ptr = ebpf + .builder + .build_alloca(context.i8_type().array_type(reserved_len as u32), "payload") + .expect("allocate payload"); + ebpf.builder + .build_store(status_ptr, context.i8_type().const_zero()) + .expect("initialize status"); + let descriptor = + RuntimeAddress::available(context.i64_type().const_int(0x1000, false), &context); + + ebpf.emit_complex_format_btree(status_ptr, data_ptr, &descriptor, reserved_len, capture) + .expect("emit B-Tree capture"); + ebpf.builder + .build_return(Some(&context.i64_type().const_zero())) + .expect("return from test function"); + ebpf.module.verify().expect("verify generated LLVM IR"); + + ebpf.module.print_to_string().to_string() + } + + fn projected_view_emitter_ir() -> String { + let context = Context::create(); + let options = crate::CompileOptions::default(); + let ebpf = EbpfContext::new(&context, "projected_view", Some(0), &options); + let mut ebpf = ebpf.expect("create eBPF context"); + let function_type = context.i64_type().fn_type(&[], false); + let function = ebpf + .module + .add_function("emit_projected_view", function_type, None); + let entry = context.append_basic_block(function, "entry"); + ebpf.builder.position_at_end(entry); + let status_ptr = ebpf + .builder + .build_alloca(context.i8_type(), "status") + .expect("allocate status"); + let reserved_len = VARIABLE_READ_ERROR_PAYLOAD_LEN; + let data_ptr = ebpf + .builder + .build_alloca(context.i8_type().array_type(reserved_len as u32), "payload") + .expect("allocate payload"); + ebpf.builder + .build_store(status_ptr, context.i8_type().const_zero()) + .expect("initialize status"); + let descriptor = + RuntimeAddress::available(context.i64_type().const_int(0x1000, false), &context); + let fields = vec![ + ProjectedViewFieldSource { + output_offset: 0, + value_len: 4, + steps: vec![ + ProjectedViewStep::Member { offset: 8 }, + ProjectedViewStep::Dereference { + pointer_size: ghostscope_dwarf::MemoryAccessSize::U64, + }, + ], + capture: ghostscope_dwarf::ProjectedViewFieldCapture::Value, + }, + ProjectedViewFieldSource { + output_offset: 4, + value_len: 8, + steps: vec![ + ProjectedViewStep::Member { offset: 16 }, + ProjectedViewStep::Dereference { + pointer_size: ghostscope_dwarf::MemoryAccessSize::U64, + }, + ], + capture: ghostscope_dwarf::ProjectedViewFieldCapture::Address, + }, + ]; + + ebpf.emit_complex_format_projected_view( + status_ptr, + data_ptr, + &descriptor, + &fields, + reserved_len, + ) + .expect("emit projected view"); + ebpf.builder + .build_return(Some(&context.i64_type().const_zero())) + .expect("return from test function"); + ebpf.module.verify().expect("verify generated LLVM IR"); + + ebpf.module.print_to_string().to_string() + } + #[test] fn complex_format_layout_records_per_arg_lengths() { let context = Context::create(); @@ -1815,4 +5394,400 @@ mod complex_format_layout_tests { ] ); } + + #[test] + fn complex_format_layout_includes_indirect_length_prefix() { + let context = Context::create(); + let args = vec![indirect_bytes_arg(&context, 64)]; + + let layout = plan_complex_format_layout(4096, 0, &args); + + assert_eq!( + layout.args[0].reserved_len, + ghostscope_protocol::INDIRECT_BYTES_LENGTH_PREFIX_SIZE + 64 + ); + } + + #[test] + fn complex_format_layout_includes_indirect_sequence_header() { + let context = Context::create(); + let args = vec![ + indirect_sequence_arg(&context, 4, 3, 12), + indirect_ring_sequence_arg(&context, 4, 3, 12), + ]; + + let layout = plan_complex_format_layout(4096, 0, &args); + + assert_eq!( + layout.args[0].reserved_len, + ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE + 12 + ); + assert_eq!( + layout.args[1].reserved_len, + ghostscope_protocol::INDIRECT_SEQUENCE_HEADER_SIZE + 24 + ); + assert_eq!( + indirect_capture_capacity( + layout.args[1].reserved_len, + 12, + IndirectCaptureShape::Sequence { + element_stride: 4, + max_elements: 3, + ring: Some(RingCaptureConfig { + start_offset: 8, + start_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + capacity_offset: 24, + capacity_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_kind: RingCaptureLengthKind::Explicit, + }), + }, + ), + 12 + ); + } + + #[test] + fn complex_format_layout_includes_hash_table_header_and_bucket_regions() { + let context = Context::create(); + let args = vec![indirect_hash_table_arg(&context, 8, 7, 63)]; + + let layout = plan_complex_format_layout(4096, 0, &args); + + assert_eq!( + layout.args[0].reserved_len, + ghostscope_protocol::HASH_TABLE_HEADER_SIZE + 63 + ); + } + + #[test] + fn hash_table_emitter_uses_dwarf_metadata_and_storage_order() { + let reverse_ir = hash_table_emitter_ir( + HashTableCaptureConfig { + control_offset: 0, + control_access_size: ghostscope_dwarf::MemoryAccessSize::U32, + length_offset: 4, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U32, + bucket_mask_offset: 8, + bucket_mask_access_size: ghostscope_dwarf::MemoryAccessSize::U32, + entry_stride: 8, + occupancy: ghostscope_dwarf::HashTableOccupancy::ControlByteHighBitClear, + buckets: HashTableBucketSource::ReverseFromControl, + bucket_order: ghostscope_dwarf::HashTableBucketOrder::Reverse, + max_buckets: 4, + }, + 36, + ); + assert!(reverse_ir.contains("probe_read_user_hash_table_controls")); + assert!(reverse_ir.contains("probe_read_user_hash_table_buckets")); + assert!(reverse_ir.contains("hash_table_reverse_bucket_source")); + assert!(reverse_ir.contains("hash_table_captured_buckets_header")); + assert!(reverse_ir.contains("hash_table_metadata_error_payload")); + assert!(reverse_ir.contains("hash_table_control_length_exceeds_limit")); + assert!(reverse_ir.contains("hash_table_control_length_bounded")); + assert!(reverse_ir.contains("hash_table_bucket_length_exceeds_limit")); + assert!(reverse_ir.contains("hash_table_bucket_length_bounded")); + + let forward_ir = hash_table_emitter_ir( + HashTableCaptureConfig { + control_offset: 0, + control_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 16, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + bucket_mask_offset: 24, + bucket_mask_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + entry_stride: 4, + occupancy: ghostscope_dwarf::HashTableOccupancy::ControlByteHighBitClear, + buckets: HashTableBucketSource::Forward { + data_offset: 8, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + }, + bucket_order: ghostscope_dwarf::HashTableBucketOrder::Forward, + max_buckets: 4, + }, + 20, + ); + assert!(forward_ir.contains("hash_table_data_metadata")); + assert!(!forward_ir.contains("hash_table_reverse_bucket_source")); + + let legacy_ir = hash_table_emitter_ir( + HashTableCaptureConfig { + control_offset: 16, + control_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + bucket_mask_offset: 0, + bucket_mask_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + entry_stride: 8, + occupancy: ghostscope_dwarf::HashTableOccupancy::NonZeroWord { word_size: 8 }, + buckets: HashTableBucketSource::LegacyAfterControl { + entry_alignment: 4, + pointer_tag_mask: 1, + }, + bucket_order: ghostscope_dwarf::HashTableBucketOrder::Forward, + max_buckets: 4, + }, + 64, + ); + assert!(legacy_ir.contains("hash_table_legacy_control_address")); + assert!(legacy_ir.contains("hash_table_legacy_hash_words_aligned_length")); + assert!(legacy_ir.contains("hash_table_legacy_bucket_source")); + assert!(!legacy_ir.contains("hash_table_data_metadata")); + } + + #[test] + fn btree_emitter_uses_dwarf_node_layout_without_runtime_loops() { + let record_size = ghostscope_protocol::BTREE_NODE_HEADER_SIZE + 2 * (4 + 2); + let llvm_ir = btree_emitter_ir( + BTreeCaptureConfig { + root_pointer_offset: 0, + root_pointer_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + root_height_offset: 8, + root_height_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 16, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + node_length_offset: 54, + node_length_access_size: ghostscope_dwarf::MemoryAccessSize::U16, + keys: BTreeArraySource { + offset: 8, + slot_stride: 4, + }, + values: Some(BTreeArraySource { + offset: 52, + slot_stride: 2, + }), + edges: BTreeEdgesSource { + offset_from_leaf: 56, + slot_stride: 8, + pointer_offset: 0, + pointer_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + edge_count: 3, + }, + node_capacity: 2, + max_nodes: 3, + }, + record_size * 3, + ); + + assert!(llvm_ir.contains("btree_length_metadata")); + assert!(llvm_ir.contains("btree_root_pointer_metadata")); + assert!(llvm_ir.contains("btree_node_0_length")); + assert!(llvm_ir.contains("btree_node_0_keys_length_verifier_masked")); + assert!(llvm_ir.contains("btree_node_0_values_length_verifier_masked")); + assert!(llvm_ir.contains("probe_read_user_btree_node_0_keys")); + assert!(llvm_ir.contains("probe_read_user_btree_node_0_values")); + assert!(llvm_ir.contains("btree_node_0_edge_0")); + assert!(llvm_ir.contains("btree_frontier_truncated")); + assert!(llvm_ir.contains("btree_invalid")); + assert!(!llvm_ir.contains("btree_node_loop")); + } + + #[test] + fn indirect_bytes_capture_respects_caps_below_error_headroom() { + let context = Context::create(); + + for max_len in 0..=3 { + let args = vec![indirect_bytes_arg(&context, max_len)]; + let layout = plan_complex_format_layout(4096, 0, &args); + let reserved_len = layout.args[0].reserved_len; + + assert_eq!( + reserved_len, + ghostscope_protocol::INDIRECT_BYTES_LENGTH_PREFIX_SIZE + max_len + ); + assert_eq!( + indirect_capture_capacity(reserved_len, max_len, IndirectCaptureShape::Bytes), + max_len + ); + } + } + + #[test] + fn indirect_bytes_emitter_writes_standard_read_error_payload() { + let llvm_ir = indirect_emitter_ir(IndirectCaptureConfig { + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + max_len: 4, + shape: IndirectCaptureShape::Bytes, + }); + + assert!(llvm_ir.contains("indirect_metadata_error_payload")); + assert!(llvm_ir.contains("indirect_errno_ptr_i8")); + assert!(llvm_ir.contains("indirect_addr_ptr_i8")); + } + + #[test] + fn projected_view_emitter_stops_after_each_failed_read() { + let llvm_ir = projected_view_emitter_ir(); + + assert!(llvm_ir.contains("projected_view_0_1_pointer_error")); + assert!(llvm_ir.contains("projected_view_1_1_pointer_error")); + assert!(llvm_ir.contains("projected_view_0_read_ok")); + assert_eq!( + llvm_ir + .lines() + .filter(|line| { + line.contains("call i64") && line.contains("%probe_read_user_memdump") + }) + .count(), + 1 + ); + assert!(llvm_ir.lines().any(|line| { + line.contains("store i64") && line.contains("%projected_view_1_output") + })); + assert!(!llvm_ir.contains("projected_view_1_read_ok")); + assert!(llvm_ir.contains("projected_view_finish")); + assert!(llvm_ir.contains("indirect_errno_ptr_i8")); + assert!(llvm_ir.contains("indirect_addr_ptr_i8")); + } + + #[test] + fn indirect_bytes_emitter_uses_dwarf_metadata_widths() { + let llvm_ir = indirect_emitter_ir(IndirectCaptureConfig { + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U32, + length_offset: 4, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U32, + max_len: 4, + shape: IndirectCaptureShape::Bytes, + }); + let metadata_loads = llvm_ir + .lines() + .filter(|line| { + let is_metadata = line.contains("loaded_value"); + let is_i32_load = line.contains("load i32"); + is_metadata && is_i32_load + }) + .count(); + let metadata_extensions = llvm_ir + .lines() + .filter(|line| { + let is_metadata = line.contains("extended"); + let is_i32_extension = line.contains("zext i32"); + is_metadata && is_i32_extension + }) + .count(); + + assert_eq!(metadata_loads, 2, "{llvm_ir}"); + assert_eq!(metadata_extensions, 2, "{llvm_ir}"); + } + + #[test] + fn indirect_sequence_emitter_records_count_and_scales_by_dwarf_stride() { + let llvm_ir = indirect_emitter_ir(IndirectCaptureConfig { + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + max_len: 8, + shape: IndirectCaptureShape::Sequence { + element_stride: 4, + max_elements: 2, + ring: None, + }, + }); + + assert!(llvm_ir.contains("indirect_captured_count_ptr_i8_nonempty")); + assert!(llvm_ir.contains("indirect_read_len_bytes")); + assert!(llvm_ir.contains("indirect_read_len_exceeds_limit")); + assert!(llvm_ir.contains("indirect_read_len_verifier_masked")); + assert!(llvm_ir.contains("indirect_read_len_masked_exceeds_limit")); + assert!(llvm_ir.contains("indirect_read_len_bounded")); + assert!(llvm_ir.contains("mul i64")); + } + + #[test] + fn probe_read_length_keeps_a_verifier_visible_bound_after_optimization() { + let assembly = clamped_probe_read_assembly(64); + let mask = assembly.find("&= 127").expect("verifier mask in assembly"); + let exact_bound = assembly[mask..] + .find("> 64") + .map(|offset| mask + offset) + .expect("exact upper-bound comparison in assembly"); + let probe_read = assembly[exact_bound..] + .find("call 112") + .map(|offset| exact_bound + offset) + .expect("probe read helper in assembly"); + + assert!(mask < exact_bound && exact_bound < probe_read, "{assembly}"); + } + + #[test] + fn zero_sized_sequence_emitter_does_not_read_element_memory() { + let llvm_ir = indirect_emitter_ir(IndirectCaptureConfig { + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 8, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + max_len: 0, + shape: IndirectCaptureShape::Sequence { + element_stride: 0, + max_elements: 4, + ring: None, + }, + }); + + assert!(llvm_ir.contains("indirect_no_read_complete")); + assert!(!llvm_ir.contains("probe_read_user_indirect")); + } + + #[test] + fn ring_sequence_emitter_reads_two_segments_in_logical_order() { + let llvm_ir = indirect_emitter_ir(IndirectCaptureConfig { + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 16, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + max_len: 16, + shape: IndirectCaptureShape::Sequence { + element_stride: 4, + max_elements: 4, + ring: Some(RingCaptureConfig { + start_offset: 8, + start_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + capacity_offset: 24, + capacity_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_kind: RingCaptureLengthKind::Explicit, + }), + }, + }); + + assert!(llvm_ir.contains("indirect_ring_metadata_valid")); + assert!(llvm_ir.contains("indirect_ring_first_address")); + assert!(llvm_ir.contains("indirect_ring_second_payload")); + assert!(llvm_ir.contains("indirect_ring_first_payload_len = zext i32")); + assert!(llvm_ir.contains("indirect_ring_first_len_exceeds_limit")); + assert!(llvm_ir.contains("indirect_ring_first_len_bounded")); + assert!(llvm_ir.contains("indirect_ring_second_len_i32_exceeds_limit")); + assert!(llvm_ir.contains("indirect_ring_second_len_i32_bounded")); + assert!(llvm_ir.contains("probe_read_user_indirect_ring_first")); + assert!(llvm_ir.contains("probe_read_user_indirect_ring_second")); + } + + #[test] + fn legacy_ring_sequence_emitter_derives_wrapped_distance() { + let llvm_ir = indirect_emitter_ir(IndirectCaptureConfig { + data_offset: 0, + data_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_offset: 16, + length_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + max_len: 16, + shape: IndirectCaptureShape::Sequence { + element_stride: 4, + max_elements: 4, + ring: Some(RingCaptureConfig { + start_offset: 8, + start_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + capacity_offset: 24, + capacity_access_size: ghostscope_dwarf::MemoryAccessSize::U64, + length_kind: RingCaptureLengthKind::End, + }), + }, + }); + + assert!(llvm_ir.contains("indirect_ring_direct_distance")); + assert!(llvm_ir.contains("indirect_ring_wrapped_distance")); + assert!(llvm_ir.contains("indirect_ring_distance")); + } } diff --git a/ghostscope-compiler/src/ebpf/codegen/mod.rs b/ghostscope-compiler/src/ebpf/codegen/mod.rs index a6e34fce..5e71f2bb 100644 --- a/ghostscope-compiler/src/ebpf/codegen/mod.rs +++ b/ghostscope-compiler/src/ebpf/codegen/mod.rs @@ -41,6 +41,64 @@ struct PrintVarRuntimeMeta { data_len_limit: usize, } +#[derive(Debug, Clone, Copy)] +enum RingSequenceLengthSource { + Explicit { + offset: u64, + access_size: ghostscope_dwarf::MemoryAccessSize, + }, + End { + offset: u64, + access_size: ghostscope_dwarf::MemoryAccessSize, + }, +} + +#[derive(Debug, Clone)] +enum ProjectedViewStep { + Member { + offset: u64, + }, + Dereference { + pointer_size: ghostscope_dwarf::MemoryAccessSize, + }, +} + +#[derive(Debug, Clone)] +struct ProjectedViewFieldSource { + output_offset: usize, + value_len: usize, + steps: Vec, + capture: ghostscope_dwarf::ProjectedViewFieldCapture, +} + +#[derive(Debug, Clone, Copy)] +struct BTreeArraySource { + offset: u64, + slot_stride: u64, +} + +#[derive(Debug, Clone, Copy)] +struct BTreeEdgesSource { + offset_from_leaf: u64, + slot_stride: u64, + pointer_offset: u64, + pointer_access_size: ghostscope_dwarf::MemoryAccessSize, + edge_count: u64, +} + +#[derive(Debug, Clone, Copy)] +enum HashTableBucketSource { + Forward { + data_offset: u64, + data_access_size: ghostscope_dwarf::MemoryAccessSize, + }, + ReverseFromControl, + LegacyAfterControl { + entry_alignment: u64, + pointer_tag_mask: u64, + }, +} + /// Source for complex formatted argument data #[derive(Debug, Clone)] enum ComplexArgSource<'ctx> { @@ -60,6 +118,81 @@ enum ComplexArgSource<'ctx> { len_value: inkwell::values::IntValue<'ctx>, max_len: usize, }, + /// Bounded byte capture through a pointer-and-length descriptor. + IndirectBytes { + descriptor: RuntimeAddress<'ctx>, + data_offset: u64, + data_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + max_len: usize, + }, + /// Bounded capture of complete elements through a pointer-and-count + /// descriptor. + IndirectSequence { + descriptor: RuntimeAddress<'ctx>, + data_offset: u64, + data_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + element_stride: u64, + max_elements: usize, + max_len: usize, + }, + /// Bounded capture of a logical sequence split across a ring buffer. + IndirectRingSequence { + descriptor: RuntimeAddress<'ctx>, + data_offset: u64, + data_access_size: ghostscope_dwarf::MemoryAccessSize, + start_offset: u64, + start_access_size: ghostscope_dwarf::MemoryAccessSize, + length: RingSequenceLengthSource, + capacity_offset: u64, + capacity_access_size: ghostscope_dwarf::MemoryAccessSize, + element_stride: u64, + max_elements: usize, + max_len: usize, + }, + /// Bounded capture of a sparse hash-table prefix. Occupancy metadata and + /// bucket bytes are stored separately in one semantic payload. + IndirectHashTable { + descriptor: RuntimeAddress<'ctx>, + control_offset: u64, + control_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + bucket_mask_offset: u64, + bucket_mask_access_size: ghostscope_dwarf::MemoryAccessSize, + entry_stride: u64, + occupancy: ghostscope_dwarf::HashTableOccupancy, + buckets: HashTableBucketSource, + bucket_order: ghostscope_dwarf::HashTableBucketOrder, + max_buckets: usize, + max_len: usize, + }, + /// Bounded breadth-first capture of Rust B-Tree nodes. + IndirectBTree { + descriptor: RuntimeAddress<'ctx>, + root_pointer_offset: u64, + root_pointer_access_size: ghostscope_dwarf::MemoryAccessSize, + root_height_offset: u64, + root_height_access_size: ghostscope_dwarf::MemoryAccessSize, + length_offset: u64, + length_access_size: ghostscope_dwarf::MemoryAccessSize, + node_length_offset: u64, + node_length_access_size: ghostscope_dwarf::MemoryAccessSize, + keys: BTreeArraySource, + values: Option, + edges: BTreeEdgesSource, + node_capacity: u64, + max_nodes: usize, + max_len: usize, + }, + /// Assemble a synthetic struct from independently projected memory reads. + ProjectedView { + descriptor: RuntimeAddress<'ctx>, + fields: Vec, + }, ImmediateBytes { bytes: Vec, }, @@ -146,8 +279,11 @@ fn allocate_dynamic_payload_reservations(max_lens: &[usize], available: usize) - return vec![0; max_lens.len()]; } - let base_caps = vec![VARIABLE_READ_ERROR_PAYLOAD_LEN; max_lens.len()]; - let base_budget = available.min(VARIABLE_READ_ERROR_PAYLOAD_LEN.saturating_mul(max_lens.len())); + let base_caps = max_lens + .iter() + .map(|max_len| (*max_len).min(VARIABLE_READ_ERROR_PAYLOAD_LEN)) + .collect::>(); + let base_budget = available.min(base_caps.iter().sum()); let mut reservations = distribute_budget_fairly(&base_caps, base_budget); let remaining_budget = available.saturating_sub(reservations.iter().sum::()); if remaining_budget == 0 { @@ -157,11 +293,7 @@ fn allocate_dynamic_payload_reservations(max_lens: &[usize], available: usize) - let extra_caps: Vec = max_lens .iter() .zip(reservations.iter()) - .map(|(max_len, reserved)| { - max_len - .max(&VARIABLE_READ_ERROR_PAYLOAD_LEN) - .saturating_sub(*reserved) - }) + .map(|(max_len, reserved)| max_len.saturating_sub(*reserved)) .collect(); let extras = distribute_budget_fairly(&extra_caps, remaining_budget); for (reservation, extra) in reservations.iter_mut().zip(extras) { @@ -171,6 +303,33 @@ fn allocate_dynamic_payload_reservations(max_lens: &[usize], available: usize) - reservations } +impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { + fn build_entry_alloca(&self, ty: T, name: &str) -> Result> + where + T: inkwell::types::BasicType<'ctx>, + { + let current_block = self.builder.get_insert_block().ok_or_else(|| { + CodeGenError::LLVMError("no current block for stack allocation".to_string()) + })?; + let function = self.current_function("allocate stack scratch")?; + let entry_block = function.get_first_basic_block().ok_or_else(|| { + CodeGenError::LLVMError("no entry block for stack allocation".to_string()) + })?; + + if let Some(first_instruction) = entry_block.get_first_instruction() { + self.builder.position_before(&first_instruction); + } else { + self.builder.position_at_end(entry_block); + } + let alloca = self + .builder + .build_alloca(ty, name) + .map_err(|error| CodeGenError::LLVMError(error.to_string()))?; + self.builder.position_at_end(current_block); + Ok(alloca) + } +} + mod args; mod backtrace; mod expr_error; diff --git a/ghostscope-compiler/src/ebpf/codegen/tests.rs b/ghostscope-compiler/src/ebpf/codegen/tests.rs index 9c5d6c03..03e836a6 100644 --- a/ghostscope-compiler/src/ebpf/codegen/tests.rs +++ b/ghostscope-compiler/src/ebpf/codegen/tests.rs @@ -43,6 +43,12 @@ fn dynamic_payload_reservations_keep_error_headroom_when_possible() { assert_eq!(reservations, vec![12, 12, 12]); } +#[test] +fn dynamic_payload_reservations_never_exceed_requested_caps() { + let reservations = allocate_dynamic_payload_reservations(&[0, 8, 11], 64); + assert_eq!(reservations, vec![0, 8, 11]); +} + #[test] fn build_errno_i32_truncates_i64_errors() { let context = inkwell::context::Context::create(); diff --git a/ghostscope-compiler/src/ebpf/context.rs b/ghostscope-compiler/src/ebpf/context.rs index 7d338db4..791b61f6 100644 --- a/ghostscope-compiler/src/ebpf/context.rs +++ b/ghostscope-compiler/src/ebpf/context.rs @@ -147,6 +147,9 @@ pub struct EbpfContext<'ctx, 'dw> { pub(super) tls_scratch_alloca: Option>, // Per-invocation event accumulation offset (u32) stored on stack (entry block) pub event_offset_alloca: Option>, + // Sequence numbers keep verifier-oriented llvm.bpf.passthrough calls + // distinct while LLVM optimizes the generated module. + pub(super) next_bpf_passthrough_sequence: u32, // Compile-time upper bound for bytes that may already be reserved in the current trace event. // This is maintained across structured control flow so later instructions can budget against // the worst-case path without double-counting sibling branches. @@ -274,6 +277,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { pm_key_alloca: None, tls_scratch_alloca: None, event_offset_alloca: None, + next_bpf_passthrough_sequence: 0, compile_time_event_bytes_upper_bound: 0, compile_options: compile_options.clone(), diff --git a/ghostscope-compiler/src/ebpf/helper_functions.rs b/ghostscope-compiler/src/ebpf/helper_functions.rs index 9b4b412f..9d48fe52 100644 --- a/ghostscope-compiler/src/ebpf/helper_functions.rs +++ b/ghostscope-compiler/src/ebpf/helper_functions.rs @@ -18,10 +18,18 @@ use inkwell::AddressSpace; struct ProbeReadResult<'ctx> { loaded_i64: IntValue<'ctx>, + helper_result: IntValue<'ctx>, combined_fail: IntValue<'ctx>, not_found: IntValue<'ctx>, } +pub(crate) struct MemoryReadDiagnostics<'ctx> { + pub(crate) value: BasicValueEnum<'ctx>, + pub(crate) helper_result: IntValue<'ctx>, + pub(crate) combined_fail: IntValue<'ctx>, + pub(crate) not_found: IntValue<'ctx>, +} + pub(crate) struct ProcModuleOffsetsLookup<'ctx> { pub(crate) found: IntValue<'ctx>, pub(crate) text: IntValue<'ctx>, @@ -1028,6 +1036,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { Ok(ProbeReadResult { loaded_i64, + helper_result: ret_i32, combined_fail, not_found, }) @@ -1129,33 +1138,48 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { size: MemoryAccessSize, status_ptr: Option>, ) -> Result> { + Ok(self + .generate_memory_read_with_diagnostics(address, size, status_ptr, "probe_read_user")? + .value) + } + + pub(crate) fn generate_memory_read_with_diagnostics( + &mut self, + address: RuntimeAddress<'ctx>, + size: MemoryAccessSize, + status_ptr: Option>, + name_suffix: &str, + ) -> Result> { let zero_const = self.context.i64_type().const_zero(); let ProbeReadResult { loaded_i64, + helper_result, combined_fail, not_found, - } = self.probe_read_user_core(address, size, "probe_read_user")?; + } = self.probe_read_user_core(address, size, name_suffix)?; if let Some(status_ptr) = status_ptr { - self.store_variable_read_status( - status_ptr, - combined_fail, - not_found, - "probe_read_user", - )?; + self.store_variable_read_status(status_ptr, combined_fail, not_found, name_suffix)?; } - self.update_any_fail_flag(combined_fail, "probe_read_user")?; + self.update_any_fail_flag(combined_fail, name_suffix)?; let zero_bv: BasicValueEnum = zero_const.into(); let val_bv: BasicValueEnum = loaded_i64.into(); - self.builder + let value = self + .builder .build_select::, _>( combined_fail, zero_bv, val_bv, - "value_or_zero", + &format!("{name_suffix}_value_or_zero"), ) - .map_err(|e| CodeGenError::LLVMError(e.to_string())) + .map_err(|e| CodeGenError::LLVMError(e.to_string()))?; + Ok(MemoryReadDiagnostics { + value, + helper_result, + combined_fail, + not_found, + }) } /// Generate a user memory read and return both the zero-on-failure value and failure flag. diff --git a/ghostscope-dwarf/src/analyzer/type_context.rs b/ghostscope-dwarf/src/analyzer/type_context.rs index 1b8bd317..ed620ea3 100644 --- a/ghostscope-dwarf/src/analyzer/type_context.rs +++ b/ghostscope-dwarf/src/analyzer/type_context.rs @@ -1,9 +1,11 @@ use super::DwarfAnalyzer; use crate::{ indexable_element_layout, member_layout, semantics::PlanError, strip_type_aliases, - CompilationUnitMetadata, CuId, MemberLayout, ModuleId, PcContext, ResolvedType, Result, - SemanticType, TypeId, TypeIdentity, TypeInfo, TypeLayoutError, TypeOrigin, TypeProjection, - TypeProjectionLayout, VariableAccessSegment, VariableReadPlan, + CompilationUnitMetadata, CuId, MemberLayout, ModuleId, PcContext, ProjectedValueRead, + ProjectedValueStep, ProjectedViewField, ProjectedViewFieldCapture, ResolvedType, Result, + SemanticType, StructMember, TypeId, TypeIdentity, TypeInfo, TypeLayoutError, TypeOrigin, + TypeProjection, TypeProjectionLayout, ValueCapturePlan, ValueReadPlan, VariableAccessSegment, + VariableReadPlan, }; use std::path::Path; @@ -43,6 +45,78 @@ impl DwarfAnalyzer { .map(|metadata| metadata.map(TypeOrigin::from)) } + fn qualified_type_name(&self, type_id: TypeId) -> Result> { + let module_path = self.module_path_for_id(type_id.module).ok_or_else(|| { + anyhow::anyhow!("Semantic module id {:?} is not loaded", type_id.module) + })?; + self.modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? + .qualified_type_name(type_id) + } + + fn type_summary(&self, type_id: TypeId) -> Result> { + let module_path = self.module_path_for_id(type_id.module).ok_or_else(|| { + anyhow::anyhow!("Semantic module id {:?} is not loaded", type_id.module) + })?; + self.modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? + .type_summary(type_id) + } + + fn hydrate_projected_type(&self, mut resolved: ResolvedType) -> Result { + // Pointer summaries intentionally stop recursive DWARF expansion with + // UnknownType. An exact projected TypeId lets us complete that one DIE + // on demand without name lookup or recursive layout guessing. + if matches!( + strip_type_aliases(&resolved.summary), + TypeInfo::UnknownType { .. } + ) { + if let Some(type_id) = resolved.identity.layout_dwarf_id() { + if let Some(summary) = self.type_summary(type_id)? { + resolved.summary = summary; + resolved.origin = self.type_origin(type_id)?; + } + } + } + Ok(resolved) + } + + fn template_type_parameter( + &self, + type_id: TypeId, + index: usize, + ) -> Result> { + let module_path = self.module_path_for_id(type_id.module).ok_or_else(|| { + anyhow::anyhow!("Semantic module id {:?} is not loaded", type_id.module) + })?; + let parameter = self + .modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? + .template_type_parameter(type_id, index)?; + let Some((parameter_id, summary)) = parameter else { + return Ok(None); + }; + + Ok(Some(ResolvedType::new( + summary, + TypeIdentity::Dwarf(parameter_id), + self.type_origin(parameter_id)?, + ))) + } + + fn type_alignment(&self, type_id: TypeId) -> Result> { + let module_path = self.module_path_for_id(type_id.module).ok_or_else(|| { + anyhow::anyhow!("Semantic module id {:?} is not loaded", type_id.module) + })?; + self.modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))? + .type_alignment(type_id) + } + /// Combine the plan's protocol-compatible type summary with its DWARF origin. pub fn semantic_type_for_plan(&self, plan: &VariableReadPlan) -> Result> { let Some(summary) = plan.dwarf_type.clone() else { @@ -121,7 +195,8 @@ impl DwarfAnalyzer { segment: &VariableAccessSegment, type_module_path: Option<&Path>, ) -> Result { - if let Some(projection) = current.project_structural(segment) { + if let Some(mut projection) = current.project_structural(segment) { + projection.resolved_type = self.hydrate_projected_type(projection.resolved_type)?; return Ok(projection); } @@ -200,10 +275,529 @@ impl DwarfAnalyzer { Ok(TypeProjection { layout, - resolved_type: ResolvedType::new(summary, identity, origin), + resolved_type: self + .hydrate_projected_type(ResolvedType::new(summary, identity, origin))?, }) } + /// Build a semantic capture plan when a source-language adapter recognizes + /// the current physical type. Unknown values keep the ordinary DWARF path. + pub fn value_read_plan( + &self, + current: &ResolvedType, + type_module_path: Option<&Path>, + ) -> Result> { + let qualified_name = match ( + crate::language::requires_dwarf_qualified_name(current), + current.identity.layout_dwarf_id(), + ) { + (true, Some(type_id)) => self.qualified_type_name(type_id)?, + _ => None, + }; + let Some(layout) = + crate::language::resolve_value_layout(current, qualified_name.as_deref()) + else { + return Ok(None); + }; + let layout = match layout { + crate::language::ValueLayout::ProjectedValue { + value_path, + presentation, + } => { + let value = + self.project_resolved_member_path(current, &value_path, type_module_path)?; + let presentation = match presentation { + crate::language::ProjectedValuePresentation::Transparent => { + crate::ValuePresentation::Dwarf + } + crate::language::ProjectedValuePresentation::SingleField { + type_name, + field_name, + } => crate::ValuePresentation::SingleField { + type_name: type_name.to_string(), + field_name: field_name.to_string(), + }, + }; + return Ok(Some(ValueReadPlan { + presentation, + capture: ValueCapturePlan::ProjectedValue { value }, + })); + } + crate::language::ValueLayout::ProjectedStruct(layout) => { + let root_size = current.summary.size(); + let mut members = Vec::with_capacity(layout.fields.len()); + for field in layout.fields { + let projected = self.project_resolved_member_path( + current, + &field.value_path, + type_module_path, + )?; + let TypeProjectionLayout::Member { offset } = projected.layout else { + return Err(anyhow::anyhow!( + "semantic struct field produced a non-member projection" + )); + }; + let member_type = projected.resolved_type.summary; + let member_end = offset + .checked_add(member_type.size()) + .ok_or_else(|| anyhow::anyhow!("semantic struct field end overflow"))?; + if member_end > root_size { + return Err(anyhow::anyhow!( + "semantic struct field '{}' exceeds its DWARF root", + field.name + )); + } + members.push(StructMember { + name: field.name.to_string(), + member_type, + offset, + bit_offset: None, + bit_size: None, + }); + } + let presentation = projected_struct_presentation(layout.presentation); + let output_type = TypeInfo::StructType { + name: layout.type_name.to_string(), + size: root_size, + members, + }; + return Ok(Some(ValueReadPlan { + presentation, + capture: ValueCapturePlan::InlineView { output_type }, + })); + } + crate::language::ValueLayout::CompositeStruct(layout) => { + let mut members = Vec::with_capacity(layout.fields.len()); + let mut fields = Vec::with_capacity(layout.fields.len()); + let mut output_size = 0u64; + for field in layout.fields { + if members + .iter() + .any(|member: &StructMember| member.name == field.name) + { + return Err(anyhow::anyhow!( + "duplicate semantic struct field '{}'", + field.name + )); + } + let value = self.project_resolved_value_path( + current, + &field.value_path, + type_module_path, + matches!( + field.capture, + crate::language::CompositeStructFieldCapture::Address + ), + )?; + let Some(value) = value else { + return Ok(None); + }; + let (member_type, capture) = match field.capture { + crate::language::CompositeStructFieldCapture::Value(requirement) => { + if !projected_value_satisfies(requirement, &value) { + return Ok(None); + } + ( + value.resolved_type.summary.clone(), + ProjectedViewFieldCapture::Value, + ) + } + crate::language::CompositeStructFieldCapture::Address => { + let Some(member_type) = projected_address_type(&value) else { + return Ok(None); + }; + (member_type, ProjectedViewFieldCapture::Address) + } + }; + let output_offset = output_size; + output_size = output_size + .checked_add(member_type.size()) + .ok_or_else(|| anyhow::anyhow!("semantic struct size overflow"))?; + members.push(StructMember { + name: field.name.to_string(), + member_type, + offset: output_offset, + bit_offset: None, + bit_size: None, + }); + fields.push(ProjectedViewField { + output_offset, + value, + capture, + }); + } + let output_type = TypeInfo::StructType { + name: layout.type_name.to_string(), + size: output_size, + members, + }; + return Ok(Some(ValueReadPlan { + presentation: projected_struct_presentation(layout.presentation), + capture: ValueCapturePlan::ProjectedView { + output_type, + fields, + }, + })); + } + crate::language::ValueLayout::BTree(layout) => { + return crate::language::btree_value_read_plan( + self, + current, + layout, + type_module_path, + ); + } + crate::language::ValueLayout::HashTable(layout) => { + return crate::language::hash_table_value_read_plan( + self, + current, + layout, + type_module_path, + ); + } + crate::language::ValueLayout::IndirectSequence(layout) => layout, + }; + let data = + self.project_resolved_member_path(current, &layout.data_path, type_module_path)?; + + let (presentation, element_stride) = match layout.kind { + crate::language::IndirectSequenceKind::Utf8String => { + (crate::ValuePresentation::Utf8String, None) + } + crate::language::IndirectSequenceKind::ByteString + | crate::language::IndirectSequenceKind::OpaqueByteString => { + (crate::ValuePresentation::ByteString, None) + } + crate::language::IndirectSequenceKind::PointerTarget => { + let TypeInfo::PointerType { target_type, .. } = + strip_type_aliases(&data.resolved_type.summary) + else { + return Ok(None); + }; + let element_type = target_type.as_ref().clone(); + if matches!( + strip_type_aliases(&element_type), + TypeInfo::UnknownType { .. } | TypeInfo::OptimizedOut { .. } + ) { + return Ok(None); + } + let element_stride = element_type.size(); + ( + crate::ValuePresentation::Sequence { + element_type: Box::new(element_type), + element_stride, + }, + Some(element_stride), + ) + } + crate::language::IndirectSequenceKind::TypeParameter { index } => { + let Some(type_id) = current.identity.layout_dwarf_id() else { + return Ok(None); + }; + let Some(element) = self.template_type_parameter(type_id, index)? else { + return Ok(None); + }; + if matches!( + strip_type_aliases(&element.summary), + TypeInfo::UnknownType { .. } | TypeInfo::OptimizedOut { .. } + ) { + return Ok(None); + } + let element_stride = element.summary.size(); + ( + crate::ValuePresentation::Sequence { + element_type: Box::new(element.summary), + element_stride, + }, + Some(element_stride), + ) + } + }; + + let capture = match layout.addressing { + crate::language::IndirectSequenceAddressing::Contiguous { length_path } => { + let length = + self.project_resolved_member_path(current, &length_path, type_module_path)?; + match element_stride { + Some(element_stride) => ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + }, + None => ValueCapturePlan::IndirectBytes { data, length }, + } + } + crate::language::IndirectSequenceAddressing::Ring { + start_path, + length_path, + length_kind, + capacity_path, + } => { + let Some(element_stride) = element_stride else { + return Ok(None); + }; + let length = + self.project_resolved_member_path(current, &length_path, type_module_path)?; + if element_stride == 0 + && matches!( + length_kind, + crate::language::RingSequenceLengthKind::Explicit + ) + { + // Physical order is unobservable for zero-sized elements. + // Avoid depending on a producer-specific capacity encoding. + ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + } + } else { + let start = + self.project_resolved_member_path(current, &start_path, type_module_path)?; + let capacity = self.project_resolved_member_path( + current, + &capacity_path, + type_module_path, + )?; + let length = Box::new(match length_kind { + crate::language::RingSequenceLengthKind::Explicit => { + crate::RingSequenceLength::Explicit(length) + } + crate::language::RingSequenceLengthKind::End => { + crate::RingSequenceLength::End(length) + } + }); + ValueCapturePlan::IndirectRingSequence { + data, + start, + length, + capacity, + element_stride, + } + } + } + }; + + Ok(Some(ValueReadPlan { + presentation, + capture, + })) + } + + fn project_resolved_member_path( + &self, + current: &ResolvedType, + path: &[String], + type_module_path: Option<&Path>, + ) -> Result { + let mut resolved_type = current.clone(); + let mut offset = 0u64; + + for field in path { + let projected = self.project_resolved_type( + &resolved_type, + &VariableAccessSegment::Field(field.clone()), + type_module_path, + )?; + let TypeProjectionLayout::Member { + offset: member_offset, + } = projected.layout + else { + return Err(anyhow::anyhow!( + "semantic member path produced a non-member projection" + )); + }; + offset = offset + .checked_add(member_offset) + .ok_or_else(|| anyhow::anyhow!("semantic member path offset overflow"))?; + resolved_type = projected.resolved_type; + } + + Ok(TypeProjection { + layout: TypeProjectionLayout::Member { offset }, + resolved_type, + }) + } + + fn project_resolved_value_path( + &self, + current: &ResolvedType, + path: &[crate::language::ProjectedPathSegment], + type_module_path: Option<&Path>, + capture_address: bool, + ) -> Result> { + let mut resolved_type = current.clone(); + let mut steps = Vec::with_capacity(path.len()); + + for (index, segment) in path.iter().enumerate() { + match segment { + crate::language::ProjectedPathSegment::Member(field) => { + let projected = self.project_semantic_member( + &resolved_type, + Some(field), + type_module_path, + capture_address && index + 1 == path.len(), + )?; + let Some(projected) = projected else { + return Ok(None); + }; + let TypeProjectionLayout::Member { offset } = projected.layout else { + return Err(anyhow::anyhow!( + "semantic value path produced a non-member projection" + )); + }; + steps.push(ProjectedValueStep::Member { offset }); + resolved_type = projected.resolved_type; + } + crate::language::ProjectedPathSegment::SoleMember => { + let Some(projected) = self.project_semantic_member( + &resolved_type, + None, + type_module_path, + false, + )? + else { + return Ok(None); + }; + let TypeProjectionLayout::Member { offset } = projected.layout else { + return Err(anyhow::anyhow!( + "semantic value path produced a non-member projection" + )); + }; + steps.push(ProjectedValueStep::Member { offset }); + resolved_type = projected.resolved_type; + } + crate::language::ProjectedPathSegment::UnwrapScalar => { + let mut depth = 0usize; + loop { + match strip_type_aliases(&resolved_type.summary) { + TypeInfo::BaseType { .. } | TypeInfo::PointerType { .. } => break, + TypeInfo::StructType { .. } => { + // rust-gdb follows the first field until GDB + // reports a scalar. Requiring a sole member + // avoids guessing through unrelated structs. + if depth == 16 { + return Ok(None); + } + let Some(projected) = self.project_semantic_member( + &resolved_type, + None, + type_module_path, + false, + )? + else { + return Ok(None); + }; + let TypeProjectionLayout::Member { offset } = projected.layout + else { + return Err(anyhow::anyhow!( + "semantic scalar wrapper produced a non-member projection" + )); + }; + steps.push(ProjectedValueStep::Member { offset }); + resolved_type = projected.resolved_type; + depth += 1; + } + _ => return Ok(None), + } + } + } + crate::language::ProjectedPathSegment::Dereference => { + let TypeInfo::PointerType { + size: pointer_size, .. + } = strip_type_aliases(&resolved_type.summary) + else { + return Ok(None); + }; + if !matches!(*pointer_size, 4 | 8) { + return Ok(None); + } + let projected = self.project_resolved_type( + &resolved_type, + &VariableAccessSegment::Dereference, + type_module_path, + )?; + if projected.layout != TypeProjectionLayout::Dereference { + return Err(anyhow::anyhow!( + "semantic value path produced a non-dereference projection" + )); + } + steps.push(ProjectedValueStep::Dereference { + pointer_size: *pointer_size, + }); + resolved_type = projected.resolved_type; + } + } + } + + Ok(Some(ProjectedValueRead { + steps, + resolved_type, + })) + } + + fn project_semantic_member( + &self, + current: &ResolvedType, + expected_name: Option<&str>, + type_module_path: Option<&Path>, + allow_trailing_address: bool, + ) -> Result> { + let TypeInfo::StructType { size, members, .. } = strip_type_aliases(¤t.summary) + else { + return Ok(None); + }; + let member = match expected_name { + Some(expected_name) => { + let mut matching = members.iter().filter(|member| member.name == expected_name); + let Some(member) = matching.next() else { + return Ok(None); + }; + if matching.next().is_some() { + return Ok(None); + } + member + } + None => { + let [member] = members.as_slice() else { + return Ok(None); + }; + member + } + }; + let Some(member_end) = member.offset.checked_add(member.member_type.size()) else { + return Ok(None); + }; + // rustc can describe a trailing DST such as `RcInner::value` as a + // one-byte type at an offset equal to the aggregate's static size. An + // address capture needs only that DWARF member offset, so permit this + // exact terminal shape. Value captures still reject the apparent + // out-of-bounds read, as do members starting anywhere else. + let is_trailing_address = allow_trailing_address && member.offset == *size; + if member.bit_offset.is_some() + || member.bit_size.is_some() + || (member_end > *size && !is_trailing_address) + { + return Ok(None); + } + + let projected = self.project_resolved_type( + current, + &VariableAccessSegment::Field(member.name.clone()), + type_module_path, + )?; + let Some(projected_end) = member + .offset + .checked_add(projected.resolved_type.summary.size()) + else { + return Ok(None); + }; + if projected_end > *size && !is_trailing_address { + return Ok(None); + } + Ok(Some(projected)) + } + fn project_type_id( &self, current: TypeId, @@ -295,3 +889,298 @@ impl DwarfAnalyzer { crate::language::resolve_access_segment(&origin, segment) } } + +impl crate::language::RustPlanContext for DwarfAnalyzer { + fn project_type( + &self, + current: &ResolvedType, + segment: &VariableAccessSegment, + type_module_path: Option<&Path>, + ) -> Result { + self.project_resolved_type(current, segment, type_module_path) + } + + fn project_member_path( + &self, + current: &ResolvedType, + path: &[String], + type_module_path: Option<&Path>, + ) -> Result { + self.project_resolved_member_path(current, path, type_module_path) + } + + fn template_type_parameter( + &self, + type_id: TypeId, + index: usize, + ) -> Result> { + DwarfAnalyzer::template_type_parameter(self, type_id, index) + } + + fn type_alignment(&self, type_id: TypeId) -> Result> { + DwarfAnalyzer::type_alignment(self, type_id) + } + + fn tuple_member_layout( + &self, + type_id: TypeId, + aggregate_type: &TypeInfo, + index: u32, + ) -> Result { + DwarfAnalyzer::tuple_member_layout(self, type_id, aggregate_type, index) + } + + fn resolve_aggregate_type_in_module( + &self, + anchor: TypeId, + lookup_names: &[&str], + exact_qualified_name: Option<&str>, + ) -> Result> { + let module_path = self.module_path_for_id(anchor.module).ok_or_else(|| { + anyhow::anyhow!("Semantic module id {:?} is not loaded", anchor.module) + })?; + let module_data = self + .modules + .get(module_path) + .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?; + + for candidate in lookup_names { + let Some(type_id) = module_data.aggregate_type_id_by_name(anchor.module, candidate) + else { + continue; + }; + if let Some(expected_name) = exact_qualified_name { + let Some(actual_name) = self.qualified_type_name(type_id)? else { + continue; + }; + if actual_name != expected_name { + continue; + } + } + let Some(summary) = self.type_summary(type_id)? else { + continue; + }; + return Ok(Some(ResolvedType::new( + summary, + TypeIdentity::Dwarf(type_id), + self.type_origin(type_id)?, + ))); + } + Ok(None) + } +} + +fn projected_struct_presentation( + presentation: crate::language::ProjectedStructPresentation, +) -> crate::ValuePresentation { + match presentation { + crate::language::ProjectedStructPresentation::SignedState { + state_field, + non_negative_label, + negative_label, + } => crate::ValuePresentation::SignedStateStruct { + state_field: state_field.to_string(), + non_negative_label: non_negative_label.to_string(), + negative_label: negative_label.to_string(), + }, + crate::language::ProjectedStructPresentation::ReferenceCounted { + strong_field, + weak_field, + implicit_weak, + } => crate::ValuePresentation::ReferenceCountedStruct { + strong_field: strong_field.to_string(), + weak_field: weak_field.to_string(), + implicit_weak, + }, + } +} + +fn projected_address_type(value: &ProjectedValueRead) -> Option { + if matches!( + strip_type_aliases(&value.resolved_type.summary), + TypeInfo::UnknownType { .. } | TypeInfo::OptimizedOut { .. } + ) { + return None; + } + let pointer_size = value.steps.iter().rev().find_map(|step| match step { + ProjectedValueStep::Dereference { pointer_size } => Some(*pointer_size), + ProjectedValueStep::Member { .. } => None, + })?; + matches!(pointer_size, 4 | 8).then(|| TypeInfo::PointerType { + target_type: Box::new(value.resolved_type.summary.clone()), + size: pointer_size, + }) +} + +fn projected_value_satisfies( + requirement: crate::language::ProjectedValueRequirement, + value: &ProjectedValueRead, +) -> bool { + let value_type = strip_type_aliases(&value.resolved_type.summary); + match requirement { + crate::language::ProjectedValueRequirement::KnownSizedOrZst => match value_type { + TypeInfo::UnknownType { .. } + | TypeInfo::OptimizedOut { .. } + | TypeInfo::ArrayType { + total_size: None, .. + } => false, + TypeInfo::BaseType { name, size: 0, .. } => name == "()", + TypeInfo::StructType { size: 0, .. } + | TypeInfo::UnionType { size: 0, .. } + | TypeInfo::ArrayType { + total_size: Some(0), + .. + } => true, + type_info => type_info.size() > 0, + }, + crate::language::ProjectedValueRequirement::SignedPointerSizedInteger => { + let TypeInfo::BaseType { size, encoding, .. } = value_type else { + return false; + }; + let signed = *encoding == gimli::DW_ATE_signed.0 as u16 + || *encoding == gimli::DW_ATE_signed_char.0 as u16; + let pointer_size = value.steps.iter().rev().find_map(|step| match step { + ProjectedValueStep::Dereference { pointer_size } => Some(*pointer_size), + ProjectedValueStep::Member { .. } => None, + }); + signed && matches!(*size, 4 | 8) && pointer_size == Some(*size) + } + crate::language::ProjectedValueRequirement::UnsignedPointerSizedInteger => { + let TypeInfo::BaseType { size, encoding, .. } = value_type else { + return false; + }; + let unsigned = *encoding == gimli::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::DW_ATE_unsigned_char.0 as u16; + let pointer_size = value.steps.iter().rev().find_map(|step| match step { + ProjectedValueStep::Dereference { pointer_size } => Some(*pointer_size), + ProjectedValueStep::Member { .. } => None, + }); + unsigned && matches!(*size, 4 | 8) && pointer_size == Some(*size) + } + } +} + +#[cfg(test)] +mod projected_value_tests { + use super::*; + + fn projected_value(summary: TypeInfo, pointer_size: Option) -> ProjectedValueRead { + let steps = pointer_size + .map(|pointer_size| ProjectedValueStep::Dereference { pointer_size }) + .into_iter() + .collect(); + ProjectedValueRead { + steps, + resolved_type: ResolvedType::new(summary, TypeIdentity::Unknown, None), + } + } + + #[test] + fn known_sized_requirement_distinguishes_zst_from_unknown_layout() { + let requirement = crate::language::ProjectedValueRequirement::KnownSizedOrZst; + let unit = projected_value( + TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }, + Some(8), + ); + let unknown = projected_value( + TypeInfo::UnknownType { + name: "T".to_string(), + }, + Some(8), + ); + + assert!(projected_value_satisfies(requirement, &unit)); + assert!(!projected_value_satisfies(requirement, &unknown)); + } + + #[test] + fn projected_address_uses_the_dwarf_pointer_width() { + let target = TypeInfo::ArrayType { + element_type: Box::new(TypeInfo::BaseType { + name: "u8".to_string(), + size: 1, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }), + element_count: None, + total_size: None, + }; + let value = projected_value(target.clone(), Some(8)); + assert_eq!( + projected_address_type(&value), + Some(TypeInfo::PointerType { + target_type: Box::new(target), + size: 8, + }) + ); + assert_eq!( + projected_address_type(&projected_value(value.resolved_type.summary, None)), + None + ); + } + + #[test] + fn signed_state_requirement_uses_dwarf_encoding_and_pointer_width() { + let requirement = crate::language::ProjectedValueRequirement::SignedPointerSizedInteger; + let signed = |size, pointer_size| { + projected_value( + TypeInfo::BaseType { + name: "isize".to_string(), + size, + encoding: gimli::DW_ATE_signed.0 as u16, + }, + pointer_size, + ) + }; + assert!(projected_value_satisfies(requirement, &signed(8, Some(8)))); + assert!(!projected_value_satisfies(requirement, &signed(4, Some(8)))); + assert!(!projected_value_satisfies(requirement, &signed(8, None))); + + let unsigned = projected_value( + TypeInfo::BaseType { + name: "usize".to_string(), + size: 8, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }, + Some(8), + ); + assert!(!projected_value_satisfies(requirement, &unsigned)); + } + + #[test] + fn unsigned_counter_requirement_uses_dwarf_encoding_and_pointer_width() { + let requirement = crate::language::ProjectedValueRequirement::UnsignedPointerSizedInteger; + let unsigned = |size, pointer_size| { + projected_value( + TypeInfo::BaseType { + name: "usize".to_string(), + size, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }, + pointer_size, + ) + }; + assert!(projected_value_satisfies( + requirement, + &unsigned(8, Some(8)) + )); + assert!(!projected_value_satisfies( + requirement, + &unsigned(4, Some(8)) + )); + assert!(!projected_value_satisfies(requirement, &unsigned(8, None))); + + let signed = projected_value( + TypeInfo::BaseType { + name: "isize".to_string(), + size: 8, + encoding: gimli::DW_ATE_signed.0 as u16, + }, + Some(8), + ); + assert!(!projected_value_satisfies(requirement, &signed)); + } +} diff --git a/ghostscope-dwarf/src/analyzer/type_lookup.rs b/ghostscope-dwarf/src/analyzer/type_lookup.rs index 632feb0b..d8bdf637 100644 --- a/ghostscope-dwarf/src/analyzer/type_lookup.rs +++ b/ghostscope-dwarf/src/analyzer/type_lookup.rs @@ -401,7 +401,9 @@ impl DwarfAnalyzer { match ty { crate::TypeInfo::StructType { .. } | crate::TypeInfo::UnionType { .. } - | crate::TypeInfo::EnumType { .. } => crate::TypeInfo::TypedefType { + | crate::TypeInfo::EnumType { .. } + | crate::TypeInfo::VariantType { .. } + | crate::TypeInfo::ScopedEnumType { .. } => crate::TypeInfo::TypedefType { name, underlying_type: Box::new(ty), }, diff --git a/ghostscope-dwarf/src/language/mod.rs b/ghostscope-dwarf/src/language/mod.rs index 57da78c1..9b7e2581 100644 --- a/ghostscope-dwarf/src/language/mod.rs +++ b/ghostscope-dwarf/src/language/mod.rs @@ -4,6 +4,30 @@ mod rust; use crate::{semantics::PlanError, SourceLanguage, TypeOrigin, VariableAccessSegment}; +pub(crate) use rust::{ + btree_value_read_plan, hash_table_value_read_plan, CompositeStructFieldCapture, + IndirectSequenceAddressing, IndirectSequenceKind, ProjectedPathSegment, + ProjectedStructPresentation, ProjectedValuePresentation, ProjectedValueRequirement, + RingSequenceLengthKind, RustPlanContext, ValueLayout, +}; + +pub(crate) fn resolve_value_layout( + current: &crate::ResolvedType, + dwarf_qualified_name: Option<&str>, +) -> Option { + rust::resolve_value_layout(current, dwarf_qualified_name) +} + +pub(crate) fn requires_dwarf_qualified_name(current: &crate::ResolvedType) -> bool { + rust::requires_dwarf_qualified_name(current) +} + +pub(crate) fn annotate_type_info(language: SourceLanguage, type_info: &mut crate::TypeInfo) { + if language == SourceLanguage::Rust { + rust::annotate_type_info(type_info); + } +} + pub(crate) fn resolve_access_segment( origin: &TypeOrigin, segment: &VariableAccessSegment, diff --git a/ghostscope-dwarf/src/language/rust/access.rs b/ghostscope-dwarf/src/language/rust/access.rs index 40a6c928..5c95f2d7 100644 --- a/ghostscope-dwarf/src/language/rust/access.rs +++ b/ghostscope-dwarf/src/language/rust/access.rs @@ -1,5 +1,13 @@ use crate::VariableAccessSegment; +pub(super) fn tuple_field_name(index: u32) -> String { + format!("__{index}") +} + +pub(super) fn is_tuple_field_name(name: &str, index: u32) -> bool { + name == tuple_field_name(index) +} + pub(super) fn resolve_tuple_index(index: u32) -> VariableAccessSegment { - VariableAccessSegment::Field(format!("__{index}")) + VariableAccessSegment::Field(tuple_field_name(index)) } diff --git a/ghostscope-dwarf/src/language/rust/mod.rs b/ghostscope-dwarf/src/language/rust/mod.rs index 0458d5d4..af3da24a 100644 --- a/ghostscope-dwarf/src/language/rust/mod.rs +++ b/ghostscope-dwarf/src/language/rust/mod.rs @@ -1,5 +1,30 @@ mod access; +mod plan; +mod value; +mod variant; + +pub(crate) use plan::{btree_value_read_plan, hash_table_value_read_plan, RustPlanContext}; +pub(crate) use value::{ + CompositeStructFieldCapture, IndirectSequenceAddressing, IndirectSequenceKind, + ProjectedPathSegment, ProjectedStructPresentation, ProjectedValuePresentation, + ProjectedValueRequirement, RingSequenceLengthKind, ValueLayout, +}; pub(super) fn resolve_tuple_index(index: u32) -> crate::VariableAccessSegment { access::resolve_tuple_index(index) } + +pub(super) fn annotate_type_info(type_info: &mut crate::TypeInfo) { + variant::annotate_type_info(type_info); +} + +pub(super) fn resolve_value_layout( + current: &crate::ResolvedType, + dwarf_qualified_name: Option<&str>, +) -> Option { + value::resolve_value_layout(current, dwarf_qualified_name) +} + +pub(super) fn requires_dwarf_qualified_name(current: &crate::ResolvedType) -> bool { + value::requires_dwarf_qualified_name(current) +} diff --git a/ghostscope-dwarf/src/language/rust/plan.rs b/ghostscope-dwarf/src/language/rust/plan.rs new file mode 100644 index 00000000..4542f167 --- /dev/null +++ b/ghostscope-dwarf/src/language/rust/plan.rs @@ -0,0 +1,797 @@ +use crate::{ + strip_type_aliases, BTreeArrayCapture, BTreeEdgesCapture, BTreeEntryPresentation, + BTreeFieldPresentation, HashTableBucketOrder, HashTableBucketSource, + HashTableEntryPresentation, HashTableFieldPresentation, HashTableOccupancy, MemberLayout, + ResolvedType, Result, StructMember, TypeId, TypeInfo, TypeProjection, TypeProjectionLayout, + ValueCapturePlan, ValuePresentation, ValueReadPlan, VariableAccessSegment, +}; +use std::path::Path; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct BTreeLayout { + pub(crate) map_path: Vec, + pub(crate) root_path: Vec, + pub(crate) length_path: Vec, + pub(crate) kind: BTreeKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BTreeKind { + Map, + Set, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct HashTableLayout { + pub(crate) entry_type_path: Vec, + pub(crate) control_path: Vec, + pub(crate) length_path: Vec, + pub(crate) bucket_mask_path: Vec, + pub(crate) occupancy: HashTableOccupancy, + pub(crate) buckets: HashTableBucketLayout, + pub(crate) bucket_order: HashTableBucketOrder, + pub(crate) kind: HashTableKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum HashTableBucketLayout { + Forward { data_path: Vec }, + ReverseFromControl, + LegacyAfterControl { pointer_tag_mask: u64 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum HashTableKind { + Map, + Set, +} + +/// Generic DWARF operations required by Rust-specific value planners. +/// +/// The language layer owns producer conventions and standard-library layout +/// validation. The analyzer implements these primitives without exposing its +/// object-file storage to the language layer. +pub(crate) trait RustPlanContext { + fn project_type( + &self, + current: &ResolvedType, + segment: &VariableAccessSegment, + type_module_path: Option<&Path>, + ) -> Result; + + fn project_member_path( + &self, + current: &ResolvedType, + path: &[String], + type_module_path: Option<&Path>, + ) -> Result; + + fn template_type_parameter( + &self, + type_id: TypeId, + index: usize, + ) -> Result>; + + fn type_alignment(&self, type_id: TypeId) -> Result>; + + fn tuple_member_layout( + &self, + type_id: TypeId, + aggregate_type: &TypeInfo, + index: u32, + ) -> Result; + + fn resolve_aggregate_type_in_module( + &self, + anchor: TypeId, + lookup_names: &[&str], + exact_qualified_name: Option<&str>, + ) -> Result>; +} + +pub(crate) fn hash_table_value_read_plan( + context: &dyn RustPlanContext, + current: &ResolvedType, + layout: HashTableLayout, + type_module_path: Option<&Path>, +) -> Result> { + let entry_type_owner = + context.project_member_path(current, &layout.entry_type_path, type_module_path)?; + let Some(entry_owner_type_id) = entry_type_owner.resolved_type.identity.layout_dwarf_id() + else { + return Ok(None); + }; + let Some(entry_type) = context.template_type_parameter(entry_owner_type_id, 0)? else { + return Ok(None); + }; + if matches!( + strip_type_aliases(&entry_type.summary), + TypeInfo::UnknownType { .. } | TypeInfo::OptimizedOut { .. } + ) { + return Ok(None); + } + let Some(entry_type_id) = entry_type.identity.layout_dwarf_id() else { + return Ok(None); + }; + let entry_stride = entry_type.summary.size(); + let field = |index| { + context + .tuple_member_layout(entry_type_id, &entry_type.summary, index) + .ok() + .map(|field| HashTableFieldPresentation { + offset: field.offset, + field_type: Box::new(field.member_type), + }) + }; + let entry = match layout.kind { + HashTableKind::Map => { + let (Some(key), Some(value)) = (field(0), field(1)) else { + return Ok(None); + }; + HashTableEntryPresentation::Map { key, value } + } + HashTableKind::Set => { + let Some(value) = field(0) else { + return Ok(None); + }; + HashTableEntryPresentation::Set { value } + } + }; + + let control = context.project_member_path(current, &layout.control_path, type_module_path)?; + let buckets = match layout.buckets { + HashTableBucketLayout::Forward { data_path } => { + let data = context.project_member_path(current, &data_path, type_module_path)?; + let TypeInfo::PointerType { target_type, .. } = + strip_type_aliases(&data.resolved_type.summary) + else { + return Ok(None); + }; + if strip_type_aliases(target_type) != strip_type_aliases(&entry_type.summary) { + return Ok(None); + } + HashTableBucketSource::Forward { data } + } + HashTableBucketLayout::ReverseFromControl => HashTableBucketSource::ReverseFromControl, + HashTableBucketLayout::LegacyAfterControl { pointer_tag_mask } => { + // Rust's allocation uses the pair type's alignment, not its size. + // Read the concrete DWARF alignment so differently aligned keys + // and zero-sized types remain data-driven. + let Some(entry_alignment) = context.type_alignment(entry_type_id)? else { + return Ok(None); + }; + if entry_alignment == 0 + || !entry_alignment.is_power_of_two() + || (entry_stride > 0 && entry_alignment > entry_stride) + { + return Ok(None); + } + HashTableBucketSource::LegacyAfterControl { + entry_alignment, + pointer_tag_mask, + } + } + }; + let length = context.project_member_path(current, &layout.length_path, type_module_path)?; + let bucket_mask = + context.project_member_path(current, &layout.bucket_mask_path, type_module_path)?; + + Ok(Some(ValueReadPlan { + presentation: ValuePresentation::HashTable { + entry_stride, + bucket_order: layout.bucket_order, + occupancy: layout.occupancy, + entry, + }, + capture: ValueCapturePlan::IndirectHashTable { + control, + length, + bucket_mask, + entry_stride, + occupancy: layout.occupancy, + buckets, + bucket_order: layout.bucket_order, + }, + })) +} + +pub(crate) fn btree_value_read_plan<'a>( + context: &'a dyn RustPlanContext, + current: &ResolvedType, + layout: BTreeLayout, + type_module_path: Option<&'a Path>, +) -> Result> { + BTreePlanner { + context, + type_module_path, + } + .value_read_plan(current, layout) +} + +struct BTreePlanner<'a> { + context: &'a dyn RustPlanContext, + type_module_path: Option<&'a Path>, +} + +impl BTreePlanner<'_> { + fn value_read_plan( + &self, + current: &ResolvedType, + layout: BTreeLayout, + ) -> Result> { + let map = self.project_member_path(current, &layout.map_path)?; + let Some(map_type_id) = map.resolved_type.identity.layout_dwarf_id() else { + return Ok(None); + }; + let Some(key_type) = self.context.template_type_parameter(map_type_id, 0)? else { + return Ok(None); + }; + if matches!( + strip_type_aliases(&key_type.summary), + TypeInfo::UnknownType { .. } + ) { + return Ok(None); + } + let value_type = match layout.kind { + BTreeKind::Map => { + let Some(value_type) = self.context.template_type_parameter(map_type_id, 1)? else { + return Ok(None); + }; + if matches!( + strip_type_aliases(&value_type.summary), + TypeInfo::UnknownType { .. } + ) { + return Ok(None); + } + Some(value_type) + } + BTreeKind::Set => None, + }; + + let root = self.project_member_path(current, &layout.root_path)?; + let length = self.project_member_path(current, &layout.length_path)?; + let Some(root_value) = self.root_value(&root.resolved_type)? else { + return Ok(None); + }; + let root_offset = member_projection_offset(&root)?; + let root_height_inner = self.project_member_path(&root_value, &["height".to_string()])?; + let root_node_inner = self.project_member_path(&root_value, &["node".to_string()])?; + let Some(root_pointer_inner) = + self.project_pointer_wrapper(&root_node_inner.resolved_type)? + else { + return Ok(None); + }; + let root_height = rebase_member_projection(root_offset, root_height_inner)?; + let root_node_offset = member_projection_offset(&root_node_inner)?; + let root_pointer = rebase_member_projection( + root_offset + .checked_add(root_node_offset) + .ok_or_else(|| anyhow::anyhow!("B-Tree root node offset overflow"))?, + root_pointer_inner, + )?; + let Some(pointer_size) = pointer_width(&root_pointer.resolved_type.summary) else { + return Ok(None); + }; + if !matches!(pointer_size, 4 | 8) + || !is_unsigned_scalar_of_size(&root_height.resolved_type.summary, pointer_size) + || !is_unsigned_scalar_of_size(&length.resolved_type.summary, pointer_size) + { + return Ok(None); + } + + let leaf = self.context.project_type( + &root_pointer.resolved_type, + &VariableAccessSegment::Dereference, + self.type_module_path, + )?; + let leaf = leaf.resolved_type; + if !matches!( + strip_type_aliases(&leaf.summary), + TypeInfo::StructType { .. } + ) { + return Ok(None); + } + let node_length = self.project_member_path(&leaf, &["len".to_string()])?; + if !is_unsigned_scalar(&node_length.resolved_type.summary) { + return Ok(None); + } + + let keys = self.project_member_path(&leaf, &["keys".to_string()])?; + let Some((node_capacity, key_stride, key_element)) = + self.array_element(&keys.resolved_type)? + else { + return Ok(None); + }; + let keys_offset = member_projection_offset(&keys)?; + if !embedded_array_fits(leaf.summary.size(), keys_offset, node_capacity, key_stride) { + return Ok(None); + } + let Some(key_value) = self.project_maybe_uninit_value(&key_element, &key_type)? else { + return Ok(None); + }; + let key_value_offset = member_projection_offset(&key_value)?; + if !embedded_value_fits(key_stride, key_value_offset, &key_type.summary) { + return Ok(None); + } + + let (entry, values_capture) = match layout.kind { + BTreeKind::Map => { + let Some(value_type) = value_type.as_ref() else { + return Ok(None); + }; + let vals = self.project_member_path(&leaf, &["vals".to_string()])?; + let Some((value_capacity, value_stride, value_element)) = + self.array_element(&vals.resolved_type)? + else { + return Ok(None); + }; + if value_capacity != node_capacity { + return Ok(None); + } + let values_offset = member_projection_offset(&vals)?; + if !embedded_array_fits( + leaf.summary.size(), + values_offset, + value_capacity, + value_stride, + ) { + return Ok(None); + } + let Some(projected_value) = + self.project_maybe_uninit_value(&value_element, value_type)? + else { + return Ok(None); + }; + let value_offset = member_projection_offset(&projected_value)?; + if !embedded_value_fits(value_stride, value_offset, &value_type.summary) { + return Ok(None); + } + ( + BTreeEntryPresentation::Map { + key: BTreeFieldPresentation { + slot_stride: key_stride, + value_offset: key_value_offset, + field_type: Box::new(key_type.summary.clone()), + }, + value: BTreeFieldPresentation { + slot_stride: value_stride, + value_offset, + field_type: Box::new(value_type.summary.clone()), + }, + }, + Some(BTreeArrayCapture { + offset: values_offset, + slot_stride: value_stride, + }), + ) + } + BTreeKind::Set => ( + BTreeEntryPresentation::Set { + value: BTreeFieldPresentation { + slot_stride: key_stride, + value_offset: key_value_offset, + field_type: Box::new(key_type.summary.clone()), + }, + }, + None, + ), + }; + + let parent = self.project_member_path(&leaf, &["parent".to_string()])?; + let Some(parent_pointer) = self.parent_pointer(&parent.resolved_type)? else { + return Ok(None); + }; + if pointer_width(&parent_pointer.resolved_type.summary) != Some(pointer_size) { + return Ok(None); + } + let internal = self.context.project_type( + &parent_pointer.resolved_type, + &VariableAccessSegment::Dereference, + self.type_module_path, + )?; + let internal = internal.resolved_type; + let internal_leaf = self.project_member_path(&internal, &["data".to_string()])?; + if !same_layout_type(&internal_leaf.resolved_type, &leaf) { + return Ok(None); + } + let internal_leaf_offset = member_projection_offset(&internal_leaf)?; + let edges = self.project_member_path(&internal, &["edges".to_string()])?; + let Some((edge_count, edge_stride, edge_element)) = + self.array_element(&edges.resolved_type)? + else { + return Ok(None); + }; + if edge_count != node_capacity.checked_add(1).unwrap_or(u64::MAX) { + return Ok(None); + } + let edges_offset = member_projection_offset(&edges)?; + if !embedded_array_fits( + internal.summary.size(), + edges_offset, + edge_count, + edge_stride, + ) { + return Ok(None); + } + let Some(edge_value) = self.project_maybe_uninit_storage(&edge_element)? else { + return Ok(None); + }; + let edge_storage_offset = member_projection_offset(&edge_value)?; + let Some(edge_pointer_inner) = self.project_pointer_wrapper(&edge_value.resolved_type)? + else { + return Ok(None); + }; + if pointer_width(&edge_pointer_inner.resolved_type.summary) != Some(pointer_size) { + return Ok(None); + } + let edge_target = self.context.project_type( + &edge_pointer_inner.resolved_type, + &VariableAccessSegment::Dereference, + self.type_module_path, + )?; + if !same_layout_type(&edge_target.resolved_type, &leaf) { + return Ok(None); + } + let edge_pointer_offset = edge_storage_offset + .checked_add(member_projection_offset(&edge_pointer_inner)?) + .ok_or_else(|| anyhow::anyhow!("B-Tree edge pointer offset overflow"))?; + if !embedded_value_fits( + edge_stride, + edge_pointer_offset, + &edge_pointer_inner.resolved_type.summary, + ) { + return Ok(None); + } + let Some(offset_from_leaf) = edges_offset.checked_sub(internal_leaf_offset) else { + return Ok(None); + }; + + Ok(Some(ValueReadPlan { + presentation: ValuePresentation::BTree { + node_capacity, + entry, + }, + capture: ValueCapturePlan::IndirectBTree { + root_pointer, + root_height, + length, + node_length, + keys: BTreeArrayCapture { + offset: keys_offset, + slot_stride: key_stride, + }, + values: values_capture, + edges: BTreeEdgesCapture { + offset_from_leaf, + slot_stride: edge_stride, + pointer_offset: edge_pointer_offset, + pointer_size, + edge_count, + }, + node_capacity, + }, + })) + } + + fn project_member_path( + &self, + current: &ResolvedType, + path: &[String], + ) -> Result { + self.context + .project_member_path(current, path, self.type_module_path) + } + + fn root_value(&self, root: &ResolvedType) -> Result> { + // Rust 1.35's bundled rust-gdb provider reads `map["root"]` + // directly. Newer providers unwrap an Option niche first. Select the + // representation from the concrete DIE rather than the compiler + // version; every member is projected and validated below. + if member_path_exists(&root.summary, &["node"]) + && member_path_exists(&root.summary, &["height"]) + { + return Ok(Some(root.clone())); + } + + let Some(payload) = self.option_payload_type(root)? else { + return Ok(None); + }; + if payload.summary.size() != root.summary.size() + || !member_path_exists(&payload.summary, &["node"]) + || !member_path_exists(&payload.summary, &["height"]) + { + return Ok(None); + } + Ok(Some(payload)) + } + + fn parent_pointer(&self, parent: &ResolvedType) -> Result> { + // Rust 1.35 stores a nullable raw parent pointer. Newer B-Tree nodes + // use a pointer-niche Option. The pointer target and width remain + // concrete DWARF checks in both cases. + if pointer_width(&parent.summary).is_some() { + return self.project_pointer_wrapper(parent); + } + + let Some(payload) = self.option_payload_type(parent)? else { + return Ok(None); + }; + if payload.summary.size() != parent.summary.size() { + return Ok(None); + } + self.project_pointer_wrapper(&payload) + } + + fn option_payload_type(&self, option: &ResolvedType) -> Result> { + let Some(type_id) = option.identity.layout_dwarf_id() else { + return Ok(None); + }; + if let Some(payload) = self.context.template_type_parameter(type_id, 0)? { + return Ok(Some(payload)); + } + let name = match strip_type_aliases(&option.summary) { + TypeInfo::StructType { name, .. } | TypeInfo::VariantType { name, .. } => name, + _ => return Ok(None), + }; + let Some(payload_name) = rust_single_generic_argument(name, "Option") else { + return Ok(None); + }; + let short_name = rust_short_qualified_type_name(payload_name); + self.context.resolve_aggregate_type_in_module( + type_id, + &[payload_name, short_name], + payload_name.contains("::").then_some(payload_name), + ) + } + + fn array_element(&self, array: &ResolvedType) -> Result> { + let TypeInfo::ArrayType { + element_count: Some(element_count), + total_size, + .. + } = strip_type_aliases(&array.summary) + else { + return Ok(None); + }; + if *element_count == 0 { + return Ok(None); + } + let element = self.context.project_type( + array, + &VariableAccessSegment::ArrayIndex(0), + self.type_module_path, + )?; + let element = element.resolved_type; + if matches!( + strip_type_aliases(&element.summary), + TypeInfo::UnknownType { .. } + ) { + return Ok(None); + } + let stride = element.summary.size(); + let Some(inferred_size) = stride.checked_mul(*element_count) else { + return Ok(None); + }; + // Rust array DIEs carry a subrange count but do not always carry a + // byte size, notably for arrays of zero-sized MaybeUninit values. + // A concrete element DIE still gives us an exact DWARF-derived stride. + if total_size.is_some_and(|total_size| total_size != inferred_size) { + return Ok(None); + } + Ok(Some((*element_count, stride, element))) + } + + fn project_maybe_uninit_value( + &self, + wrapper: &ResolvedType, + expected: &ResolvedType, + ) -> Result> { + // Rust 1.49 through 1.93 exposes MaybeUninit storage as + // `value.value`; current nightly adds a tuple `__0` wrapper. Keep both + // paths, but accept one only when its concrete DIE matches `expected`. + for path in [&["value", "value", "__0"][..], &["value", "value"][..]] { + if !member_path_exists(&wrapper.summary, path) { + continue; + } + let path = path + .iter() + .map(|field| (*field).to_string()) + .collect::>(); + let projected = self.project_member_path(wrapper, &path)?; + if same_layout_type(&projected.resolved_type, expected) { + return Ok(Some(projected)); + } + } + Ok(None) + } + + fn project_maybe_uninit_storage( + &self, + wrapper: &ResolvedType, + ) -> Result> { + // Edges use the same version-dependent MaybeUninit wrappers as keys + // and values, then undergo separate pointer and target validation. + for path in [&["value", "value", "__0"][..], &["value", "value"][..]] { + if !member_path_exists(&wrapper.summary, path) { + continue; + } + let path = path + .iter() + .map(|field| (*field).to_string()) + .collect::>(); + return self.project_member_path(wrapper, &path).map(Some); + } + Ok(None) + } + + fn project_pointer_wrapper(&self, wrapper: &ResolvedType) -> Result> { + let mut resolved_type = wrapper.clone(); + let mut offset = 0u64; + for _ in 0..16 { + if matches!( + strip_type_aliases(&resolved_type.summary), + TypeInfo::PointerType { .. } + ) { + return Ok(Some(TypeProjection { + layout: TypeProjectionLayout::Member { offset }, + resolved_type, + })); + } + let TypeInfo::StructType { size, members, .. } = + strip_type_aliases(&resolved_type.summary) + else { + return Ok(None); + }; + // Rust 1.49's BoxedNode/Unique chain uses `ptr`; current NonNull + // uses `pointer`, and newer transparent wrappers may add `__0`. + // Every hop is still resolved and range-checked against its DIE. + let member = ["ptr", "pointer", "__0"] + .iter() + .find_map(|name| unique_valid_member(members, name, *size)) + .or_else(|| unique_non_zst_member(members, *size)); + let Some(member) = member else { + return Ok(None); + }; + let projected = self.context.project_type( + &resolved_type, + &VariableAccessSegment::Field(member.name.clone()), + self.type_module_path, + )?; + offset = offset + .checked_add(member.offset) + .ok_or_else(|| anyhow::anyhow!("Rust pointer wrapper offset overflow"))?; + resolved_type = projected.resolved_type; + } + Ok(None) + } +} + +fn member_projection_offset(projection: &TypeProjection) -> Result { + match projection.layout { + TypeProjectionLayout::Member { offset } => Ok(offset), + ref layout => Err(anyhow::anyhow!( + "semantic layout expected a member projection, got {layout:?}" + )), + } +} + +fn rust_single_generic_argument<'a>(name: &'a str, type_name: &str) -> Option<&'a str> { + let generic_start = name.find('<')?; + let base = name[..generic_start].rsplit("::").next()?; + if base != type_name || !name.ends_with('>') { + return None; + } + let argument = &name[generic_start + 1..name.len() - 1]; + (!argument.is_empty()).then_some(argument) +} + +fn rust_short_qualified_type_name(name: &str) -> &str { + let generic_start = name.find('<').unwrap_or(name.len()); + let prefix = &name[..generic_start]; + let start = prefix.rfind("::").map_or(0, |index| index + 2); + &name[start..] +} + +fn rebase_member_projection(base: u64, mut projection: TypeProjection) -> Result { + let offset = base + .checked_add(member_projection_offset(&projection)?) + .ok_or_else(|| anyhow::anyhow!("semantic member projection offset overflow"))?; + projection.layout = TypeProjectionLayout::Member { offset }; + Ok(projection) +} + +fn pointer_width(type_info: &TypeInfo) -> Option { + match strip_type_aliases(type_info) { + TypeInfo::PointerType { size, .. } => Some(*size), + _ => None, + } +} + +fn is_unsigned_scalar(type_info: &TypeInfo) -> bool { + matches!( + strip_type_aliases(type_info), + TypeInfo::BaseType { encoding, .. } + if *encoding == gimli::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::DW_ATE_unsigned_char.0 as u16 + ) +} + +fn is_unsigned_scalar_of_size(type_info: &TypeInfo, expected_size: u64) -> bool { + is_unsigned_scalar(type_info) && type_info.size() == expected_size +} + +fn embedded_value_fits(stride: u64, offset: u64, value_type: &TypeInfo) -> bool { + offset + .checked_add(value_type.size()) + .is_some_and(|end| end <= stride || (stride == 0 && end == 0)) +} + +fn embedded_array_fits(container_size: u64, offset: u64, element_count: u64, stride: u64) -> bool { + element_count + .checked_mul(stride) + .and_then(|size| offset.checked_add(size)) + .is_some_and(|end| end <= container_size) +} + +fn same_layout_type(left: &ResolvedType, right: &ResolvedType) -> bool { + let matching_identity = match ( + left.identity.layout_dwarf_id(), + right.identity.layout_dwarf_id(), + ) { + (Some(left), Some(right)) => left == right, + _ => false, + }; + matching_identity || strip_type_aliases(&left.summary) == strip_type_aliases(&right.summary) +} + +fn member_path_exists(mut current: &TypeInfo, path: &[&str]) -> bool { + for field in path { + let members = match strip_type_aliases(current) { + TypeInfo::StructType { members, .. } | TypeInfo::UnionType { members, .. } => members, + _ => return false, + }; + let mut matches = members.iter().filter(|member| member.name == *field); + let Some(member) = matches.next() else { + return false; + }; + if matches.next().is_some() { + return false; + } + current = &member.member_type; + } + true +} + +fn unique_valid_member<'a>( + members: &'a [StructMember], + name: &str, + container_size: u64, +) -> Option<&'a StructMember> { + let mut matching = members.iter().filter(|member| member.name == name); + let member = matching.next()?; + if matching.next().is_some() + || member.bit_offset.is_some() + || member.bit_size.is_some() + || member + .offset + .checked_add(member.member_type.size()) + .is_none_or(|end| end > container_size) + { + return None; + } + Some(member) +} + +fn unique_non_zst_member(members: &[StructMember], container_size: u64) -> Option<&StructMember> { + let mut matching = members.iter().filter(|member| { + member.member_type.size() > 0 + && member.bit_offset.is_none() + && member.bit_size.is_none() + && member + .offset + .checked_add(member.member_type.size()) + .is_some_and(|end| end <= container_size) + }); + let member = matching.next()?; + matching.next().is_none().then_some(member) +} diff --git a/ghostscope-dwarf/src/language/rust/value.rs b/ghostscope-dwarf/src/language/rust/value.rs new file mode 100644 index 00000000..332ae3df --- /dev/null +++ b/ghostscope-dwarf/src/language/rust/value.rs @@ -0,0 +1,4698 @@ +use crate::{ + strip_type_aliases, HashTableBucketOrder, HashTableOccupancy, ResolvedType, SourceLanguage, + StructMember, TypeInfo, +}; + +use super::plan::{BTreeKind, BTreeLayout, HashTableBucketLayout, HashTableKind, HashTableLayout}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ValueLayout { + IndirectSequence(IndirectSequenceLayout), + ProjectedValue { + value_path: Vec, + presentation: ProjectedValuePresentation, + }, + ProjectedStruct(ProjectedStructLayout), + CompositeStruct(CompositeStructLayout), + HashTable(HashTableLayout), + BTree(BTreeLayout), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectedValuePresentation { + Transparent, + SingleField { + type_name: &'static str, + field_name: &'static str, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ProjectedStructLayout { + pub(crate) type_name: &'static str, + pub(crate) fields: Vec, + pub(crate) presentation: ProjectedStructPresentation, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ProjectedStructField { + pub(crate) name: &'static str, + pub(crate) value_path: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectedStructPresentation { + SignedState { + state_field: &'static str, + non_negative_label: &'static str, + negative_label: &'static str, + }, + ReferenceCounted { + strong_field: &'static str, + weak_field: &'static str, + implicit_weak: u64, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CompositeStructLayout { + pub(crate) type_name: &'static str, + pub(crate) fields: Vec, + pub(crate) presentation: ProjectedStructPresentation, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CompositeStructField { + pub(crate) name: &'static str, + pub(crate) value_path: Vec, + pub(crate) capture: CompositeStructFieldCapture, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CompositeStructFieldCapture { + Value(ProjectedValueRequirement), + Address, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ProjectedPathSegment { + Member(String), + SoleMember, + UnwrapScalar, + Dereference, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectedValueRequirement { + KnownSizedOrZst, + SignedPointerSizedInteger, + UnsignedPointerSizedInteger, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct IndirectSequenceLayout { + pub(crate) data_path: Vec, + pub(crate) addressing: IndirectSequenceAddressing, + pub(crate) kind: IndirectSequenceKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum IndirectSequenceAddressing { + Contiguous { + length_path: Vec, + }, + Ring { + start_path: Vec, + length_path: Vec, + length_kind: RingSequenceLengthKind, + capacity_path: Vec, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RingSequenceLengthKind { + Explicit, + End, +} + +impl IndirectSequenceLayout { + fn contiguous( + data_path: Vec, + length_path: Vec, + kind: IndirectSequenceKind, + ) -> Self { + Self { + data_path, + addressing: IndirectSequenceAddressing::Contiguous { length_path }, + kind, + } + } + + fn ring( + data_path: Vec, + start_path: Vec, + length_path: Vec, + length_kind: RingSequenceLengthKind, + capacity_path: Vec, + kind: IndirectSequenceKind, + ) -> Self { + Self { + data_path, + addressing: IndirectSequenceAddressing::Ring { + start_path, + length_path, + length_kind, + capacity_path, + }, + kind, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum IndirectSequenceKind { + Utf8String, + ByteString, + OpaqueByteString, + PointerTarget, + TypeParameter { index: usize }, +} + +pub(super) fn requires_dwarf_qualified_name(current: &ResolvedType) -> bool { + current.origin.as_ref().is_some_and(|origin| { + origin.language == SourceLanguage::Rust + && matches!(strip_type_aliases(¤t.summary), TypeInfo::StructType { name, .. } + if name == "String" + || name == "OsString" + || name == "PathBuf" + || is_short_vec_name(name) + || is_short_vec_deque_name(name) + || is_short_box_str_name(name) + || is_short_nonzero_name(name) + || is_short_cell_name(name) + || is_short_ref_cell_name(name) + || is_short_ref_name(name) + || is_short_ref_mut_name(name) + || is_short_rc_name(name) + || is_short_arc_name(name) + || is_short_hash_map_name(name) + || is_short_hash_set_name(name) + || is_short_btree_map_name(name) + || is_short_btree_set_name(name)) + }) +} + +pub(super) fn resolve_value_layout( + current: &ResolvedType, + dwarf_qualified_name: Option<&str>, +) -> Option { + if current.origin.as_ref()?.language != SourceLanguage::Rust { + return None; + } + + // rust-gdb does not select printers from the target CU's rustc version. + // Its wrapper adds the invoking toolchain's `lib/rustlib/etc` directory, + // while the target only requests the generic loader through + // `.debug_gdb_scripts`. GhostScope may use `origin.rustc_version()` to + // prioritize candidates, but never as proof of a private layout: every + // branch below must validate type identity, member paths, offsets, and + // widths from the target's DWARF. Any semantic fact DWARF cannot express + // must be documented at the special case that relies on it. + + let TypeInfo::StructType { name, .. } = strip_type_aliases(¤t.summary) else { + return None; + }; + + if is_std_btree_map(name, dwarf_qualified_name) { + return rust_btree_layout(¤t.summary, BTreeKind::Map).map(ValueLayout::BTree); + } + + if is_std_btree_set(name, dwarf_qualified_name) { + return rust_btree_layout(¤t.summary, BTreeKind::Set).map(ValueLayout::BTree); + } + + if is_std_hash_map(name, dwarf_qualified_name) { + return rust_hash_table_layout(¤t.summary, HashTableKind::Map) + .map(ValueLayout::HashTable); + } + + if is_std_hash_set(name, dwarf_qualified_name) { + return rust_hash_table_layout(¤t.summary, HashTableKind::Set) + .map(ValueLayout::HashTable); + } + + if is_std_rc(name, dwarf_qualified_name) { + let pointee_is_str = has_first_generic_argument(name, "Rc", "str") + || dwarf_qualified_name + .is_some_and(|name| has_first_generic_argument(name, "Rc", "str")); + return rust_reference_counted_layout(¤t.summary, "Rc", "value", pointee_is_str) + .map(ValueLayout::CompositeStruct); + } + + if is_std_arc(name, dwarf_qualified_name) { + let pointee_is_str = has_first_generic_argument(name, "Arc", "str") + || dwarf_qualified_name + .is_some_and(|name| has_first_generic_argument(name, "Arc", "str")); + return rust_reference_counted_layout(¤t.summary, "Arc", "data", pointee_is_str) + .map(ValueLayout::CompositeStruct); + } + + if is_std_ref(name, dwarf_qualified_name) || is_std_ref_mut(name, dwarf_qualified_name) { + return rust_ref_layout(¤t.summary).map(ValueLayout::CompositeStruct); + } + + if is_std_ref_cell(name, dwarf_qualified_name) { + return rust_ref_cell_layout(¤t.summary).map(ValueLayout::ProjectedStruct); + } + + if is_std_cell(name, dwarf_qualified_name) { + return rust_cell_value_path(¤t.summary).map(|value_path| { + ValueLayout::ProjectedValue { + value_path, + presentation: ProjectedValuePresentation::SingleField { + type_name: "Cell", + field_name: "value", + }, + } + }); + } + + if is_std_nonzero(name, dwarf_qualified_name) { + return rust_nonzero_value_path(¤t.summary).map(|value_path| { + ValueLayout::ProjectedValue { + value_path, + presentation: ProjectedValuePresentation::Transparent, + } + }); + } + + // rustc's bundled GDB printers have treated slice-like DWARF values as + // `data_ptr` plus `length` since Rust 1.0, without field-name compatibility + // branches. Older printers also removed explicit `'static` lifetimes before + // classifying references. These are rustc debuginfo conventions, not Rust + // ABI guarantees, so retain the language and structural checks around them. + // See rust-lang/rust's `src/etc/gdb_rust_pretty_printing.py` and + // `src/etc/gdb_providers.py`. + let sequence = if is_std_path_ref_name(name) { + // Rust's LLDB provider renders Path as its underlying platform bytes. + // GDB classifies the same identity but does not currently register a + // provider. Only current DWARF with explicit fat-pointer metadata can + // satisfy this validator; older Path* DIEs remain native DWARF. + validate_indirect_sequence_layout( + ¤t.summary, + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::OpaqueByteString, + ) + } else if matches!( + name.as_str(), + "&str" | "&mut str" | "&'static str" | "&'static mut str" + ) { + validate_indirect_sequence_layout( + ¤t.summary, + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::Utf8String, + ) + } else if is_slice_name(name) { + validate_indirect_sequence_layout( + ¤t.summary, + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::PointerTarget, + ) + } else if is_std_box_str(name, dwarf_qualified_name) { + // Rust 1.96 and older rust-gdb scripts had no Box provider. The + // current provider has no version fallback and reads data_ptr/length. + // Accept the pre-allocator generic spelling when DWARF emits it, but + // derive every physical detail from the concrete DIE. + validate_indirect_sequence_layout( + ¤t.summary, + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::Utf8String, + ) + // rustc commonly stores only `String` in DW_AT_name and represents + // `alloc::string` with enclosing namespace DIEs. GDB presents the + // reconstructed qualified name to its Rust printer. Trust the equivalent + // TypeId-backed name from the analyzer for identity; the member checks + // below remain responsible only for version-specific physical layout. + } else if is_std_string(name, dwarf_qualified_name) { + rust_string_layout(¤t.summary) + } else if is_std_os_string(name, dwarf_qualified_name) { + rust_os_string_layout(¤t.summary) + } else if is_std_path_buf(name, dwarf_qualified_name) { + rust_path_buf_layout(¤t.summary) + } else if is_std_vec(name, dwarf_qualified_name) { + rust_vec_layout(¤t.summary) + } else if is_std_vec_deque(name, dwarf_qualified_name) { + rust_vec_deque_layout(¤t.summary) + } else { + None + }; + + sequence.map(ValueLayout::IndirectSequence) +} + +fn is_slice_name(name: &str) -> bool { + // Rust 1.30's GDB support stripped an explicit `'static` lifetime before + // applying the same &[T]/&mut [T] classification used by current rust-gdb. + let Some(referenced) = name + .strip_prefix("&'static ") + .or_else(|| name.strip_prefix('&')) + else { + return false; + }; + let referenced = referenced.strip_prefix("mut ").unwrap_or(referenced); + let Some(element) = referenced + .strip_prefix('[') + .and_then(|name| name.strip_suffix(']')) + else { + return false; + }; + + !element.is_empty() +} + +fn is_short_vec_name(name: &str) -> bool { + name.starts_with("Vec<") && name.ends_with('>') +} + +fn is_short_vec_deque_name(name: &str) -> bool { + name.starts_with("VecDeque<") && name.ends_with('>') +} + +fn is_short_box_str_name(name: &str) -> bool { + box_str_arguments(name, "Box<").is_some() +} + +fn is_short_nonzero_name(name: &str) -> bool { + is_generic_nonzero_name(name) || is_legacy_nonzero_name(name) +} + +fn is_short_cell_name(name: &str) -> bool { + name.strip_prefix("Cell<") + .and_then(|arguments| arguments.strip_suffix('>')) + .is_some_and(|arguments| !arguments.is_empty()) +} + +fn is_short_ref_cell_name(name: &str) -> bool { + name.strip_prefix("RefCell<") + .and_then(|arguments| arguments.strip_suffix('>')) + .is_some_and(|arguments| !arguments.is_empty()) +} + +fn is_short_ref_name(name: &str) -> bool { + name.strip_prefix("Ref<") + .and_then(|arguments| arguments.strip_suffix('>')) + .is_some_and(|arguments| !arguments.is_empty()) +} + +fn is_short_ref_mut_name(name: &str) -> bool { + name.strip_prefix("RefMut<") + .and_then(|arguments| arguments.strip_suffix('>')) + .is_some_and(|arguments| !arguments.is_empty()) +} + +fn is_short_rc_name(name: &str) -> bool { + is_short_generic_name(name, "Rc") +} + +fn is_short_arc_name(name: &str) -> bool { + is_short_generic_name(name, "Arc") +} + +fn is_short_hash_map_name(name: &str) -> bool { + is_short_generic_name(name, "HashMap") +} + +fn is_short_hash_set_name(name: &str) -> bool { + is_short_generic_name(name, "HashSet") +} + +fn is_short_btree_map_name(name: &str) -> bool { + is_short_generic_name(name, "BTreeMap") +} + +fn is_short_btree_set_name(name: &str) -> bool { + is_short_generic_name(name, "BTreeSet") +} + +fn is_short_generic_name(name: &str, type_name: &str) -> bool { + let prefix = format!("{type_name}<"); + name.strip_prefix(&prefix) + .and_then(|arguments| arguments.strip_suffix('>')) + .is_some_and(|arguments| !arguments.is_empty()) +} + +fn has_first_generic_argument(name: &str, type_name: &str, expected: &str) -> bool { + let short_prefix = format!("{type_name}<"); + let qualified_marker = format!("::{type_name}<"); + let arguments = name.strip_prefix(&short_prefix).or_else(|| { + name.split_once(&qualified_marker) + .map(|(_, arguments)| arguments) + }); + arguments + .and_then(|arguments| arguments.strip_suffix('>')) + .and_then(|arguments| arguments.split(',').next()) + .is_some_and(|argument| argument.trim() == expected) +} + +fn is_std_cell(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_cell_name(name) + || (is_short_cell_name(name) && dwarf_qualified_name.is_some_and(is_std_cell_name)) +} + +fn is_std_cell_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("core::") else { + return false; + }; + let Some((module, arguments)) = path.split_once("::Cell<") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn is_std_ref_cell(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_ref_cell_name(name) + || (is_short_ref_cell_name(name) && dwarf_qualified_name.is_some_and(is_std_ref_cell_name)) +} + +fn is_std_ref_cell_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("core::") else { + return false; + }; + let Some((module, arguments)) = path.split_once("::RefCell<") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn is_std_ref(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_ref_name(name) + || (is_short_ref_name(name) && dwarf_qualified_name.is_some_and(is_std_ref_name)) +} + +fn is_std_ref_name(name: &str) -> bool { + is_std_core_generic_name(name, "Ref") +} + +fn is_std_ref_mut(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_ref_mut_name(name) + || (is_short_ref_mut_name(name) && dwarf_qualified_name.is_some_and(is_std_ref_mut_name)) +} + +fn is_std_ref_mut_name(name: &str) -> bool { + is_std_core_generic_name(name, "RefMut") +} + +fn is_std_rc(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_rc_name(name) + || (is_short_rc_name(name) && dwarf_qualified_name.is_some_and(is_std_rc_name)) +} + +fn is_std_rc_name(name: &str) -> bool { + is_std_alloc_generic_name(name, "Rc") +} + +fn is_std_arc(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_arc_name(name) + || (is_short_arc_name(name) && dwarf_qualified_name.is_some_and(is_std_arc_name)) +} + +fn is_std_arc_name(name: &str) -> bool { + is_std_alloc_generic_name(name, "Arc") +} + +fn is_std_hash_map(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_hash_map_name(name) + || (is_short_hash_map_name(name) && dwarf_qualified_name.is_some_and(is_std_hash_map_name)) +} + +fn is_std_hash_map_name(name: &str) -> bool { + is_std_collections_generic_name(name, "HashMap") +} + +fn is_std_hash_set(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_hash_set_name(name) + || (is_short_hash_set_name(name) && dwarf_qualified_name.is_some_and(is_std_hash_set_name)) +} + +fn is_std_hash_set_name(name: &str) -> bool { + is_std_collections_generic_name(name, "HashSet") +} + +fn is_std_btree_map(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_btree_map_name(name) + || (is_short_btree_map_name(name) + && dwarf_qualified_name.is_some_and(is_std_btree_map_name)) +} + +fn is_std_btree_map_name(name: &str) -> bool { + is_std_alloc_generic_name(name, "BTreeMap") +} + +fn is_std_btree_set(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_btree_set_name(name) + || (is_short_btree_set_name(name) + && dwarf_qualified_name.is_some_and(is_std_btree_set_name)) +} + +fn is_std_btree_set_name(name: &str) -> bool { + is_std_alloc_generic_name(name, "BTreeSet") +} + +fn is_std_collections_generic_name(name: &str, type_name: &str) -> bool { + let Some(path) = name.strip_prefix("std::collections::") else { + return false; + }; + let marker = format!("::{type_name}<"); + let Some((module, arguments)) = path.split_once(&marker) else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn is_std_core_generic_name(name: &str, type_name: &str) -> bool { + let Some(path) = name.strip_prefix("core::") else { + return false; + }; + let marker = format!("::{type_name}<"); + let Some((module, arguments)) = path.split_once(&marker) else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn is_std_alloc_generic_name(name: &str, type_name: &str) -> bool { + let Some(path) = name.strip_prefix("alloc::") else { + return false; + }; + let marker = format!("::{type_name}<"); + let Some((module, arguments)) = path.split_once(&marker) else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn is_generic_nonzero_name(name: &str) -> bool { + name.strip_prefix("NonZero<") + .and_then(|arguments| arguments.strip_suffix('>')) + .is_some_and(|arguments| !arguments.is_empty()) +} + +fn is_legacy_nonzero_name(name: &str) -> bool { + matches!( + name, + "NonZeroI8" + | "NonZeroI16" + | "NonZeroI32" + | "NonZeroI64" + | "NonZeroI128" + | "NonZeroIsize" + | "NonZeroU8" + | "NonZeroU16" + | "NonZeroU32" + | "NonZeroU64" + | "NonZeroU128" + | "NonZeroUsize" + ) +} + +fn is_std_nonzero(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_nonzero_name(name) + || (is_short_nonzero_name(name) && dwarf_qualified_name.is_some_and(is_std_nonzero_name)) +} + +fn is_std_nonzero_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("core::") else { + return false; + }; + let Some((module, type_name)) = path.rsplit_once("::") else { + return false; + }; + + let valid_module = !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }); + valid_module + && (is_generic_nonzero_name(type_name) + || ((module == "num" || module.starts_with("num::")) + && is_legacy_nonzero_name(type_name))) +} + +fn is_std_box_str(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_box_str_name(name) + || (is_short_box_str_name(name) && dwarf_qualified_name.is_some_and(is_std_box_str_name)) +} + +fn is_std_box_str_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("alloc::") else { + return false; + }; + let Some((module, _)) = path + .split_once("::Box<") + .filter(|(_, arguments)| box_str_arguments(arguments, "").is_some()) + else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) +} + +fn box_str_arguments<'a>(name: &'a str, prefix: &str) -> Option<&'a str> { + let arguments = name.strip_prefix(prefix)?.strip_suffix('>')?; + (arguments == "str" + || arguments + .strip_prefix("str,") + .is_some_and(|allocator| !allocator.is_empty())) + .then_some(arguments) +} + +fn is_std_string(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_string_name(name) + || (name == "String" && dwarf_qualified_name.is_some_and(is_std_string_name)) +} + +fn is_std_string_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("alloc::") else { + return false; + }; + let Some(module) = path.strip_suffix("::String") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) +} + +fn is_std_os_string(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_os_string_name(name) + || (name == "OsString" && dwarf_qualified_name.is_some_and(is_std_os_string_name)) +} + +fn is_std_os_string_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("std::ffi::") else { + return false; + }; + let Some(module) = path.strip_suffix("::OsString") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) +} + +fn is_std_path_ref_name(name: &str) -> bool { + let Some(referenced) = name + .strip_prefix("&'static ") + .or_else(|| name.strip_prefix('&')) + else { + return false; + }; + let referenced = referenced.strip_prefix("mut ").unwrap_or(referenced); + is_std_path_name(referenced) +} + +fn is_std_path_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("std::") else { + return false; + }; + let Some(module) = path.strip_suffix("::Path") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) +} + +fn is_std_path_buf(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_path_buf_name(name) + || (name == "PathBuf" && dwarf_qualified_name.is_some_and(is_std_path_buf_name)) +} + +fn is_std_path_buf_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("std::") else { + return false; + }; + let Some(module) = path.strip_suffix("::PathBuf") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) +} + +fn is_std_vec(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_vec_name(name) + || (is_short_vec_name(name) && dwarf_qualified_name.is_some_and(is_std_vec_name)) +} + +fn is_std_vec_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("alloc::") else { + return false; + }; + let Some((module, arguments)) = path.split_once("::Vec<") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn is_std_vec_deque(name: &str, dwarf_qualified_name: Option<&str>) -> bool { + is_std_vec_deque_name(name) + || (is_short_vec_deque_name(name) + && dwarf_qualified_name.is_some_and(is_std_vec_deque_name)) +} + +fn is_std_vec_deque_name(name: &str) -> bool { + let Some(path) = name.strip_prefix("alloc::") else { + return false; + }; + let Some((module, arguments)) = path.split_once("::VecDeque<") else { + return false; + }; + + !module.is_empty() + && module.split("::").all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'_') + }) + && !arguments.is_empty() + && arguments.ends_with('>') +} + +fn rust_nonzero_value_path(root: &TypeInfo) -> Option> { + // Rust 1.65-1.78 rust-gdb follows one sole member for legacy names such as + // NonZeroU32. Rust 1.79 through the current 1.98 nightly follows a second + // sole member for generic NonZero. Neither printer depends on field + // names, so derive the path, offsets, and widths from the concrete DIE. + let TypeInfo::StructType { members, .. } = strip_type_aliases(root) else { + return None; + }; + let [outer] = members.as_slice() else { + return None; + }; + let mut value_path = vec![outer.name.clone()]; + if let TypeInfo::StructType { + members: inner_members, + .. + } = strip_type_aliases(&outer.member_type) + { + let [inner] = inner_members.as_slice() else { + return None; + }; + value_path.push(inner.name.clone()); + }; + let value = resolve_member_path(root, &value_path)?; + let TypeInfo::BaseType { size, encoding, .. } = strip_type_aliases(value.member_type) else { + return None; + }; + let integer_encoding = matches!( + *encoding, + encoding if encoding == gimli::DW_ATE_signed.0 as u16 + || encoding == gimli::DW_ATE_signed_char.0 as u16 + || encoding == gimli::DW_ATE_unsigned.0 as u16 + || encoding == gimli::DW_ATE_unsigned_char.0 as u16 + ); + let value_end = value.offset.checked_add(*size)?; + + (*size > 0 && integer_encoding && value_end <= root.size()).then_some(value_path) +} + +fn rust_cell_value_path(root: &TypeInfo) -> Option> { + // Rust 1.46 through the current 1.98 nightly rust-gdb provider reads two + // nested `value` members. The provider has no version-specific fallback, + // so validate the same unique-member shape but derive names, offsets, and + // the projected type exclusively from the concrete DWARF DIEs. + let TypeInfo::StructType { members, .. } = strip_type_aliases(root) else { + return None; + }; + let [outer] = members.as_slice() else { + return None; + }; + let TypeInfo::StructType { + members: inner_members, + .. + } = strip_type_aliases(&outer.member_type) + else { + return None; + }; + let [inner] = inner_members.as_slice() else { + return None; + }; + let value_path = vec![outer.name.clone(), inner.name.clone()]; + let value = resolve_member_path_allowing_zst(root, &value_path)?; + let value_end = value.offset.checked_add(value.member_type.size())?; + + (!matches!( + strip_type_aliases(value.member_type), + TypeInfo::UnknownType { .. } | TypeInfo::OptimizedOut { .. } + ) && value_end <= root.size()) + .then_some(value_path) +} + +fn rust_ref_cell_layout(root: &TypeInfo) -> Option { + // Rust 1.46 through the current 1.98 nightly rust-gdb provider reads + // `value.value` and `borrow.value.value` without a version fallback. The + // outer names establish those semantic roles; all inner names, offsets, + // widths, and projected types below come from the concrete DWARF DIEs. + let TypeInfo::StructType { members, .. } = strip_type_aliases(root) else { + return None; + }; + let value_outer = unique_named_member(members, "value")?; + let borrow_outer = unique_named_member(members, "borrow")?; + + let value_inner = sole_struct_member(&value_outer.member_type)?; + let value_path = vec![value_outer.name.clone(), value_inner.name.clone()]; + + let borrow_cell_inner = sole_struct_member(&borrow_outer.member_type)?; + let borrow_inner = sole_struct_member(&borrow_cell_inner.member_type)?; + let borrow_path = vec![ + borrow_outer.name.clone(), + borrow_cell_inner.name.clone(), + borrow_inner.name.clone(), + ]; + + let value = resolve_member_path_allowing_zst(root, &value_path)?; + let borrow = resolve_member_path(root, &borrow_path)?; + let TypeInfo::BaseType { size, encoding, .. } = strip_type_aliases(borrow.member_type) else { + return None; + }; + let signed_state = *encoding == gimli::DW_ATE_signed.0 as u16 + || *encoding == gimli::DW_ATE_signed_char.0 as u16; + let supported_width = matches!(*size, 1 | 2 | 4 | 8 | 16); + let value_size = value.member_type.size(); + let value_end = value.offset.checked_add(value_size)?; + let borrow_end = borrow.offset.checked_add(*size)?; + let fields_overlap = value_size > 0 && value.offset < borrow_end && borrow.offset < value_end; + + if matches!( + strip_type_aliases(value.member_type), + TypeInfo::UnknownType { .. } | TypeInfo::OptimizedOut { .. } + ) || !signed_state + || !supported_width + || value_end > root.size() + || borrow_end > root.size() + || fields_overlap + { + return None; + } + + Some(ProjectedStructLayout { + type_name: "RefCell", + fields: vec![ + ProjectedStructField { + name: "value", + value_path, + }, + ProjectedStructField { + name: "borrow", + value_path: borrow_path, + }, + ], + presentation: ProjectedStructPresentation::SignedState { + state_field: "borrow", + non_negative_label: "borrow", + negative_label: "borrow_mut", + }, + }) +} + +fn rust_ref_layout(root: &TypeInfo) -> Option { + // The rust-gdb providers in Rust 1.46, 1.60, 1.70, 1.81, 1.88, 1.93, + // 1.95, and 1.98 use one provider for Ref and RefMut. It dereferences + // `value` and reads `borrow.borrow.value.value`, with no version fallback. + // Rust 1.49 DWARF emits `value` as a raw pointer, while DWARF inspected + // from Rust 1.81 through 1.98 emits a one-member NonNull wrapper. Follow + // the concrete DIE in either case and keep every pointer read explicit + // instead of assuming wrapper offsets. + let TypeInfo::StructType { + size: root_size, + members, + .. + } = strip_type_aliases(root) + else { + return None; + }; + let value_outer = unique_named_member(members, "value")?; + let borrow_outer = unique_named_member(members, "borrow")?; + let value_outer_range = member_range(value_outer, *root_size)?; + let borrow_outer_range = member_range(borrow_outer, *root_size)?; + if ranges_overlap(value_outer_range, borrow_outer_range) { + return None; + } + + let mut value_path = vec![ProjectedPathSegment::Member(value_outer.name.clone())]; + let value_pointer_size = match strip_type_aliases(&value_outer.member_type) { + TypeInfo::PointerType { size, .. } => *size, + TypeInfo::StructType { size, .. } => { + let value_pointer = sole_struct_member(&value_outer.member_type)?; + member_range(value_pointer, *size)?; + let TypeInfo::PointerType { size, .. } = strip_type_aliases(&value_pointer.member_type) + else { + return None; + }; + value_path.push(ProjectedPathSegment::Member(value_pointer.name.clone())); + *size + } + _ => return None, + }; + value_path.push(ProjectedPathSegment::Dereference); + + let borrow_wrapper_size = borrow_outer.member_type.size(); + let borrow_pointer = sole_struct_member(&borrow_outer.member_type)?; + member_range(borrow_pointer, borrow_wrapper_size)?; + let TypeInfo::PointerType { + size: borrow_pointer_size, + .. + } = strip_type_aliases(&borrow_pointer.member_type) + else { + return None; + }; + let borrow_path = vec![ + ProjectedPathSegment::Member(borrow_outer.name.clone()), + ProjectedPathSegment::Member(borrow_pointer.name.clone()), + ProjectedPathSegment::Dereference, + ProjectedPathSegment::SoleMember, + ProjectedPathSegment::SoleMember, + ]; + + let supported_pointer_width = + matches!(value_pointer_size, 4 | 8) && value_pointer_size == *borrow_pointer_size; + if !supported_pointer_width { + return None; + } + + Some(CompositeStructLayout { + // rust-gdb deliberately uses the same summary for Ref and RefMut. + type_name: "Ref", + fields: vec![ + CompositeStructField { + name: "*value", + value_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::KnownSizedOrZst, + ), + }, + CompositeStructField { + name: "borrow", + value_path: borrow_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::SignedPointerSizedInteger, + ), + }, + ], + presentation: ProjectedStructPresentation::SignedState { + state_field: "borrow", + non_negative_label: "borrow", + negative_label: "borrow_mut", + }, + }) +} + +fn rust_reference_counted_layout( + root: &TypeInfo, + type_name: &'static str, + value_member: &'static str, + pointee_is_str: bool, +) -> Option { + // Rust 1.46, 1.60, 1.70, 1.81, 1.88, 1.93, and 1.95 use the same + // rust-gdb provider paths for Rc and Arc. Rust 1.98 replaces the fixed + // AtomicUsize path with scalar-wrapper unwrapping because Atomic + // gained an alignment wrapper. Actual DWARF also renamed RcBox to RcInner + // between 1.81 and 1.88. None of those private names or depths are needed + // here: named semantic members select values, while every wrapper offset + // and final scalar width comes from the concrete DIE. + // + // A sized pointee uses a thin pointer. Slice-like DSTs use rustc's + // synthetic data_ptr/length aggregate in newer DWARF, while older rustc + // versions may expose only the allocation pointer. We never infer or read + // the omitted metadata here: an unsized pointee is represented by its + // DWARF-projected address. This keeps Rc and Arc on one capture path and + // avoids treating the first byte of str as a complete value. + let TypeInfo::StructType { + size: root_size, + members, + .. + } = strip_type_aliases(root) + else { + return None; + }; + let ptr_outer = unique_named_member(members, "ptr")?; + member_range(ptr_outer, *root_size)?; + let TypeInfo::StructType { + size: ptr_wrapper_size, + members: ptr_members, + .. + } = strip_type_aliases(&ptr_outer.member_type) + else { + return None; + }; + let pointer = unique_named_member(ptr_members, "pointer")?; + member_range(pointer, *ptr_wrapper_size)?; + let mut inner_path = vec![ + ProjectedPathSegment::Member(ptr_outer.name.clone()), + ProjectedPathSegment::Member(pointer.name.clone()), + ]; + let address_only = if let Some(raw_pointer) = scalar_wrapper_target(&pointer.member_type) { + let TypeInfo::PointerType { + target_type, + size: pointer_size, + } = strip_type_aliases(raw_pointer) + else { + return None; + }; + if !matches!(*pointer_size, 4 | 8) { + return None; + } + inner_path.push(ProjectedPathSegment::UnwrapScalar); + pointee_is_str || reference_counted_target_is_unsized(target_type, value_member) + } else { + let data_ptr = slice_wide_pointer_data_member(pointer)?; + inner_path.push(ProjectedPathSegment::Member(data_ptr.name.clone())); + true + }; + inner_path.push(ProjectedPathSegment::Dereference); + + let mut value_path = inner_path.clone(); + value_path.push(ProjectedPathSegment::Member(value_member.to_string())); + let mut strong_path = inner_path.clone(); + strong_path.push(ProjectedPathSegment::Member("strong".to_string())); + strong_path.push(ProjectedPathSegment::UnwrapScalar); + let mut weak_path = inner_path; + weak_path.push(ProjectedPathSegment::Member("weak".to_string())); + weak_path.push(ProjectedPathSegment::UnwrapScalar); + + Some(CompositeStructLayout { + type_name, + fields: vec![ + CompositeStructField { + name: if address_only { "ptr" } else { "value" }, + value_path, + capture: if address_only { + CompositeStructFieldCapture::Address + } else { + CompositeStructFieldCapture::Value(ProjectedValueRequirement::KnownSizedOrZst) + }, + }, + CompositeStructField { + name: "strong", + value_path: strong_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::UnsignedPointerSizedInteger, + ), + }, + CompositeStructField { + name: "weak", + value_path: weak_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::UnsignedPointerSizedInteger, + ), + }, + ], + // Rc/Arc allocations hold one implicit weak entry while strong + // owners exist. rust-gdb subtracts it from the public weak count. + presentation: ProjectedStructPresentation::ReferenceCounted { + strong_field: "strong", + weak_field: "weak", + implicit_weak: 1, + }, + }) +} + +fn reference_counted_target_is_unsized(target: &TypeInfo, value_member: &str) -> bool { + let TypeInfo::StructType { members, .. } = strip_type_aliases(target) else { + return false; + }; + let Some(value) = unique_named_member(members, value_member) else { + return false; + }; + matches!( + strip_type_aliases(&value.member_type), + TypeInfo::ArrayType { + total_size: None, + .. + } + ) +} + +fn slice_wide_pointer_data_member(pointer: &StructMember) -> Option<&StructMember> { + let TypeInfo::StructType { size, members, .. } = strip_type_aliases(&pointer.member_type) + else { + return None; + }; + let data = unique_named_member(members, "data_ptr")?; + let length = unique_named_member(members, "length")?; + let data_range = member_range(data, *size)?; + let length_range = member_range(length, *size)?; + if ranges_overlap(data_range, length_range) { + return None; + } + let TypeInfo::PointerType { + size: pointer_size, .. + } = strip_type_aliases(&data.member_type) + else { + return None; + }; + let TypeInfo::BaseType { + size: length_size, + encoding, + .. + } = strip_type_aliases(&length.member_type) + else { + return None; + }; + (matches!(*pointer_size, 4 | 8) + && pointer_size == length_size + && *encoding == gimli::DW_ATE_unsigned.0 as u16) + .then_some(data) +} + +fn rust_hash_table_layout(root: &TypeInfo, kind: HashTableKind) -> Option { + // Rust 1.36-1.51 expose RawTable's metadata directly; Rust 1.52 moved it + // under RawTable.table. HashSet wrapped std::HashMap through Rust 1.47 and + // now wraps hashbrown::HashSet. rust-gdb keeps all four path combinations. + // hashbrown also removed its dedicated `data` pointer: current tables place + // entries immediately before `ctrl`, in reverse control-index order. These + // are semantic compatibility branches only. Every selected member offset, + // pointer width, entry type, and entry field layout is resolved from DWARF. + // rust-gdb also retains a separate provider for Rust 1.35's pre-hashbrown + // table. That layout stores pointer-sized hash words followed by aligned + // pair storage in one allocation; validate it only after all hashbrown + // paths fail. + const MAP_PATHS: &[(&[&str], &[&str])] = &[ + (&["base", "table"], &["base", "table", "table"]), + (&["base", "table"], &["base", "table"]), + ]; + const SET_PATHS: &[(&[&str], &[&str])] = &[ + ( + &["base", "map", "table"], + &["base", "map", "table", "table"], + ), + (&["base", "map", "table"], &["base", "map", "table"]), + ( + &["map", "base", "table"], + &["map", "base", "table", "table"], + ), + (&["map", "base", "table"], &["map", "base", "table"]), + ]; + + let paths = match kind { + HashTableKind::Map => MAP_PATHS, + HashTableKind::Set => SET_PATHS, + }; + for (table_fields, metadata_fields) in paths { + if let Some(layout) = validate_hash_table_layout( + root, + field_path(table_fields), + field_path(metadata_fields), + kind, + ) { + return Some(layout); + } + } + validate_legacy_hash_table_layout(root, kind) +} + +fn rust_btree_layout(root: &TypeInfo, kind: BTreeKind) -> Option { + // rust-gdb reads `length` and `root` from BTreeMap, while BTreeSet + // delegates to its `map` member. Rust 1.35 stored Root directly and newer + // releases wrap Root/NodeRef in Option, so the analyzer resolves those + // concrete DIEs instead of encoding either shape in this classifier. + let map_path = match kind { + BTreeKind::Map => Vec::new(), + BTreeKind::Set => field_path(&["map"]), + }; + let map = if map_path.is_empty() { + root + } else { + resolve_member_path(root, &map_path)?.member_type + }; + let TypeInfo::StructType { + size: map_size, + members, + .. + } = strip_type_aliases(map) + else { + return None; + }; + let root_member = unique_named_member(members, "root")?; + let length_member = unique_named_member(members, "length")?; + member_range(root_member, *map_size)?; + member_range(length_member, *map_size)?; + if ranges_overlap( + ( + root_member.offset, + root_member + .offset + .checked_add(root_member.member_type.size())?, + ), + ( + length_member.offset, + length_member + .offset + .checked_add(length_member.member_type.size())?, + ), + ) { + return None; + } + let root_size = match strip_type_aliases(&root_member.member_type) { + TypeInfo::StructType { size, .. } | TypeInfo::VariantType { size, .. } => *size, + _ => return None, + }; + let TypeInfo::BaseType { + size: length_size, + encoding, + .. + } = strip_type_aliases(&length_member.member_type) + else { + return None; + }; + if root_size == 0 + || !matches!(*length_size, 4 | 8) + || *encoding != gimli::DW_ATE_unsigned.0 as u16 + { + return None; + } + + let mut root_path = map_path.clone(); + root_path.push(root_member.name.clone()); + let mut length_path = map_path.clone(); + length_path.push(length_member.name.clone()); + Some(BTreeLayout { + map_path, + root_path, + length_path, + kind, + }) +} + +fn validate_hash_table_layout( + root: &TypeInfo, + table_path: Vec, + metadata_path: Vec, + kind: HashTableKind, +) -> Option { + let table = resolve_member_path(root, &table_path)?; + if !matches!( + strip_type_aliases(table.member_type), + TypeInfo::StructType { .. } + ) { + return None; + } + let metadata = resolve_member_path(root, &metadata_path)?; + let TypeInfo::StructType { + members: metadata_members, + .. + } = strip_type_aliases(metadata.member_type) + else { + return None; + }; + + let mut control_path = metadata_path.clone(); + control_path.push("ctrl".to_string()); + let control_path = wrapped_pointer_path(root, control_path)?; + let control = resolve_member_path(root, &control_path)?; + let TypeInfo::PointerType { + target_type, + size: pointer_size, + } = strip_type_aliases(control.member_type) + else { + return None; + }; + let byte_control = matches!( + strip_type_aliases(target_type), + TypeInfo::BaseType { size: 1, encoding, .. } + if *encoding == gimli::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::DW_ATE_unsigned_char.0 as u16 + ); + if *pointer_size == 0 || !byte_control { + return None; + } + + let mut length_path = metadata_path.clone(); + length_path.push("items".to_string()); + let length_path = unsigned_metadata_path(root, length_path, *pointer_size)?; + let mut bucket_mask_path = metadata_path.clone(); + bucket_mask_path.push("bucket_mask".to_string()); + let bucket_mask_path = unsigned_metadata_path(root, bucket_mask_path, *pointer_size)?; + + let has_data = unique_named_member(metadata_members, "data").is_some(); + let (buckets, bucket_order) = if has_data { + let mut data_path = metadata_path; + data_path.push("data".to_string()); + let data_path = wrapped_pointer_path(root, data_path)?; + let data = resolve_member_path(root, &data_path)?; + let TypeInfo::PointerType { + size: data_pointer_size, + .. + } = strip_type_aliases(data.member_type) + else { + return None; + }; + if data_pointer_size != pointer_size { + return None; + } + ( + HashTableBucketLayout::Forward { data_path }, + HashTableBucketOrder::Forward, + ) + } else { + ( + HashTableBucketLayout::ReverseFromControl, + HashTableBucketOrder::Reverse, + ) + }; + + let mut ranges = Vec::with_capacity(4); + for path in [&control_path, &length_path, &bucket_mask_path] { + let member = resolve_member_path(root, path)?; + let end = member.offset.checked_add(member.member_type.size())?; + if end > root.size() { + return None; + } + ranges.push((member.offset, end)); + } + if let HashTableBucketLayout::Forward { data_path } = &buckets { + let member = resolve_member_path(root, data_path)?; + let end = member.offset.checked_add(member.member_type.size())?; + if end > root.size() { + return None; + } + ranges.push((member.offset, end)); + } + for (index, left) in ranges.iter().enumerate() { + if ranges[index + 1..] + .iter() + .any(|right| ranges_overlap(*left, *right)) + { + return None; + } + } + + Some(HashTableLayout { + entry_type_path: table_path, + control_path, + length_path, + bucket_mask_path, + occupancy: HashTableOccupancy::ControlByteHighBitClear, + buckets, + bucket_order, + kind, + }) +} + +fn validate_legacy_hash_table_layout( + root: &TypeInfo, + kind: HashTableKind, +) -> Option { + let table_path = match kind { + HashTableKind::Map => field_path(&["table"]), + HashTableKind::Set => field_path(&["map", "table"]), + }; + let table = resolve_member_path(root, &table_path)?; + let TypeInfo::StructType { + size: table_size, + members, + .. + } = strip_type_aliases(table.member_type) + else { + return None; + }; + if members.len() != 4 { + return None; + } + let capacity_mask = unique_named_member(members, "capacity_mask")?; + let size = unique_named_member(members, "size")?; + let hashes = unique_named_member(members, "hashes")?; + let marker = unique_named_member(members, "marker")?; + for member in [capacity_mask, size, hashes, marker] { + member_range(member, *table_size)?; + } + if !matches!( + strip_type_aliases(&marker.member_type), + TypeInfo::StructType { size: 0, .. } + ) { + return None; + } + + let tagged_hash = sole_struct_member(&hashes.member_type)?; + member_range(tagged_hash, hashes.member_type.size())?; + let mut control_path = table_path.clone(); + control_path.push(hashes.name.clone()); + control_path.push(tagged_hash.name.clone()); + let control_path = wrapped_pointer_path(root, control_path)?; + let control = resolve_member_path(root, &control_path)?; + let TypeInfo::PointerType { + target_type, + size: pointer_size, + } = strip_type_aliases(control.member_type) + else { + return None; + }; + let TypeInfo::BaseType { + size: word_size, + encoding, + .. + } = strip_type_aliases(target_type) + else { + return None; + }; + if !matches!(*pointer_size, 4 | 8) + || word_size != pointer_size + || *encoding != gimli::DW_ATE_unsigned.0 as u16 + { + return None; + } + + let mut length_path = table_path.clone(); + length_path.push(size.name.clone()); + let length_path = unsigned_metadata_path(root, length_path, *pointer_size)?; + let mut bucket_mask_path = table_path.clone(); + bucket_mask_path.push(capacity_mask.name.clone()); + let bucket_mask_path = unsigned_metadata_path(root, bucket_mask_path, *pointer_size)?; + let mut entry_type_path = table_path; + entry_type_path.push(marker.name.clone()); + + let paths = [&control_path, &length_path, &bucket_mask_path]; + let mut ranges = Vec::with_capacity(paths.len()); + for path in paths { + let member = resolve_member_path(root, path)?; + let end = member.offset.checked_add(member.member_type.size())?; + if end > root.size() { + return None; + } + ranges.push((member.offset, end)); + } + for (index, left) in ranges.iter().enumerate() { + if ranges[index + 1..] + .iter() + .any(|right| ranges_overlap(*left, *right)) + { + return None; + } + } + + Some(HashTableLayout { + entry_type_path, + control_path, + length_path, + bucket_mask_path, + occupancy: HashTableOccupancy::NonZeroWord { + word_size: *word_size, + }, + // TaggedHashUintPtr reserves its low pointer bit in Rust 1.35. This is + // the one implementation semantic unavailable in DWARF; widths, + // offsets, entry layout, and alignment remain DWARF-derived. + buckets: HashTableBucketLayout::LegacyAfterControl { + pointer_tag_mask: 1, + }, + bucket_order: HashTableBucketOrder::Forward, + kind, + }) +} + +fn rust_string_layout(root: &TypeInfo) -> Option { + // rust-gdb uses `buf.ptr` through Rust 1.81 and `buf.inner.ptr` from + // Rust 1.82 onward. Keep both paths and validate whichever DWARF exposes. + const DATA_PATHS: &[&[&str]] = &[ + &["vec", "buf", "inner", "ptr", "pointer"], + &["vec", "buf", "ptr", "pointer"], + ]; + + let length_path = field_path(&["vec", "len"]); + for fields in DATA_PATHS { + if let Some(layout) = validate_wrapped_pointer_layout( + root, + field_path(fields), + length_path.clone(), + IndirectSequenceKind::Utf8String, + ) { + return Some(layout); + } + } + + None +} + +fn rust_vec_layout(root: &TypeInfo) -> Option { + // rust-gdb uses `buf.ptr` through Rust 1.81 and `buf.inner.ptr` from + // Rust 1.82 onward. In the latter layout RawVecInner erases `T` to `u8`, + // so the analyzer must recover the element type from the Vec DIE's first + // DW_TAG_template_type_parameter instead of trusting the pointer target. + const DATA_PATHS: &[&[&str]] = &[ + &["buf", "inner", "ptr", "pointer"], + &["buf", "ptr", "pointer"], + ]; + + let length_path = field_path(&["len"]); + for fields in DATA_PATHS { + if let Some(layout) = validate_wrapped_pointer_layout( + root, + field_path(fields), + length_path.clone(), + IndirectSequenceKind::TypeParameter { index: 0 }, + ) { + return Some(layout); + } + } + + None +} + +fn rust_vec_deque_layout(root: &TypeInfo) -> Option { + // Rust 1.67 replaced tail/head length derivation with head/len. Rust 1.82 + // moved RawVec under `inner`; 1.75 and 1.95 introduced transparent + // wrappers around cap and head respectively. The validators below follow + // those wrappers through their first DWARF member, as rust-gdb does. + const CURRENT_STORAGE_PATHS: &[(&[&str], &[&str])] = &[ + ( + &["buf", "inner", "ptr", "pointer"], + &["buf", "inner", "cap"], + ), + (&["buf", "ptr", "pointer"], &["buf", "cap"]), + ]; + + for (data_fields, capacity_fields) in CURRENT_STORAGE_PATHS { + if let Some(layout) = validate_ring_sequence_layout( + root, + field_path(data_fields), + field_path(&["head"]), + field_path(&["len"]), + RingSequenceLengthKind::Explicit, + field_path(capacity_fields), + IndirectSequenceKind::TypeParameter { index: 0 }, + ) { + return Some(layout); + } + } + + validate_ring_sequence_layout( + root, + field_path(&["buf", "ptr", "pointer"]), + field_path(&["tail"]), + field_path(&["head"]), + RingSequenceLengthKind::End, + field_path(&["buf", "cap"]), + IndirectSequenceKind::TypeParameter { index: 0 }, + ) +} + +fn rust_os_string_layout(root: &TypeInfo) -> Option { + rust_os_string_layout_with_prefix(root, &[]) +} + +fn rust_path_buf_layout(root: &TypeInfo) -> Option { + // Rust's LLDB provider delegates PathBuf to its embedded OsString. Reuse + // that official semantic path while deriving every wrapper and offset from + // the concrete DWARF instead of assuming PathBuf's Rust layout. + rust_os_string_layout_with_prefix(root, &["inner"]) +} + +fn rust_os_string_layout_with_prefix( + root: &TypeInfo, + prefix: &[&str], +) -> Option { + // Rust 1.82 moved RawVec's pointer under `inner`. Windows used the tuple + // field `__0` for Wtf8Buf through 1.96 and now names that field `bytes`. + // Unix exposes the Vec directly. These paths mirror rust-gdb; every + // selected member offset and width is still read from DWARF. + const PATHS: &[(&[&str], &[&str])] = &[ + ( + &["inner", "inner", "buf", "inner", "ptr", "pointer"], + &["inner", "inner", "len"], + ), + ( + &["inner", "inner", "buf", "ptr", "pointer"], + &["inner", "inner", "len"], + ), + ( + &["inner", "inner", "bytes", "buf", "inner", "ptr", "pointer"], + &["inner", "inner", "bytes", "len"], + ), + ( + &["inner", "inner", "bytes", "buf", "ptr", "pointer"], + &["inner", "inner", "bytes", "len"], + ), + ( + &["inner", "inner", "__0", "buf", "inner", "ptr", "pointer"], + &["inner", "inner", "__0", "len"], + ), + ( + &["inner", "inner", "__0", "buf", "ptr", "pointer"], + &["inner", "inner", "__0", "len"], + ), + ]; + + for (data_fields, length_fields) in PATHS { + if let Some(layout) = validate_wrapped_pointer_layout( + root, + prefixed_field_path(prefix, data_fields), + prefixed_field_path(prefix, length_fields), + IndirectSequenceKind::ByteString, + ) { + return Some(layout); + } + } + + None +} + +fn validate_wrapped_pointer_layout( + root: &TypeInfo, + data_path: Vec, + length_path: Vec, + kind: IndirectSequenceKind, +) -> Option { + let data_path = wrapped_pointer_path(root, data_path)?; + validate_indirect_sequence_layout(root, data_path, length_path, kind) +} + +fn wrapped_pointer_path(root: &TypeInfo, mut data_path: Vec) -> Option> { + let pointer_or_wrapper = resolve_member_path(root, &data_path)?; + if !matches!( + strip_type_aliases(pointer_or_wrapper.member_type), + TypeInfo::PointerType { .. } + ) { + // Unique and NonNull changed shape in Rust 1.32 and 1.60. Follow the + // first DWARF member, matching rust-gdb's compatibility helper. + let TypeInfo::StructType { members, .. } = + strip_type_aliases(pointer_or_wrapper.member_type) + else { + return None; + }; + data_path.push(members.first()?.name.clone()); + let raw_pointer = resolve_member_path(root, &data_path)?; + if !matches!( + strip_type_aliases(raw_pointer.member_type), + TypeInfo::PointerType { .. } + ) { + return None; + } + } + + Some(data_path) +} + +fn unsigned_metadata_path( + root: &TypeInfo, + mut path: Vec, + expected_size: u64, +) -> Option> { + let mut resolved = resolve_member_path(root, &path)?; + if let TypeInfo::StructType { members, .. } = strip_type_aliases(resolved.member_type) { + path.push(members.first()?.name.clone()); + resolved = resolve_member_path(root, &path)?; + } + + matches!( + strip_type_aliases(resolved.member_type), + TypeInfo::BaseType { size, encoding, .. } + if *size == expected_size && *encoding == gimli::DW_ATE_unsigned.0 as u16 + ) + .then_some(path) +} + +fn validate_ring_sequence_layout( + root: &TypeInfo, + data_path: Vec, + start_path: Vec, + length_path: Vec, + length_kind: RingSequenceLengthKind, + capacity_path: Vec, + kind: IndirectSequenceKind, +) -> Option { + let data_path = wrapped_pointer_path(root, data_path)?; + let data = resolve_member_path(root, &data_path)?; + let TypeInfo::PointerType { + size: pointer_size, .. + } = strip_type_aliases(data.member_type) + else { + return None; + }; + if *pointer_size == 0 { + return None; + } + + let start_path = unsigned_metadata_path(root, start_path, *pointer_size)?; + let length_path = unsigned_metadata_path(root, length_path, *pointer_size)?; + let capacity_path = unsigned_metadata_path(root, capacity_path, *pointer_size)?; + let paths = [&data_path, &start_path, &length_path, &capacity_path]; + let mut ranges = Vec::with_capacity(paths.len()); + for path in paths { + let member = resolve_member_path(root, path)?; + let end = member.offset.checked_add(member.member_type.size())?; + if end > root.size() { + return None; + } + ranges.push((member.offset, end)); + } + for (index, left) in ranges.iter().enumerate() { + if ranges[index + 1..] + .iter() + .any(|right| left.0 < right.1 && right.0 < left.1) + { + return None; + } + } + + Some(IndirectSequenceLayout::ring( + data_path, + start_path, + length_path, + length_kind, + capacity_path, + kind, + )) +} + +fn field_path(fields: &[&str]) -> Vec { + fields.iter().map(|field| (*field).to_string()).collect() +} + +fn prefixed_field_path(prefix: &[&str], fields: &[&str]) -> Vec { + prefix + .iter() + .chain(fields) + .map(|field| (*field).to_string()) + .collect() +} + +fn unique_named_member<'a>(members: &'a [StructMember], name: &str) -> Option<&'a StructMember> { + let mut matching = members.iter().filter(|member| member.name == name); + let member = matching.next()?; + matching.next().is_none().then_some(member) +} + +fn sole_struct_member(type_info: &TypeInfo) -> Option<&StructMember> { + let TypeInfo::StructType { members, .. } = strip_type_aliases(type_info) else { + return None; + }; + let [member] = members.as_slice() else { + return None; + }; + Some(member) +} + +fn scalar_wrapper_target(type_info: &TypeInfo) -> Option<&TypeInfo> { + let mut current = type_info; + for _ in 0..16 { + match strip_type_aliases(current) { + TypeInfo::BaseType { .. } | TypeInfo::PointerType { .. } => return Some(current), + TypeInfo::StructType { size, .. } => { + let member = sole_struct_member(current)?; + member_range(member, *size)?; + current = &member.member_type; + } + _ => return None, + } + } + None +} + +fn member_range(member: &StructMember, container_size: u64) -> Option<(u64, u64)> { + if member.bit_offset.is_some() || member.bit_size.is_some() { + return None; + } + let end = member.offset.checked_add(member.member_type.size())?; + (end <= container_size).then_some((member.offset, end)) +} + +fn ranges_overlap(left: (u64, u64), right: (u64, u64)) -> bool { + left.0 < left.1 && right.0 < right.1 && left.0 < right.1 && right.0 < left.1 +} + +struct ResolvedMemberPath<'a> { + offset: u64, + member_type: &'a TypeInfo, +} + +fn resolve_member_path<'a>(root: &'a TypeInfo, path: &[String]) -> Option> { + resolve_member_path_impl(root, path, false) +} + +fn resolve_member_path_allowing_zst<'a>( + root: &'a TypeInfo, + path: &[String], +) -> Option> { + resolve_member_path_impl(root, path, true) +} + +fn resolve_member_path_impl<'a>( + root: &'a TypeInfo, + path: &[String], + allow_zero_sized: bool, +) -> Option> { + let mut current = root; + let mut offset = 0u64; + + for field in path { + let TypeInfo::StructType { size, members, .. } = strip_type_aliases(current) else { + return None; + }; + let member = members.iter().find(|member| member.name == *field)?; + let member_size = member.member_type.size(); + let member_end = member.offset.checked_add(member_size)?; + if (!allow_zero_sized && member_size == 0) + || member.bit_offset.is_some() + || member.bit_size.is_some() + || member_end > *size + { + return None; + } + offset = offset.checked_add(member.offset)?; + current = &member.member_type; + } + + Some(ResolvedMemberPath { + offset, + member_type: current, + }) +} + +fn validate_indirect_sequence_layout( + root: &TypeInfo, + data_path: Vec, + length_path: Vec, + kind: IndirectSequenceKind, +) -> Option { + let data = resolve_member_path(root, &data_path)?; + let length = resolve_member_path(root, &length_path)?; + let TypeInfo::PointerType { + target_type, + size: pointer_size, + } = strip_type_aliases(data.member_type) + else { + return None; + }; + let TypeInfo::BaseType { + size: length_size, + encoding: length_encoding, + .. + } = strip_type_aliases(length.member_type) + else { + return None; + }; + + let utf8_byte_pointer = match strip_type_aliases(target_type) { + TypeInfo::BaseType { size, encoding, .. } => { + *size == 1 + && (*encoding == gimli::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::DW_ATE_unsigned_char.0 as u16) + } + _ => false, + }; + let unsigned_length = *length_encoding == gimli::DW_ATE_unsigned.0 as u16; + // Aggregate size, member offsets, and metadata widths come from DWARF. + let aggregate_size = root.size(); + let data_end = data.offset.checked_add(*pointer_size)?; + let length_end = length.offset.checked_add(*length_size)?; + let members_overlap = data.offset < length_end && length.offset < data_end; + if *pointer_size == 0 + || *pointer_size != *length_size + || (matches!( + kind, + IndirectSequenceKind::Utf8String | IndirectSequenceKind::ByteString + ) && !utf8_byte_pointer) + || !unsigned_length + || data_end > aggregate_size + || length_end > aggregate_size + || members_overlap + { + return None; + } + + Some(IndirectSequenceLayout::contiguous( + data_path, + length_path, + kind, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CuId, ModuleId, StructMember, TypeIdentity, TypeOrigin}; + + struct RustStrLayout<'a> { + name: &'a str, + aggregate_size: u64, + data_offset: u64, + pointer_size: u64, + length_offset: u64, + length_size: u64, + language: SourceLanguage, + } + + fn rust_str_type(layout: RustStrLayout<'_>) -> ResolvedType { + let byte = TypeInfo::BaseType { + name: "u8".to_string(), + size: 1, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }; + ResolvedType::new( + TypeInfo::StructType { + name: layout.name.to_string(), + size: layout.aggregate_size, + members: vec![ + StructMember { + name: "data_ptr".to_string(), + member_type: TypeInfo::PointerType { + target_type: Box::new(byte), + size: layout.pointer_size, + }, + offset: layout.data_offset, + bit_offset: None, + bit_size: None, + }, + StructMember { + name: "length".to_string(), + member_type: TypeInfo::BaseType { + name: "usize".to_string(), + size: layout.length_size, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }, + offset: layout.length_offset, + bit_offset: None, + bit_size: None, + }, + ], + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language: layout.language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_str_type_64(name: &str, language: SourceLanguage) -> ResolvedType { + rust_str_type(RustStrLayout { + name, + aggregate_size: 16, + data_offset: 0, + pointer_size: 8, + length_offset: 8, + length_size: 8, + language, + }) + } + + fn rust_path_ref_type_64(name: &str, language: SourceLanguage) -> ResolvedType { + let mut current = rust_str_type_64(name, language); + let TypeInfo::StructType { members, .. } = &mut current.summary else { + unreachable!("test Path reference is a struct") + }; + let TypeInfo::PointerType { target_type, .. } = &mut members[0].member_type else { + unreachable!("test Path data_ptr is a pointer") + }; + *target_type = Box::new(TypeInfo::UnknownType { + name: "Path".to_string(), + }); + current + } + + fn rust_slice_type_64(name: &str, language: SourceLanguage) -> ResolvedType { + let mut current = rust_str_type_64(name, language); + let TypeInfo::StructType { members, .. } = &mut current.summary else { + unreachable!("test slice is a struct") + }; + let TypeInfo::PointerType { target_type, .. } = &mut members[0].member_type else { + unreachable!("test slice data_ptr is a pointer") + }; + *target_type = Box::new(TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::DW_ATE_signed.0 as u16, + }); + current + } + + fn member(name: &str, member_type: TypeInfo, offset: u64) -> StructMember { + StructMember { + name: name.to_string(), + member_type, + offset, + bit_offset: None, + bit_size: None, + } + } + + fn single_member_struct(name: &str, field: &str, inner: TypeInfo) -> TypeInfo { + TypeInfo::StructType { + name: name.to_string(), + size: inner.size(), + members: vec![member(field, inner, 0)], + } + } + + fn unsigned_type(name: &str, size: u64) -> TypeInfo { + TypeInfo::BaseType { + name: name.to_string(), + size, + encoding: gimli::DW_ATE_unsigned.0 as u16, + } + } + + fn signed_type(name: &str, size: u64) -> TypeInfo { + TypeInfo::BaseType { + name: name.to_string(), + size, + encoding: gimli::DW_ATE_signed.0 as u16, + } + } + + fn indirect_contiguous( + data_path: Vec, + length_path: Vec, + kind: IndirectSequenceKind, + ) -> ValueLayout { + ValueLayout::IndirectSequence(IndirectSequenceLayout::contiguous( + data_path, + length_path, + kind, + )) + } + + fn indirect_ring( + data_path: Vec, + start_path: Vec, + length_path: Vec, + length_kind: RingSequenceLengthKind, + capacity_path: Vec, + kind: IndirectSequenceKind, + ) -> ValueLayout { + ValueLayout::IndirectSequence(IndirectSequenceLayout::ring( + data_path, + start_path, + length_path, + length_kind, + capacity_path, + kind, + )) + } + + fn rust_nonzero_type( + name: &str, + outer_field: &str, + inner_field: &str, + value_type: TypeInfo, + language: SourceLanguage, + ) -> ResolvedType { + let inner = single_member_struct("NonZeroInner", inner_field, value_type); + ResolvedType::new( + single_member_struct(name, outer_field, inner), + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_cell_type( + name: &str, + outer_field: &str, + inner_field: &str, + value_type: TypeInfo, + language: SourceLanguage, + ) -> ResolvedType { + let unsafe_cell = single_member_struct("UnsafeCell", inner_field, value_type); + ResolvedType::new( + single_member_struct(name, outer_field, unsafe_cell), + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + struct RefCellTestLayout<'a> { + name: &'a str, + root_size: u64, + value_offset: u64, + borrow_offset: u64, + borrow_size: u64, + language: SourceLanguage, + } + + fn rust_ref_cell_type(layout: RefCellTestLayout<'_>, value_type: TypeInfo) -> ResolvedType { + let value = single_member_struct("UnsafeCell", "value_inner_from_dwarf", value_type); + let borrow = single_member_struct( + "Cell", + "borrow_cell_inner_from_dwarf", + single_member_struct( + "UnsafeCell", + "borrow_inner_from_dwarf", + signed_type("isize", layout.borrow_size), + ), + ); + ResolvedType::new( + TypeInfo::StructType { + name: layout.name.to_string(), + size: layout.root_size, + members: vec![ + member("borrow", borrow, layout.borrow_offset), + member("value", value, layout.value_offset), + ], + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language: layout.language, + producer: None, + dwarf_version: 5, + }), + ) + } + + struct RefTestLayout<'a> { + name: &'a str, + root_size: u64, + value_offset: u64, + borrow_offset: u64, + value_pointer_size: u64, + borrow_pointer_size: u64, + language: SourceLanguage, + marker: bool, + } + + fn rust_ref_type(layout: RefTestLayout<'_>, value_type: TypeInfo) -> ResolvedType { + let value_pointer = TypeInfo::PointerType { + target_type: Box::new(value_type), + size: layout.value_pointer_size, + }; + let borrow_pointer = TypeInfo::PointerType { + target_type: Box::new(TypeInfo::UnknownType { + name: "Cell".to_string(), + }), + size: layout.borrow_pointer_size, + }; + let mut members = vec![ + member( + "borrow", + single_member_struct("BorrowRef", "borrow_pointer_from_dwarf", borrow_pointer), + layout.borrow_offset, + ), + member( + "value", + single_member_struct("NonNull", "value_pointer_from_dwarf", value_pointer), + layout.value_offset, + ), + ]; + if layout.marker { + members.push(member( + "marker", + TypeInfo::StructType { + name: "PhantomData".to_string(), + size: 0, + members: Vec::new(), + }, + layout.root_size, + )); + } + ResolvedType::new( + TypeInfo::StructType { + name: layout.name.to_string(), + size: layout.root_size, + members, + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language: layout.language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_reference_counted_type( + name: &str, + pointer_size: u64, + nested_pointer_wrapper: bool, + language: SourceLanguage, + ) -> ResolvedType { + let raw_pointer = TypeInfo::PointerType { + target_type: Box::new(TypeInfo::UnknownType { + name: "RcOrArcInner".to_string(), + }), + size: pointer_size, + }; + let pointer = if nested_pointer_wrapper { + single_member_struct("NonZero<*mut T>", "scalar_from_dwarf", raw_pointer) + } else { + raw_pointer + }; + let ptr = TypeInfo::StructType { + name: "NonNull>".to_string(), + size: pointer_size, + members: vec![member("pointer", pointer, 0)], + }; + ResolvedType::new( + TypeInfo::StructType { + name: name.to_string(), + size: pointer_size, + members: vec![ + member("ptr", ptr, 0), + member( + "phantom", + TypeInfo::StructType { + name: "PhantomData".to_string(), + size: 0, + members: Vec::new(), + }, + pointer_size, + ), + ], + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_reference_counted_dst_type( + name: &str, + type_name: &str, + value_member: &str, + pointer_size: u64, + explicit_metadata: bool, + language: SourceLanguage, + ) -> ResolvedType { + let counter = || { + single_member_struct( + "Cell", + "value", + single_member_struct( + "UnsafeCell", + "value", + unsigned_type("usize", pointer_size), + ), + ) + }; + let inner = TypeInfo::StructType { + name: format!("{type_name}Inner"), + size: pointer_size * 2, + members: vec![ + member("strong", counter(), 0), + member("weak", counter(), pointer_size), + member( + value_member, + TypeInfo::ArrayType { + element_type: Box::new(unsigned_type("u8", 1)), + element_count: None, + total_size: None, + }, + pointer_size * 2, + ), + ], + }; + let raw_pointer = TypeInfo::PointerType { + target_type: Box::new(inner), + size: pointer_size, + }; + let pointer = if explicit_metadata { + TypeInfo::StructType { + name: format!("*const {type_name}Inner"), + size: pointer_size * 2, + members: vec![ + member("data_ptr", raw_pointer, 0), + member("length", unsigned_type("usize", pointer_size), pointer_size), + ], + } + } else { + raw_pointer + }; + let ptr = TypeInfo::StructType { + name: format!("NonNull<{type_name}Inner>"), + size: pointer.size(), + members: vec![member("pointer", pointer, 0)], + }; + ResolvedType::new( + TypeInfo::StructType { + name: name.to_string(), + size: ptr.size(), + members: vec![member("ptr", ptr, 0)], + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn hash_table_metadata(pointer_size: u64, dedicated_data: bool) -> TypeInfo { + let control_pointer = TypeInfo::PointerType { + target_type: Box::new(unsigned_type("u8", 1)), + size: pointer_size, + }; + let control = single_member_struct("NonNull", "pointer", control_pointer); + let mut members = vec![ + member("bucket_mask", unsigned_type("usize", pointer_size), 0), + member("ctrl", control, pointer_size), + member( + "growth_left", + unsigned_type("usize", pointer_size), + pointer_size * 2, + ), + member( + "items", + unsigned_type("usize", pointer_size), + pointer_size * 3, + ), + ]; + if dedicated_data { + let entry_pointer = TypeInfo::PointerType { + target_type: Box::new(TypeInfo::UnknownType { + name: "(K, V)".to_string(), + }), + size: pointer_size, + }; + members.push(member( + "data", + single_member_struct("NonNull<(K, V)>", "pointer", entry_pointer), + pointer_size * 4, + )); + } + TypeInfo::StructType { + name: "hashbrown::raw::RawTableInner".to_string(), + size: pointer_size * if dedicated_data { 5 } else { 4 }, + members, + } + } + + fn rust_hash_collection_type( + name: &str, + kind: HashTableKind, + pointer_size: u64, + nested_metadata: bool, + dedicated_data: bool, + legacy_set_wrapper: bool, + language: SourceLanguage, + ) -> ResolvedType { + let metadata = hash_table_metadata(pointer_size, dedicated_data); + let raw_table = if nested_metadata { + TypeInfo::StructType { + name: "hashbrown::raw::RawTable<(K, V)>".to_string(), + size: metadata.size(), + members: vec![member("table", metadata, 0)], + } + } else { + TypeInfo::StructType { + name: "hashbrown::raw::RawTable<(K, V)>".to_string(), + size: metadata.size(), + members: match metadata { + TypeInfo::StructType { members, .. } => members, + _ => unreachable!("hash metadata is a struct"), + }, + } + }; + let hashbrown_map = TypeInfo::StructType { + name: "hashbrown::map::HashMap".to_string(), + size: raw_table.size(), + members: vec![member("table", raw_table, 0)], + }; + let root = match (kind, legacy_set_wrapper) { + (HashTableKind::Map, _) => TypeInfo::StructType { + name: name.to_string(), + size: hashbrown_map.size(), + members: vec![member("base", hashbrown_map, 0)], + }, + (HashTableKind::Set, false) => { + let hashbrown_set = TypeInfo::StructType { + name: "hashbrown::set::HashSet".to_string(), + size: hashbrown_map.size(), + members: vec![member("map", hashbrown_map, 0)], + }; + TypeInfo::StructType { + name: name.to_string(), + size: hashbrown_set.size(), + members: vec![member("base", hashbrown_set, 0)], + } + } + (HashTableKind::Set, true) => { + let std_map = TypeInfo::StructType { + name: "std::collections::hash::map::HashMap".to_string(), + size: hashbrown_map.size(), + members: vec![member("base", hashbrown_map, 0)], + }; + TypeInfo::StructType { + name: name.to_string(), + size: std_map.size(), + members: vec![member("map", std_map, 0)], + } + } + }; + ResolvedType::new( + root, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_135_hash_collection_type( + name: &str, + kind: HashTableKind, + pointer_size: u64, + language: SourceLanguage, + ) -> ResolvedType { + let hash_word = unsigned_type("usize", pointer_size); + let raw_pointer = TypeInfo::PointerType { + target_type: Box::new(hash_word), + size: pointer_size, + }; + let unique = TypeInfo::StructType { + name: "core::ptr::Unique".to_string(), + size: pointer_size, + members: vec![ + member("pointer", raw_pointer, 0), + member( + "_marker", + TypeInfo::StructType { + name: "PhantomData".to_string(), + size: 0, + members: Vec::new(), + }, + 0, + ), + ], + }; + let hashes = TypeInfo::StructType { + name: "TaggedHashUintPtr".to_string(), + size: pointer_size, + members: vec![member("__0", unique, 0)], + }; + let marker = TypeInfo::StructType { + name: "PhantomData<(K, V)>".to_string(), + size: 0, + members: Vec::new(), + }; + let raw_table = TypeInfo::StructType { + name: "RawTable".to_string(), + size: pointer_size * 3, + members: vec![ + member("capacity_mask", unsigned_type("usize", pointer_size), 0), + member("size", unsigned_type("usize", pointer_size), pointer_size), + member("hashes", hashes, pointer_size * 2), + member("marker", marker, 0), + ], + }; + let map = TypeInfo::StructType { + name: match kind { + HashTableKind::Map => name.to_string(), + HashTableKind::Set => "std::collections::hash::map::HashMap".to_string(), + }, + size: pointer_size * 5, + members: vec![ + member( + "hash_builder", + TypeInfo::StructType { + name: "RandomState".to_string(), + size: pointer_size * 2, + members: Vec::new(), + }, + 0, + ), + member("table", raw_table, pointer_size * 2), + member( + "resize_policy", + TypeInfo::StructType { + name: "DefaultResizePolicy".to_string(), + size: 0, + members: Vec::new(), + }, + 0, + ), + ], + }; + let root = match kind { + HashTableKind::Map => map, + HashTableKind::Set => TypeInfo::StructType { + name: name.to_string(), + size: map.size(), + members: vec![member("map", map, 0)], + }, + }; + ResolvedType::new( + root, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 4, + }), + ) + } + + fn rust_btree_collection_type( + name: &str, + kind: BTreeKind, + pointer_size: u64, + language: SourceLanguage, + ) -> ResolvedType { + let root = TypeInfo::StructType { + name: "Option>".to_string(), + size: pointer_size * 2, + members: Vec::new(), + }; + let map = TypeInfo::StructType { + name: "alloc::collections::btree::map::BTreeMap".to_string(), + size: pointer_size * 3, + members: vec![ + member("root", root, 0), + member( + "length", + unsigned_type("usize", pointer_size), + pointer_size * 2, + ), + ], + }; + let root = match kind { + BTreeKind::Map => match map { + TypeInfo::StructType { size, members, .. } => TypeInfo::StructType { + name: name.to_string(), + size, + members, + }, + _ => unreachable!("test BTreeMap is a struct"), + }, + BTreeKind::Set => TypeInfo::StructType { + name: name.to_string(), + size: map.size(), + members: vec![member("map", map, 0)], + }, + }; + ResolvedType::new( + root, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_legacy_nonzero_type( + name: &str, + field: &str, + value_type: TypeInfo, + language: SourceLanguage, + ) -> ResolvedType { + ResolvedType::new( + single_member_struct(name, field, value_type), + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 4, + }), + ) + } + + fn rust_string_type( + name: &str, + language: SourceLanguage, + pointer_size: u64, + wraps_raw_pointer: bool, + uses_raw_vec_inner: bool, + ) -> ResolvedType { + let raw_pointer = TypeInfo::PointerType { + target_type: Box::new(unsigned_type("u8", 1)), + size: pointer_size, + }; + let unique_pointer = if wraps_raw_pointer { + TypeInfo::StructType { + name: "core::ptr::non_null::NonNull".to_string(), + size: pointer_size, + members: vec![member("pointer", raw_pointer, 0)], + } + } else { + raw_pointer + }; + let unique = TypeInfo::StructType { + name: "core::ptr::unique::Unique".to_string(), + size: pointer_size, + members: vec![member("pointer", unique_pointer, 0)], + }; + let raw_vec = if uses_raw_vec_inner { + let raw_vec_inner = TypeInfo::StructType { + name: "alloc::raw_vec::RawVecInner".to_string(), + size: pointer_size * 2, + members: vec![member("ptr", unique, pointer_size)], + }; + TypeInfo::StructType { + name: "alloc::raw_vec::RawVec".to_string(), + size: pointer_size * 2, + members: vec![member("inner", raw_vec_inner, 0)], + } + } else { + TypeInfo::StructType { + name: "alloc::raw_vec::RawVec".to_string(), + size: pointer_size * 2, + members: vec![member("ptr", unique, 0)], + } + }; + let vec_type = TypeInfo::StructType { + name: "Vec".to_string(), + size: pointer_size * 3, + members: vec![ + member("buf", raw_vec, 0), + member( + "len", + unsigned_type("usize", pointer_size), + pointer_size * 2, + ), + ], + }; + + ResolvedType::new( + TypeInfo::StructType { + name: name.to_string(), + size: pointer_size * 3, + members: vec![member("vec", vec_type, 0)], + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language, + producer: None, + dwarf_version: 5, + }), + ) + } + + fn rust_vec_type( + name: &str, + language: SourceLanguage, + pointer_size: u64, + wraps_raw_pointer: bool, + uses_raw_vec_inner: bool, + ) -> ResolvedType { + let string = rust_string_type( + "alloc::string::String", + language, + pointer_size, + wraps_raw_pointer, + uses_raw_vec_inner, + ); + let TypeInfo::StructType { members, .. } = string.summary else { + unreachable!("test String is a struct") + }; + let mut vec_type = members + .into_iter() + .next() + .expect("test String has a Vec member") + .member_type; + let TypeInfo::StructType { name: vec_name, .. } = &mut vec_type else { + unreachable!("test Vec is a struct") + }; + *vec_name = name.to_string(); + + ResolvedType::new(vec_type, TypeIdentity::Unknown, string.origin) + } + + fn rust_os_string_type( + name: &str, + pointer_size: u64, + uses_raw_vec_inner: bool, + windows_vec_field: Option<&str>, + ) -> ResolvedType { + let vec = rust_vec_type( + "alloc::vec::Vec", + SourceLanguage::Rust, + pointer_size, + true, + uses_raw_vec_inner, + ); + let origin = vec.origin; + let platform_buffer = match windows_vec_field { + Some(field) => single_member_struct("Wtf8Buf", field, vec.summary), + None => vec.summary, + }; + let buffer = single_member_struct("Buf", "inner", platform_buffer); + let os_string = single_member_struct(name, "inner", buffer); + + ResolvedType::new(os_string, TypeIdentity::Unknown, origin) + } + + fn rust_path_buf_type(name: &str, pointer_size: u64, uses_raw_vec_inner: bool) -> ResolvedType { + let os_string = rust_os_string_type( + "std::ffi::os_str::OsString", + pointer_size, + uses_raw_vec_inner, + None, + ); + let origin = os_string.origin; + let path_buf = single_member_struct(name, "inner", os_string.summary); + + ResolvedType::new(path_buf, TypeIdentity::Unknown, origin) + } + + struct VecDequeTestLayout<'a> { + name: &'a str, + pointer_size: u64, + uses_raw_vec_inner: bool, + wraps_capacity: bool, + wraps_head: bool, + uses_legacy_tail: bool, + } + + fn rust_vec_deque_type(layout: VecDequeTestLayout<'_>) -> ResolvedType { + let pointer_size = layout.pointer_size; + let raw_pointer = TypeInfo::PointerType { + target_type: Box::new(unsigned_type("u8", 1)), + size: pointer_size, + }; + let non_null = single_member_struct("NonNull", "pointer", raw_pointer); + let unique = single_member_struct("Unique", "pointer", non_null); + let capacity = if layout.wraps_capacity { + single_member_struct( + "alloc::raw_vec::Cap", + "__0", + unsigned_type("usize", pointer_size), + ) + } else { + unsigned_type("usize", pointer_size) + }; + let raw_vec_storage = TypeInfo::StructType { + name: "alloc::raw_vec::RawVecInner".to_string(), + size: pointer_size * 2, + members: vec![ + member("cap", capacity, 0), + member("ptr", unique, pointer_size), + ], + }; + let raw_vec = if layout.uses_raw_vec_inner { + single_member_struct("alloc::raw_vec::RawVec", "inner", raw_vec_storage) + } else { + raw_vec_storage + }; + let head = if layout.wraps_head { + single_member_struct( + "alloc::collections::vec_deque::WrappedIndex", + "__0", + unsigned_type("usize", pointer_size), + ) + } else { + unsigned_type("usize", pointer_size) + }; + let mut members = if layout.uses_legacy_tail { + vec![ + member("tail", unsigned_type("usize", pointer_size), 0), + member("head", head, pointer_size), + ] + } else { + vec![ + member("head", head, 0), + member("len", unsigned_type("usize", pointer_size), pointer_size), + ] + }; + members.push(member("buf", raw_vec, pointer_size * 2)); + + ResolvedType::new( + TypeInfo::StructType { + name: layout.name.to_string(), + size: pointer_size * 4, + members, + }, + TypeIdentity::Unknown, + Some(TypeOrigin { + module: ModuleId(0), + cu: CuId(0), + language: SourceLanguage::Rust, + producer: None, + dwarf_version: 5, + }), + ) + } + + #[test] + fn recognizes_rust_str_layout() { + let current = rust_str_type_64("&str", SourceLanguage::Rust); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::Utf8String, + )) + ); + } + + #[test] + fn recognizes_supported_rust_str_names() { + for name in ["&mut str", "&'static str", "&'static mut str"] { + let current = rust_str_type_64(name, SourceLanguage::Rust); + + assert!( + resolve_value_layout(¤t, None).is_some(), + "name={name}" + ); + } + } + + #[test] + fn recognizes_rust_slice_layout_and_supported_names() { + for name in [ + "&[i32]", + "&mut [i32]", + "&'static [i32]", + "&'static mut [i32]", + ] { + let current = rust_slice_type_64(name, SourceLanguage::Rust); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::PointerTarget, + )), + "name={name}" + ); + } + } + + #[test] + fn does_not_classify_non_slice_reference_name() { + let current = rust_slice_type_64("&[i32", SourceLanguage::Rust); + + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn recognizes_std_box_str_with_current_and_legacy_type_names() { + for name in [ + "alloc::boxed::Box", + "alloc::boxed::Box", + ] { + let current = rust_str_type_64(name, SourceLanguage::Rust); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::Utf8String, + )), + "name={name}" + ); + } + + let current = rust_str_type_64("Box", SourceLanguage::Rust); + assert!(resolve_value_layout( + ¤t, + Some("alloc::boxed::Box"), + ) + .is_some()); + } + + #[test] + fn does_not_classify_box_str_from_another_namespace() { + let current = rust_str_type_64("Box", SourceLanguage::Rust); + + assert_eq!( + resolve_value_layout(¤t, Some("app::Box"),), + None + ); + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn recognizes_unix_and_windows_os_string_layouts_across_versions() { + type LayoutCase<'a> = (bool, Option<&'a str>, &'a [&'a str], &'a [&'a str]); + + let cases: &[LayoutCase<'_>] = &[ + ( + true, + None, + &[ + "inner", "inner", "buf", "inner", "ptr", "pointer", "pointer", + ], + &["inner", "inner", "len"], + ), + ( + false, + None, + &["inner", "inner", "buf", "ptr", "pointer", "pointer"], + &["inner", "inner", "len"], + ), + ( + true, + Some("bytes"), + &[ + "inner", "inner", "bytes", "buf", "inner", "ptr", "pointer", "pointer", + ], + &["inner", "inner", "bytes", "len"], + ), + ( + false, + Some("__0"), + &["inner", "inner", "__0", "buf", "ptr", "pointer", "pointer"], + &["inner", "inner", "__0", "len"], + ), + ]; + + for (uses_raw_vec_inner, windows_field, data_fields, length_fields) in cases { + let current = rust_os_string_type( + "std::ffi::os_str::OsString", + 8, + *uses_raw_vec_inner, + *windows_field, + ); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(data_fields), + field_path(length_fields), + IndirectSequenceKind::ByteString, + )), + "raw_vec_inner={uses_raw_vec_inner} windows_field={windows_field:?}" + ); + } + } + + #[test] + fn requires_standard_namespace_for_short_os_string_name() { + let current = rust_os_string_type("OsString", 8, true, None); + + assert!(resolve_value_layout(¤t, Some("std::ffi::os_str::OsString"),).is_some()); + assert_eq!(resolve_value_layout(¤t, Some("app::OsString")), None); + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn recognizes_path_and_path_buf_byte_layouts_from_dwarf() { + for name in [ + "&std::path::Path", + "&mut std::path::Path", + "&'static std::path::Path", + "&'static mut std::path::Path", + ] { + let current = rust_path_ref_type_64(name, SourceLanguage::Rust); + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["data_ptr"]), + field_path(&["length"]), + IndirectSequenceKind::OpaqueByteString, + )), + "name={name}" + ); + } + + for pointer_size in [4, 8] { + for uses_raw_vec_inner in [false, true] { + let current = rust_path_buf_type("PathBuf", pointer_size, uses_raw_vec_inner); + let data_fields = if uses_raw_vec_inner { + &[ + "inner", "inner", "inner", "buf", "inner", "ptr", "pointer", "pointer", + ][..] + } else { + &[ + "inner", "inner", "inner", "buf", "ptr", "pointer", "pointer", + ][..] + }; + assert_eq!( + resolve_value_layout(¤t, Some("std::path::PathBuf")), + Some(indirect_contiguous( + field_path(data_fields), + field_path(&["inner", "inner", "inner", "len"]), + IndirectSequenceKind::ByteString, + )), + "pointer_size={pointer_size} raw_vec_inner={uses_raw_vec_inner}" + ); + } + } + } + + #[test] + fn rejects_path_and_path_buf_lookalikes() { + let path = rust_path_ref_type_64("&app::Path", SourceLanguage::Rust); + assert_eq!(resolve_value_layout(&path, None), None); + + let path_buf = rust_path_buf_type("PathBuf", 8, true); + assert_eq!(resolve_value_layout(&path_buf, Some("app::PathBuf")), None); + assert_eq!(resolve_value_layout(&path_buf, None), None); + + let non_rust = rust_path_ref_type_64("&std::path::Path", SourceLanguage::C); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn recognizes_32_bit_layout_from_dwarf() { + let current = rust_str_type(RustStrLayout { + name: "&str", + aggregate_size: 8, + data_offset: 0, + pointer_size: 4, + length_offset: 4, + length_size: 4, + language: SourceLanguage::Rust, + }); + + assert!(resolve_value_layout(¤t, None).is_some()); + } + + #[test] + fn rejects_mismatched_metadata_widths() { + let current = rust_str_type(RustStrLayout { + name: "&str", + aggregate_size: 16, + data_offset: 0, + pointer_size: 8, + length_offset: 8, + length_size: 4, + language: SourceLanguage::Rust, + }); + + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn rejects_members_outside_aggregate() { + let current = rust_str_type(RustStrLayout { + name: "&str", + aggregate_size: 8, + data_offset: 0, + pointer_size: 4, + length_offset: 6, + length_size: 4, + language: SourceLanguage::Rust, + }); + + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn rejects_overlapping_members() { + let current = rust_str_type(RustStrLayout { + name: "&str", + aggregate_size: 8, + data_offset: 0, + pointer_size: 4, + length_offset: 2, + length_size: 4, + language: SourceLanguage::Rust, + }); + + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn does_not_apply_rust_adapter_to_other_languages() { + assert_eq!( + resolve_value_layout(&rust_str_type_64("&str", SourceLanguage::C), None,), + None + ); + } + + #[test] + fn recognizes_current_std_string_layout() { + let current = rust_string_type("String", SourceLanguage::Rust, 8, true, true); + let layout = + resolve_value_layout(¤t, Some("alloc::string::String")).expect("String layout"); + + assert_eq!( + layout, + indirect_contiguous( + field_path(&["vec", "buf", "inner", "ptr", "pointer", "pointer"]), + field_path(&["vec", "len"]), + IndirectSequenceKind::Utf8String, + ) + ); + let ValueLayout::IndirectSequence(layout) = layout else { + panic!("String must use indirect sequence layout") + }; + assert_eq!( + resolve_member_path(¤t.summary, &layout.data_path) + .expect("data member") + .offset, + 8 + ); + let IndirectSequenceAddressing::Contiguous { length_path } = &layout.addressing else { + panic!("String must use contiguous addressing") + }; + assert_eq!( + resolve_member_path(¤t.summary, length_path) + .expect("length member") + .offset, + 16 + ); + } + + #[test] + fn recognizes_legacy_std_string_pointer_layout() { + let current = rust_string_type( + "alloc::string::String", + SourceLanguage::Rust, + 8, + false, + true, + ); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["vec", "buf", "inner", "ptr", "pointer"]), + field_path(&["vec", "len"]), + IndirectSequenceKind::Utf8String, + )) + ); + } + + #[test] + fn recognizes_pre_raw_vec_inner_string_layout() { + let current = rust_string_type( + "alloc::string::String", + SourceLanguage::Rust, + 8, + true, + false, + ); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["vec", "buf", "ptr", "pointer", "pointer"]), + field_path(&["vec", "len"]), + IndirectSequenceKind::Utf8String, + )) + ); + } + + #[test] + fn recognizes_32_bit_std_string_layout_from_dwarf() { + let current = + rust_string_type("alloc::string::String", SourceLanguage::Rust, 4, true, true); + + assert!(resolve_value_layout(¤t, None).is_some()); + } + + #[test] + fn does_not_classify_unrelated_string_type() { + let current = rust_string_type("app::String", SourceLanguage::Rust, 8, true, true); + + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn does_not_classify_short_string_from_another_namespace() { + let current = rust_string_type("String", SourceLanguage::Rust, 8, true, true); + + assert_eq!(resolve_value_layout(¤t, Some("app::String")), None); + } + + #[test] + fn does_not_classify_short_string_without_qualified_identity() { + let current = rust_string_type("String", SourceLanguage::Rust, 8, true, true); + + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn recognizes_current_std_vec_layout() { + let current = rust_vec_type( + "Vec", + SourceLanguage::Rust, + 8, + true, + true, + ); + + assert_eq!( + resolve_value_layout(¤t, Some("alloc::vec::Vec")), + Some(indirect_contiguous( + field_path(&["buf", "inner", "ptr", "pointer", "pointer"]), + field_path(&["len"]), + IndirectSequenceKind::TypeParameter { index: 0 }, + )) + ); + } + + #[test] + fn recognizes_pre_raw_vec_inner_vec_layout() { + let current = rust_vec_type( + "alloc::vec::Vec", + SourceLanguage::Rust, + 8, + true, + false, + ); + + assert_eq!( + resolve_value_layout(¤t, None), + Some(indirect_contiguous( + field_path(&["buf", "ptr", "pointer", "pointer"]), + field_path(&["len"]), + IndirectSequenceKind::TypeParameter { index: 0 }, + )) + ); + } + + #[test] + fn recognizes_vec_deque_layouts_across_rust_versions() { + let current = rust_vec_deque_type(VecDequeTestLayout { + name: "VecDeque", + pointer_size: 8, + uses_raw_vec_inner: true, + wraps_capacity: true, + wraps_head: true, + uses_legacy_tail: false, + }); + assert_eq!( + resolve_value_layout( + ¤t, + Some("alloc::collections::vec_deque::VecDeque"), + ), + Some(indirect_ring( + field_path(&["buf", "inner", "ptr", "pointer", "pointer"]), + field_path(&["head", "__0"]), + field_path(&["len"]), + RingSequenceLengthKind::Explicit, + field_path(&["buf", "inner", "cap", "__0"]), + IndirectSequenceKind::TypeParameter { index: 0 }, + )) + ); + + let pre_raw_vec_inner = rust_vec_deque_type(VecDequeTestLayout { + name: "alloc::collections::vec_deque::VecDeque", + pointer_size: 4, + uses_raw_vec_inner: false, + wraps_capacity: false, + wraps_head: false, + uses_legacy_tail: false, + }); + assert_eq!( + resolve_value_layout(&pre_raw_vec_inner, None), + Some(indirect_ring( + field_path(&["buf", "ptr", "pointer", "pointer"]), + field_path(&["head"]), + field_path(&["len"]), + RingSequenceLengthKind::Explicit, + field_path(&["buf", "cap"]), + IndirectSequenceKind::TypeParameter { index: 0 }, + )) + ); + + let legacy = rust_vec_deque_type(VecDequeTestLayout { + name: "alloc::collections::vec_deque::VecDeque", + pointer_size: 8, + uses_raw_vec_inner: false, + wraps_capacity: false, + wraps_head: false, + uses_legacy_tail: true, + }); + assert_eq!( + resolve_value_layout(&legacy, None), + Some(indirect_ring( + field_path(&["buf", "ptr", "pointer", "pointer"]), + field_path(&["tail"]), + field_path(&["head"]), + RingSequenceLengthKind::End, + field_path(&["buf", "cap"]), + IndirectSequenceKind::TypeParameter { index: 0 }, + )) + ); + } + + #[test] + fn requires_standard_namespace_for_short_vec_deque_name() { + let current = rust_vec_deque_type(VecDequeTestLayout { + name: "VecDeque", + pointer_size: 8, + uses_raw_vec_inner: true, + wraps_capacity: true, + wraps_head: false, + uses_legacy_tail: false, + }); + + assert!(resolve_value_layout( + ¤t, + Some("alloc::collections::vec_deque::VecDeque"), + ) + .is_some()); + assert_eq!( + resolve_value_layout(¤t, Some("app::VecDeque")), + None + ); + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn does_not_classify_user_vec_from_another_namespace() { + let current = rust_vec_type("Vec", SourceLanguage::Rust, 8, true, true); + + assert_eq!(resolve_value_layout(¤t, Some("app::Vec")), None); + assert_eq!(resolve_value_layout(¤t, None), None); + } + + #[test] + fn recognizes_nonzero_by_namespace_and_unique_member_structure() { + let current = rust_nonzero_type( + "NonZero", + "outer_from_dwarf", + "inner_from_dwarf", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ); + + assert_eq!( + resolve_value_layout(¤t, Some("core::num::nonzero::NonZero"),), + Some(ValueLayout::ProjectedValue { + value_path: field_path(&["outer_from_dwarf", "inner_from_dwarf"]), + presentation: ProjectedValuePresentation::Transparent, + }) + ); + + let legacy = rust_legacy_nonzero_type( + "NonZeroU32", + "legacy_field_from_dwarf", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(&legacy, Some("core::num::nonzero::NonZeroU32"),), + Some(ValueLayout::ProjectedValue { + value_path: field_path(&["legacy_field_from_dwarf"]), + presentation: ProjectedValuePresentation::Transparent, + }) + ); + + let fully_qualified_legacy = rust_legacy_nonzero_type( + "core::num::NonZeroI64", + "__0", + TypeInfo::BaseType { + name: "i64".to_string(), + size: 8, + encoding: gimli::DW_ATE_signed.0 as u16, + }, + SourceLanguage::Rust, + ); + assert!(resolve_value_layout(&fully_qualified_legacy, None).is_some()); + + let fully_qualified = rust_nonzero_type( + "core::num::nonzero::NonZero", + "__0", + "__0", + TypeInfo::BaseType { + name: "i128".to_string(), + size: 16, + encoding: gimli::DW_ATE_signed.0 as u16, + }, + SourceLanguage::Rust, + ); + assert!(resolve_value_layout(&fully_qualified, None).is_some()); + } + + #[test] + fn rejects_nonzero_lookalikes_and_invalid_layouts() { + let current = rust_nonzero_type( + "NonZero", + "__0", + "__0", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(¤t, Some("app::NonZero")), + None + ); + assert_eq!(resolve_value_layout(¤t, None), None); + + let legacy = rust_legacy_nonzero_type( + "NonZeroU32", + "__0", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ); + assert_eq!(resolve_value_layout(&legacy, Some("app::NonZeroU32")), None); + assert_eq!(resolve_value_layout(&legacy, None), None); + + let mut invalid = current.clone(); + let TypeInfo::StructType { members, .. } = &mut invalid.summary else { + unreachable!("test NonZero is a struct") + }; + members.push(member("extra", unsigned_type("u32", 4), 0)); + assert_eq!( + resolve_value_layout(&invalid, Some("core::num::nonzero::NonZero"),), + None + ); + + let non_rust = rust_nonzero_type( + "core::num::nonzero::NonZero", + "__0", + "__0", + unsigned_type("u32", 4), + SourceLanguage::C, + ); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn recognizes_cell_by_namespace_and_dwarf_member_structure() { + let current = rust_cell_type( + "Cell<(i32, u16)>", + "outer_from_dwarf", + "inner_from_dwarf", + TypeInfo::StructType { + name: "(i32, u16)".to_string(), + size: 8, + members: vec![ + member("__0", signed_type("i32", 4), 0), + member("__1", unsigned_type("u16", 2), 4), + ], + }, + SourceLanguage::Rust, + ); + + assert_eq!( + resolve_value_layout(¤t, Some("core::cell::Cell<(i32, u16)>")), + Some(ValueLayout::ProjectedValue { + value_path: field_path(&["outer_from_dwarf", "inner_from_dwarf"]), + presentation: ProjectedValuePresentation::SingleField { + type_name: "Cell", + field_name: "value", + }, + }) + ); + + let fully_qualified = rust_cell_type( + "core::cell::Cell", + "value", + "value", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ); + assert!(resolve_value_layout(&fully_qualified, None).is_some()); + } + + #[test] + fn rejects_cell_lookalikes_and_invalid_layouts() { + let current = rust_cell_type( + "Cell", + "value", + "value", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ); + assert_eq!(resolve_value_layout(¤t, Some("app::Cell")), None); + assert_eq!(resolve_value_layout(¤t, None), None); + + let mut invalid = current.clone(); + let TypeInfo::StructType { members, .. } = &mut invalid.summary else { + unreachable!("test Cell is a struct") + }; + let TypeInfo::StructType { + members: inner_members, + .. + } = &mut members[0].member_type + else { + unreachable!("test Cell contains a struct") + }; + inner_members.push(member("extra", unsigned_type("u32", 4), 0)); + assert_eq!( + resolve_value_layout(&invalid, Some("core::cell::Cell")), + None + ); + + let non_rust = rust_cell_type( + "core::cell::Cell", + "value", + "value", + unsigned_type("u32", 4), + SourceLanguage::C, + ); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn recognizes_ref_cell_with_dwarf_derived_fields_and_widths() { + let value_type = TypeInfo::StructType { + name: "(i32, u16)".to_string(), + size: 8, + members: vec![ + member("__0", signed_type("i32", 4), 0), + member("__1", unsigned_type("u16", 2), 4), + ], + }; + let current = rust_ref_cell_type( + RefCellTestLayout { + name: "RefCell<(i32, u16)>", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + borrow_size: 8, + language: SourceLanguage::Rust, + }, + value_type, + ); + + assert_eq!( + resolve_value_layout(¤t, Some("core::cell::RefCell<(i32, u16)>")), + Some(ValueLayout::ProjectedStruct(ProjectedStructLayout { + type_name: "RefCell", + fields: vec![ + ProjectedStructField { + name: "value", + value_path: field_path(&["value", "value_inner_from_dwarf"]), + }, + ProjectedStructField { + name: "borrow", + value_path: field_path(&[ + "borrow", + "borrow_cell_inner_from_dwarf", + "borrow_inner_from_dwarf", + ]), + }, + ], + presentation: ProjectedStructPresentation::SignedState { + state_field: "borrow", + non_negative_label: "borrow", + negative_label: "borrow_mut", + }, + })) + ); + + let narrow = rust_ref_cell_type( + RefCellTestLayout { + name: "core::cell::RefCell", + root_size: 8, + value_offset: 0, + borrow_offset: 4, + borrow_size: 4, + language: SourceLanguage::Rust, + }, + unsigned_type("u16", 2), + ); + assert!(resolve_value_layout(&narrow, None).is_some()); + + let zst = rust_ref_cell_type( + RefCellTestLayout { + name: "core::cell::RefCell<()>", + root_size: 8, + value_offset: 0, + borrow_offset: 0, + borrow_size: 8, + language: SourceLanguage::Rust, + }, + TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: gimli::DW_ATE_unsigned.0 as u16, + }, + ); + assert!(resolve_value_layout(&zst, None).is_some()); + } + + #[test] + fn rejects_ref_cell_lookalikes_and_invalid_layouts() { + let current = rust_ref_cell_type( + RefCellTestLayout { + name: "RefCell", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + borrow_size: 8, + language: SourceLanguage::Rust, + }, + unsigned_type("u32", 4), + ); + assert_eq!( + resolve_value_layout(¤t, Some("app::RefCell")), + None + ); + assert_eq!(resolve_value_layout(¤t, None), None); + + let mut unsigned_borrow = current.clone(); + let TypeInfo::StructType { members, .. } = &mut unsigned_borrow.summary else { + unreachable!("test RefCell is a struct") + }; + let TypeInfo::StructType { + members: cell_members, + .. + } = &mut members[0].member_type + else { + unreachable!("test borrow field is a struct") + }; + let TypeInfo::StructType { + members: inner_members, + .. + } = &mut cell_members[0].member_type + else { + unreachable!("test borrow wrapper is a struct") + }; + inner_members[0].member_type = unsigned_type("usize", 8); + assert_eq!( + resolve_value_layout(&unsigned_borrow, Some("core::cell::RefCell")), + None + ); + + let overlapping = rust_ref_cell_type( + RefCellTestLayout { + name: "core::cell::RefCell", + root_size: 16, + value_offset: 0, + borrow_offset: 4, + borrow_size: 8, + language: SourceLanguage::Rust, + }, + unsigned_type("u64", 8), + ); + assert_eq!(resolve_value_layout(&overlapping, None), None); + + let non_rust = rust_ref_cell_type( + RefCellTestLayout { + name: "core::cell::RefCell", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + borrow_size: 8, + language: SourceLanguage::C, + }, + unsigned_type("u32", 4), + ); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn recognizes_hash_collections_across_rust_raw_table_layouts() { + let current_map = rust_hash_collection_type( + "HashMap", + HashTableKind::Map, + 8, + true, + false, + false, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout( + ¤t_map, + Some("std::collections::hash::map::HashMap"), + ), + Some(ValueLayout::HashTable(HashTableLayout { + entry_type_path: field_path(&["base", "table"]), + control_path: field_path(&["base", "table", "table", "ctrl", "pointer",]), + length_path: field_path(&["base", "table", "table", "items"]), + bucket_mask_path: field_path(&["base", "table", "table", "bucket_mask",]), + occupancy: HashTableOccupancy::ControlByteHighBitClear, + buckets: HashTableBucketLayout::ReverseFromControl, + bucket_order: HashTableBucketOrder::Reverse, + kind: HashTableKind::Map, + })) + ); + + let legacy_map = rust_hash_collection_type( + "std::collections::hash::map::HashMap", + HashTableKind::Map, + 4, + false, + true, + false, + SourceLanguage::Rust, + ); + let Some(ValueLayout::HashTable(legacy_map)) = resolve_value_layout(&legacy_map, None) + else { + panic!("expected legacy HashMap layout") + }; + assert_eq!(legacy_map.entry_type_path, field_path(&["base", "table"])); + assert_eq!( + legacy_map.control_path, + field_path(&["base", "table", "ctrl", "pointer"]) + ); + assert_eq!( + legacy_map.buckets, + HashTableBucketLayout::Forward { + data_path: field_path(&["base", "table", "data", "pointer"]), + } + ); + assert_eq!( + legacy_map.occupancy, + HashTableOccupancy::ControlByteHighBitClear + ); + assert_eq!(legacy_map.bucket_order, HashTableBucketOrder::Forward); + + let current_set = rust_hash_collection_type( + "std::collections::hash::set::HashSet", + HashTableKind::Set, + 8, + true, + false, + false, + SourceLanguage::Rust, + ); + let Some(ValueLayout::HashTable(current_set)) = resolve_value_layout(¤t_set, None) + else { + panic!("expected current HashSet layout") + }; + assert_eq!( + current_set.entry_type_path, + field_path(&["base", "map", "table"]) + ); + assert_eq!(current_set.kind, HashTableKind::Set); + + let legacy_set = rust_hash_collection_type( + "HashSet", + HashTableKind::Set, + 8, + false, + true, + true, + SourceLanguage::Rust, + ); + let Some(ValueLayout::HashTable(legacy_set)) = resolve_value_layout( + &legacy_set, + Some("std::collections::hash::set::HashSet"), + ) else { + panic!("expected legacy HashSet layout") + }; + assert_eq!( + legacy_set.entry_type_path, + field_path(&["map", "base", "table"]) + ); + assert_eq!(legacy_set.bucket_order, HashTableBucketOrder::Forward); + } + + #[test] + fn recognizes_rust_135_hash_collections_from_legacy_table_metadata() { + for (name, qualified_name, kind, table_path) in [ + ( + "HashMap", + "std::collections::hash::map::HashMap", + HashTableKind::Map, + field_path(&["table"]), + ), + ( + "HashSet", + "std::collections::hash::set::HashSet", + HashTableKind::Set, + field_path(&["map", "table"]), + ), + ] { + let value = rust_135_hash_collection_type(name, kind, 8, SourceLanguage::Rust); + let Some(ValueLayout::HashTable(layout)) = + resolve_value_layout(&value, Some(qualified_name)) + else { + panic!("expected Rust 1.35 hash-table layout for {name}") + }; + let mut control_path = table_path.clone(); + control_path.extend(field_path(&["hashes", "__0", "pointer"])); + let mut marker_path = table_path.clone(); + marker_path.push("marker".to_string()); + assert_eq!(layout.entry_type_path, marker_path); + assert_eq!(layout.control_path, control_path); + assert_eq!( + layout.occupancy, + HashTableOccupancy::NonZeroWord { word_size: 8 } + ); + assert_eq!( + layout.buckets, + HashTableBucketLayout::LegacyAfterControl { + pointer_tag_mask: 1, + } + ); + assert_eq!(layout.bucket_order, HashTableBucketOrder::Forward); + } + } + + #[test] + fn rejects_hash_collection_lookalikes_and_invalid_metadata() { + let current = rust_hash_collection_type( + "HashMap", + HashTableKind::Map, + 8, + true, + false, + false, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(¤t, Some("app::HashMap")), + None + ); + assert_eq!(resolve_value_layout(¤t, None), None); + + let mut invalid_width = current.clone(); + let TypeInfo::StructType { members, .. } = &mut invalid_width.summary else { + unreachable!("test HashMap is a struct") + }; + let TypeInfo::StructType { + members: map_members, + .. + } = &mut members[0].member_type + else { + unreachable!("test base is a hashbrown map") + }; + let TypeInfo::StructType { + members: table_members, + .. + } = &mut map_members[0].member_type + else { + unreachable!("test table is a RawTable") + }; + let TypeInfo::StructType { + members: metadata_members, + .. + } = &mut table_members[0].member_type + else { + unreachable!("test table metadata is a struct") + }; + metadata_members + .iter_mut() + .find(|member| member.name == "items") + .expect("items member") + .member_type = unsigned_type("u32", 4); + assert_eq!( + resolve_value_layout( + &invalid_width, + Some("std::collections::hash::map::HashMap"), + ), + None + ); + + let non_rust = rust_hash_collection_type( + "std::collections::hash::map::HashMap", + HashTableKind::Map, + 8, + true, + false, + false, + SourceLanguage::C, + ); + assert_eq!(resolve_value_layout(&non_rust, None), None); + + let legacy = rust_135_hash_collection_type( + "HashMap", + HashTableKind::Map, + 8, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(&legacy, Some("app::HashMap")), + None + ); + + let mut invalid_legacy = legacy; + let TypeInfo::StructType { members, .. } = &mut invalid_legacy.summary else { + unreachable!("test legacy HashMap is a struct") + }; + let TypeInfo::StructType { + members: table_members, + .. + } = &mut members + .iter_mut() + .find(|member| member.name == "table") + .expect("legacy table member") + .member_type + else { + unreachable!("test legacy table is a struct") + }; + let TypeInfo::StructType { + members: tagged_members, + .. + } = &mut table_members + .iter_mut() + .find(|member| member.name == "hashes") + .expect("legacy hashes member") + .member_type + else { + unreachable!("test tagged hash pointer is a struct") + }; + tagged_members.push(member("unexpected", unsigned_type("usize", 8), 0)); + assert_eq!( + resolve_value_layout( + &invalid_legacy, + Some("std::collections::hash::map::HashMap"), + ), + None + ); + } + + #[test] + fn recognizes_btree_collections_by_namespace_and_map_metadata() { + let map = rust_btree_collection_type( + "BTreeMap", + BTreeKind::Map, + 8, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout( + &map, + Some("alloc::collections::btree::map::BTreeMap"), + ), + Some(ValueLayout::BTree(BTreeLayout { + map_path: Vec::new(), + root_path: field_path(&["root"]), + length_path: field_path(&["length"]), + kind: BTreeKind::Map, + })) + ); + + let set = rust_btree_collection_type( + "alloc::collections::btree::set::BTreeSet", + BTreeKind::Set, + 4, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(&set, None), + Some(ValueLayout::BTree(BTreeLayout { + map_path: field_path(&["map"]), + root_path: field_path(&["map", "root"]), + length_path: field_path(&["map", "length"]), + kind: BTreeKind::Set, + })) + ); + } + + #[test] + fn rejects_btree_collection_lookalikes_and_invalid_metadata() { + let map = rust_btree_collection_type( + "BTreeMap", + BTreeKind::Map, + 8, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(&map, Some("app::BTreeMap")), + None + ); + assert_eq!(resolve_value_layout(&map, None), None); + + let mut missing_length = map.clone(); + let TypeInfo::StructType { members, .. } = &mut missing_length.summary else { + unreachable!("test BTreeMap is a struct") + }; + members.retain(|member| member.name != "length"); + assert_eq!( + resolve_value_layout( + &missing_length, + Some("alloc::collections::btree::map::BTreeMap"), + ), + None + ); + + let non_rust = rust_btree_collection_type( + "alloc::collections::btree::map::BTreeMap", + BTreeKind::Map, + 8, + SourceLanguage::C, + ); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn recognizes_rc_and_arc_with_dwarf_derived_pointer_wrappers() { + let expected = |type_name, value_member: &str| { + let inner_path = vec![ + ProjectedPathSegment::Member("ptr".to_string()), + ProjectedPathSegment::Member("pointer".to_string()), + ProjectedPathSegment::UnwrapScalar, + ProjectedPathSegment::Dereference, + ]; + let mut value_path = inner_path.clone(); + value_path.push(ProjectedPathSegment::Member(value_member.to_string())); + let mut strong_path = inner_path.clone(); + strong_path.push(ProjectedPathSegment::Member("strong".to_string())); + strong_path.push(ProjectedPathSegment::UnwrapScalar); + let mut weak_path = inner_path; + weak_path.push(ProjectedPathSegment::Member("weak".to_string())); + weak_path.push(ProjectedPathSegment::UnwrapScalar); + ValueLayout::CompositeStruct(CompositeStructLayout { + type_name, + fields: vec![ + CompositeStructField { + name: "value", + value_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::KnownSizedOrZst, + ), + }, + CompositeStructField { + name: "strong", + value_path: strong_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::UnsignedPointerSizedInteger, + ), + }, + CompositeStructField { + name: "weak", + value_path: weak_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::UnsignedPointerSizedInteger, + ), + }, + ], + presentation: ProjectedStructPresentation::ReferenceCounted { + strong_field: "strong", + weak_field: "weak", + implicit_weak: 1, + }, + }) + }; + + let rc = rust_reference_counted_type("Rc<(i32, u16)>", 8, true, SourceLanguage::Rust); + assert_eq!( + resolve_value_layout(&rc, Some("alloc::rc::Rc<(i32, u16)>")), + Some(expected("Rc", "value")) + ); + + let arc = rust_reference_counted_type( + "alloc::sync::Arc<(i32, u16)>", + 4, + false, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(&arc, None), + Some(expected("Arc", "data")) + ); + } + + #[test] + fn recognizes_rc_and_arc_dst_addresses_from_thin_and_wide_dwarf_pointers() { + let expected = |type_name, value_member: &str, explicit_metadata| { + let mut inner_path = vec![ + ProjectedPathSegment::Member("ptr".to_string()), + ProjectedPathSegment::Member("pointer".to_string()), + ]; + inner_path.push(if explicit_metadata { + ProjectedPathSegment::Member("data_ptr".to_string()) + } else { + ProjectedPathSegment::UnwrapScalar + }); + inner_path.push(ProjectedPathSegment::Dereference); + let mut value_path = inner_path.clone(); + value_path.push(ProjectedPathSegment::Member(value_member.to_string())); + let mut strong_path = inner_path.clone(); + strong_path.push(ProjectedPathSegment::Member("strong".to_string())); + strong_path.push(ProjectedPathSegment::UnwrapScalar); + let mut weak_path = inner_path; + weak_path.push(ProjectedPathSegment::Member("weak".to_string())); + weak_path.push(ProjectedPathSegment::UnwrapScalar); + ValueLayout::CompositeStruct(CompositeStructLayout { + type_name, + fields: vec![ + CompositeStructField { + name: "ptr", + value_path, + capture: CompositeStructFieldCapture::Address, + }, + CompositeStructField { + name: "strong", + value_path: strong_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::UnsignedPointerSizedInteger, + ), + }, + CompositeStructField { + name: "weak", + value_path: weak_path, + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::UnsignedPointerSizedInteger, + ), + }, + ], + presentation: ProjectedStructPresentation::ReferenceCounted { + strong_field: "strong", + weak_field: "weak", + implicit_weak: 1, + }, + }) + }; + + for pointer_size in [4, 8] { + for (type_name, value_member, qualified_name) in [ + ("Rc", "value", "alloc::rc::Rc"), + ("Arc", "data", "alloc::sync::Arc"), + ] { + for explicit_metadata in [false, true] { + let current = rust_reference_counted_dst_type( + &format!("{type_name}"), + type_name, + value_member, + pointer_size, + explicit_metadata, + SourceLanguage::Rust, + ); + assert_eq!( + resolve_value_layout(¤t, Some(qualified_name)), + Some(expected(type_name, value_member, explicit_metadata)) + ); + } + } + } + } + + #[test] + fn recognizes_slice_dst_and_rejects_reference_counted_lookalikes() { + let lookalike = rust_reference_counted_dst_type( + "Rc", + "Rc", + "value", + 8, + true, + SourceLanguage::Rust, + ); + assert_eq!(resolve_value_layout(&lookalike, Some("app::Rc")), None); + + let slice = rust_reference_counted_dst_type( + "alloc::rc::Rc<[u8]>", + "Rc", + "value", + 8, + true, + SourceLanguage::Rust, + ); + assert!(matches!( + resolve_value_layout(&slice, None), + Some(ValueLayout::CompositeStruct(CompositeStructLayout { + fields, + .. + })) if fields[0].capture == CompositeStructFieldCapture::Address + )); + + let non_rust = rust_reference_counted_dst_type( + "alloc::rc::Rc", + "Rc", + "value", + 8, + true, + SourceLanguage::C, + ); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn rejects_rc_arc_lookalikes_and_non_scalar_pointers() { + let lookalike = rust_reference_counted_type("Rc", 8, false, SourceLanguage::Rust); + assert_eq!(resolve_value_layout(&lookalike, Some("app::Rc")), None); + assert_eq!(resolve_value_layout(&lookalike, None), None); + + let narrow = + rust_reference_counted_type("alloc::rc::Rc", 2, false, SourceLanguage::Rust); + assert_eq!(resolve_value_layout(&narrow, None), None); + + let mut fat = + rust_reference_counted_type("alloc::sync::Arc", 8, false, SourceLanguage::Rust); + let TypeInfo::StructType { members, .. } = &mut fat.summary else { + unreachable!("test Arc is a struct") + }; + let TypeInfo::StructType { + members: ptr_members, + .. + } = &mut members[0].member_type + else { + unreachable!("test Arc ptr is a wrapper") + }; + ptr_members[0].member_type = TypeInfo::StructType { + name: "fat pointer".to_string(), + size: 16, + members: vec![ + member("data", unsigned_type("usize", 8), 0), + member("length", unsigned_type("usize", 8), 8), + ], + }; + assert_eq!(resolve_value_layout(&fat, None), None); + + let non_rust = + rust_reference_counted_type("alloc::rc::Rc", 8, false, SourceLanguage::C); + assert_eq!(resolve_value_layout(&non_rust, None), None); + } + + #[test] + fn recognizes_ref_guards_with_dwarf_derived_pointer_layouts() { + let current = rust_ref_type( + RefTestLayout { + name: "Ref<(i32, u16)>", + root_size: 16, + value_offset: 8, + borrow_offset: 0, + value_pointer_size: 8, + borrow_pointer_size: 8, + language: SourceLanguage::Rust, + marker: false, + }, + TypeInfo::StructType { + name: "(i32, u16)".to_string(), + size: 8, + members: Vec::new(), + }, + ); + + assert_eq!( + resolve_value_layout(¤t, Some("core::cell::Ref<(i32, u16)>")), + Some(ValueLayout::CompositeStruct(CompositeStructLayout { + type_name: "Ref", + fields: vec![ + CompositeStructField { + name: "*value", + value_path: vec![ + ProjectedPathSegment::Member("value".to_string()), + ProjectedPathSegment::Member("value_pointer_from_dwarf".to_string(),), + ProjectedPathSegment::Dereference, + ], + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::KnownSizedOrZst, + ), + }, + CompositeStructField { + name: "borrow", + value_path: vec![ + ProjectedPathSegment::Member("borrow".to_string()), + ProjectedPathSegment::Member("borrow_pointer_from_dwarf".to_string(),), + ProjectedPathSegment::Dereference, + ProjectedPathSegment::SoleMember, + ProjectedPathSegment::SoleMember, + ], + capture: CompositeStructFieldCapture::Value( + ProjectedValueRequirement::SignedPointerSizedInteger, + ), + }, + ], + presentation: ProjectedStructPresentation::SignedState { + state_field: "borrow", + non_negative_label: "borrow", + negative_label: "borrow_mut", + }, + })) + ); + + let mut legacy = current.clone(); + let TypeInfo::StructType { members, .. } = &mut legacy.summary else { + unreachable!("test Ref is a struct") + }; + let value = members + .iter_mut() + .find(|member| member.name == "value") + .expect("test Ref has a value member"); + let pointer_type = { + let TypeInfo::StructType { members, .. } = &value.member_type else { + unreachable!("current test Ref value is a pointer wrapper") + }; + let [pointer] = members.as_slice() else { + unreachable!("current test Ref value wrapper has one member") + }; + pointer.member_type.clone() + }; + value.member_type = pointer_type; + let Some(ValueLayout::CompositeStruct(legacy_layout)) = + resolve_value_layout(&legacy, Some("core::cell::Ref<(i32, u16)>")) + else { + panic!("legacy raw-pointer Ref layout should be recognized") + }; + assert_eq!( + legacy_layout.fields[0].value_path, + vec![ + ProjectedPathSegment::Member("value".to_string()), + ProjectedPathSegment::Dereference, + ] + ); + + let ref_mut = rust_ref_type( + RefTestLayout { + name: "core::cell::RefMut", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + value_pointer_size: 8, + borrow_pointer_size: 8, + language: SourceLanguage::Rust, + marker: true, + }, + signed_type("i32", 4), + ); + assert!(resolve_value_layout(&ref_mut, None).is_some()); + } + + #[test] + fn rejects_ref_guard_lookalikes_and_invalid_outer_layouts() { + let make_ref = |name, language| { + rust_ref_type( + RefTestLayout { + name, + root_size: 16, + value_offset: 0, + borrow_offset: 8, + value_pointer_size: 8, + borrow_pointer_size: 8, + language, + marker: false, + }, + signed_type("i32", 4), + ) + }; + let current = make_ref("Ref", SourceLanguage::Rust); + assert_eq!(resolve_value_layout(¤t, Some("app::Ref")), None); + assert_eq!(resolve_value_layout(¤t, None), None); + assert_eq!( + resolve_value_layout(&make_ref("core::cell::Ref", SourceLanguage::C), None,), + None + ); + + let overlapping = rust_ref_type( + RefTestLayout { + name: "core::cell::Ref", + root_size: 16, + value_offset: 0, + borrow_offset: 4, + value_pointer_size: 8, + borrow_pointer_size: 8, + language: SourceLanguage::Rust, + marker: false, + }, + signed_type("i32", 4), + ); + assert_eq!(resolve_value_layout(&overlapping, None), None); + + let mismatched_widths = rust_ref_type( + RefTestLayout { + name: "core::cell::Ref", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + value_pointer_size: 8, + borrow_pointer_size: 4, + language: SourceLanguage::Rust, + marker: false, + }, + signed_type("i32", 4), + ); + assert_eq!(resolve_value_layout(&mismatched_widths, None), None); + + let mut ambiguous_wrapper = make_ref("core::cell::Ref", SourceLanguage::Rust); + let TypeInfo::StructType { members, .. } = &mut ambiguous_wrapper.summary else { + unreachable!("test Ref is a struct") + }; + let TypeInfo::StructType { + members: value_members, + .. + } = &mut members[1].member_type + else { + unreachable!("test Ref value is a wrapper") + }; + value_members.push(member("extra", signed_type("i32", 4), 0)); + assert_eq!(resolve_value_layout(&ambiguous_wrapper, None), None); + } + + #[test] + fn qualified_name_lookup_is_limited_to_ambiguous_rust_std_types() { + assert!(requires_dwarf_qualified_name(&rust_string_type( + "String", + SourceLanguage::Rust, + 8, + true, + true, + ))); + assert!(!requires_dwarf_qualified_name(&rust_string_type( + "alloc::string::String", + SourceLanguage::Rust, + 8, + true, + true, + ))); + assert!(!requires_dwarf_qualified_name(&rust_string_type( + "String", + SourceLanguage::C, + 8, + true, + true, + ))); + assert!(!requires_dwarf_qualified_name(&rust_str_type_64( + "&str", + SourceLanguage::Rust, + ))); + assert!(requires_dwarf_qualified_name(&rust_vec_type( + "Vec", + SourceLanguage::Rust, + 8, + true, + true, + ))); + assert!(!requires_dwarf_qualified_name(&rust_vec_type( + "alloc::vec::Vec", + SourceLanguage::Rust, + 8, + true, + true, + ))); + assert!(requires_dwarf_qualified_name(&rust_vec_deque_type( + VecDequeTestLayout { + name: "VecDeque", + pointer_size: 8, + uses_raw_vec_inner: true, + wraps_capacity: true, + wraps_head: false, + uses_legacy_tail: false, + }, + ))); + assert!(!requires_dwarf_qualified_name(&rust_vec_deque_type( + VecDequeTestLayout { + name: "alloc::collections::vec_deque::VecDeque", + pointer_size: 8, + uses_raw_vec_inner: true, + wraps_capacity: true, + wraps_head: false, + uses_legacy_tail: false, + }, + ))); + assert!(requires_dwarf_qualified_name(&rust_str_type_64( + "Box", + SourceLanguage::Rust, + ))); + assert!(!requires_dwarf_qualified_name(&rust_str_type_64( + "alloc::boxed::Box", + SourceLanguage::Rust, + ))); + assert!(requires_dwarf_qualified_name(&rust_os_string_type( + "OsString", 8, true, None, + ))); + assert!(!requires_dwarf_qualified_name(&rust_os_string_type( + "std::ffi::os_str::OsString", + 8, + true, + None, + ))); + assert!(requires_dwarf_qualified_name(&rust_hash_collection_type( + "HashMap", + HashTableKind::Map, + 8, + true, + false, + false, + SourceLanguage::Rust, + ))); + assert!(!requires_dwarf_qualified_name(&rust_hash_collection_type( + "std::collections::hash::map::HashMap", + HashTableKind::Map, + 8, + true, + false, + false, + SourceLanguage::Rust, + ),)); + assert!(requires_dwarf_qualified_name(&rust_nonzero_type( + "NonZero", + "__0", + "__0", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ))); + assert!(!requires_dwarf_qualified_name(&rust_nonzero_type( + "core::num::nonzero::NonZero", + "__0", + "__0", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ))); + assert!(requires_dwarf_qualified_name(&rust_legacy_nonzero_type( + "NonZeroU32", + "__0", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ))); + assert!(!requires_dwarf_qualified_name(&rust_legacy_nonzero_type( + "core::num::NonZeroU32", + "__0", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ))); + assert!(requires_dwarf_qualified_name(&rust_cell_type( + "Cell", + "value", + "value", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ))); + assert!(!requires_dwarf_qualified_name(&rust_cell_type( + "core::cell::Cell", + "value", + "value", + unsigned_type("u32", 4), + SourceLanguage::Rust, + ))); + assert!(requires_dwarf_qualified_name(&rust_ref_cell_type( + RefCellTestLayout { + name: "RefCell", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + borrow_size: 8, + language: SourceLanguage::Rust, + }, + unsigned_type("u32", 4), + ))); + assert!(!requires_dwarf_qualified_name(&rust_ref_cell_type( + RefCellTestLayout { + name: "core::cell::RefCell", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + borrow_size: 8, + language: SourceLanguage::Rust, + }, + unsigned_type("u32", 4), + ))); + assert!(requires_dwarf_qualified_name(&rust_ref_type( + RefTestLayout { + name: "Ref", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + value_pointer_size: 8, + borrow_pointer_size: 8, + language: SourceLanguage::Rust, + marker: false, + }, + signed_type("i32", 4), + ))); + assert!(!requires_dwarf_qualified_name(&rust_ref_type( + RefTestLayout { + name: "core::cell::RefMut", + root_size: 16, + value_offset: 0, + borrow_offset: 8, + value_pointer_size: 8, + borrow_pointer_size: 8, + language: SourceLanguage::Rust, + marker: true, + }, + signed_type("i32", 4), + ))); + assert!(requires_dwarf_qualified_name(&rust_reference_counted_type( + "Rc", + 8, + false, + SourceLanguage::Rust + ))); + assert!(!requires_dwarf_qualified_name( + &rust_reference_counted_type("alloc::sync::Arc", 8, false, SourceLanguage::Rust,) + )); + } +} diff --git a/ghostscope-dwarf/src/language/rust/variant.rs b/ghostscope-dwarf/src/language/rust/variant.rs new file mode 100644 index 00000000..29c921f5 --- /dev/null +++ b/ghostscope-dwarf/src/language/rust/variant.rs @@ -0,0 +1,247 @@ +use crate::{TypeInfo, VariantCase, VariantPart, VariantPayloadPresentation}; + +pub(super) fn annotate_type_info(type_info: &mut TypeInfo) { + match type_info { + TypeInfo::PointerType { target_type, .. } + | TypeInfo::ArrayType { + element_type: target_type, + .. + } + | TypeInfo::TypedefType { + underlying_type: target_type, + .. + } + | TypeInfo::QualifiedType { + underlying_type: target_type, + .. + } + | TypeInfo::BitfieldType { + underlying_type: target_type, + .. + } + | TypeInfo::EnumType { + base_type: target_type, + .. + } + | TypeInfo::ScopedEnumType { + base_type: target_type, + .. + } => annotate_type_info(target_type), + TypeInfo::StructType { members, .. } | TypeInfo::UnionType { members, .. } => { + annotate_members(members); + } + TypeInfo::FunctionType { + return_type, + parameters, + } => { + if let Some(return_type) = return_type { + annotate_type_info(return_type); + } + for parameter in parameters { + annotate_type_info(parameter); + } + } + TypeInfo::VariantType { + members, + variant_parts, + .. + } => { + annotate_members(members); + annotate_variant_parts(variant_parts); + } + TypeInfo::BaseType { .. } + | TypeInfo::UnknownType { .. } + | TypeInfo::OptimizedOut { .. } => {} + } +} + +fn annotate_members(members: &mut [crate::StructMember]) { + for member in members { + annotate_type_info(&mut member.member_type); + } +} + +fn annotate_variant_parts(parts: &mut [VariantPart]) { + for part in parts { + if let Some(discriminant) = &mut part.discriminant { + annotate_type_info(&mut discriminant.member_type); + } + for variant in &mut part.variants { + variant.payload_presentation = rust_payload_presentation(variant); + annotate_members(&mut variant.members); + annotate_variant_parts(&mut variant.variant_parts); + } + } +} + +fn rust_payload_presentation(variant: &VariantCase) -> VariantPayloadPresentation { + // rustc emits one wrapper member per enum branch. Unit wrappers have no + // fields, tuple payload fields are named `__0`, `__1`, and so on, and + // struct payloads preserve their source field names. Active-branch + // selection still comes entirely from the standard DWARF variant graph; + // this producer convention controls presentation only. + let [member] = variant.members.as_slice() else { + return VariantPayloadPresentation::Dwarf; + }; + let TypeInfo::StructType { members, .. } = member.member_type.underlying_type() else { + return VariantPayloadPresentation::Dwarf; + }; + if members.is_empty() { + return VariantPayloadPresentation::Unit; + } + if members.iter().enumerate().all(|(index, field)| { + u32::try_from(index) + .ok() + .is_some_and(|index| super::access::is_tuple_field_name(&field.name, index)) + }) { + VariantPayloadPresentation::Tuple + } else { + VariantPayloadPresentation::Struct + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{DiscriminantValue, StructMember, VariantSelector}; + + fn member(name: &str, member_type: TypeInfo) -> StructMember { + StructMember { + name: name.to_string(), + member_type, + offset: 0, + bit_offset: None, + bit_size: None, + } + } + + fn payload(name: &str, members: Vec) -> StructMember { + member( + name, + TypeInfo::StructType { + name: name.to_string(), + size: 4, + members, + }, + ) + } + + fn variant(name: &str, members: Vec) -> VariantCase { + VariantCase { + selector: VariantSelector::Default, + members: vec![payload(name, members)], + variant_parts: Vec::new(), + payload_presentation: VariantPayloadPresentation::Dwarf, + } + } + + fn scalar() -> TypeInfo { + TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::DW_ATE_signed.0 as u16, + } + } + + #[test] + fn annotates_rust_variant_payload_shapes_recursively() { + let variant_type = TypeInfo::VariantType { + name: "Example".to_string(), + size: 4, + members: Vec::new(), + variant_parts: vec![VariantPart { + discriminant: None, + variants: vec![ + variant("Unit", Vec::new()), + variant( + "Tuple", + vec![member("__0", scalar()), member("__1", scalar())], + ), + variant( + "Struct", + vec![member("left", scalar()), member("right", scalar())], + ), + ], + }], + }; + let mut type_info = TypeInfo::StructType { + name: "Container".to_string(), + size: 4, + members: vec![member("nested", variant_type)], + }; + + crate::language::annotate_type_info(crate::SourceLanguage::Rust, &mut type_info); + + let TypeInfo::StructType { members, .. } = type_info else { + panic!("expected container type"); + }; + let TypeInfo::VariantType { variant_parts, .. } = &members[0].member_type else { + panic!("expected variant type"); + }; + let presentations = variant_parts[0] + .variants + .iter() + .map(|variant| variant.payload_presentation) + .collect::>(); + assert_eq!( + presentations, + [ + VariantPayloadPresentation::Unit, + VariantPayloadPresentation::Tuple, + VariantPayloadPresentation::Struct, + ] + ); + } + + #[test] + fn does_not_annotate_other_source_languages() { + let mut type_info = TypeInfo::VariantType { + name: "CppVariant".to_string(), + size: 4, + members: Vec::new(), + variant_parts: vec![VariantPart { + discriminant: None, + variants: vec![variant("Value", vec![member("__0", scalar())])], + }], + }; + + crate::language::annotate_type_info(crate::SourceLanguage::Cpp, &mut type_info); + + let TypeInfo::VariantType { variant_parts, .. } = type_info else { + panic!("expected variant type"); + }; + assert_eq!( + variant_parts[0].variants[0].payload_presentation, + VariantPayloadPresentation::Dwarf + ); + } + + #[test] + fn rejects_non_contiguous_tuple_field_names() { + let variant = variant( + "AlmostTuple", + vec![member("__0", scalar()), member("__2", scalar())], + ); + assert_eq!( + rust_payload_presentation(&variant), + VariantPayloadPresentation::Struct + ); + } + + #[test] + fn leaves_non_struct_payloads_language_neutral() { + let variant = VariantCase { + selector: VariantSelector::Ranges(vec![crate::DiscriminantRange { + start: DiscriminantValue::Unsigned(0), + end: DiscriminantValue::Unsigned(0), + }]), + members: vec![member("Value", scalar())], + variant_parts: Vec::new(), + payload_presentation: VariantPayloadPresentation::Dwarf, + }; + assert_eq!( + rust_payload_presentation(&variant), + VariantPayloadPresentation::Dwarf + ); + } +} diff --git a/ghostscope-dwarf/src/lib.rs b/ghostscope-dwarf/src/lib.rs index 1a81b25a..6cf8bad4 100644 --- a/ghostscope-dwarf/src/lib.rs +++ b/ghostscope-dwarf/src/lib.rs @@ -43,16 +43,18 @@ pub use core::{ // Re-export semantic contract types. pub use semantics::{ indexable_element_layout, is_aggregate_type, is_pointer_or_array_type, member_layout, - strip_type_aliases, AddressOrigin, AddressSpaceInfo, CfaRulePlan, CompactUnwindRow, - CompactUnwindStats, CompactUnwindTable, CompilationUnitMetadata, FunctionParameter, - IndexableElementLayout, InlineFrame, LvalueAddressPlan, MemberLayout, PcContext, PcLineInfo, - PcRange, PlannedAddress, PlannedAddressKind, PlannedValue, ProducerInfo, RegisterRecoveryPlan, - ResolvedType, RuntimeComputedExpr, RuntimeComputedKind, SemanticType, SourceLanguage, - SyntheticTypeKind, TypeIdentity, TypeLayoutError, TypeOrigin, TypeProjection, - TypeProjectionLayout, UnwindDiagnostic, UnwindDiagnosticKind, VariableAccessPath, - VariableAccessSegment, VariableLoweringKind, VariableLoweringPlan, VariableMaterialization, - VariableMaterializationPlan, VariablePlan, VariableQueryDiagnostic, VariableReadPlan, - VisibleVariable, VisibleVariablesResult, + strip_type_aliases, AddressOrigin, AddressSpaceInfo, BTreeArrayCapture, BTreeEdgesCapture, + CfaRulePlan, CompactUnwindRow, CompactUnwindStats, CompactUnwindTable, CompilationUnitMetadata, + FunctionParameter, HashTableBucketSource, IndexableElementLayout, InlineFrame, + LvalueAddressPlan, MemberLayout, PcContext, PcLineInfo, PcRange, PlannedAddress, + PlannedAddressKind, PlannedValue, ProducerInfo, ProjectedValueRead, ProjectedValueStep, + ProjectedViewField, ProjectedViewFieldCapture, RegisterRecoveryPlan, ResolvedType, + RingSequenceLength, RuntimeComputedExpr, RuntimeComputedKind, RustcVersion, SemanticType, + SourceLanguage, SyntheticTypeKind, TypeIdentity, TypeLayoutError, TypeOrigin, TypeProjection, + TypeProjectionLayout, UnwindDiagnostic, UnwindDiagnosticKind, ValueCapturePlan, ValueReadPlan, + VariableAccessPath, VariableAccessSegment, VariableLoweringKind, VariableLoweringPlan, + VariableMaterialization, VariableMaterializationPlan, VariablePlan, VariableQueryDiagnostic, + VariableReadPlan, VisibleVariable, VisibleVariablesResult, }; pub use semantics::{ @@ -67,7 +69,11 @@ pub use semantics::is_pointer_or_array_type as is_c_pointer_or_array_type; // Re-export type definitions from protocol (avoiding circular dependencies) pub use ghostscope_protocol::{ - EnumVariant, StructMember, TypeCache, TypeInfo, TypeKind, TypeQualifier, + BTreeEntryPresentation, BTreeFieldPresentation, DiscriminantRange, DiscriminantValue, + EnumVariant, HashTableBucketOrder, HashTableEntryPresentation, HashTableFieldPresentation, + HashTableOccupancy, ScopedEnumVariant, StructMember, TypeCache, TypeInfo, TypeKind, + TypeQualifier, ValuePresentation, VariantCase, VariantPart, VariantPayloadPresentation, + VariantSelector, }; // Re-export gimli types that external users need diff --git a/ghostscope-dwarf/src/objfile/loaded.rs b/ghostscope-dwarf/src/objfile/loaded.rs index 0ff16190..4631aa8a 100644 --- a/ghostscope-dwarf/src/objfile/loaded.rs +++ b/ghostscope-dwarf/src/objfile/loaded.rs @@ -44,6 +44,8 @@ pub(crate) struct LoadedObjfile { pub(super) dwarf_index_status: crate::DwarfIndexStatus, pub(super) lazy_debug_info: bool, pub(super) indexed_debug_info_cus: Mutex>, + pub(super) compilation_unit_languages: + RwLock>, pub(super) load_parse_ms: u64, pub(super) load_index_ms: u64, pub(super) load_total_ms: u64, diff --git a/ghostscope-dwarf/src/objfile/loading.rs b/ghostscope-dwarf/src/objfile/loading.rs index e4b1a4e1..055f3c5b 100644 --- a/ghostscope-dwarf/src/objfile/loading.rs +++ b/ghostscope-dwarf/src/objfile/loading.rs @@ -394,6 +394,7 @@ impl LoadedObjfile { dwarf_index_status, lazy_debug_info, indexed_debug_info_cus: Mutex::new(HashSet::new()), + compilation_unit_languages: RwLock::new(HashMap::new()), _dwarf_mapped_file: mapped_file, _binary_mapped_file: binary_mapped, debug_info_source, diff --git a/ghostscope-dwarf/src/objfile/type_context.rs b/ghostscope-dwarf/src/objfile/type_context.rs index 1f04cd57..86b6fdd4 100644 --- a/ghostscope-dwarf/src/objfile/type_context.rs +++ b/ghostscope-dwarf/src/objfile/type_context.rs @@ -4,8 +4,8 @@ use super::{variables::complete_aggregate_declaration_entry, LoadedObjfile}; use crate::{ core::Result, semantics::{ - resolve_type_ref_with_origins, CompilationUnitMetadata, ProducerInfo, SourceLanguage, - TypeLoc, VariableAccessSegment, + resolve_attr_with_unit_origins, resolve_name_with_origins, resolve_type_ref_with_origins, + CompilationUnitMetadata, ProducerInfo, SourceLanguage, TypeLoc, VariableAccessSegment, }, CuId, ModuleId, TypeId, }; @@ -43,6 +43,45 @@ fn type_id_from_loc(module: ModuleId, loc: TypeLoc) -> TypeId { TypeId { module, cu, die } } +fn qualified_type_name_from_dwarf( + dwarf: &gimli::Dwarf, + loc: TypeLoc, +) -> Result> { + let header = dwarf.unit_header(loc.cu_off)?; + let unit = dwarf.unit(header)?; + let mut namespaces = Vec::<(isize, String)>::new(); + let mut entries = unit.entries(); + + while let Some(entry) = entries.next_dfs()? { + while namespaces + .last() + .is_some_and(|(depth, _)| *depth >= entry.depth()) + { + namespaces.pop(); + } + + if entry.offset() == loc.die_off { + let Some(name) = resolve_name_with_origins(dwarf, &unit, entry)? else { + return Ok(None); + }; + let mut components = namespaces + .iter() + .map(|(_, namespace)| namespace.as_str()) + .collect::>(); + components.push(&name); + return Ok(Some(components.join("::"))); + } + + if entry.tag() == gimli::DW_TAG_namespace { + if let Some(name) = resolve_name_with_origins(dwarf, &unit, entry)? { + namespaces.push((entry.depth(), name)); + } + } + } + + Ok(None) +} + fn compilation_unit_metadata_from_dwarf( dwarf: &gimli::Dwarf, module: ModuleId, @@ -214,6 +253,64 @@ fn projected_element_type_loc( resolve_type_ref_with_origins(dwarf, &entry, &unit) } +fn template_type_parameter_loc( + dwarf: &gimli::Dwarf, + type_name_index: &crate::index::TypeNameIndex, + loc: TypeLoc, + index: usize, +) -> Result> { + let Some(aggregate_loc) = normalize_type_loc(dwarf, type_name_index, loc)? else { + return Ok(None); + }; + let header = dwarf.unit_header(aggregate_loc.cu_off)?; + let unit = dwarf.unit(header)?; + let entry = unit.entry(aggregate_loc.die_off)?; + if !matches!( + entry.tag(), + gimli::DW_TAG_structure_type | gimli::DW_TAG_class_type | gimli::DW_TAG_union_type + ) { + return Ok(None); + } + + let mut tree = unit.entries_tree(Some(entry.offset()))?; + let root = tree.root()?; + let mut children = root.children(); + let mut type_parameter_index = 0usize; + while let Some(child) = children.next()? { + let parameter = child.entry(); + if parameter.tag() != gimli::DW_TAG_template_type_parameter { + continue; + } + if type_parameter_index == index { + let Some(parameter_loc) = resolve_type_ref_with_origins(dwarf, parameter, &unit)? + else { + return Ok(None); + }; + return normalize_type_loc(dwarf, type_name_index, parameter_loc); + } + type_parameter_index += 1; + } + + Ok(None) +} + +fn type_alignment_from_dwarf( + dwarf: &gimli::Dwarf, + type_name_index: &crate::index::TypeNameIndex, + loc: TypeLoc, +) -> Result> { + let Some(loc) = normalize_type_loc(dwarf, type_name_index, loc)? else { + return Ok(None); + }; + let header = dwarf.unit_header(loc.cu_off)?; + let unit = dwarf.unit(header)?; + let entry = unit.entry(loc.die_off)?; + Ok( + resolve_attr_with_unit_origins(&entry, &unit, gimli::DW_AT_alignment)? + .and_then(crate::core::attr_u64), + ) +} + fn projected_type_loc( dwarf: &gimli::Dwarf, type_name_index: &crate::index::TypeNameIndex, @@ -237,6 +334,37 @@ fn projected_type_loc( } impl LoadedObjfile { + pub(super) fn compilation_unit_language( + &self, + cu_off: gimli::DebugInfoOffset, + unit: &gimli::Unit, + ) -> SourceLanguage { + if let Some(language) = self + .compilation_unit_languages + .read() + .expect("compilation unit language cache lock poisoned") + .get(&cu_off) + .copied() + { + return language; + } + + let mut entries = unit.entries(); + let dwarf_language = entries.next_dfs().ok().flatten().and_then(|root| { + match root.attr_value(gimli::DW_AT_language) { + Some(gimli::AttributeValue::Language(language)) => Some(language), + _ => None, + } + }); + let language = SourceLanguage::from_dwarf(dwarf_language); + *self + .compilation_unit_languages + .write() + .expect("compilation unit language cache lock poisoned") + .entry(cu_off) + .or_insert(language) + } + pub(crate) fn compilation_unit_metadata( &self, module: ModuleId, @@ -258,6 +386,55 @@ impl LoadedObjfile { .map(|loc| loc.map(|loc| type_id_from_loc(current.module, loc))) } + pub(crate) fn type_summary(&self, current: TypeId) -> Result> { + let normalized = { + let type_name_index = self + .type_name_index + .read() + .expect("type name index lock poisoned"); + normalize_type_loc(self.dwarf(), &type_name_index, type_loc(current)?)? + }; + Ok(normalized.and_then(|loc| self.detailed_shallow_type(loc.cu_off, loc.die_off))) + } + + pub(crate) fn qualified_type_name(&self, current: TypeId) -> Result> { + qualified_type_name_from_dwarf(self.dwarf(), type_loc(current)?) + } + + pub(crate) fn template_type_parameter( + &self, + current: TypeId, + index: usize, + ) -> Result> { + let parameter_loc = { + let type_name_index = self + .type_name_index + .read() + .expect("type name index lock poisoned"); + template_type_parameter_loc(self.dwarf(), &type_name_index, type_loc(current)?, index)? + }; + let Some(parameter_loc) = parameter_loc else { + return Ok(None); + }; + let Some(summary) = self.detailed_shallow_type(parameter_loc.cu_off, parameter_loc.die_off) + else { + return Ok(None); + }; + + Ok(Some(( + type_id_from_loc(current.module, parameter_loc), + summary, + ))) + } + + pub(crate) fn type_alignment(&self, current: TypeId) -> Result> { + let type_name_index = self + .type_name_index + .read() + .expect("type name index lock poisoned"); + type_alignment_from_dwarf(self.dwarf(), &type_name_index, type_loc(current)?) + } + pub(crate) fn aggregate_type_id_by_name(&self, module: ModuleId, name: &str) -> Option { if let Err(error) = self.ensure_debug_info_for_type_name(name) { tracing::warn!( @@ -302,6 +479,7 @@ mod tests { dwarf: gimli::Dwarf, cu: CuId, pair: TypeLoc, + generic: TypeLoc, int: TypeLoc, pair_pointer: TypeLoc, int_array: TypeLoc, @@ -354,6 +532,19 @@ mod tests { unit.get_mut(member) .set(gimli::DW_AT_type, WriteAttributeValue::UnitRef(int)); + let generic = unit.add(root, gimli::DW_TAG_structure_type); + unit.get_mut(generic).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"Generic".to_vec()), + ); + let parameter = unit.add(generic, gimli::DW_TAG_template_type_parameter); + unit.get_mut(parameter).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"T".to_vec()), + ); + unit.get_mut(parameter) + .set(gimli::DW_AT_type, WriteAttributeValue::UnitRef(int)); + let pair_pointer = unit.add(root, gimli::DW_TAG_pointer_type); unit.get_mut(pair_pointer) .set(gimli::DW_AT_type, WriteAttributeValue::UnitRef(pair)); @@ -382,7 +573,7 @@ mod tests { let header = dwarf.units().next().unwrap().unwrap(); let cu_off = header.debug_info_offset().unwrap(); let unit = dwarf.unit(header).unwrap(); - let mut pair = None; + let mut structures = Vec::new(); let mut int = None; let mut pair_pointer = None; let mut int_array = None; @@ -394,17 +585,19 @@ mod tests { }; match entry.tag() { gimli::DW_TAG_base_type => int = Some(loc), - gimli::DW_TAG_structure_type => pair = Some(loc), + gimli::DW_TAG_structure_type => structures.push(loc), gimli::DW_TAG_pointer_type => pair_pointer = Some(loc), gimli::DW_TAG_array_type => int_array = Some(loc), _ => {} } } + assert_eq!(structures.len(), 2); Fixture { dwarf, cu: CuId(cu_off.0 as u32), - pair: pair.unwrap(), + pair: structures[0], + generic: structures[1], int: int.unwrap(), pair_pointer: pair_pointer.unwrap(), int_array: int_array.unwrap(), @@ -512,6 +705,78 @@ mod tests { ) } + fn build_qualified_name_fixture() -> (gimli::Dwarf, TypeLoc, TypeLoc) { + let encoding = gimli::Encoding { + format: Format::Dwarf32, + version: 5, + address_size: 8, + }; + let mut dwarf = WriteDwarf::new(); + let unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none())); + { + let unit = dwarf.units.get_mut(unit_id); + let root = unit.root(); + + let alloc = unit.add(root, gimli::DW_TAG_namespace); + unit.get_mut(alloc).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"alloc".to_vec()), + ); + let string = unit.add(alloc, gimli::DW_TAG_namespace); + unit.get_mut(string).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"string".to_vec()), + ); + let std_string = unit.add(string, gimli::DW_TAG_structure_type); + unit.get_mut(std_string).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"String".to_vec()), + ); + + let app = unit.add(root, gimli::DW_TAG_namespace); + unit.get_mut(app).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"app".to_vec()), + ); + let app_string = unit.add(app, gimli::DW_TAG_structure_type); + unit.get_mut(app_string).set( + gimli::DW_AT_name, + WriteAttributeValue::String(b"String".to_vec()), + ); + } + + let mut sections = Sections::new(EndianVec::new(LittleEndian)); + dwarf.write(&mut sections).unwrap(); + let sections = gimli::DwarfSections::load(|id| { + Ok::<_, gimli::Error>( + sections + .get(id) + .map(|section| section.slice().to_vec()) + .unwrap_or_default(), + ) + }) + .unwrap(); + let dwarf = + sections.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice()))); + + let header = dwarf.units().next().unwrap().unwrap(); + let cu_off = header.debug_info_offset().unwrap(); + let unit = dwarf.unit(header).unwrap(); + let mut strings = Vec::new(); + let mut entries = unit.entries(); + while let Some(entry) = entries.next_dfs().unwrap() { + if entry.tag() == gimli::DW_TAG_structure_type { + strings.push(TypeLoc { + cu_off, + die_off: entry.offset(), + }); + } + } + assert_eq!(strings.len(), 2); + + (dwarf, strings[0], strings[1]) + } + #[test] fn reads_language_and_producer_from_compilation_unit() { let fixture = build_fixture(); @@ -523,6 +788,10 @@ mod tests { assert_eq!(metadata.module, ModuleId(7)); assert_eq!(metadata.language, SourceLanguage::Rust); assert_eq!(metadata.dwarf_version, 5); + assert_eq!( + metadata.rustc_version(), + Some(crate::RustcVersion::new(1, 88, 0)) + ); assert_eq!( metadata .producer @@ -572,6 +841,21 @@ mod tests { assert_eq!(element, Some(fixture.int)); } + #[test] + fn resolves_template_type_parameter_by_dwarf_order() { + let fixture = build_fixture(); + let types = TypeNameIndex::default(); + + assert_eq!( + template_type_parameter_loc(&fixture.dwarf, &types, fixture.generic, 0).unwrap(), + Some(fixture.int) + ); + assert_eq!( + template_type_parameter_loc(&fixture.dwarf, &types, fixture.generic, 1).unwrap(), + None + ); + } + #[test] fn follows_cross_cu_member_type_and_changes_language_origin() { let (dwarf, wrapper, c_int) = build_cross_cu_fixture(); @@ -601,4 +885,18 @@ mod tests { Some("clang version 18") ); } + + #[test] + fn reconstructs_type_names_from_exact_namespace_ancestors() { + let (dwarf, std_string, app_string) = build_qualified_name_fixture(); + + assert_eq!( + qualified_type_name_from_dwarf(&dwarf, std_string).unwrap(), + Some("alloc::string::String".to_string()) + ); + assert_eq!( + qualified_type_name_from_dwarf(&dwarf, app_string).unwrap(), + Some("app::String".to_string()) + ); + } } diff --git a/ghostscope-dwarf/src/objfile/variables.rs b/ghostscope-dwarf/src/objfile/variables.rs index e97d8694..d1ab26ef 100644 --- a/ghostscope-dwarf/src/objfile/variables.rs +++ b/ghostscope-dwarf/src/objfile/variables.rs @@ -423,6 +423,7 @@ impl LoadedObjfile { dwarf, &unit, td.die_offset, + self.compilation_unit_language(td.cu_offset, &unit), )?; return Some(( ty, @@ -737,7 +738,7 @@ impl LoadedObjfile { None } - fn detailed_shallow_type( + pub(crate) fn detailed_shallow_type( &self, cu_off: gimli::DebugInfoOffset, die_off: gimli::UnitOffset, @@ -762,10 +763,16 @@ impl LoadedObjfile { dwarf, &def_unit, def_die_off, + self.compilation_unit_language(def_cu_off, &def_unit), ); } - crate::parser::DetailedParser::resolve_type_shallow_at_offset(dwarf, &unit, die_off) + crate::parser::DetailedParser::resolve_type_shallow_at_offset( + dwarf, + &unit, + die_off, + self.compilation_unit_language(cu_off, &unit), + ) } pub(crate) fn shallow_type_for_variable_offsets( diff --git a/ghostscope-dwarf/src/parser/detailed_parser.rs b/ghostscope-dwarf/src/parser/detailed_parser.rs index 8f71cc4f..1618600d 100644 --- a/ghostscope-dwarf/src/parser/detailed_parser.rs +++ b/ghostscope-dwarf/src/parser/detailed_parser.rs @@ -19,14 +19,16 @@ use crate::{ use gimli::Reader; // Alias gimli constants to upper-case identifiers to satisfy naming lints without allow attributes use gimli::constants::{ - DW_AT_byte_size as DW_AT_BYTE_SIZE, DW_AT_encoding as DW_AT_ENCODING, DW_AT_name as DW_AT_NAME, - DW_AT_type as DW_AT_TYPE, DW_TAG_array_type as DW_TAG_ARRAY_TYPE, - DW_TAG_base_type as DW_TAG_BASE_TYPE, DW_TAG_class_type as DW_TAG_CLASS_TYPE, - DW_TAG_const_type as DW_TAG_CONST_TYPE, DW_TAG_enumeration_type as DW_TAG_ENUMERATION_TYPE, - DW_TAG_pointer_type as DW_TAG_POINTER_TYPE, DW_TAG_restrict_type as DW_TAG_RESTRICT_TYPE, - DW_TAG_structure_type as DW_TAG_STRUCTURE_TYPE, + DW_AT_byte_size as DW_AT_BYTE_SIZE, DW_AT_discr as DW_AT_DISCR, + DW_AT_discr_list as DW_AT_DISCR_LIST, DW_AT_discr_value as DW_AT_DISCR_VALUE, + DW_AT_encoding as DW_AT_ENCODING, DW_AT_name as DW_AT_NAME, DW_AT_type as DW_AT_TYPE, + DW_TAG_array_type as DW_TAG_ARRAY_TYPE, DW_TAG_base_type as DW_TAG_BASE_TYPE, + DW_TAG_class_type as DW_TAG_CLASS_TYPE, DW_TAG_const_type as DW_TAG_CONST_TYPE, + DW_TAG_enumeration_type as DW_TAG_ENUMERATION_TYPE, DW_TAG_pointer_type as DW_TAG_POINTER_TYPE, + DW_TAG_restrict_type as DW_TAG_RESTRICT_TYPE, DW_TAG_structure_type as DW_TAG_STRUCTURE_TYPE, DW_TAG_subroutine_type as DW_TAG_SUBROUTINE_TYPE, DW_TAG_typedef as DW_TAG_TYPEDEF, - DW_TAG_union_type as DW_TAG_UNION_TYPE, DW_TAG_volatile_type as DW_TAG_VOLATILE_TYPE, + DW_TAG_union_type as DW_TAG_UNION_TYPE, DW_TAG_variant as DW_TAG_VARIANT, + DW_TAG_variant_part as DW_TAG_VARIANT_PART, DW_TAG_volatile_type as DW_TAG_VOLATILE_TYPE, }; use std::collections::HashSet; // no tracing imports needed here @@ -75,11 +77,341 @@ impl DetailedParser { Self {} } + fn parse_member_at_offset( + dwarf: &gimli::Dwarf, + unit: &gimli::Unit, + offset: gimli::UnitOffset, + fallback_name: &str, + ) -> Option { + let entry = unit.entry(offset).ok()?; + let name = entry + .attr(DW_AT_NAME) + .and_then(|attr| dwarf.attr_string(unit, attr.value()).ok()) + .and_then(|value| value.to_string_lossy().ok().map(|value| value.into_owned())) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| fallback_name.to_string()); + let member_type = resolve_type_ref_in_same_unit_with_origins(dwarf, &entry, unit) + .ok() + .flatten() + .and_then(|type_offset| { + Self::resolve_type_shallow_at_offset_impl(dwarf, unit, type_offset) + }) + .unwrap_or_else(|| TypeInfo::UnknownType { + name: "unknown".to_string(), + }); + + let mut member_offset = 0; + if let Some(location) = entry.attr(gimli::DW_AT_data_member_location) { + match location.value() { + gimli::AttributeValue::Exprloc(expression) => { + if let Some(value) = expr_errors::downgrade_optional_to_none( + DwarfExprMode::ConstOffset, + crate::dwarf_expr::const_eval::eval_const_offset( + &expression, + unit.encoding(), + ), + "variant member type display", + ) { + member_offset = value; + } + } + value => { + if let Some(value) = attr_u64(value) { + member_offset = value; + } + } + } + } + + let raw_bit_offset = entry + .attr(gimli::DW_AT_bit_offset) + .and_then(|attr| attr_u64(attr.value())); + let data_bit_offset = entry + .attr(gimli::DW_AT_data_bit_offset) + .and_then(|attr| attr_u64(attr.value())); + let bit_size = entry + .attr(gimli::DW_AT_bit_size) + .and_then(|attr| attr_u64(attr.value())) + .and_then(|value| u8::try_from(value).ok()); + let mut bit_offset = None; + + if let Some(data_bit_offset) = data_bit_offset { + bit_offset = u8::try_from(data_bit_offset % 8).ok(); + member_offset = data_bit_offset / 8; + } else if let Some(raw_bit_offset) = raw_bit_offset { + bit_offset = if let Some(bit_size) = bit_size { + let storage_bits = member_type.size().saturating_mul(8); + let bit_size = u64::from(bit_size); + if storage_bits > 0 && raw_bit_offset + bit_size <= storage_bits { + u8::try_from(storage_bits - raw_bit_offset - bit_size).ok() + } else { + u8::try_from(raw_bit_offset).ok() + } + } else { + u8::try_from(raw_bit_offset).ok() + }; + } + + let member_type = if let Some(bit_size) = bit_size { + TypeInfo::BitfieldType { + underlying_type: Box::new(member_type), + bit_offset: bit_offset.unwrap_or(0), + bit_size, + } + } else { + member_type + }; + + Some(crate::StructMember { + name, + member_type, + offset: member_offset, + bit_offset, + bit_size, + }) + } + + fn discriminant_is_unsigned(type_info: &TypeInfo) -> bool { + match type_info { + TypeInfo::BaseType { encoding, .. } => { + *encoding == gimli::constants::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16 + } + TypeInfo::EnumType { base_type, .. } + | TypeInfo::ScopedEnumType { base_type, .. } + | TypeInfo::TypedefType { + underlying_type: base_type, + .. + } + | TypeInfo::QualifiedType { + underlying_type: base_type, + .. + } + | TypeInfo::BitfieldType { + underlying_type: base_type, + .. + } => Self::discriminant_is_unsigned(base_type), + _ => false, + } + } + + fn parse_discriminant_attribute( + attribute: &gimli::Attribute, + unsigned: bool, + byte_size: u64, + ) -> Option { + use gimli::AttributeValue; + + if unsigned { + let value = match attribute.value() { + AttributeValue::Udata(value) => value, + AttributeValue::Sdata(value) => u64::try_from(value).ok()?, + AttributeValue::Data1(value) => u64::from(value), + AttributeValue::Data2(value) => u64::from(value), + AttributeValue::Data4(value) => u64::from(value), + AttributeValue::Data8(value) => value, + _ => return None, + }; + Some(crate::DiscriminantValue::Unsigned(value)) + } else { + // Rust 1.35 emits `#[repr(i8)]` value -1 as nonnegative 255 in + // `DW_FORM_sdata`. Keep negative Sdata values as semantic values, + // but interpret every nonnegative form as raw bits of the enum's + // DWARF byte size before matching it against a signed variant. + let value = match attribute.value() { + AttributeValue::Sdata(value) if value < 0 => value, + AttributeValue::Sdata(value) => { + Self::sign_extend_discriminant(value as u64, byte_size) + } + AttributeValue::Udata(value) | AttributeValue::Data8(value) => { + Self::sign_extend_discriminant(value, byte_size) + } + AttributeValue::Data1(value) => { + Self::sign_extend_discriminant(u64::from(value), byte_size) + } + AttributeValue::Data2(value) => { + Self::sign_extend_discriminant(u64::from(value), byte_size) + } + AttributeValue::Data4(value) => { + Self::sign_extend_discriminant(u64::from(value), byte_size) + } + _ => return None, + }; + Some(crate::DiscriminantValue::Signed(value)) + } + } + + fn sign_extend_discriminant(value: u64, byte_size: u64) -> i64 { + let bit_size = byte_size.saturating_mul(8); + if bit_size > 0 && bit_size < 64 { + let shift = 64 - bit_size; + ((value << shift) as i64) >> shift + } else { + value as i64 + } + } + + fn parse_discriminant_list( + mut data: R, + unsigned: bool, + ) -> Option> { + let mut ranges = Vec::new(); + while !data.is_empty() { + let marker = data.read_u8().ok()?; + let is_range = if marker == gimli::constants::DW_DSC_range.0 { + true + } else if marker == gimli::constants::DW_DSC_label.0 { + false + } else { + return None; + }; + + let start = if unsigned { + crate::DiscriminantValue::Unsigned(data.read_uleb128().ok()?) + } else { + crate::DiscriminantValue::Signed(data.read_sleb128().ok()?) + }; + let end = if is_range { + if unsigned { + crate::DiscriminantValue::Unsigned(data.read_uleb128().ok()?) + } else { + crate::DiscriminantValue::Signed(data.read_sleb128().ok()?) + } + } else { + start + }; + ranges.push(crate::DiscriminantRange { start, end }); + } + Some(ranges) + } + + fn parse_variant_at_offset( + dwarf: &gimli::Dwarf, + unit: &gimli::Unit, + offset: gimli::UnitOffset, + unsigned: bool, + byte_size: u64, + ) -> Option { + let entry = unit.entry(offset).ok()?; + let exact_value = entry.attr(DW_AT_DISCR_VALUE).and_then(|attribute| { + Self::parse_discriminant_attribute(attribute, unsigned, byte_size) + }); + let selector = if let Some(value) = exact_value { + crate::VariantSelector::Ranges(vec![crate::DiscriminantRange { + start: value, + end: value, + }]) + } else if let Some(attribute) = entry.attr(DW_AT_DISCR_LIST) { + match attribute.value() { + gimli::AttributeValue::Block(data) if !data.is_empty() => { + crate::VariantSelector::Ranges( + Self::parse_discriminant_list(data, unsigned).unwrap_or_default(), + ) + } + _ => crate::VariantSelector::Default, + } + } else { + crate::VariantSelector::Default + }; + + let mut members = Vec::new(); + let mut variant_parts = Vec::new(); + let mut tree = unit.entries_tree(Some(offset)).ok()?; + let root = tree.root().ok()?; + let mut children = root.children(); + while let Ok(Some(child)) = children.next() { + let child_entry = child.entry(); + if child_entry.tag() == gimli::DW_TAG_member { + let fallback_name = format!("member_{}", members.len()); + if let Some(member) = + Self::parse_member_at_offset(dwarf, unit, child_entry.offset(), &fallback_name) + { + members.push(member); + } + } else if child_entry.tag() == DW_TAG_VARIANT_PART { + if let Some(part) = + Self::parse_variant_part_at_offset(dwarf, unit, child_entry.offset()) + { + variant_parts.push(part); + } + } + } + + Some(crate::VariantCase { + selector, + members, + variant_parts, + payload_presentation: crate::VariantPayloadPresentation::Dwarf, + }) + } + + /// Parse the standard DWARF variant graph using the same model as GDB: + /// `DW_AT_discr` identifies the physical member, explicit values and + /// lists select branches, and a branch without either attribute is the + /// default. No Rust layout or discriminant offset is inferred here. + fn parse_variant_part_at_offset( + dwarf: &gimli::Dwarf, + unit: &gimli::Unit, + offset: gimli::UnitOffset, + ) -> Option { + let entry = unit.entry(offset).ok()?; + let discriminant = match entry.attr_value(DW_AT_DISCR) { + Some(gimli::AttributeValue::UnitRef(discriminant_offset)) => { + Self::parse_member_at_offset(dwarf, unit, discriminant_offset, "") + } + _ => None, + }; + let unsigned = discriminant + .as_ref() + .is_some_and(|member| Self::discriminant_is_unsigned(&member.member_type)); + let byte_size = discriminant + .as_ref() + .map_or(0, |member| member.member_type.size()); + + let mut variants = Vec::new(); + let mut tree = unit.entries_tree(Some(offset)).ok()?; + let root = tree.root().ok()?; + let mut children = root.children(); + while let Ok(Some(child)) = children.next() { + let child_entry = child.entry(); + if child_entry.tag() == DW_TAG_VARIANT { + if let Some(variant) = Self::parse_variant_at_offset( + dwarf, + unit, + child_entry.offset(), + unsigned, + byte_size, + ) { + variants.push(variant); + } + } + } + + Some(crate::VariantPart { + discriminant, + variants, + }) + } + // Full type resolution intentionally removed; only shallow type resolution is supported. - /// Shallow type resolution (no recursive member expansion) - /// Returns minimal TypeInfo with name/size where possible. + /// Shallow type resolution (no recursive member expansion). + /// + /// The physical graph is parsed first, then the compilation unit's source + /// language may attach presentation metadata. Producer conventions remain + /// outside the generic parser implementation. pub fn resolve_type_shallow_at_offset( + dwarf: &gimli::Dwarf, + unit: &gimli::Unit, + type_offset: gimli::UnitOffset, + source_language: crate::SourceLanguage, + ) -> Option { + let mut type_info = Self::resolve_type_shallow_at_offset_impl(dwarf, unit, type_offset)?; + crate::language::annotate_type_info(source_language, &mut type_info); + Some(type_info) + } + + fn resolve_type_shallow_at_offset_impl( dwarf: &gimli::Dwarf, unit: &gimli::Unit, mut type_offset: gimli::UnitOffset, @@ -299,6 +631,7 @@ impl DetailedParser { } // Collect only direct member DIEs let mut members: Vec = Vec::new(); + let mut variant_parts: Vec = Vec::new(); if let Ok(mut tree) = unit.entries_tree(Some(entry.offset())) { if let Ok(root) = tree.root() { let mut children = root.children(); @@ -321,9 +654,9 @@ impl DetailedParser { if let Some(gimli::AttributeValue::UnitRef(toff)) = ce.attr_value(DW_AT_TYPE) { - if let Some(ti) = - Self::resolve_type_shallow_at_offset(dwarf, unit, toff) - { + if let Some(ti) = Self::resolve_type_shallow_at_offset_impl( + dwarf, unit, toff, + ) { m_type = ti; } } @@ -412,6 +745,12 @@ impl DetailedParser { bit_offset, bit_size, }); + } else if ce.tag() == DW_TAG_VARIANT_PART { + if let Some(part) = + Self::parse_variant_part_at_offset(dwarf, unit, ce.offset()) + { + variant_parts.push(part); + } } } } @@ -456,11 +795,20 @@ impl DetailedParser { } } } - return Some(TypeInfo::StructType { - name, - size: byte_size, - members, - }); + return if variant_parts.is_empty() { + Some(TypeInfo::StructType { + name, + size: byte_size, + members, + }) + } else { + Some(TypeInfo::VariantType { + name, + size: byte_size, + members, + variant_parts, + }) + }; } DW_TAG_UNION_TYPE => { let name = alias_name.clone().unwrap_or_else(|| { @@ -493,9 +841,9 @@ impl DetailedParser { if let Some(gimli::AttributeValue::UnitRef(toff)) = ce.attr_value(DW_AT_TYPE) { - if let Some(ti) = - Self::resolve_type_shallow_at_offset(dwarf, unit, toff) - { + if let Some(ti) = Self::resolve_type_shallow_at_offset_impl( + dwarf, unit, toff, + ) { m_type = ti; } } @@ -539,7 +887,9 @@ impl DetailedParser { // If DW_AT_type refers to a base type, resolve it shallowly if let Some(gimli::AttributeValue::UnitRef(toff)) = entry.attr_value(DW_AT_TYPE) { - if let Some(ti) = Self::resolve_type_shallow_at_offset(dwarf, unit, toff) { + if let Some(ti) = + Self::resolve_type_shallow_at_offset_impl(dwarf, unit, toff) + { // Accept only base/qualified/typedef chain base type as enum underlying type base_type = ti; // If enum size missing, use underlying base type size @@ -549,8 +899,19 @@ impl DetailedParser { } } } - // Collect enum variants (one level) + let scoped = entry + .attr(gimli::DW_AT_enum_class) + .is_some_and(|attribute| { + matches!(attribute.value(), gimli::AttributeValue::Flag(true)) + }); + let unsigned = Self::discriminant_is_unsigned(&base_type); + + // Collect enum variants (one level). Legacy EnumType keeps + // its signed wire representation, while scoped enums retain + // the DWARF signedness needed for the full u64 domain. let mut variants: Vec = Vec::new(); + let mut scoped_variants: Vec = Vec::new(); + let mut variant_index = 0usize; if let Ok(mut tree) = unit.entries_tree(Some(entry.offset())) { if let Ok(root) = tree.root() { let mut children = root.children(); @@ -565,87 +926,67 @@ impl DetailedParser { } } } - let mut v_val: i64 = 0; - if let Some(cv) = ce.attr(gimli::DW_AT_const_value) { - let signed = match &base_type { - TypeInfo::BaseType { encoding, .. } => { - *encoding - == gimli::constants::DW_ATE_signed.0 as u16 - || *encoding - == gimli::constants::DW_ATE_signed_char.0 - as u16 - } - TypeInfo::TypedefType { - underlying_type, .. - } - | TypeInfo::QualifiedType { - underlying_type, .. - } => { - matches!( - &**underlying_type, - TypeInfo::BaseType { encoding, .. } - if *encoding == gimli::constants::DW_ATE_signed.0 as u16 - || *encoding - == gimli::constants::DW_ATE_signed_char.0 as u16 - ) - } - _ => true, - }; - v_val = match cv.value() { - gimli::AttributeValue::Udata(u) => u as i64, - gimli::AttributeValue::Sdata(s) => s, - gimli::AttributeValue::Data1(b) => { - let u = b as u64; - if signed && (u & 0x80) != 0 { - (u as i8) as i64 - } else { - u as i64 - } - } - gimli::AttributeValue::Data2(u) => { - let u = u as u64; - if signed && (u & 0x8000) != 0 { - (u as i16) as i64 - } else { - u as i64 - } + if v_name.is_empty() { + v_name = format!("variant_{variant_index}"); + } + let value = ce + .attr(gimli::DW_AT_const_value) + .and_then(|attribute| { + Self::parse_discriminant_attribute( + attribute, unsigned, byte_size, + ) + }) + .unwrap_or({ + if unsigned { + crate::DiscriminantValue::Unsigned(0) + } else { + crate::DiscriminantValue::Signed(0) } - gimli::AttributeValue::Data4(u) => { - let u = u as u64; - if signed && (u & 0x8000_0000) != 0 { - (u as i32) as i64 - } else { - u as i64 - } + }); + if scoped { + scoped_variants.push(crate::ScopedEnumVariant { + name: v_name, + value, + }); + } else { + let value = match value { + crate::DiscriminantValue::Signed(value) => value, + crate::DiscriminantValue::Unsigned(value) => { + value as i64 } - gimli::AttributeValue::Data8(u) => u as i64, - _ => v_val, }; + variants.push(crate::EnumVariant { + name: v_name, + value, + }); } - if v_name.is_empty() { - v_name = format!("variant_{}", variants.len()); - } - variants.push(crate::EnumVariant { - name: v_name, - value: v_val, - }); + variant_index += 1; } } } } - return Some(TypeInfo::EnumType { - name, - size: byte_size, - base_type: Box::new(base_type), - variants, - }); + return if scoped { + Some(TypeInfo::ScopedEnumType { + name, + size: byte_size, + base_type: Box::new(base_type), + variants: scoped_variants, + }) + } else { + Some(TypeInfo::EnumType { + name, + size: byte_size, + base_type: Box::new(base_type), + variants, + }) + }; } DW_TAG_ARRAY_TYPE => { // element_type shallow + total_size if available + subrange element_count (one step deeper) let mut elem_type: Option = None; if let Some(gimli::AttributeValue::UnitRef(eoff)) = entry.attr_value(DW_AT_TYPE) { - elem_type = Self::resolve_type_shallow_at_offset(dwarf, unit, eoff); + elem_type = Self::resolve_type_shallow_at_offset_impl(dwarf, unit, eoff); } let element_type = Box::new(elem_type.unwrap_or(TypeInfo::UnknownType { name: "".to_string(), @@ -933,6 +1274,66 @@ mod tests { use super::*; use crate::core::{Availability, VariableLocation}; + #[test] + fn parses_unsigned_discriminant_labels_and_ranges() { + let data = [ + gimli::constants::DW_DSC_label.0, + 5, + gimli::constants::DW_DSC_range.0, + 8, + 13, + ]; + let ranges = DetailedParser::parse_discriminant_list( + gimli::EndianSlice::new(&data, gimli::LittleEndian), + true, + ) + .expect("valid unsigned discriminant list"); + + assert_eq!( + ranges, + vec![ + crate::DiscriminantRange { + start: crate::DiscriminantValue::Unsigned(5), + end: crate::DiscriminantValue::Unsigned(5), + }, + crate::DiscriminantRange { + start: crate::DiscriminantValue::Unsigned(8), + end: crate::DiscriminantValue::Unsigned(13), + }, + ] + ); + } + + #[test] + fn parses_signed_discriminant_labels_and_ranges() { + let data = [ + gimli::constants::DW_DSC_label.0, + 0x7e, + gimli::constants::DW_DSC_range.0, + 0x7c, + 3, + ]; + let ranges = DetailedParser::parse_discriminant_list( + gimli::EndianSlice::new(&data, gimli::LittleEndian), + false, + ) + .expect("valid signed discriminant list"); + + assert_eq!( + ranges, + vec![ + crate::DiscriminantRange { + start: crate::DiscriminantValue::Signed(-2), + end: crate::DiscriminantValue::Signed(-2), + }, + crate::DiscriminantRange { + start: crate::DiscriminantValue::Signed(-4), + end: crate::DiscriminantValue::Signed(3), + }, + ] + ); + } + #[test] fn variable_reports_register_value_location() { let variable = ParsedVariable { diff --git a/ghostscope-dwarf/src/semantics/c_integer.rs b/ghostscope-dwarf/src/semantics/c_integer.rs index 9ac64adb..100af39d 100644 --- a/ghostscope-dwarf/src/semantics/c_integer.rs +++ b/ghostscope-dwarf/src/semantics/c_integer.rs @@ -52,6 +52,9 @@ pub fn c_integer_comparison_type(ty: &TypeInfo) -> Option c_integer_comparison_type(base_type).map(|mut ty| { if ty.size == 0 { ty.size = *size; @@ -82,7 +85,9 @@ pub fn is_c_signed_integer_type(ty: &TypeInfo) -> bool { *encoding == crate::constants::DW_ATE_signed.0 as u16 || *encoding == crate::constants::DW_ATE_signed_char.0 as u16 } - TypeInfo::EnumType { base_type, .. } => is_c_signed_integer_type(base_type), + TypeInfo::EnumType { base_type, .. } | TypeInfo::ScopedEnumType { base_type, .. } => { + is_c_signed_integer_type(base_type) + } TypeInfo::BitfieldType { underlying_type, .. } => is_c_signed_integer_type(underlying_type), diff --git a/ghostscope-dwarf/src/semantics/mod.rs b/ghostscope-dwarf/src/semantics/mod.rs index 4fa1d1fc..4a9c26e5 100644 --- a/ghostscope-dwarf/src/semantics/mod.rs +++ b/ghostscope-dwarf/src/semantics/mod.rs @@ -3,6 +3,7 @@ pub mod pc_context; pub mod type_context; pub mod type_layout; pub mod unwind_plan; +pub mod value; pub mod variable_plan; pub(crate) mod origins; @@ -21,8 +22,9 @@ pub use pc_context::{ AddressSpaceInfo, FunctionParameter, InlineFrame, PcContext, PcLineInfo, PcRange, }; pub use type_context::{ - CompilationUnitMetadata, ProducerInfo, ResolvedType, SemanticType, SourceLanguage, - SyntheticTypeKind, TypeIdentity, TypeOrigin, TypeProjection, TypeProjectionLayout, + CompilationUnitMetadata, ProducerInfo, ResolvedType, RustcVersion, SemanticType, + SourceLanguage, SyntheticTypeKind, TypeIdentity, TypeOrigin, TypeProjection, + TypeProjectionLayout, }; pub use type_layout::{ indexable_element_layout, is_aggregate_type, is_pointer_or_array_type, member_layout, @@ -35,6 +37,11 @@ pub use unwind_plan::{ CfaRulePlan, CompactUnwindRow, CompactUnwindStats, CompactUnwindTable, RegisterRecoveryPlan, UnwindDiagnostic, UnwindDiagnosticKind, }; +pub use value::{ + BTreeArrayCapture, BTreeEdgesCapture, HashTableBucketSource, ProjectedValueRead, + ProjectedValueStep, ProjectedViewField, ProjectedViewFieldCapture, RingSequenceLength, + ValueCapturePlan, ValueReadPlan, +}; pub(crate) use variable_plan::PlanError; pub use variable_plan::{ AddressOrigin, LvalueAddressPlan, PlannedAddress, PlannedAddressKind, PlannedValue, diff --git a/ghostscope-dwarf/src/semantics/type_context.rs b/ghostscope-dwarf/src/semantics/type_context.rs index 1d669c0c..ba8c8ac6 100644 --- a/ghostscope-dwarf/src/semantics/type_context.rs +++ b/ghostscope-dwarf/src/semantics/type_context.rs @@ -3,6 +3,7 @@ use super::type_layout::{indexable_element_layout, strip_type_aliases}; use crate::core::{CuId, ModuleId, TypeId}; use crate::{TypeInfo, VariableAccessSegment}; +use std::fmt; /// Normalized source-language family for semantic dispatch. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -49,6 +50,80 @@ impl ProducerInfo { pub fn new(raw: impl Into) -> Self { Self { raw: raw.into() } } + + /// Extract the numeric release from rustc's LLVM producer string. + /// + /// rustc currently emits strings such as + /// `clang LLVM (rustc version 1.88.0 (...))`. The channel, commit, and + /// date remain available in [`Self::raw`]; adapters need only the numeric + /// release as an advisory candidate-ordering hint. + pub fn rustc_version(&self) -> Option { + const MARKER: &str = "rustc version "; + + let marker = self.raw.find(MARKER)?; + if marker > 0 { + let preceding = self.raw.as_bytes()[marker - 1]; + if preceding.is_ascii_alphanumeric() || preceding == b'_' { + return None; + } + } + + let value = &self.raw[marker + MARKER.len()..]; + let token = value + .split(|character: char| character.is_ascii_whitespace() || character == ')') + .next()?; + RustcVersion::parse(token) + } +} + +/// Numeric release component parsed from a rustc producer string. +/// +/// Prerelease and build suffixes intentionally do not participate in ordering. +/// The complete producer remains available through [`ProducerInfo::raw`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RustcVersion { + pub major: u64, + pub minor: u64, + pub patch: u64, +} + +impl RustcVersion { + pub const fn new(major: u64, minor: u64, patch: u64) -> Self { + Self { + major, + minor, + patch, + } + } + + /// Parse a rustc release token, accepting suffixes such as `-nightly`. + pub fn parse(value: &str) -> Option { + let mut components = value.splitn(3, '.'); + let major = components.next()?.parse().ok()?; + let minor = components.next()?.parse().ok()?; + let patch_and_suffix = components.next()?; + let patch_length = patch_and_suffix + .bytes() + .take_while(u8::is_ascii_digit) + .count(); + if patch_length == 0 { + return None; + } + + let patch = patch_and_suffix[..patch_length].parse().ok()?; + let suffix = &patch_and_suffix[patch_length..]; + if !suffix.is_empty() && !suffix.starts_with(['-', '+']) { + return None; + } + + Some(Self::new(major, minor, patch)) + } +} + +impl fmt::Display for RustcVersion { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch) + } } /// Metadata that controls language-aware interpretation for one compilation unit. @@ -61,6 +136,16 @@ pub struct CompilationUnitMetadata { pub dwarf_version: u16, } +impl CompilationUnitMetadata { + /// Return the target CU's rustc release when its producer exposes one. + pub fn rustc_version(&self) -> Option { + if self.language != SourceLanguage::Rust { + return None; + } + self.producer.as_ref()?.rustc_version() + } +} + /// Stable origin for a type DIE. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct TypeOrigin { @@ -71,6 +156,16 @@ pub struct TypeOrigin { pub dwarf_version: u16, } +impl TypeOrigin { + /// Return the target type's rustc release when its CU exposes one. + pub fn rustc_version(&self) -> Option { + if self.language != SourceLanguage::Rust { + return None; + } + self.producer.as_ref()?.rustc_version() + } +} + impl From for TypeOrigin { fn from(metadata: CompilationUnitMetadata) -> Self { Self { @@ -288,7 +383,8 @@ pub struct TypeProjection { #[cfg(test)] mod tests { use super::{ - ResolvedType, SourceLanguage, SyntheticTypeKind, TypeIdentity, TypeProjectionLayout, + CompilationUnitMetadata, ProducerInfo, ResolvedType, RustcVersion, SourceLanguage, + SyntheticTypeKind, TypeIdentity, TypeProjectionLayout, }; use crate::{CuId, DieRef, ModuleId, TypeId, TypeInfo, VariableAccessSegment}; @@ -330,6 +426,51 @@ mod tests { assert_eq!(SourceLanguage::from_dwarf(None), SourceLanguage::Unknown); } + #[test] + fn parses_rustc_versions_from_llvm_producers() { + let stable = ProducerInfo::new("clang LLVM (rustc version 1.88.0 (6b00bc388 2025-06-23))"); + let nightly = + ProducerInfo::new("clang LLVM (rustc version 1.98.0-nightly (abc123 2026-05-30))"); + + assert_eq!(stable.rustc_version(), Some(RustcVersion::new(1, 88, 0))); + assert_eq!(nightly.rustc_version(), Some(RustcVersion::new(1, 98, 0))); + assert_eq!( + RustcVersion::parse("1.93.0-beta.1"), + Some(RustcVersion::new(1, 93, 0)) + ); + } + + #[test] + fn rejects_non_rustc_and_malformed_producers() { + for producer in [ + "clang version 18.1.8", + "myrustc version 1.88.0", + "my_rustc version 1.88.0", + "rustc version 1.88", + "rustc version 1.88.x", + "rustc version 1.88.0.1", + ] { + assert_eq!(ProducerInfo::new(producer).rustc_version(), None); + } + } + + #[test] + fn exposes_rustc_versions_only_for_rust_compilation_units() { + let metadata = |language| CompilationUnitMetadata { + module: ModuleId(1), + cu: CuId(2), + language, + producer: Some(ProducerInfo::new("rustc version 1.88.0")), + dwarf_version: 5, + }; + + assert_eq!( + metadata(SourceLanguage::Rust).rustc_version(), + Some(RustcVersion::new(1, 88, 0)) + ); + assert_eq!(metadata(SourceLanguage::C).rustc_version(), None); + } + #[test] fn layout_identity_looks_through_qualifiers_only() { let dwarf = TypeIdentity::Dwarf(type_id()); diff --git a/ghostscope-dwarf/src/semantics/type_layout.rs b/ghostscope-dwarf/src/semantics/type_layout.rs index c47f2f13..0f3d747e 100644 --- a/ghostscope-dwarf/src/semantics/type_layout.rs +++ b/ghostscope-dwarf/src/semantics/type_layout.rs @@ -44,7 +44,10 @@ pub fn strip_type_aliases(mut ty: &TypeInfo) -> &TypeInfo { pub fn is_aggregate_type(ty: &TypeInfo) -> bool { matches!( strip_type_aliases(ty), - TypeInfo::StructType { .. } | TypeInfo::UnionType { .. } | TypeInfo::ArrayType { .. } + TypeInfo::StructType { .. } + | TypeInfo::UnionType { .. } + | TypeInfo::ArrayType { .. } + | TypeInfo::VariantType { .. } ) } diff --git a/ghostscope-dwarf/src/semantics/value.rs b/ghostscope-dwarf/src/semantics/value.rs new file mode 100644 index 00000000..0e46e803 --- /dev/null +++ b/ghostscope-dwarf/src/semantics/value.rs @@ -0,0 +1,153 @@ +//! Semantic capture plans for values whose source-language meaning is not +//! represented by their physical DWARF aggregate alone. + +use super::TypeProjection; +use ghostscope_protocol::ValuePresentation; + +/// A language-selected presentation and the physical reads needed to produce +/// its protocol payload. +#[derive(Debug, Clone, PartialEq)] +pub struct ValueReadPlan { + pub presentation: ValuePresentation, + pub capture: ValueCapturePlan, +} + +/// Runtime source of a ring sequence's logical element count. +#[derive(Debug, Clone, PartialEq)] +pub enum RingSequenceLength { + /// Read the element count directly from this member. + Explicit(TypeProjection), + /// Compute the wrapped distance from the start index to this end index. + End(TypeProjection), +} + +/// One DWARF-derived address operation used to locate a projected value. +#[derive(Debug, Clone, PartialEq)] +pub enum ProjectedValueStep { + /// Add a concrete member offset to the current address. + Member { offset: u64 }, + /// Read a pointer of the exact DWARF width from the current address. + Dereference { pointer_size: u64 }, +} + +/// Runtime path and final semantic type for one projected value. +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectedValueRead { + pub steps: Vec, + pub resolved_type: crate::ResolvedType, +} + +/// One field assembled into a synthetic projected-view payload. +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectedViewField { + pub output_offset: u64, + pub value: ProjectedValueRead, + pub capture: ProjectedViewFieldCapture, +} + +/// How a projected-view field materializes its final runtime address. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProjectedViewFieldCapture { + /// Read the value stored at the projected address. + Value, + /// Store the projected address itself as a pointer value. + Address, +} + +/// One initialized-slot array embedded in a Rust B-Tree leaf-node layout. +#[derive(Debug, Clone, PartialEq)] +pub struct BTreeArrayCapture { + pub offset: u64, + pub slot_stride: u64, +} + +/// Child-pointer array embedded in the DWARF-described internal-node layout. +#[derive(Debug, Clone, PartialEq)] +pub struct BTreeEdgesCapture { + pub offset_from_leaf: u64, + pub slot_stride: u64, + pub pointer_offset: u64, + pub pointer_size: u64, + pub edge_count: u64, +} + +/// Runtime source of physical hash-table bucket bytes. +#[derive(Debug, Clone, PartialEq)] +pub enum HashTableBucketSource { + /// Buckets start at a dedicated pointer projected from the descriptor. + Forward { data: TypeProjection }, + /// Buckets are stored immediately before the control-byte pointer. + ReverseFromControl, + /// Rust 1.35 stores pairs after aligned pointer-sized hash words in one + /// allocation. The low pointer bit is an implementation tag. + LegacyAfterControl { + entry_alignment: u64, + pointer_tag_mask: u64, + }, +} + +/// Physical capture strategy used by a semantic value adapter. +#[derive(Debug, Clone, PartialEq)] +pub enum ValueCapturePlan { + /// Read an embedded value at a DWARF-derived member projection and present + /// it using the projected type rather than the physical wrapper type. + ProjectedValue { value: TypeProjection }, + /// Read the physical root value in place, but register and format it using + /// a DWARF-derived semantic view of selected embedded fields. + InlineView { output_type: crate::TypeInfo }, + /// Assemble a synthetic struct from independently projected values. Every + /// member offset, pointer width, and final type is derived from DWARF. + ProjectedView { + output_type: crate::TypeInfo, + fields: Vec, + }, + /// Read a pointer and length from an aggregate, then capture a bounded byte + /// sequence from the pointer. + IndirectBytes { + data: TypeProjection, + length: TypeProjection, + }, + /// Read a pointer and logical element count from an aggregate, then capture + /// a bounded number of complete, contiguous elements. + IndirectSequence { + data: TypeProjection, + length: TypeProjection, + element_stride: u64, + }, + /// Read a ring-buffer descriptor, then normalize up to two physical + /// segments into one logical sequence payload. + IndirectRingSequence { + data: TypeProjection, + start: TypeProjection, + length: Box, + capacity: TypeProjection, + element_stride: u64, + }, + /// Read hash-table metadata and capture a bounded prefix of occupancy bytes + /// plus their corresponding physical entries. The source-language adapter + /// provides occupancy semantics; all paths, widths, and entry layout are + /// derived from DWARF. + IndirectHashTable { + control: TypeProjection, + length: TypeProjection, + bucket_mask: TypeProjection, + entry_stride: u64, + occupancy: ghostscope_protocol::HashTableOccupancy, + buckets: HashTableBucketSource, + bucket_order: ghostscope_protocol::HashTableBucketOrder, + }, + /// Capture a bounded breadth-first snapshot of a Rust B-Tree. Root and + /// node metadata are projections of concrete DIEs; array capacities, + /// strides, embedded pointer offsets, and widths are likewise DWARF + /// derived. + IndirectBTree { + root_pointer: TypeProjection, + root_height: TypeProjection, + length: TypeProjection, + node_length: TypeProjection, + keys: BTreeArrayCapture, + values: Option, + edges: BTreeEdgesCapture, + node_capacity: u64, + }, +} diff --git a/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs b/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs new file mode 100644 index 00000000..15e9a450 --- /dev/null +++ b/ghostscope-dwarf/tests/fixtures/rust_value_compat.rs @@ -0,0 +1,272 @@ +#![allow(dead_code)] + +use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; +use std::ffi::OsString; +use std::num::NonZeroI32; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::Arc; + +pub enum CompatEnum { + Unit, + Tuple(i16, u16), + Struct { left: i16, right: u16 }, +} + +pub enum CompatInner { + Pair(i16, u16), +} + +pub enum CompatOuter { + Empty, + Wrapped(CompatInner), +} + +#[repr(C)] +pub enum CompatReprC { + Unit, + Tuple(i16, u16), + Struct { left: i16, right: u16 }, +} + +pub enum CompatFieldless { + First, + Second, +} + +pub enum CompatSingle { + Only(i32), +} + +#[repr(i8)] +pub enum CompatSigned { + Negative = -1, + Positive = 1, +} + +#[repr(u64)] +pub enum CompatUnsigned { + Low = 1, + High = 0x8000_0000_0000_0000, +} + +#[inline(never)] +pub fn observe_values( + string: String, + os_string: OsString, + path: &Path, + path_buf: PathBuf, + text: &str, + boxed_text: Box, + slice: &[i32], + vector: Vec, + deque: VecDeque, + btree_map: BTreeMap, + btree_set: BTreeSet, + hash_map: HashMap, + hash_set: HashSet, + rc: Rc, + rc_text: Rc, + arc: Arc, + arc_text: Arc, + cell: Cell, + ref_cell: RefCell, + nonzero: NonZeroI32, + enum_value: CompatEnum, + fieldless: CompatFieldless, + single: CompatSingle, + signed: CompatSigned, + unsigned: CompatUnsigned, + option_nonzero: Option, +) -> usize { + let enum_value = match enum_value { + CompatEnum::Unit => 0, + CompatEnum::Tuple(left, right) => i32::from(left) + i32::from(right), + CompatEnum::Struct { left, right } => i32::from(left) + i32::from(right), + }; + let fieldless = match fieldless { + CompatFieldless::First => 0, + CompatFieldless::Second => 1, + }; + let single = match single { + CompatSingle::Only(value) => value, + }; + let signed = match signed { + CompatSigned::Negative => 1, + CompatSigned::Positive => 0, + }; + let unsigned = match unsigned { + CompatUnsigned::Low => 0, + CompatUnsigned::High => 1, + }; + string.len() + + os_string.len() + + path.as_os_str().len() + + path_buf.as_os_str().len() + + text.len() + + boxed_text.len() + + slice.len() + + vector.len() + + deque.len() + + btree_map.len() + + btree_set.len() + + hash_map.len() + + hash_set.len() + + *rc as usize + + rc_text.len() + + *arc as usize + + arc_text.len() + + cell.get() as usize + + *ref_cell.borrow() as usize + + nonzero.get() as usize + + enum_value as usize + + fieldless + + single as usize + + signed + + unsigned + + option_nonzero.map_or(0, NonZeroI32::get) as usize +} + +#[inline(never)] +pub fn observe_ref(value: Ref<'_, i32>) -> i32 { + *value +} + +#[inline(never)] +pub fn observe_ref_mut(mut value: RefMut<'_, i32>) -> i32 { + *value += 1; + *value +} + +#[inline(never)] +pub fn observe_mut_str(value: &mut str) -> usize { + value.len() +} + +// Keep these enum-shaped parameters by value. Rust 1.35 emits only a +// declaration DIE for some reference targets, while the runtime matrix +// separately exercises the real reference and dereference path. +#[inline(never)] +pub fn observe_nested_enum(value: CompatOuter) -> usize { + match value { + CompatOuter::Empty => 0, + CompatOuter::Wrapped(CompatInner::Pair(left, right)) => { + left as usize + right as usize + } + } +} + +#[inline(never)] +pub fn observe_pointer_niche(some: Option<&i32>, none: Option<&i32>) -> usize { + let some = match some { + Some(value) => *value as usize, + None => 0, + }; + let none = match none { + Some(value) => *value as usize, + None => 0, + }; + some + none +} + +#[inline(never)] +pub fn observe_repr_c_enum( + unit: CompatReprC, + tuple: CompatReprC, + struct_value: CompatReprC, +) -> usize { + let unit = match unit { + CompatReprC::Unit => 1, + _ => 0, + }; + let tuple = match tuple { + CompatReprC::Tuple(left, right) => left as usize + right as usize, + _ => 0, + }; + let struct_value = match struct_value { + CompatReprC::Struct { left, right } => left as usize + right as usize, + _ => 0, + }; + unit + tuple + struct_value +} + +fn main() { + let mut deque = VecDeque::new(); + deque.push_back(5); + + let mut btree_map = BTreeMap::new(); + btree_map.insert(7, 11); + let mut btree_set = BTreeSet::new(); + btree_set.insert(13); + + let mut hash_map = HashMap::new(); + hash_map.insert(17, 19); + let mut hash_set = HashSet::new(); + hash_set.insert(23); + + let slice = [29, 31]; + let path = PathBuf::from("borrowed/path"); + let value = observe_values( + String::from("string"), + OsString::from("os-string"), + path.as_path(), + PathBuf::from("owned/path"), + "text", + String::from("boxed-text").into_boxed_str(), + &slice, + vec![3], + deque, + btree_map, + btree_set, + hash_map, + hash_set, + Rc::new(37), + Rc::from("rc-text"), + Arc::new(41), + Arc::from("arc-text"), + Cell::new(43), + RefCell::new(47), + NonZeroI32::new(53).unwrap(), + CompatEnum::Struct { + left: 59, + right: 61, + }, + CompatFieldless::Second, + CompatSingle::Only(71), + CompatSigned::Negative, + CompatUnsigned::High, + NonZeroI32::new(61), + ); + + let mut mutable_text = String::from("mutable-text"); + let mutable_text_len = observe_mut_str(mutable_text.as_mut_str()); + let nested = observe_nested_enum(CompatOuter::Wrapped(CompatInner::Pair(7, 9))); + + let pointed = 67; + let pointer_some = Some(&pointed); + let pointer_none: Option<&i32> = None; + let pointer_niche = observe_pointer_niche(pointer_some, pointer_none); + let repr_c = observe_repr_c_enum( + CompatReprC::Unit, + CompatReprC::Tuple(73, 79), + CompatReprC::Struct { + left: 83, + right: 89, + }, + ); + + let guarded = RefCell::new(97); + let shared = observe_ref(guarded.borrow()); + let exclusive = observe_ref_mut(guarded.borrow_mut()); + println!( + "{}", + value + + mutable_text_len + + nested + + pointer_niche + + repr_c + + shared as usize + + exclusive as usize + ); +} diff --git a/ghostscope-dwarf/tests/rust_version_compatibility.rs b/ghostscope-dwarf/tests/rust_version_compatibility.rs new file mode 100644 index 00000000..2772d0eb --- /dev/null +++ b/ghostscope-dwarf/tests/rust_version_compatibility.rs @@ -0,0 +1,1188 @@ +use ghostscope_dwarf::{ + BTreeEntryPresentation, DiscriminantValue, DwarfAnalyzer, HashTableEntryPresentation, + ProjectedViewFieldCapture, RustcVersion, SourceLanguage, TypeInfo, ValueCapturePlan, + ValuePresentation, ValueReadPlan, VariantCase, VariantPayloadPresentation, VariantSelector, +}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const DEFAULT_TOOLCHAINS: &str = include_str!("../../e2e-tests/rust-compat-toolchains.txt"); + +#[derive(Clone, Copy)] +enum ExpectedAdapter { + Utf8Bytes, + OsBytes, + Sequence, + RingSequence, + BTreeMap, + BTreeSet, + HashMap, + HashSet, + ReferenceCounted, + Cell, + RefCell, + RefGuard, + NonZero, + NativeDwarf, +} + +const ALL_ADAPTERS: &[(&str, ExpectedAdapter)] = &[ + ("string", ExpectedAdapter::Utf8Bytes), + ("os_string", ExpectedAdapter::OsBytes), + ("path_buf", ExpectedAdapter::OsBytes), + ("text", ExpectedAdapter::Utf8Bytes), + ("boxed_text", ExpectedAdapter::Utf8Bytes), + ("slice", ExpectedAdapter::Sequence), + ("vector", ExpectedAdapter::Sequence), + ("deque", ExpectedAdapter::RingSequence), + ("btree_map", ExpectedAdapter::BTreeMap), + ("btree_set", ExpectedAdapter::BTreeSet), + ("hash_map", ExpectedAdapter::HashMap), + ("hash_set", ExpectedAdapter::HashSet), + ("rc", ExpectedAdapter::ReferenceCounted), + ("arc", ExpectedAdapter::ReferenceCounted), + ("cell", ExpectedAdapter::Cell), + ("ref_cell", ExpectedAdapter::RefCell), + ("nonzero", ExpectedAdapter::NonZero), + ("enum_value", ExpectedAdapter::NativeDwarf), +]; + +const RUST_135_ADAPTERS: &[(&str, ExpectedAdapter)] = &[ + ("string", ExpectedAdapter::Utf8Bytes), + ("os_string", ExpectedAdapter::OsBytes), + ("path_buf", ExpectedAdapter::OsBytes), + ("text", ExpectedAdapter::Utf8Bytes), + // Box gained a dedicated rust-gdb provider later. Pinning it here + // applies that provider's semantics to Rust 1.35's concrete fat-pointer + // DIE rather than implying it was in the bundled 1.35 script. + ("boxed_text", ExpectedAdapter::Utf8Bytes), + ("slice", ExpectedAdapter::Sequence), + ("vector", ExpectedAdapter::Sequence), + ("deque", ExpectedAdapter::RingSequence), + ("btree_map", ExpectedAdapter::BTreeMap), + ("btree_set", ExpectedAdapter::BTreeSet), + ("hash_map", ExpectedAdapter::HashMap), + ("hash_set", ExpectedAdapter::HashSet), +]; + +fn configured_toolchains() -> Vec { + std::env::var("GHOSTSCOPE_RUST_COMPAT_TOOLCHAINS") + .ok() + .map(|value| { + value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .collect() + }) + .filter(|toolchains: &Vec<_>| !toolchains.is_empty()) + .unwrap_or_else(|| { + DEFAULT_TOOLCHAINS + .lines() + .map(|line| line.split('#').next().unwrap_or_default().trim()) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect() + }) +} + +fn require_all_toolchains() -> bool { + std::env::var_os("GHOSTSCOPE_REQUIRE_RUST_COMPAT_TOOLCHAINS").is_some() +} + +fn toolchain_is_installed(toolchain: &str) -> bool { + Command::new("rustup") + .args(["run", toolchain, "rustc", "--version"]) + .output() + .is_ok_and(|output| output.status.success()) +} + +fn toolchain_rustc_version(toolchain: &str) -> anyhow::Result { + let output = Command::new("rustup") + .args(["run", toolchain, "rustc", "--version"]) + .output()?; + anyhow::ensure!( + output.status.success(), + "rustc {toolchain} --version failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout)?; + let version = stdout + .split_ascii_whitespace() + .nth(1) + .and_then(RustcVersion::parse) + .ok_or_else(|| anyhow::anyhow!("unrecognized rustc version output: {stdout:?}"))?; + Ok(version) +} + +fn compile_fixture(toolchain: &str, output: &Path) -> anyhow::Result<()> { + let source = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rust_value_compat.rs"); + let result = Command::new("rustup") + .arg("run") + .arg(toolchain) + .arg("rustc") + .arg("--edition=2018") + .arg("-g") + .arg("-C") + .arg("opt-level=0") + .arg("-C") + .arg("link-dead-code") + .arg(&source) + .arg("-o") + .arg(output) + .output()?; + anyhow::ensure!( + result.status.success(), + "rustc {toolchain} failed for {}:\n{}", + source.display(), + String::from_utf8_lossy(&result.stderr) + ); + Ok(()) +} + +fn adapter_matches(plan: Option<&ValueReadPlan>, expected: ExpectedAdapter) -> bool { + let Some(plan) = plan else { + return matches!(expected, ExpectedAdapter::NativeDwarf); + }; + match (&plan.presentation, &plan.capture, expected) { + ( + ValuePresentation::Utf8String, + ValueCapturePlan::IndirectBytes { .. }, + ExpectedAdapter::Utf8Bytes, + ) + | ( + ValuePresentation::ByteString, + ValueCapturePlan::IndirectBytes { .. }, + ExpectedAdapter::OsBytes, + ) + | ( + ValuePresentation::Sequence { .. }, + ValueCapturePlan::IndirectSequence { .. }, + ExpectedAdapter::Sequence, + ) + | ( + ValuePresentation::Sequence { .. }, + ValueCapturePlan::IndirectRingSequence { .. }, + ExpectedAdapter::RingSequence, + ) + | ( + ValuePresentation::ReferenceCountedStruct { .. }, + ValueCapturePlan::ProjectedView { .. }, + ExpectedAdapter::ReferenceCounted, + ) + | ( + ValuePresentation::SingleField { .. }, + ValueCapturePlan::ProjectedValue { .. }, + ExpectedAdapter::Cell, + ) + | ( + ValuePresentation::SignedStateStruct { .. }, + ValueCapturePlan::InlineView { .. }, + ExpectedAdapter::RefCell, + ) + | ( + ValuePresentation::SignedStateStruct { .. }, + ValueCapturePlan::ProjectedView { .. }, + ExpectedAdapter::RefGuard, + ) + | ( + ValuePresentation::Dwarf, + ValueCapturePlan::ProjectedValue { .. }, + ExpectedAdapter::NonZero, + ) => true, + ( + ValuePresentation::BTree { entry, .. }, + ValueCapturePlan::IndirectBTree { .. }, + ExpectedAdapter::BTreeMap, + ) => matches!(entry, BTreeEntryPresentation::Map { .. }), + ( + ValuePresentation::BTree { entry, .. }, + ValueCapturePlan::IndirectBTree { .. }, + ExpectedAdapter::BTreeSet, + ) => matches!(entry, BTreeEntryPresentation::Set { .. }), + ( + ValuePresentation::HashTable { entry, .. }, + ValueCapturePlan::IndirectHashTable { .. }, + ExpectedAdapter::HashMap, + ) => matches!(entry, HashTableEntryPresentation::Map { .. }), + ( + ValuePresentation::HashTable { entry, .. }, + ValueCapturePlan::IndirectHashTable { .. }, + ExpectedAdapter::HashSet, + ) => matches!(entry, HashTableEntryPresentation::Set { .. }), + _ => false, + } +} + +fn member_offset(projection: &ghostscope_dwarf::TypeProjection) -> anyhow::Result { + let ghostscope_dwarf::TypeProjectionLayout::Member { offset } = projection.layout else { + anyhow::bail!("expected member projection, got {:?}", projection.layout) + }; + Ok(offset) +} + +fn assert_reference_counted_dst_plan( + toolchain: &str, + type_name: &str, + plan: &ValueReadPlan, +) -> anyhow::Result<()> { + let ValuePresentation::ReferenceCountedStruct { implicit_weak, .. } = &plan.presentation else { + anyhow::bail!("{toolchain}: unexpected {type_name} presentation") + }; + assert_eq!(*implicit_weak, 1); + + let ValueCapturePlan::ProjectedView { + output_type, + fields, + } = &plan.capture + else { + anyhow::bail!("{toolchain}: unexpected {type_name} capture") + }; + let ghostscope_dwarf::TypeInfo::StructType { name, members, .. } = output_type else { + anyhow::bail!("{toolchain}: {type_name} output is not a struct") + }; + assert_eq!(name, type_name); + assert_eq!( + members + .iter() + .map(|member| member.name.as_str()) + .collect::>(), + ["ptr", "strong", "weak"] + ); + assert!(matches!( + members[0].member_type, + ghostscope_dwarf::TypeInfo::PointerType { size: 8, .. } + )); + assert_eq!(fields.len(), 3); + assert_eq!(fields[0].capture, ProjectedViewFieldCapture::Address); + assert!(fields[1..] + .iter() + .all(|field| field.capture == ProjectedViewFieldCapture::Value)); + Ok(()) +} + +fn assert_rust_135_btree_plan(parameter: &str, plan: &ValueReadPlan) -> anyhow::Result<()> { + let ValuePresentation::BTree { + node_capacity, + entry, + } = &plan.presentation + else { + anyhow::bail!("1.35.0: unexpected presentation for {parameter}") + }; + assert_eq!(*node_capacity, 11); + match (parameter, entry) { + ("btree_map", BTreeEntryPresentation::Map { key, value }) => { + assert_eq!((key.slot_stride, key.value_offset), (4, 0)); + assert_eq!((value.slot_stride, value.value_offset), (2, 0)); + } + ("btree_set", BTreeEntryPresentation::Set { value }) => { + assert_eq!((value.slot_stride, value.value_offset), (4, 0)); + } + _ => anyhow::bail!("1.35.0: unexpected B-Tree entry for {parameter}"), + } + + let ValueCapturePlan::IndirectBTree { + root_pointer, + root_height, + length, + node_length, + keys, + values, + edges, + node_capacity: capture_capacity, + } = &plan.capture + else { + anyhow::bail!("1.35.0: unexpected capture for {parameter}") + }; + assert_eq!(member_offset(root_pointer)?, 0); + assert_eq!(member_offset(root_height)?, 8); + assert_eq!(member_offset(length)?, 16); + assert_eq!(member_offset(node_length)?, 10); + assert_eq!((keys.offset, keys.slot_stride), (12, 4)); + match (parameter, values) { + ("btree_map", Some(values)) => { + assert_eq!((values.offset, values.slot_stride), (56, 2)); + } + ("btree_set", None) => {} + _ => anyhow::bail!("1.35.0: unexpected B-Tree values for {parameter}"), + } + let expected_edges_offset = match parameter { + "btree_map" => 80, + "btree_set" => 56, + _ => unreachable!("validated B-Tree parameter"), + }; + assert_eq!( + ( + edges.offset_from_leaf, + edges.slot_stride, + edges.pointer_offset, + edges.pointer_size, + edges.edge_count, + ), + (expected_edges_offset, 8, 0, 8, 12) + ); + assert_eq!(*capture_capacity, 11); + Ok(()) +} + +fn assert_rust_135_sequence_plan(parameter: &str, plan: &ValueReadPlan) -> anyhow::Result<()> { + let ValuePresentation::Sequence { element_stride, .. } = &plan.presentation else { + anyhow::bail!("1.35.0: unexpected presentation for {parameter}") + }; + assert_eq!(*element_stride, 4); + + match (parameter, &plan.capture) { + ( + "slice", + ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + }, + ) => { + assert_eq!(member_offset(data)?, 0); + assert_eq!(member_offset(length)?, 8); + assert_eq!(*element_stride, 4); + } + ( + "vector", + ValueCapturePlan::IndirectSequence { + data, + length, + element_stride, + }, + ) => { + assert_eq!(member_offset(data)?, 0); + assert_eq!(member_offset(length)?, 16); + assert_eq!(*element_stride, 4); + } + ( + "deque", + ValueCapturePlan::IndirectRingSequence { + data, + start, + length, + capacity, + element_stride, + }, + ) => { + let ghostscope_dwarf::RingSequenceLength::End(end) = length.as_ref() else { + anyhow::bail!("1.35.0: VecDeque must derive length from tail/head") + }; + assert_eq!(member_offset(data)?, 16); + assert_eq!(member_offset(start)?, 0); + assert_eq!(member_offset(end)?, 8); + assert_eq!(member_offset(capacity)?, 24); + assert_eq!(*element_stride, 4); + } + _ => anyhow::bail!("1.35.0: unexpected sequence capture for {parameter}"), + } + Ok(()) +} + +async fn assert_parameter_adapter( + analyzer: &DwarfAnalyzer, + binary: &Path, + function: &str, + parameter: &str, + expected: ExpectedAdapter, + toolchain: &str, +) -> anyhow::Result> { + let context = analyzer + .lookup_function_addresses(function) + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing function {function}"))?; + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing {function}::{parameter}"))?; + let parameter_type = analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing type for {function}::{parameter}"))?; + let plan = analyzer.value_read_plan(¶meter_type, Some(binary))?; + anyhow::ensure!( + adapter_matches(plan.as_ref(), expected), + "{toolchain}: unexpected adapter for {function}::{parameter}: {plan:#?}\n\ + resolved type: {parameter_type:#?}" + ); + Ok(plan) +} + +async fn resolved_parameter_type( + analyzer: &DwarfAnalyzer, + function: &str, + parameter: &str, + toolchain: &str, +) -> anyhow::Result { + let context = analyzer + .lookup_function_addresses(function) + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing function {function}"))?; + let parameter_plan = analyzer + .plan_variable_by_name(&context, parameter)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing {function}::{parameter}"))?; + Ok(analyzer + .resolved_type_for_plan(¶meter_plan)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing type for {function}::{parameter}"))? + .summary) +} + +fn variant_name(variant: &VariantCase) -> Option<&str> { + match variant.members.as_slice() { + [member] => Some(member.name.as_str()), + _ => None, + } +} + +fn variant_payload_fields(variant: &VariantCase) -> Option<&[ghostscope_dwarf::StructMember]> { + let [payload] = variant.members.as_slice() else { + return None; + }; + let TypeInfo::StructType { members, .. } = payload.member_type.underlying_type() else { + return None; + }; + Some(members) +} + +fn variant_payload_field_names(variant: &VariantCase) -> Option> { + Some( + variant_payload_fields(variant)? + .iter() + .map(|member| member.name.as_str()) + .collect(), + ) +} + +fn exact_unsigned_selector(variant: &VariantCase) -> Option { + let VariantSelector::Ranges(ranges) = &variant.selector else { + return None; + }; + let [range] = ranges.as_slice() else { + return None; + }; + match (range.start, range.end) { + (DiscriminantValue::Unsigned(start), DiscriminantValue::Unsigned(end)) if start == end => { + Some(start) + } + _ => None, + } +} + +fn exact_nonnegative_selector(variant: &VariantCase) -> Option { + let VariantSelector::Ranges(ranges) = &variant.selector else { + return None; + }; + let [range] = ranges.as_slice() else { + return None; + }; + match (range.start, range.end) { + (DiscriminantValue::Unsigned(start), DiscriminantValue::Unsigned(end)) if start == end => { + Some(start) + } + (DiscriminantValue::Signed(start), DiscriminantValue::Signed(end)) + if start == end && start >= 0 => + { + u64::try_from(start).ok() + } + _ => None, + } +} + +fn assert_compat_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::VariantType { + name, + size, + members, + variant_parts, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatEnum did not resolve as a DWARF variant type") + }; + anyhow::ensure!( + name == "CompatEnum", + "{toolchain}: unexpected enum name {name}" + ); + anyhow::ensure!(*size > 0, "{toolchain}: empty CompatEnum layout"); + anyhow::ensure!(members.is_empty(), "{toolchain}: unexpected common fields"); + let [part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatEnum variant part") + }; + let discriminant = part + .discriminant + .as_ref() + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing CompatEnum discriminant"))?; + anyhow::ensure!( + discriminant.offset == 0 + && discriminant.member_type.is_unsigned_int() + && discriminant + .offset + .checked_add(discriminant.member_type.size()) + .is_some_and(|end| end <= *size), + "{toolchain}: unexpected CompatEnum discriminant {discriminant:#?}" + ); + for variant in &part.variants { + for member in &variant.members { + anyhow::ensure!( + member + .offset + .checked_add(member.member_type.size()) + .is_some_and(|end| end <= *size), + "{toolchain}: CompatEnum payload member exceeds its DWARF size: {member:#?}" + ); + } + } + anyhow::ensure!( + part.variants + .iter() + .filter_map(variant_name) + .collect::>() + == ["Unit", "Tuple", "Struct"], + "{toolchain}: unexpected CompatEnum variants: {:#?}", + part.variants + ); + anyhow::ensure!( + part.variants + .iter() + .map(|variant| variant.payload_presentation) + .collect::>() + == [ + VariantPayloadPresentation::Unit, + VariantPayloadPresentation::Tuple, + VariantPayloadPresentation::Struct, + ], + "{toolchain}: unexpected CompatEnum payload presentations" + ); + let payload_fields = part + .variants + .iter() + .map(variant_payload_field_names) + .collect::>(); + anyhow::ensure!( + payload_fields + == [ + Some(Vec::new()), + Some(vec!["__0", "__1"]), + Some(vec!["left", "right"]), + ], + "{toolchain}: unexpected CompatEnum payload fields: {payload_fields:#?}" + ); + anyhow::ensure!( + part.variants + .iter() + .map(exact_unsigned_selector) + .collect::>() + == [Some(0), Some(1), Some(2)], + "{toolchain}: unexpected CompatEnum selectors" + ); + Ok(()) +} + +fn assert_niche_option_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::VariantType { + name, + size, + variant_parts, + .. + } = type_info + else { + anyhow::bail!("{toolchain}: Option did not resolve as a variant type") + }; + anyhow::ensure!( + name.contains("Option<") && *size == 4, + "{toolchain}: unexpected niche Option type {name} size {size}" + ); + let [part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one Option variant part") + }; + let none = part + .variants + .iter() + .find(|variant| variant_name(variant) == Some("None")) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing Option::None"))?; + let some = part + .variants + .iter() + .find(|variant| variant_name(variant) == Some("Some")) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing Option::Some"))?; + anyhow::ensure!( + exact_unsigned_selector(none) == Some(0), + "{toolchain}: Option::None is not the zero niche" + ); + anyhow::ensure!( + matches!(some.selector, VariantSelector::Default), + "{toolchain}: Option::Some is not the default niche branch" + ); + anyhow::ensure!( + none.payload_presentation == VariantPayloadPresentation::Unit + && some.payload_presentation == VariantPayloadPresentation::Tuple, + "{toolchain}: unexpected Option payload presentations" + ); + Ok(()) +} + +fn assert_nested_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::VariantType { + name, + size, + members, + variant_parts, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatOuter did not resolve as a variant type: {type_info:#?}") + }; + anyhow::ensure!( + name == "CompatOuter" && *size > 0 && members.is_empty(), + "{toolchain}: unexpected CompatOuter type {type_info:#?}" + ); + let [outer_part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatOuter variant part") + }; + anyhow::ensure!( + outer_part + .variants + .iter() + .filter_map(variant_name) + .collect::>() + == ["Empty", "Wrapped"], + "{toolchain}: unexpected CompatOuter variants: {outer_part:#?}" + ); + anyhow::ensure!( + outer_part + .variants + .iter() + .map(|variant| variant.payload_presentation) + .collect::>() + == [ + VariantPayloadPresentation::Unit, + VariantPayloadPresentation::Tuple, + ], + "{toolchain}: unexpected CompatOuter payload presentations" + ); + + let wrapped = outer_part + .variants + .iter() + .find(|variant| variant_name(variant) == Some("Wrapped")) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing CompatOuter::Wrapped"))?; + let [inner_field] = variant_payload_fields(wrapped).unwrap_or_default() else { + anyhow::bail!("{toolchain}: unexpected CompatOuter::Wrapped payload {wrapped:#?}") + }; + anyhow::ensure!( + inner_field.name == "__0", + "{toolchain}: unexpected CompatOuter::Wrapped field {inner_field:#?}" + ); + + let TypeInfo::VariantType { + name, + size, + members, + variant_parts, + } = inner_field.member_type.underlying_type() + else { + anyhow::bail!("{toolchain}: CompatInner did not resolve as a nested variant type") + }; + anyhow::ensure!( + name == "CompatInner" && *size > 0 && members.is_empty(), + "{toolchain}: unexpected CompatInner type {:#?}", + inner_field.member_type + ); + let [inner_part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatInner variant part") + }; + let [pair] = inner_part.variants.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatInner variant") + }; + anyhow::ensure!( + inner_part.discriminant.is_none() + && variant_name(pair) == Some("Pair") + && matches!(pair.selector, VariantSelector::Default) + && pair.payload_presentation == VariantPayloadPresentation::Tuple + && variant_payload_field_names(pair) == Some(vec!["__0", "__1"]), + "{toolchain}: unexpected CompatInner::Pair shape {pair:#?}" + ); + Ok(()) +} + +fn assert_pointer_niche_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::VariantType { + name, + size, + variant_parts, + .. + } = type_info + else { + anyhow::bail!("{toolchain}: Option<&i32> did not resolve as a variant type") + }; + anyhow::ensure!( + name.contains("Option<") && *size > 0, + "{toolchain}: unexpected pointer niche type {name} size {size}" + ); + let [part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one Option<&i32> variant part") + }; + let none = part + .variants + .iter() + .find(|variant| variant_name(variant) == Some("None")) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing Option<&i32>::None"))?; + let some = part + .variants + .iter() + .find(|variant| variant_name(variant) == Some("Some")) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing Option<&i32>::Some"))?; + anyhow::ensure!( + exact_nonnegative_selector(none) == Some(0) + && matches!(some.selector, VariantSelector::Default), + "{toolchain}: unexpected Option<&i32> niche selectors: {part:#?}" + ); + anyhow::ensure!( + none.payload_presentation == VariantPayloadPresentation::Unit + && some.payload_presentation == VariantPayloadPresentation::Tuple + && variant_payload_field_names(none) == Some(Vec::new()) + && variant_payload_field_names(some) == Some(vec!["__0"]), + "{toolchain}: unexpected Option<&i32> payload shapes: {part:#?}" + ); + let [pointer] = variant_payload_fields(some).unwrap_or_default() else { + anyhow::bail!("{toolchain}: unexpected Option<&i32>::Some payload {some:#?}") + }; + let TypeInfo::PointerType { + target_type, + size: pointer_size, + } = pointer.member_type.underlying_type() + else { + anyhow::bail!("{toolchain}: Option<&i32>::Some payload is not a pointer") + }; + anyhow::ensure!( + target_type.type_name() == "i32" && *pointer_size > 0 && *pointer_size <= *size, + "{toolchain}: unexpected Option<&i32>::Some pointer {pointer:#?}" + ); + Ok(()) +} + +fn assert_repr_c_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::VariantType { + name, + size, + members, + variant_parts, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatReprC did not resolve as a variant type") + }; + anyhow::ensure!( + name == "CompatReprC" && *size > 0 && members.is_empty(), + "{toolchain}: unexpected CompatReprC type {type_info:#?}" + ); + let [part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatReprC variant part") + }; + let discriminant = part + .discriminant + .as_ref() + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing CompatReprC discriminant"))?; + anyhow::ensure!( + (discriminant.member_type.is_signed_int() || discriminant.member_type.is_unsigned_int()) + && discriminant + .offset + .checked_add(discriminant.member_type.size()) + .is_some_and(|end| end <= *size), + "{toolchain}: unexpected CompatReprC discriminant {discriminant:#?}" + ); + anyhow::ensure!( + part.variants + .iter() + .filter_map(variant_name) + .collect::>() + == ["Unit", "Tuple", "Struct"], + "{toolchain}: unexpected CompatReprC variants: {part:#?}" + ); + anyhow::ensure!( + part.variants + .iter() + .map(|variant| variant.payload_presentation) + .collect::>() + == [ + VariantPayloadPresentation::Unit, + VariantPayloadPresentation::Tuple, + VariantPayloadPresentation::Struct, + ], + "{toolchain}: unexpected CompatReprC payload presentations" + ); + let payload_fields = part + .variants + .iter() + .map(variant_payload_field_names) + .collect::>(); + anyhow::ensure!( + payload_fields + == [ + Some(Vec::new()), + Some(vec!["__0", "__1"]), + Some(vec!["left", "right"]), + ], + "{toolchain}: unexpected CompatReprC payload fields: {payload_fields:#?}" + ); + anyhow::ensure!( + part.variants + .iter() + .map(exact_nonnegative_selector) + .collect::>() + == [Some(0), Some(1), Some(2)], + "{toolchain}: unexpected CompatReprC selectors: {part:#?}" + ); + for variant in &part.variants { + for member in &variant.members { + anyhow::ensure!( + member + .offset + .checked_add(member.member_type.size()) + .is_some_and(|end| end <= *size), + "{toolchain}: CompatReprC member exceeds its DWARF size: {member:#?}" + ); + } + } + Ok(()) +} + +fn assert_fieldless_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::ScopedEnumType { + name, + size, + base_type, + variants, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatFieldless did not resolve as a scoped enum") + }; + anyhow::ensure!( + name == "CompatFieldless" && *size == 1 && base_type.is_unsigned_int(), + "{toolchain}: unexpected fieldless enum type {type_info:#?}" + ); + anyhow::ensure!( + variants + .iter() + .map(|variant| (variant.name.as_str(), variant.value)) + .collect::>() + == [ + ("First", DiscriminantValue::Unsigned(0)), + ("Second", DiscriminantValue::Unsigned(1)), + ], + "{toolchain}: unexpected fieldless enum variants {variants:#?}" + ); + Ok(()) +} + +fn assert_single_variant_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::VariantType { + name, + size, + members, + variant_parts, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatSingle did not resolve as a DWARF variant type") + }; + anyhow::ensure!( + name == "CompatSingle" && *size == 4 && members.is_empty(), + "{toolchain}: unexpected single-variant enum type {type_info:#?}" + ); + let [part] = variant_parts.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatSingle variant part") + }; + anyhow::ensure!( + part.discriminant.is_none(), + "{toolchain}: CompatSingle unexpectedly has a discriminant" + ); + let [variant] = part.variants.as_slice() else { + anyhow::bail!("{toolchain}: expected one CompatSingle variant") + }; + anyhow::ensure!( + variant_name(variant) == Some("Only") + && matches!(variant.selector, VariantSelector::Default) + && variant.payload_presentation == VariantPayloadPresentation::Tuple, + "{toolchain}: unexpected CompatSingle variant {variant:#?}" + ); + Ok(()) +} + +fn assert_signed_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::ScopedEnumType { + name, + size, + base_type, + variants, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatSigned did not resolve as a scoped enum") + }; + anyhow::ensure!( + name == "CompatSigned" && *size == 1 && base_type.is_signed_int(), + "{toolchain}: unexpected signed enum type {type_info:#?}" + ); + anyhow::ensure!( + variants + .iter() + .map(|variant| (variant.name.as_str(), variant.value)) + .collect::>() + == [ + ("Negative", DiscriminantValue::Signed(-1)), + ("Positive", DiscriminantValue::Signed(1)), + ], + "{toolchain}: unexpected signed enum variants {variants:#?}" + ); + Ok(()) +} + +fn assert_unsigned_enum_dwarf(toolchain: &str, type_info: &TypeInfo) -> anyhow::Result<()> { + let TypeInfo::ScopedEnumType { + name, + size, + base_type, + variants, + } = type_info + else { + anyhow::bail!("{toolchain}: CompatUnsigned did not resolve as a scoped enum") + }; + anyhow::ensure!( + name == "CompatUnsigned" && *size == 8 && base_type.is_unsigned_int(), + "{toolchain}: unexpected unsigned enum type {type_info:#?}" + ); + anyhow::ensure!( + variants + .iter() + .map(|variant| (variant.name.as_str(), variant.value)) + .collect::>() + == [ + ("Low", DiscriminantValue::Unsigned(1)), + ("High", DiscriminantValue::Unsigned(0x8000_0000_0000_0000)), + ], + "{toolchain}: unexpected unsigned enum variants {variants:#?}" + ); + Ok(()) +} + +fn assert_target_rustc_version(analyzer: &DwarfAnalyzer, toolchain: &str) -> anyhow::Result<()> { + let context = analyzer + .lookup_function_addresses("observe_values") + .into_iter() + .find_map(|address| analyzer.resolve_pc(&address).ok()) + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing function observe_values"))?; + let metadata = analyzer + .compilation_unit_metadata_for_context(&context)? + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing compilation-unit metadata"))?; + anyhow::ensure!( + metadata.language == SourceLanguage::Rust, + "{toolchain}: expected a Rust compilation unit, got {:?}", + metadata.language + ); + + let expected = toolchain_rustc_version(toolchain)?; + anyhow::ensure!( + metadata.rustc_version() == Some(expected), + "{toolchain}: target producer {:?} did not report rustc {expected}", + metadata.producer + ); + Ok(()) +} + +#[tokio::test] +async fn rust_value_adapters_follow_pinned_toolchain_dwarf() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir()?; + let toolchains = configured_toolchains(); + let require_all = require_all_toolchains(); + let mut tested = 0usize; + + for toolchain in toolchains { + if !toolchain_is_installed(&toolchain) { + anyhow::ensure!( + !require_all, + "required Rust toolchain {toolchain} is not installed" + ); + eprintln!("skipping unavailable Rust compatibility toolchain {toolchain}"); + continue; + } + + let binary = temp_dir.path().join(format!( + "rust-value-compat-{}", + toolchain.replace(['.', '-'], "_") + )); + compile_fixture(&toolchain, &binary)?; + let analyzer = DwarfAnalyzer::from_exec_path(&binary).await?; + assert_target_rustc_version(&analyzer, &toolchain)?; + + // Pin only the Rust 1.35 layouts audited against rust-gdb semantics. + // The remaining adapters keep a Rust 1.49 floor until their older + // DWARF shapes have been audited. + let adapters = if toolchain == "1.35.0" { + RUST_135_ADAPTERS + } else { + ALL_ADAPTERS + }; + for &(parameter, expected) in adapters { + let plan = assert_parameter_adapter( + &analyzer, + &binary, + "observe_values", + parameter, + expected, + &toolchain, + ) + .await?; + if toolchain == "1.35.0" && matches!(parameter, "slice" | "vector" | "deque") { + assert_rust_135_sequence_plan( + parameter, + plan.as_ref().ok_or_else(|| { + anyhow::anyhow!("1.35.0: missing sequence plan for {parameter}") + })?, + )?; + } + if toolchain == "1.35.0" && matches!(parameter, "btree_map" | "btree_set") { + assert_rust_135_btree_plan( + parameter, + plan.as_ref().ok_or_else(|| { + anyhow::anyhow!("1.35.0: missing legacy plan for {parameter}") + })?, + )?; + } + if toolchain == "1.35.0" && matches!(parameter, "hash_map" | "hash_set") { + let plan = plan.as_ref().ok_or_else(|| { + anyhow::anyhow!("1.35.0: missing legacy plan for {parameter}") + })?; + let ValuePresentation::HashTable { occupancy, .. } = plan.presentation else { + anyhow::bail!("1.35.0: unexpected presentation for {parameter}") + }; + let ValueCapturePlan::IndirectHashTable { buckets, .. } = &plan.capture else { + anyhow::bail!("1.35.0: unexpected capture for {parameter}") + }; + assert_eq!( + occupancy, + ghostscope_dwarf::HashTableOccupancy::NonZeroWord { word_size: 8 } + ); + assert!(matches!( + buckets, + ghostscope_dwarf::HashTableBucketSource::LegacyAfterControl { + entry_alignment: 4, + pointer_tag_mask: 1, + } + )); + } + } + assert_parameter_adapter( + &analyzer, + &binary, + "observe_mut_str", + "value", + ExpectedAdapter::Utf8Bytes, + &toolchain, + ) + .await?; + + let enum_type = + resolved_parameter_type(&analyzer, "observe_values", "enum_value", &toolchain).await?; + assert_compat_enum_dwarf(&toolchain, &enum_type)?; + let fieldless_type = + resolved_parameter_type(&analyzer, "observe_values", "fieldless", &toolchain).await?; + assert_fieldless_enum_dwarf(&toolchain, &fieldless_type)?; + let single_type = + resolved_parameter_type(&analyzer, "observe_values", "single", &toolchain).await?; + assert_single_variant_enum_dwarf(&toolchain, &single_type)?; + let signed_type = + resolved_parameter_type(&analyzer, "observe_values", "signed", &toolchain).await?; + assert_signed_enum_dwarf(&toolchain, &signed_type)?; + let unsigned_type = + resolved_parameter_type(&analyzer, "observe_values", "unsigned", &toolchain).await?; + assert_unsigned_enum_dwarf(&toolchain, &unsigned_type)?; + let option_type = + resolved_parameter_type(&analyzer, "observe_values", "option_nonzero", &toolchain) + .await?; + assert_niche_option_dwarf(&toolchain, &option_type)?; + + let nested_type = + resolved_parameter_type(&analyzer, "observe_nested_enum", "value", &toolchain).await?; + assert_nested_enum_dwarf(&toolchain, &nested_type)?; + + let pointer_some = + resolved_parameter_type(&analyzer, "observe_pointer_niche", "some", &toolchain).await?; + let pointer_none = + resolved_parameter_type(&analyzer, "observe_pointer_niche", "none", &toolchain).await?; + anyhow::ensure!( + pointer_some == pointer_none, + "{toolchain}: Option<&i32> parameter types differ" + ); + assert_pointer_niche_dwarf(&toolchain, &pointer_some)?; + + let repr_c_unit = + resolved_parameter_type(&analyzer, "observe_repr_c_enum", "unit", &toolchain).await?; + let repr_c_tuple = + resolved_parameter_type(&analyzer, "observe_repr_c_enum", "tuple", &toolchain).await?; + let repr_c_struct = + resolved_parameter_type(&analyzer, "observe_repr_c_enum", "struct_value", &toolchain) + .await?; + anyhow::ensure!( + repr_c_unit == repr_c_tuple && repr_c_unit == repr_c_struct, + "{toolchain}: CompatReprC parameter types differ" + ); + assert_repr_c_enum_dwarf(&toolchain, &repr_c_unit)?; + + if toolchain != "1.35.0" { + assert_parameter_adapter( + &analyzer, + &binary, + "observe_ref", + "value", + ExpectedAdapter::RefGuard, + &toolchain, + ) + .await?; + assert_parameter_adapter( + &analyzer, + &binary, + "observe_ref_mut", + "value", + ExpectedAdapter::RefGuard, + &toolchain, + ) + .await?; + } + let path_expected = if matches!(toolchain.as_str(), "1.35.0" | "1.49.0") { + ExpectedAdapter::NativeDwarf + } else { + ExpectedAdapter::OsBytes + }; + assert_parameter_adapter( + &analyzer, + &binary, + "observe_values", + "path", + path_expected, + &toolchain, + ) + .await?; + for (parameter, type_name) in [("rc_text", "Rc"), ("arc_text", "Arc")] { + let plan = assert_parameter_adapter( + &analyzer, + &binary, + "observe_values", + parameter, + ExpectedAdapter::ReferenceCounted, + &toolchain, + ) + .await?; + assert_reference_counted_dst_plan( + &toolchain, + type_name, + plan.as_ref() + .ok_or_else(|| anyhow::anyhow!("{toolchain}: missing {type_name} plan"))?, + )?; + } + tested += 1; + } + + if tested == 0 { + eprintln!("no Rust compatibility toolchains were available; skipping test"); + } + Ok(()) +} diff --git a/ghostscope-protocol/src/format_printer.rs b/ghostscope-protocol/src/format_printer.rs index b71b3da2..108177aa 100644 --- a/ghostscope-protocol/src/format_printer.rs +++ b/ghostscope-protocol/src/format_printer.rs @@ -2,12 +2,23 @@ //! //! Converts PrintComplexVariable/PrintComplexFormat payloads into formatted text in user space. +use std::borrow::Cow; + use crate::trace_context::TraceContext; use crate::trace_event::{ VariableStatus, VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET, VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET, VARIABLE_READ_ERROR_PAYLOAD_LEN, }; use crate::type_info::TypeInfo; +use crate::{ + BTreeEntryPresentation, BTreeFieldPresentation, HashTableBucketOrder, + HashTableEntryPresentation, HashTableFieldPresentation, HashTableOccupancy, ValuePresentation, + BTREE_CAPTURED_ITEM_COUNT_OFFSET, BTREE_HEADER_SIZE, BTREE_NODE_HEADER_SIZE, + BTREE_NODE_HEIGHT_OFFSET, BTREE_NODE_LENGTH_OFFSET, BTREE_NODE_SLOT_COUNT_OFFSET, + HASH_TABLE_BUCKET_DATA_OFFSET, HASH_TABLE_CAPACITY_OFFSET, HASH_TABLE_CAPTURED_BUCKETS_OFFSET, + HASH_TABLE_HEADER_SIZE, INDIRECT_BYTES_LENGTH_PREFIX_SIZE, + INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET, INDIRECT_SEQUENCE_HEADER_SIZE, +}; // Removed legacy simple variable wrapper; use complex paths only. @@ -21,6 +32,29 @@ pub struct ParsedComplexVariable { pub data: Vec, } +struct ParsedHashTablePayload<'a> { + original_count: u64, + captured_buckets: usize, + occupancy: &'a [u8], + buckets: &'a [u8], +} + +struct ParsedBTreeNode<'a> { + height: u64, + length: usize, + keys: &'a [u8], + values: Option<&'a [u8]>, +} + +struct ParsedBTreePayload<'a> { + original_count: u64, + captured_count: u64, + edge_count: usize, + nodes: Vec>>, +} + +type BTreeEntryBytes<'a> = (&'a [u8], Option<&'a [u8]>); + /// Format printer for converting PrintComplexFormat data to formatted strings pub struct FormatPrinter; @@ -135,7 +169,7 @@ impl FormatPrinter { v.status, trace_context, ); - let value_part = s.split(" = ").last().unwrap_or(&s); + let value_part = Self::formatted_value_part(&s); result.push_str(value_part); var_index += 1; } else { @@ -218,7 +252,7 @@ impl FormatPrinter { } } - // helper: format error value for a var when status != Ok/ZeroLength + // Format statuses that cannot be consumed by a conversion. let err_value_part = |idx: usize| -> Option { if idx >= vars.len() { return None; @@ -237,7 +271,16 @@ impl FormatPrinter { v.status, trace_context, ); - Some(s.split(" = ").last().unwrap_or(&s).to_string()) + Some(Self::formatted_value_part(&s).to_string()) + } + }; + let raw_err_value_part = |idx: usize| -> Option { + if vars.get(idx).is_some_and(|variable| { + Self::is_semantic_truncation(variable, trace_context) + }) { + None + } else { + err_value_part(idx) } }; @@ -363,30 +406,35 @@ impl FormatPrinter { Len::None => { if var_index >= vars.len() { result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { + } else if let Some(err) = raw_err_value_part(var_index) { result.push_str(&err); var_index += 1; continue; } else { let v = &vars[var_index]; - let b = if v.status == VariableStatus::ZeroLength as u8 + match Self::format_spec_payload_bytes(v, trace_context) { - &[][..] - } else { - v.data.as_slice() - }; - let s = b - .iter() - .map(|vv| { - if conv == 'x' { - format!("{vv:02x}") - } else { - format!("{vv:02X}") - } - }) - .collect::>() - .join(" "); - result.push_str(&s); + Ok(bytes) => { + let formatted = bytes + .iter() + .map(|byte| { + if conv == 'x' { + format!("{byte:02x}") + } else { + format!("{byte:02X}") + } + }) + .collect::>() + .join(" "); + result.push_str(&formatted); + } + Err(error) => result.push_str(error), + } + Self::append_semantic_truncation_marker( + &mut result, + v, + trace_context, + ); var_index += 1; continue; } @@ -394,7 +442,7 @@ impl FormatPrinter { } } 's' => { - let mut render_bytes = |b: &[u8]| { + let render_bytes = |b: &[u8]| { let mut out = String::new(); for &c in b.iter() { if c == 0 { @@ -406,7 +454,7 @@ impl FormatPrinter { out.push_str(&format!("\\x{c:02x}")); } } - result.push_str(&out); + out }; match lenspec { @@ -432,7 +480,7 @@ impl FormatPrinter { } else { std::cmp::min(n, full.len()) }; - render_bytes(&full[..take]); + result.push_str(&render_bytes(&full[..take])); var_index += 2; continue; } @@ -453,7 +501,7 @@ impl FormatPrinter { } else { std::cmp::min(n, full.len()) }; - render_bytes(&full[..take]); + result.push_str(&render_bytes(&full[..take])); var_index += 1; continue; } @@ -480,7 +528,7 @@ impl FormatPrinter { } else { std::cmp::min(n, full.len()) }; - render_bytes(&full[..take]); + result.push_str(&render_bytes(&full[..take])); var_index += 2; continue; } @@ -488,19 +536,24 @@ impl FormatPrinter { Len::None => { if var_index >= vars.len() { result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { + } else if let Some(err) = raw_err_value_part(var_index) { result.push_str(&err); var_index += 1; continue; } else { let v = &vars[var_index]; - let b = if v.status == VariableStatus::ZeroLength as u8 + match Self::format_spec_payload_bytes(v, trace_context) { - &[][..] - } else { - v.data.as_slice() - }; - render_bytes(b); + Ok(bytes) => { + result.push_str(&render_bytes(bytes.as_ref())); + } + Err(error) => result.push_str(error), + } + Self::append_semantic_truncation_marker( + &mut result, + v, + trace_context, + ); var_index += 1; continue; } @@ -540,7 +593,7 @@ impl FormatPrinter { v.status, trace_context, ); - let value_part = s.split(" = ").last().unwrap_or(&s); + let value_part = Self::formatted_value_part(&s); result.push_str(value_part); var_index += 1; } else { @@ -564,6 +617,34 @@ impl FormatPrinter { result } + fn formatted_value_part(formatted: &str) -> &str { + formatted + .split_once(" = ") + .map_or(formatted, |(_, value)| value) + } + + fn is_semantic_truncation( + variable: &ParsedComplexVariable, + trace_context: &TraceContext, + ) -> bool { + variable.status == VariableStatus::Truncated as u8 + && trace_context.get_value_presentation(variable.type_index) + != &ValuePresentation::Dwarf + } + + fn append_semantic_truncation_marker( + output: &mut String, + variable: &ParsedComplexVariable, + trace_context: &TraceContext, + ) { + if Self::is_semantic_truncation(variable, trace_context) { + if !output.is_empty() { + output.push(' '); + } + output.push_str(""); + } + } + /// Format a complex variable with full DWARF type information pub fn format_complex_variable( var_name_index: u16, @@ -581,7 +662,8 @@ impl FormatPrinter { None => return format!(": {var_name}"), }; - let formatted_data = Self::format_data_with_type_info(data, type_info); + let presentation = trace_context.get_value_presentation(type_index); + let formatted_data = Self::format_data_with_presentation(data, type_info, presentation); if access_path.is_empty() { format!("{var_name} = {formatted_data}") @@ -606,6 +688,32 @@ impl FormatPrinter { Some(t) => t, None => return format!(": {var_name}"), }; + let presentation = trace_context.get_value_presentation(type_index); + + if presentation != &ValuePresentation::Dwarf + && matches!( + status, + s if s == VariableStatus::Ok as u8 + || s == VariableStatus::ZeroLength as u8 + || s == VariableStatus::Truncated as u8 + ) + { + let payload_omitted = status == VariableStatus::Truncated as u8 + && Self::presentation_payload_bytes(data, presentation).is_none(); + let mut formatted_data = if payload_omitted { + "".to_string() + } else { + Self::format_data_with_presentation(data, type_info, presentation) + }; + if status == VariableStatus::Truncated as u8 && !payload_omitted { + formatted_data.push_str(" "); + } + return if access_path.is_empty() { + format!("{var_name} = {formatted_data}") + } else { + format!("{var_name}.{access_path} = {formatted_data}") + }; + } // OK path delegates to existing formatter if status == VariableStatus::Ok as u8 { @@ -674,571 +782,3118 @@ impl FormatPrinter { Self::format_data_with_type_info_impl(data, type_info, 0, 32) } - /// Internal implementation with depth control for recursion - fn format_data_with_type_info_impl( + /// Format captured data using its semantic presentation when one is + /// registered, otherwise preserve physical DWARF formatting. + pub fn format_data_with_presentation( data: &[u8], type_info: &TypeInfo, - current_depth: usize, - max_depth: usize, + presentation: &ValuePresentation, ) -> String { - if current_depth > max_depth { - return "".to_string(); + match presentation { + ValuePresentation::Dwarf => Self::format_data_with_type_info(data, type_info), + ValuePresentation::Utf8String => Self::format_utf8_string_payload(data), + ValuePresentation::Sequence { + element_type, + element_stride, + } => Self::format_sequence_payload(data, element_type, *element_stride), + ValuePresentation::ByteString => Self::format_byte_string_payload(data), + ValuePresentation::SingleField { + type_name, + field_name, + } => { + let value = Self::format_data_with_type_info(data, type_info); + format!("{type_name} {{ {field_name}: {value} }}") + } + ValuePresentation::SignedStateStruct { + state_field, + non_negative_label, + negative_label, + } => Self::format_signed_state_struct( + data, + type_info, + state_field, + non_negative_label, + negative_label, + ), + ValuePresentation::ReferenceCountedStruct { + strong_field, + weak_field, + implicit_weak, + } => Self::format_reference_counted_struct( + data, + type_info, + strong_field, + weak_field, + *implicit_weak, + ), + ValuePresentation::HashTable { + entry_stride, + bucket_order, + occupancy, + entry, + } => Self::format_hash_table_payload( + data, + *entry_stride, + *bucket_order, + *occupancy, + entry, + ), + ValuePresentation::BTree { + node_capacity, + entry, + } => Self::format_btree_payload(data, *node_capacity, entry), } + } - match type_info { - TypeInfo::BaseType { size, encoding, .. } => { - Self::format_base_type_data(data, *size, *encoding) + fn format_signed_state_struct( + data: &[u8], + type_info: &TypeInfo, + state_field: &str, + non_negative_label: &str, + negative_label: &str, + ) -> String { + let TypeInfo::StructType { name, members, .. } = type_info else { + return "".to_string(); + }; + let Some(member) = members.iter().find(|member| member.name == state_field) else { + return "".to_string(); + }; + let state = usize::try_from(member.offset) + .ok() + .and_then(|offset| data.get(offset..)) + .and_then(|data| Self::decode_signed_integer(data, &member.member_type)); + let summary = match state { + Some(state) if state >= 0 => { + format!("{name}({non_negative_label}={state})") } - TypeInfo::BitfieldType { - underlying_type, - bit_offset, - bit_size, - } => { - let u_size = underlying_type.size() as usize; - if data.len() < u_size || *bit_size == 0 { - return "".to_string(); - } - let val = - Self::extract_bits_le(&data[..u_size], *bit_offset as u32, *bit_size as u32); - Self::format_bitfield_value(val, underlying_type, *bit_size as u32) + Some(state) => { + format!("{name}({negative_label}={})", state.unsigned_abs()) } - TypeInfo::PointerType { target_type, .. } => { - if data.len() < 8 { - "".to_string() - } else { - let addr = u64::from_le_bytes([ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]); - let ty = format!("{}*", target_type.type_name()); - if addr == 0 { - format!("NULL ({ty})") - } else { - format!("0x{addr:x} ({ty})") - } + None => format!("{name}({state_field}=)"), + }; + let fields = Self::format_data_with_type_info(data, type_info); + let Some(fields) = fields.strip_prefix(name) else { + return "".to_string(); + }; + format!("{summary}{fields}") + } + + fn format_reference_counted_struct( + data: &[u8], + type_info: &TypeInfo, + strong_field: &str, + weak_field: &str, + implicit_weak: u64, + ) -> String { + let TypeInfo::StructType { name, members, .. } = type_info else { + return "".to_string(); + }; + let Some(strong_member) = members.iter().find(|member| member.name == strong_field) else { + return "".to_string(); + }; + let Some(weak_member) = members.iter().find(|member| member.name == weak_field) else { + return "".to_string(); + }; + + let strong = Self::decode_unsigned_member(data, strong_member); + let weak = Self::decode_unsigned_member(data, weak_member) + .and_then(|weak| weak.checked_sub(u128::from(implicit_weak))); + + // Keep the protocol payload raw. Adjust a user-space copy so the + // existing DWARF struct formatter shows the public weak count too. + let mut adjusted_data = data.to_vec(); + if let Some(weak) = weak { + let offset = usize::try_from(weak_member.offset).ok(); + if let Some(field_data) = offset.and_then(|offset| adjusted_data.get_mut(offset..)) { + let _ = Self::encode_unsigned_integer(field_data, &weak_member.member_type, weak); + } + } + + let strong = strong + .map(|value| value.to_string()) + .unwrap_or_else(|| "".to_string()); + let weak = weak + .map(|value| value.to_string()) + .unwrap_or_else(|| "".to_string()); + let summary = format!("{name}({strong_field}={strong}, {weak_field}={weak})"); + let fields = Self::format_data_with_type_info(&adjusted_data, type_info); + let Some(fields) = fields.strip_prefix(name) else { + return "".to_string(); + }; + format!("{summary}{fields}") + } + + fn decode_unsigned_member(data: &[u8], member: &crate::StructMember) -> Option { + let offset = usize::try_from(member.offset).ok()?; + Self::decode_unsigned_integer(data.get(offset..)?, &member.member_type) + } + + fn decode_unsigned_integer(data: &[u8], type_info: &TypeInfo) -> Option { + let type_info = match type_info { + TypeInfo::TypedefType { + underlying_type, .. + } + | TypeInfo::QualifiedType { + underlying_type, .. + } => return Self::decode_unsigned_integer(data, underlying_type), + type_info => type_info, + }; + let TypeInfo::BaseType { size, encoding, .. } = type_info else { + return None; + }; + if *encoding != gimli::constants::DW_ATE_unsigned.0 as u16 + && *encoding != gimli::constants::DW_ATE_unsigned_char.0 as u16 + { + return None; + } + + match *size { + 1 => Some(u8::from_le_bytes(data.get(..1)?.try_into().ok()?) as u128), + 2 => Some(u16::from_le_bytes(data.get(..2)?.try_into().ok()?) as u128), + 4 => Some(u32::from_le_bytes(data.get(..4)?.try_into().ok()?) as u128), + 8 => Some(u64::from_le_bytes(data.get(..8)?.try_into().ok()?) as u128), + 16 => Some(u128::from_le_bytes(data.get(..16)?.try_into().ok()?)), + _ => None, + } + } + + fn encode_unsigned_integer(data: &mut [u8], type_info: &TypeInfo, value: u128) -> bool { + let type_info = match type_info { + TypeInfo::TypedefType { + underlying_type, .. + } + | TypeInfo::QualifiedType { + underlying_type, .. + } => return Self::encode_unsigned_integer(data, underlying_type, value), + type_info => type_info, + }; + let TypeInfo::BaseType { size, encoding, .. } = type_info else { + return false; + }; + if *encoding != gimli::constants::DW_ATE_unsigned.0 as u16 + && *encoding != gimli::constants::DW_ATE_unsigned_char.0 as u16 + { + return false; + } + + let bytes = value.to_le_bytes(); + let Ok(size) = usize::try_from(*size) else { + return false; + }; + let Some(target) = data.get_mut(..size) else { + return false; + }; + let Some(source) = bytes.get(..size) else { + return false; + }; + target.copy_from_slice(source); + true + } + + fn decode_signed_integer(data: &[u8], type_info: &TypeInfo) -> Option { + let type_info = match type_info { + TypeInfo::TypedefType { + underlying_type, .. + } + | TypeInfo::QualifiedType { + underlying_type, .. + } => return Self::decode_signed_integer(data, underlying_type), + type_info => type_info, + }; + let TypeInfo::BaseType { size, encoding, .. } = type_info else { + return None; + }; + if *encoding != gimli::constants::DW_ATE_signed.0 as u16 + && *encoding != gimli::constants::DW_ATE_signed_char.0 as u16 + { + return None; + } + + match *size { + 1 => Some(i8::from_le_bytes(data.get(..1)?.try_into().ok()?) as i128), + 2 => Some(i16::from_le_bytes(data.get(..2)?.try_into().ok()?) as i128), + 4 => Some(i32::from_le_bytes(data.get(..4)?.try_into().ok()?) as i128), + 8 => Some(i64::from_le_bytes(data.get(..8)?.try_into().ok()?) as i128), + 16 => Some(i128::from_le_bytes(data.get(..16)?.try_into().ok()?)), + _ => None, + } + } + + fn format_spec_payload_bytes<'a>( + variable: &'a ParsedComplexVariable, + trace_context: &TraceContext, + ) -> Result, &'static str> { + if variable.status == VariableStatus::ZeroLength as u8 { + return Ok(Cow::Borrowed(&[])); + } + + let presentation = trace_context.get_value_presentation(variable.type_index); + if let ValuePresentation::HashTable { + entry_stride, + bucket_order, + occupancy, + .. + } = presentation + { + return Self::hash_table_occupied_bucket_bytes( + &variable.data, + *entry_stride, + *bucket_order, + *occupancy, + ) + .map(Cow::Owned) + .ok_or(""); + } + if let ValuePresentation::BTree { + node_capacity, + entry, + } = presentation + { + return match Self::btree_value_bytes(&variable.data, *node_capacity, entry) { + Some(bytes) => Ok(Cow::Owned(bytes)), + None if Self::is_semantic_truncation(variable, trace_context) => { + Ok(Cow::Borrowed(&[])) } + None => Err(""), + }; + } + match Self::presentation_payload_bytes(&variable.data, presentation) { + Some(payload) => Ok(Cow::Borrowed(payload)), + None if Self::is_semantic_truncation(variable, trace_context) => Ok(Cow::Borrowed(&[])), + None => Err(""), + } + } + + fn presentation_payload_bytes<'a>( + data: &'a [u8], + presentation: &ValuePresentation, + ) -> Option<&'a [u8]> { + match presentation { + ValuePresentation::Dwarf => Some(data), + ValuePresentation::Utf8String | ValuePresentation::ByteString => { + let prefix = data.get(..INDIRECT_BYTES_LENGTH_PREFIX_SIZE)?; + let original_len = u64::from_le_bytes(prefix.try_into().ok()?); + let payload = &data[INDIRECT_BYTES_LENGTH_PREFIX_SIZE..]; + let captured_len = usize::try_from(original_len) + .unwrap_or(usize::MAX) + .min(payload.len()); + Some(&payload[..captured_len]) } - TypeInfo::ArrayType { - element_type, - element_count, + ValuePresentation::Sequence { element_stride, .. } => { + let (_, _, payload) = Self::parse_sequence_payload(data, *element_stride)?; + Some(payload) + } + ValuePresentation::HashTable { + entry_stride, + occupancy, .. + } => Some(Self::parse_hash_table_payload(data, *entry_stride, *occupancy)?.buckets), + ValuePresentation::BTree { + node_capacity, + entry, } => { - // Special-case: char arrays -> print as string - if Self::is_char_byte_type(element_type) { - return Self::format_char_array_as_string(data, element_count); - } - let elem_size = element_type.size() as usize; - if elem_size == 0 { - return "".to_string(); - } + Self::parse_btree_payload(data, *node_capacity, entry)?; + Some(data) + } + ValuePresentation::SingleField { .. } + | ValuePresentation::SignedStateStruct { .. } + | ValuePresentation::ReferenceCountedStruct { .. } => Some(data), + } + } - let count = element_count.unwrap_or(data.len() as u64 / elem_size as u64); - let actual_count = std::cmp::min(count, data.len() as u64 / elem_size as u64); + fn format_utf8_string_payload(data: &[u8]) -> String { + let Some(captured) = Self::presentation_payload_bytes(data, &ValuePresentation::Utf8String) + else { + return "".to_string(); + }; - if actual_count == 0 { - return "[]".to_string(); + Self::format_utf8_bytes(captured) + } + + fn format_utf8_bytes(captured: &[u8]) -> String { + match std::str::from_utf8(captured) { + Ok(value) => format!("{value:?}"), + Err(error) if error.error_len().is_none() => { + let valid = &captured[..error.valid_up_to()]; + let valid = std::str::from_utf8(valid) + .expect("UTF-8 error valid_up_to always identifies valid bytes"); + format!("{valid:?}") + } + Err(_) => { + let escaped = captured + .iter() + .map(|byte| format!("\\x{byte:02x}")) + .collect::(); + format!("") + } + } + } + + fn format_byte_string_payload(data: &[u8]) -> String { + let Some(captured) = Self::presentation_payload_bytes(data, &ValuePresentation::ByteString) + else { + return "".to_string(); + }; + + let mut output = String::from("\""); + let mut remaining = captured; + while !remaining.is_empty() { + match std::str::from_utf8(remaining) { + Ok(valid) => { + for character in valid.chars() { + output.extend(character.escape_debug()); + } + break; } + Err(error) => { + let valid_len = error.valid_up_to(); + let valid = std::str::from_utf8(&remaining[..valid_len]) + .expect("UTF-8 error valid_up_to identifies valid bytes"); + for character in valid.chars() { + output.extend(character.escape_debug()); + } - let mut result = String::from("["); - for i in 0..actual_count { - if i > 0 { - result.push_str(", "); + let invalid_len = error + .error_len() + .unwrap_or_else(|| remaining.len().saturating_sub(valid_len)); + for byte in &remaining[valid_len..valid_len + invalid_len] { + output.push_str(&format!("\\x{byte:02x}")); } + remaining = &remaining[valid_len + invalid_len..]; + } + } + } + output.push('"'); + output + } - let start = i as usize * elem_size; - let end = std::cmp::min(start + elem_size, data.len()); - let elem_data = &data[start..end]; + fn payload_u64(data: &[u8], offset: usize) -> Option { + let end = offset.checked_add(std::mem::size_of::())?; + Some(u64::from_le_bytes(data.get(offset..end)?.try_into().ok()?)) + } - let formatted_elem = Self::format_data_with_type_info_impl( - elem_data, - element_type, - current_depth + 1, - max_depth, - ); - result.push_str(&formatted_elem); - } - result.push(']'); - result + fn parse_hash_table_payload( + data: &[u8], + entry_stride: u64, + occupancy: HashTableOccupancy, + ) -> Option> { + let original_count = Self::payload_u64(data, 0)?; + let capacity = Self::payload_u64(data, HASH_TABLE_CAPACITY_OFFSET)?; + let captured_buckets = Self::payload_u64(data, HASH_TABLE_CAPTURED_BUCKETS_OFFSET)?; + let bucket_offset = Self::payload_u64(data, HASH_TABLE_BUCKET_DATA_OFFSET)?; + if original_count > capacity || captured_buckets > capacity { + return None; + } + + let captured_buckets = usize::try_from(captured_buckets).ok()?; + let occupancy_width = usize::try_from(occupancy.byte_width()?).ok()?; + let occupancy_len = captured_buckets.checked_mul(occupancy_width)?; + let occupancy_end = HASH_TABLE_HEADER_SIZE.checked_add(occupancy_len)?; + let bucket_offset = usize::try_from(bucket_offset).ok()?; + // The eBPF layout fixes the bucket offset after the maximum reserved + // occupancy region so the verifier sees constant destinations. A small + // runtime table can therefore leave unused occupancy headroom here. + if bucket_offset < occupancy_end { + return None; + } + let stride = usize::try_from(entry_stride).ok()?; + let bucket_len = captured_buckets.checked_mul(stride)?; + let bucket_end = bucket_offset.checked_add(bucket_len)?; + let occupancy_bytes = data.get(HASH_TABLE_HEADER_SIZE..occupancy_end)?; + let buckets = data.get(bucket_offset..bucket_end)?; + let mut occupied = 0_u64; + for bucket_index in 0..captured_buckets { + if Self::hash_table_bucket_occupied(occupancy_bytes, occupancy, bucket_index)? { + occupied = occupied.checked_add(1)?; } - TypeInfo::StructType { name, members, .. } => { - // Allow deeper nested structures now; cutoff managed by max_depth param - if current_depth > max_depth { - return format!(""); - } + } + if occupied > original_count + || (u64::try_from(captured_buckets).ok()? == capacity && occupied != original_count) + { + return None; + } - let mut result = format!("{name} {{ "); - let mut first = true; + Some(ParsedHashTablePayload { + original_count, + captured_buckets, + occupancy: occupancy_bytes, + buckets, + }) + } - for member in members.iter() { - if !first { - result.push_str(", "); - } - first = false; + fn hash_table_bucket_occupied( + occupancy_bytes: &[u8], + occupancy: HashTableOccupancy, + bucket_index: usize, + ) -> Option { + let width = usize::try_from(occupancy.byte_width()?).ok()?; + let start = bucket_index.checked_mul(width)?; + let end = start.checked_add(width)?; + let bytes = occupancy_bytes.get(start..end)?; + match occupancy { + HashTableOccupancy::ControlByteHighBitClear => { + Some(bytes.first().copied()? & 0x80 == 0) + } + HashTableOccupancy::NonZeroWord { .. } => Some(bytes.iter().any(|byte| *byte != 0)), + } + } - let offset = member.offset as usize; - // Prefer explicit BitfieldType on member_type, else use legacy member.bit_* fields - if let TypeInfo::BitfieldType { - underlying_type, - bit_offset, - bit_size, - } = &member.member_type - { - let u_size = underlying_type.size() as usize; - if offset + u_size <= data.len() && *bit_size > 0 && *bit_size <= 64 { - let raw = &data[offset..offset + u_size]; - let val_u64 = - Self::extract_bits_le(raw, *bit_offset as u32, *bit_size as u32); - let formatted_value = Self::format_bitfield_value( - val_u64, - underlying_type, - *bit_size as u32, - ); - result.push_str(&format!("{}: {}", member.name, formatted_value)); - } else { - result.push_str(&format!("{}: ", member.name)); - } - } else if let (Some(bit_size), maybe_bit_offset) = - (member.bit_size, member.bit_offset) - { - // Handle bitfield member formatting (up to 64 bits) - let bit_size = bit_size as u32; - let bit_offset = maybe_bit_offset.unwrap_or(0) as u32; - let bytes_needed = (bit_offset + bit_size).div_ceil(8) as usize; - if offset + bytes_needed <= data.len() && bit_size > 0 && bit_size <= 64 { - let raw = &data[offset..offset + bytes_needed]; - let val_u64 = Self::extract_bits_le(raw, bit_offset, bit_size); - let formatted_value = - Self::format_bitfield_value(val_u64, &member.member_type, bit_size); - result.push_str(&format!("{}: {}", member.name, formatted_value)); - } else { - result.push_str(&format!("{}: ", member.name)); - } - } else { - let member_size = member.member_type.size() as usize; - if offset + member_size <= data.len() { - let member_data = &data[offset..offset + member_size]; - let formatted_value = Self::format_data_with_type_info_impl( - member_data, - &member.member_type, - current_depth + 1, - max_depth, - ); - result.push_str(&format!("{}: {}", member.name, formatted_value)); - } else { - result.push_str(&format!("{}: ", member.name)); - } - } - } + fn hash_table_bucket<'a>( + payload: &ParsedHashTablePayload<'a>, + entry_stride: u64, + bucket_order: HashTableBucketOrder, + control_index: usize, + ) -> Option<&'a [u8]> { + let stride = usize::try_from(entry_stride).ok()?; + let bucket_index = match bucket_order { + HashTableBucketOrder::Forward => control_index, + HashTableBucketOrder::Reverse => { + payload.captured_buckets.checked_sub(control_index + 1)? + } + }; + let start = bucket_index.checked_mul(stride)?; + let end = start.checked_add(stride)?; + payload.buckets.get(start..end) + } - // No explicit elision; show all available members - result.push_str(" }"); - result + fn hash_table_occupied_bucket_bytes( + data: &[u8], + entry_stride: u64, + bucket_order: HashTableBucketOrder, + occupancy: HashTableOccupancy, + ) -> Option> { + let payload = Self::parse_hash_table_payload(data, entry_stride, occupancy)?; + let stride = usize::try_from(entry_stride).ok()?; + let output_capacity = payload.captured_buckets.checked_mul(stride)?; + let mut entries = Vec::with_capacity(output_capacity); + for control_index in 0..payload.captured_buckets { + if Self::hash_table_bucket_occupied(payload.occupancy, occupancy, control_index)? { + entries.extend_from_slice(Self::hash_table_bucket( + &payload, + entry_stride, + bucket_order, + control_index, + )?); } - TypeInfo::UnionType { name, members, .. } => { - if members.is_empty() { - format!("union {name} {{}}") - } else { - // For unions, show the first member interpretation - let first_member = &members[0]; - let member_size = first_member.member_type.size() as usize; - let member_data = if member_size <= data.len() { - &data[..member_size] - } else { - data - }; + } + Some(entries) + } - let formatted_value = Self::format_data_with_type_info_impl( - member_data, - &first_member.member_type, - current_depth + 1, - max_depth, - ); - format!( - "union {} {{ {} = {} }}", - name, first_member.name, formatted_value - ) + fn format_hash_table_field( + entry_data: &[u8], + entry_stride: u64, + field: &HashTableFieldPresentation, + ) -> Option { + let field_end = field.offset.checked_add(field.field_type.size())?; + if field_end > entry_stride { + return None; + } + let start = usize::try_from(field.offset).ok()?; + let end = usize::try_from(field_end).ok()?; + Some(Self::format_data_with_type_info_impl( + entry_data.get(start..end)?, + &field.field_type, + 1, + 32, + )) + } + + fn format_hash_table_payload( + data: &[u8], + entry_stride: u64, + bucket_order: HashTableBucketOrder, + occupancy: HashTableOccupancy, + entry: &HashTableEntryPresentation, + ) -> String { + let Some(payload) = Self::parse_hash_table_payload(data, entry_stride, occupancy) else { + return "".to_string(); + }; + let type_name = match entry { + HashTableEntryPresentation::Map { .. } => "HashMap", + HashTableEntryPresentation::Set { .. } => "HashSet", + }; + let mut result = format!("{type_name}(size={}) {{", payload.original_count); + let mut output_index = 0usize; + for control_index in 0..payload.captured_buckets { + let Some(occupied) = + Self::hash_table_bucket_occupied(payload.occupancy, occupancy, control_index) + else { + return "".to_string(); + }; + if !occupied { + continue; + } + let Some(entry_data) = + Self::hash_table_bucket(&payload, entry_stride, bucket_order, control_index) + else { + return "".to_string(); + }; + if output_index > 0 { + result.push_str(", "); + } + match entry { + HashTableEntryPresentation::Map { key, value } => { + let Some(key) = Self::format_hash_table_field(entry_data, entry_stride, key) + else { + return "".to_string(); + }; + let Some(value) = + Self::format_hash_table_field(entry_data, entry_stride, value) + else { + return "".to_string(); + }; + result.push_str(&key); + result.push_str(": "); + result.push_str(&value); + } + HashTableEntryPresentation::Set { value } => { + let Some(value) = + Self::format_hash_table_field(entry_data, entry_stride, value) + else { + return "".to_string(); + }; + result.push_str(&value); } } - TypeInfo::EnumType { - name, - base_type, - variants, - .. - } => { - let base_value = Self::format_data_with_type_info_impl( - data, - base_type, - current_depth + 1, - max_depth, - ); + output_index += 1; + } + result.push('}'); + result + } - // Try to find matching enum variant and print both type::variant and numeric value - if let Ok(int_val) = base_value.parse::() { - for variant in variants { - if variant.value == int_val { - return format!("{}::{}({})", name, variant.name, base_value); - } - } + fn btree_fields( + entry: &BTreeEntryPresentation, + ) -> (&BTreeFieldPresentation, Option<&BTreeFieldPresentation>) { + match entry { + BTreeEntryPresentation::Map { key, value } => (key, Some(value)), + BTreeEntryPresentation::Set { value } => (value, None), + } + } + + fn btree_record_layout( + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> Option<(usize, usize, usize)> { + let capacity = usize::try_from(node_capacity).ok()?; + if capacity == 0 { + return None; + } + let (key, value) = Self::btree_fields(entry); + let key_stride = usize::try_from(key.slot_stride).ok()?; + let key_bytes = capacity.checked_mul(key_stride)?; + let value_bytes = match value { + Some(field) => usize::try_from(field.slot_stride) + .ok()? + .checked_mul(capacity)?, + None => 0, + }; + let values_offset = BTREE_NODE_HEADER_SIZE.checked_add(key_bytes)?; + let record_size = values_offset.checked_add(value_bytes)?; + Some((capacity, values_offset, record_size)) + } + + fn validate_btree_field(field: &BTreeFieldPresentation) -> bool { + field + .value_offset + .checked_add(field.field_type.size()) + .is_some_and(|end| { + end <= field.slot_stride + || (field.slot_stride == 0 && field.value_offset == 0 && end == 0) + }) + } + + fn parse_btree_payload<'a>( + data: &'a [u8], + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> Option> { + let original_count = Self::payload_u64(data, 0)?; + let node_slots = Self::payload_u64(data, BTREE_NODE_SLOT_COUNT_OFFSET)?; + let captured_count = Self::payload_u64(data, BTREE_CAPTURED_ITEM_COUNT_OFFSET)?; + if captured_count > original_count { + return None; + } + let (key, value) = Self::btree_fields(entry); + if !Self::validate_btree_field(key) + || value.is_some_and(|field| !Self::validate_btree_field(field)) + { + return None; + } + let (capacity, values_offset, record_size) = + Self::btree_record_layout(node_capacity, entry)?; + let node_slots = usize::try_from(node_slots).ok()?; + let records_len = node_slots.checked_mul(record_size)?; + let records_end = BTREE_HEADER_SIZE.checked_add(records_len)?; + let records = data.get(BTREE_HEADER_SIZE..records_end)?; + let key_bytes = values_offset.checked_sub(BTREE_NODE_HEADER_SIZE)?; + let value_bytes = record_size.checked_sub(values_offset)?; + + let mut nodes = Vec::with_capacity(node_slots); + let mut addresses = std::collections::HashSet::with_capacity(node_slots); + let mut parsed_count = 0u64; + for slot in 0..node_slots { + let start = slot.checked_mul(record_size)?; + let record = records.get(start..start.checked_add(record_size)?)?; + let address = Self::payload_u64(record, 0)?; + if address == 0 { + nodes.push(None); + continue; + } + if !addresses.insert(address) { + return None; + } + let height = Self::payload_u64(record, BTREE_NODE_HEIGHT_OFFSET)?; + let length = Self::payload_u64(record, BTREE_NODE_LENGTH_OFFSET)?; + let length = usize::try_from(length).ok()?; + if length > capacity { + return None; + } + parsed_count = parsed_count.checked_add(u64::try_from(length).ok()?)?; + let keys_end = BTREE_NODE_HEADER_SIZE.checked_add(key_bytes)?; + let keys = record.get(BTREE_NODE_HEADER_SIZE..keys_end)?; + let values = match value { + Some(_) => { + Some(record.get(values_offset..values_offset.checked_add(value_bytes)?)?) } + None => None, + }; + nodes.push(Some(ParsedBTreeNode { + height, + length, + keys, + values, + })); + } + let root_presence_valid = match (original_count, captured_count) { + (0, _) => nodes.iter().all(Option::is_none), + (_, 0) => nodes.iter().all(Option::is_none), + (_, _) => nodes.first().is_some_and(Option::is_some), + }; + if parsed_count != captured_count || !root_presence_valid { + return None; + } - // No variant matched; still print type name with raw value - format!("{name}({base_value})") + let edge_count = capacity.checked_add(1)?; + for slot in 1..nodes.len() { + let Some(node) = &nodes[slot] else { + continue; + }; + let parent_slot = (slot - 1) / edge_count; + let parent_edge = (slot - 1) % edge_count; + let Some(Some(parent)) = nodes.get(parent_slot) else { + return None; + }; + if parent.height == 0 + || parent_edge > parent.length + || node.height.checked_add(1) != Some(parent.height) + { + return None; } - TypeInfo::TypedefType { - name, - underlying_type, - .. - } => { - // Reuse aggregate formatters by substituting display name - match &**underlying_type { - TypeInfo::StructType { size, members, .. } => { - let alias_struct = TypeInfo::StructType { - name: name.clone(), - size: *size, - members: members.clone(), - }; - Self::format_data_with_type_info_impl( - data, - &alias_struct, - current_depth, - max_depth, - ) - } - TypeInfo::UnionType { size, members, .. } => { - let alias_union = TypeInfo::UnionType { - name: name.clone(), - size: *size, - members: members.clone(), - }; - Self::format_data_with_type_info_impl( - data, - &alias_union, - current_depth, - max_depth, - ) - } - _ => { - let underlying_formatted = Self::format_data_with_type_info_impl( - data, - underlying_type, - current_depth, - max_depth, - ); - format!("{name}({underlying_formatted})") - } + } + for (slot, node) in nodes.iter().enumerate() { + let Some(node) = node else { + continue; + }; + if node.height == 0 { + continue; + } + for edge in 0..=node.length { + let child = slot + .checked_mul(edge_count)? + .checked_add(1)? + .checked_add(edge)?; + if child < nodes.len() && nodes[child].is_none() { + return None; + } + if captured_count == original_count && child >= nodes.len() { + return None; } } - TypeInfo::QualifiedType { - underlying_type, .. - } => Self::format_data_with_type_info_impl( - data, - underlying_type, - current_depth, - max_depth, - ), - TypeInfo::FunctionType { .. } => { - if data.len() >= 8 { - let addr = u64::from_le_bytes([ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]); - format!("") - } else { - "".to_string() + } + + Some(ParsedBTreePayload { + original_count, + captured_count, + edge_count, + nodes, + }) + } + + fn btree_field_bytes<'a>( + slots: &'a [u8], + index: usize, + field: &BTreeFieldPresentation, + ) -> Option<&'a [u8]> { + let stride = usize::try_from(field.slot_stride).ok()?; + let value_offset = usize::try_from(field.value_offset).ok()?; + let value_size = usize::try_from(field.field_type.size()).ok()?; + let start = index.checked_mul(stride)?.checked_add(value_offset)?; + slots.get(start..start.checked_add(value_size)?) + } + + fn collect_btree_entries<'a>( + payload: &'a ParsedBTreePayload<'a>, + entry: &'a BTreeEntryPresentation, + node_index: usize, + output: &mut Vec>, + ) -> Option<()> { + let node = payload.nodes.get(node_index)?.as_ref()?; + let (key, value) = Self::btree_fields(entry); + for index in 0..node.length { + if node.height > 0 { + let child = node_index + .checked_mul(payload.edge_count)? + .checked_add(1)? + .checked_add(index)?; + if payload.nodes.get(child).is_some_and(Option::is_some) { + Self::collect_btree_entries(payload, entry, child, output)?; } } - TypeInfo::UnknownType { name } => { - format!("", data.len()) + let key_data = Self::btree_field_bytes(node.keys, index, key)?; + let value_data = match (value, node.values) { + (Some(field), Some(values)) => Some(Self::btree_field_bytes(values, index, field)?), + (None, None) => None, + _ => return None, + }; + output.push((key_data, value_data)); + } + if node.height > 0 { + let child = node_index + .checked_mul(payload.edge_count)? + .checked_add(1)? + .checked_add(node.length)?; + if payload.nodes.get(child).is_some_and(Option::is_some) { + Self::collect_btree_entries(payload, entry, child, output)?; } - TypeInfo::OptimizedOut { .. } => "".to_string(), } + Some(()) } - /// Format base type data using DWARF encoding information - fn format_base_type_data(data: &[u8], size: u64, encoding: u16) -> String { - if encoding == gimli::constants::DW_ATE_boolean.0 as u16 { - if data.is_empty() { - "".to_string() - } else { - (data[0] != 0).to_string() + fn btree_entries<'a>( + payload: &'a ParsedBTreePayload<'a>, + entry: &'a BTreeEntryPresentation, + ) -> Option>> { + let mut entries = Vec::with_capacity(usize::try_from(payload.captured_count).ok()?); + if payload.captured_count > 0 { + Self::collect_btree_entries(payload, entry, 0, &mut entries)?; + } + (u64::try_from(entries.len()).ok()? == payload.captured_count).then_some(entries) + } + + fn btree_value_bytes( + data: &[u8], + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> Option> { + let payload = Self::parse_btree_payload(data, node_capacity, entry)?; + let entries = Self::btree_entries(&payload, entry)?; + let mut values = Vec::new(); + for (key, value) in entries { + values.extend_from_slice(key); + if let Some(value) = value { + values.extend_from_slice(value); } - } else if encoding == gimli::constants::DW_ATE_float.0 as u16 { - match size { - 4 => { - if data.len() >= 4 { - let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]]; - f32::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - 8 => { - if data.len() >= 8 { - let bytes: [u8; 8] = [ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]; - f64::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - _ => format!(""), + } + Some(values) + } + + fn format_btree_payload( + data: &[u8], + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> String { + let Some(payload) = Self::parse_btree_payload(data, node_capacity, entry) else { + return "".to_string(); + }; + let Some(entries) = Self::btree_entries(&payload, entry) else { + return "".to_string(); + }; + let (key, value) = Self::btree_fields(entry); + let type_name = match entry { + BTreeEntryPresentation::Map { .. } => "BTreeMap", + BTreeEntryPresentation::Set { .. } => "BTreeSet", + }; + let mut result = format!("{type_name}(size={}) {{", payload.original_count); + for (index, (key_data, value_data)) in entries.into_iter().enumerate() { + if index > 0 { + result.push_str(", "); } - } else if encoding == gimli::constants::DW_ATE_signed.0 as u16 - || encoding == gimli::constants::DW_ATE_signed_char.0 as u16 - { - match size { - 1 => { - if !data.is_empty() { - (data[0] as i8).to_string() - } else { - "".to_string() - } - } - 2 => { - if data.len() >= 2 { - let bytes: [u8; 2] = [data[0], data[1]]; - i16::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - 4 => { - if data.len() >= 4 { - let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]]; - i32::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - 8 => { - if data.len() >= 8 { - let bytes: [u8; 8] = [ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]; - i64::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - _ => format!(""), + let formatted_key = + Self::format_data_with_type_info_impl(key_data, &key.field_type, 1, 32); + result.push_str(&formatted_key); + if let (Some(field), Some(value_data)) = (value, value_data) { + result.push_str(": "); + result.push_str(&Self::format_data_with_type_info_impl( + value_data, + &field.field_type, + 1, + 32, + )); } - } else if encoding == gimli::constants::DW_ATE_unsigned.0 as u16 - || encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16 - { - match size { - 1 => { - if !data.is_empty() { - data[0].to_string() - } else { - "".to_string() - } - } - 2 => { - if data.len() >= 2 { - let bytes: [u8; 2] = [data[0], data[1]]; - u16::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - 4 => { - if data.len() >= 4 { - let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]]; - u32::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - 8 => { - if data.len() >= 8 { - let bytes: [u8; 8] = [ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]; - u64::from_le_bytes(bytes).to_string() - } else { - "".to_string() - } - } - _ => format!(""), + } + result.push('}'); + result + } + + fn parse_sequence_payload(data: &[u8], element_stride: u64) -> Option<(u64, u64, &[u8])> { + let original_count = u64::from_le_bytes(data.get(..8)?.try_into().ok()?); + let captured_count = u64::from_le_bytes( + data.get(INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET..INDIRECT_SEQUENCE_HEADER_SIZE)? + .try_into() + .ok()?, + ); + if captured_count > original_count { + return None; + } + let stride = usize::try_from(element_stride).ok()?; + let captured = usize::try_from(captured_count).ok()?; + let byte_len = captured.checked_mul(stride)?; + let payload = data.get(INDIRECT_SEQUENCE_HEADER_SIZE..)?; + Some((original_count, captured_count, payload.get(..byte_len)?)) + } + + fn format_sequence_payload( + data: &[u8], + element_type: &TypeInfo, + element_stride: u64, + ) -> String { + if element_type.size() != element_stride { + return "".to_string(); + } + let Some((_, captured_count, payload)) = Self::parse_sequence_payload(data, element_stride) + else { + return "".to_string(); + }; + let Ok(captured_count) = usize::try_from(captured_count) else { + return "".to_string(); + }; + let Ok(stride) = usize::try_from(element_stride) else { + return "".to_string(); + }; + + let mut result = String::from("["); + for index in 0..captured_count { + if index > 0 { + result.push_str(", "); } - } else { - // Handle char-like 1-byte integers as characters (signed or unsigned) - if (encoding == gimli::constants::DW_ATE_signed_char.0 as u16 - || encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16) - && size == 1 - { - if !data.is_empty() { - if data[0] >= 32 && data[0] <= 126 { - format!("'{}'", data[0] as char) - } else { - format!("'\\x{:02x}'", data[0]) - } - } else { - "".to_string() - } + let start = index * stride; + let element_data = &payload[start..start + stride]; + if stride == 0 && element_type.type_name() == "()" { + result.push_str("()"); } else { - // Fallback for unknown encodings - format!("") + result.push_str(&Self::format_data_with_type_info_impl( + element_data, + element_type, + 1, + 32, + )); } } + result.push(']'); + result } - /// Determine if a type is a single-byte character type (signed/unsigned char) - fn is_char_byte_type(t: &TypeInfo) -> bool { - match t { - TypeInfo::BaseType { size, encoding, .. } => { - *size == 1 - && (*encoding == gimli::constants::DW_ATE_signed_char.0 as u16 - || *encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16 - || *encoding == gimli::constants::DW_ATE_unsigned.0 as u16 - || *encoding == gimli::constants::DW_ATE_signed.0 as u16) + fn discriminant_type_is_unsigned(type_info: &TypeInfo) -> bool { + match type_info { + TypeInfo::BaseType { encoding, .. } => { + *encoding == gimli::constants::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16 } - TypeInfo::TypedefType { - underlying_type, .. + TypeInfo::EnumType { base_type, .. } + | TypeInfo::ScopedEnumType { base_type, .. } + | TypeInfo::TypedefType { + underlying_type: base_type, + .. } | TypeInfo::QualifiedType { - underlying_type, .. - } => Self::is_char_byte_type(underlying_type), + underlying_type: base_type, + .. + } + | TypeInfo::BitfieldType { + underlying_type: base_type, + .. + } => Self::discriminant_type_is_unsigned(base_type), _ => false, } } - /// Format a char array as a UTF-8-ish escaped string (best-effort) - fn format_char_array_as_string(data: &[u8], element_count: &Option) -> String { - let max_len = element_count.map(|c| c as usize).unwrap_or(data.len()); - let mut s = String::new(); - s.push('"'); - let mut i = 0usize; - while i < data.len() && i < max_len { - let b = data[i]; - if b == 0 { - break; // C-string termination - } - match b { - b'"' => s.push_str("\\\""), - b'\\' => s.push_str("\\\\"), - 0x20..=0x7E => s.push(b as char), - _ => s.push_str(&format!("\\x{b:02x}")), + fn discriminant_storage_type(type_info: &TypeInfo) -> &TypeInfo { + match type_info { + TypeInfo::EnumType { base_type, .. } + | TypeInfo::ScopedEnumType { base_type, .. } + | TypeInfo::TypedefType { + underlying_type: base_type, + .. } - i += 1; + | TypeInfo::QualifiedType { + underlying_type: base_type, + .. + } => Self::discriminant_storage_type(base_type), + _ => type_info, } - s.push('"'); - s } - /// Extract bits from a little-endian byte slice, starting at bit_offset, with length bit_size (<=64) - fn extract_bits_le(raw: &[u8], bit_offset: u32, bit_size: u32) -> u64 { - // Assemble up to 8 bytes into a u64 (little-endian) - let mut word: u64 = 0; - let take = std::cmp::min(8, raw.len()); - for (i, byte) in raw.iter().take(take).enumerate() { - word |= (*byte as u64) << (8 * i); + fn decode_discriminant_value( + data: &[u8], + type_info: &TypeInfo, + ) -> Option { + let unsigned = Self::discriminant_type_is_unsigned(type_info); + let storage_type = Self::discriminant_storage_type(type_info); + + if let TypeInfo::BitfieldType { + underlying_type, + bit_offset, + bit_size, + } = storage_type + { + if *bit_size == 0 || *bit_size > 64 { + return None; + } + let storage_size = usize::try_from(underlying_type.size()).ok()?; + let raw = data.get(..storage_size)?; + let value = Self::extract_bits_le(raw, u32::from(*bit_offset), u32::from(*bit_size)); + return if unsigned { + Some(crate::DiscriminantValue::Unsigned(value)) + } else { + let shift = 64 - u32::from(*bit_size); + Some(crate::DiscriminantValue::Signed( + ((value << shift) as i64) >> shift, + )) + }; } - let shifted = word >> bit_offset; - let mask: u64 = if bit_size == 64 { - u64::MAX + + if unsigned { + Self::decode_unsigned_integer(data, storage_type) + .and_then(|value| u64::try_from(value).ok()) + .map(crate::DiscriminantValue::Unsigned) } else { - (1u64 << bit_size) - 1 + Self::decode_signed_integer(data, storage_type) + .and_then(|value| i64::try_from(value).ok()) + .map(crate::DiscriminantValue::Signed) + } + } + + fn decode_variant_discriminant( + data: &[u8], + member: &crate::StructMember, + ) -> Option { + let offset = usize::try_from(member.offset).ok()?; + Self::decode_discriminant_value(data.get(offset..)?, &member.member_type) + } + + fn discriminant_in_range( + value: crate::DiscriminantValue, + range: &crate::DiscriminantRange, + ) -> bool { + match (value, range.start, range.end) { + ( + crate::DiscriminantValue::Signed(value), + crate::DiscriminantValue::Signed(start), + crate::DiscriminantValue::Signed(end), + ) => start <= value && value <= end, + ( + crate::DiscriminantValue::Unsigned(value), + crate::DiscriminantValue::Unsigned(start), + crate::DiscriminantValue::Unsigned(end), + ) => start <= value && value <= end, + _ => false, + } + } + + fn active_variant<'a>( + data: &[u8], + part: &'a crate::VariantPart, + ) -> Option<&'a crate::VariantCase> { + let default = part + .variants + .iter() + .find(|variant| matches!(variant.selector, crate::VariantSelector::Default)); + let Some(discriminant) = &part.discriminant else { + return default.or_else(|| part.variants.first()); }; - shifted & mask + let value = Self::decode_variant_discriminant(data, discriminant)?; + + part.variants + .iter() + .find(|variant| match &variant.selector { + crate::VariantSelector::Default => false, + crate::VariantSelector::Ranges(ranges) => ranges + .iter() + .any(|range| Self::discriminant_in_range(value, range)), + }) + .or(default) } - /// Format bitfield value according to the member's TypeInfo (basic support) - fn format_bitfield_value(val: u64, ty: &TypeInfo, bit_size: u32) -> String { - // Bool by encoding - if let TypeInfo::BaseType { encoding, .. } = ty { - if *encoding == gimli::constants::DW_ATE_boolean.0 as u16 { - return if val != 0 { - "true".to_string() - } else { - "false".to_string() - }; + fn member_data<'a>(data: &'a [u8], member: &crate::StructMember) -> Option<&'a [u8]> { + let offset = usize::try_from(member.offset).ok()?; + let size = usize::try_from(member.member_type.size()).ok()?; + data.get(offset..offset.checked_add(size)?) + } + + fn format_variant_member( + data: &[u8], + enum_name: &str, + member: &crate::StructMember, + payload_presentation: crate::VariantPayloadPresentation, + current_depth: usize, + max_depth: usize, + ) -> String { + let Some(member_data) = Self::member_data(data, member) else { + return format!("{enum_name}::{}()", member.name); + }; + + if let TypeInfo::StructType { + members: payload_members, + .. + } = member.member_type.underlying_type() + { + if payload_members.is_empty() + || payload_presentation == crate::VariantPayloadPresentation::Unit + { + return format!("{enum_name}::{}", member.name); } - } - // Enum mapping - if let TypeInfo::EnumType { variants, .. } = ty { - let sval = val as i64; // interpret as non-negative; signed variants must match exact value - for v in variants { - if v.value == sval { - return v.name.clone(); + let positional = payload_presentation == crate::VariantPayloadPresentation::Tuple; + let mut values = Vec::with_capacity(payload_members.len()); + for field in payload_members { + let value = Self::member_data(member_data, field) + .map(|field_data| { + Self::format_data_with_type_info_impl( + field_data, + &field.member_type, + current_depth + 1, + max_depth, + ) + }) + .unwrap_or_else(|| "".to_string()); + if positional { + values.push(value); + } else { + values.push(format!("{}: {value}", field.name)); } } + + if positional { + return format!("{enum_name}::{}({})", member.name, values.join(", ")); + } + return format!("{enum_name}::{} {{ {} }}", member.name, values.join(", ")); } - // Signed extension if base type is signed - let is_signed = ty.is_signed_int(); - if is_signed && bit_size > 0 && bit_size <= 64 { - let sign_bit = 1u64 << (bit_size - 1); - let signed_val: i64 = if (val & sign_bit) != 0 { - // negative value, sign-extend - let ext_mask = (!0u64) << bit_size; - (val | ext_mask) as i64 - } else { - val as i64 - }; - return signed_val.to_string(); + if payload_presentation == crate::VariantPayloadPresentation::Unit { + return format!("{enum_name}::{}", member.name); } - // Default: unsigned decimal - val.to_string() + let value = Self::format_data_with_type_info_impl( + member_data, + &member.member_type, + current_depth + 1, + max_depth, + ); + format!("{enum_name}::{}({value})", member.name) } -} -#[cfg(test)] -mod tests { - use super::*; + fn format_variant_type( + data: &[u8], + name: &str, + members: &[crate::StructMember], + variant_parts: &[crate::VariantPart], + current_depth: usize, + max_depth: usize, + ) -> String { + let mut active_members = Vec::new(); + Self::collect_active_variant_members(data, variant_parts, &mut active_members); - #[test] - fn test_apply_format_basic() { - let fmt = "pid: {}, name: {}"; - let rendered: Vec = vec!["42".to_string(), "hello".to_string()]; - let result = FormatPrinter::apply_format_strings(fmt, &rendered); - assert_eq!(result, "pid: 42, name: hello"); + if members.is_empty() && active_members.len() == 1 { + let (member, payload_presentation) = active_members[0]; + return Self::format_variant_member( + data, + name, + member, + payload_presentation, + current_depth, + max_depth, + ); + } + + let mut fields = Vec::new(); + for member in members { + let value = Self::member_data(data, member) + .map(|member_data| { + Self::format_data_with_type_info_impl( + member_data, + &member.member_type, + current_depth + 1, + max_depth, + ) + }) + .unwrap_or_else(|| "".to_string()); + fields.push(format!("{}: {value}", member.name)); + } + for (member, _) in active_members { + let value = Self::member_data(data, member) + .map(|member_data| { + Self::format_data_with_type_info_impl( + member_data, + &member.member_type, + current_depth + 1, + max_depth, + ) + }) + .unwrap_or_else(|| "".to_string()); + fields.push(format!("{}: {value}", member.name)); + } + + if fields.is_empty() { + format!("{name} {{ }}") + } else { + format!("{name} {{ {} }}", fields.join(", ")) + } } - #[test] - fn test_apply_format_escape_sequences() { - let rendered: Vec = vec!["123".to_string()]; - let result = - FormatPrinter::apply_format_strings("use {{}} for braces, value: {}", &rendered); - assert_eq!(result, "use {} for braces, value: 123"); + fn collect_active_variant_members<'a>( + data: &[u8], + parts: &'a [crate::VariantPart], + members: &mut Vec<(&'a crate::StructMember, crate::VariantPayloadPresentation)>, + ) { + for part in parts { + let Some(active) = Self::active_variant(data, part) else { + continue; + }; + members.extend( + active + .members + .iter() + .map(|member| (member, active.payload_presentation)), + ); + Self::collect_active_variant_members(data, &active.variant_parts, members); + } + } + + /// Internal implementation with depth control for recursion + fn format_data_with_type_info_impl( + data: &[u8], + type_info: &TypeInfo, + current_depth: usize, + max_depth: usize, + ) -> String { + if current_depth > max_depth { + return "".to_string(); + } + + match type_info { + TypeInfo::BaseType { name, size: 0, .. } if name == "()" => "()".to_string(), + TypeInfo::BaseType { size, encoding, .. } => { + Self::format_base_type_data(data, *size, *encoding) + } + TypeInfo::BitfieldType { + underlying_type, + bit_offset, + bit_size, + } => { + let u_size = underlying_type.size() as usize; + if data.len() < u_size || *bit_size == 0 { + return "".to_string(); + } + let val = + Self::extract_bits_le(&data[..u_size], *bit_offset as u32, *bit_size as u32); + Self::format_bitfield_value(val, underlying_type, *bit_size as u32) + } + TypeInfo::PointerType { target_type, size } => { + let Ok(pointer_size) = usize::try_from(*size) else { + return "".to_string(); + }; + if pointer_size == 0 || pointer_size > 8 || data.len() < pointer_size { + "".to_string() + } else { + let mut address_bytes = [0u8; 8]; + address_bytes[..pointer_size].copy_from_slice(&data[..pointer_size]); + let addr = u64::from_le_bytes(address_bytes); + let ty = format!("{}*", target_type.type_name()); + if addr == 0 { + format!("NULL ({ty})") + } else { + format!("0x{addr:x} ({ty})") + } + } + } + TypeInfo::ArrayType { + element_type, + element_count, + .. + } => { + // Special-case: char arrays -> print as string + if Self::is_char_byte_type(element_type) { + return Self::format_char_array_as_string(data, element_count); + } + let elem_size = element_type.size() as usize; + if elem_size == 0 { + return "".to_string(); + } + + let count = element_count.unwrap_or(data.len() as u64 / elem_size as u64); + let actual_count = std::cmp::min(count, data.len() as u64 / elem_size as u64); + + if actual_count == 0 { + return "[]".to_string(); + } + + let mut result = String::from("["); + for i in 0..actual_count { + if i > 0 { + result.push_str(", "); + } + + let start = i as usize * elem_size; + let end = std::cmp::min(start + elem_size, data.len()); + let elem_data = &data[start..end]; + + let formatted_elem = Self::format_data_with_type_info_impl( + elem_data, + element_type, + current_depth + 1, + max_depth, + ); + result.push_str(&formatted_elem); + } + result.push(']'); + result + } + TypeInfo::StructType { name, members, .. } => { + // Allow deeper nested structures now; cutoff managed by max_depth param + if current_depth > max_depth { + return format!(""); + } + if members.is_empty() && type_info.size() == 0 && name == "()" { + return "()".to_string(); + } + + let mut result = format!("{name} {{ "); + let mut first = true; + + for member in members.iter() { + if !first { + result.push_str(", "); + } + first = false; + + let offset = member.offset as usize; + // Prefer explicit BitfieldType on member_type, else use legacy member.bit_* fields + if let TypeInfo::BitfieldType { + underlying_type, + bit_offset, + bit_size, + } = &member.member_type + { + let u_size = underlying_type.size() as usize; + if offset + u_size <= data.len() && *bit_size > 0 && *bit_size <= 64 { + let raw = &data[offset..offset + u_size]; + let val_u64 = + Self::extract_bits_le(raw, *bit_offset as u32, *bit_size as u32); + let formatted_value = Self::format_bitfield_value( + val_u64, + underlying_type, + *bit_size as u32, + ); + result.push_str(&format!("{}: {}", member.name, formatted_value)); + } else { + result.push_str(&format!("{}: ", member.name)); + } + } else if let (Some(bit_size), maybe_bit_offset) = + (member.bit_size, member.bit_offset) + { + // Handle bitfield member formatting (up to 64 bits) + let bit_size = bit_size as u32; + let bit_offset = maybe_bit_offset.unwrap_or(0) as u32; + let bytes_needed = (bit_offset + bit_size).div_ceil(8) as usize; + if offset + bytes_needed <= data.len() && bit_size > 0 && bit_size <= 64 { + let raw = &data[offset..offset + bytes_needed]; + let val_u64 = Self::extract_bits_le(raw, bit_offset, bit_size); + let formatted_value = + Self::format_bitfield_value(val_u64, &member.member_type, bit_size); + result.push_str(&format!("{}: {}", member.name, formatted_value)); + } else { + result.push_str(&format!("{}: ", member.name)); + } + } else { + let member_size = member.member_type.size() as usize; + if offset + member_size <= data.len() { + let member_data = &data[offset..offset + member_size]; + let formatted_value = Self::format_data_with_type_info_impl( + member_data, + &member.member_type, + current_depth + 1, + max_depth, + ); + result.push_str(&format!("{}: {}", member.name, formatted_value)); + } else { + result.push_str(&format!("{}: ", member.name)); + } + } + } + + // No explicit elision; show all available members + result.push_str(" }"); + result + } + TypeInfo::UnionType { name, members, .. } => { + if members.is_empty() { + format!("union {name} {{}}") + } else { + // For unions, show the first member interpretation + let first_member = &members[0]; + let member_size = first_member.member_type.size() as usize; + let member_data = if member_size <= data.len() { + &data[..member_size] + } else { + data + }; + + let formatted_value = Self::format_data_with_type_info_impl( + member_data, + &first_member.member_type, + current_depth + 1, + max_depth, + ); + format!( + "union {} {{ {} = {} }}", + name, first_member.name, formatted_value + ) + } + } + TypeInfo::EnumType { + name, + base_type, + variants, + .. + } => { + let base_value = Self::format_data_with_type_info_impl( + data, + base_type, + current_depth + 1, + max_depth, + ); + + // Try to find matching enum variant and print both type::variant and numeric value + if let Ok(int_val) = base_value.parse::() { + for variant in variants { + if variant.value == int_val { + return format!("{}::{}({})", name, variant.name, base_value); + } + } + } + + // No variant matched; still print type name with raw value + format!("{name}({base_value})") + } + TypeInfo::ScopedEnumType { + name, + base_type, + variants, + .. + } => { + let discriminant = Self::decode_discriminant_value(data, base_type); + let base_value = Self::format_data_with_type_info_impl( + data, + base_type, + current_depth + 1, + max_depth, + ); + if let Some(variant) = discriminant.and_then(|discriminant| { + variants + .iter() + .find(|variant| variant.value == discriminant) + }) { + return format!("{}::{}", name, variant.name); + } + format!("{name}({base_value})") + } + TypeInfo::VariantType { + name, + members, + variant_parts, + .. + } => Self::format_variant_type( + data, + name, + members, + variant_parts, + current_depth, + max_depth, + ), + TypeInfo::TypedefType { + name, + underlying_type, + .. + } => { + // Reuse aggregate formatters by substituting display name + match &**underlying_type { + TypeInfo::StructType { size, members, .. } => { + let alias_struct = TypeInfo::StructType { + name: name.clone(), + size: *size, + members: members.clone(), + }; + Self::format_data_with_type_info_impl( + data, + &alias_struct, + current_depth, + max_depth, + ) + } + TypeInfo::UnionType { size, members, .. } => { + let alias_union = TypeInfo::UnionType { + name: name.clone(), + size: *size, + members: members.clone(), + }; + Self::format_data_with_type_info_impl( + data, + &alias_union, + current_depth, + max_depth, + ) + } + TypeInfo::VariantType { + size, + members, + variant_parts, + .. + } => { + let alias_variant = TypeInfo::VariantType { + name: name.clone(), + size: *size, + members: members.clone(), + variant_parts: variant_parts.clone(), + }; + Self::format_data_with_type_info_impl( + data, + &alias_variant, + current_depth, + max_depth, + ) + } + TypeInfo::ScopedEnumType { + size, + base_type, + variants, + .. + } => { + let alias_enum = TypeInfo::ScopedEnumType { + name: name.clone(), + size: *size, + base_type: base_type.clone(), + variants: variants.clone(), + }; + Self::format_data_with_type_info_impl( + data, + &alias_enum, + current_depth, + max_depth, + ) + } + _ => { + let underlying_formatted = Self::format_data_with_type_info_impl( + data, + underlying_type, + current_depth, + max_depth, + ); + format!("{name}({underlying_formatted})") + } + } + } + TypeInfo::QualifiedType { + underlying_type, .. + } => Self::format_data_with_type_info_impl( + data, + underlying_type, + current_depth, + max_depth, + ), + TypeInfo::FunctionType { .. } => { + if data.len() >= 8 { + let addr = u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]); + format!("") + } else { + "".to_string() + } + } + TypeInfo::UnknownType { name } => { + format!("", data.len()) + } + TypeInfo::OptimizedOut { .. } => "".to_string(), + } + } + + /// Format base type data using DWARF encoding information + fn format_base_type_data(data: &[u8], size: u64, encoding: u16) -> String { + if encoding == gimli::constants::DW_ATE_boolean.0 as u16 { + if data.is_empty() { + "".to_string() + } else { + (data[0] != 0).to_string() + } + } else if encoding == gimli::constants::DW_ATE_float.0 as u16 { + match size { + 4 => { + if data.len() >= 4 { + let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]]; + f32::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 8 => { + if data.len() >= 8 { + let bytes: [u8; 8] = [ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]; + f64::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + _ => format!(""), + } + } else if encoding == gimli::constants::DW_ATE_signed.0 as u16 + || encoding == gimli::constants::DW_ATE_signed_char.0 as u16 + { + match size { + 1 => { + if !data.is_empty() { + (data[0] as i8).to_string() + } else { + "".to_string() + } + } + 2 => { + if data.len() >= 2 { + let bytes: [u8; 2] = [data[0], data[1]]; + i16::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 4 => { + if data.len() >= 4 { + let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]]; + i32::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 8 => { + if data.len() >= 8 { + let bytes: [u8; 8] = [ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]; + i64::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 16 => match data.get(..16).and_then(|bytes| bytes.try_into().ok()) { + Some(bytes) => i128::from_le_bytes(bytes).to_string(), + None => "".to_string(), + }, + _ => format!(""), + } + } else if encoding == gimli::constants::DW_ATE_unsigned.0 as u16 + || encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16 + { + match size { + 1 => { + if !data.is_empty() { + data[0].to_string() + } else { + "".to_string() + } + } + 2 => { + if data.len() >= 2 { + let bytes: [u8; 2] = [data[0], data[1]]; + u16::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 4 => { + if data.len() >= 4 { + let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]]; + u32::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 8 => { + if data.len() >= 8 { + let bytes: [u8; 8] = [ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]; + u64::from_le_bytes(bytes).to_string() + } else { + "".to_string() + } + } + 16 => match data.get(..16).and_then(|bytes| bytes.try_into().ok()) { + Some(bytes) => u128::from_le_bytes(bytes).to_string(), + None => "".to_string(), + }, + _ => format!(""), + } + } else { + // Handle char-like 1-byte integers as characters (signed or unsigned) + if (encoding == gimli::constants::DW_ATE_signed_char.0 as u16 + || encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16) + && size == 1 + { + if !data.is_empty() { + if data[0] >= 32 && data[0] <= 126 { + format!("'{}'", data[0] as char) + } else { + format!("'\\x{:02x}'", data[0]) + } + } else { + "".to_string() + } + } else { + // Fallback for unknown encodings + format!("") + } + } + } + + /// Determine if a type is a single-byte character type (signed/unsigned char) + fn is_char_byte_type(t: &TypeInfo) -> bool { + match t { + TypeInfo::BaseType { size, encoding, .. } => { + *size == 1 + && (*encoding == gimli::constants::DW_ATE_signed_char.0 as u16 + || *encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16 + || *encoding == gimli::constants::DW_ATE_unsigned.0 as u16 + || *encoding == gimli::constants::DW_ATE_signed.0 as u16) + } + TypeInfo::TypedefType { + underlying_type, .. + } + | TypeInfo::QualifiedType { + underlying_type, .. + } => Self::is_char_byte_type(underlying_type), + _ => false, + } + } + + /// Format a char array as a UTF-8-ish escaped string (best-effort) + fn format_char_array_as_string(data: &[u8], element_count: &Option) -> String { + let max_len = element_count.map(|c| c as usize).unwrap_or(data.len()); + let mut s = String::new(); + s.push('"'); + let mut i = 0usize; + while i < data.len() && i < max_len { + let b = data[i]; + if b == 0 { + break; // C-string termination + } + match b { + b'"' => s.push_str("\\\""), + b'\\' => s.push_str("\\\\"), + 0x20..=0x7E => s.push(b as char), + _ => s.push_str(&format!("\\x{b:02x}")), + } + i += 1; + } + s.push('"'); + s + } + + /// Extract bits from a little-endian byte slice, starting at bit_offset, with length bit_size (<=64) + fn extract_bits_le(raw: &[u8], bit_offset: u32, bit_size: u32) -> u64 { + // Assemble up to 8 bytes into a u64 (little-endian) + let mut word: u64 = 0; + let take = std::cmp::min(8, raw.len()); + for (i, byte) in raw.iter().take(take).enumerate() { + word |= (*byte as u64) << (8 * i); + } + let shifted = word >> bit_offset; + let mask: u64 = if bit_size == 64 { + u64::MAX + } else { + (1u64 << bit_size) - 1 + }; + shifted & mask + } + + /// Format bitfield value according to the member's TypeInfo (basic support) + fn format_bitfield_value(val: u64, ty: &TypeInfo, bit_size: u32) -> String { + // Bool by encoding + if let TypeInfo::BaseType { encoding, .. } = ty { + if *encoding == gimli::constants::DW_ATE_boolean.0 as u16 { + return if val != 0 { + "true".to_string() + } else { + "false".to_string() + }; + } + } + + // Enum mapping + if let TypeInfo::EnumType { variants, .. } = ty { + let value = val as i64; + if let Some(variant) = variants.iter().find(|variant| variant.value == value) { + return variant.name.clone(); + } + } + if let TypeInfo::ScopedEnumType { + base_type, + variants, + .. + } = ty + { + let value = if Self::discriminant_type_is_unsigned(base_type) { + crate::DiscriminantValue::Unsigned(val) + } else if bit_size > 0 && bit_size <= 64 { + let shift = 64 - bit_size; + crate::DiscriminantValue::Signed(((val << shift) as i64) >> shift) + } else { + crate::DiscriminantValue::Signed(val as i64) + }; + if let Some(variant) = variants.iter().find(|variant| variant.value == value) { + return variant.name.clone(); + } + } + + // Signed extension if base type is signed + let is_signed = ty.is_signed_int(); + if is_signed && bit_size > 0 && bit_size <= 64 { + let sign_bit = 1u64 << (bit_size - 1); + let signed_val: i64 = if (val & sign_bit) != 0 { + // negative value, sign-extend + let ext_mask = (!0u64) << bit_size; + (val | ext_mask) as i64 + } else { + val as i64 + }; + return signed_val.to_string(); + } + + // Default: unsigned decimal + val.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type BTreeMapNode<'a> = (usize, u64, u64, &'a [i32], &'a [u16]); + + #[test] + fn test_apply_format_basic() { + let fmt = "pid: {}, name: {}"; + let rendered: Vec = vec!["42".to_string(), "hello".to_string()]; + let result = FormatPrinter::apply_format_strings(fmt, &rendered); + assert_eq!(result, "pid: 42, name: hello"); + } + + #[test] + fn test_apply_format_escape_sequences() { + let rendered: Vec = vec!["123".to_string()]; + let result = + FormatPrinter::apply_format_strings("use {{}} for braces, value: {}", &rendered); + assert_eq!(result, "use {} for braces, value: 123"); + } + + #[test] + fn test_missing_arguments() { + let result = FormatPrinter::apply_format_strings("need arg: {}", &[]); + assert_eq!(result, "need arg: "); + } + + #[test] + fn test_format_print_data_with_trace_context() { + let mut trace_context = TraceContext::new(); + let format_index = trace_context + .add_string("Hello {}, you are {} years old!".to_string()) + .expect("add format string"); + let rendered: Vec = vec!["Alice".to_string(), "25".to_string()]; + let fmt = trace_context.get_string(format_index).unwrap(); + let result = FormatPrinter::apply_format_strings(fmt, &rendered); + assert_eq!(result, "Hello Alice, you are 25 years old!"); + } + + #[test] + fn test_format_complex_variable_struct() { + use crate::type_info::{StructMember, TypeInfo}; + + let mut trace_context = TraceContext::new(); + let var_name_idx = trace_context + .add_variable_name("person".to_string()) + .expect("add variable name"); + + let person_type = TypeInfo::StructType { + name: "Person".to_string(), + size: 36, + members: vec![ + StructMember { + name: "age".to_string(), + member_type: TypeInfo::BaseType { + name: "int".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }, + offset: 0, + bit_offset: None, + bit_size: None, + }, + StructMember { + name: "id".to_string(), + member_type: TypeInfo::BaseType { + name: "long".to_string(), + size: 8, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }, + offset: 4, + bit_offset: None, + bit_size: None, + }, + ], + }; + + let type_idx = trace_context + .add_type(person_type) + .expect("add person type"); + + // Data: age=25 (4 bytes) + id=12345 (8 bytes) + let data = vec![ + 25, 0, 0, 0, // age = 25 + 57, 48, 0, 0, 0, 0, 0, 0, // id = 12345 + ]; + + let result = FormatPrinter::format_complex_variable( + var_name_idx, + type_idx, + "", + &data, + &trace_context, + ); + + assert!(result.contains("person = Person")); + assert!(result.contains("age: 25")); + assert!(result.contains("id: 12345")); + } + + #[test] + fn test_complex_format_char_array() { + use crate::type_info::TypeInfo; + + let mut trace_context = TraceContext::new(); + let var_name_idx = trace_context + .add_variable_name("name".to_string()) + .expect("add variable name"); + // Define char array type: char name[16] + let char_type = TypeInfo::BaseType { + name: "char".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16, + }; + let arr_type = TypeInfo::ArrayType { + element_type: Box::new(char_type), + element_count: Some(16), + total_size: Some(16), + }; + let type_idx = trace_context.add_type(arr_type).expect("add array type"); + + // Data buffer with "Alice\0" and padding + let mut data = b"Alice\0".to_vec(); + data.resize(16, 0u8); + + let fmt_idx = trace_context + .add_string("{}".to_string()) + .expect("add format string"); + let complex_vars = vec![ParsedComplexVariable { + var_name_index: var_name_idx, + type_index: type_idx, + access_path: String::new(), + status: 0, + data, + }]; + + let result = + FormatPrinter::format_complex_print_data(fmt_idx, &complex_vars, &trace_context); + assert_eq!(result, "\"Alice\""); + } + + fn rust_str_type() -> TypeInfo { + TypeInfo::StructType { + name: "&str".to_string(), + size: 16, + members: Vec::new(), + } + } + + fn indirect_bytes_payload(original_len: u64, captured: &[u8]) -> Vec { + let mut data = original_len.to_le_bytes().to_vec(); + data.extend_from_slice(captured); + data + } + + fn indirect_sequence_payload( + original_count: u64, + captured_count: u64, + captured: &[u8], + ) -> Vec { + let mut data = original_count.to_le_bytes().to_vec(); + data.extend_from_slice(&captured_count.to_le_bytes()); + data.extend_from_slice(captured); + data + } + + fn hash_table_payload( + original_count: u64, + capacity: u64, + controls: &[u8], + buckets: &[u8], + ) -> Vec { + hash_table_payload_with_control_headroom(original_count, capacity, controls, buckets, 0) + } + + fn hash_table_payload_with_control_headroom( + original_count: u64, + capacity: u64, + controls: &[u8], + buckets: &[u8], + unused_control_bytes: usize, + ) -> Vec { + hash_table_payload_with_occupancy_headroom( + original_count, + capacity, + controls.len(), + controls, + buckets, + unused_control_bytes, + ) + } + + fn hash_table_payload_with_occupancy_headroom( + original_count: u64, + capacity: u64, + captured_buckets: usize, + occupancy: &[u8], + buckets: &[u8], + unused_occupancy_bytes: usize, + ) -> Vec { + let bucket_offset = HASH_TABLE_HEADER_SIZE + occupancy.len() + unused_occupancy_bytes; + let mut data = original_count.to_le_bytes().to_vec(); + data.extend_from_slice(&capacity.to_le_bytes()); + data.extend_from_slice(&(captured_buckets as u64).to_le_bytes()); + data.extend_from_slice(&(bucket_offset as u64).to_le_bytes()); + data.extend_from_slice(occupancy); + data.resize(bucket_offset, 0); + data.extend_from_slice(buckets); + data + } + + fn btree_map_presentation(node_capacity: u64) -> ValuePresentation { + ValuePresentation::BTree { + node_capacity, + entry: BTreeEntryPresentation::Map { + key: BTreeFieldPresentation { + slot_stride: 4, + value_offset: 0, + field_type: Box::new(signed_i32_type()), + }, + value: BTreeFieldPresentation { + slot_stride: 2, + value_offset: 0, + field_type: Box::new(unsigned_u16_type()), + }, + }, + } + } + + fn btree_set_presentation(node_capacity: u64) -> ValuePresentation { + ValuePresentation::BTree { + node_capacity, + entry: BTreeEntryPresentation::Set { + value: BTreeFieldPresentation { + slot_stride: 4, + value_offset: 0, + field_type: Box::new(signed_i32_type()), + }, + }, + } + } + + fn btree_map_payload( + original_count: u64, + captured_count: u64, + node_capacity: usize, + node_slots: usize, + nodes: &[BTreeMapNode<'_>], + ) -> Vec { + let keys_size = node_capacity * 4; + let values_offset = BTREE_NODE_HEADER_SIZE + keys_size; + let record_size = values_offset + node_capacity * 2; + let mut data = original_count.to_le_bytes().to_vec(); + data.extend_from_slice(&(node_slots as u64).to_le_bytes()); + data.extend_from_slice(&captured_count.to_le_bytes()); + data.resize(BTREE_HEADER_SIZE + node_slots * record_size, 0); + for (slot, address, height, keys, values) in nodes { + assert_eq!(keys.len(), values.len()); + assert!(keys.len() <= node_capacity); + let start = BTREE_HEADER_SIZE + slot * record_size; + data[start..start + 8].copy_from_slice(&address.to_le_bytes()); + data[start + 8..start + 16].copy_from_slice(&height.to_le_bytes()); + data[start + 16..start + 24].copy_from_slice(&(keys.len() as u64).to_le_bytes()); + for (index, key) in keys.iter().enumerate() { + let offset = start + BTREE_NODE_HEADER_SIZE + index * 4; + data[offset..offset + 4].copy_from_slice(&key.to_le_bytes()); + } + for (index, value) in values.iter().enumerate() { + let offset = start + values_offset + index * 2; + data[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); + } + } + data + } + + fn btree_set_payload( + original_count: u64, + captured_count: u64, + node_capacity: usize, + node_slots: usize, + nodes: &[(usize, u64, u64, &[i32])], + ) -> Vec { + let record_size = BTREE_NODE_HEADER_SIZE + node_capacity * 4; + let mut data = original_count.to_le_bytes().to_vec(); + data.extend_from_slice(&(node_slots as u64).to_le_bytes()); + data.extend_from_slice(&captured_count.to_le_bytes()); + data.resize(BTREE_HEADER_SIZE + node_slots * record_size, 0); + for (slot, address, height, values) in nodes { + assert!(values.len() <= node_capacity); + let start = BTREE_HEADER_SIZE + slot * record_size; + data[start..start + 8].copy_from_slice(&address.to_le_bytes()); + data[start + 8..start + 16].copy_from_slice(&height.to_le_bytes()); + data[start + 16..start + 24].copy_from_slice(&(values.len() as u64).to_le_bytes()); + for (index, value) in values.iter().enumerate() { + let offset = start + BTREE_NODE_HEADER_SIZE + index * 4; + data[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + } + data + } + + fn signed_i32_type() -> TypeInfo { + TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + } + } + + fn unsigned_u16_type() -> TypeInfo { + TypeInfo::BaseType { + name: "u16".to_string(), + size: 2, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + } + } + + fn test_member(name: &str, member_type: TypeInfo, offset: u64) -> crate::StructMember { + crate::StructMember { + name: name.to_string(), + member_type, + offset, + bit_offset: None, + bit_size: None, + } + } + + fn unsigned_selector(value: u64) -> crate::VariantSelector { + crate::VariantSelector::Ranges(vec![crate::DiscriminantRange { + start: crate::DiscriminantValue::Unsigned(value), + end: crate::DiscriminantValue::Unsigned(value), + }]) + } + + fn test_variant( + name: &str, + payload_members: Vec, + size: u64, + selector: crate::VariantSelector, + payload_presentation: crate::VariantPayloadPresentation, + ) -> crate::VariantCase { + crate::VariantCase { + selector, + members: vec![test_member( + name, + TypeInfo::StructType { + name: name.to_string(), + size, + members: payload_members, + }, + 0, + )], + variant_parts: Vec::new(), + payload_presentation, + } + } + + #[test] + fn formats_dwarf_variant_unit_tuple_and_struct_branches() { + let discriminant_type = TypeInfo::BaseType { + name: "u32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }; + let enum_type = TypeInfo::VariantType { + name: "CompatEnum".to_string(), + size: 8, + members: Vec::new(), + variant_parts: vec![crate::VariantPart { + discriminant: Some(test_member("", discriminant_type, 0)), + variants: vec![ + test_variant( + "Unit", + Vec::new(), + 8, + unsigned_selector(0), + crate::VariantPayloadPresentation::Unit, + ), + test_variant( + "Tuple", + vec![test_member("__0", signed_i32_type(), 4)], + 8, + unsigned_selector(1), + crate::VariantPayloadPresentation::Tuple, + ), + test_variant( + "Struct", + vec![test_member("value", signed_i32_type(), 4)], + 8, + unsigned_selector(2), + crate::VariantPayloadPresentation::Struct, + ), + ], + }], + }; + + let mut unit = 0_u32.to_le_bytes().to_vec(); + unit.extend_from_slice(&0_i32.to_le_bytes()); + let mut tuple = 1_u32.to_le_bytes().to_vec(); + tuple.extend_from_slice(&(-7_i32).to_le_bytes()); + let mut struct_value = 2_u32.to_le_bytes().to_vec(); + struct_value.extend_from_slice(&59_i32.to_le_bytes()); + + assert_eq!( + FormatPrinter::format_data_with_type_info(&unit, &enum_type), + "CompatEnum::Unit" + ); + assert_eq!( + FormatPrinter::format_data_with_type_info(&tuple, &enum_type), + "CompatEnum::Tuple(-7)" + ); + assert_eq!( + FormatPrinter::format_data_with_type_info(&struct_value, &enum_type), + "CompatEnum::Struct { value: 59 }" + ); + } + + #[test] + fn does_not_infer_tuple_variants_from_member_names() { + let enum_type = TypeInfo::VariantType { + name: "RawVariant".to_string(), + size: 4, + members: Vec::new(), + variant_parts: vec![crate::VariantPart { + discriminant: None, + variants: vec![test_variant( + "Value", + vec![test_member("__0", signed_i32_type(), 0)], + 4, + crate::VariantSelector::Default, + crate::VariantPayloadPresentation::Dwarf, + )], + }], + }; + + assert_eq!( + FormatPrinter::format_data_with_type_info(&(-7_i32).to_le_bytes(), &enum_type), + "RawVariant::Value { __0: -7 }" + ); + } + + #[test] + fn formats_default_dwarf_variant_for_niche_layout() { + let option_type = TypeInfo::VariantType { + name: "Option".to_string(), + size: 4, + members: Vec::new(), + variant_parts: vec![crate::VariantPart { + discriminant: Some(test_member( + "", + TypeInfo::BaseType { + name: "u32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }, + 0, + )), + variants: vec![ + test_variant( + "None", + Vec::new(), + 4, + unsigned_selector(0), + crate::VariantPayloadPresentation::Unit, + ), + test_variant( + "Some", + vec![test_member("__0", signed_i32_type(), 0)], + 4, + crate::VariantSelector::Default, + crate::VariantPayloadPresentation::Tuple, + ), + ], + }], + }; + + assert_eq!( + FormatPrinter::format_data_with_type_info(&0_i32.to_le_bytes(), &option_type), + "Option::None" + ); + assert_eq!( + FormatPrinter::format_data_with_type_info(&9_i32.to_le_bytes(), &option_type), + "Option::Some(9)" + ); + } + + #[test] + fn formats_signed_dwarf_discriminant_ranges() { + let range_type = TypeInfo::VariantType { + name: "RangeEnum".to_string(), + size: 1, + members: Vec::new(), + variant_parts: vec![crate::VariantPart { + discriminant: Some(test_member( + "", + TypeInfo::BaseType { + name: "i8".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }, + 0, + )), + variants: vec![ + test_variant( + "Negative", + Vec::new(), + 1, + crate::VariantSelector::Ranges(vec![crate::DiscriminantRange { + start: crate::DiscriminantValue::Signed(-3), + end: crate::DiscriminantValue::Signed(-1), + }]), + crate::VariantPayloadPresentation::Unit, + ), + test_variant( + "Other", + Vec::new(), + 1, + crate::VariantSelector::Default, + crate::VariantPayloadPresentation::Unit, + ), + ], + }], + }; + + assert_eq!( + FormatPrinter::format_data_with_type_info(&[0xfd], &range_type), + "RangeEnum::Negative" + ); + assert_eq!( + FormatPrinter::format_data_with_type_info(&[4], &range_type), + "RangeEnum::Other" + ); + } + + #[test] + fn formats_scoped_scalar_enum_without_numeric_suffix() { + let enum_type = TypeInfo::ScopedEnumType { + name: "CompatFieldless".to_string(), + size: 1, + base_type: Box::new(TypeInfo::BaseType { + name: "u8".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }), + variants: vec![ + crate::ScopedEnumVariant { + name: "First".to_string(), + value: crate::DiscriminantValue::Unsigned(0), + }, + crate::ScopedEnumVariant { + name: "Second".to_string(), + value: crate::DiscriminantValue::Unsigned(1), + }, + ], + }; + + assert_eq!( + FormatPrinter::format_data_with_type_info(&[1], &enum_type), + "CompatFieldless::Second" + ); + let alias = TypeInfo::TypedefType { + name: "CompatAlias".to_string(), + underlying_type: Box::new(enum_type), + }; + assert_eq!( + FormatPrinter::format_data_with_type_info(&[1], &alias), + "CompatAlias::Second" + ); + } + + #[test] + fn formats_scoped_enum_discriminants_with_dwarf_signedness() { + let unsigned_enum = TypeInfo::ScopedEnumType { + name: "UnsignedEnum".to_string(), + size: 8, + base_type: Box::new(TypeInfo::BaseType { + name: "u64".to_string(), + size: 8, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }), + variants: vec![crate::ScopedEnumVariant { + name: "High".to_string(), + value: crate::DiscriminantValue::Unsigned(1 << 63), + }], + }; + assert_eq!( + FormatPrinter::format_data_with_type_info(&(1u64 << 63).to_le_bytes(), &unsigned_enum), + "UnsignedEnum::High" + ); + + let signed_enum = TypeInfo::ScopedEnumType { + name: "SignedEnum".to_string(), + size: 1, + base_type: Box::new(TypeInfo::BaseType { + name: "i8".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }), + variants: vec![crate::ScopedEnumVariant { + name: "Negative".to_string(), + value: crate::DiscriminantValue::Signed(-1), + }], + }; + assert_eq!( + FormatPrinter::format_data_with_type_info(&[0xff], &signed_enum), + "SignedEnum::Negative" + ); + } + + #[test] + fn test_hash_table_presentation_filters_controls_and_projects_fields() { + let controls = [0xff, 0x12, 0x80, 0x01]; + let mut buckets = vec![0u8; 4 * 8]; + buckets[8..12].copy_from_slice(&(-7_i32).to_le_bytes()); + buckets[12..14].copy_from_slice(&13_u16.to_le_bytes()); + buckets[24..28].copy_from_slice(&29_i32.to_le_bytes()); + buckets[28..30].copy_from_slice(&17_u16.to_le_bytes()); + let data = hash_table_payload(2, 4, &controls, &buckets); + let presentation = ValuePresentation::HashTable { + entry_stride: 8, + bucket_order: HashTableBucketOrder::Forward, + occupancy: HashTableOccupancy::ControlByteHighBitClear, + entry: HashTableEntryPresentation::Map { + key: HashTableFieldPresentation { + offset: 0, + field_type: Box::new(signed_i32_type()), + }, + value: HashTableFieldPresentation { + offset: 4, + field_type: Box::new(unsigned_u16_type()), + }, + }, + }; + + assert_eq!( + FormatPrinter::format_data_with_presentation( + &data, + &TypeInfo::UnknownType { + name: "HashMap".to_string(), + }, + &presentation, + ), + "HashMap(size=2) {-7: 13, 29: 17}" + ); + } + + #[test] + fn test_legacy_hash_table_presentation_uses_nonzero_hash_words() { + let mut occupancy = Vec::new(); + for hash in [0_u64, 0x12, 0, 0x01] { + occupancy.extend_from_slice(&hash.to_le_bytes()); + } + let mut buckets = vec![0_u8; 4 * 8]; + buckets[8..12].copy_from_slice(&65_i32.to_le_bytes()); + buckets[12..14].copy_from_slice(&13_u16.to_le_bytes()); + buckets[24..28].copy_from_slice(&29_i32.to_le_bytes()); + buckets[28..30].copy_from_slice(&17_u16.to_le_bytes()); + let data = hash_table_payload_with_occupancy_headroom(2, 4, 4, &occupancy, &buckets, 0); + let presentation = ValuePresentation::HashTable { + entry_stride: 8, + bucket_order: HashTableBucketOrder::Forward, + occupancy: HashTableOccupancy::NonZeroWord { word_size: 8 }, + entry: HashTableEntryPresentation::Map { + key: HashTableFieldPresentation { + offset: 0, + field_type: Box::new(signed_i32_type()), + }, + value: HashTableFieldPresentation { + offset: 4, + field_type: Box::new(unsigned_u16_type()), + }, + }, + }; + + assert_eq!( + FormatPrinter::format_data_with_presentation( + &data, + &TypeInfo::UnknownType { + name: "HashMap".to_string(), + }, + &presentation, + ), + "HashMap(size=2) {65: 13, 29: 17}" + ); + + let mut trace_context = TraceContext::new(); + let format_index = trace_context.add_string("{:x}|{:s}".to_string()).unwrap(); + let type_index = trace_context + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "HashMap".to_string(), + }, + presentation, + ) + .unwrap(); + let name_index = trace_context.add_variable_name("map".to_string()).unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_index, + type_index, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data, + }; + assert_eq!( + FormatPrinter::format_complex_print_data( + format_index, + &[variable.clone(), variable], + &trace_context, + ), + "41 00 00 00 0d 00 00 00 1d 00 00 00 11 00 00 00|A" + ); + } + + #[test] + fn test_hash_table_presentation_handles_reverse_and_zst_buckets() { + let controls = [0x01, 0xff, 0x02, 0x80]; + let mut reverse_buckets = vec![0u8; 4 * 4]; + reverse_buckets[12..16].copy_from_slice(&5_i32.to_le_bytes()); + reverse_buckets[4..8].copy_from_slice(&(-9_i32).to_le_bytes()); + let reverse = hash_table_payload(2, 4, &controls, &reverse_buckets); + let set = ValuePresentation::HashTable { + entry_stride: 4, + bucket_order: HashTableBucketOrder::Reverse, + occupancy: HashTableOccupancy::ControlByteHighBitClear, + entry: HashTableEntryPresentation::Set { + value: HashTableFieldPresentation { + offset: 0, + field_type: Box::new(signed_i32_type()), + }, + }, + }; + assert_eq!( + FormatPrinter::format_data_with_presentation( + &reverse, + &TypeInfo::UnknownType { + name: "HashSet".to_string(), + }, + &set, + ), + "HashSet(size=2) {5, -9}" + ); + + let zst = hash_table_payload(1, 1, &[0x00], &[]); + let unit_set = ValuePresentation::HashTable { + entry_stride: 0, + bucket_order: HashTableBucketOrder::Reverse, + occupancy: HashTableOccupancy::ControlByteHighBitClear, + entry: HashTableEntryPresentation::Set { + value: HashTableFieldPresentation { + offset: 0, + field_type: Box::new(TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: 0, + }), + }, + }, + }; + assert_eq!( + FormatPrinter::format_data_with_presentation( + &zst, + &TypeInfo::UnknownType { + name: "HashSet".to_string(), + }, + &unit_set, + ), + "HashSet(size=1) {()}" + ); + } + + #[test] + fn test_hash_table_raw_specs_skip_unoccupied_buckets() { + let controls = [0x01, 0xff, 0x02, 0x80]; + let buckets = vec![0xde, 0xad, 0x42, 0x00, 0xbe, 0xef, 0x41, 0x00]; + let data = hash_table_payload(2, 4, &controls, &buckets); + + let presentation = ValuePresentation::HashTable { + entry_stride: 2, + bucket_order: HashTableBucketOrder::Reverse, + occupancy: HashTableOccupancy::ControlByteHighBitClear, + entry: HashTableEntryPresentation::Set { + value: HashTableFieldPresentation { + offset: 0, + field_type: Box::new(unsigned_u16_type()), + }, + }, + }; + let mut trace_context = TraceContext::new(); + let format_index = trace_context.add_string("{:x}|{:s}".to_string()).unwrap(); + let type_index = trace_context + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "HashSet".to_string(), + }, + presentation, + ) + .unwrap(); + let name_index = trace_context.add_variable_name("set".to_string()).unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_index, + type_index, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data, + }; + + assert_eq!( + FormatPrinter::format_complex_print_data( + format_index, + &[variable.clone(), variable], + &trace_context, + ), + "41 00 42 00|A" + ); + + let huge_count_format = trace_context.add_string("{:x}".to_string()).unwrap(); + let huge_count = ParsedComplexVariable { + var_name_index: name_index, + type_index, + access_path: String::new(), + status: VariableStatus::Truncated as u8, + data: hash_table_payload(u64::MAX, u64::MAX, &[0x01], &[0x34, 0x12]), + }; + assert_eq!( + FormatPrinter::format_complex_print_data( + huge_count_format, + &[huge_count], + &trace_context, + ), + "34 12 " + ); + } + + #[test] + fn test_hash_table_payload_validates_complete_occupancy_and_keeps_partial_capture() { + let presentation = ValuePresentation::HashTable { + entry_stride: 4, + bucket_order: HashTableBucketOrder::Forward, + occupancy: HashTableOccupancy::ControlByteHighBitClear, + entry: HashTableEntryPresentation::Set { + value: HashTableFieldPresentation { + offset: 0, + field_type: Box::new(signed_i32_type()), + }, + }, + }; + let mut buckets = vec![0_u8; 8]; + buckets[..4].copy_from_slice(&5_i32.to_le_bytes()); + + let invalid_complete = hash_table_payload(2, 2, &[0x01, 0xff], &buckets); + assert_eq!( + FormatPrinter::format_data_with_presentation( + &invalid_complete, + &TypeInfo::UnknownType { + name: "HashSet".to_string(), + }, + &presentation, + ), + "" + ); + + let partial = hash_table_payload_with_control_headroom(2, 4, &[0x01, 0xff], &buckets, 6); + let mut trace_context = TraceContext::new(); + let type_index = trace_context + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "HashSet".to_string(), + }, + presentation, + ) + .unwrap(); + let name_index = trace_context.add_variable_name("set".to_string()).unwrap(); + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_index, + type_index, + "", + &partial, + VariableStatus::Truncated as u8, + &trace_context, + ), + "set = HashSet(size=2) {5} " + ); + } + + #[test] + fn test_btree_map_presentation_formats_leaf_and_raw_entry_bytes() { + let presentation = btree_map_presentation(2); + let data = btree_map_payload(2, 2, 2, 1, &[(0, 0x1000, 0, &[-7, 29], &[13, 17])]); + assert_eq!( + FormatPrinter::format_data_with_presentation( + &data, + &TypeInfo::UnknownType { + name: "BTreeMap".to_string(), + }, + &presentation, + ), + "BTreeMap(size=2) {-7: 13, 29: 17}" + ); + + let mut trace_context = TraceContext::new(); + let format_index = trace_context.add_string("{:x}".to_string()).unwrap(); + let type_index = trace_context + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "BTreeMap".to_string(), + }, + presentation, + ) + .unwrap(); + let name_index = trace_context.add_variable_name("map".to_string()).unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_index, + type_index, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data, + }; + assert_eq!( + FormatPrinter::format_complex_print_data(format_index, &[variable], &trace_context,), + "f9 ff ff ff 0d 00 1d 00 00 00 11 00" + ); + } + + #[test] + fn test_btree_set_presentation_reconstructs_internal_nodes_in_order() { + let presentation = btree_set_presentation(2); + let data = btree_set_payload( + 5, + 5, + 2, + 3, + &[ + (0, 0x1000, 1, &[10]), + (1, 0x2000, 0, &[-5, 4]), + (2, 0x3000, 0, &[12, 20]), + ], + ); + assert_eq!( + FormatPrinter::format_data_with_presentation( + &data, + &TypeInfo::UnknownType { + name: "BTreeSet".to_string(), + }, + &presentation, + ), + "BTreeSet(size=5) {-5, 4, 10, 12, 20}" + ); + } + + #[test] + fn test_btree_payload_preserves_valid_prefix_when_truncated() { + let presentation = btree_set_presentation(2); + let data = btree_set_payload(3, 2, 2, 2, &[(0, 0x1000, 1, &[10]), (1, 0x2000, 0, &[1])]); + let mut trace_context = TraceContext::new(); + let type_index = trace_context + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "BTreeSet".to_string(), + }, + presentation, + ) + .unwrap(); + let name_index = trace_context.add_variable_name("set".to_string()).unwrap(); + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_index, + type_index, + "", + &data, + VariableStatus::Truncated as u8, + &trace_context, + ), + "set = BTreeSet(size=3) {1, 10} " + ); + + let empty_prefix = btree_set_payload(3, 0, 2, 0, &[]); + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_index, + type_index, + "", + &empty_prefix, + VariableStatus::Truncated as u8, + &trace_context, + ), + "set = BTreeSet(size=3) {} " + ); + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_index, + type_index, + "", + &[0; 12], + VariableStatus::Truncated as u8, + &trace_context, + ), + "set = " + ); + let raw_format = trace_context.add_string("{:x}".to_string()).unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_index, + type_index, + access_path: String::new(), + status: VariableStatus::Truncated as u8, + data: data.clone(), + }; + assert_eq!( + FormatPrinter::format_complex_print_data(raw_format, &[variable], &trace_context,), + "01 00 00 00 0a 00 00 00 " + ); + let omitted_raw = ParsedComplexVariable { + var_name_index: name_index, + type_index, + access_path: String::new(), + status: VariableStatus::Truncated as u8, + data: vec![0; 12], + }; + assert_eq!( + FormatPrinter::format_complex_print_data(raw_format, &[omitted_raw], &trace_context,), + "" + ); + + let mut malformed = data; + malformed[BTREE_CAPTURED_ITEM_COUNT_OFFSET..BTREE_HEADER_SIZE] + .copy_from_slice(&3_u64.to_le_bytes()); + assert_eq!( + FormatPrinter::format_data_with_presentation( + &malformed, + &TypeInfo::UnknownType { + name: "BTreeSet".to_string(), + }, + &btree_set_presentation(2), + ), + "" + ); + } + + #[test] + fn test_utf8_string_presentation_uses_length_not_nul_termination() { + let mut trace_context = TraceContext::new(); + let format_idx = trace_context.add_string("{}".to_string()).unwrap(); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::Utf8String) + .unwrap(); + let name_idx = trace_context + .add_variable_name("message".to_string()) + .unwrap(); + let data = indirect_bytes_payload(3, b"a\0b"); + let vars = vec![ParsedComplexVariable { + var_name_index: name_idx, + type_index: type_idx, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data, + }]; + + assert_eq!( + FormatPrinter::format_complex_print_data(format_idx, &vars, &trace_context), + "\"a\\0b\"" + ); + } + + #[test] + fn test_byte_string_presentation_preserves_utf8_and_escapes_invalid_bytes() { + let valid = indirect_bytes_payload(4, "aé\0".as_bytes()); + let invalid = indirect_bytes_payload(3, b"a\xffb"); + + assert_eq!( + FormatPrinter::format_data_with_presentation( + &valid, + &rust_str_type(), + &ValuePresentation::ByteString, + ), + "\"aé\\0\"" + ); + assert_eq!( + FormatPrinter::format_data_with_presentation( + &invalid, + &rust_str_type(), + &ValuePresentation::ByteString, + ), + "\"a\\xffb\"" + ); + } + + #[test] + fn test_single_field_presentation_wraps_dwarf_value() { + let value_type = TypeInfo::BaseType { + name: "u32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }; + let presentation = ValuePresentation::SingleField { + type_name: "Cell".to_string(), + field_name: "value".to_string(), + }; + let data = 42_u32.to_le_bytes(); + + assert_eq!( + FormatPrinter::format_data_with_presentation(&data, &value_type, &presentation), + "Cell { value: 42 }" + ); + assert_eq!( + FormatPrinter::presentation_payload_bytes(&data, &presentation), + Some(data.as_slice()) + ); + + let unit_type = TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }; + assert_eq!( + FormatPrinter::format_data_with_presentation(&[], &unit_type, &presentation), + "Cell { value: () }" + ); + } + + #[test] + fn test_signed_state_struct_presentation_formats_borrow_states() { + let type_info = TypeInfo::StructType { + name: "RefCell".to_string(), + size: 16, + members: vec![ + crate::StructMember { + name: "value".to_string(), + member_type: TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }, + offset: 0, + bit_offset: None, + bit_size: None, + }, + crate::StructMember { + name: "borrow".to_string(), + member_type: TypeInfo::BaseType { + name: "isize".to_string(), + size: 8, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }, + offset: 8, + bit_offset: None, + bit_size: None, + }, + ], + }; + let presentation = ValuePresentation::SignedStateStruct { + state_field: "borrow".to_string(), + non_negative_label: "borrow".to_string(), + negative_label: "borrow_mut".to_string(), + }; + let mut shared = [0_u8; 16]; + shared[..4].copy_from_slice(&17_i32.to_le_bytes()); + shared[8..].copy_from_slice(&2_i64.to_le_bytes()); + assert_eq!( + FormatPrinter::format_data_with_presentation(&shared, &type_info, &presentation), + "RefCell(borrow=2) { value: 17, borrow: 2 }" + ); + + let mut mutable = shared; + mutable[8..].copy_from_slice(&(-1_i64).to_le_bytes()); + assert_eq!( + FormatPrinter::format_data_with_presentation(&mutable, &type_info, &presentation), + "RefCell(borrow_mut=1) { value: 17, borrow: -1 }" + ); + + mutable[8..].copy_from_slice(&i64::MIN.to_le_bytes()); + assert!( + FormatPrinter::format_data_with_presentation(&mutable, &type_info, &presentation) + .starts_with("RefCell(borrow_mut=9223372036854775808)") + ); + assert_eq!( + FormatPrinter::presentation_payload_bytes(&shared, &presentation), + Some(shared.as_slice()) + ); + } + + #[test] + fn test_reference_counted_presentation_removes_implicit_weak_count() { + let usize_type = TypeInfo::BaseType { + name: "usize".to_string(), + size: 8, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }; + let type_info = TypeInfo::StructType { + name: "Rc".to_string(), + size: 20, + members: vec![ + crate::StructMember { + name: "value".to_string(), + member_type: TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }, + offset: 0, + bit_offset: None, + bit_size: None, + }, + crate::StructMember { + name: "strong".to_string(), + member_type: usize_type.clone(), + offset: 4, + bit_offset: None, + bit_size: None, + }, + crate::StructMember { + name: "weak".to_string(), + member_type: usize_type, + offset: 12, + bit_offset: None, + bit_size: None, + }, + ], + }; + let presentation = ValuePresentation::ReferenceCountedStruct { + strong_field: "strong".to_string(), + weak_field: "weak".to_string(), + implicit_weak: 1, + }; + let mut data = [0_u8; 20]; + data[..4].copy_from_slice(&(-7_i32).to_le_bytes()); + data[4..12].copy_from_slice(&3_u64.to_le_bytes()); + data[12..].copy_from_slice(&2_u64.to_le_bytes()); + + assert_eq!( + FormatPrinter::format_data_with_presentation(&data, &type_info, &presentation), + "Rc(strong=3, weak=1) { value: -7, strong: 3, weak: 1 }" + ); + assert_eq!(u64::from_le_bytes(data[12..].try_into().unwrap()), 2); + assert_eq!( + FormatPrinter::presentation_payload_bytes(&data, &presentation), + Some(data.as_slice()) + ); } #[test] - fn test_missing_arguments() { - let result = FormatPrinter::apply_format_strings("need arg: {}", &[]); - assert_eq!(result, "need arg: "); - } + fn test_reference_counted_presentation_formats_a_dst_pointer() { + let usize_type = TypeInfo::BaseType { + name: "usize".to_string(), + size: 8, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }; + let type_info = TypeInfo::StructType { + name: "Arc".to_string(), + size: 24, + members: vec![ + crate::StructMember { + name: "ptr".to_string(), + member_type: TypeInfo::PointerType { + target_type: Box::new(TypeInfo::ArrayType { + element_type: Box::new(TypeInfo::BaseType { + name: "u8".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }), + element_count: None, + total_size: None, + }), + size: 8, + }, + offset: 0, + bit_offset: None, + bit_size: None, + }, + crate::StructMember { + name: "strong".to_string(), + member_type: usize_type.clone(), + offset: 8, + bit_offset: None, + bit_size: None, + }, + crate::StructMember { + name: "weak".to_string(), + member_type: usize_type, + offset: 16, + bit_offset: None, + bit_size: None, + }, + ], + }; + let presentation = ValuePresentation::ReferenceCountedStruct { + strong_field: "strong".to_string(), + weak_field: "weak".to_string(), + implicit_weak: 1, + }; + let mut data = Vec::new(); + data.extend_from_slice(&0x1234_u64.to_le_bytes()); + data.extend_from_slice(&2_u64.to_le_bytes()); + data.extend_from_slice(&1_u64.to_le_bytes()); - #[test] - fn test_format_print_data_with_trace_context() { - let mut trace_context = TraceContext::new(); - let format_index = trace_context - .add_string("Hello {}, you are {} years old!".to_string()) - .expect("add format string"); - let rendered: Vec = vec!["Alice".to_string(), "25".to_string()]; - let fmt = trace_context.get_string(format_index).unwrap(); - let result = FormatPrinter::apply_format_strings(fmt, &rendered); - assert_eq!(result, "Hello Alice, you are 25 years old!"); + assert_eq!( + FormatPrinter::format_data_with_presentation(&data, &type_info, &presentation), + "Arc(strong=2, weak=0) { ptr: 0x1234 (u8[]*), strong: 2, weak: 0 }" + ); } #[test] - fn test_format_complex_variable_struct() { - use crate::type_info::{StructMember, TypeInfo}; - - let mut trace_context = TraceContext::new(); - let var_name_idx = trace_context - .add_variable_name("person".to_string()) - .expect("add variable name"); - - let person_type = TypeInfo::StructType { - name: "Person".to_string(), - size: 36, + fn test_signed_state_struct_presentation_handles_truncated_state() { + let type_info = TypeInfo::StructType { + name: "RefCell".to_string(), + size: 16, members: vec![ - StructMember { - name: "age".to_string(), + crate::StructMember { + name: "value".to_string(), member_type: TypeInfo::BaseType { - name: "int".to_string(), + name: "i32".to_string(), size: 4, encoding: gimli::constants::DW_ATE_signed.0 as u16, }, @@ -1246,82 +3901,434 @@ mod tests { bit_offset: None, bit_size: None, }, - StructMember { - name: "id".to_string(), + crate::StructMember { + name: "borrow".to_string(), member_type: TypeInfo::BaseType { - name: "long".to_string(), + name: "isize".to_string(), size: 8, encoding: gimli::constants::DW_ATE_signed.0 as u16, }, - offset: 4, + offset: 8, bit_offset: None, bit_size: None, }, ], }; + let presentation = ValuePresentation::SignedStateStruct { + state_field: "borrow".to_string(), + non_negative_label: "borrow".to_string(), + negative_label: "borrow_mut".to_string(), + }; + let mut trace_context = TraceContext::new(); + let type_index = trace_context + .add_type_with_presentation(type_info, presentation) + .unwrap(); + let name_index = trace_context.add_variable_name("cell".to_string()).unwrap(); + let value = 17_i32.to_le_bytes(); + + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_index, + type_index, + "", + &value, + VariableStatus::Truncated as u8, + &trace_context, + ), + "cell = RefCell(borrow=) { value: 17, borrow: } \ + " + ); + } + #[test] + fn test_byte_string_presentation_retains_truncation_status() { + let mut trace_context = TraceContext::new(); let type_idx = trace_context - .add_type(person_type) - .expect("add person type"); + .add_type_with_presentation(rust_str_type(), ValuePresentation::ByteString) + .unwrap(); + let name_idx = trace_context.add_variable_name("path".to_string()).unwrap(); - // Data: age=25 (4 bytes) + id=12345 (8 bytes) - let data = vec![ - 25, 0, 0, 0, // age = 25 - 57, 48, 0, 0, 0, 0, 0, 0, // id = 12345 - ]; + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_idx, + type_idx, + "", + &indirect_bytes_payload(4, b"a\xff"), + VariableStatus::Truncated as u8, + &trace_context, + ), + "path = \"a\\xff\" " + ); + } - let result = FormatPrinter::format_complex_variable( - var_name_idx, + #[test] + fn test_utf8_string_presentation_preserves_embedded_wrapper_separator() { + let mut trace_context = TraceContext::new(); + let format_idx = trace_context.add_string("{}".to_string()).unwrap(); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::Utf8String) + .unwrap(); + let name_idx = trace_context + .add_variable_name("message".to_string()) + .unwrap(); + let vars = vec![ParsedComplexVariable { + var_name_index: name_idx, + type_index: type_idx, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data: indirect_bytes_payload(5, b"a = b"), + }]; + + assert_eq!( + FormatPrinter::format_complex_print_data(format_idx, &vars, &trace_context), + r#""a = b""# + ); + } + + #[test] + fn test_raw_specs_decode_utf8_string_payload() { + let mut trace_context = TraceContext::new(); + let format_idx = trace_context.add_string("{:s}|{:x}".to_string()).unwrap(); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::Utf8String) + .unwrap(); + let name_idx = trace_context + .add_variable_name("message".to_string()) + .unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_idx, + type_index: type_idx, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data: indirect_bytes_payload(3, b"abc"), + }; + + assert_eq!( + FormatPrinter::format_complex_print_data( + format_idx, + &[variable.clone(), variable], + &trace_context, + ), + "abc|61 62 63" + ); + } + + #[test] + fn test_raw_specs_decode_byte_string_payload() { + let mut trace_context = TraceContext::new(); + let format_idx = trace_context.add_string("{:s}|{:x}".to_string()).unwrap(); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::ByteString) + .unwrap(); + let name_idx = trace_context.add_variable_name("path".to_string()).unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_idx, + type_index: type_idx, + access_path: String::new(), + status: VariableStatus::Ok as u8, + data: indirect_bytes_payload(3, b"a\xffb"), + }; + + assert_eq!( + FormatPrinter::format_complex_print_data( + format_idx, + &[variable.clone(), variable], + &trace_context, + ), + "a\\xffb|61 ff 62" + ); + } + + #[test] + fn test_raw_specs_decode_truncated_utf8_string_payload() { + let mut trace_context = TraceContext::new(); + let format_idx = trace_context.add_string("{:s}|{:x}".to_string()).unwrap(); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::Utf8String) + .unwrap(); + let name_idx = trace_context + .add_variable_name("message".to_string()) + .unwrap(); + let variable = ParsedComplexVariable { + var_name_index: name_idx, + type_index: type_idx, + access_path: String::new(), + status: VariableStatus::Truncated as u8, + data: indirect_bytes_payload(6, b"abc"), + }; + + assert_eq!( + FormatPrinter::format_complex_print_data( + format_idx, + &[variable.clone(), variable], + &trace_context, + ), + "abc |61 62 63 " + ); + } + + #[test] + fn test_utf8_string_presentation_renders_empty_and_truncated_values() { + let mut trace_context = TraceContext::new(); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::Utf8String) + .unwrap(); + let name_idx = trace_context + .add_variable_name("message".to_string()) + .unwrap(); + + let empty = FormatPrinter::format_complex_variable_with_status( + name_idx, type_idx, "", - &data, + &indirect_bytes_payload(0, &[]), + VariableStatus::ZeroLength as u8, + &trace_context, + ); + let truncated = FormatPrinter::format_complex_variable_with_status( + name_idx, + type_idx, + "", + &indirect_bytes_payload(6, b"abc"), + VariableStatus::Truncated as u8, &trace_context, ); - assert!(result.contains("person = Person")); - assert!(result.contains("age: 25")); - assert!(result.contains("id: 12345")); + assert_eq!(empty, "message = \"\""); + assert_eq!(truncated, "message = \"abc\" "); } #[test] - fn test_complex_format_char_array() { - use crate::type_info::TypeInfo; - + fn test_utf8_string_read_error_uses_standard_error_payload() { let mut trace_context = TraceContext::new(); - let var_name_idx = trace_context - .add_variable_name("name".to_string()) - .expect("add variable name"); - // Define char array type: char name[16] - let char_type = TypeInfo::BaseType { - name: "char".to_string(), - size: 1, - encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16, - }; - let arr_type = TypeInfo::ArrayType { - element_type: Box::new(char_type), - element_count: Some(16), - total_size: Some(16), - }; - let type_idx = trace_context.add_type(arr_type).expect("add array type"); + let type_idx = trace_context + .add_type_with_presentation(rust_str_type(), ValuePresentation::Utf8String) + .unwrap(); + let name_idx = trace_context + .add_variable_name("message".to_string()) + .unwrap(); + let mut payload = vec![0; VARIABLE_READ_ERROR_PAYLOAD_LEN]; + let errno_end = VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET + std::mem::size_of::(); + let addr_end = VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET + std::mem::size_of::(); + payload[VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET..errno_end] + .copy_from_slice(&(-14i32).to_le_bytes()); + payload[VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET..addr_end] + .copy_from_slice(&0x1234u64.to_le_bytes()); - // Data buffer with "Alice\0" and padding - let mut data = b"Alice\0".to_vec(); - data.resize(16, 0u8); + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_idx, + type_idx, + "", + &payload, + VariableStatus::ReadError as u8, + &trace_context, + ), + "message = (struct &str*)" + ); + } - let fmt_idx = trace_context - .add_string("{}".to_string()) - .expect("add format string"); - let complex_vars = vec![ParsedComplexVariable { - var_name_index: var_name_idx, + #[test] + fn test_sequence_presentation_formats_dwarf_elements() { + let mut trace_context = TraceContext::new(); + let format_idx = trace_context.add_string("{}".to_string()).unwrap(); + let element_type = TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }; + let type_idx = trace_context + .add_type_with_presentation( + TypeInfo::StructType { + name: "Vec".to_string(), + size: 24, + members: Vec::new(), + }, + ValuePresentation::Sequence { + element_type: Box::new(element_type), + element_stride: 4, + }, + ) + .unwrap(); + let name_idx = trace_context + .add_variable_name("values".to_string()) + .unwrap(); + let mut elements = Vec::new(); + elements.extend_from_slice(&1i32.to_le_bytes()); + elements.extend_from_slice(&(-2i32).to_le_bytes()); + elements.extend_from_slice(&3i32.to_le_bytes()); + let vars = vec![ParsedComplexVariable { + var_name_index: name_idx, type_index: type_idx, access_path: String::new(), - status: 0, - data, + status: VariableStatus::Ok as u8, + data: indirect_sequence_payload(3, 3, &elements), }]; - let result = - FormatPrinter::format_complex_print_data(fmt_idx, &complex_vars, &trace_context); - assert_eq!(result, "\"Alice\""); + assert_eq!( + FormatPrinter::format_complex_print_data(format_idx, &vars, &trace_context), + "[1, -2, 3]" + ); + } + + #[test] + fn test_sequence_presentation_formats_empty_truncated_and_zst_values() { + let mut trace_context = TraceContext::new(); + let i32_type = TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }; + let vec_type = TypeInfo::StructType { + name: "Vec".to_string(), + size: 24, + members: Vec::new(), + }; + let vec_idx = trace_context + .add_type_with_presentation( + vec_type.clone(), + ValuePresentation::Sequence { + element_type: Box::new(i32_type), + element_stride: 4, + }, + ) + .unwrap(); + let zst_idx = trace_context + .add_type_with_presentation( + vec_type, + ValuePresentation::Sequence { + element_type: Box::new(TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }), + element_stride: 0, + }, + ) + .unwrap(); + let name_idx = trace_context + .add_variable_name("values".to_string()) + .unwrap(); + let mut two_elements = Vec::new(); + two_elements.extend_from_slice(&10i32.to_le_bytes()); + two_elements.extend_from_slice(&20i32.to_le_bytes()); + + let empty = FormatPrinter::format_complex_variable_with_status( + name_idx, + vec_idx, + "", + &indirect_sequence_payload(0, 0, &[]), + VariableStatus::ZeroLength as u8, + &trace_context, + ); + let truncated = FormatPrinter::format_complex_variable_with_status( + name_idx, + vec_idx, + "", + &indirect_sequence_payload(4, 2, &two_elements), + VariableStatus::Truncated as u8, + &trace_context, + ); + let zst = FormatPrinter::format_complex_variable_with_status( + name_idx, + zst_idx, + "", + &indirect_sequence_payload(3, 3, &[]), + VariableStatus::Ok as u8, + &trace_context, + ); + + assert_eq!(empty, "values = []"); + assert_eq!(truncated, "values = [10, 20] "); + assert_eq!(zst, "values = [(), (), ()]"); + } + + #[test] + fn test_sequence_presentation_uses_dwarf_pointer_size() { + let element_type = TypeInfo::PointerType { + target_type: Box::new(TypeInfo::BaseType { + name: "u8".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16, + }), + size: 4, + }; + let presentation = ValuePresentation::Sequence { + element_type: Box::new(element_type), + element_stride: 4, + }; + let payload = indirect_sequence_payload(1, 1, &0x1234_5678u32.to_le_bytes()); + + assert_eq!( + FormatPrinter::format_data_with_presentation( + &payload, + &TypeInfo::UnknownType { + name: "Vec<*const u8>".to_string(), + }, + &presentation, + ), + "[0x12345678 (u8*)]" + ); + } + + #[test] + fn test_truncated_sequence_without_complete_header_is_not_invalid() { + let mut trace_context = TraceContext::new(); + let type_idx = trace_context + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "Vec".to_string(), + }, + ValuePresentation::Sequence { + element_type: Box::new(TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }), + element_stride: 4, + }, + ) + .unwrap(); + let name_idx = trace_context + .add_variable_name("values".to_string()) + .unwrap(); + + assert_eq!( + FormatPrinter::format_complex_variable_with_status( + name_idx, + type_idx, + "", + &[0; VARIABLE_READ_ERROR_PAYLOAD_LEN], + VariableStatus::Truncated as u8, + &trace_context, + ), + "values = " + ); + } + + #[test] + fn formats_128_bit_dwarf_integers() { + let signed = TypeInfo::BaseType { + name: "i128".to_string(), + size: 16, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }; + let unsigned = TypeInfo::BaseType { + name: "u128".to_string(), + size: 16, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }; + let signed_value = -170_141_183_460_469_231_731_687_303_715_884_105_727_i128; + let unsigned_value = 340_282_366_920_938_463_463_374_607_431_768_211_454_u128; + + assert_eq!( + FormatPrinter::format_data_with_type_info(&signed_value.to_le_bytes(), &signed), + signed_value.to_string() + ); + assert_eq!( + FormatPrinter::format_data_with_type_info(&unsigned_value.to_le_bytes(), &unsigned), + unsigned_value.to_string() + ); } #[test] @@ -1394,6 +4401,29 @@ mod tests { let data_true = [0x01u8]; let res3 = FormatPrinter::format_data_with_type_info(&data_true, &bf_bool); assert_eq!(res3, "true"); + + let scoped_enum = TypeInfo::ScopedEnumType { + name: "Scoped".to_string(), + size: 1, + base_type: Box::new(TypeInfo::BaseType { + name: "unsigned char".to_string(), + size: 1, + encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16, + }), + variants: vec![crate::ScopedEnumVariant { + name: "Second".to_string(), + value: crate::DiscriminantValue::Unsigned(1), + }], + }; + let bf_scoped_enum = TypeInfo::BitfieldType { + underlying_type: Box::new(scoped_enum), + bit_offset: 0, + bit_size: 2, + }; + assert_eq!( + FormatPrinter::format_data_with_type_info(&data_true, &bf_scoped_enum), + "Second" + ); } #[test] diff --git a/ghostscope-protocol/src/lib.rs b/ghostscope-protocol/src/lib.rs index 358d3f95..723df1f7 100644 --- a/ghostscope-protocol/src/lib.rs +++ b/ghostscope-protocol/src/lib.rs @@ -5,6 +5,7 @@ // Core modules pub mod bpf_abi; mod type_kind; +mod value_presentation; pub mod format_printer; pub mod streaming_parser; @@ -13,6 +14,15 @@ pub mod trace_event; pub mod type_info; pub use type_kind::{consts, TypeKind}; +pub use value_presentation::{ + BTreeEntryPresentation, BTreeFieldPresentation, HashTableBucketOrder, + HashTableEntryPresentation, HashTableFieldPresentation, HashTableOccupancy, ValuePresentation, + BTREE_CAPTURED_ITEM_COUNT_OFFSET, BTREE_HEADER_SIZE, BTREE_NODE_HEADER_SIZE, + BTREE_NODE_HEIGHT_OFFSET, BTREE_NODE_LENGTH_OFFSET, BTREE_NODE_SLOT_COUNT_OFFSET, + HASH_TABLE_BUCKET_DATA_OFFSET, HASH_TABLE_CAPACITY_OFFSET, HASH_TABLE_CAPTURED_BUCKETS_OFFSET, + HASH_TABLE_HEADER_SIZE, INDIRECT_BYTES_LENGTH_PREFIX_SIZE, + INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET, INDIRECT_SEQUENCE_HEADER_SIZE, +}; pub use trace_event::{ EndInstructionData, ExprErrorData, InstructionHeader, InstructionType, @@ -85,6 +95,9 @@ pub use streaming_parser::{ StreamingTraceParser, }; -pub use type_info::{EnumVariant, StructMember, TypeCache, TypeInfo, TypeQualifier}; +pub use type_info::{ + DiscriminantRange, DiscriminantValue, EnumVariant, ScopedEnumVariant, StructMember, TypeCache, + TypeInfo, TypeQualifier, VariantCase, VariantPart, VariantPayloadPresentation, VariantSelector, +}; pub use ghostscope_platform as platform; diff --git a/ghostscope-protocol/src/streaming_parser.rs b/ghostscope-protocol/src/streaming_parser.rs index 5050e43a..0ddc5e90 100644 --- a/ghostscope-protocol/src/streaming_parser.rs +++ b/ghostscope-protocol/src/streaming_parser.rs @@ -466,8 +466,10 @@ impl StreamingTraceParser { type_index ); // Use advanced formatting with full type information - crate::format_printer::FormatPrinter::format_data_with_type_info( - var_data, type_info, + crate::format_printer::FormatPrinter::format_data_with_presentation( + var_data, + type_info, + trace_context.get_value_presentation(type_index), ) } None => { diff --git a/ghostscope-protocol/src/trace_context.rs b/ghostscope-protocol/src/trace_context.rs index d611e95d..03236d16 100644 --- a/ghostscope-protocol/src/trace_context.rs +++ b/ghostscope-protocol/src/trace_context.rs @@ -4,9 +4,12 @@ //! management of all context information needed during tracing execution. use crate::type_info::TypeInfo; +use crate::ValuePresentation; use serde::{Deserialize, Serialize}; use std::fmt; +static DWARF_VALUE_PRESENTATION: ValuePresentation = ValuePresentation::Dwarf; + /// Trace context table identifiers used in overflow errors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TraceContextTable { @@ -72,6 +75,11 @@ pub struct TraceContext { /// Complete DWARF type information for perfect formatting pub types: Vec, + /// Semantic presentation associated with each type index. Older trace + /// contexts omit this table and therefore use `Dwarf` for every type. + #[serde(default)] + pub value_presentations: Vec, + /// Variable names for debugging and display pub variable_names: Vec, } @@ -82,6 +90,7 @@ impl TraceContext { Self { strings: Vec::new(), types: Vec::new(), + value_presentations: Vec::new(), variable_names: Vec::new(), } } @@ -104,6 +113,15 @@ impl TraceContext { /// Add a type to the context and return its index pub fn add_type(&mut self, type_info: TypeInfo) -> TraceContextResult { + self.add_type_with_presentation(type_info, ValuePresentation::Dwarf) + } + + /// Add a type and its semantic presentation to the context. + pub fn add_type_with_presentation( + &mut self, + type_info: TypeInfo, + presentation: ValuePresentation, + ) -> TraceContextResult { // For now, don't deduplicate types as they can be complex to compare // TODO: Implement type deduplication for performance optimization let index = self.types.len(); @@ -114,7 +132,10 @@ impl TraceContext { // Debug: Log the type being added tracing::debug!("Adding type at index {}: {:#?}", index, type_info); + self.value_presentations + .resize(index, ValuePresentation::Dwarf); self.types.push(type_info); + self.value_presentations.push(presentation); Ok(index as u16) } @@ -151,6 +172,14 @@ impl TraceContext { self.types.get(index as usize) } + /// Get the presentation for a type index, defaulting to physical DWARF + /// formatting for contexts serialized before this table was introduced. + pub fn get_value_presentation(&self, index: u16) -> &ValuePresentation { + self.value_presentations + .get(index as usize) + .unwrap_or(&DWARF_VALUE_PRESENTATION) + } + /// Get a variable name by index pub fn get_variable_name(&self, index: u16) -> Option<&str> { self.variable_names.get(index as usize).map(|s| s.as_str()) @@ -175,6 +204,7 @@ impl TraceContext { pub fn clear(&mut self) { self.strings.clear(); self.types.clear(); + self.value_presentations.clear(); self.variable_names.clear(); } @@ -270,9 +300,246 @@ mod tests { assert_eq!(ctx.get_type(idx1), Some(&type1)); assert_eq!(ctx.get_type(idx2), Some(&type2)); + assert_eq!(ctx.get_value_presentation(idx1), &ValuePresentation::Dwarf); assert_eq!(ctx.type_count(), 2); } + #[test] + fn test_trace_context_type_presentation() { + let mut ctx = TraceContext::new(); + let type_info = TypeInfo::UnknownType { + name: "&str".to_string(), + }; + let index = ctx + .add_type_with_presentation(type_info, ValuePresentation::Utf8String) + .unwrap(); + + assert_eq!( + ctx.get_value_presentation(index), + &ValuePresentation::Utf8String + ); + } + + #[test] + fn test_sequence_presentation_round_trips_with_element_type() { + let mut ctx = TraceContext::new(); + let presentation = ValuePresentation::Sequence { + element_type: Box::new(TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }), + element_stride: 4, + }; + let index = ctx + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "Vec".to_string(), + }, + presentation.clone(), + ) + .unwrap(); + + let json = serde_json::to_string(&ctx).unwrap(); + let decoded: TraceContext = serde_json::from_str(&json).unwrap(); + + assert_eq!(decoded.get_value_presentation(index), &presentation); + } + + #[test] + fn test_hash_table_presentation_round_trips_with_projected_fields() { + let mut ctx = TraceContext::new(); + let presentation = ValuePresentation::HashTable { + entry_stride: 8, + bucket_order: crate::HashTableBucketOrder::Reverse, + occupancy: crate::HashTableOccupancy::NonZeroWord { word_size: 8 }, + entry: crate::HashTableEntryPresentation::Map { + key: crate::HashTableFieldPresentation { + offset: 0, + field_type: Box::new(TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }), + }, + value: crate::HashTableFieldPresentation { + offset: 4, + field_type: Box::new(TypeInfo::BaseType { + name: "u16".to_string(), + size: 2, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }), + }, + }, + }; + let index = ctx + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "HashMap".to_string(), + }, + presentation.clone(), + ) + .unwrap(); + + let json = serde_json::to_string(&ctx).unwrap(); + let decoded: TraceContext = serde_json::from_str(&json).unwrap(); + + assert_eq!(decoded.get_value_presentation(index), &presentation); + } + + #[test] + fn test_legacy_hash_table_presentation_defaults_to_control_bytes() { + let presentation = ValuePresentation::HashTable { + entry_stride: 0, + bucket_order: crate::HashTableBucketOrder::Reverse, + occupancy: crate::HashTableOccupancy::ControlByteHighBitClear, + entry: crate::HashTableEntryPresentation::Set { + value: crate::HashTableFieldPresentation { + offset: 0, + field_type: Box::new(TypeInfo::BaseType { + name: "()".to_string(), + size: 0, + encoding: 0, + }), + }, + }, + }; + let mut json = serde_json::to_value(&presentation).unwrap(); + json.get_mut("HashTable") + .and_then(serde_json::Value::as_object_mut) + .unwrap() + .remove("occupancy"); + + let decoded: ValuePresentation = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, presentation); + } + + #[test] + fn test_btree_presentation_round_trips_with_slot_layouts() { + let mut ctx = TraceContext::new(); + let presentation = ValuePresentation::BTree { + node_capacity: 11, + entry: crate::BTreeEntryPresentation::Map { + key: crate::BTreeFieldPresentation { + slot_stride: 4, + value_offset: 0, + field_type: Box::new(TypeInfo::BaseType { + name: "i32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_signed.0 as u16, + }), + }, + value: crate::BTreeFieldPresentation { + slot_stride: 2, + value_offset: 0, + field_type: Box::new(TypeInfo::BaseType { + name: "u16".to_string(), + size: 2, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }), + }, + }, + }; + let index = ctx + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "BTreeMap".to_string(), + }, + presentation.clone(), + ) + .unwrap(); + + let json = serde_json::to_string(&ctx).unwrap(); + let decoded: TraceContext = serde_json::from_str(&json).unwrap(); + + assert_eq!(decoded.get_value_presentation(index), &presentation); + } + + #[test] + fn test_single_field_presentation_round_trips() { + let mut ctx = TraceContext::new(); + let presentation = ValuePresentation::SingleField { + type_name: "Cell".to_string(), + field_name: "value".to_string(), + }; + let index = ctx + .add_type_with_presentation( + TypeInfo::BaseType { + name: "u32".to_string(), + size: 4, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }, + presentation.clone(), + ) + .unwrap(); + + let json = serde_json::to_string(&ctx).unwrap(); + let decoded: TraceContext = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.get_value_presentation(index), &presentation); + } + + #[test] + fn test_signed_state_struct_presentation_round_trips() { + let mut ctx = TraceContext::new(); + let presentation = ValuePresentation::SignedStateStruct { + state_field: "borrow".to_string(), + non_negative_label: "borrow".to_string(), + negative_label: "borrow_mut".to_string(), + }; + let index = ctx + .add_type_with_presentation( + TypeInfo::StructType { + name: "RefCell".to_string(), + size: 8, + members: Vec::new(), + }, + presentation.clone(), + ) + .unwrap(); + + let json = serde_json::to_string(&ctx).unwrap(); + let decoded: TraceContext = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.get_value_presentation(index), &presentation); + } + + #[test] + fn test_deserialized_legacy_context_defaults_to_dwarf_presentation() { + let json = r#"{ + "strings": [], + "types": [{"UnknownType": {"name": "legacy"}}], + "variable_names": [] + }"#; + let ctx: TraceContext = serde_json::from_str(json).unwrap(); + + assert_eq!(ctx.get_value_presentation(0), &ValuePresentation::Dwarf); + } + + #[test] + fn test_appending_to_legacy_context_backfills_presentations() { + let json = r#"{ + "strings": [], + "types": [{"UnknownType": {"name": "legacy"}}], + "variable_names": [] + }"#; + let mut ctx: TraceContext = serde_json::from_str(json).unwrap(); + let new_index = ctx + .add_type_with_presentation( + TypeInfo::UnknownType { + name: "&str".to_string(), + }, + ValuePresentation::Utf8String, + ) + .unwrap(); + + assert_eq!(new_index, 1); + assert_eq!(ctx.value_presentations.len(), ctx.types.len()); + assert_eq!(ctx.get_value_presentation(0), &ValuePresentation::Dwarf); + assert_eq!( + ctx.get_value_presentation(new_index), + &ValuePresentation::Utf8String + ); + } + #[test] fn test_trace_context_combined() { let mut ctx = TraceContext::new(); diff --git a/ghostscope-protocol/src/type_info.rs b/ghostscope-protocol/src/type_info.rs index c7e8d5bf..2a88d379 100644 --- a/ghostscope-protocol/src/type_info.rs +++ b/ghostscope-protocol/src/type_info.rs @@ -83,6 +83,28 @@ pub enum TypeInfo { /// Optimized out type (variable was optimized away by compiler) OptimizedOut { name: String }, + + /// Aggregate whose active fields are selected by DWARF variant parts. + /// + /// This is distinct from `EnumType`, which represents a scalar + /// `DW_TAG_enumeration_type`. Rust enums with payloads are emitted as a + /// structure containing `DW_TAG_variant_part` and `DW_TAG_variant` DIEs. + /// Variant-bearing forms are appended after the legacy alternatives so + /// their serialized discriminants remain stable. + VariantType { + name: String, + size: u64, + members: Vec, + variant_parts: Vec, + }, + + /// Scalar `DW_TAG_enumeration_type` marked with `DW_AT_enum_class`. + ScopedEnumType { + name: String, + size: u64, + base_type: Box, + variants: Vec, + }, } /// Struct/union member information @@ -102,6 +124,73 @@ pub struct EnumVariant { pub value: i64, } +/// Enumerator of a scoped enum, preserving its DWARF signedness. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ScopedEnumVariant { + pub name: String, + pub value: DiscriminantValue, +} + +/// A `DW_TAG_variant_part` and the field used to select its active branch. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VariantPart { + /// `DW_AT_discr` points to this member. It is absent for univariant forms. + pub discriminant: Option, + pub variants: Vec, +} + +/// One `DW_TAG_variant` branch. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VariantCase { + pub selector: VariantSelector, + pub members: Vec, + pub variant_parts: Vec, + /// Source-language presentation selected before the type enters the + /// protocol. The formatter must not infer producer conventions from member + /// names. + #[serde(default)] + pub payload_presentation: VariantPayloadPresentation, +} + +/// Language-neutral rendering style for one variant payload. +/// +/// DWARF describes the physical variant graph but does not distinguish tuple +/// payloads from named-field payloads. A source-language adapter supplies this +/// metadata when the producer convention is known. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum VariantPayloadPresentation { + /// Preserve the physical DWARF member view without producer-specific name + /// inference. + #[default] + Dwarf, + Unit, + Tuple, + Struct, +} + +/// Values that activate a variant branch. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum VariantSelector { + /// Neither `DW_AT_discr_value` nor `DW_AT_discr_list` was present. + Default, + Ranges(Vec), +} + +/// Inclusive discriminant range from `DW_AT_discr_value` or +/// `DW_AT_discr_list`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DiscriminantRange { + pub start: DiscriminantValue, + pub end: DiscriminantValue, +} + +/// A discriminant preserves the signedness of its DWARF member type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DiscriminantValue { + Signed(i64), + Unsigned(u64), +} + /// Type qualifiers #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum TypeQualifier { @@ -132,6 +221,8 @@ impl TypeInfo { } => underlying_type.size(), TypeInfo::UnknownType { .. } => 0, TypeInfo::OptimizedOut { .. } => 0, // Optimized out has no size + TypeInfo::VariantType { size, .. } => *size, + TypeInfo::ScopedEnumType { size, .. } => *size, } } @@ -258,6 +349,8 @@ impl TypeInfo { ), TypeInfo::UnknownType { name } => name.clone(), TypeInfo::OptimizedOut { name } => format!(" {name}"), + TypeInfo::VariantType { name, .. } => format!("enum {name}"), + TypeInfo::ScopedEnumType { name, .. } => format!("enum {name}"), } } @@ -276,6 +369,9 @@ impl TypeInfo { TypeInfo::BitfieldType { underlying_type, .. } => underlying_type.is_signed_int(), + TypeInfo::EnumType { base_type, .. } | TypeInfo::ScopedEnumType { base_type, .. } => { + base_type.is_signed_int() + } TypeInfo::OptimizedOut { .. } => false, _ => false, } @@ -296,6 +392,9 @@ impl TypeInfo { TypeInfo::BitfieldType { underlying_type, .. } => underlying_type.is_unsigned_int(), + TypeInfo::EnumType { base_type, .. } | TypeInfo::ScopedEnumType { base_type, .. } => { + base_type.is_unsigned_int() + } TypeInfo::OptimizedOut { .. } => false, _ => false, } @@ -497,6 +596,37 @@ impl fmt::Display for TypeInfo { write!(f, " {name}") } } + TypeInfo::VariantType { name, .. } => { + if name.is_empty() { + write!(f, "enum") + } else { + write!(f, "enum {name}") + } + } + TypeInfo::ScopedEnumType { name, .. } => { + if name.is_empty() { + write!(f, "enum") + } else { + write!(f, "enum {name}") + } + } } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_variant_cases_default_to_dwarf_presentation() { + let case: VariantCase = serde_json::from_value(serde_json::json!({ + "selector": "Default", + "members": [], + "variant_parts": [] + })) + .unwrap(); + + assert_eq!(case.payload_presentation, VariantPayloadPresentation::Dwarf); + } +} diff --git a/ghostscope-protocol/src/type_kind.rs b/ghostscope-protocol/src/type_kind.rs index e32c7460..9901726d 100644 --- a/ghostscope-protocol/src/type_kind.rs +++ b/ghostscope-protocol/src/type_kind.rs @@ -119,6 +119,8 @@ impl From<&TypeInfo> for TypeKind { TypeInfo::FunctionType { .. } => TypeKind::Pointer, TypeInfo::UnknownType { .. } => TypeKind::U8, // Default to byte for unknown types TypeInfo::OptimizedOut { .. } => TypeKind::OptimizedOut, + TypeInfo::VariantType { .. } => TypeKind::Struct, + TypeInfo::ScopedEnumType { base_type, .. } => TypeKind::from(base_type.as_ref()), } } } diff --git a/ghostscope-protocol/src/value_presentation.rs b/ghostscope-protocol/src/value_presentation.rs new file mode 100644 index 00000000..b75d6e70 --- /dev/null +++ b/ghostscope-protocol/src/value_presentation.rs @@ -0,0 +1,181 @@ +//! Semantic value presentations carried alongside physical DWARF types. + +use crate::TypeInfo; +use serde::{Deserialize, Serialize}; + +/// Number of bytes used to encode the original length of an indirect byte +/// sequence before its captured payload. +pub const INDIRECT_BYTES_LENGTH_PREFIX_SIZE: usize = std::mem::size_of::(); + +/// Number of bytes used by an indirect sequence header. The first `u64` is +/// the original element count and the second is the captured element count. +pub const INDIRECT_SEQUENCE_HEADER_SIZE: usize = std::mem::size_of::() * 2; + +/// Offset of the captured element count in an indirect sequence header. +pub const INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET: usize = std::mem::size_of::(); + +/// Number of bytes used by a bounded hash-table payload. The header stores the +/// logical item count, table capacity, captured bucket count, and byte offset +/// of the bucket payload. Captured occupancy bytes and any unused reserved +/// occupancy headroom sit between the header and the bucket payload. +pub const HASH_TABLE_HEADER_SIZE: usize = std::mem::size_of::() * 4; + +/// Offset of the runtime table capacity in a bounded hash-table header. +pub const HASH_TABLE_CAPACITY_OFFSET: usize = std::mem::size_of::(); + +/// Offset of the captured bucket count in a bounded hash-table header. +pub const HASH_TABLE_CAPTURED_BUCKETS_OFFSET: usize = std::mem::size_of::() * 2; + +/// Offset of the bucket payload offset in a bounded hash-table header. +pub const HASH_TABLE_BUCKET_DATA_OFFSET: usize = std::mem::size_of::() * 3; + +/// Number of bytes in a bounded B-Tree payload header. It stores the logical +/// item count, reserved node-slot count, and captured item count. +pub const BTREE_HEADER_SIZE: usize = std::mem::size_of::() * 3; + +/// Offset of the reserved node-slot count in a B-Tree payload header. +pub const BTREE_NODE_SLOT_COUNT_OFFSET: usize = std::mem::size_of::(); + +/// Offset of the captured item count in a B-Tree payload header. +pub const BTREE_CAPTURED_ITEM_COUNT_OFFSET: usize = std::mem::size_of::() * 2; + +/// Fixed metadata at the start of each captured B-Tree node slot. It stores +/// the node address, node height, and initialized key count. +pub const BTREE_NODE_HEADER_SIZE: usize = std::mem::size_of::() * 3; + +/// Offset of a node's height in its B-Tree payload slot. +pub const BTREE_NODE_HEIGHT_OFFSET: usize = std::mem::size_of::(); + +/// Offset of a node's initialized key count in its B-Tree payload slot. +pub const BTREE_NODE_LENGTH_OFFSET: usize = std::mem::size_of::() * 2; + +/// Physical order of captured buckets relative to their occupancy metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum HashTableBucketOrder { + /// Bucket `i` is stored at increasing offsets from a dedicated data pointer. + Forward, + /// Bucket `i` is stored immediately before the control pointer, so a + /// contiguous capture appears in reverse control-index order. + Reverse, +} + +/// Physical occupancy metadata stored for each captured hash-table bucket. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum HashTableOccupancy { + /// One hashbrown control byte; its high bit is clear for occupied buckets. + #[default] + ControlByteHighBitClear, + /// One fixed-width hash word; zero marks an empty bucket. + NonZeroWord { word_size: u64 }, +} + +impl HashTableOccupancy { + /// Number of payload bytes that describe one bucket's occupancy. + pub fn byte_width(self) -> Option { + match self { + Self::ControlByteHighBitClear => Some(1), + Self::NonZeroWord { word_size } => (word_size > 0).then_some(word_size), + } + } +} + +/// One source-language value projected from a DWARF-described hash entry. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct HashTableFieldPresentation { + pub offset: u64, + pub field_type: Box, +} + +/// Source-language interpretation of one physical hash-table entry. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum HashTableEntryPresentation { + Map { + key: HashTableFieldPresentation, + value: HashTableFieldPresentation, + }, + Set { + value: HashTableFieldPresentation, + }, +} + +/// One logical B-Tree value embedded in a physical initialized-slot wrapper. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BTreeFieldPresentation { + pub slot_stride: u64, + pub value_offset: u64, + pub field_type: Box, +} + +/// Source-language interpretation of keys and values captured from B-Tree +/// node arrays. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BTreeEntryPresentation { + Map { + key: BTreeFieldPresentation, + value: BTreeFieldPresentation, + }, + Set { + value: BTreeFieldPresentation, + }, +} + +/// User-space presentation selected for a captured value. +/// +/// `Dwarf` preserves the existing physical-layout formatter. Other variants +/// define both the capture payload contract and its semantic rendering. +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub enum ValuePresentation { + #[default] + Dwarf, + /// A UTF-8 string encoded as an original-length `u64` followed by captured + /// bytes. The byte payload may be shorter than the original length. + Utf8String, + /// A contiguous sequence encoded as original and captured element counts, + /// followed by complete element bytes. The element type and stride come + /// from DWARF rather than a source-language ABI assumption. + Sequence { + element_type: Box, + element_stride: u64, + }, + /// An arbitrary byte string using the same length-prefixed payload as + /// `Utf8String`. Invalid UTF-8 bytes are rendered with `\xNN` escapes. + ByteString, + /// A projected DWARF value rendered as one named field of a semantic + /// wrapper. The payload is the raw bytes of `type_info`. + SingleField { + type_name: String, + field_name: String, + }, + /// A projected struct whose signed state field selects one of two summary + /// labels. The payload is the raw inline byte layout of `type_info`. + SignedStateStruct { + state_field: String, + non_negative_label: String, + negative_label: String, + }, + /// A reference-counted struct whose summary exposes strong and weak + /// counters. `implicit_weak` removes implementation-owned weak entries + /// from both the summary and the rendered weak field. + ReferenceCountedStruct { + strong_field: String, + weak_field: String, + implicit_weak: u64, + }, + /// A bounded sparse-table capture containing occupancy metadata and + /// physical entries. Entry stride and projected key/value fields come from + /// DWARF; the source-language adapter selects the occupancy semantics. + HashTable { + entry_stride: u64, + bucket_order: HashTableBucketOrder, + #[serde(default)] + occupancy: HashTableOccupancy, + entry: HashTableEntryPresentation, + }, + /// A bounded breadth-first snapshot of Rust B-Tree nodes. Node capacity, + /// physical slot strides, embedded value offsets, and value types all come + /// from DWARF. User space reconstructs source order from the node slots. + BTree { + node_capacity: u64, + entry: BTreeEntryPresentation, + }, +}