fix: 18 bugs across interpreter, reader, emitter, and runtime

- cljMinMax now handles float and mixed int/float arguments
- cljType* covers ckSet, ckTransient, ckAgent
- cljConj and cljVec return vectors instead of lists
- rem uses truncated division semantics (matches Clojure)
- range with negative step terminates correctly
- reduce of empty collection with no init errors instead of returning nil
- swap! extra args flatten correctly
- remove duplicate not definition (dead code)
- ratio literals (1/5) parse correctly
- scientific notation uses correct variable (numTok not tok)
- readSet returns proper set value instead of (set [...]) form
- TCP REPL catches specific exceptions instead of bare except
- initBuiltinMacros guards against duplicate registration
- doseq :while clause no longer generates dead code or infinite loop
- emitter resets all global state on each emitProgram call
- pushLeaf fills gaps with internal nodes instead of nil children

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 12:25:53 +03:00
parent 7cb8aa1ee0
commit 11fca52282
7 changed files with 91 additions and 42 deletions
+3 -7
View File
@@ -77,16 +77,12 @@ proc pushLeaf[T](node: PVecNode[T], index: int, shift: int, val: seq[T]): PVecNo
# Push a full leaf (seq of up to 32 values) into the tree at position 'index'
result = copyNode(node)
if shift == 0:
# Should not happen — we always push into internal nodes
result = newLeafNode[T](val)
else:
let childIdx = (index shr shift) and BRANCHING_MASK
if childIdx >= result.children.len:
# Need to grow children array
let oldLen = result.children.len
result.children.setLen(childIdx + 1)
for i in oldLen..<childIdx:
result.children[i] = nil
# Grow children array and fill any gaps with empty internal nodes
while result.children.len <= childIdx:
result.children.add(newInternalNode[T]())
var child = result.children[childIdx]
if child.isNil:
+28 -7
View File
@@ -277,7 +277,7 @@ proc cljConj*(coll: CljVal, item: CljVal): CljVal =
of ckVector:
var newItems = coll.listItems
newItems.add(item)
cljList(newItems)
cljVector(newItems)
else:
cljList(@[item])
@@ -315,11 +315,11 @@ proc cljSeq*(v: CljVal): CljVal =
else: cljNil()
proc cljVec*(v: CljVal): CljVal =
if v.isNil: return cljList(@[])
if v.isNil: return cljVector(@[])
case v.kind
of ckList: cljList(v.listItems)
of ckList: cljVector(v.listItems)
of ckVector: v
else: cljList(@[v])
else: cljVector(@[v])
proc cljEmpty*(v: CljVal): CljVal =
cljBool(cljCount(v) == 0)
@@ -430,18 +430,39 @@ proc cljType*(v: CljVal): CljVal =
of ckList: cljKeyword("list")
of ckVector: cljKeyword("vector")
of ckMap: cljKeyword("map")
of ckSet: cljKeyword("set")
of ckFn: cljKeyword("function")
of ckAtom: cljKeyword("atom")
of ckTransient: cljKeyword("transient")
of ckAgent: cljKeyword("agent")
proc cljMinMax*(args: seq[CljVal], isMin: bool): CljVal =
if args.len == 0: raise newException(CatchableError, "min/max requires at least 1 argument")
result = args[0]
for i in 1..<args.len:
if args[i].kind == ckInt and result.kind == ckInt:
let a = args[i]
if a.kind == ckInt and result.kind == ckInt:
if isMin:
if args[i].intVal < result.intVal: result = args[i]
if a.intVal < result.intVal: result = a
else:
if args[i].intVal > result.intVal: result = args[i]
if a.intVal > result.intVal: result = a
elif a.kind == ckFloat and result.kind == ckFloat:
if isMin:
if a.floatVal < result.floatVal: result = a
else:
if a.floatVal > result.floatVal: result = a
elif a.kind == ckInt and result.kind == ckFloat:
let af = a.intVal.float64
if isMin:
if af < result.floatVal: result = a
else:
if af > result.floatVal: result = a
elif a.kind == ckFloat and result.kind == ckInt:
let rf = result.intVal.float64
if isMin:
if a.floatVal < rf: result = a
else:
if a.floatVal > rf: result = a
proc cljAbs*(v: CljVal): CljVal =
case v.kind
+7
View File
@@ -2394,6 +2394,13 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
scopeStack = @[]
pushScope()
requiredImports = initHashSet[string]()
loopStack = @[]
loopResultVar = ""
nsAliases = @[]
libNsPrefixes = @[]
definedGlobals = initHashSet[string]()
definedFnArities = initTable[string, int]()
multiArityFns = initHashSet[string]()
var headerLines: seq[string] = @[
"# Generated by Bara Lang",
"import cljnim_runtime",
+16 -13
View File
@@ -725,9 +725,10 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
coll = args[2]
else:
coll = args[1]
if coll.kind in {ckList, ckVector} and coll.items.len > 0:
if coll.kind in {ckList, ckVector}:
if coll.items.len == 0:
return EvalResult(ok: false, error: "reduce of empty collection with no initial value")
acc = coll.items[0]
# reduce rest
var res = acc
for i in 1..<coll.items.len:
let callItems = @[fn, res, coll.items[i]]
@@ -920,12 +921,6 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
of ckAgent: "agent"
return EvalResult(ok: true, value: cljKeyword(typeName))
of "not":
numArgs(1)
let isFalsy = args[0].kind == ckNil or
(args[0].kind == ckBool and not args[0].boolVal)
return EvalResult(ok: true, value: cljBool(isFalsy))
of "abs":
numArgs(1)
if args[0].kind == ckInt:
@@ -953,7 +948,7 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
if args[0].kind == ckInt and args[1].kind == ckInt:
if args[1].intVal == 0:
return EvalResult(ok: false, error: "Division by zero")
return EvalResult(ok: true, value: cljInt(args[0].intVal mod args[1].intVal))
return EvalResult(ok: true, value: cljInt(args[0].intVal - (args[0].intVal div args[1].intVal) * args[1].intVal))
return EvalResult(ok: false, error: "rem requires integers")
of "min":
@@ -984,9 +979,16 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
if args.len == 3 and args[0].kind == ckInt and args[1].kind == ckInt and args[2].kind == ckInt:
var items: seq[CljVal] = @[]
var i = args[0].intVal
while i < args[1].intVal:
items.add(cljInt(i))
i += args[2].intVal
let step = args[2].intVal
let endVal = args[1].intVal
if step > 0:
while i < endVal:
items.add(cljInt(i))
i += step
elif step < 0:
while i > endVal:
items.add(cljInt(i))
i += step
return EvalResult(ok: true, value: cljList(items))
return EvalResult(ok: false, error: "range requires integers")
@@ -1132,7 +1134,8 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
let currentVal = atomRegistry[args[0].strVal]
var callItems = @[fn, currentVal]
if args.len > 2:
callItems.add(args[2..^1])
for extra in args[2..^1]:
callItems.add(extra)
let callRes = evalList(callItems, env)
if not callRes.ok: return callRes
atomRegistry[args[0].strVal] = callRes.value
+9 -7
View File
@@ -428,8 +428,7 @@ proc initMacros*() =
if whileExpr != nil:
let gs = cljSymbol(gensymName("dw_"))
b = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]),
cljList(@[cljSymbol("when"), whileExpr] & b & @[cljList(@[cljSymbol("recur"), gs])]),
cljList(@[cljSymbol("when"), cljBool(false), cljNil()])])]
cljList(@[cljSymbol("when"), whileExpr] & b & @[cljList(@[cljSymbol("recur"), gs])])])]
return cljList(@[cljSymbol("do")] & b)
# Build nested doseq for multiple pairs, innermost gets the body with modifiers
@@ -438,11 +437,10 @@ proc initMacros*() =
inner = @[cljList(@[cljSymbol("let"), cljVector(letBinds)] & inner)]
if whenExpr != nil:
inner = @[cljList(@[cljSymbol("when"), whenExpr] & inner)]
if whileExpr != nil:
let gs = cljSymbol(gensymName("dw_"))
inner = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]),
cljList(@[cljSymbol("when"), whileExpr] & inner & @[cljList(@[cljSymbol("recur"), gs])]),
cljList(@[cljSymbol("when"), cljBool(false), cljNil()])])]
if whileExpr != nil:
let gs = cljSymbol(gensymName("dw_"))
inner = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]),
cljList(@[cljSymbol("when"), whileExpr] & inner & @[cljList(@[cljSymbol("recur"), gs])])])]
proc makeLoop(name: CljVal, coll: CljVal, innerBody: seq[CljVal]): CljVal =
if name.kind == ckVector:
@@ -606,5 +604,9 @@ proc initMacros*() =
return cljList(@[cljSymbol("do")] & body)
)
var macrosInitialized = false
proc initBuiltinMacros*() =
if macrosInitialized: return
initMacros()
macrosInitialized = true
+17 -3
View File
@@ -227,6 +227,20 @@ proc readAtom(s: string, i: var int): CljVal =
except CatchableError:
return cljSymbol(tok)
# Check for ratio literal like 1/5, -1/5, 3/4
let slashPos = numTok.find('/')
if slashPos > 0 and slashPos < numTok.len - 1:
let numerStr = numTok[0..<slashPos]
let denomStr = numTok[slashPos+1..^1]
try:
let numer = parseInt(numerStr)
let denom = parseInt(denomStr)
if denom != 0:
return cljList(@[cljSymbol("/"), cljInt(numer.int64), cljInt(denom.int64)])
except CatchableError:
discard
return cljSymbol(tok)
# number
var isFloat = false
var isNumber = true
@@ -252,8 +266,8 @@ proc readAtom(s: string, i: var int): CljVal =
if j + 1 < numTok.len and (numTok[j+1] == '-' or numTok[j+1] == '+'):
discard # skip exponent sign
elif numTok[j] notin Digits:
if sawExp and (tok[j] == '-' or tok[j] == '+'):
if j > 0 and (tok[j-1] == 'e' or tok[j-1] == 'E'):
if sawExp and (numTok[j] == '-' or numTok[j] == '+'):
if j > 0 and (numTok[j-1] == 'e' or numTok[j-1] == 'E'):
continue
isNumber = false
break
@@ -366,7 +380,7 @@ proc readSet(s: string, i: var int): CljVal =
let form = readForm(s, i)
if form != nil:
items.add(form)
return cljList(@[cljSymbol("set"), cljVector(items)])
return cljSet(items)
proc readDispatch(s: string, i: var int): CljVal =
inc i # skip '#'
+8 -2
View File
@@ -528,8 +528,14 @@ proc handleClient(state: var ReplState, client: Socket) =
client.send($(%*{ "status": "error", "error": { "type": "repl/unknown-op", "message": "Unknown op: " & op } }) & "\n")
except EOFError:
break
except:
client.send($(%*{ "status": "error", "error": { "type": "repl/protocol-error", "message": "Protocol error" } }) & "\n")
except CatchableError as e:
client.send($(%*{ "status": "error", "error": { "type": "repl/protocol-error", "message": e.msg } }) & "\n")
break
except ValueError as e:
client.send($(%*{ "status": "error", "error": { "type": "repl/parse-error", "message": e.msg } }) & "\n")
break
except OSError as e:
client.send($(%*{ "status": "error", "error": { "type": "repl/io-error", "message": e.msg } }) & "\n")
break
client.close()