TreeHaver Unified API - Implementation Summary
Problem Identified
TreeHaver was passing through backend-specific objects instead of providing a unified API:
- MRI backend returned
::TreeSitter::Nodedirectly - Rust backend returned
TreeStump::Nodedirectly - FFI backend returned
FFI::Nodedirectly - Java backend returned
Java::Nodedirectly
This violated TreeHaver’s core promise: “write once, run anywhere”
Solution Implemented
Created unified wrapper classes that all backends now return:
1. TreeHaver::Node (new file)
- Wraps backend-specific node objects
- Provides consistent API across all backends
- Maps backend differences:
-
node.type(ruby_tree_sitter) → unifiedtype -
node.kind(tree_stump) → unifiedtype -
node.start_point→ unifiedstart_point(returns hash) -
node.start_position→ unifiedstart_point(returns hash) -
node.is_named?(tree_stump) → unifiednamed? -
node.named?(ruby_tree_sitter) → unifiednamed?
-
2. TreeHaver::Tree (new file)
- Wraps backend-specific tree objects
- Returns wrapped
TreeHaver::Nodefromroot_node - Stores source text for text extraction
3. Updated All Backends
MRI Backend (backends/mri.rb):
def parse(source)
tree = @parser.parse(source)
TreeHaver::Tree.new(tree, source: source) # Now wraps!
end
Rust Backend (backends/rust.rb):
def parse(source)
tree = @parser.parse(source)
TreeHaver::Tree.new(tree, source: source) # Now wraps!
end
FFI Backend (backends/ffi.rb):
def parse(source)
tree_ptr = Native.ts_parser_parse_string(@parser, ::FFI::Pointer::NULL, src, src.bytesize)
inner_tree = Tree.new(tree_ptr)
TreeHaver::Tree.new(inner_tree, source: src) # Now wraps!
end
Java Backend (backends/java.rb):
def parse(source)
java_tree = @parser.parse(source)
inner_tree = Tree.new(java_tree)
TreeHaver::Tree.new(inner_tree, source: source) # Now wraps!
end
API Provided by TreeHaver::Node
# Type information
node.type # String - works across all backends
# Position (bytes)
node.start_byte # Integer
node.end_byte # Integer
# Position (row/column)
node.start_point # { row: Integer, column: Integer }
node.end_point # { row: Integer, column: Integer }
# Text
node.text # String - extracted from source
# Hierarchy
node.child_count # Integer
node.child(index) # TreeHaver::Node
node.children # Array<TreeHaver::Node>
node.named_children # Array<TreeHaver::Node>
node.parent # TreeHaver::Node | nil
node.next_sibling # TreeHaver::Node | nil
node.prev_sibling # TreeHaver::Node | nil
# Fields (tree-sitter feature)
node.field(name) # TreeHaver::Node | nil
# Status
node.has_error? # Boolean
node.missing? # Boolean
node.named? # Boolean
# Iteration
node.each { |child| ... } # Yields TreeHaver::Node
Benefits
1. True Portability
Code using TreeHaver now works identically across:
- MRI (ruby_tree_sitter)
- MRI (tree_stump/Rust)
- JRuby (FFI)
- JRuby (java-tree-sitter)
- TruffleRuby (FFI)
2. No Backend-Specific Code
Applications like toml-merge don’t need to know which backend is used:
# This works with ANY backend!
parser = TreeHaver::Parser.new
parser.language = TreeHaver::Language.toml
tree = parser.parse(source)
node = tree.root_node
puts node.type # Works!
puts node.start_byte # Works!
node.children.each { |child| puts child.type } # Works!
3. Consistent Testing
Test suites can test once and know it works everywhere:
# Same test works for all backends
RSpec.shared_examples "tree-sitter backend" do
it "parses TOML correctly" do
tree = parser.parse("[section]\nkey = 'value'")
root = tree.root_node
expect(root.type).to eq("document")
expect(root.children.size).to be > 0
end
end
4. Future-Proof
New backends can be added without breaking existing code:
- Just implement backend-specific internals
- Return TreeHaver::Tree from parse()
- Everything else works automatically!
Files Modified
- ✅
tree_haver/lib/tree_haver.rb- Added Node and Tree autoloads - ✅
tree_haver/lib/tree_haver/node.rb- NEW: Unified Node wrapper - ✅
tree_haver/lib/tree_haver/tree.rb- NEW: Unified Tree wrapper - ✅
tree_haver/lib/tree_haver/backends/mri.rb- Returns wrapped Tree - ✅
tree_haver/lib/tree_haver/backends/rust.rb- Returns wrapped Tree - ✅
tree_haver/lib/tree_haver/backends/ffi.rb- Returns wrapped Tree - ✅
tree_haver/lib/tree_haver/backends/java.rb- Returns wrapped Tree
Testing Status
Running tests to verify the fix works with TreeStump backend…
Next Steps
- ✅ DONE: Fix TreeHaver to provide unified API
- TODO: Verify tests pass with TreeStump backend
- TODO: Complete toml-merge refactor to use TreeHaver
- TODO: Add Citrus backend to TreeHaver (Phase 2)
Impact on toml-merge
toml-merge now gets TreeHaver::Node objects which have a consistent API:
- No more
TreeStump::NodevsTreeSitter::Nodedifferences -
node.typeworks consistently (notnode.kindsometimes) - All position methods work the same way
- Iteration works the same way
The test failure should now be fixed because TreeHaver::Node has a type method that works with all backends!