<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged datastructure</title>
<link>https://coderlegion.com/tag/datastructure</link>
<description>Powered by Question2Answer</description>
<item>
<title>What if a rope data structure used p-adic geometry?</title>
<link>https://coderlegion.com/15302/what-if-a-rope-data-structure-used-p-adic-geometry</link>
<description>&lt;p&gt;I was building a markdown editor for my platform &lt;a rel=&quot;nofollow&quot; href=&quot;https://stringtechhub.com&quot;&gt;StringTechHub&lt;/a&gt;. At some point I needed to decide how to represent the document internally. I could use a plain string, a gap buffer, or a standard rope. Instead I asked a different question: can number theory give us a better way to organize text than size-balanced trees?&lt;/p&gt;
&lt;p&gt;This post is about what that means, what I built, what I measured, and what I learned.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-standard-approaches&quot;&gt;THE PROBLEM WITH STANDARD APPROACHES&lt;/h3&gt;
&lt;p&gt;A plain string is simple but expensive. Every insert at position i requires shifting all characters after i — O(n) per edit. For a 30,000 character document that becomes noticeable.&lt;/p&gt;
&lt;p&gt;A rope solves this by splitting the document into chunks stored in a binary tree. Insert and delete become O(log n) because you only touch the path from root to the affected leaf. Editors like Zed and Helix use rope-based structures internally for this reason.&lt;/p&gt;
&lt;p&gt;Standard ropes balance by size — keep subtree weights roughly equal, keep the tree shallow. The size-balancing heuristic is correct but blind. It knows nothing about what the text means. A paragraph and a code block of identical byte length are indistinguishable to a standard rope. I wanted the tree to know the difference.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-p-adic-numbers-are&quot;&gt;WHAT P-ADIC NUMBERS ARE&lt;/h3&gt;
&lt;p&gt;The 2-adic valuation v₂(n) counts how many times 2 divides n:&lt;/p&gt;
&lt;p&gt;v₂(8) = 3, because 8 = 2³&lt;/p&gt;
&lt;p&gt;v₂(12) = 2, because 12 = 4 × 3&lt;/p&gt;
&lt;p&gt;v₂(7) = 0, because 7 is odd&lt;/p&gt;
&lt;p&gt;In the 2-adic metric, two numbers are close when their difference is highly divisible by 2. This is a different notion of proximity than the real number line — one based on arithmetic structure rather than magnitude.&lt;/p&gt;
&lt;p&gt;The question: what happens when you use this arithmetic structure to decide which nodes in a rope tree should merge together?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-idea&quot;&gt;THE CORE IDEA&lt;/h3&gt;
&lt;p&gt;In a standard rope, rebalancing collects all leaf nodes and rebuilds by splitting arrays in half — purely by position. In a p-adic rope, rebalancing collects all leaf nodes and rebuilds by greedily fusing chunks that fit well arithmetically — merging adjacent nodes whose combined size has the highest 2-adic valuation first.&lt;/p&gt;
&lt;p&gt;If two adjacent leaves have sizes 174 and 46, their combined size is 220. v₂(220) = 2. If two other adjacent leaves have sizes 171 and 16, their combined size is 187. v₂(187) = 0. The first pair merges first.&lt;/p&gt;
&lt;p&gt;The rebuild uses a max-heap ordered by merge score, so the highest valuation pairs always merge first. For p=2, the valuation is a single CPU instruction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn valuation(n: usize) -&amp;gt; usize {
    if n == 0 { return usize::MAX; }
    n.trailing_zeros() as usize
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result: a tree where subtrees reflect arithmetic affinity between block sizes — not just positional proximity. Certain subtrees become deeper or shallower depending on number-theoretic structure, not size alone.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;WHAT I BUILT&lt;/h3&gt;
&lt;p&gt;The data structure has two node types. Internal nodes are lightweight — they store weight, total size, valuation, cached depth, and two child pointers. No content, no metadata. Leaf nodes are rich — they store raw text, a parsed block from the document parser, a dirty flag, and an AST cache.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub struct InternalNode {
    pub weight: usize,
    pub total: usize,
    pub valuation: usize,
    pub depth: usize,
    pub left: Box&amp;lt;Node&amp;gt;,
    pub right: Box&amp;lt;Node&amp;gt;,
}

pub struct LeafNode {
    pub text: String,
    pub block: RefCell&amp;lt;Block&amp;gt;,
    pub dirty: Cell&amp;lt;bool&amp;gt;,
    pub ast_cache: RefCell&amp;lt;Option&amp;lt;Block&amp;gt;&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each leaf corresponds to exactly one semantic block. A heading is one leaf. A paragraph is one leaf. A code block including all indented lines is one leaf. Split operations snap to block boundaries — the rope never cuts mid-paragraph.&lt;/p&gt;
&lt;p&gt;The depth cap guarantees O(log n) worst case: max_depth = 2.0 × log₂(total). After every concat, depth is checked. If it exceeds the cap, p-adic rebuild runs. If that still violates the cap, strict size-balanced fallback runs. The guarantee always holds.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-semantic-evolution&quot;&gt;THE SEMANTIC EVOLUTION&lt;/h3&gt;
&lt;p&gt;While building I noticed something: lists and paragraphs have fundamentally different natural groupings. A paragraph is continuous prose — binary splitting fits naturally. An unordered list is a collection of discrete items — ternary grouping fits better.&lt;/p&gt;
&lt;p&gt;So I made the prime adaptive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn block_prime(node: &amp;amp;Node) -&amp;gt; usize {
    match node {
        Node::Leaf(leaf) =&amp;gt; match &amp;amp;leaf.block.borrow().kind {
            BlockKind::UnorderedList |
            BlockKind::OrderedList =&amp;gt; 3,
            _ =&amp;gt; 2,
        },
        Node::Internal(_) =&amp;gt; 2,
    }
}

fn merge_score(left: &amp;amp;Node, right: &amp;amp;Node) -&amp;gt; usize {
    let total = left.total() + right.total();
    let p = block_prime(left).max(block_prime(right));
    valuation_p(total, p)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where the idea became more interesting: the prime is no longer fixed — it depends on the content. The math adapts to document structure. This is no longer purely p-adic. It is a mathematically inspired adaptive heuristic. I think that is more interesting than mathematical purity, and more honest than pretending the original idea was perfect.&lt;/p&gt;
&lt;h3 id=&quot;the-custom-dsl&quot;&gt;THE CUSTOM DSL&lt;/h3&gt;
&lt;p&gt;The editor uses a command-based markup language. Every block starts with a dollar-sign command at column zero. Everything indented below belongs to the same block. A new block begins when a $ appears at column zero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BLOCK COMMANDS&lt;/strong&gt;&lt;br&gt;
$H1 - heading text&lt;/p&gt;
&lt;p&gt;$H2 - heading text&lt;/p&gt;
&lt;p&gt;$H3 - heading text&lt;/p&gt;
&lt;p&gt;$H4 - heading text&lt;/p&gt;
&lt;p&gt;$P - paragraph text&lt;/p&gt;
&lt;p&gt;$B - bold block&lt;/p&gt;
&lt;p&gt;$I - italic block&lt;/p&gt;
&lt;p&gt;$BI - bold italic block&lt;/p&gt;
&lt;p&gt;$ST - strikethrough block&lt;/p&gt;
&lt;p&gt;$HR&lt;/p&gt;
&lt;p&gt;$BQ quote content here -- attribution&lt;/p&gt;
&lt;p&gt;$CODE rust fn main() { println!(&quot;hello&quot;); }&lt;/p&gt;
&lt;p&gt;$UL item one item two item three&lt;/p&gt;
&lt;p&gt;$OL first item second item&lt;/p&gt;
&lt;p&gt;$IMG - alt text | &lt;a rel=&quot;nofollow&quot; href=&quot;https://url.com/image.png&quot;&gt;https://url.com/image.png&lt;/a&gt;&lt;br&gt;
$LINK - label text | &lt;a rel=&quot;nofollow&quot; href=&quot;https://url.com&quot;&gt;https://url.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;$FN - footnote content&lt;/p&gt;
&lt;p&gt;$TOC&lt;/p&gt;
&lt;p&gt;$TB | Header 1 | Header 2 | Header 3 | |----------|----------|----------| | cell | cell | cell | | cell | cell | cell |&lt;/p&gt;
&lt;p&gt;Tables are scaffolded automatically. Type $TB and press Enter — the editor generates the header row, separator row, and data rows. Tab moves between cells, Shift+Tab moves back. For custom tables, $TB COLS|ROWS then click enter (ex: $TB 3|3).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;INLINE COMMANDS&lt;/strong&gt;&lt;br&gt;
Inline formatting uses quoted commands anywhere inside block content:&lt;/p&gt;
&lt;p&gt;&quot;$I - italic text&quot;&lt;/p&gt;
&lt;p&gt;&quot;$B - bold text&quot;&lt;/p&gt;
&lt;p&gt;&quot;$BI - bold italic&quot;&lt;/p&gt;
&lt;p&gt;&quot;$ST - strikethrough&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESCAPE&lt;/strong&gt;&lt;br&gt;
To write a literal dollar sign command without it being parsed in inline:&lt;/p&gt;
&lt;p&gt;&quot;!$B&quot; - this renders as plain text, not bold&lt;/p&gt;
&lt;p&gt;To write literal quotes inside inline content:&lt;/p&gt;
&lt;p&gt;&quot;!!this has literal quotes inside&quot;&lt;/p&gt;
&lt;p&gt;The explicit $ prefix at column zero makes block boundary detection trivial and unambiguous. No context-sensitive parsing, no lookahead, no ambiguity. This is why the rope can snap splits to block boundaries cleanly — the boundary is always a $ at column zero.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-the-measurements-showed&quot;&gt;WHAT THE MEASUREMENTS SHOWED&lt;/h3&gt;
&lt;p&gt;I built a baseline using standard size-balanced rebuild and ran both on the same document.&lt;/p&gt;
&lt;p&gt;Size-balanced baseline: depth 6, total internal nodes 27&lt;/p&gt;
&lt;p&gt;valuation distribution: flat, stops at v₂=4&lt;/p&gt;
&lt;p&gt;P-adic rope after operations:&lt;/p&gt;
&lt;p&gt;depth 9-13, total internal nodes 1528 valuation distribution: geometric decay&lt;/p&gt;
&lt;p&gt;v₂=0 → 594 nodes&lt;/p&gt;
&lt;p&gt;v₂=1 → 505 nodes&lt;/p&gt;
&lt;p&gt;v₂=2 → 240 nodes&lt;/p&gt;
&lt;p&gt;v₂=3 → 114 nodes&lt;/p&gt;
&lt;p&gt;v₂=4 → 49 nodes&lt;/p&gt;
&lt;p&gt;v₂=5 → 19 nodes&lt;/p&gt;
&lt;p&gt;v₂=6 → 5 nodes&lt;/p&gt;
&lt;p&gt;v₂=7 → 2 nodes&lt;/p&gt;
&lt;p&gt;The geometric decay is not accidental. In any distribution of natural numbers, roughly half are odd (v₂=0), a quarter divisible by 2 but not 4 (v₂=1), an eighth by 4 but not 8 (v₂=2), and so on. The histogram follows this pattern exactly — the p-adic rebuild is genuinely organizing nodes by number-theoretic properties. This suggests the structure is not arbitrary — it reflects inherent arithmetic distribution.&lt;/p&gt;
&lt;p&gt;The size-balanced tree has no such structure. Its valuation distribution is flat and arbitrary.&lt;/p&gt;
&lt;p&gt;Rebalance cost: 17µs for a real document.&lt;/p&gt;
&lt;p&gt;1000 inserts: 46ms total, 0.046ms per insert.&lt;/p&gt;
&lt;p&gt;I have not yet benchmarked against a production rope implementation like ropey. That is the honest next step. Whether p-adic balancing produces faster trees than size-balanced in real editor workloads is an open question. The tree shapes are measurably different. Whether different is better in practice I cannot yet claim.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-editor&quot;&gt;THE EDITOR&lt;/h3&gt;
&lt;p&gt;The parser compiles to WASM via wasm-pack. The editor is a split-pane textarea with live preview. The rope sits between the parser and the renderer.&lt;/p&gt;
&lt;p&gt;Each leaf caches its parsed block. Dirty flags mark which leaves need re-parsing after edits. Only dirty leaves get re-parsed on each update — re-parse cost is O(block size) not O(document size). For a 30,000 character document with a 200-character paragraph being edited, only those 200 characters get re-parsed.&lt;/p&gt;
&lt;p&gt;Currently the textarea is the source of truth and the rope is rebuilt from it on each debounce. Making the rope the primary source of truth — intercepting every keystroke and updating incrementally — is the next step. The infrastructure is there. The wiring is not complete.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;honest-assessment&quot;&gt;HONEST ASSESSMENT&lt;/h3&gt;
&lt;p&gt;What works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P-adic rebuild produces measurably different tree structure from size-balanced&lt;/li&gt;
&lt;li&gt;Geometric decay in valuation histogram is real and mathematically explainable&lt;/li&gt;
&lt;li&gt;Semantic block-prime scoring connects math to document structure&lt;/li&gt;
&lt;li&gt;Dirty flag incremental parsing is implemented and correct&lt;/li&gt;
&lt;li&gt;Depth cap guarantee holds under adversarial input&lt;pre&gt;&lt;code&gt;46ms for 1000 inserts on a real document
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Compiles to WASM, runs in browser, handles 30k character documents without lag&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What does not work yet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rope is not the source of truth — full rebuild on each debounce&lt;/li&gt;
&lt;li&gt;True incremental updates not wired end to end&lt;/li&gt;
&lt;li&gt;No undo/redo&lt;/li&gt;
&lt;li&gt;No benchmark against production rope implementations&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;WHAT I LEARNED&lt;/h3&gt;
&lt;p&gt;The most interesting discovery was not the math. It was that the prime should depend on the content type. The moment I realized lists have a natural ternary structure and paragraphs have a natural binary structure — the math stopped being a curiosity and started being a design tool.&lt;/p&gt;
&lt;p&gt;The second thing: building something real with an idea is the only way to find out if the idea works. I could have written about p-adic ropes. Instead I built one and wired it into an editor. The measurements are more honest than the theory.&lt;/p&gt;
&lt;p&gt;The open question: does p-adic balancing actually improve real editor workloads, or does it just produce different trees? I do not know yet. That is what makes it worth continuing.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nitishkumar9999/simple_editor&quot;&gt;github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Early-stage prototype. Core ideas are implemented and tested; editor integration and features are still in progress.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15302/what-if-a-rope-data-structure-used-p-adic-geometry</guid>
<pubDate>Mon, 20 Apr 2026 13:59:34 +0000</pubDate>
</item>
</channel>
</rss>