Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cmd/radFS/arttest/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"github.com/acmpesuecc/radFS/internal/art"
)

func main() {
tree := &art.Tree{}

tree.Insert([]byte("abbbbbbbc"), "first")
tree.Insert([]byte("abbbbbbbcb"), "second")
tree.Insert([]byte("abbbbbbbcbc"), "second")

art.PrintTree(tree.Root(), 0)
}
23 changes: 23 additions & 0 deletions internal/art/art.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package art

// TODO: Public API (Tree struct, Insert, Search, Delete)

type Tree struct {
root *Node
}

func (t *Tree) Root() *Node {
return t.root
}

func (t *Tree) Insert(key []byte, value string) {
t.root = insert(t.root, value, key, 0)
}

func (t *Tree) Search(key []byte) (string, bool) {
leaf := search(t.root, key, 0) // start from root and depth 0
if leaf != nil && isleaf(leaf) {
return leaf.leaf.values, true //Node->innerleaf->values
}
return "", false
}
86 changes: 86 additions & 0 deletions internal/art/insert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package art

import "fmt"

func insert(n *Node, value string, key []byte, depth int) *Node {

if n == nil {
return newleaf(value, key)
}
if isleaf(n) {
new_node := newNode4()
oldkey := n.leaf.key
i := depth
fmt.Println("depth:", depth)
fmt.Println("oldkey[depth:]:", string(oldkey[depth:]))
fmt.Println("key[depth:]:", string(key[depth:]))
Comment on lines +13 to +16

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple fmt.Println debug prints in insert (e.g., printing depth/keys/prefix). This will spam stdout for every insert and makes the package unusable as a library component. Please remove these prints or gate them behind an explicit debug logger/flag.

Copilot uses AI. Check for mistakes.

for i < len(oldkey) && i < len(key) && oldkey[i] == key[i] {
prefix_index := i - depth
if prefix_index < maxprefixlen { //index goes till 7 so prefix index<8 and not ==8
new_node.innerNode.meta.prefix[prefix_index] = key[i] // stores only the till max prefix

}

i++
}

new_node.innerNode.meta.prefixlen = i - depth // stores full prefix len even after maxprefixlen
depth = i

new_node = addchild(new_node, keycheck(key, depth), newleaf(value, key))

new_node = addchild(new_node, keycheck(oldkey, depth), n)
fmt.Println("depth:", depth)
fmt.Println("oldkey[depth:]:", string(oldkey[depth:]))
fmt.Println("key[depth:]:", string(key[depth:]))
fmt.Println(" ")
fmt.Println("prefix", string(new_node.innerNode.meta.prefix))
return new_node

}
p := checkprefix(n, key, depth)
if p != n.innerNode.meta.prefixlen {
new_node := newNode4()
new_node = addchild(new_node, keycheck(key, depth+p), newleaf(value, key))
if p < maxprefixlen {
new_node = addchild(new_node, n.innerNode.meta.prefix[p], n)

} else {
leaf := fetchleaf(n)
new_node = addchild(new_node, keycheck(leaf.leaf.key, depth+p), n) // the logic is teh leaf will contain the full key with the same prefix
}

new_node.innerNode.meta.prefixlen = p
if p < maxprefixlen {
copy(new_node.innerNode.meta.prefix, n.innerNode.meta.prefix[:p])

} else {
copy(new_node.innerNode.meta.prefix, n.innerNode.meta.prefix[:maxprefixlen])
}

oldprefixlen := n.innerNode.meta.prefixlen
n.innerNode.meta.prefixlen = n.innerNode.meta.prefixlen - (p + 1)
if len(n.innerNode.meta.prefix[p+1:oldprefixlen]) < maxprefixlen {
copy(n.innerNode.meta.prefix, n.innerNode.meta.prefix[p+1:oldprefixlen])

} else {
copy(n.innerNode.meta.prefix, n.innerNode.meta.prefix[p+1:maxprefixlen])
Comment on lines +63 to +68

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prefix-splitting code can panic when n.innerNode.meta.prefixlen > maxprefixlen: meta.prefix is only maxprefixlen bytes long, so slicing/copying using oldprefixlen (which can exceed maxprefixlen) can go out of range. Please clamp all meta.prefix[...] slice endpoints to maxprefixlen and, when the logical prefix exceeds maxprefixlen, rebuild the needed bytes from an underlying leaf key rather than from meta.prefix.

Suggested change
n.innerNode.meta.prefixlen = n.innerNode.meta.prefixlen - (p + 1)
if len(n.innerNode.meta.prefix[p+1:oldprefixlen]) < maxprefixlen {
copy(n.innerNode.meta.prefix, n.innerNode.meta.prefix[p+1:oldprefixlen])
} else {
copy(n.innerNode.meta.prefix, n.innerNode.meta.prefix[p+1:maxprefixlen])
remaining := oldprefixlen - (p + 1)
if remaining < 0 {
remaining = 0
}
n.innerNode.meta.prefixlen = remaining
if remaining > 0 {
fill := remaining
if fill > maxprefixlen {
fill = maxprefixlen
}
if oldprefixlen <= maxprefixlen {
// All logical prefix bytes are stored in meta.prefix; shift the remaining bytes down.
start := p + 1
if start > maxprefixlen {
start = maxprefixlen
}
end := start + fill
if end > maxprefixlen {
end = maxprefixlen
}
copy(n.innerNode.meta.prefix[:fill], n.innerNode.meta.prefix[start:end])
} else {
// Logical prefix exceeds maxprefixlen; rebuild remaining prefix bytes from the leaf key.
leaf := fetchleaf(n)
keyStart := depth + p + 1
keyEnd := keyStart + fill
if keyEnd > len(leaf.leaf.key) {
keyEnd = len(leaf.leaf.key)
}
if keyStart < keyEnd {
copy(n.innerNode.meta.prefix[:], leaf.leaf.key[keyStart:keyEnd])
}
}

Copilot uses AI. Check for mistakes.
}

return new_node
}

depth += n.innerNode.meta.prefixlen
next, pos := findchild(keycheck(key, depth), n)
if next != nil {
n.innerNode.children[pos] = insert(next, value, key, depth+1)
return n

} else {
n = addchild(n, keycheck(key, depth), newleaf(value, key))
return n

}

}
18 changes: 18 additions & 0 deletions internal/art/leaf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package art

// TODO: Leaf node structure for storing values

type leaf struct {
key []byte
values string
}

func newleaf(value string, key []byte) *Node {
return &Node{
leaf: &leaf{key: key, values: value},
}

}
func isleaf(n *Node) bool {
return n.leaf != nil
}
38 changes: 38 additions & 0 deletions internal/art/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package art

// TODO: Interfaces and shared node header (meta)

type NodeType int

const (
Node4 NodeType = iota
Node16
Node48
Node256
)
const (
Node4max = 4
Node16Max = 16
Node48Max = 48
Node256Max = 256

maxprefixlen = 8
)

type Node struct {
innerNode *innerNode
leaf *leaf
}

type innerNode struct {
nodeType NodeType
keys []byte
children []*Node
num_children int
meta meta
}

type meta struct {
prefix []byte
prefixlen int
}
15 changes: 15 additions & 0 deletions internal/art/node16.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package art

// TODO: Node16 implementation
func newNode16() *Node {
in := &innerNode{
nodeType: Node16,
keys: make([]byte, Node16Max),
children: make([]*Node, Node16Max),
num_children: 0,
meta: meta{
prefix: make([]byte, maxprefixlen),
},
}
return &Node{innerNode: in}
}
16 changes: 16 additions & 0 deletions internal/art/node256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package art

// TODO: Node256 implementation (direct map)

func newNode256() *Node {
in := &innerNode{
nodeType: Node256,
children: make([]*Node, Node256Max),
num_children: 0,

meta: meta{
prefix: make([]byte, maxprefixlen),
},
}
return &Node{innerNode: in}
}
15 changes: 15 additions & 0 deletions internal/art/node4.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package art

func newNode4() *Node {
in := &innerNode{
nodeType: Node4,
keys: make([]byte, Node4max),
children: make([]*Node, Node4max),
num_children: 0,
meta: meta{
prefix: make([]byte, maxprefixlen),
},
}
return &Node{innerNode: in}

}
15 changes: 15 additions & 0 deletions internal/art/node48.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package art

// TODO: Node48 implementation (indirection layer)
func newNode48() *Node {
in := &innerNode{
nodeType: Node48,
keys: make([]byte, Node256Max),
children: make([]*Node, Node48Max),
num_children: 0,
meta: meta{
prefix: make([]byte, maxprefixlen),
},
}
return &Node{innerNode: in}
}
42 changes: 42 additions & 0 deletions internal/art/print_tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package art

import "fmt"

func PrintTree(n *Node, level int) {
if n == nil {
return
}

indent := ""
for i := 0; i < level; i++ {
indent += " "
}

if isleaf(n) {
fmt.Println(indent + "Leaf: " + string(n.leaf.key))
return
}

in := n.innerNode

prefixlen := in.meta.prefixlen
prefix := ""
if prefixlen < maxprefixlen {
prefix = string(in.meta.prefix[:prefixlen])

} else {
leaf := fetchleaf(n)
prefix = string(leaf.leaf.key)
}

fmt.Println(indent+"Node(prefix=\""+prefix+"\", prefixLen=", prefixlen, ")")

// Print children
for i := 0; i < len(in.keys); i++ {
if in.children[i] != nil {
fmt.Printf("%s Edge('%c' | %d):\t", indent, in.keys[i], in.keys[i])
Comment on lines +35 to +37

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop indexes in.children[i] while iterating up to len(in.keys). For Node48, len(keys)=256 but len(children)=48, so this will panic when i >= 48 (and it also doesn’t make sense for Node256 where keys is nil). Please iterate over the appropriate child representation per node type.

Suggested change
for i := 0; i < len(in.keys); i++ {
if in.children[i] != nil {
fmt.Printf("%s Edge('%c' | %d):\t", indent, in.keys[i], in.keys[i])
for i := 0; i < len(in.children); i++ {
if in.children[i] != nil {
var edgeKey byte
if in.keys != nil && i < len(in.keys) {
edgeKey = in.keys[i]
} else {
edgeKey = byte(i)
}
fmt.Printf("%s Edge('%c' | %d):\t", indent, edgeKey, edgeKey)

Copilot uses AI. Check for mistakes.

PrintTree(in.children[i], level+1)
}
}
}
41 changes: 41 additions & 0 deletions internal/art/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package art

func search(n *Node, key []byte, depth int) *Node {
// Base case: nil node means we've reached a dead end.
// Key does not exist in this path of the tree.
if n == nil {
return nil
}

// Reached a leaf node, do a full key comparison.
// Necessary because path compression may have skipped bytes.
if isleaf(n) {
if string(n.leaf.key) == string(key) {
return n
}
return nil
Comment on lines +12 to +16

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaf key comparison converts both []byte to string, which allocates and can be expensive for large keys. Prefer bytes.Equal(n.leaf.key, key) to avoid allocations and to clearly express byte-slice equality.

Copilot uses AI. Check for mistakes.
}

// Check if the compressed prefix at this node matches the search key.
// If any byte mismatches, the entire subtree is irrelevant.
if n.innerNode.meta.prefixlen > 0 {
p := checkprefix(n, key, depth)
if p != n.innerNode.meta.prefixlen {
return nil
}
depth += n.innerNode.meta.prefixlen
}

// Get the next byte to branch on at current depth.
// Returns 0 (terminator) if key is exhausted.

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says keycheck returns 0 (terminator) if key is exhausted, but keycheck currently returns 1 when depth >= len(key). Please align the comment with the implementation, or (preferably) fix keycheck to use the intended terminator semantics.

Suggested change
// Returns 0 (terminator) if key is exhausted.
// Returns 1 (terminator) if key is exhausted.

Copilot uses AI. Check for mistakes.
k := keycheck(key, depth)

// Find the child corresponding to byte k and recurse deeper.
// Return nil if no child exists for this byte.
next, _ := findchild(k, n)
if next != nil {
return search(next, key, depth+1)
}

return nil
}
Loading
Loading