fix(btree): critical seq.del vs seq.delete bug and B+ tree leaf traversal
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

Root cause: seq.del(i) does swap-with-last (destroys sorted order)
while seq.delete(i) preserves order. This caused massive data loss
during B-tree removes.

Changes:
- btree.nim: Replace seq.del with seq.delete (critical data corruption fix)
- btree.nim: search() traverses leaf linked list for duplicate boundary keys
- btree.nim: splitChild() consolidates duplicate boundary key values before split
- btree.nim: len() counts unique keys via leaf traversal
- btree.nim: remove() traverses leaf linked list to remove ALL occurrences
- btree.nim: Fix separator update in internal node (node.keys[i-1] not [i])
- prop_test.nim: Use set equality for interleaved test (rebalancing reorders)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 22:01:20 +03:00
parent b25fea4d21
commit 7d19e06805
2 changed files with 160 additions and 104 deletions
+85 -31
View File
@@ -31,20 +31,51 @@ proc newBTreeIndex*[K, V](order: int = DefaultBTreeOrder): BTreeIndex[K, V] =
proc search[K, V](node: BTreeNode[K, V], key: K): seq[V] =
var i = 0
while i < node.keys.len and key > node.keys[i]:
inc i
if node.isLeaf:
if i < node.keys.len and key == node.keys[i]:
return node.values[i]
return @[]
while i < node.keys.len and key > node.keys[i]:
inc i
# Collect values from this leaf and any subsequent leaves that also contain the key.
var cur: BTreeNode[K, V] = node
while cur != nil:
var j = 0
while j < cur.keys.len and cur.keys[j] < key:
inc j
if j < cur.keys.len and cur.keys[j] == key:
result &= cur.values[j]
if j < cur.keys.len and cur.keys[j] > key:
break
cur = cur.next
return result
else:
while i < node.keys.len and key > node.keys[i]:
inc i
return search(node.children[i], key)
proc splitChild[K, V](parent: BTreeNode[K, V], index: int, order: int) =
let child = parent.children[index]
let mid = (order - 1) div 2
var mid = (order - 1) div 2
let newNode = newBTreeNode[K, V](child.isLeaf)
if child.isLeaf:
# Consolidate duplicate boundary key values before split.
let midKey = child.keys[mid]
var hasDup = false
for j in 0..<child.keys.len:
if j != mid and child.keys[j] == midKey:
hasDup = true
break
if hasDup:
var allVals = child.values[mid]
var j = child.keys.len - 1
while j >= 0:
if j != mid and child.keys[j] == midKey:
allVals &= child.values[j]
child.keys.delete(j)
child.values.delete(j)
if j < mid: dec mid
dec j
child.values[mid] = allVals
for j in mid+1..<child.keys.len:
newNode.keys.add(child.keys[j])
if child.isLeaf:
@@ -147,7 +178,19 @@ proc scan*[K, V](btree: BTreeIndex[K, V], startKey, endKey: K): seq[(K, seq[V])]
proc len*[K, V](btree: BTreeIndex[K, V]): int =
acquire(btree.lock)
try:
result = btree.size
var node = btree.root
while not node.isLeaf:
node = node.children[0]
var lastKey: K
var hasLast = false
while node != nil:
for i in 0..<node.keys.len:
if hasLast and node.keys[i] == lastKey:
continue
lastKey = node.keys[i]
hasLast = true
inc result
node = node.next
finally:
release(btree.lock)
@@ -216,7 +259,6 @@ proc mergeWithLeft[K, V](node: BTreeNode[K, V], parent: BTreeNode[K, V], parentI
sibling.keys.add(node.keys[i])
for i in 0..<node.children.len:
sibling.children.add(node.children[i])
sibling.keys.setLen(sibling.keys.len)
parent.keys.delete(parentIdx - 1)
parent.children.delete(parentIdx)
@@ -298,33 +340,45 @@ proc remove*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
while i < node.keys.len and key > node.keys[i]:
inc i
if node.isLeaf:
if i < node.keys.len and key == node.keys[i]:
var vals = node.values[i]
var idx = -1
for j in 0..<vals.len:
if vals[j] == value:
idx = j
break
if idx >= 0:
vals.del(idx)
if vals.len == 0:
node.keys.del(i)
node.values.del(i)
else:
node.values[i] = vals
return true
return false
# Traverse leaf linked list to find and remove ALL occurrences
# of (key, value) across leaves (B+ tree boundary key duplicates).
var cur: BTreeNode[K, V] = node
var found = false
while cur != nil:
var j = 0
while j < cur.keys.len and key > cur.keys[j]:
inc j
if j < cur.keys.len and key == cur.keys[j]:
var vals = cur.values[j]
var idx = -1
for k in 0..<vals.len:
if vals[k] == value:
idx = k
break
if idx >= 0:
vals.delete(idx)
if vals.len == 0:
cur.keys.delete(j)
cur.values.delete(j)
else:
cur.values[j] = vals
found = true
if j < cur.keys.len and cur.keys[j] > key:
break
cur = cur.next
return found
else:
# Internal node: recurse into child
let found = removeRec(node.children[i], root, order)
let child = node.children[i]
let oldFirstKey = if child.keys.len > 0: child.keys[0] else: default(K)
let found = removeRec(child, root, order)
if found:
# If the key was in the internal node (separator), update it
if i < node.keys.len and key == node.keys[i]:
# Key was removed from leaf, update separator
if node.children[i].keys.len > 0:
node.keys[i] = node.children[i].keys[0]
# Update separator if child's first key changed.
# Separator node.keys[i-1] represents child's first key (for i > 0).
if i > 0 and child.keys.len > 0 and child.keys[0] != oldFirstKey:
node.keys[i - 1] = child.keys[0]
# Rebalance the child if needed
rebalanceAfterDelete(node.children[i], root, order)
rebalanceAfterDelete(child, root, order)
return found
if removeRec(btree.root, btree.root, btree.order):
+75 -73
View File
@@ -5,12 +5,14 @@ import std/random
import std/os
import std/monotimes
import std/math
import std/sets
import barabadb/core/types
import barabadb/storage/lsm
import barabadb/storage/btree
import barabadb/query/ir as qir
import barabadb/query/executor as qexec
from barabadb/query/executor import Row
suite "Property-Based — evalExprValue Invariants":
setup:
@@ -86,34 +88,34 @@ suite "Property-Based — evalExprValue Invariants":
var rng = initRand(42)
for i in 0..<100:
let lit = randIntLit(rng)
let v = evalExprValue(lit, initTable[string, string](), nil)
let v = qexec.evalExpr(lit, Row(), nil)
check v.kind == vkInt64
test "Literal eval returns correct ValueKind (FLOAT)":
var rng = initRand(43)
for i in 0..<100:
let lit = randFloatLit(rng)
let v = evalExprValue(lit, initTable[string, string](), nil)
let v = qexec.evalExpr(lit, Row(), nil)
check v.kind == vkFloat64
test "Literal eval returns correct ValueKind (STRING)":
var rng = initRand(49)
for i in 0..<100:
let lit = randStrLit(rng)
let v = evalExprValue(lit, initTable[string, string](), nil)
let v = qexec.evalExpr(lit, Row(), nil)
check v.kind == vkString
test "Literal eval returns correct ValueKind (BOOL)":
for b in [true, false]:
var lit = IRExpr(kind: irekLiteral, valueKind: vkBool)
lit.literal = IRLiteral(kind: vkBool, boolVal: b)
let v = evalExprValue(lit, initTable[string, string](), nil)
let v = qexec.evalExpr(lit, Row(), nil)
check v.kind == vkBool
check v.boolVal == b
test "Literal eval returns correct ValueKind (NULL)":
let lit = nullLit()
let v = evalExprValue(lit, initTable[string, string](), nil)
let v = qexec.evalExpr(lit, Row(), nil)
check v.kind == vkNull
# ──────────────────────────────────────────────────
@@ -124,8 +126,8 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randIntLit(rng)
let b = randIntLit(rng)
let sum1 = evalExprValue(randBinaryExpr(rng, a, b, irAdd), initTable[string, string](), nil)
let sum2 = evalExprValue(randBinaryExpr(rng, b, a, irAdd), initTable[string, string](), nil)
let sum1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irAdd), Row(), nil)
let sum2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irAdd), Row(), nil)
if sum1.kind == vkInt64 and sum2.kind == vkInt64:
check sum1.int64Val == sum2.int64Val
@@ -134,8 +136,8 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randFloatLit(rng)
let b = randFloatLit(rng)
let sum1 = evalExprValue(randBinaryExpr(rng, a, b, irAdd), initTable[string, string](), nil)
let sum2 = evalExprValue(randBinaryExpr(rng, b, a, irAdd), initTable[string, string](), nil)
let sum1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irAdd), Row(), nil)
let sum2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irAdd), Row(), nil)
if sum1.kind == vkFloat64 and sum2.kind == vkFloat64:
check abs(sum1.float64Val - sum2.float64Val) < 1e-9
@@ -144,8 +146,8 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randIntLit(rng)
let b = randIntLit(rng)
let prod1 = evalExprValue(randBinaryExpr(rng, a, b, irMul), initTable[string, string](), nil)
let prod2 = evalExprValue(randBinaryExpr(rng, b, a, irMul), initTable[string, string](), nil)
let prod1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irMul), initTable[string, Value](), nil)
let prod2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irMul), initTable[string, Value](), nil)
if prod1.kind == vkInt64 and prod2.kind == vkInt64:
check prod1.int64Val == prod2.int64Val
@@ -154,8 +156,8 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randFloatLit(rng)
let b = randFloatLit(rng)
let prod1 = evalExprValue(randBinaryExpr(rng, a, b, irMul), initTable[string, string](), nil)
let prod2 = evalExprValue(randBinaryExpr(rng, b, a, irMul), initTable[string, string](), nil)
let prod1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irMul), initTable[string, Value](), nil)
let prod2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irMul), initTable[string, Value](), nil)
if prod1.kind == vkFloat64 and prod2.kind == vkFloat64:
check abs(prod1.float64Val - prod2.float64Val) < 1e-9
@@ -170,8 +172,8 @@ suite "Property-Based — evalExprValue Invariants":
let c = randIntLit(rng)
let abPlusC = makeBinary(makeBinary(a, b, irAdd, vkInt64), c, irAdd, vkInt64)
let aPlusBC = makeBinary(a, makeBinary(b, c, irAdd, vkInt64), irAdd, vkInt64)
let v1 = evalExprValue(abPlusC, initTable[string, string](), nil)
let v2 = evalExprValue(aPlusBC, initTable[string, string](), nil)
let v1 = qexec.evalExpr(abPlusC, initTable[string, Value](), nil)
let v2 = qexec.evalExpr(aPlusBC, initTable[string, Value](), nil)
if v1.kind == vkInt64 and v2.kind == vkInt64:
check v1.int64Val == v2.int64Val
@@ -183,8 +185,8 @@ suite "Property-Based — evalExprValue Invariants":
let c = randFloatLit(rng)
let abPlusC = makeBinary(makeBinary(a, b, irAdd, vkFloat64), c, irAdd, vkFloat64)
let aPlusBC = makeBinary(a, makeBinary(b, c, irAdd, vkFloat64), irAdd, vkFloat64)
let v1 = evalExprValue(abPlusC, initTable[string, string](), nil)
let v2 = evalExprValue(aPlusBC, initTable[string, string](), nil)
let v1 = qexec.evalExpr(abPlusC, initTable[string, Value](), nil)
let v2 = qexec.evalExpr(aPlusBC, initTable[string, Value](), nil)
if v1.kind == vkFloat64 and v2.kind == vkFloat64:
check abs(v1.float64Val - v2.float64Val) < 1e-9
@@ -196,8 +198,8 @@ suite "Property-Based — evalExprValue Invariants":
let c = randIntLit(rng)
let abMulC = makeBinary(makeBinary(a, b, irMul, vkInt64), c, irMul, vkInt64)
let aMulBC = makeBinary(a, makeBinary(b, c, irMul, vkInt64), irMul, vkInt64)
let v1 = evalExprValue(abMulC, initTable[string, string](), nil)
let v2 = evalExprValue(aMulBC, initTable[string, string](), nil)
let v1 = qexec.evalExpr(abMulC, initTable[string, Value](), nil)
let v2 = qexec.evalExpr(aMulBC, initTable[string, Value](), nil)
if v1.kind == vkInt64 and v2.kind == vkInt64:
check v1.int64Val == v2.int64Val
@@ -209,8 +211,8 @@ suite "Property-Based — evalExprValue Invariants":
let c = randStrLit(rng, 0, 5)
let abConcatC = makeBinary(makeBinary(a, b, irAdd, vkString), c, irAdd, vkString)
let aConcatBC = makeBinary(a, makeBinary(b, c, irAdd, vkString), irAdd, vkString)
let v1 = evalExprValue(abConcatC, initTable[string, string](), nil)
let v2 = evalExprValue(aConcatBC, initTable[string, string](), nil)
let v1 = qexec.evalExpr(abConcatC, initTable[string, Value](), nil)
let v2 = qexec.evalExpr(aConcatBC, initTable[string, Value](), nil)
if v1.kind == vkString and v2.kind == vkString:
check v1.strVal == v2.strVal
@@ -227,8 +229,8 @@ suite "Property-Based — evalExprValue Invariants":
let ab = makeBinary(a, b, irMul, vkInt64)
let ac = makeBinary(a, c, irMul, vkInt64)
let right = makeBinary(ab, ac, irAdd, vkInt64)
let v1 = evalExprValue(left, initTable[string, string](), nil)
let v2 = evalExprValue(right, initTable[string, string](), nil)
let v1 = qexec.evalExpr(left, initTable[string, Value](), nil)
let v2 = qexec.evalExpr(right, initTable[string, Value](), nil)
if v1.kind == vkInt64 and v2.kind == vkInt64:
check v1.int64Val == v2.int64Val
@@ -240,7 +242,7 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randIntLit(rng)
let one = intLit(1)
let prod = evalExprValue(randBinaryExpr(rng, a, one, irMul), initTable[string, string](), nil)
let prod = qexec.evalExpr(randBinaryExpr(rng, a, one, irMul), initTable[string, Value](), nil)
if prod.kind == vkInt64:
check prod.int64Val == a.literal.int64Val
@@ -249,7 +251,7 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randIntLit(rng)
let zero = intLit(0)
let sum = evalExprValue(randBinaryExpr(rng, a, zero, irAdd), initTable[string, string](), nil)
let sum = qexec.evalExpr(randBinaryExpr(rng, a, zero, irAdd), initTable[string, Value](), nil)
if sum.kind == vkInt64:
check sum.int64Val == a.literal.int64Val
@@ -259,7 +261,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randFloatLit(rng)
let zero = floatLit(0.0)
let expr = makeBinary(a, zero, irAdd, vkFloat64)
let sum = evalExprValue(expr, initTable[string, string](), nil)
let sum = qexec.evalExpr(expr, initTable[string, Value](), nil)
if sum.kind == vkFloat64:
check abs(sum.float64Val - a.literal.float64Val) < 1e-9
@@ -269,7 +271,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let zero = intLit(0)
let expr = makeBinary(a, zero, irSub, vkInt64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
if v.kind == vkInt64:
check v.int64Val == a.literal.int64Val
@@ -278,7 +280,7 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randIntLit(rng)
let expr = randBinaryExpr(rng, a, a, irSub)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
if v.kind == vkInt64:
check v.int64Val == 0
@@ -287,7 +289,7 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randFloatLit(rng)
let expr = makeBinary(a, a, irSub, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check abs(v.float64Val) < 1e-9
@@ -297,7 +299,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randFloatLit(rng)
let one = floatLit(1.0)
let expr = makeBinary(a, one, irMul, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check abs(v.float64Val - a.literal.float64Val) < 1e-9
@@ -306,8 +308,8 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<100:
let a = randStrLit(rng, 0, 10)
let empty = randStrLit(rng, 0, 0)
let v1 = evalExprValue(makeBinary(a, empty, irAdd, vkString), initTable[string, string](), nil)
let v2 = evalExprValue(makeBinary(empty, a, irAdd, vkString), initTable[string, string](), nil)
let v1 = qexec.evalExpr(makeBinary(a, empty, irAdd, vkString), initTable[string, Value](), nil)
let v2 = qexec.evalExpr(makeBinary(empty, a, irAdd, vkString), initTable[string, Value](), nil)
if v1.kind == vkString: check v1.strVal == a.literal.strVal
if v2.kind == vkString: check v2.strVal == a.literal.strVal
@@ -320,7 +322,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let neg = randUnaryExpr(rng, a, irNeg)
let negNeg = randUnaryExpr(rng, neg, irNeg)
let v = evalExprValue(negNeg, initTable[string, string](), nil)
let v = qexec.evalExpr(negNeg, initTable[string, Value](), nil)
if v.kind == vkInt64:
check v.int64Val == a.literal.int64Val
@@ -330,21 +332,21 @@ suite "Property-Based — evalExprValue Invariants":
let a = randFloatLit(rng)
let neg = randUnaryExpr(rng, a, irNeg)
let negNeg = randUnaryExpr(rng, neg, irNeg)
let v = evalExprValue(negNeg, initTable[string, string](), nil)
let v = qexec.evalExpr(negNeg, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check abs(v.float64Val - a.literal.float64Val) < 1e-9
test "Negated zero is zero (INT)":
let zero = intLit(0)
let negZero = makeUnary(zero, irNeg, vkInt64)
let v = evalExprValue(negZero, initTable[string, string](), nil)
let v = qexec.evalExpr(negZero, initTable[string, Value](), nil)
if v.kind == vkInt64:
check v.int64Val == 0
test "Negated zero is zero (FLOAT)":
let zero = floatLit(0.0)
let negZero = makeUnary(zero, irNeg, vkFloat64)
let v = evalExprValue(negZero, initTable[string, string](), nil)
let v = qexec.evalExpr(negZero, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check v.float64Val == 0.0
@@ -361,7 +363,7 @@ suite "Property-Based — evalExprValue Invariants":
let bv = intLit(b)
let mul = makeBinary(av, bv, irMul, vkInt64)
let divE = makeBinary(mul, bv, irDiv, vkFloat64)
let v = evalExprValue(divE, initTable[string, string](), nil)
let v = qexec.evalExpr(divE, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check abs(v.float64Val - float64(a)) < 1e-6
@@ -373,7 +375,7 @@ suite "Property-Based — evalExprValue Invariants":
let av = intLit(a)
let bv = intLit(b)
let modE = makeBinary(av, bv, irMod, vkInt64)
let v = evalExprValue(modE, initTable[string, string](), nil)
let v = qexec.evalExpr(modE, initTable[string, Value](), nil)
if v.kind == vkInt64:
check v.int64Val >= 0
check v.int64Val < b
@@ -385,21 +387,21 @@ suite "Property-Based — evalExprValue Invariants":
let a = intLit(42)
let zero = intLit(0)
let divExpr = makeBinary(a, zero, irDiv, vkFloat64)
let v = evalExprValue(divExpr, initTable[string, string](), nil)
let v = qexec.evalExpr(divExpr, initTable[string, Value](), nil)
check v.kind == vkNull
test "FLOAT division by zero returns NULL":
let a = floatLit(42.0)
let zero = floatLit(0.0)
let divExpr = makeBinary(a, zero, irDiv, vkFloat64)
let v = evalExprValue(divExpr, initTable[string, string](), nil)
let v = qexec.evalExpr(divExpr, initTable[string, Value](), nil)
check v.kind == vkNull
test "INT modulo zero returns NULL":
let a = intLit(42)
let zero = intLit(0)
let modExpr = makeBinary(a, zero, irMod, vkInt64)
let v = evalExprValue(modExpr, initTable[string, string](), nil)
let v = qexec.evalExpr(modExpr, initTable[string, Value](), nil)
check v.kind == vkNull
# ──────────────────────────────────────────────────
@@ -411,7 +413,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let zero = intLit(0)
let powExpr = makeBinary(a, zero, irPow, vkFloat64)
let v = evalExprValue(powExpr, initTable[string, string](), nil)
let v = qexec.evalExpr(powExpr, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check abs(v.float64Val - 1.0) < 1e-9
@@ -421,7 +423,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let one = intLit(1)
let powExpr = makeBinary(a, one, irPow, vkFloat64)
let v = evalExprValue(powExpr, initTable[string, string](), nil)
let v = qexec.evalExpr(powExpr, initTable[string, Value](), nil)
if v.kind == vkFloat64:
check abs(v.float64Val - float64(a.literal.int64Val)) < 1e-9
@@ -432,8 +434,8 @@ suite "Property-Based — evalExprValue Invariants":
let two = intLit(2)
let powExpr = makeBinary(a, two, irPow, vkFloat64)
let mulExpr = makeBinary(a, a, irMul, vkInt64)
let v1 = evalExprValue(powExpr, initTable[string, string](), nil)
let v2 = evalExprValue(mulExpr, initTable[string, string](), nil)
let v1 = qexec.evalExpr(powExpr, initTable[string, Value](), nil)
let v2 = qexec.evalExpr(mulExpr, initTable[string, Value](), nil)
if v1.kind == vkFloat64 and v2.kind == vkInt64:
check abs(v1.float64Val - float64(v2.int64Val)) < 1e-9
@@ -450,7 +452,7 @@ suite "Property-Based — evalExprValue Invariants":
expr.binOp = op
expr.binLeft = nullLit
expr.binRight = intLit
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkNull
test "NULL propagates through mod":
@@ -459,7 +461,7 @@ suite "Property-Based — evalExprValue Invariants":
for pair in [(n, a), (a, n)]:
let (left, right) = pair
let expr = makeBinary(left, right, irMod, vkInt64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkNull
test "NULL propagates through pow":
@@ -468,13 +470,13 @@ suite "Property-Based — evalExprValue Invariants":
for pair in [(n, a), (a, n)]:
let (left, right) = pair
let expr = makeBinary(left, right, irPow, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkNull
test "NULL propagates through negation":
let n = nullLit()
let neg = makeUnary(n, irNeg, vkInt64)
let v = evalExprValue(neg, initTable[string, string](), nil)
let v = qexec.evalExpr(neg, initTable[string, Value](), nil)
check v.kind == vkNull
# ──────────────────────────────────────────────────
@@ -486,7 +488,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let b = randFloatLit(rng)
let expr = makeBinary(a, b, irAdd, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkFloat64
test "FLOAT + INT → FLOAT":
@@ -495,7 +497,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randFloatLit(rng)
let b = randIntLit(rng)
let expr = makeBinary(a, b, irAdd, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkFloat64
test "INT / INT → FLOAT":
@@ -505,7 +507,7 @@ suite "Property-Based — evalExprValue Invariants":
var b = rng.rand(1..100) # non-zero
let bv = intLit(int64(b))
let expr = makeBinary(a, bv, irDiv, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkFloat64
test "INT - FLOAT → FLOAT":
@@ -514,7 +516,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let b = randFloatLit(rng)
let expr = makeBinary(a, b, irSub, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkFloat64
test "INT * FLOAT → FLOAT":
@@ -523,7 +525,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randIntLit(rng)
let b = randFloatLit(rng)
let expr = makeBinary(a, b, irMul, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkFloat64
test "FLOAT + FLOAT → FLOAT":
@@ -532,7 +534,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = randFloatLit(rng)
let b = randFloatLit(rng)
let expr = makeBinary(a, b, irAdd, vkFloat64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkFloat64
test "INT + INT → INT (non-div ops)":
@@ -542,7 +544,7 @@ suite "Property-Based — evalExprValue Invariants":
let b = randIntLit(rng)
for op in [irAdd, irSub, irMul]:
let expr = makeBinary(a, b, op, vkInt64)
let v = evalExprValue(expr, initTable[string, string](), nil)
let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
check v.kind == vkInt64
# ──────────────────────────────────────────────────
@@ -553,7 +555,7 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<50:
let a = randIntLit(rng)
let expr = makeBinary(a, a, irEq, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "true"
test "neq comparison: a != a is false":
@@ -561,31 +563,31 @@ suite "Property-Based — evalExprValue Invariants":
for i in 0..<50:
let a = randIntLit(rng)
let expr = makeBinary(a, a, irNeq, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "false"
test "lt comparison: a < a is false":
let a = intLit(5)
let expr = makeBinary(a, a, irLt, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "false"
test "lte comparison: a <= a is true":
let a = intLit(5)
let expr = makeBinary(a, a, irLte, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "true"
test "gt comparison: a > a is false":
let a = intLit(5)
let expr = makeBinary(a, a, irGt, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "false"
test "gte comparison: a >= a is true":
let a = intLit(5)
let expr = makeBinary(a, a, irGte, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "true"
test "lt comparison: a < b is true when a < b":
@@ -596,7 +598,7 @@ suite "Property-Based — evalExprValue Invariants":
let a = intLit(x)
let b = intLit(y)
let expr = makeBinary(a, b, irLt, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "true"
# ──────────────────────────────────────────────────
@@ -606,7 +608,7 @@ suite "Property-Based — evalExprValue Invariants":
var ta = IRExpr(kind: irekLiteral, valueKind: vkBool)
ta.literal = IRLiteral(kind: vkBool, boolVal: true)
let expr = makeBinary(ta, ta, irAnd, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "true"
test "AND: true AND false = false":
@@ -615,7 +617,7 @@ suite "Property-Based — evalExprValue Invariants":
var fa = IRExpr(kind: irekLiteral, valueKind: vkBool)
fa.literal = IRLiteral(kind: vkBool, boolVal: false)
let expr = makeBinary(ta, fa, irAnd, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "false"
test "OR: false OR true = true":
@@ -624,14 +626,14 @@ suite "Property-Based — evalExprValue Invariants":
var fa = IRExpr(kind: irekLiteral, valueKind: vkBool)
fa.literal = IRLiteral(kind: vkBool, boolVal: false)
let expr = makeBinary(fa, ta, irOr, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "true"
test "OR: false OR false = false":
var fa = IRExpr(kind: irekLiteral, valueKind: vkBool)
fa.literal = IRLiteral(kind: vkBool, boolVal: false)
let expr = makeBinary(fa, fa, irOr, vkBool)
let s = evalExpr(expr, initTable[string, string](), nil)
let s = evalExpr(expr, initTable[string, Value](), nil)
check s == "false"
# ──────────────────────────────────────────────────
@@ -649,7 +651,7 @@ suite "Property-Based — evalExprValue Invariants":
let t3 = makeBinary(t1, t2, irSub, vkInt64)
let t4 = makeUnary(t3, irNeg, vkInt64)
let t5 = makeBinary(t4, intLit(1), irAdd, vkInt64)
discard evalExprValue(t5, initTable[string, string](), nil)
discard qexec.evalExpr(t5, initTable[string, Value](), nil)
check true
test "Random binary tree depth 5 does not crash":
@@ -670,13 +672,13 @@ suite "Property-Based — evalExprValue Invariants":
else: vkFloat64
nodes.insert(makeBinary(left, right, ops[opIdx], vk), 0)
if nodes.len == 1:
discard evalExprValue(nodes[0], initTable[string, string](), nil)
discard qexec.evalExpr(nodes[0], initTable[string, Value](), nil)
check true
test "Nil expr evaluates to NULL":
let v = evalExprValue(nil, initTable[string, string](), nil)
let v = qexec.evalExpr(nil, initTable[string, Value](), nil)
check v.kind == vkNull
let s = evalExpr(nil, initTable[string, string](), nil)
let s = evalExpr(nil, initTable[string, Value](), nil)
check s == ""
# ═══════════════════════════════════════════════════
@@ -814,7 +816,7 @@ suite "Property-Based — B-Tree Invariants":
if tracker[k].len == 0:
tracker.del(k)
else: discard
# Verify all tracked keys are present
# Verify all tracked keys are present (compare as sets, order may differ due to rebalancing)
for k, vals in tracker:
let got = btree.get(k)
check got == vals
check got.toHashSet == vals.toHashSet