Rewritten

Dejan Lekic dejan.lekic at gmail.com
Sun Jun 7 12:40:49 UTC 2026


On Sunday, 24 May 2026 at 21:50:28 UTC, mitgedanken wrote:
> Wrong link 8)
> Anyway, check out the GitHub link.

There are many issues with the code here are the most serious 
ones:

## 1. Critical: Dangling Pointers Everywhere — `Node*` Points to 
Stack Locals, Not GC Objects

This is the **fundamental design flaw** in the entire module. In 
D, `class` is a reference type.
`Node n = new Node(...)` puts `n` (a reference) on the stack and 
the object on the GC heap.
`&n` gives you a `Node*` — a pointer to the **stack-local 
reference**, not to the GC object.

```d
Tree tree = new Tree();
TreePtr treeRef = &tree;   // pointer to stack-local `tree`
Node op = new Node(treeRef, Symbol("+"), 0);
Node one = new Node(treeRef, Symbol("1"), 0, nullableNode(&op));
//                                                ^^
//                                address of stack-local `op`
```

When `op` goes out of scope, `one._root._node` is a **dangling 
pointer**. The unittests happen
to work because everything is in the same scope, but as library 
code this is
**undefined behavior**.

**The fix:** `Node` is already a reference type. `Node` 
references are already nullable
(`Node n = null`). The entire `NullableNode` struct is 
unnecessary — you could just use
`Node` directly:

```d
// Instead of NullableNode wrapping Node*:
Node _root;   // already nullable, already a reference to the GC 
object
```

---

## 2. Critical: `opBinary("~")` with `NullableNode` Returns a 
Dangling Pointer

```d
auto opBinary(string op : "~")(NullableNode rhs) {
     Node node = new Node(
         this._tree,
         this._symbol ~ rhs.symbol.str,
         this._precedence,
         this._root,
         this._left,
         this._right
     );
     return NullableNode(&node);  // ← node is LOCAL, &node 
dangles after return
}
```

`node` is a local variable. `&node` points to the stack frame. 
After the function returns,
the `NullableNode` holds a dangling pointer — immediate 
use-after-free.

---

## 3. Critical: `NullableNode.toHash` Hashes the Stack Address, 
Not Object Identity

```d
size_t toHash() const pure nothrow @safe {
     return cast(size_t)cast(void*)this._node;
}
```

This hashes the address of the *reference variable on the stack*, 
not the identity of the
GC heap object. Two `NullableNode`s referencing the same `Node` 
object through different
local variables will have different hashes:

```d
Node n1 = new Node(...);
Node n2 = n1;               // same object
auto a = nullableNode(&n1);
auto b = nullableNode(&n2);
assert(a == b);             // FALSE — different stack addresses!
```

`opEquals` also compares with `is` (pointer equality), so this 
same bug means two
references to the same object through different variables are 
considered **not equal**.

---

## 4. Serious: `opEquals` / `toHash` Contract Violation on `Node`

```d
override bool opEquals(Object other) const {
     return (n.repr() == this.repr());   // repr() includes 
children, parent
}

override size_t toHash() const nothrow @safe {
     // only hashes symbol + precedence
}
```

If you mutate a `Node`'s children after inserting it into an 
associative array,
`toHash` stays the same but `opEquals` changes, violating the AA 
invariant. The hash
and equality must agree on what constitutes identity.

---

## 5. Serious: `_setKind` Classifies a Parentless Single-Child 
Node as `Branch`

```d
if (!this.hasParent && this.hasLeft && this.hasRight)
     this._kind = Kind.Root;       // requires BOTH children
else if (this.hasLeft || this.hasRight)
     this._kind = Kind.Branch;     // any children + parent → 
branch
```

A node with no parent and only one child gets classified as 
`Branch`, not `Root`.
This is probably wrong — a node at the top of a tree with a 
single child should be
a `Root`.

---

## 6. Serious: `Tree.repr()` Only Follows One Path

```d
while (curr !is null && !curr.isLeaf) {
     if (curr.hasLeft())
         curr = curr.left.node;     // always prefers left
     else if (curr.hasRight())
         curr = curr.right.node;
}
```

This traverses only the leftmost path, ignoring entire right 
subtrees. A proper tree
representation needs to visit all nodes (pre-order, in-order, 
post-order, or level-order).

---

## 7. Serious: `TreeCrawler.opApply` Only Visits One Level

```d
int opApply(scope int delegate(ref NodePtr) dg) {
     if (auto node = this._state._current) {
         if (node.hasLeft)  { /* visit left child */ }
         if (node.hasRight) { /* visit right child */ }
     }
     return 0;
}
```

This only visits the immediate children of the current node — it 
doesn't recursively
traverse the tree. As a "crawler" it's misleading. It also 
mutates `_state._current`
during iteration, making the crawler stateful in a confusing way.



More information about the Digitalmars-d-learn mailing list