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
+58 -3
View File
@@ -1,6 +1,6 @@
import cljnim_pvec import cljnim_pvec
import cljnim_pmap import cljnim_pmap
import strutils, sequtils, hashes, algorithm, os, osproc, locks import strutils, sequtils, hashes, algorithm, os, osproc, locks, math
type type
CljKind* = enum CljKind* = enum
@@ -79,6 +79,8 @@ proc cljBool*(v: bool): CljVal = CljVal(kind: ckBool, boolVal: v)
proc cljInt*(v: int64): CljVal = CljVal(kind: ckInt, intVal: v) proc cljInt*(v: int64): CljVal = CljVal(kind: ckInt, intVal: v)
proc cljInt*(v: int): CljVal = CljVal(kind: ckInt, intVal: v.int64) proc cljInt*(v: int): CljVal = CljVal(kind: ckInt, intVal: v.int64)
proc cljFloat*(v: float64): CljVal = CljVal(kind: ckFloat, floatVal: v) proc cljFloat*(v: float64): CljVal = CljVal(kind: ckFloat, floatVal: v)
proc cljInf*(args: seq[CljVal]): CljVal = CljVal(kind: ckFloat, floatVal: Inf)
proc cljNaN*(args: seq[CljVal]): CljVal = CljVal(kind: ckFloat, floatVal: NaN)
proc cljString*(v: string): CljVal = CljVal(kind: ckString, strVal: v) proc cljString*(v: string): CljVal = CljVal(kind: ckString, strVal: v)
proc cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: v) proc cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: v)
proc cljSymbol*(v: string): CljVal = CljVal(kind: ckSymbol, symName: v) proc cljSymbol*(v: string): CljVal = CljVal(kind: ckSymbol, symName: v)
@@ -104,6 +106,20 @@ proc cljSet*(items: seq[CljVal]): CljVal =
s = pmapAssoc(s, item, true, hash(item), cljEq) s = pmapAssoc(s, item, true, hash(item), cljEq)
CljVal(kind: ckSet, setData: s) CljVal(kind: ckSet, setData: s)
proc cljHashMap*(args: seq[CljVal]): CljVal =
if args.len mod 2 != 0:
raise newException(CatchableError, "hash-map requires even number of arguments")
var m = newPersistentMap[CljVal, CljVal]()
for i in countup(0, args.len - 1, 2):
m = pmapAssoc(m, args[i], args[i+1], hash(args[i]), cljEq)
CljVal(kind: ckMap, mapData: m)
proc cljHashSet*(args: seq[CljVal]): CljVal =
var s = newPersistentMap[CljVal, bool]()
for item in args:
s = pmapAssoc(s, item, true, hash(item), cljEq)
CljVal(kind: ckSet, setData: s)
# ---- Display ---- # ---- Display ----
proc cljRepr*(v: CljVal): string = proc cljRepr*(v: CljVal): string =
@@ -686,6 +702,30 @@ proc cljVec*(v: CljVal): CljVal =
of ckVector: v of ckVector: v
else: cljVector(@[v]) else: cljVector(@[v])
proc cljObjectArray*(n: CljVal): CljVal =
var size = 0
if not n.isNil and n.kind == ckInt:
size = n.intVal.int
var items: seq[CljVal] = @[]
for i in 0..<size:
items.add(cljNil())
return cljList(items)
proc cljSortedMap*(args: seq[CljVal]): CljVal =
# Fallback to regular hash-map
cljHashMap(args)
proc cljSortedSet*(args: seq[CljVal]): CljVal =
# Fallback to regular hash-set
cljHashSet(args)
proc cljArrayMap*(args: seq[CljVal]): CljVal =
# Fallback to regular hash-map
cljHashMap(args)
proc cljSortedQ*(v: CljVal): CljVal =
cljBool(false)
proc cljEmpty*(v: CljVal): CljVal = proc cljEmpty*(v: CljVal): CljVal =
cljBool(cljCount(v).intVal == 0) cljBool(cljCount(v).intVal == 0)
@@ -852,9 +892,24 @@ proc cljGetDefault*(m: CljVal, key: CljVal, default: CljVal): CljVal =
else: default else: default
proc cljAssoc*(m: CljVal, key: CljVal, val: CljVal): CljVal = proc cljAssoc*(m: CljVal, key: CljVal, val: CljVal): CljVal =
if m.isNil or m.kind != ckMap: if m.isNil or m.kind == ckNil:
return CljVal(kind: ckMap, mapData: pmapAssoc(newPersistentMap[CljVal, CljVal](), key, val, hash(key), cljEq)) return CljVal(kind: ckMap, mapData: pmapAssoc(newPersistentMap[CljVal, CljVal](), key, val, hash(key), cljEq))
CljVal(kind: ckMap, mapData: pmapAssoc(m.mapData, key, val, hash(key), cljEq)) case m.kind
of ckMap:
CljVal(kind: ckMap, mapData: pmapAssoc(m.mapData, key, val, hash(key), cljEq))
of ckVector:
if key.kind != ckInt:
raise newException(CatchableError, "assoc on vector requires integer index")
let idx = key.intVal
let cnt = m.vecData.count
if idx < 0 or idx > cnt:
raise newException(CatchableError, "Index out of bounds: " & $idx)
if idx == cnt:
CljVal(kind: ckVector, vecData: pvecConj(m.vecData, val))
else:
CljVal(kind: ckVector, vecData: pvecAssoc(m.vecData, idx, val))
else:
raise newException(CatchableError, "assoc expects a map or vector")
proc cljDissoc*(m: CljVal, key: CljVal): CljVal = proc cljDissoc*(m: CljVal, key: CljVal): CljVal =
if m.isNil or m.kind != ckMap: return cljMap(@[], @[]) if m.isNil or m.kind != ckMap: return cljMap(@[], @[])
+161 -30
View File
@@ -7,6 +7,7 @@ var requiredImports* = initHashSet[string]()
var emitLibMode* = false var emitLibMode* = false
var loopStack*: seq[seq[string]] = @[] var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace) var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
var scopeStack*: seq[HashSet[string]] = @[]
proc setNsAliases*(aliases: seq[(string, string)]) = proc setNsAliases*(aliases: seq[(string, string)]) =
nsAliases = aliases nsAliases = aliases
@@ -42,7 +43,50 @@ proc mangleName*(name: string): string =
of '\'': result.add("_QUOTE_") of '\'': result.add("_QUOTE_")
of '&': result.add("_AMP_") of '&': result.add("_AMP_")
of '#': result.add("_HASH") of '#': result.add("_HASH")
of '%': result.add("pct")
else: result.add(c) 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 emitExpr*(v: CljVal, indent: int = 0): string
proc emitBlock*(items: seq[CljVal], indent: int, useResult: bool = false): 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 = proc indentStr(indent: int): string =
" ".repeat(indent) " ".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 = proc runtimeName(op: string): string =
case op case op
of "+": "cljAdd" of "+": "cljAdd"
@@ -108,6 +165,8 @@ proc runtimeName(op: string): string =
of "contains?": "cljContains" of "contains?": "cljContains"
of "select-keys": "cljSelectKeys" of "select-keys": "cljSelectKeys"
of "merge": "cljMerge" of "merge": "cljMerge"
of "hash-map": "cljHashMap"
of "hash-set": "cljHashSet"
of "identity": "cljIdentity" of "identity": "cljIdentity"
of "type": "cljType" of "type": "cljType"
of "abs": "cljAbs" of "abs": "cljAbs"
@@ -120,6 +179,11 @@ proc runtimeName(op: string): string =
of "reset!": "cljReset" of "reset!": "cljReset"
of "swap!": "cljSwap" of "swap!": "cljSwap"
of "zipmap": "cljZipmap" of "zipmap": "cljZipmap"
of "object-array": "cljObjectArray"
of "sorted-map": "cljSortedMap"
of "sorted-set": "cljSortedSet"
of "sorted?": "cljSortedQ"
of "array-map": "cljArrayMap"
# ---- Type predicates ---- # ---- Type predicates ----
of "nil?": "cljIsNilP" of "nil?": "cljIsNilP"
of "some?": "cljIsSome" of "some?": "cljIsSome"
@@ -143,10 +207,12 @@ proc runtimeName(op: string): string =
# ---- Keyword/Symbol ops ---- # ---- Keyword/Symbol ops ----
of "keyword": "cljKeywordFn" of "keyword": "cljKeywordFn"
of "symbol": "cljSymbolFn" of "symbol": "cljSymbolFn"
of "inf": "cljInf"
of "nan": "cljNaN"
of "name": "cljName" of "name": "cljName"
of "namespace": "cljNamespace" of "namespace": "cljNamespace"
of "key": "cljKey" # of "key": "cljKey"
of "val": "cljEntryVal" # of "val": "cljEntryVal"
# ---- File operations ---- # ---- File operations ----
of "file/read": "cljFileRead" of "file/read": "cljFileRead"
of "file/write": "cljFileWrite" of "file/write": "cljFileWrite"
@@ -355,7 +421,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let name = items[1] let name = items[1]
if name.kind != ckSymbol: if name.kind != ckSymbol:
raise newException(EmitterError, "def name must be a symbol") 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": of "defn":
if items.len < 4: if items.len < 4:
@@ -367,9 +438,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if params.kind != ckVector: if params.kind != ckVector:
raise newException(EmitterError, "defn params must be a vector") raise newException(EmitterError, "defn params must be a vector")
var paramNames: seq[string] = @[] var paramNames: seq[string] = @[]
pushScope()
for p in params.items: for p in params.items:
if p.kind != ckSymbol: if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols") raise newException(EmitterError, "defn params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal") paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1] let body = items[3..^1]
var bodyCode = "" var bodyCode = ""
@@ -377,6 +450,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1) bodyCode = emitExpr(body[0], indent + 1)
else: else:
bodyCode = emitBlock(body, indent + 1) bodyCode = emitBlock(body, indent + 1)
popScope()
let procName = mangleName(name.symName) let procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: "" let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode 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: if params.kind != ckVector:
raise newException(EmitterError, "defn- params must be a vector") raise newException(EmitterError, "defn- params must be a vector")
var paramNames: seq[string] = @[] var paramNames: seq[string] = @[]
pushScope()
for p in params.items: for p in params.items:
if p.kind != ckSymbol: if p.kind != ckSymbol:
raise newException(EmitterError, "defn- params must be symbols") raise newException(EmitterError, "defn- params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal") paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1] let body = items[3..^1]
var bodyCode = "" var bodyCode = ""
@@ -401,6 +477,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1) bodyCode = emitExpr(body[0], indent + 1)
else: else:
bodyCode = emitBlock(body, indent + 1) bodyCode = emitBlock(body, indent + 1)
popScope()
let procName = mangleName(name.symName) let procName = mangleName(name.symName)
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode 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: if params.kind != ckVector:
raise newException(EmitterError, "fn params must be a vector") raise newException(EmitterError, "fn params must be a vector")
var paramNames: seq[string] = @[] var paramNames: seq[string] = @[]
pushScope()
for p in params.items: for p in params.items:
if p.kind != ckSymbol: if p.kind != ckSymbol:
raise newException(EmitterError, "fn params must be symbols") raise newException(EmitterError, "fn params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal") paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[2..^1] let body = items[2..^1]
var bodyCode = "" var bodyCode = ""
@@ -421,7 +500,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1) bodyCode = emitExpr(body[0], indent + 1)
else: else:
bodyCode = emitBlock(body, indent + 1) 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": of "let":
if items.len < 3: 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") raise newException(EmitterError, "let bindings must have even number of elements")
var lines: seq[string] = @[] var lines: seq[string] = @[]
lines.add(sp & "block:") lines.add(sp & "block:")
pushScope()
var bi = 0 var bi = 0
while bi < bindings.items.len: while bi < bindings.items.len:
let bname = bindings.items[bi] let bname = bindings.items[bi]
let bval = bindings.items[bi+1] let bval = bindings.items[bi+1]
if bname.kind != ckSymbol: if bname.kind != ckSymbol:
raise newException(EmitterError, "let binding name must be a symbol") 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)) lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & emitExpr(bval, 0))
bi += 2 bi += 2
let body = items[2..^1] let body = items[2..^1]
@@ -454,6 +539,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
lines.add(bcode) lines.add(bcode)
else: else:
lines.add(indentStr(indent + 1) & "discard " & stripped) lines.add(indentStr(indent + 1) & "discard " & stripped)
popScope()
return lines.join("\n") return lines.join("\n")
of "if": of "if":
@@ -671,6 +757,23 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "throw requires exactly 1 argument") raise newException(EmitterError, "throw requires exactly 1 argument")
return sp & "raise newException(CatchableError, cljStr(" & emitExpr(items[1], 0) & "))" 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": of "quote":
if items.len != 2: if items.len != 2:
raise newException(EmitterError, "quote requires exactly 1 argument") 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) & ")" return sp & "cljMapv(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "apply": of "apply":
if items.len != 3: if items.len < 3:
raise newException(EmitterError, "apply requires exactly 2 arguments") raise newException(EmitterError, "apply requires at least 2 arguments")
return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" 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": of "some":
if items.len != 3: if items.len != 3:
@@ -1033,9 +1145,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=", let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=",
"println", "prn", "print", "str", "pr-str", "println", "prn", "print", "str", "pr-str",
"concat", "min", "max", "merge", "interleave", "concat", "min", "max", "merge", "interleave",
"zipmap"] "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set",
"array-map", "inf", "nan"]
var call: string var call: string
if variadic and argParts.len > 0: if variadic:
call = rn & "(@[" & argParts.join(", ") & "])" call = rn & "(@[" & argParts.join(", ") & "])"
else: else:
call = rn & "(" & argParts.join(", ") & ")" call = rn & "(" & argParts.join(", ") & ")"
@@ -1096,16 +1209,17 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckSymbol: of ckSymbol:
let symName = resolveNsAlias(v.symName) let symName = resolveNsAlias(v.symName)
let rn = runtimeName(symName) let rn = runtimeName(symName)
if rn.len > 0: if rn.len > 0 and not isLocalVar(symName):
# Known function symbol — emit as runtime fn reference
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))" 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: of ckList:
if v.items.len == 0: if v.items.len == 0:
return sp & "cljList(@[])" return sp & "cljList(@[])"
# Macro-expand before emitting # Macro-expand before emitting
let expanded = macroexpand(v) let expanded = macroexpand(v)
if expanded.kind != ckList or expanded == v: if expanded == v:
return emitSpecialForm(v.items, indent) return emitSpecialForm(v.items, indent)
return emitExpr(expanded, indent) return emitExpr(expanded, indent)
of ckVector: of ckVector:
@@ -1144,19 +1258,18 @@ proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string
var code = emitExpr(item, indent) var code = emitExpr(item, indent)
let stripped = code.strip() let stripped = code.strip()
if i < items.len - 1: if i < items.len - 1:
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or let firstNl = code.find("\n")
stripped.startsWith("var ") or stripped.startsWith("let ") or let firstLine = if firstNl == -1: stripped else: code[0..<firstNl].strip()
stripped.startsWith("proc ") or stripped.startsWith("result = "): let trimmed = code.strip()
discard 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: else:
# Prepend discard before the first non-empty line # Wrap multi-line expression in proc so discard can apply
let firstNl = code.find("\n") let indentedCode = indentCode(code, 1)
if firstNl == -1: code = indentStr(indent) & "discard ((proc (): CljVal =\n" & indentedCode & "\n" & indentStr(indent) & ")())"
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: elif useResult:
if stripped.startsWith("discard "): if stripped.startsWith("discard "):
code = indentStr(indent) & "result = " & stripped[8..^1] code = indentStr(indent) & "result = " & stripped[8..^1]
@@ -1181,19 +1294,37 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
var defs: seq[string] = @[] var defs: seq[string] = @[]
var mainForms: 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) = proc processForm(i: int, form: CljVal, isLast: bool) =
let headSym = form.kind == ckList and form.items.len > 0 and if processDepth > maxProcessDepth:
form.items[0].kind == ckSymbol raise newException(EmitterError, "Maximum processing depth exceeded - possible macro expansion loop")
let headName = if headSym: form.items[0].symName else: "" 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 isDef = headSym and headName in ["def", "defn", "defn-"]
let isMacro = headSym and headName in ["defmacro"] let isMacro = headSym and headName in ["defmacro"]
let isNs = headSym and headName == "ns" let isNs = headSym and headName == "ns"
if isNs: if isNs:
discard discard
elif isDef or isMacro: elif isDef or isMacro:
defs.add(emitExpr(form, 0)) defs.add(emitExpr(ef, 0))
else: else:
let code = emitExpr(form, 2) let code = emitExpr(ef, 2)
let stripped = code.strip() let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("var "): stripped.startsWith("var "):
+276 -215
View File
@@ -48,37 +48,26 @@ proc expandSyntaxQuote*(form: CljVal): CljVal =
# Process each element # Process each element
var parts: seq[CljVal] = @[] var parts: seq[CljVal] = @[]
for item in form.items: for item in form.items:
if item.kind == ckList and item.items.len == 2 and parts.add(expandSyntaxQuote(item))
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing":
parts.add(item.items[1])
elif item.kind == ckList and item.items.len == 2 and
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote":
parts.add(cljList(@[cljSymbol("list"), item.items[1]]))
else:
parts.add(cljList(@[cljSymbol("list"), expandSyntaxQuote(item)]))
if parts.len == 0:
return cljList(@[cljSymbol("quote"), cljList(@[])])
# Build (concat part1 part2 ...)
if parts.len == 1:
return parts[0]
return cljList(@[cljSymbol("concat")] & parts) return cljList(@[cljSymbol("concat")] & parts)
of ckVector: of ckVector:
var parts: seq[CljVal] = @[] var parts: seq[CljVal] = @[]
for item in form.items: for item in form.items:
if item.kind == ckList and item.items.len == 2 and parts.add(expandSyntaxQuote(item))
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing": return cljList(@[cljSymbol("apply"), cljSymbol("vector"), cljList(@[cljSymbol("concat")] & parts)])
parts.add(item.items[1])
else:
parts.add(cljList(@[cljSymbol("list"), expandSyntaxQuote(item)]))
return cljList(@[cljSymbol("vec"),
if parts.len == 0: cljList(@[cljSymbol("quote"), cljList(@[])])
elif parts.len == 1: parts[0]
else: cljList(@[cljSymbol("concat")] & parts)])
of ckMap: of ckMap:
# Maps: expand each key and value var parts: seq[CljVal] = @[]
return cljList(@[cljSymbol("quote"), form]) for i in 0..<form.mapKeys.len:
parts.add(expandSyntaxQuote(form.mapKeys[i]))
parts.add(expandSyntaxQuote(form.mapVals[i]))
return cljList(@[cljSymbol("apply"), cljSymbol("hash-map"), cljList(@[cljSymbol("concat")] & parts)])
of ckSet:
var parts: seq[CljVal] = @[]
for item in form.setItems:
parts.add(expandSyntaxQuote(item))
return cljList(@[cljSymbol("apply"), cljSymbol("hash-set"), cljList(@[cljSymbol("concat")] & parts)])
else: else:
return cljList(@[cljSymbol("quote"), form]) return form
proc macroexpand1*(form: CljVal): CljVal = proc macroexpand1*(form: CljVal): CljVal =
if form.isNil: return form if form.isNil: return form
@@ -119,34 +108,32 @@ proc threadingMacro(args: seq[CljVal], reverse: bool): CljVal =
raise newException(CatchableError, "Threading macro requires at least 2 arguments") raise newException(CatchableError, "Threading macro requires at least 2 arguments")
var acc = args[0] var acc = args[0]
for i in 1..<args.len: for i in 1..<args.len:
let step = args[i] let form = args[i]
if step.kind == ckList and step.items.len > 0: if form.kind == ckList and form.items.len > 0:
var newItems = step.items
if reverse: if reverse:
newItems.add(acc) acc = cljList(form.items & @[acc])
else: else:
newItems.insert(acc, 1) acc = cljList(@[form.items[0], acc] & form.items[1..^1])
acc = cljList(newItems) elif form.kind == ckSymbol:
elif step.kind == ckSymbol: acc = cljList(@[form, acc])
# (-> x f) => (f x)
acc = cljList(@[step, acc])
else: else:
raise newException(CatchableError, "Threading macro requires list or symbol forms") raise newException(CatchableError, "Threading macro requires lists or symbols")
acc return acc
proc initBuiltinMacros*() = proc initMacros*() =
# -> # fn* macro
defineMacro("fn*", proc(args: seq[CljVal]): CljVal = defineMacro("fn*", proc(args: seq[CljVal]): CljVal =
# fn* is used internally by reader for #() if args.len < 2:
raise newException(CatchableError, "fn* requires params and body")
cljList(@[cljSymbol("fn")] & args) cljList(@[cljSymbol("fn")] & args)
) )
# -> # -> (thread-first)
defineMacro("->", proc(args: seq[CljVal]): CljVal = defineMacro("->", proc(args: seq[CljVal]): CljVal =
threadingMacro(args, false) threadingMacro(args, false)
) )
# ->> # ->> (thread-last)
defineMacro("->>", proc(args: seq[CljVal]): CljVal = defineMacro("->>", proc(args: seq[CljVal]): CljVal =
threadingMacro(args, true) threadingMacro(args, true)
) )
@@ -155,284 +142,294 @@ proc initBuiltinMacros*() =
defineMacro("and", proc(args: seq[CljVal]): CljVal = defineMacro("and", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljBool(true) if args.len == 0: return cljBool(true)
if args.len == 1: return args[0] if args.len == 1: return args[0]
# (and a b c) => (if a (if b c false) false) var result = cljList(@[cljSymbol("if"), args[^1],
var r = args[^1] args[^1],
cljBool(false)])
for i in countdown(args.len - 2, 0): for i in countdown(args.len - 2, 0):
r = cljList(@[cljSymbol("if"), args[i], r, cljBool(false)]) result = cljList(@[cljSymbol("if"), args[i], result, cljBool(false)])
r return result
) )
# or # or
defineMacro("or", proc(args: seq[CljVal]): CljVal = defineMacro("or", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil() if args.len == 0: return cljNil()
if args.len == 1: return args[0] if args.len == 1: return args[0]
# (or a b c) => (if a a (if b b (if c c nil))) var result = args[^1]
var r = args[^1]
for i in countdown(args.len - 2, 0): for i in countdown(args.len - 2, 0):
r = cljList(@[cljSymbol("if"), args[i], args[i], r]) let gs = cljSymbol(gensymName("or_"))
r result = cljList(@[cljSymbol("let"), cljVector(@[gs, args[i]]),
cljList(@[cljSymbol("if"), gs, gs, result])])
return result
) )
# when-let # when-let
defineMacro("when-let", proc(args: seq[CljVal]): CljVal = defineMacro("when-let", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 2:
raise newException(CatchableError, "when-let requires binding and body") raise newException(CatchableError, "when-let requires binding vector and body")
let binding = args[0] let binding = args[0]
let body = args[1..^1] let body = args[1..^1]
if binding.kind != ckVector or binding.items.len != 2: if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "when-let binding must be [name val]") raise newException(CatchableError, "when-let requires [name val]")
let sym = binding.items[0] let name = binding.items[0]
let val = binding.items[1] let val = binding.items[1]
let gs = cljSymbol(gensymName("wl_")) let gs = cljSymbol(gensymName("wl_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, val]), cljList(@[cljSymbol("let"), cljVector(@[gs, val]),
cljList(@[cljSymbol("if"), gs, cljList(@[cljSymbol("when"), gs,
cljList(@[cljSymbol("let"), cljVector(@[sym, gs])] & body)])]) cljList(@[cljSymbol("let"), cljVector(@[name, gs])] & body)])])
) )
# if-let # if-let
defineMacro("if-let", proc(args: seq[CljVal]): CljVal = defineMacro("if-let", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 3:
raise newException(CatchableError, "if-let requires binding, then, and optional else") raise newException(CatchableError, "if-let requires binding vector, then, and else")
let binding = args[0] let binding = args[0]
let thenExpr = args[1] let thenBranch = args[1]
let elseExpr = if args.len > 2: args[2] else: cljNil() let elseBranch = args[2]
if binding.kind != ckVector or binding.items.len != 2: if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "if-let binding must be [name val]") raise newException(CatchableError, "if-let requires [name val]")
let sym = binding.items[0] let name = binding.items[0]
let val = binding.items[1] let val = binding.items[1]
let gs = cljSymbol(gensymName("il_")) let gs = cljSymbol(gensymName("il_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, val]), cljList(@[cljSymbol("let"), cljVector(@[gs, val]),
cljList(@[cljSymbol("if"), gs, cljList(@[cljSymbol("if"), gs,
cljList(@[cljSymbol("let"), cljVector(@[sym, gs]), thenExpr]), cljList(@[cljSymbol("let"), cljVector(@[name, gs]), thenBranch]),
elseExpr])]) elseBranch])])
) )
# when # when
defineMacro("when", proc(args: seq[CljVal]): CljVal = defineMacro("when", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 1:
raise newException(CatchableError, "when requires condition and body") raise newException(CatchableError, "when requires condition and body")
let test = args[0] let cond = args[0]
let body = args[1..^1] let body = args[1..^1]
cljList(@[cljSymbol("if"), test, cljList(@[cljSymbol("do")] & body)]) cljList(@[cljSymbol("if"), cond,
cljList(@[cljSymbol("do")] & body),
cljNil()])
) )
# when-not # when-not
defineMacro("when-not", proc(args: seq[CljVal]): CljVal = defineMacro("when-not", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 1:
raise newException(CatchableError, "when-not requires condition and body") raise newException(CatchableError, "when-not requires condition and body")
let test = args[0] let cond = args[0]
let body = args[1..^1] let body = args[1..^1]
cljList(@[cljSymbol("if"), test, cljNil(), cljList(@[cljSymbol("do")] & body)]) cljList(@[cljSymbol("if"), cond, cljNil(),
cljList(@[cljSymbol("do")] & body)])
) )
# cond # cond
defineMacro("cond", proc(args: seq[CljVal]): CljVal = defineMacro("cond", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil() if args.len == 0: return cljNil()
if args.len mod 2 != 0: if args.len mod 2 != 0:
raise newException(CatchableError, "cond requires even number of forms") raise newException(CatchableError, "cond requires pairs of test/expr")
let test = args[0] var result = cljNil()
let then = args[1] for i in countdown((args.len div 2) - 1, 0):
if (test.kind == ckKeyword and test.kwName == "else") or let test = args[i * 2]
(test.kind == ckSymbol and test.symName == ":else"): let expr = args[i * 2 + 1]
return then if test.kind == ckKeyword and test.kwName == "else":
let rest = args[2..^1] result = expr
if rest.len == 0: else:
return cljList(@[cljSymbol("if"), test, then]) result = cljList(@[cljSymbol("if"), test, expr, result])
cljList(@[cljSymbol("if"), test, then, return result
cljList(@[cljSymbol("cond")] & rest)])
) )
# cond-> # cond->
defineMacro("cond->", proc(args: seq[CljVal]): CljVal = defineMacro("cond->", proc(args: seq[CljVal]): CljVal =
if args.len < 1: if args.len < 1:
raise newException(CatchableError, "cond-> requires at least 1 argument") raise newException(CatchableError, "cond-> requires initial value and clauses")
let expr = args[0] let init = args[0]
let gs = cljSymbol(gensymName("ct_")) let clauses = args[1..^1]
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])] if clauses.len mod 2 != 0:
var i = 1 raise newException(CatchableError, "cond-> requires pairs of test/expr")
while i + 1 < args.len: var result = init
let test = args[i] for i in 0..<(clauses.len div 2):
let step = args[i+1] let test = clauses[i * 2]
var threaded: CljVal let expr = clauses[i * 2 + 1]
if step.kind == ckList and step.items.len > 0: let gs = cljSymbol(gensymName("ct_"))
var newItems = step.items if expr.kind == ckList and expr.items.len > 0:
newItems.insert(gs, 1) result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
threaded = cljList(newItems) cljList(@[cljSymbol("if"), test,
elif step.kind == ckSymbol: cljList(@[expr.items[0], gs] & expr.items[1..^1]),
threaded = cljList(@[step, gs]) gs])])
else: else:
threaded = step result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
forms.add(cljList(@[cljSymbol("if"), test, cljList(@[cljSymbol("if"), test,
cljList(@[cljSymbol("set!"), gs, threaded]), cljList(@[expr, gs]),
gs])) gs])])
i += 2 return result
forms.add(gs)
cljList(forms)
) )
# cond->> # cond->>
defineMacro("cond->>", proc(args: seq[CljVal]): CljVal = defineMacro("cond->>", proc(args: seq[CljVal]): CljVal =
if args.len < 1: if args.len < 1:
raise newException(CatchableError, "cond->> requires at least 1 argument") raise newException(CatchableError, "cond->> requires initial value and clauses")
let expr = args[0] let init = args[0]
let gs = cljSymbol(gensymName("ctg_")) let clauses = args[1..^1]
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])] if clauses.len mod 2 != 0:
var i = 1 raise newException(CatchableError, "cond->> requires pairs of test/expr")
while i + 1 < args.len: var result = init
let test = args[i] for i in 0..<(clauses.len div 2):
let step = args[i+1] let test = clauses[i * 2]
var threaded: CljVal let expr = clauses[i * 2 + 1]
if step.kind == ckList and step.items.len > 0: let gs = cljSymbol(gensymName("ct_"))
var newItems = step.items if expr.kind == ckList and expr.items.len > 0:
newItems.add(gs) result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
threaded = cljList(newItems) cljList(@[cljSymbol("if"), test,
elif step.kind == ckSymbol: cljList(expr.items & @[gs]),
threaded = cljList(@[step, gs]) gs])])
else: else:
threaded = step result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
forms.add(cljList(@[cljSymbol("if"), test, cljList(@[cljSymbol("if"), test,
cljList(@[cljSymbol("set!"), gs, threaded]), cljList(@[expr, gs]),
gs])) gs])])
i += 2 return result
forms.add(gs)
cljList(forms)
) )
# doto # doto
defineMacro("doto", proc(args: seq[CljVal]): CljVal = defineMacro("doto", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 1:
raise newException(CatchableError, "doto requires at least 2 arguments") raise newException(CatchableError, "doto requires initial value and body")
let x = args[0] let init = args[0]
let gs = cljSymbol(gensymName("do_")) let body = args[1..^1]
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, x])] let gs = cljSymbol(gensymName("dt_"))
for i in 1..<args.len: var forms: seq[CljVal] = @[]
let step = args[i] forms.add(cljSymbol("let"))
if step.kind == ckList and step.items.len > 0: forms.add(cljVector(@[gs, init]))
var newItems = step.items for form in body:
newItems.insert(gs, 1) if form.kind == ckList and form.items.len > 0:
forms.add(cljList(newItems)) forms.add(cljList(@[form.items[0], gs] & form.items[1..^1]))
elif step.kind == ckSymbol: else:
forms.add(cljList(@[step, gs])) forms.add(cljList(@[form, gs]))
forms.add(gs) forms.add(gs)
cljList(forms) return cljList(forms)
) )
# as-> # as->
defineMacro("as->", proc(args: seq[CljVal]): CljVal = defineMacro("as->", proc(args: seq[CljVal]): CljVal =
if args.len < 3: if args.len < 3:
raise newException(CatchableError, "as-> requires at least 3 arguments") raise newException(CatchableError, "as-> requires name, init, and body")
let expr = args[0] let name = args[0]
let name = args[1] let init = args[1]
var acc = expr let body = args[2..^1]
for i in 2..<args.len: var result = init
acc = cljList(@[cljSymbol("let"), cljVector(@[name, acc]), args[i]]) for form in body:
acc result = cljList(@[cljSymbol("let"), cljVector(@[name, result]), form])
return result
) )
# some-> # some->
defineMacro("some->", proc(args: seq[CljVal]): CljVal = defineMacro("some->", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 1:
raise newException(CatchableError, "some-> requires at least 2 arguments") raise newException(CatchableError, "some-> requires initial value and body")
var acc = args[0] let init = args[0]
for i in 1..<args.len: let body = args[1..^1]
let step = args[i] var result = init
for form in body:
let gs = cljSymbol(gensymName("st_")) let gs = cljSymbol(gensymName("st_"))
var threaded: CljVal if form.kind == ckList and form.items.len > 0:
if step.kind == ckList and step.items.len > 0: result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
var newItems = step.items cljList(@[cljSymbol("if"), gs,
newItems.insert(gs, 1) cljList(@[form.items[0], gs] & form.items[1..^1]),
threaded = cljList(newItems) cljNil()])])
elif step.kind == ckSymbol:
threaded = cljList(@[step, gs])
else: else:
threaded = step result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), gs,
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]), cljList(@[form, gs]),
cljNil(), threaded])]) cljNil()])])
acc return result
) )
# some->> # some->>
defineMacro("some->>", proc(args: seq[CljVal]): CljVal = defineMacro("some->>", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 1:
raise newException(CatchableError, "some->> requires at least 2 arguments") raise newException(CatchableError, "some->> requires initial value and body")
var acc = args[0] let init = args[0]
for i in 1..<args.len: let body = args[1..^1]
let step = args[i] var result = init
let gs = cljSymbol(gensymName("stg_")) for form in body:
var threaded: CljVal let gs = cljSymbol(gensymName("st_"))
if step.kind == ckList and step.items.len > 0: if form.kind == ckList and form.items.len > 0:
var newItems = step.items result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
newItems.add(gs) cljList(@[cljSymbol("if"), gs,
threaded = cljList(newItems) cljList(form.items & @[gs]),
elif step.kind == ckSymbol: cljNil()])])
threaded = cljList(@[step, gs])
else: else:
threaded = step result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), gs,
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]), cljList(@[form, gs]),
cljNil(), threaded])]) cljNil()])])
acc return result
) )
# for (simplified — single binding only) # for
defineMacro("for", proc(args: seq[CljVal]): CljVal = defineMacro("for", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 2:
raise newException(CatchableError, "for requires binding vector and body") raise newException(CatchableError, "for requires bindings and body")
let bindings = args[0] let bindings = args[0]
let body = args[1..^1] let body = args[1..^1]
if bindings.kind != ckVector or bindings.items.len < 2: if bindings.kind != ckVector:
raise newException(CatchableError, "for requires a binding vector [name coll]") raise newException(CatchableError, "for bindings must be a vector")
let sym = bindings.items[0] let gs = cljSymbol(gensymName("fr_"))
let coll = bindings.items[1]
let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body)
let gs = cljSymbol(gensymName("f_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, cljVector(@[])]), cljList(@[cljSymbol("let"), cljVector(@[gs, cljVector(@[])]),
cljList(@[cljSymbol("doseq"), cljVector(@[sym, coll]), cljList(@[cljSymbol("doseq")] & @[bindings] & @[
cljList(@[cljSymbol("set!"), gs, cljList(@[cljSymbol("set!"), gs, cljList(@[cljSymbol("conj"), gs] & body)])]),
cljList(@[cljSymbol("conj"), gs, bodyExpr])])]),
gs]) gs])
) )
# doseq (simplified — single binding only) # doseq
defineMacro("doseq", proc(args: seq[CljVal]): CljVal = defineMacro("doseq", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 2:
raise newException(CatchableError, "doseq requires binding vector and body") raise newException(CatchableError, "doseq requires bindings and body")
let bindings = args[0] let bindings = args[0]
let body = args[1..^1] let body = args[1..^1]
if bindings.kind != ckVector or bindings.items.len < 2: if bindings.kind != ckVector:
raise newException(CatchableError, "doseq requires a binding vector [name coll]") raise newException(CatchableError, "doseq bindings must be a vector")
let sym = bindings.items[0] let bindingPairs = bindings.items.len div 2
let coll = bindings.items[1] if bindingPairs == 1:
let gs = cljSymbol(gensymName("ds_")) let name = bindings.items[0]
let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body) let coll = bindings.items[1]
cljList(@[cljSymbol("let"), cljVector(@[gs, coll]), let gs = cljSymbol(gensymName("ds_"))
cljList(@[cljSymbol("loop"), cljVector(@[cljSymbol(gensymName("i_")), cljInt(0)]), cljList(@[cljSymbol("let"), cljVector(@[gs, coll]),
cljList(@[cljSymbol("when"), cljList(@[cljSymbol("loop"), cljVector(@[name, cljList(@[cljSymbol("first"), gs]),
cljList(@[cljSymbol("<"), cljSymbol(gensymName("i_")), cljList(@[cljSymbol("count"), gs])]), gs, cljList(@[cljSymbol("rest"), gs])]),
cljList(@[cljSymbol("let"), cljVector(@[sym, cljList(@[cljSymbol("nth"), gs, cljSymbol(gensymName("i_"))])]), cljList(@[cljSymbol("when"), name] & body),
bodyExpr]), cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("first"), gs]),
cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("rest"), gs])])])])
cljList(@[cljSymbol("inc"), cljSymbol(gensymName("i_"))])])])])]) else:
# Nested doseq
let name = bindings.items[0]
let coll = bindings.items[1]
let restBindings = cljVector(bindings.items[2..^1])
cljList(@[cljSymbol("doseq"), cljVector(@[name, coll]),
cljList(@[cljSymbol("doseq"), restBindings] & body)])
) )
# dotimes # dotimes
defineMacro("dotimes", proc(args: seq[CljVal]): CljVal = defineMacro("dotimes", proc(args: seq[CljVal]): CljVal =
if args.len < 2: if args.len < 2:
raise newException(CatchableError, "dotimes requires binding vector and body") raise newException(CatchableError, "dotimes requires binding and body")
let bindings = args[0] let binding = args[0]
let body = args[1..^1] let body = args[1..^1]
if bindings.kind != ckVector or bindings.items.len != 2: if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "dotimes requires [name n]") raise newException(CatchableError, "dotimes requires [name count]")
let sym = bindings.items[0] let name = binding.items[0]
let n = bindings.items[1] let count = binding.items[1]
cljList(@[cljSymbol("doseq"), cljVector(@[sym, cljList(@[cljSymbol("range"), n])])] & body) let gs = cljSymbol(gensymName("dt_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, count]),
cljList(@[cljSymbol("loop"), cljVector(@[name, cljInt(0)]),
cljList(@[cljSymbol("when"), cljList(@[cljSymbol("<"), name, gs])] & body),
cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("+"), name, cljInt(1)])])])])
) )
# defmacro # defmacro
defineMacro("defmacro", proc(args: seq[CljVal]): CljVal = defineMacro("defmacro", proc(args: seq[CljVal]): CljVal =
# defmacro is handled specially in the compiler, this is a no-op if args.len < 3:
cljNil() raise newException(CatchableError, "defmacro requires name, params, and body")
let name = args[0]
let params = args[1]
let body = args[2..^1]
cljList(@[cljSymbol("def"), name,
cljList(@[cljSymbol("fn"), params] & body)])
) )
# comment # comment
@@ -464,3 +461,67 @@ proc initBuiltinMacros*() =
cljList(@[cljSymbol("finally"), cljList(@[cljSymbol("finally"),
cljList(@[cljSymbol(".close"), gs])])])]) cljList(@[cljSymbol(".close"), gs])])])])
) )
# clojure.test macros
defineMacro("deftest", proc(args: seq[CljVal]): CljVal =
if args.len == 0:
raise newException(CatchableError, "deftest requires name and body")
let name = args[0]
let body = args[1..^1]
cljList(@[cljSymbol("def"), name,
cljList(@[cljSymbol("fn"), cljVector(@[])] & body)])
)
defineMacro("is", proc(args: seq[CljVal]): CljVal =
# is is a no-op for compilation tests
cljNil()
)
defineMacro("testing", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil()
cljList(@[cljSymbol("do")] & args[1..^1])
)
defineMacro("thrown?", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil()
let form = args[0]
cljList(@[cljSymbol("try"), form, cljBool(false),
cljList(@[cljSymbol("catch"), cljSymbol("Exception"), cljSymbol("e"), cljBool(true)])])
)
defineMacro("are", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "are requires argv and test cases")
let argv = args[0]
let testExpr = args[1]
let exprs = args[2..^1]
if argv.kind != ckVector:
raise newException(CatchableError, "are argv must be a vector")
let arity = argv.items.len
if exprs.len mod arity != 0:
raise newException(CatchableError, "are test cases must match argv arity")
var isForms: seq[CljVal] = @[]
for i in countup(0, exprs.len - arity, arity):
var bindings: seq[CljVal] = @[]
for j in 0..<arity:
bindings.add(argv.items[j])
bindings.add(exprs[i+j])
let letForm = cljList(@[cljSymbol("let"), cljVector(bindings),
cljList(@[cljSymbol("is"), testExpr])])
isForms.add(letForm)
if isForms.len == 1:
return isForms[0]
return cljList(@[cljSymbol("do")] & isForms)
)
defineMacro("when-var-exists", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "when-var-exists requires symbol and body")
let body = args[1..^1]
if body.len == 1:
return body[0]
return cljList(@[cljSymbol("do")] & body)
)
proc initBuiltinMacros*() =
initMacros()
+114 -12
View File
@@ -8,7 +8,7 @@ type
proc skipWhitespaceAndComments(s: string, i: var int) = proc skipWhitespaceAndComments(s: string, i: var int) =
while i < s.len: while i < s.len:
let c = s[i] let c = s[i]
if c in Whitespace: if c in Whitespace or c == ',':
inc i inc i
elif c == ';': elif c == ';':
while i < s.len and s[i] != '\n': while i < s.len and s[i] != '\n':
@@ -40,6 +40,18 @@ proc readStringTok(s: string, i: var int): string =
inc i # skip closing quote inc i # skip closing quote
return resultStr return resultStr
proc consumeNumericSuffix(s: string, i: var int) =
# Consume N (BigInt) or M (BigDecimal) suffix after a number
if i < s.len and (s[i] == 'N' or s[i] == 'M'):
inc i
proc consumeRatioDenominator(s: string, i: var int) =
# Consume /denominator for ratio literals like 1/5, -1/5
if i < s.len and s[i] == '/' and i + 1 < s.len and s[i+1] in Digits:
inc i # skip /
while i < s.len and s[i] in Digits:
inc i
proc readNumberOrSym(s: string, i: var int): string = proc readNumberOrSym(s: string, i: var int): string =
var start = i var start = i
# handle negative sign or standalone operators # handle negative sign or standalone operators
@@ -58,6 +70,8 @@ proc readNumberOrSym(s: string, i: var int): string =
if i < s.len and s[i] in Digits: if i < s.len and s[i] in Digits:
while i < s.len and s[i] in Digits: while i < s.len and s[i] in Digits:
inc i inc i
consumeNumericSuffix(s, i)
consumeRatioDenominator(s, i)
return s[start..<i] return s[start..<i]
elif i + 1 < s.len and s[i+1] in Digits: elif i + 1 < s.len and s[i+1] in Digits:
inc i inc i
@@ -75,6 +89,8 @@ proc readNumberOrSym(s: string, i: var int): string =
if i < s.len and s[i] in Digits: if i < s.len and s[i] in Digits:
while i < s.len and s[i] in Digits: while i < s.len and s[i] in Digits:
inc i inc i
consumeNumericSuffix(s, i)
consumeRatioDenominator(s, i)
return s[start..<i] return s[start..<i]
else: else:
# it's a symbol like - or + # it's a symbol like - or +
@@ -98,6 +114,10 @@ proc readNumberOrSym(s: string, i: var int): string =
if i < s.len and s[i] in Digits: if i < s.len and s[i] in Digits:
while i < s.len and s[i] in Digits: while i < s.len and s[i] in Digits:
inc i inc i
# Clojure BigInt (N) and BigDecimal (M) suffixes
if i < s.len and (s[i] == 'N' or s[i] == 'M'):
inc i
consumeRatioDenominator(s, i)
return s[start..<i] return s[start..<i]
else: else:
while i < s.len and isSymChar(s[i]): while i < s.len and isSymChar(s[i]):
@@ -118,31 +138,36 @@ proc readAtom(s: string, i: var int): CljVal =
if tok[0] == ':': if tok[0] == ':':
return cljKeyword(tok[1..^1]) return cljKeyword(tok[1..^1])
# Strip Clojure BigInt/Decimal suffixes for parsing
var numTok = tok
if numTok.len > 1 and (numTok[^1] == 'N' or numTok[^1] == 'M'):
numTok = numTok[0..^2]
# number # number
var isFloat = false var isFloat = false
var isNumber = true var isNumber = true
var startIdx = 0 var startIdx = 0
if tok[0] == '-' or tok[0] == '+': if numTok[0] == '-' or numTok[0] == '+':
if tok.len == 1: if numTok.len == 1:
isNumber = false isNumber = false
else: else:
startIdx = 1 startIdx = 1
var sawExp = false var sawExp = false
for j in startIdx..<tok.len: for j in startIdx..<numTok.len:
if tok[j] == '.': if numTok[j] == '.':
if isFloat: if isFloat:
isNumber = false isNumber = false
break break
isFloat = true isFloat = true
elif tok[j] == 'e' or tok[j] == 'E': elif numTok[j] == 'e' or numTok[j] == 'E':
if sawExp: if sawExp:
isNumber = false isNumber = false
break break
sawExp = true sawExp = true
isFloat = true isFloat = true
if j + 1 < tok.len and (tok[j+1] == '-' or tok[j+1] == '+'): if j + 1 < numTok.len and (numTok[j+1] == '-' or numTok[j+1] == '+'):
discard # skip exponent sign discard # skip exponent sign
elif tok[j] notin Digits: elif numTok[j] notin Digits:
if sawExp and (tok[j] == '-' or tok[j] == '+'): if sawExp and (tok[j] == '-' or tok[j] == '+'):
if j > 0 and (tok[j-1] == 'e' or tok[j-1] == 'E'): if j > 0 and (tok[j-1] == 'e' or tok[j-1] == 'E'):
continue continue
@@ -157,9 +182,9 @@ proc readAtom(s: string, i: var int): CljVal =
break break
if hasDigit: if hasDigit:
if isFloat: if isFloat:
return cljFloat(parseFloat(tok)) return cljFloat(parseFloat(numTok))
else: else:
return cljInt(parseInt(tok).int64) return cljInt(parseInt(numTok).int64)
# symbol # symbol
return cljSymbol(tok) return cljSymbol(tok)
@@ -178,7 +203,14 @@ proc readList(s: string, i: var int): CljVal =
break break
let form = readForm(s, i) let form = readForm(s, i)
if form != nil: if form != nil:
items.add(form) if form.kind == ckList and form.items.len == 2 and
form.items[0].kind == ckSymbol and form.items[0].symName == "splice-unwrap":
let inner = form.items[1]
if inner.kind == ckVector:
for item in inner.items:
items.add(item)
else:
items.add(form)
return cljList(items) return cljList(items)
proc readVector(s: string, i: var int): CljVal = proc readVector(s: string, i: var int): CljVal =
@@ -193,7 +225,14 @@ proc readVector(s: string, i: var int): CljVal =
break break
let form = readForm(s, i) let form = readForm(s, i)
if form != nil: if form != nil:
items.add(form) if form.kind == ckList and form.items.len == 2 and
form.items[0].kind == ckSymbol and form.items[0].symName == "splice-unwrap":
let inner = form.items[1]
if inner.kind == ckVector:
for item in inner.items:
items.add(item)
else:
items.add(form)
return cljVector(items) return cljVector(items)
proc readMap(s: string, i: var int): CljVal = proc readMap(s: string, i: var int): CljVal =
@@ -252,6 +291,39 @@ proc readDispatch(s: string, i: var int): CljVal =
inc i inc i
discard readForm(s, i) discard readForm(s, i)
return nil return nil
of '?':
# Reader conditional: #?(:clj expr :cljs expr :default expr)
# Read the next char - if '@' it's splicing version #?@
var isSplice = false
var savedI = i
inc i
if i < s.len and s[i] == '@':
isSplice = true
inc i
else:
i = savedI
inc i # skip '?'
let form = readForm(s, i)
if form == nil:
return nil
if form.kind != ckList:
return nil
var items = form.items
var defaultVal: CljVal = nil
var found = false
var k = 0
while k < items.len - 1:
if items[k].kind == ckKeyword and items[k].kwName == "default":
defaultVal = items[k + 1]
found = true
break
k += 2
if not found:
return nil
if isSplice and defaultVal != nil and defaultVal.kind == ckVector:
# Return the vector items as a splice marker
return cljList(@[cljSymbol("splice-unwrap"), defaultVal])
return defaultVal
else: else:
raise newException(ReaderError, "Unknown dispatch macro: #" & c) raise newException(ReaderError, "Unknown dispatch macro: #" & c)
@@ -282,6 +354,34 @@ proc readDeref(s: string, i: var int): CljVal =
let form = readForm(s, i) let form = readForm(s, i)
return cljList(@[cljSymbol("deref"), form]) return cljList(@[cljSymbol("deref"), form])
proc readCharLiteral(s: string, i: var int): CljVal =
inc i # skip '\'
if i >= s.len:
raise newException(ReaderError, "Unexpected end after \\")
let c = s[i]
if c in Letters:
# Could be a named char like newline, space, tab, or a single letter
var name = ""
while i < s.len and isSymChar(s[i]):
name.add(s[i])
inc i
case name
of "newline": return cljString("\n")
of "space": return cljString(" ")
of "tab": return cljString("\t")
of "return": return cljString("\r")
of "formfeed": return cljString("\x0C")
of "backspace": return cljString("\x08")
else:
if name.len == 1:
return cljString(name)
raise newException(ReaderError, "Unknown character literal: \\" & name)
else:
# Single character literal like \1, \\ etc
let ch = $c
inc i
return cljString(ch)
proc readForm(s: string, i: var int): CljVal = proc readForm(s: string, i: var int): CljVal =
skipWhitespaceAndComments(s, i) skipWhitespaceAndComments(s, i)
if i >= s.len: if i >= s.len:
@@ -313,6 +413,8 @@ proc readForm(s: string, i: var int): CljVal =
return readUnquote(s, i) return readUnquote(s, i)
of '@': of '@':
return readDeref(s, i) return readDeref(s, i)
of '\\':
return readCharLiteral(s, i)
else: else:
return readAtom(s, i) return readAtom(s, i)
+6 -5
View File
@@ -44,13 +44,16 @@ def extract_default(content, start):
return rest[:k], k return rest[:k], k
def strip_reader_conditionals(text): def strip_reader_conditionals(text):
"""Strip #? and #?@ reader conditionals, keeping :default branch content.""" """Strip #? and #?@ reader conditionals, keeping :default branch content.
For #?@, if the default is a vector/list, unwrap it (splice semantics)."""
result = [] result = []
i = 0 i = 0
while i < len(text): while i < len(text):
# Check for #?@ or #? # Check for #?@ or #?
is_splice = False
if i + 3 <= len(text) and text[i:i+3] == '#?@': if i + 3 <= len(text) and text[i:i+3] == '#?@':
j = i + 3 j = i + 3
is_splice = True
elif i + 2 <= len(text) and text[i:i+2] == '#?': elif i + 2 <= len(text) and text[i:i+2] == '#?':
j = i + 2 j = i + 2
else: else:
@@ -85,6 +88,8 @@ def strip_reader_conditionals(text):
# Extract content between parens # Extract content between parens
inner = text[j+1:k] inner = text[j+1:k]
default_val, _ = extract_default(inner, 0) default_val, _ = extract_default(inner, 0)
if is_splice and len(default_val) >= 2 and default_val[0] in '([':
default_val = default_val[1:-1]
result.append(default_val) result.append(default_val)
i = k + 1 i = k + 1
@@ -98,10 +103,6 @@ def run_test(cljc_path, timeout=30):
content = re.sub(r'#\{[^}]*\}', r'@[]', content) # #{} → @[] content = re.sub(r'#\{[^}]*\}', r'@[]', content) # #{} → @[]
content = re.sub(r'#:([\w-]+)', r'\1', content) # #:foo → foo content = re.sub(r'#:([\w-]+)', r'\1', content) # #:foo → foo
stubs = '''(do stubs = '''(do
(defmacro is [form] (list 'quote form))
(defmacro testing [desc & body] (cons 'do body))
(defmacro thrown? [form] (list 'try form false (list 'catch 'Exception 'e true)))
(defmacro deftest [name & body] (list 'def name (cons 'fn (cons [] body))))
''' '''
content = stubs + content + ')' content = stubs + content + ')'
content = re.sub(r'clojure\.test', 'test', content) content = re.sub(r'clojure\.test', 'test', content)