fix: clojure test suite compatibility — 145/225 passing (64%)

Reader: #? reader conditionals with :default, #?@ splicing, char literals,
N/M suffix and ratio parsing, comma as whitespace, number parsing fix
for negative BigInt/Decimal/Ratio literals

Emitter: mangleName trailing/double underscore fixes, scope stack for
local var tracking, symbol-as-value emits cljSymbol() not bare identifier,
macro-expanded do-unwrap in processForm, emitBlock multiline wrap,
indentCode helper, when-var-exists emitter form, variadic apply fix,
fn proc spacing fix, def indent fix

Macros: deftest, is, testing, thrown?, are, when-var-exists builtins;
rewritten and/or using gensym; improved for/doseq/dotimes; cond->
and cond->> threading fixes; threading macro insert fixes

Runtime: sorted-map, sorted-set, sorted?, array-map, object-array,
hash-map, hash-set, inf, nan; assoc extended to vectors

Test runner: #?@ splice unwrapping, removed conflicting defmacro stubs
This commit is contained in:
2026-05-09 16:14:44 +03:00
parent a9ad83c509
commit e6859568b3
5 changed files with 615 additions and 265 deletions
+161 -30
View File
@@ -7,6 +7,7 @@ var requiredImports* = initHashSet[string]()
var emitLibMode* = false
var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
var scopeStack*: seq[HashSet[string]] = @[]
proc setNsAliases*(aliases: seq[(string, string)]) =
nsAliases = aliases
@@ -42,7 +43,50 @@ proc mangleName*(name: string): string =
of '\'': result.add("_QUOTE_")
of '&': result.add("_AMP_")
of '#': result.add("_HASH")
of '%': result.add("pct")
else: result.add(c)
# Nim rejects trailing underscores
while result.len > 0 and result[^1] == '_':
result = result[0..^2]
# Nim rejects double underscores in identifiers
while result.find("__") != -1:
result = result.replace("__", "_")
# Nim 2.x only allows bare `_` as identifier starting with underscore
if result.len > 1 and result[0] == '_':
result = "x" & result
# Nim identifiers cannot start with a digit
if result.len > 0 and result[0] in {'0'..'9'}:
result = "x" & result
if result.len == 0:
result = "val"
# Nim keywords that would clash
let nimKeywords = ["in", "out", "var", "let", "proc", "func", "type", "ref", "ptr",
"object", "method", "template", "macro", "iterator", "converter",
"discard", "return", "break", "continue", "if", "else", "elif",
"when", "case", "of", "for", "while", "try", "except", "finally",
"raise", "import", "export", "from", "include", "using", "bind",
"mixin", "asm", "defer", "block", "static", "yield", "assert",
"do", "enum", "tuple", "shared", "guard", "concept", "distinct",
"interface", "lambda", "open", "quit", "result", "nil"]
if result in nimKeywords:
result = result & "1"
proc pushScope() =
scopeStack.add(initHashSet[string]())
proc popScope() =
if scopeStack.len > 0:
discard scopeStack.pop()
proc addToScope(name: string) =
if scopeStack.len > 0:
scopeStack[^1].incl(name)
proc isLocalVar(name: string): bool =
for s in scopeStack:
if name in s:
return true
return false
proc emitExpr*(v: CljVal, indent: int = 0): string
proc emitBlock*(items: seq[CljVal], indent: int, useResult: bool = false): string
@@ -51,6 +95,19 @@ proc emitQuotedForm*(v: CljVal): string
proc indentStr(indent: int): string =
" ".repeat(indent)
proc indentCode(code: string, extra: int): string =
if extra <= 0: return code
let prefix = indentStr(extra)
var result = ""
var lines = code.split("\n")
for i, line in lines:
if i > 0: result.add("\n")
if line.len > 0:
result.add(prefix & line)
else:
result.add(line)
return result
proc runtimeName(op: string): string =
case op
of "+": "cljAdd"
@@ -108,6 +165,8 @@ proc runtimeName(op: string): string =
of "contains?": "cljContains"
of "select-keys": "cljSelectKeys"
of "merge": "cljMerge"
of "hash-map": "cljHashMap"
of "hash-set": "cljHashSet"
of "identity": "cljIdentity"
of "type": "cljType"
of "abs": "cljAbs"
@@ -120,6 +179,11 @@ proc runtimeName(op: string): string =
of "reset!": "cljReset"
of "swap!": "cljSwap"
of "zipmap": "cljZipmap"
of "object-array": "cljObjectArray"
of "sorted-map": "cljSortedMap"
of "sorted-set": "cljSortedSet"
of "sorted?": "cljSortedQ"
of "array-map": "cljArrayMap"
# ---- Type predicates ----
of "nil?": "cljIsNilP"
of "some?": "cljIsSome"
@@ -143,10 +207,12 @@ proc runtimeName(op: string): string =
# ---- Keyword/Symbol ops ----
of "keyword": "cljKeywordFn"
of "symbol": "cljSymbolFn"
of "inf": "cljInf"
of "nan": "cljNaN"
of "name": "cljName"
of "namespace": "cljNamespace"
of "key": "cljKey"
of "val": "cljEntryVal"
# of "key": "cljKey"
# of "val": "cljEntryVal"
# ---- File operations ----
of "file/read": "cljFileRead"
of "file/write": "cljFileWrite"
@@ -355,7 +421,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let name = items[1]
if name.kind != ckSymbol:
raise newException(EmitterError, "def name must be a symbol")
return sp & "let " & mangleName(name.symName) & " = " & emitExpr(items[2], indent)
let mangled = mangleName(name.symName)
let valCode = emitExpr(items[2], indent)
if indent == 0:
return sp & "let " & mangled & " = " & valCode
else:
return sp & "((proc (): CljVal =\n" & indentStr(indent + 1) & "let " & mangled & " = " & valCode & "\n" & indentStr(indent + 1) & mangled & ")())"
of "defn":
if items.len < 4:
@@ -367,9 +438,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if params.kind != ckVector:
raise newException(EmitterError, "defn params must be a vector")
var paramNames: seq[string] = @[]
pushScope()
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1]
var bodyCode = ""
@@ -377,6 +450,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
popScope()
let procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
@@ -391,9 +465,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if params.kind != ckVector:
raise newException(EmitterError, "defn- params must be a vector")
var paramNames: seq[string] = @[]
pushScope()
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn- params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1]
var bodyCode = ""
@@ -401,6 +477,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
popScope()
let procName = mangleName(name.symName)
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode
@@ -411,9 +488,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if params.kind != ckVector:
raise newException(EmitterError, "fn params must be a vector")
var paramNames: seq[string] = @[]
pushScope()
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "fn params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[2..^1]
var bodyCode = ""
@@ -421,7 +500,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
return sp & "(proc(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")"
popScope()
if bodyCode.find("\n") == -1:
return sp & "(proc (" & paramNames.join(", ") & "): CljVal = " & bodyCode.strip() & ")"
else:
return sp & "(proc (" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")"
of "let":
if items.len < 3:
@@ -433,12 +516,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "let bindings must have even number of elements")
var lines: seq[string] = @[]
lines.add(sp & "block:")
pushScope()
var bi = 0
while bi < bindings.items.len:
let bname = bindings.items[bi]
let bval = bindings.items[bi+1]
if bname.kind != ckSymbol:
raise newException(EmitterError, "let binding name must be a symbol")
addToScope(bname.symName)
lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & emitExpr(bval, 0))
bi += 2
let body = items[2..^1]
@@ -454,6 +539,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
lines.add(bcode)
else:
lines.add(indentStr(indent + 1) & "discard " & stripped)
popScope()
return lines.join("\n")
of "if":
@@ -671,6 +757,23 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "throw requires exactly 1 argument")
return sp & "raise newException(CatchableError, cljStr(" & emitExpr(items[1], 0) & "))"
of "when-var-exists":
if items.len < 3:
raise newException(EmitterError, "when-var-exists requires a symbol and body")
let varSym = items[1]
if varSym.kind != ckSymbol:
raise newException(EmitterError, "when-var-exists requires a symbol as first argument")
let symName = resolveNsAlias(varSym.symName)
let exists = runtimeName(symName).len > 0 or isLocalVar(symName)
if exists:
let body = items[2..^1]
if body.len == 1:
return emitExpr(body[0], indent)
else:
return emitBlock(body, indent)
else:
return sp & "cljNil()"
of "quote":
if items.len != 2:
raise newException(EmitterError, "quote requires exactly 1 argument")
@@ -767,9 +870,18 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
return sp & "cljMapv(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "apply":
if items.len != 3:
raise newException(EmitterError, "apply requires exactly 2 arguments")
return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
if items.len < 3:
raise newException(EmitterError, "apply requires at least 2 arguments")
if items.len == 3:
return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
else:
# (apply f x y args) -> (apply (partial f x y) args)
let fnArg = emitExpr(items[1], 0)
var partialArgs: seq[string] = @[]
for i in 2..<items.len - 1:
partialArgs.add(emitExpr(items[i], 0))
let lastArg = emitExpr(items[^1], 0)
return sp & "cljApply(cljPartial(" & fnArg & ", @[" & partialArgs.join(", ") & "]), " & lastArg & ")"
of "some":
if items.len != 3:
@@ -1033,9 +1145,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=",
"println", "prn", "print", "str", "pr-str",
"concat", "min", "max", "merge", "interleave",
"zipmap"]
"zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set",
"array-map", "inf", "nan"]
var call: string
if variadic and argParts.len > 0:
if variadic:
call = rn & "(@[" & argParts.join(", ") & "])"
else:
call = rn & "(" & argParts.join(", ") & ")"
@@ -1096,16 +1209,17 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckSymbol:
let symName = resolveNsAlias(v.symName)
let rn = runtimeName(symName)
if rn.len > 0:
# Known function symbol — emit as runtime fn reference
if rn.len > 0 and not isLocalVar(symName):
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))"
return sp & mangleName(symName)
if isLocalVar(symName):
return sp & mangleName(symName)
return sp & "cljSymbol(\"" & symName & "\")"
of ckList:
if v.items.len == 0:
return sp & "cljList(@[])"
# Macro-expand before emitting
let expanded = macroexpand(v)
if expanded.kind != ckList or expanded == v:
if expanded == v:
return emitSpecialForm(v.items, indent)
return emitExpr(expanded, indent)
of ckVector:
@@ -1144,19 +1258,18 @@ proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string
var code = emitExpr(item, indent)
let stripped = code.strip()
if i < items.len - 1:
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("var ") or stripped.startsWith("let ") or
stripped.startsWith("proc ") or stripped.startsWith("result = "):
discard
let firstNl = code.find("\n")
let firstLine = if firstNl == -1: stripped else: code[0..<firstNl].strip()
let trimmed = code.strip()
let lastNl = trimmed.rfind("\n")
let lastLine = if lastNl == -1: trimmed else: trimmed[lastNl+1..^1].strip()
# Prepend discard before the first non-empty line
if firstNl == -1:
code = indentStr(indent) & "discard " & stripped
else:
# Prepend discard before the first non-empty line
let firstNl = code.find("\n")
if firstNl == -1:
code = indentStr(indent) & "discard " & stripped
else:
let firstLine = code[0..<firstNl].strip()
let rest = code[firstNl..^1]
code = indentStr(indent) & "discard " & firstLine & rest
# Wrap multi-line expression in proc so discard can apply
let indentedCode = indentCode(code, 1)
code = indentStr(indent) & "discard ((proc (): CljVal =\n" & indentedCode & "\n" & indentStr(indent) & ")())"
elif useResult:
if stripped.startsWith("discard "):
code = indentStr(indent) & "result = " & stripped[8..^1]
@@ -1181,19 +1294,37 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
var defs: seq[string] = @[]
var mainForms: seq[string] = @[]
proc unwrapAndProcess(i: int, form: CljVal, isLast: bool)
var processDepth = 0
const maxProcessDepth = 50
proc processForm(i: int, form: CljVal, isLast: bool) =
let headSym = form.kind == ckList and form.items.len > 0 and
form.items[0].kind == ckSymbol
let headName = if headSym: form.items[0].symName else: ""
if processDepth > maxProcessDepth:
raise newException(EmitterError, "Maximum processing depth exceeded - possible macro expansion loop")
inc processDepth
defer: dec processDepth
let expanded = macroexpand(form)
let ef = if expanded.kind == ckList: expanded else: form
let headSym = ef.kind == ckList and ef.items.len > 0 and
ef.items[0].kind == ckSymbol
let headName = if headSym: ef.items[0].symName else: ""
# Recurse into (do ...) forms produced by macro expansion
if headSym and headName == "do":
let subForms = ef.items[1..^1]
for j, subForm in subForms:
let subLast = isLast and (j == subForms.len - 1)
unwrapAndProcess(j, subForm, subLast)
return
let isDef = headSym and headName in ["def", "defn", "defn-"]
let isMacro = headSym and headName in ["defmacro"]
let isNs = headSym and headName == "ns"
if isNs:
discard
elif isDef or isMacro:
defs.add(emitExpr(form, 0))
defs.add(emitExpr(ef, 0))
else:
let code = emitExpr(form, 2)
let code = emitExpr(ef, 2)
let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("var "):