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