fix: general equality (=) for all types, add type predicates & collection fns, test suite docs

- Fix: = now uses cljEqual/cljMultiEqual instead of numeric-only cljNumEq,
  so (= :a :a), (= "a" "a"), (= [1 2] [1 2]), (= nil nil) etc return true
- Fix: ns forms inside unwrapped (do ...) are skipped instead of emitting
  broken Nim comments inside cljRepr()
- Fix: defmacro forms go to defs section, not wrapped in discard cljRepr()
- Add 18 type predicates: keyword?, symbol?, string?, number?, integer?,
  float?, vector?, map?, set?, list?, seq?, coll?, sequential?, fn?,
  boolean?, true?, false?, some?
- Add collection fns: second, ffirst, nfirst, peek, pop
- Add keyword/symbol ops: keyword, symbol, name, namespace, key, val
- Update eval interpreter = to handle structural collection equality
- Add test_single.py + test_vals.clj for Clojure test suite runner
- Add docs/en/07 and docs/bg/07 for cross-dialect test suite compatibility
- Update README with test suite documentation link
This commit is contained in:
2026-05-09 12:17:10 +03:00
parent ba0b300917
commit a9ad83c509
12 changed files with 777 additions and 83 deletions
+177 -52
View File
@@ -41,10 +41,12 @@ proc mangleName*(name: string): string =
of '.': result.add('_')
of '\'': result.add("_QUOTE_")
of '&': result.add("_AMP_")
of '#': result.add("_HASH")
else: result.add(c)
proc emitExpr*(v: CljVal, indent: int = 0): string
proc emitBlock*(items: seq[CljVal], indent: int): string
proc emitBlock*(items: seq[CljVal], indent: int, useResult: bool = false): string
proc emitQuotedForm*(v: CljVal): string
proc indentStr(indent: int): string =
" ".repeat(indent)
@@ -55,7 +57,8 @@ proc runtimeName(op: string): string =
of "-": "cljSub"
of "*": "cljMul"
of "/": "cljDiv"
of "=": "cljNumEq"
of "=": "cljMultiEqual"
of "==": "cljNumEq"
of ">": "cljGt"
of "<": "cljLt"
of ">=": "cljGe"
@@ -74,6 +77,9 @@ proc runtimeName(op: string): string =
of "even?": "cljEven"
of "odd?": "cljOdd"
of "first": "cljFirst"
of "second": "cljSecond"
of "ffirst": "cljFfirst"
of "nfirst": "cljNfirst"
of "rest": "cljRest"
of "next": "cljNext"
of "last": "cljLast"
@@ -113,6 +119,34 @@ proc runtimeName(op: string): string =
of "deref": "cljDeref"
of "reset!": "cljReset"
of "swap!": "cljSwap"
of "zipmap": "cljZipmap"
# ---- Type predicates ----
of "nil?": "cljIsNilP"
of "some?": "cljIsSome"
of "keyword?": "cljIsKeyword"
of "symbol?": "cljIsSymbol"
of "string?": "cljIsString"
of "number?": "cljIsNumber"
of "integer?": "cljIsInteger"
of "float?": "cljIsFloat"
of "vector?": "cljIsVector"
of "map?": "cljIsMap"
of "set?": "cljIsSet"
of "list?": "cljIsList"
of "seq?": "cljIsSeq"
of "coll?": "cljIsColl"
of "sequential?": "cljIsSequential"
of "fn?": "cljIsFn"
of "boolean?": "cljIsBool"
of "true?": "cljIsTrueP"
of "false?": "cljIsFalseP"
# ---- Keyword/Symbol ops ----
of "keyword": "cljKeywordFn"
of "symbol": "cljSymbolFn"
of "name": "cljName"
of "namespace": "cljNamespace"
of "key": "cljKey"
of "val": "cljEntryVal"
# ---- File operations ----
of "file/read": "cljFileRead"
of "file/write": "cljFileWrite"
@@ -126,6 +160,8 @@ proc runtimeName(op: string): string =
of "git/diff": "cljGitDiff"
of "git/log": "cljGitLog"
of "disj": "cljDisj"
of "peek": "cljPeek"
of "pop": "cljPop"
of "slurp": "cljFileRead"
of "spit": "cljFileWrite"
of "read-line": "cljReadLine"
@@ -244,6 +280,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
# quote
if hName == "quote" and form.items.len == 2:
return form.items[1]
# syntax-quote
if hName == "syntax-quote" and form.items.len == 2:
return eval(expandSyntaxQuote(form.items[1]))
# list
if hName == "list":
var evaluated: seq[CljVal] = @[]
@@ -316,7 +355,7 @@ 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], 0)
return sp & "let " & mangleName(name.symName) & " = " & emitExpr(items[2], indent)
of "defn":
if items.len < 4:
@@ -338,9 +377,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
# In a proc body, replace "discard " with "result = " so that
# if/when branches return their last expression properly.
bodyCode = bodyCode.replace("discard ", "result = ")
let procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
@@ -425,37 +461,23 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "if requires condition, then, and optional else")
let condCode = emitExpr(items[1], 0)
let thenCode = emitExpr(items[2], indent + 1)
let thenStripped = thenCode.strip()
var thenFinal = thenCode
if not (thenStripped.startsWith("echo ") or thenStripped.startsWith("discard ") or
thenStripped.startsWith("block:") or thenStripped.startsWith("if ") or
thenStripped.startsWith("try:") or thenStripped.startsWith("var ") or
thenStripped.startsWith("let ") or thenStripped.startsWith("proc ") or
thenStripped.contains("\n") or thenStripped.contains(" = ")):
thenFinal = indentStr(indent + 1) & "discard " & thenStripped
var ifBlock = sp & "if cljIsTruthy(" & condCode & "):\n" & thenFinal
var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode
if items.len == 4:
let elseCode = emitExpr(items[3], indent + 1)
let elseStripped = elseCode.strip()
var elseFinal = elseCode
if not (elseStripped.startsWith("echo ") or elseStripped.startsWith("discard ") or
elseStripped.startsWith("block:") or elseStripped.startsWith("if ") or
elseStripped.startsWith("try:") or elseStripped.startsWith("var ") or
elseStripped.startsWith("let ") or elseStripped.startsWith("proc ") or
elseStripped.contains("\n") or elseStripped.contains(" = ")):
elseFinal = indentStr(indent + 1) & "discard " & elseStripped
ifBlock.add("\n" & sp & "else:\n" & elseFinal)
ifBlock.add("\n" & sp & "else:\n" & elseCode)
return ifBlock
of "when":
if items.len < 3:
raise newException(EmitterError, "when requires condition and body")
let condCode = emitExpr(items[1], 0)
let condCode = emitExpr(items[1], indent)
var lines: seq[string] = @[]
lines.add(sp & "if cljIsTruthy(" & condCode & "):")
lines.add(sp & "if cljIsTruthy(" & condCode.strip() & "):")
let body = items[2..^1]
for b in body:
lines.add(emitExpr(b, indent + 1))
lines.add(sp & "else:")
lines.add(indentStr(indent + 1) & "cljNil()")
return lines.join("\n")
of "when-let":
@@ -602,14 +624,42 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
inc ti
var lines: seq[string] = @[]
lines.add(sp & "try:")
for b in bodyForms:
lines.add(emitExpr(b, indent + 1))
for i, b in bodyForms:
let isLast = (i == bodyForms.len - 1)
let code = emitExpr(b, indent + 1)
if isLast:
lines.add(code)
else:
let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("block:") or stripped.startsWith("if ") or
stripped.startsWith("try:") or stripped.startsWith("var ") or
stripped.startsWith("let ") or stripped.startsWith("proc ") or
stripped.startsWith("result = ") or stripped.contains("\n") or
stripped.contains(" = "):
lines.add(code)
else:
lines.add(indentStr(indent + 1) & "discard " & stripped)
for (exType, exVar, exBody) in catchClauses:
lines.add(sp & "except " & exType & ":")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljMap(@[cljKeyword(\"message\")], @[cljString(getCurrentExceptionMsg())])")
lines.add(indentStr(indent + 1) & "discard cljAssoc(" & exVar & ", cljKeyword(\"type\"), cljString(\"" & exType & "\"))")
for eb in exBody:
lines.add(emitExpr(eb, indent + 1))
for i, eb in exBody:
let isLast = (i == exBody.len - 1)
let code = emitExpr(eb, indent + 1)
if isLast:
lines.add(code)
else:
let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("block:") or stripped.startsWith("if ") or
stripped.startsWith("try:") or stripped.startsWith("var ") or
stripped.startsWith("let ") or stripped.startsWith("proc ") or
stripped.startsWith("result = ") or stripped.contains("\n") or
stripped.contains(" = "):
lines.add(code)
else:
lines.add(indentStr(indent + 1) & "discard " & stripped)
if finallyBody.len > 0:
lines.add(sp & "finally:")
for fb in finallyBody:
@@ -631,12 +681,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
of ckList:
var parts: seq[string] = @[]
for item in quoted.items:
parts.add(emitExpr(item, 0))
parts.add(emitQuotedForm(item))
return sp & "cljList(@[" & parts.join(", ") & "])"
of ckVector:
var parts: seq[string] = @[]
for item in quoted.items:
parts.add(emitExpr(item, 0))
parts.add(emitQuotedForm(item))
return sp & "cljVector(@[" & parts.join(", ") & "])"
else:
return emitExpr(quoted, indent)
@@ -978,26 +1028,56 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if rn.len > 0:
var argParts: seq[string] = @[]
for i in 1..<items.len:
argParts.add(emitExpr(items[i], 0))
argParts.add(emitExpr(items[i], indent))
# Variadic functions take seq[CljVal]
let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=",
"println", "prn", "print", "str", "pr-str",
"concat", "min", "max", "merge", "interleave"]
"concat", "min", "max", "merge", "interleave",
"zipmap"]
var call: string
if variadic and argParts.len > 0:
call = rn & "(@[" & argParts.join(", ") & "])"
else:
call = rn & "(" & argParts.join(", ") & ")"
if op in ["println", "prn", "print"]:
return sp & "discard " & call
return sp & call
# ---- User-defined function call ----
var args: seq[string] = @[]
for i in 1..<items.len:
args.add(emitExpr(items[i], 0))
args.add(emitExpr(items[i], indent))
return sp & mangleName(op) & "(" & args.join(", ") & ")"
proc emitQuotedForm*(v: CljVal): string =
case v.kind
of ckSymbol:
return "cljSymbol(\"" & v.symName & "\")"
of ckKeyword:
return "cljKeyword(\"" & v.kwName & "\")"
of ckList:
var parts: seq[string] = @[]
for item in v.items:
parts.add(emitQuotedForm(item))
return "cljList(@[" & parts.join(", ") & "])"
of ckVector:
var parts: seq[string] = @[]
for item in v.items:
parts.add(emitQuotedForm(item))
return "cljVector(@[" & parts.join(", ") & "])"
of ckMap:
var keyParts: seq[string] = @[]
var valParts: seq[string] = @[]
for i in 0..<v.mapKeys.len:
keyParts.add(emitQuotedForm(v.mapKeys[i]))
valParts.add(emitQuotedForm(v.mapVals[i]))
return "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems:
parts.add(emitQuotedForm(item))
return "cljSet(@[" & parts.join(", ") & "])"
else:
return emitExpr(v, 0)
proc emitExpr*(v: CljVal, indent: int = 0): string =
let sp = indentStr(indent)
case v.kind
@@ -1031,7 +1111,7 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckVector:
var parts: seq[string] = @[]
for item in v.items:
parts.add(emitExpr(item, 0))
parts.add(emitExpr(item, indent))
return sp & "cljVector(@[" & parts.join(", ") & "])"
of ckMap:
if v.mapKeys.len == 0:
@@ -1039,13 +1119,13 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
var keyParts: seq[string] = @[]
var valParts: seq[string] = @[]
for i in 0..<v.mapKeys.len:
keyParts.add(emitExpr(v.mapKeys[i], 0))
valParts.add(emitExpr(v.mapVals[i], 0))
keyParts.add(emitExpr(v.mapKeys[i], indent))
valParts.add(emitExpr(v.mapVals[i], indent))
return sp & "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems:
parts.add(emitExpr(item, 0))
parts.add(emitExpr(item, indent))
return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckFn:
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = discard cljNil())"
@@ -1056,12 +1136,39 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckAgent:
return sp & "cljAgent(" & emitExpr(v.agentVal, 0) & ")"
proc emitBlock(items: seq[CljVal], indent: int): string =
proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string =
if items.len == 0:
return indentStr(indent) & "discard cljNil()"
var lines: seq[string] = @[]
for item in items:
lines.add(emitExpr(item, indent))
for i, item in items:
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
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
elif useResult:
if stripped.startsWith("discard "):
code = indentStr(indent) & "result = " & stripped[8..^1]
elif not stripped.startsWith("result = "):
let firstNl = code.find("\n")
if firstNl == -1:
code = indentStr(indent) & "result = " & stripped
else:
let firstLine = code[0..<firstNl].strip()
let rest = code[firstNl..^1]
code = indentStr(indent) & "result = " & firstLine & rest
lines.add(code)
return lines.join("\n")
proc emitProgramInternal(forms: seq[CljVal]): string =
@@ -1074,24 +1181,42 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
var defs: seq[string] = @[]
var mainForms: seq[string] = @[]
for i, form in forms:
let isDef = form.kind == ckList and form.items.len > 0 and
form.items[0].kind == ckSymbol and
form.items[0].symName in ["def", "defn", "defn-"]
if isDef:
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: ""
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))
else:
let code = emitExpr(form, 2)
let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("block:") or stripped.startsWith("if ") or
stripped.startsWith("try:") or stripped.startsWith("var "):
stripped.startsWith("var "):
mainForms.add(code)
elif i == forms.len - 1:
elif isLast:
mainForms.add(indentStr(2) & "echo cljRepr(" & stripped & ")")
else:
mainForms.add(indentStr(2) & "discard cljRepr(" & stripped & ")")
proc unwrapAndProcess(i: int, form: CljVal, isLast: bool) =
# Unwrap top-level (do ...) forms so each sub-form is processed individually
if form.kind == ckList and form.items.len > 0 and
form.items[0].kind == ckSymbol and form.items[0].symName == "do":
let subForms = form.items[1..^1]
for j, subForm in subForms:
let subLast = isLast and (j == subForms.len - 1)
unwrapAndProcess(j, subForm, subLast)
else:
processForm(i, form, isLast)
for i, form in forms:
unwrapAndProcess(i, form, i == forms.len - 1)
var lines = headerLines
# Add collected Nim imports
for imp in requiredImports: