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:
+161
-30
@@ -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 "):
|
||||
|
||||
Reference in New Issue
Block a user