diff --git a/.claude/commands/build-example.md b/.claude/commands/build-example.md new file mode 100644 index 00000000..f83af5b6 --- /dev/null +++ b/.claude/commands/build-example.md @@ -0,0 +1,23 @@ +# Build example + +Run the full build-examples pipeline that builds, dumps, disassembles, +tests, and verifies artifacts. + +From the `examples/` directory, run: + +```sh +cargo run --bin build-examples -- --example $ARGUMENTS +``` + +If `$ARGUMENTS` is empty, run without the `--example` flag to build all +examples: + +```sh +cargo run --bin build-examples +``` + +This executes `examples/utils/build-examples/src/main.rs` which handles +building ELF files, generating dumps/disassembly, running tests, saving +test snippets, and verifying code snippets. + +Report the outcome. If the command fails, show the relevant error output. diff --git a/.claude/commands/disassemble-example.md b/.claude/commands/disassemble-example.md new file mode 100644 index 00000000..5106f5d6 --- /dev/null +++ b/.claude/commands/disassemble-example.md @@ -0,0 +1,32 @@ +# Disassemble example + +Quick compile and disassemble cycle for a specific example program. + +Run the following commands sequentially from the example directory at +`examples/$ARGUMENTS`: + +1. Remove stale binary: + +```sh +rm -f ../target/deploy/${ARGUMENTS//-/_}.so +``` + +1. Build for the disassembly architecture: + +```sh +cargo build-sbf --arch v2 --tools-version 1.52 +``` + +1. Disassemble and write output: + +```sh +sbpf disassemble ../target/deploy/${ARGUMENTS//-/_}.so \ + > artifacts/rs-disassembly.s +``` + +If `$ARGUMENTS` is empty, infer the example name from the current +conversation context (e.g. recent file paths or discussion). If you still +cannot determine it, ask which example to disassemble. + +Report the result. Show the disassembly content or the relevant section +if the user is focused on specific functions. diff --git a/.claude/commands/test-example.md b/.claude/commands/test-example.md new file mode 100644 index 00000000..e0fb2ecf --- /dev/null +++ b/.claude/commands/test-example.md @@ -0,0 +1,18 @@ +# Test example + +Quick build and test cycle for a specific example program. + +Run the following commands sequentially from the example directory at +`examples/$ARGUMENTS`: + +```sh +cargo build-sbf --arch v3 --tools-version 1.51 +sbpf build +cargo test -- --test-threads 1 +``` + +If `$ARGUMENTS` is empty, infer the example name from the current +conversation context (e.g. recent file paths or discussion). If you still +cannot determine it, ask which example to test. + +Report the result of each step. If any step fails, stop and show the error. diff --git a/.claude/skills/manage-pentagon-agent b/.claude/skills/manage-pentagon-agent new file mode 120000 index 00000000..ae21c7ba --- /dev/null +++ b/.claude/skills/manage-pentagon-agent @@ -0,0 +1 @@ +/Users/alex/.pentagon/skills/manage-pentagon-agent \ No newline at end of file diff --git a/.claude/skills/manage-pentagon-canvas b/.claude/skills/manage-pentagon-canvas new file mode 120000 index 00000000..4ad17639 --- /dev/null +++ b/.claude/skills/manage-pentagon-canvas @@ -0,0 +1 @@ +/Users/alex/.pentagon/skills/manage-pentagon-canvas \ No newline at end of file diff --git a/.claude/skills/manage-pentagon-role b/.claude/skills/manage-pentagon-role new file mode 120000 index 00000000..193d74f3 --- /dev/null +++ b/.claude/skills/manage-pentagon-role @@ -0,0 +1 @@ +/Users/alex/.pentagon/skills/manage-pentagon-role \ No newline at end of file diff --git a/.claude/skills/manage-pentagon-team b/.claude/skills/manage-pentagon-team new file mode 120000 index 00000000..5523ef51 --- /dev/null +++ b/.claude/skills/manage-pentagon-team @@ -0,0 +1 @@ +/Users/alex/.pentagon/skills/manage-pentagon-team \ No newline at end of file diff --git a/.claude/skills/send-pentagon-message b/.claude/skills/send-pentagon-message new file mode 120000 index 00000000..34abeeab --- /dev/null +++ b/.claude/skills/send-pentagon-message @@ -0,0 +1 @@ +/Users/alex/.pentagon/skills/send-pentagon-message \ No newline at end of file diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 4412db36..5a9b63f8 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -4,7 +4,7 @@ env: SBPF_ARCH_DISASSEMBLE: 'v2' SBPF_ARCH_DUMP: 'v4' SBPF_ARCH_TEST: 'v3' - SBPF_CLI_REVISION: 'eb67fc5' + SBPF_CLI_REVISION: 'c52805f' SOLANA_VERSION: 'v3.1.2' TOOLS_VERSION_DISASSEMBLE: 'v1.52' TOOLS_VERSION_DUMP: 'v1.51' diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..974cb47d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,34 @@ +# Conventions + +## Self-Improvement + +After every task, check what was learned and route it: + +- Repeated instruction (corrected twice): add it to CLAUDE.md. +- Design decision: write or update a spec. +- Repeatable workflow: propose a skill. +- Must always happen: propose a hook. +- Learned pattern: save to memory. + +Before implementing anything non-trivial, write or update a spec and get +approval first. + +## Markdown + +- Use `-` for list markers. +- End list items with periods. +- Wrap lines to maximize use of the 80-column limit. Break at word boundaries, + not early. + +Every markdown change must pass (run both): + +```sh +cfg=cfg/pre-commit/quick-lint.yml +pre-commit run -c $cfg markdownlint-fix --files +pre-commit run -c $cfg mdformat --files +``` + +## Specs + +- `specs/` for cross-cutting concerns (build, CI, conventions). +- `examples/tree/specs/` for tree-specific design decisions. diff --git a/cfg/dictionary.txt b/cfg/dictionary.txt index fa6d00af..7568247e 100644 --- a/cfg/dictionary.txt +++ b/cfg/dictionary.txt @@ -16,8 +16,11 @@ lamports lddw ldxb ldxdw +ldxh ldxw +markdownlint mathjax +mdformat memcmp memcpy metas @@ -41,6 +44,7 @@ syscalls sysvar sysvars uninit +uoff usize vitepress xffff diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 1418e948..7a2152d4 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -40,6 +40,7 @@ export default { { text: "Fibonacci", link: "/examples/fibonacci" }, { text: "Transfer", link: "/examples/transfer" }, { text: "Counter", link: "/examples/counter" }, + { text: "Tree", link: "/examples/tree" }, ], }, { diff --git a/docs/src/examples/fibonacci.md b/docs/src/examples/fibonacci.md index c279ee7b..fb1fda1f 100644 --- a/docs/src/examples/fibonacci.md +++ b/docs/src/examples/fibonacci.md @@ -4,7 +4,7 @@ -## :1234: Fibonacci sequence background +## :1234: Background The [Fibonacci sequence] is a classic mathematical sequence where each number is the sum of the two preceding ones, typically starting with $F(0) = 0$ and diff --git a/docs/src/examples/memo.md b/docs/src/examples/memo.md index 1b0d5d8f..65b8c06c 100644 --- a/docs/src/examples/memo.md +++ b/docs/src/examples/memo.md @@ -4,7 +4,13 @@ -## :world_map: Memory map background +## :desktop_computer: Background + +This program logs arbitrary message data passed via instruction data. It accepts +no accounts and simply writes the provided bytes to the program log using the +[`sol_log_`] syscall. If any accounts are passed, the program returns an error. + +## :world_map: Memory map layout The [SBPF instruction set architecture] defines 12 registers, including 10 general-purpose registers `r0` through `r9`. At the start of instruction diff --git a/docs/src/examples/tree.md b/docs/src/examples/tree.md new file mode 100644 index 00000000..eed2f4ef --- /dev/null +++ b/docs/src/examples/tree.md @@ -0,0 +1,398 @@ +# Tree + + + +## :bulb: Background + +This example implements a [red-black tree][wikipedia tree page] in both +[SBPF assembly](../index.md) and Rust. Both implementations are compared +side-by-side with as much implementation parity as possible, using C-style Rust +(raw pointers, direct [syscalls](../indices/syscalls.md)) to minimize compiler +overhead. + +The example implements a full insertion algorithm and simple removal algorithms +(no rebalancing on remove). The full remove implementation is left as an +exercise to the reader. + +::: details Core data structures + +<<< ../../../examples/tree/artifacts/snippets/interface/tree-defs-common.txt{rs} + +::: + +Note that these data structures rely on direct addressing, which may be broken +by [ABI v2]. + +## :building_construction: Build support + +Constants, error codes, and C bindings are derived in a shared interface using +macros, then automatically inserted into the assembly program file at build +time. + +::: details Interface + +::: code-group + +<<< ../../../examples/tree/interface/src/common.rs{rs:line-numbers} + +<<< ../../../examples/tree/interface/src/asm.rs{rs:line-numbers} + +<<< ../../../examples/tree/interface/src/bindings.rs{rs:line-numbers} + +::: + +::: details `build.rs` + +<<< ../../../examples/tree/build.rs{rs:line-numbers} + +::: + +::: details Macros + +<<< ../../../examples/tree/macros/src/lib.rs{rs:line-numbers} + +::: + +## :twisted_rightwards_arrows: Entrypoint branching + +The Rust implementation does not use [`pinocchio`] for the entrypoint. Instead, +it uses C-style bindings with the [`SIMD-0321`] `r2` pointer. Note that the Rust +implementation already introduces overhead at this point in program flow due to +greedy [tail call optimizations][tail call]. + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/entrypoint-branching.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/entrypoint-branching.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + +## :rocket: Initialize + +The initialize operation creates a tree [PDA] for the entire program, then +invokes a [`CreateAccount` CPI](counter#cpi-construction), with the same +[fixed costs as in the counter example](counter#compute-unit-analysis). + +### :shield: Input checks + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/initialize-input-checks.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/initialize-input-checks.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +### :mag: PDA checks + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/initialize-pda-checks.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/initialize-pda-checks.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +### :hammer_and_wrench: Create account + +The assembly implementation includes pointer walkthrough optimizations that are +not available in Rust, since the compiler enforces +[instruction-level parallelism][ilp]. + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/initialize-create-account.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/initialize-create-account.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +## :heavy_plus_sign: Insert + +### :shield: Input checks + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-input-checks.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-input-checks.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +### :package: Allocate + +See [`AccountView::resize_unchecked`] for reference implementation. + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-allocate.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-allocate.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + + + +::: + + + +### :mag_right: Search + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-search.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-search.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +### :wrench: Insert fixup + + + +::: details Case 1 + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-fixup-case-1.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-fixup-case-1.txt{rs} [Rust] + +::: + +::: details Case 4 + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-fixup-case-4.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-fixup-case-4.txt{rs} [Rust] + +::: + +::: details Cases 5 and 6 (left) + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-l.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-l.txt{rs} [Rust] + +::: + +::: details Cases 5 and 6 (right) + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-r.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-r.txt{rs} [Rust] + +::: + +::: details Cases 2 and 3 + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/insert-fixup-case-2-3.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/insert-fixup-case-2-3.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + + + +::: + + + +## :scissors: Remove + +### :shield: Input checks + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/remove-input-checks.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/remove-input-checks.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +### :mag_right: Search + +::: details Implementations + +::: code-group + + + +<<< ../../../examples/tree/artifacts/snippets/asm/remove-search.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/remove-search.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +### :bookmark_tabs: Simple cases + + + +::: details Simple case 1: successor swap + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/remove-simple-1.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/remove-simple-1.txt{rs} [Rust] + +::: + +::: details Simple case 2: one-child replacement + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/remove-simple-2.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/remove-simple-2.txt{rs} [Rust] + +::: + +::: details Simple case 3: root leaf + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/remove-simple-3.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/remove-simple-3.txt{rs} [Rust] + +::: + +::: details Simple case 4: red leaf + +::: code-group + +<<< ../../../examples/tree/artifacts/snippets/asm/remove-simple-4.txt{asm} [Assembly] + +<<< ../../../examples/tree/artifacts/snippets/rs/remove-simple-4.txt{rs} [Rust] + +::: + +::: details Benchmarking + + + +::: + + + +[abi v2]: https://github.com/solana-foundation/solana-improvement-documents/pull/177/changes#r2861798899 +[ilp]: https://en.wikipedia.org/wiki/Instruction-level_parallelism +[pda]: https://solana.com/docs/core/pda +[tail call]: https://en.wikipedia.org/wiki/Tail_call +[wikipedia tree page]: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree +[`accountview::resize_unchecked`]: https://github.com/anza-xyz/solana-sdk/blob/bn254@v3.2.1/account-view/src/lib.rs#L363 +[`pinocchio`]: https://github.com/anza-xyz/pinocchio +[`simd-0321`]: https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0321-vm-r2-instruction-data-pointer.md diff --git a/docs/src/indices/opcodes.md b/docs/src/indices/opcodes.md index 87557174..a973ec41 100644 --- a/docs/src/indices/opcodes.md +++ b/docs/src/indices/opcodes.md @@ -9,17 +9,23 @@ example from this guide where it is used, as well as its corresponding | Opcode hex | Opcode name | Assembler mnemonic | Select example | | ---------- | ------------- | ---------------------------------- | -------------- | +| [`0x05`] | [`JA`] | [`ja off`][`0x05`] | [Tree] | | [`0x07`] | [`ADD64_IMM`] | [`add64 dst, imm`][`0x07`] | [Memo] | | [`0x14`] | [`SUB32_IMM`] | [`sub32 dst, imm`][`0x14`] | [Fibonacci] | | [`0x15`] | [`JEQ_IMM`] | [`jeq dst, imm, off`][`0x15`] | [Counter] | | [`0x17`] | [`SUB64_IMM`] | [`sub64 dst, imm`][`0x17`] | [Transfer] | | [`0x18`] | [`LD_DW_IMM`] | [`lddw dst, imm`][`0x18`] | [Quickstart] | +| [`0x1d`] | [`JEQ_REG`] | [`jeq dst, src, off`][`0x1d`] | [Tree] | | [`0x25`] | [`JGT_IMM`] | [`jgt dst, imm, off`][`0x25`] | [Fibonacci] | | [`0x27`] | [`MUL64_IMM`] | [`mul64 dst, imm`][`0x27`] | [Counter] | +| [`0x2d`] | [`JGT_REG`] | [`jgt dst, src, off`][`0x2d`] | [Tree] | | [`0x55`] | [`JNE_IMM`] | [`jne dst, imm, off`][`0x55`] | [Transfer] | | [`0x57`] | [`AND64_IMM`] | [`and64 dst, imm`][`0x57`] | [Counter] | | [`0x5d`] | [`JNE_REG`] | [`jne dst, src, off`][`0x5d`] | [Memo] | +| [`0x61`] | [`LD_W_REG`] | [`ldxw dst, [src + off]`][`0x61`] | [Tree] | +| [`0x62`] | [`ST_W_IMM`] | [`stw [dst + off], imm`][`0x62`] | [Tree] | | [`0x63`] | [`ST_W_REG`] | [`stxw [dst + off], src`][`0x63`] | [Transfer] | +| [`0x69`] | [`LD_H_REG`] | [`ldxh dst, [src + off]`][`0x69`] | [Tree] | | [`0x6a`] | [`ST_H_IMM`] | [`sth [dst + off], imm`][`0x6a`] | [Counter] | | [`0x71`] | [`LD_B_REG`] | [`ldxb dst, [src + off]`][`0x71`] | [Fibonacci] | | [`0x72`] | [`ST_B_IMM`] | [`stb [dst + off], imm`][`0x72`] | [Transfer] | @@ -29,6 +35,7 @@ example from this guide where it is used, as well as its corresponding | [`0x7b`] | [`ST_DW_REG`] | [`stxdw [dst + off], src`][`0x7b`] | [Transfer] | | [`0x85`] | [`CALL_IMM`] | [`call imm`][`0x85`] | [Quickstart] | | [`0x95`] | [`EXIT`] | [`exit`][`0x95`] | [Quickstart] | +| [`0xa5`] | [`JLT_IMM`] | [`jlt dst, imm, off`][`0xa5`] | [Tree] | | [`0xad`] | [`JLT_REG`] | [`jlt dst, src, off`][`0xad`] | [Transfer] | | [`0xb4`] | [`MOV32_IMM`] | [`mov32 dst, imm`][`0xb4`] | [Fibonacci] | | [`0xb7`] | [`MOV64_IMM`] | [`mov64 dst, imm`][`0xb7`] | [Fibonacci] | @@ -44,17 +51,24 @@ example from this guide where it is used, as well as its corresponding [rust implementation constant name]: https://docs.rs/solana-sbpf/latest/solana_sbpf/ebpf/index.html [sbpf bytecode isa]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md [transfer]: ../examples/transfer +[tree]: ../examples/tree +[`0x05`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L274 [`0x07`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L130 [`0x14`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L87 [`0x15`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L276 [`0x17`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L132 [`0x18`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L222 +[`0x1d`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L277 [`0x25`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L278 [`0x27`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L135 +[`0x2d`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L279 [`0x55`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L284 [`0x57`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L145 [`0x5d`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L285 +[`0x61`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L228 +[`0x62`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L237 [`0x63`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L246 +[`0x69`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L229 [`0x6a`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L238 [`0x71`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L230 [`0x72`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L239 @@ -64,6 +78,7 @@ example from this guide where it is used, as well as its corresponding [`0x7b`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L249 [`0x85`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L290 [`0x95`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L294 +[`0xa5`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L298 [`0xad`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L299 [`0xb4`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L117 [`0xb7`]: https://github.com/anza-xyz/sbpf/blob/v0.13.1/doc/bytecode.md?plain=1#L161 @@ -72,14 +87,20 @@ example from this guide where it is used, as well as its corresponding [`and64_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.AND64_IMM.html [`call_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.CALL_IMM.html [`exit`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.EXIT.html +[`ja`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JA.html [`jeq_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JEQ_IMM.html +[`jeq_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JEQ_REG.html [`jgt_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JGT_IMM.html +[`jgt_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JGT_REG.html +[`jlt_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JLT_IMM.html [`jlt_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JLT_REG.html [`jne_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JNE_IMM.html [`jne_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.JNE_REG.html [`ld_b_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.LD_B_REG.html [`ld_dw_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.LD_DW_IMM.html [`ld_dw_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.LD_DW_REG.html +[`ld_h_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.LD_H_REG.html +[`ld_w_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.LD_W_REG.html [`mov32_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.MOV32_IMM.html [`mov64_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.MOV64_IMM.html [`mov64_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.MOV64_REG.html @@ -89,6 +110,7 @@ example from this guide where it is used, as well as its corresponding [`st_dw_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.ST_DW_IMM.html [`st_dw_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.ST_DW_REG.html [`st_h_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.ST_H_IMM.html +[`st_w_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.ST_W_IMM.html [`st_w_reg`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.ST_W_REG.html [`sub32_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.SUB32_IMM.html [`sub64_imm`]: https://docs.rs/solana-sbpf/0.13.1/solana_sbpf/ebpf/constant.SUB64_IMM.html diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 1eb40022..ae7e8335 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -254,7 +254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -280,7 +280,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -355,7 +355,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -493,7 +493,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -517,6 +517,7 @@ dependencies = [ "regex", "solana-rent", "solana-sdk", + "tree-interface", ] [[package]] @@ -554,7 +555,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -627,7 +628,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -667,6 +668,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "counter" version = "0.1.0" @@ -767,7 +777,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -884,7 +894,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -929,7 +939,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -949,7 +959,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1255,7 +1265,7 @@ checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1378,6 +1388,16 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1576,7 +1596,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1718,9 +1738,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1731,6 +1751,7 @@ version = "0.1.0" dependencies = [ "pinocchio", "pinocchio-system", + "tree-interface", ] [[package]] @@ -1750,14 +1771,14 @@ checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2000,7 +2021,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2999,7 +3020,7 @@ dependencies = [ "bs58", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3419,9 +3440,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -3462,7 +3483,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3473,7 +3494,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3566,6 +3587,25 @@ dependencies = [ "test-utils", ] +[[package]] +name = "tree" +version = "0.1.0" +dependencies = [ + "mollusk-svm", + "pinocchio", + "solana-sdk", + "test-utils", + "tree-interface", +] + +[[package]] +name = "tree-interface" +version = "0.1.0" +dependencies = [ + "macros", + "pinocchio", +] + [[package]] name = "typenum" version = "1.19.0" @@ -3578,6 +3618,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unreachable" version = "1.0.0" @@ -3705,7 +3751,7 @@ checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3725,5 +3771,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] diff --git a/examples/Cargo.toml b/examples/Cargo.toml index a150c96a..0df48af5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,6 +7,9 @@ members = [ "hello-dasmac", "memo", "transfer", + "tree", + "tree/macros", + "tree/interface", "utils/build-examples", "utils/deps/build", "utils/deps/program" @@ -15,13 +18,18 @@ resolver = "2" [workspace.dependencies] cargo-manifest = "0.19.1" +convert_case = "0.11.0" fib-rs = "1.0.0" mollusk-svm = "0.10.1" pinocchio = {version = "0.10.1", features = ["cpi"]} pinocchio-system = "0.5.0" +proc-macro2 = "1.0.106" +quote = "1.0.44" regex = "1.12.2" solana-rent = "3.0.0" solana-sdk = "3.0.0" +syn = {version = "2.0.114", features = ["full"]} +tree-interface = {path = "tree/interface"} [workspace.lints.rust] unexpected_cfgs = {level = "warn", check-cfg = ['cfg(target_os, values("solana"))']} diff --git a/examples/counter/artifacts/tests/asm_expected_failures/result.txt b/examples/counter/artifacts/tests/asm_expected_failures/result.txt deleted file mode 100644 index f1bb1bc3..00000000 --- a/examples/counter/artifacts/tests/asm_expected_failures/result.txt +++ /dev/null @@ -1,18 +0,0 @@ -test tests::test_asm_expected_failures ... ok - Blocking waiting for file lock on package cache - Blocking waiting for file lock on package cache - Blocking waiting for file lock on package cache -warning: unused variable: `system_account` - --> counter/src/tests.rs:522:26 - | -522 | let (system_program, system_account) = program::keyed_account_for_system_program(); - | ^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_system_account` - | - = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default -warning: `counter` (lib test) generated 1 warning -[ ... DEBUG ... ] Program DASMAC... invoke [1] -[ ... DEBUG ... ] Program DASMAC... consumed 5 of 1400000 compute units -[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 -[ ... DEBUG ... ] Program DASMAC... invoke [1] -[ ... DEBUG ... ] Program DASMAC... consumed 5 of 1400000 compute units -[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 \ No newline at end of file diff --git a/examples/counter/artifacts/tests/asm_expected_failures/test.txt b/examples/counter/artifacts/tests/asm_expected_failures/test.txt deleted file mode 100644 index 13fe1d58..00000000 --- a/examples/counter/artifacts/tests/asm_expected_failures/test.txt +++ /dev/null @@ -1,27 +0,0 @@ -#[test] -fn test_asm_expected_failures() { - let setup = setup_test(ProgramLanguage::Assembly); - let (system_program, system_account) = program::keyed_account_for_system_program(); - - // Check no accounts. - setup.mollusk.process_and_validate_instruction( - &Instruction::new_with_bytes(setup.program_id, &[], vec![]), - &[], - &[Check::err(ProgramError::Custom( - constants().get("E_N_ACCOUNTS") as u32, - ))], - ); - - // Check too many accounts. - let n_accounts: usize = 4; - let account_metas = vec![AccountMeta::new_readonly(Pubkey::new_unique(), false); n_accounts]; - let account_infos = - vec![(account_metas[0].pubkey, Account::new(0, 0, &system_program),); n_accounts]; - setup.mollusk.process_and_validate_instruction( - &Instruction::new_with_bytes(setup.program_id, &[], account_metas), - &account_infos, - &[Check::err(ProgramError::Custom( - constants().get("E_N_ACCOUNTS") as u32, - ))], - ); -} \ No newline at end of file diff --git a/examples/counter/artifacts/tests/asm_pda_duplicate/result.txt b/examples/counter/artifacts/tests/asm_pda_duplicate/result.txt deleted file mode 100644 index 0a15b250..00000000 --- a/examples/counter/artifacts/tests/asm_pda_duplicate/result.txt +++ /dev/null @@ -1,15 +0,0 @@ -test tests::test_asm_pda_duplicate ... ok -warning: variant `Increment` is never constructed - --> counter/src/tests.rs:39:5 - | -37 | enum Operation { - | --------- variant in this enum -38 | Initialize, -39 | Increment, - | ^^^^^^^^^ - | - = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default -warning: `counter` (lib test) generated 1 warning -[ ... DEBUG ... ] Program DASMAC... invoke [1] -[ ... DEBUG ... ] Program DASMAC... consumed 9 of 1400000 compute units -[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 \ No newline at end of file diff --git a/examples/counter/artifacts/tests/asm_pda_duplicate/test.txt b/examples/counter/artifacts/tests/asm_pda_duplicate/test.txt deleted file mode 100644 index a64d7a03..00000000 --- a/examples/counter/artifacts/tests/asm_pda_duplicate/test.txt +++ /dev/null @@ -1,17 +0,0 @@ -#[test] -fn test_asm_pda_duplicate() { - let (setup, mut instruction, mut accounts, _checks) = - happy_path_setup(ProgramLanguage::Assembly, Operation::Initialize); - - instruction.accounts[AccountIndex::Pda as usize] = - instruction.accounts[AccountIndex::User as usize].clone(); - accounts[AccountIndex::Pda as usize] = accounts[AccountIndex::User as usize].clone(); - - setup.mollusk.process_and_validate_instruction( - &instruction, - &accounts, - &[Check::err(ProgramError::Custom( - constants().get("E_PDA_DUPLICATE") as u32, - ))], - ); -} \ No newline at end of file diff --git a/examples/counter/artifacts/tests/asm_system_program_duplicate/result.txt b/examples/counter/artifacts/tests/asm_system_program_duplicate/result.txt deleted file mode 100644 index 1f9fa530..00000000 --- a/examples/counter/artifacts/tests/asm_system_program_duplicate/result.txt +++ /dev/null @@ -1,15 +0,0 @@ -test tests::test_asm_system_program_duplicate ... ok -warning: variant `Increment` is never constructed - --> counter/src/tests.rs:39:5 - | -37 | enum Operation { - | --------- variant in this enum -38 | Initialize, -39 | Increment, - | ^^^^^^^^^ - | - = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default -warning: `counter` (lib test) generated 1 warning -[ ... DEBUG ... ] Program DASMAC... invoke [1] -[ ... DEBUG ... ] Program DASMAC... consumed 13 of 1400000 compute units -[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6 \ No newline at end of file diff --git a/examples/counter/artifacts/tests/asm_system_program_duplicate/test.txt b/examples/counter/artifacts/tests/asm_system_program_duplicate/test.txt deleted file mode 100644 index 4ef52f09..00000000 --- a/examples/counter/artifacts/tests/asm_system_program_duplicate/test.txt +++ /dev/null @@ -1,17 +0,0 @@ -#[test] -fn test_asm_system_program_duplicate() { - let (setup, mut instruction, mut accounts, _checks) = - happy_path_setup(ProgramLanguage::Assembly, Operation::Initialize); - - instruction.accounts[AccountIndex::SystemProgram as usize] = - instruction.accounts[AccountIndex::User as usize].clone(); - accounts[AccountIndex::SystemProgram as usize] = accounts[AccountIndex::User as usize].clone(); - - setup.mollusk.process_and_validate_instruction( - &instruction, - &accounts, - &[Check::err(ProgramError::Custom( - constants().get("E_SYSTEM_PROGRAM_DUPLICATE") as u32, - ))], - ); -} \ No newline at end of file diff --git a/examples/hello-dasmac/artifacts/dumps/asm.txt b/examples/hello-dasmac/artifacts/dumps/asm.txt index ef3ca6f7..754cdaca 100644 --- a/examples/hello-dasmac/artifacts/dumps/asm.txt +++ b/examples/hello-dasmac/artifacts/dumps/asm.txt @@ -43,7 +43,7 @@ There are 3 program headers, starting at offset 64 Program Headers Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align - LOAD 0x0000e8 0x00000000000000e8 0x00000000000000e8 0x00003e 0x00003e R E 0x1000 + LOAD 0x0000e8 0x00000000000000e8 0x00000000000000e8 0x000040 0x000040 R E 0x1000 LOAD 0x0001d8 0x00000000000001d8 0x00000000000001d8 0x000080 0x000080 R 0x1000 DYNAMIC 0x000128 0x0000000000000128 0x0000000000000128 0x0000b0 0x0000b0 RW 0x8 diff --git a/examples/transfer/artifacts/tests/account_offsets/result.txt b/examples/transfer/artifacts/tests/account_offsets/result.txt deleted file mode 100644 index fc6220f7..00000000 --- a/examples/transfer/artifacts/tests/account_offsets/result.txt +++ /dev/null @@ -1 +0,0 @@ -test tests::test_account_offsets ... ok \ No newline at end of file diff --git a/examples/transfer/artifacts/tests/account_offsets/test.txt b/examples/transfer/artifacts/tests/account_offsets/test.txt deleted file mode 100644 index d74d0d6a..00000000 --- a/examples/transfer/artifacts/tests/account_offsets/test.txt +++ /dev/null @@ -1,68 +0,0 @@ -#[test] -fn test_account_offsets() { - const MAX_PERMITTED_DATA_INCREASE: usize = 10240; - - #[allow(dead_code)] - #[repr(C)] - struct AccountLayout { - non_dup_marker: u8, - is_signer: u8, - is_writable: u8, - is_executable: u8, - padding: [u8; 4], - pubkey: [u8; 32], - owner: [u8; 32], - lamports: u64, - data_length: u64, - data_padded: [u8; PADDED_DATA_SIZE], - rent_epoch: u64, - } - - type StandardAccount = AccountLayout; - type SystemProgramAccount = AccountLayout<{ MAX_PERMITTED_DATA_INCREASE + 16 }>; - - // Sender. - const SENDER_OFFSET: usize = 8; - const SENDER_LAMPORTS_OFFSET: usize = 80; - const SENDER_DATA_LENGTH_OFFSET: usize = 88; - - // Recipient. - const RECIPIENT_OFFSET: usize = 10344; - const RECIPIENT_DATA_LENGTH_OFFSET: usize = 10424; - - // System program. - const SYSTEM_PROGRAM_OFFSET: usize = 20680; - - // Instruction data. - const INSTRUCTION_DATA_LENGTH_OFFSET: usize = 31032; - const INSTRUCTION_DATA_OFFSET: usize = 31040; - - assert_eq!( - SENDER_LAMPORTS_OFFSET, - SENDER_OFFSET + offset_of!(StandardAccount, lamports), - ); - assert_eq!( - SENDER_DATA_LENGTH_OFFSET, - SENDER_OFFSET + offset_of!(StandardAccount, data_length), - ); - assert_eq!( - RECIPIENT_OFFSET, - SENDER_OFFSET + size_of::() - ); - assert_eq!( - RECIPIENT_DATA_LENGTH_OFFSET, - RECIPIENT_OFFSET + offset_of!(StandardAccount, data_length), - ); - assert_eq!( - SYSTEM_PROGRAM_OFFSET, - RECIPIENT_OFFSET + size_of::() - ); - assert_eq!( - INSTRUCTION_DATA_LENGTH_OFFSET, - SYSTEM_PROGRAM_OFFSET + size_of::() - ); - assert_eq!( - INSTRUCTION_DATA_OFFSET, - INSTRUCTION_DATA_LENGTH_OFFSET + size_of::(), - ); -} \ No newline at end of file diff --git a/examples/transfer/artifacts/tests/offsets/result.txt b/examples/transfer/artifacts/tests/offsets/result.txt deleted file mode 100644 index 0b0bc214..00000000 --- a/examples/transfer/artifacts/tests/offsets/result.txt +++ /dev/null @@ -1 +0,0 @@ -test tests::test_offsets ... ok \ No newline at end of file diff --git a/examples/transfer/artifacts/tests/offsets/test.txt b/examples/transfer/artifacts/tests/offsets/test.txt deleted file mode 100644 index 44c137ba..00000000 --- a/examples/transfer/artifacts/tests/offsets/test.txt +++ /dev/null @@ -1,68 +0,0 @@ -#[test] -fn test_offsets() { - const MAX_PERMITTED_DATA_INCREASE: usize = 10240; - - #[allow(dead_code)] - #[repr(C)] - struct AccountLayout { - non_dup_marker: u8, - is_signer: u8, - is_writable: u8, - is_executable: u8, - padding: [u8; 4], - pubkey: [u8; 32], - owner: [u8; 32], - lamports: u64, - data_length: u64, - data_padded: [u8; PADDED_DATA_SIZE], - rent_epoch: u64, - } - - type StandardAccount = AccountLayout; - type SystemProgramAccount = AccountLayout<{ MAX_PERMITTED_DATA_INCREASE + 16 }>; - - // Sender. - const SENDER_OFFSET: usize = 8; - const SENDER_LAMPORTS_OFFSET: usize = 80; - const SENDER_DATA_LENGTH_OFFSET: usize = 88; - - // Recipient. - const RECIPIENT_OFFSET: usize = 10344; - const RECIPIENT_DATA_LENGTH_OFFSET: usize = 10424; - - // System program. - const SYSTEM_PROGRAM_OFFSET: usize = 20680; - - // Instruction data. - const INSTRUCTION_DATA_LENGTH_OFFSET: usize = 31032; - const INSTRUCTION_DATA_OFFSET: usize = 31040; - - assert_eq!( - SENDER_LAMPORTS_OFFSET, - SENDER_OFFSET + offset_of!(StandardAccount, lamports), - ); - assert_eq!( - SENDER_DATA_LENGTH_OFFSET, - SENDER_OFFSET + offset_of!(StandardAccount, data_length), - ); - assert_eq!( - RECIPIENT_OFFSET, - SENDER_OFFSET + size_of::() - ); - assert_eq!( - RECIPIENT_DATA_LENGTH_OFFSET, - RECIPIENT_OFFSET + offset_of!(StandardAccount, data_length), - ); - assert_eq!( - SYSTEM_PROGRAM_OFFSET, - RECIPIENT_OFFSET + size_of::() - ); - assert_eq!( - INSTRUCTION_DATA_LENGTH_OFFSET, - SYSTEM_PROGRAM_OFFSET + size_of::() - ); - assert_eq!( - INSTRUCTION_DATA_OFFSET, - INSTRUCTION_DATA_LENGTH_OFFSET + size_of::(), - ); -} \ No newline at end of file diff --git a/examples/tree/.gitignore b/examples/tree/.gitignore new file mode 100644 index 00000000..5c70e2f5 --- /dev/null +++ b/examples/tree/.gitignore @@ -0,0 +1,2 @@ +deploy/* +!deploy/tree-keypair.json diff --git a/examples/tree/Cargo.toml b/examples/tree/Cargo.toml new file mode 100644 index 00000000..1a901cb8 --- /dev/null +++ b/examples/tree/Cargo.toml @@ -0,0 +1,22 @@ +[build-dependencies] +tree-interface.workspace = true + +[dependencies] +pinocchio.workspace = true +tree-interface.workspace = true + +[dev-dependencies] +mollusk-svm.workspace = true +solana-sdk.workspace = true +test-utils.path = "../utils/test-utils" + +[lib] +crate-type = ["cdylib", "lib"] + +[lints] +workspace = true + +[package] +edition = "2021" +name = "tree" +version = "0.1.0" diff --git a/examples/tree/artifacts/dumps/asm.txt b/examples/tree/artifacts/dumps/asm.txt new file mode 100644 index 00000000..aecdf65a --- /dev/null +++ b/examples/tree/artifacts/dumps/asm.txt @@ -0,0 +1,584 @@ +tree.so +ELF Header + Magic 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 + Class ELF64 + Data 2's complement, little endian + Version 1 (current) + OS/ABI UNIX - System V + ABI Version 0 + Type DYN (Shared object file) + Machine EM_BPF + Version 0x1 + Entry point address 0xE8 + Start of program headers 64 (bytes into file) + Start of section headers 4680 (bytes into file) + Flags 0x0 + Size of this header 64 (bytes) + Size of program headers 56 (bytes) + Number of program headers 3 + Size of section headers 64 (bytes) + Number of section headers 7 + Section header string table index 6 +There are 7 section headers, starting at offset 0x1248 + +Section Headers + [Nr] Name Type Address Off Size ES Flg Lk Inf Al + [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 + [ 1] .text PROGBITS 00000000000000e8 0000e8 000fc0 00 AX 0 0 4 + [ 2] .dynamic DYNAMIC 00000000000010a8 0010a8 0000a0 10 WA 4 0 8 + [ 3] .dynsym DYNSYM 0000000000001148 001148 000060 18 A 4 1 8 + [ 4] .dynstr STRTAB 00000000000011a8 0011a8 000040 00 A 0 0 1 + [ 5] .rel.dyn REL 00000000000011e8 0011e8 000030 10 A 3 0 8 + [ 6] .s STRTAB 0000000000000000 001218 00002c 00 0 0 1 +Key to Flags + W (write), A (alloc), X (execute), M (merge), S (strings), I (info), + L (link order), O (extra OS processing required), G (group), T (TLS), + C (compressed), x (unknown), o (OS specific), E (exclude), + R (retain), p (processor specific) + +Elf file type is DYN (Shared object file) +Entry point 0xe8 +There are 3 program headers, starting at offset 64 + +Program Headers + Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align + LOAD 0x0000e8 0x00000000000000e8 0x00000000000000e8 0x000fc0 0x000fc0 R E 0x1000 + LOAD 0x001148 0x0000000000001148 0x0000000000001148 0x0000d0 0x0000d0 R 0x1000 + DYNAMIC 0x0010a8 0x00000000000010a8 0x00000000000010a8 0x0000a0 0x0000a0 RW 0x8 + + Section to Segment mapping + Segment Sections... + 00 .text + 01 .dynsym .dynstr .rel.dyn + 02 .dynamic + None .s +Dynamic section at offset 0x10a8 contains 10 entries + Tag Type Name/Value + 0x000000000000001e (FLAGS) TEXTREL + 0x0000000000000011 (REL) 0x11e8 + 0x0000000000000012 (RELSZ) 48 (bytes) + 0x0000000000000013 (RELENT) 16 (bytes) + 0x0000000000000006 (SYMTAB) 0x1148 + 0x000000000000000b (SYMENT) 24 (bytes) + 0x0000000000000005 (STRTAB) 0x11a8 + 0x000000000000000a (STRSZ) 64 (bytes) + 0x0000000000000016 (TEXTREL) 0x0 + 0x0000000000000000 (NULL) 0x0 + +Relocation section '.rel.dyn' at offset 0x11e8 contains 3 entries + Offset Info Type Symbol's Value Symbol's Name +0000000000000248 000000030000000a R_BPF_64_32 0000000000000000 sol_try_find_program_address +0000000000000468 000000020000000a R_BPF_64_32 0000000000000000 sol_invoke_signed_c +0000000000000708 000000020000000a R_BPF_64_32 0000000000000000 sol_invoke_signed_c + +Symbol table '.dynsym' contains 4 entries + Num Value Size Type Bind Vis Ndx Name + 0 0000000000000000 0 NOTYPE LOCAL DEFAULT UND + 1 00000000000000e8 0 NOTYPE GLOBAL DEFAULT 1 entrypoint + 2 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sol_invoke_signed_c + 3 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sol_try_find_program_address +There are no section groups in this file. + +tree.so file format elf64-bpf + +Disassembly of section .text + +00000000000000e8 + 29 79 29 f8 ff 00 00 00 00 r9 = *(u64 *)(r2 - 0x8) + 30 79 18 00 00 00 00 00 00 r8 = *(u64 *)(r1 + 0x0) + 31 71 27 00 00 00 00 00 00 r7 = *(u8 *)(r2 + 0x0) + 32 15 07 71 00 01 00 00 00 if r7 == 0x1 goto +0x71 + 33 15 07 46 01 02 00 00 00 if r7 == 0x2 goto +0x146 + 34 15 07 02 00 00 00 00 00 if r7 == 0x0 goto +0x2 + 35 b7 00 00 00 0b 00 00 00 r0 = 0xb + 36 95 00 00 00 00 00 00 00 exit + 37 55 09 d9 01 01 00 00 00 if r9 != 0x1 goto +0x1d9 + 38 55 08 da 01 04 00 00 00 if r8 != 0x4 goto +0x1da + 39 79 19 58 00 00 00 00 00 r9 = *(u64 *)(r1 + 0x58) + 40 55 09 ea 01 00 00 00 00 if r9 != 0x0 goto +0x1ea + 41 71 19 68 28 00 00 00 00 r9 = *(u8 *)(r1 + 0x2868) + 42 55 09 e6 01 ff 00 00 00 if r9 != 0xff goto +0x1e6 + 43 79 19 b8 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x28b8) + 44 55 09 e2 01 00 00 00 00 if r9 != 0x0 goto +0x1e2 + 45 71 19 c8 50 00 00 00 00 r9 = *(u8 *)(r1 + 0x50c8) + 46 55 09 de 01 ff 00 00 00 if r9 != 0xff goto +0x1de + 47 79 19 18 51 00 00 00 00 r9 = *(u64 *)(r1 + 0x5118) + 48 55 09 da 01 0e 00 00 00 if r9 != 0xe goto +0x1da + 49 71 19 38 79 00 00 00 00 r9 = *(u8 *)(r1 + 0x7938) + 50 55 09 d6 01 ff 00 00 00 if r9 != 0xff goto +0x1d6 + 51 79 19 40 79 00 00 00 00 r9 = *(u64 *)(r1 + 0x7940) + 52 18 08 00 00 06 a7 d5 17 00 00 00 00 19 2c 5c 51 r8 = 0x515c2c1917d5a706 ll + 54 5d 89 d0 01 00 00 00 00 if r9 != r8 goto +0x1d0 + 55 79 19 48 79 00 00 00 00 r9 = *(u64 *)(r1 + 0x7948) + 56 18 08 00 00 21 8c c9 4c 00 00 00 00 3d 4a f1 7f r8 = 0x7ff14a3d4cc98c21 ll + 58 5d 89 cc 01 00 00 00 00 if r9 != r8 goto +0x1cc + 59 79 19 50 79 00 00 00 00 r9 = *(u64 *)(r1 + 0x7950) + 60 18 08 00 00 58 da ee 08 00 00 00 00 9b a1 fd 44 r8 = 0x44fda19b08eeda58 ll + 62 5d 89 c8 01 00 00 00 00 if r9 != r8 goto +0x1c8 + 63 79 19 58 79 00 00 00 00 r9 = *(u64 *)(r1 + 0x7958) + 64 b4 08 00 00 e3 db d9 8a w8 = -0x7526241d + 65 5d 89 c5 01 00 00 00 00 if r9 != r8 goto +0x1c5 + 66 b7 02 00 00 00 00 00 00 r2 = 0x0 + 67 bf 13 00 00 00 00 00 00 r3 = r1 + 68 07 03 00 00 b9 a1 00 00 r3 += 0xa1b9 + 69 bf a4 00 00 00 00 00 00 r4 = r10 + 70 07 04 00 00 b0 ff ff ff r4 += -0x50 + 71 bf a5 00 00 00 00 00 00 r5 = r10 + 72 07 05 00 00 a0 fe ff ff r5 += -0x160 + 73 85 10 00 00 ff ff ff ff call -0x1 + 74 79 19 70 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x2870) + 75 79 48 00 00 00 00 00 00 r8 = *(u64 *)(r4 + 0x0) + 76 5d 89 b8 01 00 00 00 00 if r9 != r8 goto +0x1b8 + 77 79 19 78 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x2878) + 78 79 48 08 00 00 00 00 00 r8 = *(u64 *)(r4 + 0x8) + 79 5d 89 b5 01 00 00 00 00 if r9 != r8 goto +0x1b5 + 80 79 19 80 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x2880) + 81 79 48 10 00 00 00 00 00 r8 = *(u64 *)(r4 + 0x10) + 82 5d 89 b2 01 00 00 00 00 if r9 != r8 goto +0x1b2 + 83 79 19 88 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x2888) + 84 79 48 18 00 00 00 00 00 r8 = *(u64 *)(r4 + 0x18) + 85 5d 89 af 01 00 00 00 00 if r9 != r8 goto +0x1af + 86 7a 0a e8 fe 02 00 00 00 *(u64 *)(r10 - 0x118) = 0x2 + 87 7a 0a f8 fe 34 00 00 00 *(u64 *)(r10 - 0x108) = 0x34 + 88 79 19 90 79 00 00 00 00 r9 = *(u64 *)(r1 + 0x7990) + 89 27 09 00 00 98 00 00 00 r9 *= 0x98 + 90 7b 9a a5 fe 00 00 00 00 *(u64 *)(r10 - 0x15b) = r9 + 91 7a 0a ad fe 18 00 00 00 *(u64 *)(r10 - 0x153) = 0x18 + 92 79 39 00 00 00 00 00 00 r9 = *(u64 *)(r3 + 0x0) + 93 7b 9a b5 fe 00 00 00 00 *(u64 *)(r10 - 0x14b) = r9 + 94 79 39 08 00 00 00 00 00 r9 = *(u64 *)(r3 + 0x8) + 95 7b 9a bd fe 00 00 00 00 *(u64 *)(r10 - 0x143) = r9 + 96 79 39 10 00 00 00 00 00 r9 = *(u64 *)(r3 + 0x10) + 97 7b 9a c5 fe 00 00 00 00 *(u64 *)(r10 - 0x13b) = r9 + 98 79 39 18 00 00 00 00 00 r9 = *(u64 *)(r3 + 0x18) + 99 7b 9a cd fe 00 00 00 00 *(u64 *)(r10 - 0x133) = r9 + 100 6a 0a 08 ff 01 01 00 00 *(u16 *)(r10 - 0xf8) = 0x101 + 101 6a 0a 18 ff 01 01 00 00 *(u16 *)(r10 - 0xe8) = 0x101 + 102 6a 0a 50 ff 01 01 00 00 *(u16 *)(r10 - 0xb0) = 0x101 + 103 6a 0a 88 ff 01 01 00 00 *(u16 *)(r10 - 0x78) = 0x101 + 104 7b 5a a0 ff 00 00 00 00 *(u64 *)(r10 - 0x60) = r5 + 105 7a 0a a8 ff 01 00 00 00 *(u64 *)(r10 - 0x58) = 0x1 + 106 7a 0a 98 ff 01 00 00 00 *(u64 *)(r10 - 0x68) = 0x1 + 107 07 01 00 00 10 00 00 00 r1 += 0x10 + 108 7b 1a 00 ff 00 00 00 00 *(u64 *)(r10 - 0x100) = r1 + 109 7b 1a 20 ff 00 00 00 00 *(u64 *)(r10 - 0xe0) = r1 + 110 07 01 00 00 20 00 00 00 r1 += 0x20 + 111 7b 1a 40 ff 00 00 00 00 *(u64 *)(r10 - 0xc0) = r1 + 112 07 01 00 00 20 00 00 00 r1 += 0x20 + 113 7b 1a 28 ff 00 00 00 00 *(u64 *)(r10 - 0xd8) = r1 + 114 07 01 00 00 10 00 00 00 r1 += 0x10 + 115 7b 1a 38 ff 00 00 00 00 *(u64 *)(r10 - 0xc8) = r1 + 116 07 01 00 00 10 28 00 00 r1 += 0x2810 + 117 7b 1a 10 ff 00 00 00 00 *(u64 *)(r10 - 0xf0) = r1 + 118 7b 1a 58 ff 00 00 00 00 *(u64 *)(r10 - 0xa8) = r1 + 119 07 01 00 00 20 00 00 00 r1 += 0x20 + 120 7b 1a 78 ff 00 00 00 00 *(u64 *)(r10 - 0x88) = r1 + 121 07 01 00 00 20 00 00 00 r1 += 0x20 + 122 7b 1a 60 ff 00 00 00 00 *(u64 *)(r10 - 0xa0) = r1 + 123 07 01 00 00 10 00 00 00 r1 += 0x10 + 124 7b 1a 70 ff 00 00 00 00 *(u64 *)(r10 - 0x90) = r1 + 125 bf 16 00 00 00 00 00 00 r6 = r1 + 126 07 04 00 00 30 00 00 00 r4 += 0x30 + 127 7b 4a d8 fe 00 00 00 00 *(u64 *)(r10 - 0x128) = r4 + 128 07 04 00 00 20 ff ff ff r4 += -0xe0 + 129 7b 4a e0 fe 00 00 00 00 *(u64 *)(r10 - 0x120) = r4 + 130 07 04 00 00 a1 ff ff ff r4 += -0x5f + 131 7b 4a f0 fe 00 00 00 00 *(u64 *)(r10 - 0x110) = r4 + 132 07 04 00 00 ff 00 00 00 r4 += 0xff + 133 7b 4a 90 ff 00 00 00 00 *(u64 *)(r10 - 0x70) = r4 + 134 07 04 00 00 f0 ff ff ff r4 += -0x10 + 135 bf a1 00 00 00 00 00 00 r1 = r10 + 136 07 01 00 00 d8 fe ff ff r1 += -0x128 + 137 bf a2 00 00 00 00 00 00 r2 = r10 + 138 07 02 00 00 20 ff ff ff r2 += -0xe0 + 139 b7 03 00 00 02 00 00 00 r3 = 0x2 + 140 b7 05 00 00 01 00 00 00 r5 = 0x1 + 141 85 10 00 00 ff ff ff ff call -0x1 + 142 bf 67 00 00 00 00 00 00 r7 = r6 + 143 07 07 00 00 18 00 00 00 r7 += 0x18 + 144 7b 76 10 00 00 00 00 00 *(u64 *)(r6 + 0x10) = r7 + 145 95 00 00 00 00 00 00 00 exit + 146 55 09 6c 01 05 00 00 00 if r9 != 0x5 goto +0x16c + 147 a5 08 6d 01 02 00 00 00 if r8 < 0x2 goto +0x16d + 148 79 19 58 00 00 00 00 00 r9 = *(u64 *)(r1 + 0x58) + 149 55 09 7d 01 00 00 00 00 if r9 != 0x0 goto +0x17d + 150 71 19 68 28 00 00 00 00 r9 = *(u8 *)(r1 + 0x2868) + 151 55 09 79 01 ff 00 00 00 if r9 != 0xff goto +0x179 + 152 79 19 c8 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x28c8) + 153 55 09 51 00 00 00 00 00 if r9 != 0x0 goto +0x51 + 154 55 08 68 01 04 00 00 00 if r8 != 0x4 goto +0x168 + 155 79 19 b8 28 00 00 00 00 r9 = *(u64 *)(r1 + 0x28b8) + 156 7b 9a 68 ff 00 00 00 00 *(u64 *)(r10 - 0x98) = r9 + 157 bf 97 00 00 00 00 00 00 r7 = r9 + 158 07 09 00 00 07 00 00 00 r9 += 0x7 + 159 57 09 00 00 f8 ff ff ff r9 &= -0x8 + 160 0f 19 00 00 00 00 00 00 r9 += r1 + 161 71 98 c8 50 00 00 00 00 r8 = *(u8 *)(r9 + 0x50c8) + 162 55 08 6a 01 ff 00 00 00 if r8 != 0xff goto +0x16a + 163 79 98 18 51 00 00 00 00 r8 = *(u64 *)(r9 + 0x5118) + 164 55 08 66 01 0e 00 00 00 if r8 != 0xe goto +0x166 + 165 71 98 38 79 00 00 00 00 r8 = *(u8 *)(r9 + 0x7938) + 166 55 08 62 01 ff 00 00 00 if r8 != 0xff goto +0x162 + 167 79 98 40 79 00 00 00 00 r8 = *(u64 *)(r9 + 0x7940) + 168 18 04 00 00 06 a7 d5 17 00 00 00 00 19 2c 5c 51 r4 = 0x515c2c1917d5a706 ll + 170 5d 48 5c 01 00 00 00 00 if r8 != r4 goto +0x15c + 171 79 98 48 79 00 00 00 00 r8 = *(u64 *)(r9 + 0x7948) + 172 18 04 00 00 21 8c c9 4c 00 00 00 00 3d 4a f1 7f r4 = 0x7ff14a3d4cc98c21 ll + 174 5d 48 58 01 00 00 00 00 if r8 != r4 goto +0x158 + 175 79 98 50 79 00 00 00 00 r8 = *(u64 *)(r9 + 0x7950) + 176 18 04 00 00 58 da ee 08 00 00 00 00 9b a1 fd 44 r4 = 0x44fda19b08eeda58 ll + 178 5d 48 54 01 00 00 00 00 if r8 != r4 goto +0x154 + 179 79 98 58 79 00 00 00 00 r8 = *(u64 *)(r9 + 0x7958) + 180 b4 04 00 00 e3 db d9 8a w4 = -0x7526241d + 181 5d 48 51 01 00 00 00 00 if r8 != r4 goto +0x151 + 182 79 98 90 79 00 00 00 00 r8 = *(u64 *)(r9 + 0x7990) + 183 27 08 00 00 1d 00 00 00 r8 *= 0x1d + 184 62 0a a1 fe 02 00 00 00 *(u32 *)(r10 - 0x15f) = 0x2 + 185 7b 8a a5 fe 00 00 00 00 *(u64 *)(r10 - 0x15b) = r8 + 186 7a 0a e8 fe 02 00 00 00 *(u64 *)(r10 - 0x118) = 0x2 + 187 7a 0a f8 fe 0c 00 00 00 *(u64 *)(r10 - 0x108) = 0xc + 188 6a 0a 08 ff 01 01 00 00 *(u16 *)(r10 - 0xf8) = 0x101 + 189 72 0a 18 ff 01 00 00 00 *(u8 *)(r10 - 0xe8) = 0x1 + 190 6a 0a 50 ff 01 01 00 00 *(u16 *)(r10 - 0xb0) = 0x101 + 191 72 0a 89 ff 01 00 00 00 *(u8 *)(r10 - 0x77) = 0x1 + 192 bf 16 00 00 00 00 00 00 r6 = r1 + 193 07 01 00 00 10 00 00 00 r1 += 0x10 + 194 7b 1a 00 ff 00 00 00 00 *(u64 *)(r10 - 0x100) = r1 + 195 7b 1a 20 ff 00 00 00 00 *(u64 *)(r10 - 0xe0) = r1 + 196 07 01 00 00 20 00 00 00 r1 += 0x20 + 197 7b 1a 40 ff 00 00 00 00 *(u64 *)(r10 - 0xc0) = r1 + 198 07 01 00 00 20 00 00 00 r1 += 0x20 + 199 7b 1a 28 ff 00 00 00 00 *(u64 *)(r10 - 0xd8) = r1 + 200 07 01 00 00 10 00 00 00 r1 += 0x10 + 201 7b 1a 38 ff 00 00 00 00 *(u64 *)(r10 - 0xc8) = r1 + 202 07 01 00 00 10 28 00 00 r1 += 0x2810 + 203 7b 1a 10 ff 00 00 00 00 *(u64 *)(r10 - 0xf0) = r1 + 204 7b 1a 58 ff 00 00 00 00 *(u64 *)(r10 - 0xa8) = r1 + 205 07 01 00 00 20 00 00 00 r1 += 0x20 + 206 7b 1a 78 ff 00 00 00 00 *(u64 *)(r10 - 0x88) = r1 + 207 07 01 00 00 20 00 00 00 r1 += 0x20 + 208 7b 1a 60 ff 00 00 00 00 *(u64 *)(r10 - 0xa0) = r1 + 209 07 01 00 00 10 00 00 00 r1 += 0x10 + 210 7b 1a 70 ff 00 00 00 00 *(u64 *)(r10 - 0x90) = r1 + 211 bf a4 00 00 00 00 00 00 r4 = r10 + 212 07 04 00 00 e0 ff ff ff r4 += -0x20 + 213 7b 4a d8 fe 00 00 00 00 *(u64 *)(r10 - 0x128) = r4 + 214 07 04 00 00 20 ff ff ff r4 += -0xe0 + 215 7b 4a e0 fe 00 00 00 00 *(u64 *)(r10 - 0x120) = r4 + 216 07 04 00 00 a1 ff ff ff r4 += -0x5f + 217 7b 4a f0 fe 00 00 00 00 *(u64 *)(r10 - 0x110) = r4 + 218 bf a1 00 00 00 00 00 00 r1 = r10 + 219 07 01 00 00 d8 fe ff ff r1 += -0x128 + 220 bf 28 00 00 00 00 00 00 r8 = r2 + 221 bf a2 00 00 00 00 00 00 r2 = r10 + 222 07 02 00 00 20 ff ff ff r2 += -0xe0 + 223 b7 03 00 00 02 00 00 00 r3 = 0x2 + 224 b7 05 00 00 00 00 00 00 r5 = 0x0 + 225 85 10 00 00 ff ff ff ff call -0x1 + 226 bf 82 00 00 00 00 00 00 r2 = r8 + 227 bf 61 00 00 00 00 00 00 r1 = r6 + 228 07 07 00 00 1d 00 00 00 r7 += 0x1d + 229 7b 71 b8 28 00 00 00 00 *(u64 *)(r1 + 0x28b8) = r7 + 230 79 17 d0 28 00 00 00 00 r7 = *(u64 *)(r1 + 0x28d0) + 231 bf 79 00 00 00 00 00 00 r9 = r7 + 232 07 07 00 00 1d 00 00 00 r7 += 0x1d + 233 7b 71 d0 28 00 00 00 00 *(u64 *)(r1 + 0x28d0) = r7 + 234 05 00 02 00 00 00 00 00 goto +0x2 + 235 79 98 00 00 00 00 00 00 r8 = *(u64 *)(r9 + 0x0) + 236 7b 81 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r8 + 237 61 24 01 00 00 00 00 00 r4 = *(u32 *)(r2 + 0x1) + 238 63 49 18 00 00 00 00 00 *(u32 *)(r9 + 0x18) = r4 + 239 69 24 01 00 00 00 00 00 r4 = *(u16 *)(r2 + 0x1) + 240 79 13 c0 28 00 00 00 00 r3 = *(u64 *)(r1 + 0x28c0) + 241 15 03 06 00 00 00 00 00 if r3 == 0x0 goto +0x6 + 242 bf 32 00 00 00 00 00 00 r2 = r3 + 243 69 35 18 00 00 00 00 00 r5 = *(u16 *)(r3 + 0x18) + 244 ad 54 08 00 00 00 00 00 if r4 < r5 goto +0x8 + 245 2d 54 0f 00 00 00 00 00 if r4 > r5 goto +0xf + 246 b7 00 00 00 0e 00 00 00 r0 = 0xe + 247 95 00 00 00 00 00 00 00 exit + 248 72 09 1c 00 01 00 00 00 *(u8 *)(r9 + 0x1c) = 0x1 + 249 b7 02 00 00 00 00 00 00 r2 = 0x0 + 250 7b 29 00 00 00 00 00 00 *(u64 *)(r9 + 0x0) = r2 + 251 7b 91 c0 28 00 00 00 00 *(u64 *)(r1 + 0x28c0) = r9 + 252 95 00 00 00 00 00 00 00 exit + 253 79 23 08 00 00 00 00 00 r3 = *(u64 *)(r2 + 0x8) + 254 55 03 f3 ff 00 00 00 00 if r3 != 0x0 goto -0xd + 255 72 09 1c 00 01 00 00 00 *(u8 *)(r9 + 0x1c) = 0x1 + 256 7b 29 00 00 00 00 00 00 *(u64 *)(r9 + 0x0) = r2 + 257 7b 92 08 00 00 00 00 00 *(u64 *)(r2 + 0x8) = r9 + 258 71 26 1c 00 00 00 00 00 r6 = *(u8 *)(r2 + 0x1c) + 259 55 06 09 00 00 00 00 00 if r6 != 0x0 goto +0x9 + 260 95 00 00 00 00 00 00 00 exit + 261 79 23 10 00 00 00 00 00 r3 = *(u64 *)(r2 + 0x10) + 262 55 03 eb ff 00 00 00 00 if r3 != 0x0 goto -0x15 + 263 72 09 1c 00 01 00 00 00 *(u8 *)(r9 + 0x1c) = 0x1 + 264 7b 29 00 00 00 00 00 00 *(u64 *)(r9 + 0x0) = r2 + 265 7b 92 10 00 00 00 00 00 *(u64 *)(r2 + 0x10) = r9 + 266 71 26 1c 00 00 00 00 00 r6 = *(u8 *)(r2 + 0x1c) + 267 55 06 01 00 00 00 00 00 if r6 != 0x0 goto +0x1 + 268 95 00 00 00 00 00 00 00 exit + 269 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0x0) + 270 55 03 02 00 00 00 00 00 if r3 != 0x0 goto +0x2 + 271 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 272 95 00 00 00 00 00 00 00 exit + 273 69 34 18 00 00 00 00 00 r4 = *(u16 *)(r3 + 0x18) + 274 2d 45 27 00 00 00 00 00 if r5 > r4 goto +0x27 + 275 79 37 10 00 00 00 00 00 r7 = *(u64 *)(r3 + 0x10) + 276 15 07 02 00 00 00 00 00 if r7 == 0x0 goto +0x2 + 277 71 78 1c 00 00 00 00 00 r8 = *(u8 *)(r7 + 0x1c) + 278 55 08 4a 00 00 00 00 00 if r8 != 0x0 goto +0x4a + 279 79 26 10 00 00 00 00 00 r6 = *(u64 *)(r2 + 0x10) + 280 5d 69 0a 00 00 00 00 00 if r9 != r6 goto +0xa + 281 79 68 08 00 00 00 00 00 r8 = *(u64 *)(r6 + 0x8) + 282 7b 82 10 00 00 00 00 00 *(u64 *)(r2 + 0x10) = r8 + 283 15 08 01 00 00 00 00 00 if r8 == 0x0 goto +0x1 + 284 7b 28 00 00 00 00 00 00 *(u64 *)(r8 + 0x0) = r2 + 285 7b 26 08 00 00 00 00 00 *(u64 *)(r6 + 0x8) = r2 + 286 7b 36 00 00 00 00 00 00 *(u64 *)(r6 + 0x0) = r3 + 287 7b 62 00 00 00 00 00 00 *(u64 *)(r2 + 0x0) = r6 + 288 7b 63 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = r6 + 289 bf 29 00 00 00 00 00 00 r9 = r2 + 290 bf 62 00 00 00 00 00 00 r2 = r6 + 291 79 34 00 00 00 00 00 00 r4 = *(u64 *)(r3 + 0x0) + 292 79 28 10 00 00 00 00 00 r8 = *(u64 *)(r2 + 0x10) + 293 7b 83 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = r8 + 294 15 08 01 00 00 00 00 00 if r8 == 0x0 goto +0x1 + 295 7b 38 00 00 00 00 00 00 *(u64 *)(r8 + 0x0) = r3 + 296 7b 32 10 00 00 00 00 00 *(u64 *)(r2 + 0x10) = r3 + 297 7b 42 00 00 00 00 00 00 *(u64 *)(r2 + 0x0) = r4 + 298 7b 23 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r2 + 299 15 04 0a 00 00 00 00 00 if r4 == 0x0 goto +0xa + 300 79 48 10 00 00 00 00 00 r8 = *(u64 *)(r4 + 0x10) + 301 5d 83 04 00 00 00 00 00 if r3 != r8 goto +0x4 + 302 7b 24 10 00 00 00 00 00 *(u64 *)(r4 + 0x10) = r2 + 303 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 304 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 305 95 00 00 00 00 00 00 00 exit + 306 7b 24 08 00 00 00 00 00 *(u64 *)(r4 + 0x8) = r2 + 307 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 308 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 309 95 00 00 00 00 00 00 00 exit + 310 7b 21 c0 28 00 00 00 00 *(u64 *)(r1 + 0x28c0) = r2 + 311 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 312 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 313 95 00 00 00 00 00 00 00 exit + 314 79 37 08 00 00 00 00 00 r7 = *(u64 *)(r3 + 0x8) + 315 15 07 02 00 00 00 00 00 if r7 == 0x0 goto +0x2 + 316 71 78 1c 00 00 00 00 00 r8 = *(u8 *)(r7 + 0x1c) + 317 55 08 23 00 00 00 00 00 if r8 != 0x0 goto +0x23 + 318 79 26 08 00 00 00 00 00 r6 = *(u64 *)(r2 + 0x8) + 319 5d 69 0a 00 00 00 00 00 if r9 != r6 goto +0xa + 320 79 68 10 00 00 00 00 00 r8 = *(u64 *)(r6 + 0x10) + 321 7b 82 08 00 00 00 00 00 *(u64 *)(r2 + 0x8) = r8 + 322 15 08 01 00 00 00 00 00 if r8 == 0x0 goto +0x1 + 323 7b 28 00 00 00 00 00 00 *(u64 *)(r8 + 0x0) = r2 + 324 7b 26 10 00 00 00 00 00 *(u64 *)(r6 + 0x10) = r2 + 325 7b 36 00 00 00 00 00 00 *(u64 *)(r6 + 0x0) = r3 + 326 7b 62 00 00 00 00 00 00 *(u64 *)(r2 + 0x0) = r6 + 327 7b 63 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = r6 + 328 bf 29 00 00 00 00 00 00 r9 = r2 + 329 bf 62 00 00 00 00 00 00 r2 = r6 + 330 79 34 00 00 00 00 00 00 r4 = *(u64 *)(r3 + 0x0) + 331 79 28 08 00 00 00 00 00 r8 = *(u64 *)(r2 + 0x8) + 332 7b 83 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = r8 + 333 15 08 01 00 00 00 00 00 if r8 == 0x0 goto +0x1 + 334 7b 38 00 00 00 00 00 00 *(u64 *)(r8 + 0x0) = r3 + 335 7b 32 08 00 00 00 00 00 *(u64 *)(r2 + 0x8) = r3 + 336 7b 42 00 00 00 00 00 00 *(u64 *)(r2 + 0x0) = r4 + 337 7b 23 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r2 + 338 15 04 0a 00 00 00 00 00 if r4 == 0x0 goto +0xa + 339 79 48 10 00 00 00 00 00 r8 = *(u64 *)(r4 + 0x10) + 340 5d 83 04 00 00 00 00 00 if r3 != r8 goto +0x4 + 341 7b 24 10 00 00 00 00 00 *(u64 *)(r4 + 0x10) = r2 + 342 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 343 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 344 95 00 00 00 00 00 00 00 exit + 345 7b 24 08 00 00 00 00 00 *(u64 *)(r4 + 0x8) = r2 + 346 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 347 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 348 95 00 00 00 00 00 00 00 exit + 349 7b 21 c0 28 00 00 00 00 *(u64 *)(r1 + 0x28c0) = r2 + 350 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 351 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 352 95 00 00 00 00 00 00 00 exit + 353 72 02 1c 00 00 00 00 00 *(u8 *)(r2 + 0x1c) = 0x0 + 354 72 07 1c 00 00 00 00 00 *(u8 *)(r7 + 0x1c) = 0x0 + 355 72 03 1c 00 01 00 00 00 *(u8 *)(r3 + 0x1c) = 0x1 + 356 bf 39 00 00 00 00 00 00 r9 = r3 + 357 79 92 00 00 00 00 00 00 r2 = *(u64 *)(r9 + 0x0) + 358 55 02 a3 ff 00 00 00 00 if r2 != 0x0 goto -0x5d + 359 95 00 00 00 00 00 00 00 exit + 360 55 09 96 00 03 00 00 00 if r9 != 0x3 goto +0x96 + 361 a5 08 97 00 02 00 00 00 if r8 < 0x2 goto +0x97 + 362 79 19 58 00 00 00 00 00 r9 = *(u64 *)(r1 + 0x58) + 363 55 09 a7 00 00 00 00 00 if r9 != 0x0 goto +0xa7 + 364 71 19 68 28 00 00 00 00 r9 = *(u8 *)(r1 + 0x2868) + 365 55 09 a3 00 ff 00 00 00 if r9 != 0xff goto +0xa3 + 366 79 13 c0 28 00 00 00 00 r3 = *(u64 *)(r1 + 0x28c0) + 367 15 03 0a 00 00 00 00 00 if r3 == 0x0 goto +0xa + 368 69 24 01 00 00 00 00 00 r4 = *(u16 *)(r2 + 0x1) + 369 69 35 18 00 00 00 00 00 r5 = *(u16 *)(r3 + 0x18) + 370 1d 54 09 00 00 00 00 00 if r4 == r5 goto +0x9 + 371 2d 54 04 00 00 00 00 00 if r4 > r5 goto +0x4 + 372 79 33 08 00 00 00 00 00 r3 = *(u64 *)(r3 + 0x8) + 373 55 03 fb ff 00 00 00 00 if r3 != 0x0 goto -0x5 + 374 b7 00 00 00 0f 00 00 00 r0 = 0xf + 375 95 00 00 00 00 00 00 00 exit + 376 79 33 10 00 00 00 00 00 r3 = *(u64 *)(r3 + 0x10) + 377 55 03 f7 ff 00 00 00 00 if r3 != 0x0 goto -0x9 + 378 b7 00 00 00 0f 00 00 00 r0 = 0xf + 379 95 00 00 00 00 00 00 00 exit + 380 79 34 08 00 00 00 00 00 r4 = *(u64 *)(r3 + 0x8) + 381 15 04 09 00 00 00 00 00 if r4 == 0x0 goto +0x9 + 382 79 35 10 00 00 00 00 00 r5 = *(u64 *)(r3 + 0x10) + 383 15 05 09 00 00 00 00 00 if r5 == 0x0 goto +0x9 + 384 79 54 08 00 00 00 00 00 r4 = *(u64 *)(r5 + 0x8) + 385 15 04 02 00 00 00 00 00 if r4 == 0x0 goto +0x2 + 386 bf 45 00 00 00 00 00 00 r5 = r4 + 387 05 00 fc ff 00 00 00 00 goto -0x4 + 388 61 54 18 00 00 00 00 00 r4 = *(u32 *)(r5 + 0x18) + 389 63 43 18 00 00 00 00 00 *(u32 *)(r3 + 0x18) = r4 + 390 bf 53 00 00 00 00 00 00 r3 = r5 + 391 79 34 10 00 00 00 00 00 r4 = *(u64 *)(r3 + 0x10) + 392 15 04 1b 00 00 00 00 00 if r4 == 0x0 goto +0x1b + 393 79 35 00 00 00 00 00 00 r5 = *(u64 *)(r3 + 0x0) + 394 7b 54 00 00 00 00 00 00 *(u64 *)(r4 + 0x0) = r5 + 395 72 04 1c 00 00 00 00 00 *(u8 *)(r4 + 0x1c) = 0x0 + 396 15 05 10 00 00 00 00 00 if r5 == 0x0 goto +0x10 + 397 79 56 10 00 00 00 00 00 r6 = *(u64 *)(r5 + 0x10) + 398 5d 63 07 00 00 00 00 00 if r3 != r6 goto +0x7 + 399 7b 45 10 00 00 00 00 00 *(u64 *)(r5 + 0x10) = r4 + 400 7a 03 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = 0x0 + 401 7a 03 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = 0x0 + 402 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 403 7b 43 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r4 + 404 7b 31 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r3 + 405 95 00 00 00 00 00 00 00 exit + 406 7b 45 08 00 00 00 00 00 *(u64 *)(r5 + 0x8) = r4 + 407 7a 03 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = 0x0 + 408 7a 03 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = 0x0 + 409 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 410 7b 43 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r4 + 411 7b 31 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r3 + 412 95 00 00 00 00 00 00 00 exit + 413 7b 41 c0 28 00 00 00 00 *(u64 *)(r1 + 0x28c0) = r4 + 414 7a 03 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = 0x0 + 415 7a 03 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = 0x0 + 416 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 417 7b 43 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r4 + 418 7b 31 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r3 + 419 95 00 00 00 00 00 00 00 exit + 420 79 35 00 00 00 00 00 00 r5 = *(u64 *)(r3 + 0x0) + 421 55 05 07 00 00 00 00 00 if r5 != 0x0 goto +0x7 + 422 7a 01 c0 28 00 00 00 00 *(u64 *)(r1 + 0x28c0) = 0x0 + 423 7a 03 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = 0x0 + 424 7a 03 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = 0x0 + 425 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 426 7b 43 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r4 + 427 7b 31 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r3 + 428 95 00 00 00 00 00 00 00 exit + 429 71 34 1c 00 00 00 00 00 r4 = *(u8 *)(r3 + 0x1c) + 430 55 04 10 00 01 00 00 00 if r4 != 0x1 goto +0x10 + 431 79 54 10 00 00 00 00 00 r4 = *(u64 *)(r5 + 0x10) + 432 5d 43 07 00 00 00 00 00 if r3 != r4 goto +0x7 + 433 7a 05 10 00 00 00 00 00 *(u64 *)(r5 + 0x10) = 0x0 + 434 7a 03 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = 0x0 + 435 7a 03 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = 0x0 + 436 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 437 7b 43 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r4 + 438 7b 31 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r3 + 439 95 00 00 00 00 00 00 00 exit + 440 7a 05 08 00 00 00 00 00 *(u64 *)(r5 + 0x8) = 0x0 + 441 7a 03 08 00 00 00 00 00 *(u64 *)(r3 + 0x8) = 0x0 + 442 7a 03 10 00 00 00 00 00 *(u64 *)(r3 + 0x10) = 0x0 + 443 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 444 7b 43 00 00 00 00 00 00 *(u64 *)(r3 + 0x0) = r4 + 445 7b 31 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r3 + 446 95 00 00 00 00 00 00 00 exit + 447 bf 32 00 00 00 00 00 00 r2 = r3 + 448 79 35 00 00 00 00 00 00 r5 = *(u64 *)(r3 + 0x0) + 449 79 54 08 00 00 00 00 00 r4 = *(u64 *)(r5 + 0x8) + 450 5d 43 02 00 00 00 00 00 if r3 != r4 goto +0x2 + 451 7a 05 08 00 00 00 00 00 *(u64 *)(r5 + 0x8) = 0x0 + 452 05 00 32 00 00 00 00 00 goto +0x32 + 453 7a 05 10 00 00 00 00 00 *(u64 *)(r5 + 0x10) = 0x0 + 454 79 56 08 00 00 00 00 00 r6 = *(u64 *)(r5 + 0x8) + 455 79 67 08 00 00 00 00 00 r7 = *(u64 *)(r6 + 0x8) + 456 79 68 10 00 00 00 00 00 r8 = *(u64 *)(r6 + 0x10) + 457 71 69 1c 00 00 00 00 00 r9 = *(u8 *)(r6 + 0x1c) + 458 15 09 1c 00 01 00 00 00 if r9 == 0x1 goto +0x1c + 459 15 07 02 00 00 00 00 00 if r7 == 0x0 goto +0x2 + 460 71 79 1c 00 00 00 00 00 r9 = *(u8 *)(r7 + 0x1c) + 461 15 09 14 00 01 00 00 00 if r9 == 0x1 goto +0x14 + 462 15 08 02 00 00 00 00 00 if r8 == 0x0 goto +0x2 + 463 71 89 1c 00 00 00 00 00 r9 = *(u8 *)(r8 + 0x1c) + 464 15 09 0d 00 01 00 00 00 if r9 == 0x1 goto +0xd + 465 15 05 25 00 00 00 00 00 if r5 == 0x0 goto +0x25 + 466 71 59 1c 00 00 00 00 00 r9 = *(u8 *)(r5 + 0x1c) + 467 55 09 03 00 01 00 00 00 if r9 != 0x1 goto +0x3 + 468 72 06 1c 00 01 00 00 00 *(u8 *)(r6 + 0x1c) = 0x1 + 469 72 05 1c 00 00 00 00 00 *(u8 *)(r5 + 0x1c) = 0x0 + 470 05 00 20 00 00 00 00 00 goto +0x20 + 471 72 06 1c 00 01 00 00 00 *(u8 *)(r6 + 0x1c) = 0x1 + 472 bf 53 00 00 00 00 00 00 r3 = r5 + 473 79 35 00 00 00 00 00 00 r5 = *(u64 *)(r3 + 0x0) + 474 15 05 03 00 00 00 00 00 if r5 == 0x0 goto +0x3 + 475 79 54 08 00 00 00 00 00 r4 = *(u64 *)(r5 + 0x8) + 476 1d 43 1a 00 00 00 00 00 if r3 == r4 goto +0x1a + 477 05 00 e8 ff 00 00 00 00 goto -0x18 + 478 72 06 1c 00 01 00 00 00 *(u8 *)(r6 + 0x1c) = 0x1 + 479 72 08 1c 00 00 00 00 00 *(u8 *)(r8 + 0x1c) = 0x0 + 480 bf 67 00 00 00 00 00 00 r7 = r6 + 481 bf 86 00 00 00 00 00 00 r6 = r8 + 482 71 54 1c 00 00 00 00 00 r4 = *(u8 *)(r5 + 0x1c) + 483 73 46 1c 00 00 00 00 00 *(u8 *)(r6 + 0x1c) = r4 + 484 72 05 1c 00 00 00 00 00 *(u8 *)(r5 + 0x1c) = 0x0 + 485 72 07 1c 00 00 00 00 00 *(u8 *)(r7 + 0x1c) = 0x0 + 486 05 00 10 00 00 00 00 00 goto +0x10 + 487 72 05 1c 00 01 00 00 00 *(u8 *)(r5 + 0x1c) = 0x1 + 488 72 06 1c 00 00 00 00 00 *(u8 *)(r6 + 0x1c) = 0x0 + 489 bf 86 00 00 00 00 00 00 r6 = r8 + 490 79 64 08 00 00 00 00 00 r4 = *(u64 *)(r6 + 0x8) + 491 bf 47 00 00 00 00 00 00 r7 = r4 + 492 15 07 02 00 00 00 00 00 if r7 == 0x0 goto +0x2 + 493 71 74 1c 00 00 00 00 00 r4 = *(u8 *)(r7 + 0x1c) + 494 15 04 f3 ff 01 00 00 00 if r4 == 0x1 goto -0xd + 495 79 64 10 00 00 00 00 00 r4 = *(u64 *)(r6 + 0x10) + 496 bf 48 00 00 00 00 00 00 r8 = r4 + 497 15 08 02 00 00 00 00 00 if r8 == 0x0 goto +0x2 + 498 71 84 1c 00 00 00 00 00 r4 = *(u8 *)(r8 + 0x1c) + 499 15 04 ea ff 01 00 00 00 if r4 == 0x1 goto -0x16 + 500 72 06 1c 00 01 00 00 00 *(u8 *)(r6 + 0x1c) = 0x1 + 501 72 05 1c 00 00 00 00 00 *(u8 *)(r5 + 0x1c) = 0x0 + 502 05 00 00 00 00 00 00 00 goto +0x0 + 503 7a 02 08 00 00 00 00 00 *(u64 *)(r2 + 0x8) = 0x0 + 504 7a 02 10 00 00 00 00 00 *(u64 *)(r2 + 0x10) = 0x0 + 505 79 14 c8 28 00 00 00 00 r4 = *(u64 *)(r1 + 0x28c8) + 506 7b 42 00 00 00 00 00 00 *(u64 *)(r2 + 0x0) = r4 + 507 7b 21 c8 28 00 00 00 00 *(u64 *)(r1 + 0x28c8) = r2 + 508 95 00 00 00 00 00 00 00 exit + 509 b7 00 00 00 09 00 00 00 r0 = 0x9 + 510 95 00 00 00 00 00 00 00 exit + 511 b7 00 00 00 0c 00 00 00 r0 = 0xc + 512 95 00 00 00 00 00 00 00 exit + 513 b7 00 00 00 01 00 00 00 r0 = 0x1 + 514 95 00 00 00 00 00 00 00 exit + 515 b7 00 00 00 0d 00 00 00 r0 = 0xd + 516 95 00 00 00 00 00 00 00 exit + 517 b7 00 00 00 0a 00 00 00 r0 = 0xa + 518 95 00 00 00 00 00 00 00 exit + 519 b7 00 00 00 08 00 00 00 r0 = 0x8 + 520 95 00 00 00 00 00 00 00 exit + 521 b7 00 00 00 07 00 00 00 r0 = 0x7 + 522 95 00 00 00 00 00 00 00 exit + 523 b7 00 00 00 04 00 00 00 r0 = 0x4 + 524 95 00 00 00 00 00 00 00 exit + 525 b7 00 00 00 06 00 00 00 r0 = 0x6 + 526 95 00 00 00 00 00 00 00 exit + 527 b7 00 00 00 03 00 00 00 r0 = 0x3 + 528 95 00 00 00 00 00 00 00 exit + 529 b7 00 00 00 05 00 00 00 r0 = 0x5 + 530 95 00 00 00 00 00 00 00 exit + 531 b7 00 00 00 02 00 00 00 r0 = 0x2 + 532 95 00 00 00 00 00 00 00 exit \ No newline at end of file diff --git a/examples/tree/artifacts/dumps/rs.txt b/examples/tree/artifacts/dumps/rs.txt new file mode 100644 index 00000000..079d0390 --- /dev/null +++ b/examples/tree/artifacts/dumps/rs.txt @@ -0,0 +1,800 @@ +tree.so +ELF Header + Magic 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 + Class ELF64 + Data 2's complement, little endian + Version 1 (current) + OS/ABI UNIX - System V + ABI Version 0 + Type DYN (Shared object file) + Machine Solana Bytecode Format + Version 0x1 + Entry point address 0x0 + Start of program headers 64 (bytes into file) + Start of section headers 8128 (bytes into file) + Flags 0x4 + Size of this header 64 (bytes) + Size of program headers 56 (bytes) + Number of program headers 4 + Size of section headers 64 (bytes) + Number of section headers 8 + Section header string table index 6 +There are 8 section headers, starting at offset 0x1fc0 + +Section Headers + [Nr] Name Type Address Off Size ES Flg Lk Inf Al + [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 + [ 1] .text PROGBITS 0000000000000000 000120 0015b0 00 AX 0 0 8 + [ 2] .rodata PROGBITS 0000000100000000 0016d0 000008 00 A 0 0 1 + [ 3] .bss.stack NOBITS 0000000200000000 0016d8 001000 00 A 0 0 1 + [ 4] .bss.heap NOBITS 0000000300000000 0016d8 001000 00 A 0 0 1 + [ 5] .symtab SYMTAB 0000000000000000 0016d8 000378 18 7 36 8 + [ 6] .shstrtab STRTAB 0000000000000000 001a50 00003e 00 0 0 1 + [ 7] .strtab STRTAB 0000000000000000 001a8e 000532 00 0 0 1 +Key to Flags + W (write), A (alloc), X (execute), M (merge), S (strings), I (info), + L (link order), O (extra OS processing required), G (group), T (TLS), + C (compressed), x (unknown), o (OS specific), E (exclude), + R (retain), p (processor specific) + +Elf file type is DYN (Shared object file) +Entry point 0x0 +There are 4 program headers, starting at offset 64 + +Program Headers + Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align + LOAD 0x000120 0x0000000000000000 0x0000000000000000 0x0015b0 0x0015b0 E 0x8 + LOAD 0x0016d0 0x0000000100000000 0x0000000100000000 0x000008 0x000008 R 0x8 + LOAD 0x0016d8 0x0000000200000000 0x0000000200000000 0x000000 0x001000 RW 0x8 + LOAD 0x0016d8 0x0000000300000000 0x0000000300000000 0x000000 0x001000 RW 0x8 + + Section to Segment mapping + Segment Sections... + 00 .text + 01 .rodata + 02 .bss.stack + 03 .bss.heap + None .symtab .shstrtab .strtab + +There are no relocations in this file. + +Symbol table '.symtab' contains 37 entries + Num Value Size Type Bind Vis Ndx Name + 0 0000000000000000 0 NOTYPE LOCAL DEFAULT UND + 1 0000000000000000 0 FILE LOCAL DEFAULT ABS tree.6625ec0a981394b8-cgu.0 + 2 0000000000000000 0 FILE LOCAL DEFAULT ABS 9xo1hdtfkmmy8s7c073n5kchv + 3 0000000000000000 0 FILE LOCAL DEFAULT ABS alloc.63143c6bdd2830dc-cgu.0 + 4 0000000000000000 0 FILE LOCAL DEFAULT ABS core.e0b4368be1399d19-cgu.0 + 5 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.078 + 6 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.115 + 7 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.000 + 8 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.015 + 9 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.121 + 10 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.012 + 11 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.132 + 12 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.009 + 13 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.138 + 14 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.142 + 15 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.005 + 16 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.146 + 17 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.148 + 18 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.151 + 19 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.163 + 20 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.176 + 21 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.182 + 22 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.021 + 23 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.189 + 24 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.219 + 25 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.248 + 26 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.013 + 27 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.256 + 28 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.263 + 29 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.276 + 30 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.282 + 31 0000000000000000 0 FILE LOCAL DEFAULT ABS compiler_builtins.23e48371ed54d56c-cgu.291 + 32 0000000200000000 0 NOTYPE LOCAL DEFAULT 3 _stack_start + 33 0000000200001000 0 NOTYPE LOCAL DEFAULT 3 _stack_end + 34 0000000300000000 0 NOTYPE LOCAL DEFAULT 4 _heap_start + 35 0000000300001000 0 NOTYPE LOCAL DEFAULT 4 _heap_end + 36 0000000000000000 5552 FUNC GLOBAL DEFAULT 1 entrypoint +There are no section groups in this file. + +tree.so file format elf64-sbf + +Disassembly of section .text + +0000000000000000 + 0 07 0a 00 00 80 fe ff ff add64 r10, -0x180 + 8 9c 14 00 00 00 00 00 00 ldxdw r4, [r1 + 0x0] + 10 9c 23 f8 ff 00 00 00 00 ldxdw r3, [r2 - 0x8] + 18 2c 25 00 00 00 00 00 00 ldxb w5, [r2 + 0x0] + 20 55 05 ed 00 01 00 00 00 jne r5, 0x1, +0xed + 28 55 03 96 02 05 00 00 00 jne r3, 0x5, +0x296 + 30 a5 04 97 02 02 00 00 00 jlt r4, 0x2, +0x297 + 38 9c 13 58 00 00 00 00 00 ldxdw r3, [r1 + 0x58] + 40 55 03 97 02 00 00 00 00 jne r3, 0x0, +0x297 + 48 2c 13 68 28 00 00 00 00 ldxb w3, [r1 + 0x2868] + 50 55 03 97 02 ff 00 00 00 jne r3, 0xff, +0x297 + 58 bf 16 00 00 00 00 00 00 mov64 r6, r1 + 60 07 06 00 00 c0 28 00 00 add64 r6, 0x28c0 + 68 9c 13 c8 28 00 00 00 00 ldxdw r3, [r1 + 0x28c8] + 70 15 03 42 00 00 00 00 00 jeq r3, 0x0, +0x42 + 78 9c 34 00 00 00 00 00 00 ldxdw r4, [r3 + 0x0] + 80 9f 41 c8 28 00 00 00 00 stxdw [r1 + 0x28c8], r4 + 88 8c 21 01 00 00 00 00 00 ldxw w1, [r2 + 0x1] + 90 8f 13 18 00 00 00 00 00 stxw [r3 + 0x18], w1 + 98 9c 64 00 00 00 00 00 00 ldxdw r4, [r6 + 0x0] + a0 15 04 a8 00 00 00 00 00 jeq r4, 0x0, +0xa8 + a8 3c 21 01 00 00 00 00 00 ldxh w1, [r2 + 0x1] + b0 bf 42 00 00 00 00 00 00 mov64 r2, r4 + b8 3c 24 18 00 00 00 00 00 ldxh w4, [r2 + 0x18] + c0 bf 15 00 00 00 00 00 00 mov64 r5, r1 + c8 bd 45 03 00 00 00 00 00 jle r5, r4, +0x3 + d0 9c 24 10 00 00 00 00 00 ldxdw r4, [r2 + 0x10] + d8 55 04 fa ff 00 00 00 00 jne r4, 0x0, -0x6 + e0 05 00 09 00 00 00 00 00 ja +0x9 + e8 3d 45 83 01 00 00 00 00 jge r5, r4, +0x183 + f0 9c 24 08 00 00 00 00 00 ldxdw r4, [r2 + 0x8] + f8 55 04 f6 ff 00 00 00 00 jne r4, 0x0, -0xa + 100 9f 23 00 00 00 00 00 00 stxdw [r3 + 0x0], r2 + 108 27 03 1c 00 01 00 00 00 stb [r3 + 0x1c], 0x1 + 110 9f 32 08 00 00 00 00 00 stxdw [r2 + 0x8], r3 + 118 2c 21 1c 00 00 00 00 00 ldxb w1, [r2 + 0x1c] + 120 55 01 06 00 00 00 00 00 jne r1, 0x0, +0x6 + 128 05 00 9a 00 00 00 00 00 ja +0x9a + 130 9f 23 00 00 00 00 00 00 stxdw [r3 + 0x0], r2 + 138 27 03 1c 00 01 00 00 00 stb [r3 + 0x1c], 0x1 + 140 9f 32 10 00 00 00 00 00 stxdw [r2 + 0x10], r3 + 148 2c 21 1c 00 00 00 00 00 ldxb w1, [r2 + 0x1c] + 150 15 01 95 00 00 00 00 00 jeq r1, 0x0, +0x95 + 158 b7 00 00 00 00 00 00 00 mov64 r0, 0x0 + 160 05 00 06 00 00 00 00 00 ja +0x6 + 168 27 02 1c 00 00 00 00 00 stb [r2 + 0x1c], 0x0 + 170 27 04 1c 00 00 00 00 00 stb [r4 + 0x1c], 0x0 + 178 27 01 1c 00 01 00 00 00 stb [r1 + 0x1c], 0x1 + 180 9c 12 00 00 00 00 00 00 ldxdw r2, [r1 + 0x0] + 188 bf 13 00 00 00 00 00 00 mov64 r3, r1 + 190 15 02 a4 00 00 00 00 00 jeq r2, 0x0, +0xa4 + 198 2c 21 1c 00 00 00 00 00 ldxb w1, [r2 + 0x1c] + 1a0 15 01 a2 00 00 00 00 00 jeq r1, 0x0, +0xa2 + 1a8 9c 21 00 00 00 00 00 00 ldxdw r1, [r2 + 0x0] + 1b0 15 01 9f 00 00 00 00 00 jeq r1, 0x0, +0x9f + 1b8 9c 14 08 00 00 00 00 00 ldxdw r4, [r1 + 0x8] + 1c0 1d 42 04 00 00 00 00 00 jeq r2, r4, +0x4 + 1c8 15 04 88 00 00 00 00 00 jeq r4, 0x0, +0x88 + 1d0 2c 45 1c 00 00 00 00 00 ldxb w5, [r4 + 0x1c] + 1d8 55 05 f1 ff 00 00 00 00 jne r5, 0x0, -0xf + 1e0 05 00 85 00 00 00 00 00 ja +0x85 + 1e8 9c 14 10 00 00 00 00 00 ldxdw r4, [r1 + 0x10] + 1f0 15 04 02 00 00 00 00 00 jeq r4, 0x0, +0x2 + 1f8 2c 45 1c 00 00 00 00 00 ldxb w5, [r4 + 0x1c] + 200 55 05 ec ff 00 00 00 00 jne r5, 0x0, -0x14 + 208 9c 24 10 00 00 00 00 00 ldxdw r4, [r2 + 0x10] + 210 1d 43 a2 00 00 00 00 00 jeq r3, r4, +0xa2 + 218 bf 43 00 00 00 00 00 00 mov64 r3, r4 + 220 bf 24 00 00 00 00 00 00 mov64 r4, r2 + 228 9f 31 08 00 00 00 00 00 stxdw [r1 + 0x8], r3 + 230 9c 12 00 00 00 00 00 00 ldxdw r2, [r1 + 0x0] + 238 15 03 01 00 00 00 00 00 jeq r3, 0x0, +0x1 + 240 9f 13 00 00 00 00 00 00 stxdw [r3 + 0x0], r1 + 248 9f 24 00 00 00 00 00 00 stxdw [r4 + 0x0], r2 + 250 9f 14 10 00 00 00 00 00 stxdw [r4 + 0x10], r1 + 258 9f 41 00 00 00 00 00 00 stxdw [r1 + 0x0], r4 + 260 15 02 85 00 00 00 00 00 jeq r2, 0x0, +0x85 + 268 9c 23 10 00 00 00 00 00 ldxdw r3, [r2 + 0x10] + 270 5d 31 81 00 00 00 00 00 jne r1, r3, +0x81 + 278 07 02 00 00 10 00 00 00 add64 r2, 0x10 + 280 05 00 80 00 00 00 00 00 ja +0x80 + 288 55 04 b8 00 04 00 00 00 jne r4, 0x4, +0xb8 + 290 9c 13 b8 28 00 00 00 00 ldxdw r3, [r1 + 0x28b8] + 298 bf 35 00 00 00 00 00 00 mov64 r5, r3 + 2a0 07 05 00 00 07 00 00 00 add64 r5, 0x7 + 2a8 57 05 00 00 f8 ff ff ff and64 r5, -0x8 + 2b0 bf 14 00 00 00 00 00 00 mov64 r4, r1 + 2b8 0f 54 00 00 00 00 00 00 add64 r4, r5 + 2c0 2c 45 c8 50 00 00 00 00 ldxb w5, [r4 + 0x50c8] + 2c8 55 05 4a 02 ff 00 00 00 jne r5, 0xff, +0x24a + 2d0 9c 45 18 51 00 00 00 00 ldxdw r5, [r4 + 0x5118] + 2d8 55 05 4a 02 0e 00 00 00 jne r5, 0xe, +0x24a + 2e0 2c 45 38 79 00 00 00 00 ldxb w5, [r4 + 0x7938] + 2e8 55 05 4a 02 ff 00 00 00 jne r5, 0xff, +0x24a + 2f0 b7 00 00 00 08 00 00 00 mov64 r0, 0x8 + 2f8 b4 05 00 00 06 a7 d5 17 mov32 w5, 0x17d5a706 + 300 f7 05 00 00 19 2c 5c 51 hor64 r5, 0x515c2c19 + 308 9c 47 40 79 00 00 00 00 ldxdw r7, [r4 + 0x7940] + 310 5d 57 74 00 00 00 00 00 jne r7, r5, +0x74 + 318 b4 05 00 00 21 8c c9 4c mov32 w5, 0x4cc98c21 + 320 f7 05 00 00 3d 4a f1 7f hor64 r5, 0x7ff14a3d + 328 9c 47 48 79 00 00 00 00 ldxdw r7, [r4 + 0x7948] + 330 5d 57 70 00 00 00 00 00 jne r7, r5, +0x70 + 338 b4 05 00 00 58 da ee 08 mov32 w5, 0x8eeda58 + 340 f7 05 00 00 9b a1 fd 44 hor64 r5, 0x44fda19b + 348 9c 47 50 79 00 00 00 00 ldxdw r7, [r4 + 0x7950] + 350 5d 57 6c 00 00 00 00 00 jne r7, r5, +0x6c + 358 bf 27 00 00 00 00 00 00 mov64 r7, r2 + 360 9c 42 58 79 00 00 00 00 ldxdw r2, [r4 + 0x7958] + 368 b4 05 00 00 e3 db d9 8a mov32 w5, -0x7526241d + 370 5d 52 68 00 00 00 00 00 jne r2, r5, +0x68 + 378 9c 42 90 79 00 00 00 00 ldxdw r2, [r4 + 0x7990] + 380 96 02 00 00 1d 00 00 00 lmul64 r2, 0x1d + 388 9f 2a 74 01 00 00 00 00 stxdw [r10 + 0x174], r2 + 390 87 0a 70 01 02 00 00 00 stw [r10 + 0x170], 0x2 + 398 bf 14 00 00 00 00 00 00 mov64 r4, r1 + 3a0 07 04 00 00 70 28 00 00 add64 r4, 0x2870 + 3a8 9f 4a 28 01 00 00 00 00 stxdw [r10 + 0x128], r4 + 3b0 bf 12 00 00 00 00 00 00 mov64 r2, r1 + 3b8 07 02 00 00 10 00 00 00 add64 r2, 0x10 + 3c0 9f 2a 18 01 00 00 00 00 stxdw [r10 + 0x118], r2 + 3c8 37 0a 30 01 01 00 00 00 sth [r10 + 0x130], 0x1 + 3d0 37 0a 20 01 01 01 00 00 sth [r10 + 0x120], 0x101 + 3d8 bf 15 00 00 00 00 00 00 mov64 r5, r1 + 3e0 07 05 00 00 90 28 00 00 add64 r5, 0x2890 + 3e8 9f 5a 00 01 00 00 00 00 stxdw [r10 + 0x100], r5 + 3f0 9f 6a f8 00 00 00 00 00 stxdw [r10 + 0xf8], r6 + 3f8 9f 3a f0 00 00 00 00 00 stxdw [r10 + 0xf0], r3 + 400 bf 13 00 00 00 00 00 00 mov64 r3, r1 + 408 07 03 00 00 b0 28 00 00 add64 r3, 0x28b0 + 410 9f 3a e8 00 00 00 00 00 stxdw [r10 + 0xe8], r3 + 418 9f 4a e0 00 00 00 00 00 stxdw [r10 + 0xe0], r4 + 420 bf 13 00 00 00 00 00 00 mov64 r3, r1 + 428 07 03 00 00 30 00 00 00 add64 r3, 0x30 + 430 9f 3a c8 00 00 00 00 00 stxdw [r10 + 0xc8], r3 + 438 bf 13 00 00 00 00 00 00 mov64 r3, r1 + 440 07 03 00 00 60 00 00 00 add64 r3, 0x60 + 448 9f 3a c0 00 00 00 00 00 stxdw [r10 + 0xc0], r3 + 450 bf 13 00 00 00 00 00 00 mov64 r3, r1 + 458 07 03 00 00 50 00 00 00 add64 r3, 0x50 + 460 9f 3a b0 00 00 00 00 00 stxdw [r10 + 0xb0], r3 + 468 9f 2a a8 00 00 00 00 00 stxdw [r10 + 0xa8], r2 + 470 27 0a 12 01 00 00 00 00 stb [r10 + 0x112], 0x0 + 478 37 0a 10 01 00 01 00 00 sth [r10 + 0x110], 0x100 + 480 97 0a 08 01 00 00 00 00 stdw [r10 + 0x108], 0x0 + 488 27 0a da 00 00 00 00 00 stb [r10 + 0xda], 0x0 + 490 37 0a d8 00 01 01 00 00 sth [r10 + 0xd8], 0x101 + 498 97 0a d0 00 00 00 00 00 stdw [r10 + 0xd0], 0x0 + 4a0 97 0a b8 00 00 00 00 00 stdw [r10 + 0xb8], 0x0 + 4a8 97 0a 50 01 00 00 00 00 stdw [r10 + 0x150], 0x0 + 4b0 97 0a 48 01 00 00 00 00 stdw [r10 + 0x148], 0x0 + 4b8 97 0a 40 01 00 00 00 00 stdw [r10 + 0x140], 0x0 + 4c0 97 0a 38 01 00 00 00 00 stdw [r10 + 0x138], 0x0 + 4c8 bf a2 00 00 00 00 00 00 mov64 r2, r10 + 4d0 07 02 00 00 70 01 00 00 add64 r2, 0x170 + 4d8 9f 2a 68 00 00 00 00 00 stxdw [r10 + 0x68], r2 + 4e0 bf a2 00 00 00 00 00 00 mov64 r2, r10 + 4e8 07 02 00 00 18 01 00 00 add64 r2, 0x118 + 4f0 9f 2a 58 00 00 00 00 00 stxdw [r10 + 0x58], r2 + 4f8 bf a2 00 00 00 00 00 00 mov64 r2, r10 + 500 07 02 00 00 38 01 00 00 add64 r2, 0x138 + 508 9f 2a 50 00 00 00 00 00 stxdw [r10 + 0x50], r2 + 510 97 0a 70 00 0c 00 00 00 stdw [r10 + 0x70], 0xc + 518 97 0a 60 00 02 00 00 00 stdw [r10 + 0x60], 0x2 + 520 97 0a 90 00 00 00 00 00 stdw [r10 + 0x90], 0x0 + 528 97 0a 88 00 00 00 00 00 stdw [r10 + 0x88], 0x0 + 530 bf a3 00 00 00 00 00 00 mov64 r3, r10 + 538 07 03 00 00 50 00 00 00 add64 r3, 0x50 + 540 bf a2 00 00 00 00 00 00 mov64 r2, r10 + 548 07 02 00 00 a8 00 00 00 add64 r2, 0xa8 + 550 bf a4 00 00 00 00 00 00 mov64 r4, r10 + 558 07 04 00 00 88 00 00 00 add64 r4, 0x88 + 560 bf 18 00 00 00 00 00 00 mov64 r8, r1 + 568 bf 31 00 00 00 00 00 00 mov64 r1, r3 + 570 b7 03 00 00 02 00 00 00 mov64 r3, 0x2 + 578 b7 05 00 00 00 00 00 00 mov64 r5, 0x0 + 580 95 00 00 00 85 9c 2b a2 syscall -0x5dd4637b + 588 9c 81 b8 28 00 00 00 00 ldxdw r1, [r8 + 0x28b8] + 590 07 01 00 00 1d 00 00 00 add64 r1, 0x1d + 598 9f 18 b8 28 00 00 00 00 stxdw [r8 + 0x28b8], r1 + 5a0 9c 83 d0 28 00 00 00 00 ldxdw r3, [r8 + 0x28d0] + 5a8 bf 31 00 00 00 00 00 00 mov64 r1, r3 + 5b0 07 01 00 00 1d 00 00 00 add64 r1, 0x1d + 5b8 9f 18 d0 28 00 00 00 00 stxdw [r8 + 0x28d0], r1 + 5c0 bf 72 00 00 00 00 00 00 mov64 r2, r7 + 5c8 8c 21 01 00 00 00 00 00 ldxw w1, [r2 + 0x1] + 5d0 8f 13 18 00 00 00 00 00 stxw [r3 + 0x18], w1 + 5d8 9c 64 00 00 00 00 00 00 ldxdw r4, [r6 + 0x0] + 5e0 55 04 58 ff 00 00 00 00 jne r4, 0x0, -0xa8 + 5e8 97 03 00 00 00 00 00 00 stdw [r3 + 0x0], 0x0 + 5f0 27 03 1c 00 01 00 00 00 stb [r3 + 0x1c], 0x1 + 5f8 9f 36 00 00 00 00 00 00 stxdw [r6 + 0x0], r3 + 600 b7 00 00 00 00 00 00 00 mov64 r0, 0x0 + 608 05 00 15 00 00 00 00 00 ja +0x15 + 610 9c 24 08 00 00 00 00 00 ldxdw r4, [r2 + 0x8] + 618 1d 43 14 00 00 00 00 00 jeq r3, r4, +0x14 + 620 bf 43 00 00 00 00 00 00 mov64 r3, r4 + 628 bf 24 00 00 00 00 00 00 mov64 r4, r2 + 630 9f 31 10 00 00 00 00 00 stxdw [r1 + 0x10], r3 + 638 9c 12 00 00 00 00 00 00 ldxdw r2, [r1 + 0x0] + 640 15 03 01 00 00 00 00 00 jeq r3, 0x0, +0x1 + 648 9f 13 00 00 00 00 00 00 stxdw [r3 + 0x0], r1 + 650 9f 24 00 00 00 00 00 00 stxdw [r4 + 0x0], r2 + 658 9f 14 08 00 00 00 00 00 stxdw [r4 + 0x8], r1 + 660 9f 41 00 00 00 00 00 00 stxdw [r1 + 0x0], r4 + 668 15 02 04 00 00 00 00 00 jeq r2, 0x0, +0x4 + 670 9c 23 10 00 00 00 00 00 ldxdw r3, [r2 + 0x10] + 678 1d 31 7f ff 00 00 00 00 jeq r1, r3, -0x81 + 680 07 02 00 00 08 00 00 00 add64 r2, 0x8 + 688 bf 26 00 00 00 00 00 00 mov64 r6, r2 + 690 9f 46 00 00 00 00 00 00 stxdw [r6 + 0x0], r4 + 698 27 04 1c 00 00 00 00 00 stb [r4 + 0x1c], 0x0 + 6a0 27 01 1c 00 01 00 00 00 stb [r1 + 0x1c], 0x1 + 6a8 05 00 01 00 00 00 00 00 ja +0x1 + 6b0 27 02 1c 00 00 00 00 00 stb [r2 + 0x1c], 0x0 + 6b8 9d 00 00 00 00 00 00 00 return + 6c0 9c 43 10 00 00 00 00 00 ldxdw r3, [r4 + 0x10] + 6c8 9f 32 08 00 00 00 00 00 stxdw [r2 + 0x8], r3 + 6d0 15 03 01 00 00 00 00 00 jeq r3, 0x0, +0x1 + 6d8 9f 23 00 00 00 00 00 00 stxdw [r3 + 0x0], r2 + 6e0 9f 14 00 00 00 00 00 00 stxdw [r4 + 0x0], r1 + 6e8 9f 24 10 00 00 00 00 00 stxdw [r4 + 0x10], r2 + 6f0 9f 42 00 00 00 00 00 00 stxdw [r2 + 0x0], r4 + 6f8 9f 41 10 00 00 00 00 00 stxdw [r1 + 0x10], r4 + 700 9c 43 08 00 00 00 00 00 ldxdw r3, [r4 + 0x8] + 708 9f 31 10 00 00 00 00 00 stxdw [r1 + 0x10], r3 + 710 9c 12 00 00 00 00 00 00 ldxdw r2, [r1 + 0x0] + 718 55 03 e5 ff 00 00 00 00 jne r3, 0x0, -0x1b + 720 05 00 e5 ff 00 00 00 00 ja -0x1b + 728 9c 43 08 00 00 00 00 00 ldxdw r3, [r4 + 0x8] + 730 9f 32 10 00 00 00 00 00 stxdw [r2 + 0x10], r3 + 738 15 03 01 00 00 00 00 00 jeq r3, 0x0, +0x1 + 740 9f 23 00 00 00 00 00 00 stxdw [r3 + 0x0], r2 + 748 9f 14 00 00 00 00 00 00 stxdw [r4 + 0x0], r1 + 750 9f 24 08 00 00 00 00 00 stxdw [r4 + 0x8], r2 + 758 9f 42 00 00 00 00 00 00 stxdw [r2 + 0x0], r4 + 760 9f 41 08 00 00 00 00 00 stxdw [r1 + 0x8], r4 + 768 9c 43 10 00 00 00 00 00 ldxdw r3, [r4 + 0x10] + 770 9f 31 08 00 00 00 00 00 stxdw [r1 + 0x8], r3 + 778 9c 12 00 00 00 00 00 00 ldxdw r2, [r1 + 0x0] + 780 55 03 57 ff 00 00 00 00 jne r3, 0x0, -0xa9 + 788 05 00 57 ff 00 00 00 00 ja -0xa9 + 790 55 05 1c 01 02 00 00 00 jne r5, 0x2, +0x11c + 798 55 03 a8 01 03 00 00 00 jne r3, 0x3, +0x1a8 + 7a0 a5 04 a9 01 02 00 00 00 jlt r4, 0x2, +0x1a9 + 7a8 9c 13 58 00 00 00 00 00 ldxdw r3, [r1 + 0x58] + 7b0 55 03 a9 01 00 00 00 00 jne r3, 0x0, +0x1a9 + 7b8 2c 13 68 28 00 00 00 00 ldxb w3, [r1 + 0x2868] + 7c0 55 03 a9 01 ff 00 00 00 jne r3, 0xff, +0x1a9 + 7c8 b7 00 00 00 0f 00 00 00 mov64 r0, 0xf + 7d0 9c 14 c0 28 00 00 00 00 ldxdw r4, [r1 + 0x28c0] + 7d8 15 04 db ff 00 00 00 00 jeq r4, 0x0, -0x25 + 7e0 bf 13 00 00 00 00 00 00 mov64 r3, r1 + 7e8 07 03 00 00 c0 28 00 00 add64 r3, 0x28c0 + 7f0 3c 25 01 00 00 00 00 00 ldxh w5, [r2 + 0x1] + 7f8 05 00 02 00 00 00 00 00 ja +0x2 + 800 9c 44 10 00 00 00 00 00 ldxdw r4, [r4 + 0x10] + 808 15 04 d5 ff 00 00 00 00 jeq r4, 0x0, -0x2b + 810 3c 46 18 00 00 00 00 00 ldxh w6, [r4 + 0x18] + 818 bf 57 00 00 00 00 00 00 mov64 r7, r5 + 820 2d 67 fb ff 00 00 00 00 jgt r7, r6, -0x5 + 828 9c 42 08 00 00 00 00 00 ldxdw r2, [r4 + 0x8] + 830 3d 67 05 00 00 00 00 00 jge r7, r6, +0x5 + 838 bf 24 00 00 00 00 00 00 mov64 r4, r2 + 840 55 02 f9 ff 00 00 00 00 jne r2, 0x0, -0x7 + 848 05 00 cd ff 00 00 00 00 ja -0x33 + 850 b7 00 00 00 0d 00 00 00 mov64 r0, 0xd + 858 05 00 cb ff 00 00 00 00 ja -0x35 + 860 9f 1a 40 00 00 00 00 00 stxdw [r10 + 0x40], r1 + 868 15 02 08 00 00 00 00 00 jeq r2, 0x0, +0x8 + 870 9c 41 10 00 00 00 00 00 ldxdw r1, [r4 + 0x10] + 878 15 01 1e 00 00 00 00 00 jeq r1, 0x0, +0x1e + 880 bf 12 00 00 00 00 00 00 mov64 r2, r1 + 888 9c 21 08 00 00 00 00 00 ldxdw r1, [r2 + 0x8] + 890 55 01 fd ff 00 00 00 00 jne r1, 0x0, -0x3 + 898 8c 21 18 00 00 00 00 00 ldxw w1, [r2 + 0x18] + 8a0 8f 14 18 00 00 00 00 00 stxw [r4 + 0x18], w1 + 8a8 05 00 01 00 00 00 00 00 ja +0x1 + 8b0 bf 42 00 00 00 00 00 00 mov64 r2, r4 + 8b8 9c 26 00 00 00 00 00 00 ldxdw r6, [r2 + 0x0] + 8c0 bf 21 00 00 00 00 00 00 mov64 r1, r2 + 8c8 07 01 00 00 08 00 00 00 add64 r1, 0x8 + 8d0 9f 1a 38 00 00 00 00 00 stxdw [r10 + 0x38], r1 + 8d8 9c 21 10 00 00 00 00 00 ldxdw r1, [r2 + 0x10] + 8e0 15 01 07 00 00 00 00 00 jeq r1, 0x0, +0x7 + 8e8 9f 61 00 00 00 00 00 00 stxdw [r1 + 0x0], r6 + 8f0 27 01 1c 00 00 00 00 00 stb [r1 + 0x1c], 0x0 + 8f8 15 06 4c 00 00 00 00 00 jeq r6, 0x0, +0x4c + 900 9c 63 10 00 00 00 00 00 ldxdw r3, [r6 + 0x10] + 908 1d 32 48 00 00 00 00 00 jeq r2, r3, +0x48 + 910 07 06 00 00 08 00 00 00 add64 r6, 0x8 + 918 05 00 47 00 00 00 00 00 ja +0x47 + 920 15 06 85 01 00 00 00 00 jeq r6, 0x0, +0x185 + 928 9c 61 10 00 00 00 00 00 ldxdw r1, [r6 + 0x10] + 930 2c 24 1c 00 00 00 00 00 ldxb w4, [r2 + 0x1c] + 938 55 04 10 00 01 00 00 00 jne r4, 0x1, +0x10 + 940 b7 03 00 00 10 00 00 00 mov64 r3, 0x10 + 948 1d 12 01 00 00 00 00 00 jeq r2, r1, +0x1 + 950 b7 03 00 00 08 00 00 00 mov64 r3, 0x8 + 958 0f 36 00 00 00 00 00 00 add64 r6, r3 + 960 97 06 00 00 00 00 00 00 stdw [r6 + 0x0], 0x0 + 968 05 00 d8 00 00 00 00 00 ja +0xd8 + 970 bf 41 00 00 00 00 00 00 mov64 r1, r4 + 978 07 01 00 00 08 00 00 00 add64 r1, 0x8 + 980 9c 45 00 00 00 00 00 00 ldxdw r5, [r4 + 0x0] + 988 9f 52 00 00 00 00 00 00 stxdw [r2 + 0x0], r5 + 990 27 02 1c 00 00 00 00 00 stb [r2 + 0x1c], 0x0 + 998 15 05 64 00 00 00 00 00 jeq r5, 0x0, +0x64 + 9a0 9c 53 10 00 00 00 00 00 ldxdw r3, [r5 + 0x10] + 9a8 1d 34 60 00 00 00 00 00 jeq r4, r3, +0x60 + 9b0 07 05 00 00 08 00 00 00 add64 r5, 0x8 + 9b8 05 00 5f 00 00 00 00 00 ja +0x5f + 9c0 b4 04 00 00 01 00 00 00 mov32 w4, 0x1 + 9c8 9f 4a 30 00 00 00 00 00 stxdw [r10 + 0x30], r4 + 9d0 1d 12 02 00 00 00 00 00 jeq r2, r1, +0x2 + 9d8 b4 01 00 00 00 00 00 00 mov32 w1, 0x0 + 9e0 9f 1a 30 00 00 00 00 00 stxdw [r10 + 0x30], r1 + 9e8 bf 61 00 00 00 00 00 00 mov64 r1, r6 + 9f0 07 01 00 00 08 00 00 00 add64 r1, 0x8 + 9f8 9c a7 30 00 00 00 00 00 ldxdw r7, [r10 + 0x30] + a00 bf 75 00 00 00 00 00 00 mov64 r5, r7 + a08 67 05 00 00 03 00 00 00 lsh64 r5, 0x3 + a10 bf 14 00 00 00 00 00 00 mov64 r4, r1 + a18 0f 54 00 00 00 00 00 00 add64 r4, r5 + a20 97 04 00 00 00 00 00 00 stdw [r4 + 0x0], 0x0 + a28 a7 07 00 00 01 00 00 00 xor64 r7, 0x1 + a30 bf 74 00 00 00 00 00 00 mov64 r4, r7 + a38 67 04 00 00 03 00 00 00 lsh64 r4, 0x3 + a40 0f 41 00 00 00 00 00 00 add64 r1, r4 + a48 9c 18 00 00 00 00 00 00 ldxdw r8, [r1 + 0x0] + a50 bf 89 00 00 00 00 00 00 mov64 r9, r8 + a58 07 09 00 00 1c 00 00 00 add64 r9, 0x1c + a60 bf 80 00 00 00 00 00 00 mov64 r0, r8 + a68 07 00 00 00 08 00 00 00 add64 r0, 0x8 + a70 bf 01 00 00 00 00 00 00 mov64 r1, r0 + a78 0f 51 00 00 00 00 00 00 add64 r1, r5 + a80 9c 11 00 00 00 00 00 00 ldxdw r1, [r1 + 0x0] + a88 2c 85 1c 00 00 00 00 00 ldxb w5, [r8 + 0x1c] + a90 15 05 1b 00 00 00 00 00 jeq r5, 0x0, +0x1b + a98 bf 64 00 00 00 00 00 00 mov64 r4, r6 + aa0 bf 75 00 00 00 00 00 00 mov64 r5, r7 + aa8 67 05 00 00 03 00 00 00 lsh64 r5, 0x3 + ab0 bf 46 00 00 00 00 00 00 mov64 r6, r4 + ab8 0f 56 00 00 00 00 00 00 add64 r6, r5 + ac0 9c 40 00 00 00 00 00 00 ldxdw r0, [r4 + 0x0] + ac8 9f 16 08 00 00 00 00 00 stxdw [r6 + 0x8], r1 + ad0 9c a5 30 00 00 00 00 00 ldxdw r5, [r10 + 0x30] + ad8 67 05 00 00 03 00 00 00 lsh64 r5, 0x3 + ae0 bf 86 00 00 00 00 00 00 mov64 r6, r8 + ae8 0f 56 00 00 00 00 00 00 add64 r6, r5 + af0 07 06 00 00 08 00 00 00 add64 r6, 0x8 + af8 15 01 01 00 00 00 00 00 jeq r1, 0x0, +0x1 + b00 9f 41 00 00 00 00 00 00 stxdw [r1 + 0x0], r4 + b08 9f 46 00 00 00 00 00 00 stxdw [r6 + 0x0], r4 + b10 9f 08 00 00 00 00 00 00 stxdw [r8 + 0x0], r0 + b18 9f 84 00 00 00 00 00 00 stxdw [r4 + 0x0], r8 + b20 bf 36 00 00 00 00 00 00 mov64 r6, r3 + b28 15 00 3f 00 00 00 00 00 jeq r0, 0x0, +0x3f + b30 9c 05 10 00 00 00 00 00 ldxdw r5, [r0 + 0x10] + b38 1d 45 3b 00 00 00 00 00 jeq r5, r4, +0x3b + b40 07 00 00 00 08 00 00 00 add64 r0, 0x8 + b48 05 00 3a 00 00 00 00 00 ja +0x3a + b50 07 06 00 00 10 00 00 00 add64 r6, 0x10 + b58 bf 63 00 00 00 00 00 00 mov64 r3, r6 + b60 9f 13 00 00 00 00 00 00 stxdw [r3 + 0x0], r1 + b68 05 00 98 00 00 00 00 00 ja +0x98 + b70 0f 40 00 00 00 00 00 00 add64 r0, r4 + b78 05 00 15 00 00 00 00 00 ja +0x15 + b80 bf 97 00 00 00 00 00 00 mov64 r7, r9 + b88 a7 07 00 00 01 00 00 00 xor64 r7, 0x1 + b90 bf 71 00 00 00 00 00 00 mov64 r1, r7 + b98 67 01 00 00 03 00 00 00 lsh64 r1, 0x3 + ba0 bf 45 00 00 00 00 00 00 mov64 r5, r4 + ba8 0f 15 00 00 00 00 00 00 add64 r5, r1 + bb0 9c 58 08 00 00 00 00 00 ldxdw r8, [r5 + 0x8] + bb8 bf 85 00 00 00 00 00 00 mov64 r5, r8 + bc0 07 05 00 00 08 00 00 00 add64 r5, 0x8 + bc8 bf 50 00 00 00 00 00 00 mov64 r0, r5 + bd0 0f 10 00 00 00 00 00 00 add64 r0, r1 + bd8 9f 9a 30 00 00 00 00 00 stxdw [r10 + 0x30], r9 + be0 bf 91 00 00 00 00 00 00 mov64 r1, r9 + be8 67 01 00 00 03 00 00 00 lsh64 r1, 0x3 + bf0 0f 15 00 00 00 00 00 00 add64 r5, r1 + bf8 bf 89 00 00 00 00 00 00 mov64 r9, r8 + c00 07 09 00 00 1c 00 00 00 add64 r9, 0x1c + c08 9c 51 00 00 00 00 00 00 ldxdw r1, [r5 + 0x0] + c10 2c 85 1c 00 00 00 00 00 ldxb w5, [r8 + 0x1c] + c18 bf 46 00 00 00 00 00 00 mov64 r6, r4 + c20 55 05 cf ff 00 00 00 00 jne r5, 0x0, -0x31 + c28 9c 04 00 00 00 00 00 00 ldxdw r4, [r0 + 0x0] + c30 15 04 02 00 00 00 00 00 jeq r4, 0x0, +0x2 + c38 2c 45 1c 00 00 00 00 00 ldxb w5, [r4 + 0x1c] + c40 55 05 3d 00 00 00 00 00 jne r5, 0x0, +0x3d + c48 15 01 02 00 00 00 00 00 jeq r1, 0x0, +0x2 + c50 2c 14 1c 00 00 00 00 00 ldxb w4, [r1 + 0x1c] + c58 55 04 3c 00 00 00 00 00 jne r4, 0x0, +0x3c + c60 2c 61 1c 00 00 00 00 00 ldxb w1, [r6 + 0x1c] + c68 27 09 00 00 01 00 00 00 stb [r9 + 0x0], 0x1 + c70 15 01 34 00 01 00 00 00 jeq r1, 0x1, +0x34 + c78 9c 64 00 00 00 00 00 00 ldxdw r4, [r6 + 0x0] + c80 15 04 75 00 00 00 00 00 jeq r4, 0x0, +0x75 + c88 9c 41 10 00 00 00 00 00 ldxdw r1, [r4 + 0x10] + c90 b4 09 00 00 01 00 00 00 mov32 w9, 0x1 + c98 1d 16 dc ff 00 00 00 00 jeq r6, r1, -0x24 + ca0 b4 09 00 00 00 00 00 00 mov32 w9, 0x0 + ca8 05 00 da ff 00 00 00 00 ja -0x26 + cb0 07 05 00 00 10 00 00 00 add64 r5, 0x10 + cb8 bf 53 00 00 00 00 00 00 mov64 r3, r5 + cc0 9f 23 00 00 00 00 00 00 stxdw [r3 + 0x0], r2 + cc8 97 01 08 00 00 00 00 00 stdw [r1 + 0x8], 0x0 + cd0 97 01 00 00 00 00 00 00 stdw [r1 + 0x0], 0x0 + cd8 9c a1 40 00 00 00 00 00 ldxdw r1, [r10 + 0x40] + ce0 9c 12 c8 28 00 00 00 00 ldxdw r2, [r1 + 0x28c8] + ce8 9f 24 00 00 00 00 00 00 stxdw [r4 + 0x0], r2 + cf0 9f 41 c8 28 00 00 00 00 stxdw [r1 + 0x28c8], r4 + cf8 b7 00 00 00 00 00 00 00 mov64 r0, 0x0 + d00 05 00 36 ff 00 00 00 00 ja -0xca + d08 b7 00 00 00 0e 00 00 00 mov64 r0, 0xe + d10 05 00 34 ff 00 00 00 00 ja -0xcc + d18 07 00 00 00 10 00 00 00 add64 r0, 0x10 + d20 bf 06 00 00 00 00 00 00 mov64 r6, r0 + d28 9f 86 00 00 00 00 00 00 stxdw [r6 + 0x0], r8 + d30 27 04 1c 00 01 00 00 00 stb [r4 + 0x1c], 0x1 + d38 27 09 00 00 00 00 00 00 stb [r9 + 0x0], 0x0 + d40 bf 75 00 00 00 00 00 00 mov64 r5, r7 + d48 67 05 00 00 03 00 00 00 lsh64 r5, 0x3 + d50 bf 10 00 00 00 00 00 00 mov64 r0, r1 + d58 07 00 00 00 08 00 00 00 add64 r0, 0x8 + d60 bf 06 00 00 00 00 00 00 mov64 r6, r0 + d68 0f 56 00 00 00 00 00 00 add64 r6, r5 + d70 9c 68 00 00 00 00 00 00 ldxdw r8, [r6 + 0x0] + d78 15 08 06 00 00 00 00 00 jeq r8, 0x0, +0x6 + d80 2c 85 1c 00 00 00 00 00 ldxb w5, [r8 + 0x1c] + d88 15 05 04 00 00 00 00 00 jeq r5, 0x0, +0x4 + d90 bf 46 00 00 00 00 00 00 mov64 r6, r4 + d98 bf 19 00 00 00 00 00 00 mov64 r9, r1 + da0 bf 84 00 00 00 00 00 00 mov64 r4, r8 + da8 05 00 2f 00 00 00 00 00 ja +0x2f + db0 9c a5 30 00 00 00 00 00 ldxdw r5, [r10 + 0x30] + db8 67 05 00 00 03 00 00 00 lsh64 r5, 0x3 + dc0 0f 50 00 00 00 00 00 00 add64 r0, r5 + dc8 9c 09 00 00 00 00 00 00 ldxdw r9, [r0 + 0x0] + dd0 15 09 05 00 00 00 00 00 jeq r9, 0x0, +0x5 + dd8 2c 95 1c 00 00 00 00 00 ldxb w5, [r9 + 0x1c] + de0 15 05 03 00 00 00 00 00 jeq r5, 0x0, +0x3 + de8 bf 46 00 00 00 00 00 00 mov64 r6, r4 + df0 bf 14 00 00 00 00 00 00 mov64 r4, r1 + df8 05 00 0a 00 00 00 00 00 ja +0xa + e00 07 04 00 00 1c 00 00 00 add64 r4, 0x1c + e08 27 01 1c 00 01 00 00 00 stb [r1 + 0x1c], 0x1 + e10 05 00 42 00 00 00 00 00 ja +0x42 + e18 07 06 00 00 1c 00 00 00 add64 r6, 0x1c + e20 bf 64 00 00 00 00 00 00 mov64 r4, r6 + e28 05 00 3f 00 00 00 00 00 ja +0x3f + e30 bf 89 00 00 00 00 00 00 mov64 r9, r8 + e38 05 00 1d 00 00 00 00 00 ja +0x1d + e40 bf 19 00 00 00 00 00 00 mov64 r9, r1 + e48 bf 84 00 00 00 00 00 00 mov64 r4, r8 + e50 67 07 00 00 03 00 00 00 lsh64 r7, 0x3 + e58 bf 90 00 00 00 00 00 00 mov64 r0, r9 + e60 0f 70 00 00 00 00 00 00 add64 r0, r7 + e68 9c a1 30 00 00 00 00 00 ldxdw r1, [r10 + 0x30] + e70 67 01 00 00 03 00 00 00 lsh64 r1, 0x3 + e78 bf 45 00 00 00 00 00 00 mov64 r5, r4 + e80 0f 15 00 00 00 00 00 00 add64 r5, r1 + e88 9c 41 00 00 00 00 00 00 ldxdw r1, [r4 + 0x0] + e90 9c 07 08 00 00 00 00 00 ldxdw r7, [r0 + 0x8] + e98 9f 75 08 00 00 00 00 00 stxdw [r5 + 0x8], r7 + ea0 07 00 00 00 08 00 00 00 add64 r0, 0x8 + ea8 15 07 01 00 00 00 00 00 jeq r7, 0x0, +0x1 + eb0 9f 47 00 00 00 00 00 00 stxdw [r7 + 0x0], r4 + eb8 9f 40 00 00 00 00 00 00 stxdw [r0 + 0x0], r4 + ec0 9f 19 00 00 00 00 00 00 stxdw [r9 + 0x0], r1 + ec8 9f 94 00 00 00 00 00 00 stxdw [r4 + 0x0], r9 + ed0 bf 30 00 00 00 00 00 00 mov64 r0, r3 + ed8 15 01 06 00 00 00 00 00 jeq r1, 0x0, +0x6 + ee0 9c 15 10 00 00 00 00 00 ldxdw r5, [r1 + 0x10] + ee8 1d 45 02 00 00 00 00 00 jeq r5, r4, +0x2 + ef0 07 01 00 00 08 00 00 00 add64 r1, 0x8 + ef8 05 00 01 00 00 00 00 00 ja +0x1 + f00 07 01 00 00 10 00 00 00 add64 r1, 0x10 + f08 bf 10 00 00 00 00 00 00 mov64 r0, r1 + f10 9f 90 00 00 00 00 00 00 stxdw [r0 + 0x0], r9 + f18 27 04 1c 00 01 00 00 00 stb [r4 + 0x1c], 0x1 + f20 27 09 1c 00 00 00 00 00 stb [r9 + 0x1c], 0x0 + f28 9c a5 30 00 00 00 00 00 ldxdw r5, [r10 + 0x30] + f30 bf 51 00 00 00 00 00 00 mov64 r1, r5 + f38 a7 01 00 00 01 00 00 00 xor64 r1, 0x1 + f40 67 01 00 00 03 00 00 00 lsh64 r1, 0x3 + f48 bf 68 00 00 00 00 00 00 mov64 r8, r6 + f50 0f 18 00 00 00 00 00 00 add64 r8, r1 + f58 67 05 00 00 03 00 00 00 lsh64 r5, 0x3 + f60 9c 81 08 00 00 00 00 00 ldxdw r1, [r8 + 0x8] + f68 bf 10 00 00 00 00 00 00 mov64 r0, r1 + f70 0f 50 00 00 00 00 00 00 add64 r0, r5 + f78 9c 65 00 00 00 00 00 00 ldxdw r5, [r6 + 0x0] + f80 9c 07 08 00 00 00 00 00 ldxdw r7, [r0 + 0x8] + f88 9f 78 08 00 00 00 00 00 stxdw [r8 + 0x8], r7 + f90 07 00 00 00 08 00 00 00 add64 r0, 0x8 + f98 15 07 01 00 00 00 00 00 jeq r7, 0x0, +0x1 + fa0 9f 67 00 00 00 00 00 00 stxdw [r7 + 0x0], r6 + fa8 9f 60 00 00 00 00 00 00 stxdw [r0 + 0x0], r6 + fb0 9f 51 00 00 00 00 00 00 stxdw [r1 + 0x0], r5 + fb8 9f 16 00 00 00 00 00 00 stxdw [r6 + 0x0], r1 + fc0 15 05 06 00 00 00 00 00 jeq r5, 0x0, +0x6 + fc8 9c 53 10 00 00 00 00 00 ldxdw r3, [r5 + 0x10] + fd0 1d 63 02 00 00 00 00 00 jeq r3, r6, +0x2 + fd8 07 05 00 00 08 00 00 00 add64 r5, 0x8 + fe0 05 00 01 00 00 00 00 00 ja +0x1 + fe8 07 05 00 00 10 00 00 00 add64 r5, 0x10 + ff0 bf 53 00 00 00 00 00 00 mov64 r3, r5 + ff8 9f 13 00 00 00 00 00 00 stxdw [r3 + 0x0], r1 + 1000 2c 61 1c 00 00 00 00 00 ldxb w1, [r6 + 0x1c] + 1008 54 01 00 00 01 00 00 00 and32 w1, 0x1 + 1010 2f 19 1c 00 00 00 00 00 stxb [r9 + 0x1c], w1 + 1018 27 06 1c 00 00 00 00 00 stb [r6 + 0x1c], 0x0 + 1020 07 04 00 00 1c 00 00 00 add64 r4, 0x1c + 1028 27 04 00 00 00 00 00 00 stb [r4 + 0x0], 0x0 + 1030 9c a1 38 00 00 00 00 00 ldxdw r1, [r10 + 0x38] + 1038 97 01 08 00 00 00 00 00 stdw [r1 + 0x8], 0x0 + 1040 97 01 00 00 00 00 00 00 stdw [r1 + 0x0], 0x0 + 1048 9c a1 40 00 00 00 00 00 ldxdw r1, [r10 + 0x40] + 1050 9c 13 c8 28 00 00 00 00 ldxdw r3, [r1 + 0x28c8] + 1058 9f 32 00 00 00 00 00 00 stxdw [r2 + 0x0], r3 + 1060 9f 21 c8 28 00 00 00 00 stxdw [r1 + 0x28c8], r2 + 1068 b7 00 00 00 00 00 00 00 mov64 r0, 0x0 + 1070 05 00 c8 fe 00 00 00 00 ja -0x138 + 1078 bf 18 00 00 00 00 00 00 mov64 r8, r1 + 1080 55 05 a3 00 00 00 00 00 jne r5, 0x0, +0xa3 + 1088 55 03 8a 00 01 00 00 00 jne r3, 0x1, +0x8a + 1090 55 04 8b 00 04 00 00 00 jne r4, 0x4, +0x8b + 1098 9c 81 58 00 00 00 00 00 ldxdw r1, [r8 + 0x58] + 10a0 55 01 8b 00 00 00 00 00 jne r1, 0x0, +0x8b + 10a8 2c 81 68 28 00 00 00 00 ldxb w1, [r8 + 0x2868] + 10b0 55 01 8b 00 ff 00 00 00 jne r1, 0xff, +0x8b + 10b8 9c 81 b8 28 00 00 00 00 ldxdw r1, [r8 + 0x28b8] + 10c0 55 01 99 00 00 00 00 00 jne r1, 0x0, +0x99 + 10c8 2c 81 c8 50 00 00 00 00 ldxb w1, [r8 + 0x50c8] + 10d0 55 01 89 00 ff 00 00 00 jne r1, 0xff, +0x89 + 10d8 9c 81 18 51 00 00 00 00 ldxdw r1, [r8 + 0x5118] + 10e0 55 01 89 00 0e 00 00 00 jne r1, 0xe, +0x89 + 10e8 2c 81 38 79 00 00 00 00 ldxb w1, [r8 + 0x7938] + 10f0 55 01 89 00 ff 00 00 00 jne r1, 0xff, +0x89 + 10f8 b7 00 00 00 08 00 00 00 mov64 r0, 0x8 + 1100 b4 01 00 00 06 a7 d5 17 mov32 w1, 0x17d5a706 + 1108 f7 01 00 00 19 2c 5c 51 hor64 r1, 0x515c2c19 + 1110 9c 82 40 79 00 00 00 00 ldxdw r2, [r8 + 0x7940] + 1118 5d 12 b3 fe 00 00 00 00 jne r2, r1, -0x14d + 1120 b4 01 00 00 21 8c c9 4c mov32 w1, 0x4cc98c21 + 1128 f7 01 00 00 3d 4a f1 7f hor64 r1, 0x7ff14a3d + 1130 9c 82 48 79 00 00 00 00 ldxdw r2, [r8 + 0x7948] + 1138 5d 12 af fe 00 00 00 00 jne r2, r1, -0x151 + 1140 b4 01 00 00 58 da ee 08 mov32 w1, 0x8eeda58 + 1148 f7 01 00 00 9b a1 fd 44 hor64 r1, 0x44fda19b + 1150 9c 82 50 79 00 00 00 00 ldxdw r2, [r8 + 0x7950] + 1158 5d 12 ab fe 00 00 00 00 jne r2, r1, -0x155 + 1160 bf 87 00 00 00 00 00 00 mov64 r7, r8 + 1168 9c 71 58 79 00 00 00 00 ldxdw r1, [r7 + 0x7958] + 1170 b4 02 00 00 e3 db d9 8a mov32 w2, -0x7526241d + 1178 5d 21 a7 fe 00 00 00 00 jne r1, r2, -0x159 + 1180 bf 76 00 00 00 00 00 00 mov64 r6, r7 + 1188 07 06 00 00 b9 a1 00 00 add64 r6, 0xa1b9 + 1190 bf a4 00 00 00 00 00 00 mov64 r4, r10 + 1198 07 04 00 00 a8 00 00 00 add64 r4, 0xa8 + 11a0 bf a5 00 00 00 00 00 00 mov64 r5, r10 + 11a8 07 05 00 00 4f 00 00 00 add64 r5, 0x4f + 11b0 bf 71 00 00 00 00 00 00 mov64 r1, r7 + 11b8 b7 02 00 00 00 00 00 00 mov64 r2, 0x0 + 11c0 bf 63 00 00 00 00 00 00 mov64 r3, r6 + 11c8 95 00 00 00 38 4a 50 48 syscall 0x48504a38 + 11d0 9c a1 a8 00 00 00 00 00 ldxdw r1, [r10 + 0xa8] + 11d8 9c 72 70 28 00 00 00 00 ldxdw r2, [r7 + 0x2870] + 11e0 5d 21 73 00 00 00 00 00 jne r1, r2, +0x73 + 11e8 9c a1 b0 00 00 00 00 00 ldxdw r1, [r10 + 0xb0] + 11f0 9c 82 78 28 00 00 00 00 ldxdw r2, [r8 + 0x2878] + 11f8 5d 21 70 00 00 00 00 00 jne r1, r2, +0x70 + 1200 9c a1 b8 00 00 00 00 00 ldxdw r1, [r10 + 0xb8] + 1208 9c 82 80 28 00 00 00 00 ldxdw r2, [r8 + 0x2880] + 1210 5d 21 6d 00 00 00 00 00 jne r1, r2, +0x6d + 1218 9c a1 c0 00 00 00 00 00 ldxdw r1, [r10 + 0xc0] + 1220 9c 82 88 28 00 00 00 00 ldxdw r2, [r8 + 0x2888] + 1228 5d 21 6a 00 00 00 00 00 jne r1, r2, +0x6a + 1230 bf 81 00 00 00 00 00 00 mov64 r1, r8 + 1238 07 01 00 00 70 28 00 00 add64 r1, 0x2870 + 1240 9c 82 90 79 00 00 00 00 ldxdw r2, [r8 + 0x7990] + 1248 9c 63 18 00 00 00 00 00 ldxdw r3, [r6 + 0x18] + 1250 9f 3a 7c 00 00 00 00 00 stxdw [r10 + 0x7c], r3 + 1258 9c 63 10 00 00 00 00 00 ldxdw r3, [r6 + 0x10] + 1260 9f 3a 74 00 00 00 00 00 stxdw [r10 + 0x74], r3 + 1268 9c 63 08 00 00 00 00 00 ldxdw r3, [r6 + 0x8] + 1270 9f 3a 6c 00 00 00 00 00 stxdw [r10 + 0x6c], r3 + 1278 9c 63 00 00 00 00 00 00 ldxdw r3, [r6 + 0x0] + 1280 9f 3a 64 00 00 00 00 00 stxdw [r10 + 0x64], r3 + 1288 96 02 00 00 98 00 00 00 lmul64 r2, 0x98 + 1290 9f 2a 54 00 00 00 00 00 stxdw [r10 + 0x54], r2 + 1298 97 0a 5c 00 18 00 00 00 stdw [r10 + 0x5c], 0x18 + 12a0 87 0a 50 00 00 00 00 00 stw [r10 + 0x50], 0x0 + 12a8 9f 1a 98 00 00 00 00 00 stxdw [r10 + 0x98], r1 + 12b0 bf 82 00 00 00 00 00 00 mov64 r2, r8 + 12b8 07 02 00 00 10 00 00 00 add64 r2, 0x10 + 12c0 9f 2a 88 00 00 00 00 00 stxdw [r10 + 0x88], r2 + 12c8 37 0a a0 00 01 01 00 00 sth [r10 + 0xa0], 0x101 + 12d0 37 0a 90 00 01 01 00 00 sth [r10 + 0x90], 0x101 + 12d8 bf 83 00 00 00 00 00 00 mov64 r3, r8 + 12e0 07 03 00 00 90 28 00 00 add64 r3, 0x2890 + 12e8 9f 3a 00 01 00 00 00 00 stxdw [r10 + 0x100], r3 + 12f0 bf 83 00 00 00 00 00 00 mov64 r3, r8 + 12f8 07 03 00 00 c0 28 00 00 add64 r3, 0x28c0 + 1300 9f 3a f8 00 00 00 00 00 stxdw [r10 + 0xf8], r3 + 1308 bf 83 00 00 00 00 00 00 mov64 r3, r8 + 1310 07 03 00 00 b0 28 00 00 add64 r3, 0x28b0 + 1318 9f 3a e8 00 00 00 00 00 stxdw [r10 + 0xe8], r3 + 1320 9f 1a e0 00 00 00 00 00 stxdw [r10 + 0xe0], r1 + 1328 bf 81 00 00 00 00 00 00 mov64 r1, r8 + 1330 07 01 00 00 30 00 00 00 add64 r1, 0x30 + 1338 9f 1a c8 00 00 00 00 00 stxdw [r10 + 0xc8], r1 + 1340 bf 81 00 00 00 00 00 00 mov64 r1, r8 + 1348 07 01 00 00 60 00 00 00 add64 r1, 0x60 + 1350 9f 1a c0 00 00 00 00 00 stxdw [r10 + 0xc0], r1 + 1358 bf 81 00 00 00 00 00 00 mov64 r1, r8 + 1360 07 01 00 00 50 00 00 00 add64 r1, 0x50 + 1368 9f 1a b0 00 00 00 00 00 stxdw [r10 + 0xb0], r1 + 1370 9f 2a a8 00 00 00 00 00 stxdw [r10 + 0xa8], r2 + 1378 27 0a 12 01 00 00 00 00 stb [r10 + 0x112], 0x0 + 1380 37 0a 10 01 01 01 00 00 sth [r10 + 0x110], 0x101 + 1388 97 0a 08 01 00 00 00 00 stdw [r10 + 0x108], 0x0 + 1390 97 0a f0 00 00 00 00 00 stdw [r10 + 0xf0], 0x0 + 1398 27 0a da 00 00 00 00 00 stb [r10 + 0xda], 0x0 + 13a0 37 0a d8 00 01 01 00 00 sth [r10 + 0xd8], 0x101 + 13a8 97 0a d0 00 00 00 00 00 stdw [r10 + 0xd0], 0x0 + 13b0 97 0a b8 00 00 00 00 00 stdw [r10 + 0xb8], 0x0 + 13b8 97 0a 30 01 00 00 00 00 stdw [r10 + 0x130], 0x0 + 13c0 97 0a 28 01 00 00 00 00 stdw [r10 + 0x128], 0x0 + 13c8 97 0a 20 01 00 00 00 00 stdw [r10 + 0x120], 0x0 + 13d0 97 0a 18 01 00 00 00 00 stdw [r10 + 0x118], 0x0 + 13d8 bf a1 00 00 00 00 00 00 mov64 r1, r10 + 13e0 07 01 00 00 50 00 00 00 add64 r1, 0x50 + 13e8 9f 1a 50 01 00 00 00 00 stxdw [r10 + 0x150], r1 + 13f0 bf a1 00 00 00 00 00 00 mov64 r1, r10 + 13f8 07 01 00 00 88 00 00 00 add64 r1, 0x88 + 1400 9f 1a 40 01 00 00 00 00 stxdw [r10 + 0x140], r1 + 1408 bf a1 00 00 00 00 00 00 mov64 r1, r10 + 1410 07 01 00 00 18 01 00 00 add64 r1, 0x118 + 1418 9f 1a 38 01 00 00 00 00 stxdw [r10 + 0x138], r1 + 1420 97 0a 58 01 34 00 00 00 stdw [r10 + 0x158], 0x34 + 1428 97 0a 48 01 02 00 00 00 stdw [r10 + 0x148], 0x2 + 1430 bf a1 00 00 00 00 00 00 mov64 r1, r10 + 1438 07 01 00 00 4f 00 00 00 add64 r1, 0x4f + 1440 9f 1a 60 01 00 00 00 00 stxdw [r10 + 0x160], r1 + 1448 97 0a 68 01 01 00 00 00 stdw [r10 + 0x168], 0x1 + 1450 bf a1 00 00 00 00 00 00 mov64 r1, r10 + 1458 07 01 00 00 60 01 00 00 add64 r1, 0x160 + 1460 9f 1a 70 01 00 00 00 00 stxdw [r10 + 0x170], r1 + 1468 97 0a 78 01 01 00 00 00 stdw [r10 + 0x178], 0x1 + 1470 bf a1 00 00 00 00 00 00 mov64 r1, r10 + 1478 07 01 00 00 38 01 00 00 add64 r1, 0x138 + 1480 bf a2 00 00 00 00 00 00 mov64 r2, r10 + 1488 07 02 00 00 a8 00 00 00 add64 r2, 0xa8 + 1490 bf a4 00 00 00 00 00 00 mov64 r4, r10 + 1498 07 04 00 00 70 01 00 00 add64 r4, 0x170 + 14a0 b7 03 00 00 02 00 00 00 mov64 r3, 0x2 + 14a8 b7 05 00 00 01 00 00 00 mov64 r5, 0x1 + 14b0 95 00 00 00 85 9c 2b a2 syscall -0x5dd4637b + 14b8 bf 81 00 00 00 00 00 00 mov64 r1, r8 + 14c0 07 01 00 00 d8 28 00 00 add64 r1, 0x28d8 + 14c8 9f 18 d0 28 00 00 00 00 stxdw [r8 + 0x28d0], r1 + 14d0 b7 00 00 00 00 00 00 00 mov64 r0, 0x0 + 14d8 05 00 3b fe 00 00 00 00 ja -0x1c5 + 14e0 b7 00 00 00 0c 00 00 00 mov64 r0, 0xc + 14e8 05 00 39 fe 00 00 00 00 ja -0x1c7 + 14f0 b7 00 00 00 01 00 00 00 mov64 r0, 0x1 + 14f8 05 00 37 fe 00 00 00 00 ja -0x1c9 + 1500 b7 00 00 00 02 00 00 00 mov64 r0, 0x2 + 1508 05 00 35 fe 00 00 00 00 ja -0x1cb + 1510 b7 00 00 00 05 00 00 00 mov64 r0, 0x5 + 1518 05 00 33 fe 00 00 00 00 ja -0x1cd + 1520 b7 00 00 00 06 00 00 00 mov64 r0, 0x6 + 1528 05 00 31 fe 00 00 00 00 ja -0x1cf + 1530 b7 00 00 00 04 00 00 00 mov64 r0, 0x4 + 1538 05 00 2f fe 00 00 00 00 ja -0x1d1 + 1540 b7 00 00 00 07 00 00 00 mov64 r0, 0x7 + 1548 05 00 2d fe 00 00 00 00 ja -0x1d3 + 1550 9c a1 40 00 00 00 00 00 ldxdw r1, [r10 + 0x40] + 1558 97 01 c0 28 00 00 00 00 stdw [r1 + 0x28c0], 0x0 + 1560 9c a3 38 00 00 00 00 00 ldxdw r3, [r10 + 0x38] + 1568 97 03 08 00 00 00 00 00 stdw [r3 + 0x8], 0x0 + 1570 97 03 00 00 00 00 00 00 stdw [r3 + 0x0], 0x0 + 1578 05 00 5a ff 00 00 00 00 ja -0xa6 + 1580 b7 00 00 00 0a 00 00 00 mov64 r0, 0xa + 1588 05 00 25 fe 00 00 00 00 ja -0x1db + 1590 b7 00 00 00 03 00 00 00 mov64 r0, 0x3 + 1598 05 00 23 fe 00 00 00 00 ja -0x1dd + 15a0 b7 00 00 00 0b 00 00 00 mov64 r0, 0xb + 15a8 05 00 21 fe 00 00 00 00 ja -0x1df \ No newline at end of file diff --git a/examples/tree/artifacts/rs-disassembly.s b/examples/tree/artifacts/rs-disassembly.s new file mode 100644 index 00000000..62f98877 --- /dev/null +++ b/examples/tree/artifacts/rs-disassembly.s @@ -0,0 +1,870 @@ +.globl entrypoint + +entrypoint: + add64 r10, -384 + ldxdw r4, [r1+0] + ldxdw r3, [r2-8] + ldxb r5, [r2+0] + jne r5, 1, jmp_0780 + jne r3, 5, jmp_1518 + jlt r4, 2, jmp_1528 + ldxdw r3, [r1+88] + jne r3, 0, jmp_1538 + ldxb r3, [r1+10344] + jne r3, 255, jmp_1548 + mov64 r6, r1 + add64 r6, 10432 + ldxdw r3, [r1+10440] + jeq r3, 0, jmp_0278 + ldxdw r4, [r3+0] + stxdw [r1+10440], r4 + ldxw r1, [r2+1] + stxw [r3+24], r1 + ldxdw r4, [r6+0] + jeq r4, 0, jmp_05d8 + +jmp_00a8: + ldxh r1, [r2+1] + +jmp_00b0: + mov64 r2, r4 + ldxh r4, [r2+24] + mov64 r5, r1 + jle r5, r4, jmp_00e8 + ldxdw r4, [r2+16] + jne r4, 0, jmp_00b0 + ja jmp_0120 + +jmp_00e8: + jge r5, r4, jmp_0ce0 + ldxdw r4, [r2+8] + jne r4, 0, jmp_00b0 + stxdw [r3+0], r2 + stb [r3+28], 1 + stxdw [r2+8], r3 + ja jmp_0138 + +jmp_0120: + stxdw [r3+0], r2 + stb [r3+28], 1 + stxdw [r2+16], r3 + +jmp_0138: + ldxb r1, [r2+28] + jeq r1, 0, jmp_05f0 + mov64 r0, 0 + ja jmp_0188 + +jmp_0158: + stb [r2+28], 0 + stb [r4+28], 0 + stb [r1+28], 1 + ldxdw r2, [r1+0] + mov64 r3, r1 + jeq r2, 0, jmp_06a8 + +jmp_0188: + ldxb r1, [r2+28] + jeq r1, 0, jmp_06a8 + ldxdw r1, [r2+0] + jeq r1, 0, jmp_0680 + ldxdw r4, [r1+8] + jeq r2, r4, jmp_01d8 + jeq r4, 0, jmp_0600 + ldxb r5, [r4+28] + jne r5, 0, jmp_0158 + ja jmp_0600 + +jmp_01d8: + ldxdw r4, [r1+16] + jeq r4, 0, jmp_01f8 + ldxb r5, [r4+28] + jne r5, 0, jmp_0158 + +jmp_01f8: + ldxdw r4, [r2+16] + jeq r3, r4, jmp_0718 + mov64 r3, r4 + mov64 r4, r2 + stxdw [r1+8], r3 + ldxdw r2, [r1+0] + jeq r3, 0, jmp_0238 + +jmp_0230: + stxdw [r3+0], r1 + +jmp_0238: + stxdw [r4+0], r2 + stxdw [r4+16], r1 + stxdw [r1+0], r4 + jeq r2, 0, jmp_0690 + ldxdw r3, [r2+16] + jne r1, r3, jmp_0670 + +jmp_0268: + stxdw [r2+16], r4 + ja jmp_0698 + +jmp_0278: + jne r4, 4, jmp_0830 + ldxdw r3, [r1+10424] + mov64 r5, r3 + add64 r5, 7 + and64 r5, -8 + mov64 r4, r1 + add64 r4, r5 + ldxb r5, [r4+20680] + jne r5, 255, jmp_1558 + ldxdw r5, [r4+20760] + jne r5, 14, jmp_1568 + ldxb r5, [r4+31032] + jne r5, 255, jmp_1578 + mov64 r0, 8 + mov32 r5, 399877894 + hor64 r5, 1364995097 + ldxdw r7, [r4+31040] + jne r7, r5, jmp_06a8 + mov32 r5, 1288277025 + hor64 r5, 2146519613 + ldxdw r7, [r4+31048] + jne r7, r5, jmp_06a8 + mov32 r5, 149871192 + hor64 r5, 1157472667 + ldxdw r7, [r4+31056] + jne r7, r5, jmp_06a8 + mov64 r7, r2 + ldxdw r2, [r4+31064] + mov32 r5, -1965433885 + jne r2, r5, jmp_06a8 + ldxdw r2, [r4+31120] + lmul64 r2, 29 + stxdw [r10+372], r2 + stw [r10+368], 2 + mov64 r4, r1 + add64 r4, 10352 + stxdw [r10+296], r4 + mov64 r2, r1 + add64 r2, 16 + stxdw [r10+280], r2 + sth [r10+304], 1 + sth [r10+288], 257 + mov64 r5, r1 + add64 r5, 10384 + stxdw [r10+256], r5 + stxdw [r10+248], r6 + stxdw [r10+240], r3 + mov64 r3, r1 + add64 r3, 10416 + stxdw [r10+232], r3 + stxdw [r10+224], r4 + mov64 r3, r1 + add64 r3, 48 + stxdw [r10+200], r3 + mov64 r3, r1 + add64 r3, 96 + stxdw [r10+192], r3 + mov64 r3, r1 + add64 r3, 80 + stxdw [r10+176], r3 + stxdw [r10+168], r2 + stb [r10+274], 0 + sth [r10+272], 256 + stdw [r10+264], 0 + stb [r10+218], 0 + sth [r10+216], 257 + stdw [r10+208], 0 + stdw [r10+184], 0 + stdw [r10+336], 0 + stdw [r10+328], 0 + stdw [r10+320], 0 + stdw [r10+312], 0 + mov64 r2, r10 + add64 r2, 368 + stxdw [r10+104], r2 + mov64 r2, r10 + add64 r2, 280 + stxdw [r10+88], r2 + mov64 r2, r10 + add64 r2, 312 + stxdw [r10+80], r2 + stdw [r10+112], 12 + stdw [r10+96], 2 + stdw [r10+144], 0 + stdw [r10+136], 0 + mov64 r3, r10 + add64 r3, 80 + mov64 r2, r10 + add64 r2, 168 + mov64 r4, r10 + add64 r4, 136 + mov64 r8, r1 + mov64 r1, r3 + mov64 r3, 2 + mov64 r5, 0 + call sol_invoke_signed_c + ldxdw r1, [r8+10424] + add64 r1, 29 + stxdw [r8+10424], r1 + ldxdw r3, [r8+10448] + mov64 r1, r3 + add64 r1, 29 + stxdw [r8+10448], r1 + mov64 r2, r7 + ldxw r1, [r2+1] + stxw [r3+24], r1 + ldxdw r4, [r6+0] + jne r4, 0, jmp_00a8 + +jmp_05d8: + stdw [r3+0], 0 + stb [r3+28], 1 + stxdw [r6+0], r3 + +jmp_05f0: + mov64 r0, 0 + ja jmp_06a8 + +jmp_0600: + ldxdw r4, [r2+8] + jeq r3, r4, jmp_06b0 + mov64 r3, r4 + mov64 r4, r2 + stxdw [r1+16], r3 + ldxdw r2, [r1+0] + jeq r3, 0, jmp_0640 + +jmp_0638: + stxdw [r3+0], r1 + +jmp_0640: + stxdw [r4+0], r2 + stxdw [r4+8], r1 + stxdw [r1+0], r4 + jeq r2, 0, jmp_0690 + ldxdw r3, [r2+16] + jeq r1, r3, jmp_0268 + +jmp_0670: + stxdw [r2+8], r4 + ja jmp_0698 + +jmp_0680: + stb [r2+28], 0 + ja jmp_06a8 + +jmp_0690: + stxdw [r6+0], r4 + +jmp_0698: + stb [r4+28], 0 + stb [r1+28], 1 + +jmp_06a8: + exit + +jmp_06b0: + ldxdw r3, [r4+16] + stxdw [r2+8], r3 + jeq r3, 0, jmp_06d0 + stxdw [r3+0], r2 + +jmp_06d0: + stxdw [r4+0], r1 + stxdw [r4+16], r2 + stxdw [r2+0], r4 + stxdw [r1+16], r4 + ldxdw r3, [r4+8] + stxdw [r1+16], r3 + ldxdw r2, [r1+0] + jne r3, 0, jmp_0638 + ja jmp_0640 + +jmp_0718: + ldxdw r3, [r4+8] + stxdw [r2+16], r3 + jeq r3, 0, jmp_0738 + stxdw [r3+0], r2 + +jmp_0738: + stxdw [r4+0], r1 + stxdw [r4+8], r2 + stxdw [r2+0], r4 + stxdw [r1+8], r4 + ldxdw r3, [r4+16] + stxdw [r1+8], r3 + ldxdw r2, [r1+0] + jne r3, 0, jmp_0230 + ja jmp_0238 + +jmp_0780: + jne r5, 2, jmp_10b0 + jne r3, 3, jmp_1518 + jlt r4, 2, jmp_1528 + ldxdw r3, [r1+88] + jne r3, 0, jmp_1538 + ldxb r3, [r1+10344] + jne r3, 255, jmp_1548 + mov64 r0, 15 + ldxdw r3, [r1+10432] + jeq r3, 0, jmp_06a8 + ldxh r4, [r2+1] + ja jmp_07f0 + +jmp_07e0: + ldxdw r3, [r3+16] + jeq r3, 0, jmp_06a8 + +jmp_07f0: + ldxh r5, [r3+24] + mov64 r6, r4 + jgt r6, r5, jmp_07e0 + ldxdw r2, [r3+8] + jge r6, r5, jmp_0840 + mov64 r3, r2 + jne r2, 0, jmp_07f0 + ja jmp_06a8 + +jmp_0830: + mov64 r0, 13 + ja jmp_06a8 + +jmp_0840: + stxdw [r10+64], r1 + jeq r2, 0, jmp_0890 + ldxdw r1, [r3+16] + jeq r1, 0, jmp_0938 + +jmp_0860: + mov64 r2, r1 + ldxdw r1, [r2+8] + jne r1, 0, jmp_0860 + ldxw r1, [r2+24] + stxw [r3+24], r1 + ja jmp_0898 + +jmp_0890: + mov64 r2, r3 + +jmp_0898: + ldxdw r4, [r2+0] + mov64 r1, r2 + add64 r1, 8 + stxdw [r10+48], r1 + ldxdw r1, [r2+16] + jeq r1, 0, jmp_0900 + stxdw [r1+0], r4 + stb [r1+28], 0 + jeq r4, 0, jmp_0988 + ldxdw r3, [r4+16] + jeq r2, r3, jmp_0b10 + stxdw [r4+8], r1 + ja jmp_1070 + +jmp_0900: + jeq r4, 0, jmp_1588 + ldxdw r1, [r4+16] + ldxb r3, [r2+28] + jne r3, 1, jmp_09a0 + jeq r2, r1, jmp_0cd0 + stdw [r4+8], 0 + ja jmp_1070 + +jmp_0938: + mov64 r1, r3 + add64 r1, 8 + ldxdw r4, [r3+0] + stxdw [r2+0], r4 + stb [r2+28], 0 + jeq r4, 0, jmp_0b20 + ldxdw r5, [r4+16] + jeq r3, r5, jmp_0c90 + stxdw [r4+8], r2 + ja jmp_0c98 + +jmp_0988: + ldxdw r3, [r10+64] + stxdw [r3+10432], r1 + ja jmp_1070 + +jmp_09a0: + mov32 r3, 1 + stxdw [r10+32], r3 + jeq r2, r1, jmp_09c8 + mov32 r1, 0 + stxdw [r10+32], r1 + +jmp_09c8: + mov64 r8, r4 + add64 r8, 8 + ldxdw r5, [r10+32] + mov64 r6, r5 + lsh64 r6, 3 + mov64 r1, r8 + add64 r1, r6 + stdw [r1+0], 0 + xor64 r5, 1 + mov64 r9, r5 + lsh64 r9, 3 + add64 r8, r9 + ldxdw r3, [r8+0] + mov64 r7, r3 + add64 r7, 28 + mov64 r1, r3 + add64 r1, 8 + mov64 r0, r1 + add64 r0, r6 + ldxdw r6, [r0+0] + stxdw [r10+56], r6 + stxdw [r10+40], r3 + ldxb r6, [r3+28] + jeq r6, 0, jmp_0b38 + mov64 r6, r4 + ldxdw r1, [r6+0] + ldxdw r3, [r10+56] + stxdw [r8+0], r3 + jeq r3, 0, jmp_0ac0 + +jmp_0ab0: + ldxdw r3, [r10+56] + stxdw [r3+0], r6 + +jmp_0ac0: + stxdw [r0+0], r6 + ldxdw r3, [r10+40] + stxdw [r3+0], r1 + stxdw [r6+0], r3 + jeq r1, 0, jmp_0cf0 + ldxdw r4, [r1+16] + jeq r6, r4, jmp_0d10 + ldxdw r3, [r10+40] + stxdw [r1+8], r3 + ja jmp_0d20 + +jmp_0b10: + stxdw [r4+16], r1 + ja jmp_1070 + +jmp_0b20: + ldxdw r4, [r10+64] + stxdw [r4+10432], r2 + ja jmp_0c98 + +jmp_0b38: + add64 r1, r9 + ja jmp_0c00 + +jmp_0b48: + mov64 r5, r3 + xor64 r5, 1 + mov64 r4, r5 + lsh64 r4, 3 + mov64 r8, r6 + add64 r8, r4 + ldxdw r9, [r8+8] + mov64 r0, r9 + add64 r0, 8 + mov64 r1, r0 + add64 r1, r4 + stxdw [r10+32], r3 + mov64 r4, r3 + lsh64 r4, 3 + add64 r0, r4 + mov64 r7, r9 + add64 r7, 28 + ldxdw r3, [r0+0] + stxdw [r10+56], r3 + stxdw [r10+40], r9 + ldxb r3, [r9+28] + mov64 r4, r6 + jne r3, 0, jmp_0ef8 + +jmp_0c00: + ldxdw r1, [r1+0] + jeq r1, 0, jmp_0c20 + ldxb r0, [r1+28] + jne r0, 0, jmp_0f40 + +jmp_0c20: + ldxdw r9, [r10+56] + jeq r9, 0, jmp_0c40 + ldxb r1, [r9+28] + jne r1, 0, jmp_0df8 + +jmp_0c40: + ldxb r1, [r4+28] + stb [r7+0], 1 + jeq r1, 1, jmp_0ee8 + ldxdw r6, [r4+0] + jeq r6, 0, jmp_1070 + ldxdw r1, [r6+16] + mov32 r3, 1 + jeq r4, r1, jmp_0b48 + mov32 r3, 0 + ja jmp_0b48 + +jmp_0c90: + stxdw [r4+16], r2 + +jmp_0c98: + stdw [r1+8], 0 + stdw [r1+0], 0 + ldxdw r1, [r10+64] + ldxdw r2, [r1+10440] + stxdw [r3+0], r2 + stxdw [r1+10440], r3 + ja jmp_05f0 + +jmp_0cd0: + stdw [r4+16], 0 + ja jmp_1070 + +jmp_0ce0: + mov64 r0, 14 + ja jmp_06a8 + +jmp_0cf0: + ldxdw r1, [r10+64] + ldxdw r3, [r10+40] + stxdw [r1+10432], r3 + ja jmp_0d20 + +jmp_0d10: + ldxdw r3, [r10+40] + stxdw [r1+16], r3 + +jmp_0d20: + stb [r6+28], 1 + stb [r7+0], 0 + mov64 r3, r5 + lsh64 r3, 3 + ldxdw r1, [r10+56] + add64 r1, 8 + mov64 r4, r1 + add64 r4, r3 + ldxdw r3, [r4+0] + stxdw [r10+40], r3 + jeq r3, 0, jmp_0da8 + ldxdw r3, [r10+40] + ldxb r4, [r3+28] + jeq r4, 0, jmp_0da8 + mov64 r4, r6 + ldxdw r9, [r10+56] + ja jmp_0f78 + +jmp_0da8: + ldxdw r3, [r10+32] + lsh64 r3, 3 + add64 r1, r3 + ldxdw r9, [r1+0] + jeq r9, 0, jmp_0ec8 + ldxb r1, [r9+28] + jeq r1, 0, jmp_0ec8 + mov64 r4, r6 + ldxdw r1, [r10+56] + stxdw [r10+40], r1 + +jmp_0df8: + mov64 r1, r5 + lsh64 r1, 3 + mov64 r0, r9 + add64 r0, r1 + ldxdw r1, [r10+32] + lsh64 r1, 3 + ldxdw r6, [r10+40] + mov64 r3, r6 + add64 r3, r1 + ldxdw r1, [r6+0] + ldxdw r6, [r0+8] + stxdw [r3+8], r6 + add64 r0, 8 + jeq r6, 0, jmp_0e78 + ldxdw r3, [r10+40] + stxdw [r6+0], r3 + +jmp_0e78: + ldxdw r3, [r10+40] + stxdw [r0+0], r3 + stxdw [r9+0], r1 + stxdw [r3+0], r9 + jeq r1, 0, jmp_0f28 + ldxdw r3, [r1+16] + ldxdw r0, [r10+40] + jeq r0, r3, jmp_0f58 + stxdw [r1+8], r9 + ja jmp_0f60 + +jmp_0ec8: + ldxdw r1, [r10+56] + stb [r1+28], 1 + stb [r6+28], 0 + ja jmp_1070 + +jmp_0ee8: + stb [r4+28], 0 + ja jmp_1070 + +jmp_0ef8: + add64 r8, 8 + ldxdw r1, [r6+0] + ldxdw r3, [r10+56] + stxdw [r8+0], r3 + jne r3, 0, jmp_0ab0 + ja jmp_0ac0 + +jmp_0f28: + ldxdw r1, [r10+64] + stxdw [r1+10432], r9 + ja jmp_0f60 + +jmp_0f40: + ldxdw r9, [r10+40] + stxdw [r10+40], r1 + ja jmp_0f78 + +jmp_0f58: + stxdw [r1+16], r9 + +jmp_0f60: + ldxdw r1, [r10+40] + stb [r1+28], 1 + stb [r9+28], 0 + +jmp_0f78: + lsh64 r5, 3 + mov64 r3, r4 + add64 r3, r5 + ldxdw r5, [r10+32] + lsh64 r5, 3 + ldxdw r1, [r3+8] + mov64 r0, r1 + add64 r0, r5 + ldxdw r5, [r4+0] + ldxdw r6, [r0+8] + stxdw [r3+8], r6 + add64 r0, 8 + jeq r6, 0, jmp_0fe8 + stxdw [r6+0], r4 + +jmp_0fe8: + stxdw [r0+0], r4 + stxdw [r1+0], r5 + stxdw [r4+0], r1 + jeq r5, 0, jmp_1028 + ldxdw r3, [r5+16] + jeq r4, r3, jmp_1040 + stxdw [r5+8], r1 + ja jmp_1048 + +jmp_1028: + ldxdw r5, [r10+64] + stxdw [r5+10432], r1 + ja jmp_1048 + +jmp_1040: + stxdw [r5+16], r1 + +jmp_1048: + ldxb r1, [r4+28] + stxb [r9+28], r1 + stb [r4+28], 0 + ldxdw r1, [r10+40] + stb [r1+28], 0 + +jmp_1070: + ldxdw r1, [r10+48] + stdw [r1+8], 0 + stdw [r1+0], 0 + ldxdw r1, [r10+64] + +jmp_1090: + ldxdw r3, [r1+10440] + stxdw [r2+0], r3 + stxdw [r1+10440], r2 + ja jmp_05f0 + +jmp_10b0: + mov64 r8, r1 + jne r5, 0, jmp_15c8 + jne r3, 1, jmp_1518 + jne r4, 4, jmp_1528 + ldxdw r1, [r8+88] + jne r1, 0, jmp_1538 + ldxb r1, [r8+10344] + jne r1, 255, jmp_1548 + ldxdw r1, [r8+10424] + jne r1, 0, jmp_15b8 + ldxb r1, [r8+20680] + jne r1, 255, jmp_1558 + ldxdw r1, [r8+20760] + jne r1, 14, jmp_1568 + ldxb r1, [r8+31032] + jne r1, 255, jmp_1578 + mov64 r0, 8 + mov32 r1, 399877894 + hor64 r1, 1364995097 + ldxdw r2, [r8+31040] + jne r2, r1, jmp_06a8 + mov32 r1, 1288277025 + hor64 r1, 2146519613 + ldxdw r2, [r8+31048] + jne r2, r1, jmp_06a8 + mov32 r1, 149871192 + hor64 r1, 1157472667 + ldxdw r2, [r8+31056] + jne r2, r1, jmp_06a8 + mov64 r7, r8 + ldxdw r1, [r7+31064] + mov32 r2, -1965433885 + jne r1, r2, jmp_06a8 + mov64 r6, r7 + add64 r6, 41401 + mov64 r4, r10 + add64 r4, 168 + mov64 r5, r10 + add64 r5, 79 + mov64 r1, r7 + mov64 r2, 0 + mov64 r3, r6 + call sol_try_find_program_address + mov64 r0, 10 + ldxdw r1, [r10+168] + ldxdw r2, [r7+10352] + jne r1, r2, jmp_06a8 + ldxdw r1, [r10+176] + ldxdw r2, [r8+10360] + jne r1, r2, jmp_06a8 + ldxdw r1, [r10+184] + ldxdw r2, [r8+10368] + jne r1, r2, jmp_06a8 + ldxdw r1, [r10+192] + ldxdw r2, [r8+10376] + jne r1, r2, jmp_06a8 + mov64 r1, r8 + add64 r1, 10352 + ldxdw r2, [r8+31120] + ldxdw r3, [r6+24] + stxdw [r10+124], r3 + ldxdw r3, [r6+16] + stxdw [r10+116], r3 + ldxdw r3, [r6+8] + stxdw [r10+108], r3 + ldxdw r3, [r6+0] + stxdw [r10+100], r3 + lmul64 r2, 152 + stxdw [r10+84], r2 + stdw [r10+92], 24 + stw [r10+80], 0 + stxdw [r10+152], r1 + mov64 r2, r8 + add64 r2, 16 + stxdw [r10+136], r2 + sth [r10+160], 257 + sth [r10+144], 257 + mov64 r3, r8 + add64 r3, 10384 + stxdw [r10+256], r3 + mov64 r3, r8 + add64 r3, 10432 + stxdw [r10+248], r3 + mov64 r3, r8 + add64 r3, 10416 + stxdw [r10+232], r3 + stxdw [r10+224], r1 + mov64 r1, r8 + add64 r1, 48 + stxdw [r10+200], r1 + mov64 r1, r8 + add64 r1, 96 + stxdw [r10+192], r1 + mov64 r1, r8 + add64 r1, 80 + stxdw [r10+176], r1 + stxdw [r10+168], r2 + stb [r10+274], 0 + sth [r10+272], 257 + stdw [r10+264], 0 + stdw [r10+240], 0 + stb [r10+218], 0 + sth [r10+216], 257 + stdw [r10+208], 0 + stdw [r10+184], 0 + stdw [r10+304], 0 + stdw [r10+296], 0 + stdw [r10+288], 0 + stdw [r10+280], 0 + mov64 r1, r10 + add64 r1, 80 + stxdw [r10+336], r1 + mov64 r1, r10 + add64 r1, 136 + stxdw [r10+320], r1 + mov64 r1, r10 + add64 r1, 280 + stxdw [r10+312], r1 + stdw [r10+344], 52 + stdw [r10+328], 2 + mov64 r1, r10 + add64 r1, 79 + stxdw [r10+352], r1 + stdw [r10+360], 1 + mov64 r1, r10 + add64 r1, 352 + stxdw [r10+368], r1 + stdw [r10+376], 1 + mov64 r1, r10 + add64 r1, 312 + mov64 r2, r10 + add64 r2, 168 + mov64 r4, r10 + add64 r4, 368 + mov64 r3, 2 + mov64 r5, 1 + call sol_invoke_signed_c + mov64 r1, r8 + add64 r1, 10456 + stxdw [r8+10448], r1 + ja jmp_05f0 + +jmp_1518: + mov64 r0, 12 + ja jmp_06a8 + +jmp_1528: + mov64 r0, 1 + ja jmp_06a8 + +jmp_1538: + mov64 r0, 2 + ja jmp_06a8 + +jmp_1548: + mov64 r0, 5 + ja jmp_06a8 + +jmp_1558: + mov64 r0, 6 + ja jmp_06a8 + +jmp_1568: + mov64 r0, 4 + ja jmp_06a8 + +jmp_1578: + mov64 r0, 7 + ja jmp_06a8 + +jmp_1588: + ldxdw r1, [r10+64] + stdw [r1+10432], 0 + ldxdw r3, [r10+48] + stdw [r3+8], 0 + stdw [r3+0], 0 + ja jmp_1090 + +jmp_15b8: + mov64 r0, 3 + ja jmp_06a8 + +jmp_15c8: + mov64 r0, 11 + ja jmp_06a8 diff --git a/examples/tree/artifacts/snippets/asm/constants.txt b/examples/tree/artifacts/snippets/asm/constants.txt new file mode 100644 index 00000000..d6bec776 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/constants.txt @@ -0,0 +1,239 @@ +# Error codes. +# ------------ +.equ E_N_ACCOUNTS, 1 # An invalid number of accounts were passed. +.equ E_USER_DATA_LEN, 2 # The user account has invalid data length. +.equ E_TREE_DATA_LEN, 3 # The tree account has invalid data length. +# The System Program account has invalid data length. +.equ E_SYSTEM_PROGRAM_DATA_LEN, 4 +.equ E_TREE_DUPLICATE, 5 # The tree account is a duplicate. +# The System Program account is a duplicate. +.equ E_SYSTEM_PROGRAM_DUPLICATE, 6 +.equ E_RENT_DUPLICATE, 7 # The rent sysvar account is a duplicate. +.equ E_RENT_ADDRESS, 8 # The rent sysvar account has invalid data length. +# Instruction data provided during initialization instruction. +.equ E_INSTRUCTION_DATA, 9 +# The passed PDA does not match the expected address. +.equ E_PDA_MISMATCH, 10 +.equ E_INSTRUCTION_DISCRIMINATOR, 11 # Invalid instruction discriminator. +.equ E_INSTRUCTION_DATA_LEN, 12 # Invalid instruction data length. +# Not enough accounts passed for insertion allocation. +.equ E_N_ACCOUNTS_INSERT_ALLOCATION, 13 +.equ E_KEY_EXISTS, 14 # Key already exists in tree during insertion. +.equ E_KEY_DOES_NOT_EXIST, 15 # Key does not exist in tree during removal. + +# Type sizes. +# ----------- +.equ SIZE_OF_U8, 1 # Size of u8. +.equ SIZE_OF_U64, 8 # Size of u64. +.equ SIZE_OF_ADDRESS, 32 # Size of Address. +.equ SIZE_OF_U128, 16 # Size of u128. +.equ SIZE_OF_TREE_HEADER, 24 # Size of TreeHeader. +.equ SIZE_OF_INITIALIZE_INSTRUCTION, 1 # Size of InitializeInstruction. +.equ SIZE_OF_INSERT_INSTRUCTION, 5 # Size of InsertInstruction. +.equ SIZE_OF_REMOVE_INSTRUCTION, 3 # Size of RemoveInstruction. +.equ SIZE_OF_TREE_NODE, 29 # Size of TreeNode. + +# Data layout constants. +# ---------------------- +.equ DATA_LEN_ZERO, 0 # Data length of zero. +.equ BPF_ALIGN_OF_U128, 8 # Data alignment during runtime. +.equ OFFSET_ZERO, 0 # No offset. +.equ NULL, 0 # Null pointer. +.equ DATA_LEN_AND_MASK, -8 # And mask for data length alignment. +.equ MAX_DATA_PAD, 7 # Maximum possible data length padding. +.equ BOOL_TRUE, 1 # Boolean true value. + +# Pubkey chunking offsets. +# ------------------------ +.equ PUBKEY_CHUNK_OFF_0, 0 # Offset for the first 8 bytes. +.equ PUBKEY_CHUNK_OFF_1, 8 # Offset for the second 8 bytes. +.equ PUBKEY_CHUNK_OFF_2, 16 # Offset for the third 8 bytes. +.equ PUBKEY_CHUNK_OFF_3, 24 # Offset for the fourth 8 bytes. + +# Input buffer layout. +# -------------------- +.equ IB_N_ACCOUNTS_OFF, 0 # Number of accounts field. +.equ IB_USER_ACCOUNT_OFF, 8 # User runtime account. +.equ IB_USER_LAMPORTS_OFF, 80 # User Lamports field. +.equ IB_USER_DATA_OFF, 96 # User data field. +.equ IB_USER_OWNER_OFF, 48 # User owner field. +.equ IB_TREE_LAMPORTS_OFF, 10416 # Tree Lamports field. +.equ IB_TREE_DATA_OFF, 10432 # Tree data field. +.equ IB_TREE_OWNER_OFF, 10384 # Tree owner field. +.equ IB_TREE_ACCOUNT_OFF, 10344 # Tree runtime account header. +.equ IB_TREE_ADDRESS_OFF, 10352 # Tree address field. +# System Program runtime account header. +.equ IB_SYSTEM_PROGRAM_ACCOUNT_OFF, 20680 +.equ IB_RENT_ACCOUNT_OFF, 31032 # Rent sysvar account header. +.equ IB_RENT_DATA_OFF, 31120 # Rent sysvar account data. +# Expected number of accounts for general instructions. +.equ IB_N_ACCOUNTS_GENERAL, 2 +# Expected number of accounts for tree initialization. +.equ IB_N_ACCOUNTS_INIT, 4 +# Expected data length of system program account. +.equ IB_SYSTEM_PROGRAM_DATA_LEN, 14 +.equ IB_RENT_DATA_LEN, 17 # Expected data length of rent sysvar account. +.equ IB_USER_ADDRESS_OFF, 16 # User address field. +.equ IB_USER_DATA_LEN_OFF, 88 # User data length field. +.equ IB_NON_DUP_MARKER, 255 # Non-duplicate marker value. +.equ IB_TREE_NON_DUP_MARKER_OFF, 10344 # Tree non-duplicate marker field. +.equ IB_TREE_ADDRESS_OFF_0, 10352 # Tree address field (chunk index 0). +.equ IB_TREE_ADDRESS_OFF_1, 10360 # Tree address field (chunk index 1). +.equ IB_TREE_ADDRESS_OFF_2, 10368 # Tree address field (chunk index 2). +.equ IB_TREE_ADDRESS_OFF_3, 10376 # Tree address field (chunk index 3). +.equ IB_TREE_DATA_LEN_OFF, 10424 # Tree data length field. +# System Program non-duplicate marker field. +.equ IB_SYSTEM_PROGRAM_NON_DUP_MARKER_OFF, 20680 +# System Program data length field. +.equ IB_SYSTEM_PROGRAM_DATA_LEN_OFF, 20760 +# Rent account non-duplicate marker field. +.equ IB_RENT_NON_DUP_MARKER_OFF, 31032 +.equ IB_RENT_ADDRESS_OFF_0, 31040 # Rent address field (chunk index 0). +.equ IB_RENT_ADDRESS_OFF_1, 31048 # Rent address field (chunk index 1). +.equ IB_RENT_ADDRESS_OFF_2, 31056 # Rent address field (chunk index 2). +.equ IB_RENT_ADDRESS_OFF_3, 31064 # Rent address field (chunk index 3). +.equ IB_RENT_ID_CHUNK_0, 5862609301215225606 # Rent sysvar ID (chunk 0). +.equ IB_RENT_ID_CHUNK_0_LO, 399877894 # Rent sysvar ID (chunk 0 lo). +.equ IB_RENT_ID_CHUNK_0_HI, 1364995097 # Rent sysvar ID (chunk 0 hi). +.equ IB_RENT_ID_CHUNK_1, 9219231539345853473 # Rent sysvar ID (chunk 1). +.equ IB_RENT_ID_CHUNK_1_LO, 1288277025 # Rent sysvar ID (chunk 1 lo). +.equ IB_RENT_ID_CHUNK_1_HI, 2146519613 # Rent sysvar ID (chunk 1 hi). +.equ IB_RENT_ID_CHUNK_2, 4971307250928769624 # Rent sysvar ID (chunk 2). +.equ IB_RENT_ID_CHUNK_2_LO, 149871192 # Rent sysvar ID (chunk 2 lo). +.equ IB_RENT_ID_CHUNK_2_HI, 1157472667 # Rent sysvar ID (chunk 2 hi). +.equ IB_RENT_ID_CHUNK_3, 2329533411 # Rent sysvar ID (chunk 3). +.equ IB_RENT_ID_CHUNK_3_LO, -1965433885 # Rent sysvar ID (chunk 3 lo). +.equ IB_RENT_ID_CHUNK_3_HI, 0 # Rent sysvar ID (chunk 3 hi). +# Program ID field for initialize instruction. +.equ IB_INIT_PROGRAM_ID_OFF_IMM, 41401 +.equ IB_TREE_DATA_TOP_OFF, 10440 # Tree top pointer field within tree data. +# Tree next pointer field within tree data. +.equ IB_TREE_DATA_NEXT_OFF, 10448 +# Tree root pointer field within tree data. +.equ IB_TREE_DATA_ROOT_OFF, 10432 +# Relative offset from user data field to tree pubkey field. +.equ IB_USER_DATA_TO_TREE_ADDRESS_REL_OFF_IMM, 10256 + +# Offsets for instruction processing. +# ----------------------------------- +.equ INSN_DISCRIMINATOR_OFF, 0 # Offset to instruction discriminator byte. +# Initialize instruction discriminator. +.equ INSN_DISCRIMINATOR_INITIALIZE, 0 +.equ INSN_DISCRIMINATOR_INSERT, 1 # Insert instruction discriminator. +.equ INSN_DISCRIMINATOR_REMOVE, 2 # Remove instruction discriminator. +.equ INSN_INSERT_KEY_OFF, 1 # Key field in insert instruction. +.equ INSN_INSERT_VALUE_OFF, 3 # Value field in insert instruction. +.equ INSN_REMOVE_KEY_OFF, 1 # Key field in remove instruction. + +# Init stack frame layout. +# ------------------------ +.equ SF_INIT_BUMP_SEED_OFF, -352 # Bump seed. +.equ SF_INIT_SIGNER_SEED_ADDR_OFF, -96 # Bump signer seed address field. +.equ SF_INIT_SIGNER_SEED_LEN_OFF, -88 # Bump signer seed length field. +.equ SF_INIT_PDA_OFF, -80 # PDA address field. +# Discriminator field in CPI instruction data. +.equ SF_INIT_CREATE_ACCOUNT_DISCRIMINATOR_UOFF, -351 +# Lamports field in CreateAccount instruction data. +.equ SF_INIT_CREATE_ACCOUNT_LAMPORTS_UOFF, -347 +# Space address field in CreateAccount instruction data. +.equ SF_INIT_CREATE_ACCOUNT_SPACE_UOFF, -339 +# Owner field in CreateAccount instruction data (chunk index 0). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_0, -331 +# Owner field in CreateAccount instruction data (chunk index 1). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_1, -323 +# Owner field in CreateAccount instruction data (chunk index 2). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_2, -315 +# Owner field in CreateAccount instruction data (chunk index 3). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_3, -307 +.equ SF_INIT_SIGNERS_SEEDS_ADDR_OFF, -112 # Signers seeds address field. +.equ SF_INIT_SIGNERS_SEEDS_LEN_OFF, -104 # Signers seeds length field. +.equ SF_INIT_SYSTEM_PROGRAM_ADDRESS_OFF, -32 # System Program address. +.equ SF_INIT_INSN_PROGRAM_ID_OFF, -296 # SolInstruction program_id field. +.equ SF_INIT_INSN_ACCOUNTS_OFF, -288 # SolInstruction accounts field. +.equ SF_INIT_INSN_ACCOUNT_LEN_OFF, -280 # SolInstruction account_len field. +.equ SF_INIT_INSN_DATA_OFF, -272 # SolInstruction data field. +.equ SF_INIT_INSN_DATA_LEN_OFF, -264 # SolInstruction data_len field. +# SolAccountMeta is_writable field for user account. +.equ SF_INIT_USER_META_IS_WRITABLE_OFF, -248 +# SolAccountMeta is_writable field for tree account. +.equ SF_INIT_TREE_META_IS_WRITABLE_OFF, -232 +# SolAccountInfo is_signer field for user account. +.equ SF_INIT_USER_INFO_IS_SIGNER_OFF, -176 +# SolAccountMeta pubkey field for user account. +.equ SF_INIT_USER_META_PUBKEY_OFF, -256 +# SolAccountInfo pubkey field for user account. +.equ SF_INIT_USER_INFO_PUBKEY_OFF, -224 +# SolAccountInfo owner field for user account. +.equ SF_INIT_USER_INFO_OWNER_OFF, -192 +# SolAccountInfo lamports field for user account. +.equ SF_INIT_USER_INFO_LAMPORTS_OFF, -216 +# SolAccountInfo data_len field for user account. +.equ SF_INIT_USER_INFO_DATA_OFF, -200 +# SolAccountInfo data_len for tree account. +.equ SF_INIT_TREE_INFO_DATA_LEN_OFF, -152 +# SolAccountInfo is_signer field for tree account. +.equ SF_INIT_TREE_INFO_IS_SIGNER_OFF, -120 +# SolAccountInfo is_writable field for tree account. +.equ SF_INIT_TREE_INFO_IS_WRITABLE_UOFF, -119 +# SolAccountMeta pubkey field for tree account. +.equ SF_INIT_TREE_META_PUBKEY_OFF, -240 +# SolAccountInfo pubkey field for tree account. +.equ SF_INIT_TREE_INFO_PUBKEY_OFF, -168 +# SolAccountInfo owner field for tree account. +.equ SF_INIT_TREE_INFO_OWNER_OFF, -136 +# SolAccountInfo lamports field for tree account. +.equ SF_INIT_TREE_INFO_LAMPORTS_OFF, -160 +# SolAccountInfo data_len field for tree account. +.equ SF_INIT_TREE_INFO_DATA_OFF, -144 +# Relative offset from PDA on stack to System Program ID. +.equ SF_INIT_PDA_TO_SYSTEM_PROGRAM_ID_REL_OFF_IMM, 48 +# Relative offset from System Program ID to first SolAccountMeta. +.equ SF_INIT_SYSTEM_PROGRAM_ID_TO_ACCT_METAS_REL_OFF_IMM, -224 +# Relative offset from SolAccountMeta array to instruction data. +.equ SF_INIT_ACCT_METAS_TO_INSN_DATA_REL_OFF_IMM, -95 +# Relative offset from instruction data to signer seeds. +.equ SF_INIT_INSN_DATA_TO_SIGNER_SEEDS_REL_OFF_IMM, 255 +# Relative offset from signer seeds to signers seeds. +.equ SF_INIT_SIGNER_SEEDS_TO_SIGNERS_SEEDS_REL_OFF_IMM, -16 +.equ SF_INIT_ACCT_INFOS_OFF, -224 # Account infos array. + +# CPI-specific constants. +# ----------------------- +.equ CPI_N_ACCOUNTS, 2 # User and tree accounts. +# The tree account is a PDA for CreateAccount CPI. +.equ CPI_N_PDA_SIGNERS, 1 +# Number of seeds for CreateAccount PDA signer (bump only). +.equ CPI_N_SEEDS_CREATE_ACCOUNT, 1 +# PDA signers for Transfer CPI (none — user is already a signer). +.equ CPI_N_PDA_SIGNERS_TRANSFER, 0 +.equ CPI_N_SEEDS_TRY_FIND_PDA, 0 # Number of seeds for PDA generation. +.equ CPI_TREE_DATA_LEN, 24 # Tree account data length. +# Account data scalar for base rent calculation. +.equ CPI_ACCOUNT_DATA_SCALAR, 152 +# CreateAccount discriminator for CPI. +.equ CPI_CREATE_ACCOUNT_DISCRIMINATOR, 0 +# Length of CreateAccount instruction data. +.equ CPI_CREATE_ACCOUNT_INSN_DATA_LEN, 52 +.equ CPI_TRANSFER_DISCRIMINATOR, 2 # Transfer discriminator for CPI. +.equ CPI_TRANSFER_INSN_DATA_LEN, 12 # Length of Transfer instruction data. +.equ CPI_WRITABLE_SIGNER, 0x0101 # Mask for writable signer. +.equ CPI_USER_ACCOUNT_INDEX, 0 # Account index for user account in CPI. +.equ CPI_TREE_ACCOUNT_INDEX, 1 # Account index for tree account in CPI. +.equ CPI_RENT_EPOCH_NULL, 0 # Null rent epoch. + +# Tree constants. +# --------------- +.equ TREE_N_CHILDREN, 2 # Max number of children per node. +.equ TREE_DIR_L, 0 # Left direction. +.equ TREE_DIR_R, 1 # Right direction. +.equ TREE_COLOR_B, 0 # Black color. +.equ TREE_COLOR_R, 1 # Red color. +.equ TREE_HEADER_TOP_OFF, 8 # Stack top field in header. +.equ TREE_HEADER_NEXT_OFF, 16 # Next node field in header. +.equ TREE_DISCRIMINATOR_INSERT, 1 # Discriminator for insert instruction. +.equ TREE_NODE_KEY_OFF, 24 # Node key field. +.equ TREE_NODE_VALUE_OFF, 26 # Node value field. +.equ TREE_NODE_CHILD_L_OFF, 8 # Node left child field. +.equ TREE_NODE_CHILD_R_OFF, 16 # Node right child field. +.equ TREE_NODE_PARENT_OFF, 0 # Node parent field. +.equ TREE_NODE_COLOR_OFF, 28 # Color field. \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/entrypoint-branching.txt b/examples/tree/artifacts/snippets/asm/entrypoint-branching.txt new file mode 100644 index 00000000..c60e08e4 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/entrypoint-branching.txt @@ -0,0 +1,17 @@ +.globl entrypoint + +entrypoint: + # Read instruction data length and discriminator. + # --------------------------------------------------------------------- + ldxdw r9, [r2 - SIZE_OF_U64] # Get instruction data length. + ldxdw r8, [r1 + IB_N_ACCOUNTS_OFF] # Get n input buffer accounts. + ldxb r7, [r2 + OFFSET_ZERO] # Get discriminator. + + # Jump to branch for given discriminator. + # --------------------------------------------------------------------- + jeq r7, INSN_DISCRIMINATOR_INSERT, insert + jeq r7, INSN_DISCRIMINATOR_REMOVE, remove + jeq r7, INSN_DISCRIMINATOR_INITIALIZE, initialize + # Error if invalid discriminator provided. + mov64 r0, E_INSTRUCTION_DISCRIMINATOR + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/initialize-create-account.txt b/examples/tree/artifacts/snippets/asm/initialize-create-account.txt new file mode 100644 index 00000000..854c18ec --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/initialize-create-account.txt @@ -0,0 +1,147 @@ + # Pack SolInstruction. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] System Program ID pointer. + # - [x] Account metas pointer. + # - [x] Instruction data pointer. + # --------------------------------------------------------------------- + stdw [r10 + SF_INIT_INSN_ACCOUNT_LEN_OFF], CPI_N_ACCOUNTS + stdw [r10 + SF_INIT_INSN_DATA_LEN_OFF], CPI_CREATE_ACCOUNT_INSN_DATA_LEN + + # Pack CreateAccount instruction data. + # --------------------------------------------------------------------- + # - Discriminator is already set to 0 since stack is zero initialized. + # - Reuses r3 from PDA syscall. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_RENT_DATA_OFF] # Load lamports per byte + mul64 r9, CPI_ACCOUNT_DATA_SCALAR # Multiply to get rent-exempt cost. + # Store in instruction data. + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_LAMPORTS_UOFF], r9 + # Store new account data length. + stdw [r10 + SF_INIT_CREATE_ACCOUNT_SPACE_UOFF], CPI_TREE_DATA_LEN + # Copy in program ID to instruction data. + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_0] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_0], r9 + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_1] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_1], r9 + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_2] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_2], r9 + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_3] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_3], r9 + + # Pack SolAccountMeta for user and tree. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] User pubkey pointer. + # - [x] Tree pubkey pointer. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_META_IS_WRITABLE_OFF], CPI_WRITABLE_SIGNER + sth [r10 + SF_INIT_TREE_META_IS_WRITABLE_OFF], CPI_WRITABLE_SIGNER + + # Pack SolAccountInfo for user and tree. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] User pubkey pointer. + # - [x] Tree pubkey pointer. + # - [x] User lamports pointer. + # - [x] Tree lamports pointer. + # - [x] User data pointer. + # - [x] Tree data pointer. + # - [x] User owner pointer. + # - [x] Tree owner pointer. + # Skipped due to zero-initialized stack memory: + # - User data length (already checked as zero). + # - Tree data length (already checked as zero). + # - User rent epoch. + # - Tree rent epoch. + # - User executable. + # - Tree executable. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_INFO_IS_SIGNER_OFF], CPI_WRITABLE_SIGNER + sth [r10 + SF_INIT_TREE_INFO_IS_SIGNER_OFF], CPI_WRITABLE_SIGNER + + # Initialize signer seed for PDA bump seed. + # --------------------------------------------------------------------- + # Reuses r5 from PDA derivation syscall. + # --------------------------------------------------------------------- + # Store pointer to bump seed. + stxdw [r10 + SF_INIT_SIGNER_SEED_ADDR_OFF], r5 + stdw [r10 + SF_INIT_SIGNER_SEED_LEN_OFF], SIZE_OF_U8 # Store length. + + # Initialize signers seeds for PDA. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] Signer seed pointer. + # --------------------------------------------------------------------- + stdw [r10 + SF_INIT_SIGNERS_SEEDS_LEN_OFF], CPI_N_SEEDS_CREATE_ACCOUNT + + # Bulk assign/load pointers for account metas and infos. + # --------------------------------------------------------------------- + # Since pointers must be loaded from registers, this block steps + # through the input buffer in order to reduce intermediate loads. + # --------------------------------------------------------------------- + add64 r1, IB_USER_ADDRESS_OFF # Point to user address in input buffer. + stxdw [r10 + SF_INIT_USER_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_USER_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user owner. + stxdw [r10 + SF_INIT_USER_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user lamports. + stxdw [r10 + SF_INIT_USER_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to user data. + stxdw [r10 + SF_INIT_USER_INFO_DATA_OFF], r1 # Store in account info. + # Advance to tree address field. + add64 r1, IB_USER_DATA_TO_TREE_ADDRESS_REL_OFF_IMM + stxdw [r10 + SF_INIT_TREE_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_TREE_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree owner. + stxdw [r10 + SF_INIT_TREE_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree lamports. + stxdw [r10 + SF_INIT_TREE_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to tree data. + stxdw [r10 + SF_INIT_TREE_INFO_DATA_OFF], r1 # Store in account info. + mov64 r6, r1 # Store tree data pointer for later. + + # Bulk assign/load pointers for CPI bindings. + # --------------------------------------------------------------------- + # This block steps through the stack frame, optimizing assignments in + # preparation for the impending CreateAccount CPI, which requires: + # - [x] r1 = pointer to instruction. + # - [x] r2 = pointer to account infos. + # - [x] r4 = pointer to signers seeds. + # Notably, it reuses r4 from the PDA derivation syscall to walk through + # pointers on the stack, before advancing it to its final value. + # --------------------------------------------------------------------- + # Advance to System Program ID pointer on zero-initialized stack. + add64 r4, SF_INIT_PDA_TO_SYSTEM_PROGRAM_ID_REL_OFF_IMM + # Store in SolInstruction. + stxdw [r10 + SF_INIT_INSN_PROGRAM_ID_OFF], r4 + # Advance to SolAccountMeta array pointer. + add64 r4, SF_INIT_SYSTEM_PROGRAM_ID_TO_ACCT_METAS_REL_OFF_IMM + stxdw [r10 + SF_INIT_INSN_ACCOUNTS_OFF], r4 # Store in SolInstruction. + # Advance to instruction data pointer. + add64 r4, SF_INIT_ACCT_METAS_TO_INSN_DATA_REL_OFF_IMM + stxdw [r10 + SF_INIT_INSN_DATA_OFF], r4 # Store in SolInstruction. + # Advance to signer seeds pointer. + add64 r4, SF_INIT_INSN_DATA_TO_SIGNER_SEEDS_REL_OFF_IMM + stxdw [r10 + SF_INIT_SIGNERS_SEEDS_ADDR_OFF], r4 + # Advance to signers seeds pointer. + add64 r4, SF_INIT_SIGNER_SEEDS_TO_SIGNERS_SEEDS_REL_OFF_IMM + # Assign remaining syscall pointers. + mov64 r1, r10 + add64 r1, SF_INIT_INSN_PROGRAM_ID_OFF + mov64 r2, r10 + add64 r2, SF_INIT_ACCT_INFOS_OFF + + # Invoke CPI. + # --------------------------------------------------------------------- + mov64 r3, CPI_N_ACCOUNTS + mov64 r5, CPI_N_PDA_SIGNERS + call sol_invoke_signed_c + + # Store next pointer in tree header. + # --------------------------------------------------------------------- + mov64 r7, r6 # Get copy of tree data pointer. + add64 r7, SIZE_OF_TREE_HEADER # Advance to next node. + stxdw [r6 + TREE_HEADER_NEXT_OFF], r7 # Store in next field. + + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/initialize-input-checks.txt b/examples/tree/artifacts/snippets/asm/initialize-input-checks.txt new file mode 100644 index 00000000..ff67dcfc --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/initialize-input-checks.txt @@ -0,0 +1,51 @@ +initialize: + # Error if invalid instruction data length. + # --------------------------------------------------------------------- + jne r9, SIZE_OF_INITIALIZE_INSTRUCTION, e_instruction_data_len + + # Error if invalid number of accounts. + # --------------------------------------------------------------------- + jne r8, IB_N_ACCOUNTS_INIT, e_n_accounts + + # Error if user has data. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_USER_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_user_data_len + + # Error if tree is duplicate or has data. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_TREE_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_tree_duplicate + ldxdw r9, [r1 + IB_TREE_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_tree_data_len + + # Error if System Program is duplicate or has invalid data length. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_SYSTEM_PROGRAM_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_system_program_duplicate + ldxdw r9, [r1 + IB_SYSTEM_PROGRAM_DATA_LEN_OFF] + jne r9, IB_SYSTEM_PROGRAM_DATA_LEN, e_system_program_data_len + + # Error if Rent account is duplicate or has incorrect address. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_RENT_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_rent_duplicate + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_0] + lddw r8, IB_RENT_ID_CHUNK_0 + jne r9, r8, e_rent_address + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_1] + lddw r8, IB_RENT_ID_CHUNK_1 + jne r9, r8, e_rent_address + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_2] + lddw r8, IB_RENT_ID_CHUNK_2 + jne r9, r8, e_rent_address + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_3] + # Optimize out the following line, which costs two CUs due to two + # 32-bit immediate loads across two opcodes: + # ``` + # lddw r8, IB_RENT_ID_CHUNK_3 + # ``` + # Instead, replace with mov32, which only loads one 32-bit immediate, + # since the rent sysvar address has all chunk 3 hi bits unset. + mov32 r8, IB_RENT_ID_CHUNK_3_LO + jne r9, r8, e_rent_address \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/initialize-pda-checks.txt b/examples/tree/artifacts/snippets/asm/initialize-pda-checks.txt new file mode 100644 index 00000000..e7617371 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/initialize-pda-checks.txt @@ -0,0 +1,28 @@ + # Compute PDA. + # --------------------------------------------------------------------- + # Skip assignment for r1, since no seeds need to be parsed and this + # argument is effectively ignored. + # --------------------------------------------------------------------- + mov64 r2, CPI_N_SEEDS_TRY_FIND_PDA # Declare no seeds to parse. + mov64 r3, r1 # Get input buffer pointer. + add64 r3, IB_INIT_PROGRAM_ID_OFF_IMM # Point at program ID. + mov64 r4, r10 # Get stack frame pointer. + add64 r4, SF_INIT_PDA_OFF # Point to PDA region on stack. + mov64 r5, r10 # Get stack frame pointer. + add64 r5, SF_INIT_BUMP_SEED_OFF # Point to bump seed region on stack. + call sol_try_find_program_address # Find PDA. + + # Compare computed PDA against passed account. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_0] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_0] + jne r9, r8, e_pda_mismatch + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_1] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_1] + jne r9, r8, e_pda_mismatch + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_2] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_2] + jne r9, r8, e_pda_mismatch + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_3] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_3] + jne r9, r8, e_pda_mismatch \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-allocate.txt b/examples/tree/artifacts/snippets/asm/insert-allocate.txt new file mode 100644 index 00000000..3ec3098c --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-allocate.txt @@ -0,0 +1,147 @@ + # Branch based on state of stack top. + # --------------------------------------------------------------------- + # Get stack top pointer. + ldxdw r9, [r1 + IB_TREE_DATA_TOP_OFF] + jne r9, NULL, insert_pop # Pop node from stack if non-null. + +insert_allocate: + # Error if wrong number of accounts for allocation. + # --------------------------------------------------------------------- + jne r8, IB_N_ACCOUNTS_INIT, e_n_accounts_insert_allocation + + # Compute shifted input buffer pointer based on tree data length. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_TREE_DATA_LEN_OFF] # Get tree data length. + # Store in account info for CPI. + stxdw [r10 + SF_INIT_TREE_INFO_DATA_LEN_OFF], r9 + mov64 r7, r9 # Store copy for later. + add64 r9, MAX_DATA_PAD # Add max possible padding. + and64 r9, DATA_LEN_AND_MASK # Truncate to 8-byte alignment. + add64 r9, r1 # Increment by input buffer. + + # Check system program is not duplicate and has correct data length. + # --------------------------------------------------------------------- + ldxb r8, [r9 + IB_SYSTEM_PROGRAM_NON_DUP_MARKER_OFF] + jne r8, IB_NON_DUP_MARKER, e_system_program_duplicate + ldxdw r8, [r9 + IB_SYSTEM_PROGRAM_DATA_LEN_OFF] + jne r8, IB_SYSTEM_PROGRAM_DATA_LEN, e_system_program_data_len + + # Check rent sysvar is not duplicate and has correct address. + # --------------------------------------------------------------------- + ldxb r8, [r9 + IB_RENT_NON_DUP_MARKER_OFF] + jne r8, IB_NON_DUP_MARKER, e_rent_duplicate + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_0] + lddw r4, IB_RENT_ID_CHUNK_0 + jne r8, r4, e_rent_address + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_1] + lddw r4, IB_RENT_ID_CHUNK_1 + jne r8, r4, e_rent_address + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_2] + lddw r4, IB_RENT_ID_CHUNK_2 + jne r8, r4, e_rent_address + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_3] + mov32 r4, IB_RENT_ID_CHUNK_3_LO + jne r8, r4, e_rent_address + + # Calculate transfer lamports. + # --------------------------------------------------------------------- + ldxdw r8, [r9 + IB_RENT_DATA_OFF] # Load lamports per byte. + mul64 r8, SIZE_OF_TREE_NODE # Multiply to get transfer cost. + + # Pack Transfer instruction data in CreateAccount slot on stack. + # --------------------------------------------------------------------- + stw [r10 + SF_INIT_CREATE_ACCOUNT_DISCRIMINATOR_UOFF], CPI_TRANSFER_DISCRIMINATOR + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_LAMPORTS_UOFF], r8 + + # Pack SolInstruction. + # --------------------------------------------------------------------- + stdw [r10 + SF_INIT_INSN_ACCOUNT_LEN_OFF], CPI_N_ACCOUNTS + stdw [r10 + SF_INIT_INSN_DATA_LEN_OFF], CPI_TRANSFER_INSN_DATA_LEN + + # Pack SolAccountMeta flags for user and tree. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_META_IS_WRITABLE_OFF], CPI_WRITABLE_SIGNER + stb [r10 + SF_INIT_TREE_META_IS_WRITABLE_OFF], BOOL_TRUE + + # Pack SolAccountInfo flags for user and tree. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_INFO_IS_SIGNER_OFF], CPI_WRITABLE_SIGNER + stb [r10 + SF_INIT_TREE_INFO_IS_WRITABLE_UOFF], BOOL_TRUE + + # Bulk assign/load pointers for account metas and infos. + # --------------------------------------------------------------------- + mov64 r6, r1 # Store input buffer pointer for later. + add64 r1, IB_USER_ADDRESS_OFF # Point to user address in input buffer. + stxdw [r10 + SF_INIT_USER_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_USER_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user owner. + stxdw [r10 + SF_INIT_USER_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user lamports. + stxdw [r10 + SF_INIT_USER_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to user data. + stxdw [r10 + SF_INIT_USER_INFO_DATA_OFF], r1 # Store in account info. + # Advance to tree address field. + add64 r1, IB_USER_DATA_TO_TREE_ADDRESS_REL_OFF_IMM + stxdw [r10 + SF_INIT_TREE_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_TREE_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree owner. + stxdw [r10 + SF_INIT_TREE_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree lamports. + stxdw [r10 + SF_INIT_TREE_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to tree data. + stxdw [r10 + SF_INIT_TREE_INFO_DATA_OFF], r1 # Store in account info. + + # Bulk assign/load pointers for CPI bindings. + # --------------------------------------------------------------------- + # Point to System Program ID on zero-initialized stack. + mov64 r4, r10 + add64 r4, SF_INIT_SYSTEM_PROGRAM_ADDRESS_OFF + # Store in SolInstruction. + stxdw [r10 + SF_INIT_INSN_PROGRAM_ID_OFF], r4 + # Advance to SolAccountMeta array pointer. + add64 r4, SF_INIT_SYSTEM_PROGRAM_ID_TO_ACCT_METAS_REL_OFF_IMM + # Store in SolInstruction. + stxdw [r10 + SF_INIT_INSN_ACCOUNTS_OFF], r4 + # Advance to instruction data pointer. + add64 r4, SF_INIT_ACCT_METAS_TO_INSN_DATA_REL_OFF_IMM + stxdw [r10 + SF_INIT_INSN_DATA_OFF], r4 # Store in SolInstruction. + + # Invoke Transfer CPI. + # --------------------------------------------------------------------- + mov64 r1, r10 + add64 r1, SF_INIT_INSN_PROGRAM_ID_OFF + mov64 r8, r2 # Save instruction data pointer for later. + mov64 r2, r10 + add64 r2, SF_INIT_ACCT_INFOS_OFF + mov64 r3, CPI_N_ACCOUNTS + # Ignore PDA signer seeds pointer, since none required. + mov64 r5, CPI_N_PDA_SIGNERS_TRANSFER + call sol_invoke_signed_c + mov64 r2, r8 # Restore instruction data pointer. + mov64 r1, r6 # Restore input buffer pointer. + + # Update tree data length. + # --------------------------------------------------------------------- + add64 r7, SIZE_OF_TREE_NODE # Increment data length. + stxdw [r1 + IB_TREE_DATA_LEN_OFF], r7 # Store in input buffer. + + # Get node = next, then advance next by one TreeNode. + # --------------------------------------------------------------------- + ldxdw r7, [r1 + IB_TREE_DATA_NEXT_OFF] # Get pointer to next node. + mov64 r9, r7 # Store node pointer for later, the new node. + add64 r7, SIZE_OF_TREE_NODE # Increment to point to new next. + stxdw [r1 + IB_TREE_DATA_NEXT_OFF], r7 # Advance next. + + # Continue insert. + # --------------------------------------------------------------------- + ja insert_store_key_value_pair + +insert_pop: + # Pop node from free stack. + # --------------------------------------------------------------------- + ldxdw r8, [r9 + OFFSET_ZERO] # Load StackNode.next. + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r8 # Update top in header. + +insert_store_key_value_pair: + ldxw r4, [r2 + INSN_INSERT_KEY_OFF] # Load two fields together. + stxw [r9 + TREE_NODE_KEY_OFF], r4 # Store both fields together. \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-fixup-case-1.txt b/examples/tree/artifacts/snippets/asm/insert-fixup-case-1.txt new file mode 100644 index 00000000..e089ddb2 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-fixup-case-1.txt @@ -0,0 +1,8 @@ +insert_fixup_main: + # r2 := parent + # r5 := parent.key + # Case 1. # r9 := node + # --------------------------------------------------------------------- + ldxb r6, [r2 + TREE_NODE_COLOR_OFF] # r6 = parent.color; + jne r6, TREE_COLOR_B, insert_fixup_check_case_4 + exit # If parent is black, tree is still valid, so exit. \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-fixup-case-2-3.txt b/examples/tree/artifacts/snippets/asm/insert-fixup-case-2-3.txt new file mode 100644 index 00000000..2fdee3c2 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-fixup-case-2-3.txt @@ -0,0 +1,13 @@ +insert_fixup_case_2: + # r2 := parent + # r3 := grandparent + # r7 := uncle + # Case 2/3. # r9 := node + # --------------------------------------------------------------------- + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r7 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # uncle.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + mov64 r9, r3 # node = grandparent; + ldxdw r2, [r9 + TREE_NODE_PARENT_OFF] # parent = node.parent; + jne r2, NULL, insert_fixup_main + exit # Case 3. \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-fixup-case-4.txt b/examples/tree/artifacts/snippets/asm/insert-fixup-case-4.txt new file mode 100644 index 00000000..fe17101b --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-fixup-case-4.txt @@ -0,0 +1,7 @@ +insert_fixup_check_case_4: + # Check case 4. + # --------------------------------------------------------------------- + ldxdw r3, [r2 + TREE_NODE_PARENT_OFF] # r3 = grandparent; + jne r3, NULL, insert_fixup_check_case_5_6 + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-l.txt b/examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-l.txt new file mode 100644 index 00000000..c57fb729 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-l.txt @@ -0,0 +1,58 @@ +insert_fixup_check_case_5_6: + # Get uncle and check for case 5 or 6. + # --------------------------------------------------------------------- + ldxh r4, [r3 + TREE_NODE_KEY_OFF] # r4 = grandparent.key; + jgt r5, r4, insert_fixup_check_case_5_6_dir_r + +insert_fixup_check_case_5_6_dir_l: + ldxdw r7, [r3 + TREE_NODE_CHILD_R_OFF] # r7 = uncle; + jeq r7, NULL, insert_fixup_case_5_6_dir_l + ldxb r8, [r7 + TREE_NODE_COLOR_OFF] # r8 = uncle.color; + jne r8, TREE_COLOR_B, insert_fixup_case_2 + +insert_fixup_case_5_6_dir_l: + ldxdw r6, [r2 + TREE_NODE_CHILD_R_OFF] # r6 = new_root = parent.child[R]; + jne r9, r6, insert_fixup_case_6_dir_l + +insert_fixup_case_5_dir_l: + ldxdw r8, [r6 + TREE_NODE_CHILD_L_OFF] # r8 = new_child = new_root.child[L]; + stxdw [r2 + TREE_NODE_CHILD_R_OFF], r8 # parent.child[R] = new_child; + jeq r8, NULL, insert_fixup_case_5_dir_l_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r2 # new_child.parent = parent; +insert_fixup_case_5_dir_l_skip: + stxdw [r6 + TREE_NODE_CHILD_L_OFF], r2 # new_root.child[L] = parent; + stxdw [r6 + TREE_NODE_PARENT_OFF], r3 # new_root.parent = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r6 # parent.parent = new_root; + stxdw [r3 + TREE_NODE_CHILD_L_OFF], r6 # grandparent.child[L] = new_root; + mov64 r9, r2 # node = old parent; + mov64 r2, r6 # parent = new_root; + +insert_fixup_case_6_dir_l: + ldxdw r4, [r3 + TREE_NODE_PARENT_OFF] # r4 = great-grandparent; + ldxdw r8, [r2 + TREE_NODE_CHILD_R_OFF] # r8 = new_child = parent.child[R]; + stxdw [r3 + TREE_NODE_CHILD_L_OFF], r8 # grandparent.child[L] = new_child; + jeq r8, NULL, insert_fixup_case_6_dir_l_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r3 # new_child.parent = grandparent; +insert_fixup_case_6_dir_l_skip: + stxdw [r2 + TREE_NODE_CHILD_R_OFF], r3 # parent.child[R] = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r4 # parent.parent = great-grandparent; + stxdw [r3 + TREE_NODE_PARENT_OFF], r2 # grandparent.parent = parent; + jeq r4, NULL, insert_fixup_case_6_dir_l_root + ldxdw r8, [r4 + TREE_NODE_CHILD_R_OFF] # r8 = great-grandparent.child[R]; + jne r3, r8, insert_fixup_case_6_dir_l_left + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_R_OFF], r2 # great-grandparent.child[R] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_l_left: + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_L_OFF], r2 # great-grandparent.child[L] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_l_root: + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r2 # tree.root = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-r.txt b/examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-r.txt new file mode 100644 index 00000000..5fa3a39e --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-fixup-case-5-6-dir-r.txt @@ -0,0 +1,52 @@ +insert_fixup_check_case_5_6_dir_r: + ldxdw r7, [r3 + TREE_NODE_CHILD_L_OFF] # r7 = uncle; + jeq r7, NULL, insert_fixup_case_5_6_dir_r + ldxb r8, [r7 + TREE_NODE_COLOR_OFF] # r8 = uncle.color; + jne r8, TREE_COLOR_B, insert_fixup_case_2 + +insert_fixup_case_5_6_dir_r: + ldxdw r6, [r2 + TREE_NODE_CHILD_L_OFF] # r6 = new_root = parent.child[L]; + jne r9, r6, insert_fixup_case_6_dir_r + +insert_fixup_case_5_dir_r: + ldxdw r8, [r6 + TREE_NODE_CHILD_R_OFF] # r8 = new_child = new_root.child[R]; + stxdw [r2 + TREE_NODE_CHILD_L_OFF], r8 # parent.child[L] = new_child; + jeq r8, NULL, insert_fixup_case_5_dir_r_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r2 # new_child.parent = parent; +insert_fixup_case_5_dir_r_skip: + stxdw [r6 + TREE_NODE_CHILD_R_OFF], r2 # new_root.child[R] = parent; + stxdw [r6 + TREE_NODE_PARENT_OFF], r3 # new_root.parent = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r6 # parent.parent = new_root; + stxdw [r3 + TREE_NODE_CHILD_R_OFF], r6 # grandparent.child[R] = new_root; + mov64 r9, r2 # node = old parent; + mov64 r2, r6 # parent = new_root; + +insert_fixup_case_6_dir_r: + ldxdw r4, [r3 + TREE_NODE_PARENT_OFF] # r4 = great-grandparent; + ldxdw r8, [r2 + TREE_NODE_CHILD_L_OFF] # r8 = new_child = parent.child[L]; + stxdw [r3 + TREE_NODE_CHILD_R_OFF], r8 # grandparent.child[R] = new_child; + jeq r8, NULL, insert_fixup_case_6_dir_r_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r3 # new_child.parent = grandparent; +insert_fixup_case_6_dir_r_skip: + stxdw [r2 + TREE_NODE_CHILD_L_OFF], r3 # parent.child[L] = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r4 # parent.parent = great-grandparent; + stxdw [r3 + TREE_NODE_PARENT_OFF], r2 # grandparent.parent = parent; + jeq r4, NULL, insert_fixup_case_6_dir_r_root + ldxdw r8, [r4 + TREE_NODE_CHILD_R_OFF] # r8 = great-grandparent.child[R]; + jne r3, r8, insert_fixup_case_6_dir_r_left + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_R_OFF], r2 # great-grandparent.child[R] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_r_left: + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_L_OFF], r2 # great-grandparent.child[L] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_r_root: + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r2 # tree.root = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-input-checks.txt b/examples/tree/artifacts/snippets/asm/insert-input-checks.txt new file mode 100644 index 00000000..c2c9e5a2 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-input-checks.txt @@ -0,0 +1,18 @@ +insert: + # Error if invalid instruction data length. + # --------------------------------------------------------------------- + jne r9, SIZE_OF_INSERT_INSTRUCTION, e_instruction_data_len + + # Error if too few accounts. + # --------------------------------------------------------------------- + jlt r8, IB_N_ACCOUNTS_GENERAL, e_n_accounts + + # Error if user has data. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_USER_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_user_data_len + + # Error if tree is duplicate. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_TREE_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_tree_duplicate \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/insert-search.txt b/examples/tree/artifacts/snippets/asm/insert-search.txt new file mode 100644 index 00000000..7bbb1c8f --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/insert-search.txt @@ -0,0 +1,41 @@ +insert_search: # r9 = node + ldxh r4, [r2 + INSN_INSERT_KEY_OFF] # r4 = insn.key; + ldxdw r3, [r1 + IB_TREE_DATA_ROOT_OFF] # r3 = cursor = root; + jeq r3, NULL, insert_root + +insert_search_loop: + mov64 r2, r3 # r2 = parent = cursor; + ldxh r5, [r3 + TREE_NODE_KEY_OFF] # r5 = cursor.key; + jlt r4, r5, insert_search_branch_l + jgt r4, r5, insert_search_branch_r + mov64 r0, E_KEY_EXISTS # Error if key already exists. + exit + +insert_root: + # Root is null: new node becomes root. + # --------------------------------------------------------------------- + stb [r9 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # node.color = red; + mov64 r2, NULL # r2 = parent = null; + stxdw [r9 + TREE_NODE_PARENT_OFF], r2 # node.parent = null; + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r9 # root = node; + exit + +insert_search_branch_l: + ldxdw r3, [r2 + TREE_NODE_CHILD_L_OFF] # r3 = parent.child[L]; + jne r3, NULL, insert_search_loop + # Null child: insert node as left child of parent. + stb [r9 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # node.color = red; + stxdw [r9 + TREE_NODE_PARENT_OFF], r2 # node.parent = parent; + stxdw [r2 + TREE_NODE_CHILD_L_OFF], r9 # parent.child[L] = node; + # Inline case 1: if parent is black, tree is valid. + ldxb r6, [r2 + TREE_NODE_COLOR_OFF] # r6 = parent.color; + jne r6, TREE_COLOR_B, insert_fixup_check_case_4 + exit + +insert_search_branch_r: + ldxdw r3, [r2 + TREE_NODE_CHILD_R_OFF] # r3 = parent.child[R]; + jne r3, NULL, insert_search_loop + # Null child: insert node as right child of parent. + stb [r9 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # node.color = red; + stxdw [r9 + TREE_NODE_PARENT_OFF], r2 # node.parent = parent; + stxdw [r2 + TREE_NODE_CHILD_R_OFF], r9 # parent.child[R] = node; \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-complex.txt b/examples/tree/artifacts/snippets/asm/remove-complex.txt new file mode 100644 index 00000000..e0384604 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-complex.txt @@ -0,0 +1,105 @@ +remove_complex: + mov64 r2, r3 # r2 = deleted node (save for recycle); + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # r5 = parent; + ldxdw r4, [r5 + TREE_NODE_CHILD_L_OFF] # r4 = parent.child[L]; + jne r3, r4, remove_complex_dir_r # if node != parent.child[L] goto remove_complex_dir_r +remove_complex_dir_l: + # Prepare to loop for when node direction is left. + # --------------------------------------------------------------------- + stdw [r5 + TREE_NODE_CHILD_L_OFF], NULL # parent.child[L] = null; + ja remove_complex_loop_dir_l # goto remove_complex_loop_dir_l +remove_complex_dir_r: + # Prepare to loop for when node direction is right. + # --------------------------------------------------------------------- + stdw [r5 + TREE_NODE_CHILD_R_OFF], NULL # parent.child[R] = null; +remove_complex_loop_dir_r: + # Rebalance loop for when node direction is right. + # --------------------------------------------------------------------- + ldxdw r6, [r5 + TREE_NODE_CHILD_L_OFF] # r6 = sibling = parent.child[L]; + ldxdw r7, [r6 + TREE_NODE_CHILD_L_OFF] # r7 = distant_nephew = sibling.child[L]; + ldxdw r8, [r6 + TREE_NODE_CHILD_R_OFF] # r8 = close_nephew = sibling.child[R]; + # Check for red sibling. + # --------------------------------------------------------------------- + ldxb r9, [r6 + TREE_NODE_COLOR_OFF] # r9 = sibling.color; + jeq r9, TREE_COLOR_R, remove_complex_loop_dir_r_sibling_red # if sibling.color == red goto remove_complex_loop_dir_r_sibling_red + # Check for red distant nephew. + # --------------------------------------------------------------------- + jeq r7, NULL, remove_complex_loop_dir_r_check_case_5 # if distant_nephew == null goto remove_complex_loop_dir_r_check_case_5 + ldxb r9, [r7 + TREE_NODE_COLOR_OFF] # r9 = distant_nephew.color; + jeq r9, TREE_COLOR_R, remove_complex_case_6_dir_r # if distant_nephew.color == red goto remove_complex_case_6_dir_r +remove_complex_loop_dir_r_check_case_5: + # Check for red close nephew. + # --------------------------------------------------------------------- + jeq r8, NULL, remove_complex_loop_dir_r_check_case_1 # if close_nephew == null goto remove_complex_loop_dir_r_check_case_1 + ldxb r9, [r8 + TREE_NODE_COLOR_OFF] # r9 = close_nephew.color; + jeq r9, TREE_COLOR_R, remove_complex_case_5_dir_r # if close_nephew.color == red goto remove_complex_case_5_dir_r +remove_complex_loop_dir_r_check_case_1: + # Check for no parent. + # --------------------------------------------------------------------- + jeq r5, NULL, remove_complex_recycle_node # if parent == null goto remove_complex_recycle_node; + # Check for red parent. + # --------------------------------------------------------------------- + ldxb r9, [r5 + TREE_NODE_COLOR_OFF] # r9 = parent.color; + jne r9, TREE_COLOR_R, remove_complex_loop_dir_r_case_2 # if parent.color != red goto remove_complex_loop_dir_r_case_2 + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + ja remove_complex_recycle_node # goto remove_complex_recycle_node; +remove_complex_loop_dir_r_case_2: + # Fall through to case 2 at end of loop. + # --------------------------------------------------------------------- + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + mov64 r3, r5 # node = parent; + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # parent = node.parent; + # Check if parent exists, and if so, get direction for next loop. + # --------------------------------------------------------------------- + jeq r5, NULL, remove_complex_case_5_dir_r # if parent == null goto remove_complex_case_5_dir_r + ldxdw r4, [r5 + TREE_NODE_CHILD_L_OFF] # r4 = parent.child[L] + jeq r3, r4, remove_complex_loop_dir_l # if node == parent.child[L] goto remove_complex_loop_dir_l + ja remove_complex_loop_dir_r # goto remove_complex_loop_dir_r +remove_complex_case_5_dir_r: + # rotate_subtree(tree, sibling, 1-dir) + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + stb [r8 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # close_nephew.color = black; + mov64 r7, r6 # distant_nephew = sibling; + mov64 r6, r8 # sibling = close_nephew; +remove_complex_case_6_dir_r: + # rotate_subtree(tree, parent, dir) + ldxb r4, [r5 + TREE_NODE_COLOR_OFF] # r4 = parent.color; + stxb [r6 + TREE_NODE_COLOR_OFF], r4 # sibling.color = parent.color; + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r7 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # distant_nephew.color = black; + ja remove_complex_recycle_node +remove_complex_loop_dir_r_sibling_red: + # Rebalance for red sibling case. + # --------------------------------------------------------------------- + # rotate_subtree(tree, parent, dir) + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # parent.color = red; + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # sibling.color = black; + mov64 r6, r8 # sibling = close_nephew; + ldxdw r4, [r6 + TREE_NODE_CHILD_L_OFF] # r4 = sibling.child[L]; + mov64 r7, r4 # distant_nephew = sibling.child[L]; + jeq r7, NULL, remove_complex_loop_dir_r_sibling_red_check_case_5 # if distant_nephew == null goto remove_complex_loop_dir_r_sibling_red_check_case_5 + ldxb r4, [r7 + TREE_NODE_COLOR_OFF] # r4 = distant_nephew.color; + jeq r4, TREE_COLOR_R, remove_complex_case_6_dir_r # if distant_nephew.color == red goto remove_complex_case_6_dir_r +remove_complex_loop_dir_r_sibling_red_check_case_5: + ldxdw r4, [r6 + TREE_NODE_CHILD_R_OFF] # r4 = sibling.child[R]; + mov64 r8, r4 # close_nephew = sibling.child[R]; + jeq r8, NULL, remove_complex_loop_dir_r_sibling_red_case_4 # if close_nephew == null goto remove_complex_loop_dir_r_sibling_red_case_4 + ldxb r4, [r8 + TREE_NODE_COLOR_OFF] # r4 = close_nephew.color; + jeq r4, TREE_COLOR_R, remove_complex_case_5_dir_r # if close_nephew.color == red goto remove_complex_case_5_dir_r +remove_complex_loop_dir_r_sibling_red_case_4: + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + ja remove_complex_recycle_node + +remove_complex_loop_dir_l: + # Mirror of remove_complex_loop_r. + # Needs all the other sub labels for the same cases, but with dir reversed. + +remove_complex_recycle_node: + stdw [r2 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r2 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r2 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r2 # header.top = node; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-input-checks.txt b/examples/tree/artifacts/snippets/asm/remove-input-checks.txt new file mode 100644 index 00000000..d5296112 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-input-checks.txt @@ -0,0 +1,18 @@ +remove: + # Error if invalid instruction data length. + # --------------------------------------------------------------------- + jne r9, SIZE_OF_REMOVE_INSTRUCTION, e_instruction_data_len + + # Error if too few accounts. + # --------------------------------------------------------------------- + jlt r8, IB_N_ACCOUNTS_GENERAL, e_n_accounts + + # Error if user has data. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_USER_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_user_data_len + + # Error if tree is duplicate. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_TREE_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_tree_duplicate \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-search.txt b/examples/tree/artifacts/snippets/asm/remove-search.txt new file mode 100644 index 00000000..72a634af --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-search.txt @@ -0,0 +1,23 @@ +remove_search: + ldxdw r3, [r1 + IB_TREE_DATA_ROOT_OFF] # r3 = node = root; + jeq r3, NULL, e_key_does_not_exist + ldxh r4, [r2 + INSN_REMOVE_KEY_OFF] # r4 = key; + +remove_search_loop: + ldxh r5, [r3 + TREE_NODE_KEY_OFF] # r5 = node.key; + jeq r4, r5, remove_found + jgt r4, r5, remove_search_r + +remove_search_l: + ldxdw r3, [r3 + TREE_NODE_CHILD_L_OFF] # r3 = node.child[L]; + jne r3, NULL, remove_search_loop + mov64 r0, E_KEY_DOES_NOT_EXIST + exit + +remove_search_r: + ldxdw r3, [r3 + TREE_NODE_CHILD_R_OFF] # r3 = node.child[R]; + jne r3, NULL, remove_search_loop + +e_key_does_not_exist: + mov64 r0, E_KEY_DOES_NOT_EXIST + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-simple-1.txt b/examples/tree/artifacts/snippets/asm/remove-simple-1.txt new file mode 100644 index 00000000..3e2c7a65 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-simple-1.txt @@ -0,0 +1,20 @@ +remove_found: + ldxdw r4, [r3 + TREE_NODE_CHILD_L_OFF] # r4 = node.child[L]; + jeq r4, NULL, remove_check_child_r + ldxdw r5, [r3 + TREE_NODE_CHILD_R_OFF] # r5 = node.child[R]; + jeq r5, NULL, remove_simple_2_child_l + + # Simple case 1: successor swap. + # --------------------------------------------------------------------- +remove_successor_loop: + ldxdw r4, [r5 + TREE_NODE_CHILD_L_OFF] # r4 = successor.child[L]; + jeq r4, NULL, remove_successor_copy + mov64 r5, r4 # successor = left; + ja remove_successor_loop + +remove_successor_copy: + # Copy key/value pair as u32 from successor to found node. + # --------------------------------------------------------------------- + ldxw r4, [r5 + TREE_NODE_KEY_OFF] # r4 = successor.{key,value}; + stxw [r3 + TREE_NODE_KEY_OFF], r4 # node.{key,value} = r4; + mov64 r3, r5 # node = successor; \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-simple-2.txt b/examples/tree/artifacts/snippets/asm/remove-simple-2.txt new file mode 100644 index 00000000..34c9f2b3 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-simple-2.txt @@ -0,0 +1,38 @@ +remove_check_child_r: # r3 = node + ldxdw r4, [r3 + TREE_NODE_CHILD_R_OFF] # r4 = node.child[R]; + jeq r4, NULL, remove_check_simple_3_4 + +remove_simple_2_child_l: # r4 = child + # Simple case 2: replace node with child, recolor child black. + # --------------------------------------------------------------------- + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # r5 = parent; + stxdw [r4 + TREE_NODE_PARENT_OFF], r5 # child.parent = parent; + stb [r4 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # child.color = black; + jeq r5, NULL, remove_simple_2_root + ldxdw r6, [r5 + TREE_NODE_CHILD_R_OFF] # r6 = parent.child[R]; + jne r3, r6, remove_simple_2_dir_l + stxdw [r5 + TREE_NODE_CHILD_R_OFF], r4 # parent.child[R] = child; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit + +remove_simple_2_dir_l: + stxdw [r5 + TREE_NODE_CHILD_L_OFF], r4 # parent.child[L] = child; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit + +remove_simple_2_root: + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r4 # tree.root = child; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-simple-3.txt b/examples/tree/artifacts/snippets/asm/remove-simple-3.txt new file mode 100644 index 00000000..b09f3383 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-simple-3.txt @@ -0,0 +1,12 @@ +remove_check_simple_3_4: # r3 = node + # Simple case 3: root leaf — clear root. + # --------------------------------------------------------------------- + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # r5 = parent; + jne r5, NULL, remove_check_simple_4 + stdw [r1 + IB_TREE_DATA_ROOT_OFF], NULL # tree.root = null; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/asm/remove-simple-4.txt b/examples/tree/artifacts/snippets/asm/remove-simple-4.txt new file mode 100644 index 00000000..54dea2a2 --- /dev/null +++ b/examples/tree/artifacts/snippets/asm/remove-simple-4.txt @@ -0,0 +1,23 @@ +remove_check_simple_4: # r5 = parent + # Simple case 4: red leaf — detach from parent. + # --------------------------------------------------------------------- + ldxb r4, [r3 + TREE_NODE_COLOR_OFF] # r4 = node.color; + jne r4, TREE_COLOR_R, remove_complex + ldxdw r4, [r5 + TREE_NODE_CHILD_R_OFF] # r4 = parent.child[R]; + jne r3, r4, remove_simple_4_dir_l + stdw [r5 + TREE_NODE_CHILD_R_OFF], NULL # parent.child[R] = null; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit + +remove_simple_4_dir_l: + stdw [r5 + TREE_NODE_CHILD_L_OFF], NULL # parent.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/interface/instructions.txt b/examples/tree/artifacts/snippets/interface/instructions.txt new file mode 100644 index 00000000..a0c98eff --- /dev/null +++ b/examples/tree/artifacts/snippets/interface/instructions.txt @@ -0,0 +1,53 @@ +#[repr(u8)] +pub enum Instruction { + /// Initialize the tree. + Initialize, + /// Insert key-value pair. + Insert, + /// Remove key-value pair. + Remove, +} + +#[repr(C, packed)] +pub struct InstructionHeader { + pub discriminator: u8, +} + +#[repr(C, packed)] +pub struct InitializeInstruction { + pub header: InstructionHeader, +} + +#[repr(C, packed)] +pub struct InsertInstruction { + pub header: InstructionHeader, + pub key: u16, + pub value: u16, +} + +#[repr(C, packed)] +pub struct RemoveInstruction { + pub header: InstructionHeader, + pub key: u16, +} + +constant_group! { + /// Offsets for instruction processing. + instruction { + prefix = "INSN", + /// Offset to instruction discriminator byte. + offset!(DISCRIMINATOR, InstructionHeader.discriminator), + /// Initialize instruction discriminator. + DISCRIMINATOR_INITIALIZE: u8 = Instruction::Initialize as u8, + /// Insert instruction discriminator. + DISCRIMINATOR_INSERT: u8 = Instruction::Insert as u8, + /// Remove instruction discriminator. + DISCRIMINATOR_REMOVE: u8 = Instruction::Remove as u8, + /// Key field in insert instruction. + offset!(INSERT_KEY, InsertInstruction.key), + /// Value field in insert instruction. + offset!(INSERT_VALUE, InsertInstruction.value), + /// Key field in remove instruction. + offset!(REMOVE_KEY, RemoveInstruction.key), + } +} diff --git a/examples/tree/artifacts/snippets/interface/tree-defs-common.txt b/examples/tree/artifacts/snippets/interface/tree-defs-common.txt new file mode 100644 index 00000000..d7218122 --- /dev/null +++ b/examples/tree/artifacts/snippets/interface/tree-defs-common.txt @@ -0,0 +1,58 @@ +#[repr(u8)] +#[derive(PartialEq)] +pub enum Color { + Black, + Red, +} + +#[repr(usize)] +pub enum Direction { + Left, + Right, +} + +constant_group! { + /// Tree constants. + tree { + /// Max number of children per node. + N_CHILDREN: usize = 2, + /// Left direction. + DIR_L = Direction::Left as usize, + /// Right direction. + DIR_R = Direction::Right as usize, + /// Black color. + COLOR_B = Color::Black as u8, + /// Red color. + COLOR_R = Color::Red as u8, + /// Stack top field in header. + offset!(HEADER_TOP, TreeHeader.top), + /// Next node field in header. + offset!(HEADER_NEXT, TreeHeader.next), + } +} + +#[repr(C, packed)] +/// Tree account data header. Contains pointer to tree root and top of free node stack. +pub struct TreeHeader { + /// Absolute pointer to tree root in memory map. + pub root: *mut TreeNode, + /// Absolute pointer to stack top in memory map. + pub top: *mut StackNode, + /// Absolute pointer to where the next node should be allocated in memory map. + pub next: *mut TreeNode, +} + +#[array_fields] +#[repr(C, packed)] +pub struct TreeNode { + pub parent: *mut TreeNode, + pub child: [*mut TreeNode; tree::N_CHILDREN], + pub key: u16, + pub value: u16, + pub color: Color, +} + +#[repr(C, packed)] +pub struct StackNode { + pub next: *mut StackNode, +} \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/entrypoint-branching.txt b/examples/tree/artifacts/snippets/rs/entrypoint-branching.txt new file mode 100644 index 00000000..44c0257d --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/entrypoint-branching.txt @@ -0,0 +1,18 @@ +no_allocator!(); +nostd_panic_handler!(); + +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, instruction_data: *mut u8) -> u64 { + let instruction_data_len = ldxdw(instruction_data, -(size_of::() as i16)); + let n_accounts = ldxdw(input, input_buffer::N_ACCOUNTS_OFF); + let instruction_discriminator = ldxb(instruction_data, instruction::DISCRIMINATOR_OFF); + if likely(instruction_discriminator == instruction::DISCRIMINATOR_INSERT) { + insert(input, instruction_data, instruction_data_len, n_accounts) + } else if likely(instruction_discriminator == instruction::DISCRIMINATOR_REMOVE) { + remove(input, instruction_data, instruction_data_len, n_accounts) + } else if likely(instruction_discriminator == instruction::DISCRIMINATOR_INITIALIZE) { + initialize(input, instruction_data, instruction_data_len, n_accounts) + } else { + error::INSTRUCTION_DISCRIMINATOR.into() + } +} \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/initialize-create-account.txt b/examples/tree/artifacts/snippets/rs/initialize-create-account.txt new file mode 100644 index 00000000..441f2914 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/initialize-create-account.txt @@ -0,0 +1,93 @@ + // Pack CreateAccount instruction data. + let create_account_instruction_data = CreateAccountInstructionData { + discriminator: cpi::CREATE_ACCOUNT_DISCRIMINATOR, + lamports: (cpi::ACCOUNT_DATA_SCALAR as u64) * ldxdw(input, input_buffer::RENT_DATA_OFF), + space: cpi::TREE_DATA_LEN as u64, + owner: read_unaligned( + input + .add(input_buffer::INIT_PROGRAM_ID_OFF_IMM as usize) + .cast(), + ), + }; + + // Pack account metas and infos. + let user_key = input.add(input_buffer::USER_ADDRESS_OFF as usize).cast(); + let tree_key = input.add(input_buffer::TREE_ADDRESS_OFF as usize).cast(); + let sol_account_metas = [ + SolAccountMeta { + pubkey: user_key, + is_writable: true, + is_signer: true, + }, + SolAccountMeta { + pubkey: tree_key, + is_writable: true, + is_signer: true, + }, + ]; + let sol_account_infos = [ + SolAccountInfo { + key: user_key, + owner: input.add(input_buffer::USER_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::USER_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::USER_DATA_OFF as usize), + data_len: data::DATA_LEN_ZERO, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: true, + is_writable: true, + executable: false, + }, + SolAccountInfo { + key: tree_key, + owner: input.add(input_buffer::TREE_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::TREE_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::TREE_DATA_OFF as usize), + data_len: data::DATA_LEN_ZERO, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: true, + is_writable: true, + executable: false, + }, + ]; + + // Pack instruction. + let system_program_address = Address::default(); + let sol_instruction = SolInstruction { + program_id: addr_of!(system_program_address).cast_mut().cast(), + accounts: sol_account_metas.as_ptr().cast_mut().cast(), + account_len: sol_account_metas.len() as u64, + data: addr_of!(create_account_instruction_data).cast_mut().cast(), + data_len: cpi::CREATE_ACCOUNT_INSN_DATA_LEN as u64, + }; + + // Initialize signer seed for PDA bump. + let bump_seed = SolSignerSeed { + addr: addr_of!(bump).cast(), + len: size_of::() as u64, + }; + + // Initialize signer seeds for PDA. + let signers_seeds = SolSignerSeeds { + addr: addr_of!(bump_seed).cast(), + len: cpi::N_SEEDS_CREATE_ACCOUNT as u64, + }; + + #[cfg(target_os = "solana")] + sol_invoke_signed_c( + addr_of!(sol_instruction).cast(), + addr_of!(sol_account_infos).cast(), + cpi::N_ACCOUNTS as u64, + addr_of!(signers_seeds).cast(), + cpi::N_PDA_SIGNERS as u64, + ); + #[cfg(not(target_os = "solana"))] + #[allow(path_statements)] + { + signers_seeds; + sol_account_infos; + sol_instruction; + } + + // Store next pointer in tree header. + let tree_data: *mut TreeHeader = input.add(input_buffer::TREE_DATA_OFF as usize).cast(); + (*tree_data).next = tree_data.add(1).cast(); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/initialize-input-checks.txt b/examples/tree/artifacts/snippets/rs/initialize-input-checks.txt new file mode 100644 index 00000000..e4744660 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/initialize-input-checks.txt @@ -0,0 +1,23 @@ +#[inline(always)] +unsafe fn initialize( + input: *mut u8, + _instruction_data: *mut u8, + instruction_data_len: u64, + n_accounts: u64, +) -> u64 { + check_instruction_data_len!(instruction_data_len, InitializeInstruction); + + // Error if incorrect number of accounts. + if_err!( + n_accounts != input_buffer::N_ACCOUNTS_INIT, + error::N_ACCOUNTS + ); + + // Error if user has data. + let _user = user_account!(input); + + // Error if tree is duplicate or has data. + let tree = account_non_dup!(input, input_buffer::TREE_ACCOUNT_OFF, error::TREE_DUPLICATE); + check_data_len!(tree, data::DATA_LEN_ZERO, error::TREE_DATA_LEN); + + check_cpi_accounts!(input); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/initialize-pda-checks.txt b/examples/tree/artifacts/snippets/rs/initialize-pda-checks.txt new file mode 100644 index 00000000..99e7e8a4 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/initialize-pda-checks.txt @@ -0,0 +1,27 @@ + #[cfg(target_os = "solana")] + // Invoke syscall. + let (pda, bump) = { + let mut pda = MaybeUninit::
::uninit(); + let mut bump = MaybeUninit::::uninit(); + // Get input buffer footer pointer. + sol_try_find_program_address( + // Pass a declared pointer instead of null to prevent unnecessary register assignment. + input, + cpi::N_SEEDS_TRY_FIND_PDA, + input.add(input_buffer::INIT_PROGRAM_ID_OFF_IMM as usize), + pda.as_mut_ptr().cast(), + bump.as_mut_ptr(), + ); + (pda.assume_init(), bump.assume_init()) + }; + #[cfg(not(target_os = "solana"))] + let (pda, bump) = (Address::default(), 0); + + // Compare result with passed PDA. + if_err!( + !address_eq( + addr_of!(pda), + input.add(input_buffer::TREE_ADDRESS_OFF_0 as usize).cast() + ), + error::PDA_MISMATCH + ); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-allocate.txt b/examples/tree/artifacts/snippets/rs/insert-allocate.txt new file mode 100644 index 00000000..93dde9c4 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-allocate.txt @@ -0,0 +1,114 @@ + // Allocate or recycle a node. + let tree_header: *mut TreeHeader = input.add(input_buffer::TREE_DATA_OFF as usize).cast(); + let mut node: *mut TreeNode = if (*tree_header).top.is_null() { + // Error if wrong number of accounts passed, since need extra accounts to allocate space. + if_err!( + n_accounts != input_buffer::N_ACCOUNTS_INIT, + error::N_ACCOUNTS_INSERT_ALLOCATION + ); + + // Get shifted input buffer pointer based on tree data length. + let tree_data_len: *mut u64 = addr_of_mut!((*tree).data_len); + let shifted_input = + input.add((*tree_data_len).next_multiple_of(data::BPF_ALIGN_OF_U128 as u64) as usize); + + // Check system program and rent sysvar accounts using shifted input buffer pointer. + check_cpi_accounts!(shifted_input); + + // Calculate additional lamports for rent exemption of one TreeNode. + let lamports_per_byte = ldxdw(shifted_input, input_buffer::RENT_DATA_OFF); + let transfer_lamports = size_of::() as u64 * lamports_per_byte; + + // Pack Transfer instruction data. + let transfer_instruction_data = TransferInstructionData { + discriminator: cpi::TRANSFER_DISCRIMINATOR, + lamports: transfer_lamports, + }; + + // Pack account metas and infos. + let user_key = input.add(input_buffer::USER_ADDRESS_OFF as usize).cast(); + let tree_key = input.add(input_buffer::TREE_ADDRESS_OFF as usize).cast(); + let sol_account_metas = [ + SolAccountMeta { + pubkey: user_key, + is_writable: true, + is_signer: true, + }, + SolAccountMeta { + pubkey: tree_key, + is_writable: true, + is_signer: false, + }, + ]; + let sol_account_infos = [ + SolAccountInfo { + key: user_key, + owner: input.add(input_buffer::USER_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::USER_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::USER_DATA_OFF as usize), + data_len: data::DATA_LEN_ZERO, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: true, + is_writable: true, + executable: false, + }, + SolAccountInfo { + key: tree_key, + owner: input.add(input_buffer::TREE_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::TREE_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::TREE_DATA_OFF as usize), + data_len: *tree_data_len, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: false, + is_writable: true, + executable: false, + }, + ]; + + // Pack instruction. + let system_program_address = Address::default(); + let sol_instruction = SolInstruction { + program_id: addr_of!(system_program_address).cast_mut().cast(), + accounts: sol_account_metas.as_ptr().cast_mut().cast(), + account_len: sol_account_metas.len() as u64, + data: addr_of!(transfer_instruction_data).cast_mut().cast(), + data_len: cpi::TRANSFER_INSN_DATA_LEN as u64, + }; + + // No signers needed, since user is already a signer on the transaction. + let empty_signers = SolSignerSeeds { + addr: core::ptr::null(), + len: 0, + }; + + #[cfg(target_os = "solana")] + sol_invoke_signed_c( + addr_of!(sol_instruction).cast(), + addr_of!(sol_account_infos).cast(), + cpi::N_ACCOUNTS as u64, + addr_of!(empty_signers).cast(), + cpi::N_PDA_SIGNERS_TRANSFER, + ); + #[cfg(not(target_os = "solana"))] + #[allow(path_statements)] + { + empty_signers; + sol_account_infos; + sol_instruction; + } + + // Increase tree data length by size of one TreeNode. + *tree_data_len += size_of::() as u64; + + // Advance next pointer by one TreeNode. + let node = (*tree_header).next; + (*tree_header).next = (*tree_header).next.add(1); + node + } else { + // Pop node from free stack. + let top = (*tree_header).top; + (*tree_header).top = (*top).next; + top.cast() + }; + // Set key and value together as a single word. + *addr_of_mut!((*node).key).cast() = ldxw(instruction_data, instruction::INSERT_KEY_OFF); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-fixup-case-1.txt b/examples/tree/artifacts/snippets/rs/insert-fixup-case-1.txt new file mode 100644 index 00000000..29d599e9 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-fixup-case-1.txt @@ -0,0 +1,6 @@ + // Main insert fixup. + loop { + // Case 1. + if (*parent).color == Color::Black { + return SUCCESS; + } \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-fixup-case-2-3.txt b/examples/tree/artifacts/snippets/rs/insert-fixup-case-2-3.txt new file mode 100644 index 00000000..82d0bc0e --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-fixup-case-2-3.txt @@ -0,0 +1,14 @@ + // Case 2. + (*parent).color = Color::Black; + (*uncle).color = Color::Black; + (*grandparent).color = Color::Red; + node = grandparent; + + parent = (*node).parent; + if parent.is_null() { + break; + } + } + // Case 3. + SUCCESS +} \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-fixup-case-4.txt b/examples/tree/artifacts/snippets/rs/insert-fixup-case-4.txt new file mode 100644 index 00000000..8c6a6708 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-fixup-case-4.txt @@ -0,0 +1,6 @@ + let grandparent = (*parent).parent; + if grandparent.is_null() { + // Case 4. + (*parent).color = Color::Black; + return SUCCESS; + } \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-l.txt b/examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-l.txt new file mode 100644 index 00000000..ec1c9701 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-l.txt @@ -0,0 +1,70 @@ + // Determine direction and uncle with hardcoded child indices. + let uncle; + if parent == (*grandparent).child[tree::DIR_L] { + // dir_l: parent is left child of grandparent. + uncle = (*grandparent).child[tree::DIR_R]; + if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5 dir_l: rotate parent in DIR_L. + // + // Grandparent is guaranteed non-null by the case 4 check, so + // no root-replacement path is needed. Parent is known to be + // grandparent.child[DIR_L] from the dir_l branch, so the + // child pointer update is hardcoded without comparison. + if node == (*parent).child[tree::DIR_R] { + let new_root = (*parent).child[tree::DIR_R]; + let new_child = (*new_root).child[tree::DIR_L]; + + (*parent).child[tree::DIR_R] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[tree::DIR_L] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[tree::DIR_L] = new_root; + + node = parent; + parent = new_root; + } + + // Case 6 dir_l: rotate grandparent in DIR_R. + // + // The new root of this rotation is parent + // (= grandparent.child[DIR_L]), already in scope, + // eliminating the generic version's load of + // subtree.child[opposite(direction)]. + // + // Great-grandparent may be null (grandparent could be root), + // so the null check and root-replacement path are retained. + // Grandparent's position under great-grandparent is unrelated + // to dir, so the pointer comparison is also retained. + { + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[tree::DIR_R]; + + (*grandparent).child[tree::DIR_L] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[tree::DIR_R] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + if grandparent == (*great_grandparent).child[tree::DIR_R] { + (*great_grandparent).child[tree::DIR_R] = parent; + } else { + (*great_grandparent).child[tree::DIR_L] = parent; + } + } else { + (*tree_header).root = parent; + } + } + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; + } \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-r.txt b/examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-r.txt new file mode 100644 index 00000000..c976f321 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-fixup-case-5-6-dir-r.txt @@ -0,0 +1,69 @@ + } else { + // dir_r: parent is right child of grandparent. + uncle = (*grandparent).child[tree::DIR_L]; + if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5 dir_r: rotate parent in DIR_R. + // + // Grandparent is guaranteed non-null by the case 4 check, so + // no root-replacement path is needed. Parent is known to be + // grandparent.child[DIR_R] from the dir_r branch, so the + // child pointer update is hardcoded without comparison. + if node == (*parent).child[tree::DIR_L] { + let new_root = (*parent).child[tree::DIR_L]; + let new_child = (*new_root).child[tree::DIR_R]; + + (*parent).child[tree::DIR_L] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[tree::DIR_R] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[tree::DIR_R] = new_root; + + node = parent; + parent = new_root; + } + + // Case 6 dir_r: rotate grandparent in DIR_L. + // + // The new root of this rotation is parent + // (= grandparent.child[DIR_R]), already in scope, + // eliminating the generic version's load of + // subtree.child[opposite(direction)]. + // + // Great-grandparent may be null (grandparent could be root), + // so the null check and root-replacement path are retained. + // Grandparent's position under great-grandparent is unrelated + // to dir, so the pointer comparison is also retained. + { + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[tree::DIR_L]; + + (*grandparent).child[tree::DIR_R] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[tree::DIR_L] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + if grandparent == (*great_grandparent).child[tree::DIR_R] { + (*great_grandparent).child[tree::DIR_R] = parent; + } else { + (*great_grandparent).child[tree::DIR_L] = parent; + } + } else { + (*tree_header).root = parent; + } + } + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; + } + } \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-input-checks.txt b/examples/tree/artifacts/snippets/rs/insert-input-checks.txt new file mode 100644 index 00000000..440102e4 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-input-checks.txt @@ -0,0 +1,21 @@ +#[allow(unused_assignments)] +#[inline(always)] +unsafe fn insert( + input: *mut u8, + instruction_data: *mut u8, + instruction_data_len: u64, + n_accounts: u64, +) -> u64 { + check_instruction_data_len!(instruction_data_len, InsertInstruction); + + // Error if too few accounts. + if_err!( + n_accounts < input_buffer::N_ACCOUNTS_GENERAL, + error::N_ACCOUNTS + ); + + // Error if user has data. + let _user = user_account!(input); + + // Error if tree is duplicate. + let tree = account_non_dup!(input, input_buffer::TREE_ACCOUNT_OFF, error::TREE_DUPLICATE); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/insert-search.txt b/examples/tree/artifacts/snippets/rs/insert-search.txt new file mode 100644 index 00000000..68640e07 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/insert-search.txt @@ -0,0 +1,41 @@ + let key = ldxh(instruction_data, instruction::INSERT_KEY_OFF); + let mut cursor = (*tree_header).root; + + // Root is null: new node becomes root. + if cursor.is_null() { + (*node).color = Color::Red; + (*node).parent = null_mut(); + (*tree_header).root = node; + return SUCCESS; + } + + let mut parent: *mut TreeNode; + loop { + parent = cursor; + let cursor_key = (*cursor).key; + if likely(key > cursor_key) { + cursor = (*parent).child[tree::DIR_R]; + if cursor.is_null() { + (*node).color = Color::Red; + (*node).parent = parent; + (*parent).child[tree::DIR_R] = node; + if (*parent).color == Color::Black { + return SUCCESS; + } + break; + } + } else if likely(key < cursor_key) { + cursor = (*parent).child[tree::DIR_L]; + if cursor.is_null() { + (*node).color = Color::Red; + (*node).parent = parent; + (*parent).child[tree::DIR_L] = node; + if (*parent).color == Color::Black { + return SUCCESS; + } + break; + } + } else { + return error::KEY_EXISTS.into(); + } + } \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-complex.txt b/examples/tree/artifacts/snippets/rs/remove-complex.txt new file mode 100644 index 00000000..a2edc32a --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-complex.txt @@ -0,0 +1,84 @@ + let mut parent = (*node).parent; + let mut dir = direction(node); + (*parent).child[dir] = null_mut(); + let removed_node = node; + + let mut sibling: *mut TreeNode; + let mut close_nephew: *mut TreeNode; + let mut distant_nephew: *mut TreeNode; + // 0 = done (cases 1/2/4), 5 = case 5+6, 6 = case 6 only. + let mut exit_case: u32 = 0; + + loop { + sibling = (*parent).child[1 - dir]; + distant_nephew = (*sibling).child[1 - dir]; + close_nephew = (*sibling).child[dir]; + + if (*sibling).color == Color::Red { + // Case 3. + rotate_subtree(tree_header, parent, dir); + (*parent).color = Color::Red; + (*sibling).color = Color::Black; + sibling = close_nephew; + + distant_nephew = (*sibling).child[1 - dir]; + if !distant_nephew.is_null() && (*distant_nephew).color == Color::Red { + exit_case = 6; + break; + } + close_nephew = (*sibling).child[dir]; + if !close_nephew.is_null() && (*close_nephew).color == Color::Red { + exit_case = 5; + break; + } + + // Case 4. + (*sibling).color = Color::Red; + (*parent).color = Color::Black; + break; + } + + if !distant_nephew.is_null() && (*distant_nephew).color == Color::Red { + exit_case = 6; + break; + } + + if !close_nephew.is_null() && (*close_nephew).color == Color::Red { + exit_case = 5; + break; + } + + if (*parent).color == Color::Red { + // Case 4. + (*sibling).color = Color::Red; + (*parent).color = Color::Black; + break; + } + + // Case 2. + (*sibling).color = Color::Red; + node = parent; + parent = (*node).parent; + if parent.is_null() { + break; + } + dir = direction(node); + } + + // Case 5 falls through to case 6. + if exit_case == 5 { + rotate_subtree(tree_header, sibling, 1 - dir); + (*sibling).color = Color::Red; + (*close_nephew).color = Color::Black; + distant_nephew = sibling; + sibling = close_nephew; + } + + if exit_case >= 5 { + rotate_subtree(tree_header, parent, dir); + (*sibling).color = read_unaligned(addr_of!((*parent).color)); + (*parent).color = Color::Black; + (*distant_nephew).color = Color::Black; + } + + remove_recycle_node!(removed_node, tree_header); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-input-checks.txt b/examples/tree/artifacts/snippets/rs/remove-input-checks.txt new file mode 100644 index 00000000..104ce10c --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-input-checks.txt @@ -0,0 +1,20 @@ +#[inline(always)] +unsafe fn remove( + input: *mut u8, + instruction_data: *mut u8, + instruction_data_len: u64, + n_accounts: u64, +) -> u64 { + check_instruction_data_len!(instruction_data_len, RemoveInstruction); + + // Error if too few accounts. + if_err!( + n_accounts < input_buffer::N_ACCOUNTS_GENERAL, + error::N_ACCOUNTS + ); + + // Error if user has data. + let _user = user_account!(input); + + // Error if tree is duplicate. + let _tree = account_non_dup!(input, input_buffer::TREE_ACCOUNT_OFF, error::TREE_DUPLICATE); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-search.txt b/examples/tree/artifacts/snippets/rs/remove-search.txt new file mode 100644 index 00000000..4b152a1d --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-search.txt @@ -0,0 +1,24 @@ + let tree_header: *mut TreeHeader = input.add(input_buffer::TREE_DATA_OFF as usize).cast(); + let mut node = (*tree_header).root; + + if node.is_null() { + return error::KEY_DOES_NOT_EXIST.into(); + } + + let key = ldxh(instruction_data, instruction::REMOVE_KEY_OFF); + loop { + let node_key = (*node).key; + if key > node_key { + node = (*node).child[tree::DIR_R]; + if node.is_null() { + return error::KEY_DOES_NOT_EXIST.into(); + } + } else if key < node_key { + node = (*node).child[tree::DIR_L]; + if node.is_null() { + return error::KEY_DOES_NOT_EXIST.into(); + } + } else { + break; + } + } \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-simple-1.txt b/examples/tree/artifacts/snippets/rs/remove-simple-1.txt new file mode 100644 index 00000000..ec13ff51 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-simple-1.txt @@ -0,0 +1,18 @@ + if !(*node).child[tree::DIR_L].is_null() { + if !(*node).child[tree::DIR_R].is_null() { + // Simple case 1: successor swap. + let mut successor = (*node).child[tree::DIR_R]; + loop { + let left = (*successor).child[tree::DIR_L]; + if left.is_null() { + break; + } + successor = left; + } + // Copy successor's key/value to the found node as a u32 + // pair. The successor's fields are left as-is (insert + // overwrites both when reusing the node from the stack). + let node_kv = addr_of_mut!((*node).key).cast::(); + let successor_kv = addr_of!((*successor).key).cast::(); + *node_kv = *successor_kv; + node = successor; \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-simple-2.txt b/examples/tree/artifacts/snippets/rs/remove-simple-2.txt new file mode 100644 index 00000000..4f283024 --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-simple-2.txt @@ -0,0 +1,10 @@ + } else { + // Simple case 2: one child (L). + let child = (*node).child[tree::DIR_L]; + remove_simple_2_child_replace!(node, child, tree_header); + } + }; + if !(*node).child[tree::DIR_R].is_null() { + // Simple case 2: one child (R). + let child = (*node).child[tree::DIR_R]; + remove_simple_2_child_replace!(node, child, tree_header); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-simple-3.txt b/examples/tree/artifacts/snippets/rs/remove-simple-3.txt new file mode 100644 index 00000000..edc8e40b --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-simple-3.txt @@ -0,0 +1,4 @@ + } else if unlikely((*node).parent.is_null()) { + // Simple case 3: root leaf. + (*tree_header).root = null_mut(); + remove_recycle_node!(node, tree_header); \ No newline at end of file diff --git a/examples/tree/artifacts/snippets/rs/remove-simple-4.txt b/examples/tree/artifacts/snippets/rs/remove-simple-4.txt new file mode 100644 index 00000000..cf38478e --- /dev/null +++ b/examples/tree/artifacts/snippets/rs/remove-simple-4.txt @@ -0,0 +1,9 @@ + } else if (*node).color == Color::Red { + // Simple case 4: red leaf. + let parent = (*node).parent; + if node == (*parent).child[tree::DIR_R] { + (*parent).child[tree::DIR_R] = null_mut(); + } else { + (*parent).child[tree::DIR_L] = null_mut(); + } + remove_recycle_node!(node, tree_header); \ No newline at end of file diff --git a/examples/tree/artifacts/tests/entrypoint_branching/result.txt b/examples/tree/artifacts/tests/entrypoint_branching/result.txt new file mode 100644 index 00000000..9d4f1aea --- /dev/null +++ b/examples/tree/artifacts/tests/entrypoint_branching/result.txt @@ -0,0 +1,10 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Invalid instruction discriminator | 8 | 11 | +3 | +37.5% | +test tests::test_entrypoint_branching ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 8 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xb +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 11 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xb \ No newline at end of file diff --git a/examples/tree/artifacts/tests/entrypoint_branching/test.txt b/examples/tree/artifacts/tests/entrypoint_branching/test.txt new file mode 100644 index 00000000..6c056146 --- /dev/null +++ b/examples/tree/artifacts/tests/entrypoint_branching/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_entrypoint_branching() { + print_comparison_table(entrypoint::EntrypointCase::CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/initialize_create_account/result.txt b/examples/tree/artifacts/tests/initialize_create_account/result.txt new file mode 100644 index 00000000..67c9695b --- /dev/null +++ b/examples/tree/artifacts/tests/initialize_create_account/result.txt @@ -0,0 +1,36 @@ +| Test case | Fixed CU costs | ASM (net CUs) | Rust (net CUs) | Overhead | Overhead % | +|-----------|----------------|---------------|----------------|----------|------------| +| System Program is wrong address | 2446 | 108 | 142 | +34 | +31.5% | +| User has insufficient Lamports | 2596 | 108 | 142 | +34 | +31.5% | +| CreateAccount happy path | 2596 | 112 | 148 | +36 | +32.1% | +test tests::test_initialize_create_account ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Unknown program 11111111111111111111111111111111 +[ ... DEBUG ... ] Program DASMAC... consumed 2554 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: An account required by the instruction is missing +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Unknown program 11111111111111111111111111111111 +[ ... DEBUG ... ] Program DASMAC... consumed 2588 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: An account required by the instruction is missing +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Transfer: insufficient lamports 0, need 528960 +[ ... DEBUG ... ] Program 11111111111111111111111111111111 failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... consumed 2704 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Transfer: insufficient lamports 0, need 528960 +[ ... DEBUG ... ] Program 11111111111111111111111111111111 failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... consumed 2738 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 success +[ ... DEBUG ... ] Program DASMAC... consumed 2708 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 success +[ ... DEBUG ... ] Program DASMAC... consumed 2744 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success \ No newline at end of file diff --git a/examples/tree/artifacts/tests/initialize_create_account/test.txt b/examples/tree/artifacts/tests/initialize_create_account/test.txt new file mode 100644 index 00000000..966c88b0 --- /dev/null +++ b/examples/tree/artifacts/tests/initialize_create_account/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_initialize_create_account() { + print_comparison_table(init::InitCase::CPI_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/initialize_input_checks/result.txt b/examples/tree/artifacts/tests/initialize_input_checks/result.txt new file mode 100644 index 00000000..b452491f --- /dev/null +++ b/examples/tree/artifacts/tests/initialize_input_checks/result.txt @@ -0,0 +1,122 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Invalid instruction data length | 9 | 12 | +3 | +33.3% | +| Too few accounts | 10 | 13 | +3 | +30.0% | +| Too many accounts | 10 | 13 | +3 | +30.0% | +| User has nonzero data length | 12 | 15 | +3 | +25.0% | +| Tree account is duplicate | 14 | 17 | +3 | +21.4% | +| Tree has nonzero data length | 16 | 19 | +3 | +18.8% | +| System program is duplicate | 18 | 21 | +3 | +16.7% | +| System program wrong data length | 20 | 23 | +3 | +15.0% | +| Rent sysvar is duplicate | 22 | 25 | +3 | +13.6% | +| Rent address mismatch word 0 | 25 | 28 | +3 | +12.0% | +| Rent address mismatch word 1 | 25 | 28 | +3 | +12.0% | +| Rent address mismatch word 2 | 28 | 32 | +4 | +14.3% | +| Rent address mismatch word 3 | 28 | 32 | +4 | +14.3% | +| Rent address mismatch word 4 | 31 | 36 | +5 | +16.1% | +| Rent address mismatch word 5 | 31 | 36 | +5 | +16.1% | +| Rent address mismatch word 6 | 34 | 40 | +6 | +17.6% | +| Rent address mismatch word 7 | 34 | 40 | +6 | +17.6% | +test tests::test_initialize_input_checks ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 9 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 12 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 13 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 13 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 12 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 15 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 14 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 17 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 16 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x3 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 19 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x3 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 18 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 21 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 20 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x4 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 23 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x4 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 22 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x7 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 25 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x7 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 25 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 28 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 25 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 28 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 28 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 32 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 28 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 32 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 36 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 36 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 34 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 40 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 34 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 40 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 \ No newline at end of file diff --git a/examples/tree/artifacts/tests/initialize_input_checks/test.txt b/examples/tree/artifacts/tests/initialize_input_checks/test.txt new file mode 100644 index 00000000..d179220a --- /dev/null +++ b/examples/tree/artifacts/tests/initialize_input_checks/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_initialize_input_checks() { + print_comparison_table(init::InitCase::CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/initialize_pda_checks/result.txt b/examples/tree/artifacts/tests/initialize_pda_checks/result.txt new file mode 100644 index 00000000..544de8c8 --- /dev/null +++ b/examples/tree/artifacts/tests/initialize_pda_checks/result.txt @@ -0,0 +1,31 @@ +| Test case | Fixed CU costs | ASM (net CUs) | Rust (net CUs) | Overhead | Overhead % | +|-----------|----------------|---------------|----------------|----------|------------| +| PDA mismatch chunk 0 | 1500 | 45 | 55 | +10 | +22.2% | +| PDA mismatch chunk 1 | 1500 | 48 | 58 | +10 | +20.8% | +| PDA mismatch chunk 2 | 1500 | 51 | 61 | +10 | +19.6% | +| PDA mismatch chunk 3 | 1500 | 54 | 64 | +10 | +18.5% | +test tests::test_initialize_pda_checks ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1545 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1555 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1548 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1558 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1551 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1561 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1554 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 1564 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xa \ No newline at end of file diff --git a/examples/tree/artifacts/tests/initialize_pda_checks/test.txt b/examples/tree/artifacts/tests/initialize_pda_checks/test.txt new file mode 100644 index 00000000..5bd90196 --- /dev/null +++ b/examples/tree/artifacts/tests/initialize_pda_checks/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_initialize_pda_checks() { + print_comparison_table(init::InitCase::PDA_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_alloc/result.txt b/examples/tree/artifacts/tests/insert_alloc/result.txt new file mode 100644 index 00000000..caac00fe --- /dev/null +++ b/examples/tree/artifacts/tests/insert_alloc/result.txt @@ -0,0 +1,25 @@ +| Test case | Fixed CU costs | ASM (net CUs) | Rust (net CUs) | Overhead | Overhead % | +|-----------|----------------|---------------|----------------|----------|------------| +| Insert alloc happy path | 1096 | 100 | 129 | +29 | +29.0% | +| Alloc exceeds max data length | 1096 | 1398904 | 1398904 | +0 | +0.0% | +test tests::test_insert_alloc ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 success +[ ... DEBUG ... ] Program DASMAC... consumed 1196 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 success +[ ... DEBUG ... ] Program DASMAC... consumed 1225 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 success +[ ... DEBUG ... ] Program DASMAC... consumed 43131 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: Failed to reallocate account data +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2] +[ ... DEBUG ... ] Program 11111111111111111111111111111111 success +[ ... DEBUG ... ] Program DASMAC... consumed 43160 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: Failed to reallocate account data \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_alloc/test.txt b/examples/tree/artifacts/tests/insert_alloc/test.txt new file mode 100644 index 00000000..272fa7a1 --- /dev/null +++ b/examples/tree/artifacts/tests/insert_alloc/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_insert_alloc() { + print_comparison_table(insert::InsertCase::ALLOC_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_alloc_checks/result.txt b/examples/tree/artifacts/tests/insert_alloc_checks/result.txt new file mode 100644 index 00000000..0453694c --- /dev/null +++ b/examples/tree/artifacts/tests/insert_alloc_checks/result.txt @@ -0,0 +1,59 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Wrong N accounts for allocation | 15 | 19 | +4 | +26.7% | +| System program is duplicate | 23 | 27 | +4 | +17.4% | +| System program wrong data length | 25 | 29 | +4 | +16.0% | +| Rent sysvar is duplicate | 27 | 31 | +4 | +14.8% | +| Rent address mismatch chunk 0 | 30 | 34 | +4 | +13.3% | +| Rent address mismatch chunk 1 | 33 | 38 | +5 | +15.2% | +| Rent address mismatch chunk 2 | 36 | 42 | +6 | +16.7% | +| Rent address mismatch chunk 3 | 39 | 46 | +7 | +17.9% | +test tests::test_insert_alloc_checks ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 15 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xd +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 19 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xd +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 23 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 25 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x4 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 29 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x4 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x7 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x7 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 30 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 34 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 36 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 39 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 46 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x8 \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_alloc_checks/test.txt b/examples/tree/artifacts/tests/insert_alloc_checks/test.txt new file mode 100644 index 00000000..a65656e6 --- /dev/null +++ b/examples/tree/artifacts/tests/insert_alloc_checks/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_insert_alloc_checks() { + print_comparison_table(insert::InsertCase::ALLOC_CHECK_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_input_checks/result.txt b/examples/tree/artifacts/tests/insert_input_checks/result.txt new file mode 100644 index 00000000..4130a17c --- /dev/null +++ b/examples/tree/artifacts/tests/insert_input_checks/result.txt @@ -0,0 +1,38 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Instruction data too short | 7 | 9 | +2 | +28.6% | +| Instruction data too long | 7 | 9 | +2 | +28.6% | +| Too few accounts | 8 | 10 | +2 | +25.0% | +| User has nonzero data length | 10 | 12 | +2 | +20.0% | +| Tree account is duplicate | 12 | 14 | +2 | +16.7% | +test tests::test_insert_input_checks ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 7 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 9 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 7 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 9 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 8 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 12 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 12 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 14 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_input_checks/test.txt b/examples/tree/artifacts/tests/insert_input_checks/test.txt new file mode 100644 index 00000000..58e622ae --- /dev/null +++ b/examples/tree/artifacts/tests/insert_input_checks/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_insert_input_checks() { + print_comparison_table(insert::InsertCase::INPUT_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_search/result.txt b/examples/tree/artifacts/tests/insert_search/result.txt new file mode 100644 index 00000000..173d658d --- /dev/null +++ b/examples/tree/artifacts/tests/insert_search/result.txt @@ -0,0 +1,24 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Dup at root | 25 | 30 | +5 | +20.0% | +| Dup in left | 30 | 37 | +7 | +23.3% | +| Dup in right | 31 | 36 | +5 | +16.1% | +test tests::test_insert_search ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 25 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xe +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 30 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xe +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 30 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xe +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 37 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xe +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xe +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 36 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xe \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_search/test.txt b/examples/tree/artifacts/tests/insert_search/test.txt new file mode 100644 index 00000000..7edbdf49 --- /dev/null +++ b/examples/tree/artifacts/tests/insert_search/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_insert_search() { + print_comparison_table(insert::InsertCase::SEARCH_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_to_tree/result.txt b/examples/tree/artifacts/tests/insert_to_tree/result.txt new file mode 100644 index 00000000..a7ec0cf1 --- /dev/null +++ b/examples/tree/artifacts/tests/insert_to_tree/result.txt @@ -0,0 +1,164 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Empty tree | 24 | 27 | +3 | +12.5% | +| Case 1: left child | 30 | 38 | +8 | +26.7% | +| Case 1: right child | 31 | 37 | +6 | +19.4% | +| Case 4: left child | 33 | 42 | +9 | +27.3% | +| Case 4: right child | 34 | 42 | +8 | +23.5% | +| Case 2+3: left-left | 49 | 60 | +11 | +22.4% | +| Case 2+3: left-right | 50 | 60 | +10 | +20.0% | +| Case 2+3: right-left | 50 | 58 | +8 | +16.0% | +| Case 2+3: right-right | 51 | 58 | +7 | +13.7% | +| Case 2+1: left | 56 | 69 | +13 | +23.2% | +| Case 2+1: right | 59 | 66 | +7 | +11.9% | +| Case 6: left-left null uncle | 54 | 67 | +13 | +24.1% | +| Case 6: right-right null uncle | 56 | 65 | +9 | +16.1% | +| Case 5+6: left-right null uncle | 64 | 74 | +10 | +15.6% | +| Case 5+6: right-left null uncle | 64 | 72 | +8 | +12.5% | +| Case 6: GGP non-null, LL GP-left | 61 | 78 | +17 | +27.9% | +| Case 6: GGP non-null, LL GP-right | 62 | 78 | +16 | +25.8% | +| Case 6: GGP non-null, RR GP-right | 64 | 76 | +12 | +18.8% | +| Case 6: GGP non-null, RR GP-left | 63 | 76 | +13 | +20.6% | +| Case 2+6: non-null new_child dir_l | 83 | 100 | +17 | +20.5% | +| Case 2+6: non-null new_child dir_r | 87 | 96 | +9 | +10.3% | +| Case 2+5+6: non-null new_child dir_l | 94 | 106 | +12 | +12.8% | +| Case 2+5+6: non-null new_child dir_r | 96 | 104 | +8 | +8.3% | +test tests::test_insert_to_tree ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 24 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 30 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 37 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 34 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 49 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 60 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 50 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 60 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 50 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 58 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 51 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 58 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 56 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 69 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 59 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 66 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 54 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 67 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 56 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 65 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 64 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 74 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 64 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 72 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 61 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 78 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 62 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 78 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 64 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 76 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 63 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 76 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 83 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 100 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 87 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 96 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 94 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 106 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 96 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 104 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success \ No newline at end of file diff --git a/examples/tree/artifacts/tests/insert_to_tree/test.txt b/examples/tree/artifacts/tests/insert_to_tree/test.txt new file mode 100644 index 00000000..c6455408 --- /dev/null +++ b/examples/tree/artifacts/tests/insert_to_tree/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_insert_to_tree() { + print_comparison_table(insert::InsertCase::TREE_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/multi_insert/result.txt b/examples/tree/artifacts/tests/multi_insert/result.txt new file mode 100644 index 00000000..4cd05d76 --- /dev/null +++ b/examples/tree/artifacts/tests/multi_insert/result.txt @@ -0,0 +1,122 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| 3-node balanced (10,5,15) | 88 | 106 | +18 | +20.5% | +| Left-skew rotation (10,5,1) | 111 | 136 | +25 | +22.5% | +| Right-skew rotation (10,15,20) | 114 | 134 | +20 | +17.5% | +| Zigzag double rotation (10,5,7) | 121 | 143 | +22 | +18.2% | +| 7-node full tree (10,5,15,3,7,12,20) | 246 | 297 | +51 | +20.7% | +test tests::test_multi_insert ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 24 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 37 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 24 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 54 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 67 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 24 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 34 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 56 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 65 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 24 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 64 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 74 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 24 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 49 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 36 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 36 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 37 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 27 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 42 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 37 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 60 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 44 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 44 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 43 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success \ No newline at end of file diff --git a/examples/tree/artifacts/tests/multi_insert/test.txt b/examples/tree/artifacts/tests/multi_insert/test.txt new file mode 100644 index 00000000..ac4468c9 --- /dev/null +++ b/examples/tree/artifacts/tests/multi_insert/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_multi_insert() { + print_comparison_table(insert::MultiInsertCase::CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/remove_input_checks/result.txt b/examples/tree/artifacts/tests/remove_input_checks/result.txt new file mode 100644 index 00000000..39952db4 --- /dev/null +++ b/examples/tree/artifacts/tests/remove_input_checks/result.txt @@ -0,0 +1,38 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Data too short | 8 | 10 | +2 | +25.0% | +| Data too long | 8 | 10 | +2 | +25.0% | +| Too few accounts | 9 | 11 | +2 | +22.2% | +| User has data | 11 | 13 | +2 | +18.2% | +| Tree is duplicate | 13 | 15 | +2 | +15.4% | +test tests::test_remove_input_checks ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 8 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 8 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xc +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 9 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 11 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 11 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 13 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 13 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 15 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5 \ No newline at end of file diff --git a/examples/tree/artifacts/tests/remove_input_checks/test.txt b/examples/tree/artifacts/tests/remove_input_checks/test.txt new file mode 100644 index 00000000..d2700325 --- /dev/null +++ b/examples/tree/artifacts/tests/remove_input_checks/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_remove_input_checks() { + print_comparison_table(remove::RemoveCase::INPUT_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/remove_search/result.txt b/examples/tree/artifacts/tests/remove_search/result.txt new file mode 100644 index 00000000..50254ed4 --- /dev/null +++ b/examples/tree/artifacts/tests/remove_search/result.txt @@ -0,0 +1,31 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Empty tree | 15 | 16 | +1 | +6.7% | +| Not found (left) | 21 | 28 | +7 | +33.3% | +| Not found (right) | 21 | 25 | +4 | +19.0% | +| Not found (deep) | 26 | 33 | +7 | +26.9% | +test tests::test_remove_search ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 15 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 16 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 21 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 28 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 21 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 25 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 26 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 33 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0xf \ No newline at end of file diff --git a/examples/tree/artifacts/tests/remove_search/test.txt b/examples/tree/artifacts/tests/remove_search/test.txt new file mode 100644 index 00000000..23652da4 --- /dev/null +++ b/examples/tree/artifacts/tests/remove_search/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_remove_search() { + print_comparison_table(remove::RemoveCase::SEARCH_CASES); +} \ No newline at end of file diff --git a/examples/tree/artifacts/tests/remove_simple/result.txt b/examples/tree/artifacts/tests/remove_simple/result.txt new file mode 100644 index 00000000..6998b83b --- /dev/null +++ b/examples/tree/artifacts/tests/remove_simple/result.txt @@ -0,0 +1,87 @@ +| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % | +|-----------|-----------|------------|----------|------------| +| Successor immediate R (SC 1) | 40 | 59 | +19 | +47.5% | +| Successor deep L descent (SC 1) | 44 | 63 | +19 | +43.2% | +| Successor with R child (SC 1) | 40 | 59 | +19 | +47.5% | +| One child root R (SC 2) | 31 | 48 | +17 | +54.8% | +| One child root L (SC 2) | 31 | 44 | +13 | +41.9% | +| One child non-root R,R (SC 2) | 38 | 57 | +19 | +50.0% | +| One child non-root L,L (SC 2) | 38 | 56 | +18 | +47.4% | +| One child non-root R,L (SC 2) | 38 | 60 | +22 | +57.9% | +| One child non-root L,R (SC 2) | 38 | 53 | +15 | +39.5% | +| Root leaf (SC 3) | 29 | 46 | +17 | +58.6% | +| Red leaf L (SC 4) | 38 | 60 | +22 | +57.9% | +| Red leaf R (SC 4) | 38 | 57 | +19 | +50.0% | +test tests::test_remove_simple ... ok +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 40 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 59 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 44 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 63 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 40 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 59 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 48 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 31 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 44 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 57 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 56 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 60 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 53 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 29 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 46 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 60 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 38 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success +[ ... DEBUG ... ] Program DASMAC... invoke [1] +[ ... DEBUG ... ] Program DASMAC... consumed 57 of 1400000 compute units +[ ... DEBUG ... ] Program DASMAC... success \ No newline at end of file diff --git a/examples/tree/artifacts/tests/remove_simple/test.txt b/examples/tree/artifacts/tests/remove_simple/test.txt new file mode 100644 index 00000000..3e523a84 --- /dev/null +++ b/examples/tree/artifacts/tests/remove_simple/test.txt @@ -0,0 +1,4 @@ +#[test] +fn test_remove_simple() { + print_comparison_table(remove::RemoveCase::SIMPLE_CASES); +} \ No newline at end of file diff --git a/examples/tree/build.rs b/examples/tree/build.rs new file mode 100644 index 00000000..ed64ea76 --- /dev/null +++ b/examples/tree/build.rs @@ -0,0 +1,162 @@ +use std::{collections::HashSet, fs, path::Path}; +use tree_interface::*; + +const CONSTANTS_ANCHOR_START: &str = "# ANCHOR: constants"; +const CONSTANTS_ANCHOR_END: &str = "# ANCHOR_END: constants"; + +macro_rules! asm_groups { + ($($group:ident),* $(,)?) => { + [$($group::to_asm()),*] + }; +} + +fn main() { + // Collect all constant groups. + let groups = asm_groups![ + error_codes, + sizes, + data, + pubkey_chunk, + input_buffer, + instruction, + init_stack_frame, + cpi, + tree + ]; + + // Read in the assembly file. + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let manifest_path = Path::new(manifest_dir); + let asm_path = manifest_path.join("src/tree/tree.s"); + let content = fs::read_to_string(&asm_path).unwrap(); + + // Find the constants anchor region. + let anchor_start = content + .find(CONSTANTS_ANCHOR_START) + .expect("missing '# ANCHOR: constants' in assembly file"); + let anchor_end = content + .find(CONSTANTS_ANCHOR_END) + .expect("missing '# ANCHOR_END: constants' in assembly file"); + assert!( + anchor_start < anchor_end, + "ANCHOR: constants must come before ANCHOR_END: constants" + ); + + // Check for duplicate constant names. + let mut seen = HashSet::new(); + for group in &groups { + for line in group.lines() { + if let Some(name) = line.strip_prefix(".equ ") { + if let Some(name) = name.split(',').next() { + if !seen.insert(name.to_string()) { + panic!("Duplicate constant name: {}", name); + } + } + } + } + } + + // Generate the constants and insert inside the anchor region. + let constants = groups.join("\n"); + let constants = constants.trim(); + let before_anchor = &content[..anchor_start + CONSTANTS_ANCHOR_START.len()]; + let after_anchor = &content[anchor_end..]; + let new_content = format!("{}\n{}\n{}", before_anchor, constants, after_anchor); + if new_content != content { + fs::write(&asm_path, &new_content).unwrap(); + } + + // Extract ANCHOR snippets from source files (use new_content for asm since it's canonical). + extract_snippets(&new_content, manifest_path, "asm"); + + let rs_path = manifest_path.join("src/program.rs"); + let rs_content = fs::read_to_string(&rs_path).unwrap(); + extract_snippets(&rs_content, manifest_path, "rs"); + + let common_path = manifest_path.join("interface/src/common.rs"); + let common_content = fs::read_to_string(&common_path).unwrap(); + extract_snippets(&common_content, manifest_path, "interface"); +} + +/// Extract ANCHOR snippets from source content and write to artifacts/snippets/{kind}/. +fn extract_snippets(content: &str, manifest_dir: &Path, kind: &str) { + let snippets_dir = manifest_dir.join(format!("artifacts/snippets/{}", kind)); + + // Wipe existing snippets directory. + if snippets_dir.exists() { + fs::remove_dir_all(&snippets_dir).unwrap(); + } + + let mut current_anchor: Option = None; + let mut current_lines: Vec<&str> = Vec::new(); + let mut snippets: Vec<(String, String)> = Vec::new(); + let mut seen_names: HashSet = HashSet::new(); + + for line in content.lines() { + let trimmed = line.trim(); + + // Check for ANCHOR: start tag (supports # or // comments). + if let Some(rest) = trimmed + .strip_prefix("# ANCHOR:") + .or(trimmed.strip_prefix("// ANCHOR:")) + { + let name = rest.trim().to_string(); + assert!( + current_anchor.is_none(), + "Nested ANCHOR not allowed: found '{}' while inside '{}'", + name, + current_anchor.unwrap() + ); + assert!( + seen_names.insert(name.clone()), + "Duplicate ANCHOR name: '{}'", + name + ); + current_anchor = Some(name); + current_lines.clear(); + continue; + } + + // Check for ANCHOR_END: end tag. + if let Some(rest) = trimmed + .strip_prefix("# ANCHOR_END:") + .or(trimmed.strip_prefix("// ANCHOR_END:")) + { + let name = rest.trim(); + if let Some(ref anchor_name) = current_anchor { + assert_eq!( + anchor_name, name, + "ANCHOR_END mismatch: expected '{}', found '{}'", + anchor_name, name + ); + let snippet_content = current_lines.join("\n"); + snippets.push((anchor_name.clone(), snippet_content)); + current_anchor = None; + current_lines.clear(); + } else { + panic!("ANCHOR_END '{}' without matching ANCHOR", name); + } + continue; + } + + // Collect lines inside an anchor. + if current_anchor.is_some() { + current_lines.push(line); + } + } + + assert!( + current_anchor.is_none(), + "Unclosed ANCHOR: '{}'", + current_anchor.unwrap() + ); + + // Write snippets to files. + if !snippets.is_empty() { + fs::create_dir_all(&snippets_dir).unwrap(); + for (name, content) in snippets { + let snippet_path = snippets_dir.join(format!("{}.txt", name)); + fs::write(&snippet_path, content).unwrap(); + } + } +} diff --git a/examples/tree/deploy/tree-keypair.json b/examples/tree/deploy/tree-keypair.json new file mode 100644 index 00000000..dbfa0759 --- /dev/null +++ b/examples/tree/deploy/tree-keypair.json @@ -0,0 +1,66 @@ +[ + 144, + 189, + 2, + 152, + 30, + 247, + 50, + 7, + 149, + 57, + 33, + 33, + 80, + 8, + 156, + 153, + 50, + 94, + 197, + 163, + 207, + 177, + 54, + 147, + 219, + 169, + 113, + 90, + 102, + 192, + 17, + 126, + 180, + 183, + 33, + 135, + 191, + 238, + 124, + 63, + 243, + 193, + 203, + 40, + 64, + 131, + 49, + 191, + 80, + 28, + 145, + 125, + 189, + 141, + 216, + 39, + 227, + 119, + 47, + 101, + 198, + 184, + 72, + 169 +] diff --git a/examples/tree/interface/Cargo.toml b/examples/tree/interface/Cargo.toml new file mode 100644 index 00000000..12e2214f --- /dev/null +++ b/examples/tree/interface/Cargo.toml @@ -0,0 +1,8 @@ +[dependencies] +macros.path = "../macros" +pinocchio.workspace = true + +[package] +edition = "2021" +name = "tree-interface" +version = "0.1.0" diff --git a/examples/tree/interface/src/asm.rs b/examples/tree/interface/src/asm.rs new file mode 100644 index 00000000..a7b974af --- /dev/null +++ b/examples/tree/interface/src/asm.rs @@ -0,0 +1,285 @@ +extern crate alloc; + +use crate::bindings::{ + SolAccountInfo, SolAccountMeta, SolInstruction, SolSignerSeed, SolSignerSeeds, +}; +use crate::common::{ + cpi, CreateAccountInstructionData, Direction, GeneralInputBufferHeader, InitInputBuffer, + InitializeInstruction, InputBufferHeader, InsertInstruction, Instruction, RemoveInstruction, + TreeHeader, TreeNode, +}; +use macros::{asm_constant_group, extend_constant_group, pubkey_chunk_group, sizes, stack_frame}; +use pinocchio::{ + entrypoint::NON_DUP_MARKER, + sysvars::rent::{Rent, RENT_ID}, + Address, +}; + +pubkey_chunk_group!(); + +sizes! { + u8, + u64, + Address, + u128, + TreeHeader, + InitializeInstruction, + InsertInstruction, + RemoveInstruction, + TreeNode, +} + +extend_constant_group!(data { + /// No offset. + OFFSET_ZERO = 0, + /// Null pointer. + NULL = 0, + /// And mask for data length alignment. + DATA_LEN_AND_MASK = -8, + /// Maximum possible data length padding. + MAX_DATA_PAD = 7, + /// Boolean true value. + BOOL_TRUE = 1, +}); + +extend_constant_group!(input_buffer { + prefix = "IB", + /// User address field. + offset!(USER_ADDRESS, InputBufferHeader.user.header.address), + /// User data length field. + offset!(USER_DATA_LEN, InputBufferHeader.user.header.data_len), + /// Non-duplicate marker value. + NON_DUP_MARKER = NON_DUP_MARKER, + /// Tree non-duplicate marker field. + offset!(TREE_NON_DUP_MARKER, InputBufferHeader.tree_header.borrow_state), + /// Tree address field. + pubkey_offset!(TREE_ADDRESS, InputBufferHeader.tree_header.address), + /// Tree data length field. + offset!(TREE_DATA_LEN, InputBufferHeader.tree_header.data_len), + /// System Program non-duplicate marker field. + offset!( + SYSTEM_PROGRAM_NON_DUP_MARKER, + InitInputBuffer.header.system_program.header.borrow_state + ), + /// System Program data length field. + offset!(SYSTEM_PROGRAM_DATA_LEN, InitInputBuffer.header.system_program.header.data_len), + /// Rent account non-duplicate marker field. + offset!(RENT_NON_DUP_MARKER, InitInputBuffer.header.rent.header.borrow_state), + /// Rent address field. + pubkey_offset!(RENT_ADDRESS, InitInputBuffer.header.rent.header.address), + /// Rent sysvar ID. + pubkey_value!(RENT_ID, RENT_ID), + /// Program ID field for initialize instruction. + offset_immediate!(INIT_PROGRAM_ID, InitInputBuffer.footer.program_id), + /// Tree top pointer field within tree data. + offset!(TREE_DATA_TOP, GeneralInputBufferHeader.tree_data.top), + /// Tree next pointer field within tree data. + offset!(TREE_DATA_NEXT, GeneralInputBufferHeader.tree_data.next), + /// Tree root pointer field within tree data. + offset!(TREE_DATA_ROOT, GeneralInputBufferHeader.tree_data.root), + /// Relative offset from user data field to tree pubkey field. + relative_offset_immediate!( + USER_DATA, + TREE_ADDRESS, + InputBufferHeader.user.data, + InputBufferHeader.tree_header.address + ), +}); + +#[stack_frame] +struct InitStackFrame { + bump_seed: u8, + instruction_data: CreateAccountInstructionData, + instruction: SolInstruction, + account_metas: [SolAccountMeta; cpi::N_ACCOUNTS], + account_infos: [SolAccountInfo; cpi::N_ACCOUNTS], + signers_seeds: [SolSignerSeeds; cpi::N_PDA_SIGNERS], + signer_seeds: [SolSignerSeed; cpi::N_SEEDS_CREATE_ACCOUNT], + pda: Address, + rent: Rent, + /// Zero-initialized on stack. + system_program_address: Address, +} + +asm_constant_group! { + /// Init stack frame layout. + init_stack_frame { + prefix = "SF_INIT", + /// Bump seed. + stack_frame_offset!(BUMP_SEED, InitStackFrame.bump_seed), + /// Bump signer seed address field. + stack_frame_offset!(SIGNER_SEED_ADDR, InitStackFrame.signer_seeds[0].addr), + /// Bump signer seed length field. + stack_frame_offset!(SIGNER_SEED_LEN, InitStackFrame.signer_seeds[0].len), + /// PDA address field. + stack_frame_offset!(PDA, InitStackFrame.pda), + /// Discriminator field in CPI instruction data. + stack_frame_offset_unaligned!( + CREATE_ACCOUNT_DISCRIMINATOR, + InitStackFrame.instruction_data.discriminator + ), + /// Lamports field in CreateAccount instruction data. + stack_frame_offset_unaligned!( + CREATE_ACCOUNT_LAMPORTS, + InitStackFrame.instruction_data.lamports + ), + /// Space address field in CreateAccount instruction data. + stack_frame_offset_unaligned!(CREATE_ACCOUNT_SPACE, InitStackFrame.instruction_data.space), + /// Owner field in CreateAccount instruction data. + stack_frame_pubkey_offset_unaligned!( + CREATE_ACCOUNT_OWNER, + InitStackFrame.instruction_data.owner + ), + /// Signers seeds address field. + stack_frame_offset!(SIGNERS_SEEDS_ADDR, InitStackFrame.signers_seeds), + /// Signers seeds length field. + stack_frame_offset!(SIGNERS_SEEDS_LEN, InitStackFrame.signers_seeds[0].len), + /// System Program address. + stack_frame_offset!(SYSTEM_PROGRAM_ADDRESS, InitStackFrame.system_program_address), + /// SolInstruction program_id field. + stack_frame_offset!(INSN_PROGRAM_ID, InitStackFrame.instruction.program_id), + /// SolInstruction accounts field. + stack_frame_offset!(INSN_ACCOUNTS, InitStackFrame.instruction.accounts), + /// SolInstruction account_len field. + stack_frame_offset!(INSN_ACCOUNT_LEN, InitStackFrame.instruction.account_len), + /// SolInstruction data field. + stack_frame_offset!(INSN_DATA, InitStackFrame.instruction.data), + /// SolInstruction data_len field. + stack_frame_offset!(INSN_DATA_LEN, InitStackFrame.instruction.data_len), + /// SolAccountMeta is_writable field for user account. + stack_frame_offset!( + USER_META_IS_WRITABLE, + InitStackFrame.account_metas[cpi::USER_ACCOUNT_INDEX].is_writable + ), + /// SolAccountMeta is_writable field for tree account. + stack_frame_offset!( + TREE_META_IS_WRITABLE, + InitStackFrame.account_metas[cpi::TREE_ACCOUNT_INDEX].is_writable + ), + /// SolAccountInfo is_signer field for user account. + stack_frame_offset!( + USER_INFO_IS_SIGNER, + InitStackFrame.account_infos[cpi::USER_ACCOUNT_INDEX].is_signer + ), + /// SolAccountMeta pubkey field for user account. + stack_frame_offset!( + USER_META_PUBKEY, + InitStackFrame.account_metas[cpi::USER_ACCOUNT_INDEX].pubkey + ), + /// SolAccountInfo pubkey field for user account. + stack_frame_offset!( + USER_INFO_PUBKEY, + InitStackFrame.account_infos[cpi::USER_ACCOUNT_INDEX].key + ), + /// SolAccountInfo owner field for user account. + stack_frame_offset!( + USER_INFO_OWNER, + InitStackFrame.account_infos[cpi::USER_ACCOUNT_INDEX].owner + ), + /// SolAccountInfo lamports field for user account. + stack_frame_offset!( + USER_INFO_LAMPORTS, + InitStackFrame.account_infos[cpi::USER_ACCOUNT_INDEX].lamports + ), + /// SolAccountInfo data_len field for user account. + stack_frame_offset!( + USER_INFO_DATA, + InitStackFrame.account_infos[cpi::USER_ACCOUNT_INDEX].data + ), + /// SolAccountInfo data_len for tree account. + stack_frame_offset!( + TREE_INFO_DATA_LEN, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].data_len + ), + /// SolAccountInfo is_signer field for tree account. + stack_frame_offset!( + TREE_INFO_IS_SIGNER, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].is_signer + ), + /// SolAccountInfo is_writable field for tree account. + stack_frame_offset_unaligned!( + TREE_INFO_IS_WRITABLE, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].is_writable + ), + /// SolAccountMeta pubkey field for tree account. + stack_frame_offset!( + TREE_META_PUBKEY, + InitStackFrame.account_metas[cpi::TREE_ACCOUNT_INDEX].pubkey + ), + /// SolAccountInfo pubkey field for tree account. + stack_frame_offset!( + TREE_INFO_PUBKEY, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].key + ), + /// SolAccountInfo owner field for tree account. + stack_frame_offset!( + TREE_INFO_OWNER, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].owner + ), + /// SolAccountInfo lamports field for tree account. + stack_frame_offset!( + TREE_INFO_LAMPORTS, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].lamports + ), + /// SolAccountInfo data_len field for tree account. + stack_frame_offset!( + TREE_INFO_DATA, + InitStackFrame.account_infos[cpi::TREE_ACCOUNT_INDEX].data + ), + /// Relative offset from PDA on stack to System Program ID. + relative_offset_immediate!( + PDA, + SYSTEM_PROGRAM_ID, + InitStackFrame.pda, + InitStackFrame.system_program_address + ), + /// Relative offset from System Program ID to first SolAccountMeta. + relative_offset_immediate!( + SYSTEM_PROGRAM_ID, + ACCT_METAS, + InitStackFrame.system_program_address, + InitStackFrame.account_metas + ), + /// Relative offset from SolAccountMeta array to instruction data. + relative_offset_immediate!( + ACCT_METAS, + INSN_DATA, + InitStackFrame.account_metas, + InitStackFrame.instruction_data + ), + /// Relative offset from instruction data to signer seeds. + relative_offset_immediate!( + INSN_DATA, + SIGNER_SEEDS, + InitStackFrame.instruction_data, + InitStackFrame.signer_seeds + ), + /// Relative offset from signer seeds to signers seeds. + relative_offset_immediate!( + SIGNER_SEEDS, + SIGNERS_SEEDS, + InitStackFrame.signer_seeds, + InitStackFrame.signers_seeds + ), + /// Account infos array. + stack_frame_offset!(ACCT_INFOS, InitStackFrame.account_infos), + } +} + +extend_constant_group!(tree { + prefix = "TREE", + /// Discriminator for insert instruction. + DISCRIMINATOR_INSERT = Instruction::Insert as u8, + /// Node key field. + offset!(NODE_KEY, TreeNode.key), + /// Node value field. + offset!(NODE_VALUE, TreeNode.value), + /// Node left child field. + offset!(NODE_CHILD_L, TreeNode.child[Direction::Left as usize]), + /// Node right child field. + offset!(NODE_CHILD_R, TreeNode.child[Direction::Right as usize]), + /// Node parent field. + offset!(NODE_PARENT, TreeNode.parent), + /// Color field. + offset!(NODE_COLOR, TreeNode.color), +}); diff --git a/examples/tree/interface/src/bindings.rs b/examples/tree/interface/src/bindings.rs new file mode 100644 index 00000000..68481dc4 --- /dev/null +++ b/examples/tree/interface/src/bindings.rs @@ -0,0 +1,74 @@ +/// Generated from Agave using bindgen. +use pinocchio::Address; + +/// SolInstruction from cpi.h. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SolInstruction { + /// Pubkey of the instruction processor that executes this instruction. + pub program_id: *mut Address, + /// Metadata for what accounts should be passed to the instruction processor. + pub accounts: *mut SolAccountMeta, + /// Number of SolAccountMetas. + pub account_len: u64, + /// Opaque data passed to the instruction processor. + pub data: *mut u8, + /// Length of the data in bytes. + pub data_len: u64, +} + +/// SolAccountMeta from cpi.h. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SolAccountMeta { + /// An account's public key. + pub pubkey: *mut Address, + /// True if the `pubkey` can be loaded as a read-write account. + pub is_writable: bool, + /// True if an Instruction requires a Transaction signature matching `pubkey`. + pub is_signer: bool, +} + +/// SolAccountInfo from entrypoint.h. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SolAccountInfo { + /// Public key of the account. + pub key: *mut Address, + /// Number of lamports owned by this account. + pub lamports: *mut u64, + /// Length of data in bytes. + pub data_len: u64, + /// On-chain data within this account. + pub data: *mut u8, + /// Program that owns this account. + pub owner: *mut Address, + /// The epoch at which this account will next owe rent. + pub rent_epoch: u64, + /// Transaction was signed by this account's key? + pub is_signer: bool, + /// Is the account writable? + pub is_writable: bool, + /// This account's data contains a loaded program (and is now read-only). + pub executable: bool, +} + +/// SolSignerSeed from pubkey.h. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SolSignerSeed { + /// Seed bytes. + pub addr: *const u8, + /// Length of the seed bytes. + pub len: u64, +} + +/// SolSignerSeeds from pubkey.h. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SolSignerSeeds { + /// An array of a signer's seeds. + pub addr: *const SolSignerSeed, + /// Number of seeds. + pub len: u64, +} diff --git a/examples/tree/interface/src/common.rs b/examples/tree/interface/src/common.rs new file mode 100644 index 00000000..d1564466 --- /dev/null +++ b/examples/tree/interface/src/common.rs @@ -0,0 +1,320 @@ +use core::mem::size_of; +use macros::{array_fields, constant_group, error_codes}; +use pinocchio::{ + account::{RuntimeAccount as RuntimeAccountHeader, MAX_PERMITTED_DATA_INCREASE}, + sysvars::rent::{Rent, ACCOUNT_STORAGE_OVERHEAD}, + Address, +}; + +error_codes! { + /// An invalid number of accounts were passed. + N_ACCOUNTS, + /// The user account has invalid data length. + USER_DATA_LEN, + /// The tree account has invalid data length. + TREE_DATA_LEN, + /// The System Program account has invalid data length. + SYSTEM_PROGRAM_DATA_LEN, + /// The tree account is a duplicate. + TREE_DUPLICATE, + /// The System Program account is a duplicate. + SYSTEM_PROGRAM_DUPLICATE, + /// The rent sysvar account is a duplicate. + RENT_DUPLICATE, + /// The rent sysvar account has invalid data length. + RENT_ADDRESS, + /// Instruction data provided during initialization instruction. + INSTRUCTION_DATA, + /// The passed PDA does not match the expected address. + PDA_MISMATCH, + /// Invalid instruction discriminator. + INSTRUCTION_DISCRIMINATOR, + /// Invalid instruction data length. + INSTRUCTION_DATA_LEN, + /// Not enough accounts passed for insertion allocation. + N_ACCOUNTS_INSERT_ALLOCATION, + /// Key already exists in tree during insertion. + KEY_EXISTS, + /// Key does not exist in tree during removal. + KEY_DOES_NOT_EXIST, +} + +constant_group! { + /// Input buffer layout. + input_buffer { + /// Number of accounts field. + offset!(N_ACCOUNTS, InputBufferHeader.n_accounts), + /// User runtime account. + offset!(USER_ACCOUNT, InputBufferHeader.user), + /// User Lamports field. + offset!(USER_LAMPORTS, InputBufferHeader.user.header.lamports), + /// User data field. + offset!(USER_DATA, InputBufferHeader.user.data), + /// User owner field. + offset!(USER_OWNER, InputBufferHeader.user.header.owner), + /// Tree Lamports field. + offset!(TREE_LAMPORTS, InputBufferHeader.tree_header.lamports), + /// Tree data field. + offset!(TREE_DATA, InitInputBuffer.header.tree.data), + /// Tree owner field. + offset!(TREE_OWNER, InputBufferHeader.tree_header.owner), + /// Tree runtime account header. + offset!(TREE_ACCOUNT, InputBufferHeader.tree_header), + /// Tree address field. + offset!(TREE_ADDRESS, InputBufferHeader.tree_header.address), + /// System Program runtime account header. + offset!(SYSTEM_PROGRAM_ACCOUNT, InitInputBuffer.header.system_program), + /// Rent sysvar account header. + offset!(RENT_ACCOUNT, InitInputBuffer.header.rent), + /// Rent sysvar account data. + offset!(RENT_DATA, InitInputBuffer.header.rent.data), + /// Expected number of accounts for general instructions. + N_ACCOUNTS_GENERAL: u64 = 2, + /// Expected number of accounts for tree initialization. + N_ACCOUNTS_INIT: u64 = 4, + /// Expected data length of system program account. + SYSTEM_PROGRAM_DATA_LEN: usize = b"system_program".len(), + /// Expected data length of rent sysvar account. + // Includes extra byte for deprecated burn_percent field that is still present in test + // framework. + RENT_DATA_LEN: usize = size_of::() + size_of::(), + } +} + +constant_group! { + /// CPI-specific constants. + cpi { + prefix = "CPI", + /// User and tree accounts. + N_ACCOUNTS: usize = 2, + /// The tree account is a PDA for CreateAccount CPI. + N_PDA_SIGNERS: usize = 1, + /// Number of seeds for CreateAccount PDA signer (bump only). + N_SEEDS_CREATE_ACCOUNT: usize = 1, + /// PDA signers for Transfer CPI (none — user is already a signer). + N_PDA_SIGNERS_TRANSFER: u64 = 0, + /// Number of seeds for PDA generation. + N_SEEDS_TRY_FIND_PDA: u64 = 0, + /// Tree account data length. + TREE_DATA_LEN: usize = size_of::(), + /// Account data scalar for base rent calculation. + ACCOUNT_DATA_SCALAR: usize = (ACCOUNT_STORAGE_OVERHEAD as usize) + TREE_DATA_LEN, + /// CreateAccount discriminator for CPI. + CREATE_ACCOUNT_DISCRIMINATOR: u32 = 0, + /// Length of CreateAccount instruction data. + CREATE_ACCOUNT_INSN_DATA_LEN: usize = size_of::(), + /// Transfer discriminator for CPI. + TRANSFER_DISCRIMINATOR: u32 = 2, + /// Length of Transfer instruction data. + TRANSFER_INSN_DATA_LEN: usize = size_of::(), + /// Mask for writable signer. + WRITABLE_SIGNER: u64 = 0x0101, + /// Account index for user account in CPI. + USER_ACCOUNT_INDEX: usize = 0, + /// Account index for tree account in CPI. + TREE_ACCOUNT_INDEX: usize = 1, + /// Null rent epoch. + RENT_EPOCH_NULL: u64 = 0, + } +} + +#[repr(C, packed)] +/// For CPI to create tree account. +pub struct CreateAccountInstructionData { + pub discriminator: u32, + pub lamports: u64, + pub space: u64, + pub owner: Address, +} + +#[repr(C, packed)] +/// For CPI to transfer lamports. +pub struct TransferInstructionData { + pub discriminator: u32, + pub lamports: u64, +} + +constant_group! { + /// Data layout constants. + data { + /// Data length of zero. + DATA_LEN_ZERO: u64 = 0, + /// Data alignment during runtime. + BPF_ALIGN_OF_U128: usize = 8, + } +} + +#[repr(C, packed)] +/// Input buffer header for all instructions. +pub struct InputBufferHeader { + pub n_accounts: u64, + pub user: EmptyRuntimeAccount, + pub tree_header: RuntimeAccountHeader, +} + +#[repr(C, packed)] +/// Input buffer for tree initialization instruction. Broken up to fit relative offsets in i16. +pub struct InitInputBuffer { + pub header: InitInputBufferHeader, + pub footer: InitInputBufferFooter, +} + +#[repr(C, packed)] +pub struct InitInputBufferHeader { + pub _n_accounts: u64, + pub _user: EmptyRuntimeAccount, + pub tree: EmptyRuntimeAccount, + pub system_program: SystemProgramRuntimeAccount, + pub rent: RentRuntimeAccount, +} + +#[repr(C, packed)] +/// Input buffer header for general tree instructions. +pub struct GeneralInputBufferHeader { + pub n_accounts: u64, + pub user: EmptyRuntimeAccount, + pub tree_header: RuntimeAccountHeader, + pub tree_data: TreeHeader, +} + +// ANCHOR: tree-defs-common +#[repr(u8)] +#[derive(PartialEq)] +pub enum Color { + Black, + Red, +} + +#[repr(usize)] +pub enum Direction { + Left, + Right, +} + +constant_group! { + /// Tree constants. + tree { + /// Max number of children per node. + N_CHILDREN: usize = 2, + /// Left direction. + DIR_L = Direction::Left as usize, + /// Right direction. + DIR_R = Direction::Right as usize, + /// Black color. + COLOR_B = Color::Black as u8, + /// Red color. + COLOR_R = Color::Red as u8, + /// Stack top field in header. + offset!(HEADER_TOP, TreeHeader.top), + /// Next node field in header. + offset!(HEADER_NEXT, TreeHeader.next), + } +} + +#[repr(C, packed)] +/// Tree account data header. Contains pointer to tree root and top of free node stack. +pub struct TreeHeader { + /// Absolute pointer to tree root in memory map. + pub root: *mut TreeNode, + /// Absolute pointer to stack top in memory map. + pub top: *mut StackNode, + /// Absolute pointer to where the next node should be allocated in memory map. + pub next: *mut TreeNode, +} + +#[array_fields] +#[repr(C, packed)] +pub struct TreeNode { + pub parent: *mut TreeNode, + pub child: [*mut TreeNode; tree::N_CHILDREN], + pub key: u16, + pub value: u16, + pub color: Color, +} + +#[repr(C, packed)] +pub struct StackNode { + pub next: *mut StackNode, +} +// ANCHOR_END: tree-defs-common + +// ANCHOR: instructions +#[repr(u8)] +pub enum Instruction { + /// Initialize the tree. + Initialize, + /// Insert key-value pair. + Insert, + /// Remove key-value pair. + Remove, +} + +#[repr(C, packed)] +pub struct InstructionHeader { + pub discriminator: u8, +} + +#[repr(C, packed)] +pub struct InitializeInstruction { + pub header: InstructionHeader, +} + +#[repr(C, packed)] +pub struct InsertInstruction { + pub header: InstructionHeader, + pub key: u16, + pub value: u16, +} + +#[repr(C, packed)] +pub struct RemoveInstruction { + pub header: InstructionHeader, + pub key: u16, +} + +constant_group! { + /// Offsets for instruction processing. + instruction { + prefix = "INSN", + /// Offset to instruction discriminator byte. + offset!(DISCRIMINATOR, InstructionHeader.discriminator), + /// Initialize instruction discriminator. + DISCRIMINATOR_INITIALIZE: u8 = Instruction::Initialize as u8, + /// Insert instruction discriminator. + DISCRIMINATOR_INSERT: u8 = Instruction::Insert as u8, + /// Remove instruction discriminator. + DISCRIMINATOR_REMOVE: u8 = Instruction::Remove as u8, + /// Key field in insert instruction. + offset!(INSERT_KEY, InsertInstruction.key), + /// Value field in insert instruction. + offset!(INSERT_VALUE, InsertInstruction.value), + /// Key field in remove instruction. + offset!(REMOVE_KEY, RemoveInstruction.key), + } +} + +// ANCHOR_END: instructions + +#[repr(C, packed)] +pub struct InitInputBufferFooter { + pub instruction_data_len: u64, + pub instruction: InitializeInstruction, + pub program_id: Address, +} + +#[repr(C)] +pub struct RuntimeAccount { + pub header: RuntimeAccountHeader, + pub data: [u8; DATA_SIZE], + pub rent_epoch: u64, +} + +type EmptyRuntimeAccount = RuntimeAccount<{ runtime_data_size(data::DATA_LEN_ZERO as usize) }>; +type SystemProgramRuntimeAccount = + RuntimeAccount<{ runtime_data_size(input_buffer::SYSTEM_PROGRAM_DATA_LEN) }>; +type RentRuntimeAccount = RuntimeAccount<{ runtime_data_size(input_buffer::RENT_DATA_LEN) }>; + +/// Compute the data buffer size for a runtime account with the given data length. +const fn runtime_data_size(data_len: usize) -> usize { + MAX_PERMITTED_DATA_INCREASE + data_len.next_multiple_of(data::BPF_ALIGN_OF_U128) +} diff --git a/examples/tree/interface/src/lib.rs b/examples/tree/interface/src/lib.rs new file mode 100644 index 00000000..4194df4e --- /dev/null +++ b/examples/tree/interface/src/lib.rs @@ -0,0 +1,15 @@ +#![no_std] + +extern crate alloc; + +mod asm; +mod bindings; +mod common; + +pub use asm::*; +pub use bindings::{SolAccountInfo, SolAccountMeta, SolInstruction, SolSignerSeed, SolSignerSeeds}; +pub use common::{ + cpi, error_codes, instruction, Color, CreateAccountInstructionData, Direction, + InitializeInstruction, InsertInstruction, Instruction, InstructionHeader, RemoveInstruction, + StackNode, TransferInstructionData, TreeHeader, TreeNode, +}; diff --git a/examples/tree/macros/Cargo.toml b/examples/tree/macros/Cargo.toml new file mode 100644 index 00000000..5405b0fa --- /dev/null +++ b/examples/tree/macros/Cargo.toml @@ -0,0 +1,13 @@ +[dependencies] +convert_case.workspace = true +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true + +[lib] +proc-macro = true + +[package] +edition = "2021" +name = "macros" +version = "0.1.0" diff --git a/examples/tree/macros/src/lib.rs b/examples/tree/macros/src/lib.rs new file mode 100644 index 00000000..016ad7c1 --- /dev/null +++ b/examples/tree/macros/src/lib.rs @@ -0,0 +1,2549 @@ +// cspell:word strs +// cspell:word idents +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + braced, bracketed, + parse::{discouraged::Speculative, Parse, ParseStream}, + parse_macro_input, Ident, Lit, LitInt, Meta, Token, +}; + +/// Maximum line length for ASM output. +const MAX_LINE_LEN: usize = 75; + +/// BPF alignment requirement (align_of::()). +const BPF_ALIGN: i64 = 8; + +/// Maximum comment length (accounting for `# ` prefix). +const MAX_COMMENT_LEN: usize = MAX_LINE_LEN - "# ".len(); + +/// Error code entry: doc comment + snake_case name. +struct ErrorCodeEntry { + doc: String, + name: Ident, +} + +/// Input for error_codes! macro. +struct ErrorCodesInput { + entries: Vec, +} + +impl Parse for ErrorCodesInput { + fn parse(input: ParseStream) -> syn::Result { + let mut entries = Vec::new(); + + while !input.is_empty() { + let attrs = input.call(syn::Attribute::parse_outer)?; + let doc = extract_doc_comment(&attrs) + .ok_or_else(|| input.error("Error code must have a doc comment"))?; + + if let Err(e) = validate_doc_comment(&doc) { + return Err(input.error(e)); + } + + let name: Ident = input.parse()?; + + // Optional trailing comma. + let _ = input.parse::(); + + entries.push(ErrorCodeEntry { doc, name }); + } + + Ok(ErrorCodesInput { entries }) + } +} + +/// Macro for defining error codes shared between Rust and ASM. +/// +/// Creates an `Error` enum with `#[repr(u32)]` and auto-numbered variants starting at 1. +/// Variant names are SCREAMING_SNAKE_CASE. ASM names have `E_` prefix added. +/// +/// # Example +/// ```ignore +/// error_codes! { +/// /// An invalid number of accounts were passed. +/// N_ACCOUNTS_INVALID, +/// /// The user account has nonzero data length. +/// USER_HAS_DATA, +/// } +/// ``` +/// +/// Generates: +/// - Rust: `enum Error { N_ACCOUNTS_INVALID, USER_HAS_DATA }` with `From for u32` +/// - ASM: `.equ E_N_ACCOUNTS_INVALID, 1` and `.equ E_USER_HAS_DATA, 2` +#[proc_macro] +pub fn error_codes(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ErrorCodesInput); + + let mut variant_defs = Vec::new(); + let mut asm_lines = Vec::new(); + + for (idx, entry) in input.entries.iter().enumerate() { + let doc = &entry.doc; + let variant_name = &entry.name; + // Just add E_ prefix for ASM. + let asm_name = format!("E_{}", entry.name); + let value = (idx + 1) as u32; + + variant_defs.push(quote! { + #[doc = #doc] + #variant_name = #value + }); + + asm_lines.push(asm_equ_line(&asm_name, value, doc)); + } + + let header = asm_header("Error codes."); + let body = asm_lines.join("\n"); + + let n_codes = input.entries.len() as u32; + + let expanded = quote! { + pub mod error_codes { + #[repr(u32)] + #[allow(non_camel_case_types)] + pub enum error { + #(#variant_defs),* + } + + impl error { + /// Total number of error codes. + pub const N_CODES: u32 = #n_codes; + } + + impl From for u32 { + fn from(e: error) -> u32 { + e as u32 + } + } + + impl From for u64 { + fn from(e: error) -> u64 { + e as u64 + } + } + + /// Generate ASM constants for error codes. + pub fn to_asm() -> alloc::string::String { + alloc::format!("{}\n{}\n", #header, #body) + } + } + }; + + TokenStream::from(expanded) +} + +/// Generate an ASM section header with auto-width dashes. +fn asm_header(title: &str) -> String { + let dash_len = title.len().min(MAX_COMMENT_LEN); + format!("# {}\n# {}", title, "-".repeat(dash_len)) +} + +/// Validate a doc comment: must end with period and fit within max length. +fn validate_doc_comment(comment: &str) -> Result<(), String> { + if !comment.ends_with('.') { + return Err(format!("Doc comment must end with a period: {:?}", comment)); + } + if comment.len() > MAX_COMMENT_LEN { + return Err(format!( + "Doc comment exceeds max length of {} chars (got {}): {:?}", + MAX_COMMENT_LEN, + comment.len(), + comment + )); + } + Ok(()) +} + +/// Format an ASM .equ line. If inline comment would exceed max line length, +/// put the comment on its own line above. +fn asm_equ_line(name: &str, value: impl std::fmt::Display, comment: &str) -> String { + let inline = format!(".equ {}, {} # {}", name, value, comment); + if inline.len() <= MAX_LINE_LEN { + inline + } else { + format!("# {}\n.equ {}, {}", comment, name, value) + } +} + +/// Extract the doc comment from attributes. +fn extract_doc_comment(attrs: &[syn::Attribute]) -> Option { + let mut doc_parts = Vec::new(); + + for attr in attrs { + if attr.path().is_ident("doc") { + if let Meta::NameValue(meta) = &attr.meta { + if let syn::Expr::Lit(expr_lit) = &meta.value { + if let Lit::Str(lit_str) = &expr_lit.lit { + doc_parts.push(lit_str.value().trim().to_string()); + } + } + } + } + } + + if doc_parts.is_empty() { + None + } else { + Some(doc_parts.join(" ")) + } +} + +enum ConstantKind { + /// Regular constant with explicit type and value. + Value { + ty: syn::Type, + value: syn::Expr, + /// Original literal string for ASM output (preserves hex/binary). + literal_repr: Option, + }, + /// Offset derived from struct field path (i16 validated). + /// Name gets `_OFF` suffix appended. + /// Supports optional array indexing via `___fields` companion module. + Offset { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + }, + /// Offset derived from struct field path (i32 validated). + /// Name gets `_OFF_IMM` suffix appended. For use as BPF immediate operand. + /// Supports optional array indexing via `___fields` companion module. + OffsetImmediate { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + }, + /// Negative offset from end of a stack frame struct (i16 validated). + /// Computed as `offset_of!(Struct, field) - size_of::()`. + /// Name gets `_OFF` suffix (aligned) or `_UOFF` suffix (unaligned). + /// Array element types are resolved via the `___fields` companion module + /// generated by `#[stack_frame]`. + StackFrameOffset { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + aligned: bool, + }, + /// Pubkey chunk offset (i16 validated). + /// Base offset + chunk_index * 8 for 8-byte register loads. + /// Name gets `_OFF_{chunk_index}` suffix appended. + PubkeyChunkOffset { + struct_name: Ident, + field_path: Vec, + chunk_index: usize, + }, + /// Negative stack frame offset for pubkey chunks (i16 validated). + /// Computed as `offset_of!(Struct, field) + chunk_index * 8 - size_of::()`. + /// Always unaligned. Name gets `_UOFF_{chunk_index}` suffix appended. + StackFramePubkeyChunkOffset { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + chunk_index: usize, + }, + /// Relative offset between two fields (i32 validated). + /// Computed as `offset_of!(Struct, to_field) - offset_of!(Struct, from_field)`. + /// Name is `{FROM}_TO_{TO}_REL_OFF_IMM`. + RelativeOffsetImmediate { + from_struct_name: Ident, + from_field_path: Vec, + to_struct_name: Ident, + to_field_path: Vec, + }, +} + +/// Array index info for stack frame offset computation. +/// Supports expression indices (e.g., `CpiAccountIndex::User`) and +/// optional inner field access (e.g., `.is_writable`). +struct ArrayIndexInfo { + /// The array field name on the struct (for type alias lookup). + array_field_name: Ident, + /// The index expression (e.g., `0`, `CpiAccountIndex::User`). + index_expr: syn::Expr, + /// Optional inner field path after the array index (e.g., `is_writable`). + inner_field_path: Option, +} + +struct ConstantDef { + doc: String, + name: Ident, + kind: ConstantKind, +} + +struct ConstantGroup { + doc: String, + name: Ident, + prefix: Option, + constants: Vec, +} + +impl Parse for ConstantGroup { + fn parse(input: ParseStream) -> syn::Result { + // Parse doc comments for the module. + let attrs = input.call(syn::Attribute::parse_outer)?; + let doc = extract_doc_comment(&attrs) + .ok_or_else(|| input.error("Module must have a doc comment"))?; + + // Validate group doc comment. + if let Err(e) = validate_doc_comment(&doc) { + return Err(input.error(format!("Group doc comment: {}", e))); + } + + // Parse group name. + let name: Ident = input.parse()?; + + // Parse module body. + let content; + braced!(content in input); + + // Parse optional header parameter: prefix = "..." + let prefix = parse_group_params(&content)?; + + // Parse constants. + let mut constants = Vec::new(); + while !content.is_empty() { + let const_attrs = content.call(syn::Attribute::parse_outer)?; + let const_doc = extract_doc_comment(&const_attrs) + .ok_or_else(|| content.error("Constant must have a doc comment"))?; + + // Validate constant doc comment. + if let Err(e) = validate_doc_comment(&const_doc) { + return Err(content.error(e)); + } + + let ident: Ident = content.parse()?; + + // pubkey_offset!(NAME, Struct.field) expands to 4 chunk constants. + if ident == "pubkey_offset" { + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let mut field_path = Vec::new(); + while inner.peek(Token![.]) { + inner.parse::()?; + field_path.push(inner.parse::()?); + } + if field_path.is_empty() { + return Err(inner.error("Expected at least one field after struct name")); + } + let _ = content.parse::(); + + let base_doc = const_doc.trim_end_matches('.'); + for chunk in 0..4usize { + let chunk_name = + Ident::new(&format!("{}_OFF_{}", base_name, chunk), base_name.span()); + let chunk_doc = format!("{} (chunk index {}).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&chunk_doc) { + return Err(content.error(e)); + } + constants.push(ConstantDef { + doc: chunk_doc, + name: chunk_name, + kind: ConstantKind::PubkeyChunkOffset { + struct_name: struct_name.clone(), + field_path: field_path.clone(), + chunk_index: chunk, + }, + }); + } + continue; + } + + // stack_frame_pubkey_offset_unaligned!(NAME, Struct.field) expands to 4 chunk constants. + if ident == "stack_frame_pubkey_offset_unaligned" { + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let _ = content.parse::(); + + let base_doc = const_doc.trim_end_matches('.'); + for chunk in 0..4usize { + let chunk_name = + Ident::new(&format!("{}_UOFF_{}", base_name, chunk), base_name.span()); + let chunk_doc = format!("{} (chunk index {}).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&chunk_doc) { + return Err(content.error(e)); + } + constants.push(ConstantDef { + doc: chunk_doc, + name: chunk_name, + kind: ConstantKind::StackFramePubkeyChunkOffset { + struct_name: struct_name.clone(), + field_path_tokens: parsed.field_path_tokens.clone(), + array_index: parsed.array_index.as_ref().map(|a| ArrayIndexInfo { + array_field_name: a.array_field_name.clone(), + index_expr: a.index_expr.clone(), + inner_field_path: a.inner_field_path.clone(), + }), + chunk_index: chunk, + }, + }); + } + continue; + } + + // Support `offset!(NAME, Struct.field)`, `offset_immediate!(NAME, Struct.field)`, + // `relative_offset_immediate!(FROM, TO, Struct.from, Struct.to)`, + // `NAME: type = value`, and `NAME = expr as Type` forms. + let (const_name, kind) = if ident == "relative_offset_immediate" { + // relative_offset_immediate!(FROM_NAME, TO_NAME, Struct.from.path, Struct.to.path) + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let from_name: Ident = inner.parse()?; + inner.parse::()?; + let to_name: Ident = inner.parse()?; + inner.parse::()?; + let from_struct_name: Ident = inner.parse()?; + let mut from_field_path = Vec::new(); + while inner.peek(Token![.]) { + inner.parse::()?; + from_field_path.push(inner.parse::()?); + } + if from_field_path.is_empty() { + return Err(inner.error("Expected at least one field after struct name")); + } + inner.parse::()?; + let to_struct_name: Ident = inner.parse()?; + let mut to_field_path = Vec::new(); + while inner.peek(Token![.]) { + inner.parse::()?; + to_field_path.push(inner.parse::()?); + } + if to_field_path.is_empty() { + return Err(inner.error("Expected at least one field after struct name")); + } + let full_name = Ident::new( + &format!("{}_TO_{}_REL_OFF_IMM", from_name, to_name), + from_name.span(), + ); + ( + full_name, + ConstantKind::RelativeOffsetImmediate { + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + }, + ) + } else if ident == "offset" { + // offset!(NAME, Struct.field[expr].nested) + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let full_name = Ident::new(&format!("{}_OFF", base_name), base_name.span()); + ( + full_name, + ConstantKind::Offset { + struct_name, + field_path_tokens: parsed.field_path_tokens, + array_index: parsed.array_index, + }, + ) + } else if ident == "offset_immediate" { + // offset_immediate!(NAME, Struct.field[expr].nested) + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let full_name = Ident::new(&format!("{}_OFF_IMM", base_name), base_name.span()); + ( + full_name, + ConstantKind::OffsetImmediate { + struct_name, + field_path_tokens: parsed.field_path_tokens, + array_index: parsed.array_index, + }, + ) + } else if ident == "stack_frame_offset" || ident == "stack_frame_offset_unaligned" { + let aligned = ident == "stack_frame_offset"; + let suffix = if aligned { "_OFF" } else { "_UOFF" }; + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let full_name = Ident::new(&format!("{}{}", base_name, suffix), base_name.span()); + ( + full_name, + ConstantKind::StackFrameOffset { + struct_name, + field_path_tokens: parsed.field_path_tokens, + array_index: parsed.array_index, + aligned, + }, + ) + } else if content.peek(Token![:]) { + // NAME: type = value + content.parse::()?; + let ty: syn::Type = content.parse()?; + content.parse::()?; + + // Try literal first to preserve hex/binary representation. + let fork = content.fork(); + if let Ok(lit) = fork.parse::() { + content.advance_to(&fork); + let repr = lit.to_string(); + let expr = syn::Expr::Lit(syn::ExprLit { + attrs: vec![], + lit: Lit::Int(lit), + }); + ( + ident, + ConstantKind::Value { + ty, + value: expr, + literal_repr: Some(repr), + }, + ) + } else { + let expr: syn::Expr = content.parse()?; + ( + ident, + ConstantKind::Value { + ty, + value: expr, + literal_repr: None, + }, + ) + } + } else { + // NAME = expr (type inferred from `as Type` cast). + content.parse::()?; + let expr: syn::Expr = content.parse()?; + + let ty = if let syn::Expr::Cast(cast) = &expr { + (*cast.ty).clone() + } else { + return Err(content.error( + "Expression must include `as Type` when type annotation is omitted", + )); + }; + + ( + ident, + ConstantKind::Value { + ty, + value: expr, + literal_repr: None, + }, + ) + }; + + // Optional trailing comma. + let _ = content.parse::(); + + constants.push(ConstantDef { + doc: const_doc, + name: const_name, + kind, + }); + } + + // Reject multiple groups in a single macro invocation. + if !input.is_empty() { + return Err(input.error( + "Only one constant group per macro invocation; use separate constant_group! calls", + )); + } + + Ok(ConstantGroup { + doc, + name, + prefix, + constants, + }) + } +} + +/// Macro for defining groups of constants shared between Rust and ASM. +/// +/// Values are validated at compile time to fit within i32 range (sBPF immediate constraint). +/// The prefix is automatically joined with an underscore. +/// +/// Two syntaxes are supported: +/// - `NAME: type = value` — explicit type, literal values preserve hex/binary in ASM. +/// - `NAME = expr as Type` — type inferred from `as` cast, value computed at build time. +/// +/// # Example +/// ```ignore +/// constant_group! { +/// /// Input buffer layout. +/// input_buffer { +/// /// Number of accounts expected. +/// N_ACCOUNTS: u64 = 2, +/// /// Expected data length of system program account. +/// SYSTEM_PROGRAM_DATA_LEN = b"system_program".len() as u64, +/// } +/// } +/// ``` +/// +/// To extend a group with ASM-only constants, use `extend_constant_group!`. +#[proc_macro] +pub fn constant_group(input: TokenStream) -> TokenStream { + let group = parse_macro_input!(input as ConstantGroup); + + let mod_name = &group.name; + let max_line_len = MAX_LINE_LEN; + let header = asm_header(&group.doc); + + // Generate Rust constants with i32 bounds checking for ASM compatibility. + let mut const_defs = Vec::new(); + let mut const_value_strs: Vec> = Vec::new(); + + for c in &group.constants { + let name = &c.name; + let doc = &c.doc; + + match &c.kind { + ConstantKind::Value { + ty, + value, + literal_repr, + } => { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS_I32", name), name.span()); + const_value_strs.push(literal_repr.clone()); + + if literal_repr.is_some() { + // Literal value - no scope wrapper needed. + const_defs.push(quote! { + #[doc = #doc] + pub const #name: #ty = #value; + + const #assert_name: () = assert!( + (#value as i64) >= (i32::MIN as i64) && (#value as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + } else { + // Expression value - use super::* for scope access. + const_defs.push(quote! { + #[doc = #doc] + pub const #name: #ty = { use super::*; #value }; + + const #assert_name: () = assert!( + ({ use super::*; #value } as i64) >= (i32::MIN as i64) + && ({ use super::*; #value } as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + } + } + ConstantKind::Offset { + struct_name, + field_path_tokens, + array_index, + } => { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + const_value_strs.push(None); + let offset_expr = gen_offset_expr(struct_name, field_path_tokens, array_index); + + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i16 = { use super::*; (#offset_expr) as i16 }; + + const #assert_name: () = assert!( + { use super::*; #offset_expr } >= (i16::MIN as i64) + && { use super::*; #offset_expr } <= (i16::MAX as i64), + "Offset must fit in i16 range" + ); + }); + } + ConstantKind::OffsetImmediate { + struct_name, + field_path_tokens, + array_index, + } => { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + const_value_strs.push(None); + let offset_expr = gen_offset_expr(struct_name, field_path_tokens, array_index); + + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i32 = { use super::*; (#offset_expr) as i32 }; + + const #assert_name: () = assert!( + { use super::*; #offset_expr } >= (i32::MIN as i64) + && { use super::*; #offset_expr } <= (i32::MAX as i64), + "Offset immediate must fit in i32 range" + ); + }); + } + ConstantKind::StackFrameOffset { + struct_name, + field_path_tokens, + array_index, + aligned, + } => { + let (const_def, literal_repr) = gen_stack_frame_offset_code( + name, + doc, + struct_name, + field_path_tokens, + array_index, + *aligned, + ); + const_value_strs.push(literal_repr); + const_defs.push(const_def); + } + ConstantKind::PubkeyChunkOffset { + struct_name, + field_path, + chunk_index, + } => { + const_value_strs.push(None); + const_defs.push(gen_pubkey_chunk_offset_code( + name, + doc, + struct_name, + field_path, + *chunk_index, + )); + } + ConstantKind::StackFramePubkeyChunkOffset { + struct_name, + field_path_tokens, + array_index, + chunk_index, + } => { + let (const_def, literal_repr) = gen_stack_frame_pubkey_chunk_offset_code( + name, + doc, + struct_name, + field_path_tokens, + array_index, + *chunk_index, + ); + const_value_strs.push(literal_repr); + const_defs.push(const_def); + } + ConstantKind::RelativeOffsetImmediate { + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + } => { + const_value_strs.push(None); + const_defs.push(gen_relative_offset_immediate_code( + name, + doc, + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + )); + } + } + } + + let const_names: Vec = group.constants.iter().map(|c| c.name.to_string()).collect(); + let const_idents: Vec<&Ident> = group.constants.iter().map(|c| &c.name).collect(); + let const_docs: Vec = group.constants.iter().map(|c| c.doc.clone()).collect(); + + let value_str_opts: Vec<_> = const_value_strs + .iter() + .map(|opt| match opt { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }) + .collect(); + + // Generate to_asm function signature based on whether prefix is baked in. + let to_asm_fn = if let Some(ref prefix) = group.prefix { + let name_format = quote! { format!("{}_{}", #prefix, names[i]) }; + quote! { + /// Generate ASM constants for this module. + pub fn to_asm() -> alloc::string::String { + use alloc::string::String; + use alloc::format; + + let mut result = String::from(#header); + result.push('\n'); + + let names = [#(#const_names),*]; + let computed_values: &[i64] = &[#(#const_idents as i64),*]; + let literal_values: &[Option<&str>] = &[#(#value_str_opts),*]; + let docs = [#(#const_docs),*]; + + for i in 0..names.len() { + let full_name = #name_format; + let value_str = match literal_values[i] { + Some(lit) => String::from(lit), + None => format!("{}", computed_values[i]), + }; + let inline = format!(".equ {}, {} # {}", full_name, value_str, docs[i]); + if inline.len() <= #max_line_len { + result.push_str(&inline); + } else { + result.push_str(&format!("# {}\n.equ {}, {}", docs[i], full_name, value_str)); + } + result.push('\n'); + } + + result + } + } + } else { + quote! { + /// Generate ASM constants for this module with the given prefix. + /// Prefix is automatically joined with underscore (e.g., "IB" -> "IB_NAME"). + pub fn to_asm(prefix: &str) -> alloc::string::String { + use alloc::string::String; + use alloc::format; + + let mut result = String::from(#header); + result.push('\n'); + + let names = [#(#const_names),*]; + let computed_values: &[i64] = &[#(#const_idents as i64),*]; + let literal_values: &[Option<&str>] = &[#(#value_str_opts),*]; + let docs = [#(#const_docs),*]; + + for i in 0..names.len() { + let full_name = if prefix.is_empty() { + String::from(names[i]) + } else { + format!("{}_{}", prefix, names[i]) + }; + let value_str = match literal_values[i] { + Some(lit) => String::from(lit), + None => format!("{}", computed_values[i]), + }; + let inline = format!(".equ {}, {} # {}", full_name, value_str, docs[i]); + if inline.len() <= #max_line_len { + result.push_str(&inline); + } else { + result.push_str(&format!("# {}\n.equ {}, {}", docs[i], full_name, value_str)); + } + result.push('\n'); + } + + result + } + } + }; + + let expanded = quote! { + pub mod #mod_name { + #(#const_defs)* + + #to_asm_fn + } + }; + + TokenStream::from(expanded) +} + +/// ASM-only constant (no Rust type needed). +struct AsmConstantDef { + doc: String, + name: Ident, + kind: AsmConstantKind, +} + +/// Kind of ASM constant. +enum AsmConstantKind { + /// Literal value (i32 validated) - preserves original representation (hex, etc.). + Literal(LitInt), + /// Expression value (i32 validated) - computed at runtime, shown as decimal. + Expr(syn::Expr), + /// Offset derived from struct field path (i16 validated). + /// Name gets `_OFF` suffix appended. + /// Supports optional array indexing via `___fields` companion module. + Offset { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + }, + /// Offset derived from struct field path (i32 validated). + /// Name gets `_OFF_IMM` suffix appended. For use as BPF immediate operand. + /// Supports optional array indexing via `___fields` companion module. + OffsetImmediate { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + }, + /// Negative offset from end of a stack frame struct (i16 validated). + /// Computed as `offset_of!(Struct, field) - size_of::()`. + /// Name gets `_OFF` suffix (aligned) or `_UOFF` suffix (unaligned). + /// Array element types are resolved via the `___fields` companion module + /// generated by `#[stack_frame]`. + StackFrameOffset { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + aligned: bool, + }, + /// Pubkey chunk offset (i16 validated). + /// Base offset + chunk_index * 8 for 8-byte register loads. + /// Name gets `_OFF_{chunk_index}` suffix appended. + PubkeyChunkOffset { + struct_name: Ident, + field_path: Vec, + chunk_index: usize, + }, + /// Negative stack frame offset for pubkey chunks (i16 validated). + /// Computed as `offset_of!(Struct, field) + chunk_index * 8 - size_of::()`. + /// Always unaligned. Name gets `_UOFF_{chunk_index}` suffix appended. + StackFramePubkeyChunkOffset { + struct_name: Ident, + field_path_tokens: proc_macro2::TokenStream, + array_index: Option, + chunk_index: usize, + }, + /// Chunk of a known pubkey/address constant. + /// Extracts `width` bytes at `byte_offset` from the 32-byte address. + /// Width 4 produces i32 (for LO/HI halves), width 8 produces i64 (for lddw). + PubkeyValueChunk { + expr: syn::Expr, + byte_offset: usize, + width: usize, + }, + /// Relative offset between two fields (i32 validated). + /// Computed as `offset_of!(Struct, to_field) - offset_of!(Struct, from_field)`. + /// Name is `{FROM}_TO_{TO}_REL_OFF_IMM`. + RelativeOffsetImmediate { + from_struct_name: Ident, + from_field_path: Vec, + to_struct_name: Ident, + to_field_path: Vec, + }, +} + +/// Input for asm_constant_group! macro. +struct AsmConstantGroupInput { + doc: String, + name: Ident, + prefix: Option, + constants: Vec, +} + +/// Parse a dot-separated field path for `offset_of!`. +/// +/// Expects tokens like `.field` or `.field1.field2.field3`. +/// Returns a `TokenStream` suitable for use inside `offset_of!(Type, )`. +fn parse_field_path(inner: ParseStream) -> syn::Result { + let mut tokens = proc_macro2::TokenStream::new(); + let mut has_field = false; + + while inner.peek(Token![.]) { + inner.parse::()?; + let field: Ident = inner.parse()?; + if has_field { + tokens.extend(quote! { . #field }); + } else { + tokens.extend(quote! { #field }); + has_field = true; + } + } + + if !has_field { + return Err(inner.error("Expected at least one field in path")); + } + + Ok(tokens) +} + +/// Result of parsing a field path with optional array indexing. +struct IndexedFieldPath { + /// Token stream for `offset_of!(Struct, )` (fields before any bracket). + field_path_tokens: proc_macro2::TokenStream, + /// Optional array index info (bracket expression, inner field access). + array_index: Option, +} + +/// Parse a field path with optional array indexing and inner field access. +/// +/// Supports: +/// - `Struct.field` → simple field access +/// - `Struct.field[expr]` → array element access +/// - `Struct.field[expr].inner` → array element + inner field access +/// - `Struct.a.b.c` → nested field access (no array) +/// +/// Array element types are resolved via `___fields::` type aliases +/// generated by `#[stack_frame]` or `#[array_fields]`. +fn parse_indexed_field_path(inner: ParseStream) -> syn::Result { + let mut fields: Vec = Vec::new(); + let mut tokens = proc_macro2::TokenStream::new(); + + // Parse dot-separated field segments until we hit a bracket or end. + while inner.peek(Token![.]) { + inner.parse::()?; + let field: Ident = inner.parse()?; + + if !fields.is_empty() { + tokens.extend(quote! { . }); + } + tokens.extend(quote! { #field }); + fields.push(field); + + // Check for array index after this field. + if inner.peek(syn::token::Bracket) { + let bracket_content; + bracketed!(bracket_content in inner); + let index_expr: syn::Expr = bracket_content.parse()?; + + // Parse optional inner field path. + let inner_field_path = if inner.peek(Token![.]) { + Some(parse_field_path(inner)?) + } else { + None + }; + + let array_field_name = fields.last().unwrap().clone(); + + return Ok(IndexedFieldPath { + field_path_tokens: tokens, + array_index: Some(ArrayIndexInfo { + array_field_name, + index_expr, + inner_field_path, + }), + }); + } + } + + if fields.is_empty() { + return Err(inner.error("Expected at least one field in path")); + } + + Ok(IndexedFieldPath { + field_path_tokens: tokens, + array_index: None, + }) +} + +/// Extract the last path segment name from a type (for auto-generating constant names). +fn extract_type_name(ty: &syn::Type) -> Option { + if let syn::Type::Path(type_path) = ty { + type_path + .path + .segments + .last() + .map(|seg| seg.ident.to_string()) + } else { + None + } +} + +/// Convert a type name to UPPER_SNAKE_CASE for ASM constants. +/// +/// Uses `convert_case` but fixes primitive types like `u8`, `i16` etc. +/// where the default conversion inserts an unwanted underscore (`U_8` → `U8`). +fn type_name_to_upper_snake(name: &str) -> String { + let s = name.to_case(Case::UpperSnake); + // Primitive types: single letter + digits (e.g. U_8, I_16, F_32). + // Remove the underscore between the letter and digits. + if s.starts_with(|c: char| c.is_ascii_uppercase()) + && s.as_bytes().get(1) == Some(&b'_') + && s[2..].chars().all(|c| c.is_ascii_digit()) + { + format!("{}{}", &s[..1], &s[2..]) + } else { + s + } +} + +/// Parse optional `prefix = "..."` group header parameter. +fn parse_group_params(content: ParseStream) -> syn::Result> { + // Check for prefix = "..." before constants. + if content.peek(Ident) { + let fork = content.fork(); + let ident: Ident = fork.parse()?; + if ident == "prefix" && fork.peek(Token![=]) { + content.parse::()?; + content.parse::()?; + let prefix_lit: syn::LitStr = content.parse()?; + content.parse::()?; + return Ok(Some(prefix_lit.value())); + } + } + Ok(None) +} + +/// Parse ASM constants (shared between asm_constant_group! and extend_constant_group!). +fn parse_asm_constants(content: ParseStream) -> syn::Result> { + let mut constants = Vec::new(); + while !content.is_empty() { + let const_attrs = content.call(syn::Attribute::parse_outer)?; + let const_doc = extract_doc_comment(&const_attrs) + .ok_or_else(|| content.error("Constant must have a doc comment"))?; + + if let Err(e) = validate_doc_comment(&const_doc) { + return Err(content.error(e)); + } + + // Check for identifier. + let lookahead = content.lookahead1(); + if !lookahead.peek(Ident) { + return Err(lookahead.error()); + } + let ident: Ident = content.parse()?; + + // pubkey_offset!(NAME, Struct.field) expands to 4 chunk constants. + if ident == "pubkey_offset" { + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let mut field_path = Vec::new(); + while inner.peek(Token![.]) { + inner.parse::()?; + field_path.push(inner.parse::()?); + } + if field_path.is_empty() { + return Err(inner.error("Expected at least one field after struct name")); + } + let _ = content.parse::(); + + let base_doc = const_doc.trim_end_matches('.'); + for chunk in 0..4usize { + let chunk_name = + Ident::new(&format!("{}_OFF_{}", base_name, chunk), base_name.span()); + let chunk_doc = format!("{} (chunk index {}).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&chunk_doc) { + return Err(content.error(e)); + } + constants.push(AsmConstantDef { + doc: chunk_doc, + name: chunk_name, + kind: AsmConstantKind::PubkeyChunkOffset { + struct_name: struct_name.clone(), + field_path: field_path.clone(), + chunk_index: chunk, + }, + }); + } + continue; + } + + // stack_frame_pubkey_offset_unaligned!(NAME, Struct.field) expands to 4 chunk constants. + if ident == "stack_frame_pubkey_offset_unaligned" { + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let _ = content.parse::(); + + let base_doc = const_doc.trim_end_matches('.'); + for chunk in 0..4usize { + let chunk_name = + Ident::new(&format!("{}_UOFF_{}", base_name, chunk), base_name.span()); + let chunk_doc = format!("{} (chunk index {}).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&chunk_doc) { + return Err(content.error(e)); + } + constants.push(AsmConstantDef { + doc: chunk_doc, + name: chunk_name, + kind: AsmConstantKind::StackFramePubkeyChunkOffset { + struct_name: struct_name.clone(), + field_path_tokens: parsed.field_path_tokens.clone(), + array_index: parsed.array_index.as_ref().map(|a| ArrayIndexInfo { + array_field_name: a.array_field_name.clone(), + index_expr: a.index_expr.clone(), + inner_field_path: a.inner_field_path.clone(), + }), + chunk_index: chunk, + }, + }); + } + continue; + } + + // pubkey_value!(NAME, expr) expands to 8 chunk constants (4 LO/HI pairs). + if ident == "pubkey_value" { + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let expr: syn::Expr = inner.parse()?; + let _ = content.parse::(); + + let base_doc = const_doc.trim_end_matches('.'); + for chunk in 0..4usize { + let byte_base = chunk * 8; + + // Full i64 chunk (for lddw). + let full_name = + Ident::new(&format!("{}_CHUNK_{}", base_name, chunk), base_name.span()); + let full_doc = format!("{} (chunk {}).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&full_doc) { + return Err(content.error(e)); + } + constants.push(AsmConstantDef { + doc: full_doc, + name: full_name, + kind: AsmConstantKind::PubkeyValueChunk { + expr: expr.clone(), + byte_offset: byte_base, + width: 8, + }, + }); + + // Low i32 half (for jne reg, imm32 when sign-extension is safe). + let lo_name = Ident::new( + &format!("{}_CHUNK_{}_LO", base_name, chunk), + base_name.span(), + ); + let lo_doc = format!("{} (chunk {} lo).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&lo_doc) { + return Err(content.error(e)); + } + constants.push(AsmConstantDef { + doc: lo_doc, + name: lo_name, + kind: AsmConstantKind::PubkeyValueChunk { + expr: expr.clone(), + byte_offset: byte_base, + width: 4, + }, + }); + + // High i32 half (to check if lddw can be avoided). + let hi_name = Ident::new( + &format!("{}_CHUNK_{}_HI", base_name, chunk), + base_name.span(), + ); + let hi_doc = format!("{} (chunk {} hi).", base_doc, chunk); + if let Err(e) = validate_doc_comment(&hi_doc) { + return Err(content.error(e)); + } + constants.push(AsmConstantDef { + doc: hi_doc, + name: hi_name, + kind: AsmConstantKind::PubkeyValueChunk { + expr: expr.clone(), + byte_offset: byte_base + 4, + width: 4, + }, + }); + } + continue; + } + + let (const_name, kind) = if ident == "relative_offset_immediate" { + // relative_offset_immediate!(FROM_NAME, TO_NAME, Struct.from.path, Struct.to.path) + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let from_name: Ident = inner.parse()?; + inner.parse::()?; + let to_name: Ident = inner.parse()?; + inner.parse::()?; + let from_struct_name: Ident = inner.parse()?; + let mut from_field_path = Vec::new(); + while inner.peek(Token![.]) { + inner.parse::()?; + from_field_path.push(inner.parse::()?); + } + if from_field_path.is_empty() { + return Err(inner.error("Expected at least one field after struct name")); + } + inner.parse::()?; + let to_struct_name: Ident = inner.parse()?; + let mut to_field_path = Vec::new(); + while inner.peek(Token![.]) { + inner.parse::()?; + to_field_path.push(inner.parse::()?); + } + if to_field_path.is_empty() { + return Err(inner.error("Expected at least one field after struct name")); + } + let full_name = Ident::new( + &format!("{}_TO_{}_REL_OFF_IMM", from_name, to_name), + from_name.span(), + ); + ( + full_name, + AsmConstantKind::RelativeOffsetImmediate { + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + }, + ) + } else if ident == "offset" { + // Parse offset!(NAME, Struct.field[expr].nested) + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let full_name = Ident::new(&format!("{}_OFF", base_name), base_name.span()); + ( + full_name, + AsmConstantKind::Offset { + struct_name, + field_path_tokens: parsed.field_path_tokens, + array_index: parsed.array_index, + }, + ) + } else if ident == "offset_immediate" { + // Parse offset_immediate!(NAME, Struct.field[expr].nested) + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let full_name = Ident::new(&format!("{}_OFF_IMM", base_name), base_name.span()); + ( + full_name, + AsmConstantKind::OffsetImmediate { + struct_name, + field_path_tokens: parsed.field_path_tokens, + array_index: parsed.array_index, + }, + ) + } else if ident == "stack_frame_offset" || ident == "stack_frame_offset_unaligned" { + let aligned = ident == "stack_frame_offset"; + let suffix = if aligned { "_OFF" } else { "_UOFF" }; + content.parse::()?; + let inner; + syn::parenthesized!(inner in content); + let base_name: Ident = inner.parse()?; + inner.parse::()?; + let struct_name: Ident = inner.parse()?; + let parsed = parse_indexed_field_path(&inner)?; + let full_name = Ident::new(&format!("{}{}", base_name, suffix), base_name.span()); + ( + full_name, + AsmConstantKind::StackFrameOffset { + struct_name, + field_path_tokens: parsed.field_path_tokens, + array_index: parsed.array_index, + aligned, + }, + ) + } else { + // Regular NAME = value syntax. + content.parse::()?; + // Try to parse as literal first (to preserve hex/binary representation), + // otherwise parse as expression (for constants like NON_DUP_MARKER). + let fork = content.fork(); + if let Ok(lit) = fork.parse::() { + content.advance_to(&fork); + (ident, AsmConstantKind::Literal(lit)) + } else { + let expr: syn::Expr = content.parse()?; + (ident, AsmConstantKind::Expr(expr)) + } + }; + + // Optional trailing comma. + let _ = content.parse::(); + + constants.push(AsmConstantDef { + doc: const_doc, + name: const_name, + kind, + }); + } + Ok(constants) +} + +impl Parse for AsmConstantGroupInput { + fn parse(input: ParseStream) -> syn::Result { + // Parse doc comment for the group. + let attrs = input.call(syn::Attribute::parse_outer)?; + let doc = extract_doc_comment(&attrs) + .ok_or_else(|| input.error("Group must have a doc comment"))?; + + if let Err(e) = validate_doc_comment(&doc) { + return Err(input.error(format!("Group doc comment: {}", e))); + } + + // Parse group name. + let name: Ident = input.parse()?; + + // Parse body. + let content; + braced!(content in input); + + // Parse optional header parameter: prefix = "..." + let prefix = parse_group_params(&content)?; + + // Parse constants. + let constants = parse_asm_constants(&content)?; + + // Reject multiple groups. + if !input.is_empty() { + return Err(input.error("Only one group per macro invocation")); + } + + Ok(AsmConstantGroupInput { + doc, + name, + prefix, + constants, + }) + } +} + +/// Generate an offset expression for a struct field with optional array indexing. +/// +/// Without array index: `offset_of!(super::Struct, field) as i64`. +/// With array index: adds `+ index * Struct::__FIELD_STRIDE` using the hidden +/// associated constant generated by `#[array_fields]`. +fn gen_offset_expr( + struct_name: &Ident, + field_path_tokens: &proc_macro2::TokenStream, + array_index: &Option, +) -> proc_macro2::TokenStream { + match array_index { + Some(info) => { + let array_field_name = &info.array_field_name; + let index_expr = &info.index_expr; + let stride_const = Ident::new( + &format!("__{}_STRIDE", array_field_name.to_string().to_uppercase()), + array_field_name.span(), + ); + quote! { + core::mem::offset_of!(super::#struct_name, #field_path_tokens) as i64 + + (#index_expr) as i64 * (super::#struct_name::#stride_const) as i64 + } + } + None => quote! { + core::mem::offset_of!(super::#struct_name, #field_path_tokens) as i64 + }, + } +} + +/// Generate code for a `stack_frame_offset!` constant. +/// +/// Returns `(const_def_tokens, literal_repr)` where literal_repr is always `None` +/// (offsets are always computed). +fn gen_stack_frame_offset_code( + name: &Ident, + doc: &str, + struct_name: &Ident, + field_path_tokens: &proc_macro2::TokenStream, + array_index: &Option, + aligned: bool, +) -> (proc_macro2::TokenStream, Option) { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + let fields_mod = Ident::new(&format!("__{}_fields", struct_name), struct_name.span()); + + let offset_expr = match array_index { + Some(info) => { + let array_field_name = &info.array_field_name; + let index_expr = &info.index_expr; + let inner_offset = match &info.inner_field_path { + Some(path) => quote! { + + core::mem::offset_of!(#fields_mod::#array_field_name, #path) as i64 + }, + None => quote! {}, + }; + quote! { + core::mem::offset_of!(#struct_name, #field_path_tokens) as i64 + + (#index_expr) as i64 * core::mem::size_of::<#fields_mod::#array_field_name>() as i64 + #inner_offset + } + } + None => quote! { + core::mem::offset_of!(#struct_name, #field_path_tokens) as i64 + }, + }; + + let align_assertion = if aligned { + let bpf_align = BPF_ALIGN; + quote! { + assert!( + result % #bpf_align == 0, + "Stack frame offset must be aligned" + ); + } + } else { + quote! {} + }; + + let const_def = quote! { + #[doc = #doc] + pub const #name: i16 = { + use super::*; + (#offset_expr - core::mem::size_of::<#struct_name>() as i64) as i16 + }; + + const #assert_name: () = { + use super::*; + let result = #offset_expr - core::mem::size_of::<#struct_name>() as i64; + assert!( + result >= (i16::MIN as i64) && result <= (i16::MAX as i64), + "Stack frame offset must fit in i16" + ); + assert!(result < 0, "Stack frame offset must be negative"); + #align_assertion + }; + }; + + (const_def, None) +} + +/// Generate code for a `stack_frame_pubkey_offset_unaligned!` constant (single chunk). +/// +/// Computes `offset_of!(Struct, field) + chunk_index * 8 - size_of::()` as an i16 constant. +fn gen_stack_frame_pubkey_chunk_offset_code( + name: &Ident, + doc: &str, + struct_name: &Ident, + field_path_tokens: &proc_macro2::TokenStream, + array_index: &Option, + chunk_index: usize, +) -> (proc_macro2::TokenStream, Option) { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + let fields_mod = Ident::new(&format!("__{}_fields", struct_name), struct_name.span()); + let chunk_byte_offset = (chunk_index * BPF_ALIGN as usize) as i64; + + let offset_expr = match array_index { + Some(info) => { + let array_field_name = &info.array_field_name; + let index_expr = &info.index_expr; + let inner_offset = match &info.inner_field_path { + Some(path) => quote! { + + core::mem::offset_of!(#fields_mod::#array_field_name, #path) as i64 + }, + None => quote! {}, + }; + quote! { + core::mem::offset_of!(#struct_name, #field_path_tokens) as i64 + + (#index_expr) as i64 * core::mem::size_of::<#fields_mod::#array_field_name>() as i64 + #inner_offset + } + } + None => quote! { + core::mem::offset_of!(#struct_name, #field_path_tokens) as i64 + }, + }; + + let const_def = quote! { + #[doc = #doc] + pub const #name: i16 = { + use super::*; + (#offset_expr + #chunk_byte_offset - core::mem::size_of::<#struct_name>() as i64) as i16 + }; + + const #assert_name: () = { + use super::*; + let result = #offset_expr + #chunk_byte_offset - core::mem::size_of::<#struct_name>() as i64; + assert!( + result >= (i16::MIN as i64) && result <= (i16::MAX as i64), + "Stack frame offset must fit in i16" + ); + assert!(result < 0, "Stack frame offset must be negative"); + }; + }; + + (const_def, None) +} + +/// Generate code for a `pubkey_offset!` constant (single chunk). +/// +/// Computes `offset_of!(Struct, field) + chunk_index * 8` as an i16 constant. +fn gen_pubkey_chunk_offset_code( + name: &Ident, + doc: &str, + struct_name: &Ident, + field_path: &[Ident], + chunk_index: usize, +) -> proc_macro2::TokenStream { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + let chunk_byte_offset = (chunk_index * BPF_ALIGN as usize) as i64; + + quote! { + #[doc = #doc] + pub const #name: i16 = (core::mem::offset_of!(super::#struct_name, #(#field_path).*) as i64 + #chunk_byte_offset) as i16; + + const #assert_name: () = assert!( + (core::mem::offset_of!(super::#struct_name, #(#field_path).*) as i64 + #chunk_byte_offset) >= (i16::MIN as i64) + && (core::mem::offset_of!(super::#struct_name, #(#field_path).*) as i64 + #chunk_byte_offset) <= (i16::MAX as i64), + "Offset must fit in i16 range" + ); + } +} + +/// Generate code for a `relative_offset_immediate!` constant. +/// +/// Computes `offset_of!(Struct, to_field) - offset_of!(Struct, from_field)` as an i32 constant. +fn gen_relative_offset_immediate_code( + name: &Ident, + doc: &str, + from_struct_name: &Ident, + from_field_path: &[Ident], + to_struct_name: &Ident, + to_field_path: &[Ident], +) -> proc_macro2::TokenStream { + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + + quote! { + #[doc = #doc] + pub const #name: i32 = ( + core::mem::offset_of!(super::#to_struct_name, #(#to_field_path).*) as i64 + - core::mem::offset_of!(super::#from_struct_name, #(#from_field_path).*) as i64 + ) as i32; + + const #assert_name: () = assert!( + (core::mem::offset_of!(super::#to_struct_name, #(#to_field_path).*) as i64 + - core::mem::offset_of!(super::#from_struct_name, #(#from_field_path).*) as i64) >= (i32::MIN as i64) + && (core::mem::offset_of!(super::#to_struct_name, #(#to_field_path).*) as i64 + - core::mem::offset_of!(super::#from_struct_name, #(#from_field_path).*) as i64) <= (i32::MAX as i64), + "Relative offset immediate must fit in i32 range" + ); + } +} + +/// Generate code for a `pubkey_value!` constant (single chunk). +/// +/// Extracts `width` bytes at `byte_offset` from a 32-byte address constant. +/// Width 4 produces i32, width 8 produces i64. +fn gen_pubkey_value_chunk_code( + name: &Ident, + doc: &str, + expr: &syn::Expr, + byte_offset: usize, + width: usize, +) -> proc_macro2::TokenStream { + if width == 8 { + let b0 = byte_offset; + let b1 = byte_offset + 1; + let b2 = byte_offset + 2; + let b3 = byte_offset + 3; + let b4 = byte_offset + 4; + let b5 = byte_offset + 5; + let b6 = byte_offset + 6; + let b7 = byte_offset + 7; + + quote! { + #[doc = #doc] + pub const #name: i64 = { + use super::*; + let bytes: [u8; 32] = unsafe { core::mem::transmute(#expr) }; + i64::from_le_bytes([bytes[#b0], bytes[#b1], bytes[#b2], bytes[#b3], bytes[#b4], bytes[#b5], bytes[#b6], bytes[#b7]]) + }; + } + } else { + let b0 = byte_offset; + let b1 = byte_offset + 1; + let b2 = byte_offset + 2; + let b3 = byte_offset + 3; + + quote! { + #[doc = #doc] + pub const #name: i32 = { + use super::*; + let bytes: [u8; 32] = unsafe { core::mem::transmute(#expr) }; + i32::from_le_bytes([bytes[#b0], bytes[#b1], bytes[#b2], bytes[#b3]]) + }; + } + } +} + +/// Macro for defining ASM-only constant groups. +/// +/// Constants don't need types - values are `i64`, offsets are `i16`. +/// All values are validated at compile time to fit within i32 range (sBPF constraint). +/// +/// Supports two constant syntaxes: +/// - `NAME = value` - direct value (i64, validated for i32 range) +/// - `offset!(NAME, Struct.field)` - offset (i16, validated for i16 range, `_OFF` suffix added) +/// +/// # Example +/// ```ignore +/// asm_constant_group! { +/// /// Miscellaneous constants. +/// misc { +/// prefix = "M", +/// /// Data length of zero. +/// DATA_LENGTH_ZERO = 0, +/// } +/// } +/// // Creates `misc` module with: +/// // - pub const DATA_LENGTH_ZERO: i64 = 0; +/// // - to_asm() outputs ".equ M_DATA_LENGTH_ZERO, 0 # ..." +/// ``` +#[proc_macro] +pub fn asm_constant_group(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as AsmConstantGroupInput); + + let mod_name = &input.name; + let max_line_len = MAX_LINE_LEN; + let header = asm_header(&input.doc); + + // Generate constant definitions and collect info for ASM. + let mut const_defs = Vec::new(); + let mut const_names = Vec::new(); + let mut const_docs = Vec::new(); + // Track value representations: Some(literal_str) for values, None for offsets. + let mut const_value_strs: Vec> = Vec::new(); + + for c in &input.constants { + let name = &c.name; + let doc = &c.doc; + let name_str = name.to_string(); + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + + const_names.push(name_str); + const_docs.push(doc.clone()); + + match &c.kind { + AsmConstantKind::Literal(value) => { + // Preserve original literal representation (hex, binary, etc.). + const_value_strs.push(Some(value.to_string())); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i64 = #value; + + const #assert_name: () = assert!( + (#value as i64) >= (i32::MIN as i64) && (#value as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + } + AsmConstantKind::Expr(expr) => { + // Expression (e.g., constant from another crate) - computed at runtime. + // Use super::* to access imports from parent scope. + const_value_strs.push(None); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i64 = { use super::*; #expr as i64 }; + + const #assert_name: () = assert!( + ({ use super::*; #expr } as i64) >= (i32::MIN as i64) + && ({ use super::*; #expr } as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + } + AsmConstantKind::Offset { + struct_name, + field_path_tokens, + array_index, + } => { + const_value_strs.push(None); + let offset_expr = gen_offset_expr(struct_name, field_path_tokens, array_index); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i16 = { use super::*; (#offset_expr) as i16 }; + + const #assert_name: () = assert!( + { use super::*; #offset_expr } >= (i16::MIN as i64) + && { use super::*; #offset_expr } <= (i16::MAX as i64), + "Offset must fit in i16 range" + ); + }); + } + AsmConstantKind::OffsetImmediate { + struct_name, + field_path_tokens, + array_index, + } => { + const_value_strs.push(None); + let offset_expr = gen_offset_expr(struct_name, field_path_tokens, array_index); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i32 = { use super::*; (#offset_expr) as i32 }; + + const #assert_name: () = assert!( + { use super::*; #offset_expr } >= (i32::MIN as i64) + && { use super::*; #offset_expr } <= (i32::MAX as i64), + "Offset immediate must fit in i32 range" + ); + }); + } + AsmConstantKind::StackFrameOffset { + struct_name, + field_path_tokens, + array_index, + aligned, + } => { + let (const_def, literal_repr) = gen_stack_frame_offset_code( + name, + doc, + struct_name, + field_path_tokens, + array_index, + *aligned, + ); + const_value_strs.push(literal_repr); + const_defs.push(const_def); + } + AsmConstantKind::PubkeyChunkOffset { + struct_name, + field_path, + chunk_index, + } => { + const_value_strs.push(None); + const_defs.push(gen_pubkey_chunk_offset_code( + name, + doc, + struct_name, + field_path, + *chunk_index, + )); + } + AsmConstantKind::StackFramePubkeyChunkOffset { + struct_name, + field_path_tokens, + array_index, + chunk_index, + } => { + let (const_def, literal_repr) = gen_stack_frame_pubkey_chunk_offset_code( + name, + doc, + struct_name, + field_path_tokens, + array_index, + *chunk_index, + ); + const_value_strs.push(literal_repr); + const_defs.push(const_def); + } + AsmConstantKind::PubkeyValueChunk { + expr, + byte_offset, + width, + } => { + const_value_strs.push(None); + const_defs.push(gen_pubkey_value_chunk_code( + name, + doc, + expr, + *byte_offset, + *width, + )); + } + AsmConstantKind::RelativeOffsetImmediate { + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + } => { + const_value_strs.push(None); + const_defs.push(gen_relative_offset_immediate_code( + name, + doc, + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + )); + } + } + } + + let const_idents: Vec<_> = input.constants.iter().map(|c| &c.name).collect(); + + // Generate name formatting logic based on whether prefix is present. + let name_format = match &input.prefix { + Some(prefix) => quote! { format!("{}_{}", #prefix, names[i]) }, + None => quote! { String::from(names[i]) }, + }; + + // Generate value string options for preserving hex/binary literals. + let value_str_opts: Vec<_> = const_value_strs + .iter() + .map(|opt| match opt { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }) + .collect(); + + let expanded = quote! { + pub mod #mod_name { + use alloc::string::String; + use alloc::format; + + #(#const_defs)* + + /// Generate ASM constants. + pub fn to_asm() -> String { + let mut result = String::from(#header); + result.push('\n'); + + let names: &[&str] = &[#(#const_names),*]; + let computed_values: &[i64] = &[#(#const_idents as i64),*]; + let literal_values: &[Option<&str>] = &[#(#value_str_opts),*]; + let docs: &[&str] = &[#(#const_docs),*]; + + for i in 0..names.len() { + let full_name = #name_format; + // Use original literal if available, otherwise use computed value. + let value_str = match literal_values[i] { + Some(lit) => String::from(lit), + None => format!("{}", computed_values[i]), + }; + let inline = format!(".equ {}, {} # {}", full_name, value_str, docs[i]); + if inline.len() <= #max_line_len { + result.push_str(&inline); + } else { + result.push_str(&format!("# {}\n.equ {}, {}", docs[i], full_name, value_str)); + } + result.push('\n'); + } + + result + } + } + }; + + TokenStream::from(expanded) +} + +/// Input for extend_constant_group! macro. +struct ExtendConstantGroupInput { + name: Ident, + prefix: Option, + constants: Vec, +} + +impl Parse for ExtendConstantGroupInput { + fn parse(input: ParseStream) -> syn::Result { + // Parse module name. + let name: Ident = input.parse()?; + + // Parse body. + let content; + braced!(content in input); + + // Parse optional header parameter: prefix = "..." + let prefix = parse_group_params(&content)?; + + // Parse constants using shared parser. + let constants = parse_asm_constants(&content)?; + + Ok(ExtendConstantGroupInput { + name, + prefix, + constants, + }) + } +} + +/// Macro for extending a constant group with ASM-only constants. +/// +/// This creates a module that re-exports the base group's constants from +/// `crate::common::{name}` and adds ASM-only constants. The `to_asm()` function +/// combines both under one header. The prefix is automatically joined with an underscore. +/// +/// Supports two constant syntaxes: +/// - `NAME = value` - direct value (validated for i32 range) +/// - `offset!(NAME, Struct.field)` - offset (validated for i16 range, `_OFF` suffix added) +/// +/// # Example +/// ```ignore +/// extend_constant_group!(input_buffer { +/// prefix = "IB", +/// /// Offset to number of accounts field. +/// offset!(N_ACCOUNTS, InputBuffer.n_accounts), +/// }); +/// // Creates `input_buffer` module that: +/// // - Re-exports all constants from crate::common::input_buffer +/// // - Adds N_ACCOUNTS_OFF constant derived from offset_of!(InputBuffer, n_accounts) +/// // - to_asm() outputs ".equ IB_N_ACCOUNTS_OFF, 0 # ..." +/// ``` +#[proc_macro] +pub fn extend_constant_group(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ExtendConstantGroupInput); + + let mod_name = &input.name; + let max_line_len = MAX_LINE_LEN; + + // Generate constant definitions and collect info for ASM. + let mut const_defs = Vec::new(); + let mut const_names = Vec::new(); + let mut const_docs = Vec::new(); + // Track value representations: Some(literal_str) for values, None for offsets. + let mut const_value_strs: Vec> = Vec::new(); + + for c in &input.constants { + let name = &c.name; + let doc = &c.doc; + let name_str = name.to_string(); + let assert_name = Ident::new(&format!("_ASSERT_{}_FITS", name), name.span()); + + const_names.push(name_str); + const_docs.push(doc.clone()); + + match &c.kind { + AsmConstantKind::Literal(value) => { + // Preserve original literal representation (hex, binary, etc.). + const_value_strs.push(Some(value.to_string())); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i32 = #value; + + const #assert_name: () = assert!( + (#value as i64) >= (i32::MIN as i64) && (#value as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + } + AsmConstantKind::Expr(expr) => { + // Expression (e.g., constant from another crate) - computed at runtime. + // Use super::* to access imports from parent scope. + const_value_strs.push(None); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i32 = { use super::*; #expr as i32 }; + + const #assert_name: () = assert!( + ({ use super::*; #expr } as i64) >= (i32::MIN as i64) + && ({ use super::*; #expr } as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + } + AsmConstantKind::Offset { + struct_name, + field_path_tokens, + array_index, + } => { + const_value_strs.push(None); + let offset_expr = gen_offset_expr(struct_name, field_path_tokens, array_index); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i16 = { use super::*; (#offset_expr) as i16 }; + + const #assert_name: () = assert!( + { use super::*; #offset_expr } >= (i16::MIN as i64) + && { use super::*; #offset_expr } <= (i16::MAX as i64), + "Offset must fit in i16 range" + ); + }); + } + AsmConstantKind::OffsetImmediate { + struct_name, + field_path_tokens, + array_index, + } => { + const_value_strs.push(None); + let offset_expr = gen_offset_expr(struct_name, field_path_tokens, array_index); + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i32 = { use super::*; (#offset_expr) as i32 }; + + const #assert_name: () = assert!( + { use super::*; #offset_expr } >= (i32::MIN as i64) + && { use super::*; #offset_expr } <= (i32::MAX as i64), + "Offset immediate must fit in i32 range" + ); + }); + } + AsmConstantKind::StackFrameOffset { + struct_name, + field_path_tokens, + array_index, + aligned, + } => { + let (const_def, literal_repr) = gen_stack_frame_offset_code( + name, + doc, + struct_name, + field_path_tokens, + array_index, + *aligned, + ); + const_value_strs.push(literal_repr); + const_defs.push(const_def); + } + AsmConstantKind::PubkeyChunkOffset { + struct_name, + field_path, + chunk_index, + } => { + const_value_strs.push(None); + const_defs.push(gen_pubkey_chunk_offset_code( + name, + doc, + struct_name, + field_path, + *chunk_index, + )); + } + AsmConstantKind::StackFramePubkeyChunkOffset { + struct_name, + field_path_tokens, + array_index, + chunk_index, + } => { + let (const_def, literal_repr) = gen_stack_frame_pubkey_chunk_offset_code( + name, + doc, + struct_name, + field_path_tokens, + array_index, + *chunk_index, + ); + const_value_strs.push(literal_repr); + const_defs.push(const_def); + } + AsmConstantKind::PubkeyValueChunk { + expr, + byte_offset, + width, + } => { + const_value_strs.push(None); + const_defs.push(gen_pubkey_value_chunk_code( + name, + doc, + expr, + *byte_offset, + *width, + )); + } + AsmConstantKind::RelativeOffsetImmediate { + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + } => { + const_value_strs.push(None); + const_defs.push(gen_relative_offset_immediate_code( + name, + doc, + from_struct_name, + from_field_path, + to_struct_name, + to_field_path, + )); + } + } + } + + // Collect const idents for ASM output. + let const_idents: Vec<_> = input.constants.iter().map(|c| &c.name).collect(); + + // Generate name formatting logic based on whether prefix is present. + let base_prefix = match &input.prefix { + Some(prefix) => quote! { #prefix }, + None => quote! { "" }, + }; + let name_format = match &input.prefix { + Some(prefix) => quote! { format!("{}_{}", #prefix, names[i]) }, + None => quote! { String::from(names[i]) }, + }; + + // Generate value string options for preserving hex/binary literals. + let value_str_opts: Vec<_> = const_value_strs + .iter() + .map(|opt| match opt { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }) + .collect(); + + let expanded = quote! { + pub mod #mod_name { + use alloc::string::String; + use alloc::format; + + // Re-export base group's constants. + pub use crate::common::#mod_name::*; + + #(#const_defs)* + + /// Generate combined ASM (base + extension). + pub fn to_asm() -> String { + // Base group adds header and its constants. + let mut result = crate::common::#mod_name::to_asm(#base_prefix); + + // Add extension constants (no separate header). + let names: &[&str] = &[#(#const_names),*]; + let computed_values: &[i64] = &[#(#const_idents as i64),*]; + let literal_values: &[Option<&str>] = &[#(#value_str_opts),*]; + let docs: &[&str] = &[#(#const_docs),*]; + + for i in 0..names.len() { + let full_name = #name_format; + // Use original literal if available, otherwise use computed value. + let value_str = match literal_values[i] { + Some(lit) => String::from(lit), + None => format!("{}", computed_values[i]), + }; + let inline = format!(".equ {}, {} # {}", full_name, value_str, docs[i]); + if inline.len() <= #max_line_len { + result.push_str(&inline); + } else { + result.push_str(&format!("# {}\n.equ {}, {}", docs[i], full_name, value_str)); + } + result.push('\n'); + } + + result + } + } + }; + + TokenStream::from(expanded) +} + +/// Input for sizes! macro. +struct SizesInput { + types: Vec, +} + +impl Parse for SizesInput { + fn parse(input: ParseStream) -> syn::Result { + let mut types = Vec::new(); + while !input.is_empty() { + let ty: syn::Type = input.parse()?; + types.push(ty); + // Optional trailing comma. + let _ = input.parse::(); + } + Ok(SizesInput { types }) + } +} + +/// Macro for generating type size constants for ASM. +/// +/// Takes a list of types and generates `SIZE_OF_` constants. +/// Creates a `sizes` module with a `to_asm()` function for build-time ASM generation. +/// +/// # Example +/// ```ignore +/// sizes! { +/// u8, +/// SolAccountMeta, +/// Rent, +/// } +/// ``` +/// +/// Generates ASM: +/// ```text +/// # Type sizes. +/// # ----------- +/// .equ SIZE_OF_U8, 1 # Size of u8. +/// .equ SIZE_OF_SOL_ACCOUNT_META, 24 # Size of SolAccountMeta. +/// .equ SIZE_OF_RENT, 17 # Size of Rent. +/// ``` +#[proc_macro] +pub fn sizes(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as SizesInput); + + let max_line_len = MAX_LINE_LEN; + let header = asm_header("Type sizes."); + + let mut const_defs = Vec::new(); + let mut const_name_strs = Vec::new(); + let mut const_doc_strs = Vec::new(); + + for ty in &input.types { + let type_name = + extract_type_name(ty).unwrap_or_else(|| panic!("Expected a named type path in sizes!")); + let screaming = type_name_to_upper_snake(&type_name); + let name_str = format!("SIZE_OF_{}", screaming); + let name = Ident::new(&name_str, proc_macro2::Span::call_site()); + let assert_name = Ident::new( + &format!("_ASSERT_{}_FITS", name_str), + proc_macro2::Span::call_site(), + ); + let doc = format!("Size of {}.", type_name); + + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i64 = { use super::*; core::mem::size_of::<#ty>() as i64 }; + + const #assert_name: () = assert!( + ({ use super::*; core::mem::size_of::<#ty>() } as i64) >= (i32::MIN as i64) + && ({ use super::*; core::mem::size_of::<#ty>() } as i64) <= (i32::MAX as i64), + "ASM immediate must fit in i32 range" + ); + }); + + const_name_strs.push(name_str); + const_doc_strs.push(doc); + } + + let const_idents: Vec<_> = const_name_strs + .iter() + .map(|n| Ident::new(n, proc_macro2::Span::call_site())) + .collect(); + + let expanded = quote! { + pub mod sizes { + use alloc::string::String; + use alloc::format; + + #(#const_defs)* + + /// Generate ASM constants for type sizes. + pub fn to_asm() -> String { + let mut result = String::from(#header); + result.push('\n'); + + let names: &[&str] = &[#(#const_name_strs),*]; + let values: &[i64] = &[#(#const_idents as i64),*]; + let docs: &[&str] = &[#(#const_doc_strs),*]; + + for i in 0..names.len() { + let inline = format!(".equ {}, {} # {}", names[i], values[i], docs[i]); + if inline.len() <= #max_line_len { + result.push_str(&inline); + } else { + result.push_str(&format!("# {}\n.equ {}, {}", docs[i], names[i], values[i])); + } + result.push('\n'); + } + + result + } + } + }; + + TokenStream::from(expanded) +} + +/// Macro for generating pubkey chunking offset constants. +/// +/// A pubkey (Address) is 32 bytes. For 8-byte register loads, it is +/// accessed in 4 chunks of 8 bytes each. This macro generates a +/// `pubkey_chunk` module with `OFF_0` through `OFF_3` constants +/// and a `to_asm()` function. +/// +/// # Example +/// ```ignore +/// pubkey_chunk_group!(); +/// ``` +/// +/// Generates ASM: +/// ```text +/// # Pubkey chunking offsets. +/// # ------------------------ +/// .equ PUBKEY_CHUNK_OFF_0, 0 # Offset for the first 8 bytes. +/// .equ PUBKEY_CHUNK_OFF_1, 8 # Offset for the second 8 bytes. +/// .equ PUBKEY_CHUNK_OFF_2, 16 # Offset for the third 8 bytes. +/// .equ PUBKEY_CHUNK_OFF_3, 24 # Offset for the fourth 8 bytes. +/// ``` +#[proc_macro] +pub fn pubkey_chunk_group(_input: TokenStream) -> TokenStream { + const PUBKEY_SIZE: usize = 32; + const CHUNK_SIZE: usize = BPF_ALIGN as usize; + const N_CHUNKS: usize = PUBKEY_SIZE / CHUNK_SIZE; + const ORDINALS: [&str; N_CHUNKS] = ["first", "second", "third", "fourth"]; + + let max_line_len = MAX_LINE_LEN; + let header = asm_header("Pubkey chunking offsets."); + + let mut const_defs = Vec::new(); + let mut const_name_strs = Vec::new(); + let mut const_doc_strs = Vec::new(); + + for (i, ordinal) in ORDINALS.iter().enumerate() { + let offset = (i * CHUNK_SIZE) as i64; + let name_str = format!("PUBKEY_CHUNK_OFF_{}", i); + let name = Ident::new(&format!("OFF_{}", i), proc_macro2::Span::call_site()); + let doc = format!("Offset for the {} 8 bytes.", ordinal); + + const_defs.push(quote! { + #[doc = #doc] + pub const #name: i64 = #offset; + }); + + const_name_strs.push(name_str); + const_doc_strs.push(doc); + } + + let const_idents: Vec<_> = (0..N_CHUNKS) + .map(|i| Ident::new(&format!("OFF_{}", i), proc_macro2::Span::call_site())) + .collect(); + + let expanded = quote! { + pub mod pubkey_chunk { + use alloc::string::String; + use alloc::format; + + #(#const_defs)* + + /// Generate ASM constants for pubkey chunking offsets. + pub fn to_asm() -> String { + let mut result = String::from(#header); + result.push('\n'); + + let names: &[&str] = &[#(#const_name_strs),*]; + let values: &[i64] = &[#(#const_idents as i64),*]; + let docs: &[&str] = &[#(#const_doc_strs),*]; + + for i in 0..names.len() { + let inline = format!(".equ {}, {} # {}", names[i], values[i], docs[i]); + if inline.len() <= #max_line_len { + result.push_str(&inline); + } else { + result.push_str(&format!("# {}\n.equ {}, {}", docs[i], names[i], values[i])); + } + result.push('\n'); + } + + result + } + } + }; + + TokenStream::from(expanded) +} + +/// Attribute macro for stack frame structs. +/// +/// Adds `#[repr(C, align(8))]` to ensure C layout and 8-byte alignment. +/// Any existing `#[repr(...)]` attributes are removed. +/// +/// Also generates a companion module `___fields` containing type aliases +/// for each array field's element type. This allows `stack_frame_offset!` to resolve +/// element types without requiring them in the syntax. +/// +/// # Example +/// ```ignore +/// #[stack_frame] +/// struct InitStackFrame { +/// data: [u8; 32], +/// metas: [SolAccountMeta; 2], +/// } +/// ``` +/// +/// Generates: +/// ```ignore +/// #[repr(C, align(8))] +/// struct InitStackFrame { ... } +/// +/// mod __InitStackFrame_fields { +/// use super::*; +/// pub type metas = SolAccountMeta; +/// pub type data = u8; +/// } +/// ``` +/// Generate the `___fields` companion module containing type +/// aliases for each array field's element type. +fn gen_fields_module(input: &syn::ItemStruct) -> proc_macro2::TokenStream { + let struct_name = &input.ident; + let fields_mod = Ident::new(&format!("__{}_fields", struct_name), struct_name.span()); + + let mut type_aliases = Vec::new(); + if let syn::Fields::Named(ref fields) = input.fields { + for field in &fields.named { + if let Some(ref field_name) = field.ident { + if let syn::Type::Array(array_type) = &field.ty { + let elem_type = &*array_type.elem; + type_aliases.push(quote! { + #[allow(non_camel_case_types)] + pub type #field_name = #elem_type; + }); + } + } + } + } + + quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + pub mod #fields_mod { + use super::*; + #(#type_aliases)* + } + } +} + +#[proc_macro_attribute] +pub fn stack_frame(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(item as syn::ItemStruct); + + // Remove existing repr attributes to avoid conflicts. + input.attrs.retain(|attr| !attr.path().is_ident("repr")); + + let fields_mod = gen_fields_module(&input); + + let expanded = quote! { + #[repr(C, align(8))] + #input + + #fields_mod + }; + + TokenStream::from(expanded) +} + +/// Attribute macro that generates hidden associated constants for array field +/// element sizes. +/// +/// Enables `offset!` and `offset_immediate!` to resolve element sizes when +/// using array index syntax (e.g., `Struct.field[expr]`). +/// +/// Unlike `#[stack_frame]`, this does not modify the struct's `#[repr]`. +/// +/// # Example +/// ```ignore +/// #[array_fields] +/// #[repr(C, packed)] +/// struct TreeNode { +/// child: [*mut TreeNode; 2], +/// key: u16, +/// } +/// ``` +/// +/// Generates: +/// ```ignore +/// impl TreeNode { +/// const __CHILD_STRIDE: usize = core::mem::size_of::<*mut TreeNode>(); +/// } +/// ``` +#[proc_macro_attribute] +pub fn array_fields(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as syn::ItemStruct); + let struct_name = &input.ident; + + let mut stride_consts = Vec::new(); + if let syn::Fields::Named(ref fields) = input.fields { + for field in &fields.named { + if let Some(ref field_name) = field.ident { + if let syn::Type::Array(array_type) = &field.ty { + let elem_type = &*array_type.elem; + let const_name = Ident::new( + &format!("__{}_STRIDE", field_name.to_string().to_uppercase()), + field_name.span(), + ); + stride_consts.push(quote! { + #[doc(hidden)] + pub const #const_name: usize = core::mem::size_of::<#elem_type>(); + }); + } + } + } + } + + let expanded = quote! { + #input + + #[doc(hidden)] + #[allow(dead_code)] + impl #struct_name { + #(#stride_consts)* + } + }; + + TokenStream::from(expanded) +} diff --git a/examples/tree/specs/cu-overhead-analysis.md b/examples/tree/specs/cu-overhead-analysis.md new file mode 100644 index 00000000..40afc584 --- /dev/null +++ b/examples/tree/specs/cu-overhead-analysis.md @@ -0,0 +1,104 @@ + + +# CU overhead analysis + +Methodology for attributing CU overhead in Rust implementations +relative to hand-written assembly. Produces actionable +optimization targets. + +## Inputs + +Each analysis requires: + +- ASM snippet for the section under review (from + `artifacts/tests/*/snippets/`). +- RS disassembly snippet for the same section (from + `artifacts/rs-disassembly.s`). +- CU table from `artifacts/tests/*/result.txt`. +- Relevant algorithm spec for context (from `specs/`). + +## Workflow + +### Step 1 — Section isolation + +Pick one ANCHOR section. Work on one section at a time. +Identify the corresponding ASM and RS code blocks. + +### Step 2 — Instruction count + +Count executed instructions per test case path for both ASM +and RS. Record the delta (RS - ASM) for each test case. + +### Step 3 — Structural alignment + +Walk both snippets in parallel, marking each instruction as: + +- **Matched.** Same operation, same position. +- **Reordered.** Same operation, different position. +- **Extra (RS).** Present in RS but not ASM. +- **Extra (ASM).** Present in ASM but not RS. +- **Divergent.** Different operation at corresponding position. + +### Step 4 — Overhead classification + +Classify every extra RS instruction into a category: + +| Category | Code | Description | +| ---------------- | ---- | ----------------------------------------------- | +| Redundant branch | RB | Null check or conditional that ASM avoids. | +| Macrodup. | MD | Instruction duplicated across macro exp. sites. | +| Register reload | RR | Value loaded from memory that ASM keeps in reg. | +| Register spill | RS | Stack spill/reload from register pressure. | +| Jump overhead | JO | Extra branch to reach shared code. | +| Compiler art. | CA | No logical purpose (nop, identity move). | + +### Step 5 — Per-test CU attribution + +For each test case, trace the executed path and attribute the +CU delta to specific extra instructions with their category +codes. + +### Step 6 — Pattern aggregation + +Aggregate across all test cases: + +- Total extra CUs per category. +- Percentage of total overhead per category. +- Reducibility assessment (can this overhead be eliminated + through Rust source changes?). + +### Step 7 — Optimization target ranking + +Rank by reducible CUs (highest first). Each target becomes a +work item for Engineering. + +## Deliverable format + +Each analysis produces a document at +`examples//specs/cu-analysis-
.md` containing: + +- Summary table (section, test count, mean/min/max overhead). +- Per-test attribution tables. +- Pattern aggregation table. +- Optimization targets (prioritized). +- Engineering recommendations. + +## Overhead baselines + +- **Target:** \<=10% for hot paths. +- **Acceptable:** 10-20% for cold paths. +- **Needs work:** >20%. + +## Relationship to parity workflow + +Analysis runs first to identify WHERE overhead comes from and +WHY. The parity optimization loop (see `parity-workflow.md`) +then drives improvements on the identified targets. After +Engineering implements changes, Analysis re-measures to confirm +overhead reduction. + +```text +Analysis identifies overhead → Spec documents targets → +Engineering optimizes Rust → Build + test → Analysis +re-measures → QA verifies → repeat until baseline met +``` diff --git a/examples/tree/specs/insert-fixup-hardcoded-branches.md b/examples/tree/specs/insert-fixup-hardcoded-branches.md new file mode 100644 index 00000000..274edd30 --- /dev/null +++ b/examples/tree/specs/insert-fixup-hardcoded-branches.md @@ -0,0 +1,249 @@ +# Insert fixup hardcoded branches + +## Scope + +Replace computed `dir`/`opposite(dir)` indexing in the insert fixup +loop of `program.rs` with explicit `dir_l`/`dir_r` branches that +use hardcoded `child[DIR_L]` and `child[DIR_R]` accesses. This +matches the assembly, where cases 5/6 are expanded into separate +`dir_l` and `dir_r` labels with constant offsets. + +## Problem + +The current Rust code computes `dir` at runtime via `direction()` +and indexes children with `child[dir]` / `child[opposite(dir)]`. +The compiler cannot resolve these to constant offsets, so every +child access compiles to a shift-add-load sequence (~3 instructions) +instead of a single load with a constant offset. This causes: + +- ~14 extra ALU instructions for computed indexing. +- 3 stack spills + 3 reloads from register pressure. +- 2 extra instructions for `direction()` (pointer comparison + + bool materialization vs. direct branch). + +Total overhead: ~18 instructions (42%) vs. the hand-written +assembly on the case 5+6 critical path. + +## Approach + +### Direction check + +Replace `direction()` + runtime `dir` variable with an early branch +on `parent == (*grandparent).child[DIR_L]`. Each arm has fully +hardcoded child accesses. The `direction()` and `opposite()` +functions become unused and are removed. + +### Uncle computation + +Move inside each branch with a hardcoded child index: + +```rust +if parent == (*grandparent).child[tree::DIR_L] { + let uncle = (*grandparent).child[tree::DIR_R]; + ... +} else { + let uncle = (*grandparent).child[tree::DIR_L]; + ... +} +``` + +### Case 5 redundant load + +The current code loads `(*parent).child[opposite(dir)]` twice: +once for the case 5 check and once as `new_root`. Hoist into a +local: + +```rust +let pivot = (*parent).child[tree::DIR_R]; // dir_l variant +if node == pivot { + // pivot is new_root — no second load +} +``` + +### Case 5/6 bodies + +Identical to the current inline expansions from +`rotate-subtree-specialization.md`, but with every `child[dir]` +and `child[opposite(dir)]` replaced by `child[DIR_L]` or +`child[DIR_R]`. + +### Case 2 and recolor + +Case 2 (red uncle recolor) and the case 6 recolor + return are +shared across both branches. Case 2 is placed after the +`dir_l`/`dir_r` block using a shared `uncle` binding. The case +5/6 recolor remains inside the uncle-is-black guard (both +branches return SUCCESS). + +## Resulting code + +```rust +// ANCHOR: insert-fixup +// Get child direction, set at parent. +let child_dir = (key > (*parent).key) as usize; +(*parent).child[child_dir] = node; + +// Main insert fixup. +loop { + // Case 1. + if (*parent).color == Color::Black { + return SUCCESS; + } + + let grandparent = (*parent).parent; + if grandparent.is_null() { + // Case 4. + (*parent).color = Color::Black; + return SUCCESS; + } + + let uncle; + if parent == (*grandparent).child[tree::DIR_L] { + // dir_l: parent is left child of grandparent. + uncle = (*grandparent).child[tree::DIR_R]; + if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5 dir_l: rotate parent LEFT. + let pivot = (*parent).child[tree::DIR_R]; + if node == pivot { + let new_root = pivot; + let new_child = (*new_root).child[tree::DIR_L]; + + (*parent).child[tree::DIR_R] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[tree::DIR_L] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[tree::DIR_L] = new_root; + + node = parent; + parent = new_root; + } + + // Case 6 dir_l: rotate grandparent RIGHT. + { + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[tree::DIR_R]; + + (*grandparent).child[tree::DIR_L] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[tree::DIR_R] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + let idx = (grandparent + == (*great_grandparent).child[tree::DIR_R]) + as usize; + (*great_grandparent).child[idx] = parent; + } else { + (*tree_header).root = parent; + } + } + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; + } + } else { + // dir_r: parent is right child of grandparent. + uncle = (*grandparent).child[tree::DIR_L]; + if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5 dir_r: rotate parent RIGHT. + let pivot = (*parent).child[tree::DIR_L]; + if node == pivot { + let new_root = pivot; + let new_child = (*new_root).child[tree::DIR_R]; + + (*parent).child[tree::DIR_L] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[tree::DIR_R] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[tree::DIR_R] = new_root; + + node = parent; + parent = new_root; + } + + // Case 6 dir_r: rotate grandparent LEFT. + { + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[tree::DIR_L]; + + (*grandparent).child[tree::DIR_R] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[tree::DIR_L] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + let idx = (grandparent + == (*great_grandparent).child[tree::DIR_R]) + as usize; + (*great_grandparent).child[idx] = parent; + } else { + (*tree_header).root = parent; + } + } + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; + } + } + + // Case 2. + (*parent).color = Color::Black; + (*uncle).color = Color::Black; + (*grandparent).color = Color::Red; + node = grandparent; + + parent = (*node).parent; + if parent.is_null() { + break; + } +} +// Case 3. +SUCCESS +// ANCHOR_END: insert-fixup +``` + +## Diff from current code + +The `dir_l` and `dir_r` blocks are exact mirrors: every +`child[DIR_L]` swaps with `child[DIR_R]`. + +## Functions removed + +- `direction()` — no longer called; the branch replaces it. +- `opposite()` — no longer called; hardcoded indices replace it. + +Both are currently only used in the insert fixup loop. If a future +operation (e.g. delete) needs them, they can be re-added. + +## Child direction assignment + +The initial `let dir = if key > ... { DIR_R } else { DIR_L }` +before the loop is replaced with +`let child_dir = (key > (*parent).key) as usize` to use branchless +bool-to-index. This matches the existing search pattern in `program.rs:616`. + +## Verification + +After the change, rebuild and compare the disassembly to confirm +that all `child[]` accesses use constant offsets (+8 or +16) and +that no stack spills remain in the fixup loop. diff --git a/examples/tree/specs/insert-fixup-rotation.md b/examples/tree/specs/insert-fixup-rotation.md new file mode 100644 index 00000000..700b42be --- /dev/null +++ b/examples/tree/specs/insert-fixup-rotation.md @@ -0,0 +1,206 @@ + + + + +# Insert fixup rotation specification + +## Scope + +Assembly implementation of cases 5 and 6 in the insert fixup loop, +covering the four labels: `insert_fixup_case_5_dir_l`, +`insert_fixup_case_6_dir_l`, `insert_fixup_case_5_dir_r`, +`insert_fixup_case_6_dir_r`. Each label inlines the `rotate_subtree` +call from `program.rs` with the direction hardcoded. + +## Conventions + +- `dir_l`: parent is the LEFT child of grandparent (dir = LEFT). +- `dir_r`: parent is the RIGHT child of grandparent (dir = RIGHT). +- Case 5 rotates parent in dir, then reassigns node/parent. +- Case 6 rotates grandparent in opposite(dir), recolors, exits. + +## Register contract + +### Live registers at entry to case 5/6 + +| Reg | Value | Source | +| --- | ------------ | ------------------------------------ | +| r1 | input buffer | entrypoint, never modified | +| r2 | parent | insert_search / case 5 reassignment | +| r3 | grandparent | insert_fixup_check_case_4 (non-null) | +| r9 | node | insert_store_key_value_pair / case 5 | + +### Scratch registers + +Available for rotation temporaries. These do not need to be +preserved because cases 5/6 always exit with SUCCESS and never +reach `insert_fixup_case_2` or loop back to `insert_fixup_main`. + +| Reg | Prior value | Use in rotation | +| --- | --------------- | ------------------------------- | +| r4 | grandparent.key | great-grandparent (case 6 only) | +| r5 | parent.key | free | +| r6 | parent.color | new_root (case 5 only) | +| r7 | uncle | free | +| r8 | check scratch | new_child / cmp scratch | + +## Control flow + +```text +case_5_dir_l -> (fall through) -> case_6_dir_l -> exit +case_5_dir_r -> (fall through) -> case_6_dir_r -> exit + +(or, skipping case 5:) +case_6_dir_l -> exit +case_6_dir_r -> exit +``` + +## Rotation algorithm + +Reference (`program.rs:575`): + +```rust +let parent = (*subtree).parent; +let new_root = (*subtree).child[opposite(direction)]; +let new_child = (*new_root).child[direction]; + +(*subtree).child[opposite(direction)] = new_child; +if !new_child.is_null() { (*new_child).parent = subtree; } + +(*new_root).child[direction] = subtree; +(*new_root).parent = parent; +(*subtree).parent = new_root; + +if !parent.is_null() { + (*parent).child[(subtree == (*parent).child[DIR_R]) as usize] = new_root; +} else { + (*tree).root = new_root; +} +``` + +## Case 5, dir_l — rotate(parent, LEFT) + +Subtree = parent (r2). Parent-of-subtree = grandparent (r3, +non-null per case 4 check). Direction = LEFT, opposite = RIGHT. + +```text +r6 = parent.child[RIGHT] # new_root +r8 = r6.child[LEFT] # new_child + +parent.child[RIGHT] = r8 # detach new_child +if r8 != null: r8.parent = parent # reparent new_child + +r6.child[LEFT] = parent # new_root adopts subtree +r6.parent = grandparent # new_root links up +parent.parent = r6 # subtree links to new_root + +grandparent.child[LEFT] = r6 # parent is LEFT child, hardcoded + +# Rust post-rotation: +r9 = r2 # node = old parent +r2 = r6 # parent = new_root +# fall through to case_6_dir_l +``` + +## Case 6, dir_l — rotate(grandparent, RIGHT) + +Subtree = grandparent (r3). Direction = RIGHT, opposite = LEFT. +New_root = grandparent.child[LEFT] = parent = r2 (already in +register). + +```text +r4 = grandparent.parent # great-grandparent +r8 = r2.child[RIGHT] # new_child + +grandparent.child[LEFT] = r8 # detach new_child +if r8 != null: r8.parent = grandparent # reparent new_child + +r2.child[RIGHT] = grandparent # parent adopts grandparent +r2.parent = r4 # parent links up +grandparent.parent = r2 # grandparent links to parent + +if r4 != null: + r8 = r4.child[RIGHT] + if grandparent == r8: + r4.child[RIGHT] = r2 + else: + r4.child[LEFT] = r2 +else: + tree.root = r2 # [r1 + IB_TREE_DATA_ROOT_OFF] + +# Recolor and exit: +parent.color = BLACK +grandparent.color = RED +exit +``` + +## Case 5, dir_r — rotate(parent, RIGHT) + +Mirror of case 5 dir_l. Subtree = parent (r2). Direction = RIGHT, +opposite = LEFT. + +```text +r6 = parent.child[LEFT] # new_root +r8 = r6.child[RIGHT] # new_child + +parent.child[LEFT] = r8 # detach new_child +if r8 != null: r8.parent = parent # reparent new_child + +r6.child[RIGHT] = parent # new_root adopts subtree +r6.parent = grandparent # new_root links up +parent.parent = r6 # subtree links to new_root + +grandparent.child[RIGHT] = r6 # parent is RIGHT child, hardcoded + +# Rust post-rotation: +r9 = r2 # node = old parent +r2 = r6 # parent = new_root +# fall through to case_6_dir_r +``` + +## Case 6, dir_r — rotate(grandparent, LEFT) + +Mirror of case 6 dir_l. Subtree = grandparent (r3). Direction = +LEFT, opposite = RIGHT. New_root = grandparent.child[RIGHT] = +parent = r2. + +```text +r4 = grandparent.parent # great-grandparent +r8 = r2.child[LEFT] # new_child + +grandparent.child[RIGHT] = r8 # detach new_child +if r8 != null: r8.parent = grandparent # reparent new_child + +r2.child[LEFT] = grandparent # parent adopts grandparent +r2.parent = r4 # parent links up +grandparent.parent = r2 # grandparent links to parent + +if r4 != null: + r8 = r4.child[RIGHT] + if grandparent == r8: + r4.child[RIGHT] = r2 + else: + r4.child[LEFT] = r2 +else: + tree.root = r2 # [r1 + IB_TREE_DATA_ROOT_OFF] + +# Recolor and exit: +parent.color = BLACK +grandparent.color = RED +exit +``` + +## Key observations + +- Case 5 never needs a null/root check because grandparent is + guaranteed non-null by the case 4 check. The direction of + grandparent's child update is hardcoded (LEFT for dir_l, RIGHT + for dir_r). +- Case 6 needs the full null check because great-grandparent can + be null (grandparent may be root). The great-grandparent child + update cannot be hardcoded by direction — it requires a pointer + comparison. +- In case 6, new_root is always parent (r2), avoiding an extra + load. +- The dir_l/dir_r variants are exact mirrors: every CHILD_L swaps + with CHILD_R. diff --git a/examples/tree/specs/insert-tests.md b/examples/tree/specs/insert-tests.md new file mode 100644 index 00000000..add31a66 --- /dev/null +++ b/examples/tree/specs/insert-tests.md @@ -0,0 +1,523 @@ + + + + +# Insert-to-tree test specification + +## Scope + +Tests for the search, insertion, and rebalancing logic in the insert +instruction — everything after node allocation. The existing insert +tests cover input validation and the allocation/recycle paths; this +spec covers what happens once we have a node and need to place it in +the tree. + +## Approach + +### Pre-built tree states + +Each rebalancing case requires a specific tree shape. Rather than +chaining multiple insert instructions, construct the tree layout +directly in account memory before processing the insert instruction. +This gives precise control over which case is triggered. + +A helper builds the account data buffer by: + +- Allocating space for `TreeHeader` + N existing `TreeNode`s + 1 free + node on the stack. +- Setting all pointers as virtual addresses + (`MM_INPUT_START + input_buffer::TREE_DATA_OFF + offset`). +- Setting `header.top` to the free stack node so the insert pops it + instead of allocating (same pattern as `insert_skip_alloc_setup`). + +### Fixed costs + +Targeted case tests pop from the free stack, so they have zero fixed +costs. Multi-insert integration tests use the allocation path (CPI +transfer) for each insert. Their `fixed_costs()` must return +`CPI_BASE + SYSTEM_PROGRAM` per insert so the comparison table +subtracts the transfer CPI overhead and isolates the tree logic. + +### Full-state assertion + +Every targeted case test asserts the **entire** tree data account +after the insert instruction. Nothing is left unchecked. + +**TreeHeader** (every field): + +- `root` — pointer to the expected root node. +- `top` — pointer to the new free-stack top (null when the only + free node was consumed). +- `next` — allocation pointer (unchanged by pop-from-stack inserts). + +**Every TreeNode** (every field of every node in the buffer): + +- `parent` — pointer to the expected parent (null for root). +- `child[L]`, `child[R]` — pointers to expected children (null for + leaves). +- `key` — expected key. +- `value` — expected value (pre-set for existing nodes, from + instruction data for the inserted node). +- `color` — expected color after rebalancing. + +Each case below specifies the full expected state using compact +notation: + +```text +Header: root=N0 top=null next= +N0: B key=10 parent=-- L=N1 R=N2 +N1: R key=5 parent=N0 L=-- R=-- <- inserted +N2: B key=15 parent=N0 L=-- R=-- +``` + +`--` means null. `` is the address past the last node slot. +Node indices (N0, N1, ...) reflect memory layout order, not tree +position — N0 is always at offset `sizeof(TreeHeader)` in the +account data. Values are omitted from the notation for brevity; +pre-existing nodes keep their original values, and the inserted +node gets the instruction data value. + +## Cases + +### Search + +Error cases — the tree account must be unchanged after the +instruction returns `KEY_EXISTS`. + +| Case | Setup | Key | Expected | +| ------------ | ------------------- | --- | ------------------ | +| Dup at root | Root with key 10 | 10 | `KEY_EXISTS` error | +| Dup in left | Root 10, L child 5 | 5 | `KEY_EXISTS` error | +| Dup in right | Root 10, R child 15 | 15 | `KEY_EXISTS` error | + +### Insert to empty tree + +```text +Before: + Header: root=-- top=N0 next= + N0: (free stack node) + +After insert key=42: + Header: root=N0 top=-- next= + N0: R key=42 parent=-- L=-- R=-- +``` + +### Case 1: parent is black + +No rebalancing needed. Inserted node stays red under a black parent. + +Left child variant (insert key=5): + +```text +Before: + Header: root=N0 top=N1 next= + N0: B key=10 parent=-- L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=10 parent=-- L=N1 R=-- + N1: R key=5 parent=N0 L=-- R=-- <- inserted +``` + +Right child variant (insert key=15): mirror of above, N1 at R of +N0. + +### Case 4: parent is root and red + +Parent is root (no grandparent). Recolor parent to black. + +Left child variant (insert key=5): + +```text +Before: + Header: root=N0 top=N1 next= + N0: R key=10 parent=-- L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=10 parent=-- L=N1 R=-- <- recolored B + N1: R key=5 parent=N0 L=-- R=-- <- inserted +``` + +Right child variant (insert key=15): mirror of above. + +### Case 2 + 3: red uncle, propagate to root + +Parent and uncle are both red. Recolor parent, uncle, grandparent. +Grandparent is root, so the loop exits (case 3). + +Left-left variant (insert key=1): + +```text +Before: + Header: root=N0 top=N3 next= + N0: B key=10 parent=-- L=N1 R=N2 + N1: R key=5 parent=N0 L=-- R=-- + N2: R key=15 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: R key=10 parent=-- L=N1 R=N2 <- recolored R + N1: B key=5 parent=N0 L=N3 R=-- <- recolored B + N2: B key=15 parent=N0 L=-- R=-- <- recolored B + N3: R key=1 parent=N1 L=-- R=-- <- inserted +``` + +All four child positions trigger the same recolor path — only the +inserted node's position differs. + +| Variant | Insert key | Inserted at | +| ----------- | ---------- | ----------- | +| Left-left | 1 | N1.L | +| Left-right | 7 | N1.R | +| Right-left | 12 | N2.L | +| Right-right | 20 | N2.R | + +### Case 2 + 1: red uncle, propagate to black ancestor + +Same recolor as case 2+3, but grandparent is not root — its parent +is black, so case 1 terminates. + +Left-left variant (insert key=1): + +```text +Before: + Header: root=N0 top=N4 next= + N0: B key=20 parent=-- L=N1 R=-- + N1: B key=10 parent=N0 L=N2 R=N3 + N2: R key=5 parent=N1 L=-- R=-- + N3: R key=15 parent=N1 L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=20 parent=-- L=N1 R=-- + N1: R key=10 parent=N0 L=N2 R=N3 <- recolored R + N2: B key=5 parent=N1 L=N4 R=-- <- recolored B + N3: B key=15 parent=N1 L=-- R=-- <- recolored B + N4: R key=1 parent=N2 L=-- R=-- <- inserted +``` + +Mirror variant: B(2) as root with B(10) as right child, insert +key=20. Same recolor pattern, mirrored. + +### Case 6: black uncle, outer child — single rotation + +Uncle is black or null. Node is an outer child (same direction as +parent relative to grandparent). Rotate grandparent, recolor. + +Left-left, null uncle variant (insert key=1): + +```text +Before: + Header: root=N0 top=N2 next= + N0: B key=10 parent=-- L=N1 R=-- + N1: R key=5 parent=N0 L=-- R=-- + +After: + Header: root=N1 top=-- next= + N0: R key=10 parent=N1 L=-- R=-- <- recolored R + N1: B key=5 parent=-- L=N2 R=N0 <- recolored B, new root + N2: R key=1 parent=N1 L=-- R=-- <- inserted +``` + +A non-null black uncle at the same depth as a null uncle is +impossible in a valid red-black tree. The parent is red (otherwise +no fixup triggers), so it contributes 0 to the black height. A +non-null black uncle contributes at least 1. These cannot balance +(RBT-4 violation). A non-null black uncle only appears after +case 2 propagation up the tree, which is covered by the case 2+6 +tests below. + +Right-right variants: mirror of above (insert key=20, parent is +R(15), rotation goes left). + +### Case 5 + 6: black uncle, inner child — double rotation + +Uncle is black or null. Node is an inner child (opposite direction +from parent relative to grandparent). Rotate parent first (case 5), +then fall through to case 6. + +Left-right, null uncle variant (insert key=7): + +```text +Before: + Header: root=N0 top=N2 next= + N0: B key=10 parent=-- L=N1 R=-- + N1: R key=5 parent=N0 L=-- R=-- + +After: + Header: root=N2 top=-- next= + N0: R key=10 parent=N2 L=-- R=-- <- recolored R + N1: R key=5 parent=N2 L=-- R=-- + N2: B key=7 parent=-- L=N1 R=N0 <- inserted, new root +``` + +As with case 6 above, a non-null black uncle at the same depth +is impossible in a valid red-black tree (RBT-4). Case 2+5+6 +tests cover the non-null black uncle scenario via propagation. + +Right-left variants: mirror of above (insert key=12, parent is +R(15), double rotation goes right then left). + +### Case 6: non-null great-grandparent + +The existing case 6 tests have grandparent as root, so +great-grandparent is always null and the root-replacement path fires. +These variants place the rotation under a non-root grandparent to +cover the great-grandparent child pointer update branches in the +assembly (`insert_fixup_case_6_dir_l_left`, +`insert_fixup_case_6_dir_r_left`, and their fall-through +counterparts). + +Left-left, GP is left child of GGP (insert key=1): + +```text +Before: + Header: root=N0 top=N4 next= + N0: B key=20 parent=-- L=N1 R=N3 + N1: B key=10 parent=N0 L=N2 R=-- + N2: R key=5 parent=N1 L=-- R=-- + N3: B key=25 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=20 parent=-- L=N2 R=N3 + N1: R key=10 parent=N2 L=-- R=-- <- recolored R + N2: B key=5 parent=N0 L=N4 R=N1 <- recolored B + N3: B key=25 parent=N0 L=-- R=-- + N4: R key=1 parent=N2 L=-- R=-- <- inserted +``` + +Covers: GGP non-null, GP is left child of GGP (dir_l path, +`insert_fixup_case_6_dir_l_left`). + +Left-left, GP is right child of GGP (insert key=10): + +```text +Before: + Header: root=N0 top=N4 next= + N0: B key=5 parent=-- L=N1 R=N2 + N1: B key=3 parent=N0 L=-- R=-- + N2: B key=20 parent=N0 L=N3 R=-- + N3: R key=15 parent=N2 L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=5 parent=-- L=N1 R=N3 + N1: B key=3 parent=N0 L=-- R=-- + N2: R key=20 parent=N3 L=-- R=-- <- recolored R + N3: B key=15 parent=N0 L=N4 R=N2 <- recolored B + N4: R key=10 parent=N3 L=-- R=-- <- inserted +``` + +Covers: GGP non-null, GP is right child of GGP (dir_l path, +fall-through at `jne r3, r8`). + +Right-right, GP is right child of GGP (insert key=25): + +```text +Before: + Header: root=N0 top=N4 next= + N0: B key=5 parent=-- L=N1 R=N2 + N1: B key=3 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=N3 + N3: R key=20 parent=N2 L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=5 parent=-- L=N1 R=N3 + N1: B key=3 parent=N0 L=-- R=-- + N2: R key=15 parent=N3 L=-- R=-- <- recolored R + N3: B key=20 parent=N0 L=N2 R=N4 <- recolored B + N4: R key=25 parent=N3 L=-- R=-- <- inserted +``` + +Covers: GGP non-null, GP is right child of GGP (dir_r path, +fall-through at `jne r3, r8`). + +Right-right, GP is left child of GGP (insert key=17): + +```text +Before: + Header: root=N0 top=N4 next= + N0: B key=20 parent=-- L=N1 R=N3 + N1: B key=10 parent=N0 L=-- R=N2 + N2: R key=15 parent=N1 L=-- R=-- + N3: B key=25 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=-- next= + N0: B key=20 parent=-- L=N2 R=N3 + N1: R key=10 parent=N2 L=-- R=-- <- recolored R + N2: B key=15 parent=N0 L=N1 R=N4 <- recolored B + N3: B key=25 parent=N0 L=-- R=-- + N4: R key=17 parent=N2 L=-- R=-- <- inserted +``` + +Covers: GGP non-null, GP is left child of GGP (dir_r path, +`insert_fixup_case_6_dir_r_left`). + +### Case 2 + 6: non-null new_child in rotation + +The existing case 6 tests have null `new_child` in the rotation +(parent has no child on the transferred side). These variants use +case 2 propagation to reach case 6 with a populated subtree, so the +`new_child` pointer is non-null and the reparenting branch fires +(`insert_fixup_case_6_dir_l_skip` / `dir_r_skip` fall-through). + +Dir_l variant (insert key=1): + +```text +Before: + Header: root=N0 top=N7 next= + N0: B key=20 parent=-- L=N1 R=N3 + N1: R key=10 parent=N0 L=N2 R=N6 + N2: B key=5 parent=N1 L=N4 R=N5 + N3: B key=25 parent=N0 L=-- R=-- + N4: R key=3 parent=N2 L=-- R=-- + N5: R key=7 parent=N2 L=-- R=-- + N6: B key=15 parent=N1 L=-- R=-- + +After: + Header: root=N1 top=-- next= + N0: R key=20 parent=N1 L=N6 R=N3 <- recolored R + N1: B key=10 parent=-- L=N2 R=N0 <- recolored B, new root + N2: R key=5 parent=N1 L=N4 R=N5 <- recolored R (case 2) + N3: B key=25 parent=N0 L=-- R=-- + N4: B key=3 parent=N2 L=N7 R=-- <- recolored B (case 2) + N5: B key=7 parent=N2 L=-- R=-- <- recolored B (case 2) + N6: B key=15 parent=N0 L=-- R=-- <- reparented + N7: R key=1 parent=N4 L=-- R=-- <- inserted +``` + +Path: insert at N4.L → case 2 (uncle N5 red) recolors +N4/N5/N2 → node=N2, parent=N1 → case 6 dir_l rotates N0 +right with `new_child = N6` (non-null). + +Dir_r variant (insert key=30): + +```text +Before: + Header: root=N0 top=N7 next= + N0: B key=5 parent=-- L=N1 R=N2 + N1: B key=3 parent=N0 L=-- R=-- + N2: R key=15 parent=N0 L=N3 R=N4 + N3: B key=10 parent=N2 L=-- R=-- + N4: B key=20 parent=N2 L=N5 R=N6 + N5: R key=17 parent=N4 L=-- R=-- + N6: R key=25 parent=N4 L=-- R=-- + +After: + Header: root=N2 top=-- next= + N0: R key=5 parent=N2 L=N1 R=N3 <- recolored R + N1: B key=3 parent=N0 L=-- R=-- + N2: B key=15 parent=-- L=N0 R=N4 <- recolored B, new root + N3: B key=10 parent=N0 L=-- R=-- <- reparented + N4: R key=20 parent=N2 L=N5 R=N6 <- recolored R (case 2) + N5: B key=17 parent=N4 L=-- R=-- <- recolored B (case 2) + N6: B key=25 parent=N4 L=-- R=N7 <- recolored B (case 2) + N7: R key=30 parent=N6 L=-- R=-- <- inserted +``` + +Path: insert at N6.R → case 2 (uncle N5 red) recolors +N5/N6/N4 → node=N4, parent=N2 → case 6 dir_r rotates N0 +left with `new_child = N3` (non-null). + +### Case 2 + 5 + 6: non-null new_child in rotations + +The existing case 5+6 tests have null `new_child` in both rotations. +These variants use case 2 propagation to reach case 5 with a +populated subtree, producing non-null `new_child` in both the case 5 +and case 6 rotations (`insert_fixup_case_5_dir_l_skip` / +`dir_r_skip` and `insert_fixup_case_6_dir_l_skip` / `dir_r_skip` +fall-through). + +Dir_l variant (insert key=11): + +```text +Before: + Header: root=N0 top=N7 next= + N0: B key=20 parent=-- L=N1 R=N4 + N1: R key=10 parent=N0 L=N2 R=N3 + N2: B key=5 parent=N1 L=-- R=-- + N3: B key=15 parent=N1 L=N5 R=N6 + N4: B key=25 parent=N0 L=-- R=-- + N5: R key=12 parent=N3 L=-- R=-- + N6: R key=17 parent=N3 L=-- R=-- + +After: + Header: root=N3 top=-- next= + N0: R key=20 parent=N3 L=N6 R=N4 <- recolored R + N1: R key=10 parent=N3 L=N2 R=N5 <- reparented + N2: B key=5 parent=N1 L=-- R=-- + N3: B key=15 parent=-- L=N1 R=N0 <- recolored B, new root + N4: B key=25 parent=N0 L=-- R=-- + N5: B key=12 parent=N1 L=N7 R=-- <- recolored B, reparented + N6: B key=17 parent=N0 L=-- R=-- <- recolored B, reparented + N7: R key=11 parent=N5 L=-- R=-- <- inserted +``` + +Path: insert at N5.L → case 2 (uncle N6 red) recolors +N5/N6/N3 → node=N3, parent=N1 → case 5 dir_l rotates N1 +left with `new_child = N5` (non-null) → case 6 dir_l rotates +N0 right with `new_child = N6` (non-null). + +Dir_r variant (insert key=18): + +```text +Before: + Header: root=N0 top=N7 next= + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: R key=20 parent=N0 L=N3 R=N4 + N3: B key=15 parent=N2 L=N5 R=N6 + N4: B key=25 parent=N2 L=-- R=-- + N5: R key=12 parent=N3 L=-- R=-- + N6: R key=17 parent=N3 L=-- R=-- + +After: + Header: root=N3 top=-- next= + N0: R key=10 parent=N3 L=N1 R=N5 <- recolored R + N1: B key=5 parent=N0 L=-- R=-- + N2: R key=20 parent=N3 L=N6 R=N4 <- reparented + N3: B key=15 parent=-- L=N0 R=N2 <- recolored B, new root + N4: B key=25 parent=N2 L=-- R=-- + N5: B key=12 parent=N0 L=-- R=-- <- recolored B, reparented + N6: B key=17 parent=N2 L=-- R=N7 <- recolored B, reparented + N7: R key=18 parent=N6 L=-- R=-- <- inserted +``` + +Path: insert at N6.R → case 2 (uncle N5 red) recolors +N5/N6/N3 → node=N3, parent=N2 → case 5 dir_r rotates N2 +right with `new_child = N6` (non-null) → case 6 dir_r rotates +N0 left with `new_child = N5` (non-null). + +## Multi-insert integration tests + +A handful of sequential-insert tests to validate that chained +insertions produce correct trees. Process multiple insert instructions +in sequence, feeding each resulting account state into the next. + +| Test | Sequence | Purpose | +| ----------- | ----------------------- | ---------------- | +| 3-node | 10, 5, 15 | Minimal balanced | +| Left-skew | 10, 5, 1 | Right rotation | +| Right-skew | 10, 15, 20 | Left rotation | +| Zigzag | 10, 5, 7 | Double rotation | +| 7-node full | 10, 5, 15, 3, 7, 12, 20 | Multiple rounds | + +## Multi-insert verification + +Multi-insert integration tests also assert full state. After each +insert in the sequence, the expected complete tree layout is +specified. The test feeds the resulting account data from one +instruction into the next and asserts the full state at each step. + +## Test helpers needed + +- `build_tree(nodes: &[NodeSpec]) -> Vec` — Serialize a tree + layout into account data with correct virtual address pointers + and a free stack node for the new insertion. +- `assert_tree(account_data: &[u8], expected: &TreeSpec)` — Assert + every field of the header and every field of every node against + the expected specification. Panics with a diff on mismatch. diff --git a/examples/tree/specs/parity-workflow.md b/examples/tree/specs/parity-workflow.md new file mode 100644 index 00000000..cbbe6a1a --- /dev/null +++ b/examples/tree/specs/parity-workflow.md @@ -0,0 +1,140 @@ +# Parity workflow + +Reusable workflow for verifying that Rust and assembly +implementations produce equivalent behavior and comparable +instruction-level output. Applicable to any example in +`examples/`. + +## Generating and comparing disassembly + +Use `/disassemble-example ` to generate Rust disassembly +(e.g., `/disassemble-example tree`). The output lives at +`examples//artifacts/rs-disassembly.s`. Compare it +side-by-side with the corresponding assembly source: + +- Run `/disassemble-example ` to regenerate the Rust + disassembly artifact. +- Open `examples//artifacts/rs-disassembly.s` and locate + the function or block under review. +- Open the corresponding section of the assembly source and + align the two side by side. +- Walk through instruction by instruction, noting structural + differences: extra jumps, reordered operations, missing + inlining, different register usage. + +The goal is structural equivalence, not byte-identical output. +The Rust compiler may reorder instructions or use different +registers while producing equivalent behavior. + +## Identifying optimization opportunities + +Disassembly comparison reveals concrete optimization targets. +Look for: + +- **Duplicated instruction sequences.** Repeated blocks that + could be factored into a shared label or inlined at each site. +- **Unnecessary jumps.** Branches to code that immediately + follows — often introduced by nested `if/else` or macro + expansion. +- **Register pressure differences.** Cases where the Rust + compiler spills to the stack while the assembly keeps values + in registers, or vice versa. +- **Missed inlining opportunities.** Function calls that the + compiler did not inline despite `#[inline(always)]`, or + macros that expand into more instructions than the equivalent + hand-written code. + +Macro expansion can produce duplicated code at each call site. +Before restructuring to eliminate duplication, evaluate whether +it actually hurts CU count — the compiler often optimizes each +expansion independently, producing tighter code than a unified +block. Only restructure when measurable improvement is +confirmed. + +## Branch coverage verification checklist + +Trace each test through both the Rust and assembly +implementations to confirm that both paths execute the same +logical branches. + +- Start with a branch table that maps each conditional jump to + the test numbers that exercise it. For example, the "Assembly + branches" table in `remove-tests.md`: + + ```text + | ID | Branch | Tests | + | --- | ------------------------- | ----------------- | + | B1 | found : L==null | 10,12,14,16,17,18 | + ... + ``` + +- For each branch ID, pick a representative test and manually + trace it through the assembly path, confirming which branch + is taken or not taken. + +- Trace the same test through the Rust path, confirming the + corresponding conditional takes the same direction. + +- Record any discrepancies. A branch exercised in assembly but + unreachable in Rust (or vice versa) indicates a structural + divergence that must be resolved. + +Use the branch-to-test mapping format as a template for new +instructions. Each new instruction's spec should include an +equivalent table covering every conditional jump in both +implementations. + +## Iterative optimization loop + +Parity verification is iterative. The cycle: + +- **Edit code.** Make a targeted change to the Rust + implementation (restructure control flow, adjust macro + boundaries, inline a helper). +- **Regenerate disassembly.** Run `/disassemble-example ` + to produce updated Rust disassembly. +- **Compare with assembly.** Diff the new disassembly against + the assembly source to confirm the change had the intended + effect. +- **Run tests.** Run `/build-example ` and wait for the + full pipeline to finish (build, dump, disassemble, test, + snippets, verification). Then check the result file at + `examples//artifacts/tests/*/result.txt` to confirm + all tests pass and review the CU comparison table for + overhead changes. +- **Verify parity.** Confirm that the Rust disassembly now + matches the assembly structure more closely (or produces + equivalent/better code). Use the CU table in the result + file as the ground truth for overhead — do not manually + calculate CU from logs. +- **Repeat.** Continue until the target section is at parity. + +Stop when one of these conditions holds: + +- The Rust disassembly matches the assembly structure for the + section under review (same labels, same branch topology, same + instruction count within a small margin). +- The Rust disassembly produces demonstrably better code (fewer + instructions, fewer branches) while maintaining identical + test results. +- The Rust overhead is within acceptable bounds for the use + case, or further optimization would require restructuring + that degrades code clarity without meaningful CU improvement. + +## CU overhead analysis + +Before optimizing, run the CU overhead analysis workflow to +identify WHERE overhead comes from and prioritize targets. See +`cu-overhead-analysis.md` for the full methodology. + +The analysis pipeline feeds into the optimization loop: + +- **Analysis** identifies overhead sources and classifies them + (redundant branches, macro duplication, register spills, + etc.). +- **Optimization targets** are ranked by reducible CUs and + handed to Engineering. +- **This parity workflow** drives the iterative optimization + cycle on each target. +- **Analysis re-measures** after each optimization to confirm + overhead reduction. diff --git a/examples/tree/specs/remove-tests.md b/examples/tree/specs/remove-tests.md new file mode 100644 index 00000000..500bea5b --- /dev/null +++ b/examples/tree/specs/remove-tests.md @@ -0,0 +1,1180 @@ + + + + +# Remove-to-tree test specification + +## Scope + +Tests for the remove instruction: input validation, search, BST +deletion, red-black rebalancing, node recycling, and return value +encoding. Mirrors the structure of the insert test specification. + +## Shared test helpers + +The insert tests define helpers (`NodeSpec`, `TreeSpec`, +`build_tree_account`, `assert_tree_account`, `node()`, `B`, `R`, +`node_vaddr`, `opt_vaddr`, `build_empty_tree`) that are equally +useful for remove tests. These should be extracted into a shared +module (e.g. `tests/common.rs`) imported by both `tests/insert.rs` +and `tests/remove.rs`. + +Helpers that stay in `tests/insert.rs` (insert-specific): + +- `insert_instruction_data`, `insert_setup`, + `insert_skip_alloc_setup`, `insert_tree_setup`. +- `run_success`, `run_dup_error`. +- `InsertCase`, `MultiInsertCase`. + +New helpers in `tests/remove.rs` (remove-specific): + +- `remove_setup(lang, desc, remove_key)` -- build a pre-populated + tree account and a `RemoveInstruction` with the given key. Uses + two accounts (user + tree), no CPI accounts needed. +- `run_remove_success(lang, desc, remove_key, expected_value, expected_tree)` -- + execute the remove instruction, verify the + return code encodes the expected value, then assert full tree + state including the freed node slot. +- `run_remove_not_found(lang, desc, remove_key)` -- execute and + verify `KEY_DOES_NOT_EXIST` error. + +## Return value verification + +The program returns `0` (`SUCCESS`) on a successful remove. The +SVM only persists account modifications when the program returns +zero; any non-zero `r0` is interpreted as +`ProgramError::Custom(r0)` and all account changes are reverted. + +Tests verify `MolluskResult::Success` (not a custom error code). + +### Removed value verification + +Because the return code cannot carry the removed value (non-zero +would revert the tree mutation), tests verify the value by +inspecting the freed node on the stack. The freed node's `key`, +`value`, and `color` fields are not cleared by remove (see "Freed +node verification" below), so `assert_tree_account` confirms that +the freed node retains the correct value. + +## Freed node verification + +When a node is removed it is cast to a `StackNode` and pushed onto +the free stack. The full-state assertion must verify: + +- **`header.top`** points to the freed node slot. +- **Freed node `child[L]`** is null (zeroed by remove). +- **Freed node `child[R]`** is null (zeroed by remove). +- **Freed node `parent`** (offset 0, overlaps `StackNode.next`) + equals the previous stack top (null if the stack was empty before + removal). + +The `key`, `value`, and `color` fields of the freed node are not +cleared by remove (insert overwrites them when the node is +recycled). Tests must assert these fields retain their pre-removal +values. In particular, the freed node's `value` field serves as +the primary mechanism for verifying which value was removed -- +since the return code is `0` (see "Return value verification" +above), the value cannot be communicated via `r0`. + +The existing `assert_tree_account` checks every field of every +node in the buffer, so including the freed node slot in the +expected `TreeSpec.nodes` list is sufficient. The freed node is a +`NodeSpec` with null children, `parent` set to the old stack top +index, and the original `key`/`value`/`color` preserved. + +## `build_tree_account` adjustment + +The insert helper always appends one extra free slot after the +existing nodes (for the node to be inserted). For remove tests, +the tree should contain only the existing nodes with no extra free +slot -- there is nothing to insert, and the removed node itself +becomes the new free slot. A `build_tree_account_no_free` variant +(or a flag) is needed: + +- `header.top = null` (no pre-existing free nodes). +- `header.next = null` (unused). +- Data length = `sizeof(TreeHeader) + N * sizeof(TreeNode)`. + +After removal, the freed node's slot is already within the +existing data. `header.top` will point to the freed node, and +`StackNode.next` will be null (stack was empty). + +If the tree already has free nodes on the stack before removal +(tested in multi-step scenarios), `header.top` is set to the +existing free node and the freed node's `StackNode.next` must +chain to it. + +## Input check tests + +Same checks as insert, using `RemoveInstruction` (discriminator 2, +3 bytes). Each test constructs a valid two-account remove setup, +mutates one property, and expects the corresponding error. + +| Case | Mutation | Expected error | +| ----------------- | ------------------------ | ---------------------- | +| Data too short | 1-byte instruction data | `INSTRUCTION_DATA_LEN` | +| Data too long | 4-byte instruction data | `INSTRUCTION_DATA_LEN` | +| Too few accounts | 1 account (user only) | `N_ACCOUNTS` | +| User has data | User account with 1 byte | `USER_DATA_LEN` | +| Tree is duplicate | Tree = copy of user | `TREE_DUPLICATE` | + +## Search error tests + +| Case | Tree setup | Key | Expected | +| -------------- | ------------------ | --- | -------------------- | +| Empty tree | Root is null | 10 | `KEY_DOES_NOT_EXIST` | +| Not found (L) | Root key=10 | 5 | `KEY_DOES_NOT_EXIST` | +| Not found (R) | Root key=10 | 15 | `KEY_DOES_NOT_EXIST` | +| Not found deep | Root 10, L=5, R=15 | 12 | `KEY_DOES_NOT_EXIST` | + +Search error tests must also verify the tree account data is +unchanged after the failed instruction. + +## Wikipedia reference -- simple cases + +Verbatim from Wikipedia, preceding the rebalancing algorithm: + +> When the deleted node has 2 children (non-NULL), then we can +> swap its value with its in-order successor (the leftmost child +> of the right subtree), and then delete the successor instead. +> Since the successor is leftmost, it can only have a right +> child (non-NULL) or no child at all. +> +> When the deleted node has only 1 child (non-NULL). In this +> case, just replace the node with its child, and color it +> black. The single child (non-NULL) must be red according to +> conclusion 5, and the deleted node must be black according to +> requirement 3. +> +> When the deleted node has no children (both NULL) and is the +> root, replace it with NULL. The tree is empty. +> +> When the deleted node has no children (both NULL), and is red, +> simply remove the leaf node. +> +> When the deleted node has no children (both NULL), and is +> black, deleting it will create an imbalance, and requires a +> rebalance, as covered in the next section. +> +> **Removal of a black non-root leaf** +> +> The complex case is when N is not the root, colored black and +> has no proper child (⇔ only NULL children). In the first +> iteration, N is replaced by NULL. + +Mapping to test sections: + +- Simple case 1 (2 children) → Successor swap tests (#19--#21). +- Simple case 2 (1 child) → Tests #10--#15. +- Simple case 3 (root leaf) → Test #16. +- Simple case 4 (red leaf) → Tests #17--#18. +- Complex case (black leaf) → Rebalancing tests (#22--#45). + +## Simple removal cases + +These cases do not trigger rebalancing. + +### Simple case 2: node with one child + +A node with exactly one child must be black, and the child must +be red (RB invariant). Replace the node with its child and +recolor the child black. + +Right child at root (remove key=10): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=-- R=N1 + N1: R key=15 parent=N0 L=-- R=-- + +After: + Header: root=N1 top=N0 next=-- + N0: key=10 color=B parent=-- L=-- R=-- <- freed + N1: B key=15 parent=-- L=-- R=-- <- recolored B, new root +``` + +Left child at root (remove key=10, child is N1 at L): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=-- + N1: R key=5 parent=N0 L=-- R=-- + +After: + Header: root=N1 top=N0 next=-- + N0: key=10 color=B parent=-- L=-- R=-- <- freed + N1: B key=5 parent=-- L=-- R=-- <- recolored B, new root +``` + +Non-root right child (remove key=15): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=N3 + N3: R key=20 parent=N2 L=-- R=-- + +After: + Header: root=N0 top=N2 next=-- + N0: B key=10 parent=-- L=N1 R=N3 + N1: B key=5 parent=N0 L=-- R=-- + N2: key=15 color=B parent=-- L=-- R=-- <- freed + N3: B key=20 parent=N0 L=-- R=-- <- recolored B +``` + +Non-root left child (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=N3 R=-- + N2: B key=15 parent=N0 L=-- R=-- + N3: R key=3 parent=N1 L=-- R=-- + +After: + Header: root=N0 top=N1 next=-- + N0: B key=10 parent=-- L=N3 R=N2 + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=15 parent=N0 L=-- R=-- + N3: B key=3 parent=N0 L=-- R=-- <- recolored B +``` + +Non-root right child, left position (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=N3 + N2: B key=15 parent=N0 L=-- R=-- + N3: R key=7 parent=N1 L=-- R=-- + +After: + Header: root=N0 top=N1 next=-- + N0: B key=10 parent=-- L=N3 R=N2 + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=15 parent=N0 L=-- R=-- + N3: B key=7 parent=N0 L=-- R=-- <- recolored B +``` + +Node N1 has only a right child and is the left child of its +parent. Exercises `parent.child[L] = child` in the R-child call +site of `remove_simple_2_child_replace` (Rust line 633). In the +assembly, exercises `remove_simple_2_dir_l` entered via +`remove_check_child_r`. + +Non-root left child, right position (remove key=15): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=N3 R=-- + N3: R key=12 parent=N2 L=-- R=-- + +After: + Header: root=N0 top=N2 next=-- + N0: B key=10 parent=-- L=N1 R=N3 + N1: B key=5 parent=N0 L=-- R=-- + N2: key=15 color=B parent=-- L=-- R=-- <- freed + N3: B key=12 parent=N0 L=-- R=-- <- recolored B +``` + +Node N2 has only a left child and is the right child of its +parent. Exercises `parent.child[R] = child` in the L-child call +site of `remove_simple_2_child_replace` (Rust line 627). In the +assembly, exercises `parent.child[R] = child` entered via +`remove_simple_2_child_l` from the simple-1 L-only branch. + +### Simple case 3: remove root leaf + +Single-node tree. Root becomes null, node is freed. + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 val=10 parent=-- L=-- R=-- + +After remove key=10 (returns value=10): + Header: root=-- top=N0 next=-- + N0: key=10 val=10 color=B parent=-- L=-- R=-- <- freed +``` + +`parent` (= `StackNode.next`) is null because the stack was empty. + +### Simple case 4: remove red leaf + +Red leaf with a black parent. Detach the leaf. + +Left child variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=-- + N1: R key=5 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=N1 next=-- + N0: B key=10 parent=-- L=-- R=-- + N1: key=5 color=R parent=-- L=-- R=-- <- freed +``` + +Right child variant (remove key=15): mirror with N1 as right +child of N0. + +## Successor swap cases + +When the node to delete has two children, copy key/value from +the in-order successor (leftmost in right subtree), then delete +the successor instead. + +### Successor is immediate right child + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: R key=5 parent=N0 L=-- R=-- + N2: R key=15 parent=N0 L=-- R=-- + +After remove key=10 (returns value=10): + Header: root=N0 top=N2 next=-- + N0: B key=15 val=15 parent=-- L=N1 R=-- <- copied from successor + N1: R key=5 parent=N0 L=-- R=-- + N2: key=15 color=R parent=-- L=-- R=-- <- freed (was successor) +``` + +The successor (N2, key=15) is a red leaf -- simple case 3 after +the swap. + +### Successor with deep left descent + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=20 parent=N0 L=N3 R=N4 + N3: R key=15 parent=N2 L=-- R=-- + N4: R key=25 parent=N2 L=-- R=-- + +After remove key=10 (returns value=10): + Header: root=N0 top=N3 next=-- + N0: B key=15 val=15 parent=-- L=N1 R=N2 <- copied from successor + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=20 parent=N0 L=-- R=N4 + N3: key=15 color=R parent=-- L=-- R=-- <- freed (was successor) + N4: R key=25 parent=N2 L=-- R=-- +``` + +The successor (N3, key=15) is a red leaf -- simple case 3 after +the swap. + +### Successor with right child + +The successor has no left child but may have a right child. +This triggers simple case 1 (one-child node) after the swap. + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=N3 + N3: R key=20 parent=N2 L=-- R=-- + +After remove key=10 (returns value=10): + Header: root=N0 top=N2 next=-- + N0: B key=15 val=15 parent=-- L=N1 R=N3 <- copied from successor + N1: B key=5 parent=N0 L=-- R=-- + N2: key=15 color=B parent=-- L=-- R=-- <- freed (was successor) + N3: B key=20 parent=N0 L=-- R=-- <- recolored B +``` + +Successor N2 has one child (N3). Simple case 1: replace N2 with +N3, recolor N3 black. + +## Simple case branch coverage + +### Assembly branches (`tree.s`) + +Each branch corresponds to a conditional jump in the remove path. +"Taken/not-taken" refers to the `jeq`/`jne` outcome. + +| ID | Branch | Tests | +| --- | ------------------------- | ----------------- | +| B1 | found : L==null | 10,12,14,16,17,18 | +| B2 | found : R==null | 11,13,15 | +| B3 | found : both non-null | 19--21 | +| B4 | successor : L==null | 19,20,21 | +| B5 | successor : L!=null | 20 | +| B6 | check_child_r : R==null | 16,17,18,19,20 | +| B7 | check_child_r : R!=null | 10,12,14,21 | +| B8 | simple_2 : parent==null | 10,11 | +| B9 | simple_2 : R direction | 12,15,21 | +| B10 | simple_2 : L direction | 13,14 | +| B11 | simple_3_4 : parent==null | 16 | +| B12 | simple_3_4 : parent!=null | 17--20 | +| B13 | simple_4 : color!=RED | (rebalancing) | +| B14 | simple_4 : R direction | 18,19 | +| B15 | simple_4 : L direction | 17,20 | + +All branches except B13 are exercised by the simple removal +tests. B13 is the entry to the complex rebalancing case, covered +by tests #22--#45. + +### Rust macro call-site coverage + +`remove_simple_2_child_replace!` expands at two call sites, +producing separate compiled code for each. Full branch coverage +requires exercising all three parent-direction branches at both +sites. + +| Site | Parent dir | Tests | +| ------- | ---------- | ------ | +| L-child | null | 11 | +| L-child | R | 15 | +| L-child | L | 13 | +| R-child | null | 10 | +| R-child | R | 12, 21 | +| R-child | L | 14 | + +`remove_recycle_node!` is a straight-line macro (no branches) +called from four sites: both `remove_simple_2_child_replace!` +expansions, simple case 3, and simple case 4. All sites are +exercised by the simple tests. + +Any macro used for decomposition must have its call-site +variants covered by tests. Free-stack state (empty vs +non-empty) is covered by the multi-step integration tests +(#46--#48). + +## Wikipedia reference -- rebalancing loop invariant + +Verbatim from Wikipedia, following the rebalancing algorithm: + +> The rebalancing loop of the delete operation has the following +> invariant: +> +> At the beginning of each iteration the black height of N +> equals the iteration number minus one, which means that in the +> first iteration it is zero and that N is a true black node in +> higher iterations. +> +> The number of black nodes on the paths through N is one less +> than before the deletion, whereas it is unchanged on all other +> paths, so that there is a black-violation at P if other paths +> exist. +> +> All other properties (including requirement 3) are satisfied +> throughout the tree. + +## Rebalancing cases + +Entry condition: a black leaf was removed from `parent.child[dir]`. +The rebalancing loop executes with `(parent, dir)`. + +Cases follow the Wikipedia algorithm numbering. Each case lists +both direction variants (dir_l where the removed node was a left +child, dir_r where it was a right child) unless noted otherwise. + +### Case 4: red parent, black sibling, black nephews + +Recolor sibling red and parent black. No rotation. + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: R key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=N1 next=-- + N0: B key=10 parent=-- L=-- R=N2 <- recolored B + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: R key=15 parent=N0 L=-- R=-- <- recolored R +``` + +Dir_r variant (remove key=15): mirror of above. + +### Case 6: black sibling, distant nephew red + +Single rotation at parent. Sibling takes parent's color, parent +and distant nephew become black. + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=N3 + N3: R key=20 parent=N2 L=-- R=-- + +After: + Header: root=N2 top=N1 next=-- + N0: B key=10 parent=N2 L=-- R=-- <- recolored B + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=15 parent=-- L=N0 R=N3 <- new root + N3: B key=20 parent=N2 L=-- R=-- <- recolored B +``` + +Dir_r variant (remove key=20): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=N3 R=-- + N2: B key=20 parent=N0 L=-- R=-- + N3: R key=3 parent=N1 L=-- R=-- + +After: + Header: root=N1 top=N2 next=-- + N0: B key=10 parent=N1 L=-- R=-- <- recolored B + N1: B key=5 parent=-- L=N3 R=N0 <- new root + N2: key=20 color=B parent=-- L=-- R=-- <- freed + N3: B key=3 parent=N1 L=-- R=-- <- recolored B +``` + +### Case 6: non-null new_child in rotation + +The rotation transfers sibling's child on the `dir` side to +parent. When that child is non-null, it must be reparented. + +The sibling must have both children non-null: the distant nephew +is RED (triggering case 6) and the close nephew is the new_child +that gets transferred. For a valid RBT with a black leaf on one +side and sibling bh=1 on the other, the sibling has two RED +children (both bh=0). + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=20 parent=N0 L=N3 R=N4 + N3: R key=15 parent=N2 L=-- R=-- + N4: R key=25 parent=N2 L=-- R=-- + +After: + Header: root=N2 top=N1 next=-- + N0: B key=10 parent=N2 L=-- R=N3 <- R=N3 (new_child) + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=20 parent=-- L=N0 R=N4 <- new root + N3: R key=15 parent=N0 L=-- R=-- <- reparented to N0 + N4: B key=25 parent=N2 L=-- R=-- <- recolored B +``` + +Dir_r variant (remove key=20): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=N3 R=N4 + N2: B key=20 parent=N0 L=-- R=-- + N3: R key=3 parent=N1 L=-- R=-- + N4: R key=7 parent=N1 L=-- R=-- + +After: + Header: root=N1 top=N2 next=-- + N0: B key=10 parent=N1 L=N4 R=-- <- L=N4 (new_child) + N1: B key=5 parent=-- L=N3 R=N0 <- new root + N2: key=20 color=B parent=-- L=-- R=-- <- freed + N3: B key=3 parent=N1 L=-- R=-- <- recolored B + N4: R key=7 parent=N0 L=-- R=-- <- reparented to N0 +``` + +### Case 5 + 6: close nephew red, distant nephew black + +Rotate sibling away from dir (case 5), then rotate parent toward +dir (case 6). + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=20 parent=N0 L=N3 R=-- + N3: R key=15 parent=N2 L=-- R=-- + +After: + Header: root=N3 top=N1 next=-- + N0: B key=10 parent=N3 L=-- R=-- <- recolored B + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=20 parent=N3 L=-- R=-- <- recolored B + N3: B key=15 parent=-- L=N0 R=N2 <- new root +``` + +Trace: case 5 sets `distant_nephew = sibling` (N2), then case 6 +sets `distant_nephew->color = BLACK`. N2 ends up BLACK, not RED. + +Dir_r variant (remove key=20): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=N3 + N2: B key=20 parent=N0 L=-- R=-- + N3: R key=7 parent=N1 L=-- R=-- + +After: + Header: root=N3 top=N2 next=-- + N0: B key=10 parent=N3 L=-- R=-- <- recolored B + N1: B key=5 parent=N3 L=-- R=-- <- recolored B + N2: key=20 color=B parent=-- L=-- R=-- <- freed + N3: B key=7 parent=-- L=N1 R=N0 <- new root +``` + +Trace: case 5 sets `distant_nephew = sibling` (N1), then case 6 +sets `distant_nephew->color = BLACK`. N1 ends up BLACK, not RED. + +### Case 3 + case 4: red sibling, then recolor + +Red sibling is rotated, making the old close_nephew the new +sibling. If the new sibling has two black/null nephews, recolor +(case 4). + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: R key=20 parent=N0 L=N3 R=N4 + N3: B key=15 parent=N2 L=-- R=-- + N4: B key=25 parent=N2 L=-- R=-- + +After: + Header: root=N2 top=N1 next=-- + N0: B key=10 parent=N2 L=-- R=N3 <- recolored B + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=20 parent=-- L=N0 R=N4 <- new root, recolored B + N3: R key=15 parent=N0 L=-- R=-- <- recolored R (case 4) + N4: B key=25 parent=N2 L=-- R=-- +``` + +Trace: case 3 rotates N0 left (N2 becomes root), recolors +`N0=RED, N2=BLACK`, new sibling = N3. New nephews both null -> +case 4 (parent N0 is RED): `N3.color = RED, N0.color = BLACK`. + +Dir_r variant (remove key=20): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: R key=5 parent=N0 L=N3 R=N4 + N2: B key=20 parent=N0 L=-- R=-- + N3: B key=3 parent=N1 L=-- R=-- + N4: B key=7 parent=N1 L=-- R=-- + +After: + Header: root=N1 top=N2 next=-- + N0: B key=10 parent=N1 L=N4 R=-- <- recolored B + N1: B key=5 parent=-- L=N3 R=N0 <- new root, recolored B + N2: key=20 color=B parent=-- L=-- R=-- <- freed + N3: B key=3 parent=N1 L=-- R=-- + N4: R key=7 parent=N0 L=-- R=-- <- recolored R (case 4) +``` + +Trace: case 3 rotates N0 right (N1 becomes root), recolors +`N0=RED, N1=BLACK`, new sibling = N4. New nephews both null -> +case 4 (parent N0 is RED): `N4.color = RED, N0.color = BLACK`. + +### Case 3 + case 6: red sibling, then distant nephew red + +After the case 3 rotation, the new sibling's distant nephew is +red. Jump directly to case 6. + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: R key=20 parent=N0 L=N3 R=N5 + N3: B key=15 parent=N2 L=-- R=N4 + N4: R key=17 parent=N3 L=-- R=-- + N5: B key=25 parent=N2 L=-- R=-- + +After: + Header: root=N2 top=N1 next=-- + N0: B key=10 parent=N3 L=-- R=-- <- recolored B + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=20 parent=-- L=N3 R=N5 <- new root, recolored B + N3: R key=15 parent=N2 L=N0 R=N4 <- recolored R + N4: B key=17 parent=N3 L=-- R=-- <- recolored B + N5: B key=25 parent=N2 L=-- R=-- +``` + +Trace: case 3 rotates N0 left (N2 becomes root), recolors +`N0=RED, N2=BLACK`, new sibling = N3. Distant nephew = +`N3.child[R]` = N4 (RED) -> case 6. Rotate N0 left again (N3 +becomes N0's parent), `N3.color = N0.color = RED`, +`N0.color = BLACK`, `N4.color = BLACK`. + +Dir_r variant (remove key=20): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: R key=5 parent=N0 L=N4 R=N3 + N2: B key=20 parent=N0 L=-- R=-- + N3: B key=7 parent=N1 L=N5 R=-- + N4: B key=3 parent=N1 L=-- R=-- + N5: R key=6 parent=N3 L=-- R=-- + +After: + Header: root=N1 top=N2 next=-- + N0: B key=10 parent=N3 L=-- R=-- <- recolored B + N1: B key=5 parent=-- L=N4 R=N3 <- new root, recolored B + N2: key=20 color=B parent=-- L=-- R=-- <- freed + N3: R key=7 parent=N1 L=N5 R=N0 <- recolored R + N4: B key=3 parent=N1 L=-- R=-- + N5: B key=6 parent=N3 L=-- R=-- <- recolored B +``` + +Trace: case 3 rotates N0 right (N1 becomes root), recolors +`N0=RED, N1=BLACK`, new sibling = N3. Distant nephew = +`N3.child[L]` = N5 (RED) -> case 6. Rotate N0 right again (N3 +becomes N0's parent), `N3.color = N0.color = RED`, +`N0.color = BLACK`, `N5.color = BLACK`. + +### Case 3 + case 5 + case 6: red sibling, then double rotation + +After the case 3 rotation, the new sibling's close nephew is red +and distant nephew is black. Case 5 rotates, then case 6 rotates. + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: R key=20 parent=N0 L=N3 R=N5 + N3: B key=15 parent=N2 L=N4 R=-- + N4: R key=13 parent=N3 L=-- R=-- + N5: B key=25 parent=N2 L=-- R=-- + +After: + Header: root=N2 top=N1 next=-- + N0: B key=10 parent=N4 L=-- R=-- <- recolored B + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: B key=20 parent=-- L=N4 R=N5 <- root, recolored B + N3: B key=15 parent=N4 L=-- R=-- <- recolored B + N4: R key=13 parent=N2 L=N0 R=N3 <- recolored R + N5: B key=25 parent=N2 L=-- R=-- +``` + +Trace: case 3 rotates N0 left (N2 becomes root), recolors +`N0=RED, N2=BLACK`, new sibling = N3. Distant nephew = +`N3.child[R]` = null -> not red. Close nephew = +`N3.child[L]` = N4 (RED) -> case 5. Case 5 rotates N3 right +(N4 takes N3's place), `N3.color = RED`, `N4.color = BLACK`, +`distant_nephew = N3`, `sibling = N4`. Case 6 rotates N0 left +(N4 takes N0's place), `N4.color = N0.color = RED`, +`N0.color = BLACK`, `N3.color = BLACK`. + +Dir_r variant (remove key=20): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: R key=5 parent=N0 L=N4 R=N3 + N2: B key=20 parent=N0 L=-- R=-- + N3: B key=7 parent=N1 L=-- R=N5 + N4: B key=3 parent=N1 L=-- R=-- + N5: R key=8 parent=N3 L=-- R=-- + +After: + Header: root=N1 top=N2 next=-- + N0: B key=10 parent=N5 L=-- R=-- <- recolored B + N1: B key=5 parent=-- L=N4 R=N5 <- root, recolored B + N2: key=20 color=B parent=-- L=-- R=-- <- freed + N3: B key=7 parent=N5 L=-- R=-- <- recolored B + N4: B key=3 parent=N1 L=-- R=-- + N5: R key=8 parent=N1 L=N3 R=N0 <- recolored R +``` + +Trace: case 3 rotates N0 right (N1 becomes root), recolors +`N0=RED, N1=BLACK`, new sibling = N3. Distant nephew = +`N3.child[L]` = null -> not red. Close nephew = +`N3.child[R]` = N5 (RED) -> case 5. Case 5 rotates N3 left +(N5 takes N3's place), `N3.color = RED`, `N5.color = BLACK`, +`distant_nephew = N3`, `sibling = N5`. Case 6 rotates N0 right +(N5 takes N0's place), `N5.color = N0.color = RED`, +`N0.color = BLACK`, `N3.color = BLACK`. + +### Case 2: propagation + +Black sibling, both nephews black, black parent. Recolor sibling +red and propagate upward with `node = parent`. + +Dir_l variant (remove key=5): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=N1 next=-- + N0: B key=10 parent=-- L=-- R=N2 + N1: key=5 color=B parent=-- L=-- R=-- <- freed + N2: R key=15 parent=N0 L=-- R=-- <- recolored R (case 2) +``` + +Trace: sibling N2 is BLACK, both nephews null, parent N0 is +BLACK -> case 2: `N2.color = RED`, `node = N0`, +`parent = N0.parent = null` -> loop exits (case 1: root). +Tree black height decreases by one uniformly. + +Dir_r variant (remove key=15): + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=10 parent=-- L=N1 R=N2 + N1: B key=5 parent=N0 L=-- R=-- + N2: B key=15 parent=N0 L=-- R=-- + +After: + Header: root=N0 top=N2 next=-- + N0: B key=10 parent=-- L=N1 R=-- + N1: R key=5 parent=N0 L=-- R=-- <- recolored R (case 2) + N2: key=15 color=B parent=-- L=-- R=-- <- freed +``` + +Trace: mirror of dir_l. + +### Case 2 + case 4: propagate then red parent + +Case 2 propagates upward, reaching a red parent. Case 4 recolors +and terminates. + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=20 parent=-- L=N1 R=N4 + N1: R key=10 parent=N0 L=N2 R=N3 + N2: B key=5 parent=N1 L=-- R=-- + N3: B key=15 parent=N1 L=-- R=-- + N4: B key=25 parent=N0 L=-- R=-- + +After (remove key=5): + Header: root=N0 top=N2 next=-- + N0: B key=20 parent=-- L=N1 R=N4 + N1: B key=10 parent=N0 L=-- R=N3 <- recolored B (case 4) + N2: key=5 color=B parent=-- L=-- R=-- <- freed + N3: R key=15 parent=N1 L=-- R=-- <- recolored R (case 4) + N4: B key=25 parent=N0 L=-- R=-- +``` + +Trace: N2 is black leaf, dir=L, parent=N1. Sibling N3 BLACK, +nephews null. `parent->color == RED` (N1 is RED) -> case 4: +`N3.color = RED`, `N1.color = BLACK`, return. Note: despite the +test name, this exercises case 4 directly (not case 2 then 4), +because the parent is already red on the first iteration. The +deeper tree (5 nodes vs 3 in the plain case 4 tests) exercises +the code path where the rebalanced subtree is not the root. + +### Case 2 + case 6: propagate then rotation + +Case 2 propagates upward, reaching a position where the distant +nephew is red. Case 6 rotates and terminates. + +The right subtree of the root must have bh=2 (matching the left) +with a RED distant nephew. This requires B(30) with B(25) left +and R(35) right, where R(35) has two B leaf children (33, 40), +giving bh=1 per child path under B(30). Total: 9 nodes. + +```text +Before: + Header: root=N0 top=-- next=-- + N0: B key=20 parent=-- L=N1 R=N4 + N1: B key=10 parent=N0 L=N2 R=N3 + N2: B key=5 parent=N1 L=-- R=-- + N3: B key=15 parent=N1 L=-- R=-- + N4: B key=30 parent=N0 L=N5 R=N6 + N5: B key=25 parent=N4 L=-- R=-- + N6: R key=35 parent=N4 L=N7 R=N8 + N7: B key=33 parent=N6 L=-- R=-- + N8: B key=40 parent=N6 L=-- R=-- + +After (remove key=5): + Header: root=N4 top=N2 next=-- + N0: B key=20 parent=N4 L=N1 R=N5 <- reparented, R=N5 + N1: B key=10 parent=N0 L=-- R=N3 + N2: key=5 color=B parent=-- L=-- R=-- <- freed + N3: R key=15 parent=N1 L=-- R=-- <- recolored R (case 2) + N4: B key=30 parent=-- L=N0 R=N6 <- new root + N5: B key=25 parent=N0 L=-- R=-- <- reparented to N0 + N6: B key=35 parent=N4 L=N7 R=N8 <- recolored B + N7: B key=33 parent=N6 L=-- R=-- + N8: B key=40 parent=N6 L=-- R=-- +``` + +Trace: N2 is black leaf, dir=L, parent=N1. Sibling N3 BLACK, +nephews null, parent N1 BLACK -> case 2: `N3.color = RED`, +`node = N1`, `parent = N0`. Recompute `dir = direction(N1) = L`. +Sibling = N0.child[R] = N4 (BLACK). Distant nephew = +N4.child[R] = N6 (RED) -> case 6. Rotate N0 left: N5 (B(25)) +reparented from N4.child[L] to N0.child[R]. +`N4.color = N0.color = B`, `N0.color = BLACK`, +`N6.color = BLACK`. + +## Case 6: parent null check variants + +Case 6 rotates parent. If parent is the tree root, the rotation +must update `tree.root`. If parent has a parent, the +great-grandparent's child pointer must be updated (left or right +depending on direction). + +| Variant | Parent position | Covers | +| --------------------- | ----------------------- | ------------------- | +| Parent is root | Parent has no parent | Root replacement | +| Parent is left child | Parent is GGP's L child | GGP.child[L] update | +| Parent is right child | Parent is GGP's R child | GGP.child[R] update | + +Each variant needs dir_l and dir_r sub-variants. + +### Case 3: parent null check + +Case 3 rotates parent. Same root vs. non-root variants as +case 6. + +## Test case summary + +### Input checks + +| # | Case | +| --- | ----------------- | +| 1 | Data too short | +| 2 | Data too long | +| 3 | Too few accounts | +| 4 | User has data | +| 5 | Tree is duplicate | + +### Search errors + +| # | Case | +| --- | ----------------- | +| 6 | Empty tree | +| 7 | Not found (left) | +| 8 | Not found (right) | +| 9 | Not found (deep) | + +### Simple removal (no rebalancing) + +| # | Case | Direction | +| --- | ------------------------------- | -------------- | +| 10 | One child at root (sc 2) | R child | +| 11 | One child at root (sc 2) | L child | +| 12 | One child non-root (sc 2) | R child, R pos | +| 13 | One child non-root (sc 2) | L child, L pos | +| 14 | One child non-root (sc 2) | R child, L pos | +| 15 | One child non-root (sc 2) | L child, R pos | +| 16 | Root leaf (sc 3) | -- | +| 17 | Red leaf (sc 4) | L | +| 18 | Red leaf (sc 4) | R | +| 19 | Successor immediate R (sc 1) | -- | +| 20 | Successor deep L descent (sc 1) | -- | +| 21 | Successor with R child (sc 1) | -- | + +### Rebalancing (complex case, black leaf removal) + +| # | Path | Dir | +| --- | ----------------------------- | --- | +| 22 | Case 4 | L | +| 23 | Case 4 | R | +| 24 | Case 6 | L | +| 25 | Case 6 | R | +| 26 | Case 5 + 6 | L | +| 27 | Case 5 + 6 | R | +| 28 | Case 3 → 4 | L | +| 29 | Case 3 → 4 | R | +| 30 | Case 3 → 6 | L | +| 31 | Case 3 → 6 | R | +| 32 | Case 3 → 5 → 6 | L | +| 33 | Case 3 → 5 → 6 | R | +| 34 | Case 2 (propagate to root) | L | +| 35 | Case 2 (propagate to root) | R | +| 36 | Case 2 → 4 | -- | +| 37 | Case 2 → 6 | -- | +| 38 | Case 6 non-null new_child | L | +| 39 | Case 6 non-null new_child | R | +| 40 | Case 6 parent=root | L | +| 41 | Case 6 parent=root | R | +| 42 | Case 6 parent=GGP left child | L | +| 43 | Case 6 parent=GGP right child | R | +| 44 | Case 3 parent=root | L | +| 45 | Case 3 parent=root | R | + +### Multi-step integration + +| # | Case | +| --- | --------------------------------- | +| 46 | Insert 3, remove 1 (minimal) | +| 47 | Insert 7, remove all (full cycle) | +| 48 | Insert-remove-insert (recycling) | + +## Multi-step integration tests + +Sequential operations that verify insert and remove interact +correctly. These use `build_empty_tree` to pre-allocate free +slots, then chain insert and remove instructions, asserting full +tree state after each step. + +| Test | Sequence | Purpose | +| ---------- | ---------------------- | ---------------- | +| Minimal | Insert 10,5,15; rm 5 | Basic remove | +| Full cycle | Insert 7 nodes; rm all | All nodes freed | +| Recycle | Insert 3; rm 1; insert | Reuse from stack | + +The recycle test is critical: after removing a node, the free +stack must correctly provide it for the next insert. This +validates the `StackNode.next` chain and `header.top` updates +across both operations. + +## Coverage notes + +The test list above covers all paths through the Wikipedia +rebalancing algorithm, both direction variants, successor swap +edge cases, and the `rotate_subtree` null/non-null checks. As +optimizations are introduced (inlined rotations, hardcoded +direction branches), additional test cases may be needed to +ensure full branch coverage of the new code paths. Each +optimization should reference which existing tests cover it and +add new cases for any branches not yet exercised. + +## Reference algorithm + +The verbatim Wikipedia remove-rebalancing algorithm (source of +truth for case numbering and control flow): + +```c +void remove(Tree* tree, Node* node) { + Node* parent = node->parent; + + Node* sibling; + Node* close_nephew; + Node* distant_nephew; + + Direction dir = direction(node); + + parent->child[dir] = NULL; + goto start_balance; + + do { + dir = direction(node); +start_balance: + sibling = parent->child[1 - dir]; + distant_nephew = sibling->child[1 - dir]; + close_nephew = sibling->child[dir]; + if (sibling->color == RED) { + // Case #3 + rotate_subtree(tree, parent, dir); + parent->color = RED; + sibling->color = BLACK; + sibling = close_nephew; + + distant_nephew = sibling->child[1 - dir]; + if (distant_nephew + && distant_nephew->color == RED) { + goto case_6; + } + close_nephew = sibling->child[dir]; + if (close_nephew + && close_nephew->color == RED) { + goto case_5; + } + + // Case #4 + sibling->color = RED; + parent->color = BLACK; + return; + } + + if (distant_nephew + && distant_nephew->color == RED) { + goto case_6; + } + + if (close_nephew + && close_nephew->color == RED) { + goto case_5; + } + + if (!parent) { + // Case #1 + return; + } + + if (parent->color == RED) { + // Case #4 + sibling->color = RED; + parent->color = BLACK; + return; + } + + // Case #2 + sibling->color = RED; + node = parent; + + } while (parent = node->parent); + +case_5: + + rotate_subtree(tree, sibling, 1 - dir); + sibling->color = RED; + close_nephew->color = BLACK; + distant_nephew = sibling; + sibling = close_nephew; + +case_6: + + rotate_subtree(tree, parent, dir); + sibling->color = parent->color; + parent->color = BLACK; + distant_nephew->color = BLACK; + return; +} +``` + +The test cases map to paths through this algorithm: + +- **Case 2**: the `sibling->color = RED; node = parent` path, + loop continues. +- **Case 3**: `sibling->color == RED` branch, then falls through + to check nephews of the new sibling. +- **Case 4**: `parent->color == RED` recolor (both direct and + after case 3). +- **Case 5**: `close_nephew` is red, `goto case_5` from either + the case 3 block or the main block. +- **Case 6**: `distant_nephew` is red, `goto case_6` from either + the case 3 block or the main block. diff --git a/examples/tree/specs/remove.md b/examples/tree/specs/remove.md new file mode 100644 index 00000000..968aa3fb --- /dev/null +++ b/examples/tree/specs/remove.md @@ -0,0 +1,612 @@ + + + + + + +# Remove instruction specification + +## Scope + +Implementation of the remove instruction for `tree.s` (assembly) +and `program.rs` (Rust). Covers input validation, BST search, node +deletion with in-order successor swap, red-black tree rebalancing, +node recycling to the free stack, and return value encoding. + +## Implementation phases + +The implementation proceeds incrementally: + +1. **Branching, input checks, error codes.** Add the + `KEY_DOES_NOT_EXIST` error code, entrypoint discriminator + branch, and input validation for both `tree.s` and `program.rs`. +1. **Search.** BST key lookup returning the found node or error. +1. **BST delete preparation and simple removal.** Successor swap, + simple cases 1--4. +1. **Rebalancing.** Complex case (black leaf) fixup loop using + `rotate_subtree`. +1. **Node recycling and return value.** Free-stack push and + `RemoveReturn` encoding. + +Later phases may optimize rotations (inline, hardcoded branches) +once the baseline implementation is correct. + +## Interface changes + +### Error codes + +Add `KEY_DOES_NOT_EXIST` after `KEY_EXISTS` in the `error_codes!` +invocation in `interface/src/common.rs`: + +```rust +/// Key does not exist in tree during removal. +KEY_DOES_NOT_EXIST, +``` + +This increments `N_CODES` from 14 to 15. `REMOVE_STATUS_OK` +(defined as `error::N_CODES as u16`) automatically updates to 15. +Assembly constants are generated by the build script and do not +need manual updates. + +### Entrypoint + +Add remove discriminator branch in both implementations. In +`tree.s`: + +```asm +jeq r7, INSN_DISCRIMINATOR_INSERT, insert +jeq r7, INSN_DISCRIMINATOR_REMOVE, remove +jeq r7, INSN_DISCRIMINATOR_INITIALIZE, initialize +``` + +In `program.rs`, add a branch for `DISCRIMINATOR_REMOVE` in the +entrypoint function. + +## Input validation + +Identical to insert's general-instruction checks: + +- Instruction data length equals `SIZE_OF_REMOVE_INSTRUCTION` + (3 bytes). +- At least `N_ACCOUNTS_GENERAL` (2) accounts passed. +- User account has zero data length. +- Tree account is not a duplicate. + +Account layout: user followed by tree account (same as insert's +two-account path). + +## Algorithm + +### Overview + +1. Validate inputs. +1. Search for the key in the tree. +1. If not found, return `KEY_DOES_NOT_EXIST`. +1. Save `value = node.value` for the return. +1. If the node has two children, copy key/value from in-order + successor to node, then delete the successor instead. +1. Handle simple removal cases (no rebalancing). +1. Handle complex case: black leaf removal with rebalancing. +1. Recycle the removed node to the free stack. +1. Return `RemoveReturn { value, REMOVE_STATUS_OK }`. + +### Wikipedia reference -- simple cases + +Verbatim from Wikipedia, preceding the rebalancing algorithm: + +> When the deleted node has 2 children (non-NULL), then we can +> swap its value with its in-order successor (the leftmost child +> of the right subtree), and then delete the successor instead. +> Since the successor is leftmost, it can only have a right +> child (non-NULL) or no child at all. +> +> When the deleted node has only 1 child (non-NULL). In this +> case, just replace the node with its child, and color it +> black. The single child (non-NULL) must be red according to +> conclusion 5, and the deleted node must be black according to +> requirement 3. +> +> When the deleted node has no children (both NULL) and is the +> root, replace it with NULL. The tree is empty. +> +> When the deleted node has no children (both NULL), and is red, +> simply remove the leaf node. +> +> When the deleted node has no children (both NULL), and is +> black, deleting it will create an imbalance, and requires a +> rebalance, as covered in the next section. +> +> **Removal of a black non-root leaf** +> +> The complex case is when N is not the root, colored black and +> has no proper child (⇔ only NULL children). In the first +> iteration, N is replaced by NULL. + +Mapping to cases and implementation steps: + +- Simple case 1 (2 children) → Step 2 (successor swap). +- Simple case 2 (1 child) → Step 3. +- Simple case 3 (root leaf) → Step 3. +- Simple case 4 (red leaf) → Step 3. +- Complex case (black leaf) → Step 4 (rebalancing). + +### Step 1 -- search + +Based on the insert search pattern but without inlined fixup +sections. The loop walks left/right with explicit branches and +returns either the matching node or an error. + +Assembly structure: + +```asm +remove_search: + ldxh r4, [r2 + INSN_REMOVE_KEY_OFF] # r4 = key + ldxdw r3, [r1 + IB_TREE_DATA_ROOT_OFF] # r3 = cursor = root + jeq r3, NULL, e_key_does_not_exist + +remove_search_loop: + ldxh r5, [r3 + TREE_NODE_KEY_OFF] # r5 = cursor.key + jeq r4, r5, remove_found # found + jgt r4, r5, remove_search_r + +remove_search_l: + ldxdw r3, [r3 + TREE_NODE_CHILD_L_OFF] + jne r3, NULL, remove_search_loop + mov64 r0, E_KEY_DOES_NOT_EXIST + exit + +remove_search_r: + ldxdw r3, [r3 + TREE_NODE_CHILD_R_OFF] + jne r3, NULL, remove_search_loop + mov64 r0, E_KEY_DOES_NOT_EXIST + exit + +remove_found: + # r3 = found node +``` + +Rust uses the existing `search()` function, which has the same +logic in compact form. + +### Step 2 -- BST delete preparation (simple case 1) + +**Simple case 1 -- two children** (both children non-null): + +Find the in-order successor (leftmost node in the right subtree) +and copy its data: + +```text +successor = node.child[R] +while successor.child[L] != null: + successor = successor.child[L] +node.key = successor.key +node.value = successor.value +node = successor +``` + +After this step, `node` has at most one child. An in-order +successor has no left child; it may have a right child. + +If `node` originally had zero or one child, this step is skipped. + +### Step 3 -- removal + +Let `child` be the non-null child of `node` (or null if `node` is +a leaf), and `parent = node.parent`. + +**Simple case 2 -- one child** (`child != null`): + +In a valid red-black tree, a node with exactly one child must be +black, and the child must be red. Replace node with child and +recolor: + +```text +child.parent = parent +if parent != null: + parent.child[direction(node)] = child +else: + tree.root = child +child.color = Black +``` + +No rebalancing needed. + +**Simple case 3 -- root leaf** (`parent == null`, no children): + +```text +tree.root = null +``` + +No rebalancing needed. + +**Simple case 4 -- red leaf** (`node.color == Red`, +`parent != null`): + +```text +parent.child[direction(node)] = null +``` + +No rebalancing needed. + +**Complex case -- black leaf** (`node.color == Black`, +`parent != null`): + +Detach the node, then rebalance. The direction must be computed +before detaching because `direction(node)` compares `node` against +`parent.child[DIR_R]`, which is cleared by the detach: + +```text +dir = direction(node) +parent.child[dir] = null +# Execute rebalancing with (parent, dir). +``` + +### Step 4 -- rebalancing (complex case only) + +Verbatim from Wikipedia, following the rebalancing algorithm: + +> The rebalancing loop of the delete operation has the following +> invariant: +> +> At the beginning of each iteration the black height of N +> equals the iteration number minus one, which means that in the +> first iteration it is zero and that N is a true black node in +> higher iterations. +> +> The number of black nodes on the paths through N is one less +> than before the deletion, whereas it is unchanged on all other +> paths, so that there is a black-violation at P if other paths +> exist. +> +> All other properties (including requirement 3) are satisfied +> throughout the tree. + +Entry: `parent` is the former parent of the removed node, `dir` is +the direction of the removed node relative to `parent`. + +Based on the Wikipedia red-black tree deletion algorithm. + +The first implementation uses the generic `rotate_subtree` helper +for all rotations. Inlining and direction-hardcoded branches can +be applied as later optimizations (see "Rotation inlining +optimizations" below). + +The verbatim reference algorithm (source of truth): + +```c +void remove(Tree* tree, Node* node) { + Node* parent = node->parent; + + Node* sibling; + Node* close_nephew; + Node* distant_nephew; + + Direction dir = direction(node); + + parent->child[dir] = NULL; + goto start_balance; + + do { + dir = direction(node); +start_balance: + sibling = parent->child[1 - dir]; + distant_nephew = sibling->child[1 - dir]; + close_nephew = sibling->child[dir]; + if (sibling->color == RED) { + // Case #3 + rotate_subtree(tree, parent, dir); + parent->color = RED; + sibling->color = BLACK; + sibling = close_nephew; + + distant_nephew = sibling->child[1 - dir]; + if (distant_nephew && distant_nephew->color == RED) { + goto case_6; + } + close_nephew = sibling->child[dir]; + if (close_nephew && close_nephew->color == RED) { + goto case_5; + } + + // Case #4 + sibling->color = RED; + parent->color = BLACK; + return; + } + + if (distant_nephew && distant_nephew->color == RED) { + goto case_6; + } + + if (close_nephew && close_nephew->color == RED) { + goto case_5; + } + + if (!parent) { + // Case #1 + return; + } + + if (parent->color == RED) { + // Case #4 + sibling->color = RED; + parent->color = BLACK; + return; + } + + // Case #2 + sibling->color = RED; + node = parent; + + } while (parent = node->parent); + +case_5: + + rotate_subtree(tree, sibling, 1 - dir); + sibling->color = RED; + close_nephew->color = BLACK; + distant_nephew = sibling; + sibling = close_nephew; + +case_6: + + rotate_subtree(tree, parent, dir); + sibling->color = parent->color; + parent->color = BLACK; + distant_nephew->color = BLACK; + return; +} +``` + +Notes on the algorithm structure: + +- The `goto start_balance` at entry skips the + `dir = direction(node)` recomputation because `dir` was already + set before detaching the node in step 3. +- The `if (!parent)` check (case 1) is unreachable in this + do-while structure: the first iteration enters via + `goto start_balance` with a non-null parent from step 3, and + subsequent iterations only enter when the while condition + `parent = node->parent` is non-null. It is included for + completeness with the Wikipedia source. +- Case 5 and case 6 are placed outside the loop. The `goto` + targets preserve `dir` from the current iteration. + +### Step 5 -- node recycling + +Null both child pointers (insert does not overwrite these fields +when reusing a node from the stack) and push onto the free stack: + +```text +node.child[L] = null +node.child[R] = null +(StackNode)node.next = tree_header.top +tree_header.top = (StackNode)node +``` + +`StackNode.next` occupies offset 0, which overlaps with +`TreeNode.parent`. Insert overwrites the parent field, so +rewriting it here is harmless. The key, value, and color fields +are also overwritten by insert, so they do not need clearing. + +### Return value + +On success, return `r0 = 0` (`SUCCESS`). The SVM only persists +account modifications when the program returns zero; any non-zero +`r0` is treated as `ProgramError::Custom(r0)` and all account +changes are reverted. This means the removed value cannot be +encoded in `r0`. + +The removed value can be read from the freed node after +execution: since remove does not clear the freed node's `key`, +`value`, or `color` fields, the caller can dereference +`header.top` in the resulting account data to recover the +value that was associated with the removed key. + +## Rust implementation notes + +- Add `remove()` with `#[inline(always)]` and the same parameter + signature as `insert()`. + +- Use the existing `search()` function for key lookup. + +- Use `rotate_subtree()` for rebalancing cases 3, 5, and 6. + +- Add a `direction()` helper: + + ```rust + #[inline(always)] + unsafe fn direction( + node: *const TreeNode, + ) -> usize { + (node == (*(*node).parent).child[tree::DIR_R]) as usize + } + ``` + +- The `goto case_5` / `goto case_6` exits from the rebalancing + loop do not map directly to Rust. Two options: + + - Break out of the loop with a flag or enum indicating which + case to execute, then handle case 5/6 after the loop. + - Inline case 5+6 at each goto site (four sites: two inside + the red-sibling block, two outside). + +## Rust implementation — macro-based decomposition + +The Rust remove function uses nested conditional checks with +macros for code reuse. After finding the node to remove and +saving `value = node.value`: + +- Check if left child exists. If yes, check right child: + - Both children present → **simple case 1** (successor swap). + Copy key/value from the in-order successor to the found + node, then reassign `node = successor`. Fall through to + check the successor's right child. + - Left child only → **simple case 2** (L-only child). Invoke + `remove_simple_2_child_replace!` with + `child = node.child[L]`. +- Check right child (reached when no left child, or after + simple case 1): + - Right child present → **simple case 3** (R-only child). + Invoke `remove_simple_2_child_replace!` with + `child = node.child[R]`. + - No right child → **simple case 4** (leaf removal). Handle + root-leaf, red-leaf, or black-leaf (complex case) inline. + +`remove_simple_2_child_replace!` handles child replacement at +two call sites (one for the L-child case, one for the R-child +case). Each expansion produces independent compiled code. +`remove_recycle_node!` handles recycling the removed node at +each removal path. + +This macro-based structure with nested `if/else` produces +better compiler output than a linear restructure. The compiler +optimizes each macro expansion independently, producing tighter +code than a unified block. + +## Recycle inlining optimization + +The six `ja remove_recycle` jumps in `tree.s` are each inlined with +the 5-instruction recycle sequence plus `exit`. The +`remove_recycle:` label is preserved as a reference comment in the +assembly source. + +Rationale: this matches what the Rust compiler already does. The +Rust `remove()` function has no shared recycle subroutine — each +removal path inlines the free-stack push and exit. Inlining in +assembly eliminates six jumps to a shared epilogue, trading code +size for fewer branch instructions. + +The inlined sequence at each site: + +- Null both child pointers (`child[L]` and `child[R]`). +- Load current `tree_header.top`. +- Set `node.next = old_top` (using the `StackNode.next` overlay at + offset 0). +- Set `tree_header.top = node`. +- `exit`. + +## Child replacement — assembly vs Rust + +The assembly uses a single `remove_simple_2_child_l` block for +both the L-only child path (simple case 2) and the R-only child +path reached via `remove_check_child_r` (simple case 3). Both +paths load the child pointer into `r4` before jumping to the +shared block, regardless of direction. In assembly, explicit +register management makes a unified block natural. + +The Rust intentionally uses two `remove_simple_2_child_replace!` +call sites rather than a unified block. Each call site handles a +different direction (L-child vs R-child path), and the compiler +optimizes each expansion independently. This produces tighter +code than a single shared block because the compiler can +specialize each path. + +The two approaches achieve equivalent results: + +- Assembly: unified block with child pointer in `r4`, + direction-agnostic. +- Rust: separate macro expansions, compiler-optimized per path. + +## Assembly implementation notes + +The remove instruction label is added after the insert instruction +in `tree.s`. Input validation follows the same pattern as +`insert_input_checks`. + +The first implementation uses `rotate_subtree` with a runtime +`dir` register. The `goto`-based control flow maps directly to +assembly `ja` instructions. Later optimization passes may inline +rotations or introduce hardcoded dir_l/dir_r branches. + +## Rotation inlining optimizations + +When `rotate_subtree` is eventually inlined, several checks inside +the generic rotation can be eliminated based on red-black tree +invariants that hold at each call site. + +### Generic `rotate_subtree` checks + +The generic helper has two conditional branches: + +1. **`new_child` null check**: if `new_child` is non-null, + reparent it to `subtree`. +1. **`parent` null check**: if `subtree.parent` is null, update + `tree.root`; otherwise update the parent's child pointer (with + a direction comparison). + +### Case 3: `rotate(tree, parent, dir)` + +Sibling is red. In a valid red-black tree, a red node must have +two non-null black children. The deficit side (where the black +leaf was removed) requires equal black height through the sibling +subtree, which means both `close_nephew` and `distant_nephew` are +non-null. + +After rotation, `new_child = close_nephew`, which is the sibling's +child on the `dir` side. + +- **`new_child` null check: eliminate.** `close_nephew` is + guaranteed non-null (red sibling invariant). +- **`parent` null check: keep.** `parent` could be the tree root. + +### Case 5: `rotate(tree, sibling, 1 - dir)` + +Sibling is black and is `parent.child[1 - dir]`. Parent is +guaranteed non-null (case 1 / while-condition filters null +parents before reaching case 5). + +After rotation, `new_child = close_nephew.child[1 - dir]`. +`close_nephew` is red (that is the condition for entering case 5), +but its children may be null black leaves. + +- **`new_child` null check: keep.** `close_nephew`'s children may + be null. +- **`parent` null check: eliminate.** `sibling.parent = parent`, + which is non-null. Additionally, the direction of sibling + relative to parent is known (`1 - dir`), so the child pointer + update can be hardcoded to `parent.child[1 - dir] = new_root` + without a direction comparison. + +### Case 6: `rotate(tree, parent, dir)` + +Sibling is black, `distant_nephew` is red. `new_child` is +`sibling.child[dir]`, which may be null. + +- **`new_child` null check: keep.** `sibling.child[dir]` may be + null. +- **`parent` null check: depends on entry path.** + - **After case 3:** Parent was rotated in case 3, making it a + child of the old sibling (now grandparent). Parent's parent + is non-null. **Eliminate.** + - **Direct entry (no case 3):** Parent may be the tree root. + **Keep.** + - Splitting case 6 into post-case-3 and direct variants allows + the check to be eliminated at the first site. + +### Summary + +| Rotation | `new_child` null | `parent` null | Direction hardcode | +| -------- | ---------------- | --------------- | ------------------ | +| Case 3 | eliminate | keep | no | +| Case 5 | keep | eliminate | yes (`1 - dir`) | +| Case 6 | keep | keep or elim.\* | no | + +\*Can be eliminated when case 6 follows case 3. + +## Key invariants + +- `direction(node)` requires `node.parent` to be non-null. This + holds at every call site: the complex case guarantees a non-null + parent at entry, and the while condition filters null parents on + re-entry. +- A node with exactly one child must be black with a red child. + This follows from red-black tree properties (equal black height + on all paths). +- The in-order successor has no left child. It may have a right + child, which is handled by simple case 2 after the swap. +- After case 3 (red sibling rotation), the new sibling is + `close_nephew` from before the rotation. The algorithm + reassigns `sibling = close_nephew` before checking nephews. +- A red node must have two non-null black children. This guarantees + that both of a red sibling's children are non-null in case 3. diff --git a/examples/tree/specs/rotate-subtree-specialization.md b/examples/tree/specs/rotate-subtree-specialization.md new file mode 100644 index 00000000..1117ece1 --- /dev/null +++ b/examples/tree/specs/rotate-subtree-specialization.md @@ -0,0 +1,131 @@ +# Rotate subtree specialization + +## Scope + +Inline the two `rotate_subtree` calls in the insert fixup case 5/6 +block of `program.rs` with specialized logic that exploits +invariants known at each call site. This matches the assembly, +where each rotation is expanded inline with hardcoded directions. +The generic `rotate_subtree` is preserved for potential use by +delete or other operations. + +## Convention + +`dir` is "the direction of parent relative to grandparent", +computed by `direction(parent)`. All comments reference `dir` and +`opposite(dir)` rather than concrete LEFT/RIGHT. + +## Case 5: rotate(parent, dir) + +Replace `rotate_subtree(tree_header, parent, dir)` with: + +```rust +// Case 5: rotate parent in dir. +// +// Grandparent is guaranteed non-null by the case 4 check, so +// no root-replacement path is needed. Parent is known to be +// grandparent.child[dir] from the direction() call, so the +// child pointer update is hardcoded without comparison. +{ + let new_root = (*parent).child[opposite(dir)]; + let new_child = (*new_root).child[dir]; + + (*parent).child[opposite(dir)] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[dir] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[dir] = new_root; + + node = parent; + parent = new_root; +} +``` + +Optimizations vs generic `rotate_subtree`: + +- **No null check on grandparent**: Case 4 already checked and + returned if grandparent were null. +- **No pointer comparison for child update**: Parent is + `grandparent.child[dir]` by definition, so + `grandparent.child[dir] = new_root` is correct without + comparing `subtree == parent.child[DIR_R]`. +- **No `tree` parameter needed**: The root is never updated. + +The `parent = (*grandparent).child[dir]` load from the original +code is replaced by `parent = new_root`, which is the same pointer +(the rotation placed new_root at `grandparent.child[dir]`). + +## Case 6: rotate(grandparent, opposite(dir)) + +Replace `rotate_subtree(tree_header, grandparent, opposite(dir))` +with: + +```rust +// Case 6: rotate grandparent in opposite(dir). +// +// The new root of this rotation is parent +// (= grandparent.child[dir]), which the caller already has, +// eliminating the generic version's load of +// subtree.child[opposite(direction)]. +// +// Great-grandparent may be null (grandparent could be root), +// so the null check and root-replacement path are retained. +// Grandparent's position under great-grandparent is unrelated +// to dir, so the pointer comparison is also retained. +{ + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[opposite(dir)]; + + (*grandparent).child[dir] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[opposite(dir)] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + let idx = (grandparent + == (*great_grandparent).child[tree::DIR_R]) + as usize; + (*great_grandparent).child[idx] = parent; + } else { + (*tree_header).root = parent; + } +} +``` + +Optimizations vs generic `rotate_subtree`: + +- **No new_root load**: The generic version loads + `subtree.child[opposite(direction)]`. Here, new_root is + `parent`, already in scope. +- **Great-grandparent null check retained**: Grandparent may be + root. +- **Pointer comparison retained**: `dir` describes parent's + position under grandparent, not grandparent's position under + great-grandparent. + +## Resulting caller + +```rust +if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5. + if node == (*parent).child[opposite(dir)] { + // + } + + // Case 6. + // + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; +} +``` diff --git a/examples/tree/specs/test-framework-cleanup.md b/examples/tree/specs/test-framework-cleanup.md new file mode 100644 index 00000000..5da2803d --- /dev/null +++ b/examples/tree/specs/test-framework-cleanup.md @@ -0,0 +1,280 @@ + + + + + + +# Test framework cleanup specification + +## Scope + +Recommendations for reducing duplication, improving consistency, +and simplifying the test harness across `tests.rs`, `init.rs`, +`insert.rs`, and `entrypoint.rs`. + +## 1. Unify error-checking helpers + +### Problem + +Three distinct error-match patterns are copy-pasted throughout the +test modules: + +- **`check_error`** (shared) handles `ProgramError::Custom(code)`. +- **Inline match for stdlib errors** (`InvalidRealloc`, + `NotEnoughAccountKeys`, `Custom(1)`) is duplicated in + `AllocMaxDataLen`, `UserInsufficientLamports`, and + `SystemProgramAddress`. +- **Success-with-validation** is duplicated in `AllocHappyPath` + and `CreateAccountHappyPath`. + +### Proposal + +Generalize `check_error` into `check_result` that accepts a +`ProgramError` directly: + +```rust +fn check_result( + setup: &TestSetup, + instruction: &Instruction, + accounts: &[(Pubkey, Account)], + expected: ProgramError, +) -> CaseResult { ... } +``` + +The existing `check_error` becomes a thin wrapper: + +```rust +fn check_error(..., code: error_codes::error) -> CaseResult { + check_result(..., ProgramError::Custom(code.into())) +} +``` + +This eliminates the inline match blocks in `AllocMaxDataLen`, +`UserInsufficientLamports`, and `SystemProgramAddress`. + +## 2. Merge `NodeDesc` and `ExpectedNode` + +### Problem + +`NodeDesc` and `ExpectedNode` are structurally identical. The only +behavioral difference is that `node()` sets `value = key` while +`expected()` takes an explicit value. Two types for the same shape +adds noise. + +### Proposal + +Single type `NodeSpec` with one constructor and a builder method: + +```rust +struct NodeSpec { + key: u16, + value: u16, + color: u8, + parent: Option, + left: Option, + right: Option, +} + +fn node(key, color, parent, left, right) -> NodeSpec { + NodeSpec { key, value: key, color, parent, left, right } +} +``` + +For expected nodes where value differs from key (the inserted node), +use a builder: + +```rust +impl NodeSpec { + fn val(mut self, v: u16) -> Self { self.value = v; self } +} +``` + +Usage: + +```rust +// Pre-existing node: value = key (default). +node(10, B, None, Some(1), None) +// Inserted node: value = 1. +node(42, R, Some(0), None, None).val(1) +``` + +`TreeDesc` and `ExpectedTree` merge into a single `TreeSpec` with +an optional `top` field (`TreeDesc` never sets it explicitly — +`build_tree_account` computes it; `ExpectedTree` specifies it for +assertion). Making `top` `Option>` or always +computing it from context keeps one type. + +## 3. Reduce `fixed_costs` match verbosity + +### Problem + +`InitCase::fixed_costs()` enumerates all 17 zero-cost variants +explicitly. Adding a new input-check variant requires updating the +match arm. `InsertCase::fixed_costs()` uses `_ => 0` — simpler +and forward-compatible. + +### Proposal + +All `fixed_costs()` implementations should use `_ => 0` as the +default arm. Only list the non-zero cases explicitly. + +## 4. Fix `PdaMismatchChunk` display name indexing + +### Problem + +Variant names are `PdaMismatchChunk0..3` but display names are +"PDA mismatch chunk 1..4". The off-by-one makes the name not match +the variant. + +### Proposal + +Display as "PDA mismatch chunk 0..3" to match the variant names. + +## 5. Remove unused `allow_asm_failures` / `allow_rust_failures` + +### Problem + +Both parameters are always `false` in every call site. They add two +booleans to every `print_comparison_table` call that are never +exercised. + +### Proposal + +Remove both parameters. If per-case failure tolerance is ever +needed, add it as a `TestCase` trait method (`fn allow_failure`) +so the table function doesn't need configuration. + +## 6. Factor common setup into builders + +### Problem + +`insert_setup`, `insert_skip_alloc_setup`, `insert_max_data_setup`, +`init_setup`, and `pda_init_setup` share the same boilerplate: + +1. Create `TestSetup`. +1. Get system program and rent sysvar keyed accounts. +1. Create user and tree pubkeys. +1. Build instruction with account metas. +1. Assemble `(Pubkey, Account)` vec. + +Steps 2-5 are nearly identical across all five functions. The +differences are: + +- Whether rent is configured (SIMD-0194 threshold). +- Tree pubkey derivation (unique vs PDA). +- Which accounts are included (2 vs 4). +- Tree account initial state (empty, header-only, pre-built, max + data length). +- Instruction data (init discriminator vs insert struct). + +### Proposal + +Introduce a `SetupBuilder` that handles the common parts: + +```rust +struct SetupBuilder { + setup: TestSetup, + user: (Pubkey, Account), + tree: (Pubkey, Account), + system_program: Option<(Pubkey, Account)>, + rent_sysvar: Option<(Pubkey, Account)>, +} +``` + +Methods on the builder configure the tree account, account list, +and instruction, then finalize into the existing +`(TestSetup, Instruction, Vec<(Pubkey, Account)>)` tuple. Each +existing setup function becomes a few builder calls. + +Weigh this against the cost of added indirection — if the builder +doesn't clearly reduce total lines, keep the current explicit +functions but extract just the shared preamble (getting +system_program/rent keyed accounts) into a helper. + +## 7. Share imports via parent module + +### Problem + +Each test module imports `mollusk_svm::program`, `AccountMeta`, +`Rent`, `Check`, `Config` independently. These are used by both +`init.rs` and `insert.rs`. + +### Proposal + +Re-export the common types from `tests.rs` so that `use super::*` +covers them: + +```rust +// tests.rs +use mollusk_svm::program; +use mollusk_svm::result::{Check, Config}; +use pinocchio::sysvars::rent::Rent; +use solana_sdk::instruction::AccountMeta; +``` + +The child modules already use `use super::*` and would pick these +up without additional imports. + +## 8. Unify address-mismatch helpers + +### Problem + +`run_address_mismatch` (init) and `insert_alloc_address_mismatch` +(insert) do the same bit-flip logic with different setup functions +and word sizes. The core pattern — flip a byte in a pubkey chunk +to trigger a mismatch — is identical. + +### Proposal + +Extract the bit-flip operation into a shared helper in `tests.rs`: + +```rust +fn flip_account_address( + instruction: &mut Instruction, + accounts: &mut [(Pubkey, Account)], + account_index: usize, + chunk_index: usize, + chunk_size: usize, +) { + let flip_index = (chunk_index * chunk_size) + chunk_size - 1; + accounts[account_index].0.as_mut()[flip_index] ^= 1; + instruction.accounts[account_index].pubkey = + accounts[account_index].0; +} +``` + +The module-specific helpers become: call setup, call +`flip_account_address`, call `check_error`. + +## 9. Implement multi-insert integration tests + +### Problem + +The insert-tests spec (section "Multi-insert integration tests") +describes five sequential-insert test sequences. These are not yet +implemented. + +### Proposal + +Implement them as specified, using the existing `build_tree_account` +and `assert_tree_account` infrastructure. Each test processes +multiple insert instructions in sequence, feeding resulting account +state forward. + +## Priority + +Recommendations ordered by impact-to-effort ratio: + +1. **Fix PDA mismatch display names** — trivial, correctness. +1. **Remove `allow_*_failures` params** — trivial, noise + reduction. +1. **Unify `check_error` → `check_result`** — small change, + removes three inline match blocks. +1. **Default `_ => 0` for `fixed_costs`** — one-line fix. +1. **Share imports via parent** — small, removes duplicate + imports. +1. **Merge `NodeDesc`/`ExpectedNode`** — moderate, cleaner API. +1. **Extract address-flip helper** — moderate, deduplicates. +1. **Factor setup builders** — larger refactor, weigh benefit. +1. **Multi-insert integration tests** — new feature, already + specced. diff --git a/examples/tree/specs/tree-invariants.md b/examples/tree/specs/tree-invariants.md new file mode 100644 index 00000000..9ae752e5 --- /dev/null +++ b/examples/tree/specs/tree-invariants.md @@ -0,0 +1,240 @@ + + + + + + +# Tree invariant specification + +## Scope + +Defines the structural invariants that every `TreeSpec` must +satisfy. These invariants are independent of the insert/remove +algorithm logic -- they follow directly from the definitions of a +binary search tree and a red-black tree. Test helpers in +`tests/common.rs` should verify these invariants on every +fabricated tree description (both before and after states) so that +specification errors surface as invariant violations rather than +silently producing incorrect test expectations. + +## Binary search tree invariant + +From Wikipedia: + +> A binary search tree is a rooted binary tree in which nodes are +> arranged in strict total order in which the nodes with keys +> greater than any particular node A is stored on the right +> sub-trees to that node A and the nodes with keys equal to or +> less than A are stored on the left sub-trees to A, satisfying +> the binary search property. + +### BST-1: ordering + +For every node N in the tree: + +- Every key in the left subtree of N is strictly less than N.key. +- Every key in the right subtree of N is strictly greater than + N.key. + +Note: the tree does not permit duplicate keys. Insert rejects +`KEY_EXISTS` and remove uses strict equality for lookup. + +### BST-2: parent-child consistency + +For every non-root node N: + +- N.parent is non-null. +- N appears as exactly one of `N.parent.child[L]` or + `N.parent.child[R]`. + +For the root node: + +- N.parent is null. +- `header.root` points to N. + +For every node N with a non-null child C at position `child[d]`: + +- C.parent equals N. + +## Red-black tree invariants + +From Wikipedia: + +> In addition to the requirements imposed on a binary search tree +> the following must be satisfied by a red-black tree: +> +> 1. Every node is either red or black. +> 1. All null nodes are considered black. +> 1. A red node does not have a red child. +> 1. Every path from a given node to any of its leaf nodes (that +> is, to any descendant null node) goes through the same number +> of black nodes. +> 1. (Conclusion) If a node N has exactly one child, the child +> must be red. If the child were black, its leaves would sit at +> a different black depth than N's null node (which is +> considered black by rule 2), violating requirement 4. + +The invariants below restate these rules with labels used +throughout the test framework. + +### RBT-1: valid coloring + +Every node is either red (1) or black (0). No other values are +permitted. (Wikipedia rule 1.) + +### RBT-2: null nodes are black + +All null child pointers are considered black. This is a +definitional rule used by the other invariants and does not +require an explicit check -- null is implicitly black in the +checks below. (Wikipedia rule 2.) + +### RBT-3: no red-red + +A red node does not have a red child. Equivalently: if a node is +red, both of its children (if non-null) must be black. (Wikipedia +rule 3.) + +### RBT-4: uniform black depth + +Every path from a given node to any of its descendant null nodes +passes through the same number of black nodes. This count +(excluding the node itself, including null as black) is the +node's _black height_. (Wikipedia rule 4.) + +### RBT-5: root is black (omitted) + +From Wikipedia: + +> Some authors, e.g. Cormen & al., claim "the root is black" as +> fifth requirement; but not Mehlhorn & Sanders or Sedgewick & +> Wayne. Since the root can always be changed from red to black, +> this rule has little effect on analysis. This article also omits +> it, because it slightly disturbs the recursive algorithms and +> proofs. + +This invariant is **not checked** by `assert_invariants`. The +insert algorithm in this codebase follows the Wikipedia +formulation which omits the root-is-black rule. A red root is +valid: it does not violate any of the four core rules (RBT-1 +through RBT-4) and does not affect the structural correctness of +the tree. + +### RBT-C: one-child corollary + +If a node N has exactly one child, that child must be red. If +the child were black, its null leaves would sit at a different +black depth than N's null child (which is black by RBT-2), +violating RBT-4. (Wikipedia rule 5 / conclusion.) + +This is a derived property, not an independent axiom, but +checking it explicitly produces clearer error messages than +detecting the black-height mismatch. + +## Free stack invariants + +Freed nodes are linked through `StackNode.next` at offset 0 +(overlapping `TreeNode.parent`). Free stack nodes are not part of +the tree and must not be visited during BST/RBT traversal. + +### FS-1: stack top + +If `header.top` is non-null, it points to a valid node slot +within the account data. + +### FS-2: no overlap with tree + +No node reachable from `header.root` via parent/child pointers +appears on the free stack, and vice versa. The set of tree nodes +and the set of free stack nodes are disjoint. + +## Verification approach + +### Where to check + +Add an `assert_invariants(desc: &TreeSpec)` function in +`tests/common.rs`. This operates purely on the `TreeSpec` +description (node indices, not virtual addresses), making it +independent of account serialization. + +Call `assert_invariants` on: + +- Every `desc` (before state) passed to `run_remove_success`, + `run_insert_success`, and similar helpers. +- Every `exp` (expected after state) passed to these helpers. +- Every intermediate tree state in multi-step tests. + +This catches spec-level errors at test construction time, +before any program code executes. + +### What to check + +The function should verify, given a `TreeSpec`: + +1. **BST-1 (ordering):** In-order traversal of the tree (starting + from `root`, following `left`/`right` indices) yields strictly + increasing keys. +1. **BST-2 (parent-child consistency):** For each node, verify + that the parent index matches (root has no parent; non-root + nodes appear in their parent's child list). For each child + pointer, verify the child's parent points back. +1. **RBT-1 (valid coloring):** Every node's color is `B` or `R`. +1. **RBT-3 (no red-red):** If a node is red, its children (if + present) are black. +1. **RBT-4 (uniform black depth):** Compute the black height of + each node recursively. All paths from the root to null must + yield the same count. +1. **RBT-C (one-child corollary):** If a node has exactly one + child, that child is red. +1. **FS-1 (stack top):** If `top` is `Some(i)`, then `i` is a + valid node index. +1. **FS-2 (disjoint sets):** Collect the set of node indices + reachable from root. Collect the set of node indices reachable + via the free stack (`top`, then following `parent` as + `StackNode.next`). Verify the two sets are disjoint and their + union covers all node indices in the buffer. + +### Freed nodes + +Freed nodes on the stack have null children (cleared by remove) +and use the `parent` field as `StackNode.next`. These nodes +must not be traversed as part of the BST/RBT checks. The +`TreeSpec` already distinguishes freed nodes implicitly: they +are reachable from `top` but not from `root`. + +To identify freed nodes from a `TreeSpec`: + +- Walk the free stack starting from `top`, following `parent` as + the next pointer, until null. +- All remaining nodes (reachable from `root`) are tree nodes and + must satisfy BST and RBT invariants. + +### Error reporting + +`assert_invariants` should return `Result<(), String>` with a +descriptive message identifying which invariant failed and which +node(s) are involved. Example: + +```text +RBT-3 (no red-red): N2 (RED) has red child N4 at child[R] +RBT-4 (uniform black depth): root black height L=3, R=2 +BST-1 (ordering): N3.key=15 in left subtree of N0.key=10 +``` + +### Interaction with existing tests + +The invariant checks run on `TreeSpec` descriptions, not on +program output. This makes them orthogonal to the program logic: + +- If a test's **before** state fails invariants, the test setup + is invalid and should be fixed in the test spec. +- If a test's **after** state fails invariants, the test + expectation is wrong and should be traced against the reference + algorithm. +- If the program produces output that passes `assert_tree_account` + but the expected `TreeSpec` fails invariants, the spec has a + bug. + +This separation means invariant checking does not depend on or +validate program correctness -- it validates the test +specifications themselves. diff --git a/examples/tree/src/lib.rs b/examples/tree/src/lib.rs new file mode 100644 index 00000000..569ef2f5 --- /dev/null +++ b/examples/tree/src/lib.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(test), no_std)] +mod program; + +#[cfg(test)] +mod tests; diff --git a/examples/tree/src/program.rs b/examples/tree/src/program.rs new file mode 100644 index 00000000..0dd9f439 --- /dev/null +++ b/examples/tree/src/program.rs @@ -0,0 +1,935 @@ +use core::ptr::{addr_of, addr_of_mut, null_mut, read_unaligned}; +use pinocchio::{ + account::RuntimeAccount, + entrypoint::NON_DUP_MARKER, + hint::{likely, unlikely}, + no_allocator, nostd_panic_handler, + sysvars::rent::RENT_ID, + Address, SUCCESS, +}; +use tree_interface::{ + cpi, data, error_codes::error, input_buffer, instruction, tree, Color, + CreateAccountInstructionData, InitializeInstruction, InsertInstruction, RemoveInstruction, + SolAccountInfo, SolAccountMeta, SolInstruction, SolSignerSeed, SolSignerSeeds, + TransferInstructionData, TreeHeader, TreeNode, +}; +#[cfg(target_os = "solana")] +use { + core::mem::MaybeUninit, + pinocchio::syscalls::{ + sol_invoke_signed_c, sol_log_compute_units_, sol_try_find_program_address, + }, +}; + +#[inline(always)] +unsafe fn account_at(input: *mut u8, offset: i16) -> *mut RuntimeAccount { + input.add(offset as usize).cast() +} + +#[inline(always)] +unsafe fn ldxb(ptr: *const u8, offset: i16) -> u8 { + read_unaligned(ptr.add(offset as usize)) +} + +#[inline(always)] +unsafe fn ldxh(ptr: *const u8, offset: i16) -> u16 { + read_unaligned(ptr.add(offset as usize).cast()) +} + +#[inline(always)] +unsafe fn ldxw(ptr: *const u8, offset: i16) -> u32 { + read_unaligned(ptr.add(offset as usize).cast()) +} + +#[inline(always)] +unsafe fn ldxdw(ptr: *const u8, offset: i16) -> u64 { + read_unaligned(ptr.add(offset as usize).cast()) +} + +/// Checks if the account is a duplicate by checking if it's borrowed, since this is equivalent +/// via the underlying API due to the borrow state implementation. +#[inline(always)] +unsafe fn is_duplicate(account: *const RuntimeAccount) -> bool { + (*account).borrow_state != NON_DUP_MARKER +} + +/// Compares two addresses by pointer, avoiding references in calling code while harnessing +/// the underlying `address_eq` implementation which is assembly-optimal. +#[inline(always)] +unsafe fn address_eq(a: *const Address, b: *const Address) -> bool { + use pinocchio::address::address_eq as eq; + eq(&*a, &*b) +} + +/// Insert a syscall to log CUs, useful for sectioning off disassembled program. +#[allow(dead_code)] +unsafe fn log_cus() { + #[cfg(target_os = "solana")] + sol_log_compute_units_(); +} + +macro_rules! if_err { + ($condition:expr, $error:expr) => { + if unlikely($condition) { + return $error.into(); + } + }; +} + +macro_rules! check_instruction_data_len { + ($instruction_data_len:expr, $type:ty) => { + if_err!( + $instruction_data_len != size_of::<$type>() as u64, + error::INSTRUCTION_DATA_LEN + ); + }; +} + +macro_rules! user_account { + ($input:expr) => {{ + let user = account_at($input, input_buffer::USER_ACCOUNT_OFF); + if_err!( + (*user).data_len != data::DATA_LEN_ZERO, + error::USER_DATA_LEN + ); + user + }}; +} + +macro_rules! check_data_len { + ($account:expr, $expected:expr, $error:expr) => { + if_err!((*$account).data_len != $expected, $error); + }; +} + +macro_rules! account_non_dup { + ($input:expr, $offset:expr, $error:expr) => {{ + let account = account_at($input, $offset); + if_err!(is_duplicate(account), $error); + account + }}; +} + +/// Checks the System Program and Rent sysvar accounts relative to a given input buffer pointer. +/// In `initialize`, this is the base `input`; in `insert`, it is `shifted_input` which accounts +/// for the tree's existing data length. +macro_rules! check_cpi_accounts { + ($input:expr) => { + let system_program = account_non_dup!( + $input, + input_buffer::SYSTEM_PROGRAM_ACCOUNT_OFF, + error::SYSTEM_PROGRAM_DUPLICATE + ); + check_data_len!( + system_program, + input_buffer::SYSTEM_PROGRAM_DATA_LEN as u64, + error::SYSTEM_PROGRAM_DATA_LEN + ); + let rent_sysvar = account_non_dup!( + $input, + input_buffer::RENT_ACCOUNT_OFF, + error::RENT_DUPLICATE + ); + let rent_id = RENT_ID; + if_err!( + !address_eq(addr_of!((*rent_sysvar).address), addr_of!(rent_id)), + error::RENT_ADDRESS + ); + }; +} + +macro_rules! remove_recycle_node { + ($node:expr, $tree_header:expr) => { + (*$node).child[tree::DIR_L] = null_mut(); + (*$node).child[tree::DIR_R] = null_mut(); + (*$node).parent = (*$tree_header).top.cast(); + (*$tree_header).top = $node.cast(); + return SUCCESS; + }; +} + +macro_rules! remove_simple_2_child_replace { + ($node:expr, $child:expr, $tree_header:expr) => { + let parent = (*$node).parent; + (*$child).parent = parent; + (*$child).color = Color::Black; + if !parent.is_null() { + if $node == (*parent).child[tree::DIR_R] { + (*parent).child[tree::DIR_R] = $child; + } else { + (*parent).child[tree::DIR_L] = $child; + } + } else { + (*$tree_header).root = $child; + } + remove_recycle_node!($node, $tree_header); + }; +} + +// ANCHOR: entrypoint-branching +no_allocator!(); +nostd_panic_handler!(); + +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8, instruction_data: *mut u8) -> u64 { + let instruction_data_len = ldxdw(instruction_data, -(size_of::() as i16)); + let n_accounts = ldxdw(input, input_buffer::N_ACCOUNTS_OFF); + let instruction_discriminator = ldxb(instruction_data, instruction::DISCRIMINATOR_OFF); + if likely(instruction_discriminator == instruction::DISCRIMINATOR_INSERT) { + insert(input, instruction_data, instruction_data_len, n_accounts) + } else if likely(instruction_discriminator == instruction::DISCRIMINATOR_REMOVE) { + remove(input, instruction_data, instruction_data_len, n_accounts) + } else if likely(instruction_discriminator == instruction::DISCRIMINATOR_INITIALIZE) { + initialize(input, instruction_data, instruction_data_len, n_accounts) + } else { + error::INSTRUCTION_DISCRIMINATOR.into() + } +} +// ANCHOR_END: entrypoint-branching + +// ANCHOR: insert-input-checks +#[allow(unused_assignments)] +#[inline(always)] +unsafe fn insert( + input: *mut u8, + instruction_data: *mut u8, + instruction_data_len: u64, + n_accounts: u64, +) -> u64 { + check_instruction_data_len!(instruction_data_len, InsertInstruction); + + // Error if too few accounts. + if_err!( + n_accounts < input_buffer::N_ACCOUNTS_GENERAL, + error::N_ACCOUNTS + ); + + // Error if user has data. + let _user = user_account!(input); + + // Error if tree is duplicate. + let tree = account_non_dup!(input, input_buffer::TREE_ACCOUNT_OFF, error::TREE_DUPLICATE); + // ANCHOR_END: insert-input-checks + + // ANCHOR: insert-allocate + // Allocate or recycle a node. + let tree_header: *mut TreeHeader = input.add(input_buffer::TREE_DATA_OFF as usize).cast(); + let mut node: *mut TreeNode = if (*tree_header).top.is_null() { + // Error if wrong number of accounts passed, since need extra accounts to allocate space. + if_err!( + n_accounts != input_buffer::N_ACCOUNTS_INIT, + error::N_ACCOUNTS_INSERT_ALLOCATION + ); + + // Get shifted input buffer pointer based on tree data length. + let tree_data_len: *mut u64 = addr_of_mut!((*tree).data_len); + let shifted_input = + input.add((*tree_data_len).next_multiple_of(data::BPF_ALIGN_OF_U128 as u64) as usize); + + // Check system program and rent sysvar accounts using shifted input buffer pointer. + check_cpi_accounts!(shifted_input); + + // Calculate additional lamports for rent exemption of one TreeNode. + let lamports_per_byte = ldxdw(shifted_input, input_buffer::RENT_DATA_OFF); + let transfer_lamports = size_of::() as u64 * lamports_per_byte; + + // Pack Transfer instruction data. + let transfer_instruction_data = TransferInstructionData { + discriminator: cpi::TRANSFER_DISCRIMINATOR, + lamports: transfer_lamports, + }; + + // Pack account metas and infos. + let user_key = input.add(input_buffer::USER_ADDRESS_OFF as usize).cast(); + let tree_key = input.add(input_buffer::TREE_ADDRESS_OFF as usize).cast(); + let sol_account_metas = [ + SolAccountMeta { + pubkey: user_key, + is_writable: true, + is_signer: true, + }, + SolAccountMeta { + pubkey: tree_key, + is_writable: true, + is_signer: false, + }, + ]; + let sol_account_infos = [ + SolAccountInfo { + key: user_key, + owner: input.add(input_buffer::USER_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::USER_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::USER_DATA_OFF as usize), + data_len: data::DATA_LEN_ZERO, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: true, + is_writable: true, + executable: false, + }, + SolAccountInfo { + key: tree_key, + owner: input.add(input_buffer::TREE_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::TREE_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::TREE_DATA_OFF as usize), + data_len: *tree_data_len, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: false, + is_writable: true, + executable: false, + }, + ]; + + // Pack instruction. + let system_program_address = Address::default(); + let sol_instruction = SolInstruction { + program_id: addr_of!(system_program_address).cast_mut().cast(), + accounts: sol_account_metas.as_ptr().cast_mut().cast(), + account_len: sol_account_metas.len() as u64, + data: addr_of!(transfer_instruction_data).cast_mut().cast(), + data_len: cpi::TRANSFER_INSN_DATA_LEN as u64, + }; + + // No signers needed, since user is already a signer on the transaction. + let empty_signers = SolSignerSeeds { + addr: core::ptr::null(), + len: 0, + }; + + #[cfg(target_os = "solana")] + sol_invoke_signed_c( + addr_of!(sol_instruction).cast(), + addr_of!(sol_account_infos).cast(), + cpi::N_ACCOUNTS as u64, + addr_of!(empty_signers).cast(), + cpi::N_PDA_SIGNERS_TRANSFER, + ); + #[cfg(not(target_os = "solana"))] + #[allow(path_statements)] + { + empty_signers; + sol_account_infos; + sol_instruction; + } + + // Increase tree data length by size of one TreeNode. + *tree_data_len += size_of::() as u64; + + // Advance next pointer by one TreeNode. + let node = (*tree_header).next; + (*tree_header).next = (*tree_header).next.add(1); + node + } else { + // Pop node from free stack. + let top = (*tree_header).top; + (*tree_header).top = (*top).next; + top.cast() + }; + // Set key and value together as a single word. + *addr_of_mut!((*node).key).cast() = ldxw(instruction_data, instruction::INSERT_KEY_OFF); + // ANCHOR_END: insert-allocate + + // ANCHOR: insert-search + let key = ldxh(instruction_data, instruction::INSERT_KEY_OFF); + let mut cursor = (*tree_header).root; + + // Root is null: new node becomes root. + if cursor.is_null() { + (*node).color = Color::Red; + (*node).parent = null_mut(); + (*tree_header).root = node; + return SUCCESS; + } + + let mut parent: *mut TreeNode; + loop { + parent = cursor; + let cursor_key = (*cursor).key; + if likely(key > cursor_key) { + cursor = (*parent).child[tree::DIR_R]; + if cursor.is_null() { + (*node).color = Color::Red; + (*node).parent = parent; + (*parent).child[tree::DIR_R] = node; + if (*parent).color == Color::Black { + return SUCCESS; + } + break; + } + } else if likely(key < cursor_key) { + cursor = (*parent).child[tree::DIR_L]; + if cursor.is_null() { + (*node).color = Color::Red; + (*node).parent = parent; + (*parent).child[tree::DIR_L] = node; + if (*parent).color == Color::Black { + return SUCCESS; + } + break; + } + } else { + return error::KEY_EXISTS.into(); + } + } + // ANCHOR_END: insert-search + + // ANCHOR: insert-fixup-case-1 + // Main insert fixup. + loop { + // Case 1. + if (*parent).color == Color::Black { + return SUCCESS; + } + // ANCHOR_END: insert-fixup-case-1 + + // ANCHOR: insert-fixup-case-4 + let grandparent = (*parent).parent; + if grandparent.is_null() { + // Case 4. + (*parent).color = Color::Black; + return SUCCESS; + } + // ANCHOR_END: insert-fixup-case-4 + + // ANCHOR: insert-fixup-case-5-6-dir-l + // Determine direction and uncle with hardcoded child indices. + let uncle; + if parent == (*grandparent).child[tree::DIR_L] { + // dir_l: parent is left child of grandparent. + uncle = (*grandparent).child[tree::DIR_R]; + if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5 dir_l: rotate parent in DIR_L. + // + // Grandparent is guaranteed non-null by the case 4 check, so + // no root-replacement path is needed. Parent is known to be + // grandparent.child[DIR_L] from the dir_l branch, so the + // child pointer update is hardcoded without comparison. + if node == (*parent).child[tree::DIR_R] { + let new_root = (*parent).child[tree::DIR_R]; + let new_child = (*new_root).child[tree::DIR_L]; + + (*parent).child[tree::DIR_R] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[tree::DIR_L] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[tree::DIR_L] = new_root; + + node = parent; + parent = new_root; + } + + // Case 6 dir_l: rotate grandparent in DIR_R. + // + // The new root of this rotation is parent + // (= grandparent.child[DIR_L]), already in scope, + // eliminating the generic version's load of + // subtree.child[opposite(direction)]. + // + // Great-grandparent may be null (grandparent could be root), + // so the null check and root-replacement path are retained. + // Grandparent's position under great-grandparent is unrelated + // to dir, so the pointer comparison is also retained. + { + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[tree::DIR_R]; + + (*grandparent).child[tree::DIR_L] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[tree::DIR_R] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + if grandparent == (*great_grandparent).child[tree::DIR_R] { + (*great_grandparent).child[tree::DIR_R] = parent; + } else { + (*great_grandparent).child[tree::DIR_L] = parent; + } + } else { + (*tree_header).root = parent; + } + } + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; + } + // ANCHOR_END: insert-fixup-case-5-6-dir-l + // ANCHOR: insert-fixup-case-5-6-dir-r + } else { + // dir_r: parent is right child of grandparent. + uncle = (*grandparent).child[tree::DIR_L]; + if uncle.is_null() || (*uncle).color == Color::Black { + // Case 5 dir_r: rotate parent in DIR_R. + // + // Grandparent is guaranteed non-null by the case 4 check, so + // no root-replacement path is needed. Parent is known to be + // grandparent.child[DIR_R] from the dir_r branch, so the + // child pointer update is hardcoded without comparison. + if node == (*parent).child[tree::DIR_L] { + let new_root = (*parent).child[tree::DIR_L]; + let new_child = (*new_root).child[tree::DIR_R]; + + (*parent).child[tree::DIR_L] = new_child; + if !new_child.is_null() { + (*new_child).parent = parent; + } + + (*new_root).child[tree::DIR_R] = parent; + (*new_root).parent = grandparent; + (*parent).parent = new_root; + + (*grandparent).child[tree::DIR_R] = new_root; + + node = parent; + parent = new_root; + } + + // Case 6 dir_r: rotate grandparent in DIR_L. + // + // The new root of this rotation is parent + // (= grandparent.child[DIR_R]), already in scope, + // eliminating the generic version's load of + // subtree.child[opposite(direction)]. + // + // Great-grandparent may be null (grandparent could be root), + // so the null check and root-replacement path are retained. + // Grandparent's position under great-grandparent is unrelated + // to dir, so the pointer comparison is also retained. + { + let great_grandparent = (*grandparent).parent; + let new_child = (*parent).child[tree::DIR_L]; + + (*grandparent).child[tree::DIR_R] = new_child; + if !new_child.is_null() { + (*new_child).parent = grandparent; + } + + (*parent).child[tree::DIR_L] = grandparent; + (*parent).parent = great_grandparent; + (*grandparent).parent = parent; + + if !great_grandparent.is_null() { + if grandparent == (*great_grandparent).child[tree::DIR_R] { + (*great_grandparent).child[tree::DIR_R] = parent; + } else { + (*great_grandparent).child[tree::DIR_L] = parent; + } + } else { + (*tree_header).root = parent; + } + } + + (*parent).color = Color::Black; + (*grandparent).color = Color::Red; + return SUCCESS; + } + } + // ANCHOR_END: insert-fixup-case-5-6-dir-r + + // ANCHOR: insert-fixup-case-2-3 + // Case 2. + (*parent).color = Color::Black; + (*uncle).color = Color::Black; + (*grandparent).color = Color::Red; + node = grandparent; + + parent = (*node).parent; + if parent.is_null() { + break; + } + } + // Case 3. + SUCCESS +} +// ANCHOR_END: insert-fixup-case-2-3 + +// ANCHOR: remove-input-checks +#[inline(always)] +unsafe fn remove( + input: *mut u8, + instruction_data: *mut u8, + instruction_data_len: u64, + n_accounts: u64, +) -> u64 { + check_instruction_data_len!(instruction_data_len, RemoveInstruction); + + // Error if too few accounts. + if_err!( + n_accounts < input_buffer::N_ACCOUNTS_GENERAL, + error::N_ACCOUNTS + ); + + // Error if user has data. + let _user = user_account!(input); + + // Error if tree is duplicate. + let _tree = account_non_dup!(input, input_buffer::TREE_ACCOUNT_OFF, error::TREE_DUPLICATE); + // ANCHOR_END: remove-input-checks + + // ANCHOR: remove-search + let tree_header: *mut TreeHeader = input.add(input_buffer::TREE_DATA_OFF as usize).cast(); + let mut node = (*tree_header).root; + + if node.is_null() { + return error::KEY_DOES_NOT_EXIST.into(); + } + + let key = ldxh(instruction_data, instruction::REMOVE_KEY_OFF); + loop { + let node_key = (*node).key; + if key > node_key { + node = (*node).child[tree::DIR_R]; + if node.is_null() { + return error::KEY_DOES_NOT_EXIST.into(); + } + } else if key < node_key { + node = (*node).child[tree::DIR_L]; + if node.is_null() { + return error::KEY_DOES_NOT_EXIST.into(); + } + } else { + break; + } + } + // ANCHOR_END: remove-search + + // ANCHOR: remove-simple-1 + if !(*node).child[tree::DIR_L].is_null() { + if !(*node).child[tree::DIR_R].is_null() { + // Simple case 1: successor swap. + let mut successor = (*node).child[tree::DIR_R]; + loop { + let left = (*successor).child[tree::DIR_L]; + if left.is_null() { + break; + } + successor = left; + } + // Copy successor's key/value to the found node as a u32 + // pair. The successor's fields are left as-is (insert + // overwrites both when reusing the node from the stack). + let node_kv = addr_of_mut!((*node).key).cast::(); + let successor_kv = addr_of!((*successor).key).cast::(); + *node_kv = *successor_kv; + node = successor; + // ANCHOR_END: remove-simple-1 + // ANCHOR: remove-simple-2 + } else { + // Simple case 2: one child (L). + let child = (*node).child[tree::DIR_L]; + remove_simple_2_child_replace!(node, child, tree_header); + } + }; + if !(*node).child[tree::DIR_R].is_null() { + // Simple case 2: one child (R). + let child = (*node).child[tree::DIR_R]; + remove_simple_2_child_replace!(node, child, tree_header); + // ANCHOR_END: remove-simple-2 + // ANCHOR: remove-simple-3 + } else if unlikely((*node).parent.is_null()) { + // Simple case 3: root leaf. + (*tree_header).root = null_mut(); + remove_recycle_node!(node, tree_header); + // ANCHOR_END: remove-simple-3 + // ANCHOR: remove-simple-4 + } else if (*node).color == Color::Red { + // Simple case 4: red leaf. + let parent = (*node).parent; + if node == (*parent).child[tree::DIR_R] { + (*parent).child[tree::DIR_R] = null_mut(); + } else { + (*parent).child[tree::DIR_L] = null_mut(); + } + remove_recycle_node!(node, tree_header); + // ANCHOR_END: remove-simple-4 + }; + // ANCHOR: remove-complex + let mut parent = (*node).parent; + let mut dir = direction(node); + (*parent).child[dir] = null_mut(); + let removed_node = node; + + let mut sibling: *mut TreeNode; + let mut close_nephew: *mut TreeNode; + let mut distant_nephew: *mut TreeNode; + // 0 = done (cases 1/2/4), 5 = case 5+6, 6 = case 6 only. + let mut exit_case: u32 = 0; + + loop { + sibling = (*parent).child[1 - dir]; + distant_nephew = (*sibling).child[1 - dir]; + close_nephew = (*sibling).child[dir]; + + if (*sibling).color == Color::Red { + // Case 3. + rotate_subtree(tree_header, parent, dir); + (*parent).color = Color::Red; + (*sibling).color = Color::Black; + sibling = close_nephew; + + distant_nephew = (*sibling).child[1 - dir]; + if !distant_nephew.is_null() && (*distant_nephew).color == Color::Red { + exit_case = 6; + break; + } + close_nephew = (*sibling).child[dir]; + if !close_nephew.is_null() && (*close_nephew).color == Color::Red { + exit_case = 5; + break; + } + + // Case 4. + (*sibling).color = Color::Red; + (*parent).color = Color::Black; + break; + } + + if !distant_nephew.is_null() && (*distant_nephew).color == Color::Red { + exit_case = 6; + break; + } + + if !close_nephew.is_null() && (*close_nephew).color == Color::Red { + exit_case = 5; + break; + } + + if (*parent).color == Color::Red { + // Case 4. + (*sibling).color = Color::Red; + (*parent).color = Color::Black; + break; + } + + // Case 2. + (*sibling).color = Color::Red; + node = parent; + parent = (*node).parent; + if parent.is_null() { + break; + } + dir = direction(node); + } + + // Case 5 falls through to case 6. + if exit_case == 5 { + rotate_subtree(tree_header, sibling, 1 - dir); + (*sibling).color = Color::Red; + (*close_nephew).color = Color::Black; + distant_nephew = sibling; + sibling = close_nephew; + } + + if exit_case >= 5 { + rotate_subtree(tree_header, parent, dir); + (*sibling).color = read_unaligned(addr_of!((*parent).color)); + (*parent).color = Color::Black; + (*distant_nephew).color = Color::Black; + } + + remove_recycle_node!(removed_node, tree_header); + // ANCHOR_END: remove-complex +} + +// ANCHOR: initialize-input-checks +#[inline(always)] +unsafe fn initialize( + input: *mut u8, + _instruction_data: *mut u8, + instruction_data_len: u64, + n_accounts: u64, +) -> u64 { + check_instruction_data_len!(instruction_data_len, InitializeInstruction); + + // Error if incorrect number of accounts. + if_err!( + n_accounts != input_buffer::N_ACCOUNTS_INIT, + error::N_ACCOUNTS + ); + + // Error if user has data. + let _user = user_account!(input); + + // Error if tree is duplicate or has data. + let tree = account_non_dup!(input, input_buffer::TREE_ACCOUNT_OFF, error::TREE_DUPLICATE); + check_data_len!(tree, data::DATA_LEN_ZERO, error::TREE_DATA_LEN); + + check_cpi_accounts!(input); + // ANCHOR_END: initialize-input-checks + + // ANCHOR: initialize-pda-checks + #[cfg(target_os = "solana")] + // Invoke syscall. + let (pda, bump) = { + let mut pda = MaybeUninit::
::uninit(); + let mut bump = MaybeUninit::::uninit(); + // Get input buffer footer pointer. + sol_try_find_program_address( + // Pass a declared pointer instead of null to prevent unnecessary register assignment. + input, + cpi::N_SEEDS_TRY_FIND_PDA, + input.add(input_buffer::INIT_PROGRAM_ID_OFF_IMM as usize), + pda.as_mut_ptr().cast(), + bump.as_mut_ptr(), + ); + (pda.assume_init(), bump.assume_init()) + }; + #[cfg(not(target_os = "solana"))] + let (pda, bump) = (Address::default(), 0); + + // Compare result with passed PDA. + if_err!( + !address_eq( + addr_of!(pda), + input.add(input_buffer::TREE_ADDRESS_OFF_0 as usize).cast() + ), + error::PDA_MISMATCH + ); + // ANCHOR_END: initialize-pda-checks + + // ANCHOR: initialize-create-account + // Pack CreateAccount instruction data. + let create_account_instruction_data = CreateAccountInstructionData { + discriminator: cpi::CREATE_ACCOUNT_DISCRIMINATOR, + lamports: (cpi::ACCOUNT_DATA_SCALAR as u64) * ldxdw(input, input_buffer::RENT_DATA_OFF), + space: cpi::TREE_DATA_LEN as u64, + owner: read_unaligned( + input + .add(input_buffer::INIT_PROGRAM_ID_OFF_IMM as usize) + .cast(), + ), + }; + + // Pack account metas and infos. + let user_key = input.add(input_buffer::USER_ADDRESS_OFF as usize).cast(); + let tree_key = input.add(input_buffer::TREE_ADDRESS_OFF as usize).cast(); + let sol_account_metas = [ + SolAccountMeta { + pubkey: user_key, + is_writable: true, + is_signer: true, + }, + SolAccountMeta { + pubkey: tree_key, + is_writable: true, + is_signer: true, + }, + ]; + let sol_account_infos = [ + SolAccountInfo { + key: user_key, + owner: input.add(input_buffer::USER_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::USER_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::USER_DATA_OFF as usize), + data_len: data::DATA_LEN_ZERO, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: true, + is_writable: true, + executable: false, + }, + SolAccountInfo { + key: tree_key, + owner: input.add(input_buffer::TREE_OWNER_OFF as usize).cast(), + lamports: input.add(input_buffer::TREE_LAMPORTS_OFF as usize).cast(), + data: input.add(input_buffer::TREE_DATA_OFF as usize), + data_len: data::DATA_LEN_ZERO, + rent_epoch: cpi::RENT_EPOCH_NULL, + is_signer: true, + is_writable: true, + executable: false, + }, + ]; + + // Pack instruction. + let system_program_address = Address::default(); + let sol_instruction = SolInstruction { + program_id: addr_of!(system_program_address).cast_mut().cast(), + accounts: sol_account_metas.as_ptr().cast_mut().cast(), + account_len: sol_account_metas.len() as u64, + data: addr_of!(create_account_instruction_data).cast_mut().cast(), + data_len: cpi::CREATE_ACCOUNT_INSN_DATA_LEN as u64, + }; + + // Initialize signer seed for PDA bump. + let bump_seed = SolSignerSeed { + addr: addr_of!(bump).cast(), + len: size_of::() as u64, + }; + + // Initialize signer seeds for PDA. + let signers_seeds = SolSignerSeeds { + addr: addr_of!(bump_seed).cast(), + len: cpi::N_SEEDS_CREATE_ACCOUNT as u64, + }; + + #[cfg(target_os = "solana")] + sol_invoke_signed_c( + addr_of!(sol_instruction).cast(), + addr_of!(sol_account_infos).cast(), + cpi::N_ACCOUNTS as u64, + addr_of!(signers_seeds).cast(), + cpi::N_PDA_SIGNERS as u64, + ); + #[cfg(not(target_os = "solana"))] + #[allow(path_statements)] + { + signers_seeds; + sol_account_infos; + sol_instruction; + } + + // Store next pointer in tree header. + let tree_data: *mut TreeHeader = input.add(input_buffer::TREE_DATA_OFF as usize).cast(); + (*tree_data).next = tree_data.add(1).cast(); + // ANCHOR_END: initialize-create-account + + SUCCESS +} + +#[inline(always)] +unsafe fn direction(node: *const TreeNode) -> usize { + (node == (*(*node).parent).child[tree::DIR_R]) as usize +} + +/// Rotate the subtree rooted at `subtree` in the given direction, returning new root of subtree. +#[inline(always)] +unsafe fn rotate_subtree( + tree: *mut TreeHeader, + subtree: *mut TreeNode, + direction: usize, +) -> *mut TreeNode { + let parent = (*subtree).parent; + let opposite = 1 - direction; + let new_root = (*subtree).child[opposite]; + let new_child = (*new_root).child[direction]; + + (*subtree).child[opposite] = new_child; + + if !new_child.is_null() { + (*new_child).parent = subtree; + } + + (*new_root).child[direction] = subtree; + (*new_root).parent = parent; + (*subtree).parent = new_root; + + if !parent.is_null() { + if subtree == (*parent).child[tree::DIR_R] { + (*parent).child[tree::DIR_R] = new_root; + } else { + (*parent).child[tree::DIR_L] = new_root; + } + } else { + (*tree).root = new_root; + } + + new_root +} diff --git a/examples/tree/src/tests.rs b/examples/tree/src/tests.rs new file mode 100644 index 00000000..2f580728 --- /dev/null +++ b/examples/tree/src/tests.rs @@ -0,0 +1,256 @@ +mod common; +mod entrypoint; +mod init; +mod insert; +#[allow(dead_code, unused_imports)] +mod remove; + +use mollusk_svm::program; +use mollusk_svm::result::{Check, Config, ProgramResult as MolluskResult}; +use pinocchio::sysvars::rent::Rent; +use solana_sdk::account::Account; +use solana_sdk::instruction::{AccountMeta, Instruction}; +use solana_sdk::program_error::ProgramError; +use solana_sdk::pubkey::Pubkey; +use test_utils::{setup_test, ProgramLanguage, TestSetup}; +use tree_interface::{cpi, error_codes}; + +const USER_LAMPORTS: u64 = 1_000_000; + +/// Virtual address of the input buffer in the SVM memory map. +/// See `solana_sbpf::ebpf::MM_INPUT_START`. +const MM_INPUT_START: u64 = 0x400000000; + +/// Rent exemption threshold per SIMD-0194. +const SIMD0194_EXEMPTION_THRESHOLD: f64 = 1.0; + +/// Set up a test with SIMD-0194 rent exemption threshold. +fn setup_test_with_rent(lang: ProgramLanguage) -> TestSetup { + let mut setup = setup_test(lang); + setup.mollusk.sysvars.rent.exemption_threshold = SIMD0194_EXEMPTION_THRESHOLD; + setup +} + +/// Cast a sized value to its raw byte representation. +unsafe fn as_bytes(val: &T) -> &[u8] { + core::slice::from_raw_parts(val as *const T as *const u8, size_of::()) +} + +/// Fixed costs for syscalls and CPI operations. +mod fixed_costs { + /// Cost for sol_try_find_program_address syscall. + pub const CREATE_PROGRAM_ADDRESS: u64 = 1500; + /// CPI base invocation cost (SIMD-0339). + pub const CPI_BASE: u64 = 946; + /// System Program operation cost. + pub const SYSTEM_PROGRAM: u64 = 150; +} + +enum AccountIndex { + User = 0, + Tree = 1, + SystemProgram = 2, + RentSysvar = 3, +} + +struct CaseResult { + cu: u64, + error: Option, +} + +fn check_result( + setup: &TestSetup, + instruction: &Instruction, + accounts: &[(Pubkey, Account)], + expected: ProgramError, +) -> CaseResult { + let result = setup.mollusk.process_instruction(instruction, accounts); + match &result.program_result { + MolluskResult::Failure(err) if *err == expected => CaseResult { + cu: result.compute_units_consumed, + error: None, + }, + other => CaseResult { + cu: result.compute_units_consumed, + error: Some(format!("expected Failure({:?}), got {:?}", expected, other)), + }, + } +} + +fn flip_account_address( + instruction: &mut Instruction, + accounts: &mut [(Pubkey, Account)], + account_index: usize, + chunk_index: usize, + chunk_size: usize, +) { + let flip_index = (chunk_index * chunk_size) + chunk_size - 1; + accounts[account_index].0.as_mut()[flip_index] ^= 1; + instruction.accounts[account_index].pubkey = accounts[account_index].0; +} + +fn check_error( + setup: &TestSetup, + instruction: &Instruction, + accounts: &[(Pubkey, Account)], + expected_error: error_codes::error, +) -> CaseResult { + check_result( + setup, + instruction, + accounts, + ProgramError::Custom(expected_error.into()), + ) +} + +trait TestCase: Copy { + fn name(&self) -> &'static str; + fn run(&self, lang: ProgramLanguage) -> CaseResult; + /// Returns the fixed syscall/CPI costs for this case. + /// These costs are identical for both ASM and Rust implementations. + fn fixed_costs(&self) -> u64 { + 0 + } +} + +fn print_comparison_table(cases: &[T]) { + let mut failures = Vec::new(); + let has_fixed_costs = cases.iter().any(|c| c.fixed_costs() > 0); + + if has_fixed_costs { + println!("| Test case | Fixed CU costs | ASM (net CUs) | Rust (net CUs) | Overhead | Overhead % |"); + println!("|-----------|----------------|---------------|----------------|----------|------------|"); + } else { + println!("| Test case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % |"); + println!("|-----------|-----------|------------|----------|------------|"); + } + + for case in cases { + let asm = case.run(ProgramLanguage::Assembly); + let rs = case.run(ProgramLanguage::Rust); + let fixed = case.fixed_costs(); + + if has_fixed_costs { + let asm_net = asm.cu.saturating_sub(fixed); + let rs_net = rs.cu.saturating_sub(fixed); + let overhead = rs_net as i64 - asm_net as i64; + let overhead_pct = if asm_net > 0 { + format!("{:+.1}%", (overhead as f64 / asm_net as f64) * 100.0) + } else { + "N/A".to_string() + }; + println!( + "| {} | {} | {} | {} | {:+} | {} |", + case.name(), + fixed, + asm_net, + rs_net, + overhead, + overhead_pct + ); + } else { + let overhead = rs.cu as i64 - asm.cu as i64; + let overhead_pct = if asm.cu > 0 { + (overhead as f64 / asm.cu as f64) * 100.0 + } else { + 0.0 + }; + println!( + "| {} | {} | {} | {:+} | {:+.1}% |", + case.name(), + asm.cu, + rs.cu, + overhead, + overhead_pct + ); + } + + if let Some(err) = &asm.error { + failures.push(format!(" ASM {}: {}", case.name(), err)); + } + if let Some(err) = &rs.error { + failures.push(format!(" Rust {}: {}", case.name(), err)); + } + } + + assert!( + failures.is_empty(), + "\nFailed cases:\n{}", + failures.join("\n") + ); +} + +#[test] +fn test_entrypoint_branching() { + print_comparison_table(entrypoint::EntrypointCase::CASES); +} + +#[test] +fn test_insert_input_checks() { + print_comparison_table(insert::InsertCase::INPUT_CASES); +} + +#[test] +fn test_insert_alloc_checks() { + print_comparison_table(insert::InsertCase::ALLOC_CHECK_CASES); +} + +#[test] +fn test_insert_alloc() { + print_comparison_table(insert::InsertCase::ALLOC_CASES); +} + +#[test] +fn test_initialize_input_checks() { + print_comparison_table(init::InitCase::CASES); +} + +#[test] +fn test_initialize_pda_checks() { + print_comparison_table(init::InitCase::PDA_CASES); +} + +#[test] +fn test_initialize_create_account() { + print_comparison_table(init::InitCase::CPI_CASES); +} + +#[test] +fn test_insert_search() { + print_comparison_table(insert::InsertCase::SEARCH_CASES); +} + +#[test] +fn test_insert_to_tree() { + print_comparison_table(insert::InsertCase::TREE_CASES); +} + +#[test] +fn test_multi_insert() { + print_comparison_table(insert::MultiInsertCase::CASES); +} + +#[test] +fn test_remove_input_checks() { + print_comparison_table(remove::RemoveCase::INPUT_CASES); +} + +#[test] +fn test_remove_search() { + print_comparison_table(remove::RemoveCase::SEARCH_CASES); +} + +#[test] +fn test_remove_simple() { + print_comparison_table(remove::RemoveCase::SIMPLE_CASES); +} + +// #[test] +// fn test_remove_rebalance() { +// print_comparison_table(remove::RemoveCase::REBALANCE_CASES); +// } + +// #[test] +// fn test_multi_remove() { +// print_comparison_table(remove::MultiRemoveCase::CASES); +// } diff --git a/examples/tree/src/tests/common.rs b/examples/tree/src/tests/common.rs new file mode 100644 index 00000000..19c13d17 --- /dev/null +++ b/examples/tree/src/tests/common.rs @@ -0,0 +1,556 @@ +// cspell:word inorder +// cspell:word vaddr +use super::*; +use tree_interface::{input_buffer, tree, Color, StackNode, TreeHeader, TreeNode}; + +// --------------------------------------------------------------------------- +// Helpers: tree description types +// --------------------------------------------------------------------------- + +pub(super) struct NodeSpec { + pub key: u16, + pub value: u16, + pub color: u8, + pub parent: Option, + pub left: Option, + pub right: Option, +} + +impl NodeSpec { + pub fn val(mut self, v: u16) -> Self { + self.value = v; + self + } +} + +pub(super) struct TreeSpec<'a> { + pub root: Option, + pub top: Option, + pub nodes: &'a [NodeSpec], +} + +/// Compute the virtual address of node slot `i` in the tree account. +pub(super) fn node_vaddr(i: usize) -> u64 { + MM_INPUT_START + + input_buffer::TREE_DATA_OFF as u64 + + size_of::() as u64 + + (i as u64) * (size_of::() as u64) +} + +/// Convert an optional node index to a virtual address (0 for None). +pub(super) fn opt_vaddr(idx: Option) -> u64 { + match idx { + Some(i) => node_vaddr(i), + None => 0, + } +} + +// --------------------------------------------------------------------------- +// Helper: build tree account data +// --------------------------------------------------------------------------- + +/// Build tree account data with pre-existing nodes and one free StackNode. +/// +/// Memory layout: TreeHeader | node[0] | node[1] | ... | node[N-1] | free_slot +/// +/// - `header.root` → virtual address of `nodes[root]`, or null. +/// - `header.top` → virtual address of the free slot (index = nodes.len()). +/// - `header.next` → 0 (unused in skip-alloc path). +pub(super) fn build_tree_account(desc: &TreeSpec, program_id: &Pubkey) -> (Pubkey, Account) { + let n = desc.nodes.len(); + // N existing nodes + 1 free slot. + let data_len = size_of::() + (n + 1) * size_of::(); + let mut data = vec![0u8; data_len]; + + // Write header. + let header = data.as_mut_ptr() as *mut TreeHeader; + unsafe { + (*header).root = opt_vaddr(desc.root) as *mut TreeNode; + (*header).top = node_vaddr(n) as *mut StackNode; + (*header).next = core::ptr::null_mut(); + } + + // Write existing nodes. + write_nodes(&mut data, desc.nodes); + + // Free slot is already zeroed (StackNode.next = null). + + let pubkey = Pubkey::new_unique(); + let mut account = Account::new(0, data_len, program_id); + account.data = data; + (pubkey, account) +} + +/// Build tree account data with pre-existing nodes and no free slot. +/// +/// Memory layout: TreeHeader | node[0] | node[1] | ... | node[N-1] +/// +/// - `header.root` → virtual address of `nodes[root]`, or null. +/// - `header.top` → null (no pre-existing free nodes). +/// - `header.next` → null (unused). +pub(super) fn build_tree_account_no_free( + desc: &TreeSpec, + program_id: &Pubkey, +) -> (Pubkey, Account) { + let n = desc.nodes.len(); + let data_len = size_of::() + n * size_of::(); + let mut data = vec![0u8; data_len]; + + let header = data.as_mut_ptr() as *mut TreeHeader; + unsafe { + (*header).root = opt_vaddr(desc.root) as *mut TreeNode; + (*header).top = core::ptr::null_mut(); + (*header).next = core::ptr::null_mut(); + } + + write_nodes(&mut data, desc.nodes); + + let pubkey = Pubkey::new_unique(); + let mut account = Account::new(0, data_len, program_id); + account.data = data; + (pubkey, account) +} + +fn write_nodes(data: &mut [u8], nodes: &[NodeSpec]) { + for (i, node) in nodes.iter().enumerate() { + let offset = size_of::() + i * size_of::(); + let ptr = unsafe { data.as_mut_ptr().add(offset) as *mut TreeNode }; + unsafe { + (*ptr).parent = opt_vaddr(node.parent) as *mut TreeNode; + (*ptr).child[tree::DIR_L] = opt_vaddr(node.left) as *mut TreeNode; + (*ptr).child[tree::DIR_R] = opt_vaddr(node.right) as *mut TreeNode; + (*ptr).key = node.key; + (*ptr).value = node.value; + (*ptr).color = core::mem::transmute(node.color); + } + } +} + +// --------------------------------------------------------------------------- +// Helper: assert tree account (full state) +// --------------------------------------------------------------------------- + +/// Assert every field of the tree account data against expected state. +/// Returns Ok(()) on match, Err(description) on mismatch. +pub(super) fn assert_tree_account(data: &[u8], expected: &TreeSpec) -> Result<(), String> { + let mut errors = Vec::new(); + let n = expected.nodes.len(); + + // Check data length (at least enough for the expected nodes). + let min_len = size_of::() + n * size_of::(); + if data.len() < min_len { + errors.push(format!( + "data len: expected at least {}, got {}", + min_len, + data.len() + )); + } + + // Check header. + let header = data.as_ptr() as *const TreeHeader; + unsafe { + let root_addr = (*header).root as u64; + let expected_root = opt_vaddr(expected.root); + if root_addr != expected_root { + errors.push(format!( + "header.root: expected {:#x}, got {:#x}", + expected_root, root_addr + )); + } + + let top_addr = (*header).top as u64; + let expected_top = opt_vaddr(expected.top); + if top_addr != expected_top { + errors.push(format!( + "header.top: expected {:#x}, got {:#x}", + expected_top, top_addr + )); + } + + let next_addr = (*header).next as u64; + if next_addr != 0 { + errors.push(format!("header.next: expected 0x0, got {:#x}", next_addr)); + } + } + + // Check each node. + for i in 0..n { + let offset = size_of::() + i * size_of::(); + if offset + size_of::() > data.len() { + errors.push(format!("N{}: out of bounds", i)); + continue; + } + let ptr = unsafe { data.as_ptr().add(offset) as *const TreeNode }; + let exp = &expected.nodes[i]; + let label = format!("N{}", i); + + unsafe { + let parent_addr = core::ptr::read_unaligned(core::ptr::addr_of!((*ptr).parent)) as u64; + let expected_parent = opt_vaddr(exp.parent); + if parent_addr != expected_parent { + errors.push(format!( + "{}.parent: expected {:#x}, got {:#x}", + label, expected_parent, parent_addr + )); + } + + let left_addr = + core::ptr::read_unaligned(core::ptr::addr_of!((*ptr).child[tree::DIR_L])) as u64; + let expected_left = opt_vaddr(exp.left); + if left_addr != expected_left { + errors.push(format!( + "{}.L: expected {:#x}, got {:#x}", + label, expected_left, left_addr + )); + } + + let right_addr = + core::ptr::read_unaligned(core::ptr::addr_of!((*ptr).child[tree::DIR_R])) as u64; + let expected_right = opt_vaddr(exp.right); + if right_addr != expected_right { + errors.push(format!( + "{}.R: expected {:#x}, got {:#x}", + label, expected_right, right_addr + )); + } + + let key = core::ptr::read_unaligned(core::ptr::addr_of!((*ptr).key)); + if key != exp.key { + errors.push(format!("{}.key: expected {}, got {}", label, exp.key, key)); + } + + let value = core::ptr::read_unaligned(core::ptr::addr_of!((*ptr).value)); + if value != exp.value { + errors.push(format!( + "{}.value: expected {}, got {}", + label, exp.value, value + )); + } + + let color = core::ptr::read_unaligned(core::ptr::addr_of!((*ptr).color)) as u8; + if color != exp.color { + let color_name = |c: u8| if c == 0 { "B" } else { "R" }; + errors.push(format!( + "{}.color: expected {}, got {}", + label, + color_name(exp.color), + color_name(color) + )); + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } +} + +// --------------------------------------------------------------------------- +// Invariant checker (operates on TreeSpec, not account data) +// --------------------------------------------------------------------------- + +/// Check all structural invariants of a `TreeSpec`. +/// +/// Verifies BST ordering, parent-child consistency, RBT coloring rules, +/// and free stack validity. Returns `Ok(())` if all invariants hold, +/// or `Err` with descriptions of all violations found. +/// +/// See `specs/tree-invariants.md` for the full specification. +pub(super) fn assert_invariants(desc: &TreeSpec) -> Result<(), String> { + let mut errors = Vec::new(); + let n = desc.nodes.len(); + + // --- Collect tree nodes (reachable from root via child pointers) --- + let mut tree_set = vec![false; n]; + if let Some(root) = desc.root { + if root >= n { + errors.push(format!("root index {} out of bounds (n={})", root, n)); + } else { + collect_reachable(desc.nodes, root, &mut tree_set, &mut errors); + } + } + + // --- Collect free stack nodes --- + let mut stack_set = vec![false; n]; + { + let mut cur = desc.top; + while let Some(i) = cur { + if i >= n { + break; // Slot beyond described nodes (pre-allocated). + } + if stack_set[i] { + errors.push(format!("FS: cycle in free stack at N{}", i)); + break; + } + stack_set[i] = true; + cur = desc.nodes[i].parent; // parent field = StackNode.next + } + } + + // --- FS-2: disjoint sets and coverage --- + for i in 0..n { + if tree_set[i] && stack_set[i] { + errors.push(format!( + "FS-2 (disjoint): N{} in both tree and free stack", + i + )); + } + if !tree_set[i] && !stack_set[i] { + errors.push(format!( + "FS-2 (coverage): N{} neither in tree nor on free stack", + i + )); + } + } + + // --- BST and RBT checks (only on tree nodes) --- + if let Some(root) = desc.root { + if root < n { + // BST-2: root has no parent. + if desc.nodes[root].parent.is_some() { + errors.push(format!( + "BST-2: root N{} has parent={:?}", + root, desc.nodes[root].parent + )); + } + } + + for i in 0..n { + if !tree_set[i] { + continue; + } + let nd = &desc.nodes[i]; + + // RBT-1: valid coloring. + if nd.color != B && nd.color != R { + errors.push(format!("RBT-1 (valid coloring): N{} color={}", i, nd.color)); + } + + // BST-2: parent-child consistency (non-root). + if i != root { + match nd.parent { + None => errors.push(format!("BST-2: non-root N{} has no parent", i)), + Some(p) => { + if p >= n { + errors.push(format!("BST-2: N{}.parent={} out of bounds", i, p)); + } else { + let par = &desc.nodes[p]; + if par.left != Some(i) && par.right != Some(i) { + errors.push(format!( + "BST-2: N{}.parent=N{} but N{} has L={:?} R={:?}", + i, p, p, par.left, par.right + )); + } + } + } + } + } + + // BST-2: children point back. + if let Some(l) = nd.left { + if l < n && desc.nodes[l].parent != Some(i) { + errors.push(format!( + "BST-2: N{}.L=N{} but N{}.parent={:?}", + i, l, l, desc.nodes[l].parent + )); + } + } + if let Some(r) = nd.right { + if r < n && desc.nodes[r].parent != Some(i) { + errors.push(format!( + "BST-2: N{}.R=N{} but N{}.parent={:?}", + i, r, r, desc.nodes[r].parent + )); + } + } + + // RBT-3: no red-red. + if nd.color == R { + if let Some(l) = nd.left { + if l < n && desc.nodes[l].color == R { + errors.push(format!( + "RBT-3 (no red-red): N{} (RED) has red child N{} at L", + i, l + )); + } + } + if let Some(r) = nd.right { + if r < n && desc.nodes[r].color == R { + errors.push(format!( + "RBT-3 (no red-red): N{} (RED) has red child N{} at R", + i, r + )); + } + } + } + + // RBT-C: one-child corollary. + let has_l = nd.left.is_some(); + let has_r = nd.right.is_some(); + if has_l != has_r { + let child = nd.left.or(nd.right).unwrap(); + if child < n && desc.nodes[child].color != R { + errors.push(format!( + "RBT-C (one-child): N{} has one child N{} which is BLACK", + i, child + )); + } + } + } + + // BST-1: in-order traversal yields strictly increasing keys. + if root < n { + let mut keys = Vec::new(); + inorder_keys(desc.nodes, root, &mut keys); + for w in keys.windows(2) { + if w[0] >= w[1] { + errors.push(format!( + "BST-1 (ordering): keys not strictly increasing: {} >= {}", + w[0], w[1] + )); + break; + } + } + + // RBT-4: uniform black depth. + if let Err(e) = black_height(desc.nodes, root) { + errors.push(e); + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } +} + +/// Walk tree from `idx` via child pointers, marking visited nodes. +fn collect_reachable( + nodes: &[NodeSpec], + idx: usize, + visited: &mut Vec, + errors: &mut Vec, +) { + if idx >= nodes.len() { + errors.push(format!( + "child index {} out of bounds (n={})", + idx, + nodes.len() + )); + return; + } + if visited[idx] { + errors.push(format!("cycle in tree at N{}", idx)); + return; + } + visited[idx] = true; + if let Some(l) = nodes[idx].left { + collect_reachable(nodes, l, visited, errors); + } + if let Some(r) = nodes[idx].right { + collect_reachable(nodes, r, visited, errors); + } +} + +/// Collect keys via in-order traversal. +fn inorder_keys(nodes: &[NodeSpec], idx: usize, keys: &mut Vec) { + if let Some(l) = nodes[idx].left { + if l < nodes.len() { + inorder_keys(nodes, l, keys); + } + } + keys.push(nodes[idx].key); + if let Some(r) = nodes[idx].right { + if r < nodes.len() { + inorder_keys(nodes, r, keys); + } + } +} + +/// Compute the black height of a subtree. Returns `Err` if any node has +/// unequal left/right black heights (RBT-4 violation). +fn black_height(nodes: &[NodeSpec], idx: usize) -> Result { + let nd = &nodes[idx]; + let lbh = match nd.left { + Some(l) if l < nodes.len() => black_height(nodes, l)?, + _ => 0, + }; + let rbh = match nd.right { + Some(r) if r < nodes.len() => black_height(nodes, r)?, + _ => 0, + }; + if lbh != rbh { + return Err(format!( + "RBT-4 (uniform black depth): N{} (key={}) L black height={}, R black height={}", + idx, nd.key, lbh, rbh + )); + } + Ok(lbh + if nd.color == B { 1 } else { 0 }) +} + +// --------------------------------------------------------------------------- +// Shorthand constructors +// --------------------------------------------------------------------------- + +pub(super) const B: u8 = Color::Black as u8; +pub(super) const R: u8 = Color::Red as u8; + +pub(super) fn node( + key: u16, + color: u8, + parent: Option, + left: Option, + right: Option, +) -> NodeSpec { + NodeSpec { + key, + value: key, + color, + parent, + left, + right, + } +} + +// --------------------------------------------------------------------------- +// Helper: build empty tree with pre-allocated free slots +// --------------------------------------------------------------------------- + +/// Build an empty tree account with `n` pre-allocated free slots. +pub(super) fn build_empty_tree(n: usize, program_id: &Pubkey) -> (Pubkey, Account) { + let data_len = size_of::() + n * size_of::(); + let mut data = vec![0u8; data_len]; + + let header = data.as_mut_ptr() as *mut TreeHeader; + unsafe { + (*header).root = core::ptr::null_mut(); + (*header).top = if n > 0 { + node_vaddr(0) as *mut StackNode + } else { + core::ptr::null_mut() + }; + (*header).next = core::ptr::null_mut(); + } + + // Link free slots into a singly-linked list. + for i in 0..n { + let offset = size_of::() + i * size_of::(); + let slot = unsafe { data.as_mut_ptr().add(offset) as *mut StackNode }; + unsafe { + (*slot).next = if i + 1 < n { + node_vaddr(i + 1) as *mut StackNode + } else { + core::ptr::null_mut() + }; + } + } + + let pubkey = Pubkey::new_unique(); + let mut account = Account::new(0, data_len, program_id); + account.data = data; + (pubkey, account) +} diff --git a/examples/tree/src/tests/entrypoint.rs b/examples/tree/src/tests/entrypoint.rs new file mode 100644 index 00000000..dcfc3304 --- /dev/null +++ b/examples/tree/src/tests/entrypoint.rs @@ -0,0 +1,54 @@ +use super::*; + +#[derive(Clone, Copy)] +pub(super) enum EntrypointCase { + InvalidDiscriminator, +} + +impl EntrypointCase { + pub(super) const CASES: &'static [Self] = &[Self::InvalidDiscriminator]; +} + +impl TestCase for EntrypointCase { + fn name(&self) -> &'static str { + match self { + Self::InvalidDiscriminator => "Invalid instruction discriminator", + } + } + + fn run(&self, lang: ProgramLanguage) -> CaseResult { + match self { + Self::InvalidDiscriminator => { + let setup = setup_test(lang); + let (system_program_pubkey, _) = program::keyed_account_for_system_program(); + + let user_pubkey = Pubkey::new_unique(); + let tree_pubkey = Pubkey::new_unique(); + + let instruction = Instruction::new_with_bytes( + setup.program_id, + &[255], // Invalid discriminator. + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, Account::default()), + ]; + + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::INSTRUCTION_DISCRIMINATOR, + ) + } + } + } +} diff --git a/examples/tree/src/tests/init.rs b/examples/tree/src/tests/init.rs new file mode 100644 index 00000000..6e40de9f --- /dev/null +++ b/examples/tree/src/tests/init.rs @@ -0,0 +1,484 @@ +use super::*; +use tree_interface::{input_buffer, Instruction as TreeInstruction, TreeHeader}; + +fn init_setup( + program_language: ProgramLanguage, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let setup = setup_test(program_language); + let (system_program_pubkey, system_program_account) = + program::keyed_account_for_system_program(); + let (rent_sysvar_pubkey, rent_sysvar_account) = + setup.mollusk.sysvars.keyed_account_for_rent_sysvar(); + + let user_pubkey = Pubkey::new_unique(); + let tree_pubkey = Pubkey::new_unique(); + + let instruction = Instruction::new_with_bytes( + setup.program_id, + &[TreeInstruction::Initialize as u8], + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + AccountMeta::new_readonly(system_program_pubkey, false), + AccountMeta::new_readonly(rent_sysvar_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, Account::new(0, 0, &system_program_pubkey)), + (system_program_pubkey, system_program_account), + (rent_sysvar_pubkey, rent_sysvar_account), + ]; + + (setup, instruction, accounts) +} + +fn pda_init_setup( + program_language: ProgramLanguage, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let setup = setup_test_with_rent(program_language); + let (system_program_pubkey, system_program_account) = + program::keyed_account_for_system_program(); + let (rent_sysvar_pubkey, rent_sysvar_account) = + setup.mollusk.sysvars.keyed_account_for_rent_sysvar(); + + let user_pubkey = Pubkey::new_unique(); + let (tree_pubkey, _bump) = Pubkey::find_program_address(&[], &setup.program_id); + + let instruction = Instruction::new_with_bytes( + setup.program_id, + &[TreeInstruction::Initialize as u8], + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + AccountMeta::new_readonly(system_program_pubkey, false), + AccountMeta::new_readonly(rent_sysvar_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, Account::new(0, 0, &system_program_pubkey)), + (system_program_pubkey, system_program_account), + (rent_sysvar_pubkey, rent_sysvar_account), + ]; + + (setup, instruction, accounts) +} + +fn run_address_mismatch( + lang: ProgramLanguage, + account_index: usize, + chunk_index: usize, + chunk_size: usize, + expected_error: error_codes::error, +) -> CaseResult { + let (setup, mut instruction, mut accounts) = pda_init_setup(lang); + flip_account_address( + &mut instruction, + &mut accounts, + account_index, + chunk_index, + chunk_size, + ); + check_error(&setup, &instruction, &accounts, expected_error) +} + +#[derive(Clone, Copy)] +pub(super) enum InitCase { + InstructionData, + NAccountsTooFew, + NAccountsTooMany, + UserDataLen, + TreeDuplicate, + TreeDataLen, + SystemProgramDuplicate, + SystemProgramDataLen, + RentDuplicate, + RentAddressWord0, + RentAddressWord1, + RentAddressWord2, + RentAddressWord3, + RentAddressWord4, + RentAddressWord5, + RentAddressWord6, + RentAddressWord7, + PdaMismatchChunk0, + PdaMismatchChunk1, + PdaMismatchChunk2, + PdaMismatchChunk3, + UserInsufficientLamports, + SystemProgramAddress, + CreateAccountHappyPath, +} + +impl InitCase { + pub(super) const CASES: &'static [Self] = &[ + Self::InstructionData, + Self::NAccountsTooFew, + Self::NAccountsTooMany, + Self::UserDataLen, + Self::TreeDuplicate, + Self::TreeDataLen, + Self::SystemProgramDuplicate, + Self::SystemProgramDataLen, + Self::RentDuplicate, + Self::RentAddressWord0, + Self::RentAddressWord1, + Self::RentAddressWord2, + Self::RentAddressWord3, + Self::RentAddressWord4, + Self::RentAddressWord5, + Self::RentAddressWord6, + Self::RentAddressWord7, + ]; + + pub(super) const PDA_CASES: &'static [Self] = &[ + Self::PdaMismatchChunk0, + Self::PdaMismatchChunk1, + Self::PdaMismatchChunk2, + Self::PdaMismatchChunk3, + ]; + + pub(super) const CPI_CASES: &'static [Self] = &[ + Self::SystemProgramAddress, + Self::UserInsufficientLamports, + Self::CreateAccountHappyPath, + ]; +} + +impl TestCase for InitCase { + fn name(&self) -> &'static str { + match self { + Self::InstructionData => "Invalid instruction data length", + Self::NAccountsTooFew => "Too few accounts", + Self::NAccountsTooMany => "Too many accounts", + Self::UserDataLen => "User has nonzero data length", + Self::TreeDuplicate => "Tree account is duplicate", + Self::TreeDataLen => "Tree has nonzero data length", + Self::SystemProgramDuplicate => "System program is duplicate", + Self::SystemProgramDataLen => "System program wrong data length", + Self::RentDuplicate => "Rent sysvar is duplicate", + Self::RentAddressWord0 => "Rent address mismatch word 0", + Self::RentAddressWord1 => "Rent address mismatch word 1", + Self::RentAddressWord2 => "Rent address mismatch word 2", + Self::RentAddressWord3 => "Rent address mismatch word 3", + Self::RentAddressWord4 => "Rent address mismatch word 4", + Self::RentAddressWord5 => "Rent address mismatch word 5", + Self::RentAddressWord6 => "Rent address mismatch word 6", + Self::RentAddressWord7 => "Rent address mismatch word 7", + Self::PdaMismatchChunk0 => "PDA mismatch chunk 0", + Self::PdaMismatchChunk1 => "PDA mismatch chunk 1", + Self::PdaMismatchChunk2 => "PDA mismatch chunk 2", + Self::PdaMismatchChunk3 => "PDA mismatch chunk 3", + Self::UserInsufficientLamports => "User has insufficient Lamports", + Self::SystemProgramAddress => "System Program is wrong address", + Self::CreateAccountHappyPath => "CreateAccount happy path", + } + } + + fn fixed_costs(&self) -> u64 { + match self { + // PDA checks - sol_try_find_program_address only. + Self::PdaMismatchChunk0 + | Self::PdaMismatchChunk1 + | Self::PdaMismatchChunk2 + | Self::PdaMismatchChunk3 => fixed_costs::CREATE_PROGRAM_ADDRESS, + // CPI with system program not found (never executes). + Self::SystemProgramAddress => { + fixed_costs::CREATE_PROGRAM_ADDRESS + fixed_costs::CPI_BASE + } + // CPI with system program executing. + Self::UserInsufficientLamports | Self::CreateAccountHappyPath => { + fixed_costs::CREATE_PROGRAM_ADDRESS + + fixed_costs::CPI_BASE + + fixed_costs::SYSTEM_PROGRAM + } + _ => 0, + } + } + + fn run(&self, lang: ProgramLanguage) -> CaseResult { + match self { + Self::InstructionData => { + let (setup, mut instruction, accounts) = init_setup(lang); + instruction.data = vec![TreeInstruction::Initialize as u8, 0]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::INSTRUCTION_DATA_LEN, + ) + } + Self::NAccountsTooFew => { + let (setup, mut instruction, mut accounts) = init_setup(lang); + // Remove rent sysvar (3 accounts instead of 4). + instruction.accounts.pop(); + accounts.pop(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::N_ACCOUNTS, + ) + } + Self::NAccountsTooMany => { + let (setup, mut instruction, mut accounts) = init_setup(lang); + // Add a bogus account (5 accounts instead of 4). + let bogus_pubkey = Pubkey::new_unique(); + instruction + .accounts + .push(AccountMeta::new_readonly(bogus_pubkey, false)); + accounts.push((bogus_pubkey, Account::default())); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::N_ACCOUNTS, + ) + } + Self::UserDataLen => { + let (setup, instruction, mut accounts) = init_setup(lang); + accounts[AccountIndex::User as usize].1.data = vec![1u8; 1]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::USER_DATA_LEN, + ) + } + Self::TreeDuplicate => { + let (setup, mut instruction, mut accounts) = init_setup(lang); + instruction.accounts[AccountIndex::Tree as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::Tree as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::TREE_DUPLICATE, + ) + } + Self::TreeDataLen => { + let (setup, instruction, mut accounts) = init_setup(lang); + accounts[AccountIndex::Tree as usize].1.data = vec![1u8; 1]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::TREE_DATA_LEN, + ) + } + Self::SystemProgramDuplicate => { + let (setup, mut instruction, mut accounts) = init_setup(lang); + instruction.accounts[AccountIndex::SystemProgram as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::SystemProgram as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::SYSTEM_PROGRAM_DUPLICATE, + ) + } + Self::SystemProgramDataLen => { + let (setup, instruction, mut accounts) = init_setup(lang); + accounts[AccountIndex::SystemProgram as usize].1.data = vec![]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::SYSTEM_PROGRAM_DATA_LEN, + ) + } + Self::RentDuplicate => { + let (setup, mut instruction, mut accounts) = init_setup(lang); + instruction.accounts[AccountIndex::RentSysvar as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::RentSysvar as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::RENT_DUPLICATE, + ) + } + Self::RentAddressWord0 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 0, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord1 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 1, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord2 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 2, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord3 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 3, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord4 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 4, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord5 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 5, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord6 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 6, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::RentAddressWord7 => run_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 7, + size_of::(), + error_codes::error::RENT_ADDRESS, + ), + Self::PdaMismatchChunk0 => run_address_mismatch( + lang, + AccountIndex::Tree as usize, + 0, + size_of::(), + error_codes::error::PDA_MISMATCH, + ), + Self::PdaMismatchChunk1 => run_address_mismatch( + lang, + AccountIndex::Tree as usize, + 1, + size_of::(), + error_codes::error::PDA_MISMATCH, + ), + Self::PdaMismatchChunk2 => run_address_mismatch( + lang, + AccountIndex::Tree as usize, + 2, + size_of::(), + error_codes::error::PDA_MISMATCH, + ), + Self::PdaMismatchChunk3 => run_address_mismatch( + lang, + AccountIndex::Tree as usize, + 3, + size_of::(), + error_codes::error::PDA_MISMATCH, + ), + Self::UserInsufficientLamports => { + let (setup, instruction, mut accounts) = pda_init_setup(lang); + accounts[AccountIndex::User as usize].1.lamports = 0; + // SystemError::ResultWithNegativeLamports. + check_result(&setup, &instruction, &accounts, ProgramError::Custom(1)) + } + Self::SystemProgramAddress => { + let (setup, mut instruction, mut accounts) = pda_init_setup(lang); + let fake_pubkey = Pubkey::new_unique(); + accounts[AccountIndex::SystemProgram as usize].0 = fake_pubkey; + instruction.accounts[AccountIndex::SystemProgram as usize].pubkey = fake_pubkey; + check_result( + &setup, + &instruction, + &accounts, + ProgramError::NotEnoughAccountKeys, + ) + } + Self::CreateAccountHappyPath => { + let (setup, instruction, accounts) = pda_init_setup(lang); + let result = setup.mollusk.process_instruction(&instruction, &accounts); + match &result.program_result { + MolluskResult::Success => { + let tree = &result.resulting_accounts[AccountIndex::Tree as usize].1; + let rent_data = &accounts[AccountIndex::RentSysvar as usize].1.data; + let rent = Rent::from_bytes(rent_data).unwrap(); + let expected_lamports = + rent.try_minimum_balance(cpi::TREE_DATA_LEN).unwrap(); + let mut errors = Vec::new(); + if tree.owner != setup.program_id { + errors.push(format!( + "owner: expected {:?}, got {:?}", + setup.program_id, tree.owner + )); + } + if tree.data.len() != cpi::TREE_DATA_LEN { + errors.push(format!( + "data len: expected {}, got {}", + cpi::TREE_DATA_LEN, + tree.data.len() + )); + } + if tree.lamports != expected_lamports { + errors.push(format!( + "lamports: expected {}, got {}", + expected_lamports, tree.lamports + )); + } + let expected_next = MM_INPUT_START + + input_buffer::TREE_DATA_OFF as u64 + + size_of::() as u64; + let header = unsafe { &*(tree.data.as_ptr() as *const TreeHeader) }; + let actual_next = header.next as u64; + if actual_next != expected_next { + errors.push(format!( + "next: expected {:#x}, got {:#x}", + expected_next, actual_next + )); + } + let config = Config { + panic: false, + verbose: false, + }; + if !result.run_checks(&[Check::all_rent_exempt()], &config, &setup.mollusk) + { + errors.push("not all accounts are rent exempt".to_string()); + } + CaseResult { + cu: result.compute_units_consumed, + error: if errors.is_empty() { + None + } else { + Some(errors.join("; ")) + }, + } + } + other => CaseResult { + cu: result.compute_units_consumed, + error: Some(format!("expected Success, got {:?}", other)), + }, + } + } + } + } +} diff --git a/examples/tree/src/tests/insert.rs b/examples/tree/src/tests/insert.rs new file mode 100644 index 00000000..035e9a58 --- /dev/null +++ b/examples/tree/src/tests/insert.rs @@ -0,0 +1,1704 @@ +// cspell:word sysprog +use super::common::*; +use super::*; +use tree_interface::{ + input_buffer, tree, InsertInstruction, Instruction as TreeInstruction, InstructionHeader, + StackNode, TreeHeader, TreeNode, +}; + +// --------------------------------------------------------------------------- +// Helpers: allocation setup +// --------------------------------------------------------------------------- + +fn insert_instruction_data() -> InsertInstruction { + InsertInstruction { + header: InstructionHeader { + discriminator: TreeInstruction::Insert as u8, + }, + key: 42, + value: 1, + } +} + +fn insert_setup( + program_language: ProgramLanguage, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let setup = setup_test_with_rent(program_language); + let (system_program_pubkey, system_program_account) = + program::keyed_account_for_system_program(); + let (rent_sysvar_pubkey, rent_sysvar_account) = + setup.mollusk.sysvars.keyed_account_for_rent_sysvar(); + + let user_pubkey = Pubkey::new_unique(); + let tree_pubkey = Pubkey::new_unique(); + + let insn_data = insert_instruction_data(); + let instruction = Instruction::new_with_bytes( + setup.program_id, + unsafe { as_bytes(&insn_data) }, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + AccountMeta::new_readonly(system_program_pubkey, false), + AccountMeta::new_readonly(rent_sysvar_pubkey, false), + ], + ); + + // Tree starts with TREE_DATA_LEN (header only), top = null to trigger allocation. + let rent = Rent::from_bytes(&rent_sysvar_account.data).unwrap(); + let tree_lamports = rent.try_minimum_balance(cpi::TREE_DATA_LEN).unwrap(); + let mut tree_account = Account::new(tree_lamports, cpi::TREE_DATA_LEN, &setup.program_id); + // top is null (zeroed) — triggers allocation path. + // next pointer must point to the first allocation slot (right after header). + let next_ptr = + MM_INPUT_START + input_buffer::TREE_DATA_OFF as u64 + size_of::() as u64; + let next_off = tree::HEADER_NEXT_OFF as usize; + tree_account.data[next_off..next_off + size_of::<*mut TreeNode>()] + .copy_from_slice(&next_ptr.to_le_bytes()); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account), + (system_program_pubkey, system_program_account), + (rent_sysvar_pubkey, rent_sysvar_account), + ]; + + (setup, instruction, accounts) +} + +fn insert_skip_alloc_setup( + program_language: ProgramLanguage, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let setup = setup_test(program_language); + let (system_program_pubkey, _) = program::keyed_account_for_system_program(); + + let user_pubkey = Pubkey::new_unique(); + let tree_pubkey = Pubkey::new_unique(); + + let insn_data = insert_instruction_data(); + let instruction = Instruction::new_with_bytes( + setup.program_id, + unsafe { as_bytes(&insn_data) }, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + ], + ); + + // Initialize tree account with a free node on the stack so insert pops instead of allocating. + let tree_data_len = cpi::TREE_DATA_LEN + size_of::(); + let mut tree_data = vec![0u8; tree_data_len]; + // top points to the free node (right after header in memory map). + let top_ptr = + MM_INPUT_START + input_buffer::TREE_DATA_OFF as u64 + size_of::() as u64; + let top_off = tree::HEADER_TOP_OFF as usize; + tree_data[top_off..top_off + size_of::<*mut StackNode>()] + .copy_from_slice(&top_ptr.to_le_bytes()); + // Free node's next is null (zeroed) — only one free node on the stack. + let mut tree_account = Account::new(0, tree_data_len, &setup.program_id); + tree_account.data = tree_data; + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account), + ]; + + (setup, instruction, accounts) +} + +fn insert_alloc_address_mismatch( + lang: ProgramLanguage, + account_index: usize, + chunk_index: usize, + expected_error: error_codes::error, +) -> CaseResult { + let (setup, mut instruction, mut accounts) = insert_setup(lang); + flip_account_address( + &mut instruction, + &mut accounts, + account_index, + chunk_index, + size_of::(), + ); + check_error(&setup, &instruction, &accounts, expected_error) +} + +/// Set up an insert where the tree account is already at +/// `MAX_PERMITTED_DATA_LENGTH` so that allocating one more `TreeNode` +/// exceeds the absolute account size limit. +fn insert_max_data_setup( + program_language: ProgramLanguage, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + const MAX_PERMITTED_DATA_LENGTH: usize = 10 * 1024 * 1024; + + let setup = setup_test_with_rent(program_language); + let (system_program_pubkey, system_program_account) = + program::keyed_account_for_system_program(); + let (rent_sysvar_pubkey, rent_sysvar_account) = + setup.mollusk.sysvars.keyed_account_for_rent_sysvar(); + + let user_pubkey = Pubkey::new_unique(); + let tree_pubkey = Pubkey::new_unique(); + + let insn_data = insert_instruction_data(); + let instruction = Instruction::new_with_bytes( + setup.program_id, + unsafe { as_bytes(&insn_data) }, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + AccountMeta::new_readonly(system_program_pubkey, false), + AccountMeta::new_readonly(rent_sysvar_pubkey, false), + ], + ); + + // Tree account already at MAX_PERMITTED_DATA_LENGTH. + let rent = Rent::from_bytes(&rent_sysvar_account.data).unwrap(); + let tree_lamports = rent.try_minimum_balance(MAX_PERMITTED_DATA_LENGTH).unwrap(); + let mut tree_account = + Account::new(tree_lamports, MAX_PERMITTED_DATA_LENGTH, &setup.program_id); + // top = null → forces allocation path (no free nodes to recycle). + // next → points right after current data (where the new node would go). + let next_ptr = + MM_INPUT_START + input_buffer::TREE_DATA_OFF as u64 + MAX_PERMITTED_DATA_LENGTH as u64; + let next_off = tree::HEADER_NEXT_OFF as usize; + tree_account.data[next_off..next_off + size_of::<*mut TreeNode>()] + .copy_from_slice(&next_ptr.to_le_bytes()); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account), + (system_program_pubkey, system_program_account), + (rent_sysvar_pubkey, rent_sysvar_account), + ]; + + (setup, instruction, accounts) +} + +// --------------------------------------------------------------------------- +// Helpers: tree test setup and runners +// --------------------------------------------------------------------------- + +fn insert_tree_setup( + lang: ProgramLanguage, + desc: &TreeSpec, + insert_key: u16, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let setup = setup_test(lang); + let (system_program_pubkey, _) = mollusk_svm::program::keyed_account_for_system_program(); + + let user_pubkey = Pubkey::new_unique(); + let (tree_pubkey, tree_account) = build_tree_account(desc, &setup.program_id); + + let insn_data = InsertInstruction { + header: InstructionHeader { + discriminator: TreeInstruction::Insert as u8, + }, + key: insert_key, + value: 1, + }; + + let instruction = Instruction::new_with_bytes( + setup.program_id, + unsafe { as_bytes(&insn_data) }, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account), + ]; + + (setup, instruction, accounts) +} + +/// Run an insert and assert success with full tree state check. +fn run_success( + lang: ProgramLanguage, + desc: &TreeSpec, + insert_key: u16, + expected: &TreeSpec, +) -> CaseResult { + if let Err(e) = assert_invariants(desc) { + return CaseResult { + cu: 0, + error: Some(format!("desc invariant: {}", e)), + }; + } + if let Err(e) = assert_invariants(expected) { + return CaseResult { + cu: 0, + error: Some(format!("exp invariant: {}", e)), + }; + } + let (setup, instruction, accounts) = insert_tree_setup(lang, desc, insert_key); + let result = setup.mollusk.process_instruction(&instruction, &accounts); + match &result.program_result { + MolluskResult::Success => { + let tree_data = &result.resulting_accounts[AccountIndex::Tree as usize] + .1 + .data; + match assert_tree_account(tree_data, expected) { + Ok(()) => CaseResult { + cu: result.compute_units_consumed, + error: None, + }, + Err(e) => CaseResult { + cu: result.compute_units_consumed, + error: Some(e), + }, + } + } + other => CaseResult { + cu: result.compute_units_consumed, + error: Some(format!("expected Success, got {:?}", other)), + }, + } +} + +/// Run an insert and check for KEY_EXISTS error. +fn run_dup_error(lang: ProgramLanguage, desc: &TreeSpec, insert_key: u16) -> CaseResult { + if let Err(e) = assert_invariants(desc) { + return CaseResult { + cu: 0, + error: Some(format!("desc invariant: {}", e)), + }; + } + let (setup, instruction, accounts) = insert_tree_setup(lang, desc, insert_key); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::KEY_EXISTS, + ) +} + +// --------------------------------------------------------------------------- +// Test case enum +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +pub(super) enum InsertCase { + // Input validation. + InputDataShort, + InputDataLong, + InputNAccounts, + InputUserDataLen, + InputTreeDuplicate, + // Allocation checks. + AllocNAccounts, + AllocSysprogDuplicate, + AllocSysprogDataLen, + AllocRentDuplicate, + AllocRentAddrChunk0, + AllocRentAddrChunk1, + AllocRentAddrChunk2, + AllocRentAddrChunk3, + // Allocation happy path (CPI overhead). + AllocHappyPath, + // Allocation exceeds max permitted data length. + AllocMaxDataLen, + // Search — expect KEY_EXISTS error. + SearchDupRoot, + SearchDupLeft, + SearchDupRight, + // Insert to empty tree. + EmptyTree, + // Case 1: parent is black. + Case1Left, + Case1Right, + // Case 4: parent is root and red. + Case4Left, + Case4Right, + // Case 2+3: red uncle, propagate to root. + Case23LeftLeft, + Case23LeftRight, + Case23RightLeft, + Case23RightRight, + // Case 2+1: red uncle, propagate to black ancestor. + Case21Left, + Case21Right, + // Case 6: single rotation (outer child, null uncle). + Case6LeftNull, + Case6RightNull, + // Case 5+6: double rotation (inner child, null uncle). + Case56LeftNull, + Case56RightNull, + // Case 6: non-null great-grandparent. + Case6GgpLeftLeft, + Case6GgpLeftRight, + Case6GgpRightRight, + Case6GgpRightLeft, + // Case 2+6: non-null new_child in rotation. + Case26Left, + Case26Right, + // Case 2+5+6: non-null new_child in rotations. + Case256Left, + Case256Right, +} + +impl InsertCase { + pub(super) const INPUT_CASES: &'static [Self] = &[ + Self::InputDataShort, + Self::InputDataLong, + Self::InputNAccounts, + Self::InputUserDataLen, + Self::InputTreeDuplicate, + ]; + + pub(super) const ALLOC_CHECK_CASES: &'static [Self] = &[ + Self::AllocNAccounts, + Self::AllocSysprogDuplicate, + Self::AllocSysprogDataLen, + Self::AllocRentDuplicate, + Self::AllocRentAddrChunk0, + Self::AllocRentAddrChunk1, + Self::AllocRentAddrChunk2, + Self::AllocRentAddrChunk3, + ]; + + pub(super) const ALLOC_CASES: &'static [Self] = &[Self::AllocHappyPath, Self::AllocMaxDataLen]; + + pub(super) const SEARCH_CASES: &'static [Self] = &[ + Self::SearchDupRoot, + Self::SearchDupLeft, + Self::SearchDupRight, + ]; + + pub(super) const TREE_CASES: &'static [Self] = &[ + Self::EmptyTree, + Self::Case1Left, + Self::Case1Right, + Self::Case4Left, + Self::Case4Right, + Self::Case23LeftLeft, + Self::Case23LeftRight, + Self::Case23RightLeft, + Self::Case23RightRight, + Self::Case21Left, + Self::Case21Right, + Self::Case6LeftNull, + Self::Case6RightNull, + Self::Case56LeftNull, + Self::Case56RightNull, + Self::Case6GgpLeftLeft, + Self::Case6GgpLeftRight, + Self::Case6GgpRightRight, + Self::Case6GgpRightLeft, + Self::Case26Left, + Self::Case26Right, + Self::Case256Left, + Self::Case256Right, + ]; +} + +impl TestCase for InsertCase { + fn name(&self) -> &'static str { + match self { + Self::InputDataShort => "Instruction data too short", + Self::InputDataLong => "Instruction data too long", + Self::InputNAccounts => "Too few accounts", + Self::InputUserDataLen => "User has nonzero data length", + Self::InputTreeDuplicate => "Tree account is duplicate", + Self::AllocNAccounts => "Wrong N accounts for allocation", + Self::AllocSysprogDuplicate => "System program is duplicate", + Self::AllocSysprogDataLen => "System program wrong data length", + Self::AllocRentDuplicate => "Rent sysvar is duplicate", + Self::AllocRentAddrChunk0 => "Rent address mismatch chunk 0", + Self::AllocRentAddrChunk1 => "Rent address mismatch chunk 1", + Self::AllocRentAddrChunk2 => "Rent address mismatch chunk 2", + Self::AllocRentAddrChunk3 => "Rent address mismatch chunk 3", + Self::AllocHappyPath => "Insert alloc happy path", + Self::AllocMaxDataLen => "Alloc exceeds max data length", + Self::SearchDupRoot => "Dup at root", + Self::SearchDupLeft => "Dup in left", + Self::SearchDupRight => "Dup in right", + Self::EmptyTree => "Empty tree", + Self::Case1Left => "Case 1: left child", + Self::Case1Right => "Case 1: right child", + Self::Case4Left => "Case 4: left child", + Self::Case4Right => "Case 4: right child", + Self::Case23LeftLeft => "Case 2+3: left-left", + Self::Case23LeftRight => "Case 2+3: left-right", + Self::Case23RightLeft => "Case 2+3: right-left", + Self::Case23RightRight => "Case 2+3: right-right", + Self::Case21Left => "Case 2+1: left", + Self::Case21Right => "Case 2+1: right", + Self::Case6LeftNull => "Case 6: left-left null uncle", + Self::Case6RightNull => "Case 6: right-right null uncle", + Self::Case56LeftNull => "Case 5+6: left-right null uncle", + Self::Case56RightNull => "Case 5+6: right-left null uncle", + Self::Case6GgpLeftLeft => "Case 6: GGP non-null, LL GP-left", + Self::Case6GgpLeftRight => "Case 6: GGP non-null, LL GP-right", + Self::Case6GgpRightRight => "Case 6: GGP non-null, RR GP-right", + Self::Case6GgpRightLeft => "Case 6: GGP non-null, RR GP-left", + Self::Case26Left => "Case 2+6: non-null new_child dir_l", + Self::Case26Right => "Case 2+6: non-null new_child dir_r", + Self::Case256Left => "Case 2+5+6: non-null new_child dir_l", + Self::Case256Right => "Case 2+5+6: non-null new_child dir_r", + } + } + + fn fixed_costs(&self) -> u64 { + match self { + Self::AllocHappyPath | Self::AllocMaxDataLen => { + fixed_costs::CPI_BASE + fixed_costs::SYSTEM_PROGRAM + } + _ => 0, + } + } + + fn run(&self, lang: ProgramLanguage) -> CaseResult { + match self { + // ----- Input validation ----- + Self::InputDataShort => { + let (setup, mut instruction, accounts) = insert_skip_alloc_setup(lang); + // Correct discriminator but wrong length (1 byte instead of 5). + instruction.data = vec![TreeInstruction::Insert as u8]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::INSTRUCTION_DATA_LEN, + ) + } + Self::InputDataLong => { + let (setup, mut instruction, accounts) = insert_skip_alloc_setup(lang); + // Correct discriminator but wrong length (6 bytes instead of 5). + instruction.data = vec![TreeInstruction::Insert as u8, 0, 0, 0, 0, 0]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::INSTRUCTION_DATA_LEN, + ) + } + Self::InputNAccounts => { + let (setup, mut instruction, mut accounts) = insert_skip_alloc_setup(lang); + // Remove tree account (1 account instead of 2). + instruction.accounts.pop(); + accounts.pop(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::N_ACCOUNTS, + ) + } + Self::InputUserDataLen => { + let (setup, instruction, mut accounts) = insert_skip_alloc_setup(lang); + accounts[AccountIndex::User as usize].1.data = vec![1u8; 1]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::USER_DATA_LEN, + ) + } + Self::InputTreeDuplicate => { + let (setup, mut instruction, mut accounts) = insert_skip_alloc_setup(lang); + instruction.accounts[AccountIndex::Tree as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::Tree as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::TREE_DUPLICATE, + ) + } + + // ----- Allocation checks ----- + Self::AllocNAccounts => { + // Use insert_setup (top=null triggers allocation) but strip CPI accounts. + let (setup, mut instruction, mut accounts) = insert_setup(lang); + instruction.accounts.truncate(2); + accounts.truncate(2); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::N_ACCOUNTS_INSERT_ALLOCATION, + ) + } + Self::AllocSysprogDuplicate => { + let (setup, mut instruction, mut accounts) = insert_setup(lang); + instruction.accounts[AccountIndex::SystemProgram as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::SystemProgram as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::SYSTEM_PROGRAM_DUPLICATE, + ) + } + Self::AllocSysprogDataLen => { + let (setup, instruction, mut accounts) = insert_setup(lang); + accounts[AccountIndex::SystemProgram as usize].1.data = vec![]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::SYSTEM_PROGRAM_DATA_LEN, + ) + } + Self::AllocRentDuplicate => { + let (setup, mut instruction, mut accounts) = insert_setup(lang); + instruction.accounts[AccountIndex::RentSysvar as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::RentSysvar as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::RENT_DUPLICATE, + ) + } + Self::AllocRentAddrChunk0 => insert_alloc_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 0, + error_codes::error::RENT_ADDRESS, + ), + Self::AllocRentAddrChunk1 => insert_alloc_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 1, + error_codes::error::RENT_ADDRESS, + ), + Self::AllocRentAddrChunk2 => insert_alloc_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 2, + error_codes::error::RENT_ADDRESS, + ), + Self::AllocRentAddrChunk3 => insert_alloc_address_mismatch( + lang, + AccountIndex::RentSysvar as usize, + 3, + error_codes::error::RENT_ADDRESS, + ), + + // ----- Allocation happy path ----- + Self::AllocHappyPath => { + let (setup, instruction, accounts) = insert_setup(lang); + let result = setup.mollusk.process_instruction(&instruction, &accounts); + match &result.program_result { + MolluskResult::Success => { + let tree = &result.resulting_accounts[AccountIndex::Tree as usize].1; + let rent_data = &accounts[AccountIndex::RentSysvar as usize].1.data; + let rent = Rent::from_bytes(rent_data).unwrap(); + let expected_data_len = cpi::TREE_DATA_LEN + size_of::(); + let expected_lamports = + rent.try_minimum_balance(expected_data_len).unwrap(); + let mut errors = Vec::new(); + if tree.data.len() != expected_data_len { + errors.push(format!( + "data len: expected {}, got {}", + expected_data_len, + tree.data.len() + )); + } + if tree.lamports != expected_lamports { + errors.push(format!( + "lamports: expected {}, got {}", + expected_lamports, tree.lamports + )); + } + // Verify header pointers. + let header = unsafe { &*(tree.data.as_ptr() as *const TreeHeader) }; + let node_addr = MM_INPUT_START + + input_buffer::TREE_DATA_OFF as u64 + + size_of::() as u64; + let expected_next = node_addr + size_of::() as u64; + if header.next as u64 != expected_next { + errors.push(format!( + "next: expected {:#x}, got {:#x}", + expected_next, header.next as u64 + )); + } + if header.root as u64 != node_addr { + errors.push(format!( + "root: expected {:#x}, got {:#x}", + node_addr, header.root as u64 + )); + } + // Verify node key and value. + let node = unsafe { + &*(tree.data.as_ptr().add(size_of::()) as *const TreeNode) + }; + let key = node.key; + let value = node.value; + if key != 42 { + errors.push(format!("key: expected 42, got {}", key)); + } + if value != 1 { + errors.push(format!("value: expected 1, got {}", value)); + } + let config = Config { + panic: false, + verbose: false, + }; + if !result.run_checks(&[Check::all_rent_exempt()], &config, &setup.mollusk) + { + errors.push("not all accounts are rent exempt".to_string()); + } + CaseResult { + cu: result.compute_units_consumed, + error: if errors.is_empty() { + None + } else { + Some(errors.join("; ")) + }, + } + } + other => CaseResult { + cu: result.compute_units_consumed, + error: Some(format!("expected Success, got {:?}", other)), + }, + } + } + + // ----- Allocation: max data length ----- + Self::AllocMaxDataLen => { + let (setup, instruction, accounts) = insert_max_data_setup(lang); + check_result( + &setup, + &instruction, + &accounts, + ProgramError::InvalidRealloc, + ) + } + + // ----- Search: duplicate key errors ----- + + // Root with key 10, insert 10. + Self::SearchDupRoot => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + run_dup_error(lang, &desc, 10) + } + // Root 10, left child 5, insert 5. + Self::SearchDupLeft => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + ], + }; + run_dup_error(lang, &desc, 5) + } + // Root 10, right child 15, insert 15. + Self::SearchDupRight => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None), + ], + }; + run_dup_error(lang, &desc, 15) + } + + // ----- Insert to empty tree ----- + Self::EmptyTree => { + let desc = TreeSpec { + root: None, + top: None, + nodes: &[], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(42, R, None, None, None).val(1)], + }; + run_success(lang, &desc, 42, &exp) + } + + // ----- Case 1: parent is black ----- + + // B(10) root, insert 5 → left child. + Self::Case1Left => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None).val(1), + ], + }; + run_success(lang, &desc, 5, &exp) + } + // B(10) root, insert 15 → right child. + Self::Case1Right => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None).val(1), + ], + }; + run_success(lang, &desc, 15, &exp) + } + + // ----- Case 4: parent is root and red ----- + + // R(10) root, insert 5 → left child, parent recolored B. + Self::Case4Left => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, R, None, None, None)], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None).val(1), + ], + }; + run_success(lang, &desc, 5, &exp) + } + // R(10) root, insert 15 → right child, parent recolored B. + Self::Case4Right => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, R, None, None, None)], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None).val(1), + ], + }; + run_success(lang, &desc, 15, &exp) + } + + // ----- Case 2+3: red uncle, propagate to root ----- + // Before: B(10) root, R(5) left, R(15) right. + // After recolor: R(10), B(5), B(15), inserted node red. + Self::Case23LeftLeft => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)), + node(5, B, Some(0), Some(3), None), + node(15, B, Some(0), None, None), + node(1, R, Some(1), None, None), + ], + }; + run_success(lang, &desc, 1, &exp) + } + Self::Case23LeftRight => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)), + node(5, B, Some(0), None, Some(3)), + node(15, B, Some(0), None, None), + node(7, R, Some(1), None, None).val(1), + ], + }; + run_success(lang, &desc, 7, &exp) + } + Self::Case23RightLeft => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), Some(3), None), + node(12, R, Some(2), None, None).val(1), + ], + }; + run_success(lang, &desc, 12, &exp) + } + Self::Case23RightRight => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, Some(3)), + node(20, R, Some(2), None, None).val(1), + ], + }; + run_success(lang, &desc, 20, &exp) + } + + // ----- Case 2+1: red uncle, propagate to black ancestor ----- + + // Before: B(20) root, B(10) left with R(5)/R(15), B(25) right. + // After: B(20), R(10), B(5)/B(15), B(25), R(1) inserted. + Self::Case21Left => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(4)), + node(10, B, Some(0), Some(2), Some(3)), + node(5, R, Some(1), None, None), + node(15, R, Some(1), None, None), + node(25, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(4)), + node(10, R, Some(0), Some(2), Some(3)), + node(5, B, Some(1), Some(5), None), + node(15, B, Some(1), None, None), + node(25, B, Some(0), None, None), + node(1, R, Some(2), None, None), + ], + }; + run_success(lang, &desc, 1, &exp) + } + // Mirror: B(2) root, B(1) left, B(10) right with R(5)/R(15). + Self::Case21Right => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(2, B, None, Some(4), Some(1)), + node(10, B, Some(0), Some(2), Some(3)), + node(5, R, Some(1), None, None), + node(15, R, Some(1), None, None), + node(1, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(2, B, None, Some(4), Some(1)), + node(10, R, Some(0), Some(2), Some(3)), + node(5, B, Some(1), None, None), + node(15, B, Some(1), None, Some(5)), + node(1, B, Some(0), None, None), + node(20, R, Some(3), None, None).val(1), + ], + }; + run_success(lang, &desc, 20, &exp) + } + + // ----- Case 6: single rotation (outer child) ----- + + // Left-left, null uncle: B(10) root, R(5) left, insert 1. + // After: B(5) new root, R(1) left, R(10) right. + Self::Case6LeftNull => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: None, + nodes: &[ + node(10, R, Some(1), None, None), + node(5, B, None, Some(2), Some(0)), + node(1, R, Some(1), None, None), + ], + }; + run_success(lang, &desc, 1, &exp) + } + // Right-right, null uncle: B(10) root, R(15) right, insert 20. + // After: B(15) new root, R(10) left, R(20) right. + Self::Case6RightNull => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: None, + nodes: &[ + node(10, R, Some(1), None, None), + node(15, B, None, Some(0), Some(2)), + node(20, R, Some(1), None, None).val(1), + ], + }; + run_success(lang, &desc, 20, &exp) + } + // ----- Case 5+6: double rotation (inner child) ----- + + // Left-right, null uncle: B(10) root, R(5) left, insert 7. + // After: B(7) new root, R(5) left, R(10) right. + Self::Case56LeftNull => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: None, + nodes: &[ + node(10, R, Some(2), None, None), + node(5, R, Some(2), None, None), + node(7, B, None, Some(1), Some(0)).val(1), + ], + }; + run_success(lang, &desc, 7, &exp) + } + // Right-left, null uncle: B(10) root, R(15) right, insert 12. + // After: B(12) new root, R(10) left, R(15) right. + Self::Case56RightNull => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: None, + nodes: &[ + node(10, R, Some(2), None, None), + node(15, R, Some(2), None, None), + node(12, B, None, Some(0), Some(1)).val(1), + ], + }; + run_success(lang, &desc, 12, &exp) + } + // ----- Case 6: non-null great-grandparent ----- + + // LL, GP is left child of GGP. Insert 1. + // B(20) root, B(10) left with R(5) left, B(25) right. + // Case 6 dir_l rotates GP=B(10) right under GGP=B(20). + // GGP.child[L] = parent (GP was left child). + Self::Case6GgpLeftLeft => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(3)), + node(10, B, Some(0), Some(2), None), + node(5, R, Some(1), None, None), + node(25, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(2), Some(3)), + node(10, R, Some(2), None, None), + node(5, B, Some(0), Some(4), Some(1)), + node(25, B, Some(0), None, None), + node(1, R, Some(2), None, None), + ], + }; + run_success(lang, &desc, 1, &exp) + } + // LL, GP is right child of GGP. Insert 10. + // B(5) root, B(3) left, B(20) right with R(15) left. + // Case 6 dir_l rotates GP=B(20) right under GGP=B(5). + // GGP.child[R] = parent (GP was right child). + Self::Case6GgpLeftRight => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(5, B, None, Some(1), Some(2)), + node(3, B, Some(0), None, None), + node(20, B, Some(0), Some(3), None), + node(15, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(5, B, None, Some(1), Some(3)), + node(3, B, Some(0), None, None), + node(20, R, Some(3), None, None), + node(15, B, Some(0), Some(4), Some(2)), + node(10, R, Some(3), None, None).val(1), + ], + }; + run_success(lang, &desc, 10, &exp) + } + // RR, GP is right child of GGP. Insert 25. + // B(5) root, B(3) left, B(15) right with R(20) right. + // Case 6 dir_r rotates GP=B(15) left under GGP=B(5). + // GGP.child[R] = parent (GP was right child). + Self::Case6GgpRightRight => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(5, B, None, Some(1), Some(2)), + node(3, B, Some(0), None, None), + node(15, B, Some(0), None, Some(3)), + node(20, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(5, B, None, Some(1), Some(3)), + node(3, B, Some(0), None, None), + node(15, R, Some(3), None, None), + node(20, B, Some(0), Some(2), Some(4)), + node(25, R, Some(3), None, None).val(1), + ], + }; + run_success(lang, &desc, 25, &exp) + } + // RR, GP is left child of GGP. Insert 17. + // B(20) root, B(10) left with R(15) right, B(25) right. + // Case 6 dir_r rotates GP=B(10) left under GGP=B(20). + // GGP.child[L] = parent (GP was left child). + Self::Case6GgpRightLeft => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(3)), + node(10, B, Some(0), None, Some(2)), + node(15, R, Some(1), None, None), + node(25, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(2), Some(3)), + node(10, R, Some(2), None, None), + node(15, B, Some(0), Some(1), Some(4)), + node(25, B, Some(0), None, None), + node(17, R, Some(2), None, None).val(1), + ], + }; + run_success(lang, &desc, 17, &exp) + } + + // ----- Case 2+6: non-null new_child in rotation ----- + + // Dir_l: insert 1 into 7-node tree. + // Case 2 recolors at bottom, then case 6 dir_l rotates with + // new_child = B(15) non-null. + Self::Case26Left => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(3)), + node(10, R, Some(0), Some(2), Some(6)), + node(5, B, Some(1), Some(4), Some(5)), + node(25, B, Some(0), None, None), + node(3, R, Some(2), None, None), + node(7, R, Some(2), None, None), + node(15, B, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: None, + nodes: &[ + node(20, R, Some(1), Some(6), Some(3)), + node(10, B, None, Some(2), Some(0)), + node(5, R, Some(1), Some(4), Some(5)), + node(25, B, Some(0), None, None), + node(3, B, Some(2), Some(7), None), + node(7, B, Some(2), None, None), + node(15, B, Some(0), None, None), + node(1, R, Some(4), None, None), + ], + }; + run_success(lang, &desc, 1, &exp) + } + // Dir_r: insert 30 into 7-node tree. + // Case 2 recolors at bottom, then case 6 dir_r rotates with + // new_child = B(10) non-null. + Self::Case26Right => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(5, B, None, Some(1), Some(2)), + node(3, B, Some(0), None, None), + node(15, R, Some(0), Some(3), Some(4)), + node(10, B, Some(2), None, None), + node(20, B, Some(2), Some(5), Some(6)), + node(17, R, Some(4), None, None), + node(25, R, Some(4), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: None, + nodes: &[ + node(5, R, Some(2), Some(1), Some(3)), + node(3, B, Some(0), None, None), + node(15, B, None, Some(0), Some(4)), + node(10, B, Some(0), None, None), + node(20, R, Some(2), Some(5), Some(6)), + node(17, B, Some(4), None, None), + node(25, B, Some(4), None, Some(7)), + node(30, R, Some(6), None, None).val(1), + ], + }; + run_success(lang, &desc, 30, &exp) + } + + // ----- Case 2+5+6: non-null new_child in rotations ----- + + // Dir_l: insert 11 into 7-node tree. + // Case 2 recolors at bottom, then case 5 dir_l rotates with + // new_child = B(12) non-null, then case 6 dir_l rotates with + // new_child = B(17) non-null. + Self::Case256Left => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(4)), + node(10, R, Some(0), Some(2), Some(3)), + node(5, B, Some(1), None, None), + node(15, B, Some(1), Some(5), Some(6)), + node(25, B, Some(0), None, None), + node(12, R, Some(3), None, None), + node(17, R, Some(3), None, None), + ], + }; + let exp = TreeSpec { + root: Some(3), + top: None, + nodes: &[ + node(20, R, Some(3), Some(6), Some(4)), + node(10, R, Some(3), Some(2), Some(5)), + node(5, B, Some(1), None, None), + node(15, B, None, Some(1), Some(0)), + node(25, B, Some(0), None, None), + node(12, B, Some(1), Some(7), None), + node(17, B, Some(0), None, None), + node(11, R, Some(5), None, None).val(1), + ], + }; + run_success(lang, &desc, 11, &exp) + } + // Dir_r: insert 18 into 7-node tree. + // Case 2 recolors at bottom, then case 5 dir_r rotates with + // new_child = B(17) non-null, then case 6 dir_r rotates with + // new_child = B(12) non-null. + Self::Case256Right => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, R, Some(0), Some(3), Some(4)), + node(15, B, Some(2), Some(5), Some(6)), + node(25, B, Some(2), None, None), + node(12, R, Some(3), None, None), + node(17, R, Some(3), None, None), + ], + }; + let exp = TreeSpec { + root: Some(3), + top: None, + nodes: &[ + node(10, R, Some(3), Some(1), Some(5)), + node(5, B, Some(0), None, None), + node(20, R, Some(3), Some(6), Some(4)), + node(15, B, None, Some(0), Some(2)), + node(25, B, Some(2), None, None), + node(12, B, Some(0), None, None), + node(17, B, Some(2), None, Some(7)), + node(18, R, Some(6), None, None).val(1), + ], + }; + run_success(lang, &desc, 18, &exp) + } + } + } +} + +// --------------------------------------------------------------------------- +// Multi-insert integration tests +// --------------------------------------------------------------------------- + +struct MultiInsertStep<'a> { + key: u16, + expected: TreeSpec<'a>, +} + +fn run_multi_insert(lang: ProgramLanguage, steps: &[MultiInsertStep]) -> CaseResult { + let setup = setup_test(lang); + let (system_program_pubkey, _) = program::keyed_account_for_system_program(); + + let user_pubkey = Pubkey::new_unique(); + let (tree_pubkey, mut tree_account) = build_empty_tree(steps.len(), &setup.program_id); + + let mut total_cu = 0u64; + + for (i, step) in steps.iter().enumerate() { + if let Err(e) = assert_invariants(&step.expected) { + return CaseResult { + cu: 0, + error: Some(format!( + "step {} (key={}) exp invariant: {}", + i, step.key, e + )), + }; + } + + let insn_data = InsertInstruction { + header: InstructionHeader { + discriminator: TreeInstruction::Insert as u8, + }, + key: step.key, + value: 1, + }; + + let instruction = Instruction::new_with_bytes( + setup.program_id, + unsafe { as_bytes(&insn_data) }, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account.clone()), + ]; + + let result = setup.mollusk.process_instruction(&instruction, &accounts); + total_cu += result.compute_units_consumed; + match &result.program_result { + MolluskResult::Success => { + tree_account = result.resulting_accounts[AccountIndex::Tree as usize] + .1 + .clone(); + if let Err(e) = assert_tree_account(&tree_account.data, &step.expected) { + return CaseResult { + cu: total_cu, + error: Some(format!("step {} (key={}): {}", i, step.key, e)), + }; + } + } + other => { + return CaseResult { + cu: total_cu, + error: Some(format!( + "step {} (key={}): expected Success, got {:?}", + i, step.key, other + )), + }; + } + } + } + + CaseResult { + cu: total_cu, + error: None, + } +} + +// --------------------------------------------------------------------------- +// Multi-insert case enum +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +pub(super) enum MultiInsertCase { + /// 10, 5, 15 — case 2+3 recolor only. + Balanced3, + /// 10, 5, 1 — case 5+6 right rotation. + LeftSkew, + /// 10, 15, 20 — case 5+6 left rotation. + RightSkew, + /// 10, 5, 7 — case 4+5+6 double rotation. + Zigzag, + /// 10, 5, 15, 3, 7, 12, 20 — full 7-node tree. + Full7, +} + +impl MultiInsertCase { + pub(super) const CASES: &'static [Self] = &[ + Self::Balanced3, + Self::LeftSkew, + Self::RightSkew, + Self::Zigzag, + Self::Full7, + ]; +} + +impl TestCase for MultiInsertCase { + fn name(&self) -> &'static str { + match self { + Self::Balanced3 => "3-node balanced (10,5,15)", + Self::LeftSkew => "Left-skew rotation (10,5,1)", + Self::RightSkew => "Right-skew rotation (10,15,20)", + Self::Zigzag => "Zigzag double rotation (10,5,7)", + Self::Full7 => "7-node full tree (10,5,15,3,7,12,20)", + } + } + + fn run(&self, lang: ProgramLanguage) -> CaseResult { + match self { + Self::Balanced3 => run_balanced_3(lang), + Self::LeftSkew => run_left_skew(lang), + Self::RightSkew => run_right_skew(lang), + Self::Zigzag => run_zigzag(lang), + Self::Full7 => run_full_7(lang), + } + } +} + +fn run_balanced_3(lang: ProgramLanguage) -> CaseResult { + run_multi_insert( + lang, + &[ + MultiInsertStep { + key: 10, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiInsertStep { + key: 5, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None).val(1), + node(5, R, Some(0), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 15, + expected: TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)).val(1), + node(5, R, Some(0), None, None).val(1), + node(15, R, Some(0), None, None).val(1), + ], + }, + }, + ], + ) +} + +fn run_left_skew(lang: ProgramLanguage) -> CaseResult { + run_multi_insert( + lang, + &[ + MultiInsertStep { + key: 10, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiInsertStep { + key: 5, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None).val(1), + node(5, R, Some(0), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 1, + expected: TreeSpec { + root: Some(1), + top: None, + nodes: &[ + node(10, R, Some(1), None, None).val(1), + node(5, B, None, Some(2), Some(0)).val(1), + node(1, R, Some(1), None, None).val(1), + ], + }, + }, + ], + ) +} + +fn run_right_skew(lang: ProgramLanguage) -> CaseResult { + run_multi_insert( + lang, + &[ + MultiInsertStep { + key: 10, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiInsertStep { + key: 15, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, None, Some(1)).val(1), + node(15, R, Some(0), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 20, + expected: TreeSpec { + root: Some(1), + top: None, + nodes: &[ + node(10, R, Some(1), None, None).val(1), + node(15, B, None, Some(0), Some(2)).val(1), + node(20, R, Some(1), None, None).val(1), + ], + }, + }, + ], + ) +} + +fn run_zigzag(lang: ProgramLanguage) -> CaseResult { + run_multi_insert( + lang, + &[ + MultiInsertStep { + key: 10, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiInsertStep { + key: 5, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None).val(1), + node(5, R, Some(0), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 7, + expected: TreeSpec { + root: Some(2), + top: None, + nodes: &[ + node(10, R, Some(2), None, None).val(1), + node(5, R, Some(2), None, None).val(1), + node(7, B, None, Some(1), Some(0)).val(1), + ], + }, + }, + ], + ) +} + +fn run_full_7(lang: ProgramLanguage) -> CaseResult { + run_multi_insert( + lang, + &[ + MultiInsertStep { + key: 10, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiInsertStep { + key: 5, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None).val(1), + node(5, R, Some(0), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 15, + expected: TreeSpec { + root: Some(0), + top: Some(3), + nodes: &[ + node(10, B, None, Some(1), Some(2)).val(1), + node(5, R, Some(0), None, None).val(1), + node(15, R, Some(0), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 3, + expected: TreeSpec { + root: Some(0), + top: Some(4), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), None).val(1), + node(15, B, Some(0), None, None).val(1), + node(3, R, Some(1), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 7, + expected: TreeSpec { + root: Some(0), + top: Some(5), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), Some(4)).val(1), + node(15, B, Some(0), None, None).val(1), + node(3, R, Some(1), None, None).val(1), + node(7, R, Some(1), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 12, + expected: TreeSpec { + root: Some(0), + top: Some(6), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), Some(4)).val(1), + node(15, B, Some(0), Some(5), None).val(1), + node(3, R, Some(1), None, None).val(1), + node(7, R, Some(1), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + ], + }, + }, + MultiInsertStep { + key: 20, + expected: TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), Some(4)).val(1), + node(15, B, Some(0), Some(5), Some(6)).val(1), + node(3, R, Some(1), None, None).val(1), + node(7, R, Some(1), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + node(20, R, Some(2), None, None).val(1), + ], + }, + }, + ], + ) +} diff --git a/examples/tree/src/tests/remove.rs b/examples/tree/src/tests/remove.rs new file mode 100644 index 00000000..4ea11633 --- /dev/null +++ b/examples/tree/src/tests/remove.rs @@ -0,0 +1,1803 @@ +// cspell:word reparented +use super::common::*; +use super::*; +use tree_interface::{ + input_buffer, InsertInstruction, Instruction as TreeInstruction, InstructionHeader, + RemoveInstruction, StackNode, TreeHeader, TreeNode, +}; + +// --------------------------------------------------------------------------- +// Helpers: remove test setup +// --------------------------------------------------------------------------- + +fn remove_setup( + lang: ProgramLanguage, + desc: &TreeSpec, + remove_key: u16, +) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let setup = setup_test(lang); + let (system_program_pubkey, _) = program::keyed_account_for_system_program(); + + let user_pubkey = Pubkey::new_unique(); + let (tree_pubkey, tree_account) = build_tree_account_no_free(desc, &setup.program_id); + + let insn_data = RemoveInstruction { + header: InstructionHeader { + discriminator: TreeInstruction::Remove as u8, + }, + key: remove_key, + }; + + let instruction = Instruction::new_with_bytes( + setup.program_id, + unsafe { as_bytes(&insn_data) }, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account), + ]; + + (setup, instruction, accounts) +} + +/// A minimal two-account setup for input check tests. +fn remove_input_setup(lang: ProgramLanguage) -> (TestSetup, Instruction, Vec<(Pubkey, Account)>) { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + remove_setup(lang, &desc, 10) +} + +// --------------------------------------------------------------------------- +// Helpers: remove runners +// --------------------------------------------------------------------------- + +/// Execute a remove and verify success with full tree state. +/// +/// On success, the program returns 0. Account changes are only persisted +/// by the SVM when the program returns 0. +fn run_remove_success( + lang: ProgramLanguage, + desc: &TreeSpec, + remove_key: u16, + expected: &TreeSpec, +) -> CaseResult { + if let Err(e) = assert_invariants(desc) { + return CaseResult { + cu: 0, + error: Some(format!("desc invariant: {}", e)), + }; + } + if let Err(e) = assert_invariants(expected) { + return CaseResult { + cu: 0, + error: Some(format!("exp invariant: {}", e)), + }; + } + let (setup, instruction, accounts) = remove_setup(lang, desc, remove_key); + let result = setup.mollusk.process_instruction(&instruction, &accounts); + match &result.program_result { + MolluskResult::Success => { + let tree_data = &result.resulting_accounts[AccountIndex::Tree as usize] + .1 + .data; + match assert_tree_account(tree_data, expected) { + Ok(()) => CaseResult { + cu: result.compute_units_consumed, + error: None, + }, + Err(e) => CaseResult { + cu: result.compute_units_consumed, + error: Some(e), + }, + } + } + other => CaseResult { + cu: result.compute_units_consumed, + error: Some(format!("expected Success, got {:?}", other)), + }, + } +} + +/// Execute a remove and verify KEY_DOES_NOT_EXIST error. +fn run_remove_not_found(lang: ProgramLanguage, desc: &TreeSpec, remove_key: u16) -> CaseResult { + if let Err(e) = assert_invariants(desc) { + return CaseResult { + cu: 0, + error: Some(format!("desc invariant: {}", e)), + }; + } + let (setup, instruction, accounts) = remove_setup(lang, desc, remove_key); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::KEY_DOES_NOT_EXIST, + ) +} + +// --------------------------------------------------------------------------- +// Helpers: multi-step operations +// --------------------------------------------------------------------------- + +enum MultiStep { + Insert { key: u16, value: u16 }, + Remove { key: u16 }, +} + +struct MultiStepCase<'a> { + step: MultiStep, + expected: TreeSpec<'a>, +} + +fn run_multi_step(lang: ProgramLanguage, n_slots: usize, steps: &[MultiStepCase]) -> CaseResult { + let setup = setup_test(lang); + let (system_program_pubkey, _) = program::keyed_account_for_system_program(); + + let user_pubkey = Pubkey::new_unique(); + let (tree_pubkey, mut tree_account) = build_empty_tree(n_slots, &setup.program_id); + + let mut total_cu = 0u64; + + for (i, step) in steps.iter().enumerate() { + if let Err(e) = assert_invariants(&step.expected) { + let step_desc = match &step.step { + MultiStep::Insert { key, .. } => format!("insert key={}", key), + MultiStep::Remove { key, .. } => format!("remove key={}", key), + }; + return CaseResult { + cu: 0, + error: Some(format!("step {} ({}) exp invariant: {}", i, step_desc, e)), + }; + } + + let insn_bytes: Vec = match &step.step { + MultiStep::Insert { key, value } => { + let insn = InsertInstruction { + header: InstructionHeader { + discriminator: TreeInstruction::Insert as u8, + }, + key: *key, + value: *value, + }; + unsafe { as_bytes(&insn) }.to_vec() + } + MultiStep::Remove { key } => { + let insn = RemoveInstruction { + header: InstructionHeader { + discriminator: TreeInstruction::Remove as u8, + }, + key: *key, + }; + unsafe { as_bytes(&insn) }.to_vec() + } + }; + + let instruction = Instruction::new_with_bytes( + setup.program_id, + &insn_bytes, + vec![ + AccountMeta::new(user_pubkey, true), + AccountMeta::new(tree_pubkey, false), + ], + ); + + let accounts = vec![ + ( + user_pubkey, + Account::new(USER_LAMPORTS, 0, &system_program_pubkey), + ), + (tree_pubkey, tree_account.clone()), + ]; + + let result = setup.mollusk.process_instruction(&instruction, &accounts); + total_cu += result.compute_units_consumed; + + // Both insert and remove return 0 on success. + if result.program_result != MolluskResult::Success { + let step_desc = match &step.step { + MultiStep::Insert { key, .. } => format!("insert key={}", key), + MultiStep::Remove { key, .. } => format!("remove key={}", key), + }; + return CaseResult { + cu: total_cu, + error: Some(format!( + "step {} ({}): expected Success, got {:?}", + i, step_desc, result.program_result + )), + }; + } + + tree_account = result.resulting_accounts[AccountIndex::Tree as usize] + .1 + .clone(); + if let Err(e) = assert_tree_account(&tree_account.data, &step.expected) { + let step_desc = match &step.step { + MultiStep::Insert { key, .. } => format!("insert key={}", key), + MultiStep::Remove { key, .. } => format!("remove key={}", key), + }; + return CaseResult { + cu: total_cu, + error: Some(format!("step {} ({}): {}", i, step_desc, e)), + }; + } + } + + CaseResult { + cu: total_cu, + error: None, + } +} + +// --------------------------------------------------------------------------- +// Test case enum +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +pub(super) enum RemoveCase { + // Input validation (cases 1-5). + InputDataShort, + InputDataLong, + InputNAccounts, + InputUserDataLen, + InputTreeDuplicate, + // Search errors (cases 6-9). + SearchEmptyTree, + SearchNotFoundLeft, + SearchNotFoundRight, + SearchNotFoundDeep, + // Simple removal (cases 10-18). + SimpleRootLeaf, + SimpleRedLeafL, + SimpleRedLeafR, + SimpleOneChildRootR, + SimpleOneChildRootL, + SimpleOneChildNonRootR, + SimpleOneChildNonRootL, + SimpleOneChildNonRootRL, + SimpleOneChildNonRootLR, + // Successor swap (cases 19-21). + SuccessorImmediate, + SuccessorDeep, + SuccessorWithChild, + // Rebalancing (cases 22-45). + Case4L, + Case4R, + Case6L, + Case6R, + Case56L, + Case56R, + Case3Then4L, + Case3Then4R, + Case3Then6L, + Case3Then6R, + Case3Then56L, + Case3Then56R, + Case2PropL, + Case2PropR, + Case2Then4, + Case2Then6, + Case6NewChildL, + Case6NewChildR, + Case6ParentRootL, + Case6ParentRootR, + Case6ParentGgpL, + Case6ParentGgpR, + Case3ParentRootL, + Case3ParentRootR, +} + +impl RemoveCase { + pub(super) const INPUT_CASES: &'static [Self] = &[ + Self::InputDataShort, + Self::InputDataLong, + Self::InputNAccounts, + Self::InputUserDataLen, + Self::InputTreeDuplicate, + ]; + + pub(super) const SEARCH_CASES: &'static [Self] = &[ + Self::SearchEmptyTree, + Self::SearchNotFoundLeft, + Self::SearchNotFoundRight, + Self::SearchNotFoundDeep, + ]; + + pub(super) const SIMPLE_CASES: &'static [Self] = &[ + Self::SuccessorImmediate, + Self::SuccessorDeep, + Self::SuccessorWithChild, + Self::SimpleOneChildRootR, + Self::SimpleOneChildRootL, + Self::SimpleOneChildNonRootR, + Self::SimpleOneChildNonRootL, + Self::SimpleOneChildNonRootRL, + Self::SimpleOneChildNonRootLR, + Self::SimpleRootLeaf, + Self::SimpleRedLeafL, + Self::SimpleRedLeafR, + ]; + + pub(super) const REBALANCE_CASES: &'static [Self] = &[ + Self::Case4L, + Self::Case4R, + Self::Case6L, + Self::Case6R, + Self::Case56L, + Self::Case56R, + Self::Case3Then4L, + Self::Case3Then4R, + Self::Case3Then6L, + Self::Case3Then6R, + Self::Case3Then56L, + Self::Case3Then56R, + Self::Case2PropL, + Self::Case2PropR, + Self::Case2Then4, + Self::Case2Then6, + Self::Case6NewChildL, + Self::Case6NewChildR, + Self::Case6ParentRootL, + Self::Case6ParentRootR, + Self::Case6ParentGgpL, + Self::Case6ParentGgpR, + Self::Case3ParentRootL, + Self::Case3ParentRootR, + ]; +} + +impl TestCase for RemoveCase { + fn name(&self) -> &'static str { + match self { + Self::InputDataShort => "Data too short", + Self::InputDataLong => "Data too long", + Self::InputNAccounts => "Too few accounts", + Self::InputUserDataLen => "User has data", + Self::InputTreeDuplicate => "Tree is duplicate", + Self::SearchEmptyTree => "Empty tree", + Self::SearchNotFoundLeft => "Not found (left)", + Self::SearchNotFoundRight => "Not found (right)", + Self::SearchNotFoundDeep => "Not found (deep)", + Self::SimpleRootLeaf => "Root leaf (SC 3)", + Self::SimpleRedLeafL => "Red leaf L (SC 4)", + Self::SimpleRedLeafR => "Red leaf R (SC 4)", + Self::SimpleOneChildRootR => "One child root R (SC 2)", + Self::SimpleOneChildRootL => "One child root L (SC 2)", + Self::SimpleOneChildNonRootR => "One child non-root R,R (SC 2)", + Self::SimpleOneChildNonRootL => "One child non-root L,L (SC 2)", + Self::SimpleOneChildNonRootRL => "One child non-root R,L (SC 2)", + Self::SimpleOneChildNonRootLR => "One child non-root L,R (SC 2)", + Self::SuccessorImmediate => "Successor immediate R (SC 1)", + Self::SuccessorDeep => "Successor deep L descent (SC 1)", + Self::SuccessorWithChild => "Successor with R child (SC 1)", + Self::Case4L => "Case 4 dir_l", + Self::Case4R => "Case 4 dir_r", + Self::Case6L => "Case 6 dir_l", + Self::Case6R => "Case 6 dir_r", + Self::Case56L => "Case 5+6 dir_l", + Self::Case56R => "Case 5+6 dir_r", + Self::Case3Then4L => "Case 3->4 dir_l", + Self::Case3Then4R => "Case 3->4 dir_r", + Self::Case3Then6L => "Case 3->6 dir_l", + Self::Case3Then6R => "Case 3->6 dir_r", + Self::Case3Then56L => "Case 3->5->6 dir_l", + Self::Case3Then56R => "Case 3->5->6 dir_r", + Self::Case2PropL => "Case 2 propagate L", + Self::Case2PropR => "Case 2 propagate R", + Self::Case2Then4 => "Case 2->4", + Self::Case2Then6 => "Case 2->6", + Self::Case6NewChildL => "Case 6 new_child L", + Self::Case6NewChildR => "Case 6 new_child R", + Self::Case6ParentRootL => "Case 6 parent=root L", + Self::Case6ParentRootR => "Case 6 parent=root R", + Self::Case6ParentGgpL => "Case 6 parent=GGP L", + Self::Case6ParentGgpR => "Case 6 parent=GGP R", + Self::Case3ParentRootL => "Case 3 parent=root L", + Self::Case3ParentRootR => "Case 3 parent=root R", + } + } + + fn run(&self, lang: ProgramLanguage) -> CaseResult { + match self { + // ----- Input validation ----- + Self::InputDataShort => { + let (setup, mut instruction, accounts) = remove_input_setup(lang); + instruction.data = vec![TreeInstruction::Remove as u8]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::INSTRUCTION_DATA_LEN, + ) + } + Self::InputDataLong => { + let (setup, mut instruction, accounts) = remove_input_setup(lang); + instruction.data = vec![TreeInstruction::Remove as u8, 0, 0, 0]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::INSTRUCTION_DATA_LEN, + ) + } + Self::InputNAccounts => { + let (setup, mut instruction, mut accounts) = remove_input_setup(lang); + instruction.accounts.pop(); + accounts.pop(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::N_ACCOUNTS, + ) + } + Self::InputUserDataLen => { + let (setup, instruction, mut accounts) = remove_input_setup(lang); + accounts[AccountIndex::User as usize].1.data = vec![1u8; 1]; + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::USER_DATA_LEN, + ) + } + Self::InputTreeDuplicate => { + let (setup, mut instruction, mut accounts) = remove_input_setup(lang); + instruction.accounts[AccountIndex::Tree as usize] = + instruction.accounts[AccountIndex::User as usize].clone(); + accounts[AccountIndex::Tree as usize] = + accounts[AccountIndex::User as usize].clone(); + check_error( + &setup, + &instruction, + &accounts, + error_codes::error::TREE_DUPLICATE, + ) + } + + // ----- Search errors ----- + Self::SearchEmptyTree => { + let desc = TreeSpec { + root: None, + top: None, + nodes: &[], + }; + run_remove_not_found(lang, &desc, 10) + } + Self::SearchNotFoundLeft => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + run_remove_not_found(lang, &desc, 5) + } + Self::SearchNotFoundRight => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + run_remove_not_found(lang, &desc, 15) + } + Self::SearchNotFoundDeep => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, None), + ], + }; + run_remove_not_found(lang, &desc, 12) + } + + // ----- Simple removal ----- + + // Simple case 3: root leaf. + Self::SimpleRootLeaf => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[node(10, B, None, None, None)], + }; + let exp = TreeSpec { + root: None, + top: Some(0), + nodes: &[node(10, B, None, None, None)], + }; + run_remove_success(lang, &desc, 10, &exp) + } + + // Simple case 4: red leaf (left child). + Self::SimpleRedLeafL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, B, None, None, None), node(5, R, None, None, None)], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Simple case 4: red leaf (right child). + Self::SimpleRedLeafR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, B, None, None, None), node(15, R, None, None, None)], + }; + run_remove_success(lang, &desc, 15, &exp) + } + + // Simple case 2: one child at root (right child). + Self::SimpleOneChildRootR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, None, Some(1)), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(0), + nodes: &[node(10, B, None, None, None), node(15, B, None, None, None)], + }; + run_remove_success(lang, &desc, 10, &exp) + } + + // Simple case 2: one child at root (left child). + Self::SimpleOneChildRootL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(0), + nodes: &[node(10, B, None, None, None), node(5, B, None, None, None)], + }; + run_remove_success(lang, &desc, 10, &exp) + } + + // Simple case 2: one child non-root (R child, R pos). + Self::SimpleOneChildNonRootR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, Some(3)), + node(20, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), Some(3)), + node(5, B, Some(0), None, None), + node(15, B, None, None, None), + node(20, B, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 15, &exp) + } + // Simple case 2: one child non-root (L child, L pos). + Self::SimpleOneChildNonRootL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), Some(3), None), + node(15, B, Some(0), None, None), + node(3, R, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, Some(3), Some(2)), + node(5, B, None, None, None), + node(15, B, Some(0), None, None), + node(3, B, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Simple case 2: one child non-root (R child, L pos). + // Node has R child, node is L child of parent. + Self::SimpleOneChildNonRootRL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, Some(3)), + node(15, B, Some(0), None, None), + node(7, R, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, Some(3), Some(2)), + node(5, B, None, None, None), + node(15, B, Some(0), None, None), + node(7, B, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Simple case 2: one child non-root (L child, R pos). + // Node has L child, node is R child of parent. + Self::SimpleOneChildNonRootLR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), Some(3), None), + node(12, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), Some(3)), + node(5, B, Some(0), None, None), + node(15, B, None, None, None), + node(12, B, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 15, &exp) + } + + // ----- Successor swap ----- + + // Successor is immediate right child. + // B(10) with R(5) left, R(15) right. Remove 10: swap with + // successor N2(15), then delete N2 (red leaf, simple case 4). + Self::SuccessorImmediate => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(15, B, None, Some(1), None).val(15), + node(5, R, Some(0), None, None), + node(15, R, None, None, None), + ], + }; + run_remove_success(lang, &desc, 10, &exp) + } + + // Successor with deep left descent. + Self::SuccessorDeep => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, B, Some(0), Some(3), Some(4)), + node(15, R, Some(2), None, None), + node(25, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(3), + nodes: &[ + node(15, B, None, Some(1), Some(2)).val(15), + node(5, B, Some(0), None, None), + node(20, B, Some(0), None, Some(4)), + node(15, R, None, None, None), + node(25, R, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 10, &exp) + } + + // Successor with right child. + Self::SuccessorWithChild => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, Some(3)), + node(20, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(15, B, None, Some(1), Some(3)).val(15), + node(5, B, Some(0), None, None), + node(15, B, None, None, None), + node(20, B, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 10, &exp) + } + + // ----- Rebalancing ----- + + // Case 4 dir_l: red parent, black sibling, black nephews. + Self::Case4L => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, None, Some(2)), + node(5, B, None, None, None), + node(15, R, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 4 dir_r. + Self::Case4R => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + node(15, B, None, None, None), + ], + }; + run_remove_success(lang, &desc, 15, &exp) + } + + // Case 6 dir_l: black sibling, distant nephew red. + Self::Case6L => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, Some(3)), + node(20, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(2), None, None), + node(5, B, None, None, None), + node(15, B, None, Some(0), Some(3)), + node(20, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 6 dir_r. + Self::Case6R => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), Some(3), None), + node(20, B, Some(0), None, None), + node(3, R, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(1), None, None), + node(5, B, None, Some(3), Some(0)), + node(20, B, None, None, None), + node(3, B, Some(1), None, None), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 5+6 dir_l. + Self::Case56L => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, B, Some(0), Some(3), None), + node(15, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(3), + top: Some(1), + nodes: &[ + node(10, B, Some(3), None, None), + node(5, B, None, None, None), + node(20, B, Some(3), None, None), + node(15, B, None, Some(0), Some(2)), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 5+6 dir_r. + Self::Case56R => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, Some(3)), + node(20, B, Some(0), None, None), + node(7, R, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(3), + top: Some(2), + nodes: &[ + node(10, B, Some(3), None, None), + node(5, B, Some(3), None, None), + node(20, B, None, None, None), + node(7, B, None, Some(1), Some(0)), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 3 -> 4 dir_l. + Self::Case3Then4L => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, R, Some(0), Some(3), Some(4)), + node(15, B, Some(2), None, None), + node(25, B, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(2), None, Some(3)), + node(5, B, None, None, None), + node(20, B, None, Some(0), Some(4)), + node(15, R, Some(0), None, None), + node(25, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 3 -> 4 dir_r. + Self::Case3Then4R => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), Some(3), Some(4)), + node(20, B, Some(0), None, None), + node(3, B, Some(1), None, None), + node(7, B, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(1), Some(4), None), + node(5, B, None, Some(3), Some(0)), + node(20, B, None, None, None), + node(3, B, Some(1), None, None), + node(7, R, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 3 -> 6 dir_l. + Self::Case3Then6L => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, R, Some(0), Some(3), Some(5)), + node(15, B, Some(2), None, Some(4)), + node(17, R, Some(3), None, None), + node(25, B, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(3), None, None), + node(5, B, None, None, None), + node(20, B, None, Some(3), Some(5)), + node(15, R, Some(2), Some(0), Some(4)), + node(17, B, Some(3), None, None), + node(25, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 3 -> 6 dir_r. + Self::Case3Then6R => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), Some(4), Some(3)), + node(20, B, Some(0), None, None), + node(7, B, Some(1), Some(5), None), + node(3, B, Some(1), None, None), + node(6, R, Some(3), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(3), None, None), + node(5, B, None, Some(4), Some(3)), + node(20, B, None, None, None), + node(7, R, Some(1), Some(5), Some(0)), + node(3, B, Some(1), None, None), + node(6, B, Some(3), None, None), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 3 -> 5 -> 6 dir_l. + Self::Case3Then56L => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, R, Some(0), Some(3), Some(5)), + node(15, B, Some(2), Some(4), None), + node(13, R, Some(3), None, None), + node(25, B, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(4), None, None), + node(5, B, None, None, None), + node(20, B, None, Some(4), Some(5)), + node(15, B, Some(4), None, None), + node(13, R, Some(2), Some(0), Some(3)), + node(25, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 3 -> 5 -> 6 dir_r. + Self::Case3Then56R => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), Some(4), Some(3)), + node(20, B, Some(0), None, None), + node(7, B, Some(1), None, Some(5)), + node(3, B, Some(1), None, None), + node(8, R, Some(3), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(5), None, None), + node(5, B, None, Some(4), Some(5)), + node(20, B, None, None, None), + node(7, B, Some(5), None, None), + node(3, B, Some(1), None, None), + node(8, R, Some(1), Some(3), Some(0)), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 2: propagate to root (dir_l). + Self::Case2PropL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, None, Some(2)), + node(5, B, None, None, None), + node(15, R, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 2: propagate to root (dir_r). + Self::Case2PropR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + node(15, B, None, None, None), + ], + }; + run_remove_success(lang, &desc, 15, &exp) + } + + // Case 2 -> 4: propagate then red parent. + Self::Case2Then4 => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(4)), + node(10, R, Some(0), Some(2), Some(3)), + node(5, B, Some(1), None, None), + node(15, B, Some(1), None, None), + node(25, B, Some(0), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(20, B, None, Some(1), Some(4)), + node(10, B, Some(0), None, Some(3)), + node(5, B, None, None, None), + node(15, R, Some(1), None, None), + node(25, B, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 2 -> 6: propagate then distant nephew red. + // 9-node tree with bh=3 at root. Remove B(5) leaf: + // case 2 at B(10), propagate to B(20), then case 6 + // (distant nephew R(35) is red). + Self::Case2Then6 => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(20, B, None, Some(1), Some(4)), + node(10, B, Some(0), Some(2), Some(3)), + node(5, B, Some(1), None, None), + node(15, B, Some(1), None, None), + node(30, B, Some(0), Some(5), Some(6)), + node(25, B, Some(4), None, None), + node(35, R, Some(4), Some(7), Some(8)), + node(33, B, Some(6), None, None), + node(40, B, Some(6), None, None), + ], + }; + let exp = TreeSpec { + root: Some(4), + top: Some(2), + nodes: &[ + node(20, B, Some(4), Some(1), Some(5)), + node(10, B, Some(0), None, Some(3)), + node(5, B, None, None, None), + node(15, R, Some(1), None, None), + node(30, B, None, Some(0), Some(6)), + node(25, B, Some(0), None, None), + node(35, B, Some(4), Some(7), Some(8)), + node(33, B, Some(6), None, None), + node(40, B, Some(6), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 6 with non-null new_child dir_l. + // B(10) root, B(5) left, B(20) right with R(15)/R(25). + // Remove B(5): case 6 rotates left; new_child R(15) is + // reparented from B(20).child[L] to B(10).child[R]. + Self::Case6NewChildL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, B, Some(0), Some(3), Some(4)), + node(15, R, Some(2), None, None), + node(25, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(2), None, Some(3)), + node(5, B, None, None, None), + node(20, B, None, Some(0), Some(4)), + node(15, R, Some(0), None, None), + node(25, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 6 with non-null new_child dir_r. + // B(10) root, B(5) left with R(3)/R(7), B(20) right. + // Remove B(20): case 6 rotates right; new_child R(7) is + // reparented from B(5).child[R] to B(10).child[L]. + Self::Case6NewChildR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), Some(3), Some(4)), + node(20, B, Some(0), None, None), + node(3, R, Some(1), None, None), + node(7, R, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(1), Some(4), None), + node(5, B, None, Some(3), Some(0)), + node(20, B, None, None, None), + node(3, B, Some(1), None, None), + node(7, R, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 6 parent=root dir_l (same tree as Case6L). + Self::Case6ParentRootL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(15, B, Some(0), None, Some(3)), + node(20, R, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(2), None, None), + node(5, B, None, None, None), + node(15, B, None, Some(0), Some(3)), + node(20, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 6 parent=root dir_r. + Self::Case6ParentRootR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), Some(3), None), + node(20, B, Some(0), None, None), + node(3, R, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(1), None, None), + node(5, B, None, Some(3), Some(0)), + node(20, B, None, None, None), + node(3, B, Some(1), None, None), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + + // Case 6 parent=GGP left child dir_l. + // 8-node tree, bh=3 at root. N1(B(10)) is left child of + // root B(30). Remove B(5) from N1's left: case 6 rotates + // N1 left, N3(B(20)) takes N1's place. GGP N0 updates + // child[L] = N3. + Self::Case6ParentGgpL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(30, B, None, Some(1), Some(5)), + node(10, B, Some(0), Some(2), Some(3)), + node(5, B, Some(1), None, None), + node(20, B, Some(1), None, Some(4)), + node(25, R, Some(3), None, None), + node(40, B, Some(0), Some(6), Some(7)), + node(35, B, Some(5), None, None), + node(45, B, Some(5), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(30, B, None, Some(3), Some(5)), + node(10, B, Some(3), None, None), + node(5, B, None, None, None), + node(20, B, Some(0), Some(1), Some(4)), + node(25, B, Some(3), None, None), + node(40, B, Some(0), Some(6), Some(7)), + node(35, B, Some(5), None, None), + node(45, B, Some(5), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 6 parent=GGP right child dir_r. + // 8-node tree, bh=3 at root. N1(B(20)) is right child of + // root B(5). Remove B(25) from N1's right: case 6 rotates + // N1 right, N2(B(10)) takes N1's place. GGP N0 updates + // child[R] = N2. + Self::Case6ParentGgpR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(5, B, None, Some(5), Some(1)), + node(20, B, Some(0), Some(2), Some(3)), + node(10, B, Some(1), Some(4), None), + node(25, B, Some(1), None, None), + node(7, R, Some(2), None, None), + node(2, B, Some(0), Some(6), Some(7)), + node(1, B, Some(5), None, None), + node(3, B, Some(5), None, None), + ], + }; + let exp = TreeSpec { + root: Some(0), + top: Some(3), + nodes: &[ + node(5, B, None, Some(5), Some(2)), + node(20, B, Some(2), None, None), + node(10, B, Some(0), Some(4), Some(1)), + node(25, B, None, None, None), + node(7, B, Some(2), None, None), + node(2, B, Some(0), Some(6), Some(7)), + node(1, B, Some(5), None, None), + node(3, B, Some(5), None, None), + ], + }; + run_remove_success(lang, &desc, 25, &exp) + } + + // Case 3 parent=root dir_l. + Self::Case3ParentRootL => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, B, Some(0), None, None), + node(20, R, Some(0), Some(3), Some(4)), + node(15, B, Some(2), None, None), + node(25, B, Some(2), None, None), + ], + }; + let exp = TreeSpec { + root: Some(2), + top: Some(1), + nodes: &[ + node(10, B, Some(2), None, Some(3)), + node(5, B, None, None, None), + node(20, B, None, Some(0), Some(4)), + node(15, R, Some(0), None, None), + node(25, B, Some(2), None, None), + ], + }; + run_remove_success(lang, &desc, 5, &exp) + } + + // Case 3 parent=root dir_r. + Self::Case3ParentRootR => { + let desc = TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), Some(3), Some(4)), + node(20, B, Some(0), None, None), + node(3, B, Some(1), None, None), + node(7, B, Some(1), None, None), + ], + }; + let exp = TreeSpec { + root: Some(1), + top: Some(2), + nodes: &[ + node(10, B, Some(1), Some(4), None), + node(5, B, None, Some(3), Some(0)), + node(20, B, None, None, None), + node(3, B, Some(1), None, None), + node(7, R, Some(0), None, None), + ], + }; + run_remove_success(lang, &desc, 20, &exp) + } + } + } +} + +// --------------------------------------------------------------------------- +// Multi-step integration tests +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +pub(super) enum MultiRemoveCase { + /// Insert 10,5,15; remove 5. + Minimal, + /// Insert 7 nodes; remove all. + FullCycle, + /// Insert 3; remove 1; insert 1. + Recycle, +} + +impl MultiRemoveCase { + pub(super) const CASES: &'static [Self] = &[Self::Minimal, Self::FullCycle, Self::Recycle]; +} + +impl TestCase for MultiRemoveCase { + fn name(&self) -> &'static str { + match self { + Self::Minimal => "Insert 3, remove 1", + Self::FullCycle => "Insert 7, remove all", + Self::Recycle => "Insert-remove-insert (recycle)", + } + } + + fn run(&self, lang: ProgramLanguage) -> CaseResult { + match self { + Self::Minimal => run_multi_step( + lang, + 3, + &[ + MultiStepCase { + step: MultiStep::Insert { key: 10, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 5, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None).val(1), + node(5, R, Some(0), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 15, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)).val(1), + node(5, R, Some(0), None, None).val(1), + node(15, R, Some(0), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 5 }, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, None, Some(2)).val(1), + // Freed node: key/value/color retained, children nulled, + // parent (= StackNode.next) = null (stack was empty). + node(5, R, None, None, None).val(1), + node(15, R, Some(0), None, None).val(1), + ], + }, + }, + ], + ), + + Self::FullCycle => run_multi_step( + lang, + 7, + &[ + // Insert 10,5,15,3,7,12,20. + MultiStepCase { + step: MultiStep::Insert { key: 10, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None).val(1)], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 5, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None).val(1), + node(5, R, Some(0), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 15, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(3), + nodes: &[ + node(10, B, None, Some(1), Some(2)).val(1), + node(5, R, Some(0), None, None).val(1), + node(15, R, Some(0), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 3, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(4), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), None).val(1), + node(15, B, Some(0), None, None).val(1), + node(3, R, Some(1), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 7, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(5), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), Some(4)).val(1), + node(15, B, Some(0), None, None).val(1), + node(3, R, Some(1), None, None).val(1), + node(7, R, Some(1), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 12, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: Some(6), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), Some(4)).val(1), + node(15, B, Some(0), Some(5), None).val(1), + node(3, R, Some(1), None, None).val(1), + node(7, R, Some(1), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 20, value: 1 }, + expected: TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), Some(3), Some(4)).val(1), + node(15, B, Some(0), Some(5), Some(6)).val(1), + node(3, R, Some(1), None, None).val(1), + node(7, R, Some(1), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + node(20, R, Some(2), None, None).val(1), + ], + }, + }, + // Remove all: 3, 20, 7, 12, 5, 15, 10. + // Removing leaves first to simplify expected states. + MultiStepCase { + step: MultiStep::Remove { key: 3 }, + expected: TreeSpec { + root: Some(0), + top: Some(3), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), None, Some(4)).val(1), + node(15, B, Some(0), Some(5), Some(6)).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(1), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + node(20, R, Some(2), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 20 }, + expected: TreeSpec { + root: Some(0), + top: Some(6), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), None, Some(4)).val(1), + node(15, B, Some(0), Some(5), None).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(1), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + node(20, R, Some(3), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 7 }, + expected: TreeSpec { + root: Some(0), + top: Some(4), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), None, None).val(1), + node(15, B, Some(0), Some(5), None).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(6), None, None).val(1), + node(12, R, Some(2), None, None).val(1), + node(20, R, Some(3), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 12 }, + expected: TreeSpec { + root: Some(0), + top: Some(5), + nodes: &[ + node(10, R, None, Some(1), Some(2)).val(1), + node(5, B, Some(0), None, None).val(1), + node(15, B, Some(0), None, None).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(6), None, None).val(1), + node(12, R, Some(4), None, None).val(1), + node(20, R, Some(3), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 5 }, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, None, Some(2)).val(1), + node(5, B, Some(5), None, None).val(1), + node(15, R, Some(0), None, None).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(6), None, None).val(1), + node(12, R, Some(4), None, None).val(1), + node(20, R, Some(3), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 15 }, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, None, None).val(1), + node(5, B, Some(5), None, None).val(1), + node(15, R, Some(1), None, None).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(6), None, None).val(1), + node(12, R, Some(4), None, None).val(1), + node(20, R, Some(3), None, None).val(1), + ], + }, + }, + MultiStepCase { + step: MultiStep::Remove { key: 10 }, + expected: TreeSpec { + root: None, + top: Some(0), + nodes: &[ + node(10, B, Some(2), None, None).val(1), + node(5, B, Some(5), None, None).val(1), + node(15, R, Some(1), None, None).val(1), + node(3, R, None, None, None).val(1), + node(7, R, Some(6), None, None).val(1), + node(12, R, Some(4), None, None).val(1), + node(20, R, Some(3), None, None).val(1), + ], + }, + }, + ], + ), + + Self::Recycle => run_multi_step( + lang, + 3, + &[ + // Insert 10,5,15. + MultiStepCase { + step: MultiStep::Insert { key: 10, value: 10 }, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[node(10, R, None, None, None)], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 5, value: 5 }, + expected: TreeSpec { + root: Some(0), + top: Some(2), + nodes: &[ + node(10, B, None, Some(1), None), + node(5, R, Some(0), None, None), + ], + }, + }, + MultiStepCase { + step: MultiStep::Insert { key: 15, value: 15 }, + expected: TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(5, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }, + }, + // Remove 5: red leaf removal, N1 freed onto stack. + MultiStepCase { + step: MultiStep::Remove { key: 5 }, + expected: TreeSpec { + root: Some(0), + top: Some(1), + nodes: &[ + node(10, B, None, None, Some(2)), + node(5, R, None, None, None), + node(15, R, Some(0), None, None), + ], + }, + }, + // Insert 7: pops N1 from stack, reuses slot. + MultiStepCase { + step: MultiStep::Insert { key: 7, value: 7 }, + expected: TreeSpec { + root: Some(0), + top: None, + nodes: &[ + node(10, B, None, Some(1), Some(2)), + node(7, R, Some(0), None, None), + node(15, R, Some(0), None, None), + ], + }, + }, + ], + ), + } + } +} diff --git a/examples/tree/src/tree/tree.s b/examples/tree/src/tree/tree.s new file mode 100644 index 00000000..924c45a3 --- /dev/null +++ b/examples/tree/src/tree/tree.s @@ -0,0 +1,1172 @@ +# ANCHOR: constants +# Error codes. +# ------------ +.equ E_N_ACCOUNTS, 1 # An invalid number of accounts were passed. +.equ E_USER_DATA_LEN, 2 # The user account has invalid data length. +.equ E_TREE_DATA_LEN, 3 # The tree account has invalid data length. +# The System Program account has invalid data length. +.equ E_SYSTEM_PROGRAM_DATA_LEN, 4 +.equ E_TREE_DUPLICATE, 5 # The tree account is a duplicate. +# The System Program account is a duplicate. +.equ E_SYSTEM_PROGRAM_DUPLICATE, 6 +.equ E_RENT_DUPLICATE, 7 # The rent sysvar account is a duplicate. +.equ E_RENT_ADDRESS, 8 # The rent sysvar account has invalid data length. +# Instruction data provided during initialization instruction. +.equ E_INSTRUCTION_DATA, 9 +# The passed PDA does not match the expected address. +.equ E_PDA_MISMATCH, 10 +.equ E_INSTRUCTION_DISCRIMINATOR, 11 # Invalid instruction discriminator. +.equ E_INSTRUCTION_DATA_LEN, 12 # Invalid instruction data length. +# Not enough accounts passed for insertion allocation. +.equ E_N_ACCOUNTS_INSERT_ALLOCATION, 13 +.equ E_KEY_EXISTS, 14 # Key already exists in tree during insertion. +.equ E_KEY_DOES_NOT_EXIST, 15 # Key does not exist in tree during removal. + +# Type sizes. +# ----------- +.equ SIZE_OF_U8, 1 # Size of u8. +.equ SIZE_OF_U64, 8 # Size of u64. +.equ SIZE_OF_ADDRESS, 32 # Size of Address. +.equ SIZE_OF_U128, 16 # Size of u128. +.equ SIZE_OF_TREE_HEADER, 24 # Size of TreeHeader. +.equ SIZE_OF_INITIALIZE_INSTRUCTION, 1 # Size of InitializeInstruction. +.equ SIZE_OF_INSERT_INSTRUCTION, 5 # Size of InsertInstruction. +.equ SIZE_OF_REMOVE_INSTRUCTION, 3 # Size of RemoveInstruction. +.equ SIZE_OF_TREE_NODE, 29 # Size of TreeNode. + +# Data layout constants. +# ---------------------- +.equ DATA_LEN_ZERO, 0 # Data length of zero. +.equ BPF_ALIGN_OF_U128, 8 # Data alignment during runtime. +.equ OFFSET_ZERO, 0 # No offset. +.equ NULL, 0 # Null pointer. +.equ DATA_LEN_AND_MASK, -8 # And mask for data length alignment. +.equ MAX_DATA_PAD, 7 # Maximum possible data length padding. +.equ BOOL_TRUE, 1 # Boolean true value. + +# Pubkey chunking offsets. +# ------------------------ +.equ PUBKEY_CHUNK_OFF_0, 0 # Offset for the first 8 bytes. +.equ PUBKEY_CHUNK_OFF_1, 8 # Offset for the second 8 bytes. +.equ PUBKEY_CHUNK_OFF_2, 16 # Offset for the third 8 bytes. +.equ PUBKEY_CHUNK_OFF_3, 24 # Offset for the fourth 8 bytes. + +# Input buffer layout. +# -------------------- +.equ IB_N_ACCOUNTS_OFF, 0 # Number of accounts field. +.equ IB_USER_ACCOUNT_OFF, 8 # User runtime account. +.equ IB_USER_LAMPORTS_OFF, 80 # User Lamports field. +.equ IB_USER_DATA_OFF, 96 # User data field. +.equ IB_USER_OWNER_OFF, 48 # User owner field. +.equ IB_TREE_LAMPORTS_OFF, 10416 # Tree Lamports field. +.equ IB_TREE_DATA_OFF, 10432 # Tree data field. +.equ IB_TREE_OWNER_OFF, 10384 # Tree owner field. +.equ IB_TREE_ACCOUNT_OFF, 10344 # Tree runtime account header. +.equ IB_TREE_ADDRESS_OFF, 10352 # Tree address field. +# System Program runtime account header. +.equ IB_SYSTEM_PROGRAM_ACCOUNT_OFF, 20680 +.equ IB_RENT_ACCOUNT_OFF, 31032 # Rent sysvar account header. +.equ IB_RENT_DATA_OFF, 31120 # Rent sysvar account data. +# Expected number of accounts for general instructions. +.equ IB_N_ACCOUNTS_GENERAL, 2 +# Expected number of accounts for tree initialization. +.equ IB_N_ACCOUNTS_INIT, 4 +# Expected data length of system program account. +.equ IB_SYSTEM_PROGRAM_DATA_LEN, 14 +.equ IB_RENT_DATA_LEN, 17 # Expected data length of rent sysvar account. +.equ IB_USER_ADDRESS_OFF, 16 # User address field. +.equ IB_USER_DATA_LEN_OFF, 88 # User data length field. +.equ IB_NON_DUP_MARKER, 255 # Non-duplicate marker value. +.equ IB_TREE_NON_DUP_MARKER_OFF, 10344 # Tree non-duplicate marker field. +.equ IB_TREE_ADDRESS_OFF_0, 10352 # Tree address field (chunk index 0). +.equ IB_TREE_ADDRESS_OFF_1, 10360 # Tree address field (chunk index 1). +.equ IB_TREE_ADDRESS_OFF_2, 10368 # Tree address field (chunk index 2). +.equ IB_TREE_ADDRESS_OFF_3, 10376 # Tree address field (chunk index 3). +.equ IB_TREE_DATA_LEN_OFF, 10424 # Tree data length field. +# System Program non-duplicate marker field. +.equ IB_SYSTEM_PROGRAM_NON_DUP_MARKER_OFF, 20680 +# System Program data length field. +.equ IB_SYSTEM_PROGRAM_DATA_LEN_OFF, 20760 +# Rent account non-duplicate marker field. +.equ IB_RENT_NON_DUP_MARKER_OFF, 31032 +.equ IB_RENT_ADDRESS_OFF_0, 31040 # Rent address field (chunk index 0). +.equ IB_RENT_ADDRESS_OFF_1, 31048 # Rent address field (chunk index 1). +.equ IB_RENT_ADDRESS_OFF_2, 31056 # Rent address field (chunk index 2). +.equ IB_RENT_ADDRESS_OFF_3, 31064 # Rent address field (chunk index 3). +.equ IB_RENT_ID_CHUNK_0, 5862609301215225606 # Rent sysvar ID (chunk 0). +.equ IB_RENT_ID_CHUNK_0_LO, 399877894 # Rent sysvar ID (chunk 0 lo). +.equ IB_RENT_ID_CHUNK_0_HI, 1364995097 # Rent sysvar ID (chunk 0 hi). +.equ IB_RENT_ID_CHUNK_1, 9219231539345853473 # Rent sysvar ID (chunk 1). +.equ IB_RENT_ID_CHUNK_1_LO, 1288277025 # Rent sysvar ID (chunk 1 lo). +.equ IB_RENT_ID_CHUNK_1_HI, 2146519613 # Rent sysvar ID (chunk 1 hi). +.equ IB_RENT_ID_CHUNK_2, 4971307250928769624 # Rent sysvar ID (chunk 2). +.equ IB_RENT_ID_CHUNK_2_LO, 149871192 # Rent sysvar ID (chunk 2 lo). +.equ IB_RENT_ID_CHUNK_2_HI, 1157472667 # Rent sysvar ID (chunk 2 hi). +.equ IB_RENT_ID_CHUNK_3, 2329533411 # Rent sysvar ID (chunk 3). +.equ IB_RENT_ID_CHUNK_3_LO, -1965433885 # Rent sysvar ID (chunk 3 lo). +.equ IB_RENT_ID_CHUNK_3_HI, 0 # Rent sysvar ID (chunk 3 hi). +# Program ID field for initialize instruction. +.equ IB_INIT_PROGRAM_ID_OFF_IMM, 41401 +.equ IB_TREE_DATA_TOP_OFF, 10440 # Tree top pointer field within tree data. +# Tree next pointer field within tree data. +.equ IB_TREE_DATA_NEXT_OFF, 10448 +# Tree root pointer field within tree data. +.equ IB_TREE_DATA_ROOT_OFF, 10432 +# Relative offset from user data field to tree pubkey field. +.equ IB_USER_DATA_TO_TREE_ADDRESS_REL_OFF_IMM, 10256 + +# Offsets for instruction processing. +# ----------------------------------- +.equ INSN_DISCRIMINATOR_OFF, 0 # Offset to instruction discriminator byte. +# Initialize instruction discriminator. +.equ INSN_DISCRIMINATOR_INITIALIZE, 0 +.equ INSN_DISCRIMINATOR_INSERT, 1 # Insert instruction discriminator. +.equ INSN_DISCRIMINATOR_REMOVE, 2 # Remove instruction discriminator. +.equ INSN_INSERT_KEY_OFF, 1 # Key field in insert instruction. +.equ INSN_INSERT_VALUE_OFF, 3 # Value field in insert instruction. +.equ INSN_REMOVE_KEY_OFF, 1 # Key field in remove instruction. + +# Init stack frame layout. +# ------------------------ +.equ SF_INIT_BUMP_SEED_OFF, -352 # Bump seed. +.equ SF_INIT_SIGNER_SEED_ADDR_OFF, -96 # Bump signer seed address field. +.equ SF_INIT_SIGNER_SEED_LEN_OFF, -88 # Bump signer seed length field. +.equ SF_INIT_PDA_OFF, -80 # PDA address field. +# Discriminator field in CPI instruction data. +.equ SF_INIT_CREATE_ACCOUNT_DISCRIMINATOR_UOFF, -351 +# Lamports field in CreateAccount instruction data. +.equ SF_INIT_CREATE_ACCOUNT_LAMPORTS_UOFF, -347 +# Space address field in CreateAccount instruction data. +.equ SF_INIT_CREATE_ACCOUNT_SPACE_UOFF, -339 +# Owner field in CreateAccount instruction data (chunk index 0). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_0, -331 +# Owner field in CreateAccount instruction data (chunk index 1). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_1, -323 +# Owner field in CreateAccount instruction data (chunk index 2). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_2, -315 +# Owner field in CreateAccount instruction data (chunk index 3). +.equ SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_3, -307 +.equ SF_INIT_SIGNERS_SEEDS_ADDR_OFF, -112 # Signers seeds address field. +.equ SF_INIT_SIGNERS_SEEDS_LEN_OFF, -104 # Signers seeds length field. +.equ SF_INIT_SYSTEM_PROGRAM_ADDRESS_OFF, -32 # System Program address. +.equ SF_INIT_INSN_PROGRAM_ID_OFF, -296 # SolInstruction program_id field. +.equ SF_INIT_INSN_ACCOUNTS_OFF, -288 # SolInstruction accounts field. +.equ SF_INIT_INSN_ACCOUNT_LEN_OFF, -280 # SolInstruction account_len field. +.equ SF_INIT_INSN_DATA_OFF, -272 # SolInstruction data field. +.equ SF_INIT_INSN_DATA_LEN_OFF, -264 # SolInstruction data_len field. +# SolAccountMeta is_writable field for user account. +.equ SF_INIT_USER_META_IS_WRITABLE_OFF, -248 +# SolAccountMeta is_writable field for tree account. +.equ SF_INIT_TREE_META_IS_WRITABLE_OFF, -232 +# SolAccountInfo is_signer field for user account. +.equ SF_INIT_USER_INFO_IS_SIGNER_OFF, -176 +# SolAccountMeta pubkey field for user account. +.equ SF_INIT_USER_META_PUBKEY_OFF, -256 +# SolAccountInfo pubkey field for user account. +.equ SF_INIT_USER_INFO_PUBKEY_OFF, -224 +# SolAccountInfo owner field for user account. +.equ SF_INIT_USER_INFO_OWNER_OFF, -192 +# SolAccountInfo lamports field for user account. +.equ SF_INIT_USER_INFO_LAMPORTS_OFF, -216 +# SolAccountInfo data_len field for user account. +.equ SF_INIT_USER_INFO_DATA_OFF, -200 +# SolAccountInfo data_len for tree account. +.equ SF_INIT_TREE_INFO_DATA_LEN_OFF, -152 +# SolAccountInfo is_signer field for tree account. +.equ SF_INIT_TREE_INFO_IS_SIGNER_OFF, -120 +# SolAccountInfo is_writable field for tree account. +.equ SF_INIT_TREE_INFO_IS_WRITABLE_UOFF, -119 +# SolAccountMeta pubkey field for tree account. +.equ SF_INIT_TREE_META_PUBKEY_OFF, -240 +# SolAccountInfo pubkey field for tree account. +.equ SF_INIT_TREE_INFO_PUBKEY_OFF, -168 +# SolAccountInfo owner field for tree account. +.equ SF_INIT_TREE_INFO_OWNER_OFF, -136 +# SolAccountInfo lamports field for tree account. +.equ SF_INIT_TREE_INFO_LAMPORTS_OFF, -160 +# SolAccountInfo data_len field for tree account. +.equ SF_INIT_TREE_INFO_DATA_OFF, -144 +# Relative offset from PDA on stack to System Program ID. +.equ SF_INIT_PDA_TO_SYSTEM_PROGRAM_ID_REL_OFF_IMM, 48 +# Relative offset from System Program ID to first SolAccountMeta. +.equ SF_INIT_SYSTEM_PROGRAM_ID_TO_ACCT_METAS_REL_OFF_IMM, -224 +# Relative offset from SolAccountMeta array to instruction data. +.equ SF_INIT_ACCT_METAS_TO_INSN_DATA_REL_OFF_IMM, -95 +# Relative offset from instruction data to signer seeds. +.equ SF_INIT_INSN_DATA_TO_SIGNER_SEEDS_REL_OFF_IMM, 255 +# Relative offset from signer seeds to signers seeds. +.equ SF_INIT_SIGNER_SEEDS_TO_SIGNERS_SEEDS_REL_OFF_IMM, -16 +.equ SF_INIT_ACCT_INFOS_OFF, -224 # Account infos array. + +# CPI-specific constants. +# ----------------------- +.equ CPI_N_ACCOUNTS, 2 # User and tree accounts. +# The tree account is a PDA for CreateAccount CPI. +.equ CPI_N_PDA_SIGNERS, 1 +# Number of seeds for CreateAccount PDA signer (bump only). +.equ CPI_N_SEEDS_CREATE_ACCOUNT, 1 +# PDA signers for Transfer CPI (none — user is already a signer). +.equ CPI_N_PDA_SIGNERS_TRANSFER, 0 +.equ CPI_N_SEEDS_TRY_FIND_PDA, 0 # Number of seeds for PDA generation. +.equ CPI_TREE_DATA_LEN, 24 # Tree account data length. +# Account data scalar for base rent calculation. +.equ CPI_ACCOUNT_DATA_SCALAR, 152 +# CreateAccount discriminator for CPI. +.equ CPI_CREATE_ACCOUNT_DISCRIMINATOR, 0 +# Length of CreateAccount instruction data. +.equ CPI_CREATE_ACCOUNT_INSN_DATA_LEN, 52 +.equ CPI_TRANSFER_DISCRIMINATOR, 2 # Transfer discriminator for CPI. +.equ CPI_TRANSFER_INSN_DATA_LEN, 12 # Length of Transfer instruction data. +.equ CPI_WRITABLE_SIGNER, 0x0101 # Mask for writable signer. +.equ CPI_USER_ACCOUNT_INDEX, 0 # Account index for user account in CPI. +.equ CPI_TREE_ACCOUNT_INDEX, 1 # Account index for tree account in CPI. +.equ CPI_RENT_EPOCH_NULL, 0 # Null rent epoch. + +# Tree constants. +# --------------- +.equ TREE_N_CHILDREN, 2 # Max number of children per node. +.equ TREE_DIR_L, 0 # Left direction. +.equ TREE_DIR_R, 1 # Right direction. +.equ TREE_COLOR_B, 0 # Black color. +.equ TREE_COLOR_R, 1 # Red color. +.equ TREE_HEADER_TOP_OFF, 8 # Stack top field in header. +.equ TREE_HEADER_NEXT_OFF, 16 # Next node field in header. +.equ TREE_DISCRIMINATOR_INSERT, 1 # Discriminator for insert instruction. +.equ TREE_NODE_KEY_OFF, 24 # Node key field. +.equ TREE_NODE_VALUE_OFF, 26 # Node value field. +.equ TREE_NODE_CHILD_L_OFF, 8 # Node left child field. +.equ TREE_NODE_CHILD_R_OFF, 16 # Node right child field. +.equ TREE_NODE_PARENT_OFF, 0 # Node parent field. +.equ TREE_NODE_COLOR_OFF, 28 # Color field. +# ANCHOR_END: constants + +# ANCHOR: entrypoint-branching +.globl entrypoint + +entrypoint: + # Read instruction data length and discriminator. + # --------------------------------------------------------------------- + ldxdw r9, [r2 - SIZE_OF_U64] # Get instruction data length. + ldxdw r8, [r1 + IB_N_ACCOUNTS_OFF] # Get n input buffer accounts. + ldxb r7, [r2 + OFFSET_ZERO] # Get discriminator. + + # Jump to branch for given discriminator. + # --------------------------------------------------------------------- + jeq r7, INSN_DISCRIMINATOR_INSERT, insert + jeq r7, INSN_DISCRIMINATOR_REMOVE, remove + jeq r7, INSN_DISCRIMINATOR_INITIALIZE, initialize + # Error if invalid discriminator provided. + mov64 r0, E_INSTRUCTION_DISCRIMINATOR + exit + # ANCHOR_END: entrypoint-branching + +# ANCHOR: initialize-input-checks +initialize: + # Error if invalid instruction data length. + # --------------------------------------------------------------------- + jne r9, SIZE_OF_INITIALIZE_INSTRUCTION, e_instruction_data_len + + # Error if invalid number of accounts. + # --------------------------------------------------------------------- + jne r8, IB_N_ACCOUNTS_INIT, e_n_accounts + + # Error if user has data. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_USER_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_user_data_len + + # Error if tree is duplicate or has data. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_TREE_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_tree_duplicate + ldxdw r9, [r1 + IB_TREE_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_tree_data_len + + # Error if System Program is duplicate or has invalid data length. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_SYSTEM_PROGRAM_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_system_program_duplicate + ldxdw r9, [r1 + IB_SYSTEM_PROGRAM_DATA_LEN_OFF] + jne r9, IB_SYSTEM_PROGRAM_DATA_LEN, e_system_program_data_len + + # Error if Rent account is duplicate or has incorrect address. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_RENT_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_rent_duplicate + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_0] + lddw r8, IB_RENT_ID_CHUNK_0 + jne r9, r8, e_rent_address + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_1] + lddw r8, IB_RENT_ID_CHUNK_1 + jne r9, r8, e_rent_address + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_2] + lddw r8, IB_RENT_ID_CHUNK_2 + jne r9, r8, e_rent_address + ldxdw r9, [r1 + IB_RENT_ADDRESS_OFF_3] + # Optimize out the following line, which costs two CUs due to two + # 32-bit immediate loads across two opcodes: + # ``` + # lddw r8, IB_RENT_ID_CHUNK_3 + # ``` + # Instead, replace with mov32, which only loads one 32-bit immediate, + # since the rent sysvar address has all chunk 3 hi bits unset. + mov32 r8, IB_RENT_ID_CHUNK_3_LO + jne r9, r8, e_rent_address + # ANCHOR_END: initialize-input-checks + + # ANCHOR: initialize-pda-checks + # Compute PDA. + # --------------------------------------------------------------------- + # Skip assignment for r1, since no seeds need to be parsed and this + # argument is effectively ignored. + # --------------------------------------------------------------------- + mov64 r2, CPI_N_SEEDS_TRY_FIND_PDA # Declare no seeds to parse. + mov64 r3, r1 # Get input buffer pointer. + add64 r3, IB_INIT_PROGRAM_ID_OFF_IMM # Point at program ID. + mov64 r4, r10 # Get stack frame pointer. + add64 r4, SF_INIT_PDA_OFF # Point to PDA region on stack. + mov64 r5, r10 # Get stack frame pointer. + add64 r5, SF_INIT_BUMP_SEED_OFF # Point to bump seed region on stack. + call sol_try_find_program_address # Find PDA. + + # Compare computed PDA against passed account. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_0] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_0] + jne r9, r8, e_pda_mismatch + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_1] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_1] + jne r9, r8, e_pda_mismatch + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_2] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_2] + jne r9, r8, e_pda_mismatch + ldxdw r9, [r1 + IB_TREE_ADDRESS_OFF_3] + ldxdw r8, [r4 + PUBKEY_CHUNK_OFF_3] + jne r9, r8, e_pda_mismatch + # ANCHOR_END: initialize-pda-checks + + // ANCHOR: initialize-create-account + # Pack SolInstruction. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] System Program ID pointer. + # - [x] Account metas pointer. + # - [x] Instruction data pointer. + # --------------------------------------------------------------------- + stdw [r10 + SF_INIT_INSN_ACCOUNT_LEN_OFF], CPI_N_ACCOUNTS + stdw [r10 + SF_INIT_INSN_DATA_LEN_OFF], CPI_CREATE_ACCOUNT_INSN_DATA_LEN + + # Pack CreateAccount instruction data. + # --------------------------------------------------------------------- + # - Discriminator is already set to 0 since stack is zero initialized. + # - Reuses r3 from PDA syscall. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_RENT_DATA_OFF] # Load lamports per byte + mul64 r9, CPI_ACCOUNT_DATA_SCALAR # Multiply to get rent-exempt cost. + # Store in instruction data. + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_LAMPORTS_UOFF], r9 + # Store new account data length. + stdw [r10 + SF_INIT_CREATE_ACCOUNT_SPACE_UOFF], CPI_TREE_DATA_LEN + # Copy in program ID to instruction data. + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_0] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_0], r9 + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_1] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_1], r9 + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_2] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_2], r9 + ldxdw r9, [r3 + PUBKEY_CHUNK_OFF_3] + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_OWNER_UOFF_3], r9 + + # Pack SolAccountMeta for user and tree. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] User pubkey pointer. + # - [x] Tree pubkey pointer. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_META_IS_WRITABLE_OFF], CPI_WRITABLE_SIGNER + sth [r10 + SF_INIT_TREE_META_IS_WRITABLE_OFF], CPI_WRITABLE_SIGNER + + # Pack SolAccountInfo for user and tree. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] User pubkey pointer. + # - [x] Tree pubkey pointer. + # - [x] User lamports pointer. + # - [x] Tree lamports pointer. + # - [x] User data pointer. + # - [x] Tree data pointer. + # - [x] User owner pointer. + # - [x] Tree owner pointer. + # Skipped due to zero-initialized stack memory: + # - User data length (already checked as zero). + # - Tree data length (already checked as zero). + # - User rent epoch. + # - Tree rent epoch. + # - User executable. + # - Tree executable. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_INFO_IS_SIGNER_OFF], CPI_WRITABLE_SIGNER + sth [r10 + SF_INIT_TREE_INFO_IS_SIGNER_OFF], CPI_WRITABLE_SIGNER + + # Initialize signer seed for PDA bump seed. + # --------------------------------------------------------------------- + # Reuses r5 from PDA derivation syscall. + # --------------------------------------------------------------------- + # Store pointer to bump seed. + stxdw [r10 + SF_INIT_SIGNER_SEED_ADDR_OFF], r5 + stdw [r10 + SF_INIT_SIGNER_SEED_LEN_OFF], SIZE_OF_U8 # Store length. + + # Initialize signers seeds for PDA. + # --------------------------------------------------------------------- + # Packed later during bulk pointer load operation: + # - [x] Signer seed pointer. + # --------------------------------------------------------------------- + stdw [r10 + SF_INIT_SIGNERS_SEEDS_LEN_OFF], CPI_N_SEEDS_CREATE_ACCOUNT + + # Bulk assign/load pointers for account metas and infos. + # --------------------------------------------------------------------- + # Since pointers must be loaded from registers, this block steps + # through the input buffer in order to reduce intermediate loads. + # --------------------------------------------------------------------- + add64 r1, IB_USER_ADDRESS_OFF # Point to user address in input buffer. + stxdw [r10 + SF_INIT_USER_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_USER_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user owner. + stxdw [r10 + SF_INIT_USER_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user lamports. + stxdw [r10 + SF_INIT_USER_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to user data. + stxdw [r10 + SF_INIT_USER_INFO_DATA_OFF], r1 # Store in account info. + # Advance to tree address field. + add64 r1, IB_USER_DATA_TO_TREE_ADDRESS_REL_OFF_IMM + stxdw [r10 + SF_INIT_TREE_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_TREE_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree owner. + stxdw [r10 + SF_INIT_TREE_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree lamports. + stxdw [r10 + SF_INIT_TREE_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to tree data. + stxdw [r10 + SF_INIT_TREE_INFO_DATA_OFF], r1 # Store in account info. + mov64 r6, r1 # Store tree data pointer for later. + + # Bulk assign/load pointers for CPI bindings. + # --------------------------------------------------------------------- + # This block steps through the stack frame, optimizing assignments in + # preparation for the impending CreateAccount CPI, which requires: + # - [x] r1 = pointer to instruction. + # - [x] r2 = pointer to account infos. + # - [x] r4 = pointer to signers seeds. + # Notably, it reuses r4 from the PDA derivation syscall to walk through + # pointers on the stack, before advancing it to its final value. + # --------------------------------------------------------------------- + # Advance to System Program ID pointer on zero-initialized stack. + add64 r4, SF_INIT_PDA_TO_SYSTEM_PROGRAM_ID_REL_OFF_IMM + # Store in SolInstruction. + stxdw [r10 + SF_INIT_INSN_PROGRAM_ID_OFF], r4 + # Advance to SolAccountMeta array pointer. + add64 r4, SF_INIT_SYSTEM_PROGRAM_ID_TO_ACCT_METAS_REL_OFF_IMM + stxdw [r10 + SF_INIT_INSN_ACCOUNTS_OFF], r4 # Store in SolInstruction. + # Advance to instruction data pointer. + add64 r4, SF_INIT_ACCT_METAS_TO_INSN_DATA_REL_OFF_IMM + stxdw [r10 + SF_INIT_INSN_DATA_OFF], r4 # Store in SolInstruction. + # Advance to signer seeds pointer. + add64 r4, SF_INIT_INSN_DATA_TO_SIGNER_SEEDS_REL_OFF_IMM + stxdw [r10 + SF_INIT_SIGNERS_SEEDS_ADDR_OFF], r4 + # Advance to signers seeds pointer. + add64 r4, SF_INIT_SIGNER_SEEDS_TO_SIGNERS_SEEDS_REL_OFF_IMM + # Assign remaining syscall pointers. + mov64 r1, r10 + add64 r1, SF_INIT_INSN_PROGRAM_ID_OFF + mov64 r2, r10 + add64 r2, SF_INIT_ACCT_INFOS_OFF + + # Invoke CPI. + # --------------------------------------------------------------------- + mov64 r3, CPI_N_ACCOUNTS + mov64 r5, CPI_N_PDA_SIGNERS + call sol_invoke_signed_c + + # Store next pointer in tree header. + # --------------------------------------------------------------------- + mov64 r7, r6 # Get copy of tree data pointer. + add64 r7, SIZE_OF_TREE_HEADER # Advance to next node. + stxdw [r6 + TREE_HEADER_NEXT_OFF], r7 # Store in next field. + + exit + // ANCHOR_END: initialize-create-account + +# ANCHOR: insert-input-checks +insert: + # Error if invalid instruction data length. + # --------------------------------------------------------------------- + jne r9, SIZE_OF_INSERT_INSTRUCTION, e_instruction_data_len + + # Error if too few accounts. + # --------------------------------------------------------------------- + jlt r8, IB_N_ACCOUNTS_GENERAL, e_n_accounts + + # Error if user has data. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_USER_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_user_data_len + + # Error if tree is duplicate. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_TREE_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_tree_duplicate + # ANCHOR_END: insert-input-checks + + # ANCHOR: insert-allocate + # Branch based on state of stack top. + # --------------------------------------------------------------------- + # Get stack top pointer. + ldxdw r9, [r1 + IB_TREE_DATA_TOP_OFF] + jne r9, NULL, insert_pop # Pop node from stack if non-null. + +insert_allocate: + # Error if wrong number of accounts for allocation. + # --------------------------------------------------------------------- + jne r8, IB_N_ACCOUNTS_INIT, e_n_accounts_insert_allocation + + # Compute shifted input buffer pointer based on tree data length. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_TREE_DATA_LEN_OFF] # Get tree data length. + # Store in account info for CPI. + stxdw [r10 + SF_INIT_TREE_INFO_DATA_LEN_OFF], r9 + mov64 r7, r9 # Store copy for later. + add64 r9, MAX_DATA_PAD # Add max possible padding. + and64 r9, DATA_LEN_AND_MASK # Truncate to 8-byte alignment. + add64 r9, r1 # Increment by input buffer. + + # Check system program is not duplicate and has correct data length. + # --------------------------------------------------------------------- + ldxb r8, [r9 + IB_SYSTEM_PROGRAM_NON_DUP_MARKER_OFF] + jne r8, IB_NON_DUP_MARKER, e_system_program_duplicate + ldxdw r8, [r9 + IB_SYSTEM_PROGRAM_DATA_LEN_OFF] + jne r8, IB_SYSTEM_PROGRAM_DATA_LEN, e_system_program_data_len + + # Check rent sysvar is not duplicate and has correct address. + # --------------------------------------------------------------------- + ldxb r8, [r9 + IB_RENT_NON_DUP_MARKER_OFF] + jne r8, IB_NON_DUP_MARKER, e_rent_duplicate + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_0] + lddw r4, IB_RENT_ID_CHUNK_0 + jne r8, r4, e_rent_address + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_1] + lddw r4, IB_RENT_ID_CHUNK_1 + jne r8, r4, e_rent_address + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_2] + lddw r4, IB_RENT_ID_CHUNK_2 + jne r8, r4, e_rent_address + ldxdw r8, [r9 + IB_RENT_ADDRESS_OFF_3] + mov32 r4, IB_RENT_ID_CHUNK_3_LO + jne r8, r4, e_rent_address + + # Calculate transfer lamports. + # --------------------------------------------------------------------- + ldxdw r8, [r9 + IB_RENT_DATA_OFF] # Load lamports per byte. + mul64 r8, SIZE_OF_TREE_NODE # Multiply to get transfer cost. + + # Pack Transfer instruction data in CreateAccount slot on stack. + # --------------------------------------------------------------------- + stw [r10 + SF_INIT_CREATE_ACCOUNT_DISCRIMINATOR_UOFF], CPI_TRANSFER_DISCRIMINATOR + stxdw [r10 + SF_INIT_CREATE_ACCOUNT_LAMPORTS_UOFF], r8 + + # Pack SolInstruction. + # --------------------------------------------------------------------- + stdw [r10 + SF_INIT_INSN_ACCOUNT_LEN_OFF], CPI_N_ACCOUNTS + stdw [r10 + SF_INIT_INSN_DATA_LEN_OFF], CPI_TRANSFER_INSN_DATA_LEN + + # Pack SolAccountMeta flags for user and tree. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_META_IS_WRITABLE_OFF], CPI_WRITABLE_SIGNER + stb [r10 + SF_INIT_TREE_META_IS_WRITABLE_OFF], BOOL_TRUE + + # Pack SolAccountInfo flags for user and tree. + # --------------------------------------------------------------------- + sth [r10 + SF_INIT_USER_INFO_IS_SIGNER_OFF], CPI_WRITABLE_SIGNER + stb [r10 + SF_INIT_TREE_INFO_IS_WRITABLE_UOFF], BOOL_TRUE + + # Bulk assign/load pointers for account metas and infos. + # --------------------------------------------------------------------- + mov64 r6, r1 # Store input buffer pointer for later. + add64 r1, IB_USER_ADDRESS_OFF # Point to user address in input buffer. + stxdw [r10 + SF_INIT_USER_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_USER_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user owner. + stxdw [r10 + SF_INIT_USER_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to user lamports. + stxdw [r10 + SF_INIT_USER_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to user data. + stxdw [r10 + SF_INIT_USER_INFO_DATA_OFF], r1 # Store in account info. + # Advance to tree address field. + add64 r1, IB_USER_DATA_TO_TREE_ADDRESS_REL_OFF_IMM + stxdw [r10 + SF_INIT_TREE_META_PUBKEY_OFF], r1 # Store in account meta. + stxdw [r10 + SF_INIT_TREE_INFO_PUBKEY_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree owner. + stxdw [r10 + SF_INIT_TREE_INFO_OWNER_OFF], r1 # Store in account info. + add64 r1, SIZE_OF_ADDRESS # Advance to tree lamports. + stxdw [r10 + SF_INIT_TREE_INFO_LAMPORTS_OFF], r1 # Store in acct info. + add64 r1, SIZE_OF_U128 # Advance to tree data. + stxdw [r10 + SF_INIT_TREE_INFO_DATA_OFF], r1 # Store in account info. + + # Bulk assign/load pointers for CPI bindings. + # --------------------------------------------------------------------- + # Point to System Program ID on zero-initialized stack. + mov64 r4, r10 + add64 r4, SF_INIT_SYSTEM_PROGRAM_ADDRESS_OFF + # Store in SolInstruction. + stxdw [r10 + SF_INIT_INSN_PROGRAM_ID_OFF], r4 + # Advance to SolAccountMeta array pointer. + add64 r4, SF_INIT_SYSTEM_PROGRAM_ID_TO_ACCT_METAS_REL_OFF_IMM + # Store in SolInstruction. + stxdw [r10 + SF_INIT_INSN_ACCOUNTS_OFF], r4 + # Advance to instruction data pointer. + add64 r4, SF_INIT_ACCT_METAS_TO_INSN_DATA_REL_OFF_IMM + stxdw [r10 + SF_INIT_INSN_DATA_OFF], r4 # Store in SolInstruction. + + # Invoke Transfer CPI. + # --------------------------------------------------------------------- + mov64 r1, r10 + add64 r1, SF_INIT_INSN_PROGRAM_ID_OFF + mov64 r8, r2 # Save instruction data pointer for later. + mov64 r2, r10 + add64 r2, SF_INIT_ACCT_INFOS_OFF + mov64 r3, CPI_N_ACCOUNTS + # Ignore PDA signer seeds pointer, since none required. + mov64 r5, CPI_N_PDA_SIGNERS_TRANSFER + call sol_invoke_signed_c + mov64 r2, r8 # Restore instruction data pointer. + mov64 r1, r6 # Restore input buffer pointer. + + # Update tree data length. + # --------------------------------------------------------------------- + add64 r7, SIZE_OF_TREE_NODE # Increment data length. + stxdw [r1 + IB_TREE_DATA_LEN_OFF], r7 # Store in input buffer. + + # Get node = next, then advance next by one TreeNode. + # --------------------------------------------------------------------- + ldxdw r7, [r1 + IB_TREE_DATA_NEXT_OFF] # Get pointer to next node. + mov64 r9, r7 # Store node pointer for later, the new node. + add64 r7, SIZE_OF_TREE_NODE # Increment to point to new next. + stxdw [r1 + IB_TREE_DATA_NEXT_OFF], r7 # Advance next. + + # Continue insert. + # --------------------------------------------------------------------- + ja insert_store_key_value_pair + +insert_pop: + # Pop node from free stack. + # --------------------------------------------------------------------- + ldxdw r8, [r9 + OFFSET_ZERO] # Load StackNode.next. + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r8 # Update top in header. + +insert_store_key_value_pair: + ldxw r4, [r2 + INSN_INSERT_KEY_OFF] # Load two fields together. + stxw [r9 + TREE_NODE_KEY_OFF], r4 # Store both fields together. + # ANCHOR_END: insert-allocate + +# ANCHOR: insert-search +insert_search: # r9 = node + ldxh r4, [r2 + INSN_INSERT_KEY_OFF] # r4 = insn.key; + ldxdw r3, [r1 + IB_TREE_DATA_ROOT_OFF] # r3 = cursor = root; + jeq r3, NULL, insert_root + +insert_search_loop: + mov64 r2, r3 # r2 = parent = cursor; + ldxh r5, [r3 + TREE_NODE_KEY_OFF] # r5 = cursor.key; + jlt r4, r5, insert_search_branch_l + jgt r4, r5, insert_search_branch_r + mov64 r0, E_KEY_EXISTS # Error if key already exists. + exit + +insert_root: + # Root is null: new node becomes root. + # --------------------------------------------------------------------- + stb [r9 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # node.color = red; + mov64 r2, NULL # r2 = parent = null; + stxdw [r9 + TREE_NODE_PARENT_OFF], r2 # node.parent = null; + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r9 # root = node; + exit + +insert_search_branch_l: + ldxdw r3, [r2 + TREE_NODE_CHILD_L_OFF] # r3 = parent.child[L]; + jne r3, NULL, insert_search_loop + # Null child: insert node as left child of parent. + stb [r9 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # node.color = red; + stxdw [r9 + TREE_NODE_PARENT_OFF], r2 # node.parent = parent; + stxdw [r2 + TREE_NODE_CHILD_L_OFF], r9 # parent.child[L] = node; + # Inline case 1: if parent is black, tree is valid. + ldxb r6, [r2 + TREE_NODE_COLOR_OFF] # r6 = parent.color; + jne r6, TREE_COLOR_B, insert_fixup_check_case_4 + exit + +insert_search_branch_r: + ldxdw r3, [r2 + TREE_NODE_CHILD_R_OFF] # r3 = parent.child[R]; + jne r3, NULL, insert_search_loop + # Null child: insert node as right child of parent. + stb [r9 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # node.color = red; + stxdw [r9 + TREE_NODE_PARENT_OFF], r2 # node.parent = parent; + stxdw [r2 + TREE_NODE_CHILD_R_OFF], r9 # parent.child[R] = node; +# ANCHOR_END: insert-search + +# ANCHOR: insert-fixup-case-1 +insert_fixup_main: + # r2 := parent + # r5 := parent.key + # Case 1. # r9 := node + # --------------------------------------------------------------------- + ldxb r6, [r2 + TREE_NODE_COLOR_OFF] # r6 = parent.color; + jne r6, TREE_COLOR_B, insert_fixup_check_case_4 + exit # If parent is black, tree is still valid, so exit. +# ANCHOR_END: insert-fixup-case-1 + +# ANCHOR: insert-fixup-case-4 +insert_fixup_check_case_4: + # Check case 4. + # --------------------------------------------------------------------- + ldxdw r3, [r2 + TREE_NODE_PARENT_OFF] # r3 = grandparent; + jne r3, NULL, insert_fixup_check_case_5_6 + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + exit +# ANCHOR_END: insert-fixup-case-4 + +# ANCHOR: insert-fixup-case-5-6-dir-l +insert_fixup_check_case_5_6: + # Get uncle and check for case 5 or 6. + # --------------------------------------------------------------------- + ldxh r4, [r3 + TREE_NODE_KEY_OFF] # r4 = grandparent.key; + jgt r5, r4, insert_fixup_check_case_5_6_dir_r + +insert_fixup_check_case_5_6_dir_l: + ldxdw r7, [r3 + TREE_NODE_CHILD_R_OFF] # r7 = uncle; + jeq r7, NULL, insert_fixup_case_5_6_dir_l + ldxb r8, [r7 + TREE_NODE_COLOR_OFF] # r8 = uncle.color; + jne r8, TREE_COLOR_B, insert_fixup_case_2 + +insert_fixup_case_5_6_dir_l: + ldxdw r6, [r2 + TREE_NODE_CHILD_R_OFF] # r6 = new_root = parent.child[R]; + jne r9, r6, insert_fixup_case_6_dir_l + +insert_fixup_case_5_dir_l: + ldxdw r8, [r6 + TREE_NODE_CHILD_L_OFF] # r8 = new_child = new_root.child[L]; + stxdw [r2 + TREE_NODE_CHILD_R_OFF], r8 # parent.child[R] = new_child; + jeq r8, NULL, insert_fixup_case_5_dir_l_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r2 # new_child.parent = parent; +insert_fixup_case_5_dir_l_skip: + stxdw [r6 + TREE_NODE_CHILD_L_OFF], r2 # new_root.child[L] = parent; + stxdw [r6 + TREE_NODE_PARENT_OFF], r3 # new_root.parent = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r6 # parent.parent = new_root; + stxdw [r3 + TREE_NODE_CHILD_L_OFF], r6 # grandparent.child[L] = new_root; + mov64 r9, r2 # node = old parent; + mov64 r2, r6 # parent = new_root; + +insert_fixup_case_6_dir_l: + ldxdw r4, [r3 + TREE_NODE_PARENT_OFF] # r4 = great-grandparent; + ldxdw r8, [r2 + TREE_NODE_CHILD_R_OFF] # r8 = new_child = parent.child[R]; + stxdw [r3 + TREE_NODE_CHILD_L_OFF], r8 # grandparent.child[L] = new_child; + jeq r8, NULL, insert_fixup_case_6_dir_l_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r3 # new_child.parent = grandparent; +insert_fixup_case_6_dir_l_skip: + stxdw [r2 + TREE_NODE_CHILD_R_OFF], r3 # parent.child[R] = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r4 # parent.parent = great-grandparent; + stxdw [r3 + TREE_NODE_PARENT_OFF], r2 # grandparent.parent = parent; + jeq r4, NULL, insert_fixup_case_6_dir_l_root + ldxdw r8, [r4 + TREE_NODE_CHILD_R_OFF] # r8 = great-grandparent.child[R]; + jne r3, r8, insert_fixup_case_6_dir_l_left + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_R_OFF], r2 # great-grandparent.child[R] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_l_left: + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_L_OFF], r2 # great-grandparent.child[L] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_l_root: + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r2 # tree.root = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +# ANCHOR_END: insert-fixup-case-5-6-dir-l + +# ANCHOR: insert-fixup-case-5-6-dir-r +insert_fixup_check_case_5_6_dir_r: + ldxdw r7, [r3 + TREE_NODE_CHILD_L_OFF] # r7 = uncle; + jeq r7, NULL, insert_fixup_case_5_6_dir_r + ldxb r8, [r7 + TREE_NODE_COLOR_OFF] # r8 = uncle.color; + jne r8, TREE_COLOR_B, insert_fixup_case_2 + +insert_fixup_case_5_6_dir_r: + ldxdw r6, [r2 + TREE_NODE_CHILD_L_OFF] # r6 = new_root = parent.child[L]; + jne r9, r6, insert_fixup_case_6_dir_r + +insert_fixup_case_5_dir_r: + ldxdw r8, [r6 + TREE_NODE_CHILD_R_OFF] # r8 = new_child = new_root.child[R]; + stxdw [r2 + TREE_NODE_CHILD_L_OFF], r8 # parent.child[L] = new_child; + jeq r8, NULL, insert_fixup_case_5_dir_r_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r2 # new_child.parent = parent; +insert_fixup_case_5_dir_r_skip: + stxdw [r6 + TREE_NODE_CHILD_R_OFF], r2 # new_root.child[R] = parent; + stxdw [r6 + TREE_NODE_PARENT_OFF], r3 # new_root.parent = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r6 # parent.parent = new_root; + stxdw [r3 + TREE_NODE_CHILD_R_OFF], r6 # grandparent.child[R] = new_root; + mov64 r9, r2 # node = old parent; + mov64 r2, r6 # parent = new_root; + +insert_fixup_case_6_dir_r: + ldxdw r4, [r3 + TREE_NODE_PARENT_OFF] # r4 = great-grandparent; + ldxdw r8, [r2 + TREE_NODE_CHILD_L_OFF] # r8 = new_child = parent.child[L]; + stxdw [r3 + TREE_NODE_CHILD_R_OFF], r8 # grandparent.child[R] = new_child; + jeq r8, NULL, insert_fixup_case_6_dir_r_skip + stxdw [r8 + TREE_NODE_PARENT_OFF], r3 # new_child.parent = grandparent; +insert_fixup_case_6_dir_r_skip: + stxdw [r2 + TREE_NODE_CHILD_L_OFF], r3 # parent.child[L] = grandparent; + stxdw [r2 + TREE_NODE_PARENT_OFF], r4 # parent.parent = great-grandparent; + stxdw [r3 + TREE_NODE_PARENT_OFF], r2 # grandparent.parent = parent; + jeq r4, NULL, insert_fixup_case_6_dir_r_root + ldxdw r8, [r4 + TREE_NODE_CHILD_R_OFF] # r8 = great-grandparent.child[R]; + jne r3, r8, insert_fixup_case_6_dir_r_left + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_R_OFF], r2 # great-grandparent.child[R] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_r_left: + # Inline color + exit to eliminate `ja` (saves 1 CU). + stxdw [r4 + TREE_NODE_CHILD_L_OFF], r2 # great-grandparent.child[L] = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +insert_fixup_case_6_dir_r_root: + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r2 # tree.root = parent; + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + exit +# ANCHOR_END: insert-fixup-case-5-6-dir-r + +# ANCHOR: insert-fixup-case-2-3 +insert_fixup_case_2: + # r2 := parent + # r3 := grandparent + # r7 := uncle + # Case 2/3. # r9 := node + # --------------------------------------------------------------------- + stb [r2 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r7 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # uncle.color = black; + stb [r3 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # grandparent.color = red; + mov64 r9, r3 # node = grandparent; + ldxdw r2, [r9 + TREE_NODE_PARENT_OFF] # parent = node.parent; + jne r2, NULL, insert_fixup_main + exit # Case 3. +# ANCHOR_END: insert-fixup-case-2-3 + +# ANCHOR: remove-input-checks +remove: + # Error if invalid instruction data length. + # --------------------------------------------------------------------- + jne r9, SIZE_OF_REMOVE_INSTRUCTION, e_instruction_data_len + + # Error if too few accounts. + # --------------------------------------------------------------------- + jlt r8, IB_N_ACCOUNTS_GENERAL, e_n_accounts + + # Error if user has data. + # --------------------------------------------------------------------- + ldxdw r9, [r1 + IB_USER_DATA_LEN_OFF] + jne r9, DATA_LEN_ZERO, e_user_data_len + + # Error if tree is duplicate. + # --------------------------------------------------------------------- + ldxb r9, [r1 + IB_TREE_NON_DUP_MARKER_OFF] + jne r9, IB_NON_DUP_MARKER, e_tree_duplicate + # ANCHOR_END: remove-input-checks + +# ANCHOR: remove-search +remove_search: + ldxdw r3, [r1 + IB_TREE_DATA_ROOT_OFF] # r3 = node = root; + jeq r3, NULL, e_key_does_not_exist + ldxh r4, [r2 + INSN_REMOVE_KEY_OFF] # r4 = key; + +remove_search_loop: + ldxh r5, [r3 + TREE_NODE_KEY_OFF] # r5 = node.key; + jeq r4, r5, remove_found + jgt r4, r5, remove_search_r + +remove_search_l: + ldxdw r3, [r3 + TREE_NODE_CHILD_L_OFF] # r3 = node.child[L]; + jne r3, NULL, remove_search_loop + mov64 r0, E_KEY_DOES_NOT_EXIST + exit + +remove_search_r: + ldxdw r3, [r3 + TREE_NODE_CHILD_R_OFF] # r3 = node.child[R]; + jne r3, NULL, remove_search_loop + +e_key_does_not_exist: + mov64 r0, E_KEY_DOES_NOT_EXIST + exit +# ANCHOR_END: remove-search + +# ANCHOR: remove-simple-1 +remove_found: + ldxdw r4, [r3 + TREE_NODE_CHILD_L_OFF] # r4 = node.child[L]; + jeq r4, NULL, remove_check_child_r + ldxdw r5, [r3 + TREE_NODE_CHILD_R_OFF] # r5 = node.child[R]; + jeq r5, NULL, remove_simple_2_child_l + + # Simple case 1: successor swap. + # --------------------------------------------------------------------- +remove_successor_loop: + ldxdw r4, [r5 + TREE_NODE_CHILD_L_OFF] # r4 = successor.child[L]; + jeq r4, NULL, remove_successor_copy + mov64 r5, r4 # successor = left; + ja remove_successor_loop + +remove_successor_copy: + # Copy key/value pair as u32 from successor to found node. + # --------------------------------------------------------------------- + ldxw r4, [r5 + TREE_NODE_KEY_OFF] # r4 = successor.{key,value}; + stxw [r3 + TREE_NODE_KEY_OFF], r4 # node.{key,value} = r4; + mov64 r3, r5 # node = successor; +# ANCHOR_END: remove-simple-1 + +# ANCHOR: remove-simple-2 +remove_check_child_r: # r3 = node + ldxdw r4, [r3 + TREE_NODE_CHILD_R_OFF] # r4 = node.child[R]; + jeq r4, NULL, remove_check_simple_3_4 + +remove_simple_2_child_l: # r4 = child + # Simple case 2: replace node with child, recolor child black. + # --------------------------------------------------------------------- + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # r5 = parent; + stxdw [r4 + TREE_NODE_PARENT_OFF], r5 # child.parent = parent; + stb [r4 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # child.color = black; + jeq r5, NULL, remove_simple_2_root + ldxdw r6, [r5 + TREE_NODE_CHILD_R_OFF] # r6 = parent.child[R]; + jne r3, r6, remove_simple_2_dir_l + stxdw [r5 + TREE_NODE_CHILD_R_OFF], r4 # parent.child[R] = child; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit + +remove_simple_2_dir_l: + stxdw [r5 + TREE_NODE_CHILD_L_OFF], r4 # parent.child[L] = child; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit + +remove_simple_2_root: + stxdw [r1 + IB_TREE_DATA_ROOT_OFF], r4 # tree.root = child; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit +# ANCHOR_END: remove-simple-2 + +# ANCHOR: remove-simple-3 +remove_check_simple_3_4: # r3 = node + # Simple case 3: root leaf — clear root. + # --------------------------------------------------------------------- + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # r5 = parent; + jne r5, NULL, remove_check_simple_4 + stdw [r1 + IB_TREE_DATA_ROOT_OFF], NULL # tree.root = null; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit +# ANCHOR_END: remove-simple-3 + +# ANCHOR: remove-simple-4 +remove_check_simple_4: # r5 = parent + # Simple case 4: red leaf — detach from parent. + # --------------------------------------------------------------------- + ldxb r4, [r3 + TREE_NODE_COLOR_OFF] # r4 = node.color; + jne r4, TREE_COLOR_R, remove_complex + ldxdw r4, [r5 + TREE_NODE_CHILD_R_OFF] # r4 = parent.child[R]; + jne r3, r4, remove_simple_4_dir_l + stdw [r5 + TREE_NODE_CHILD_R_OFF], NULL # parent.child[R] = null; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit + +remove_simple_4_dir_l: + stdw [r5 + TREE_NODE_CHILD_L_OFF], NULL # parent.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r3 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r3 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r3 # header.top = node; + exit +# ANCHOR_END: remove-simple-4 + +# ANCHOR: remove-complex +remove_complex: + mov64 r2, r3 # r2 = deleted node (save for recycle); + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # r5 = parent; + ldxdw r4, [r5 + TREE_NODE_CHILD_L_OFF] # r4 = parent.child[L]; + jne r3, r4, remove_complex_dir_r # if node != parent.child[L] goto remove_complex_dir_r +remove_complex_dir_l: + # Prepare to loop for when node direction is left. + # --------------------------------------------------------------------- + stdw [r5 + TREE_NODE_CHILD_L_OFF], NULL # parent.child[L] = null; + ja remove_complex_loop_dir_l # goto remove_complex_loop_dir_l +remove_complex_dir_r: + # Prepare to loop for when node direction is right. + # --------------------------------------------------------------------- + stdw [r5 + TREE_NODE_CHILD_R_OFF], NULL # parent.child[R] = null; +remove_complex_loop_dir_r: + # Rebalance loop for when node direction is right. + # --------------------------------------------------------------------- + ldxdw r6, [r5 + TREE_NODE_CHILD_L_OFF] # r6 = sibling = parent.child[L]; + ldxdw r7, [r6 + TREE_NODE_CHILD_L_OFF] # r7 = distant_nephew = sibling.child[L]; + ldxdw r8, [r6 + TREE_NODE_CHILD_R_OFF] # r8 = close_nephew = sibling.child[R]; + # Check for red sibling. + # --------------------------------------------------------------------- + ldxb r9, [r6 + TREE_NODE_COLOR_OFF] # r9 = sibling.color; + jeq r9, TREE_COLOR_R, remove_complex_loop_dir_r_sibling_red # if sibling.color == red goto remove_complex_loop_dir_r_sibling_red + # Check for red distant nephew. + # --------------------------------------------------------------------- + jeq r7, NULL, remove_complex_loop_dir_r_check_case_5 # if distant_nephew == null goto remove_complex_loop_dir_r_check_case_5 + ldxb r9, [r7 + TREE_NODE_COLOR_OFF] # r9 = distant_nephew.color; + jeq r9, TREE_COLOR_R, remove_complex_case_6_dir_r # if distant_nephew.color == red goto remove_complex_case_6_dir_r +remove_complex_loop_dir_r_check_case_5: + # Check for red close nephew. + # --------------------------------------------------------------------- + jeq r8, NULL, remove_complex_loop_dir_r_check_case_1 # if close_nephew == null goto remove_complex_loop_dir_r_check_case_1 + ldxb r9, [r8 + TREE_NODE_COLOR_OFF] # r9 = close_nephew.color; + jeq r9, TREE_COLOR_R, remove_complex_case_5_dir_r # if close_nephew.color == red goto remove_complex_case_5_dir_r +remove_complex_loop_dir_r_check_case_1: + # Check for no parent. + # --------------------------------------------------------------------- + jeq r5, NULL, remove_complex_recycle_node # if parent == null goto remove_complex_recycle_node; + # Check for red parent. + # --------------------------------------------------------------------- + ldxb r9, [r5 + TREE_NODE_COLOR_OFF] # r9 = parent.color; + jne r9, TREE_COLOR_R, remove_complex_loop_dir_r_case_2 # if parent.color != red goto remove_complex_loop_dir_r_case_2 + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + ja remove_complex_recycle_node # goto remove_complex_recycle_node; +remove_complex_loop_dir_r_case_2: + # Fall through to case 2 at end of loop. + # --------------------------------------------------------------------- + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + mov64 r3, r5 # node = parent; + ldxdw r5, [r3 + TREE_NODE_PARENT_OFF] # parent = node.parent; + # Check if parent exists, and if so, get direction for next loop. + # --------------------------------------------------------------------- + jeq r5, NULL, remove_complex_case_5_dir_r # if parent == null goto remove_complex_case_5_dir_r + ldxdw r4, [r5 + TREE_NODE_CHILD_L_OFF] # r4 = parent.child[L] + jeq r3, r4, remove_complex_loop_dir_l # if node == parent.child[L] goto remove_complex_loop_dir_l + ja remove_complex_loop_dir_r # goto remove_complex_loop_dir_r +remove_complex_case_5_dir_r: + # rotate_subtree(tree, sibling, 1-dir) + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + stb [r8 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # close_nephew.color = black; + mov64 r7, r6 # distant_nephew = sibling; + mov64 r6, r8 # sibling = close_nephew; +remove_complex_case_6_dir_r: + # rotate_subtree(tree, parent, dir) + ldxb r4, [r5 + TREE_NODE_COLOR_OFF] # r4 = parent.color; + stxb [r6 + TREE_NODE_COLOR_OFF], r4 # sibling.color = parent.color; + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + stb [r7 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # distant_nephew.color = black; + ja remove_complex_recycle_node +remove_complex_loop_dir_r_sibling_red: + # Rebalance for red sibling case. + # --------------------------------------------------------------------- + # rotate_subtree(tree, parent, dir) + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # parent.color = red; + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # sibling.color = black; + mov64 r6, r8 # sibling = close_nephew; + ldxdw r4, [r6 + TREE_NODE_CHILD_L_OFF] # r4 = sibling.child[L]; + mov64 r7, r4 # distant_nephew = sibling.child[L]; + jeq r7, NULL, remove_complex_loop_dir_r_sibling_red_check_case_5 # if distant_nephew == null goto remove_complex_loop_dir_r_sibling_red_check_case_5 + ldxb r4, [r7 + TREE_NODE_COLOR_OFF] # r4 = distant_nephew.color; + jeq r4, TREE_COLOR_R, remove_complex_case_6_dir_r # if distant_nephew.color == red goto remove_complex_case_6_dir_r +remove_complex_loop_dir_r_sibling_red_check_case_5: + ldxdw r4, [r6 + TREE_NODE_CHILD_R_OFF] # r4 = sibling.child[R]; + mov64 r8, r4 # close_nephew = sibling.child[R]; + jeq r8, NULL, remove_complex_loop_dir_r_sibling_red_case_4 # if close_nephew == null goto remove_complex_loop_dir_r_sibling_red_case_4 + ldxb r4, [r8 + TREE_NODE_COLOR_OFF] # r4 = close_nephew.color; + jeq r4, TREE_COLOR_R, remove_complex_case_5_dir_r # if close_nephew.color == red goto remove_complex_case_5_dir_r +remove_complex_loop_dir_r_sibling_red_case_4: + stb [r6 + TREE_NODE_COLOR_OFF], TREE_COLOR_R # sibling.color = red; + stb [r5 + TREE_NODE_COLOR_OFF], TREE_COLOR_B # parent.color = black; + ja remove_complex_recycle_node + +remove_complex_loop_dir_l: + # Mirror of remove_complex_loop_r. + # Needs all the other sub labels for the same cases, but with dir reversed. + +remove_complex_recycle_node: + stdw [r2 + TREE_NODE_CHILD_L_OFF], NULL # node.child[L] = null; + stdw [r2 + TREE_NODE_CHILD_R_OFF], NULL # node.child[R] = null; + ldxdw r4, [r1 + IB_TREE_DATA_TOP_OFF] # r4 = header.top; + stxdw [r2 + TREE_NODE_PARENT_OFF], r4 # node.next = top; + stxdw [r1 + IB_TREE_DATA_TOP_OFF], r2 # header.top = node; + exit +# ANCHOR_END: remove-complex + +e_instruction_data: + mov64 r0, E_INSTRUCTION_DATA + exit + +e_instruction_data_len: + mov64 r0, E_INSTRUCTION_DATA_LEN + exit + +e_n_accounts: + mov64 r0, E_N_ACCOUNTS + exit + +e_n_accounts_insert_allocation: + mov64 r0, E_N_ACCOUNTS_INSERT_ALLOCATION + exit + +e_pda_mismatch: + mov64 r0, E_PDA_MISMATCH + exit + +e_rent_address: + mov64 r0, E_RENT_ADDRESS + exit + +e_rent_duplicate: + mov64 r0, E_RENT_DUPLICATE + exit + +e_system_program_data_len: + mov64 r0, E_SYSTEM_PROGRAM_DATA_LEN + exit + +e_system_program_duplicate: + mov64 r0, E_SYSTEM_PROGRAM_DUPLICATE + exit + +e_tree_data_len: + mov64 r0, E_TREE_DATA_LEN + exit + +e_tree_duplicate: + mov64 r0, E_TREE_DUPLICATE + exit + +e_user_data_len: + mov64 r0, E_USER_DATA_LEN + exit diff --git a/examples/utils/build-examples/src/main.rs b/examples/utils/build-examples/src/main.rs index 41b5d9be..eed62c70 100644 --- a/examples/utils/build-examples/src/main.rs +++ b/examples/utils/build-examples/src/main.rs @@ -450,6 +450,11 @@ fn run_and_save_test_snippets(path: &Path, package_name: &str) { let tests = discover_tests(&tests_path); let tests_dir = path.join("artifacts/tests"); + // Clear existing test artifacts before regenerating. + if tests_dir.exists() { + fs::remove_dir_all(&tests_dir).expect("failed to clear test artifacts directory"); + } + for (test_name, test_code) in tests { // Verify test name starts with "test_". assert!( diff --git a/examples/utils/deps/build/Cargo.toml b/examples/utils/deps/build/Cargo.toml index 955ddf9a..450be059 100644 --- a/examples/utils/deps/build/Cargo.toml +++ b/examples/utils/deps/build/Cargo.toml @@ -3,6 +3,7 @@ cargo-manifest.workspace = true pinocchio.workspace = true pinocchio-system.workspace = true regex.workspace = true +tree-interface.workspace = true [dev-dependencies] fib-rs.workspace = true diff --git a/examples/utils/deps/program/Cargo.toml b/examples/utils/deps/program/Cargo.toml index 4bda35bd..de08a813 100644 --- a/examples/utils/deps/program/Cargo.toml +++ b/examples/utils/deps/program/Cargo.toml @@ -1,6 +1,7 @@ [dependencies] pinocchio.workspace = true pinocchio-system.workspace = true +tree-interface.workspace = true [lib] crate-type = ["cdylib", "lib"] diff --git a/specs/asm-offset-safety.md b/specs/asm-offset-safety.md new file mode 100644 index 00000000..5aa4873e --- /dev/null +++ b/specs/asm-offset-safety.md @@ -0,0 +1,37 @@ +# Assembly offset safety + +## Rule: no arithmetic on offset constants in assembly + +SBF load/store instructions encode memory offsets as i16 immediates. When two or +more offset constants are added together in an assembly instruction, no +compile-time check validates that the sum fits in i16. The assembler would +eventually reject an overflow, but the error is late and unclear. + +### Prohibited + +Adding multiple offset constants in a single instruction: + +```asm +ldxdw r9, [r1 + TREE_DATA_OFF + TREE_TOP_OFF] +``` + +### Required + +Define a single constant for the combined offset so that the full +value is validated at definition time. Then use that constant +alone in the instruction: + +```asm +ldxdw r9, [r1 + IB_TREE_DATA_TOP_OFF] +``` + +### When separate offsets are fine + +Using an offset constant in an `add64` followed by a different +offset in a subsequent load/store is safe, since each immediate +is validated independently: + +```asm +add64 r6, TREE_DATA_OFF +stxdw [r6 + TREE_TOP_OFF], r4 +```